feat: add invitation link limits
This commit is contained in:
@@ -2,6 +2,14 @@ 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";
|
||||
@@ -23,6 +31,25 @@ function readString(source: Record<string, unknown>, key: string): string {
|
||||
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");
|
||||
}
|
||||
@@ -55,6 +82,26 @@ export async function POST(request: Request, context: RouteContext): Promise<Res
|
||||
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];
|
||||
@@ -81,7 +128,9 @@ export async function POST(request: Request, context: RouteContext): Promise<Res
|
||||
token: createInvitationToken(),
|
||||
status: "active",
|
||||
createdByAccountId: auth.context.account.id,
|
||||
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
||||
expiresAt: new Date(Date.now() + validityDaysResult.value * 24 * 60 * 60 * 1000),
|
||||
maxUses: maxUsesResult.value,
|
||||
usedCount: 0,
|
||||
})
|
||||
.returning();
|
||||
const invitation = rows[0];
|
||||
@@ -96,7 +145,7 @@ export async function POST(request: Request, context: RouteContext): Promise<Res
|
||||
targetType: "organizationInvitation",
|
||||
targetId: invitation.id,
|
||||
result: "success",
|
||||
reason: `创建机构邀请:${email || "通用邀请"}`,
|
||||
reason: `创建机构邀请:${email || "通用邀请"},有效期 ${validityDaysResult.value} 天,最多使用 ${maxUsesResult.value} 次`,
|
||||
});
|
||||
|
||||
return jsonSuccess("邀请已创建", { invitation }, 201);
|
||||
|
||||
Reference in New Issue
Block a user