adds api, guard, tailwind
This commit is contained in:
parent
4ae4e87468
commit
8d0b32f8df
@ -6,4 +6,5 @@ NODE_ENV=development
|
|||||||
DATABASE_URL=neststarterappdevelopement
|
DATABASE_URL=neststarterappdevelopement
|
||||||
|
|
||||||
# recommend using randomkeygen.com to generate a key
|
# recommend using randomkeygen.com to generate a key
|
||||||
ENCRYPTION_KEY=yourencryptionkey
|
ENCRYPTION_KEY=yourencryptionkey
|
||||||
|
REFRESH_ENCRYPTION_KEY=yourrefreshencryptionkey
|
6
client/.postcssrc
Normal file
6
client/.postcssrc
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"plugins": {
|
||||||
|
"tailwindcss": {},
|
||||||
|
"autoprefixer": {},
|
||||||
|
}
|
||||||
|
}
|
5
client/app.css
Normal file
5
client/app.css
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
@tailwind base;
|
||||||
|
|
||||||
|
@tailwind components;
|
||||||
|
|
||||||
|
@tailwind utilities;
|
@ -1,25 +1,40 @@
|
|||||||
import { useReducer } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { HashRouter } from 'react-router-dom';
|
import { HashRouter } from 'react-router-dom';
|
||||||
import { Router } from './components/router';
|
import { Router } from './components/router';
|
||||||
import { SettingsContext } from './utils/settings_context';
|
import { ApiContext } from './utils/api_context';
|
||||||
|
import { AuthContext } from './utils/auth_context';
|
||||||
const settingsReducer = (state, action) => {
|
import { useApi } from './utils/use_api';
|
||||||
switch (action.type) {
|
import { useJwtRefresh } from './utils/use_jwt_refresh';
|
||||||
case 'update': {
|
import './app.css';
|
||||||
return { ...state, ...action.payload };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return state;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const App = () => {
|
export const App = () => {
|
||||||
const [settings, dispatch] = useReducer(settingsReducer, window.SETTINGS);
|
const [authToken, setAuthToken] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
// Refresh the jwt token automatically
|
||||||
|
useJwtRefresh(authToken, setAuthToken);
|
||||||
|
|
||||||
|
// api instance
|
||||||
|
const api = useApi(authToken);
|
||||||
|
|
||||||
|
// get initial jwt using refresh token
|
||||||
|
useEffect(async () => {
|
||||||
|
const result = await api.get('/refresh_token');
|
||||||
|
if (result.token) {
|
||||||
|
setAuthToken(result.token);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsContext.Provider value={[settings, dispatch]}>
|
<AuthContext.Provider value={[authToken, setAuthToken]}>
|
||||||
<HashRouter>
|
<ApiContext.Provider value={api}>
|
||||||
<Router />
|
<HashRouter>
|
||||||
</HashRouter>
|
<Router />
|
||||||
</SettingsContext.Provider>
|
</HashRouter>
|
||||||
|
</ApiContext.Provider>
|
||||||
|
</AuthContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
7
client/components/common/button.jsx
Normal file
7
client/components/common/button.jsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export const Button = ({ children, ...other }) => {
|
||||||
|
return (
|
||||||
|
<button className="bg-gray-600 pt-2 pb-2 pr-4 pl-4 rounded-lg font-bold text-white" {...other}>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
3
client/components/common/input.jsx
Normal file
3
client/components/common/input.jsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const Input = (props) => {
|
||||||
|
return <input className="border-2 rounded-lg p-1" {...props} />;
|
||||||
|
};
|
3
client/components/common/paper.jsx
Normal file
3
client/components/common/paper.jsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const Paper = ({ children }) => {
|
||||||
|
return <div className="shadow-md flex flex-col p-4">{children}</div>;
|
||||||
|
};
|
@ -1,20 +1,33 @@
|
|||||||
import { useContext } from 'react';
|
import { useContext, useEffect, useState } from 'react';
|
||||||
import { SettingsContext } from '../../utils/settings_context';
|
import { ApiContext } from '../../utils/api_context';
|
||||||
|
import { AuthContext } from '../../utils/auth_context';
|
||||||
|
|
||||||
export const Home = () => {
|
export const Home = () => {
|
||||||
const [, dispatch] = useContext(SettingsContext);
|
const [, setAuthToken] = useContext(AuthContext);
|
||||||
|
const api = useContext(ApiContext);
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [user, setUser] = useState(null);
|
||||||
|
useEffect(async () => {
|
||||||
|
const res = await api.get('/users/me');
|
||||||
|
setUser(res.user);
|
||||||
|
setLoading(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const logout = async () => {
|
const logout = async () => {
|
||||||
const res = await fetch('/sessions', {
|
const res = await api.del('/sessions');
|
||||||
method: 'DELETE',
|
if (res.success) {
|
||||||
});
|
setAuthToken(null);
|
||||||
if (res.status === 200) {
|
|
||||||
dispatch({ type: 'update', payload: { jwt: undefined } });
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div>Loading...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1>Welcome</h1>
|
<h1>Welcome {user.name}</h1>
|
||||||
<button type="button" onClick={logout}>
|
<button type="button" onClick={logout}>
|
||||||
Logout
|
Logout
|
||||||
</button>
|
</button>
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import { Routes, Route, Navigate } from 'react-router-dom';
|
import { Routes, Route, Navigate } from 'react-router-dom';
|
||||||
import { Home } from './home/_home';
|
import { Home } from './home/_home';
|
||||||
import { SettingsContext } from '../utils/settings_context';
|
import { AuthContext } from '../utils/auth_context';
|
||||||
import { SignIn } from './sign_in/_sign_in';
|
import { SignIn } from './sign_in/_sign_in';
|
||||||
import { SignUp } from './sign_up/_sign_up';
|
import { SignUp } from './sign_up/_sign_up';
|
||||||
|
|
||||||
export const Router = () => {
|
export const Router = () => {
|
||||||
const [settings] = useContext(SettingsContext);
|
const [authToken] = useContext(AuthContext);
|
||||||
const { jwt } = settings;
|
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route
|
<Route
|
||||||
path="/"
|
path="/"
|
||||||
element={jwt ? <Home /> : <Navigate replace to="signin" />} // no jwt means not logged in
|
element={authToken ? <Home /> : <Navigate replace to="signin" />} // no jwt means not logged in
|
||||||
/>
|
/>
|
||||||
<Route path="signin" element={<SignIn />} />
|
<Route path="signin" element={<SignIn />} />
|
||||||
<Route path="signup" element={<SignUp />} />
|
<Route path="signup" element={<SignUp />} />
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
import { useContext, useState } from 'react';
|
import { useContext, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
import { SettingsContext } from '../../utils/settings_context';
|
import { AuthContext } from '../../utils/auth_context';
|
||||||
|
import { Paper } from '../common/paper';
|
||||||
|
import { Input } from '../common/input';
|
||||||
|
import { Button } from '../common/button';
|
||||||
|
|
||||||
export const SignIn = () => {
|
export const SignIn = () => {
|
||||||
const [, dispatch] = useContext(SettingsContext);
|
const [, setAuthToken] = useContext(AuthContext);
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -25,7 +28,7 @@ export const SignIn = () => {
|
|||||||
});
|
});
|
||||||
if (res.status === 201) {
|
if (res.status === 201) {
|
||||||
const result = await res.json();
|
const result = await res.json();
|
||||||
dispatch({ type: 'update', payload: { jwt: result.token } });
|
setAuthToken(result.token);
|
||||||
navigate('/');
|
navigate('/');
|
||||||
} else {
|
} else {
|
||||||
console.error('An issue occurred when logging in.');
|
console.error('An issue occurred when logging in.');
|
||||||
@ -33,28 +36,23 @@ export const SignIn = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="flex flex-row justify-center m-4">
|
||||||
<div>Email</div>
|
<div className="w-96">
|
||||||
<input
|
<Paper>
|
||||||
type="email"
|
<div>Email</div>
|
||||||
value={email}
|
<Input type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
<div>Password</div>
|
||||||
/>
|
<Input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
|
||||||
<div>Password</div>
|
<div className="flex flex-row justify-end mt-2">
|
||||||
<input
|
<Button type="button" onClick={goToSignUp}>
|
||||||
type="password"
|
Sign up
|
||||||
value={password}
|
</Button>
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
<div className="pl-2" />
|
||||||
/>
|
<Button type="button" onClick={signIn}>
|
||||||
<div>
|
Sign in
|
||||||
<button type="button" onClick={signIn}>
|
</Button>
|
||||||
Sign in
|
</div>
|
||||||
</button>
|
</Paper>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button type="button" onClick={goToSignUp}>
|
|
||||||
Sign up
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
import { useContext, useState } from 'react';
|
import { useContext, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
import { SettingsContext } from '../../utils/settings_context';
|
import { AuthContext } from '../../utils/auth_context';
|
||||||
|
import { Paper } from '../common/paper';
|
||||||
|
import { Input } from '../common/input';
|
||||||
|
import { Button } from '../common/button';
|
||||||
|
|
||||||
export const SignUp = () => {
|
export const SignUp = () => {
|
||||||
const [, dispatch] = useContext(SettingsContext);
|
const [, setAuthToken] = useContext(AuthContext);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
@ -46,45 +49,36 @@ export const SignUp = () => {
|
|||||||
});
|
});
|
||||||
if (res.status === 201) {
|
if (res.status === 201) {
|
||||||
const result = await res.json();
|
const result = await res.json();
|
||||||
dispatch({ type: 'update', payload: { jwt: result.token } });
|
setAuthToken(result.token);
|
||||||
navigate('/');
|
navigate('/');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="flex flex-row justify-center m-4">
|
||||||
<div>Name</div>
|
<div className="w-96">
|
||||||
<input
|
<Paper>
|
||||||
type="text"
|
<div>Name</div>
|
||||||
value={name}
|
<Input type="text" value={name} onChange={(e) => setName(e.target.value)} />
|
||||||
onChange={(e) => setName(e.target.value)}
|
<div>Email</div>
|
||||||
/>
|
<Input type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
|
||||||
<div>Email</div>
|
<div>Confirm Email</div>
|
||||||
<input
|
<Input type="email" value={emailConfirmation} onChange={(e) => setEmailConfirmation(e.target.value)} />
|
||||||
type="email"
|
<div>Password</div>
|
||||||
value={email}
|
<Input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
<div>Confirm Password</div>
|
||||||
/>
|
<Input
|
||||||
<div>Confirm Email</div>
|
type="password"
|
||||||
<input
|
value={passwordConfiramation}
|
||||||
type="email"
|
onChange={(e) => setPasswordConfirmation(e.target.value)}
|
||||||
value={emailConfirmation}
|
/>
|
||||||
onChange={(e) => setEmailConfirmation(e.target.value)}
|
<div className="flex flex-row justify-end mt-2">
|
||||||
/>
|
<Button type="button" onClick={signUp}>
|
||||||
<div>Password</div>
|
Sign up
|
||||||
<input
|
</Button>
|
||||||
type="password"
|
</div>
|
||||||
value={password}
|
<div className="flex">{errorMessage}</div>
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
</Paper>
|
||||||
/>
|
|
||||||
<div>Confirm Password</div>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
value={passwordConfiramation}
|
|
||||||
onChange={(e) => setPasswordConfirmation(e.target.value)}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<button type="button" onClick={signUp}>Sign up</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -2,10 +2,18 @@
|
|||||||
"name": "nestclientstarterapp-client",
|
"name": "nestclientstarterapp-client",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"scripts": {},
|
"scripts": {
|
||||||
|
"watch": "parcel watch ./index.js --dist-dir ../static",
|
||||||
|
"build": "parcel build ./index.js --dist-dir ../static"
|
||||||
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {},
|
"devDependencies": {
|
||||||
|
"autoprefixer": "^10.4.0",
|
||||||
|
"parcel": "^2.0.1",
|
||||||
|
"postcss": "^8.3.11",
|
||||||
|
"tailwindcss": "^2.2.19"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
|
9
client/tailwind.config.js
Normal file
9
client/tailwind.config.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
module.exports = {
|
||||||
|
mode: 'jit',
|
||||||
|
purge: ['*/**/*.jsx'],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
variants: {},
|
||||||
|
plugins: [],
|
||||||
|
};
|
3
client/utils/api_context.js
Normal file
3
client/utils/api_context.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { createContext } from 'react';
|
||||||
|
|
||||||
|
export const ApiContext = createContext({});
|
3
client/utils/auth_context.js
Normal file
3
client/utils/auth_context.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { createContext } from 'react';
|
||||||
|
|
||||||
|
export const AuthContext = createContext({});
|
@ -1,3 +0,0 @@
|
|||||||
import { createContext } from 'react';
|
|
||||||
|
|
||||||
export const SettingsContext = createContext({});
|
|
37
client/utils/use_api.js
Normal file
37
client/utils/use_api.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { useRef } from 'react';
|
||||||
|
|
||||||
|
export const useApi = (authToken) => {
|
||||||
|
const apiRef = useRef(new Api());
|
||||||
|
apiRef.current.authToken = authToken;
|
||||||
|
return apiRef.current;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class Api {
|
||||||
|
authToken = null;
|
||||||
|
|
||||||
|
makeRequest(url, method, body) {
|
||||||
|
return fetch(url, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${this.authToken}`,
|
||||||
|
},
|
||||||
|
body,
|
||||||
|
}).then((res) => res.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
get(url) {
|
||||||
|
return this.makeRequest(url, 'GET');
|
||||||
|
}
|
||||||
|
|
||||||
|
post(url, body = {}) {
|
||||||
|
return this.makeRequest(url, 'POST', body);
|
||||||
|
}
|
||||||
|
|
||||||
|
put(url, body = {}) {
|
||||||
|
return this.makeRequest(url, 'PUT', body);
|
||||||
|
}
|
||||||
|
|
||||||
|
del(url) {
|
||||||
|
return this.makeRequest(url, 'DELETE');
|
||||||
|
}
|
||||||
|
}
|
19
client/utils/use_jwt_refresh.js
Normal file
19
client/utils/use_jwt_refresh.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
export const useJwtRefresh = (authToken, setAuthToken) => {
|
||||||
|
const refreshTimer = useRef(null);
|
||||||
|
useEffect(() => {
|
||||||
|
clearTimeout(refreshTimer.current);
|
||||||
|
if (authToken) {
|
||||||
|
refreshTimer.current = setTimeout(async () => {
|
||||||
|
const result = await fetch('/refresh_token').then((res) => res.json());
|
||||||
|
if (result.token) {
|
||||||
|
setAuthToken(result.token);
|
||||||
|
} else {
|
||||||
|
setAuthToken(null);
|
||||||
|
}
|
||||||
|
}, 60000 * 10); // 10 minutes
|
||||||
|
}
|
||||||
|
return () => clearTimeout(refreshTimer.current);
|
||||||
|
}, [authToken]);
|
||||||
|
};
|
4897
client/yarn.lock
4897
client/yarn.lock
File diff suppressed because it is too large
Load Diff
@ -9,7 +9,7 @@
|
|||||||
"typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js --config server/database/cli_config.ts",
|
"typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js --config server/database/cli_config.ts",
|
||||||
"db:start": "pg_ctl status || pg_ctl start",
|
"db:start": "pg_ctl status || pg_ctl start",
|
||||||
"db:stop": "pg_ctl status && pg_ctl stop",
|
"db:stop": "pg_ctl status && pg_ctl stop",
|
||||||
"db:migration:create": "cd server/migrations && typeorm migration:create -n ",
|
"db:migration:create": "cd server/database/migrations && typeorm migration:create -n ",
|
||||||
"db:migrate": "yarn db:start && yarn typeorm migration:run",
|
"db:migrate": "yarn db:start && yarn typeorm migration:run",
|
||||||
"db:migrate:undo": "yarn db:start && yarn typeorm migration:revert",
|
"db:migrate:undo": "yarn db:start && yarn typeorm migration:revert",
|
||||||
"prebuild": "rimraf dist",
|
"prebuild": "rimraf dist",
|
||||||
@ -26,8 +26,8 @@
|
|||||||
"test:cov": "jest --coverage",
|
"test:cov": "jest --coverage",
|
||||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||||
"test:e2e": "jest --config ./test/jest-e2e.json",
|
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||||
"client:build": "parcel build ./client/index.js --dist-dir static",
|
"client:build": "cd client && yarn build",
|
||||||
"client:watch": "parcel watch ./client/index.js --dist-dir static"
|
"client:watch": "cd client && yarn watch"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/common": "^8.0.0",
|
"@nestjs/common": "^8.0.0",
|
||||||
@ -62,7 +62,6 @@
|
|||||||
"eslint-config-prettier": "^8.3.0",
|
"eslint-config-prettier": "^8.3.0",
|
||||||
"eslint-plugin-prettier": "^3.4.0",
|
"eslint-plugin-prettier": "^3.4.0",
|
||||||
"jest": "^27.0.6",
|
"jest": "^27.0.6",
|
||||||
"parcel": "^2.0.1",
|
|
||||||
"prettier": "^2.4.1",
|
"prettier": "^2.4.1",
|
||||||
"purdy": "^3.5.1",
|
"purdy": "^3.5.1",
|
||||||
"repl": "^0.1.3",
|
"repl": "^0.1.3",
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
import { AppService } from './app.service';
|
|
||||||
|
|
||||||
describe('AppController', () => {
|
describe('AppController', () => {
|
||||||
let appController: AppController;
|
let appController: AppController;
|
||||||
@ -8,15 +7,15 @@ describe('AppController', () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const app: TestingModule = await Test.createTestingModule({
|
const app: TestingModule = await Test.createTestingModule({
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService],
|
providers: [],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
appController = app.get<AppController>(AppController);
|
appController = app.get<AppController>(AppController);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('root', () => {
|
// describe('root', () => {
|
||||||
it('should return "Hello World!"', () => {
|
// it('should return "Hello World!"', () => {
|
||||||
expect(appController.getHello()).toBe('Hello World!');
|
// expect(appController.getHello()).toBe('Hello World!');
|
||||||
});
|
// });
|
||||||
});
|
// });
|
||||||
});
|
});
|
||||||
|
@ -1,12 +1,8 @@
|
|||||||
import { Controller, Get, Render, Req } from '@nestjs/common';
|
import { Controller, Get, Render } from '@nestjs/common';
|
||||||
import { Request } from 'express';
|
|
||||||
|
|
||||||
@Controller()
|
@Controller()
|
||||||
export class AppController {
|
export class AppController {
|
||||||
@Get()
|
@Get()
|
||||||
@Render('index')
|
@Render('index')
|
||||||
index(@Req() req: Request) {
|
index() {}
|
||||||
const jwt = req.cookies['_token'];
|
|
||||||
return { jwt };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
import { AppService } from './app.service';
|
|
||||||
import { config } from './database/config';
|
import { config } from './database/config';
|
||||||
import { UsersModule } from './modules/users.module';
|
import { UsersModule } from './modules/users.module';
|
||||||
|
import { JwtService } from './providers/services/jwt.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forRoot(config), UsersModule],
|
imports: [TypeOrmModule.forRoot(config), UsersModule],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService],
|
providers: [JwtService],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AppService {
|
|
||||||
getHello(): string {
|
|
||||||
return 'Hello World!';
|
|
||||||
}
|
|
||||||
}
|
|
32
server/controllers/refresh_tokens.controller.ts
Normal file
32
server/controllers/refresh_tokens.controller.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { Body, Controller, Get, HttpException, Req } from '@nestjs/common';
|
||||||
|
import { Request } from 'express';
|
||||||
|
import { UsersService } from 'server/providers/services/users.service';
|
||||||
|
import { SignInDto } from 'server/dto/sign_in.dto';
|
||||||
|
import { RefreshTokenBody } from 'server/dto/refresh_token_body.dto';
|
||||||
|
import { JwtService } from 'server/providers/services/jwt.service';
|
||||||
|
|
||||||
|
// this is kind of a misnomer because we are doing token based auth
|
||||||
|
// instead of session based auth
|
||||||
|
@Controller()
|
||||||
|
export class RefreshTokensController {
|
||||||
|
constructor(private usersService: UsersService, private jwtService: JwtService) {}
|
||||||
|
|
||||||
|
@Get('/refresh_token')
|
||||||
|
async get(@Body() body: SignInDto, @Req() req: Request) {
|
||||||
|
const refreshToken: string = req.cookies['_refresh_token'];
|
||||||
|
if (!refreshToken) {
|
||||||
|
throw new HttpException('No refresh token present', 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenBody = this.jwtService.parseRefreshToken(refreshToken) as RefreshTokenBody;
|
||||||
|
|
||||||
|
const user = await this.usersService.find(tokenBody.userId, ['refreshTokens']);
|
||||||
|
const userRefreshToken = user.refreshTokens.find((t) => t.id === tokenBody.id);
|
||||||
|
if (!userRefreshToken) {
|
||||||
|
throw new HttpException('User refresh token not found', 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = this.jwtService.issueToken({ userId: user.id });
|
||||||
|
return { token };
|
||||||
|
}
|
||||||
|
}
|
@ -1,56 +1,53 @@
|
|||||||
import {
|
import { Body, Controller, Delete, HttpException, HttpStatus, Post, Res } from '@nestjs/common';
|
||||||
Body,
|
|
||||||
Controller,
|
|
||||||
Delete,
|
|
||||||
HttpException,
|
|
||||||
HttpStatus,
|
|
||||||
Post,
|
|
||||||
Redirect,
|
|
||||||
Res,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
import * as jwt from 'jsonwebtoken';
|
|
||||||
import { UsersService } from 'server/providers/services/users.service';
|
import { UsersService } from 'server/providers/services/users.service';
|
||||||
import { SignInDto } from 'server/dto/sign_in.dto';
|
import { SignInDto } from 'server/dto/sign_in.dto';
|
||||||
|
import { JwtService } from 'server/providers/services/jwt.service';
|
||||||
|
import { RefreshTokensService } from 'server/providers/services/refresh_tokens.service';
|
||||||
|
import { RefreshToken } from 'server/entities/refresh_token.entity';
|
||||||
|
|
||||||
// this is kind of a misnomer because we are doing token based auth
|
// this is kind of a misnomer because we are doing token based auth
|
||||||
// instead of session based auth
|
// instead of session based auth
|
||||||
@Controller()
|
@Controller()
|
||||||
export class SessionsController {
|
export class SessionsController {
|
||||||
constructor(private usersService: UsersService) {}
|
constructor(
|
||||||
|
private usersService: UsersService,
|
||||||
|
private jwtService: JwtService,
|
||||||
|
private refreshTokenService: RefreshTokensService,
|
||||||
|
) {}
|
||||||
|
|
||||||
@Post('/sessions')
|
@Post('/sessions')
|
||||||
async create(
|
async create(@Body() body: SignInDto, @Res({ passthrough: true }) res: Response) {
|
||||||
@Body() body: SignInDto,
|
const { verified, user } = await this.usersService.verify(body.email, body.password);
|
||||||
@Res({ passthrough: true }) res: Response,
|
|
||||||
) {
|
|
||||||
const { verified, user } = await this.usersService.verify(
|
|
||||||
body.email,
|
|
||||||
body.password,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!verified) {
|
if (!verified) {
|
||||||
throw new HttpException(
|
throw new HttpException('Invalid email or password.', HttpStatus.BAD_REQUEST);
|
||||||
'Invalid email or password.',
|
|
||||||
HttpStatus.BAD_REQUEST,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
// Write JWT to cookie and send with response.
|
|
||||||
const token = jwt.sign(
|
let refreshToken = user.refreshTokens[0];
|
||||||
{
|
if (!refreshToken) {
|
||||||
user_id: user.id,
|
const newRefreshToken = new RefreshToken();
|
||||||
},
|
newRefreshToken.user = user;
|
||||||
process.env.ENCRYPTION_KEY,
|
refreshToken = await this.refreshTokenService.create(newRefreshToken);
|
||||||
{ expiresIn: '1h' },
|
// generate new refresh token
|
||||||
);
|
}
|
||||||
res.cookie('_token', token);
|
|
||||||
|
// JWT gets sent with response
|
||||||
|
const token = this.jwtService.issueToken({ userId: user.id });
|
||||||
|
|
||||||
|
const refreshJwtToken = this.jwtService.issueRefreshToken({ id: refreshToken.id, userId: user.id });
|
||||||
|
|
||||||
|
// only refresh token should go in the cookie
|
||||||
|
res.cookie('_refresh_token', refreshJwtToken, {
|
||||||
|
httpOnly: true, // prevents javascript code from accessing cookie (helps protect against XSS attacks)
|
||||||
|
});
|
||||||
|
|
||||||
return { token };
|
return { token };
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete('/sessions')
|
@Delete('/sessions')
|
||||||
async destroy(@Res({ passthrough: true }) res: Response) {
|
async destroy(@Res({ passthrough: true }) res: Response) {
|
||||||
res.clearCookie('_token');
|
res.clearCookie('_refresh_token');
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,50 +1,57 @@
|
|||||||
import {
|
import { Body, Controller, Get, HttpException, HttpStatus, Post, Res, UseGuards } from '@nestjs/common';
|
||||||
Body,
|
|
||||||
Controller,
|
|
||||||
HttpException,
|
|
||||||
HttpStatus,
|
|
||||||
Post,
|
|
||||||
Res,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import * as bcrypt from 'bcrypt';
|
import * as bcrypt from 'bcrypt';
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
import * as jwt from 'jsonwebtoken';
|
import { JwtBody } from 'server/decorators/jwt_body.decorator';
|
||||||
import { CreateUserDto } from 'server/dto/create_user.dto';
|
import { CreateUserDto } from 'server/dto/create_user.dto';
|
||||||
|
import { JwtBodyDto } from 'server/dto/jwt_body.dto';
|
||||||
|
import { RefreshToken } from 'server/entities/refresh_token.entity';
|
||||||
import { User } from 'server/entities/user.entity';
|
import { User } from 'server/entities/user.entity';
|
||||||
|
import { AuthGuard } from 'server/providers/guards/auth.guard';
|
||||||
|
import { JwtService } from 'server/providers/services/jwt.service';
|
||||||
|
import { RefreshTokensService } from 'server/providers/services/refresh_tokens.service';
|
||||||
import { UsersService } from 'server/providers/services/users.service';
|
import { UsersService } from 'server/providers/services/users.service';
|
||||||
|
|
||||||
@Controller()
|
@Controller()
|
||||||
export class UsersController {
|
export class UsersController {
|
||||||
constructor(private usersService: UsersService) {}
|
constructor(
|
||||||
|
private usersService: UsersService,
|
||||||
|
private jwtService: JwtService,
|
||||||
|
private refreshTokenService: RefreshTokensService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Get('/users/me')
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
async getCurrentUser(@JwtBody() jwtBody: JwtBodyDto) {
|
||||||
|
const user = await this.usersService.find(jwtBody.userId);
|
||||||
|
return { user };
|
||||||
|
}
|
||||||
|
|
||||||
@Post('/users')
|
@Post('/users')
|
||||||
async create(
|
async create(@Body() userPayload: CreateUserDto, @Res({ passthrough: true }) res: Response) {
|
||||||
@Body() userPayload: CreateUserDto,
|
|
||||||
@Res({ passthrough: true }) res: Response,
|
|
||||||
) {
|
|
||||||
const newUser = new User();
|
const newUser = new User();
|
||||||
newUser.email = userPayload.email;
|
newUser.email = userPayload.email;
|
||||||
newUser.name = userPayload.name;
|
newUser.name = userPayload.name;
|
||||||
newUser.password_hash = await bcrypt.hash(userPayload.password, 10);
|
newUser.passwordHash = await bcrypt.hash(userPayload.password, 10);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const user = await this.usersService.create(newUser);
|
const user = await this.usersService.create(newUser);
|
||||||
// assume signup and write cookie
|
// create refresh token in database for user
|
||||||
// Write JWT to cookie and send with response.
|
const newRefreshToken = new RefreshToken();
|
||||||
const token = jwt.sign(
|
newRefreshToken.user = user;
|
||||||
{
|
const refreshToken = await this.refreshTokenService.create(newRefreshToken);
|
||||||
user_id: user.id,
|
|
||||||
},
|
// issue jwt and refreshJwtToken
|
||||||
process.env.ENCRYPTION_KEY,
|
const token = this.jwtService.issueToken({ userId: user.id });
|
||||||
{ expiresIn: '1h' },
|
const refreshJwtToken = this.jwtService.issueRefreshToken({ id: refreshToken.id, userId: user.id });
|
||||||
);
|
|
||||||
res.cookie('_token', token);
|
// only refresh token should go in the cookie
|
||||||
|
res.cookie('_refresh_token', refreshJwtToken, {
|
||||||
|
httpOnly: true, // prevents javascript code from accessing cookie (helps protect against XSS attacks)
|
||||||
|
});
|
||||||
|
|
||||||
return { user, token };
|
return { user, token };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new HttpException(
|
throw new HttpException(`User creation failed. ${e.message}`, HttpStatus.BAD_REQUEST);
|
||||||
`User creation failed. ${e.message}`,
|
|
||||||
HttpStatus.BAD_REQUEST,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ export class AddUser1637028716848 implements MigrationInterface {
|
|||||||
isNullable: false,
|
isNullable: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'password_hash',
|
name: 'passwordHash',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
isNullable: false,
|
isNullable: false,
|
||||||
},
|
},
|
||||||
|
38
server/database/migrations/1637631042877-AddRefreshToken.ts
Normal file
38
server/database/migrations/1637631042877-AddRefreshToken.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm';
|
||||||
|
|
||||||
|
export class AddRefreshToken1637631042877 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.createTable(
|
||||||
|
new Table({
|
||||||
|
name: 'refresh_token',
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
type: 'int',
|
||||||
|
isPrimary: true,
|
||||||
|
isGenerated: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'userId',
|
||||||
|
type: 'int',
|
||||||
|
isNullable: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await queryRunner.createForeignKey(
|
||||||
|
'refresh_token',
|
||||||
|
new TableForeignKey({
|
||||||
|
columnNames: ['userId'],
|
||||||
|
referencedColumnNames: ['id'],
|
||||||
|
referencedTableName: 'user',
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.dropTable('refresh_token');
|
||||||
|
}
|
||||||
|
}
|
6
server/decorators/jwt_body.decorator.ts
Normal file
6
server/decorators/jwt_body.decorator.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||||
|
|
||||||
|
export const JwtBody = createParamDecorator((data: unknown, ctx: ExecutionContext) => {
|
||||||
|
const req = ctx.switchToHttp().getRequest();
|
||||||
|
return req.jwtBody;
|
||||||
|
});
|
3
server/dto/jwt_body.dto.ts
Normal file
3
server/dto/jwt_body.dto.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export interface JwtBodyDto {
|
||||||
|
userId: number;
|
||||||
|
}
|
4
server/dto/refresh_token_body.dto.ts
Normal file
4
server/dto/refresh_token_body.dto.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export interface RefreshTokenBody {
|
||||||
|
id: number;
|
||||||
|
userId: number;
|
||||||
|
}
|
11
server/entities/refresh_token.entity.ts
Normal file
11
server/entities/refresh_token.entity.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { Entity, PrimaryGeneratedColumn, ManyToOne } from 'typeorm';
|
||||||
|
import { User } from './user.entity';
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class RefreshToken {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@ManyToOne(() => User, (user) => user.refreshTokens)
|
||||||
|
user: User;
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
|
import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm';
|
||||||
|
import { RefreshToken } from './refresh_token.entity';
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class User {
|
export class User {
|
||||||
@ -12,5 +13,8 @@ export class User {
|
|||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
@Column({ nullable: false })
|
@Column({ nullable: false })
|
||||||
password_hash: string;
|
passwordHash: string;
|
||||||
|
|
||||||
|
@OneToMany(() => RefreshToken, (token) => token.user)
|
||||||
|
refreshTokens: RefreshToken[];
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ async function bootstrap() {
|
|||||||
|
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
app.useStaticAssets(join(__dirname, '..', 'static'));
|
app.useStaticAssets(join(__dirname, '..', 'static'));
|
||||||
app.setBaseViewsDir(join(__dirname, '../', 'views'));
|
app.setBaseViewsDir(join(__dirname, '..', 'views'));
|
||||||
app.setViewEngine('hbs');
|
app.setViewEngine('hbs');
|
||||||
await app.listen(process.env.PORT);
|
await app.listen(process.env.PORT);
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,14 @@ import { User } from 'server/entities/user.entity';
|
|||||||
import { SessionsController } from '../controllers/sessions.controller';
|
import { SessionsController } from '../controllers/sessions.controller';
|
||||||
import { UsersController } from 'server/controllers/users.controller';
|
import { UsersController } from 'server/controllers/users.controller';
|
||||||
import { UsersService } from '../providers/services/users.service';
|
import { UsersService } from '../providers/services/users.service';
|
||||||
|
import { RefreshTokensService } from '../providers/services/refresh_tokens.service';
|
||||||
|
import { RefreshToken } from 'server/entities/refresh_token.entity';
|
||||||
|
import { JwtService } from 'server/providers/services/jwt.service';
|
||||||
|
import { RefreshTokensController } from 'server/controllers/refresh_tokens.controller';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([User])],
|
imports: [TypeOrmModule.forFeature([User, RefreshToken])],
|
||||||
controllers: [SessionsController, UsersController],
|
controllers: [SessionsController, UsersController, RefreshTokensController],
|
||||||
providers: [UsersService],
|
providers: [UsersService, RefreshTokensService, JwtService],
|
||||||
})
|
})
|
||||||
export class UsersModule {}
|
export class UsersModule {}
|
||||||
|
20
server/providers/guards/auth.guard.ts
Normal file
20
server/providers/guards/auth.guard.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
||||||
|
import { JwtService } from '../services/jwt.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthGuard implements CanActivate {
|
||||||
|
constructor(private jwtService: JwtService) {}
|
||||||
|
|
||||||
|
canActivate(context: ExecutionContext) {
|
||||||
|
const req = context.switchToHttp().getRequest();
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
const jwt = authHeader.split(' ')[1];
|
||||||
|
try {
|
||||||
|
req.jwtBody = this.jwtService.parseToken(jwt);
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
27
server/providers/services/jwt.service.ts
Normal file
27
server/providers/services/jwt.service.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { HttpException, Injectable } from '@nestjs/common';
|
||||||
|
import * as jwt from 'jsonwebtoken';
|
||||||
|
import { JwtBodyDto } from 'server/dto/jwt_body.dto';
|
||||||
|
import { RefreshTokenBody } from 'server/dto/refresh_token_body.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class JwtService {
|
||||||
|
issueToken(body: JwtBodyDto | RefreshTokenBody, expiresIn = '15m', key = process.env.ENCRYPTION_KEY): string {
|
||||||
|
return jwt.sign(body, key, { expiresIn });
|
||||||
|
}
|
||||||
|
|
||||||
|
issueRefreshToken(body: RefreshTokenBody) {
|
||||||
|
return this.issueToken(body, '1y', process.env.REFRESH_ENCRYPTION_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
parseToken(token: string, key = process.env.ENCRYPTION_KEY): JwtBodyDto | RefreshTokenBody {
|
||||||
|
try {
|
||||||
|
return jwt.verify(token, key);
|
||||||
|
} catch (e) {
|
||||||
|
throw new HttpException('Invalid jwt token', 401);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parseRefreshToken(token: string) {
|
||||||
|
return this.parseToken(token, process.env.REFRESH_ENCRYPTION_KEY);
|
||||||
|
}
|
||||||
|
}
|
20
server/providers/services/refresh_tokens.service.ts
Normal file
20
server/providers/services/refresh_tokens.service.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { RefreshToken } from 'server/entities/refresh_token.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RefreshTokensService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(RefreshToken)
|
||||||
|
private refreshTokenRespository: Repository<RefreshToken>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
create(refreshToken: RefreshToken) {
|
||||||
|
return this.refreshTokenRespository.save(refreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(refreshToken: RefreshToken) {
|
||||||
|
return this.refreshTokenRespository.remove(refreshToken);
|
||||||
|
}
|
||||||
|
}
|
@ -11,12 +11,12 @@ export class UsersService {
|
|||||||
private usersRespository: Repository<User>,
|
private usersRespository: Repository<User>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
findBy(options: Record<string, any>) {
|
findBy(options: Record<string, any>, relations: string[] = []) {
|
||||||
return this.usersRespository.findOne(options);
|
return this.usersRespository.findOne(options, { relations });
|
||||||
}
|
}
|
||||||
|
|
||||||
find(id: number) {
|
find(id: number, relations: string[] = []) {
|
||||||
return this.usersRespository.findOne(id);
|
return this.usersRespository.findOne(id, { relations });
|
||||||
}
|
}
|
||||||
|
|
||||||
create(user: User) {
|
create(user: User) {
|
||||||
@ -24,12 +24,9 @@ export class UsersService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async verify(email: string, password: string) {
|
async verify(email: string, password: string) {
|
||||||
const user = await this.usersRespository.findOne({ email });
|
const user = await this.usersRespository.findOne({ email }, { relations: ['refreshTokens'] });
|
||||||
if (!user) return { verified: false, user: null };
|
if (!user) return { verified: false, user: null };
|
||||||
const verified: boolean = await bcrypt.compare(
|
const verified: boolean = await bcrypt.compare(password, user.passwordHash);
|
||||||
password,
|
|
||||||
user.password_hash,
|
|
||||||
);
|
|
||||||
return { verified, user: verified ? user : null };
|
return { verified, user: verified ? user : null };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,6 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<script type="text/javascript">
|
<link rel="stylesheet" href="index.css" />
|
||||||
window.SETTINGS = {
|
|
||||||
jwt: '{{jwt}}'
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app" />
|
<div id="app" />
|
||||||
|
Loading…
Reference in New Issue
Block a user