You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
85 lines
2.2 KiB
85 lines
2.2 KiB
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
|
|
}
|
|
}
|