feat: user login, loginWithToken and register
This commit is contained in:
parent
08ef1fea91
commit
ef1801f531
@ -47,7 +47,8 @@
|
||||
"typescript": "^4.9.5",
|
||||
"uuid": "^9.0.0",
|
||||
"vite-express": "^0.10.0",
|
||||
"yup": "^1.2.0"
|
||||
"yup": "^1.2.0",
|
||||
"zustand": "^4.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcryptjs": "^2.4.3",
|
||||
|
@ -106,6 +106,9 @@ dependencies:
|
||||
yup:
|
||||
specifier: ^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:
|
||||
'@types/bcryptjs':
|
||||
@ -1960,7 +1963,6 @@ packages:
|
||||
|
||||
/@types/prop-types@15.7.5:
|
||||
resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==}
|
||||
dev: true
|
||||
|
||||
/@types/qs@6.9.8:
|
||||
resolution: {integrity: sha512-u95svzDlTysU5xecFNTgfFG5RUWu1A9P0VzgpcIiGZA9iraHOdSzcxMxQ55DyeRaGCSxQi7LxXDI4rzq/MYfdg==}
|
||||
@ -1982,7 +1984,6 @@ packages:
|
||||
'@types/prop-types': 15.7.5
|
||||
'@types/scheduler': 0.16.3
|
||||
csstype: 3.1.2
|
||||
dev: true
|
||||
|
||||
/@types/request-ip@0.0.38:
|
||||
resolution: {integrity: sha512-1yeq8UuK/tUBqLXRY24gjeFvrSNaGNcOcZLQjHlnuw8iu+qE/vTQ64TUcLWorr607NKLfFakdoYEXXHXrLLKCw==}
|
||||
@ -1992,7 +1993,6 @@ packages:
|
||||
|
||||
/@types/scheduler@0.16.3:
|
||||
resolution: {integrity: sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==}
|
||||
dev: true
|
||||
|
||||
/@types/send@0.17.1:
|
||||
resolution: {integrity: sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==}
|
||||
@ -5856,6 +5856,14 @@ packages:
|
||||
punycode: 2.3.0
|
||||
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:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
dev: true
|
||||
@ -6034,3 +6042,23 @@ packages:
|
||||
toposort: 2.0.2
|
||||
type-fest: 2.19.0
|
||||
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 { Settings } from './pages/Settings';
|
||||
import { Servers } from './pages/Servers';
|
||||
import { useUserStore } from './store/user';
|
||||
import { Register } from './pages/Register';
|
||||
|
||||
function App() {
|
||||
const { info } = useUserStore();
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route element={<Layout />}>
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
<Route path="/monitor" element={<Monitor />} />
|
||||
<Route path="/website" element={<Website />} />
|
||||
<Route path="/servers" element={<Servers />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
</Route>
|
||||
{info && (
|
||||
<Route element={<Layout />}>
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
<Route path="/monitor" element={<Monitor />} />
|
||||
<Route path="/website" element={<Website />} />
|
||||
<Route path="/servers" element={<Servers />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
</Route>
|
||||
)}
|
||||
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/register" element={<Register />} />
|
||||
|
||||
<Route
|
||||
path="*"
|
||||
element={<Navigate to="/dashboard" replace={true} />}
|
||||
element={
|
||||
<Navigate to={info ? '/dashboard' : '/login'} replace={true} />
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</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 React from 'react';
|
||||
import { useEvent } from '../hooks/useEvent';
|
||||
import axios from 'axios';
|
||||
import React, { useEffect } from 'react';
|
||||
import { model } from '../api/model';
|
||||
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(() => {
|
||||
const handleLogin = useEvent(async (values: any) => {
|
||||
await axios.post('/api/user/login', {
|
||||
username: values.username,
|
||||
password: values.password,
|
||||
});
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [{ loading }, handleLogin] = useRequest(async (values: any) => {
|
||||
await model.user.login(values.username, values.password);
|
||||
navigate('/dashboard');
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const token = getJWT();
|
||||
if (token) {
|
||||
loginWithToken().then(() => {
|
||||
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}>Tianji</Typography.Title>
|
||||
<Form layout="vertical" onFinish={handleLogin}>
|
||||
<Form layout="vertical" disabled={loading} onFinish={handleLogin}>
|
||||
<Form.Item
|
||||
label="Username"
|
||||
name="username"
|
||||
@ -28,13 +40,31 @@ export const Login: React.FC = React.memo(() => {
|
||||
name="password"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Input size="large" />
|
||||
<Input.Password size="large" />
|
||||
</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
|
||||
</Button>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button
|
||||
size="large"
|
||||
htmlType="button"
|
||||
block={true}
|
||||
onClick={() => {
|
||||
navigate('/register');
|
||||
}}
|
||||
>
|
||||
Register
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</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((err: any, req: any, res: any, next: any) => {
|
||||
res.status(500);
|
||||
res.json({ error: err.message });
|
||||
console.error(err);
|
||||
res.status(500).json({ message: err.message });
|
||||
});
|
||||
|
||||
ViteExpress.listen(app, port, () => {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { authUser, findUser } from '../model/user';
|
||||
import { findUser } from '../model/user';
|
||||
import passport from 'passport';
|
||||
import { Handler } from 'express';
|
||||
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 jwtAudience = process.env.JWT_AUDIENCE || 'msgbyte.com';
|
||||
|
||||
interface JWTPayload {
|
||||
id: string;
|
||||
username: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
passport.use(
|
||||
new JwtStrategy(
|
||||
{
|
||||
@ -18,7 +24,7 @@ passport.use(
|
||||
audience: jwtAudience,
|
||||
},
|
||||
function (jwt_payload, done) {
|
||||
findUser(jwt_payload.sub)
|
||||
findUser(jwt_payload.id)
|
||||
.then((user) => {
|
||||
if (user) {
|
||||
done(null, user);
|
||||
@ -41,16 +47,33 @@ passport.deserializeUser(function (user: any, cb) {
|
||||
cb(null, user);
|
||||
});
|
||||
|
||||
export function jwtSign(payload: {}): string {
|
||||
const token = jwt.sign(payload, jwtSecret, {
|
||||
issuer: jwtIssuer,
|
||||
audience: jwtAudience,
|
||||
expiresIn: '30d',
|
||||
});
|
||||
export function jwtSign(payload: JWTPayload): string {
|
||||
const token = jwt.sign(
|
||||
{
|
||||
id: payload.id,
|
||||
username: payload.username,
|
||||
role: payload.role,
|
||||
},
|
||||
jwtSecret,
|
||||
{
|
||||
issuer: jwtIssuer,
|
||||
audience: jwtAudience,
|
||||
expiresIn: '30d',
|
||||
}
|
||||
);
|
||||
|
||||
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 {
|
||||
return passport.authenticate('jwt', {
|
||||
session: false,
|
||||
|
@ -18,4 +18,4 @@ export function validate(...validator: ValidationChain[]): Handler {
|
||||
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 bcryptjs from 'bcryptjs';
|
||||
import { ROLES } from '../utils/const';
|
||||
import { jwtVerify } from '../middleware/auth';
|
||||
|
||||
async function hashPassword(password: string) {
|
||||
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
|
||||
*/
|
||||
@ -13,7 +50,9 @@ export async function createAdminUser(username: string, password: string) {
|
||||
const count = await prisma.user.count();
|
||||
|
||||
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({
|
||||
@ -34,9 +73,7 @@ export async function createAdminUser(username: string, password: string) {
|
||||
],
|
||||
},
|
||||
},
|
||||
include: {
|
||||
workspaces: true,
|
||||
},
|
||||
select: createUserSelect,
|
||||
});
|
||||
|
||||
if (user.workspaces[0]) {
|
||||
@ -45,10 +82,12 @@ export async function createAdminUser(username: string, password: string) {
|
||||
id: user.id,
|
||||
},
|
||||
data: {
|
||||
currentWorkspaceId: user.workspaces[0].workspaceId,
|
||||
currentWorkspaceId: user.workspaces[0].workspace.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
export async function createUser(username: string, password: string) {
|
||||
@ -80,9 +119,7 @@ export async function createUser(username: string, password: string) {
|
||||
],
|
||||
},
|
||||
},
|
||||
include: {
|
||||
workspaces: true,
|
||||
},
|
||||
select: createUserSelect,
|
||||
});
|
||||
|
||||
if (user.workspaces[0]) {
|
||||
@ -91,26 +128,46 @@ export async function createUser(username: string, password: string) {
|
||||
id: user.id,
|
||||
},
|
||||
data: {
|
||||
currentWorkspaceId: user.workspaces[0].workspaceId,
|
||||
currentWorkspaceId: user.workspaces[0].workspace.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
export async function authUser(username: string, password: string) {
|
||||
const user = await prisma.user.findUniqueOrThrow({
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
username,
|
||||
password: await hashPassword(password),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
role: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
deletedAt: true,
|
||||
select: { ...createUserSelect, password: true },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new Error('User not existed');
|
||||
}
|
||||
|
||||
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;
|
||||
|
@ -1,6 +1,12 @@
|
||||
import { Router } from 'express';
|
||||
import { body, validate } from '../middleware/validate';
|
||||
import { authUser, createAdminUser } from '../model/user';
|
||||
import { header, body, validate } from '../middleware/validate';
|
||||
import {
|
||||
authUser,
|
||||
authUserWithToken,
|
||||
createAdminUser,
|
||||
createUser,
|
||||
getUserCount,
|
||||
} from '../model/user';
|
||||
import { auth, jwtSign } from '../middleware/auth';
|
||||
|
||||
export const userRouter = Router();
|
||||
@ -16,13 +22,53 @@ userRouter.post(
|
||||
|
||||
const user = await authUser(username, password);
|
||||
|
||||
const token = jwtSign({
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
role: user.role,
|
||||
});
|
||||
const token = jwtSign(user);
|
||||
|
||||
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