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