feat: add support for legacy traditional login methods

This commit is contained in:
moonrailgun 2024-07-31 00:40:04 +08:00
parent 06d6ecd2a3
commit 3afac062c4
24 changed files with 460 additions and 76 deletions

View File

@ -350,6 +350,9 @@ importers:
specifier: ^4.4.1 specifier: ^4.4.1
version: 4.4.1(@types/react@18.2.78)(immer@9.0.21)(react@18.2.0) version: 4.4.1(@types/react@18.2.78)(immer@9.0.21)(react@18.2.0)
devDependencies: devDependencies:
'@auth/core':
specifier: ^0.34.1
version: 0.34.1(nodemailer@6.9.8)
'@i18next-toolkit/cli': '@i18next-toolkit/cli':
specifier: ^1.2.2 specifier: ^1.2.2
version: 1.2.2(buffer@6.0.3)(typescript@5.5.4) version: 1.2.2(buffer@6.0.3)(typescript@5.5.4)
@ -422,9 +425,6 @@ importers:
'@auth/express': '@auth/express':
specifier: ^0.5.5 specifier: ^0.5.5
version: 0.5.6(express@4.18.2)(nodemailer@6.9.8) 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': '@paralleldrive/cuid2':
specifier: ^2.2.2 specifier: ^2.2.2
version: 2.2.2 version: 2.2.2
@ -1173,11 +1173,6 @@ packages:
peerDependencies: peerDependencies:
express: ^4.18.2 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': '@babel/code-frame@7.24.7':
resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==} resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@ -13990,15 +13985,6 @@ snapshots:
- '@simplewebauthn/server' - '@simplewebauthn/server'
- nodemailer - 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': '@babel/code-frame@7.24.7':
dependencies: dependencies:
'@babel/highlight': 7.24.7 '@babel/highlight': 7.24.7

View File

@ -43,7 +43,7 @@ const AppRouter: React.FC = React.memo(() => {
<RouterProvider router={router} context={{ userInfo }} /> <RouterProvider router={router} context={{ userInfo }} />
</TooltipProvider> </TooltipProvider>
<Toaster /> <Toaster position="top-center" />
</BrowserRouter> </BrowserRouter>
); );
}); });

View File

@ -1,15 +1,44 @@
import axios from 'axios';
/**
* @deprecated
*/
const TOKEN_STORAGE_KEY = 'jsonwebtoken'; const TOKEN_STORAGE_KEY = 'jsonwebtoken';
/**
* @deprecated
*/
export function getJWT(): string | null { export function getJWT(): string | null {
const token = window.localStorage.getItem(TOKEN_STORAGE_KEY); const token = window.localStorage.getItem(TOKEN_STORAGE_KEY);
return token ?? null; return token ?? null;
} }
/**
* @deprecated
*/
export function setJWT(jwt: string) { export function setJWT(jwt: string) {
window.localStorage.setItem(TOKEN_STORAGE_KEY, jwt); window.localStorage.setItem(TOKEN_STORAGE_KEY, jwt);
} }
/**
* @deprecated
*/
export function clearJWT() { export function clearJWT() {
window.localStorage.removeItem(TOKEN_STORAGE_KEY); 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;
}

View File

@ -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<Session | null>;
export type SessionContextValue<R extends boolean = false> = 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<LiteralUnion<BuiltInProviderType>, 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<R extends boolean = true>(
options?: SignOutParams<R>
): Promise<R extends true ? undefined : SignOutResponse> {
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;
}

View File

@ -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 extends U, U = string> =
| T
| (U & Record<never, never>);
export interface ClientSafeProvider {
id: LiteralUnion<BuiltInProviderType>;
name: string;
type: ProviderType;
signinUrl: string;
callbackUrl: string;
}
export interface SignInOptions extends Record<string, unknown> {
/**
* 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<string, string>
| URLSearchParams;
/** [Documentation](https://next-auth.js.org/getting-started/client#using-the-redirect-false-option-1) */
export interface SignOutResponse {
url: string;
}
export interface SignOutParams<R extends boolean = true> {
/** [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;
}

View File

@ -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,
};
}

View File

@ -1,7 +1,7 @@
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useUserStore } from '../../store/user'; import { useUserStore } from '../../store/user';
import { useEvent } from '../../hooks/useEvent'; import { useEvent } from '../../hooks/useEvent';
import { clearJWT } from '../auth'; import { clearJWT } from '../authjs';
/** /**
* Mock * Mock

View File

@ -1,7 +1,7 @@
import { message } from 'antd'; import { message } from 'antd';
import axios from 'axios'; import axios from 'axios';
import { get } from 'lodash-es'; import { get } from 'lodash-es';
import { getJWT } from './auth'; import { getJWT } from './authjs';
class RequestError extends Error {} class RequestError extends Error {}

View File

@ -1,5 +1,5 @@
import { io, Socket } from 'socket.io-client'; import { io, Socket } from 'socket.io-client';
import { getJWT } from './auth'; import { getJWT } from './authjs';
import type { SubscribeEventMap, SocketEventMap } from '../../server/ws/shared'; import type { SubscribeEventMap, SocketEventMap } from '../../server/ws/shared';
import { create } from 'zustand'; import { create } from 'zustand';
import { useEvent } from '../hooks/useEvent'; import { useEvent } from '../hooks/useEvent';

View File

@ -8,7 +8,7 @@ import {
splitLink, splitLink,
TRPCClientErrorLike, TRPCClientErrorLike,
} from '@trpc/client'; } from '@trpc/client';
import { getJWT } from './auth'; import { getJWT } from './authjs';
import { message } from 'antd'; import { message } from 'antd';
import { isDev } from '../utils/env'; import { isDev } from '../utils/env';
@ -22,9 +22,14 @@ export type AppRouterOutput = inferRouterOutputs<AppRouter>;
const url = '/trpc'; const url = '/trpc';
function headers() { function headers() {
return { const jwt = getJWT();
Authorization: `Bearer ${getJWT()}`, if (jwt) {
}; return {
Authorization: `Bearer ${getJWT()}`,
};
}
return {};
} }
export const trpcClient = trpc.createClient({ export const trpcClient = trpc.createClient({

View File

@ -1,5 +1,4 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { getJWT, setJWT } from '../api/auth';
import { Loading } from './Loading'; import { Loading } from './Loading';
import { trpc } from '../api/trpc'; import { trpc } from '../api/trpc';
import { setUserInfo } from '../store/user'; import { setUserInfo } from '../store/user';
@ -7,26 +6,19 @@ import { setUserInfo } from '../store/user';
export const TokenLoginContainer: React.FC<React.PropsWithChildren> = export const TokenLoginContainer: React.FC<React.PropsWithChildren> =
React.memo((props) => { React.memo((props) => {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const mutation = trpc.user.loginWithToken.useMutation(); const trpcUtils = trpc.useUtils();
useEffect(() => { useEffect(() => {
const token = getJWT(); trpcUtils.user.info
if (token) { .fetch()
mutation .then((userInfo) => {
.mutateAsync({ if (userInfo) {
token, setUserInfo(userInfo);
}) }
.then((res) => { })
setJWT(res.token); .finally(() => {
setUserInfo(res.info); setLoading(false);
}) });
.catch((err) => {})
.finally(() => {
setLoading(false);
});
} else {
setLoading(false);
}
}, []); }, []);
if (loading) { if (loading) {

View File

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

View File

@ -98,6 +98,7 @@
"zustand": "^4.4.1" "zustand": "^4.4.1"
}, },
"devDependencies": { "devDependencies": {
"@auth/core": "^0.34.1",
"@i18next-toolkit/cli": "^1.2.2", "@i18next-toolkit/cli": "^1.2.2",
"@tanstack/router-vite-plugin": "^1.20.5", "@tanstack/router-vite-plugin": "^1.20.5",
"@types/jsonexport": "^3.0.5", "@types/jsonexport": "^3.0.5",

View File

@ -1,6 +1,4 @@
import { createFileRoute, redirect, useNavigate } from '@tanstack/react-router'; import { createFileRoute, redirect, useNavigate } from '@tanstack/react-router';
import { useRequest } from '@/hooks/useRequest';
import { setJWT } from '@/api/auth';
import { useGlobalConfig } from '@/hooks/useConfig'; import { useGlobalConfig } from '@/hooks/useConfig';
import { trpc } from '@/api/trpc'; import { trpc } from '@/api/trpc';
import { Form, Typography } from 'antd'; import { Form, Typography } from 'antd';
@ -9,6 +7,9 @@ import { setUserInfo } from '@/store/user';
import { z } from 'zod'; import { z } from 'zod';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; 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')({ export const Route = createFileRoute('/login')({
validateSearch: z.object({ validateSearch: z.object({
@ -28,17 +29,27 @@ export const Route = createFileRoute('/login')({
function LoginComponent() { function LoginComponent() {
const navigate = useNavigate(); const navigate = useNavigate();
const { t } = useTranslation(); const { t } = useTranslation();
const loginMutation = trpc.user.login.useMutation();
const search = Route.useSearch(); const search = Route.useSearch();
const trpcUtils = trpc.useUtils();
const [{ loading }, handleLogin] = useRequest(async (values: any) => { const { loginWithPassword } = useAuth();
const res = await loginMutation.mutateAsync({
username: values.username, const [handleLogin, loading] = useEventWithLoading(async (values: any) => {
password: values.password, 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({ navigate({
to: search.redirect ?? '/', to: search.redirect ?? '/',
replace: true, replace: true,

View File

@ -1,7 +1,7 @@
import { Form, Typography } from 'antd'; import { Form, Typography } from 'antd';
import { useRequest } from '../hooks/useRequest'; import { useRequest } from '../hooks/useRequest';
import { trpc } from '../api/trpc'; import { trpc } from '../api/trpc';
import { setJWT } from '../api/auth'; import { setJWT } from '../api/authjs';
import { setUserInfo } from '../store/user'; import { setUserInfo } from '../store/user';
import { useTranslation } from '@i18next-toolkit/react'; import { useTranslation } from '@i18next-toolkit/react';
import { createFileRoute, useNavigate } from '@tanstack/react-router'; import { createFileRoute, useNavigate } from '@tanstack/react-router';

View File

@ -2,7 +2,7 @@ import { create } from 'zustand';
import { createSocketIOClient } from '../api/socketio'; import { createSocketIOClient } from '../api/socketio';
import { AppRouterOutput } from '../api/trpc'; import { AppRouterOutput } from '../api/trpc';
type UserLoginInfo = AppRouterOutput['user']['loginWithToken']['info']; type UserLoginInfo = NonNullable<AppRouterOutput['user']['info']>;
interface UserState { interface UserState {
info: UserLoginInfo | null; info: UserLoginInfo | null;
@ -17,7 +17,6 @@ export function setUserInfo(info: UserLoginInfo) {
// Make sure currentWorkspace existed // Make sure currentWorkspace existed
info.currentWorkspace = { info.currentWorkspace = {
...info.workspaces[0].workspace, ...info.workspaces[0].workspace,
dashboardLayout: null,
}; };
} }

View File

@ -33,6 +33,9 @@ export default defineConfig({
'/trpc': { '/trpc': {
target: 'http://localhost:12345', target: 'http://localhost:12345',
}, },
'/api/auth/': {
target: 'http://localhost:12345',
},
'/api/workspace': { '/api/workspace': {
target: 'http://localhost:12345', target: 'http://localhost:12345',
}, },

View File

@ -28,11 +28,7 @@ export const userInfoSchema = z.object({
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
deletedAt: z.date().nullable(), deletedAt: z.date().nullable(),
currentWorkspace: workspaceSchema.merge( currentWorkspace: workspaceSchema,
z.object({
dashboardLayout: workspaceDashboardLayoutSchema.nullable(),
})
),
workspaces: z.array( workspaces: z.array(
z.object({ z.object({
role: z.string(), role: z.string(),

View File

@ -14,11 +14,14 @@ import { authUser, createUserWithAuthjs } from './user.js';
import { createTransport } from 'nodemailer'; import { createTransport } from 'nodemailer';
import { Theme } from '@auth/core/types'; import { Theme } from '@auth/core/types';
import { generateSMTPHTML } from '../utils/smtp.js'; import { generateSMTPHTML } from '../utils/smtp.js';
import { SYSTEM_ROLES } from '@tianji/shared';
import _ from 'lodash';
export const authConfig: Omit<AuthConfig, 'raw'> = { export const authConfig: Omit<AuthConfig, 'raw'> = {
debug: true, debug: true,
providers: [ providers: [
Credentials({ Credentials({
id: 'account',
name: 'Account', name: 'Account',
credentials: { credentials: {
username: { label: 'Username' }, username: { label: 'Username' },
@ -41,6 +44,7 @@ export const authConfig: Omit<AuthConfig, 'raw'> = {
}, },
}), }),
Nodemailer({ Nodemailer({
id: 'email',
name: 'Email', name: 'Email',
...env.auth.email, ...env.auth.email,
async sendVerificationRequest(params) { async sendVerificationRequest(params) {
@ -63,6 +67,23 @@ export const authConfig: Omit<AuthConfig, 'raw'> = {
], ],
adapter: TianjiPrismaAdapter(prisma), adapter: TianjiPrismaAdapter(prisma),
secret: env.auth.secret, 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( function toAdapterUser(
@ -132,9 +153,16 @@ function TianjiPrismaAdapter(
where: { sessionToken }, where: { sessionToken },
include: { user: true }, include: { user: true },
}); });
if (!userAndSession) return null; if (!userAndSession) {
return null;
}
const { user, ...session } = userAndSession; const { user, ...session } = userAndSession;
return { user, session } as {
return {
user: toAdapterUser(user),
session,
} as {
user: AdapterUser; user: AdapterUser;
session: AdapterSession; session: AdapterSession;
}; };

View File

@ -36,8 +36,6 @@ const createUserSelect = {
select: { select: {
id: true, id: true,
name: true, name: true,
dashboardOrder: true,
dashboardLayout: true,
}, },
}, },
workspaces: { workspaces: {
@ -184,6 +182,17 @@ export async function createUserWithAuthjs(data: Omit<AdapterUser, 'id'>) {
return user; 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) { export async function authUser(username: string, password: string) {
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({
where: { where: {

View File

@ -7,6 +7,7 @@ import {
createAdminUser, createAdminUser,
createUser, createUser,
getUserCount, getUserCount,
getUserInfo,
} from '../../model/user.js'; } from '../../model/user.js';
import { jwtSign } from '../../middleware/auth.js'; import { jwtSign } from '../../middleware/auth.js';
import { TRPCError } from '@trpc/server'; import { TRPCError } from '@trpc/server';
@ -134,4 +135,10 @@ export const userRouter = router({
return changeUserPassword(userId, oldPassword, newPassword); return changeUserPassword(userId, oldPassword, newPassword);
}), }),
info: protectProedure
.input(z.void())
.output(userInfoSchema.nullable())
.query(async ({ input, ctx }) => {
return getUserInfo(ctx.user.id);
}),
}); });

View File

@ -4,10 +4,12 @@ import { z } from 'zod';
import { jwtVerify } from '../middleware/auth.js'; import { jwtVerify } from '../middleware/auth.js';
import { getWorkspaceUser } from '../model/workspace.js'; import { getWorkspaceUser } from '../model/workspace.js';
import { ROLES, SYSTEM_ROLES } from '@tianji/shared'; import { ROLES, SYSTEM_ROLES } from '@tianji/shared';
import type { IncomingMessage } from 'http'; import type { Request } from 'express';
import { OpenApiMeta } from 'trpc-openapi'; 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 authorization = req.headers['authorization'] ?? '';
const token = authorization.replace('Bearer ', ''); const token = authorization.replace('Bearer ', '');
@ -24,23 +26,40 @@ export const router = t.router;
export const publicProcedure = t.procedure; export const publicProcedure = t.procedure;
const isUser = middleware(async (opts) => { const isUser = middleware(async (opts) => {
// auth with token
const token = opts.ctx.token; const token = opts.ctx.token;
if (!token) { if (token) {
throw new TRPCError({ code: 'UNAUTHORIZED', message: 'NoToken' }); try {
const user = jwtVerify(token);
return opts.next({
ctx: {
user,
},
});
} catch (err) {
throw new TRPCError({ code: 'UNAUTHORIZED', message: 'TokenInvalid' });
}
} }
try { // auth with session
const user = jwtVerify(token); const req = opts.ctx.req;
const session = await getSession(req, authConfig);
if (session) {
return opts.next({ return opts.next({
ctx: { 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); export const protectProedure = t.procedure.use(isUser);

View File

@ -8,7 +8,6 @@
"moduleResolution": "NodeNext", "moduleResolution": "NodeNext",
"sourceMap": true, "sourceMap": true,
"outDir": "dist", "outDir": "dist",
"baseUrl": ".",
"skipLibCheck": true, "skipLibCheck": true,
"strict": true, "strict": true,
"noEmit": false, "noEmit": false,

18
src/server/types/@auth/core/index.d.ts vendored Normal file
View File

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