adds api, guard, tailwind

This commit is contained in:
Joseph Ditton 2021-11-23 14:04:12 -07:00
parent 4ae4e87468
commit 8d0b32f8df
42 changed files with 5440 additions and 3371 deletions

View File

@ -6,4 +6,5 @@ NODE_ENV=development
DATABASE_URL=neststarterappdevelopement
# recommend using randomkeygen.com to generate a key
ENCRYPTION_KEY=yourencryptionkey
ENCRYPTION_KEY=yourencryptionkey
REFRESH_ENCRYPTION_KEY=yourrefreshencryptionkey

6
client/.postcssrc Normal file
View File

@ -0,0 +1,6 @@
{
"plugins": {
"tailwindcss": {},
"autoprefixer": {},
}
}

5
client/app.css Normal file
View File

@ -0,0 +1,5 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@ -1,25 +1,40 @@
import { useReducer } from 'react';
import { useState, useEffect } from 'react';
import { HashRouter } from 'react-router-dom';
import { Router } from './components/router';
import { SettingsContext } from './utils/settings_context';
const settingsReducer = (state, action) => {
switch (action.type) {
case 'update': {
return { ...state, ...action.payload };
}
}
return state;
};
import { ApiContext } from './utils/api_context';
import { AuthContext } from './utils/auth_context';
import { useApi } from './utils/use_api';
import { useJwtRefresh } from './utils/use_jwt_refresh';
import './app.css';
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 (
<SettingsContext.Provider value={[settings, dispatch]}>
<HashRouter>
<Router />
</HashRouter>
</SettingsContext.Provider>
<AuthContext.Provider value={[authToken, setAuthToken]}>
<ApiContext.Provider value={api}>
<HashRouter>
<Router />
</HashRouter>
</ApiContext.Provider>
</AuthContext.Provider>
);
};

View 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>
);
};

View File

@ -0,0 +1,3 @@
export const Input = (props) => {
return <input className="border-2 rounded-lg p-1" {...props} />;
};

View File

@ -0,0 +1,3 @@
export const Paper = ({ children }) => {
return <div className="shadow-md flex flex-col p-4">{children}</div>;
};

View File

@ -1,20 +1,33 @@
import { useContext } from 'react';
import { SettingsContext } from '../../utils/settings_context';
import { useContext, useEffect, useState } from 'react';
import { ApiContext } from '../../utils/api_context';
import { AuthContext } from '../../utils/auth_context';
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 res = await fetch('/sessions', {
method: 'DELETE',
});
if (res.status === 200) {
dispatch({ type: 'update', payload: { jwt: undefined } });
const res = await api.del('/sessions');
if (res.success) {
setAuthToken(null);
}
};
if (loading) {
return <div>Loading...</div>;
}
return (
<div>
<h1>Welcome</h1>
<h1>Welcome {user.name}</h1>
<button type="button" onClick={logout}>
Logout
</button>

View File

@ -1,18 +1,18 @@
import { useContext } from 'react';
import { Routes, Route, Navigate } from 'react-router-dom';
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 { SignUp } from './sign_up/_sign_up';
export const Router = () => {
const [settings] = useContext(SettingsContext);
const { jwt } = settings;
const [authToken] = useContext(AuthContext);
return (
<Routes>
<Route
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="signup" element={<SignUp />} />

View File

@ -1,9 +1,12 @@
import { useContext, useState } from 'react';
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 = () => {
const [, dispatch] = useContext(SettingsContext);
const [, setAuthToken] = useContext(AuthContext);
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const navigate = useNavigate();
@ -25,7 +28,7 @@ export const SignIn = () => {
});
if (res.status === 201) {
const result = await res.json();
dispatch({ type: 'update', payload: { jwt: result.token } });
setAuthToken(result.token);
navigate('/');
} else {
console.error('An issue occurred when logging in.');
@ -33,28 +36,23 @@ export const SignIn = () => {
};
return (
<div>
<div>Email</div>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<div>Password</div>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<div>
<button type="button" onClick={signIn}>
Sign in
</button>
</div>
<div>
<button type="button" onClick={goToSignUp}>
Sign up
</button>
<div className="flex flex-row justify-center m-4">
<div className="w-96">
<Paper>
<div>Email</div>
<Input type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
<div>Password</div>
<Input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
<div className="flex flex-row justify-end mt-2">
<Button type="button" onClick={goToSignUp}>
Sign up
</Button>
<div className="pl-2" />
<Button type="button" onClick={signIn}>
Sign in
</Button>
</div>
</Paper>
</div>
</div>
);

View File

@ -1,9 +1,12 @@
import { useContext, useState } from 'react';
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 = () => {
const [, dispatch] = useContext(SettingsContext);
const [, setAuthToken] = useContext(AuthContext);
const navigate = useNavigate();
const [name, setName] = useState('');
const [email, setEmail] = useState('');
@ -46,45 +49,36 @@ export const SignUp = () => {
});
if (res.status === 201) {
const result = await res.json();
dispatch({ type: 'update', payload: { jwt: result.token } });
setAuthToken(result.token);
navigate('/');
}
};
return (
<div>
<div>Name</div>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<div>Email</div>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<div>Confirm Email</div>
<input
type="email"
value={emailConfirmation}
onChange={(e) => setEmailConfirmation(e.target.value)}
/>
<div>Password</div>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<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 className="flex flex-row justify-center m-4">
<div className="w-96">
<Paper>
<div>Name</div>
<Input type="text" value={name} onChange={(e) => setName(e.target.value)} />
<div>Email</div>
<Input type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
<div>Confirm Email</div>
<Input type="email" value={emailConfirmation} onChange={(e) => setEmailConfirmation(e.target.value)} />
<div>Password</div>
<Input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
<div>Confirm Password</div>
<Input
type="password"
value={passwordConfiramation}
onChange={(e) => setPasswordConfirmation(e.target.value)}
/>
<div className="flex flex-row justify-end mt-2">
<Button type="button" onClick={signUp}>
Sign up
</Button>
</div>
<div className="flex">{errorMessage}</div>
</Paper>
</div>
</div>
);

View File

@ -2,10 +2,18 @@
"name": "nestclientstarterapp-client",
"version": "1.0.0",
"description": "",
"scripts": {},
"scripts": {
"watch": "parcel watch ./index.js --dist-dir ../static",
"build": "parcel build ./index.js --dist-dir ../static"
},
"author": "",
"license": "ISC",
"devDependencies": {},
"devDependencies": {
"autoprefixer": "^10.4.0",
"parcel": "^2.0.1",
"postcss": "^8.3.11",
"tailwindcss": "^2.2.19"
},
"dependencies": {
"react": "^17.0.2",
"react-dom": "^17.0.2",

View File

@ -0,0 +1,9 @@
module.exports = {
mode: 'jit',
purge: ['*/**/*.jsx'],
theme: {
extend: {},
},
variants: {},
plugins: [],
};

View File

@ -0,0 +1,3 @@
import { createContext } from 'react';
export const ApiContext = createContext({});

View File

@ -0,0 +1,3 @@
import { createContext } from 'react';
export const AuthContext = createContext({});

View File

@ -1,3 +0,0 @@
import { createContext } from 'react';
export const SettingsContext = createContext({});

37
client/utils/use_api.js Normal file
View 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');
}
}

View 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]);
};

File diff suppressed because it is too large Load Diff

View File

@ -9,7 +9,7 @@
"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: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:undo": "yarn db:start && yarn typeorm migration:revert",
"prebuild": "rimraf dist",
@ -26,8 +26,8 @@
"test:cov": "jest --coverage",
"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",
"client:build": "parcel build ./client/index.js --dist-dir static",
"client:watch": "parcel watch ./client/index.js --dist-dir static"
"client:build": "cd client && yarn build",
"client:watch": "cd client && yarn watch"
},
"dependencies": {
"@nestjs/common": "^8.0.0",
@ -62,7 +62,6 @@
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^3.4.0",
"jest": "^27.0.6",
"parcel": "^2.0.1",
"prettier": "^2.4.1",
"purdy": "^3.5.1",
"repl": "^0.1.3",

View File

@ -1,6 +1,5 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
describe('AppController', () => {
let appController: AppController;
@ -8,15 +7,15 @@ describe('AppController', () => {
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
providers: [],
}).compile();
appController = app.get<AppController>(AppController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
});
});
// describe('root', () => {
// it('should return "Hello World!"', () => {
// expect(appController.getHello()).toBe('Hello World!');
// });
// });
});

View File

@ -1,12 +1,8 @@
import { Controller, Get, Render, Req } from '@nestjs/common';
import { Request } from 'express';
import { Controller, Get, Render } from '@nestjs/common';
@Controller()
export class AppController {
@Get()
@Render('index')
index(@Req() req: Request) {
const jwt = req.cookies['_token'];
return { jwt };
}
index() {}
}

View File

@ -1,13 +1,13 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { config } from './database/config';
import { UsersModule } from './modules/users.module';
import { JwtService } from './providers/services/jwt.service';
@Module({
imports: [TypeOrmModule.forRoot(config), UsersModule],
controllers: [AppController],
providers: [AppService],
providers: [JwtService],
})
export class AppModule {}

View File

@ -1,8 +0,0 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

View 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 };
}
}

View File

@ -1,56 +1,53 @@
import {
Body,
Controller,
Delete,
HttpException,
HttpStatus,
Post,
Redirect,
Res,
} from '@nestjs/common';
import { Body, Controller, Delete, HttpException, HttpStatus, Post, Res } from '@nestjs/common';
import { Response } from 'express';
import * as jwt from 'jsonwebtoken';
import { UsersService } from 'server/providers/services/users.service';
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
// instead of session based auth
@Controller()
export class SessionsController {
constructor(private usersService: UsersService) {}
constructor(
private usersService: UsersService,
private jwtService: JwtService,
private refreshTokenService: RefreshTokensService,
) {}
@Post('/sessions')
async create(
@Body() body: SignInDto,
@Res({ passthrough: true }) res: Response,
) {
const { verified, user } = await this.usersService.verify(
body.email,
body.password,
);
async create(@Body() body: SignInDto, @Res({ passthrough: true }) res: Response) {
const { verified, user } = await this.usersService.verify(body.email, body.password);
if (!verified) {
throw new HttpException(
'Invalid email or password.',
HttpStatus.BAD_REQUEST,
);
throw new HttpException('Invalid email or password.', HttpStatus.BAD_REQUEST);
}
// Write JWT to cookie and send with response.
const token = jwt.sign(
{
user_id: user.id,
},
process.env.ENCRYPTION_KEY,
{ expiresIn: '1h' },
);
res.cookie('_token', token);
let refreshToken = user.refreshTokens[0];
if (!refreshToken) {
const newRefreshToken = new RefreshToken();
newRefreshToken.user = user;
refreshToken = await this.refreshTokenService.create(newRefreshToken);
// generate new refresh 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 };
}
@Delete('/sessions')
async destroy(@Res({ passthrough: true }) res: Response) {
res.clearCookie('_token');
res.clearCookie('_refresh_token');
return { success: true };
}
}

View File

@ -1,50 +1,57 @@
import {
Body,
Controller,
HttpException,
HttpStatus,
Post,
Res,
} from '@nestjs/common';
import { Body, Controller, Get, HttpException, HttpStatus, Post, Res, UseGuards } from '@nestjs/common';
import * as bcrypt from 'bcrypt';
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 { JwtBodyDto } from 'server/dto/jwt_body.dto';
import { RefreshToken } from 'server/entities/refresh_token.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';
@Controller()
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')
async create(
@Body() userPayload: CreateUserDto,
@Res({ passthrough: true }) res: Response,
) {
async create(@Body() userPayload: CreateUserDto, @Res({ passthrough: true }) res: Response) {
const newUser = new User();
newUser.email = userPayload.email;
newUser.name = userPayload.name;
newUser.password_hash = await bcrypt.hash(userPayload.password, 10);
newUser.passwordHash = await bcrypt.hash(userPayload.password, 10);
try {
const user = await this.usersService.create(newUser);
// assume signup and write cookie
// Write JWT to cookie and send with response.
const token = jwt.sign(
{
user_id: user.id,
},
process.env.ENCRYPTION_KEY,
{ expiresIn: '1h' },
);
res.cookie('_token', token);
// create refresh token in database for user
const newRefreshToken = new RefreshToken();
newRefreshToken.user = user;
const refreshToken = await this.refreshTokenService.create(newRefreshToken);
// issue jwt and refreshJwtToken
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 { user, token };
} catch (e) {
throw new HttpException(
`User creation failed. ${e.message}`,
HttpStatus.BAD_REQUEST,
);
throw new HttpException(`User creation failed. ${e.message}`, HttpStatus.BAD_REQUEST);
}
}
}

View File

@ -18,7 +18,7 @@ export class AddUser1637028716848 implements MigrationInterface {
isNullable: false,
},
{
name: 'password_hash',
name: 'passwordHash',
type: 'text',
isNullable: false,
},

View 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');
}
}

View 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;
});

View File

@ -0,0 +1,3 @@
export interface JwtBodyDto {
userId: number;
}

View File

@ -0,0 +1,4 @@
export interface RefreshTokenBody {
id: number;
userId: number;
}

View 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;
}

View File

@ -1,4 +1,5 @@
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm';
import { RefreshToken } from './refresh_token.entity';
@Entity()
export class User {
@ -12,5 +13,8 @@ export class User {
name: string;
@Column({ nullable: false })
password_hash: string;
passwordHash: string;
@OneToMany(() => RefreshToken, (token) => token.user)
refreshTokens: RefreshToken[];
}

View File

@ -21,7 +21,7 @@ async function bootstrap() {
app.use(cookieParser());
app.useStaticAssets(join(__dirname, '..', 'static'));
app.setBaseViewsDir(join(__dirname, '../', 'views'));
app.setBaseViewsDir(join(__dirname, '..', 'views'));
app.setViewEngine('hbs');
await app.listen(process.env.PORT);
}

View File

@ -4,10 +4,14 @@ import { User } from 'server/entities/user.entity';
import { SessionsController } from '../controllers/sessions.controller';
import { UsersController } from 'server/controllers/users.controller';
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({
imports: [TypeOrmModule.forFeature([User])],
controllers: [SessionsController, UsersController],
providers: [UsersService],
imports: [TypeOrmModule.forFeature([User, RefreshToken])],
controllers: [SessionsController, UsersController, RefreshTokensController],
providers: [UsersService, RefreshTokensService, JwtService],
})
export class UsersModule {}

View 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;
}
}

View 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);
}
}

View 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);
}
}

View File

@ -11,12 +11,12 @@ export class UsersService {
private usersRespository: Repository<User>,
) {}
findBy(options: Record<string, any>) {
return this.usersRespository.findOne(options);
findBy(options: Record<string, any>, relations: string[] = []) {
return this.usersRespository.findOne(options, { relations });
}
find(id: number) {
return this.usersRespository.findOne(id);
find(id: number, relations: string[] = []) {
return this.usersRespository.findOne(id, { relations });
}
create(user: User) {
@ -24,12 +24,9 @@ export class UsersService {
}
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 };
const verified: boolean = await bcrypt.compare(
password,
user.password_hash,
);
const verified: boolean = await bcrypt.compare(password, user.passwordHash);
return { verified, user: verified ? user : null };
}
}

View File

@ -1,10 +1,6 @@
<html>
<head>
<script type="text/javascript">
window.SETTINGS = {
jwt: '{{jwt}}'
};
</script>
<link rel="stylesheet" href="index.css" />
</head>
<body>
<div id="app" />

3223
yarn.lock

File diff suppressed because it is too large Load Diff