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(server): update core modules and services

+156 -302
-5
apps/server/src/modules/app/app.resolver.ts
··· 6 6 export class AppResolver { 7 7 constructor(private readonly appService: AppService) {} 8 8 9 - @Query(() => String) 10 - hello(): string { 11 - return this.appService.getHello(); 12 - } 13 - 14 9 @Query(() => HealthResponse) 15 10 health(): HealthResponse { 16 11 return new HealthResponse(this.appService.getHealth());
-4
apps/server/src/modules/app/app.service.ts
··· 11 11 export class AppService { 12 12 private readonly startTime = Date.now(); 13 13 14 - getHello(): string { 15 - return "Hello from NestJS server"; 16 - } 17 - 18 14 getHealth(): HealthResponse { 19 15 const now = new Date(); 20 16 return {
+12
apps/server/src/modules/auth/auth.dto.ts
··· 11 11 12 12 export interface AuthResponse { 13 13 access_token: string; 14 + refresh_token: string; 15 + expires_at: Date; 14 16 user: { 15 17 id: string; 16 18 email: string; ··· 18 20 createdAt: Date; 19 21 }; 20 22 } 23 + 24 + export interface RefreshTokenDto { 25 + refresh_token: string; 26 + } 27 + 28 + export interface RefreshTokenResponse { 29 + access_token: string; 30 + refresh_token: string; 31 + expires_at: Date; 32 + }
+6 -14
apps/server/src/modules/auth/auth.module.ts
··· 1 - import { Global, Module } from "@nestjs/common"; 1 + import { Module } from "@nestjs/common"; 2 2 import { ConfigModule, ConfigService } from "@nestjs/config"; 3 3 import { JwtModule } from "@nestjs/jwt"; 4 - import { DatabaseModule } from "../database/database.module"; 4 + import { DatabaseModule } from "@/modules/database/database.module"; 5 + import { UserModule } from "@/modules/user/user.module"; 5 6 import { AuthResolver } from "./auth.resolver"; 6 7 import { AuthService } from "./auth.service"; 7 8 import { JwtAuthGuard } from "./jwt-auth.guard"; 8 9 import { MeResolver } from "./me.resolver"; 9 - import { UserMapper } from "./user.mapper"; 10 - import { UserService } from "./user.service"; 11 10 12 - @Global() 13 11 @Module({ 14 12 imports: [ 15 13 ConfigModule, 16 14 DatabaseModule, 15 + UserModule, 17 16 JwtModule.registerAsync({ 18 17 imports: [ConfigModule], 19 18 useFactory: async (configService: ConfigService) => { ··· 26 25 inject: [ConfigService], 27 26 }), 28 27 ], 29 - providers: [ 30 - AuthService, 31 - UserService, 32 - AuthResolver, 33 - MeResolver, 34 - JwtAuthGuard, 35 - UserMapper, 36 - ], 37 - exports: [AuthService, UserService, JwtModule, JwtAuthGuard, UserMapper], 28 + providers: [AuthService, AuthResolver, MeResolver, JwtAuthGuard], 29 + exports: [AuthService, JwtModule, JwtAuthGuard], 38 30 }) 39 31 export class AuthModule {}
+13 -3
apps/server/src/modules/auth/auth.resolver.ts
··· 1 1 import { Args, Mutation, Resolver } from "@nestjs/graphql"; 2 2 import { AuthService } from "./auth.service"; 3 - import { AuthResponse } from "./auth.type"; 3 + import { AuthResponse, RefreshTokenResponse } from "./auth.type"; 4 4 5 5 @Resolver() 6 6 export class AuthResolver { ··· 11 11 @Args("email") email: string, 12 12 @Args("password") password: string, 13 13 ): Promise<AuthResponse> { 14 - return this.authService.login({ email, password }); 14 + const result = await this.authService.login({ email, password }); 15 + return AuthResponse.fromDomain(result); 15 16 } 16 17 17 18 @Mutation(() => AuthResponse) ··· 20 21 @Args("email") email: string, 21 22 @Args("password") password: string, 22 23 ): Promise<AuthResponse> { 23 - return this.authService.register({ name, email, password }); 24 + const result = await this.authService.register({ name, email, password }); 25 + return AuthResponse.fromDomain(result); 26 + } 27 + 28 + @Mutation(() => RefreshTokenResponse) 29 + async refreshToken( 30 + @Args("refresh_token") refresh_token: string, 31 + ): Promise<RefreshTokenResponse> { 32 + const result = await this.authService.refreshToken({ refresh_token }); 33 + return RefreshTokenResponse.fromDomain(result); 24 34 } 25 35 }
+67 -9
apps/server/src/modules/auth/auth.service.ts
··· 5 5 } from "@nestjs/common"; 6 6 import { JwtService } from "@nestjs/jwt"; 7 7 import * as bcrypt from "bcryptjs"; 8 - import type { AuthResponse, LoginDto, RegisterDto } from "./auth.dto"; 9 - import type { User } from "./user.entity"; 10 - import { UserService } from "./user.service"; 8 + import { JwtConfigService } from "@/config/jwt.config"; 9 + import type { User } from "@/modules/user/user.entity"; 10 + import { UserService } from "@/modules/user/user.service"; 11 + import type { 12 + AuthResponse, 13 + LoginDto, 14 + RefreshTokenDto, 15 + RefreshTokenResponse, 16 + RegisterDto, 17 + } from "./auth.dto"; 11 18 12 19 @Injectable() 13 20 export class AuthService { 14 21 constructor( 15 22 private userService: UserService, 16 23 private jwtService: JwtService, 24 + private jwtConfig: JwtConfigService, 17 25 ) {} 18 26 27 + private generateTokens(user: User): { 28 + access_token: string; 29 + refresh_token: string; 30 + expires_at: Date; 31 + } { 32 + const payload = { sub: user.id, email: user.email }; 33 + 34 + // Get token expiration times from config 35 + const accessTokenExpiry = this.jwtConfig.getAccessTokenExpiry(); 36 + const refreshTokenExpiry = this.jwtConfig.getRefreshTokenExpiry(); 37 + 38 + // Access token (short-lived) 39 + const access_token = this.jwtService.sign(payload, { 40 + expiresIn: accessTokenExpiry, 41 + }); 42 + 43 + // Refresh token (long-lived) 44 + const refresh_token = this.jwtService.sign( 45 + { sub: user.id, type: "refresh" }, 46 + { expiresIn: refreshTokenExpiry }, 47 + ); 48 + 49 + // Calculate expiry time based on the access token expiry 50 + const expires_at = this.jwtConfig.calculateAccessTokenExpiryDate(); 51 + 52 + return { access_token, refresh_token, expires_at }; 53 + } 54 + 19 55 async register({ 20 56 email, 21 57 name, ··· 29 65 30 66 const user = await this.userService.create(email, name, hashedPassword); 31 67 32 - const payload = { sub: user.id, email: user.email }; 33 - const access_token = this.jwtService.sign(payload); 68 + const tokens = this.generateTokens(user); 34 69 35 70 return { 36 - access_token, 71 + ...tokens, 37 72 user: { 38 73 id: user.id, 39 74 email: user.email, ··· 53 88 54 89 const user = await this.userService.findByEmailOrFail(email); 55 90 56 - const payload = { sub: user.id, email: user.email }; 57 - const access_token = this.jwtService.sign(payload); 91 + const tokens = this.generateTokens(user); 58 92 59 93 return { 60 - access_token, 94 + ...tokens, 61 95 user: { 62 96 id: user.id, 63 97 email: user.email, ··· 71 105 } 72 106 // If user not found, throw unauthorized for security 73 107 throw new UnauthorizedException("Invalid credentials"); 108 + } 109 + } 110 + 111 + async refreshToken({ 112 + refresh_token, 113 + }: RefreshTokenDto): Promise<RefreshTokenResponse> { 114 + try { 115 + // Verify the refresh token 116 + const payload = await this.jwtService.verifyAsync(refresh_token); 117 + 118 + // Ensure this is actually a refresh token 119 + if (payload.type !== "refresh") { 120 + throw new UnauthorizedException("Invalid refresh token"); 121 + } 122 + 123 + // Get the user 124 + const user = await this.userService.findByIdOrFail(payload.sub); 125 + 126 + // Generate new tokens 127 + const tokens = this.generateTokens(user); 128 + 129 + return tokens; 130 + } catch (_error) { 131 + throw new UnauthorizedException("Invalid or expired refresh token"); 74 132 } 75 133 } 76 134
+53 -3
apps/server/src/modules/auth/auth.type.ts
··· 1 1 import { Field, ObjectType } from "@nestjs/graphql"; 2 - import { User } from "./user.type"; 2 + import { User } from "@/modules/user/user.type"; 3 3 4 4 @ObjectType() 5 5 export class AuthResponse { 6 6 @Field() 7 7 access_token: string; 8 8 9 + @Field() 10 + refresh_token: string; 11 + 12 + @Field() 13 + expires_at: string; 14 + 9 15 @Field(() => User) 10 16 user: User; 11 17 12 - constructor(access_token: string, user: User) { 18 + constructor( 19 + access_token: string, 20 + refresh_token: string, 21 + expires_at: Date, 22 + user: User, 23 + ) { 13 24 this.access_token = access_token; 25 + this.refresh_token = refresh_token; 26 + this.expires_at = expires_at.toISOString(); 14 27 this.user = user; 15 28 } 16 29 17 30 static fromDomain(domainAuth: { 18 31 access_token: string; 32 + refresh_token: string; 33 + expires_at: Date; 19 34 user: User; 20 35 }): AuthResponse { 21 - return new AuthResponse(domainAuth.access_token, domainAuth.user); 36 + return new AuthResponse( 37 + domainAuth.access_token, 38 + domainAuth.refresh_token, 39 + domainAuth.expires_at, 40 + domainAuth.user, 41 + ); 42 + } 43 + } 44 + 45 + @ObjectType() 46 + export class RefreshTokenResponse { 47 + @Field() 48 + access_token: string; 49 + 50 + @Field() 51 + refresh_token: string; 52 + 53 + @Field() 54 + expires_at: string; 55 + 56 + constructor(access_token: string, refresh_token: string, expires_at: Date) { 57 + this.access_token = access_token; 58 + this.refresh_token = refresh_token; 59 + this.expires_at = expires_at.toISOString(); 60 + } 61 + 62 + static fromDomain(tokens: { 63 + access_token: string; 64 + refresh_token: string; 65 + expires_at: Date; 66 + }): RefreshTokenResponse { 67 + return new RefreshTokenResponse( 68 + tokens.access_token, 69 + tokens.refresh_token, 70 + tokens.expires_at, 71 + ); 22 72 } 23 73 }
+2 -2
apps/server/src/modules/auth/me.resolver.ts
··· 1 1 import { UseGuards } from "@nestjs/common"; 2 2 import { Query, Resolver } from "@nestjs/graphql"; 3 + import { UserService } from "@/modules/user/user.service"; 4 + import { User } from "@/modules/user/user.type"; 3 5 import { CurrentUser } from "./current-user.decorator"; 4 6 import { JwtAuthGuard } from "./jwt-auth.guard"; 5 - import { UserService } from "./user.service"; 6 - import { User } from "./user.type"; 7 7 8 8 @Resolver(() => User) 9 9 @UseGuards(JwtAuthGuard)
-22
apps/server/src/modules/auth/user.entity.ts
··· 1 - import type { Organization as PrismaOrganization } from "@prisma/client"; 2 - import { BaseEntity } from "../base/base.entity"; 3 - 4 - export class User extends BaseEntity { 5 - email: string; 6 - name: string; 7 - organizations?: PrismaOrganization[] | undefined; 8 - 9 - constructor( 10 - id: string, 11 - email: string, 12 - name: string, 13 - createdAt: Date, 14 - updatedAt: Date, 15 - organizations?: PrismaOrganization[] | undefined, 16 - ) { 17 - super(id, createdAt, updatedAt); 18 - this.email = email; 19 - this.name = name; 20 - this.organizations = organizations; 21 - } 22 - }
-64
apps/server/src/modules/auth/user.mapper.ts
··· 1 - import { Injectable } from "@nestjs/common"; 2 - import type { 3 - Organization as PrismaOrganization, 4 - User as PrismaUser, 5 - } from "@prisma/client"; 6 - import type { BaseMapper } from "../base/mapper.interface"; 7 - import { User } from "./user.entity"; 8 - 9 - /** 10 - * Mapper service for converting between Prisma User entities and domain User entities 11 - */ 12 - @Injectable() 13 - export class UserMapper implements BaseMapper<PrismaUser, User> { 14 - /** 15 - * Maps a Prisma User entity to a domain User entity 16 - * Uses overloads to return the correct type based on input 17 - */ 18 - toDomain(prismaUser: null): null; 19 - toDomain(prismaUser: PrismaUser): User; 20 - toDomain(prismaUser: PrismaUser | null): User | null; 21 - toDomain(prismaUser: PrismaUser | null): User | null { 22 - if (prismaUser === null) { 23 - return null; 24 - } 25 - return new User( 26 - prismaUser.id, 27 - prismaUser.email, 28 - prismaUser.name, 29 - prismaUser.createdAt, 30 - prismaUser.updatedAt, 31 - ); 32 - } 33 - 34 - /** 35 - * Maps an array of Prisma User entities to domain User entities 36 - */ 37 - mapToDomain(prismaUsers: PrismaUser[]): User[] { 38 - return prismaUsers.map((user) => this.toDomain(user)); 39 - } 40 - 41 - /** 42 - * Maps a Prisma user with joined organizations to a domain User preserving organizations 43 - */ 44 - toDomainWithOrganizations( 45 - prismaUser: 46 - | (PrismaUser & { 47 - organizations: Array<{ organization: PrismaOrganization }>; 48 - }) 49 - | null, 50 - ): User | null { 51 - if (prismaUser === null) return null; 52 - const organizations = prismaUser.organizations.map( 53 - (uo: { organization: PrismaOrganization }) => uo.organization, 54 - ); 55 - return new User( 56 - prismaUser.id, 57 - prismaUser.email, 58 - prismaUser.name, 59 - prismaUser.createdAt, 60 - prismaUser.updatedAt, 61 - organizations, 62 - ); 63 - } 64 - }
-116
apps/server/src/modules/auth/user.service.ts
··· 1 - import { Injectable, NotFoundException } from "@nestjs/common"; 2 - import { PrismaService } from "../database/prisma.service"; 3 - import { User } from "./user.entity"; 4 - import { UserMapper } from "./user.mapper"; 5 - 6 - @Injectable() 7 - export class UserService { 8 - constructor( 9 - private prisma: PrismaService, 10 - private userMapper: UserMapper, 11 - ) {} 12 - 13 - async create(email: string, name: string, password: string): Promise<User> { 14 - const prismaUser = await this.prisma.user.create({ 15 - data: { 16 - email, 17 - name, 18 - password, 19 - }, 20 - }); 21 - 22 - return this.userMapper.toDomain(prismaUser); 23 - } 24 - 25 - async findByEmail(email: string): Promise<User | null> { 26 - const prismaUser = await this.prisma.user.findUnique({ 27 - where: { email }, 28 - }); 29 - 30 - return this.userMapper.toDomain(prismaUser); 31 - } 32 - 33 - async findById(id: string): Promise<User | null> { 34 - const prismaUser = await this.prisma.user.findUnique({ 35 - where: { id }, 36 - }); 37 - 38 - return this.userMapper.toDomain(prismaUser); 39 - } 40 - 41 - async exists(email: string): Promise<boolean> { 42 - const user = await this.prisma.user.findUnique({ 43 - where: { email }, 44 - select: { id: true }, 45 - }); 46 - 47 - return user !== null; 48 - } 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 - 80 - async findByEmailOrFail(email: string): Promise<User> { 81 - const user = await this.findByEmail(email); 82 - if (!user) { 83 - throw new NotFoundException(`User with email ${email} not found`); 84 - } 85 - return user; 86 - } 87 - 88 - async findByIdOrFail(id: string): Promise<User> { 89 - const user = await this.findById(id); 90 - if (!user) { 91 - throw new NotFoundException(`User with id ${id} not found`); 92 - } 93 - return user; 94 - } 95 - 96 - async findByIdWithOrganizations(id: string): Promise<User> { 97 - const prismaUser = await this.prisma.user.findUnique({ 98 - where: { id }, 99 - include: { 100 - organizations: { 101 - include: { 102 - organization: true, 103 - }, 104 - }, 105 - }, 106 - }); 107 - 108 - if (!prismaUser) { 109 - throw new NotFoundException(`User with id ${id} not found`); 110 - } 111 - 112 - const user = this.userMapper.toDomainWithOrganizations(prismaUser); 113 - // user is non-null due to guard above 114 - return user as User; 115 - } 116 - }
-56
apps/server/src/modules/auth/user.type.ts
··· 1 - import { Field, ID, ObjectType } from "@nestjs/graphql"; 2 - import { UserJobExperience } from "../job-experience/employment/user-job-experience.type"; 3 - import { Organization } from "../organization/organization.entity"; 4 - import type { User as DomainUser } from "./user.entity"; 5 - 6 - @ObjectType() 7 - export class User { 8 - @Field(() => ID) 9 - id: string; 10 - 11 - @Field() 12 - email: string; 13 - 14 - @Field() 15 - name: string; 16 - 17 - @Field(() => Date) 18 - createdAt: Date; 19 - 20 - @Field(() => [Organization], { nullable: true }) 21 - organizations?: Organization[] | undefined; 22 - 23 - @Field(() => [UserJobExperience], { nullable: true }) 24 - experience?: UserJobExperience[] | undefined; 25 - 26 - constructor( 27 - id: string, 28 - email: string, 29 - name: string, 30 - createdAt: Date, 31 - organizations?: Organization[] | undefined, 32 - experience?: UserJobExperience[] | undefined, 33 - ) { 34 - this.id = id; 35 - this.email = email; 36 - this.name = name; 37 - this.createdAt = createdAt; 38 - this.organizations = organizations ?? undefined; 39 - this.experience = experience ?? undefined; 40 - } 41 - 42 - static fromDomain( 43 - domainUser: DomainUser, 44 - organizations?: Organization[], 45 - experience?: UserJobExperience[], 46 - ): User { 47 - return new User( 48 - domainUser.id, 49 - domainUser.email, 50 - domainUser.name, 51 - domainUser.createdAt, 52 - organizations, 53 - experience, 54 - ); 55 - } 56 - }
+1 -2
apps/server/src/modules/database/database.module.ts
··· 1 - import { Global, Module } from "@nestjs/common"; 1 + import { Module } from "@nestjs/common"; 2 2 import { PrismaService } from "./prisma.service"; 3 3 4 - @Global() 5 4 @Module({ 6 5 providers: [PrismaService], 7 6 exports: [PrismaService],
+2 -2
apps/server/src/modules/database/prisma.service.ts
··· 22 22 } 23 23 24 24 async onModuleInit() { 25 - await this.$connect(); 25 + await this["$connect"](); 26 26 } 27 27 28 28 async onModuleDestroy() { 29 - await this.$disconnect(); 29 + await this["$disconnect"](); 30 30 } 31 31 }