diff --git a/server/service/auth/index.ts b/server/service/auth/index.ts index 7751318..00e0e82 100644 --- a/server/service/auth/index.ts +++ b/server/service/auth/index.ts @@ -1,12 +1,200 @@ -import { dbGlobal } from "drizzle-pkg/lib/db"; -import { users as usersTable } from "drizzle-pkg/lib/schema/auth"; -import { eq } from "drizzle-orm"; -import log4js from "logger"; - -const logger = log4js.getLogger("AUTH") - -export async function getUsers() { - const users = await dbGlobal.select().from(usersTable) - logger.info("users (formatted): %s \n", JSON.stringify(users, null, 2)); - return users; -} \ No newline at end of file +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: JSON.stringify([passwordHash]), + }) + .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 sessions = await dbGlobal + .select() + .from(userSessions) + .where(isNull(userSessions.revokedAt)); + + let matchedSession: (typeof sessions)[0] | null = null; + for (const s of sessions) { + if (s.id === refreshToken && s.expiresAt > new Date()) { + matchedSession = s; + break; + } + } + + if (!matchedSession) { + throw { code: "SESSION_REVOKED", message: "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(); \ No newline at end of file