diff --git a/eslint.config.mjs b/eslint.config.mjs index ff47986..9d6a0ba 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,6 +25,8 @@ export default tseslint.config( }, }, { + + '@typescript-eslint/no-explicit-any': 'error', rules: { '@typescript-eslint/no-explicit-any': 'off', // FAST-PASS: the codebase predates `recommendedTypeChecked` and `any` is @@ -52,3 +54,6 @@ export default tseslint.config( }, }, ); + + + diff --git a/src/admin/admin.controller.ts b/src/admin/admin.controller.ts index 2a27932..b931e5d 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,14 @@ 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.walletAddress); return this.adminService.suspendCampaign( id, dto, diff --git a/src/api-keys/api-keys.controller.ts b/src/api-keys/api-keys.controller.ts index 7a2c195..f74d29d 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,25 +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: AuthRequest): Promise<{ id: string; key: string; prefix: string; scope: string; createdAt: Date }> { async create(@Req() req: Request & { user: JwtUser }): Promise<{ id: string; key: string; @@ -48,7 +44,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, @@ -58,12 +54,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 } }); @@ -83,3 +79,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 7114490..d1e4148 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 { @@ -62,24 +62,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)); @@ -89,7 +89,7 @@ export class CampaignsController { ); } - return this.campaignsService.updateCampaign(req.user.id, id, body); + return this.campaignsService.updateCampaign(req.user.sub, id, body); } @Get() @@ -149,7 +149,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); @@ -165,7 +165,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'; @@ -174,7 +174,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( @@ -205,3 +205,6 @@ export class AdminCampaignsController { return this.campaignsService.featureCampaign(id); } } + + + diff --git a/src/campaigns/campaigns.service.ts b/src/campaigns/campaigns.service.ts index 5099b39..b643a79 100644 --- a/src/campaigns/campaigns.service.ts +++ b/src/campaigns/campaigns.service.ts @@ -1,4 +1,4 @@ -import { +import { BadRequestException, Injectable, NotFoundException, @@ -36,7 +36,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, })); @@ -125,7 +125,7 @@ export class CampaignsService { } if (status) { - where.status = status as any; + where.status = status as Prisma.EnumCampaignStatusFilter; } let orderBy: Prisma.CampaignOrderByWithRelationInput; @@ -440,7 +440,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 }; } @@ -518,3 +518,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 cf37cb7..2a8b5a8 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,6 +102,8 @@ export class EmailService { ); // In dev mode with jsonTransport, log the message content + if (info.messageId && (info as { message?: string }).message) { + this.logger.debug(`Email body preview: ${(info as { message?: string }).message}`); if (info.messageId && info.message) { this.logger.debug(`Email body preview: ${info.message}`); } @@ -113,3 +115,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 e082e71..3635199 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,6 +161,7 @@ export class SorobanService { const response = await this.server.sendTransaction(finalTx); if (response.status === 'ERROR') { + throw this.parseTxResultError(((response as { errorResultXdr?: string; errorResult?: string }).errorResultXdr ?? (response as { errorResultXdr?: string; errorResult?: string }).errorResult) ?? ''); throw this.parseTxResultError( (response as any).errorResultXdr || (response as any).errorResult, ); @@ -182,7 +183,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); } } } @@ -275,3 +276,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 d2ef560..f793c73 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: { @@ -413,7 +413,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], @@ -499,8 +499,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; @@ -537,3 +537,4 @@ export class UsersService { return { status: state }; } } +