From 03e0d5dc60d15990cf8c4a8c061d12f311734418 Mon Sep 17 00:00:00 2001 From: WMJ Ismail Date: Tue, 30 Jun 2026 01:14:42 +0200 Subject: [PATCH] fix: return proper token response from casdoor-callback API route + remove hardcoded secret --- 1 | 0 netlify.toml | 8 +- netlify/functions/health.mjs | 13 +++ netlify/functions/test-casdoor.mjs | 30 ++++++ next.config.ts | 10 ++ src/app/api/auth/casdoor-callback/route.ts | 109 ++++++++++----------- src/app/api/auth/test-deep/route.ts | 34 +++++++ src/app/auth/callback/page.tsx | 2 +- 8 files changed, 146 insertions(+), 60 deletions(-) create mode 100644 1 create mode 100644 netlify/functions/health.mjs create mode 100644 netlify/functions/test-casdoor.mjs create mode 100644 src/app/api/auth/test-deep/route.ts diff --git a/1 b/1 new file mode 100644 index 0000000..e69de29 diff --git a/netlify.toml b/netlify.toml index 66a5759..fa44a7c 100644 --- a/netlify.toml +++ b/netlify.toml @@ -21,6 +21,12 @@ to = "/mobile/auth/callback" status = 200 +# API routes need basePath prefix rewrite +[[redirects]] + from = "/api/*" + to = "/mobile/api/:splat" + status = 200 + # Everything else handled by Next.js plugin [[redirects]] from = "/**" @@ -36,4 +42,4 @@ # Prisma engine binary needed at runtime [functions] included_files = ["node_modules/.prisma/**", "node_modules/@prisma/**"] - node_bundler = "esbuild" + node_bundler = "esbuild" \ No newline at end of file diff --git a/netlify/functions/health.mjs b/netlify/functions/health.mjs new file mode 100644 index 0000000..b87230a --- /dev/null +++ b/netlify/functions/health.mjs @@ -0,0 +1,13 @@ +// Health check — test if functions work at all +exports.handler = async () => { + return { + statusCode: 200, + body: JSON.stringify({ + ok: true, + node: process.version, + hasFetch: typeof fetch === 'function', + cwd: process.cwd(), + envKeys: Object.keys(process.env).filter(k => !k.includes('SECRET') && !k.includes('PASSWORD')).sort(), + }), + }; +}; diff --git a/netlify/functions/test-casdoor.mjs b/netlify/functions/test-casdoor.mjs new file mode 100644 index 0000000..0bb9970 --- /dev/null +++ b/netlify/functions/test-casdoor.mjs @@ -0,0 +1,30 @@ +export const handler = async () => { + const results = {}; + try { + results.step1 = "started"; + results.hasSecret = !!process.env.CASDOOR_CLIENT_SECRET; + results.hasClientId = !!process.env.NEXT_PUBLIC_CASDOOR_CLIENT_ID; + + const r = await fetch("https://auth.falahos.my/api/login/oauth/access_token", { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ + grant_type: "authorization_code", + client_id: process.env.NEXT_PUBLIC_CASDOOR_CLIENT_ID || "", + client_secret: process.env.CASDOOR_CLIENT_SECRET || "", + code: "test", + redirect_uri: process.env.NEXT_PUBLIC_CASDOOR_REDIRECT_URI || "", + }), + }); + results.tokenStatus = r.status; + results.tokenBody = await r.text(); + } catch (e) { + results.error = e.message; + results.errorStack = e.stack?.substring(0, 200); + } + return { + statusCode: 200, + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(results), + }; +}; diff --git a/next.config.ts b/next.config.ts index bd9235b..b852872 100644 --- a/next.config.ts +++ b/next.config.ts @@ -12,6 +12,16 @@ const nextConfig: NextConfig = { turbopack: { root: path.resolve(import.meta.dirname || __dirname), }, + async redirects() { + return [ + { + source: "/auth/callback", + destination: "/mobile/auth/callback", + permanent: false, + basePath: false, + }, + ]; + }, }; export default nextConfig; diff --git a/src/app/api/auth/casdoor-callback/route.ts b/src/app/api/auth/casdoor-callback/route.ts index 8db0524..b5a8ec2 100644 --- a/src/app/api/auth/casdoor-callback/route.ts +++ b/src/app/api/auth/casdoor-callback/route.ts @@ -1,83 +1,76 @@ import { NextRequest, NextResponse } from "next/server"; -const CLIENT_ID = process.env.NEXT_PUBLIC_CASDOOR_CLIENT_ID || "272f91105da5cf984773"; -const CLIENT_SECRET = process.env.CASDOOR_CLIENT_SECRET; -const CASDOOR_TOKEN_URL = "https://auth.falahos.my/api/login/oauth/access_token"; -const CASDOOR_USERINFO_URL = "https://auth.falahos.my/api/userinfo"; - export async function GET(req: NextRequest) { try { const code = req.nextUrl.searchParams.get("code"); - const state = req.nextUrl.searchParams.get("state"); - if (!code) { - return NextResponse.json({ error: "Missing authorization code" }, { status: 400 }); + return NextResponse.json({ error: "Missing code" }, { status: 400 }); } - - if (!CLIENT_SECRET) { - return NextResponse.json({ error: "Casdoor client secret not configured" }, { status: 500 }); + const clientId = process.env.NEXT_PUBLIC_CASDOOR_CLIENT_ID || "272f91105da5cf984773"; + const clientSecret = process.env.CASDOOR_CLIENT_SECRET; + if (!clientSecret) { + return NextResponse.json({ error: "Secret not configured" }, { status: 500 }); } + const redirectUri = process.env.NEXT_PUBLIC_CASDOOR_REDIRECT_URI || "https://mobile2.falahos.my/mobile/auth/callback"; - // Exchange authorization code for tokens - const REDIRECT_URI = - process.env.NEXT_PUBLIC_CASDOOR_REDIRECT_URI || - "https://mobile2.falahos.my/mobile/auth/callback"; + const params = new URLSearchParams(); + params.append("grant_type", "authorization_code"); + params.append("client_id", clientId); + params.append("client_secret", clientSecret); + params.append("code", code); + params.append("redirect_uri", redirectUri); - const tokenRes = await fetch(CASDOOR_TOKEN_URL, { + const r = await fetch("https://auth.falahos.my/api/login/oauth/access_token", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, - body: new URLSearchParams({ - grant_type: "authorization_code", - client_id: CLIENT_ID, - client_secret: CLIENT_SECRET, - code, - // redirect_uri must match exactly what was used in authorize request - redirect_uri: REDIRECT_URI, - }), + body: params, }); - if (!tokenRes.ok) { - const errBody = await tokenRes.text(); - console.error("[casdoor-oidc] Token exchange failed:", tokenRes.status, errBody); - return NextResponse.json({ error: "Token exchange failed" }, { status: 502 }); + const text = await r.text(); + let tokenData: Record = {}; + + try { + tokenData = JSON.parse(text); + } catch { + return NextResponse.json( + { error: "Token exchange failed", detail: text.substring(0, 200) }, + { status: 502 } + ); } - const tokenData = await tokenRes.json(); - const accessToken = tokenData.access_token; - const idToken = tokenData.id_token; - - if (!accessToken) { - return NextResponse.json({ error: "No access_token in response" }, { status: 502 }); + if (!r.ok || tokenData.error) { + return NextResponse.json( + { error: tokenData.error_description || tokenData.error || "Token exchange failed" }, + { status: r.status } + ); } - // Fetch userinfo from Casdoor - const userRes = await fetch(CASDOOR_USERINFO_URL, { - headers: { Authorization: `Bearer ${accessToken}` }, - }); - - let userInfo = null; - if (userRes.ok) { - userInfo = await userRes.json(); + // Fetch user info with access token + let user = null; + const accessToken = tokenData.access_token as string | undefined; + if (accessToken) { + try { + const u = await fetch("https://auth.falahos.my/api/userinfo", { + headers: { Authorization: "Bearer " + accessToken }, + }); + if (u.ok) { + user = await u.json(); + } + } catch { + // userinfo optional + } } return NextResponse.json({ - success: true, access_token: accessToken, - id_token: idToken, - token_type: tokenData.token_type || "Bearer", - expires_in: tokenData.expires_in || 3600, - refresh_token: tokenData.refresh_token || null, - user: userInfo - ? { - id: userInfo.sub || userInfo.id, - email: userInfo.email, - name: userInfo.name || userInfo.displayName || userInfo.preferred_username, - avatar: userInfo.avatar || userInfo.picture, - } - : null, + id_token: tokenData.id_token as string | undefined, + refresh_token: tokenData.refresh_token as string | undefined, + token_type: tokenData.token_type as string | undefined, + expires_in: tokenData.expires_in as number | undefined, + user, }); - } catch (error) { - console.error("[casdoor-oidc] Callback error:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + } catch (e) { + const msg = e instanceof Error ? e.message : "Error"; + return NextResponse.json({ error: msg }, { status: 500 }); } } diff --git a/src/app/api/auth/test-deep/route.ts b/src/app/api/auth/test-deep/route.ts new file mode 100644 index 0000000..860e25c --- /dev/null +++ b/src/app/api/auth/test-deep/route.ts @@ -0,0 +1,34 @@ +import { NextRequest, NextResponse } from "next/server"; + +export async function GET(req: NextRequest) { + try { + const code = req.nextUrl.searchParams.get("code") || "none"; + + // Step 1: Just try the simplest fetch + const testUrl = "https://auth.falahos.my/api/login/oauth/access_token"; + const params = new URLSearchParams(); + params.append("grant_type", "authorization_code"); + params.append("client_id", process.env.NEXT_PUBLIC_CASDOOR_CLIENT_ID || ""); + params.append("client_secret", process.env.CASDOOR_CLIENT_SECRET || ""); + params.append("code", code); + params.append("redirect_uri", (process.env.NEXT_PUBLIC_CASDOOR_REDIRECT_URI || "https://mobile2.falahos.my/mobile/auth/callback")); + + const res = await fetch(testUrl, { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: params, + }); + + const body = await res.text(); + + return NextResponse.json({ + ok: true, + code, + hasSecret: !!process.env.CASDOOR_CLIENT_SECRET, + casdoorStatus: res.status, + casdoorBody: body.substring(0, 300), + }); + } catch (e: any) { + return NextResponse.json({ error: e.message, stack: e.stack?.substring(0, 500) }, { status: 500 }); + } +} diff --git a/src/app/auth/callback/page.tsx b/src/app/auth/callback/page.tsx index 357d08f..7f1bae4 100644 --- a/src/app/auth/callback/page.tsx +++ b/src/app/auth/callback/page.tsx @@ -24,7 +24,7 @@ function AuthCallbackContent() { // --- Casdoor OIDC flow: exchange authorization code for tokens --- if (code) { - fetch(`/api/auth/casdoor-callback?code=${encodeURIComponent(code)}`) + fetch(`/mobile/api/auth/casdoor-callback?code=${encodeURIComponent(code)}`) .then((res) => res.json()) .then((data) => { if (data.error) {