Browse Source

feat(auth): implement Strategy pattern - PasswordStrategy + OAuthStrategy abstract

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
npmrun 1 week ago
parent
commit
4ffab854d2
  1. 15
      server/service/auth/strategies/index.ts
  2. 19
      server/service/auth/strategies/oauth.strategy.ts
  3. 85
      server/service/auth/strategies/password.strategy.ts
  4. 45
      server/types/auth.ts

15
server/service/auth/strategies/index.ts

@ -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;
}

19
server/service/auth/strategies/oauth.strategy.ts

@ -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()");
}
}

85
server/service/auth/strategies/password.strategy.ts

@ -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
}
}

45
server/types/auth.ts

@ -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…
Cancel
Save