feat: user login, loginWithToken and register
This commit is contained in:
parent
08ef1fea91
commit
ef1801f531
@ -47,7 +47,8 @@
|
|||||||
"typescript": "^4.9.5",
|
"typescript": "^4.9.5",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
"vite-express": "^0.10.0",
|
"vite-express": "^0.10.0",
|
||||||
"yup": "^1.2.0"
|
"yup": "^1.2.0",
|
||||||
|
"zustand": "^4.4.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bcryptjs": "^2.4.3",
|
"@types/bcryptjs": "^2.4.3",
|
||||||
|
@ -106,6 +106,9 @@ dependencies:
|
|||||||
yup:
|
yup:
|
||||||
specifier: ^1.2.0
|
specifier: ^1.2.0
|
||||||
version: 1.2.0
|
version: 1.2.0
|
||||||
|
zustand:
|
||||||
|
specifier: ^4.4.1
|
||||||
|
version: 4.4.1(@types/react@18.2.21)(react@18.2.0)
|
||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@types/bcryptjs':
|
'@types/bcryptjs':
|
||||||
@ -1960,7 +1963,6 @@ packages:
|
|||||||
|
|
||||||
/@types/prop-types@15.7.5:
|
/@types/prop-types@15.7.5:
|
||||||
resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==}
|
resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@types/qs@6.9.8:
|
/@types/qs@6.9.8:
|
||||||
resolution: {integrity: sha512-u95svzDlTysU5xecFNTgfFG5RUWu1A9P0VzgpcIiGZA9iraHOdSzcxMxQ55DyeRaGCSxQi7LxXDI4rzq/MYfdg==}
|
resolution: {integrity: sha512-u95svzDlTysU5xecFNTgfFG5RUWu1A9P0VzgpcIiGZA9iraHOdSzcxMxQ55DyeRaGCSxQi7LxXDI4rzq/MYfdg==}
|
||||||
@ -1982,7 +1984,6 @@ packages:
|
|||||||
'@types/prop-types': 15.7.5
|
'@types/prop-types': 15.7.5
|
||||||
'@types/scheduler': 0.16.3
|
'@types/scheduler': 0.16.3
|
||||||
csstype: 3.1.2
|
csstype: 3.1.2
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@types/request-ip@0.0.38:
|
/@types/request-ip@0.0.38:
|
||||||
resolution: {integrity: sha512-1yeq8UuK/tUBqLXRY24gjeFvrSNaGNcOcZLQjHlnuw8iu+qE/vTQ64TUcLWorr607NKLfFakdoYEXXHXrLLKCw==}
|
resolution: {integrity: sha512-1yeq8UuK/tUBqLXRY24gjeFvrSNaGNcOcZLQjHlnuw8iu+qE/vTQ64TUcLWorr607NKLfFakdoYEXXHXrLLKCw==}
|
||||||
@ -1992,7 +1993,6 @@ packages:
|
|||||||
|
|
||||||
/@types/scheduler@0.16.3:
|
/@types/scheduler@0.16.3:
|
||||||
resolution: {integrity: sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==}
|
resolution: {integrity: sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@types/send@0.17.1:
|
/@types/send@0.17.1:
|
||||||
resolution: {integrity: sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==}
|
resolution: {integrity: sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==}
|
||||||
@ -5856,6 +5856,14 @@ packages:
|
|||||||
punycode: 2.3.0
|
punycode: 2.3.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/use-sync-external-store@1.2.0(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
|
dependencies:
|
||||||
|
react: 18.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/util-deprecate@1.0.2:
|
/util-deprecate@1.0.2:
|
||||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
dev: true
|
dev: true
|
||||||
@ -6034,3 +6042,23 @@ packages:
|
|||||||
toposort: 2.0.2
|
toposort: 2.0.2
|
||||||
type-fest: 2.19.0
|
type-fest: 2.19.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/zustand@4.4.1(@types/react@18.2.21)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-QCPfstAS4EBiTQzlaGP1gmorkh/UL1Leaj2tdj+zZCZ/9bm0WS7sI2wnfD5lpOszFqWJ1DcPnGoY8RDL61uokw==}
|
||||||
|
engines: {node: '>=12.7.0'}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '>=16.8'
|
||||||
|
immer: '>=9.0'
|
||||||
|
react: '>=16.8'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
immer:
|
||||||
|
optional: true
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@types/react': 18.2.21
|
||||||
|
react: 18.2.0
|
||||||
|
use-sync-external-store: 1.2.0(react@18.2.0)
|
||||||
|
dev: false
|
||||||
|
@ -6,25 +6,34 @@ import { Monitor } from './pages/Monitor';
|
|||||||
import { Website } from './pages/Website';
|
import { Website } from './pages/Website';
|
||||||
import { Settings } from './pages/Settings';
|
import { Settings } from './pages/Settings';
|
||||||
import { Servers } from './pages/Servers';
|
import { Servers } from './pages/Servers';
|
||||||
|
import { useUserStore } from './store/user';
|
||||||
|
import { Register } from './pages/Register';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const { info } = useUserStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="App">
|
<div className="App">
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route element={<Layout />}>
|
{info && (
|
||||||
<Route path="/dashboard" element={<Dashboard />} />
|
<Route element={<Layout />}>
|
||||||
<Route path="/monitor" element={<Monitor />} />
|
<Route path="/dashboard" element={<Dashboard />} />
|
||||||
<Route path="/website" element={<Website />} />
|
<Route path="/monitor" element={<Monitor />} />
|
||||||
<Route path="/servers" element={<Servers />} />
|
<Route path="/website" element={<Website />} />
|
||||||
<Route path="/settings" element={<Settings />} />
|
<Route path="/servers" element={<Servers />} />
|
||||||
</Route>
|
<Route path="/settings" element={<Settings />} />
|
||||||
|
</Route>
|
||||||
|
)}
|
||||||
|
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
|
<Route path="/register" element={<Register />} />
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="*"
|
path="*"
|
||||||
element={<Navigate to="/dashboard" replace={true} />}
|
element={
|
||||||
|
<Navigate to={info ? '/dashboard' : '/login'} replace={true} />
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
11
src/client/api/auth.ts
Normal file
11
src/client/api/auth.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
const TOKEN_STORAGE_KEY = 'jsonwebtoken';
|
||||||
|
|
||||||
|
export function getJWT(): string | null {
|
||||||
|
const token = window.localStorage.getItem(TOKEN_STORAGE_KEY);
|
||||||
|
|
||||||
|
return token ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setJWT(jwt: string) {
|
||||||
|
window.localStorage.setItem(TOKEN_STORAGE_KEY, jwt);
|
||||||
|
}
|
5
src/client/api/model/index.ts
Normal file
5
src/client/api/model/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import * as user from './user';
|
||||||
|
|
||||||
|
export const model = {
|
||||||
|
user,
|
||||||
|
};
|
32
src/client/api/model/user.ts
Normal file
32
src/client/api/model/user.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { setUserInfo, UserLoginInfo } from '../../store/user';
|
||||||
|
import { getJWT, setJWT } from '../auth';
|
||||||
|
import { request } from '../request';
|
||||||
|
|
||||||
|
export async function login(username: string, password: string) {
|
||||||
|
const { data } = await request.post('/api/user/login', {
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
|
||||||
|
setJWT(data.token);
|
||||||
|
setUserInfo(data.info as UserLoginInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loginWithToken() {
|
||||||
|
const { data } = await request.post('/api/user/loginWithToken', {
|
||||||
|
token: getJWT(),
|
||||||
|
});
|
||||||
|
|
||||||
|
setJWT(data.token);
|
||||||
|
setUserInfo(data.info as UserLoginInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function register(username: string, password: string) {
|
||||||
|
const { data } = await request.post('/api/user/register', {
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
|
||||||
|
setJWT(data.token);
|
||||||
|
setUserInfo(data.info as UserLoginInfo);
|
||||||
|
}
|
76
src/client/api/request.ts
Normal file
76
src/client/api/request.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { message } from 'antd';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { get } from 'lodash-es';
|
||||||
|
import { getJWT } from './auth';
|
||||||
|
|
||||||
|
class RequestError extends Error {}
|
||||||
|
|
||||||
|
function createRequest() {
|
||||||
|
const ins = axios.create();
|
||||||
|
|
||||||
|
ins.interceptors.request.use(async (val) => {
|
||||||
|
if (
|
||||||
|
['post', 'get'].includes(String(val.method).toLowerCase()) &&
|
||||||
|
!val.headers.Authorization
|
||||||
|
) {
|
||||||
|
val.headers.Authorization = `Bearer ${getJWT()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return val;
|
||||||
|
});
|
||||||
|
|
||||||
|
ins.interceptors.response.use(
|
||||||
|
(val) => {
|
||||||
|
return val;
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
console.log(err);
|
||||||
|
const responseData = get(err, 'response.data') ?? {};
|
||||||
|
let errorMsg: string = responseData.message;
|
||||||
|
const code: number = responseData.code;
|
||||||
|
|
||||||
|
const statusCode = get(err, 'response.header.code');
|
||||||
|
if (
|
||||||
|
statusCode === 401 // Unauthorized (jwt expired)
|
||||||
|
) {
|
||||||
|
backToLoginPage();
|
||||||
|
|
||||||
|
return { data: { result: false, msg: errorMsg, code } };
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new RequestError(errorMsg ?? err.message);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return ins;
|
||||||
|
}
|
||||||
|
|
||||||
|
const backToLoginPage = (() => {
|
||||||
|
let timer: number;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (timer) {
|
||||||
|
// Skip if existed
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
window.location.pathname.startsWith('/login') ||
|
||||||
|
window.location.pathname.startsWith('/register')
|
||||||
|
) {
|
||||||
|
// Skip login page
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.warning(
|
||||||
|
'The account authorization has expired. It will automatically jump to the login page in 2 seconds.'
|
||||||
|
);
|
||||||
|
|
||||||
|
timer = window.setTimeout(() => {
|
||||||
|
window.clearTimeout(timer);
|
||||||
|
window.location.href = '/login';
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
export const request = createRequest();
|
28
src/client/hooks/useRequest.ts
Normal file
28
src/client/hooks/useRequest.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { message } from 'antd';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useEvent } from './useEvent';
|
||||||
|
|
||||||
|
export function useRequest<T, P>(queryFn: (...args: P[]) => Promise<T>) {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [data, setData] = useState<T | undefined>(undefined);
|
||||||
|
|
||||||
|
const run = useEvent(async (...args: P[]) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const res = await queryFn(...args);
|
||||||
|
setData(res);
|
||||||
|
} catch (err: any) {
|
||||||
|
message.error(err.message ?? String(err));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
loading,
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
run,
|
||||||
|
] as const;
|
||||||
|
}
|
@ -1,21 +1,33 @@
|
|||||||
import { Button, Form, Input, Typography } from 'antd';
|
import { Button, Form, Input, Typography } from 'antd';
|
||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useEvent } from '../hooks/useEvent';
|
import { model } from '../api/model';
|
||||||
import axios from 'axios';
|
import { useNavigate } from 'react-router';
|
||||||
|
import { loginWithToken } from '../api/model/user';
|
||||||
|
import { getJWT } from '../api/auth';
|
||||||
|
import { useRequest } from '../hooks/useRequest';
|
||||||
|
|
||||||
export const Login: React.FC = React.memo(() => {
|
export const Login: React.FC = React.memo(() => {
|
||||||
const handleLogin = useEvent(async (values: any) => {
|
const navigate = useNavigate();
|
||||||
await axios.post('/api/user/login', {
|
|
||||||
username: values.username,
|
const [{ loading }, handleLogin] = useRequest(async (values: any) => {
|
||||||
password: values.password,
|
await model.user.login(values.username, values.password);
|
||||||
});
|
navigate('/dashboard');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const token = getJWT();
|
||||||
|
if (token) {
|
||||||
|
loginWithToken().then(() => {
|
||||||
|
navigate('/dashboard');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full flex justify-center items-center">
|
<div className="w-full h-full flex justify-center items-center">
|
||||||
<div className="w-80 -translate-y-1/4">
|
<div className="w-80 -translate-y-1/4">
|
||||||
<Typography.Title level={2}>Tianji</Typography.Title>
|
<Typography.Title level={2}>Tianji</Typography.Title>
|
||||||
<Form layout="vertical" onFinish={handleLogin}>
|
<Form layout="vertical" disabled={loading} onFinish={handleLogin}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="Username"
|
label="Username"
|
||||||
name="username"
|
name="username"
|
||||||
@ -28,13 +40,31 @@ export const Login: React.FC = React.memo(() => {
|
|||||||
name="password"
|
name="password"
|
||||||
rules={[{ required: true }]}
|
rules={[{ required: true }]}
|
||||||
>
|
>
|
||||||
<Input size="large" />
|
<Input.Password size="large" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Button type="primary" size="large" htmlType="submit" block={true}>
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
htmlType="submit"
|
||||||
|
block={true}
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
Login
|
Login
|
||||||
</Button>
|
</Button>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item>
|
||||||
|
<Button
|
||||||
|
size="large"
|
||||||
|
htmlType="button"
|
||||||
|
block={true}
|
||||||
|
onClick={() => {
|
||||||
|
navigate('/register');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Register
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
50
src/client/pages/Register.tsx
Normal file
50
src/client/pages/Register.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { Button, Form, Input, Typography } from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
import { model } from '../api/model';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
|
import { useRequest } from '../hooks/useRequest';
|
||||||
|
|
||||||
|
export const Register: React.FC = React.memo(() => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const [{ loading }, handleRegister] = useRequest(async (values: any) => {
|
||||||
|
await model.user.register(values.username, values.password);
|
||||||
|
navigate('/dashboard');
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full flex justify-center items-center">
|
||||||
|
<div className="w-80 -translate-y-1/4">
|
||||||
|
<Typography.Title level={2}>Register Account</Typography.Title>
|
||||||
|
<Form layout="vertical" disabled={loading} onFinish={handleRegister}>
|
||||||
|
<Form.Item
|
||||||
|
label="Username"
|
||||||
|
name="username"
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
>
|
||||||
|
<Input size="large" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="Password"
|
||||||
|
name="password"
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
>
|
||||||
|
<Input.Password size="large" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
htmlType="submit"
|
||||||
|
block={true}
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
Register
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
Register.displayName = 'Register';
|
32
src/client/store/user.ts
Normal file
32
src/client/store/user.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
export interface UserLoginInfo {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
role: string;
|
||||||
|
currentWorkspace: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
workspaces: {
|
||||||
|
role: string;
|
||||||
|
workspace: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserState {
|
||||||
|
info: UserLoginInfo | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUserStore = create<UserState>(() => ({
|
||||||
|
info: null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export function setUserInfo(info: UserInfo) {
|
||||||
|
useUserStore.setState({
|
||||||
|
info,
|
||||||
|
});
|
||||||
|
}
|
@ -22,8 +22,8 @@ app.use('/api/user', userRouter);
|
|||||||
app.use('/api/website', websiteRouter);
|
app.use('/api/website', websiteRouter);
|
||||||
|
|
||||||
app.use((err: any, req: any, res: any, next: any) => {
|
app.use((err: any, req: any, res: any, next: any) => {
|
||||||
res.status(500);
|
console.error(err);
|
||||||
res.json({ error: err.message });
|
res.status(500).json({ message: err.message });
|
||||||
});
|
});
|
||||||
|
|
||||||
ViteExpress.listen(app, port, () => {
|
ViteExpress.listen(app, port, () => {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { authUser, findUser } from '../model/user';
|
import { findUser } from '../model/user';
|
||||||
import passport from 'passport';
|
import passport from 'passport';
|
||||||
import { Handler } from 'express';
|
import { Handler } from 'express';
|
||||||
import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt';
|
import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt';
|
||||||
@ -9,6 +9,12 @@ export const jwtSecret = process.env.JWT_SECRET || nanoid();
|
|||||||
export const jwtIssuer = process.env.JWT_ISSUER || 'tianji.msgbyte.com';
|
export const jwtIssuer = process.env.JWT_ISSUER || 'tianji.msgbyte.com';
|
||||||
export const jwtAudience = process.env.JWT_AUDIENCE || 'msgbyte.com';
|
export const jwtAudience = process.env.JWT_AUDIENCE || 'msgbyte.com';
|
||||||
|
|
||||||
|
interface JWTPayload {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
role: string;
|
||||||
|
}
|
||||||
|
|
||||||
passport.use(
|
passport.use(
|
||||||
new JwtStrategy(
|
new JwtStrategy(
|
||||||
{
|
{
|
||||||
@ -18,7 +24,7 @@ passport.use(
|
|||||||
audience: jwtAudience,
|
audience: jwtAudience,
|
||||||
},
|
},
|
||||||
function (jwt_payload, done) {
|
function (jwt_payload, done) {
|
||||||
findUser(jwt_payload.sub)
|
findUser(jwt_payload.id)
|
||||||
.then((user) => {
|
.then((user) => {
|
||||||
if (user) {
|
if (user) {
|
||||||
done(null, user);
|
done(null, user);
|
||||||
@ -41,16 +47,33 @@ passport.deserializeUser(function (user: any, cb) {
|
|||||||
cb(null, user);
|
cb(null, user);
|
||||||
});
|
});
|
||||||
|
|
||||||
export function jwtSign(payload: {}): string {
|
export function jwtSign(payload: JWTPayload): string {
|
||||||
const token = jwt.sign(payload, jwtSecret, {
|
const token = jwt.sign(
|
||||||
issuer: jwtIssuer,
|
{
|
||||||
audience: jwtAudience,
|
id: payload.id,
|
||||||
expiresIn: '30d',
|
username: payload.username,
|
||||||
});
|
role: payload.role,
|
||||||
|
},
|
||||||
|
jwtSecret,
|
||||||
|
{
|
||||||
|
issuer: jwtIssuer,
|
||||||
|
audience: jwtAudience,
|
||||||
|
expiresIn: '30d',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function jwtVerify(token: string): JWTPayload {
|
||||||
|
const payload = jwt.verify(token, jwtSecret, {
|
||||||
|
issuer: jwtIssuer,
|
||||||
|
audience: jwtAudience,
|
||||||
|
});
|
||||||
|
|
||||||
|
return payload as JWTPayload;
|
||||||
|
}
|
||||||
|
|
||||||
export function auth(): Handler {
|
export function auth(): Handler {
|
||||||
return passport.authenticate('jwt', {
|
return passport.authenticate('jwt', {
|
||||||
session: false,
|
session: false,
|
||||||
|
@ -18,4 +18,4 @@ export function validate(...validator: ValidationChain[]): Handler {
|
|||||||
return compose([...validator, handler as any]);
|
return compose([...validator, handler as any]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { body, query, param } from 'express-validator';
|
export { body, query, param, header } from 'express-validator';
|
||||||
|
@ -1,11 +1,48 @@
|
|||||||
import { prisma } from './_client';
|
import { prisma } from './_client';
|
||||||
import bcryptjs from 'bcryptjs';
|
import bcryptjs from 'bcryptjs';
|
||||||
import { ROLES } from '../utils/const';
|
import { ROLES } from '../utils/const';
|
||||||
|
import { jwtVerify } from '../middleware/auth';
|
||||||
|
|
||||||
async function hashPassword(password: string) {
|
async function hashPassword(password: string) {
|
||||||
return await bcryptjs.hash(password, 10);
|
return await bcryptjs.hash(password, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function comparePassword(password: string, hash: string): Promise<boolean> {
|
||||||
|
return bcryptjs.compare(password, hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUserCount(): Promise<number> {
|
||||||
|
const count = await prisma.user.count();
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
const createUserSelect = {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
role: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
deletedAt: true,
|
||||||
|
currentWorkspace: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
workspaces: {
|
||||||
|
select: {
|
||||||
|
role: true,
|
||||||
|
workspace: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create User
|
* Create User
|
||||||
*/
|
*/
|
||||||
@ -13,7 +50,9 @@ export async function createAdminUser(username: string, password: string) {
|
|||||||
const count = await prisma.user.count();
|
const count = await prisma.user.count();
|
||||||
|
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
throw new Error('Create Admin User Just Only allow in non people exist');
|
throw new Error(
|
||||||
|
'Create Admin User Just Only allow in non people exist, you can Grant Privilege with admin user'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await prisma.user.create({
|
const user = await prisma.user.create({
|
||||||
@ -34,9 +73,7 @@ export async function createAdminUser(username: string, password: string) {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
include: {
|
select: createUserSelect,
|
||||||
workspaces: true,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (user.workspaces[0]) {
|
if (user.workspaces[0]) {
|
||||||
@ -45,10 +82,12 @@ export async function createAdminUser(username: string, password: string) {
|
|||||||
id: user.id,
|
id: user.id,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
currentWorkspaceId: user.workspaces[0].workspaceId,
|
currentWorkspaceId: user.workspaces[0].workspace.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createUser(username: string, password: string) {
|
export async function createUser(username: string, password: string) {
|
||||||
@ -80,9 +119,7 @@ export async function createUser(username: string, password: string) {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
include: {
|
select: createUserSelect,
|
||||||
workspaces: true,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (user.workspaces[0]) {
|
if (user.workspaces[0]) {
|
||||||
@ -91,26 +128,46 @@ export async function createUser(username: string, password: string) {
|
|||||||
id: user.id,
|
id: user.id,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
currentWorkspaceId: user.workspaces[0].workspaceId,
|
currentWorkspaceId: user.workspaces[0].workspace.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function authUser(username: string, password: string) {
|
export async function authUser(username: string, password: string) {
|
||||||
const user = await prisma.user.findUniqueOrThrow({
|
const user = await prisma.user.findUnique({
|
||||||
where: {
|
where: {
|
||||||
username,
|
username,
|
||||||
password: await hashPassword(password),
|
|
||||||
},
|
},
|
||||||
select: {
|
select: { ...createUserSelect, password: true },
|
||||||
id: true,
|
});
|
||||||
username: true,
|
|
||||||
role: true,
|
if (!user) {
|
||||||
createdAt: true,
|
throw new Error('User not existed');
|
||||||
updatedAt: true,
|
}
|
||||||
deletedAt: true,
|
|
||||||
|
const checkPassword = await comparePassword(password, user.password);
|
||||||
|
if (!checkPassword) {
|
||||||
|
throw new Error('Password incorrected');
|
||||||
|
}
|
||||||
|
|
||||||
|
delete (user as any)['password'];
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function authUserWithToken(token: string) {
|
||||||
|
const payload = jwtVerify(token);
|
||||||
|
|
||||||
|
const id = payload.id;
|
||||||
|
|
||||||
|
const user = await prisma.user.findUniqueOrThrow({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
},
|
},
|
||||||
|
select: createUserSelect,
|
||||||
});
|
});
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
|
@ -1,6 +1,12 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import { body, validate } from '../middleware/validate';
|
import { header, body, validate } from '../middleware/validate';
|
||||||
import { authUser, createAdminUser } from '../model/user';
|
import {
|
||||||
|
authUser,
|
||||||
|
authUserWithToken,
|
||||||
|
createAdminUser,
|
||||||
|
createUser,
|
||||||
|
getUserCount,
|
||||||
|
} from '../model/user';
|
||||||
import { auth, jwtSign } from '../middleware/auth';
|
import { auth, jwtSign } from '../middleware/auth';
|
||||||
|
|
||||||
export const userRouter = Router();
|
export const userRouter = Router();
|
||||||
@ -16,13 +22,53 @@ userRouter.post(
|
|||||||
|
|
||||||
const user = await authUser(username, password);
|
const user = await authUser(username, password);
|
||||||
|
|
||||||
const token = jwtSign({
|
const token = jwtSign(user);
|
||||||
id: user.id,
|
|
||||||
username: user.username,
|
|
||||||
role: user.role,
|
|
||||||
});
|
|
||||||
|
|
||||||
res.json({ token });
|
res.json({ info: user, token });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
userRouter.post(
|
||||||
|
'/register',
|
||||||
|
validate(
|
||||||
|
body('username').exists().withMessage('Username should be existed'),
|
||||||
|
body('password').exists().withMessage('Password should be existed')
|
||||||
|
),
|
||||||
|
async (req, res) => {
|
||||||
|
const { username, password } = req.body;
|
||||||
|
|
||||||
|
const userCount = await getUserCount();
|
||||||
|
if (userCount === 0) {
|
||||||
|
const user = await createAdminUser(username, password);
|
||||||
|
|
||||||
|
const token = jwtSign(user);
|
||||||
|
|
||||||
|
res.json({ info: user, token });
|
||||||
|
} else {
|
||||||
|
const user = await createUser(username, password);
|
||||||
|
|
||||||
|
const token = jwtSign(user);
|
||||||
|
|
||||||
|
res.json({ info: user, token });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
userRouter.post(
|
||||||
|
'/loginWithToken',
|
||||||
|
validate(body('token').exists().withMessage('Token should be existed')),
|
||||||
|
async (req, res) => {
|
||||||
|
const { token } = req.body;
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
throw new Error('Cannot get token');
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await authUserWithToken(token);
|
||||||
|
|
||||||
|
const newToken = jwtSign(user);
|
||||||
|
|
||||||
|
res.json({ info: user, token: newToken });
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user