Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- CreateEnum
CREATE TYPE "UserType" AS ENUM ('PLANNER', 'SPEAKER');

-- AlterTable
ALTER TABLE "User" ADD COLUMN "type" "UserType" NOT NULL DEFAULT 'SPEAKER';
6 changes: 6 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ enum TalkLevel {
ADVANCED
}

enum UserType {
PLANNER
SPEAKER
}

model Room {
id String @id @default(cuid())
name String
Expand Down Expand Up @@ -67,6 +72,7 @@ model User {
id String @id @default(cuid())
email String @unique
password String
type UserType @default(SPEAKER)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
3 changes: 3 additions & 0 deletions src/adapters/api/controller/room.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
ApiOperation,
ApiUnauthorizedResponse,
} from '@nestjs/swagger';
import { Roles } from '../decorator/roles.decorator';
import { UserType } from '../../../core/domain/type/UserType';

@Controller('/rooms')
export class RoomController {
Expand Down Expand Up @@ -63,6 +65,7 @@ export class RoomController {
}

@UseGuards(JwtAuthGuard)
@Roles(UserType.PLANNER)
@Post()
@ApiOperation({ summary: 'Create a new room' })
@ApiCreatedResponse({
Expand Down
4 changes: 4 additions & 0 deletions src/adapters/api/controller/talk.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import { GetAllTalksByStatusUseCase } from '../../../core/usecases/get-all-talks
import { TalkStatus } from '../../../core/domain/type/TalkStatus';
import { GetAllTalksWithRoomDetailResponse } from '../response/get-all-talks-with-room-detail.response';
import { GetAllTalksWithRoomDetailMapper } from '../mapper/get-all-talks-with-room-detail.mapper';
import { UserType } from '../../../core/domain/type/UserType';
import { Roles } from '../decorator/roles.decorator';

@Controller('/talks')
export class TalkController {
Expand Down Expand Up @@ -68,6 +70,7 @@ export class TalkController {

@UseGuards(JwtAuthGuard)
@Post()
@Roles(UserType.PLANNER, UserType.SPEAKER)
@ApiOperation({ summary: 'Create a new talk' })
@ApiCreatedResponse({
description: 'Talk successfully created',
Expand Down Expand Up @@ -99,6 +102,7 @@ export class TalkController {
}

@UseGuards(JwtAuthGuard)
@Roles(UserType.PLANNER)
@Post('/:talkId/approve-or-reject')
@ApiOperation({ summary: 'Accept or reject a talk' })
@ApiNoContentResponse({
Expand Down
4 changes: 4 additions & 0 deletions src/adapters/api/decorator/roles.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { SetMetadata } from '@nestjs/common';
export const ROLES_KEY = 'roles';

export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
47 changes: 47 additions & 0 deletions src/adapters/api/guards/roles.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Request } from 'express';
import { UserType } from '../../../core/domain/type/UserType';
import { ROLES_KEY } from '../decorator/roles.decorator';

type AuthenticatedUser = {
id: string;
email: string;
type: UserType;
};

@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}

canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<UserType[]>(
ROLES_KEY,
[context.getHandler(), context.getClass()],
);

if (!requiredRoles || requiredRoles.length === 0) {
return true;
}

const request = context
.switchToHttp()
.getRequest<Request & { user?: AuthenticatedUser }>();
const user = request.user;

if (!user) {
throw new UnauthorizedException('User not authenticated');
}

if (!requiredRoles.includes(user.type)) {
throw new UnauthorizedException('Insufficient permissions');
}

return true;
}
}
3 changes: 2 additions & 1 deletion src/adapters/in-memory/in-memory-user.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ import { Injectable } from '@nestjs/common';
export class InMemoryUserRepository implements UserRepository {
private users: Map<string, User> = new Map();

create(data: Pick<User, 'email' | 'password'>): User {
create(data: Pick<User, 'email' | 'password' | 'type'>): User {
const user = new User(
crypto.randomUUID(),
data.email,
data.password,
data.type,
new Date(),
new Date(),
);
Expand Down
17 changes: 16 additions & 1 deletion src/adapters/prisma/mapper/prisma-user.mapper.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { EntityMapper } from '../../../core/base/entity-mapper';
import { User } from '../../../core/domain/model/User';
import { User as UserEntity } from '@prisma/client';
import { User as UserEntity, $Enums } from '@prisma/client';
import { Injectable } from '@nestjs/common';

import { UserType } from '../../../core/domain/type/UserType';

@Injectable()
export class PrismaUserMapper implements EntityMapper<User, UserEntity> {
fromDomain(model: User): UserEntity {
return {
id: model.id,
email: model.email,
password: model.password,
type: model.type,
updatedAt: model.updatedAt,
createdAt: model.createdAt,
};
Expand All @@ -20,8 +23,20 @@ export class PrismaUserMapper implements EntityMapper<User, UserEntity> {
id: entity.id,
email: entity.email,
password: entity.password,
type: this.mapUserTypeToDomain(entity.type),
updatedAt: entity.updatedAt,
createdAt: entity.createdAt,
};
}

private mapUserTypeToDomain(type: $Enums.UserType): UserType {
switch (type) {
case 'PLANNER':
return UserType.PLANNER;
case 'SPEAKER':
return UserType.SPEAKER;
default:
throw new Error('Invalid user type');
}
}
}
3 changes: 2 additions & 1 deletion src/adapters/prisma/prisma-user.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,10 @@ export class PrismaUserRepository implements UserRepository {
}

async update(id: string, user: User): Promise<User | null> {
const entity = this.mapper.fromDomain(user);
const updatedEntity = await this.prisma.user.update({
where: { id },
data: user,
data: entity,
});
if (!updatedEntity) {
return null;
Expand Down
4 changes: 4 additions & 0 deletions src/core/domain/model/User.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
import { DomainModel } from '../../base/domain-model';
import { UserType } from '../type/UserType';

export class User extends DomainModel {
email: string;
password: string;
type: UserType;
updatedAt: Date;
createdAt: Date;

constructor(
id: string,
email: string,
password: string,
type: UserType,
updatedAt?: Date,
createdAt?: Date,
) {
super(id);
this.email = email;
this.password = password;
this.type = type;
this.updatedAt = updatedAt || new Date();
this.createdAt = createdAt || new Date();
}
Expand Down
4 changes: 4 additions & 0 deletions src/core/domain/type/UserType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum UserType {
PLANNER = 'PLANNER',
SPEAKER = 'SPEAKER',
}
1 change: 1 addition & 0 deletions src/core/usecases/__test__/create-user.use-case.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ describe('CreateUserUseCase', () => {
email: 'john.doe@example.com',
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
password: expect.any(String),
type: 'SPEAKER',
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
createdAt: expect.any(Date),
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
Expand Down
8 changes: 7 additions & 1 deletion src/core/usecases/create-user.use-case.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { WrongEmailFormatError } from '../domain/error/WrongEmailFormatError';
import { WrongPasswordFormatError } from '../domain/error/WrongPasswordFormatError';
import { User } from '../domain/model/User';
import { UserRepository } from '../domain/repository/user.repository';
import { UserType } from '../domain/type/UserType';
import * as bcrypt from 'bcryptjs';

export type CreateUserCommand = {
Expand Down Expand Up @@ -36,7 +37,12 @@ export class CreateUserUseCase implements UseCase<CreateUserCommand, User> {
}

const hashedPassword = await bcrypt.hash(password, 10);
const user = new User(this.generateId(), email, hashedPassword);
const user = new User(
this.generateId(),
email,
hashedPassword,
UserType.SPEAKER,
);

await this.userRepository.create(user);
return user;
Expand Down