Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// @ts-check
// @ts-check
import eslint from '@eslint/js';
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
import globals from 'globals';
Expand All @@ -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
Expand Down Expand Up @@ -52,3 +54,6 @@ export default tseslint.config(
},
},
);



8 changes: 5 additions & 3 deletions src/admin/admin.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {
import {
Controller,
Post,
Param,
Expand All @@ -12,20 +12,22 @@ 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)
@Roles('admin')
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,
Expand Down
19 changes: 8 additions & 11 deletions src/api-keys/api-keys.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {
import {
Controller,
Post,
Delete,
Expand All @@ -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;
Expand All @@ -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,
Expand All @@ -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 } });

Expand All @@ -83,3 +79,4 @@ export class ApiKeysController {
return { message: 'API key revoked successfully' };
}
}

9 changes: 5 additions & 4 deletions src/auth/jwt.strategy.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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',
};
}
}
23 changes: 13 additions & 10 deletions src/campaigns/campaigns.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {
import {
BadRequestException,
Controller,
Delete,
Expand All @@ -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 {
Expand Down Expand Up @@ -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));
Expand All @@ -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()
Expand Down Expand Up @@ -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);
Expand All @@ -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<void> {
const userId = req.user?.sub as string;
const isAdmin = req.user?.role === 'ADMIN';
Expand All @@ -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(
Expand Down Expand Up @@ -205,3 +205,6 @@ export class AdminCampaignsController {
return this.campaignsService.featureCampaign(id);
}
}



10 changes: 6 additions & 4 deletions src/campaigns/campaigns.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {
import {
BadRequestException,
Injectable,
NotFoundException,
Expand Down Expand Up @@ -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,
}));

Expand Down Expand Up @@ -125,7 +125,7 @@ export class CampaignsService {
}

if (status) {
where.status = status as any;
where.status = status as Prisma.EnumCampaignStatusFilter;
}

let orderBy: Prisma.CampaignOrderByWithRelationInput;
Expand Down Expand Up @@ -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<typeof item> => item !== undefined);

return { data: ordered, total, page, limit };
}
Expand Down Expand Up @@ -518,3 +518,5 @@ function sqlCampaignFilters(input: { category?: string; status?: string }) {

return { whereSql };
}


21 changes: 21 additions & 0 deletions src/common/types/auth-request.interface.ts
Original file line number Diff line number Diff line change
@@ -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;
}
11 changes: 7 additions & 4 deletions src/donations/donations.controller.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { AuthRequest } from '../common/types/auth-request.interface';
import {
Controller,
Post,
Expand All @@ -15,7 +16,6 @@ import {
DonationResponseDto,
PlatformTipResponseDto,
} from './dto/donation.dto';
import { Request as ExpressRequest } from 'express';

@Controller('donations')
export class DonationsController {
Expand All @@ -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 ?? '');
Expand All @@ -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);
}
Expand All @@ -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);
Expand All @@ -69,3 +69,6 @@ export class DonationsController {
};
}
}



Loading
Loading