diff --git a/app/(app)/app/layout.tsx b/app/(app)/app/layout.tsx index 43eeb15..6f71ec6 100644 --- a/app/(app)/app/layout.tsx +++ b/app/(app)/app/layout.tsx @@ -1,5 +1,6 @@ import { redirect } from "next/navigation"; +import { PendingAssignment } from "@/modules/auth/components/PendingAssignment"; import { getBootstrapState, getCurrentAuthContext } from "@/modules/core/server/auth"; import { AppShell } from "@/modules/shared/components/AppShell"; @@ -20,6 +21,10 @@ export default async function AppLayout({ children }: AppLayoutProps): Promise; + } + return ( {children} diff --git a/app/(app)/app/settings/global/page.tsx b/app/(app)/app/settings/global/page.tsx new file mode 100644 index 0000000..82be15d --- /dev/null +++ b/app/(app)/app/settings/global/page.tsx @@ -0,0 +1,32 @@ +import { redirect } from "next/navigation"; + +import { Badge } from "@/components/ui/badge"; +import { getCurrentAuthContext } from "@/modules/core/server/auth"; +import { hasPermission } from "@/modules/core/server/permissions"; +import { getSystemSettings } from "@/modules/core/server/system-settings"; +import { GlobalSettingsClient } from "@/modules/settings/components/GlobalSettingsClient"; + +export default async function GlobalSettingsPage(): Promise { + const context = await getCurrentAuthContext(); + if (!context) { + redirect("/login"); + } + + if (!hasPermission(context.permissions, "platform:manage")) { + redirect("/app/dashboard"); + } + + const settings = await getSystemSettings(); + + return ( +
+
+ Global +

全局配置

+

平台级注册策略、OIDC 默认配置和用户头像声明映射。

+
+ + +
+ ); +} diff --git a/app/(app)/app/settings/users/page.tsx b/app/(app)/app/settings/users/page.tsx index 581b53d..592e65f 100644 --- a/app/(app)/app/settings/users/page.tsx +++ b/app/(app)/app/settings/users/page.tsx @@ -10,6 +10,7 @@ import type { RoleId } from "@/modules/core/types"; import { TableToolbar } from "@/modules/settings/components/TableToolbar"; import { UserAccountActions, UserManagementClient } from "@/modules/settings/components/UserManagementClient"; import { getPage, getPageCount, getSearchParam, paginate, type SearchParams } from "@/modules/settings/lib/pagination"; +import { UserAvatar } from "@/modules/shared/components/UserAvatar"; type UsersPageProps = { searchParams?: Promise; @@ -105,8 +106,13 @@ export default async function UsersPage({ searchParams }: UsersPageProps): Promi {visibleAccounts.map((account) => ( -

{account.name}

-

{account.email}

+
+ +
+

{account.name}

+

{account.email}

+
+
{formatRoleLabel(account.role)} {account.organization ?? "-"} diff --git a/app/api/auth/register/route.ts b/app/api/auth/register/route.ts index 2c10621..a5c88a7 100644 --- a/app/api/auth/register/route.ts +++ b/app/api/auth/register/route.ts @@ -41,9 +41,9 @@ export async function POST(request: Request): Promise { if (created.session) { await setSessionCookie(created.session.id); } - return jsonSuccess(organizationId ? "申请已提交,待机构管理员审批" : "账号已创建", { + return jsonSuccess(organizationId && !invitationToken ? "账号已创建,机构加入申请已提交" : "账号已创建", { account: created.account, - pendingApproval: !created.session, + pendingApproval: false, }); } catch (error) { const reason = error instanceof Error ? error.message : "账号创建失败"; diff --git a/app/api/organizations/[id]/route.ts b/app/api/organizations/[id]/route.ts index ad4df02..ba9cf9b 100644 --- a/app/api/organizations/[id]/route.ts +++ b/app/api/organizations/[id]/route.ts @@ -61,6 +61,7 @@ export async function PATCH(request: Request, context: RouteContext): Promise { return typeof value === "object" && value !== null && !Array.isArray(value); @@ -28,11 +29,14 @@ function createSlug(name: string): string { export async function GET(): Promise { const database = getDatabase(); - const rows = await database.select().from(organizations).where(eq(organizations.status, "active")); + const [settings, rows] = await Promise.all([ + getSystemSettings(), + database.select().from(organizations).where(eq(organizations.status, "active")), + ]); return jsonSuccess("机构列表已加载", { organizations: rows - .filter((organization) => organization.registrationEnabled) + .filter((organization) => settings.registrationEnabled && organization.registrationEnabled) .map((organization) => ({ id: organization.id, name: organization.name, @@ -45,6 +49,7 @@ export async function GET(): Promise { oidcHasClientSecret: organization.oidcClientSecret.length > 0, oidcScopes: organization.oidcScopes, oidcRedirectUri: organization.oidcRedirectUri, + oidcAvatarClaim: organization.oidcAvatarClaim, oidcAutoProvision: organization.oidcAutoProvision, createdAt: organization.createdAt.toISOString(), updatedAt: organization.updatedAt.toISOString(), diff --git a/app/api/settings/accounts/[id]/route.ts b/app/api/settings/accounts/[id]/route.ts index a97ef64..bc057d9 100644 --- a/app/api/settings/accounts/[id]/route.ts +++ b/app/api/settings/accounts/[id]/route.ts @@ -46,6 +46,7 @@ function toOrganization(row: typeof organizations.$inferSelect): Organization { oidcHasClientSecret: row.oidcClientSecret.length > 0, oidcScopes: row.oidcScopes, oidcRedirectUri: row.oidcRedirectUri, + oidcAvatarClaim: row.oidcAvatarClaim, oidcAutoProvision: row.oidcAutoProvision, createdAt: row.createdAt.toISOString(), updatedAt: row.updatedAt.toISOString(), @@ -70,6 +71,8 @@ export async function PATCH(request: Request, context: RouteContext): Promise { - if (name || status) { + if (name || hasAvatarUrl || status) { const updateValues: { name?: string; + avatarUrl?: string; status?: AccountStatus; updatedAt: Date; } = { @@ -124,6 +128,9 @@ export async function PATCH(request: Request, context: RouteContext): Promise { const name = readString(body, "name"); const email = readString(body, "email"); + const avatarUrl = readString(body, "avatarUrl"); const password = readString(body, "password"); const roleId = readString(body, "roleId"); const organizationId = readString(body, "organizationId") || auth.context.organization?.id; @@ -119,6 +120,7 @@ export async function POST(request: Request): Promise { .values({ name, email: normalizedEmail, + avatarUrl, passwordHash: passwordHash.hash, passwordSalt: passwordHash.salt, status: "active", @@ -162,6 +164,7 @@ export async function POST(request: Request): Promise { oidcHasClientSecret: created.organization.oidcClientSecret.length > 0, oidcScopes: created.organization.oidcScopes, oidcRedirectUri: created.organization.oidcRedirectUri, + oidcAvatarClaim: created.organization.oidcAvatarClaim, oidcAutoProvision: created.organization.oidcAutoProvision, createdAt: created.organization.createdAt.toISOString(), updatedAt: created.organization.updatedAt.toISOString(), diff --git a/app/api/settings/global/route.ts b/app/api/settings/global/route.ts new file mode 100644 index 0000000..57fcaee --- /dev/null +++ b/app/api/settings/global/route.ts @@ -0,0 +1,92 @@ +import { jsonFailure, jsonSuccess, readJsonBody } from "@/modules/core/server/api"; +import { recordAuditLog } from "@/modules/core/server/audit"; +import { requirePermission } from "@/modules/core/server/auth"; +import { getSystemSettings, updateSystemSettings } from "@/modules/core/server/system-settings"; + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function readString(source: Record, key: string): string { + const value = source[key]; + return typeof value === "string" ? value.trim() : ""; +} + +function readBoolean(source: Record, key: string): boolean | null { + const value = source[key]; + return typeof value === "boolean" ? value : null; +} + +export async function GET(): Promise { + const auth = await requirePermission("platform:manage", { + action: "systemSettings.read", + targetType: "systemSettings", + targetId: "global", + }); + + if (!auth.success) { + return auth.response; + } + + const settings = await getSystemSettings(); + return jsonSuccess("系统配置已加载", { settings }); +} + +export async function PATCH(request: Request): Promise { + const auth = await requirePermission("platform:manage", { + action: "systemSettings.update", + targetType: "systemSettings", + targetId: "global", + }); + + if (!auth.success) { + return auth.response; + } + + const body = await readJsonBody(request); + if (!isRecord(body)) { + return jsonFailure("请求数据格式无效"); + } + + const registrationEnabled = "registrationEnabled" in body ? readBoolean(body, "registrationEnabled") : null; + const oidcEnabled = "oidcEnabled" in body ? readBoolean(body, "oidcEnabled") : null; + const oidcAutoProvision = "oidcAutoProvision" in body ? readBoolean(body, "oidcAutoProvision") : null; + if ("registrationEnabled" in body && registrationEnabled === null) { + return jsonFailure("注册开关无效"); + } + if ("oidcEnabled" in body && oidcEnabled === null) { + return jsonFailure("OIDC开关无效"); + } + if ("oidcAutoProvision" in body && oidcAutoProvision === null) { + return jsonFailure("OIDC自动开户配置无效"); + } + + const oidcIssuerUrl = readString(body, "oidcIssuerUrl"); + const oidcClientId = readString(body, "oidcClientId"); + if (oidcEnabled === true && (!oidcIssuerUrl || !oidcClientId)) { + return jsonFailure("启用 OIDC 前请填写 Issuer URL 和 Client ID"); + } + + const settings = await updateSystemSettings({ + registrationEnabled: registrationEnabled ?? undefined, + oidcEnabled: oidcEnabled ?? undefined, + oidcIssuerUrl, + oidcClientId, + oidcClientSecret: readString(body, "oidcClientSecret"), + oidcScopes: readString(body, "oidcScopes") || "openid profile email", + oidcRedirectUri: readString(body, "oidcRedirectUri"), + oidcAvatarClaim: readString(body, "oidcAvatarClaim") || "picture", + oidcAutoProvision: oidcAutoProvision ?? undefined, + }); + + await recordAuditLog({ + actor: auth.context.account, + action: "systemSettings.update", + targetType: "systemSettings", + targetId: settings.id, + result: "success", + reason: "更新全局系统配置", + }); + + return jsonSuccess("系统配置已保存", { settings }); +} diff --git a/drizzle/0002_equal_bishop.sql b/drizzle/0002_equal_bishop.sql new file mode 100644 index 0000000..5bd361b --- /dev/null +++ b/drizzle/0002_equal_bishop.sql @@ -0,0 +1,17 @@ +CREATE TABLE "system_settings" ( + "id" text PRIMARY KEY DEFAULT 'global' NOT NULL, + "registration_enabled" boolean DEFAULT true NOT NULL, + "oidc_enabled" boolean DEFAULT false NOT NULL, + "oidc_issuer_url" text DEFAULT '' NOT NULL, + "oidc_client_id" text DEFAULT '' NOT NULL, + "oidc_client_secret" text DEFAULT '' NOT NULL, + "oidc_scopes" text DEFAULT 'openid profile email' NOT NULL, + "oidc_redirect_uri" text DEFAULT '' NOT NULL, + "oidc_avatar_claim" text DEFAULT 'picture' NOT NULL, + "oidc_auto_provision" boolean DEFAULT false NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "accounts" ADD COLUMN "avatar_url" text DEFAULT '' NOT NULL;--> statement-breakpoint +ALTER TABLE "organizations" ADD COLUMN "oidc_avatar_claim" text DEFAULT 'picture' NOT NULL; \ No newline at end of file diff --git a/drizzle/meta/0002_snapshot.json b/drizzle/meta/0002_snapshot.json new file mode 100644 index 0000000..b84b467 --- /dev/null +++ b/drizzle/meta/0002_snapshot.json @@ -0,0 +1,2098 @@ +{ + "id": "336517ea-29ac-4c0d-86c2-d95c94b2eb88", + "prevId": "f535bccb-22d5-4167-85e8-89fb281c29cb", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "platform_role_id": { + "name": "platform_role_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_salt": { + "name": "password_salt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "account_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "accounts_email_unique": { + "name": "accounts_email_unique", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "accounts_platform_role_id_roles_id_fk": { + "name": "accounts_platform_role_id_roles_id_fk", + "tableFrom": "accounts", + "tableTo": "roles", + "columnsFrom": [ + "platform_role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.admissions": { + "name": "admissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "elder_id": { + "name": "elder_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "bed_id": { + "name": "bed_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "admission_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "admitted_at": { + "name": "admitted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "discharged_at": { + "name": "discharged_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "admissions_organization_id_organizations_id_fk": { + "name": "admissions_organization_id_organizations_id_fk", + "tableFrom": "admissions", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "admissions_elder_id_elders_id_fk": { + "name": "admissions_elder_id_elders_id_fk", + "tableFrom": "admissions", + "tableTo": "elders", + "columnsFrom": [ + "elder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "admissions_bed_id_beds_id_fk": { + "name": "admissions_bed_id_beds_id_fk", + "tableFrom": "admissions", + "tableTo": "beds", + "columnsFrom": [ + "bed_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "actor_account_id": { + "name": "actor_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result": { + "name": "result", + "type": "audit_result", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "audit_logs_organization_id_organizations_id_fk": { + "name": "audit_logs_organization_id_organizations_id_fk", + "tableFrom": "audit_logs", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "audit_logs_actor_account_id_accounts_id_fk": { + "name": "audit_logs_actor_account_id_accounts_id_fk", + "tableFrom": "audit_logs", + "tableTo": "accounts", + "columnsFrom": [ + "actor_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.beds": { + "name": "beds", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "room_id": { + "name": "room_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "bed_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'available'" + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "beds_code_room_unique": { + "name": "beds_code_room_unique", + "columns": [ + { + "expression": "room_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "beds_organization_id_organizations_id_fk": { + "name": "beds_organization_id_organizations_id_fk", + "tableFrom": "beds", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "beds_room_id_rooms_id_fk": { + "name": "beds_room_id_rooms_id_fk", + "tableFrom": "beds", + "tableTo": "rooms", + "columnsFrom": [ + "room_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.buildings": { + "name": "buildings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "campus_id": { + "name": "campus_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "buildings_organization_id_organizations_id_fk": { + "name": "buildings_organization_id_organizations_id_fk", + "tableFrom": "buildings", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "buildings_campus_id_campuses_id_fk": { + "name": "buildings_campus_id_campuses_id_fk", + "tableFrom": "buildings", + "tableTo": "campuses", + "columnsFrom": [ + "campus_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.campuses": { + "name": "campuses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "campuses_organization_id_organizations_id_fk": { + "name": "campuses_organization_id_organizations_id_fk", + "tableFrom": "campuses", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.elders": { + "name": "elders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "gender": { + "name": "gender", + "type": "gender", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "age": { + "name": "age", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "care_level": { + "name": "care_level", + "type": "care_level", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "elder_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "primary_contact": { + "name": "primary_contact", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "medical_notes": { + "name": "medical_notes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "elders_organization_id_organizations_id_fk": { + "name": "elders_organization_id_organizations_id_fk", + "tableFrom": "elders", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.floors": { + "name": "floors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "building_id": { + "name": "building_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "floors_organization_id_organizations_id_fk": { + "name": "floors_organization_id_organizations_id_fk", + "tableFrom": "floors", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "floors_building_id_buildings_id_fk": { + "name": "floors_building_id_buildings_id_fk", + "tableFrom": "floors", + "tableTo": "buildings", + "columnsFrom": [ + "building_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.join_requests": { + "name": "join_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "join_request_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "reviewed_by_account_id": { + "name": "reviewed_by_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "reviewed_at": { + "name": "reviewed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "join_requests_account_id_accounts_id_fk": { + "name": "join_requests_account_id_accounts_id_fk", + "tableFrom": "join_requests", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "join_requests_organization_id_organizations_id_fk": { + "name": "join_requests_organization_id_organizations_id_fk", + "tableFrom": "join_requests", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "join_requests_reviewed_by_account_id_accounts_id_fk": { + "name": "join_requests_reviewed_by_account_id_accounts_id_fk", + "tableFrom": "join_requests", + "tableTo": "accounts", + "columnsFrom": [ + "reviewed_by_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memberships": { + "name": "memberships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "membership_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "memberships_account_organization_unique": { + "name": "memberships_account_organization_unique", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memberships_account_id_accounts_id_fk": { + "name": "memberships_account_id_accounts_id_fk", + "tableFrom": "memberships", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "memberships_organization_id_organizations_id_fk": { + "name": "memberships_organization_id_organizations_id_fk", + "tableFrom": "memberships", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "memberships_role_id_roles_id_fk": { + "name": "memberships_role_id_roles_id_fk", + "tableFrom": "memberships", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_invitations": { + "name": "organization_invitations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role_id": { + "name": "role_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "accepted_by_account_id": { + "name": "accepted_by_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organization_invitations_token_unique": { + "name": "organization_invitations_token_unique", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_invitations_organization_id_organizations_id_fk": { + "name": "organization_invitations_organization_id_organizations_id_fk", + "tableFrom": "organization_invitations", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_invitations_role_id_roles_id_fk": { + "name": "organization_invitations_role_id_roles_id_fk", + "tableFrom": "organization_invitations", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "organization_invitations_created_by_account_id_accounts_id_fk": { + "name": "organization_invitations_created_by_account_id_accounts_id_fk", + "tableFrom": "organization_invitations", + "tableTo": "accounts", + "columnsFrom": [ + "created_by_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "organization_invitations_accepted_by_account_id_accounts_id_fk": { + "name": "organization_invitations_accepted_by_account_id_accounts_id_fk", + "tableFrom": "organization_invitations", + "tableTo": "accounts", + "columnsFrom": [ + "accepted_by_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "organization_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "registration_enabled": { + "name": "registration_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "oidc_enabled": { + "name": "oidc_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "oidc_issuer_url": { + "name": "oidc_issuer_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "oidc_client_id": { + "name": "oidc_client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "oidc_client_secret": { + "name": "oidc_client_secret", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "oidc_scopes": { + "name": "oidc_scopes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'openid profile email'" + }, + "oidc_redirect_uri": { + "name": "oidc_redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "oidc_avatar_claim": { + "name": "oidc_avatar_claim", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'picture'" + }, + "oidc_auto_provision": { + "name": "oidc_auto_provision", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organizations_slug_unique": { + "name": "organizations_slug_unique", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permissions": { + "name": "permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.role_permissions": { + "name": "role_permissions", + "schema": "", + "columns": { + "role_id": { + "name": "role_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "permission_id": { + "name": "permission_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "role_permissions_role_id_roles_id_fk": { + "name": "role_permissions_role_id_roles_id_fk", + "tableFrom": "role_permissions", + "tableTo": "roles", + "columnsFrom": [ + "role_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "role_permissions_permission_id_permissions_id_fk": { + "name": "role_permissions_permission_id_permissions_id_fk", + "tableFrom": "role_permissions", + "tableTo": "permissions", + "columnsFrom": [ + "permission_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "role_permissions_role_id_permission_id_pk": { + "name": "role_permissions_role_id_permission_id_pk", + "columns": [ + "role_id", + "permission_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "role_scope", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "roles_key_organization_unique": { + "name": "roles_key_organization_unique", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "roles_organization_id_organizations_id_fk": { + "name": "roles_organization_id_organizations_id_fk", + "tableFrom": "roles", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rooms": { + "name": "rooms", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "floor_id": { + "name": "floor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "capacity": { + "name": "capacity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "status": { + "name": "status", + "type": "organization_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "rooms_code_organization_unique": { + "name": "rooms_code_organization_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "rooms_organization_id_organizations_id_fk": { + "name": "rooms_organization_id_organizations_id_fk", + "tableFrom": "rooms", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "rooms_floor_id_floors_id_fk": { + "name": "rooms_floor_id_floors_id_fk", + "tableFrom": "rooms", + "tableTo": "floors", + "columnsFrom": [ + "floor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_account_id_accounts_id_fk": { + "name": "sessions_account_id_accounts_id_fk", + "tableFrom": "sessions", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sessions_active_organization_id_organizations_id_fk": { + "name": "sessions_active_organization_id_organizations_id_fk", + "tableFrom": "sessions", + "tableTo": "organizations", + "columnsFrom": [ + "active_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.system_incidents": { + "name": "system_incidents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "severity": { + "name": "severity", + "type": "incident_severity", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "incident_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "acknowledged_by_account_id": { + "name": "acknowledged_by_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "acknowledged_at": { + "name": "acknowledged_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "resolved_by_account_id": { + "name": "resolved_by_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "system_incidents_organization_id_organizations_id_fk": { + "name": "system_incidents_organization_id_organizations_id_fk", + "tableFrom": "system_incidents", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "system_incidents_acknowledged_by_account_id_accounts_id_fk": { + "name": "system_incidents_acknowledged_by_account_id_accounts_id_fk", + "tableFrom": "system_incidents", + "tableTo": "accounts", + "columnsFrom": [ + "acknowledged_by_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "system_incidents_resolved_by_account_id_accounts_id_fk": { + "name": "system_incidents_resolved_by_account_id_accounts_id_fk", + "tableFrom": "system_incidents", + "tableTo": "accounts", + "columnsFrom": [ + "resolved_by_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.system_settings": { + "name": "system_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "default": "'global'" + }, + "registration_enabled": { + "name": "registration_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "oidc_enabled": { + "name": "oidc_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "oidc_issuer_url": { + "name": "oidc_issuer_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "oidc_client_id": { + "name": "oidc_client_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "oidc_client_secret": { + "name": "oidc_client_secret", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "oidc_scopes": { + "name": "oidc_scopes", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'openid profile email'" + }, + "oidc_redirect_uri": { + "name": "oidc_redirect_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "oidc_avatar_claim": { + "name": "oidc_avatar_claim", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'picture'" + }, + "oidc_auto_provision": { + "name": "oidc_auto_provision", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.account_status": { + "name": "account_status", + "schema": "public", + "values": [ + "active", + "disabled", + "pending" + ] + }, + "public.admission_status": { + "name": "admission_status", + "schema": "public", + "values": [ + "active", + "transferred", + "discharged" + ] + }, + "public.audit_result": { + "name": "audit_result", + "schema": "public", + "values": [ + "success", + "denied", + "failure" + ] + }, + "public.bed_status": { + "name": "bed_status", + "schema": "public", + "values": [ + "available", + "occupied", + "maintenance", + "disabled" + ] + }, + "public.care_level": { + "name": "care_level", + "schema": "public", + "values": [ + "self-care", + "semi-assisted", + "assisted", + "intensive" + ] + }, + "public.elder_status": { + "name": "elder_status", + "schema": "public", + "values": [ + "active", + "pending", + "discharged" + ] + }, + "public.gender": { + "name": "gender", + "schema": "public", + "values": [ + "male", + "female", + "other" + ] + }, + "public.incident_severity": { + "name": "incident_severity", + "schema": "public", + "values": [ + "info", + "warning", + "critical" + ] + }, + "public.incident_status": { + "name": "incident_status", + "schema": "public", + "values": [ + "open", + "acknowledged", + "resolved", + "closed" + ] + }, + "public.join_request_status": { + "name": "join_request_status", + "schema": "public", + "values": [ + "pending", + "approved", + "rejected" + ] + }, + "public.membership_status": { + "name": "membership_status", + "schema": "public", + "values": [ + "active", + "disabled", + "pending" + ] + }, + "public.organization_status": { + "name": "organization_status", + "schema": "public", + "values": [ + "active", + "disabled" + ] + }, + "public.role_scope": { + "name": "role_scope", + "schema": "public", + "values": [ + "platform", + "organization" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 3603b30..2c98746 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -15,6 +15,13 @@ "when": 1782994149630, "tag": "0001_tired_queen_noir", "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1782998189463, + "tag": "0002_equal_bishop", + "breakpoints": true } ] } \ No newline at end of file diff --git a/modules/auth/components/PendingAssignment.tsx b/modules/auth/components/PendingAssignment.tsx new file mode 100644 index 0000000..8616491 --- /dev/null +++ b/modules/auth/components/PendingAssignment.tsx @@ -0,0 +1,63 @@ +import Link from "next/link"; +import { Clock3, MailCheck, ShieldCheck } from "lucide-react"; + +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { SignOutButton } from "@/modules/auth/components/SignOutButton"; +import type { PublicAccount } from "@/modules/core/types"; +import { UserAvatar } from "@/modules/shared/components/UserAvatar"; + +type PendingAssignmentProps = { + account: PublicAccount; +}; + +export function PendingAssignment({ account }: PendingAssignmentProps): React.ReactElement { + return ( +
+
+
+
+ +
+

{account.name}

+

{account.email}

+
+
+ +
+ + + + + 等待分配 + + 账号已创建,等待管理员分配机构和角色 + + +
+
+
+
+
+
+
+
+ +
+
+
+
+ ); +} diff --git a/modules/core/server/auth.ts b/modules/core/server/auth.ts index acc12d8..f6fa636 100644 --- a/modules/core/server/auth.ts +++ b/modules/core/server/auth.ts @@ -16,6 +16,7 @@ import { roles, sessions, } from "@/modules/core/server/schema"; +import { getSystemSettings } from "@/modules/core/server/system-settings"; import type { Account, AuthContext, Membership, Organization, Permission, PublicAccount, Session } from "@/modules/core/types"; const SESSION_COOKIE = "teatea_session"; @@ -48,6 +49,7 @@ function toOrganization(row: OrganizationRow): Organization { oidcHasClientSecret: row.oidcClientSecret.length > 0, oidcScopes: row.oidcScopes, oidcRedirectUri: row.oidcRedirectUri, + oidcAvatarClaim: row.oidcAvatarClaim, oidcAutoProvision: row.oidcAutoProvision, createdAt: toIsoString(row.createdAt), updatedAt: toIsoString(row.updatedAt), @@ -88,6 +90,7 @@ export function toPublicAccount(account: AccountRow | Account, roleKey?: string, platformRoleId: "platformRoleId" in account ? account.platformRoleId ?? undefined : undefined, name: account.name, email: account.email, + avatarUrl: "avatarUrl" in account ? account.avatarUrl : "", role: roleKey ?? fallbackRole, organization: organization?.name ?? fallbackOrganization, organizationId: organization?.id ?? fallbackOrganizationId, @@ -272,6 +275,7 @@ export async function createRegistration(input: { const database = getDatabase(); const password = hashPassword(input.password); const expiresAt = new Date(Date.now() + SESSION_MAX_AGE_SECONDS * 1000); + const settings = await getSystemSettings(); const created = await database.transaction(async (transaction) => { const normalizedEmail = normalizeEmail(input.email); @@ -296,6 +300,9 @@ export async function createRegistration(input: { if (invitation && (invitation.invitation.status !== "active" || invitation.invitation.expiresAt < new Date())) { throw new Error("邀请链接已失效"); } + if (!invitation && !settings.registrationEnabled) { + throw new Error("系统暂未开放注册"); + } const targetOrganizationId = invitation?.invitation.organizationId ?? input.organizationId; if (targetOrganizationId && !invitation) { @@ -320,7 +327,7 @@ export async function createRegistration(input: { email: normalizedEmail, passwordHash: password.hash, passwordSalt: password.salt, - status: targetOrganizationId && !invitation ? "pending" : "active", + status: "active", }) .returning(); const account = accountRows[0]; @@ -346,16 +353,14 @@ export async function createRegistration(input: { .where(eq(organizationInvitations.id, invitation.invitation.id)); } - const sessionRows = targetOrganizationId && !invitation - ? [] - : await transaction - .insert(sessions) - .values({ - accountId: account.id, - activeOrganizationId: invitation?.invitation.organizationId, - expiresAt, - }) - .returning(); + const sessionRows = await transaction + .insert(sessions) + .values({ + accountId: account.id, + activeOrganizationId: invitation?.invitation.organizationId, + expiresAt, + }) + .returning(); const session = sessionRows[0]; if (targetOrganizationId && !invitation) { diff --git a/modules/core/server/permissions.ts b/modules/core/server/permissions.ts index 79be301..81b237d 100644 --- a/modules/core/server/permissions.ts +++ b/modules/core/server/permissions.ts @@ -2,6 +2,7 @@ import { and, eq, inArray, isNull, or } from "drizzle-orm"; import { getDatabase } from "@/modules/core/server/db"; import { permissions, rolePermissions, roles } from "@/modules/core/server/schema"; +import { ensureSystemSettings } from "@/modules/core/server/system-settings"; import type { Permission, RoleDefinition, RoleId } from "@/modules/core/types"; import { PERMISSIONS, ROLE_LABELS } from "@/modules/core/types"; @@ -132,6 +133,7 @@ export const SYSTEM_ROLE_DEFINITIONS: SeedRole[] = [ export async function ensureSystemDefaults(): Promise { const database = getDatabase(); + await ensureSystemSettings(); await database .insert(permissions) diff --git a/modules/core/server/schema.ts b/modules/core/server/schema.ts index 34737a0..9630d93 100644 --- a/modules/core/server/schema.ts +++ b/modules/core/server/schema.ts @@ -24,6 +24,21 @@ export const incidentStatusEnum = pgEnum("incident_status", ["open", "acknowledg export const incidentSeverityEnum = pgEnum("incident_severity", ["info", "warning", "critical"]); export const joinRequestStatusEnum = pgEnum("join_request_status", ["pending", "approved", "rejected"]); +export const systemSettings = pgTable("system_settings", { + id: text("id").primaryKey().default("global"), + registrationEnabled: boolean("registration_enabled").notNull().default(true), + oidcEnabled: boolean("oidc_enabled").notNull().default(false), + oidcIssuerUrl: text("oidc_issuer_url").notNull().default(""), + oidcClientId: text("oidc_client_id").notNull().default(""), + oidcClientSecret: text("oidc_client_secret").notNull().default(""), + oidcScopes: text("oidc_scopes").notNull().default("openid profile email"), + oidcRedirectUri: text("oidc_redirect_uri").notNull().default(""), + oidcAvatarClaim: text("oidc_avatar_claim").notNull().default("picture"), + oidcAutoProvision: boolean("oidc_auto_provision").notNull().default(false), + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), +}); + export const organizations = pgTable("organizations", { id: uuid("id").primaryKey().defaultRandom(), name: text("name").notNull(), @@ -36,6 +51,7 @@ export const organizations = pgTable("organizations", { oidcClientSecret: text("oidc_client_secret").notNull().default(""), oidcScopes: text("oidc_scopes").notNull().default("openid profile email"), oidcRedirectUri: text("oidc_redirect_uri").notNull().default(""), + oidcAvatarClaim: text("oidc_avatar_claim").notNull().default("picture"), oidcAutoProvision: boolean("oidc_auto_provision").notNull().default(false), createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), @@ -79,6 +95,7 @@ export const accounts = pgTable("accounts", { platformRoleId: uuid("platform_role_id").references(() => roles.id), name: text("name").notNull(), email: text("email").notNull(), + avatarUrl: text("avatar_url").notNull().default(""), passwordHash: text("password_hash").notNull(), passwordSalt: text("password_salt").notNull(), status: accountStatusEnum("status").notNull().default("active"), diff --git a/modules/core/server/store.ts b/modules/core/server/store.ts index 872f94e..10f862b 100644 --- a/modules/core/server/store.ts +++ b/modules/core/server/store.ts @@ -14,6 +14,7 @@ import { roles, rooms, sessions, + systemSettings, systemIncidents, } from "@/modules/core/server/schema"; import type { @@ -50,6 +51,7 @@ export async function readData(): Promise { admissionRows, auditLogRows, incidentRows, + systemSettingsRows, ] = await Promise.all([ database.select().from(organizations).orderBy(desc(organizations.createdAt)), database.select().from(accounts).orderBy(desc(accounts.createdAt)), @@ -107,6 +109,7 @@ export async function readData(): Promise { .orderBy(desc(admissions.admittedAt)), listAuditLogs({ limit: 100 }), database.select().from(systemIncidents).orderBy(desc(systemIncidents.createdAt)), + database.select().from(systemSettings).limit(1), ]); const roleById = new Map(membershipRows.map((row) => [row.membership.id, row.role])); @@ -132,6 +135,7 @@ export async function readData(): Promise { oidcHasClientSecret: row.oidcClientSecret.length > 0, oidcScopes: row.oidcScopes, oidcRedirectUri: row.oidcRedirectUri, + oidcAvatarClaim: row.oidcAvatarClaim, oidcAutoProvision: row.oidcAutoProvision, createdAt: iso(row.createdAt), updatedAt: iso(row.updatedAt), @@ -142,6 +146,7 @@ export async function readData(): Promise { platformRoleId: row.platformRoleId ?? undefined, name: row.name, email: row.email, + avatarUrl: row.avatarUrl, role: row.platformRoleId ?? "viewer", passwordHash: row.passwordHash, passwordSalt: row.passwordSalt, @@ -276,7 +281,37 @@ export async function readData(): Promise { updatedAt: iso(row.updatedAt), })); + const systemSettingsRow = systemSettingsRows[0]; + const systemSettingsData = systemSettingsRow + ? { + id: systemSettingsRow.id, + registrationEnabled: systemSettingsRow.registrationEnabled, + oidcEnabled: systemSettingsRow.oidcEnabled, + oidcIssuerUrl: systemSettingsRow.oidcIssuerUrl, + oidcClientId: systemSettingsRow.oidcClientId, + oidcHasClientSecret: systemSettingsRow.oidcClientSecret.length > 0, + oidcScopes: systemSettingsRow.oidcScopes, + oidcRedirectUri: systemSettingsRow.oidcRedirectUri, + oidcAvatarClaim: systemSettingsRow.oidcAvatarClaim, + oidcAutoProvision: systemSettingsRow.oidcAutoProvision, + updatedAt: iso(systemSettingsRow.updatedAt), + } + : { + id: "global", + registrationEnabled: true, + oidcEnabled: false, + oidcIssuerUrl: "", + oidcClientId: "", + oidcHasClientSecret: false, + oidcScopes: "openid profile email", + oidcRedirectUri: "", + oidcAvatarClaim: "picture", + oidcAutoProvision: false, + updatedAt: new Date(0).toISOString(), + }; + return { + systemSettings: systemSettingsData, organizations: organizationsData, accounts: accountsData, memberships: membershipsData, diff --git a/modules/core/server/system-settings.ts b/modules/core/server/system-settings.ts new file mode 100644 index 0000000..288a2bd --- /dev/null +++ b/modules/core/server/system-settings.ts @@ -0,0 +1,129 @@ +import { eq } from "drizzle-orm"; + +import { getDatabase } from "@/modules/core/server/db"; +import { systemSettings } from "@/modules/core/server/schema"; +import type { SystemSettings } from "@/modules/core/types"; + +const GLOBAL_SETTINGS_ID = "global"; + +type SystemSettingsRow = typeof systemSettings.$inferSelect; + +export type SystemSettingsUpdate = { + registrationEnabled?: boolean; + oidcEnabled?: boolean; + oidcIssuerUrl?: string; + oidcClientId?: string; + oidcClientSecret?: string; + oidcScopes?: string; + oidcRedirectUri?: string; + oidcAvatarClaim?: string; + oidcAutoProvision?: boolean; +}; + +function toSystemSettings(row: SystemSettingsRow): SystemSettings { + return { + id: row.id, + registrationEnabled: row.registrationEnabled, + oidcEnabled: row.oidcEnabled, + oidcIssuerUrl: row.oidcIssuerUrl, + oidcClientId: row.oidcClientId, + oidcHasClientSecret: row.oidcClientSecret.length > 0, + oidcScopes: row.oidcScopes, + oidcRedirectUri: row.oidcRedirectUri, + oidcAvatarClaim: row.oidcAvatarClaim, + oidcAutoProvision: row.oidcAutoProvision, + updatedAt: row.updatedAt.toISOString(), + }; +} + +async function ensureSystemSettingsRow(): Promise { + const database = getDatabase(); + const existingRows = await database + .select() + .from(systemSettings) + .where(eq(systemSettings.id, GLOBAL_SETTINGS_ID)) + .limit(1); + const existing = existingRows[0]; + if (existing) { + return existing; + } + + const createdRows = await database + .insert(systemSettings) + .values({ id: GLOBAL_SETTINGS_ID }) + .onConflictDoNothing() + .returning(); + const created = createdRows[0]; + if (created) { + return created; + } + + const fallbackRows = await database + .select() + .from(systemSettings) + .where(eq(systemSettings.id, GLOBAL_SETTINGS_ID)) + .limit(1); + const fallback = fallbackRows[0]; + if (!fallback) { + throw new Error("系统配置初始化失败"); + } + + return fallback; +} + +export async function ensureSystemSettings(): Promise { + await ensureSystemSettingsRow(); +} + +export async function getSystemSettings(): Promise { + const row = await ensureSystemSettingsRow(); + return toSystemSettings(row); +} + +export async function updateSystemSettings(input: SystemSettingsUpdate): Promise { + const database = getDatabase(); + await ensureSystemSettingsRow(); + + const updateValues: SystemSettingsUpdate & { updatedAt: Date } = { + updatedAt: new Date(), + }; + if (input.registrationEnabled !== undefined) { + updateValues.registrationEnabled = input.registrationEnabled; + } + if (input.oidcEnabled !== undefined) { + updateValues.oidcEnabled = input.oidcEnabled; + } + if (input.oidcIssuerUrl !== undefined) { + updateValues.oidcIssuerUrl = input.oidcIssuerUrl; + } + if (input.oidcClientId !== undefined) { + updateValues.oidcClientId = input.oidcClientId; + } + if (input.oidcClientSecret !== undefined && input.oidcClientSecret.length > 0) { + updateValues.oidcClientSecret = input.oidcClientSecret; + } + if (input.oidcScopes !== undefined) { + updateValues.oidcScopes = input.oidcScopes; + } + if (input.oidcRedirectUri !== undefined) { + updateValues.oidcRedirectUri = input.oidcRedirectUri; + } + if (input.oidcAvatarClaim !== undefined) { + updateValues.oidcAvatarClaim = input.oidcAvatarClaim; + } + if (input.oidcAutoProvision !== undefined) { + updateValues.oidcAutoProvision = input.oidcAutoProvision; + } + + const rows = await database + .update(systemSettings) + .set(updateValues) + .where(eq(systemSettings.id, GLOBAL_SETTINGS_ID)) + .returning(); + const row = rows[0]; + if (!row) { + throw new Error("系统配置不存在"); + } + + return toSystemSettings(row); +} diff --git a/modules/core/types.ts b/modules/core/types.ts index d0230d8..47693f8 100644 --- a/modules/core/types.ts +++ b/modules/core/types.ts @@ -75,11 +75,26 @@ export type Organization = { oidcHasClientSecret: boolean; oidcScopes: string; oidcRedirectUri: string; + oidcAvatarClaim: string; oidcAutoProvision: boolean; createdAt: string; updatedAt: string; }; +export type SystemSettings = { + id: string; + registrationEnabled: boolean; + oidcEnabled: boolean; + oidcIssuerUrl: string; + oidcClientId: string; + oidcHasClientSecret: boolean; + oidcScopes: string; + oidcRedirectUri: string; + oidcAvatarClaim: string; + oidcAutoProvision: boolean; + updatedAt: string; +}; + export type MembershipStatus = "active" | "disabled" | "pending"; export type Membership = { @@ -134,6 +149,7 @@ export type Account = { platformRoleId?: string; name: string; email: string; + avatarUrl: string; role: RoleId | string; organization?: string; organizationId?: string; @@ -229,6 +245,7 @@ export type SystemIncident = { }; export type AppData = { + systemSettings: SystemSettings; organizations: Organization[]; accounts: Account[]; memberships: Membership[]; diff --git a/modules/settings/components/GlobalSettingsClient.tsx b/modules/settings/components/GlobalSettingsClient.tsx new file mode 100644 index 0000000..731d325 --- /dev/null +++ b/modules/settings/components/GlobalSettingsClient.tsx @@ -0,0 +1,157 @@ +"use client"; + +import { FormEvent, useState } from "react"; +import { Globe2, Save, ShieldCheck } from "lucide-react"; +import { useRouter } from "next/navigation"; + +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import type { ApiResult } from "@/modules/core/server/api"; +import type { SystemSettings } from "@/modules/core/types"; + +type GlobalSettingsClientProps = { + settings: SystemSettings; +}; + +export function GlobalSettingsClient({ settings }: GlobalSettingsClientProps): React.ReactElement { + const router = useRouter(); + const [message, setMessage] = useState(""); + const [isPending, setIsPending] = useState(false); + + async function handleSubmit(event: FormEvent): Promise { + event.preventDefault(); + setMessage(""); + setIsPending(true); + + const formData = new FormData(event.currentTarget); + const response = await fetch("/api/settings/global", { + method: "PATCH", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + registrationEnabled: formData.get("registrationEnabled") === "on", + oidcEnabled: formData.get("oidcEnabled") === "on", + oidcIssuerUrl: String(formData.get("oidcIssuerUrl") ?? ""), + oidcClientId: String(formData.get("oidcClientId") ?? ""), + oidcClientSecret: String(formData.get("oidcClientSecret") ?? ""), + oidcScopes: String(formData.get("oidcScopes") ?? ""), + oidcRedirectUri: String(formData.get("oidcRedirectUri") ?? ""), + oidcAvatarClaim: String(formData.get("oidcAvatarClaim") ?? ""), + oidcAutoProvision: formData.get("oidcAutoProvision") === "on", + }), + }); + const result = (await response.json()) as ApiResult<{ settings: SystemSettings }>; + setIsPending(false); + + if (!result.success) { + setMessage(result.reason); + return; + } + + setMessage(result.reason); + router.refresh(); + } + + return ( +
+ + +
+
+ 控制全站公开注册能力和 OIDC 登录入口总开关。 +
+ + + + + + + +
+ + + +
+
+ 保存标准 OIDC issuer、client、scope 和头像 claim。 +
+ +
+ + +
+ + + +
+ + +
+ + + + {message ? ( +

+ {message} +

+ ) : null} + +
+ + {settings.oidcEnabled ? "OIDC 已启用" : "OIDC 未启用"} + + +
+
+
+
+ ); +} diff --git a/modules/settings/components/OrganizationDetailClient.tsx b/modules/settings/components/OrganizationDetailClient.tsx index 44887e8..b688495 100644 --- a/modules/settings/components/OrganizationDetailClient.tsx +++ b/modules/settings/components/OrganizationDetailClient.tsx @@ -44,6 +44,7 @@ export function OrganizationDetailClient({ oidcClientSecret: String(formData.get("oidcClientSecret") ?? ""), oidcScopes: String(formData.get("oidcScopes") ?? ""), oidcRedirectUri: String(formData.get("oidcRedirectUri") ?? ""), + oidcAvatarClaim: String(formData.get("oidcAvatarClaim") ?? ""), oidcAutoProvision: formData.get("oidcAutoProvision") === "on", }), }); @@ -115,6 +116,11 @@ export function OrganizationDetailClient({ + + + +