feat: add global auth settings and pending users

This commit is contained in:
2026-07-02 06:27:32 -07:00
parent f6b7924d0c
commit 4bb4312b08
24 changed files with 2831 additions and 23 deletions

View File

@@ -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<Re
}
const name = readString(body, "name");
const avatarUrl = readString(body, "avatarUrl");
const hasAvatarUrl = "avatarUrl" in body;
const status = "status" in body ? readStatus(body.status) : null;
const roleId = readString(body, "roleId");
const requestedOrganizationId = readString(body, "organizationId");
@@ -79,7 +82,7 @@ export async function PATCH(request: Request, context: RouteContext): Promise<Re
return jsonFailure("账号状态无效");
}
if (!name && !status && !roleId) {
if (!name && !hasAvatarUrl && !status && !roleId) {
return jsonFailure("请至少填写一个要更新的字段");
}
@@ -113,9 +116,10 @@ export async function PATCH(request: Request, context: RouteContext): Promise<Re
try {
await database.transaction(async (transaction) => {
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<Re
if (name) {
updateValues.name = name;
}
if (hasAvatarUrl) {
updateValues.avatarUrl = avatarUrl;
}
if (status) {
updateValues.status = status;
}

View File

@@ -76,6 +76,7 @@ export async function POST(request: Request): Promise<Response> {
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<Response> {
.values({
name,
email: normalizedEmail,
avatarUrl,
passwordHash: passwordHash.hash,
passwordSalt: passwordHash.salt,
status: "active",
@@ -162,6 +164,7 @@ export async function POST(request: Request): Promise<Response> {
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(),

View File

@@ -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<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function readString(source: Record<string, unknown>, key: string): string {
const value = source[key];
return typeof value === "string" ? value.trim() : "";
}
function readBoolean(source: Record<string, unknown>, key: string): boolean | null {
const value = source[key];
return typeof value === "boolean" ? value : null;
}
export async function GET(): Promise<Response> {
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<Response> {
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 });
}