diff --git a/Dockerfile b/Dockerfile index b34cc21..a35c949 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,14 +25,17 @@ FROM node:20-alpine WORKDIR /app COPY package*.json ./ -RUN npm install +RUN npm config set registry https://registry.npmjs.org/ && \ + npm ci --prefer-offline || npm install --fetch-retry-mintimeout 20000 \ + --fetch-retry-maxtimeout 120000 \ + --fetch-retries 5 COPY . . # Generate Prisma client RUN npx prisma generate -EXPOSE 3000 +EXPOSE 3001 # CMD ["sh", "-c", "npx prisma migrate deploy && npm run start:dev"] CMD ["npm", "run", "start:dev"] diff --git a/docker-compose.yml b/docker-compose.yml index ae6c03b..1e0163f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,7 @@ services: context: . dockerfile: Dockerfile ports: - - "3000:3000" + - "3001:3001" - "5555:5555" volumes: - .:/app diff --git a/prisma/migrations/20260226015842_add_app_logs/migration.sql b/prisma/migrations/20260226015842_add_app_logs/migration.sql new file mode 100644 index 0000000..a089ef4 --- /dev/null +++ b/prisma/migrations/20260226015842_add_app_logs/migration.sql @@ -0,0 +1,15 @@ +-- CreateEnum +CREATE TYPE "LogLevel" AS ENUM ('INFO', 'WARN', 'ERROR'); + +-- CreateTable +CREATE TABLE "AppLog" ( + "id" TEXT NOT NULL, + "level" "LogLevel" NOT NULL, + "action" TEXT NOT NULL, + "message" TEXT NOT NULL, + "userId" TEXT, + "metadata" JSONB, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "AppLog_pkey" PRIMARY KEY ("id") +); diff --git a/prisma/migrations/20260226233649_add_custody_status/migration.sql b/prisma/migrations/20260226233649_add_custody_status/migration.sql new file mode 100644 index 0000000..49c840f --- /dev/null +++ b/prisma/migrations/20260226233649_add_custody_status/migration.sql @@ -0,0 +1,26 @@ +/* + Warnings: + + - You are about to drop the column `gender` on the `pets` table. All the data in the column will be lost. + - You are about to drop the column `size` on the `pets` table. All the data in the column will be lost. + - You are about to drop the `documents` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "documents" DROP CONSTRAINT "documents_adoption_id_fkey"; + +-- DropForeignKey +ALTER TABLE "documents" DROP CONSTRAINT "documents_uploaded_by_id_fkey"; + +-- AlterTable +ALTER TABLE "pets" DROP COLUMN "gender", +DROP COLUMN "size"; + +-- DropTable +DROP TABLE "documents"; + +-- DropEnum +DROP TYPE "PetGender"; + +-- DropEnum +DROP TYPE "PetSize"; diff --git a/prisma/migrations/20260227054055_add_document_model/migration.sql b/prisma/migrations/20260227054055_add_document_model/migration.sql new file mode 100644 index 0000000..5e4b5c1 --- /dev/null +++ b/prisma/migrations/20260227054055_add_document_model/migration.sql @@ -0,0 +1,21 @@ +-- CreateTable +CREATE TABLE "Document" ( + "id" TEXT NOT NULL, + "fileName" TEXT NOT NULL, + "fileUrl" TEXT NOT NULL, + "publicId" TEXT NOT NULL, + "mimeType" TEXT NOT NULL, + "size" INTEGER NOT NULL, + "uploadedById" TEXT NOT NULL, + "adoptionId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Document_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "Document" ADD CONSTRAINT "Document_uploadedById_fkey" FOREIGN KEY ("uploadedById") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Document" ADD CONSTRAINT "Document_adoptionId_fkey" FOREIGN KEY ("adoptionId") REFERENCES "adoptions"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 2bf7f0f..422860f 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1,3 +1,4 @@ + generator client { provider = "prisma-client-js" } @@ -22,6 +23,9 @@ enum PetSpecies { OTHER } + + + enum AdoptionStatus { REQUESTED PENDING @@ -32,12 +36,13 @@ enum AdoptionStatus { CANCELLED } + enum CustodyStatus { - PENDING ACTIVE RETURNED CANCELLED VIOLATION + PENDING } enum CustodyType { @@ -46,6 +51,8 @@ enum CustodyType { SHELTER } + + enum EscrowStatus { CREATED FUNDED @@ -54,6 +61,7 @@ enum EscrowStatus { DISPUTED } + enum EventEntityType { USER PET @@ -62,6 +70,7 @@ enum EventEntityType { ESCROW } + enum EventType { USER_REGISTERED PET_REGISTERED @@ -76,17 +85,11 @@ enum EventType { TRUST_SCORE_UPDATED } -enum PetGender { - MALE - FEMALE -} - -enum PetSize { - SMALL - MEDIUM - LARGE +enum LogLevel { + INFO + WARN + ERROR } - // ─── Models ────────────────────────────────────────────── model User { @@ -97,7 +100,7 @@ model User { lastName String @map("last_name") role UserRole @default(USER) - trustScore Float @default(50) + trustScore Float @default(50) stellarPublicKey String? @unique @map("stellar_public_key") avatarUrl String? @map("avatar_url") @@ -105,33 +108,33 @@ model User { createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") - petsOwned Pet[] @relation("PetOwner") + petsOwned Pet[] @relation("PetOwner") adoptionsAsAdopter Adoption[] @relation("Adopter") adoptionsAsOwner Adoption[] @relation("Owner") custodiesHeld Custody[] @relation("CustodyHolder") - events EventLog[] @relation("ActorEvents") + events EventLog[] @relation("ActorEvents") + documents Document[] @@map("users") } + + model Pet { - id String @id @default(uuid()) + id String @id @default(uuid()) name String species PetSpecies breed String? age Int? description String? - imageUrl String? @map("image_url") + imageUrl String? @map("image_url") - gender PetGender? // Optional gender field - size PetSize? // Optional size field - - currentOwnerId String? @map("current_owner_id") - currentOwner User? @relation("PetOwner", fields: [currentOwnerId], references: [id]) + currentOwnerId String? @map("current_owner_id") + currentOwner User? @relation("PetOwner", fields: [currentOwnerId], references: [id]) createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@ -144,6 +147,7 @@ model Pet { @@map("pets") } + model Adoption { id String @id @default(uuid()) status AdoptionStatus @default(REQUESTED) @@ -158,12 +162,12 @@ model Adoption { ownerId String @map("owner_id") owner User @relation("Owner", fields: [ownerId], references: [id]) - escrowId String? @unique @map("escrow_id") + escrowId String? @unique @map("escrow_id") escrow Escrow? @relation(fields: [escrowId], references: [id]) - - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") + documents Document[] + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") @@index([status]) @@index([adopterId]) @@ -171,14 +175,16 @@ model Adoption { @@map("adoptions") } + + model Custody { - id String @id @default(uuid()) - status CustodyStatus @default(ACTIVE) - type CustodyType + id String @id @default(uuid()) + status CustodyStatus @default(ACTIVE) + type CustodyType depositAmount Decimal? @db.Decimal(12, 2) - startDate DateTime @map("start_date") + startDate DateTime @map("start_date") endDate DateTime? petId String @map("pet_id") @@ -198,20 +204,21 @@ model Custody { @@map("custodies") } + model Escrow { id String @id @default(uuid()) - stellarPublicKey String @unique @map("stellar_public_key") + stellarPublicKey String @unique @map("stellar_public_key") stellarSecretEncrypted String @map("stellar_secret_encrypted") - assetCode String @default("XLM") @map("asset_code") + assetCode String @default("XLM") @map("asset_code") assetIssuer String? @map("asset_issuer") amount Decimal @db.Decimal(12, 2) - fundingTxHash String? @map("funding_tx_hash") - releaseTxHash String? @map("release_tx_hash") - refundTxHash String? @map("refund_tx_hash") + fundingTxHash String? @map("funding_tx_hash") + releaseTxHash String? @map("release_tx_hash") + refundTxHash String? @map("refund_tx_hash") requiredSignatures Int @default(2) @@ -227,11 +234,23 @@ model Escrow { @@map("escrows") } + +model AppLog { + id String @id @default(uuid()) + level LogLevel + action String + message String + userId String? + metadata Json? + createdAt DateTime @default(now()) +} + + model EventLog { - id String @id @default(uuid()) - entityType EventEntityType @map("entity_type") - entityId String @map("entity_id") - eventType EventType @map("event_type") + id String @id @default(uuid()) + entityType EventEntityType @map("entity_type") + entityId String @map("entity_id") + eventType EventType @map("event_type") actorId String? @map("actor_id") actor User? @relation("ActorEvents", fields: [actorId], references: [id]) @@ -251,22 +270,18 @@ model EventLog { } model Document { - id String @id @default(uuid()) - fileName String @map("file_name") - fileUrl String @map("file_url") - publicId String @map("public_id") - mimeType String @map("mime_type") - size Int - - uploadedById String @map("uploaded_by_id") - uploadedBy User @relation(fields: [uploadedById], references: [id], onDelete: Cascade) - - adoptionId String? @map("adoption_id") - adoption Adoption? @relation(fields: [adoptionId], references: [id], onDelete: Cascade) - - createdAt DateTime @default(now()) @map("created_at") - - @@index([uploadedById]) - @@index([adoptionId]) - @@map("documents") + id String @id @default(cuid()) + fileName String + fileUrl String + publicId String + mimeType String + size Int + uploadedById String + adoptionId String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + uploadedBy User @relation(fields: [uploadedById], references: [id]) + adoption Adoption @relation(fields: [adoptionId], references: [id]) } + diff --git a/src/app.module.ts b/src/app.module.ts index cc9cc24..df5ab83 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -11,9 +11,10 @@ import { EventsModule } from './events/events.module'; import { StellarModule } from './stellar/stellar.module'; import { AuthModule } from './auth/auth.module'; import { HealthModule } from './health/health.module'; -import { CloudinaryModule } from './cloudinary/cloudinary.module'; -import { UsersModule } from './users/users.module'; -import { EmailModule } from './email/email.module'; +import { LoggingModule } from './logging/logging.module'; +import { HttpExceptionFilter } from './common/filters/http-exception.filter'; +import { APP_INTERCEPTOR } from '@nestjs/core'; +import { LoggingInterceptor } from './logging/logging.interceptor'; @Module({ imports: [ @@ -27,11 +28,18 @@ import { EmailModule } from './email/email.module'; StellarModule, AuthModule, HealthModule, - CloudinaryModule, - UsersModule, - EmailModule, + LoggingModule, + ], + controllers: [AppController], - providers: [AppService], + providers: [ + { + provide: APP_INTERCEPTOR, + useClass: LoggingInterceptor, + }, + AppService, HttpExceptionFilter], + }) -export class AppModule {} + +export class AppModule { } diff --git a/src/common/filters/http-exception.filter.ts b/src/common/filters/http-exception.filter.ts index 01357c1..92ba7e9 100644 --- a/src/common/filters/http-exception.filter.ts +++ b/src/common/filters/http-exception.filter.ts @@ -6,10 +6,15 @@ import { HttpStatus, } from '@nestjs/common'; import { Request, Response } from 'express'; +import { LoggingService } from '../../logging/logging.service'; @Catch() export class HttpExceptionFilter implements ExceptionFilter { - catch(exception: unknown, host: ArgumentsHost): void { + constructor( + private readonly loggingService: LoggingService, + ) {} + + async catch(exception: unknown, host: ArgumentsHost): Promise { const ctx = host.switchToHttp(); const response = ctx.getResponse(); const request = ctx.getRequest(); @@ -18,14 +23,27 @@ export class HttpExceptionFilter implements ExceptionFilter { let message: string | object = 'Internal server error'; if (exception instanceof HttpException) { - const httpException = exception; - status = httpException.getStatus(); - message = httpException.getResponse(); + status = exception.getStatus(); + message = exception.getResponse(); } else if (exception instanceof Error) { - const error = exception; - message = error.message; + message = exception.message; } + await this.loggingService.log({ + level: 'ERROR', + action: 'HTTP_EXCEPTION', + message: + typeof message === 'string' + ? message + : JSON.stringify(message), + userId: (request as any).user?.sub, + metadata: { + statusCode: status, + path: request.url, + method: request.method, + }, + }); + response.status(status).json({ success: false, statusCode: status, @@ -35,3 +53,14 @@ export class HttpExceptionFilter implements ExceptionFilter { }); } } + + + + + + + + + + + diff --git a/src/common/guards/jwt-auth.guard.ts b/src/common/guards/jwt-auth.guard.ts new file mode 100644 index 0000000..a310097 --- /dev/null +++ b/src/common/guards/jwt-auth.guard.ts @@ -0,0 +1,32 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + UnauthorizedException, +} from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import { Request } from 'express'; + +@Injectable() +export class JwtAuthGuard implements CanActivate { + constructor(private readonly jwtService: JwtService) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const authHeader = request.headers.authorization; + + if (!authHeader || !authHeader.startsWith('Bearer ')) { + throw new UnauthorizedException('Missing or invalid token'); + } + + const token = authHeader.split(' ')[1]; + + try { + const payload = await this.jwtService.verifyAsync(token); + request.user = payload; // 👈 attach user + return true; + } catch { + throw new UnauthorizedException('Invalid or expired token'); + } + } +} \ No newline at end of file diff --git a/src/common/interceptors/logging.interceptor.ts b/src/common/interceptors/logging.interceptor.ts index 7e4a2c0..b9400f2 100644 --- a/src/common/interceptors/logging.interceptor.ts +++ b/src/common/interceptors/logging.interceptor.ts @@ -4,17 +4,60 @@ import { ExecutionContext, CallHandler, } from '@nestjs/common'; -import { Observable, tap } from 'rxjs'; +import { Observable, tap, catchError } from 'rxjs'; +import { LoggingService } from '../../logging/logging.service'; +import { Request, Response } from 'express'; @Injectable() export class LoggingInterceptor implements NestInterceptor { - intercept(context: ExecutionContext, next: CallHandler): Observable { - const request = context.switchToHttp().getRequest(); - const { method, url } = request; - const now = Date.now(); - - return next - .handle() - .pipe(tap(() => console.log(`${method} ${url} - ${Date.now() - now}ms`))); + constructor(private readonly loggingService: LoggingService) {} + + intercept( + context: ExecutionContext, + next: CallHandler, + ): Observable { + const ctx = context.switchToHttp(); + const request = ctx.getRequest(); + const response = ctx.getResponse(); + + const { method, originalUrl } = request; + const startTime = Date.now(); + + return next.handle().pipe( + tap(() => { + const duration = Date.now() - startTime; + + this.loggingService.log({ + level: 'INFO', + action: 'HTTP_REQUEST', + message: `${method} ${originalUrl} ${response.statusCode}`, + userId: request.user?.sub, + metadata: { + method, + path: originalUrl, + statusCode: response.statusCode, + duration, + }, + }); + }), + + catchError((error) => { + const duration = Date.now() - startTime; + + this.loggingService.log({ + level: 'ERROR', + action: 'HTTP_ERROR', + message: error.message, + userId: request.user?.sub, + metadata: { + method, + path: originalUrl, + duration, + }, + }); + + throw error; + }), + ); } -} +} \ No newline at end of file diff --git a/src/custody/custody.service.ts b/src/custody/custody.service.ts index 12d2622..7775f71 100644 --- a/src/custody/custody.service.ts +++ b/src/custody/custody.service.ts @@ -8,6 +8,7 @@ import { EventsService } from '../events/events.service'; import { EscrowService } from '../escrow/escrow.service'; import { CreateCustodyDto } from './dto/create-custody.dto'; import { CustodyResponseDto } from './dto/custody-response.dto'; +import { CustodyStatus } from '@prisma/client'; @Injectable() export class CustodyService { @@ -113,7 +114,7 @@ export class CustodyService { // Create custody record const custodyRecord = await tx.custody.create({ data: { - status: 'PENDING', + status: CustodyStatus.PENDING, type: 'TEMPORARY', holderId: userId, petId, diff --git a/src/logging/logging.interceptor.ts b/src/logging/logging.interceptor.ts new file mode 100644 index 0000000..bc28462 --- /dev/null +++ b/src/logging/logging.interceptor.ts @@ -0,0 +1,63 @@ +import { + Injectable, + NestInterceptor, + ExecutionContext, + CallHandler, +} from '@nestjs/common'; +import { Observable, tap, catchError } from 'rxjs'; +import { LoggingService } from './logging.service'; +import { Request, Response } from 'express'; + +@Injectable() +export class LoggingInterceptor implements NestInterceptor { + constructor(private readonly loggingService: LoggingService) {} + + intercept( + context: ExecutionContext, + next: CallHandler, + ): Observable { + const ctx = context.switchToHttp(); + const request = ctx.getRequest(); + const response = ctx.getResponse(); + + const { method, originalUrl } = request; + const startTime = Date.now(); + + return next.handle().pipe( + tap(() => { + const duration = Date.now() - startTime; + + this.loggingService.log({ + level: 'INFO', + action: 'HTTP_REQUEST', + message: `${method} ${originalUrl} ${response.statusCode}`, + userId: request.user?.sub, + metadata: { + method, + path: originalUrl, + statusCode: response.statusCode, + duration, + }, + }); + }), + + catchError((error) => { + const duration = Date.now() - startTime; + + this.loggingService.log({ + level: 'ERROR', + action: 'HTTP_ERROR', + message: error.message, + userId: request.user?.sub, + metadata: { + method, + path: originalUrl, + duration, + }, + }); + + throw error; + }), + ); + } +} \ No newline at end of file diff --git a/src/logging/logging.module.ts b/src/logging/logging.module.ts new file mode 100644 index 0000000..3171e1c --- /dev/null +++ b/src/logging/logging.module.ts @@ -0,0 +1,13 @@ + +import { Module } from '@nestjs/common'; +import { LoggingService } from './logging.service'; +import { LoggingInterceptor } from './logging.interceptor'; +import { PrismaModule } from '../prisma/prisma.module'; + + +@Module({ + imports: [PrismaModule], + providers: [LoggingService, LoggingInterceptor], + exports: [LoggingService, LoggingInterceptor], +}) +export class LoggingModule {} \ No newline at end of file diff --git a/src/logging/logging.service.spec.ts b/src/logging/logging.service.spec.ts new file mode 100644 index 0000000..9c1c0c0 --- /dev/null +++ b/src/logging/logging.service.spec.ts @@ -0,0 +1,58 @@ + +import { Test, TestingModule } from '@nestjs/testing'; +import { LoggingService } from './logging.service'; +import { PrismaService } from '../prisma/prisma.service'; + +describe('LoggingService', () => { + let service: LoggingService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + LoggingService, + { + provide: PrismaService, + useValue: { + appLog: { + create: jest.fn().mockResolvedValue({ + id: '1', + level: 'INFO', + action: 'TEST', + message: 'Test log', + userId: null, + metadata: null, + }), + }, + }, + }, + ], + }).compile(); + + service = module.get(LoggingService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + it('should create a log entry', async () => { + const result = await service.log({ + level: 'INFO', + action: 'TEST', + message: 'Test log', + }); + expect(result).toBeDefined(); + }); + + it('should return null and not throw when db fails', async () => { + jest.spyOn(service['prisma'].appLog, 'create').mockRejectedValueOnce( + new Error('DB down'), + ); + const result = await service.log({ + level: 'ERROR', + action: 'TEST_FAIL', + message: 'Should not throw', + }); + expect(result).toBeNull(); + }); +}); diff --git a/src/logging/logging.service.ts b/src/logging/logging.service.ts new file mode 100644 index 0000000..64916df --- /dev/null +++ b/src/logging/logging.service.ts @@ -0,0 +1,78 @@ + +// import { Injectable, InternalServerErrorException } from '@nestjs/common'; +// import { PrismaService } from '../../src/prisma/prisma.service'; + +// export type LogLevel = 'INFO' | 'WARN' | 'ERROR'; + +// @Injectable() +// export class LoggingService { +// constructor(private readonly prisma: PrismaService) {} + +// async log(params: { +// level: LogLevel; +// action: string; +// message: string; +// userId?: string; +// metadata?: Record; +// }) { + +// try { +// return await this.prisma.appLog.create({ +// data: { +// level: params.level, +// action: params.action, +// message: params.message, +// userId: params.userId, +// metadata: params.metadata, +// }, +// }); +// } catch (error) { +// // ❗ Acceptance Criteria: no silent failures +// throw new InternalServerErrorException( +// 'Failed to record application log', +// ); +// } +// } +// } + + + +import { Injectable, Logger } from '@nestjs/common'; +import { PrismaService } from '../../src/prisma/prisma.service'; + +export type LogLevel = 'INFO' | 'WARN' | 'ERROR'; + +@Injectable() +export class LoggingService { + private readonly logger = new Logger(LoggingService.name); + + constructor(private readonly prisma: PrismaService) {} + + async log(params: { + level: LogLevel; + action: string; + message: string; + userId?: string; + metadata?: Record; + }) { + try { + return await this.prisma.appLog.create({ + data: { + level: params.level, + action: params.action, + message: params.message, + userId: params.userId, + metadata: params.metadata, + }, + }); + } catch (error) { + // Surface the failure visibly without crashing the caller + this.logger.error( + `Failed to record application log | action: ${params.action} | ${error?.message}`, + error?.stack, + ); + // Return null so callers can optionally handle it + return null; + } + } +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 43f85a0..d487d20 100644 --- a/src/main.ts +++ b/src/main.ts @@ -22,9 +22,13 @@ async function bootstrap() { }), ); - app.useGlobalFilters(new HttpExceptionFilter()); +app.useGlobalFilters( + app.get(HttpExceptionFilter), +); - app.useGlobalInterceptors(new LoggingInterceptor()); +// const loggingInterceptor = app.get(LoggingInterceptor); +// app.useGlobalInterceptors(loggingInterceptor); + app.useLogger(new AppLogger()); diff --git a/src/prisma/prisma.service.spec.ts b/src/prisma/prisma.service.spec.ts index 8062d68..081d417 100644 --- a/src/prisma/prisma.service.spec.ts +++ b/src/prisma/prisma.service.spec.ts @@ -5,6 +5,7 @@ describe('PrismaService', () => { let service: PrismaService; beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ providers: [PrismaService], }).compile(); @@ -15,15 +16,18 @@ describe('PrismaService', () => { afterEach(async () => { await service.$disconnect(); // Ensure proper teardown }); + it('should be defined', () => { expect(service).toBeDefined(); }); + it('should be defined as PrismaService', () => { expect(service.constructor.name).toBe('PrismaService'); }); + it('should have onModuleInit method', () => { expect(typeof service.onModuleInit).toBe('function'); }); diff --git a/src/types/express.d.ts b/src/types/express.d.ts new file mode 100644 index 0000000..1d0e76a --- /dev/null +++ b/src/types/express.d.ts @@ -0,0 +1,13 @@ +import { JwtPayload } from 'jsonwebtoken'; + + + +declare global { + namespace Express { + interface Request { + user?: JwtPayload | any; + } + } +} + +export {}; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 8cbe5b0..77950b5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,12 +20,14 @@ "forceConsistentCasingInFileNames": true, "noImplicitAny": false, "strictBindCallApply": false, - "noFallthroughCasesInSwitch": false - + "noFallthroughCasesInSwitch": false, + "typeRoots": ["./node_modules/@types", "./src/types"], + "types": ["multer"] + }, "watchOptions": { "watchFile": "fixedPollingInterval" } - - + } +