1 changed files with 200 additions and 12 deletions
@ -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"; |
|||
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 logger = log4js.getLogger("AUTH") |
|||
const REFRESH_TOKEN_EXPIRY_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|||
|
|||
export async function getUsers() { |
|||
const users = await dbGlobal.select().from(usersTable) |
|||
logger.info("users (formatted): %s \n", JSON.stringify(users, null, 2)); |
|||
return users; |
|||
export class AuthService { |
|||
async register(opts: { |
|||
email: string; |
|||
password: string; |
|||
username: string; |
|||
ip?: string; |
|||
userAgent?: string; |
|||
}): Promise<AuthUser> { |
|||
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<void> { |
|||
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<void> { |
|||
await dbGlobal |
|||
.update(userSessions) |
|||
.set({ revokedAt: new Date() }) |
|||
.where(eq(userSessions.id, sessionId)); |
|||
} |
|||
|
|||
private async createSession( |
|||
userId: number, |
|||
ip?: string, |
|||
userAgent?: string |
|||
): Promise<string> { |
|||
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<TokenPair> { |
|||
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(); |
|||
Loading…
Reference in new issue