120 lines
4.4 KiB
TypeScript
120 lines
4.4 KiB
TypeScript
export const SESSION_COOKIE_NAME = "zhinian_session";
|
|
export const AUTH_STATE_COOKIE_NAME = "zhinian_auth_state";
|
|
|
|
export type AuthRuntimeConfig = {
|
|
required: boolean;
|
|
configured: boolean;
|
|
missing: string[];
|
|
authBaseUrl?: string;
|
|
authorizeUrl?: string;
|
|
tokenUrl?: string;
|
|
jwksUrl?: string;
|
|
logoutUrl?: string;
|
|
clientId: string;
|
|
clientSecret?: string;
|
|
scope: string;
|
|
issuer: string;
|
|
sessionSecret?: string;
|
|
clockSkewSeconds: number;
|
|
};
|
|
|
|
export function getAuthRuntimeConfig(): AuthRuntimeConfig {
|
|
const authBaseUrl = trimTrailingSlash(envValue("ZHINIAN_AUTH_BASE_URL", "AUTH_BASE"));
|
|
const clientId = envValue("ZHINIAN_AUTH_CLIENT_ID", "AUTH_CLIENT_ID") || "customPC";
|
|
const clientSecret = envValue("ZHINIAN_AUTH_CLIENT_SECRET", "AUTH_CLIENT_SECRET");
|
|
const scope = envValue("ZHINIAN_AUTH_SCOPE", "AUTH_SCOPE") || "server";
|
|
const issuer = envValue("ZHINIAN_AUTH_ISSUER", "AUTH_ISSUER") || "https://pig4cloud.com";
|
|
const sessionSecret = envValue("ZHINIAN_AUTH_SESSION_SECRET", "AUTH_SESSION_SECRET", "NEXTAUTH_SECRET");
|
|
const explicitRequired = boolEnv("ZHINIAN_AUTH_REQUIRED");
|
|
const disabled = boolEnv("ZHINIAN_AUTH_DISABLED") === true;
|
|
const hasAnyAuthConfig = Boolean(authBaseUrl || clientSecret || sessionSecret);
|
|
const required = disabled ? false : explicitRequired ?? (process.env.NODE_ENV === "production" || Boolean(authBaseUrl));
|
|
const wantsConfiguration = required || hasAnyAuthConfig;
|
|
const missing: string[] = [];
|
|
|
|
if (wantsConfiguration && !authBaseUrl) missing.push("ZHINIAN_AUTH_BASE_URL");
|
|
if (wantsConfiguration && !clientSecret) missing.push("ZHINIAN_AUTH_CLIENT_SECRET");
|
|
if (wantsConfiguration && !sessionSecret) missing.push("ZHINIAN_AUTH_SESSION_SECRET");
|
|
|
|
return {
|
|
required,
|
|
configured: wantsConfiguration && missing.length === 0,
|
|
missing,
|
|
authBaseUrl,
|
|
authorizeUrl: endpointUrl(authBaseUrl, "ZHINIAN_AUTH_AUTHORIZE_URL", "/oauth2/authorize"),
|
|
tokenUrl: endpointUrl(authBaseUrl, "ZHINIAN_AUTH_TOKEN_URL", "/oauth2/token"),
|
|
jwksUrl: endpointUrl(authBaseUrl, "ZHINIAN_AUTH_JWKS_URL", "/oauth2/jwks"),
|
|
logoutUrl: endpointUrl(authBaseUrl, "ZHINIAN_AUTH_LOGOUT_URL", "/token/logout"),
|
|
clientId,
|
|
clientSecret,
|
|
scope,
|
|
issuer,
|
|
sessionSecret,
|
|
clockSkewSeconds: numberEnv("ZHINIAN_AUTH_CLOCK_SKEW_SECONDS") ?? 60
|
|
};
|
|
}
|
|
|
|
export function safeNextPath(value: string | null | undefined, fallback = "/create"): string {
|
|
if (!value || !value.startsWith("/") || value.startsWith("//")) return fallback;
|
|
try {
|
|
const parsed = new URL(value, "http://zhinian.local");
|
|
if (parsed.origin !== "http://zhinian.local") return fallback;
|
|
const path = `${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
if (path.startsWith("/api/auth") || path.startsWith("/auth/login")) return fallback;
|
|
return path;
|
|
} catch {
|
|
return fallback;
|
|
}
|
|
}
|
|
|
|
export function authConfigSummary(config = getAuthRuntimeConfig()) {
|
|
if (!config.required) return "disabled";
|
|
return config.configured ? "configured" : "missing";
|
|
}
|
|
|
|
export function shouldUseSecureAuthCookie(requestUrl?: string): boolean {
|
|
const explicit = boolEnv("ZHINIAN_AUTH_COOKIE_SECURE");
|
|
if (explicit !== undefined) return explicit;
|
|
const configured = envValue("NEXT_PUBLIC_APP_URL", "ZHINIAN_PUBLIC_BASE_URL");
|
|
const candidate = configured || requestUrl;
|
|
if (!candidate) return false;
|
|
try {
|
|
return new URL(candidate).protocol === "https:";
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function endpointUrl(authBaseUrl: string | undefined, overrideKey: string, path: string) {
|
|
const override = envValue(overrideKey);
|
|
if (override) return override.replace(/\/$/, "");
|
|
return authBaseUrl ? `${authBaseUrl}${path}` : undefined;
|
|
}
|
|
|
|
function envValue(...names: string[]): string | undefined {
|
|
for (const name of names) {
|
|
const value = process.env[name]?.trim();
|
|
if (value) return value;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function boolEnv(name: string): boolean | undefined {
|
|
const value = process.env[name]?.trim().toLowerCase();
|
|
if (!value || value === "auto") return undefined;
|
|
if (["1", "true", "yes", "on"].includes(value)) return true;
|
|
if (["0", "false", "no", "off"].includes(value)) return false;
|
|
return undefined;
|
|
}
|
|
|
|
function numberEnv(name: string): number | undefined {
|
|
const value = process.env[name]?.trim();
|
|
if (!value) return undefined;
|
|
const parsed = Number(value);
|
|
return Number.isFinite(parsed) ? parsed : undefined;
|
|
}
|
|
|
|
function trimTrailingSlash(value?: string) {
|
|
return value?.replace(/\/$/, "");
|
|
}
|