summaryrefslogtreecommitdiff
path: root/server
diff options
context:
space:
mode:
authorJoseph Ditton <jditton.atomic@gmail.com>2021-12-01 20:18:26 -0700
committerJoseph Ditton <jditton.atomic@gmail.com>2021-12-01 20:18:26 -0700
commit84b45cd6b11347e66437cd92dc20372d0abd6eb9 (patch)
tree6e42b5861278485c67159dc57c225983e3fd69f8 /server
parentd803aaaf1be441f55fe674c3b0c6793e77a9203f (diff)
downloadlocchat-84b45cd6b11347e66437cd92dc20372d0abd6eb9.tar.gz
locchat-84b45cd6b11347e66437cd92dc20372d0abd6eb9.zip
adds roles
Diffstat (limited to 'server')
-rw-r--r--server/app.controller.ts3
-rw-r--r--server/app.module.ts13
-rw-r--r--server/controllers/refresh_tokens.controller.ts12
-rw-r--r--server/controllers/sessions.controller.ts9
-rw-r--r--server/controllers/users.controller.ts25
-rw-r--r--server/database/seeds.ts8
-rw-r--r--server/decorators/roles.decorator.ts5
-rw-r--r--server/decorators/skip.decorator.ts5
-rw-r--r--server/dto/class.dto.ts1
-rw-r--r--server/dto/jwt_body.dto.ts3
-rw-r--r--server/entities/role.entity.ts15
-rw-r--r--server/entities/user_role.entity.ts8
-rw-r--r--server/modules/users.module.ts8
-rw-r--r--server/providers/guards/auth.guard.ts17
-rw-r--r--server/providers/guards/roles.guard.ts37
-rw-r--r--server/providers/services/roles.service.ts25
-rw-r--r--server/providers/services/users.service.ts6
17 files changed, 176 insertions, 24 deletions
diff --git a/server/app.controller.ts b/server/app.controller.ts
index a6bcf58..53c975f 100644
--- a/server/app.controller.ts
+++ b/server/app.controller.ts
@@ -1,8 +1,11 @@
import { Controller, Get, Render } from '@nestjs/common';
+import { Skip } from './decorators/skip.decorator';
+import { AuthGuard } from './providers/guards/auth.guard';
@Controller()
export class AppController {
@Get()
@Render('index')
+ @Skip(AuthGuard)
index() {}
}
diff --git a/server/app.module.ts b/server/app.module.ts
index e82aa66..41446f8 100644
--- a/server/app.module.ts
+++ b/server/app.module.ts
@@ -1,13 +1,24 @@
import { Module } from '@nestjs/common';
+import { APP_GUARD } from '@nestjs/core';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { config } from './database/config';
import { UsersModule } from './modules/users.module';
+import { AuthGuard } from './providers/guards/auth.guard';
+import { RolesGuard } from './providers/guards/roles.guard';
import { JwtService } from './providers/services/jwt.service';
+import { RolesService } from './providers/services/roles.service';
+import { UsersService } from './providers/services/users.service';
@Module({
imports: [TypeOrmModule.forRoot(config), UsersModule],
controllers: [AppController],
- providers: [JwtService],
+ providers: [
+ UsersService,
+ RolesService,
+ JwtService,
+ { provide: APP_GUARD, useClass: AuthGuard }, // auth guard should come before roles guard
+ { provide: APP_GUARD, useClass: RolesGuard }, // otherwise users won't be authenticated before roles check
+ ],
})
export class AppModule {}
diff --git a/server/controllers/refresh_tokens.controller.ts b/server/controllers/refresh_tokens.controller.ts
index 2a24abe..6aa696f 100644
--- a/server/controllers/refresh_tokens.controller.ts
+++ b/server/controllers/refresh_tokens.controller.ts
@@ -4,14 +4,18 @@ 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';
+import { Skip } from 'server/decorators/skip.decorator';
+import { AuthGuard } from 'server/providers/guards/auth.guard';
+import { RolesService } from 'server/providers/services/roles.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) {}
+ constructor(private usersService: UsersService, private rolesService: RolesService, private jwtService: JwtService) {}
@Get('/refresh_token')
+ @Skip(AuthGuard)
async get(@Body() body: SignInDto, @Req() req: Request) {
const refreshToken: string = req.cookies['_refresh_token'];
if (!refreshToken) {
@@ -20,13 +24,15 @@ export class RefreshTokensController {
const tokenBody = this.jwtService.parseRefreshToken(refreshToken) as RefreshTokenBody;
- const user = await this.usersService.find(tokenBody.userId, ['refreshTokens']);
+ const user = await this.usersService.find(tokenBody.userId, ['refreshTokens', 'userRoles']);
+ const userRoles = await this.rolesService.findByIds(user.userRoles.map((ur) => ur.roleId));
+
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 });
+ const token = this.jwtService.issueToken({ userId: user.id, roles: userRoles.map((r) => r.key) });
return { token };
}
}
diff --git a/server/controllers/sessions.controller.ts b/server/controllers/sessions.controller.ts
index 9ae647b..e1d1155 100644
--- a/server/controllers/sessions.controller.ts
+++ b/server/controllers/sessions.controller.ts
@@ -5,6 +5,9 @@ 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';
+import { Skip } from 'server/decorators/skip.decorator';
+import { AuthGuard } from 'server/providers/guards/auth.guard';
+import { RolesService } from 'server/providers/services/roles.service';
// this is kind of a misnomer because we are doing token based auth
// instead of session based auth
@@ -13,10 +16,12 @@ export class SessionsController {
constructor(
private usersService: UsersService,
private jwtService: JwtService,
+ private rolesService: RolesService,
private refreshTokenService: RefreshTokensService,
) {}
@Post('/sessions')
+ @Skip(AuthGuard)
async create(@Body() body: SignInDto, @Res({ passthrough: true }) res: Response) {
const { verified, user } = await this.usersService.verify(body.email, body.password);
@@ -32,8 +37,10 @@ export class SessionsController {
// generate new refresh token
}
+ const userRoles = await this.rolesService.findByIds(user.userRoles.map((ur) => ur.roleId));
+
// JWT gets sent with response
- const token = this.jwtService.issueToken({ userId: user.id });
+ const token = this.jwtService.issueToken({ userId: user.id, roles: userRoles.map((r) => r.key) });
const refreshJwtToken = this.jwtService.issueRefreshToken({ id: refreshToken.id, userId: user.id });
diff --git a/server/controllers/users.controller.ts b/server/controllers/users.controller.ts
index f9aba90..fda71b3 100644
--- a/server/controllers/users.controller.ts
+++ b/server/controllers/users.controller.ts
@@ -2,36 +2,53 @@ import { Body, Controller, Get, HttpException, HttpStatus, Post, Res, UseGuards
import * as bcrypt from 'bcrypt';
import { Response } from 'express';
import { JwtBody } from 'server/decorators/jwt_body.decorator';
+import { Roles } from 'server/decorators/roles.decorator';
+import { Skip } from 'server/decorators/skip.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 { RoleKey } from 'server/entities/role.entity';
import { User } from 'server/entities/user.entity';
+import { UserRole } from 'server/entities/user_role.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 { RolesService } from 'server/providers/services/roles.service';
import { UsersService } from 'server/providers/services/users.service';
@Controller()
export class UsersController {
constructor(
private usersService: UsersService,
+ private rolesService: RolesService,
private jwtService: JwtService,
private refreshTokenService: RefreshTokensService,
) {}
+ @Get('/users')
+ @Roles(RoleKey.ADMIN)
+ async index() {
+ const users = await this.usersService.findAll();
+ return { users };
+ }
+
@Get('/users/me')
- @UseGuards(AuthGuard)
async getCurrentUser(@JwtBody() jwtBody: JwtBodyDto) {
const user = await this.usersService.find(jwtBody.userId);
return { user };
}
@Post('/users')
+ @Skip(AuthGuard)
async create(@Body() userPayload: CreateUserDto, @Res({ passthrough: true }) res: Response) {
const newUser = new User();
newUser.email = userPayload.email;
newUser.name = userPayload.name;
newUser.passwordHash = await bcrypt.hash(userPayload.password, 10);
+ const [role] = await this.rolesService.findByKey(RoleKey.USER);
+ const userRole = new UserRole();
+ userRole.role = role;
+ newUser.userRoles = [userRole];
try {
const user = await this.usersService.create(newUser);
@@ -39,9 +56,11 @@ export class UsersController {
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 });
+ // note the roles hard coded to just USER.
+ // If you want to allow users to sign up as different roles then
+ // you will need to update this here.
+ const token = this.jwtService.issueToken({ userId: user.id, roles: [RoleKey.USER] });
const refreshJwtToken = this.jwtService.issueRefreshToken({ id: refreshToken.id, userId: user.id });
// only refresh token should go in the cookie
diff --git a/server/database/seeds.ts b/server/database/seeds.ts
index 101b48b..94e69ef 100644
--- a/server/database/seeds.ts
+++ b/server/database/seeds.ts
@@ -1,7 +1,7 @@
import { Factory, Seeder } from 'typeorm-seeding';
-import { Connection, Db } from 'typeorm';
+import { Connection } from 'typeorm';
import { User } from '../entities/user.entity';
-import { Role } from '../entities/role.entity';
+import { Role, RoleKey } from '../entities/role.entity';
import * as dotenv from 'dotenv';
import * as bcrypt from 'bcrypt';
import { UserRole } from '../entities/user_role.entity';
@@ -11,6 +11,7 @@ export default class Seeds implements Seeder {
public async run(factory: Factory, connection: Connection): Promise<any> {
// CREATE ROLES
console.log('\nCreating Roles');
+
const roleObjects = Role.ROLES.map((key) => ({ key }));
const roleRepository = connection.getRepository(Role);
for (const roleObj of roleObjects) {
@@ -26,10 +27,9 @@ export default class Seeds implements Seeder {
// CREATE ADMIN USER
const userRepository = connection.getRepository(User);
- const userRoleRepository = connection.getRepository(UserRole);
let adminUser = await userRepository.findOne({ email: process.env.ADMIN_EMAIL });
if (!adminUser) {
- const adminRole = await roleRepository.findOne({ key: Role.ADMIN });
+ const adminRole = await roleRepository.findOne({ key: RoleKey.ADMIN });
console.log(`\nCreating Admin User with email ${process.env.ADMIN_EMAIL}`);
console.log(adminRole);
const passwordHash = await bcrypt.hash(process.env.ADMIN_PASSWORD, 10);
diff --git a/server/decorators/roles.decorator.ts b/server/decorators/roles.decorator.ts
new file mode 100644
index 0000000..c51d3d0
--- /dev/null
+++ b/server/decorators/roles.decorator.ts
@@ -0,0 +1,5 @@
+import { SetMetadata } from '@nestjs/common';
+import { RoleKey } from 'server/entities/role.entity';
+
+export const ROLES_CONTEXT_KEY = 'roles';
+export const Roles = (...roles: RoleKey[]) => SetMetadata(ROLES_CONTEXT_KEY, roles);
diff --git a/server/decorators/skip.decorator.ts b/server/decorators/skip.decorator.ts
new file mode 100644
index 0000000..6cd438f
--- /dev/null
+++ b/server/decorators/skip.decorator.ts
@@ -0,0 +1,5 @@
+import { CanActivate, SetMetadata } from '@nestjs/common';
+import { Class } from 'server/dto/class.dto';
+
+export const SKIP_KEY = 'skip';
+export const Skip = (...guards: Class<CanActivate>[]) => SetMetadata(SKIP_KEY, guards);
diff --git a/server/dto/class.dto.ts b/server/dto/class.dto.ts
new file mode 100644
index 0000000..b4181a1
--- /dev/null
+++ b/server/dto/class.dto.ts
@@ -0,0 +1 @@
+export type Class<T> = { new (...args: any[]): T };
diff --git a/server/dto/jwt_body.dto.ts b/server/dto/jwt_body.dto.ts
index f8a1179..dc2e1b6 100644
--- a/server/dto/jwt_body.dto.ts
+++ b/server/dto/jwt_body.dto.ts
@@ -1,3 +1,6 @@
+import { RoleKey } from 'server/entities/role.entity';
+
export interface JwtBodyDto {
userId: number;
+ roles: RoleKey[];
}
diff --git a/server/entities/role.entity.ts b/server/entities/role.entity.ts
index 35b4ac3..da33726 100644
--- a/server/entities/role.entity.ts
+++ b/server/entities/role.entity.ts
@@ -1,20 +1,21 @@
import { Entity, PrimaryGeneratedColumn, OneToMany, Column } from 'typeorm';
import { UserRole } from './user_role.entity';
+// Make sure to add aditional roles here then reseed
+export enum RoleKey {
+ ADMIN = 'admin',
+ USER = 'user',
+}
+
@Entity()
export class Role {
- static ADMIN = 'admin';
- static USER = 'user';
-
- // make sure add additional roles to this arraylist as it
- // will be used during seeds to initiallize all roles.
- static ROLES = [Role.ADMIN, Role.USER];
+ static ROLES = [RoleKey.ADMIN, RoleKey.USER];
@PrimaryGeneratedColumn()
id: number;
@Column()
- key: string;
+ key: RoleKey;
@OneToMany(() => UserRole, (userRole) => userRole.role)
userRoles: UserRole[];
diff --git a/server/entities/user_role.entity.ts b/server/entities/user_role.entity.ts
index 0a6c5c6..e680a3a 100644
--- a/server/entities/user_role.entity.ts
+++ b/server/entities/user_role.entity.ts
@@ -1,4 +1,4 @@
-import { Entity, PrimaryGeneratedColumn, ManyToOne } from 'typeorm';
+import { Entity, PrimaryGeneratedColumn, ManyToOne, Column } from 'typeorm';
import { Role } from './role.entity';
import { User } from './user.entity';
@@ -7,6 +7,12 @@ export class UserRole {
@PrimaryGeneratedColumn()
id: number;
+ @Column()
+ roleId: number;
+
+ @Column()
+ userId: number;
+
@ManyToOne(() => Role, (role) => role.userRoles)
role: Role;
diff --git a/server/modules/users.module.ts b/server/modules/users.module.ts
index 4519937..69c533b 100644
--- a/server/modules/users.module.ts
+++ b/server/modules/users.module.ts
@@ -8,10 +8,14 @@ import { RefreshTokensService } from '../providers/services/refresh_tokens.servi
import { RefreshToken } from 'server/entities/refresh_token.entity';
import { JwtService } from 'server/providers/services/jwt.service';
import { RefreshTokensController } from 'server/controllers/refresh_tokens.controller';
+import { Role } from 'server/entities/role.entity';
+import { RolesService } from 'server/providers/services/roles.service';
+import { UserRole } from 'server/entities/user_role.entity';
@Module({
- imports: [TypeOrmModule.forFeature([User, RefreshToken])],
+ imports: [TypeOrmModule.forFeature([User, RefreshToken, Role, UserRole])],
controllers: [SessionsController, UsersController, RefreshTokensController],
- providers: [UsersService, RefreshTokensService, JwtService],
+ providers: [UsersService, RolesService, RefreshTokensService, JwtService],
+ exports: [TypeOrmModule],
})
export class UsersModule {}
diff --git a/server/providers/guards/auth.guard.ts b/server/providers/guards/auth.guard.ts
index d7da81e..8c03ad8 100644
--- a/server/providers/guards/auth.guard.ts
+++ b/server/providers/guards/auth.guard.ts
@@ -1,13 +1,28 @@
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { JwtService } from '../services/jwt.service';
+import { SKIP_KEY } from 'server/decorators/skip.decorator';
+import { Reflector } from '@nestjs/core';
+import { Class } from 'server/dto/class.dto';
@Injectable()
export class AuthGuard implements CanActivate {
- constructor(private jwtService: JwtService) {}
+ constructor(private reflector: Reflector, private jwtService: JwtService) {}
canActivate(context: ExecutionContext) {
+ const skippedGuards = this.reflector.getAllAndOverride<Class<CanActivate>[]>(SKIP_KEY, [
+ context.getHandler(),
+ context.getClass(),
+ ]);
+ if (skippedGuards) {
+ const skippedGuard = skippedGuards.find((guard) => this instanceof guard);
+ if (skippedGuard) {
+ return true;
+ }
+ }
const req = context.switchToHttp().getRequest();
const authHeader = req.headers.authorization;
+ if (!authHeader) return false;
+
const jwt = authHeader.split(' ')[1];
try {
req.jwtBody = this.jwtService.parseToken(jwt);
diff --git a/server/providers/guards/roles.guard.ts b/server/providers/guards/roles.guard.ts
index e69de29..3ecc392 100644
--- a/server/providers/guards/roles.guard.ts
+++ b/server/providers/guards/roles.guard.ts
@@ -0,0 +1,37 @@
+import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
+import { Reflector } from '@nestjs/core';
+import { ROLES_CONTEXT_KEY } from 'server/decorators/roles.decorator';
+import { JwtBodyDto } from 'server/dto/jwt_body.dto';
+import { RoleKey } from 'server/entities/role.entity';
+import { RolesService } from '../services/roles.service';
+import { UsersService } from '../services/users.service';
+import { some } from 'lodash';
+
+@Injectable()
+export class RolesGuard implements CanActivate {
+ constructor(private reflector: Reflector, private usersService: UsersService, private rolesService: RolesService) {}
+
+ async canActivate(context: ExecutionContext): Promise<boolean> {
+ const requiredRoles = this.reflector.getAllAndOverride<RoleKey[]>(ROLES_CONTEXT_KEY, [
+ context.getHandler(),
+ context.getClass(),
+ ]);
+ console.log(requiredRoles);
+
+ if (!requiredRoles) {
+ return true;
+ }
+
+ const jwtBody: JwtBodyDto = context.switchToHttp().getRequest().jwtBody;
+
+ if (!jwtBody) return false; // unauthenticated users are not authorized
+
+ const user = await this.usersService.find(jwtBody.userId, ['userRoles']);
+ const roles = await this.rolesService.findByKey(...requiredRoles);
+ const roleMatches = user.userRoles.map((userRole) => {
+ return !!roles.find((role) => role.id === userRole.roleId);
+ });
+
+ return some(roleMatches);
+ }
+}
diff --git a/server/providers/services/roles.service.ts b/server/providers/services/roles.service.ts
new file mode 100644
index 0000000..7cd17ef
--- /dev/null
+++ b/server/providers/services/roles.service.ts
@@ -0,0 +1,25 @@
+import { Injectable } from '@nestjs/common';
+import { InjectRepository } from '@nestjs/typeorm';
+import { In, Repository } from 'typeorm';
+import * as bcrypt from 'bcrypt';
+import { Role, RoleKey } from 'server/entities/role.entity';
+
+@Injectable()
+export class RolesService {
+ constructor(
+ @InjectRepository(Role)
+ private rolesRepository: Repository<Role>,
+ ) {}
+
+ findByKey(...keys: RoleKey[]) {
+ return this.rolesRepository.find({ where: { key: In(keys) } });
+ }
+
+ findByIds(ids: number[]) {
+ return this.rolesRepository.findByIds(ids);
+ }
+
+ find(id: number, relations: string[] = []) {
+ return this.rolesRepository.findOne(id, { relations });
+ }
+}
diff --git a/server/providers/services/users.service.ts b/server/providers/services/users.service.ts
index 47a0360..c3ee086 100644
--- a/server/providers/services/users.service.ts
+++ b/server/providers/services/users.service.ts
@@ -11,6 +11,10 @@ export class UsersService {
private usersRespository: Repository<User>,
) {}
+ findAll(relations: string[] = []) {
+ return this.usersRespository.find({ relations });
+ }
+
findBy(options: Record<string, any>, relations: string[] = []) {
return this.usersRespository.findOne(options, { relations });
}
@@ -24,7 +28,7 @@ export class UsersService {
}
async verify(email: string, password: string) {
- const user = await this.usersRespository.findOne({ email }, { relations: ['refreshTokens'] });
+ const user = await this.usersRespository.findOne({ email }, { relations: ['refreshTokens', 'userRoles'] });
if (!user) return { verified: false, user: null };
const verified: boolean = await bcrypt.compare(password, user.passwordHash);
return { verified, user: verified ? user : null };