feat: add support for legacy traditional login methods
This commit is contained in:
parent
06d6ecd2a3
commit
3afac062c4
@ -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
|
||||
|
@ -43,7 +43,7 @@ const AppRouter: React.FC = React.memo(() => {
|
||||
<RouterProvider router={router} context={{ userInfo }} />
|
||||
</TooltipProvider>
|
||||
|
||||
<Toaster />
|
||||
<Toaster position="top-center" />
|
||||
</BrowserRouter>
|
||||
);
|
||||
});
|
||||
|
@ -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;
|
||||
}
|
185
src/client/api/authjs/lib.tsx
Normal file
185
src/client/api/authjs/lib.tsx
Normal 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;
|
||||
}
|
55
src/client/api/authjs/types.ts
Normal file
55
src/client/api/authjs/types.ts
Normal 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;
|
||||
}
|
20
src/client/api/authjs/useAuth.ts
Normal file
20
src/client/api/authjs/useAuth.ts
Normal 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,
|
||||
};
|
||||
}
|
@ -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
|
||||
|
@ -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 {}
|
||||
|
||||
|
@ -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';
|
||||
|
@ -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<AppRouter>;
|
||||
const url = '/trpc';
|
||||
|
||||
function headers() {
|
||||
const jwt = getJWT();
|
||||
if (jwt) {
|
||||
return {
|
||||
Authorization: `Bearer ${getJWT()}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
export const trpcClient = trpc.createClient({
|
||||
|
@ -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.PropsWithChildren> =
|
||||
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,
|
||||
trpcUtils.user.info
|
||||
.fetch()
|
||||
.then((userInfo) => {
|
||||
if (userInfo) {
|
||||
setUserInfo(userInfo);
|
||||
}
|
||||
})
|
||||
.then((res) => {
|
||||
setJWT(res.token);
|
||||
setUserInfo(res.info);
|
||||
})
|
||||
.catch((err) => {})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
|
22
src/client/hooks/useOnline.ts
Normal file
22
src/client/hooks/useOnline.ts
Normal 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;
|
||||
}
|
@ -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",
|
||||
|
@ -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,
|
||||
|
@ -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';
|
||||
|
@ -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<AppRouterOutput['user']['info']>;
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -33,6 +33,9 @@ export default defineConfig({
|
||||
'/trpc': {
|
||||
target: 'http://localhost:12345',
|
||||
},
|
||||
'/api/auth/': {
|
||||
target: 'http://localhost:12345',
|
||||
},
|
||||
'/api/workspace': {
|
||||
target: 'http://localhost:12345',
|
||||
},
|
||||
|
@ -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(),
|
||||
|
@ -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<AuthConfig, 'raw'> = {
|
||||
debug: true,
|
||||
providers: [
|
||||
Credentials({
|
||||
id: 'account',
|
||||
name: 'Account',
|
||||
credentials: {
|
||||
username: { label: 'Username' },
|
||||
@ -41,6 +44,7 @@ export const authConfig: Omit<AuthConfig, 'raw'> = {
|
||||
},
|
||||
}),
|
||||
Nodemailer({
|
||||
id: 'email',
|
||||
name: 'Email',
|
||||
...env.auth.email,
|
||||
async sendVerificationRequest(params) {
|
||||
@ -63,6 +67,23 @@ export const authConfig: Omit<AuthConfig, 'raw'> = {
|
||||
],
|
||||
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;
|
||||
};
|
||||
|
@ -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<AdapterUser, 'id'>) {
|
||||
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: {
|
||||
|
@ -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);
|
||||
}),
|
||||
});
|
||||
|
@ -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,12 +26,10 @@ 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);
|
||||
|
||||
@ -41,6 +41,25 @@ const isUser = middleware(async (opts) => {
|
||||
} catch (err) {
|
||||
throw new TRPCError({ code: 'UNAUTHORIZED', message: 'TokenInvalid' });
|
||||
}
|
||||
}
|
||||
|
||||
// auth with session
|
||||
const req = opts.ctx.req;
|
||||
const session = await getSession(req, authConfig);
|
||||
|
||||
if (session) {
|
||||
return opts.next({
|
||||
ctx: {
|
||||
user: {
|
||||
id: session.user?.id,
|
||||
username: session.user?.name,
|
||||
role: SYSTEM_ROLES.user,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
throw new TRPCError({ code: 'UNAUTHORIZED', message: 'No Token or Session' });
|
||||
});
|
||||
|
||||
export const protectProedure = t.procedure.use(isUser);
|
||||
|
@ -8,7 +8,6 @@
|
||||
"moduleResolution": "NodeNext",
|
||||
"sourceMap": true,
|
||||
"outDir": "dist",
|
||||
"baseUrl": ".",
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": false,
|
||||
|
18
src/server/types/@auth/core/index.d.ts
vendored
Normal file
18
src/server/types/@auth/core/index.d.ts
vendored
Normal 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;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user