104 lines
3.4 KiB
TypeScript
104 lines
3.4 KiB
TypeScript
import type { NextResponse } from "next/server";
|
|
import { SESSION_COOKIE_NAME, shouldUseSecureAuthCookie } from "@/lib/auth/config";
|
|
import type { AuthMode, AuthSession, AuthUser } from "@/lib/auth/session";
|
|
import type { PlatformOrganization, PlatformUserRecord } from "@/lib/types";
|
|
import { clearSessionCookieValues, setSessionCookieValue } from "@/lib/server/auth/session-cookie";
|
|
import { createSessionCookieValue } from "@/lib/auth/session";
|
|
import { getPlatformOrganization } from "@/lib/server/account-store";
|
|
|
|
export const LOCAL_SESSION_TTL_SECONDS = 24 * 60 * 60;
|
|
|
|
const ipAttempts = new Map<string, { count: number; resetAt: number }>();
|
|
|
|
export function authUserFromPlatformRecord(user: PlatformUserRecord, organization?: PlatformOrganization | null): AuthUser {
|
|
const role = user.role;
|
|
const authorities = role === "super_admin"
|
|
? ["ROLE_SUPER_ADMIN", "SUPER_ADMIN"]
|
|
: role === "organization_admin"
|
|
? ["ROLE_ORGANIZATION_ADMIN", "ORGANIZATION_ADMIN"]
|
|
: ["ROLE_USER"];
|
|
return {
|
|
id: user.id,
|
|
subject: user.id,
|
|
username: user.phone,
|
|
phone: user.phone,
|
|
displayName: user.displayName,
|
|
clientId: "platform",
|
|
organizationId: user.organizationId,
|
|
organizationName: organization?.name,
|
|
role,
|
|
status: user.status,
|
|
authorities,
|
|
scope: []
|
|
};
|
|
}
|
|
|
|
export async function createPlatformSession(user: PlatformUserRecord): Promise<AuthSession> {
|
|
const organization = user.organizationId ? await getPlatformOrganization(user.organizationId) : null;
|
|
const now = Math.floor(Date.now() / 1000);
|
|
const authMode: AuthMode = user.role === "user" ? "user" : "admin";
|
|
return {
|
|
version: 1,
|
|
authMode,
|
|
user: authUserFromPlatformRecord(user, organization),
|
|
issuedAt: now,
|
|
expiresAt: now + LOCAL_SESSION_TTL_SECONDS,
|
|
sessionVersion: user.sessionVersion
|
|
};
|
|
}
|
|
|
|
export async function setPlatformSessionCookie(
|
|
response: NextResponse,
|
|
requestUrl: string,
|
|
session: AuthSession
|
|
) {
|
|
const secret = process.env.ZHINIAN_AUTH_SESSION_SECRET || process.env.AUTH_SESSION_SECRET || process.env.NEXTAUTH_SECRET;
|
|
if (!secret) throw new Error("ZHINIAN_AUTH_SESSION_SECRET 未配置。");
|
|
setSessionCookieValue(
|
|
response,
|
|
requestUrl,
|
|
await createSessionCookieValue(session, secret),
|
|
new Date(session.expiresAt * 1000)
|
|
);
|
|
}
|
|
|
|
export function clearPlatformSessionCookies(response: NextResponse, requestUrl: string) {
|
|
clearSessionCookieValues(response, requestUrl);
|
|
response.cookies.set(SESSION_COOKIE_NAME, "", {
|
|
httpOnly: true,
|
|
sameSite: "lax",
|
|
secure: shouldUseSecureAuthCookie(requestUrl),
|
|
path: "/",
|
|
maxAge: 0
|
|
});
|
|
}
|
|
|
|
export function clientIpFromRequest(request: Request): string {
|
|
return request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ||
|
|
request.headers.get("x-real-ip")?.trim() ||
|
|
"unknown";
|
|
}
|
|
|
|
export function checkIpLoginRateLimit(ip: string): void {
|
|
const now = Date.now();
|
|
const current = ipAttempts.get(ip);
|
|
if (!current || current.resetAt <= now) {
|
|
ipAttempts.set(ip, { count: 1, resetAt: now + 15 * 60 * 1000 });
|
|
return;
|
|
}
|
|
if (current.count >= 30) {
|
|
const error = new Error("请求过于频繁,请稍后再试。") as Error & { status: number };
|
|
error.status = 429;
|
|
throw error;
|
|
}
|
|
current.count += 1;
|
|
}
|
|
|
|
export function clearIpLoginRateLimit(ip: string): void {
|
|
ipAttempts.delete(ip);
|
|
}
|
|
|
|
export function resetLocalAuthRateLimitForTests(): void {
|
|
ipAttempts.clear();
|
|
}
|