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:
@@ -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"),
|
||||
|
||||
@@ -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 };
|
||||
Reference in New Issue
Block a user