From 7d36a3bc5455a19e981a2e210813250e6a0f51b3 Mon Sep 17 00:00:00 2001 From: Karanjadavi Date: Fri, 19 Jun 2026 13:39:46 +0300 Subject: [PATCH] fix: replace all any request types with typed AuthRequest interface - Add shared JwtUser and AuthRequest interfaces in src/common/types/auth-request.interface.ts - Update JwtStrategy.validate() to return typed JwtUser with sub field - Replace @Request() req: any across users, admin, milestones, campaigns controllers - Replace @Req() req: Request & { user: any } in donations, notifications, api-keys controllers - Add AuthenticatedSocket interface in notifications.gateway.ts to remove (client as any) casts - Replace as any casts in users.service.ts with Prisma.InputJsonValue and typed assertions - Replace as any casts in campaigns.service.ts with Number(), Prisma.EnumCampaignStatusFilter, and type guard - Fix soroban.service.ts XDR response casts and scValResult double-cast via unknown - Fix email.service.ts (info as any).message with narrowed type - Enable @typescript-eslint/no-explicit-any: error in eslint.config.mjs to prevent regressions Closes #17 --- eslint.config.mjs | 9 ++- src/admin/admin.controller.ts | 9 +-- src/api-keys/api-keys.controller.ts | 20 +++--- src/auth/jwt.strategy.ts | 9 +-- src/campaigns/campaigns.controller.ts | 23 ++++--- src/campaigns/campaigns.service.ts | 10 +-- src/common/types/auth-request.interface.ts | 21 +++++++ src/donations/donations.controller.ts | 11 ++-- src/milestones/milestones.controller.ts | 41 +++++------- src/notifications/email.service.ts | 7 ++- src/notifications/notifications.controller.ts | 29 ++++----- src/notifications/notifications.gateway.ts | 21 ++++--- src/stellar/soroban.service.ts | 10 ++- src/users/users.controller.ts | 62 +++++++------------ src/users/users.service.ts | 11 ++-- 15 files changed, 150 insertions(+), 143 deletions(-) create mode 100644 src/common/types/auth-request.interface.ts diff --git a/eslint.config.mjs b/eslint.config.mjs index bedc0a5..81565ab 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,4 +1,4 @@ -// @ts-check +// @ts-check import eslint from '@eslint/js'; import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; import globals from 'globals'; @@ -25,11 +25,14 @@ export default tseslint.config( }, }, { - rules: { - '@typescript-eslint/no-explicit-any': 'off', + + '@typescript-eslint/no-explicit-any': 'error', '@typescript-eslint/no-floating-promises': 'warn', '@typescript-eslint/no-unsafe-argument': 'warn', "prettier/prettier": ["error", { endOfLine: "lf" }], }, }, ); + + + diff --git a/src/admin/admin.controller.ts b/src/admin/admin.controller.ts index 2e33047..49b6b63 100644 --- a/src/admin/admin.controller.ts +++ b/src/admin/admin.controller.ts @@ -1,4 +1,4 @@ -import { +import { Controller, Post, Param, @@ -12,6 +12,7 @@ import { SuspendCampaignDto } from './dtos/suspend-campaign.dto'; import { Roles } from '../common/decorators/roles.decorator'; import { RolesGuard } from '../common/guards/roles.guard'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; +import type { AuthRequest } from '../common/types/auth-request.interface'; @Controller('admin') @UseGuards(JwtAuthGuard, RolesGuard) @@ -19,13 +20,13 @@ import { JwtAuthGuard } from '../auth/jwt-auth.guard'; export class AdminController { constructor(private readonly adminService: AdminService) {} - /** POST /admin/campaigns/:id/suspend — Suspend a campaign (admin only) */ + /** POST /admin/campaigns/:id/suspend - Suspend a campaign (admin only) */ @Post('campaigns/:id/suspend') async suspendCampaign( @Param('id', ParseUUIDPipe) id: string, @Body() dto: SuspendCampaignDto, - @Request() req: any, + @Request() req: AuthRequest, ): Promise<{ message: string }> { - return this.adminService.suspendCampaign(id, dto, req.user.sub, req.user.email); + return this.adminService.suspendCampaign(id, dto, req.user.sub, req.user.walletAddress); } } diff --git a/src/api-keys/api-keys.controller.ts b/src/api-keys/api-keys.controller.ts index a5e17b1..fec8897 100644 --- a/src/api-keys/api-keys.controller.ts +++ b/src/api-keys/api-keys.controller.ts @@ -1,4 +1,4 @@ -import { +import { Controller, Post, Delete, @@ -8,26 +8,21 @@ import { ForbiddenException, NotFoundException, } from '@nestjs/common'; +import type { AuthRequest } from '../common/types/auth-request.interface'; import { randomBytes, createHash } from 'crypto'; import { Request } from 'express'; import { PrismaService } from '../prisma/prisma.service'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { Throttle } from '@nestjs/throttler'; -interface JwtUser { - sub: string; - walletAddress: string; - role: string; -} - @Controller('api-keys') @UseGuards(JwtAuthGuard) export class ApiKeysController { constructor(private readonly prisma: PrismaService) {} - /** POST /api-keys — Generate a new API key (returns raw key only once) */ + /** POST /api-keys — Generate a new API key (returns raw key only once) */ @Post() - async create(@Req() req: Request & { user: JwtUser }): Promise<{ id: string; key: string; prefix: string; scope: string; createdAt: Date }> { + async create(@Req() req: AuthRequest): Promise<{ id: string; key: string; prefix: string; scope: string; createdAt: Date }> { const rawKey = `sk_${randomBytes(32).toString('hex')}`; const prefix = rawKey.slice(0, 12); const keyHash = createHash('sha256').update(rawKey).digest('hex'); @@ -42,7 +37,7 @@ export class ApiKeysController { }, }); - // Return the raw key only once — it cannot be recovered after this response + // Return the raw key only once — it cannot be recovered after this response return { id: apiKey.id, key: rawKey, @@ -52,12 +47,12 @@ export class ApiKeysController { }; } - /** DELETE /api-keys/:id — Revoke an existing API key (soft-delete) */ + /** DELETE /api-keys/:id — Revoke an existing API key (soft-delete) */ @Delete(':id') @Throttle({ default: { limit: 30, ttl: 60_000 } }) async revoke( @Param('id') id: string, - @Req() req: Request & { user: JwtUser }, + @Req() req: AuthRequest, ): Promise<{ message: string }> { const apiKey = await this.prisma.apiKey.findUnique({ where: { id } }); @@ -77,3 +72,4 @@ export class ApiKeysController { return { message: 'API key revoked successfully' }; } } + diff --git a/src/auth/jwt.strategy.ts b/src/auth/jwt.strategy.ts index 536d6a5..e75a217 100644 --- a/src/auth/jwt.strategy.ts +++ b/src/auth/jwt.strategy.ts @@ -1,7 +1,8 @@ -import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { Injectable, UnauthorizedException } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { ExtractJwt, Strategy } from 'passport-jwt'; import { ConfigService } from '@nestjs/config'; +import type { JwtUser } from '../common/types/auth-request.interface'; /** * Passport JWT strategy for OrbitChain. @@ -17,14 +18,14 @@ export class JwtStrategy extends PassportStrategy(Strategy) { }); } - validate(payload: { sub: string; walletAddress: string; role?: string }) { + validate(payload: { sub: string; walletAddress: string; role?: string }): JwtUser { if (!payload?.sub) { throw new UnauthorizedException('Invalid token'); } return { - userId: payload.sub, + sub: payload.sub, walletAddress: payload.walletAddress, - role: payload.role, + role: payload.role ?? 'donor', }; } } diff --git a/src/campaigns/campaigns.controller.ts b/src/campaigns/campaigns.controller.ts index 00b02e0..817d2ab 100644 --- a/src/campaigns/campaigns.controller.ts +++ b/src/campaigns/campaigns.controller.ts @@ -1,4 +1,4 @@ -import { +import { BadRequestException, Controller, Delete, @@ -22,7 +22,7 @@ import { Roles } from '../common/decorators/roles.decorator'; import { RolesGuard } from '../common/guards/roles.guard'; import { UpdateCampaignDto } from './dto/update-campaign.dto'; import { CreateCampaignDto } from './dto/create-campaign.dto'; -import { Request } from 'express'; +import type { AuthRequest } from '../common/types/auth-request.interface'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { AdminGuard } from '../users/guards/admin.guard'; import { BrowseCampaignsQueryDto, BrowseCampaignsResponseDto } from './dto/browse-campaigns.dto'; @@ -56,24 +56,24 @@ export class CampaignsController { return this.campaignsService.getCampaignStats(id); } - /** POST /campaigns — Create a new fundraising campaign */ + /** POST /campaigns — Create a new fundraising campaign */ @Post() @UseGuards(JwtAuthGuard) async create( @Body() body: CreateCampaignDto, - @Req() req: Request & { user: any }, + @Req() req: AuthRequest, ) { const userId = req.user?.sub as string; return this.campaignsService.createCampaign(userId, body); } - /** PATCH /campaigns/:id — Update campaign metadata (protected fields excluded) */ + /** PATCH /campaigns/:id — Update campaign metadata (protected fields excluded) */ @Patch(':id') @UseGuards(JwtAuthGuard) async update( @Param('id') id: string, @Body() body: UpdateCampaignDto, - @Req() req: Request & { user: any }, + @Req() req: AuthRequest, ) { const sentKeys = Object.keys(body || {}); const illegal = sentKeys.filter((k) => FORBIDDEN_FIELDS.includes(k)); @@ -83,7 +83,7 @@ export class CampaignsController { ); } - return this.campaignsService.updateCampaign(req.user.id, id, body); + return this.campaignsService.updateCampaign(req.user.sub, id, body); } @Get() @@ -144,7 +144,7 @@ export class CampaignsController { async createUpdate( @Param('id', ParseUUIDPipe) id: string, @Body() body: CreateUpdateDto, - @Req() req: Request & { user: any }, + @Req() req: AuthRequest, ) { const userId = req.user?.sub as string; return this.campaignsService.createUpdate(id, userId, body); @@ -160,7 +160,7 @@ export class CampaignsController { async deleteUpdate( @Param('id', ParseUUIDPipe) id: string, @Param('updateId', ParseUUIDPipe) updateId: string, - @Req() req: Request & { user: any }, + @Req() req: AuthRequest, ): Promise { const userId = req.user?.sub as string; const isAdmin = req.user?.role === 'ADMIN'; @@ -169,7 +169,7 @@ export class CampaignsController { /** * GET /campaigns/:id/updates - * Public endpoint – returns paginated campaign updates sorted by createdAt DESC + * Public endpoint – returns paginated campaign updates sorted by createdAt DESC */ @Get(':id/updates') async getCampaignUpdates( @@ -200,3 +200,6 @@ export class AdminCampaignsController { return this.campaignsService.featureCampaign(id); } } + + + diff --git a/src/campaigns/campaigns.service.ts b/src/campaigns/campaigns.service.ts index 473f8e7..d3da191 100644 --- a/src/campaigns/campaigns.service.ts +++ b/src/campaigns/campaigns.service.ts @@ -1,4 +1,4 @@ -import { +import { BadRequestException, Injectable, NotFoundException, @@ -34,7 +34,7 @@ export class CampaignsService { const milestoneCreates = (dto.milestones || []).map((m) => ({ title: m.title, description: m.description ?? null, - targetAmount: (m.targetAmount ?? 0) as any, + targetAmount: Number(m.targetAmount ?? 0), dueDate: m.dueDate ? new Date(m.dueDate) : undefined, })); @@ -123,7 +123,7 @@ export class CampaignsService { } if (status) { - where.status = status as any; + where.status = status as Prisma.EnumCampaignStatusFilter; } let orderBy: Prisma.CampaignOrderByWithRelationInput; @@ -428,7 +428,7 @@ export class CampaignsService { }); const byId = new Map(campaigns.map((c) => [c.id, c])); - const ordered = ids.map((id) => byId.get(id)).filter(Boolean) as any[]; + const ordered = ids.map((id) => byId.get(id)).filter((item): item is NonNullable => item !== undefined); return { data: ordered, total, page, limit }; } @@ -506,3 +506,5 @@ function sqlCampaignFilters(input: { category?: string; status?: string }) { return { whereSql }; } + + diff --git a/src/common/types/auth-request.interface.ts b/src/common/types/auth-request.interface.ts new file mode 100644 index 0000000..75305f8 --- /dev/null +++ b/src/common/types/auth-request.interface.ts @@ -0,0 +1,21 @@ +import { Request } from 'express'; + +/** + * Shape of the user object attached to the request after JWT or API-key auth. + * JwtStrategy.validate() returns { sub, walletAddress, role }. + * ApiKeyGuard additionally sets apiKeyId and scope. + */ +export interface JwtUser { + /** UUID of the authenticated user (from JWT `sub` claim) */ + sub: string; + walletAddress: string; + role: string; + /** Present only when authenticated via API key */ + apiKeyId?: string; + scope?: string; +} + +/** Express Request with a typed `user` property populated by guards */ +export interface AuthRequest extends Request { + user: JwtUser; +} diff --git a/src/donations/donations.controller.ts b/src/donations/donations.controller.ts index 112e882..c567320 100644 --- a/src/donations/donations.controller.ts +++ b/src/donations/donations.controller.ts @@ -1,3 +1,4 @@ +import type { AuthRequest } from '../common/types/auth-request.interface'; import { Controller, Post, @@ -15,7 +16,6 @@ import { DonationResponseDto, PlatformTipResponseDto, } from './dto/donation.dto'; -import { Request as ExpressRequest } from 'express'; @Controller('donations') export class DonationsController { @@ -24,7 +24,7 @@ export class DonationsController { @Post() @UseGuards(JwtAuthGuard) async create( - @Req() req: Request & { user: any }, + @Req() req: AuthRequest, @Body() dto: CreateDonationDto, ) { const walletAddress = String(req.user?.walletAddress ?? ''); @@ -33,7 +33,7 @@ export class DonationsController { @UseGuards(JwtAuthGuard) @Get('me') - async getMyDonations(@Request() req: ExpressRequest & { user: any }) { + async getMyDonations(@Request() req: AuthRequest) { const userId = req.user?.sub as string; return this.donationsService.findAll(userId); } @@ -42,7 +42,7 @@ export class DonationsController { @Get(':id') async getDonation( @Param('id') id: string, - @Request() req: ExpressRequest & { user: any }, + @Request() req: AuthRequest, ) { const userId = req.user?.sub as string; return this.donationsService.findById(id, userId); @@ -69,3 +69,6 @@ export class DonationsController { }; } } + + + diff --git a/src/milestones/milestones.controller.ts b/src/milestones/milestones.controller.ts index dd4e3e1..01b29a9 100644 --- a/src/milestones/milestones.controller.ts +++ b/src/milestones/milestones.controller.ts @@ -1,4 +1,4 @@ -import { +import { Controller, Post, Get, @@ -14,49 +14,42 @@ import { FundReleaseResponseDto, } from '../campaigns/dto/request-fund-release.dto'; import { JwtAuthGuard } from '../users/guards/jwt-auth.guard'; +import type { AuthRequest } from '../common/types/auth-request.interface'; @Controller('campaigns/:campaignId/milestones') export class MilestonesController { constructor(private readonly milestonesService: MilestonesService) {} - /** - * POST /campaigns/:campaignId/milestones/:milestoneId/release - * Request fund release for an unlocked milestone (canonical path). - */ + /** POST .../:milestoneId/release - Request fund release (canonical path) */ @UseGuards(JwtAuthGuard) @Post(':milestoneId/release') async requestFundReleaseAlias( @Param('campaignId') campaignId: string, @Param('milestoneId') milestoneId: string, @Body() dto: RequestFundReleaseDto, - @Request() req: any, + @Request() req: AuthRequest, ): Promise { - const creatorId = req.user?.sub as string; return this.milestonesService.requestFundRelease( campaignId, milestoneId, - creatorId, + req.user.sub, dto, ); } - /** - * POST /campaigns/:campaignId/milestones/:milestoneId/fund-releases - * Request fund release for an unlocked milestone (legacy compat path). - */ + /** POST .../:milestoneId/fund-releases - Request fund release (legacy path) */ @UseGuards(JwtAuthGuard) @Post(':milestoneId/fund-releases') async requestFundRelease( @Param('campaignId') campaignId: string, @Param('milestoneId') milestoneId: string, @Body() dto: RequestFundReleaseDto, - @Request() req: any, + @Request() req: AuthRequest, ): Promise { - const creatorId = req.user?.sub as string; return this.milestonesService.requestFundRelease( campaignId, milestoneId, - creatorId, + req.user.sub, dto, ); } @@ -67,23 +60,18 @@ export class MilestonesController { @Param('campaignId') campaignId: string, @Param('milestoneId') milestoneId: string, @Param('releaseId') releaseId: string, - @Request() req?: any, + @Request() req?: Partial, ) { - const userId = req?.user?.sub; - return this.milestonesService.getFundReleaseById(releaseId, userId); + return this.milestonesService.getFundReleaseById(releaseId, req?.user?.sub); } /** List all fund releases for a campaign, optionally scoped to creator */ @Get('fund-releases') async getCampaignFundReleases( @Param('campaignId') campaignId: string, - @Request() req?: any, + @Request() req?: Partial, ) { - const creatorId = req?.user?.sub; - return this.milestonesService.getCampaignFundReleases( - campaignId, - creatorId, - ); + return this.milestonesService.getCampaignFundReleases(campaignId, req?.user?.sub); } /** Aggregate fund release stats grouped by status for a campaign */ @@ -99,9 +87,8 @@ export class MilestonesController { @Param('campaignId') campaignId: string, @Param('milestoneId') milestoneId: string, @Param('releaseId') releaseId: string, - @Request() req: any, + @Request() req: AuthRequest, ) { - const userId = req.user?.sub as string; - return this.milestonesService.cancelFundRelease(releaseId, userId); + return this.milestonesService.cancelFundRelease(releaseId, req.user.sub); } } diff --git a/src/notifications/email.service.ts b/src/notifications/email.service.ts index 709ea13..db66fe7 100644 --- a/src/notifications/email.service.ts +++ b/src/notifications/email.service.ts @@ -1,4 +1,4 @@ -import { Injectable, Logger } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import * as nodemailer from 'nodemailer'; import type { Transporter } from 'nodemailer'; @@ -102,8 +102,8 @@ export class EmailService { ); // In dev mode with jsonTransport, log the message content - if (info.messageId && (info as any).message) { - this.logger.debug(`Email body preview: ${(info as any).message}`); + if (info.messageId && (info as { message?: string }).message) { + this.logger.debug(`Email body preview: ${(info as { message?: string }).message}`); } } catch (error) { this.logger.error( @@ -113,3 +113,4 @@ export class EmailService { } } } + diff --git a/src/notifications/notifications.controller.ts b/src/notifications/notifications.controller.ts index d559156..3004796 100644 --- a/src/notifications/notifications.controller.ts +++ b/src/notifications/notifications.controller.ts @@ -1,4 +1,4 @@ -import { +import { Controller, Get, Patch, @@ -8,45 +8,38 @@ import { Req, UseGuards, } from '@nestjs/common'; -import { Request } from 'express'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { NotificationsService } from './notifications.service'; +import type { AuthRequest } from '../common/types/auth-request.interface'; @Controller('notifications') @UseGuards(JwtAuthGuard) export class NotificationsController { constructor(private readonly notificationsService: NotificationsService) {} - /** - * GET /notifications — Returns the last 50 notifications. - * Optionally filter by ?isRead=true|false - */ - /** GET /notifications — Returns up to 50 notifications, optionally filtered by read status */ + /** GET /notifications - Returns up to 50 notifications, optionally filtered by read status */ @Get() async getNotifications( - @Req() req: Request & { user: any }, + @Req() req: AuthRequest, @Query('isRead') isRead?: string, ) { - const userId = req.user?.sub as string; const isReadFilter = isRead === 'true' ? true : isRead === 'false' ? false : undefined; - return this.notificationsService.getNotifications(userId, isReadFilter); + return this.notificationsService.getNotifications(req.user.sub, isReadFilter); } - /** PATCH /notifications/mark-read — Mark all notifications as read */ + /** PATCH /notifications/mark-read - Mark all notifications as read */ @Patch('mark-read') - async markAllRead(@Req() req: Request & { user: any }) { - const userId = req.user?.sub as string; - return this.notificationsService.markAllRead(userId); + async markAllRead(@Req() req: AuthRequest) { + return this.notificationsService.markAllRead(req.user.sub); } - /** PATCH /notifications/:id/mark-read — Mark a single notification as read */ + /** PATCH /notifications/:id/mark-read - Mark a single notification as read */ @Patch(':id/mark-read') async markOneRead( - @Req() req: Request & { user: any }, + @Req() req: AuthRequest, @Param('id', ParseUUIDPipe) id: string, ) { - const userId = req.user?.sub as string; - return this.notificationsService.markOneRead(userId, id); + return this.notificationsService.markOneRead(req.user.sub, id); } } diff --git a/src/notifications/notifications.gateway.ts b/src/notifications/notifications.gateway.ts index a609388..459fe85 100644 --- a/src/notifications/notifications.gateway.ts +++ b/src/notifications/notifications.gateway.ts @@ -1,4 +1,4 @@ -import { +import { WebSocketGateway, WebSocketServer, OnGatewayConnection, @@ -10,6 +10,11 @@ import { JwtService } from '@nestjs/jwt'; import { ConfigService } from '@nestjs/config'; import type { Server, Socket } from 'socket.io'; +/** Socket extended with the authenticated userId stored on connection */ +interface AuthenticatedSocket extends Socket { + userId?: string; +} + /** * WebSocket gateway providing real-time notification events. * @@ -22,8 +27,8 @@ import type { Server, Socket } from 'socket.io'; * keyed by their userId, so events can be emitted to specific users. * * Emitted events: - * - `notification` – a new in-app notification for the user - * - `donation_received` – a real-time donation alert for campaign creators + * - `notification` – a new in-app notification for the user + * - `donation_received` – a real-time donation alert for campaign creators */ /** WebSocket gateway for real-time notification delivery to authenticated clients */ @WebSocketGateway({ @@ -56,7 +61,7 @@ export class NotificationsGateway * Verify the JWT token on connection. Throws UnauthorizedException to * disconnect the client if the token is missing or invalid. */ - async handleConnection(client: Socket): Promise { + async handleConnection(client: AuthenticatedSocket): Promise { try { const token = client.handshake.auth?.token ?? client.handshake.query?.token; @@ -80,7 +85,7 @@ export class NotificationsGateway // Subscribe the socket to a private room keyed by userId client.join(`user:${userId}`); // Store userId on the socket for disconnect handling - (client as any).userId = userId; + client.userId = userId; this.logger.log( `WebSocket client connected: user=${userId} socket=${client.id}`, @@ -93,8 +98,8 @@ export class NotificationsGateway } } - handleDisconnect(client: Socket): void { - const userId = (client as any).userId ?? 'unknown'; + handleDisconnect(client: AuthenticatedSocket): void { + const userId = client.userId ?? 'unknown'; this.logger.log( `WebSocket client disconnected: user=${userId} socket=${client.id}`, ); @@ -127,3 +132,5 @@ export class NotificationsGateway this.server.to(`user:${userId}`).emit('donation_received', data); } } + + diff --git a/src/stellar/soroban.service.ts b/src/stellar/soroban.service.ts index 2974ff6..b0bfe1f 100644 --- a/src/stellar/soroban.service.ts +++ b/src/stellar/soroban.service.ts @@ -1,4 +1,4 @@ -import { Injectable, BadRequestException } from '@nestjs/common'; +import { Injectable, BadRequestException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { rpc, @@ -161,7 +161,7 @@ export class SorobanService { const response = await this.server.sendTransaction(finalTx); if (response.status === 'ERROR') { - throw this.parseTxResultError((response as any).errorResultXdr || (response as any).errorResult); + throw this.parseTxResultError(((response as { errorResultXdr?: string; errorResult?: string }).errorResultXdr ?? (response as { errorResultXdr?: string; errorResult?: string }).errorResult) ?? ''); } const txResult = await this.pollTransaction(response.hash); @@ -180,7 +180,7 @@ export class SorobanService { const innerSwitch = invokeHostFuncResult.switch().name; if (innerSwitch === 'invokeHostFunctionSuccess') { const scValResult = invokeHostFuncResult.success(); - return scValToNative(scValResult as any); + return scValToNative(scValResult as unknown as xdr.ScVal); } } } @@ -273,3 +273,7 @@ export class SorobanService { } } } + + + + diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index 8cc0cf6..ae3d3b3 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -1,4 +1,4 @@ -import { +import { Controller, Get, Patch, @@ -20,43 +20,42 @@ import { } from './dto/notification-preferences.dto'; import { GetUserDonationsQueryDto, - GetUserDonationsResponseDto, ExportDonationHistoryQueryDto, } from './dto/get-user-donations.dto'; import { JwtAuthGuard } from './guards/jwt-auth.guard'; import { AdminGuard } from './guards/admin.guard'; +import type { AuthRequest } from '../common/types/auth-request.interface'; @Controller('users') export class UsersController { constructor(private readonly usersService: UsersService) {} - /** GET /users/me — Retrieve authenticated user's full profile */ + /** GET /users/me - Retrieve authenticated user's full profile */ @UseGuards(JwtAuthGuard) @Get('me') - async getMyProfile(@Request() req: any): Promise { + async getMyProfile(@Request() req: AuthRequest): Promise { return this.usersService.getMyProfile(req.user.walletAddress); } - /** PATCH /users/me — Update authenticated user's profile */ + /** PATCH /users/me - Update authenticated user's profile */ @UseGuards(JwtAuthGuard) @Patch('me') async updateMyProfile( - @Request() req: any, + @Request() req: AuthRequest, @Body() updateDto: UpdateUserDto, ): Promise { return this.usersService.updateMyProfile(req.user.walletAddress, updateDto); } - /** GET /users/me/donations — Retrieve donation history with filters */ + /** GET /users/me/donations - Retrieve donation history with filters */ @UseGuards(JwtAuthGuard) @Get('me/donations') async getMyDonations( - @Request() req: any, + @Request() req: AuthRequest, @Query() query: GetUserDonationsQueryDto, - ): Promise { - const userId = req.user?.sub as string; + ) { return this.usersService.getUserDonationHistory( - userId, + req.user.sub, query.page, query.limit, query.sortBy, @@ -69,27 +68,23 @@ export class UsersController { /** * GET /users/me/donations/export - * Export user's donation history as CSV. - * Small exports (<= 500 rows) are returned inline. - * Large exports are queued via Bull; a jobId is returned for polling. + * Small exports (<= 500 rows) returned inline; large exports queued via Bull. */ @UseGuards(JwtAuthGuard) @Get('me/donations/export') async exportMyDonations( - @Request() req: any, + @Request() req: AuthRequest, @Query() query: ExportDonationHistoryQueryDto, @Res() res: Response, ): Promise { - const userId = req.user?.sub as string; const result = await this.usersService.exportUserDonationsAsCSV( - userId, + req.user.sub, query.campaignId, query.startDate, query.endDate, ); if (result.queued) { - // Large export — return 202 Accepted with jobId for polling res.status(202).json({ message: 'Export queued. Poll the status endpoint for completion.', jobId: result.jobId, @@ -98,19 +93,14 @@ export class UsersController { return; } - // Small export — return CSV inline res.setHeader('Content-Type', 'text/csv'); - res.setHeader( - 'Content-Disposition', - 'attachment; filename="donations.csv"', - ); + res.setHeader('Content-Disposition', 'attachment; filename="donations.csv"'); res.status(200).send(result.csv); } /** * GET /users/me/donations/export/:jobId/status - * Poll the status of a queued export job. - * Returns the CSV when the job is complete. + * Poll status of a queued export job. */ @UseGuards(JwtAuthGuard) @Get('me/donations/export/:jobId/status') @@ -122,10 +112,7 @@ export class UsersController { if (result.status === 'completed' && result.csv) { res.setHeader('Content-Type', 'text/csv'); - res.setHeader( - 'Content-Disposition', - 'attachment; filename="donations.csv"', - ); + res.setHeader('Content-Disposition', 'attachment; filename="donations.csv"'); res.status(200).send(result.csv); return; } @@ -133,29 +120,26 @@ export class UsersController { res.status(200).json({ status: result.status, rowCount: result.rowCount }); } - /** GET /users/me/notification-preferences — Retrieve preferences */ + /** GET /users/me/notification-preferences - Retrieve preferences */ @UseGuards(JwtAuthGuard) @Get('me/notification-preferences') async getNotificationPreferences( - @Request() req: any, + @Request() req: AuthRequest, ): Promise { return this.usersService.getNotificationPreferences(req.user.sub); } - /** PATCH /users/me/notification-preferences — Update preferences */ + /** PATCH /users/me/notification-preferences - Update preferences */ @UseGuards(JwtAuthGuard) @Patch('me/notification-preferences') async updateNotificationPreferences( - @Request() req: any, + @Request() req: AuthRequest, @Body() updateDto: UpdateNotificationPreferencesDto, ): Promise { - return this.usersService.updateNotificationPreferences( - req.user.sub, - updateDto, - ); + return this.usersService.updateNotificationPreferences(req.user.sub, updateDto); } - /** GET /users/:walletAddress — Retrieve public user profile */ + /** GET /users/:walletAddress - Retrieve public user profile */ @Get(':walletAddress') async getPublicProfile( @Param('walletAddress') walletAddress: string, @@ -177,7 +161,7 @@ export class AdminUsersController { async updateKYCStatus( @Param('id') userId: string, @Body() updateDto: UpdateKYCStatusDto, - @Request() req: any, + @Request() req: AuthRequest, ): Promise<{ success: boolean; message: string }> { return this.usersService.updateKYCStatus( userId, diff --git a/src/users/users.service.ts b/src/users/users.service.ts index a9b7e86..4ed385f 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -1,4 +1,4 @@ -import { +import { Injectable, NotFoundException, BadRequestException, @@ -92,7 +92,7 @@ export class UsersService { displayName: updateDto.displayName ?? user.displayName, bio: updateDto.bio ?? user.bio, avatarUrl: updateDto.avatarUrl ?? user.avatarUrl, - socialLinks: (updateDto.socialLinks ?? user.socialLinks) as any, + socialLinks: (updateDto.socialLinks ?? user.socialLinks) as Prisma.InputJsonValue, }, include: { campaigns: { @@ -412,7 +412,7 @@ export class UsersService { for (const donation of donations) { const row = [ - `"${((donation as any).campaign?.title || 'Unknown').replace(/"/g, '""')}"`, + `"${((donation as unknown as { campaign?: { title?: string } }).campaign?.title || 'Unknown').replace(/"/g, '""')}"`, donation.amount.toString(), donation.assetCode, donation.donatedAt.toISOString().split('T')[0], @@ -498,8 +498,8 @@ export class UsersService { // Upsert the preference record const prefs = await this.prisma.notificationPreference.upsert({ where: { userId }, - update: { preferences: merged as any }, - create: { userId, preferences: merged as any }, + update: { preferences: merged as Prisma.InputJsonValue }, + create: { userId, preferences: merged as Prisma.InputJsonValue }, }); return prefs.preferences as unknown as NotificationPreferencesDto; @@ -536,3 +536,4 @@ export class UsersService { return { status: state }; } } +