diff --git a/next.config.ts b/next.config.ts index 7b496f7..1782c73 100644 --- a/next.config.ts +++ b/next.config.ts @@ -7,6 +7,7 @@ const nextConfig: NextConfig = { NEXT_PUBLIC_API_URL: "https://api.app.witlab.us", // NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL, NEXT_PUBLIC_APP_URL: "https://app.witlab.us", + NEXT_PUBLIC_AUTH_REDIRECT_URL: "https://witlab.us", }, images: { remotePatterns: [ diff --git a/src/middleware.ts b/src/middleware.ts index 688b599..56226f0 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,8 +1,50 @@ +import { NextRequest, NextResponse } from "next/server"; import createMiddleware from "next-intl/middleware"; import { routing } from "./i18n/routing"; +import { ROUTES } from "./shared/constants/client-routes"; -export default createMiddleware(routing); +const publicRoutes = [ + ROUTES.authCallback(), + ROUTES.paymentSuccess(), + ROUTES.paymentFailed(), +]; + +function isPublicRoute(pathname: string): boolean { + const pathWithoutLocale = pathname.replace(/^\/[a-z]{2}\//, "/"); + return publicRoutes.some(route => pathWithoutLocale.startsWith(route)); +} + +function createAuthMiddleware() { + return async function authMiddleware(request: NextRequest) { + const pathname = request.nextUrl.pathname; + + if (isPublicRoute(pathname)) { + return NextResponse.next(); + } + + const token = request.cookies.get("accessToken")?.value; + + if (!token) { + return NextResponse.redirect( + process.env.NEXT_PUBLIC_AUTH_REDIRECT_URL || "" + ); + } + + return NextResponse.next(); + }; +} + +const intlMiddleware = createMiddleware(routing); + +export default async function middleware(request: NextRequest) { + const authResponse = await createAuthMiddleware()(request); + if (authResponse.status !== 200) { + return authResponse; + } + + return intlMiddleware(request); +} export const config = { // Match all pathnames except for diff --git a/src/shared/api/httpClient.ts b/src/shared/api/httpClient.ts index 26c0b4a..bf969d1 100644 --- a/src/shared/api/httpClient.ts +++ b/src/shared/api/httpClient.ts @@ -1,3 +1,4 @@ +import { redirect } from "next/navigation"; import { z } from "zod"; import { getServerAccessToken } from "../auth/token"; @@ -18,6 +19,7 @@ type RequestOpts = Omit & { query?: Record; // query-string schema?: z.ZodTypeAny; // runtime validation revalidate?: number; + skipAuthRedirect?: boolean; }; class HttpClient { @@ -44,7 +46,14 @@ class HttpClient { body?: unknown, errorMessage?: string ): Promise { - const { tags = [], schema, query, revalidate = 300, ...rest } = opts; + const { + tags = [], + schema, + query, + revalidate = 300, + skipAuthRedirect = false, + ...rest + } = opts; const headers = new Headers(); const accessToken = await getServerAccessToken(); @@ -61,7 +70,12 @@ class HttpClient { const payload = await res.json().catch(() => null); - if (!res.ok) throw new ApiError(res.status, payload, errorMessage); + if (!res.ok) { + if (res.status === 401 && !skipAuthRedirect) { + redirect(process.env.NEXT_PUBLIC_AUTH_REDIRECT_URL || ""); + } + throw new ApiError(res.status, payload, errorMessage); + } const data = payload as T; return schema ? schema.parse(data) : data; diff --git a/src/shared/constants/client-routes.ts b/src/shared/constants/client-routes.ts index edea7e4..9777ba9 100644 --- a/src/shared/constants/client-routes.ts +++ b/src/shared/constants/client-routes.ts @@ -10,6 +10,9 @@ const createRoute = (segments: string[]): string => { export const ROUTES = { home: () => createRoute([]), + // Auth + authCallback: () => createRoute(["auth", "callback"]), + // Breath breath: (id: string) => createRoute(["breath", id]), breathResult: (id: string, resultId: string) =>