xiaso-flnxi-website/lib/authOptions.ts
2025-12-24 14:41:11 +08:00

224 lines
6.2 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { AuthOptions } from "next-auth"
import CredentialsProvider from "next-auth/providers/credentials"
import type { User } from "next-auth"
import bcrypt from 'bcryptjs'
import { db } from '@/lib/db'
import crypto from 'crypto'
type WeChatTokens = {
access_token?: string
refresh_token?: string
expires_in?: number
openid?: string
unionid?: string
scope?: string
[key: string]: unknown
}
function WeChatProvider(): any {
const appId = process.env.WECHAT_APP_ID
const appSecret = process.env.WECHAT_APP_SECRET
if (!appId || !appSecret) {
console.error('❌ 微信登录配置缺失: WECHAT_APP_ID 或 WECHAT_APP_SECRET 未设置')
} else {
console.log('✅ 微信登录配置已加载:', { appId: appId.substring(0, 8) + '...' })
}
return {
id: 'wechat',
name: 'WeChat',
type: 'oauth',
version: '2.0',
checks: ['pkce', 'state'],
authorization: {
url: 'https://open.weixin.qq.com/connect/qrconnect',
params: {
appid: appId,
scope: 'snsapi_login',
response_type: 'code',
// redirect_uri 由 NextAuth 自动注入
},
},
token: {
url: 'https://api.weixin.qq.com/sns/oauth2/access_token',
params: {
appid: appId,
secret: appSecret,
grant_type: 'authorization_code',
},
async request({
params,
provider,
}: {
params: Record<string, string>
provider: any
}) {
const url = `${provider.token!.url}?${new URLSearchParams(
params as Record<string, string>
).toString()}`
const res = await fetch(url, { method: 'GET' })
if (!res.ok) {
throw new Error(`WeChat token request failed: ${res.status}`)
}
const tokens = (await res.json()) as WeChatTokens
return { tokens }
},
},
userinfo: {
// 需要 access_token + openid
async request({ tokens }: { tokens: WeChatTokens }) {
const accessToken = tokens.access_token as string
const openid = (tokens as any).openid as string
if (!accessToken || !openid) {
throw new Error('Missing access_token or openid from WeChat')
}
const url = `https://api.weixin.qq.com/sns/userinfo?${new URLSearchParams({
access_token: accessToken,
openid,
lang: 'zh_CN',
}).toString()}`
const res = await fetch(url)
if (!res.ok) {
throw new Error(`WeChat userinfo failed: ${res.status}`)
}
return res.json()
},
},
profile(profile: any) {
return {
id: profile.unionid || profile.openid,
name: profile.nickname || '微信用户',
email: profile.email || `${profile.openid}@wechat.flnxi.com`,
image: profile.headimgurl,
}
},
clientId: appId,
clientSecret: appSecret,
}
}
export const authOptions: AuthOptions = {
providers: [
// 邮箱密码登录
CredentialsProvider({
name: 'credentials',
credentials: {
email: { label: "邮箱", type: "email", placeholder: "user@example.com" },
password: { label: "密码", type: "password" }
},
async authorize(credentials): Promise<User | null> {
try {
if (!credentials?.email || !credentials?.password) {
console.log('❌ 登录失败: 缺少邮箱或密码')
return null
}
// 从数据库查询用户
const user = await db.findUserByEmail(credentials.email)
if (!user) {
console.log('❌ 登录失败: 用户不存在 -', credentials.email)
return null
}
// 使用 bcrypt 验证密码
const isValidPassword = await bcrypt.compare(
credentials.password,
user.password
)
if (!isValidPassword) {
console.log('❌ 登录失败: 密码错误 -', credentials.email)
return null
}
console.log('✅ 登录成功:', {
id: user.id,
name: user.name,
email: user.email,
})
// 返回用户信息(不包含密码)
return {
id: user.id.toString(),
name: user.name,
email: user.email,
}
} catch (error) {
console.error('登录错误:', error)
return null
}
}
}),
// 微信扫码登录
WeChatProvider(),
],
// 在 signIn 阶段将微信用户写入数据库
callbacks: {
async signIn({ user, account, profile }) {
if (account?.provider === 'wechat') {
const unionid = (profile as any)?.unionid || (profile as any)?.id
const openid = (profile as any)?.openid
const name = (profile as any)?.nickname || user.name || '微信用户'
const email =
(profile as any)?.email ||
user.email ||
`${openid || unionid || crypto.randomUUID()}@wechat.flnxi.com`
const randomPassword = crypto.randomBytes(16).toString('hex')
const hashedPassword = await bcrypt.hash(randomPassword, 10)
const dbUser = await db.upsertWechatUser({
name,
email,
unionid: unionid ?? null,
openid: openid ?? null,
hashedPassword,
})
// 将数据库中的 id/email/name 赋回 user,后续 jwt/session 使用
user.id = dbUser.id.toString()
user.email = dbUser.email
user.name = dbUser.name
}
return true
},
// 自定义页面
pages: {
signIn: '/auth/signin',
},
async jwt({ token, user }) {
if (user) {
token.id = user.id
token.email = user.email
token.name = user.name
}
return token
},
async session({ session, token }) {
if (session.user) {
session.user.id = token.id as string
session.user.email = token.email as string
session.user.name = token.name as string
}
return session
},
},
// 会话策略
session: {
strategy: "jwt",
maxAge: 30 * 24 * 60 * 60, // 30 天
},
// 密钥
secret: process.env.NEXTAUTH_SECRET,
// 调试模式(生产环境设为false)
debug: process.env.NODE_ENV === 'development',
}