1 changed files with 200 additions and 12 deletions
@ -1,12 +1,200 @@ |
|||||
import { dbGlobal } from "drizzle-pkg/lib/db"; |
import { v4 as uuidv4 } from "uuid"; |
||||
import { users as usersTable } from "drizzle-pkg/lib/schema/auth"; |
import { eq, isNull } from "drizzle-orm"; |
||||
import { eq } from "drizzle-orm"; |
import { dbGlobal } from "@/drizzle-pkg/lib/db"; |
||||
import log4js from "logger"; |
import { users, userSessions } from "@/drizzle-pkg/lib/schema/auth"; |
||||
|
import { signAccessToken } from "./lib/jwt"; |
||||
const logger = log4js.getLogger("AUTH") |
import { hashPassword, validatePasswordStrength } from "./lib/password"; |
||||
|
import type { AuthUser, TokenPair } from "../../types/auth"; |
||||
export async function getUsers() { |
import { getStrategy } from "./strategies"; |
||||
const users = await dbGlobal.select().from(usersTable) |
|
||||
logger.info("users (formatted): %s \n", JSON.stringify(users, null, 2)); |
const REFRESH_TOKEN_EXPIRY_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
||||
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