224 lines
6.2 KiB
TypeScript
224 lines
6.2 KiB
TypeScript
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',
|
||
}
|
||
|