From a5f76f01adaf4fe076c2badf46f0ed9cfde47c14 Mon Sep 17 00:00:00 2001 From: OkaykOrhmn Date: Thu, 24 Jul 2025 09:24:37 +0330 Subject: [PATCH] added middleware --- src/answer/answer.controller.ts | 7 +-- src/answer/answer.service.ts | 14 +++--- src/attachment/attachment.controller.ts | 7 +-- src/attachment/attachment.service.ts | 14 ++---- src/attachment/dto/create_attachment.dto.ts | 5 ++- src/formResult/formResult.controller.ts | 2 +- src/main.ts | 2 + src/middleware/jwtMiddleware.ts | 38 ++++++++++++++++ src/participant/participant.controller.ts | 7 +-- src/participant/participant.service.ts | 48 +++++---------------- 10 files changed, 75 insertions(+), 69 deletions(-) create mode 100644 src/middleware/jwtMiddleware.ts diff --git a/src/answer/answer.controller.ts b/src/answer/answer.controller.ts index d1d5a4a..53504f8 100644 --- a/src/answer/answer.controller.ts +++ b/src/answer/answer.controller.ts @@ -1,8 +1,9 @@ -import { Controller, Get, Post, Patch, Delete, Param, Body, UsePipes, ValidationPipe, Query, ParseUUIDPipe, HttpCode, HttpStatus } from '@nestjs/common'; +import { Controller, Get, Post, Patch, Delete, Param, Body, UsePipes, ValidationPipe, Query, ParseUUIDPipe, HttpCode, HttpStatus, Request } from '@nestjs/common'; import { AnswerService } from './answer.service'; import { CreateAnswerDto } from './dto/create-answer.dto'; import { UpdateAnswerDto } from './dto/update-answer.dto'; import { Answer } from './entity/answer.entity'; +import { AuthRequest } from '../middleware/jwtMiddleware'; @Controller('answers') export class AnswerController { @@ -10,8 +11,8 @@ export class AnswerController { @Post() @UsePipes(new ValidationPipe({ transform: true, whitelist: true })) - async create(@Body() body: CreateAnswerDto): Promise { - return this.answerService.create(body); + async create(@Body() body: CreateAnswerDto, @Request() req: AuthRequest): Promise { + return this.answerService.create(body, req.user); } @Get('findAll') diff --git a/src/answer/answer.service.ts b/src/answer/answer.service.ts index ba762d5..e9a57d7 100644 --- a/src/answer/answer.service.ts +++ b/src/answer/answer.service.ts @@ -1,11 +1,10 @@ -import { Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common'; +import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Answer } from './entity/answer.entity'; import { CreateAnswerDto } from './dto/create-answer.dto'; import { UpdateAnswerDto } from './dto/update-answer.dto'; -import { CreateAttachmentDto } from '../attachment/dto/create_attachment.dto'; -import { Attachment } from '../attachment/entity/attachment.entity'; +import { AuthRequest } from '../middleware/jwtMiddleware'; @Injectable() export class AnswerService { @@ -14,18 +13,15 @@ export class AnswerService { private readonly answerRepository: Repository, ) {} - async create(data: CreateAnswerDto, token: any): Promise { - if (!token || !token.sub) { - throw new InternalServerErrorException('Failed to extract sub from token'); - } + async create(data: CreateAnswerDto, user: AuthRequest['user']): Promise { const answer = this.answerRepository.create({ ...data, - participantId: token.sub, // token.sub is userId, but we want participantId ?! + participantId: user.sub, }); return await this.answerRepository.save(answer); } - async findAll( page = 1, limit = 10 ): Promise<{ + async findAll(page = 1, limit = 10): Promise<{ docs: Answer[]; totalDocs: number; limit: number; diff --git a/src/attachment/attachment.controller.ts b/src/attachment/attachment.controller.ts index 61ebea4..2e08fcb 100644 --- a/src/attachment/attachment.controller.ts +++ b/src/attachment/attachment.controller.ts @@ -1,8 +1,9 @@ -import { Controller, Get, Post, Patch, Delete, Param, Body, UsePipes, ValidationPipe, Query, ParseUUIDPipe, HttpCode, HttpStatus } from '@nestjs/common'; +import { Controller, Get, Post, Patch, Delete, Param, Body, UsePipes, ValidationPipe, Query, ParseUUIDPipe, HttpCode, HttpStatus, Request } from '@nestjs/common'; import { AttachmentService } from './attachment.service'; import { CreateAttachmentDto } from './dto/create_attachment.dto'; import { UpdateAttachmentDto } from './dto/update_attachment.dto'; import { Attachment } from './entity/attachment.entity'; +import { AuthRequest } from '../middleware/jwtMiddleware'; @Controller('attachments') export class AttachmentController { @@ -10,8 +11,8 @@ export class AttachmentController { @Post() @UsePipes(new ValidationPipe({ transform: true, whitelist: true })) - async create(@Body() body: CreateAttachmentDto): Promise { - return this.attachmentService.create(body); + async create(@Body() body: CreateAttachmentDto, @Request() req: AuthRequest): Promise { + return this.attachmentService.create(body, req.user); } @Get('findAll') diff --git a/src/attachment/attachment.service.ts b/src/attachment/attachment.service.ts index fdd0136..2393faa 100644 --- a/src/attachment/attachment.service.ts +++ b/src/attachment/attachment.service.ts @@ -1,11 +1,10 @@ -import { Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common'; +import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Attachment } from './entity/attachment.entity'; import { CreateAttachmentDto } from './dto/create_attachment.dto'; import { UpdateAttachmentDto } from './dto/update_attachment.dto'; -import axios from 'axios'; -import { decode } from 'jsonwebtoken'; +import { AuthRequest } from '../middleware/jwtMiddleware'; @Injectable() export class AttachmentService { @@ -14,15 +13,10 @@ export class AttachmentService { private readonly attachmentRepository: Repository, ) {} - async create(data: CreateAttachmentDto, token: any): Promise { - // Extract sub from decoded token - if (!token || !token.sub) { - throw new InternalServerErrorException('Failed to extract sub from token'); - } - // Create the attachment with ownerId set to token.sub + async create(data: CreateAttachmentDto, user: AuthRequest['user']): Promise { const attachment = this.attachmentRepository.create({ ...data, - ownerId: token.sub, // Set ownerId to the token's sub + ownerId: user.sub, }); return await this.attachmentRepository.save(attachment); } diff --git a/src/attachment/dto/create_attachment.dto.ts b/src/attachment/dto/create_attachment.dto.ts index 6c1eacc..c46ba83 100644 --- a/src/attachment/dto/create_attachment.dto.ts +++ b/src/attachment/dto/create_attachment.dto.ts @@ -14,6 +14,7 @@ export class CreateAttachmentDto extends BaseDto { @IsUrl() storageUrl: string; - @IsUUID('4') - ownerId: string; + // @IsUUID('4') + // @IsOptional() + // ownerId?: string; } \ No newline at end of file diff --git a/src/formResult/formResult.controller.ts b/src/formResult/formResult.controller.ts index 60b8987..fc093b8 100644 --- a/src/formResult/formResult.controller.ts +++ b/src/formResult/formResult.controller.ts @@ -45,7 +45,7 @@ export class FormResultController { } @Get(':id/refresh') - async getFormStatistics( // updates data in database and then returns + async getFormStatistics( // updates data in database and then returns it @Param('formId', new ParseUUIDPipe()) formId: string, ): Promise { return this.formResultService.getFormStatistics(formId); diff --git a/src/main.ts b/src/main.ts index d6a0e76..db94881 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,9 +1,11 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; +import { JwtMiddleware } from './middleware/jwtMiddleware'; async function bootstrap() { const app = await NestFactory.create(AppModule); app.enableCors({ origin: 'http://localhost:3001' }); + app.use(new JwtMiddleware().use); await app.listen(process.env.PORT ?? 3000); } bootstrap(); diff --git a/src/middleware/jwtMiddleware.ts b/src/middleware/jwtMiddleware.ts new file mode 100644 index 0000000..64a24c1 --- /dev/null +++ b/src/middleware/jwtMiddleware.ts @@ -0,0 +1,38 @@ +import { Injectable, NestMiddleware, UnauthorizedException } from '@nestjs/common'; +import { Request, Response, NextFunction } from 'express'; +import * as jwt from 'jsonwebtoken'; +import { validate as isValidUUID } from 'uuid'; + +@Injectable() +export class JwtMiddleware implements NestMiddleware { + use(req: Request, res: Response, next: NextFunction) { + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + throw new UnauthorizedException('Missing or invalid Authorization header'); + } + + const token = authHeader.split(' ')[1]; + try { + const decoded = jwt.decode(token) as any; + if (!decoded || !decoded.sub) { + throw new UnauthorizedException('Invalid token: missing sub'); + } + if (!isValidUUID(decoded.sub)) { + throw new UnauthorizedException('Invalid token: sub is not a valid UUIDv4'); + } + const { sub, realm_access, resource_access } = decoded; + const roles = [ // extract the role you wanna use in guards + ...(realm_access?.roles || []), + // ...(resource_access?.account?.roles || []), + ]; + req['user'] = { sub, roles }; + next(); + } catch (error) { + throw new UnauthorizedException('Invalid token'); + } + } +} + +export interface AuthRequest extends Request { + user: { sub: string; roles: string[] }; +} \ No newline at end of file diff --git a/src/participant/participant.controller.ts b/src/participant/participant.controller.ts index f662e0b..7d100dd 100644 --- a/src/participant/participant.controller.ts +++ b/src/participant/participant.controller.ts @@ -1,8 +1,9 @@ -import { Controller, Get, Post, Patch, Delete, Param, Body, UsePipes, ValidationPipe, Query } from '@nestjs/common'; +import { Controller, Get, Post, Patch, Delete, Param, Body, UsePipes, ValidationPipe, Query, Request } from '@nestjs/common'; import { ParticipantService } from './participant.service'; import { CreateParticipantDto } from './dto/create-participant.dto'; import { UpdateParticipantDto } from './dto/update-participant.dto'; import { Participant } from './entity/participant.entity'; +import { AuthRequest } from '../middleware/jwtMiddleware'; @Controller('participants') export class ParticipantController { @@ -10,8 +11,8 @@ export class ParticipantController { @Post() @UsePipes(new ValidationPipe({ transform: true })) - async create(@Body() body: CreateParticipantDto): Promise { - return this.participantService.create(body); + async create(@Body() body: CreateParticipantDto, @Request() req: AuthRequest): Promise { + return this.participantService.create(body, req.user); } @Get('findAll') diff --git a/src/participant/participant.service.ts b/src/participant/participant.service.ts index b8562f6..7c806be 100644 --- a/src/participant/participant.service.ts +++ b/src/participant/participant.service.ts @@ -1,11 +1,10 @@ -import { Injectable, InternalServerErrorException } from '@nestjs/common'; +import { Injectable, InternalServerErrorException, ConflictException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { CreateParticipantDto } from './dto/create-participant.dto'; import { Participant } from './entity/participant.entity'; import { UpdateParticipantDto } from './dto/update-participant.dto'; -import axios from 'axios'; -import { decode } from 'jsonwebtoken'; +import { AuthRequest } from '../middleware/jwtMiddleware'; @Injectable() export class ParticipantService { @@ -14,51 +13,24 @@ export class ParticipantService { private readonly participantRepository: Repository, ) {} - async create(data: CreateParticipantDto): Promise { + async create(data: CreateParticipantDto, user: AuthRequest['user']): Promise { try { - // Here we send a sample request, in final implementation, there'll be no need for that, we'll get the decoded token - const authResponse = await axios.post( - 'https://auth.didvan.com/realms/didvan/protocol/openid-connect/token', - new URLSearchParams({ - client_id: 'didvan-app', - username: 'bob', - password: 'developer_password', - grant_type: 'password', - }), - { - headers: { - 'content-type': 'application/x-www-form-urlencoded', - }, - }, - ); - - // Decode the access token to extract the 'sub' claim - const token = authResponse.data.access_token; - const decodedToken: any = decode(token); - if (!decodedToken || !decodedToken.sub) { - throw new InternalServerErrorException('Failed to decode token or extract sub'); - } - - // Check if participant with this sub already exists - const existingParticipant = await this.participantRepository.findOne({ where: { id: decodedToken.sub } }); + const existingParticipant = await this.participantRepository.findOne({ where: { userId: user.sub } }); if (existingParticipant) { - throw new InternalServerErrorException(`Participant with ID ${decodedToken.sub} already exists`); + throw new ConflictException(`Participant with userId ${user.sub} already exists`); } - // Create the participant with the 'sub' as the ID const participant = this.participantRepository.create({ ...data, - // id: , let it assign a random id - userId: decodedToken.sub, // Set userId to the same sub - metadata: data.metadata - ? { entries: data.metadata.entries } - : { entries: [] }, - status: data.status || 'active', + userId: user.sub, + role: data.role || 'user', }); - // Save the participant to the database return await this.participantRepository.save(participant); } catch (error) { + if (error instanceof ConflictException) { + throw error; + } throw new InternalServerErrorException(`Failed to create participant: ${error.message}`); } }