Files

153 lines
4.6 KiB
TypeScript

import { randomBytes } from "node:crypto";
import { and, eq } from "drizzle-orm";
import {
DEFAULT_INVITATION_MAX_USES,
DEFAULT_INVITATION_VALIDITY_DAYS,
MAX_INVITATION_MAX_USES,
MAX_INVITATION_VALIDITY_DAYS,
MIN_INVITATION_MAX_USES,
MIN_INVITATION_VALIDITY_DAYS,
} from "@/modules/core/invitation-limits";
import { jsonFailure, jsonSuccess, readJsonBody } from "@/modules/core/server/api";
import { recordAuditLog } from "@/modules/core/server/audit";
import { requirePermission } from "@/modules/core/server/auth";
import { getDatabase } from "@/modules/core/server/db";
import { organizationInvitations, organizations, roles } from "@/modules/core/server/schema";
type RouteContext = {
params: Promise<{
id: string;
}>;
};
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 readBoundedInteger(
source: Record<string, unknown>,
key: string,
options: {
defaultValue: number;
min: number;
max: number;
label: string;
},
): { success: true; value: number } | { success: false; reason: string } {
const rawValue = source[key];
const value = rawValue === undefined || rawValue === null ? options.defaultValue : Number(rawValue);
if (!Number.isInteger(value) || value < options.min || value > options.max) {
return { success: false, reason: `${options.label}需为 ${options.min}-${options.max} 的整数` };
}
return { success: true, value };
}
function createInvitationToken(): string {
return randomBytes(24).toString("base64url");
}
export async function POST(request: Request, context: RouteContext): Promise<Response> {
const { id } = await context.params;
const auth = await requirePermission("account:manage", {
action: "organizationInvitation.create",
targetType: "organization",
targetId: id,
});
if (!auth.success) {
return auth.response;
}
const activeOrganizationId = auth.context.organization?.id;
if (activeOrganizationId && activeOrganizationId !== id) {
return jsonFailure("不能为其他机构创建邀请", 403);
}
const body = await readJsonBody(request);
if (!isRecord(body)) {
return jsonFailure("请求数据格式无效");
}
const email = readString(body, "email");
const roleId = readString(body, "roleId");
if (!roleId) {
return jsonFailure("请选择邀请角色");
}
const validityDaysResult = readBoundedInteger(body, "validityDays", {
defaultValue: DEFAULT_INVITATION_VALIDITY_DAYS,
min: MIN_INVITATION_VALIDITY_DAYS,
max: MAX_INVITATION_VALIDITY_DAYS,
label: "邀请有效期",
});
if (!validityDaysResult.success) {
return jsonFailure(validityDaysResult.reason);
}
const maxUsesResult = readBoundedInteger(body, "maxUses", {
defaultValue: DEFAULT_INVITATION_MAX_USES,
min: MIN_INVITATION_MAX_USES,
max: MAX_INVITATION_MAX_USES,
label: "最大使用次数",
});
if (!maxUsesResult.success) {
return jsonFailure(maxUsesResult.reason);
}
const database = getDatabase();
const organizationRows = await database.select().from(organizations).where(eq(organizations.id, id)).limit(1);
const organization = organizationRows[0];
if (!organization) {
return jsonFailure("机构不存在", 404);
}
const roleRows = await database
.select()
.from(roles)
.where(and(eq(roles.id, roleId), eq(roles.organizationId, id), eq(roles.isEnabled, true)))
.limit(1);
const role = roleRows[0];
if (!role) {
return jsonFailure("角色不存在", 404);
}
const rows = await database
.insert(organizationInvitations)
.values({
organizationId: id,
roleId: role.id,
email,
token: createInvitationToken(),
status: "active",
createdByAccountId: auth.context.account.id,
expiresAt: new Date(Date.now() + validityDaysResult.value * 24 * 60 * 60 * 1000),
maxUses: maxUsesResult.value,
usedCount: 0,
})
.returning();
const invitation = rows[0];
if (!invitation) {
return jsonFailure("邀请创建失败", 500);
}
await recordAuditLog({
actor: auth.context.account,
organizationId: id,
action: "organizationInvitation.create",
targetType: "organizationInvitation",
targetId: invitation.id,
result: "success",
reason: `创建机构邀请:${email || "通用邀请"},有效期 ${validityDaysResult.value} 天,最多使用 ${maxUsesResult.value}`,
});
return jsonSuccess("邀请已创建", { invitation }, 201);
}