feat: add logout and socketio auth
This commit is contained in:
parent
3afac062c4
commit
e9c64c57e7
@ -161,6 +161,7 @@ export async function signOut<R extends boolean = true>(
|
||||
method: 'post',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-Auth-Return-Redirect': '1',
|
||||
},
|
||||
// @ts-expect-error
|
||||
body: new URLSearchParams({
|
||||
|
@ -1,20 +1,54 @@
|
||||
import { useEvent } from '@/hooks/useEvent';
|
||||
import { signIn } from './lib';
|
||||
import { signIn, SignInResponse, signOut } from './lib';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { toast } from 'sonner';
|
||||
import { trpc } from '../trpc';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
|
||||
export function useAuth() {
|
||||
const trpcUtils = trpc.useUtils();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const loginWithPassword = useEvent(
|
||||
async (username: string, password: string) => {
|
||||
const res = await signIn('account', {
|
||||
username,
|
||||
password,
|
||||
redirect: false,
|
||||
});
|
||||
let res: SignInResponse | undefined;
|
||||
try {
|
||||
res = await signIn('account', {
|
||||
username,
|
||||
password,
|
||||
redirect: false,
|
||||
});
|
||||
} catch (err) {
|
||||
toast.error(t('Login failed'));
|
||||
throw err;
|
||||
}
|
||||
|
||||
return res;
|
||||
if (res?.error) {
|
||||
toast.error(t('Login failed, please check your username and password'));
|
||||
throw new Error('Login failed');
|
||||
}
|
||||
|
||||
const userInfo = await trpcUtils.user.info.fetch();
|
||||
if (!userInfo) {
|
||||
toast.error(t('Can not get current user info'));
|
||||
throw new Error('Login failed, ');
|
||||
}
|
||||
|
||||
return userInfo;
|
||||
}
|
||||
);
|
||||
|
||||
const logout = useEvent(async () => {
|
||||
await signOut({
|
||||
redirect: false,
|
||||
});
|
||||
|
||||
useUserStore.setState({ info: null });
|
||||
window.location.href = '/login'; // not good, need to invest to find better way.
|
||||
});
|
||||
|
||||
return {
|
||||
loginWithPassword,
|
||||
logout,
|
||||
};
|
||||
}
|
||||
|
@ -1,7 +1,4 @@
|
||||
import dayjs from 'dayjs';
|
||||
import { useUserStore } from '../../store/user';
|
||||
import { useEvent } from '../../hooks/useEvent';
|
||||
import { clearJWT } from '../authjs';
|
||||
|
||||
/**
|
||||
* Mock
|
||||
@ -10,14 +7,3 @@ import { clearJWT } from '../authjs';
|
||||
export function getUserTimezone(): string {
|
||||
return dayjs.tz.guess() ?? 'utc';
|
||||
}
|
||||
|
||||
export function useLogout() {
|
||||
const logout = useEvent(() => {
|
||||
window.location.href = '/login'; // not good, need to invest to find better way.
|
||||
|
||||
useUserStore.setState({ info: null });
|
||||
clearJWT();
|
||||
});
|
||||
|
||||
return logout;
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { message } from 'antd';
|
||||
import axios from 'axios';
|
||||
import { get } from 'lodash-es';
|
||||
import { getJWT } from './authjs';
|
||||
|
||||
class RequestError extends Error {}
|
||||
|
||||
@ -9,10 +8,6 @@ function createRequest() {
|
||||
const ins = axios.create();
|
||||
|
||||
ins.interceptors.request.use(async (val) => {
|
||||
if (!val.headers.Authorization) {
|
||||
val.headers.Authorization = `Bearer ${getJWT()}`;
|
||||
}
|
||||
|
||||
return val;
|
||||
});
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { io, Socket } from 'socket.io-client';
|
||||
import { getJWT } from './authjs';
|
||||
import type { SubscribeEventMap, SocketEventMap } from '../../server/ws/shared';
|
||||
import { create } from 'zustand';
|
||||
import { useEvent } from '../hooks/useEvent';
|
||||
@ -13,13 +12,9 @@ const useSocketStore = create<{
|
||||
}));
|
||||
|
||||
export function createSocketIOClient(workspaceId: string) {
|
||||
const token = getJWT();
|
||||
const socket = io(`/${workspaceId}`, {
|
||||
transports: ['websocket'],
|
||||
reconnectionDelayMax: 10000,
|
||||
auth: {
|
||||
token,
|
||||
},
|
||||
forceNew: true,
|
||||
});
|
||||
|
||||
|
@ -8,7 +8,6 @@ import {
|
||||
splitLink,
|
||||
TRPCClientErrorLike,
|
||||
} from '@trpc/client';
|
||||
import { getJWT } from './authjs';
|
||||
import { message } from 'antd';
|
||||
import { isDev } from '../utils/env';
|
||||
|
||||
@ -22,13 +21,6 @@ export type AppRouterOutput = inferRouterOutputs<AppRouter>;
|
||||
const url = '/trpc';
|
||||
|
||||
function headers() {
|
||||
const jwt = getJWT();
|
||||
if (jwt) {
|
||||
return {
|
||||
Authorization: `Bearer ${getJWT()}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { createFileRoute, redirect, useNavigate } from '@tanstack/react-router';
|
||||
import { useGlobalConfig } from '@/hooks/useConfig';
|
||||
import { trpc } from '@/api/trpc';
|
||||
import { Form, Typography } from 'antd';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
import { setUserInfo } from '@/store/user';
|
||||
@ -8,7 +7,6 @@ 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')({
|
||||
@ -30,23 +28,11 @@ function LoginComponent() {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const search = Route.useSearch();
|
||||
const trpcUtils = trpc.useUtils();
|
||||
|
||||
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;
|
||||
}
|
||||
const userInfo = await loginWithPassword(values.username, values.password);
|
||||
|
||||
setUserInfo(userInfo);
|
||||
|
||||
|
@ -7,6 +7,7 @@ import { useTranslation } from '@i18next-toolkit/react';
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { useAuth } from '@/api/authjs/useAuth';
|
||||
|
||||
export const Route = createFileRoute('/register')({
|
||||
component: RegisterComponent,
|
||||
@ -16,15 +17,18 @@ function RegisterComponent() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { loginWithPassword } = useAuth();
|
||||
const mutation = trpc.user.register.useMutation();
|
||||
|
||||
const [{ loading }, handleRegister] = useRequest(async (values: any) => {
|
||||
const res = await mutation.mutateAsync({
|
||||
await mutation.mutateAsync({
|
||||
username: values.username,
|
||||
password: values.password,
|
||||
});
|
||||
setJWT(res.token);
|
||||
setUserInfo(res.info);
|
||||
|
||||
const userInfo = await loginWithPassword(values.username, values.password);
|
||||
|
||||
setUserInfo(userInfo);
|
||||
|
||||
navigate({
|
||||
to: '/',
|
||||
|
@ -4,13 +4,13 @@ import { useTranslation } from '@i18next-toolkit/react';
|
||||
import { CommonWrapper } from '@/components/CommonWrapper';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Card, Form, Input, Modal, Popconfirm, Typography } from 'antd';
|
||||
import { useLogout } from '@/api/model/user';
|
||||
import { trpc, defaultSuccessHandler, defaultErrorHandler } from '@/api/trpc';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { useState } from 'react';
|
||||
import { CommonHeader } from '@/components/CommonHeader';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useAuth } from '@/api/authjs/useAuth';
|
||||
|
||||
export const Route = createFileRoute('/settings/profile')({
|
||||
beforeLoad: routeAuthBeforeLoad,
|
||||
@ -27,7 +27,7 @@ function PageComponent() {
|
||||
onError: defaultErrorHandler,
|
||||
});
|
||||
|
||||
const logout = useLogout();
|
||||
const { logout } = useAuth();
|
||||
|
||||
return (
|
||||
<CommonWrapper header={<CommonHeader title={t('Profile')} />}>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { AuthConfig } from '@auth/core';
|
||||
import { Auth, AuthConfig, createActionURL } from '@auth/core';
|
||||
import Nodemailer from '@auth/core/providers/nodemailer';
|
||||
import Credentials from '@auth/core/providers/credentials';
|
||||
import { env } from '../utils/env.js';
|
||||
@ -16,9 +16,18 @@ import { Theme } from '@auth/core/types';
|
||||
import { generateSMTPHTML } from '../utils/smtp.js';
|
||||
import { SYSTEM_ROLES } from '@tianji/shared';
|
||||
import _ from 'lodash';
|
||||
import { IncomingMessage } from 'http';
|
||||
import { type Session } from '@auth/express';
|
||||
|
||||
export interface UserAuthPayload {
|
||||
id: string;
|
||||
username: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
export const authConfig: Omit<AuthConfig, 'raw'> = {
|
||||
debug: true,
|
||||
debug: env.isProd ? false : true,
|
||||
basePath: '/api/auth',
|
||||
providers: [
|
||||
Credentials({
|
||||
id: 'account',
|
||||
@ -86,6 +95,42 @@ export const authConfig: Omit<AuthConfig, 'raw'> = {
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Pure request of auth session
|
||||
*/
|
||||
export async function getAuthSession(
|
||||
req: IncomingMessage,
|
||||
secure = false
|
||||
): Promise<Session> {
|
||||
const protocol = secure ? 'https:' : 'http:';
|
||||
const url = createActionURL(
|
||||
'session',
|
||||
protocol,
|
||||
// @ts-expect-error
|
||||
new Headers(req.headers),
|
||||
process.env,
|
||||
authConfig.basePath
|
||||
);
|
||||
|
||||
const response = await Auth(
|
||||
new Request(url, { headers: { cookie: req.headers.cookie ?? '' } }),
|
||||
authConfig
|
||||
);
|
||||
|
||||
const { status = 200 } = response;
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data || !Object.keys(data).length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (status === 200) {
|
||||
return data;
|
||||
}
|
||||
throw new Error(data.message);
|
||||
}
|
||||
|
||||
function toAdapterUser(
|
||||
user: Pick<
|
||||
User,
|
||||
|
@ -4,6 +4,7 @@ import { jwtVerify } from '../middleware/auth.js';
|
||||
import { socketEventBus } from './shared.js';
|
||||
import { isCuid } from '../utils/common.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
import { getAuthSession, UserAuthPayload } from '../model/auth.js';
|
||||
|
||||
export function initSocketio(httpServer: HTTPServer) {
|
||||
const io = new SocketIOServer(httpServer, {
|
||||
@ -24,27 +25,36 @@ export function initSocketio(httpServer: HTTPServer) {
|
||||
// Auth
|
||||
try {
|
||||
const token = socket.handshake.auth['token'];
|
||||
if (typeof token !== 'string') {
|
||||
throw new Error('Token cannot be empty');
|
||||
let user: UserAuthPayload;
|
||||
if (token) {
|
||||
user = jwtVerify(token);
|
||||
} else {
|
||||
const session = await getAuthSession(
|
||||
socket.request,
|
||||
socket.handshake.secure
|
||||
);
|
||||
if (!session) {
|
||||
throw new Error('Can not get user info.');
|
||||
}
|
||||
|
||||
user = {
|
||||
id: session.user.id,
|
||||
username: session.user.name,
|
||||
role: session.user.role,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const user = jwtVerify(token);
|
||||
logger.info('[Socket] Authenticated via JWT:', user.id, user.username);
|
||||
|
||||
logger.info('[Socket] Authenticated via JWT:', user.username);
|
||||
socket.data.user = user;
|
||||
socket.data.token = token;
|
||||
|
||||
socket.data.user = user;
|
||||
socket.data.token = token;
|
||||
const workspaceId = socket.nsp.name.replace(/^\//, '');
|
||||
socket.data.workspaceId = workspaceId;
|
||||
|
||||
const workspaceId = socket.nsp.name.replace(/^\//, '');
|
||||
socket.data.workspaceId = workspaceId;
|
||||
|
||||
next();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
next(new Error('TokenInvalid'));
|
||||
}
|
||||
next();
|
||||
} catch (err: any) {
|
||||
console.error('[Socket] Authenticated throw error:', err);
|
||||
next(err);
|
||||
}
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user