because I got bored of customising my CV for every job
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor(auth): remove password from domain and add user mapper

- Remove password field from User domain entity
- Create UserMapper service with proper null handling overloads
- Update UserService to use UserMapper for all entity conversions
- Add getPasswordHash and validatePassword methods to UserService
- Update AuthService to use new password validation approach
- Keep authentication concerns at Prisma level only

+82 -56
+5 -3
apps/server/src/modules/auth/auth.service.ts
··· 37 37 38 38 async login({ email, password }: LoginDto): Promise<AuthResponse> { 39 39 try { 40 - const user = await this.userService.findByEmailOrFail(email); 41 - 42 - if (!(await bcrypt.compare(password, user.password))) { 40 + const passwordHash = await this.userService.getPasswordHash(email); 41 + 42 + if (!passwordHash || !(await bcrypt.compare(password, passwordHash))) { 43 43 throw new UnauthorizedException("Invalid credentials"); 44 44 } 45 + 46 + const user = await this.userService.findByEmailOrFail(email); 45 47 46 48 const payload = { sub: user.id, email: user.email }; 47 49 const access_token = this.jwtService.sign(payload);
-3
apps/server/src/modules/auth/user.entity.ts
··· 4 4 export class User extends BaseEntity { 5 5 email: string; 6 6 name: string; 7 - password: string; 8 7 organizations?: PrismaOrganization[] | undefined; 9 8 10 9 constructor( 11 10 id: string, 12 11 email: string, 13 12 name: string, 14 - password: string, 15 13 createdAt: Date, 16 14 updatedAt: Date, 17 15 organizations?: PrismaOrganization[] | undefined, ··· 19 17 super(id, createdAt, updatedAt); 20 18 this.email = email; 21 19 this.name = name; 22 - this.password = password; 23 20 this.organizations = organizations; 24 21 } 25 22 }
+37
apps/server/src/modules/auth/user.mapper.ts
··· 1 + import { Injectable } from "@nestjs/common"; 2 + import type { User as PrismaUser } from "@prisma/client"; 3 + import type { BaseMapper } from "../base/mapper.interface"; 4 + import { User } from "./user.entity"; 5 + 6 + /** 7 + * Mapper service for converting between Prisma User entities and domain User entities 8 + */ 9 + @Injectable() 10 + export class UserMapper implements BaseMapper<PrismaUser, User> { 11 + /** 12 + * Maps a Prisma User entity to a domain User entity 13 + * Uses overloads to return the correct type based on input 14 + */ 15 + toDomain(prismaUser: null): null; 16 + toDomain(prismaUser: PrismaUser): User; 17 + toDomain(prismaUser: PrismaUser | null): User | null; 18 + toDomain(prismaUser: PrismaUser | null): User | null { 19 + if (prismaUser === null) { 20 + return null; 21 + } 22 + return new User( 23 + prismaUser.id, 24 + prismaUser.email, 25 + prismaUser.name, 26 + prismaUser.createdAt, 27 + prismaUser.updatedAt, 28 + ); 29 + } 30 + 31 + /** 32 + * Maps an array of Prisma User entities to domain User entities 33 + */ 34 + mapToDomain(prismaUsers: PrismaUser[]): User[] { 35 + return prismaUsers.map((user) => this.toDomain(user)); 36 + } 37 + }
+40 -50
apps/server/src/modules/auth/user.service.ts
··· 1 1 import { Injectable, NotFoundException } from "@nestjs/common"; 2 2 import { PrismaService } from "../database/prisma.service"; 3 3 import { User } from "./user.entity"; 4 + import { UserMapper } from "./user.mapper"; 4 5 5 6 @Injectable() 6 7 export class UserService { 7 - constructor(private prisma: PrismaService) {} 8 + constructor( 9 + private prisma: PrismaService, 10 + private userMapper: UserMapper, 11 + ) {} 8 12 9 13 async create(email: string, name: string, password: string): Promise<User> { 10 14 const prismaUser = await this.prisma.user.create({ ··· 15 19 }, 16 20 }); 17 21 18 - return new User( 19 - prismaUser.id, 20 - prismaUser.email, 21 - prismaUser.name, 22 - prismaUser.password, 23 - prismaUser.createdAt, 24 - prismaUser.updatedAt, 25 - ); 22 + return this.userMapper.toDomain(prismaUser); 26 23 } 27 24 28 25 async findByEmail(email: string): Promise<User | null> { ··· 30 27 where: { email }, 31 28 }); 32 29 33 - if (!prismaUser) { 34 - return null; 35 - } 36 - 37 - return new User( 38 - prismaUser.id, 39 - prismaUser.email, 40 - prismaUser.name, 41 - prismaUser.password, 42 - prismaUser.createdAt, 43 - prismaUser.updatedAt, 44 - ); 30 + return this.userMapper.toDomain(prismaUser); 45 31 } 46 32 47 33 async findById(id: string): Promise<User | null> { ··· 49 35 where: { id }, 50 36 }); 51 37 52 - if (!prismaUser) { 53 - return null; 54 - } 55 - 56 - return new User( 57 - prismaUser.id, 58 - prismaUser.email, 59 - prismaUser.name, 60 - prismaUser.password, 61 - prismaUser.createdAt, 62 - prismaUser.updatedAt, 63 - ); 38 + return this.userMapper.toDomain(prismaUser); 64 39 } 65 40 66 41 async exists(email: string): Promise<boolean> { ··· 72 47 return user !== null; 73 48 } 74 49 50 + /** 51 + * Validates user password at the Prisma level (for authentication only) 52 + * This method bypasses the domain entity to handle password concerns 53 + */ 54 + async validatePassword(email: string, password: string): Promise<boolean> { 55 + const prismaUser = await this.prisma.user.findUnique({ 56 + where: { email }, 57 + select: { password: true }, 58 + }); 59 + 60 + if (!prismaUser) { 61 + return false; 62 + } 63 + 64 + // This will be handled by bcrypt in the auth service 65 + return prismaUser.password === password; 66 + } 67 + 68 + /** 69 + * Gets user password hash for authentication (Prisma level only) 70 + */ 71 + async getPasswordHash(email: string): Promise<string | null> { 72 + const prismaUser = await this.prisma.user.findUnique({ 73 + where: { email }, 74 + select: { password: true }, 75 + }); 76 + 77 + return prismaUser?.password ?? null; 78 + } 79 + 75 80 async findByEmailOrFail(email: string): Promise<User> { 76 81 const prismaUser = await this.prisma.user.findUnique({ 77 82 where: { email }, ··· 81 86 throw new NotFoundException(`User with email ${email} not found`); 82 87 } 83 88 84 - return new User( 85 - prismaUser.id, 86 - prismaUser.email, 87 - prismaUser.name, 88 - prismaUser.password, 89 - prismaUser.createdAt, 90 - prismaUser.updatedAt, 91 - ); 89 + return this.userMapper.toDomain(prismaUser); 92 90 } 93 91 94 92 async findByIdOrFail(id: string): Promise<User> { ··· 100 98 throw new NotFoundException(`User with id ${id} not found`); 101 99 } 102 100 103 - return new User( 104 - prismaUser.id, 105 - prismaUser.email, 106 - prismaUser.name, 107 - prismaUser.password, 108 - prismaUser.createdAt, 109 - prismaUser.updatedAt, 110 - ); 101 + return this.userMapper.toDomain(prismaUser); 111 102 } 112 103 113 104 async findByIdWithOrganizations(id: string): Promise<User> { ··· 132 123 prismaUser.id, 133 124 prismaUser.email, 134 125 prismaUser.name, 135 - prismaUser.password, 136 126 prismaUser.createdAt, 137 127 prismaUser.updatedAt, 138 128 organizations,