diff --git a/src/client/api/authjs/useAuth.ts b/src/client/api/authjs/useAuth.ts index 8723a59..083489b 100644 --- a/src/client/api/authjs/useAuth.ts +++ b/src/client/api/authjs/useAuth.ts @@ -4,6 +4,7 @@ import { useUserStore } from '@/store/user'; import { toast } from 'sonner'; import { trpc } from '../trpc'; import { useTranslation } from '@i18next-toolkit/react'; +import { BuiltInProviderType } from '@auth/core/providers'; export function useAuth() { const trpcUtils = trpc.useUtils(); @@ -38,6 +39,32 @@ export function useAuth() { } ); + const loginWithOAuth = useEvent(async (provider: BuiltInProviderType) => { + let res: SignInResponse | undefined; + try { + res = await signIn(provider, { + redirect: false, + }); + console.log('res', res); + } catch (err) { + toast.error(t('Login failed')); + throw err; + } + + if (res?.error) { + toast.error(t('Login failed')); + throw new Error('Login failed'); + } + + const userInfo = await trpcUtils.user.info.fetch(); + if (!userInfo) { + toast.error(t('Can not get current user info')); + throw new Error('Login failed, '); + } + + return userInfo; + }); + const logout = useEvent(async () => { await signOut({ redirect: false, @@ -49,6 +76,7 @@ export function useAuth() { return { loginWithPassword, + loginWithOAuth, logout, }; } diff --git a/src/client/hooks/useConfig.ts b/src/client/hooks/useConfig.ts index d87fdff..f30d38e 100644 --- a/src/client/hooks/useConfig.ts +++ b/src/client/hooks/useConfig.ts @@ -6,6 +6,7 @@ const defaultGlobalConfig: AppRouterOutput['global']['config'] = { allowRegister: false, alphaMode: false, disableAnonymousTelemetry: false, + authProvider: [], }; const callAnonymousTelemetry = once(() => { diff --git a/src/client/routes/login.tsx b/src/client/routes/login.tsx index cf95203..327d0f7 100644 --- a/src/client/routes/login.tsx +++ b/src/client/routes/login.tsx @@ -1,6 +1,6 @@ import { createFileRoute, redirect, useNavigate } from '@tanstack/react-router'; import { useGlobalConfig } from '@/hooks/useConfig'; -import { Form, Typography } from 'antd'; +import { Divider, Form, Typography } from 'antd'; import { useTranslation } from '@i18next-toolkit/react'; import { setUserInfo } from '@/store/user'; import { z } from 'zod'; @@ -8,6 +8,7 @@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { useAuth } from '@/api/authjs/useAuth'; import { useEventWithLoading } from '@/hooks/useEvent'; +import { LuGithub } from 'react-icons/lu'; export const Route = createFileRoute('/login')({ validateSearch: z.object({ @@ -29,7 +30,7 @@ function LoginComponent() { const { t } = useTranslation(); const search = Route.useSearch(); - const { loginWithPassword } = useAuth(); + const { loginWithPassword, loginWithOAuth } = useAuth(); const [handleLogin, loading] = useEventWithLoading(async (values: any) => { const userInfo = await loginWithPassword(values.username, values.password); @@ -41,7 +42,7 @@ function LoginComponent() { replace: true, }); }); - const { allowRegister } = useGlobalConfig(); + const { allowRegister, authProvider } = useGlobalConfig(); return (
@@ -96,6 +97,22 @@ function LoginComponent() { )} + + {authProvider.length > 0 && ( + <> + {t('Or')} + +
+ +
+ + )}
); diff --git a/src/server/model/auth.ts b/src/server/model/auth.ts index 3fbcc6f..c1bc7cb 100644 --- a/src/server/model/auth.ts +++ b/src/server/model/auth.ts @@ -1,6 +1,7 @@ import { Auth, AuthConfig, createActionURL } from '@auth/core'; import Nodemailer from '@auth/core/providers/nodemailer'; import Credentials from '@auth/core/providers/credentials'; +import Github from '@auth/core/providers/github'; import { env } from '../utils/env.js'; import { prisma } from './_client.js'; import type { PrismaClient, Prisma, User } from '@prisma/client'; @@ -28,7 +29,8 @@ export interface UserAuthPayload { export const authConfig: Omit = { debug: env.isProd ? false : true, basePath: '/api/auth', - providers: [ + trustHost: true, + providers: _.compact([ Credentials({ id: 'account', name: 'Account', @@ -52,28 +54,41 @@ export const authConfig: Omit = { return toAdapterUser(user); }, }), - Nodemailer({ - id: 'email', - name: 'Email', - ...env.auth.email, - async sendVerificationRequest(params) { - const { identifier, url, provider, theme } = params; - const { host } = new URL(url); - const transport = createTransport(provider.server); - const result = await transport.sendMail({ - to: identifier, - from: provider.from, - subject: `Sign in Tianji to ${host}`, - text: `Sign in Tianji to ${host}\n${url}\n\n`, - html: nodemailHtmlBody({ url, host, theme }), - }); - const failed = result.rejected.concat(result.pending).filter(Boolean); - if (failed.length) { - throw new Error(`Email (${failed.join(', ')}) could not be sent`); - } - }, - }), - ], + env.auth.provider.includes('email') && + Nodemailer({ + id: 'email', + name: 'Email', + ...env.auth.email, + async sendVerificationRequest(params) { + const { identifier, url, provider, theme } = params; + const { host } = new URL(url); + const transport = createTransport(provider.server); + const result = await transport.sendMail({ + to: identifier, + from: provider.from, + subject: `Sign in Tianji to ${host}`, + text: `Sign in Tianji to ${host}\n${url}\n\n`, + html: nodemailHtmlBody({ url, host, theme }), + }); + const failed = result.rejected.concat(result.pending).filter(Boolean); + if (failed.length) { + throw new Error(`Email (${failed.join(', ')}) could not be sent`); + } + }, + }), + env.auth.provider.includes('github') && + Github({ + id: 'github', + name: 'Github', + ...env.auth.github, + }), + env.auth.provider.includes('google') && + Github({ + id: 'google', + name: 'Google', + ...env.auth.google, + }), + ]), adapter: TianjiPrismaAdapter(prisma), secret: env.auth.secret, session: { diff --git a/src/server/trpc/routers/global.ts b/src/server/trpc/routers/global.ts index d03b936..7289121 100644 --- a/src/server/trpc/routers/global.ts +++ b/src/server/trpc/routers/global.ts @@ -23,6 +23,7 @@ export const globalRouter = router({ alphaMode: z.boolean(), disableAnonymousTelemetry: z.boolean(), customTrackerScriptName: z.string().optional(), + authProvider: z.array(z.string()), }) ) .query(async ({ input }) => { @@ -34,6 +35,7 @@ export const globalRouter = router({ alphaMode: env.alphaMode, disableAnonymousTelemetry: env.disableAnonymousTelemetry, customTrackerScriptName: env.customTrackerScriptName, + authProvider: env.auth.provider, }; }), }); diff --git a/src/server/utils/env.ts b/src/server/utils/env.ts index 2386cee..ca0db57 100644 --- a/src/server/utils/env.ts +++ b/src/server/utils/env.ts @@ -1,5 +1,6 @@ import { v1 as uuid } from 'uuid'; import md5 from 'md5'; +import _ from 'lodash'; const jwtSecret = !process.env.JWT_SECRET || @@ -13,11 +14,24 @@ export const env = { jwtSecret, port: Number(process.env.PORT || 12345), auth: { + provider: _.compact([ + !!process.env.EMAIL_SERVER && 'email', + !!process.env.AUTH_GITHUB_ID && 'github', + !!process.env.AUTH_GOOGLE_ID && 'google', + ]), secret: process.env.AUTH_SECRET || md5(jwtSecret), email: { server: process.env.EMAIL_SERVER, from: process.env.EMAIL_FROM, }, + github: { + clientId: process.env.AUTH_GITHUB_ID, + clientSecret: process.env.AUTH_GITHUB_SECRET, + }, + google: { + clientId: process.env.AUTH_GOOGLE_ID, + clientSecret: process.env.AUTH_GOOGLE_SECRET, + }, }, allowRegister: checkEnvTrusty(process.env.ALLOW_REGISTER), allowOpenapi: checkEnvTrusty(process.env.ALLOW_OPENAPI ?? 'true'),