feat: add database-refreshed identity authorization
This commit is contained in:
@@ -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> {
|
||||
|
||||
69
lib/server/auth/platform-authorization-store.ts
Normal file
69
lib/server/auth/platform-authorization-store.ts
Normal 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)
|
||||
}
|
||||
};
|
||||
}
|
||||
105
lib/server/auth/platform-session.ts
Normal file
105
lib/server/auth/platform-session.ts
Normal 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"];
|
||||
}
|
||||
Reference in New Issue
Block a user