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;
+ }
+}