feat(frontend): in-page Casdoor login form + env-gated Developer Mode panel

- redesigned /login (Tailwind, RTL): centered username/password/organization
  form posting to POST /api/v1/auth/login — session starts without leaving
  the page; PM lands on the inventory panel, others on the procurement office
- Developer Mode button fixed top-left opens a role/project test panel
  (admin role added) gated by VITE_ENABLE_DEV_LOGIN (DEV_LOGIN_ENABLED build
  arg in Docker, default enabled)
- removed the old Casdoor redirect button, the PKCE ?code= callback, and the
  public casdoor-config.js — the client secret is no longer shipped to the
  browser; backend mediates the password grant instead
This commit is contained in:
ali_mohaghegh
2026-08-29 14:44:13 +03:30
parent fde2168606
commit 9caa4d804e
8 changed files with 262 additions and 174 deletions
+38
View File
@@ -1340,6 +1340,44 @@ export interface UserRow {
}
export const apiV26 = {
auth: {
// ورود درون‌صفحه‌ای: بک‌اند با grant_type=password از Casdoor توکن می‌گیرد
// و نقش/پروفایل را از جدول users خودمان برمی‌گرداند.
login: async (body: {
username: string;
password: string;
organization?: string;
}): Promise<{ access_token: string; expires_in: number | null; user: import("../store/auth").AuthUser }> => {
const res = await request<{
data: {
access_token: string;
token_type: string;
expires_in: number | null;
user: {
id: string;
casdoor_id: string;
first_name: string;
last_name: string;
phone: string | null;
role: "pm" | "procurement_manager" | "admin" | null;
role_label: string | null;
project_id: string | null;
project_name: string | null;
};
};
}>("/auth/login", { method: "POST", body: JSON.stringify(body) });
const u = res.data.user;
return {
access_token: res.data.access_token,
expires_in: res.data.expires_in,
user: {
...u,
display_name: `${u.first_name} ${u.last_name}`.trim() || u.casdoor_id,
},
};
},
},
lookup: {
categories: async (): Promise<RawCat[]> => getData<RawCat[]>("/material-categories?page_size=100"),
units: async (): Promise<RawUnitL[]> => getData<RawUnitL[]>("/units?page_size=100"),
-79
View File
@@ -1,79 +0,0 @@
// ============================================================
// Casdoor OIDC + PKCE login — v2.6
// Config lives in /casdoor-config.js (window.CASDOOR_CONFIG), served
// statically next to index.html (same values as the shipped dash).
// ============================================================
const CASDOOR_CONFIG = (window as unknown as { CASDOOR_CONFIG?: CasdoorConfig }).CASDOOR_CONFIG;
export interface CasdoorConfig {
serverUrl: string;
clientId: string;
clientSecret: string;
organization: string;
appName: string;
redirectUri: string;
}
function b64url(bytes: Uint8Array): string {
let s = "";
for (const b of bytes) s += String.fromCharCode(b);
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
function randomBytes(n: number): Uint8Array {
const arr = new Uint8Array(n);
crypto.getRandomValues(arr);
return arr;
}
async function sha256b64url(text: string): Promise<string> {
const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
return b64url(new Uint8Array(buf));
}
export function startCasdoorLogin(): void {
if (!CASDOOR_CONFIG) {
alert("تنظیمات Casdoor یافت نشد — فایل casdoor-config.js را بررسی کنید");
return;
}
const verifier = b64url(randomBytes(64));
const state = b64url(randomBytes(16));
sessionStorage.setItem("casdoor-pkce", JSON.stringify({ verifier, state }));
void sha256b64url(verifier).then((challenge) => {
const params = new URLSearchParams({
client_id: CASDOOR_CONFIG.clientId,
redirect_uri: CASDOOR_CONFIG.redirectUri,
response_type: "code",
scope: "read",
state,
organization: CASDOOR_CONFIG.organization,
application: CASDOOR_CONFIG.appName,
code_challenge: challenge,
code_challenge_method: "S256",
});
window.location.href = `${CASDOOR_CONFIG.serverUrl}/login/oauth/authorize?${params.toString()}`;
});
}
/** Exchange the OIDC authorization code for a token (called on /callback). */
export async function exchangeCodeForToken(code: string): Promise<{ access_token: string; id_token?: string }> {
if (!CASDOOR_CONFIG) throw new Error("تنظیمات Casdoor یافت نشد");
const pkce = JSON.parse(sessionStorage.getItem("casdoor-pkce") || "{}") as { verifier?: string };
const resp = await fetch(`${CASDOOR_CONFIG.serverUrl}/api/login/oauth/access_token`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
client_id: CASDOOR_CONFIG.clientId,
client_secret: CASDOOR_CONFIG.clientSecret,
code,
redirect_uri: CASDOOR_CONFIG.redirectUri,
code_verifier: pkce.verifier || "",
}),
});
if (!resp.ok) throw new Error("خطا در دریافت توکن از Casdoor");
return resp.json();
}
export { CASDOOR_CONFIG };