4 changed files with 164 additions and 0 deletions
@ -0,0 +1,15 @@ |
|||
import { PasswordStrategy } from "./password.strategy"; |
|||
import { OAuthStrategy } from "./oauth.strategy"; |
|||
import type { IAuthStrategy } from "../../types/auth"; |
|||
|
|||
export { PasswordStrategy, OAuthStrategy }; |
|||
|
|||
export const strategyRegistry: Record<string, IAuthStrategy> = { |
|||
password: new PasswordStrategy(), |
|||
}; |
|||
|
|||
export function getStrategy(type: string): IAuthStrategy { |
|||
const s = strategyRegistry[type]; |
|||
if (!s) throw new Error(`Unknown auth strategy: ${type}`); |
|||
return s; |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
import type { IAuthStrategy, AuthUser, TokenPair } from "../../types/auth"; |
|||
|
|||
export abstract class OAuthStrategy implements IAuthStrategy { |
|||
abstract readonly provider: string; |
|||
abstract readonly type: "oauth"; |
|||
|
|||
abstract authenticate(oauthToken: string): Promise<AuthUser>; |
|||
|
|||
async generateTokens( |
|||
user: AuthUser, |
|||
sessionId: string |
|||
): Promise<TokenPair> { |
|||
throw new Error("Use AuthService.generateTokens()"); |
|||
} |
|||
|
|||
async revokeSession(sessionId: string): Promise<void> { |
|||
throw new Error("Use AuthService.revokeSession()"); |
|||
} |
|||
} |
|||
@ -0,0 +1,85 @@ |
|||
import { eq } from "drizzle-orm"; |
|||
import { dbGlobal } from "@/drizzle-pkg/lib/db"; |
|||
import { users } from "@/drizzle-pkg/lib/schema/auth"; |
|||
import type { IAuthStrategy, AuthUser } from "../../types/auth"; |
|||
import { |
|||
verifyPassword, |
|||
isLocked, |
|||
} from "../lib/password"; |
|||
import { signAccessToken } from "../lib/jwt"; |
|||
|
|||
export class PasswordStrategy implements IAuthStrategy { |
|||
readonly type = "password" as const; |
|||
|
|||
async authenticate(credentials: { |
|||
email: string; |
|||
password: string; |
|||
ip?: string; |
|||
userAgent?: string; |
|||
}): Promise<AuthUser> { |
|||
const { email, password, ip, userAgent } = credentials; |
|||
|
|||
const [user] = await dbGlobal |
|||
.select() |
|||
.from(users) |
|||
.where(eq(users.email, email)) |
|||
.limit(1); |
|||
|
|||
if (!user) { |
|||
throw { code: "INVALID_CREDENTIALS", message: "邮箱或密码错误" }; |
|||
} |
|||
|
|||
if (user.lockoutUntil && isLocked(user.lockoutUntil)) { |
|||
throw { code: "ACCOUNT_LOCKED", message: "账户已锁定,请稍后再试" }; |
|||
} |
|||
|
|||
const valid = await verifyPassword(password, user.password); |
|||
if (!valid) { |
|||
const attempts = (user.failedLoginAttempts || 0) + 1; |
|||
const lockoutUntil = attempts >= 5 ? new Date(Date.now() + 15 * 60_000) : null; |
|||
await dbGlobal |
|||
.update(users) |
|||
.set({ |
|||
failedLoginAttempts: attempts, |
|||
lockoutUntil: lockoutUntil ?? null, |
|||
}) |
|||
.where(eq(users.id, user.id)); |
|||
|
|||
throw { code: "INVALID_CREDENTIALS", message: "邮箱或密码错误" }; |
|||
} |
|||
|
|||
await dbGlobal |
|||
.update(users) |
|||
.set({ |
|||
failedLoginAttempts: 0, |
|||
lockoutUntil: null, |
|||
lastLoginAt: new Date(), |
|||
lastLoginIp: ip ?? null, |
|||
}) |
|||
.where(eq(users.id, user.id)); |
|||
|
|||
return { |
|||
id: user.id, |
|||
email: user.email, |
|||
username: user.username, |
|||
role: user.role, |
|||
status: user.status, |
|||
}; |
|||
} |
|||
|
|||
async generateTokens( |
|||
user: AuthUser, |
|||
sessionId: string |
|||
): Promise<{ accessToken: string; refreshToken: string }> { |
|||
const accessToken = await signAccessToken({ |
|||
userId: user.id, |
|||
sessionId, |
|||
role: user.role, |
|||
}); |
|||
return { accessToken, refreshToken: "" }; |
|||
} |
|||
|
|||
async revokeSession(sessionId: string): Promise<void> { |
|||
// delegated to AuthService
|
|||
} |
|||
} |
|||
@ -0,0 +1,45 @@ |
|||
export interface TokenPair { |
|||
accessToken: string; |
|||
refreshToken: string; |
|||
} |
|||
|
|||
export interface Session { |
|||
id: string; |
|||
userId: number; |
|||
userAgent: string | null; |
|||
ip: string | null; |
|||
createdAt: Date; |
|||
expiresAt: Date; |
|||
revokedAt: Date | null; |
|||
} |
|||
|
|||
export interface AuthUser { |
|||
id: number; |
|||
email: string | null; |
|||
username: string; |
|||
role: string; |
|||
status: string; |
|||
} |
|||
|
|||
export type AuthErrorCode = |
|||
| "INVALID_CREDENTIALS" |
|||
| "ACCOUNT_LOCKED" |
|||
| "WEAK_PASSWORD" |
|||
| "EMAIL_EXISTS" |
|||
| "TOKEN_EXPIRED" |
|||
| "SESSION_REVOKED" |
|||
| "RATE_LIMITED"; |
|||
|
|||
export interface AuthError { |
|||
error: { |
|||
code: AuthErrorCode; |
|||
message: string; |
|||
}; |
|||
} |
|||
|
|||
export interface IAuthStrategy { |
|||
readonly type: "password" | "oauth"; |
|||
authenticate(credentials: unknown): Promise<AuthUser>; |
|||
generateTokens(user: AuthUser, sessionId: string): Promise<TokenPair>; |
|||
revokeSession(sessionId: string): Promise<void>; |
|||
} |
|||
Loading…
Reference in new issue