mobile2: Casdoor OIDC auth deployment

- New branch for mobile2.falah-os.com with Casdoor OIDC
- Auth page with 'Sign in with Casdoor' button
- OIDC callback: exchanges code via server-side API route
- env vars configured on Netlify
- DNS auto-configured via Netlify (falah-os.com zone)
This commit is contained in:
2026-06-29 00:09:32 +02:00
parent 2dd257533e
commit 6f31d6043c
7 changed files with 202 additions and 9 deletions
@@ -0,0 +1,79 @@
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 });
}
if (!CLIENT_SECRET) {
return NextResponse.json({ error: "Casdoor client secret not configured" }, { status: 500 });
}
// Exchange authorization code for tokens
const tokenRes = await fetch(CASDOOR_TOKEN_URL, {
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: "https://mobile2.falah-os.com/auth/callback",
}),
});
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 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 });
}
// 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();
}
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,
});
} catch (error) {
console.error("[casdoor-oidc] Callback error:", error);
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
}
}
+49 -4
View File
@@ -13,6 +13,7 @@ function AuthCallbackContent() {
useEffect(() => {
const token = searchParams.get("token");
const code = searchParams.get("code");
const error = searchParams.get("error");
if (error) {
@@ -21,17 +22,62 @@ function AuthCallbackContent() {
return;
}
// --- Casdoor OIDC flow: exchange authorization code for tokens ---
if (code) {
fetch(`/api/auth/casdoor-callback?code=${encodeURIComponent(code)}`)
.then((res) => res.json())
.then((data) => {
if (data.error) {
setStatus("error");
setErrorMsg(data.error);
return;
}
// Store the access token
localStorage.setItem("flh_token", data.access_token);
if (data.id_token) {
localStorage.setItem("flh_id_token", data.id_token);
}
if (data.refresh_token) {
localStorage.setItem("flh_refresh_token", data.refresh_token);
}
if (data.user) {
localStorage.setItem("flh_user", JSON.stringify(data.user));
}
// Dispatch storage event so AuthContext picks it up
window.dispatchEvent(
new StorageEvent("storage", {
key: "flh_token",
newValue: data.access_token,
oldValue: null,
storageArea: localStorage,
})
);
setStatus("success");
// Redirect to home
setTimeout(() => router.push("/"), 500);
})
.catch((err) => {
console.error("[oidc-callback] Token exchange error:", err);
setStatus("error");
setErrorMsg("Failed to complete authentication. Please try again.");
});
return;
}
// --- Legacy UmmahID flow: token passed directly in URL ---
if (!token) {
setStatus("error");
setErrorMsg("No authentication token received.");
setErrorMsg("No authentication code received.");
return;
}
// Store token in localStorage
localStorage.setItem("flh_token", token);
// Dispatch a custom event so the AuthContext can pick it up
// (used by popup-based OAuth where the opener listens)
window.dispatchEvent(
new StorageEvent("storage", {
key: "flh_token",
@@ -47,7 +93,6 @@ function AuthCallbackContent() {
if (window.opener) {
setTimeout(() => window.close(), 500);
} else {
// Redirect to home
setTimeout(() => router.push("/"), 500);
}
}, [searchParams, router]);
+26 -1
View File
@@ -3,9 +3,18 @@
import { Suspense, useState, FormEvent, useEffect } from "react";
import { useAuth } from "@/lib/AuthContext";
import { useRouter, useSearchParams } from "next/navigation";
import { Mail, ArrowLeft } from "lucide-react";
import { Mail, ArrowLeft, Shield } from "lucide-react";
import ErrorFeedback from "@/components/ErrorFeedback";
const getCasdoorUrl = () => {
const state = Math.random().toString(36).slice(2, 10);
const base = process.env.NEXT_PUBLIC_CASDOOR_SERVER_URL || "https://auth.falahos.my";
const clientId = process.env.NEXT_PUBLIC_CASDOOR_CLIENT_ID || "272f91105da5cf984773";
const redirect = process.env.NEXT_PUBLIC_CASDOOR_REDIRECT_URI ||
"https://mobile2.falah-os.com/auth/callback";
return `${base}/login/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirect)}&response_type=code&scope=openid+profile+email&state=${state}`;
};
type AuthMode = "login" | "register";
type PageState = "select" | "email-form";
@@ -89,6 +98,22 @@ function AuthPageInner() {
{/* Sign-in Methods */}
<div className="space-y-3">
{/* Casdoor OIDC */}
<a
href={getCasdoorUrl()}
className="w-full flex items-center gap-4 px-5 py-4 rounded-2xl bg-green-900/20 border border-green-600/30 active:bg-green-900/30 transition-all touch-manipulation"
>
<div className="w-6 h-6 shrink-0 flex items-center justify-center">
<Shield size={22} className="text-green-400" />
</div>
<div className="text-left">
<span className="text-sm font-medium text-green-400 block">
Sign in with Casdoor
</span>
<span className="text-xs text-gray-600">FalahOS Identity Service</span>
</div>
</a>
{/* Email */}
<button
onClick={handleEmailClick}