From b4cb14f7fbb29c1c53e562c654a4ab5495d69e9f Mon Sep 17 00:00:00 2001 From: rajdip-b Date: Mon, 25 Dec 2023 23:16:58 +0530 Subject: [PATCH] feat: Add RBAC --- apps/api/jest.config.ts | 6 +- apps/api/project.json | 6 + apps/api/src/app/app.controller.spec.ts | 22 +- apps/api/src/app/app.controller.ts | 6 +- apps/api/src/app/app.module.ts | 29 ++- apps/api/src/auth/admin.guard.spec.ts | 7 + apps/api/src/auth/admin.guard.ts | 13 + apps/api/src/auth/auth.controller.ts | 32 ++- apps/api/src/auth/auth.guard.spec.ts | 7 + apps/api/src/auth/auth.guard.ts | 45 ++++ apps/api/src/auth/auth.module.ts | 14 +- apps/api/src/auth/auth.service.spec.ts | 30 +-- apps/api/src/auth/auth.service.ts | 95 +++---- apps/api/src/auth/auth.types.ts | 6 +- apps/api/src/common/common.module.ts | 2 +- apps/api/src/decorators/decorators.module.ts | 4 + apps/api/src/decorators/public.decorator.ts | 4 + apps/api/src/decorators/user.decorator.ts | 9 + apps/api/src/main.ts | 38 ++- apps/api/src/prisma/prisma.module.ts | 6 +- apps/api/src/prisma/prisma.repository.ts | 236 +++++++++--------- apps/api/src/prisma/prisma.service.spec.ts | 20 +- apps/api/src/prisma/prisma.service.ts | 8 +- apps/api/src/resend/resend.module.ts | 18 +- apps/api/src/resend/resend.service.spec.ts | 22 +- apps/api/src/resend/services/mail.resend.ts | 48 ++-- .../services/resend.service.interface.ts | 6 +- apps/api/src/resend/services/test.resend.ts | 24 +- apps/api/src/supabase/supabase.module.ts | 6 +- .../api/src/supabase/supabase.service.spec.ts | 22 +- apps/api/src/supabase/supabase.service.ts | 20 +- apps/api/tsconfig.spec.json | 7 +- apps/api/webpack.config.js | 8 +- package.json | 1 + 34 files changed, 461 insertions(+), 366 deletions(-) create mode 100644 apps/api/src/auth/admin.guard.spec.ts create mode 100644 apps/api/src/auth/admin.guard.ts create mode 100644 apps/api/src/auth/auth.guard.spec.ts create mode 100644 apps/api/src/auth/auth.guard.ts create mode 100644 apps/api/src/decorators/decorators.module.ts create mode 100644 apps/api/src/decorators/public.decorator.ts create mode 100644 apps/api/src/decorators/user.decorator.ts diff --git a/apps/api/jest.config.ts b/apps/api/jest.config.ts index 4b247b35..3374f7cd 100644 --- a/apps/api/jest.config.ts +++ b/apps/api/jest.config.ts @@ -4,8 +4,8 @@ export default { preset: '../../jest.preset.js', testEnvironment: 'node', transform: { - '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }] }, moduleFileExtensions: ['ts', 'js', 'html'], - coverageDirectory: '../../coverage/apps/api', -}; + coverageDirectory: '../../coverage/apps/api' +} diff --git a/apps/api/project.json b/apps/api/project.json index 5b6cd73f..3a3b524d 100644 --- a/apps/api/project.json +++ b/apps/api/project.json @@ -41,6 +41,12 @@ "executor": "@nx/eslint:lint", "outputs": ["{options.outputFile}"] }, + "prettier:fix": { + "command": "pnpx prettier -w .", + "options": { + "cwd": "apps/api" + } + }, "test": { "executor": "@nx/jest:jest", "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], diff --git a/apps/api/src/app/app.controller.spec.ts b/apps/api/src/app/app.controller.spec.ts index fcd3bd70..88d2f7ea 100644 --- a/apps/api/src/app/app.controller.spec.ts +++ b/apps/api/src/app/app.controller.spec.ts @@ -1,21 +1,21 @@ -import { Test, TestingModule } from '@nestjs/testing'; +import { Test, TestingModule } from '@nestjs/testing' -import { AppController } from './app.controller'; +import { AppController } from './app.controller' describe('AppController', () => { - let app: TestingModule; + let app: TestingModule beforeAll(async () => { app = await Test.createTestingModule({ controllers: [AppController], - providers: [], - }).compile(); - }); + providers: [] + }).compile() + }) describe('healthCheck', () => { it('should return "Hello API"', () => { - const appController = app.get(AppController); - expect(appController.health()).toEqual('UP'); - }); - }); -}); + const appController = app.get(AppController) + expect(appController.health()).toEqual('UP') + }) + }) +}) diff --git a/apps/api/src/app/app.controller.ts b/apps/api/src/app/app.controller.ts index 66151b01..ace249fd 100644 --- a/apps/api/src/app/app.controller.ts +++ b/apps/api/src/app/app.controller.ts @@ -1,11 +1,13 @@ -import { Controller, Get } from '@nestjs/common'; +import { Controller, Get } from '@nestjs/common' +import { Public } from '../decorators/public.decorator' @Controller() export class AppController { constructor() {} @Get('health') + @Public() health(): string { - return 'UP'; + return 'UP' } } diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index 21d87184..e560d08c 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -1,18 +1,20 @@ -import { Module } from '@nestjs/common'; -import { AppController } from './app.controller'; -import { SupabaseModule } from '../supabase/supabase.module'; -import { ConfigModule } from '@nestjs/config'; -import { PassportModule } from '@nestjs/passport'; -import { AuthModule } from '../auth/auth.module'; -import { PrismaModule } from '../prisma/prisma.module'; -import { CommonModule } from '../common/common.module'; -import { ResendModule } from '../resend/resend.module'; +import { Module } from '@nestjs/common' +import { AppController } from './app.controller' +import { SupabaseModule } from '../supabase/supabase.module' +import { ConfigModule } from '@nestjs/config' +import { PassportModule } from '@nestjs/passport' +import { AuthModule } from '../auth/auth.module' +import { PrismaModule } from '../prisma/prisma.module' +import { CommonModule } from '../common/common.module' +import { ResendModule } from '../resend/resend.module' +import { APP_GUARD } from '@nestjs/core' +import { AuthGuard } from '../auth/auth.guard' @Module({ controllers: [AppController], imports: [ ConfigModule.forRoot({ - isGlobal: true, + isGlobal: true }), PassportModule, SupabaseModule, @@ -22,6 +24,11 @@ import { ResendModule } from '../resend/resend.module'; ResendModule, SupabaseModule ], - providers: [], + providers: [ + { + provide: APP_GUARD, + useClass: AuthGuard + } + ] }) export class AppModule {} diff --git a/apps/api/src/auth/admin.guard.spec.ts b/apps/api/src/auth/admin.guard.spec.ts new file mode 100644 index 00000000..8e02788a --- /dev/null +++ b/apps/api/src/auth/admin.guard.spec.ts @@ -0,0 +1,7 @@ +import { AdminGuard } from './admin.guard' + +describe('AdminGuard', () => { + it('should be defined', () => { + expect(new AdminGuard()).toBeDefined() + }) +}) diff --git a/apps/api/src/auth/admin.guard.ts b/apps/api/src/auth/admin.guard.ts new file mode 100644 index 00000000..6f99c6fb --- /dev/null +++ b/apps/api/src/auth/admin.guard.ts @@ -0,0 +1,13 @@ +import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common' +import { User } from '@prisma/client' +import { Observable } from 'rxjs' + +@Injectable() +export class AdminGuard implements CanActivate { + canActivate(context: ExecutionContext): boolean | Promise | Observable { + const request = context.switchToHttp().getRequest() + const user: User = request.user + + return user.isAdmin + } +} diff --git a/apps/api/src/auth/auth.controller.ts b/apps/api/src/auth/auth.controller.ts index 1f67bcd5..d361be0e 100644 --- a/apps/api/src/auth/auth.controller.ts +++ b/apps/api/src/auth/auth.controller.ts @@ -1,23 +1,21 @@ -import { Controller, Param, Post, Query } from '@nestjs/common'; -import { AuthService } from './auth.service'; -import { UserAuthenticatedResponse } from './auth.types'; +import { Controller, Param, Post, Query } from '@nestjs/common' +import { AuthService } from './auth.service' +import { UserAuthenticatedResponse } from './auth.types' +import { Public } from '../decorators/public.decorator' @Controller('auth') export class AuthController { - constructor( - private authService: AuthService - ) {} + constructor(private authService: AuthService) {} - @Post('send-otp/:email') - async sendOtp(@Param('email') email: string): Promise { - await this.authService.sendOtp(email); - } + @Public() + @Post('send-otp/:email') + async sendOtp(@Param('email') email: string): Promise { + await this.authService.sendOtp(email) + } - @Post('validate-otp') - async validateOtp( - @Query('email') email: string, - @Query('otp') otp: string) - : Promise { - return await this.authService.validateOtp(email, otp); - } + @Public() + @Post('validate-otp') + async validateOtp(@Query('email') email: string, @Query('otp') otp: string): Promise { + return await this.authService.validateOtp(email, otp) + } } diff --git a/apps/api/src/auth/auth.guard.spec.ts b/apps/api/src/auth/auth.guard.spec.ts new file mode 100644 index 00000000..2da36533 --- /dev/null +++ b/apps/api/src/auth/auth.guard.spec.ts @@ -0,0 +1,7 @@ +import { AuthGuard } from './auth.guard' + +describe('AuthGuard', () => { + it('should be defined', () => { + expect(new AuthGuard(null, null, null)).toBeDefined() + }) +}) diff --git a/apps/api/src/auth/auth.guard.ts b/apps/api/src/auth/auth.guard.ts new file mode 100644 index 00000000..a8b7c6fa --- /dev/null +++ b/apps/api/src/auth/auth.guard.ts @@ -0,0 +1,45 @@ +import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common' +import { JwtService } from '@nestjs/jwt' +import { Request } from 'express' +import { PrimsaRepository } from '../prisma/prisma.repository' +import { Reflector } from '@nestjs/core' +import { IS_PUBLIC_KEY } from '../decorators/public.decorator' + +@Injectable() +export class AuthGuard implements CanActivate { + constructor( + private jwtService: JwtService, + private repository: PrimsaRepository, + private reflector: Reflector + ) {} + + async canActivate(context: ExecutionContext): Promise { + const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ + context.getHandler(), + context.getClass() + ]) + if (isPublic) { + return true + } + + const request = context.switchToHttp().getRequest() + const token = this.extractTokenFromHeader(request) + if (!token) { + throw new UnauthorizedException() + } + try { + const payload = await this.jwtService.verifyAsync(token, { + secret: process.env.JWT_SECRET + }) + request['user'] = await this.repository.findUserById(payload.id) + } catch { + throw new UnauthorizedException() + } + return true + } + + private extractTokenFromHeader(request: Request): string | undefined { + const [type, token] = request.headers.authorization?.split(' ') ?? [] + return type === 'Bearer' ? token : undefined + } +} diff --git a/apps/api/src/auth/auth.module.ts b/apps/api/src/auth/auth.module.ts index 1d2ae87c..6968fb66 100644 --- a/apps/api/src/auth/auth.module.ts +++ b/apps/api/src/auth/auth.module.ts @@ -1,17 +1,17 @@ -import { Module } from '@nestjs/common'; -import { AuthService } from './auth.service'; -import { AuthController } from './auth.controller'; -import { JwtModule } from '@nestjs/jwt'; +import { Module } from '@nestjs/common' +import { AuthService } from './auth.service' +import { AuthController } from './auth.controller' +import { JwtModule } from '@nestjs/jwt' @Module({ imports: [ JwtModule.register({ global: true, secret: process.env.JWT_SECRET, - signOptions: { - expiresIn: '1d' , + signOptions: { + expiresIn: '1d', issuer: 'keyshade.xyz', - algorithm: 'HS256', + algorithm: 'HS256' } }) ], diff --git a/apps/api/src/auth/auth.service.spec.ts b/apps/api/src/auth/auth.service.spec.ts index d920251e..ae480971 100644 --- a/apps/api/src/auth/auth.service.spec.ts +++ b/apps/api/src/auth/auth.service.spec.ts @@ -1,13 +1,13 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { AuthService } from './auth.service'; -import { PrimsaRepository } from '../prisma/prisma.repository'; -import { TestResend } from '../resend/services/test.resend'; -import { RESEND_SERVICE } from '../resend/services/resend.service.interface'; -import { JwtService } from '@nestjs/jwt'; -import { PrismaService } from '../prisma/prisma.service'; +import { Test, TestingModule } from '@nestjs/testing' +import { AuthService } from './auth.service' +import { PrimsaRepository } from '../prisma/prisma.repository' +import { TestResend } from '../resend/services/test.resend' +import { RESEND_SERVICE } from '../resend/services/resend.service.interface' +import { JwtService } from '@nestjs/jwt' +import { PrismaService } from '../prisma/prisma.service' describe('AuthService', () => { - let service: AuthService; + let service: AuthService beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -17,13 +17,13 @@ describe('AuthService', () => { { provide: RESEND_SERVICE, useClass: TestResend }, JwtService, PrismaService - ], - }).compile(); + ] + }).compile() - service = module.get(AuthService); - }); + service = module.get(AuthService) + }) it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); + expect(service).toBeDefined() + }) +}) diff --git a/apps/api/src/auth/auth.service.ts b/apps/api/src/auth/auth.service.ts index 123a131e..8ecf8785 100644 --- a/apps/api/src/auth/auth.service.ts +++ b/apps/api/src/auth/auth.service.ts @@ -1,56 +1,59 @@ -import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common'; -import { PrimsaRepository } from '../prisma/prisma.repository'; -import { randomUUID } from 'crypto'; -import { JwtService } from '@nestjs/jwt'; -import { UserAuthenticatedResponse } from './auth.types'; -import { IResendService, RESEND_SERVICE } from '../resend/services/resend.service.interface'; +import { HttpException, HttpStatus, Inject, Injectable, Logger, LoggerService } from '@nestjs/common' +import { PrimsaRepository } from '../prisma/prisma.repository' +import { randomUUID } from 'crypto' +import { JwtService } from '@nestjs/jwt' +import { UserAuthenticatedResponse } from './auth.types' +import { IResendService, RESEND_SERVICE } from '../resend/services/resend.service.interface' @Injectable() export class AuthService { - private readonly OTP_EXPIRY = 5 * 60 * 1000; // 5 minutes - constructor( - private repository: PrimsaRepository, - @Inject(RESEND_SERVICE) private resend: IResendService, - private jwt: JwtService - ) {} - - async sendOtp(email: string): Promise { - if (!email || !email.includes('@')) { - console.error(`Invalid email address: ${email}`); - throw new HttpException('Please enter a valid email address', HttpStatus.BAD_REQUEST); - } - - // We need to create the user if it doesn't exist yet - if (!await this.repository.findUserByEmail(email)) { - await this.repository.createUser(email); - } - - const otp = await this.repository.createOtp( - email, - randomUUID().slice(0, 6).toUpperCase(), - this.OTP_EXPIRY); - - await this.resend.sendOtp(email, otp.code); - console.info(`Login code sent to ${email}: ${otp.code}`); + private readonly OTP_EXPIRY = 5 * 60 * 1000 // 5 minutes + private readonly logger: LoggerService + + constructor( + private repository: PrimsaRepository, + @Inject(RESEND_SERVICE) private resend: IResendService, + private jwt: JwtService + ) { + this.logger = new Logger(AuthService.name) + } + + async sendOtp(email: string): Promise { + if (!email || !email.includes('@')) { + this.logger.error(`Invalid email address: ${email}`) + throw new HttpException('Please enter a valid email address', HttpStatus.BAD_REQUEST) } - async validateOtp(email: string, otp: string): Promise { - const user = await this.repository.findUserByEmail(email); - if (!user) { - console.error(`User not found: ${email}`); - throw new HttpException('User not found', HttpStatus.NOT_FOUND); - } + // We need to create the user if it doesn't exist yet + if (!(await this.repository.findUserByEmail(email))) { + await this.repository.createUser(email) + } + + const otp = await this.repository.createOtp(email, randomUUID().slice(0, 6).toUpperCase(), this.OTP_EXPIRY) + + await this.resend.sendOtp(email, otp.code) + this.logger.log(`Login code sent to ${email}: ${otp.code}`) + } + + async validateOtp(email: string, otp: string): Promise { + const user = await this.repository.findUserByEmail(email) + if (!user) { + this.logger.error(`User not found: ${email}`) + throw new HttpException('User not found', HttpStatus.NOT_FOUND) + } + + if (!(await this.repository.isOtpValid(email, otp))) { + this.logger.error(`Invalid login code for ${email}: ${otp}`) + throw new HttpException('Invalid login code', HttpStatus.UNAUTHORIZED) + } - if (!await this.repository.isOtpValid(email, otp)) { - console.error(`Invalid login code for ${email}: ${otp}`); - throw new HttpException('Invalid login code', HttpStatus.UNAUTHORIZED); - } + await this.repository.deleteOtp(email, otp) - await this.repository.deleteOtp(email, otp); + this.logger.log(`User logged in: ${email}`) - return { - ...user, - token: await this.jwt.signAsync({ id: user.id, email: user.email }) - }; + return { + ...user, + token: await this.jwt.signAsync({ id: user.id }) } + } } diff --git a/apps/api/src/auth/auth.types.ts b/apps/api/src/auth/auth.types.ts index 9788c114..8f3831e8 100644 --- a/apps/api/src/auth/auth.types.ts +++ b/apps/api/src/auth/auth.types.ts @@ -1,5 +1,5 @@ -import { User } from "@prisma/client"; +import { User } from '@prisma/client' export type UserAuthenticatedResponse = User & { - token: string; -} \ No newline at end of file + token: string +} diff --git a/apps/api/src/common/common.module.ts b/apps/api/src/common/common.module.ts index ae7c0c16..01be450f 100644 --- a/apps/api/src/common/common.module.ts +++ b/apps/api/src/common/common.module.ts @@ -1,4 +1,4 @@ -import { Global, Module } from '@nestjs/common'; +import { Global, Module } from '@nestjs/common' @Global() @Module({ diff --git a/apps/api/src/decorators/decorators.module.ts b/apps/api/src/decorators/decorators.module.ts new file mode 100644 index 00000000..c0c4f443 --- /dev/null +++ b/apps/api/src/decorators/decorators.module.ts @@ -0,0 +1,4 @@ +import { Module } from '@nestjs/common' + +@Module({}) +export class DecoratorsModule {} diff --git a/apps/api/src/decorators/public.decorator.ts b/apps/api/src/decorators/public.decorator.ts new file mode 100644 index 00000000..91c2399b --- /dev/null +++ b/apps/api/src/decorators/public.decorator.ts @@ -0,0 +1,4 @@ +import { SetMetadata } from '@nestjs/common' + +export const IS_PUBLIC_KEY = 'isPublic' +export const Public = () => SetMetadata(IS_PUBLIC_KEY, true) diff --git a/apps/api/src/decorators/user.decorator.ts b/apps/api/src/decorators/user.decorator.ts new file mode 100644 index 00000000..bb32a758 --- /dev/null +++ b/apps/api/src/decorators/user.decorator.ts @@ -0,0 +1,9 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common' +import { User as DBUser } from '@prisma/client' + +export const CurrentUser = createParamDecorator( + (_: unknown, ctx: ExecutionContext) => { + const request = ctx.switchToHttp().getRequest() + return request.user + } +) diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 34970084..f89642e9 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -3,43 +3,41 @@ * This is only a minimal backend to get started. */ -import { LoggerService } from '@nestjs/common'; -import { NestFactory } from '@nestjs/core'; +import { LoggerService } from '@nestjs/common' +import { NestFactory } from '@nestjs/core' -import { AppModule } from './app/app.module'; -import chalk from 'chalk'; -import moment from 'moment'; +import { AppModule } from './app/app.module' +import chalk from 'chalk' +import moment from 'moment' class CustomLogger implements LoggerService { log(message: string) { - this.info(message); + this.info(message) } info(message: string) { - console.info(`${chalk.green('[INFO]')} ${chalk.green(moment().format('YYYY-MM-DD HH:mm:ss'))} - ${message}`); + console.info(`${chalk.green('[INFO]')} ${chalk.green(moment().format('YYYY-MM-DD HH:mm:ss'))} - ${message}`) } error(message: string) { - console.error(`${chalk.red('[ERROR]')} ${chalk.red(moment().format('YYYY-MM-DD HH:mm:ss'))} - ${message}`); + console.error(`${chalk.red('[ERROR]')} ${chalk.red(moment().format('YYYY-MM-DD HH:mm:ss'))} - ${message}`) } - + warn(message: string) { - console.warn(`${chalk.yellow('[WARN]')} ${chalk.yellow(moment().format('YYYY-MM-DD HH:mm:ss'))} - ${message}`); + console.warn(`${chalk.yellow('[WARN]')} ${chalk.yellow(moment().format('YYYY-MM-DD HH:mm:ss'))} - ${message}`) } } async function bootstrap() { - const logger = new CustomLogger(); + const logger = new CustomLogger() const app = await NestFactory.create(AppModule, { logger - }); - const globalPrefix = 'api'; - app.setGlobalPrefix(globalPrefix); - const port = 4200; - await app.listen(port); - logger.log( - `🚀 Application is running on: http://localhost:${port}/${globalPrefix}` - ); + }) + const globalPrefix = 'api' + app.setGlobalPrefix(globalPrefix) + const port = 4200 + await app.listen(port) + logger.log(`🚀 Application is running on: http://localhost:${port}/${globalPrefix}`) } -bootstrap(); +bootstrap() diff --git a/apps/api/src/prisma/prisma.module.ts b/apps/api/src/prisma/prisma.module.ts index bed324ae..029839be 100644 --- a/apps/api/src/prisma/prisma.module.ts +++ b/apps/api/src/prisma/prisma.module.ts @@ -1,6 +1,6 @@ -import { Global, Module } from '@nestjs/common'; -import { PrismaService } from './prisma.service'; -import { PrimsaRepository } from './prisma.repository'; +import { Global, Module } from '@nestjs/common' +import { PrismaService } from './prisma.service' +import { PrimsaRepository } from './prisma.repository' @Global() @Module({ diff --git a/apps/api/src/prisma/prisma.repository.ts b/apps/api/src/prisma/prisma.repository.ts index ac11a12c..527358dd 100644 --- a/apps/api/src/prisma/prisma.repository.ts +++ b/apps/api/src/prisma/prisma.repository.ts @@ -1,129 +1,129 @@ -import { Injectable } from "@nestjs/common"; -import { PrismaService } from "./prisma.service"; -import { Otp, User } from "@prisma/client"; +import { Injectable } from '@nestjs/common' +import { PrismaService } from './prisma.service' +import { Otp, User } from '@prisma/client' @Injectable() export class PrimsaRepository { - constructor( - private prisma: PrismaService - ) {} + constructor(private prisma: PrismaService) {} - /** - * Find a user by email - * @param email the email to search for - * @returns the user if found, null otherwise - */ - async findUserByEmail(email: string): Promise { - return await this.prisma.user.findUnique({ - where: { - email - } - }); - } + /** + * Find a user by email + * @param email the email to search for + * @returns the user if found, null otherwise + */ + async findUserByEmail(email: string): Promise { + return await this.prisma.user.findUnique({ + where: { + email + } + }) + } - /** - * Find a user by the user id - * @param id The id of the user to find - * @returns the user if found, null otherwise - */ - async findUserById(id: string): Promise { - return await this.prisma.user.findUnique({ - where: { - id - } - }); - } + /** + * Find a user by the user id + * @param id The id of the user to find + * @returns the user if found, null otherwise + */ + async findUserById(id: string): Promise { + return await this.prisma.user.findUnique({ + where: { + id + } + }) + } - /** - * Create a user with the given email. The onboarding process - * will aim at updating the user further. - * @param email The email of the user to create - * @returns - */ - async createUser(email: string): Promise { - return await this.prisma.user.create({ - data: { - email - } - }); - } + /** + * Create a user with the given email. The onboarding process + * will aim at updating the user further. + * @param email The email of the user to create + * @returns + */ + async createUser(email: string): Promise { + return await this.prisma.user.create({ + data: { + email + } + }) + } - /** - * Update an existing user - * @param id ID of the user to update - * @param data The data to update (can not update email or id) - * @returns The updated user - */ - async updateUser(id: string, data: Partial): Promise { - delete data.email; - delete data.id; - return await this.prisma.user.update({ - where: { - id - }, - data - }); - } + /** + * Update an existing user + * @param id ID of the user to update + * @param data The data to update (can not update email or id) + * @returns The updated user + */ + async updateUser(id: string, data: Partial): Promise { + delete data.email + delete data.id + return await this.prisma.user.update({ + where: { + id + }, + data + }) + } - /** - * Delete a user by id - * @param id The id of the user to delete - * @returns The deleted user - */ - async deleteUser(id: string): Promise { - return await this.prisma.user.delete({ - where: { - id - } - }); - } + /** + * Delete a user by id + * @param id The id of the user to delete + * @returns The deleted user + */ + async deleteUser(id: string): Promise { + return await this.prisma.user.delete({ + where: { + id + } + }) + } - /** - * An OTP is valid if it exists, is not expired, and is associated with the given email - * @param email the email against which to check the OTP - * @param otp the OTP code to check - * @returns returns true if the OTP is valid, false otherwise - */ - async isOtpValid(email: string, otp: string): Promise { - const timeNow = new Date(); - return await this.prisma.otp.count({ - where: { - code: otp, - user: { - email - }, - expiresAt: { - gt: timeNow - } - } - }) > 0; - } + /** + * An OTP is valid if it exists, is not expired, and is associated with the given email + * @param email the email against which to check the OTP + * @param otp the OTP code to check + * @returns returns true if the OTP is valid, false otherwise + */ + async isOtpValid(email: string, otp: string): Promise { + const timeNow = new Date() + return ( + (await this.prisma.otp.count({ + where: { + code: otp, + user: { + email + }, + expiresAt: { + gt: timeNow + } + } + })) > 0 + ) + } - async createOtp(email: string, otp: string, expiresAfter: number): Promise { - const timeNow = new Date(); - return await this.prisma.otp.create({ - data: { - code: otp, - expiresAt: new Date(timeNow.getTime() + expiresAfter), - user: { - connect: { - email - } - } - } - }); - } + async createOtp(email: string, otp: string, expiresAfter: number): Promise { + const timeNow = new Date() + return await this.prisma.otp.create({ + data: { + code: otp, + expiresAt: new Date(timeNow.getTime() + expiresAfter), + user: { + connect: { + email + } + } + } + }) + } - async deleteOtp(email: string, otp: string): Promise { - await this.prisma.otp.delete({ - where: { - code: otp, - AND: { - user: { - email - } - } - } - }); - } -} \ No newline at end of file + async deleteOtp(email: string, otp: string): Promise { + await this.prisma.otp.delete({ + where: { + code: otp, + AND: { + user: { + email + } + } + } + }) + } +} diff --git a/apps/api/src/prisma/prisma.service.spec.ts b/apps/api/src/prisma/prisma.service.spec.ts index a68cb9e3..c9b50669 100644 --- a/apps/api/src/prisma/prisma.service.spec.ts +++ b/apps/api/src/prisma/prisma.service.spec.ts @@ -1,18 +1,18 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { PrismaService } from './prisma.service'; +import { Test, TestingModule } from '@nestjs/testing' +import { PrismaService } from './prisma.service' describe('PrismaService', () => { - let service: PrismaService; + let service: PrismaService beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [PrismaService], - }).compile(); + providers: [PrismaService] + }).compile() - service = module.get(PrismaService); - }); + service = module.get(PrismaService) + }) it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); + expect(service).toBeDefined() + }) +}) diff --git a/apps/api/src/prisma/prisma.service.ts b/apps/api/src/prisma/prisma.service.ts index a53ce76f..66fb4bee 100644 --- a/apps/api/src/prisma/prisma.service.ts +++ b/apps/api/src/prisma/prisma.service.ts @@ -1,9 +1,9 @@ -import { Injectable, OnModuleInit } from '@nestjs/common'; -import { PrismaClient } from '@prisma/client'; +import { Injectable, OnModuleInit } from '@nestjs/common' +import { PrismaClient } from '@prisma/client' @Injectable() export class PrismaService extends PrismaClient implements OnModuleInit { async onModuleInit() { - await this.$connect(); + await this.$connect() } -} \ No newline at end of file +} diff --git a/apps/api/src/resend/resend.module.ts b/apps/api/src/resend/resend.module.ts index 1899c6e2..30f89eea 100644 --- a/apps/api/src/resend/resend.module.ts +++ b/apps/api/src/resend/resend.module.ts @@ -1,18 +1,12 @@ -import { Global, Module } from '@nestjs/common'; -import { MailResend } from './services/mail.resend'; -import { RESEND_SERVICE } from './services/resend.service.interface'; -import { TestResend } from './services/test.resend'; +import { Global, Module } from '@nestjs/common' +import { MailResend } from './services/mail.resend' +import { RESEND_SERVICE } from './services/resend.service.interface' +import { TestResend } from './services/test.resend' const customProvider = { provide: RESEND_SERVICE, - useClass: process.env.NODE_ENV === 'development' ? TestResend : MailResend, -}; -// } -// { -// provide: RESEND_SERVICE, -// useClass: process.env.NODE_ENV === 'development' ? TestResend : MailResend, -// } -// ]; + useClass: process.env.NODE_ENV === 'development' ? MailResend : TestResend +} @Global() @Module({ diff --git a/apps/api/src/resend/resend.service.spec.ts b/apps/api/src/resend/resend.service.spec.ts index 4cb25535..e825495e 100644 --- a/apps/api/src/resend/resend.service.spec.ts +++ b/apps/api/src/resend/resend.service.spec.ts @@ -1,19 +1,19 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { IResendService } from './services/resend.service.interface'; -import { TestResend } from './services/test.resend'; +import { Test, TestingModule } from '@nestjs/testing' +import { IResendService } from './services/resend.service.interface' +import { TestResend } from './services/test.resend' describe('ResendService', () => { - let service: IResendService; + let service: IResendService beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [TestResend], - }).compile(); + providers: [TestResend] + }).compile() - service = module.get(TestResend); - }); + service = module.get(TestResend) + }) it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); + expect(service).toBeDefined() + }) +}) diff --git a/apps/api/src/resend/services/mail.resend.ts b/apps/api/src/resend/services/mail.resend.ts index 998e83b9..b1be1f2d 100644 --- a/apps/api/src/resend/services/mail.resend.ts +++ b/apps/api/src/resend/services/mail.resend.ts @@ -1,18 +1,19 @@ -import { Injectable } from '@nestjs/common'; -import { Resend } from 'resend'; -import { IResendService } from './resend.service.interface'; +import { Injectable, Logger } from '@nestjs/common' +import { Resend } from 'resend' +import { IResendService } from './resend.service.interface' @Injectable() export class MailResend implements IResendService { - private readonly resend: Resend; + private readonly resend: Resend + private readonly log = new Logger(MailResend.name) - constructor() { - this.resend = new Resend(process.env.RESEND_API_KEY) - } + constructor() { + this.resend = new Resend(process.env.RESEND_API_KEY) + } - async sendOtp(email: string, otp: string): Promise { - const subject = 'Your Login OTP'; - const body = ` + async sendOtp(email: string, otp: string): Promise { + const subject = 'Your Login OTP' + const body = ` OTP Verification @@ -29,20 +30,21 @@ export class MailResend implements IResendService {

keyshade Team

- `; - await this.sendEmail(email, subject, body); - } + ` + await this.sendEmail(email, subject, body) + } - private async sendEmail(email: string, subject: string, body: string): Promise { - const {error} = await this.resend.emails.send({ - from: process.env.FROM_EMAIL, - to: email, - subject, - html: body - }); + private async sendEmail(email: string, subject: string, body: string): Promise { + const { error } = await this.resend.emails.send({ + from: process.env.FROM_EMAIL, + to: email, + subject, + html: body + }) - if (error) { - throw new Error(error.message); - } + if (error) { + this.log.error(`Error sending email to ${email}: ${error.message}`) + throw new Error(error.message) } + } } diff --git a/apps/api/src/resend/services/resend.service.interface.ts b/apps/api/src/resend/services/resend.service.interface.ts index 6484ba73..08f6dcdd 100644 --- a/apps/api/src/resend/services/resend.service.interface.ts +++ b/apps/api/src/resend/services/resend.service.interface.ts @@ -1,5 +1,5 @@ -export const RESEND_SERVICE = 'RESEND_SERVICE'; +export const RESEND_SERVICE = 'RESEND_SERVICE' export interface IResendService { - sendOtp(email: string, otp: string): Promise; -} \ No newline at end of file + sendOtp(email: string, otp: string): Promise +} diff --git a/apps/api/src/resend/services/test.resend.ts b/apps/api/src/resend/services/test.resend.ts index 2dedd67c..bd28be3f 100644 --- a/apps/api/src/resend/services/test.resend.ts +++ b/apps/api/src/resend/services/test.resend.ts @@ -1,15 +1,17 @@ -import { Injectable } from "@nestjs/common"; -import { IResendService } from "./resend.service.interface"; -import { Resend } from "resend"; +import { Injectable, Logger } from '@nestjs/common' +import { IResendService } from './resend.service.interface' +import { Resend } from 'resend' @Injectable() export class TestResend implements IResendService { - constructor() { - // Check if resend is working - new Resend('SOME KEY'); - } + private readonly log = new Logger(TestResend.name) - async sendOtp(email: string, otp: string): Promise { - console.info(`OTP for ${email} is ${otp}`); - } -} \ No newline at end of file + constructor() { + // Check if resend is working + new Resend('SOME KEY') + } + + async sendOtp(email: string, otp: string): Promise { + this.log.log(`OTP for ${email} is ${otp}`) + } +} diff --git a/apps/api/src/supabase/supabase.module.ts b/apps/api/src/supabase/supabase.module.ts index 0c9abb64..0e0f3087 100644 --- a/apps/api/src/supabase/supabase.module.ts +++ b/apps/api/src/supabase/supabase.module.ts @@ -1,9 +1,9 @@ -import { Global, Module } from '@nestjs/common'; -import { SupabaseService } from './supabase.service'; +import { Global, Module } from '@nestjs/common' +import { SupabaseService } from './supabase.service' @Global() @Module({ providers: [SupabaseService], - exports: [SupabaseService], + exports: [SupabaseService] }) export class SupabaseModule {} diff --git a/apps/api/src/supabase/supabase.service.spec.ts b/apps/api/src/supabase/supabase.service.spec.ts index 335ccdc5..0061986b 100644 --- a/apps/api/src/supabase/supabase.service.spec.ts +++ b/apps/api/src/supabase/supabase.service.spec.ts @@ -1,19 +1,19 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { SupabaseService } from './supabase.service'; -import { ConfigService } from '@nestjs/config'; +import { Test, TestingModule } from '@nestjs/testing' +import { SupabaseService } from './supabase.service' +import { ConfigService } from '@nestjs/config' describe('SupabaseService', () => { - let service: SupabaseService; + let service: SupabaseService beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [SupabaseService, ConfigService], - }).compile(); + providers: [SupabaseService, ConfigService] + }).compile() - service = await module.resolve(SupabaseService); - }); + service = await module.resolve(SupabaseService) + }) it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); + expect(service).toBeDefined() + }) +}) diff --git a/apps/api/src/supabase/supabase.service.ts b/apps/api/src/supabase/supabase.service.ts index 6e25f3ed..6a7c18e6 100644 --- a/apps/api/src/supabase/supabase.service.ts +++ b/apps/api/src/supabase/supabase.service.ts @@ -1,25 +1,23 @@ -import { Injectable, Scope } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; +import { Injectable, Scope } from '@nestjs/common' +import { ConfigService } from '@nestjs/config' -import { createClient, SupabaseClient } from '@supabase/supabase-js'; +import { createClient, SupabaseClient } from '@supabase/supabase-js' @Injectable({ scope: Scope.REQUEST }) export class SupabaseService { - private clientInstance: SupabaseClient; + private clientInstance: SupabaseClient - constructor( - private readonly configService: ConfigService, - ) {} + constructor(private readonly configService: ConfigService) {} async getClient() { if (this.clientInstance) { - return this.clientInstance; + return this.clientInstance } this.clientInstance = createClient( this.configService.get('SUPABASE_API_URL'), - this.configService.get('SUPABASE_ANON_KEY'), - ); - return this.clientInstance; + this.configService.get('SUPABASE_ANON_KEY') + ) + return this.clientInstance } } diff --git a/apps/api/tsconfig.spec.json b/apps/api/tsconfig.spec.json index 9b2a121d..f6d8ffcc 100644 --- a/apps/api/tsconfig.spec.json +++ b/apps/api/tsconfig.spec.json @@ -5,10 +5,5 @@ "module": "commonjs", "types": ["jest", "node"] }, - "include": [ - "jest.config.ts", - "src/**/*.test.ts", - "src/**/*.spec.ts", - "src/**/*.d.ts" - ] + "include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"] } diff --git a/apps/api/webpack.config.js b/apps/api/webpack.config.js index 6abb3ed2..d7731705 100644 --- a/apps/api/webpack.config.js +++ b/apps/api/webpack.config.js @@ -1,13 +1,13 @@ -const { composePlugins, withNx } = require('@nx/webpack'); +const { composePlugins, withNx } = require('@nx/webpack') // Nx plugins for webpack. module.exports = composePlugins( withNx({ - target: 'node', + target: 'node' }), (config) => { // Update the webpack config as needed here. // e.g. `config.plugins.push(new MyPlugin())` - return config; + return config } -); +) diff --git a/package.json b/package.json index 7f70964e..fe379ecd 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "test:workspace": "nx run workspace:test", "test:cli": "nx run cli:test", "test:sdk-node": "nx run sdk-js:test", + "db:seed": "cd apps/api/src/prisma && pnpx ts-node seed.ts", "db:generate-types": "nx run api:prisma:generate", "db:generate-migrations": "cd apps/api/src/prisma && pnpx prisma migrate dev --create-only --skip-seed", "db:deploy-migrations": "cd apps/api/src/prisma && pnpx prisma migrate deploy",