diff --git a/app/api/auth/callback/route.ts b/app/api/auth/callback/route.ts index 575d75b..819942f 100644 --- a/app/api/auth/callback/route.ts +++ b/app/api/auth/callback/route.ts @@ -1,27 +1,28 @@ import { getCookieDomain } from "@/lib/getDomain"; +import { env } from "@/lib/env"; import { NextResponse } from "next/server"; +const BASE_URL = env.APP_URL; export async function GET(req: Request) { const url = new URL(req.url); const code = url.searchParams.get("code"); - const origin = url.origin; const isHttps = url.protocol === "https:"; - const domain = getCookieDomain(url.hostname); // ← domain only + const domain = env.COOKIE_DOMAIN ?? getCookieDomain(url.hostname); if (!code) { - return NextResponse.redirect(`${origin}/login?error=missing_code`); + return NextResponse.redirect(`${BASE_URL}/login?error=missing_code`); } const redirectUri = `${origin}/api/auth/callback`; const tokenRes = await fetch( - "https://keycloak.petermaquiran.xyz/realms/tvone/protocol/openid-connect/token", + `${env.KEYCLOAK_BASE_URL}/realms/${env.KEYCLOAK_REALM}/protocol/openid-connect/token`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ - client_id: "tvone-web", - client_secret: "7jQUciQCCf2WRFRe170UANKzGKVWFIkY", + client_id: env.KEYCLOAK_CLIENT_ID, + client_secret: env.KEYCLOAK_CLIENT_SECRET, grant_type: "authorization_code", code, redirect_uri: redirectUri, @@ -34,15 +35,15 @@ export async function GET(req: Request) { try { data = JSON.parse(text) as typeof data; } catch { - return NextResponse.redirect(`${origin}/login?error=token_parse`); + return NextResponse.redirect(`${BASE_URL}/login?error=token_parse`); } if (!tokenRes.ok || !data.access_token) { console.error("token exchange failed", tokenRes.status, text); - return NextResponse.redirect(`${origin}/login?error=token_exchange`); + return NextResponse.redirect(`${BASE_URL}/login?error=token_exchange`); } - const res = NextResponse.redirect(`${origin}/admin/create-news`); + const res = NextResponse.redirect(`${BASE_URL}/admin/create-news`); // Secure cookies are ignored on http:// (e.g. localhost) — browser drops them. res.cookies.set("access_token", data.access_token, { diff --git a/app/api/auth/login/route.ts b/app/api/auth/login/route.ts index 26c15ba..9a5bded 100644 --- a/app/api/auth/login/route.ts +++ b/app/api/auth/login/route.ts @@ -1,18 +1,19 @@ +import { env } from "@/lib/env"; -export async function GET(req: Request) { - const url = new URL(req.url); - const origin = url.origin; - - const redirect = encodeURIComponent( - `${origin}/api/auth/callback` - ); - - const keycloakUrl = - `https://keycloak.petermaquiran.xyz/realms/tvone/protocol/openid-connect/auth` + - `?client_id=tvone-web` + - `&response_type=code` + - `&scope=openid` + - `&redirect_uri=${redirect}`; - - return Response.redirect(keycloakUrl); +const BASE_URL = env.APP_URL; + + +export async function GET() { + const redirect = encodeURIComponent( + `${BASE_URL}/api/auth/callback` + ); + + const keycloakUrl = + `${env.KEYCLOAK_BASE_URL}/realms/${env.KEYCLOAK_REALM}/protocol/openid-connect/auth` + + `?client_id=${encodeURIComponent(env.KEYCLOAK_CLIENT_ID)}` + + `&response_type=code` + + `&scope=openid` + + `&redirect_uri=${redirect}`; + + return Response.redirect(keycloakUrl); } \ No newline at end of file diff --git a/app/api/auth/refresh/route.ts b/app/api/auth/refresh/route.ts index a7b678a..75abf35 100644 --- a/app/api/auth/refresh/route.ts +++ b/app/api/auth/refresh/route.ts @@ -1,6 +1,8 @@ import { NextResponse } from "next/server"; +import { env } from "@/lib/env"; export async function GET(req: Request) { + const isHttps = new URL(req.url).protocol === "https:"; const cookie = req.headers.get("cookie"); const refreshToken = cookie @@ -13,8 +15,7 @@ export async function GET(req: Request) { } try { - // Call your auth server (Keycloak or NestJS) - const res = await fetch("http://api.example.com/auth/refresh", { + const res = await fetch(`${env.AUTH_API_URL}/auth/refresh`, { method: "POST", headers: { "Content-Type": "application/json", @@ -33,19 +34,19 @@ export async function GET(req: Request) { // 🍪 Set new access token response.cookies.set("access_token", data.access_token, { httpOnly: true, - secure: true, + secure: isHttps, sameSite: "lax", path: "/", - domain: ".example.com", + ...(env.COOKIE_DOMAIN ? { domain: env.COOKIE_DOMAIN } : {}), maxAge: data.expires_in, }); response.cookies.set("refresh_token", data.refresh_token, { httpOnly: true, - secure: true, + secure: isHttps, sameSite: "lax", path: "/", - domain: ".example.com", + ...(env.COOKIE_DOMAIN ? { domain: env.COOKIE_DOMAIN } : {}), maxAge: data.expires_in, }); diff --git a/lib/env.ts b/lib/env.ts new file mode 100644 index 0000000..31eff8a --- /dev/null +++ b/lib/env.ts @@ -0,0 +1,26 @@ +import { loadEnvConfig } from "@next/env"; + +loadEnvConfig(process.cwd()); + +function getRequiredEnv(name: string): string { + const value = process.env[name]; + if (!value) { + throw new Error(`Missing required environment variable: ${name}`); + } + return value; +} + +function getOptionalEnv(name: string): string | undefined { + const value = process.env[name]; + return value && value.trim().length > 0 ? value : undefined; +} + +export const env = { + APP_URL: getRequiredEnv("APP_URL"), + KEYCLOAK_BASE_URL: getRequiredEnv("KEYCLOAK_BASE_URL"), + KEYCLOAK_REALM: getRequiredEnv("KEYCLOAK_REALM"), + KEYCLOAK_CLIENT_ID: getRequiredEnv("KEYCLOAK_CLIENT_ID"), + KEYCLOAK_CLIENT_SECRET: getRequiredEnv("KEYCLOAK_CLIENT_SECRET"), + AUTH_API_URL: getRequiredEnv("AUTH_API_URL"), + COOKIE_DOMAIN: getOptionalEnv("COOKIE_DOMAIN"), +}; diff --git a/package.json b/package.json index 33acc07..b39a6bf 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", + "@next/env": "^16.2.4", "@react-oauth/google": "^0.13.5", "@tinymce/tinymce-react": "^6.3.0", "framer-motion": "^12.38.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bfbd6e8..2572c58 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@dnd-kit/utilities': specifier: ^3.2.2 version: 3.2.2(react@19.2.4) + '@next/env': + specifier: ^16.2.4 + version: 16.2.4 '@react-oauth/google': specifier: ^0.13.5 version: 0.13.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -415,6 +418,9 @@ packages: '@next/env@16.2.1': resolution: {integrity: sha512-n8P/HCkIWW+gVal2Z8XqXJ6aB3J0tuM29OcHpCsobWlChH/SITBs1DFBk/HajgrwDkqqBXPbuUuzgDvUekREPg==} + '@next/env@16.2.4': + resolution: {integrity: sha512-dKkkOzOSwFYe5RX6y26fZgkSpVAlIOJKQHIiydQcrWH6y/97+RceSOAdjZ14Qa3zLduVUy0TXcn+EiM6t4rPgw==} + '@next/eslint-plugin-next@16.2.1': resolution: {integrity: sha512-r0epZGo24eT4g08jJlg2OEryBphXqO8aL18oajoTKLzHJ6jVr6P6FI58DLMug04MwD3j8Fj0YK0slyzneKVyzA==} @@ -2414,6 +2420,8 @@ snapshots: '@next/env@16.2.1': {} + '@next/env@16.2.4': {} + '@next/eslint-plugin-next@16.2.1': dependencies: fast-glob: 3.3.1