UNPKG

next-auth-oauth

Version:

`next-auth-oauth` 是一个基于 Next.js 和 NextAuth 的增强插件,用于简化和增强授权登录的处理。该插件提供了丰富的功能,包括第三方登录绑定、账户管理等,使得授权流程更加高效和灵活。

349 lines (338 loc) 9.82 kB
// 账号注册 import NextAuth, { type Account, CredentialsSignin, type NextAuthResult, } from 'next-auth' import type { Adapter, AdapterUser } from 'next-auth/adapters' import type { OAuthProviderButtonStyles, Provider } from 'next-auth/providers' import credentials from 'next-auth/providers/credentials' import { cookies } from 'next/headers' import type { BindoAuthAccountInfo, CallbackJwtFunction, CallbackSessionInFunction, CallbackSignInFunction, IUserService, NextAuthConfig, NextAuthResultType, } from './type' async function cleanBindAccountInfo() { const cookie = await cookies() cookie.delete('nextauth.bind.account') cookie.delete('nextauth.bind.user') } /** * 从cookie获得绑定账号信息 * @returns */ export async function loadTempOauthUser(): Promise<BindoAuthAccountInfo> { const cookie = await cookies() try { const account = JSON.parse( cookie.get('nextauth.bind.account')?.value ?? 'null', ) as Account | null const user = JSON.parse( cookie.get('nextauth.bind.user')?.value ?? 'null', ) as AdapterUser | null const bindAccount = !!(account && user) return { user, bindAccount, account } } catch (_error) { return { user: null, bindAccount: false, account: null } } } export class CredentialsOauth { private userService: IUserService private authAdapter: Adapter private bindPage: string private autoBind: boolean constructor( userService: IUserService, nextAuthAdapter: Adapter, /** * 配置绑定UI */ bindPage: string = '/auth/bind', /** * 登录过的账号自动绑定 */ autoBind: boolean = false, ) { this.userService = userService this.authAdapter = nextAuthAdapter this.bindPage = bindPage this.autoBind = autoBind } /** * 构建账号密码登录的provider * @param options * @returns */ getCredentialsProvider() { return credentials({ credentials: { username: { placeholder: '登录账号' }, password: { placeholder: '密码/验证码', }, type: { name: 'type', label: '登录方式', type: 'radio', value: 'password', children: '密码登录', }, type2: { label: '', name: 'type', type: 'radio', value: 'mobile', children: '手机验证码', }, autoBindTempAccount: { type: 'hidden' }, }, authorize: async (credentials) => { if ( typeof credentials.username === 'string' && typeof credentials.password === 'string' ) { const { bindAccount, account } = await loadTempOauthUser() const autoBindAccount = credentials['autoBindTempAccount'] == 'string' && credentials['autoBindTempAccount'] === 'true' const user = await this.userService.login( credentials.username, credentials.password, (credentials.type ?? 'password') as 'password' | 'mobile', ) if (autoBindAccount && user && bindAccount && account) { await this.authAdapter.linkAccount?.({ ...account, userId: user.id, type: (account.type ?? 'oauth') as 'oauth', }) cleanBindAccountInfo() } return user } throw new CredentialsSignin('账号或者密码错误') }, }) } private async signInCallback( params: Parameters<CallbackSignInFunction>[0], auth: NextAuthResultType['auth'], ) { const authUser = await auth() const { user, account } = params if (account?.type !== 'oauth' && account?.type !== 'oidc') { return true } if (account) { const databseUser = await this.authAdapter.getUserByAccount?.({ provider: account.provider, providerAccountId: account.providerAccountId, }) if (databseUser) { return true } if (authUser?.user?.id && this.autoBind) { await this.authAdapter.linkAccount?.({ ...account, userId: authUser.user.id, type: account?.type as 'oauth', }) return true } } const cookie = await cookies() cookie.set('nextauth.bind.account', JSON.stringify(account)) cookie.set('nextauth.bind.user', JSON.stringify(user)) return this.bindPage } private async sessionCallback( params: Parameters<CallbackSessionInFunction>[0], ) { const { session, token, user } = params const newSession = { ...session, user: { ...session.user, name: token.name ?? user?.name, id: user?.id ?? token?.sub, }, } return newSession } private async jwtCallback(params: Parameters<CallbackJwtFunction>[0]) { const { token, user, trigger } = params if (trigger === 'signIn') { // @ts-expect-error token is JWT token.name = user?.nickname ?? user?.name token.sub = user?.id token.email = user?.email token.picture = user?.image } return token } /** * * @param config * @returns */ nextAuth(config: NextAuthConfig): NextAuthResultType { const nextAuthInstance = NextAuth({ ...config, providers: (config.providers ?? []).concat(this.getCredentialsProvider()), callbacks: { signIn: async (params) => { const reuslt = await this.signInCallback( params, nextAuthInstance.auth, ) if ( reuslt === true && typeof config.callbacks?.signIn === 'function' ) { return config.callbacks?.signIn(params) } return reuslt }, session: async (params) => { const session = await this.sessionCallback(params) if (typeof config.callbacks?.session === 'function') { return config.callbacks?.session({ ...params, session }) } return session }, jwt: async (params) => { const token = await this.jwtCallback(params) if (typeof config.callbacks?.jwt === 'function') { return config.callbacks?.jwt({ ...params, token }) } return token }, }, }) const oauthProviders = config.providers ?.map((provider: Provider) => { if (typeof provider === 'function') { provider = provider() } return { id: provider.id, name: provider.name, // @ts-expect-error provider.type is undefined style: provider.style as OAuthProviderButtonStyles, } }) .filter((provider) => provider.id !== 'credentials') /** * 账号注册,并自动绑定 * 注意这是一个ServerAction * @param formData */ const signUp = async (formData: FormData) => { const { user, bindAccount, account } = await loadTempOauthUser() // 获得账号密码 const { username, autoBindTempAccount, password, redirectTo, ...formUser } = // @ts-expect-error formData is FormData Object.fromEntries(formData) // 创建账号 const adapterUser = await this.userService.registUser({ username: username.toString(), password: password.toString(), formData: formUser as { [key: string]: string }, }) if ( autoBindTempAccount === 'true' && bindAccount && account && user && adapterUser ) { await this.authAdapter.linkAccount?.({ ...account, userId: adapterUser.id, type: 'oauth', }) cleanBindAccountInfo() return nextAuthInstance.signIn('credentials', { username, password, redirectTo: redirectTo?.toString(), }) } } const signInAndBindAccount: NextAuthResultType['signInAndBindAccount'] = async (options, params) => { options?.append('autoBindTempAccount', 'true') return nextAuthInstance.signIn('credentials', options, params) } const signUpAndBindAccount = async (formData: FormData) => { formData?.append('autoBindTempAccount', 'true') return signUp(formData) } /** * 列出绑定的授权账户列表 * @returns */ const listAccount = async () => { const session = await nextAuthInstance.auth() const userId = session?.user?.id if (userId) { return this.userService.listAccount(userId) } return [] } return { ...nextAuthInstance, oauthProviders, signUpAndBindAccount: signUpAndBindAccount.bind(this), listAccount: listAccount.bind(this), signUp, signInAndBindAccount, tempOauthUser: loadTempOauthUser, } } } export type AdavanceNextAuthConfig = NextAuthConfig & { /** * 第三方账号首次登录绑定页面 */ bindPage?: string /** * 已登录后账号默认是否自动绑定 * @default false */ autoBind?: boolean /** * 配置用户数据库服务 */ userService: IUserService adapter: Adapter } /** * 封装好的支持授权绑定的服务 * 1. 分装好regist注册`ServerAction` * 2. 封装好`Credentials`的认证逻辑 * 3. 分装好`OauthCallcak`的逻辑,自动判断账号有效性 * @param config * @returns */ export function AdavanceNextAuth(config: AdavanceNextAuthConfig) { const { bindPage, userService, autoBind, ...nextAuthConfig } = config const credentialsProvider = new CredentialsOauth( userService, config.adapter, bindPage, autoBind, ) return credentialsProvider.nextAuth(nextAuthConfig) }