feat: add database-refreshed identity authorization

This commit is contained in:
2026-08-13 11:56:36 +08:00
parent 716a8031b1
commit c849077591
12 changed files with 1809 additions and 15 deletions

View File

@@ -3,8 +3,8 @@ import { SESSION_COOKIE_NAME, getAuthRuntimeConfig } from "@/lib/auth/config";
import { hasAdminSessionAccess, hasSuperAdminAccess } from "@/lib/auth/permissions";
import { parseSessionCookieValue, readChunkedCookieValue, type AuthSession, type AuthUser } from "@/lib/auth/session";
import { DEFAULT_OWNER_ID } from "@/lib/server/runtime";
import { getPlatformOrganization, getPlatformUserById } from "@/lib/server/account-store";
import { authUserFromPlatformRecord } from "@/lib/server/auth/local";
import { loadPlatformAuthorizationSnapshot } from "@/lib/server/auth/platform-authorization-store";
import { authorizePlatformSession } from "@/lib/server/auth/platform-session";
export class AuthRequiredError extends Error {
status = 401;
@@ -57,18 +57,9 @@ export async function getOptionalAuthSession(): Promise<AuthSession | null> {
readChunkedCookieValue(SESSION_COOKIE_NAME, (name) => cookieStore.get(name)?.value),
config.sessionSecret
);
if (!session || session.user.clientId !== "platform") return null;
const account = await getPlatformUserById(session.user.id);
if (!account || account.status !== "active") return null;
if (session.sessionVersion && session.sessionVersion !== account.sessionVersion) return null;
const organization = account.organizationId ? await getPlatformOrganization(account.organizationId) : null;
if (account.role !== "super_admin" && account.organizationId && (!organization || organization.status !== "active")) return null;
return {
...session,
authMode: account.role === "user" ? "user" : "admin",
user: authUserFromPlatformRecord(account, organization),
sessionVersion: account.sessionVersion
};
if (!session) return null;
const authorization = await authorizePlatformSession(session, loadPlatformAuthorizationSnapshot);
return authorization.outcome === "authenticated" ? authorization.session : null;
}
export async function requireAppSession(): Promise<AuthSession> {

View File

@@ -0,0 +1,69 @@
import "server-only";
import { getPlatformOrganization, getPlatformUserById } from "@/lib/server/account-store";
import type { PlatformAuthorizationSnapshot } from "@/lib/server/auth/platform-session";
import { isPostgresBackend, queryDatabase } from "@/lib/server/database";
const AUTHORIZATION_SQL = `SELECT
users.id AS account_id,
users.phone AS account_phone,
users.display_name AS account_display_name,
users.role AS account_role,
users.organization_id AS account_organization_id,
users.status AS account_status,
users.session_version AS account_session_version,
organizations.id AS organization_id,
organizations.name AS organization_name,
organizations.status AS organization_status
FROM public.platform_users AS users
LEFT JOIN public.platform_organizations AS organizations
ON organizations.id = users.organization_id
WHERE users.id = $1`;
export async function loadPlatformAuthorizationSnapshot(
accountId: string
): Promise<PlatformAuthorizationSnapshot | null> {
if (!isPostgresBackend()) {
const account = await getPlatformUserById(accountId, { includeDisabled: true });
if (!account) return null;
const organization = account.organizationId
? await getPlatformOrganization(account.organizationId)
: null;
return {
account: {
id: account.id,
phone: account.phone,
displayName: account.displayName,
role: account.role,
organizationId: account.organizationId,
status: account.status,
sessionVersion: account.sessionVersion
},
organization: organization
? { id: organization.id, name: organization.name, status: organization.status }
: null
};
}
const { rows } = await queryDatabase<Record<string, unknown>>(AUTHORIZATION_SQL, [accountId]);
const row = rows[0];
if (!row) return null;
return {
account: {
id: String(row.account_id),
phone: String(row.account_phone),
displayName: String(row.account_display_name),
role: String(row.account_role),
organizationId: row.account_organization_id == null ? undefined : String(row.account_organization_id),
status: String(row.account_status),
sessionVersion: Number(row.account_session_version)
},
organization: row.organization_id == null
? null
: {
id: String(row.organization_id),
name: String(row.organization_name),
status: String(row.organization_status)
}
};
}

View File

@@ -0,0 +1,105 @@
import type { AuthSession, AuthUser } from "@/lib/auth/session";
import type { PlatformRole } from "@/lib/types";
export type PlatformAuthorizationAccount = {
id: string;
phone: string;
displayName: string;
role: string;
organizationId?: string;
status: string;
sessionVersion: number;
};
export type PlatformAuthorizationOrganization = {
id: string;
name: string;
status: string;
};
export type PlatformAuthorizationSnapshot = {
account: PlatformAuthorizationAccount;
organization: PlatformAuthorizationOrganization | null;
};
export type PlatformSessionAuthorizationResult =
| { outcome: "authenticated"; session: AuthSession }
| { outcome: "unauthenticated"; reason: PlatformSessionRejectionReason };
export type PlatformSessionRejectionReason =
| "client_mismatch"
| "account_not_found"
| "account_disabled"
| "invalid_role"
| "session_version_mismatch"
| "organization_required"
| "organization_not_active";
export type PlatformAuthorizationLoader = (
accountId: string,
) => PlatformAuthorizationSnapshot | null | Promise<PlatformAuthorizationSnapshot | null>;
export async function authorizePlatformSession(
session: AuthSession,
loadSnapshot: PlatformAuthorizationLoader,
requiredClientId = "platform",
): Promise<PlatformSessionAuthorizationResult> {
if (session.user.clientId !== requiredClientId) {
return { outcome: "unauthenticated", reason: "client_mismatch" };
}
const snapshot = await loadSnapshot(session.user.id);
if (!snapshot) return { outcome: "unauthenticated", reason: "account_not_found" };
const { account, organization } = snapshot;
if (account.status !== "active") {
return { outcome: "unauthenticated", reason: "account_disabled" };
}
if (session.sessionVersion && session.sessionVersion !== account.sessionVersion) {
return { outcome: "unauthenticated", reason: "session_version_mismatch" };
}
if (!isPlatformRole(account.role)) {
return { outcome: "unauthenticated", reason: "invalid_role" };
}
if (account.role !== "super_admin") {
if (!account.organizationId) {
return { outcome: "unauthenticated", reason: "organization_required" };
}
if (!organization || organization.id !== account.organizationId || organization.status !== "active") {
return { outcome: "unauthenticated", reason: "organization_not_active" };
}
}
return {
outcome: "authenticated",
session: {
...session,
authMode: account.role === "user" ? "user" : "admin",
sessionVersion: account.sessionVersion,
user: {
id: account.id,
subject: account.id,
username: account.phone,
phone: account.phone,
displayName: account.displayName,
clientId: requiredClientId,
organizationId: account.organizationId,
organizationName: organization?.name,
role: account.role,
status: "active",
authorities: authoritiesForRole(account.role),
scope: [],
},
},
};
}
function isPlatformRole(role: string): role is PlatformRole {
return role === "user" || role === "organization_admin" || role === "super_admin";
}
function authoritiesForRole(role: PlatformRole): AuthUser["authorities"] {
if (role === "super_admin") return ["ROLE_SUPER_ADMIN", "SUPER_ADMIN"];
if (role === "organization_admin") return ["ROLE_ORGANIZATION_ADMIN", "ORGANIZATION_ADMIN"];
return ["ROLE_USER"];
}