diff --git a/.github/workflows/deploy-vercel.yml b/.github/workflows/deploy-vercel.yml new file mode 100644 index 0000000..37facee --- /dev/null +++ b/.github/workflows/deploy-vercel.yml @@ -0,0 +1,40 @@ +name: Deploy to Vercel + +on: + push: + branches: + - deploy + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: "20.16.0" + + - name: Install Vercel CLI + run: npm install --global vercel@latest + + - name: Print environment info + run: | + node -v + npm -v + vercel --version + + - name: Install dependencies with legacy peer deps + run: npm install --legacy-peer-deps + + - name: Build with Next.js + run: npm run build + + - name: Deploy to Vercel + run: vercel deploy --prod --yes --token=${{ secrets.VERCEL_TOKEN }} + env: + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} diff --git a/.gitignore b/.gitignore index 5ef6a52..29ec709 100644 --- a/.gitignore +++ b/.gitignore @@ -34,7 +34,7 @@ yarn-error.log* .env* # vercel -.vercel +# .vercel # typescript *.tsbuildinfo diff --git a/.vercel/README.txt b/.vercel/README.txt new file mode 100644 index 0000000..525d8ce --- /dev/null +++ b/.vercel/README.txt @@ -0,0 +1,11 @@ +> Why do I have a folder named ".vercel" in my project? +The ".vercel" folder is created when you link a directory to a Vercel project. + +> What does the "project.json" file contain? +The "project.json" file contains: +- The ID of the Vercel project that you linked ("projectId") +- The ID of the user or team your Vercel project is owned by ("orgId") + +> Should I commit the ".vercel" folder? +No, you should not share the ".vercel" folder with anyone. +Upon creation, it will be automatically added to your ".gitignore" file. diff --git a/.vercel/project.json b/.vercel/project.json new file mode 100644 index 0000000..5297720 --- /dev/null +++ b/.vercel/project.json @@ -0,0 +1,15 @@ +{ + "projectId": "prj_te8nIZ8DyvHEtnS5zjg84EWRNhNo", + "orgId": "team_Hdro0YYN8FfVYjQGRSNOr3xB", + "settings": { + "createdAt": 1749712056965, + "framework": "nextjs", + "devCommand": null, + "installCommand": null, + "buildCommand": null, + "outputDirectory": null, + "rootDirectory": null, + "directoryListing": false, + "nodeVersion": "20.x" + } +} diff --git a/.vercelignore b/.vercelignore new file mode 100644 index 0000000..0d6239b --- /dev/null +++ b/.vercelignore @@ -0,0 +1,3 @@ +# Add to your .vercelignore + +node_modules diff --git a/README.md b/README.md index 9c55e35..50e927e 100644 --- a/README.md +++ b/README.md @@ -81,12 +81,20 @@ Ensure you have the following installed: npm install prisma --save-dev ``` -2. **Set up your database connection:** +2. **Set up your environment variables:** - - Create a `.env` file in the root of your project with the following content: - ``` - DATABASE_URL="postgresql://user:password@localhost:5432/gotogether" + - This project uses environment variables for configuration. A template file `.env.example` is provided in the root directory. + - Copy this file to a new file named `.env`: + ```bash + cp .env.example .env ``` + - **Important**: Open the `.env` file and fill in the required values for your local development environment. This includes: + - `DATABASE_URL`: Your PostgreSQL connection string. For local development, it might look like `postgresql://YOUR_USER:YOUR_PASSWORD@localhost:5432/gotogether?schema=public`. If using Docker, the hostname might be `db` (e.g., `postgresql://user:password@db:5432/gotogether`). + - `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET`: Your Google OAuth credentials. + - `UPLOADTHING_SECRET` and `NEXT_PUBLIC_UPLOADTHING_APP_ID`: Your UploadThing credentials. + - URLs for backend services (`NEXT_PUBLIC_BACKEND_URL`, `NEXT_PUBLIC_GO_BACKEND_URL`) if they differ from the defaults. + - Keycloak URLs if you are using a custom Keycloak instance. + - The `.env` file is already listed in `.gitignore` and should not be committed to your repository. 3. **Run Prisma migrations:** diff --git a/src/app/(auth)/actions.ts b/src/app/(auth)/actions.ts index 32e584e..a51e24d 100644 --- a/src/app/(auth)/actions.ts +++ b/src/app/(auth)/actions.ts @@ -20,6 +20,6 @@ export async function logout() { path: "/api/auth/refresh", maxAge: 0, }); - const keycloakLogoutUrl = `http://localhost:8084/realms/kong/protocol/openid-connect/logout?client_id=kong-oidc&post_logout_redirect_uri=http://localhost:3000/login`; + const keycloakLogoutUrl = `${process.env.NEXT_PUBLIC_KEYCLOAK_LOGOUT_URL || "http://localhost:8084/realms/kong/protocol/openid-connect/logout"}?client_id=kong-oidc&post_logout_redirect_uri=${process.env.NEXT_PUBLIC_KEYCLOAK_REDIRECT_URI || "http://localhost:3000/login"}`; return redirect(keycloakLogoutUrl); } diff --git a/src/app/(auth)/login/GoogleSignInButton.tsx b/src/app/(auth)/login/GoogleSignInButton.tsx index 84af2b9..917bf53 100644 --- a/src/app/(auth)/login/GoogleSignInButton.tsx +++ b/src/app/(auth)/login/GoogleSignInButton.tsx @@ -8,7 +8,10 @@ export default function GoogleSignInButton() { asChild > diff --git a/src/app/(auth)/signup/actions.ts b/src/app/(auth)/signup/actions.ts index fb10165..b228215 100644 --- a/src/app/(auth)/signup/actions.ts +++ b/src/app/(auth)/signup/actions.ts @@ -5,25 +5,29 @@ import { isRedirectError } from "next/dist/client/components/redirect"; import { redirect } from "next/navigation"; export async function signUp( - credentials: SignUpValues + credentials: SignUpValues, ): Promise<{ error: string } | void> { try { - const { username, email, password, firstName, lastName } = signUpSchema.parse(credentials); + const { username, email, password, firstName, lastName } = + signUpSchema.parse(credentials); // 👇 New: Call your Spring Boot backend API - const res = await fetch("http://localhost:8080/api/users/register", { - method: "POST", - headers: { - "Content-Type": "application/json", + const res = await fetch( + `${process.env.NEXT_PUBLIC_BACKEND_URL || "http://localhost:8080"}/api/users/register`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + username, + email, + password, + firstName, // <-- Optional, you can ask these fields from user if needed + lastName, + }), }, - body: JSON.stringify({ - username, - email, - password, - firstName, // <-- Optional, you can ask these fields from user if needed - lastName, - }), - }); + ); if (!res.ok) { const err = await res.text(); @@ -32,7 +36,6 @@ export async function signUp( } return redirect("/login"); - } catch (error) { if (isRedirectError(error)) throw error; console.error(error); diff --git a/src/app/(main)/place/[placeId]/actions.ts b/src/app/(main)/place/[placeId]/actions.ts index 5a3e734..41d2ff7 100644 --- a/src/app/(main)/place/[placeId]/actions.ts +++ b/src/app/(main)/place/[placeId]/actions.ts @@ -1,70 +1,81 @@ "use server"; - import { goKyInstance } from "../../../../../src/lib/ky"; // Adjusted path to src/lib/ky - import type { PlaceDetailsResponse, PlaceDetails } from "@/types/location-types"; // Verify path +import { goKyInstance } from "../../../../../src/lib/ky"; // Adjusted path to src/lib/ky +import type { + PlaceDetailsResponse, + PlaceDetails, +} from "@/types/location-types"; // Verify path - interface GetPlaceDetailsResult { - success: boolean; - data?: PlaceDetails; // This is the 'result' object from PlaceDetailsResponse - error?: string; - status?: string; // To pass along Google's status like "NOT_FOUND", "INVALID_REQUEST" - } +interface GetPlaceDetailsResult { + success: boolean; + data?: PlaceDetails; // This is the 'result' object from PlaceDetailsResponse + error?: string; + status?: string; // To pass along Google's status like "NOT_FOUND", "INVALID_REQUEST" +} - export async function getPlaceDetailsByIdAction( - placeId: string, - ): Promise { - if (!placeId || placeId.trim() === "") { - return { success: false, error: "Place ID is required.", status: "INVALID_REQUEST_CLIENT" }; - } +export async function getPlaceDetailsByIdAction( + placeId: string, +): Promise { + if (!placeId || placeId.trim() === "") { + return { + success: false, + error: "Place ID is required.", + status: "INVALID_REQUEST_CLIENT", + }; + } - try { - // The API path is /maps/place/:place_id - // goKyInstance will append this to its prefixUrl: http://localhost:8000 - // So the final URL will be http://localhost:8000/maps/place/{placeId} - const response = await goKyInstance.get(`maps/place/${placeId}`).json(); + try { + // The API path is /maps/place/:place_id + // goKyInstance will append this to its prefixUrl (NEXT_PUBLIC_GO_BACKEND_URL, defaults to http://localhost:8083) + // So the final URL will be /maps/place/{placeId} + const response = await goKyInstance + .get(`maps/place/${placeId}`) + .json(); - // According to the API documentation for /maps/place/:place_id: - // - Success (200 OK) contains the PlaceDetailsResponse structure. - // - The 'status' field within the response body indicates Google's processing status. - // - Errors like 400, 403, 404, 429 are also possible from our backend, - // but ky throws HTTPError for non-2xx responses, which is caught below. - // If the backend wraps Google's error status (like NOT_FOUND) in a 200 OK response from *our* service, - // then we check response.status here. + // According to the API documentation for /maps/place/:place_id: + // - Success (200 OK) contains the PlaceDetailsResponse structure. + // - The 'status' field within the response body indicates Google's processing status. + // - Errors like 400, 403, 404, 429 are also possible from our backend, + // but ky throws HTTPError for non-2xx responses, which is caught below. + // If the backend wraps Google's error status (like NOT_FOUND) in a 200 OK response from *our* service, + // then we check response.status here. - if (response.status === "OK") { - return { success: true, data: response.result, status: response.status }; - } else { - // Handles cases like "ZERO_RESULTS", "NOT_FOUND", "INVALID_REQUEST" etc. - // returned by Google but wrapped in a 200 OK from our service. - console.error( - `API returned non-OK status for place ${placeId}: ${response.status} - ${response.error_message || ""}`, - ); - return { - success: false, - error: response.error_message || `API Error: ${response.status}`, - status: response.status, - }; - } - } catch (error: any) { - console.error(`Error fetching details for place ${placeId}:`, error); - let errorMessage = "Failed to fetch place details."; - let errorStatus = "UNKNOWN_ERROR_CLIENT"; + if (response.status === "OK") { + return { success: true, data: response.result, status: response.status }; + } else { + // Handles cases like "ZERO_RESULTS", "NOT_FOUND", "INVALID_REQUEST" etc. + // returned by Google but wrapped in a 200 OK from our service. + console.error( + `API returned non-OK status for place ${placeId}: ${response.status} - ${response.error_message || ""}`, + ); + return { + success: false, + error: response.error_message || `API Error: ${response.status}`, + status: response.status, + }; + } + } catch (error: any) { + console.error(`Error fetching details for place ${placeId}:`, error); + let errorMessage = "Failed to fetch place details."; + let errorStatus = "UNKNOWN_ERROR_CLIENT"; - if (error.name === 'HTTPError') { // Ky specific HTTP error - try { - const errorResponse = await error.response.json(); - // Assuming error response from our Go service might look like: - // { "error": "message", "details": "...", "status": "GOOGLE_STATUS_CODE" } - // or directly Google's PlaceDetailsResponse structure with a non-OK status. - errorMessage = errorResponse.error_message || errorResponse.error || error.message; - errorStatus = errorResponse.status || `HTTP_${error.response.status}`; - } catch (e) { - errorMessage = `API Error: ${error.response.status} - ${error.message}`; - errorStatus = `HTTP_${error.response.status}`; - } - } else if (error instanceof Error) { - errorMessage = error.message; - } - return { success: false, error: errorMessage, status: errorStatus }; + if (error.name === "HTTPError") { + // Ky specific HTTP error + try { + const errorResponse = await error.response.json(); + // Assuming error response from our Go service might look like: + // { "error": "message", "details": "...", "status": "GOOGLE_STATUS_CODE" } + // or directly Google's PlaceDetailsResponse structure with a non-OK status. + errorMessage = + errorResponse.error_message || errorResponse.error || error.message; + errorStatus = errorResponse.status || `HTTP_${error.response.status}`; + } catch (e) { + errorMessage = `API Error: ${error.response.status} - ${error.message}`; + errorStatus = `HTTP_${error.response.status}`; } + } else if (error instanceof Error) { + errorMessage = error.message; } + return { success: false, error: errorMessage, status: errorStatus }; + } +} diff --git a/src/app/(main)/place/[placeId]/page.tsx b/src/app/(main)/place/[placeId]/page.tsx index 0bff1fe..df0d4c8 100644 --- a/src/app/(main)/place/[placeId]/page.tsx +++ b/src/app/(main)/place/[placeId]/page.tsx @@ -400,7 +400,10 @@ export default function PlaceDetailPage({ params }: PlaceDetailPageProps) { ({ + ...place, + vicinity: place.vicinity || "", + }))} // relatedPlaces are PlaceDetails[], compatible with LocationDetail[] images={relatedPlaces.map(p => p.photo_urls?.[0] || DEFAULT_IMAGE_URL)} scrollButton={{ route: "" as any, loading: false }} // Effectively hides "See all" handleNavigation={() => {}} // Dummy function as "See all" is hidden diff --git a/src/app/api/uploadthing/core.ts b/src/app/api/uploadthing/core.ts index 4629606..da62795 100644 --- a/src/app/api/uploadthing/core.ts +++ b/src/app/api/uploadthing/core.ts @@ -9,8 +9,10 @@ export const fileRouter = { image: { maxFileSize: "512KB" }, }) .middleware(async ({ req }) => { - const cookieHeader = req.headers.get('cookie'); - const { user, token } = await validateRequest({ headers: { cookie: cookieHeader ?? undefined } }); + const cookieHeader = req.headers.get("cookie"); + const { user, token } = await validateRequest({ + headers: { cookie: cookieHeader ?? undefined }, + }); if (!user) throw new UploadThingError("Unauthorized"); return { user, token }; }) @@ -18,8 +20,14 @@ export const fileRouter = { const oldAvatarUrl = metadata?.user?.avatarUrl; // Clean up old avatar file from UploadThing if it exists - if (oldAvatarUrl?.includes(`/a/${process.env.NEXT_PUBLIC_UPLOADTHING_APP_ID}/`)) { - const key = oldAvatarUrl.split(`/a/${process.env.NEXT_PUBLIC_UPLOADTHING_APP_ID}/`)[1]; + if ( + oldAvatarUrl?.includes( + `/a/${process.env.NEXT_PUBLIC_UPLOADTHING_APP_ID}/`, + ) + ) { + const key = oldAvatarUrl.split( + `/a/${process.env.NEXT_PUBLIC_UPLOADTHING_APP_ID}/`, + )[1]; if (key) { await new UTApi().deleteFiles(key); } @@ -28,18 +36,21 @@ export const fileRouter = { // Build new avatar URL using UploadThing slug format const newAvatarUrl = file.url.replace( "/f/", - `/a/${process.env.NEXT_PUBLIC_UPLOADTHING_APP_ID}/` + `/a/${process.env.NEXT_PUBLIC_UPLOADTHING_APP_ID}/`, ); // Sync avatar update to Spring Boot backend - await fetch(`http://localhost:8080/api/users/${metadata.user.id}/avatar`, { - method: "PUT", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${metadata.token}`, + await fetch( + `${process.env.NEXT_PUBLIC_BACKEND_URL || "http://localhost:8080"}/api/users/${metadata.user.id}/avatar`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${metadata.token}`, + }, + body: JSON.stringify({ avatarUrl: newAvatarUrl }), }, - body: JSON.stringify({ avatarUrl: newAvatarUrl }), - }); + ); return { avatarUrl: newAvatarUrl }; }), @@ -49,10 +60,12 @@ export const fileRouter = { video: { maxFileSize: "64MB", maxFileCount: 5 }, }) .middleware(async ({ req }) => { - const cookieHeader = req.headers.get('cookie'); + const cookieHeader = req.headers.get("cookie"); // Assuming the second middleware also needs user and token, if not, it might just need to pass token if user is validated once. // For now, let's assume it also needs user for consistency or future use. - const { user, token } = await validateRequest({ headers: { cookie: cookieHeader ?? undefined } }); + const { user, token } = await validateRequest({ + headers: { cookie: cookieHeader ?? undefined }, + }); if (!user) throw new UploadThingError("Unauthorized"); return { token }; // This middleware only returns token, but validation implies user exists. }) @@ -60,17 +73,20 @@ export const fileRouter = { const mediaType = file.type.startsWith("image") ? "IMAGE" : "VIDEO"; const url = file.url.replace( "/f/", - `/a/${process.env.NEXT_PUBLIC_UPLOADTHING_APP_ID}/` + `/a/${process.env.NEXT_PUBLIC_UPLOADTHING_APP_ID}/`, ); - const response = await fetch("http://localhost:8080/api/media", { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${metadata.token}`, + const response = await fetch( + `${process.env.NEXT_PUBLIC_BACKEND_URL || "http://localhost:8080"}/api/media`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${metadata.token}`, + }, + body: JSON.stringify({ url, type: mediaType }), }, - body: JSON.stringify({ url, type: mediaType }), - }); + ); if (!response.ok) { throw new UploadThingError("Failed to create media record in backend."); diff --git a/src/app/api/validate.ts b/src/app/api/validate.ts index 364c016..89a0bc3 100644 --- a/src/app/api/validate.ts +++ b/src/app/api/validate.ts @@ -5,12 +5,13 @@ import { validateRequest } from "../../auth"; // Replace this with your actual user info fetch logic async function getUserInfo(accessToken: string) { const response = await fetch( - "http://localhost:8081/realms/kong/protocol/openid-connect/userinfo", + process.env.NEXT_PUBLIC_KEYCLOAK_USERINFO_ENDPOINT || + "http://localhost:8081/realms/kong/protocol/openid-connect/userinfo", { headers: { Authorization: `Bearer ${accessToken}`, }, - } + }, ); if (!response.ok) { @@ -20,7 +21,10 @@ async function getUserInfo(accessToken: string) { return await response.json(); } -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - const { user } = await validateRequest(req); - return res.status(200).json({ user }); - } \ No newline at end of file +export default async function handler( + req: NextApiRequest, + res: NextApiResponse, +) { + const { user } = await validateRequest(req); + return res.status(200).json({ user }); +} diff --git a/src/auth.ts b/src/auth.ts index 90f2a53..6efa3c2 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -23,7 +23,9 @@ const adapter = { console.warn("Lucia adapter setSession not fully implemented."); }, updateSessionExpiration: async (sessionId: string, expiresAt: Date) => { - console.warn("Lucia adapter updateSessionExpiration not fully implemented."); + console.warn( + "Lucia adapter updateSessionExpiration not fully implemented.", + ); }, deleteSession: async (sessionId: string) => { console.warn("Lucia adapter deleteSession not fully implemented."); @@ -33,12 +35,12 @@ const adapter = { }, deleteExpiredSessions: async () => { console.warn("Lucia adapter deleteExpiredSessions not fully implemented."); - } + }, // You might also need setUser, updateUser, etc. depending on your adapter }; - -export const lucia = new Lucia(adapter as any, { // Cast as any due to placeholder +export const lucia = new Lucia(adapter as any, { + // Cast as any due to placeholder sessionCookie: { attributes: { secure: process.env.NODE_ENV === "production", @@ -60,12 +62,14 @@ export const lucia = new Lucia(adapter as any, { // Cast as any due to placehold export const google = new Google( process.env.GOOGLE_CLIENT_ID || "YOUR_GOOGLE_CLIENT_ID", // Fallback for build to pass process.env.GOOGLE_CLIENT_SECRET || "YOUR_GOOGLE_CLIENT_SECRET", // Fallback - process.env.GOOGLE_REDIRECT_URI || "http://localhost:3000/login/google/callback" // Fallback + process.env.NEXT_PUBLIC_GOOGLE_REDIRECT_URI || + "http://localhost:3000/login/google/callback", // Fallback ); export async function getUserInfo(accessToken: string) { const response = await fetch( - "http://localhost:8081/realms/kong/protocol/openid-connect/userinfo", + process.env.NEXT_PUBLIC_KEYCLOAK_USERINFO_ENDPOINT || + "http://localhost:8081/realms/kong/protocol/openid-connect/userinfo", { headers: { Authorization: `Bearer ${accessToken}`, diff --git a/src/components/posts/editor/actions.ts b/src/components/posts/editor/actions.ts index c86a3c7..65b67a0 100644 --- a/src/components/posts/editor/actions.ts +++ b/src/components/posts/editor/actions.ts @@ -10,22 +10,27 @@ export async function submitPost(input: { }) { const { caption, email, mediaIds } = createPostSchema.parse(input); - const response = await axios.post(`http://localhost:8080/api/posts/create`, { - email, - caption, - mediaIds, - }); + const response = await axios.post( + `${process.env.NEXT_PUBLIC_BACKEND_URL || "http://localhost:8080"}/api/posts/create`, + { + email, + caption, + mediaIds, + }, + ); return response.data; } - export async function updatePost(input: { postId: string; caption: string }) { const { postId, caption } = input; - const response = await axios.put(`http://localhost:8080/api/posts/${postId}`, { - caption, - }); + const response = await axios.put( + `${process.env.NEXT_PUBLIC_BACKEND_URL || "http://localhost:8080"}/api/posts/${postId}`, + { + caption, + }, + ); return response.data; } diff --git a/src/lib/ky.ts b/src/lib/ky.ts index 21e9db4..5a83742 100644 --- a/src/lib/ky.ts +++ b/src/lib/ky.ts @@ -10,7 +10,7 @@ let failedRequestsQueue: { }[] = []; const defaultOptions: Options = { - prefixUrl: "http://localhost:8080", + prefixUrl: process.env.NEXT_PUBLIC_BACKEND_URL || "http://localhost:8080", credentials: "include", parseJson: (text) => { if (!text || text.trim() === "") return null; @@ -53,7 +53,10 @@ const kyInstance = ky.create({ async (request, options, response) => { const requestUrl = new URL(request.url); // Get full URL for proper check // Check if it's a 401 and not a refresh attempt that failed - if (response.status === 401 && !requestUrl.pathname.endsWith("/api/auth/refresh")) { + if ( + response.status === 401 && + !requestUrl.pathname.endsWith("/api/auth/refresh") + ) { if (!isRefreshing) { isRefreshing = true; try { @@ -78,11 +81,13 @@ const kyInstance = ky.create({ // Retry the original request and all queued requests const originalRetry = ky(request); // Retry original first const queuedRetries = failedRequestsQueue.map( - (prom) => ky(prom.request) // Then retry queued + (prom) => ky(prom.request), // Then retry queued ); // Resolve all promises - failedRequestsQueue.forEach((prom, index) => prom.resolve(queuedRetries[index])); + failedRequestsQueue.forEach((prom, index) => + prom.resolve(queuedRetries[index]), + ); failedRequestsQueue = []; isRefreshing = false; return originalRetry; // Return the promise for the original request @@ -93,7 +98,9 @@ const kyInstance = ky.create({ await refreshResponse.text(), ); // Reject all queued requests - failedRequestsQueue.forEach((prom) => prom.reject(refreshResponse.clone())); // Clone response for multiple rejections + failedRequestsQueue.forEach((prom) => + prom.reject(refreshResponse.clone()), + ); // Clone response for multiple rejections failedRequestsQueue = []; isRefreshing = false; // If refresh fails, the original request's 401 response will be returned @@ -113,23 +120,34 @@ const kyInstance = ky.create({ // Token is already refreshing, queue this request console.log("Token is refreshing. Queuing request:", request.url); return new Promise((resolve, reject) => { - failedRequestsQueue.push({ resolve, reject, request: request.clone() }); // Clone request + failedRequestsQueue.push({ + resolve, + reject, + request: request.clone(), + }); // Clone request }); } - } else if (response.status === 401 && requestUrl.pathname.endsWith("/api/auth/refresh")) { - // This means the refresh token itself is invalid or expired. - console.error("Refresh token is invalid or expired. User needs to re-authenticate."); - isRefreshing = false; // Reset flag - // Reject all queued requests because refresh is impossible - failedRequestsQueue.forEach(prom => prom.reject(response.clone())); // Clone response - failedRequestsQueue = []; + } else if ( + response.status === 401 && + requestUrl.pathname.endsWith("/api/auth/refresh") + ) { + // This means the refresh token itself is invalid or expired. + console.error( + "Refresh token is invalid or expired. User needs to re-authenticate.", + ); + isRefreshing = false; // Reset flag + // Reject all queued requests because refresh is impossible + failedRequestsQueue.forEach((prom) => prom.reject(response.clone())); // Clone response + failedRequestsQueue = []; - // Redirect to login if refresh token is invalid - if (typeof window !== 'undefined') { - console.log('Redirecting to login page due to invalid refresh token.'); - window.location.href = '/login'; - } - return response; // Return the original 401 response for the refresh call + // Redirect to login if refresh token is invalid + if (typeof window !== "undefined") { + console.log( + "Redirecting to login page due to invalid refresh token.", + ); + window.location.href = "/login"; + } + return response; // Return the original 401 response for the refresh call } return response; // For non-401 responses }, @@ -139,7 +157,7 @@ const kyInstance = ky.create({ // New instance for Go backend (remains unchanged) export const goKyInstance = ky.create({ - prefixUrl: "http://localhost:8083", + prefixUrl: process.env.NEXT_PUBLIC_GO_BACKEND_URL || "http://localhost:8083", credentials: "include", parseJson: (text) => { if (!text || text.trim() === "") return null; diff --git a/vercel.json b/vercel.json index 74ab2ea..a43e0ec 100644 --- a/vercel.json +++ b/vercel.json @@ -1,4 +1,8 @@ { + "framework": "nextjs", + "installCommand": "npm install --legacy-peer-deps", + "buildCommand": "npm run build", + "outputDirectory": ".next", "crons": [ { "path": "/api/clear-uploads",