import { v4 as uuidv4 } from "uuid"; import { eq, isNull } from "drizzle-orm"; import { dbGlobal } from "@/drizzle-pkg/lib/db"; import { users, userSessions } from "@/drizzle-pkg/lib/schema/auth"; import { signAccessToken } from "./lib/jwt"; import { hashPassword, validatePasswordStrength } from "./lib/password"; import type { AuthUser, TokenPair } from "../../types/auth"; import { getStrategy } from "./strategies"; const REFRESH_TOKEN_EXPIRY_MS = 7 * 24 * 60 * 60 * 1000; // 7 days export class AuthService { async register(opts: { email: string; password: string; username: string; ip?: string; userAgent?: string; }): Promise { const { email, password, username, ip, userAgent } = opts; // 检查邮箱是否已存在 const [existing] = await dbGlobal .select({ id: users.id }) .from(users) .where(eq(users.email, email)) .limit(1); if (existing) { throw { code: "EMAIL_EXISTS", message: "该邮箱已注册" }; } // 密码强度校验 const { valid, errors } = validatePasswordStrength(password); if (!valid) { throw { code: "WEAK_PASSWORD", message: errors.join(";") }; } const passwordHash = await hashPassword(password); // 插入用户 const [user] = await dbGlobal .insert(users) .values({ email, username, password: passwordHash, role: "user", status: "active", passwordHistory: "[]", }) .returning(); // 创建 session await this.createSession(user.id, ip, userAgent); return { id: user.id, email: user.email ?? null, username: user.username, role: user.role, status: user.status, }; } async login(opts: { email: string; password: string; ip?: string; userAgent?: string; }): Promise<{ user: AuthUser; accessToken: string; refreshToken: string }> { const { email, password, ip, userAgent } = opts; // 用 PasswordStrategy 认证 const strategy = getStrategy("password"); const user = await strategy.authenticate({ email, password, ip, userAgent }); // 创建 session const sessionId = await this.createSession(user.id, ip, userAgent); // 生成 tokens const tokens = await this.generateTokens(user, sessionId); return { user, ...tokens }; } async logout(sessionId: string): Promise { await dbGlobal .update(userSessions) .set({ revokedAt: new Date() }) .where(eq(userSessions.id, sessionId)); } async refreshToken( refreshToken: string ): Promise<{ accessToken: string; newRefreshToken: string }> { // 查找有效 session const [session] = await dbGlobal .select() .from(userSessions) .where(eq(userSessions.id, refreshToken)) .limit(1); if (!session || session.revokedAt !== null || session.expiresAt <= new Date()) { throw { code: "SESSION_REVOKED", message: "Session 已失效" }; } const matchedSession = session; const [user] = await dbGlobal .select() .from(users) .where(eq(users.id, matchedSession.userId)) .limit(1); if (!user || user.status !== "active") { throw { code: "INVALID_CREDENTIALS", message: "用户状态异常" }; } // 作废旧 session await dbGlobal .update(userSessions) .set({ revokedAt: new Date() }) .where(eq(userSessions.id, matchedSession.id)); // 创建新 session (refresh token rotation) const newSessionId = await this.createSession( user.id, matchedSession.ip ?? undefined, matchedSession.userAgent ?? undefined ); const authUser: AuthUser = { id: user.id, email: user.email ?? null, username: user.username, role: user.role, status: user.status, }; const tokens = await this.generateTokens(authUser, newSessionId); return { accessToken: tokens.accessToken, newRefreshToken: tokens.refreshToken }; } async revokeSession(sessionId: string): Promise { await dbGlobal .update(userSessions) .set({ revokedAt: new Date() }) .where(eq(userSessions.id, sessionId)); } private async createSession( userId: number, ip?: string, userAgent?: string ): Promise { const id = uuidv4(); const expiresAt = new Date(Date.now() + REFRESH_TOKEN_EXPIRY_MS); await dbGlobal.insert(userSessions).values({ id, userId, refreshTokenHash: "", // filled by generateTokens userAgent: userAgent ?? null, ip: ip ?? null, createdAt: new Date(), expiresAt, revokedAt: null, }); return id; } private async generateTokens( user: AuthUser, sessionId: string ): Promise { const accessToken = await signAccessToken({ userId: user.id, sessionId, role: user.role, }); // refreshToken = sessionId itself (UUIDv4 is sufficiently random) const refreshToken = sessionId; await dbGlobal .update(userSessions) .set({ refreshTokenHash: refreshToken }) .where(eq(userSessions.id, sessionId)); return { accessToken, refreshToken }; } } export const authService = new AuthService();