Files
zhuiguang-ai/src/lib/auth.ts
T
ZhuiGuangAI Dev e263e28c81 全面审查优化: 43项修复(安全/性能/可靠性)+ 文档更新
🔴 P0 安全: .env入gitignore, CSP/HSTS, Logo安全(5MB限制+SVG消毒+SHA256), middleware权限收紧(VIP不能访问admin), 发布去重, GitHub认证, 删除危险API, skill-discoverer绕过审核修复, prisma默认连接修复
🔴 P0 可靠性: deepseek/github超时重试, 脚本层DeepSeek保护, 脚本启动验证, OAuth竞态修复
🔴 P0 性能: skill-discoverer N+1优化(↓94%), 后台API分页限制, getUserStats聚合优化
🟡 P1: 分类动态加载, 星级字段统一, fetch-logos并发Bug修复, 批量发布冲突不删除, check-tools重试, 前端空指针修复(13处), logo-fetcher日志, rate-limit清理, 公开API缓存
2026-05-26 00:30:36 +08:00

148 lines
4.2 KiB
TypeScript

import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import GithubProvider from "next-auth/providers/github";
import { prisma } from "@/lib/prisma";
import bcrypt from "bcryptjs";
const loginAttempts = new Map<string, { count: number; resetAt: number }>();
const MAX_LOGIN_ATTEMPTS = 7;
const LOGIN_WINDOW_MS = 60_000;
function checkLoginRateLimit(email: string): boolean {
const now = Date.now();
const record = loginAttempts.get(email);
if (!record || now > record.resetAt) {
loginAttempts.set(email, { count: 1, resetAt: now + LOGIN_WINDOW_MS });
return true;
}
if (record.count >= MAX_LOGIN_ATTEMPTS) return false;
record.count++;
return true;
}
export const authOptions = {
providers: [
CredentialsProvider({
name: "credentials",
credentials: {
email: { label: "邮箱", type: "email" },
password: { label: "密码", type: "password" },
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) return null;
if (!checkLoginRateLimit(credentials.email)) {
throw new Error("登录尝试过于频繁,请1分钟后再试");
}
const user = await prisma.user.findUnique({
where: { email: credentials.email },
});
if (!user || !user.passwordHash) return null;
const isValid = await bcrypt.compare(
credentials.password,
user.passwordHash
);
if (!isValid) return null;
loginAttempts.delete(credentials.email);
return {
id: user.id.toString(),
email: user.email,
name: user.name,
role: user.role,
level: user.level,
points: user.points,
};
},
}),
GithubProvider({
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
}),
],
callbacks: {
async signIn({ user, account, profile }: any) {
if (account?.provider === "github" && profile) {
const oauthId = profile.id?.toString();
const email = profile.email || user.email;
if (!oauthId) return false;
const existing = await prisma.user.findFirst({
where: {
oauthProvider: "github",
oauthId,
},
});
if (!existing) {
try {
const newUser = await prisma.user.create({
data: {
email: email || `github_${oauthId}@placeholder.com`,
name: profile.name || user.name || null,
avatarUrl: profile.avatar_url || user.image || null,
oauthProvider: "github",
oauthId,
role: "user",
},
});
user.id = newUser.id.toString();
user.role = newUser.role;
} catch (createErr: unknown) {
const ce = createErr as { code?: string };
if (ce.code === "P2002") {
const retry = await prisma.user.findFirst({
where: { oauthProvider: "github", oauthId },
});
if (retry) {
user.id = retry.id.toString();
user.role = retry.role;
return true;
}
}
throw createErr;
}
} else {
user.id = existing.id.toString();
user.role = existing.role;
}
}
return true;
},
async jwt({ token, user }: any) {
if (user) {
token.id = user.id;
token.role = user.role;
token.level = user.level;
token.points = user.points;
}
return token;
},
async session({ session, token }: any) {
if (session.user) {
session.user.id = token.id;
session.user.role = token.role;
session.user.level = token.level;
session.user.points = token.points;
}
return session;
},
},
pages: {
signIn: "/admin/login",
},
session: {
strategy: "jwt" as const,
maxAge: 30 * 24 * 60 * 60,
},
secret: process.env.NEXTAUTH_SECRET,
};
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };