From 3afac062c417bfeef0536ee49a459de96ac7ae72 Mon Sep 17 00:00:00 2001 From: moonrailgun Date: Wed, 31 Jul 2024 00:40:04 +0800 Subject: [PATCH] feat: add support for legacy traditional login methods --- pnpm-lock.yaml | 20 +- src/client/App.tsx | 2 +- src/client/api/{auth.ts => authjs/index.ts} | 29 +++ src/client/api/authjs/lib.tsx | 185 ++++++++++++++++++ src/client/api/authjs/types.ts | 55 ++++++ src/client/api/authjs/useAuth.ts | 20 ++ src/client/api/model/user.ts | 2 +- src/client/api/request.ts | 2 +- src/client/api/socketio.ts | 2 +- src/client/api/trpc.ts | 13 +- src/client/components/TokenLoginContainer.tsx | 30 ++- src/client/hooks/useOnline.ts | 22 +++ src/client/package.json | 1 + src/client/routes/login.tsx | 31 ++- src/client/routes/register.tsx | 2 +- src/client/store/user.ts | 3 +- src/client/vite.config.ts | 3 + src/server/model/_schema/index.ts | 6 +- src/server/model/auth.ts | 32 ++- src/server/model/user.ts | 13 +- src/server/trpc/routers/user.ts | 7 + src/server/trpc/trpc.ts | 37 +++- src/server/tsconfig.json | 1 - src/server/types/@auth/core/index.d.ts | 18 ++ 24 files changed, 460 insertions(+), 76 deletions(-) rename src/client/api/{auth.ts => authjs/index.ts} (50%) create mode 100644 src/client/api/authjs/lib.tsx create mode 100644 src/client/api/authjs/types.ts create mode 100644 src/client/api/authjs/useAuth.ts create mode 100644 src/client/hooks/useOnline.ts create mode 100644 src/server/types/@auth/core/index.d.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f2fcbf4..b6413d3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -350,6 +350,9 @@ importers: specifier: ^4.4.1 version: 4.4.1(@types/react@18.2.78)(immer@9.0.21)(react@18.2.0) devDependencies: + '@auth/core': + specifier: ^0.34.1 + version: 0.34.1(nodemailer@6.9.8) '@i18next-toolkit/cli': specifier: ^1.2.2 version: 1.2.2(buffer@6.0.3)(typescript@5.5.4) @@ -422,9 +425,6 @@ importers: '@auth/express': specifier: ^0.5.5 version: 0.5.6(express@4.18.2)(nodemailer@6.9.8) - '@auth/prisma-adapter': - specifier: ^2.1.0 - version: 2.4.1(@prisma/client@5.14.0(prisma@5.14.0))(nodemailer@6.9.8) '@paralleldrive/cuid2': specifier: ^2.2.2 version: 2.2.2 @@ -1173,11 +1173,6 @@ packages: peerDependencies: express: ^4.18.2 - '@auth/prisma-adapter@2.4.1': - resolution: {integrity: sha512-VF5IOTHEWHX6WHUxIbsbc12m34cp5T82fzJfi7DmzwBZb89UsoROejV8l0B1dCTZ+pDIS0d4zN9dSa2gIKywNQ==} - peerDependencies: - '@prisma/client': '>=2.26.0 || >=3 || >=4 || >=5' - '@babel/code-frame@7.24.7': resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==} engines: {node: '>=6.9.0'} @@ -13990,15 +13985,6 @@ snapshots: - '@simplewebauthn/server' - nodemailer - '@auth/prisma-adapter@2.4.1(@prisma/client@5.14.0(prisma@5.14.0))(nodemailer@6.9.8)': - dependencies: - '@auth/core': 0.34.1(nodemailer@6.9.8) - '@prisma/client': 5.14.0(prisma@5.14.0) - transitivePeerDependencies: - - '@simplewebauthn/browser' - - '@simplewebauthn/server' - - nodemailer - '@babel/code-frame@7.24.7': dependencies: '@babel/highlight': 7.24.7 diff --git a/src/client/App.tsx b/src/client/App.tsx index d1464b0..cf79c78 100644 --- a/src/client/App.tsx +++ b/src/client/App.tsx @@ -43,7 +43,7 @@ const AppRouter: React.FC = React.memo(() => { - + ); }); diff --git a/src/client/api/auth.ts b/src/client/api/authjs/index.ts similarity index 50% rename from src/client/api/auth.ts rename to src/client/api/authjs/index.ts index 85bdd00..2bd12ce 100644 --- a/src/client/api/auth.ts +++ b/src/client/api/authjs/index.ts @@ -1,15 +1,44 @@ +import axios from 'axios'; + +/** + * @deprecated + */ const TOKEN_STORAGE_KEY = 'jsonwebtoken'; +/** + * @deprecated + */ export function getJWT(): string | null { const token = window.localStorage.getItem(TOKEN_STORAGE_KEY); return token ?? null; } +/** + * @deprecated + */ export function setJWT(jwt: string) { window.localStorage.setItem(TOKEN_STORAGE_KEY, jwt); } +/** + * @deprecated + */ export function clearJWT() { window.localStorage.removeItem(TOKEN_STORAGE_KEY); } + +export async function getSession(): Promise<{ + user: { + email?: string; + }; + expires: string; +} | null> { + const { data } = await axios.get('/api/auth/session'); + + if (!data) { + return null; + } + + return data; +} diff --git a/src/client/api/authjs/lib.tsx b/src/client/api/authjs/lib.tsx new file mode 100644 index 0000000..641604d --- /dev/null +++ b/src/client/api/authjs/lib.tsx @@ -0,0 +1,185 @@ +/** + * This file is fork from next-auth/react + */ + +import type { + ClientSafeProvider, + LiteralUnion, + SignInAuthorizationParams, + SignInOptions, + SignInResponse, + SignOutParams, + SignOutResponse, +} from './types'; +import type { + BuiltInProviderType, + RedirectableProviderType, +} from '@auth/core/providers'; +import { Session } from '@auth/core/types'; +import axios from 'axios'; + +export * from './types'; + +type UpdateSession = (data?: any) => Promise; + +export type SessionContextValue = R extends true + ? + | { update: UpdateSession; data: Session; status: 'authenticated' } + | { update: UpdateSession; data: null; status: 'loading' } + : + | { update: UpdateSession; data: Session; status: 'authenticated' } + | { + update: UpdateSession; + data: null; + status: 'unauthenticated' | 'loading'; + }; + +/** + * Returns the current Cross Site Request Forgery Token (CSRF Token) + * required to make POST requests (e.g. for signing in and signing out). + * You likely only need to use this if you are not using the built-in + * `signIn()` and `signOut()` methods. + * + * [Documentation](https://next-auth.js.org/getting-started/client#getcsrftoken) + */ +export async function getCsrfToken() { + const { data } = await axios.get<{ csrfToken: string }>('/api/auth/csrf'); + + return data.csrfToken; +} + +/** + * It calls `/api/auth/providers` and returns + * a list of the currently configured authentication providers. + * It can be useful if you are creating a dynamic custom sign in page. + * + * [Documentation](https://next-auth.js.org/getting-started/client#getproviders) + */ +export async function getProviders() { + const { data } = await axios.get< + Record, ClientSafeProvider> + >('/api/auth/providers'); + + return data; +} + +/** + * Client-side method to initiate a signin flow + * or send the user to the signin page listing all possible providers. + * Automatically adds the CSRF token to the request. + * + * [Documentation](https://next-auth.js.org/getting-started/client#signin) + */ +export async function signIn< + P extends RedirectableProviderType | undefined = undefined, +>( + provider?: LiteralUnion< + P extends RedirectableProviderType + ? P | BuiltInProviderType + : BuiltInProviderType + >, + options?: SignInOptions, + authorizationParams?: SignInAuthorizationParams +): Promise< + P extends RedirectableProviderType ? SignInResponse | undefined : undefined +> { + const { callbackUrl = window.location.href, redirect = true } = options ?? {}; + + const baseUrl = '/api/auth'; + const providers = await getProviders(); + + if (!providers) { + window.location.href = `${baseUrl}/error`; + return; + } + + if (!provider || !(provider in providers)) { + window.location.href = `${baseUrl}/signin?${new URLSearchParams({ + callbackUrl, + })}`; + return; + } + + const isCredentials = providers[provider].type === 'credentials'; + const isEmail = providers[provider].type === 'email'; + const isSupportingReturn = isCredentials || isEmail; + + const signInUrl = `${baseUrl}/${ + isCredentials ? 'callback' : 'signin' + }/${provider}`; + + const _signInUrl = `${signInUrl}${authorizationParams ? `?${new URLSearchParams(authorizationParams)}` : ''}`; + + const res = await fetch(_signInUrl, { + method: 'post', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'X-Auth-Return-Redirect': '1', + }, + // @ts-expect-error + body: new URLSearchParams({ + ...options, + csrfToken: await getCsrfToken(), + callbackUrl, + json: true, + }), + }); + + const data = await res.json(); + + // TODO: Do not redirect for Credentials and Email providers by default in next major + if (redirect || !isSupportingReturn) { + const url = data.url ?? callbackUrl; + window.location.href = url; + // If url contains a hash, the browser does not reload the page. We reload manually + if (url.includes('#')) window.location.reload(); + return; + } + + const error = new URL(data.url).searchParams.get('error'); + + return { + error, + status: res.status, + ok: res.ok, + url: error ? null : data.url, + } as any; +} + +/** + * Signs the user out, by removing the session cookie. + * Automatically adds the CSRF token to the request. + * + * [Documentation](https://next-auth.js.org/getting-started/client#signout) + */ +export async function signOut( + options?: SignOutParams +): Promise { + const { callbackUrl = window.location.href } = options ?? {}; + const baseUrl = '/api/auth'; + const fetchOptions = { + method: 'post', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + // @ts-expect-error + body: new URLSearchParams({ + csrfToken: await getCsrfToken(), + callbackUrl, + json: true, + }), + }; + const res = await fetch(`${baseUrl}/signout`, fetchOptions); + const data = await res.json(); + + if (options?.redirect ?? true) { + const url = data.url ?? callbackUrl; + window.location.href = url; + // If url contains a hash, the browser does not reload the page. We reload manually + if (url.includes('#')) window.location.reload(); + // @ts-expect-error + return; + } + + return data; +} diff --git a/src/client/api/authjs/types.ts b/src/client/api/authjs/types.ts new file mode 100644 index 0000000..aeb2bdb --- /dev/null +++ b/src/client/api/authjs/types.ts @@ -0,0 +1,55 @@ +import type { Session } from '@auth/core/types'; +import type { BuiltInProviderType, ProviderType } from '@auth/core/providers'; + +/** + * Util type that matches some strings literally, but allows any other string as well. + * @source https://github.com/microsoft/TypeScript/issues/29729#issuecomment-832522611 + */ +export type LiteralUnion = + | T + | (U & Record); + +export interface ClientSafeProvider { + id: LiteralUnion; + name: string; + type: ProviderType; + signinUrl: string; + callbackUrl: string; +} + +export interface SignInOptions extends Record { + /** + * Specify to which URL the user will be redirected after signing in. Defaults to the page URL the sign-in is initiated from. + * + * [Documentation](https://next-auth.js.org/getting-started/client#specifying-a-callbackurl) + */ + callbackUrl?: string; + /** [Documentation](https://next-auth.js.org/getting-started/client#using-the-redirect-false-option) */ + redirect?: boolean; +} + +export interface SignInResponse { + error: string | null; + status: number; + ok: boolean; + url: string | null; +} + +/** Match `inputType` of `new URLSearchParams(inputType)` */ +export type SignInAuthorizationParams = + | string + | string[][] + | Record + | URLSearchParams; + +/** [Documentation](https://next-auth.js.org/getting-started/client#using-the-redirect-false-option-1) */ +export interface SignOutResponse { + url: string; +} + +export interface SignOutParams { + /** [Documentation](https://next-auth.js.org/getting-started/client#specifying-a-callbackurl-1) */ + callbackUrl?: string; + /** [Documentation](https://next-auth.js.org/getting-started/client#using-the-redirect-false-option-1 */ + redirect?: R; +} diff --git a/src/client/api/authjs/useAuth.ts b/src/client/api/authjs/useAuth.ts new file mode 100644 index 0000000..9805baf --- /dev/null +++ b/src/client/api/authjs/useAuth.ts @@ -0,0 +1,20 @@ +import { useEvent } from '@/hooks/useEvent'; +import { signIn } from './lib'; + +export function useAuth() { + const loginWithPassword = useEvent( + async (username: string, password: string) => { + const res = await signIn('account', { + username, + password, + redirect: false, + }); + + return res; + } + ); + + return { + loginWithPassword, + }; +} diff --git a/src/client/api/model/user.ts b/src/client/api/model/user.ts index a5e55d8..825bfa0 100644 --- a/src/client/api/model/user.ts +++ b/src/client/api/model/user.ts @@ -1,7 +1,7 @@ import dayjs from 'dayjs'; import { useUserStore } from '../../store/user'; import { useEvent } from '../../hooks/useEvent'; -import { clearJWT } from '../auth'; +import { clearJWT } from '../authjs'; /** * Mock diff --git a/src/client/api/request.ts b/src/client/api/request.ts index 9d809f8..2a2ac5d 100644 --- a/src/client/api/request.ts +++ b/src/client/api/request.ts @@ -1,7 +1,7 @@ import { message } from 'antd'; import axios from 'axios'; import { get } from 'lodash-es'; -import { getJWT } from './auth'; +import { getJWT } from './authjs'; class RequestError extends Error {} diff --git a/src/client/api/socketio.ts b/src/client/api/socketio.ts index 80b1ebe..2adab1f 100644 --- a/src/client/api/socketio.ts +++ b/src/client/api/socketio.ts @@ -1,5 +1,5 @@ import { io, Socket } from 'socket.io-client'; -import { getJWT } from './auth'; +import { getJWT } from './authjs'; import type { SubscribeEventMap, SocketEventMap } from '../../server/ws/shared'; import { create } from 'zustand'; import { useEvent } from '../hooks/useEvent'; diff --git a/src/client/api/trpc.ts b/src/client/api/trpc.ts index 42fd5e0..56adbc1 100644 --- a/src/client/api/trpc.ts +++ b/src/client/api/trpc.ts @@ -8,7 +8,7 @@ import { splitLink, TRPCClientErrorLike, } from '@trpc/client'; -import { getJWT } from './auth'; +import { getJWT } from './authjs'; import { message } from 'antd'; import { isDev } from '../utils/env'; @@ -22,9 +22,14 @@ export type AppRouterOutput = inferRouterOutputs; const url = '/trpc'; function headers() { - return { - Authorization: `Bearer ${getJWT()}`, - }; + const jwt = getJWT(); + if (jwt) { + return { + Authorization: `Bearer ${getJWT()}`, + }; + } + + return {}; } export const trpcClient = trpc.createClient({ diff --git a/src/client/components/TokenLoginContainer.tsx b/src/client/components/TokenLoginContainer.tsx index 9147075..8862c0b 100644 --- a/src/client/components/TokenLoginContainer.tsx +++ b/src/client/components/TokenLoginContainer.tsx @@ -1,5 +1,4 @@ import React, { useEffect, useState } from 'react'; -import { getJWT, setJWT } from '../api/auth'; import { Loading } from './Loading'; import { trpc } from '../api/trpc'; import { setUserInfo } from '../store/user'; @@ -7,26 +6,19 @@ import { setUserInfo } from '../store/user'; export const TokenLoginContainer: React.FC = React.memo((props) => { const [loading, setLoading] = useState(true); - const mutation = trpc.user.loginWithToken.useMutation(); + const trpcUtils = trpc.useUtils(); useEffect(() => { - const token = getJWT(); - if (token) { - mutation - .mutateAsync({ - token, - }) - .then((res) => { - setJWT(res.token); - setUserInfo(res.info); - }) - .catch((err) => {}) - .finally(() => { - setLoading(false); - }); - } else { - setLoading(false); - } + trpcUtils.user.info + .fetch() + .then((userInfo) => { + if (userInfo) { + setUserInfo(userInfo); + } + }) + .finally(() => { + setLoading(false); + }); }, []); if (loading) { diff --git a/src/client/hooks/useOnline.ts b/src/client/hooks/useOnline.ts new file mode 100644 index 0000000..3721f4b --- /dev/null +++ b/src/client/hooks/useOnline.ts @@ -0,0 +1,22 @@ +import { useEffect, useState } from 'react'; + +export function useOnline() { + const [isOnline, setIsOnline] = useState( + typeof navigator !== 'undefined' ? navigator.onLine : false + ); + + const setOnline = () => setIsOnline(true); + const setOffline = () => setIsOnline(false); + + useEffect(() => { + window.addEventListener('online', setOnline); + window.addEventListener('offline', setOffline); + + return () => { + window.removeEventListener('online', setOnline); + window.removeEventListener('offline', setOffline); + }; + }, []); + + return isOnline; +} diff --git a/src/client/package.json b/src/client/package.json index f8029a6..7e4d69e 100644 --- a/src/client/package.json +++ b/src/client/package.json @@ -98,6 +98,7 @@ "zustand": "^4.4.1" }, "devDependencies": { + "@auth/core": "^0.34.1", "@i18next-toolkit/cli": "^1.2.2", "@tanstack/router-vite-plugin": "^1.20.5", "@types/jsonexport": "^3.0.5", diff --git a/src/client/routes/login.tsx b/src/client/routes/login.tsx index 006d6f1..c447e45 100644 --- a/src/client/routes/login.tsx +++ b/src/client/routes/login.tsx @@ -1,6 +1,4 @@ import { createFileRoute, redirect, useNavigate } from '@tanstack/react-router'; -import { useRequest } from '@/hooks/useRequest'; -import { setJWT } from '@/api/auth'; import { useGlobalConfig } from '@/hooks/useConfig'; import { trpc } from '@/api/trpc'; import { Form, Typography } from 'antd'; @@ -9,6 +7,9 @@ import { setUserInfo } from '@/store/user'; import { z } from 'zod'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; +import { useAuth } from '@/api/authjs/useAuth'; +import { toast } from 'sonner'; +import { useEventWithLoading } from '@/hooks/useEvent'; export const Route = createFileRoute('/login')({ validateSearch: z.object({ @@ -28,17 +29,27 @@ export const Route = createFileRoute('/login')({ function LoginComponent() { const navigate = useNavigate(); const { t } = useTranslation(); - const loginMutation = trpc.user.login.useMutation(); const search = Route.useSearch(); + const trpcUtils = trpc.useUtils(); - const [{ loading }, handleLogin] = useRequest(async (values: any) => { - const res = await loginMutation.mutateAsync({ - username: values.username, - password: values.password, - }); + const { loginWithPassword } = useAuth(); + + const [handleLogin, loading] = useEventWithLoading(async (values: any) => { + const res = await loginWithPassword(values.username, values.password); + + if (res?.error) { + toast.error(t('Login failed, please check your username and password')); + return; + } + + const userInfo = await trpcUtils.user.info.fetch(); + if (!userInfo) { + toast.error(t('Can not get current user info')); + return; + } + + setUserInfo(userInfo); - setJWT(res.token); - setUserInfo(res.info); navigate({ to: search.redirect ?? '/', replace: true, diff --git a/src/client/routes/register.tsx b/src/client/routes/register.tsx index 249701b..758e038 100644 --- a/src/client/routes/register.tsx +++ b/src/client/routes/register.tsx @@ -1,7 +1,7 @@ import { Form, Typography } from 'antd'; import { useRequest } from '../hooks/useRequest'; import { trpc } from '../api/trpc'; -import { setJWT } from '../api/auth'; +import { setJWT } from '../api/authjs'; import { setUserInfo } from '../store/user'; import { useTranslation } from '@i18next-toolkit/react'; import { createFileRoute, useNavigate } from '@tanstack/react-router'; diff --git a/src/client/store/user.ts b/src/client/store/user.ts index a3daea4..e1bd725 100644 --- a/src/client/store/user.ts +++ b/src/client/store/user.ts @@ -2,7 +2,7 @@ import { create } from 'zustand'; import { createSocketIOClient } from '../api/socketio'; import { AppRouterOutput } from '../api/trpc'; -type UserLoginInfo = AppRouterOutput['user']['loginWithToken']['info']; +type UserLoginInfo = NonNullable; interface UserState { info: UserLoginInfo | null; @@ -17,7 +17,6 @@ export function setUserInfo(info: UserLoginInfo) { // Make sure currentWorkspace existed info.currentWorkspace = { ...info.workspaces[0].workspace, - dashboardLayout: null, }; } diff --git a/src/client/vite.config.ts b/src/client/vite.config.ts index bb51736..9184365 100644 --- a/src/client/vite.config.ts +++ b/src/client/vite.config.ts @@ -33,6 +33,9 @@ export default defineConfig({ '/trpc': { target: 'http://localhost:12345', }, + '/api/auth/': { + target: 'http://localhost:12345', + }, '/api/workspace': { target: 'http://localhost:12345', }, diff --git a/src/server/model/_schema/index.ts b/src/server/model/_schema/index.ts index 73ed074..1a6321a 100644 --- a/src/server/model/_schema/index.ts +++ b/src/server/model/_schema/index.ts @@ -28,11 +28,7 @@ export const userInfoSchema = z.object({ createdAt: z.date(), updatedAt: z.date(), deletedAt: z.date().nullable(), - currentWorkspace: workspaceSchema.merge( - z.object({ - dashboardLayout: workspaceDashboardLayoutSchema.nullable(), - }) - ), + currentWorkspace: workspaceSchema, workspaces: z.array( z.object({ role: z.string(), diff --git a/src/server/model/auth.ts b/src/server/model/auth.ts index 339665c..f9af142 100644 --- a/src/server/model/auth.ts +++ b/src/server/model/auth.ts @@ -14,11 +14,14 @@ import { authUser, createUserWithAuthjs } from './user.js'; import { createTransport } from 'nodemailer'; import { Theme } from '@auth/core/types'; import { generateSMTPHTML } from '../utils/smtp.js'; +import { SYSTEM_ROLES } from '@tianji/shared'; +import _ from 'lodash'; export const authConfig: Omit = { debug: true, providers: [ Credentials({ + id: 'account', name: 'Account', credentials: { username: { label: 'Username' }, @@ -41,6 +44,7 @@ export const authConfig: Omit = { }, }), Nodemailer({ + id: 'email', name: 'Email', ...env.auth.email, async sendVerificationRequest(params) { @@ -63,6 +67,23 @@ export const authConfig: Omit = { ], adapter: TianjiPrismaAdapter(prisma), secret: env.auth.secret, + session: { + strategy: 'jwt', + }, + callbacks: { + jwt({ token }) { + return { + ...token, + role: SYSTEM_ROLES.user, + }; + }, + session({ session, token, user }) { + _.set(session, ['user', 'id'], token.sub); + _.set(session, ['user', 'role'], token.role); + + return session; + }, + }, }; function toAdapterUser( @@ -132,9 +153,16 @@ function TianjiPrismaAdapter( where: { sessionToken }, include: { user: true }, }); - if (!userAndSession) return null; + if (!userAndSession) { + return null; + } + const { user, ...session } = userAndSession; - return { user, session } as { + + return { + user: toAdapterUser(user), + session, + } as { user: AdapterUser; session: AdapterSession; }; diff --git a/src/server/model/user.ts b/src/server/model/user.ts index 7b66022..c55c8a9 100644 --- a/src/server/model/user.ts +++ b/src/server/model/user.ts @@ -36,8 +36,6 @@ const createUserSelect = { select: { id: true, name: true, - dashboardOrder: true, - dashboardLayout: true, }, }, workspaces: { @@ -184,6 +182,17 @@ export async function createUserWithAuthjs(data: Omit) { return user; } +export async function getUserInfo(userId: string) { + const user = await prisma.user.findUnique({ + where: { + id: userId, + }, + select: createUserSelect, + }); + + return user; +} + export async function authUser(username: string, password: string) { const user = await prisma.user.findUnique({ where: { diff --git a/src/server/trpc/routers/user.ts b/src/server/trpc/routers/user.ts index 398cce7..f46d206 100644 --- a/src/server/trpc/routers/user.ts +++ b/src/server/trpc/routers/user.ts @@ -7,6 +7,7 @@ import { createAdminUser, createUser, getUserCount, + getUserInfo, } from '../../model/user.js'; import { jwtSign } from '../../middleware/auth.js'; import { TRPCError } from '@trpc/server'; @@ -134,4 +135,10 @@ export const userRouter = router({ return changeUserPassword(userId, oldPassword, newPassword); }), + info: protectProedure + .input(z.void()) + .output(userInfoSchema.nullable()) + .query(async ({ input, ctx }) => { + return getUserInfo(ctx.user.id); + }), }); diff --git a/src/server/trpc/trpc.ts b/src/server/trpc/trpc.ts index 9998017..519e115 100644 --- a/src/server/trpc/trpc.ts +++ b/src/server/trpc/trpc.ts @@ -4,10 +4,12 @@ import { z } from 'zod'; import { jwtVerify } from '../middleware/auth.js'; import { getWorkspaceUser } from '../model/workspace.js'; import { ROLES, SYSTEM_ROLES } from '@tianji/shared'; -import type { IncomingMessage } from 'http'; +import type { Request } from 'express'; import { OpenApiMeta } from 'trpc-openapi'; +import { getSession } from '@auth/express'; +import { authConfig } from '../model/auth.js'; -export function createContext({ req }: { req: IncomingMessage }) { +export async function createContext({ req }: { req: Request }) { const authorization = req.headers['authorization'] ?? ''; const token = authorization.replace('Bearer ', ''); @@ -24,23 +26,40 @@ export const router = t.router; export const publicProcedure = t.procedure; const isUser = middleware(async (opts) => { + // auth with token const token = opts.ctx.token; - if (!token) { - throw new TRPCError({ code: 'UNAUTHORIZED', message: 'NoToken' }); + if (token) { + try { + const user = jwtVerify(token); + + return opts.next({ + ctx: { + user, + }, + }); + } catch (err) { + throw new TRPCError({ code: 'UNAUTHORIZED', message: 'TokenInvalid' }); + } } - try { - const user = jwtVerify(token); + // auth with session + const req = opts.ctx.req; + const session = await getSession(req, authConfig); + if (session) { return opts.next({ ctx: { - user, + user: { + id: session.user?.id, + username: session.user?.name, + role: SYSTEM_ROLES.user, + }, }, }); - } catch (err) { - throw new TRPCError({ code: 'UNAUTHORIZED', message: 'TokenInvalid' }); } + + throw new TRPCError({ code: 'UNAUTHORIZED', message: 'No Token or Session' }); }); export const protectProedure = t.procedure.use(isUser); diff --git a/src/server/tsconfig.json b/src/server/tsconfig.json index 9db9f8e..e602288 100644 --- a/src/server/tsconfig.json +++ b/src/server/tsconfig.json @@ -8,7 +8,6 @@ "moduleResolution": "NodeNext", "sourceMap": true, "outDir": "dist", - "baseUrl": ".", "skipLibCheck": true, "strict": true, "noEmit": false, diff --git a/src/server/types/@auth/core/index.d.ts b/src/server/types/@auth/core/index.d.ts new file mode 100644 index 0000000..39d47c8 --- /dev/null +++ b/src/server/types/@auth/core/index.d.ts @@ -0,0 +1,18 @@ +import type { DefaultSession, User, DefaultJWT } from '@auth/express'; +import type { SYSTEM_ROLES } from '@tianji/shared'; + +declare module '@auth/express' { + interface Session extends DefaultSession { + user: { + id: string; + name: string; + email: string; + image: string; + role: SYSTEM_ROLES; + }; + } + + export interface JWT { + role: SYSTEM_ROLES; + } +}