From 410a7f379b822679c50168fcb41375ed299aae13 Mon Sep 17 00:00:00 2001 From: JACOB STANLEY Date: Sun, 21 Jun 2026 00:05:12 +0100 Subject: [PATCH 1/2] Add Swagger request/response examples for all DTOs and endpoints --- src/app.controller.ts | 9 +- src/auth/auth.controller.ts | 111 +++++++++++- src/auth/dto/login-stellar.dto.ts | 6 +- src/auth/dto/login.dto.ts | 4 +- src/auth/dto/signup.dto.ts | 8 +- src/health/health.controller.ts | 55 +++++- src/notifications/notifications.controller.ts | 52 +++++- src/profiles/dto/analytics.dto.ts | 2 + src/profiles/dto/create-profile.dto.ts | 6 +- src/profiles/dto/update-social-links.dto.ts | 8 +- src/profiles/profiles.controller.ts | 135 +++++++++++++- src/shared/dto/pagination.dto.ts | 3 +- src/stellar/stellar.controller.ts | 52 +++++- src/tips/dto/create-tip.dto.ts | 10 +- src/tips/tips.controller.ts | 167 +++++++++++++++++- 15 files changed, 601 insertions(+), 27 deletions(-) diff --git a/src/app.controller.ts b/src/app.controller.ts index e0d4abe..3ea4176 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -1,5 +1,5 @@ import { Controller, Get } from '@nestjs/common'; -import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { ApiOperation, ApiTags, ApiResponse } from '@nestjs/swagger'; import { AppService } from './app.service'; @ApiTags('app') @@ -8,6 +8,13 @@ export class AppController { constructor(private readonly appService: AppService) {} @ApiOperation({ summary: 'API welcome message' }) + @ApiResponse({ + status: 200, + description: 'Welcome message retrieved successfully', + schema: { + example: 'Welcome to StellarTip-Backend API!', + }, + }) @Get() getHello(): string { return this.appService.getHello(); diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index fe38ea9..a969926 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -10,7 +10,7 @@ import { HttpStatus, } from '@nestjs/common'; import { Request } from 'express'; -import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; +import { ApiTags, ApiOperation, ApiBearerAuth, ApiBody, ApiResponse } from '@nestjs/swagger'; import { AuthService } from './auth.service'; import { SignupDto } from './dto/signup.dto'; import { LoginDto } from './dto/login.dto'; @@ -24,6 +24,34 @@ export class AuthController { constructor(private readonly authService: AuthService) {} @ApiOperation({ summary: 'Login with Stellar wallet address' }) + @ApiBody({ + schema: { + type: 'object', + properties: { + walletAddress: { + type: 'string', + example: 'GABC123XYZ456DEF789GHI012JKL345MNO678PQR890STU123VWX456YZ', + }, + }, + required: ['walletAddress'], + }, + }) + @ApiResponse({ + status: 200, + description: 'Successfully authenticated with Stellar wallet', + schema: { + example: { + access_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...', + refresh_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...', + expires_in: 3600, + user: { + id: '123e4567-e89b-12d3-a456-426614174000', + username: 'johndoe', + email: 'user@example.com', + }, + }, + }, + }) @Post('stellar/login') @AuthThrottle() async loginStellar(@Body('walletAddress') walletAddress: string): Promise<{ @@ -50,6 +78,16 @@ export class AuthController { } @ApiOperation({ summary: 'Get authentication nonce for Stellar wallet' }) + @ApiResponse({ + status: 200, + description: 'Nonce generated successfully', + schema: { + example: { + nonce: 'abc123def456', + message: 'Please sign this message to authenticate with StellarTip-Backend. Nonce: abc123def456 Timestamp: 2024-01-01T00:00:00Z', + }, + }, + }) @Get('nonce') @AuthThrottle() getNonce(@Query('walletAddress') walletAddress: string): { @@ -66,6 +104,23 @@ export class AuthController { } @ApiOperation({ summary: 'Create a new account' }) + @ApiResponse({ + status: 201, + description: 'Account created successfully', + schema: { + example: { + access_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...', + refresh_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...', + expires_in: 3600, + user: { + id: '123e4567-e89b-12d3-a456-426614174000', + username: 'johndoe', + email: 'user@example.com', + displayName: 'John Doe', + }, + }, + }, + }) @Post('signup') @AuthThrottle() async signup(@Body() signupDto: SignupDto): Promise<{ @@ -83,6 +138,22 @@ export class AuthController { } @ApiOperation({ summary: 'Login with email and password' }) + @ApiResponse({ + status: 200, + description: 'Successfully logged in', + schema: { + example: { + access_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...', + refresh_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...', + expires_in: 3600, + user: { + id: '123e4567-e89b-12d3-a456-426614174000', + username: 'johndoe', + email: 'user@example.com', + }, + }, + }, + }) @Post('login') @AuthThrottle() async login(@Body() loginDto: LoginDto): Promise<{ @@ -95,6 +166,29 @@ export class AuthController { } @ApiOperation({ summary: 'Refresh access token using refresh token' }) + @ApiBody({ + schema: { + type: 'object', + properties: { + refresh_token: { + type: 'string', + example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...', + }, + }, + required: ['refresh_token'], + }, + }) + @ApiResponse({ + status: 200, + description: 'Token refreshed successfully', + schema: { + example: { + access_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...', + refresh_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...', + expires_in: 3600, + }, + }, + }) @Post('refresh') async refresh(@Body('refresh_token') refreshToken: string): Promise<{ access_token: string; @@ -111,6 +205,21 @@ export class AuthController { } @ApiOperation({ summary: 'Get current user profile from JWT' }) + @ApiResponse({ + status: 200, + description: 'User profile retrieved successfully', + schema: { + example: { + id: '123e4567-e89b-12d3-a456-426614174000', + username: 'johndoe', + email: 'user@example.com', + displayName: 'John Doe', + bio: 'Content creator and developer', + avatarUrl: 'https://example.com/avatar.jpg', + createdAt: '2024-01-01T00:00:00Z', + }, + }, + }) @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Get('profile') diff --git a/src/auth/dto/login-stellar.dto.ts b/src/auth/dto/login-stellar.dto.ts index 5fa4e82..da0e672 100644 --- a/src/auth/dto/login-stellar.dto.ts +++ b/src/auth/dto/login-stellar.dto.ts @@ -2,17 +2,17 @@ import { IsString, IsNotEmpty } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; export class LoginStellarDto { - @ApiProperty({ description: 'Stellar wallet public key (G...)' }) + @ApiProperty({ description: 'Stellar wallet public key (G...)', example: 'GABC123XYZ456DEF789GHI012JKL345MNO678PQR890STU123VWX456YZ' }) @IsString() @IsNotEmpty() walletAddress: string; - @ApiProperty({ description: 'Signed message for verification' }) + @ApiProperty({ description: 'Signed message for verification', example: 'Please sign this message to authenticate with StellarTip-Backend. Timestamp: 2024-01-01T00:00:00Z' }) @IsString() @IsNotEmpty() message: string; - @ApiProperty({ description: 'Base64-encoded Stellar signature' }) + @ApiProperty({ description: 'Base64-encoded Stellar signature', example: 'ABC123XYZ456DEF789GHI012JKL345MNO678PQR890STU123VWX456YZ==' }) @IsString() @IsNotEmpty() signature: string; diff --git a/src/auth/dto/login.dto.ts b/src/auth/dto/login.dto.ts index a242ff6..c45615a 100644 --- a/src/auth/dto/login.dto.ts +++ b/src/auth/dto/login.dto.ts @@ -2,12 +2,12 @@ import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; export class LoginDto { - @ApiProperty({ description: 'Registered email address' }) + @ApiProperty({ description: 'Registered email address', example: 'user@example.com' }) @IsEmail() @IsNotEmpty() email: string; - @ApiProperty({ description: 'Account password' }) + @ApiProperty({ description: 'Account password', example: 'SecurePassword123!' }) @IsString() @IsNotEmpty() password: string; diff --git a/src/auth/dto/signup.dto.ts b/src/auth/dto/signup.dto.ts index c24a27a..c31e539 100644 --- a/src/auth/dto/signup.dto.ts +++ b/src/auth/dto/signup.dto.ts @@ -8,24 +8,24 @@ import { import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; export class SignupDto { - @ApiProperty({ description: 'Unique username', minLength: 3 }) + @ApiProperty({ description: 'Unique username', minLength: 3, example: 'johndoe' }) @IsString() @IsNotEmpty() @MinLength(3) username: string; - @ApiProperty({ description: 'User email address' }) + @ApiProperty({ description: 'User email address', example: 'user@example.com' }) @IsEmail() @IsNotEmpty() email: string; - @ApiProperty({ description: 'Account password', minLength: 8 }) + @ApiProperty({ description: 'Account password', minLength: 8, example: 'SecurePassword123!' }) @IsString() @IsNotEmpty() @MinLength(8) password: string; - @ApiPropertyOptional({ description: 'Display name shown on profile' }) + @ApiPropertyOptional({ description: 'Display name shown on profile', example: 'John Doe' }) @IsOptional() @IsString() displayName?: string; diff --git a/src/health/health.controller.ts b/src/health/health.controller.ts index 4234230..1f9e49f 100644 --- a/src/health/health.controller.ts +++ b/src/health/health.controller.ts @@ -1,6 +1,6 @@ import { Controller, Get, HttpStatus, Res } from '@nestjs/common'; import { Response } from 'express'; -import { ApiTags, ApiOperation } from '@nestjs/swagger'; +import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; import { HealthService } from './health.service'; import { SkipApiThrottle } from '../config/throttle.config'; @@ -11,6 +11,18 @@ export class HealthController { constructor(private readonly healthService: HealthService) {} @ApiOperation({ summary: 'Basic health check' }) + @ApiResponse({ + status: 200, + description: 'Health check successful', + schema: { + example: { + status: 'ok', + timestamp: '2024-01-15T00:00:00Z', + uptime: 3600, + version: '1.0.0', + }, + }, + }) @Get() getHealth(): { status: string; @@ -22,6 +34,26 @@ export class HealthController { } @ApiOperation({ summary: 'Readiness probe (checks database)' }) + @ApiResponse({ + status: 200, + description: 'Service is ready', + schema: { + example: { + status: 'ok', + database: 'connected', + }, + }, + }) + @ApiResponse({ + status: 503, + description: 'Service is not ready', + schema: { + example: { + status: 'error', + database: 'disconnected', + }, + }, + }) @Get('ready') async getReady(@Res() res: Response): Promise { const result = await this.healthService.getReadiness(); @@ -31,6 +63,27 @@ export class HealthController { } @ApiOperation({ summary: 'Remote health check (checks Stellar Horizon)' }) + @ApiResponse({ + status: 200, + description: 'Remote services are healthy', + schema: { + example: { + status: 'ok', + horizon: 'connected', + latency: 150, + }, + }, + }) + @ApiResponse({ + status: 503, + description: 'Remote services are unhealthy', + schema: { + example: { + status: 'error', + horizon: 'disconnected', + }, + }, + }) @Get('remote') async getRemote(@Res() res: Response): Promise { const result = await this.healthService.getRemoteHealth(); diff --git a/src/notifications/notifications.controller.ts b/src/notifications/notifications.controller.ts index 1319560..6fb57a3 100644 --- a/src/notifications/notifications.controller.ts +++ b/src/notifications/notifications.controller.ts @@ -8,7 +8,7 @@ import { Req, } from '@nestjs/common'; import { Request } from 'express'; -import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; +import { ApiTags, ApiOperation, ApiBearerAuth, ApiResponse } from '@nestjs/swagger'; import { NotificationsService } from './notifications.service'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { Notification } from '../entities/notification.entity'; @@ -19,6 +19,31 @@ export class NotificationsController { constructor(private readonly notificationsService: NotificationsService) {} @ApiOperation({ summary: 'Get user notifications with pagination' }) + @ApiResponse({ + status: 200, + description: 'Notifications retrieved successfully', + schema: { + example: { + data: [ + { + id: '123e4567-e89b-12d3-a456-426614174000', + userId: '123e4567-e89b-12d3-a456-426614174000', + type: 'tip_received', + title: 'You received a tip!', + message: 'John Doe sent you 10.5 XLM', + read: false, + createdAt: '2024-01-15T00:00:00Z', + }, + ], + total: 50, + page: 1, + limit: 20, + totalPages: 3, + hasNextPage: true, + hasPreviousPage: false, + }, + }, + }) @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Get() @@ -43,6 +68,15 @@ export class NotificationsController { } @ApiOperation({ summary: 'Get count of unread notifications' }) + @ApiResponse({ + status: 200, + description: 'Unread count retrieved successfully', + schema: { + example: { + unreadCount: 5, + }, + }, + }) @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Get('unread-count') @@ -51,6 +85,22 @@ export class NotificationsController { } @ApiOperation({ summary: 'Mark a notification as read' }) + @ApiResponse({ + status: 200, + description: 'Notification marked as read successfully', + schema: { + example: { + id: '123e4567-e89b-12d3-a456-426614174000', + userId: '123e4567-e89b-12d3-a456-426614174000', + type: 'tip_received', + title: 'You received a tip!', + message: 'John Doe sent you 10.5 XLM', + read: true, + createdAt: '2024-01-15T00:00:00Z', + updatedAt: '2024-01-15T00:05:00Z', + }, + }, + }) @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Patch(':id/read') diff --git a/src/profiles/dto/analytics.dto.ts b/src/profiles/dto/analytics.dto.ts index 247644c..dbbf770 100644 --- a/src/profiles/dto/analytics.dto.ts +++ b/src/profiles/dto/analytics.dto.ts @@ -14,6 +14,7 @@ export class AnalyticsQueryDto { description: 'Time period for analytics', enum: AnalyticsPeriod, default: AnalyticsPeriod.MONTH, + example: AnalyticsPeriod.MONTH, }) @IsOptional() @IsEnum(AnalyticsPeriod) @@ -21,6 +22,7 @@ export class AnalyticsQueryDto { @ApiPropertyOptional({ description: 'Filter by asset type (XLM or USDC)', + example: 'XLM', }) @IsOptional() @IsString() diff --git a/src/profiles/dto/create-profile.dto.ts b/src/profiles/dto/create-profile.dto.ts index ad860ca..ea4e3f2 100644 --- a/src/profiles/dto/create-profile.dto.ts +++ b/src/profiles/dto/create-profile.dto.ts @@ -2,18 +2,18 @@ import { IsString, IsOptional, IsUrl, MaxLength } from 'class-validator'; import { ApiPropertyOptional } from '@nestjs/swagger'; export class CreateProfileDto { - @ApiPropertyOptional({ description: 'Short bio', maxLength: 500 }) + @ApiPropertyOptional({ description: 'Short bio', maxLength: 500, example: 'Content creator and developer passionate about open source' }) @IsString() @IsOptional() @MaxLength(500) bio?: string; - @ApiPropertyOptional({ description: 'Display name shown on profile' }) + @ApiPropertyOptional({ description: 'Display name shown on profile', example: 'John Doe' }) @IsString() @IsOptional() displayName?: string; - @ApiPropertyOptional({ description: 'URL to avatar image' }) + @ApiPropertyOptional({ description: 'URL to avatar image', example: 'https://example.com/avatar.jpg' }) @IsUrl() @IsOptional() avatarUrl?: string; diff --git a/src/profiles/dto/update-social-links.dto.ts b/src/profiles/dto/update-social-links.dto.ts index ceb15d0..a1e7707 100644 --- a/src/profiles/dto/update-social-links.dto.ts +++ b/src/profiles/dto/update-social-links.dto.ts @@ -2,25 +2,25 @@ import { IsOptional, IsUrl, MaxLength } from 'class-validator'; import { ApiPropertyOptional } from '@nestjs/swagger'; export class UpdateSocialLinksDto { - @ApiPropertyOptional({ description: 'Twitter profile URL (https://...)' }) + @ApiPropertyOptional({ description: 'Twitter profile URL (https://...)', example: 'https://twitter.com/johndoe' }) @IsOptional() @IsUrl({ require_tld: false, require_protocol: true, protocols: ['https'] }) @MaxLength(200) twitter?: string; - @ApiPropertyOptional({ description: 'GitHub profile URL (https://...)' }) + @ApiPropertyOptional({ description: 'GitHub profile URL (https://...)', example: 'https://github.com/johndoe' }) @IsOptional() @IsUrl({ require_tld: false, require_protocol: true, protocols: ['https'] }) @MaxLength(200) github?: string; - @ApiPropertyOptional({ description: 'YouTube channel URL (https://...)' }) + @ApiPropertyOptional({ description: 'YouTube channel URL (https://...)', example: 'https://youtube.com/@johndoe' }) @IsOptional() @IsUrl({ require_tld: false, require_protocol: true, protocols: ['https'] }) @MaxLength(200) youtube?: string; - @ApiPropertyOptional({ description: 'Personal website URL (https://...)' }) + @ApiPropertyOptional({ description: 'Personal website URL (https://...)', example: 'https://johndoe.com' }) @IsOptional() @IsUrl({ require_tld: false, require_protocol: true, protocols: ['https'] }) @MaxLength(200) diff --git a/src/profiles/profiles.controller.ts b/src/profiles/profiles.controller.ts index 6e9444d..d4b521a 100644 --- a/src/profiles/profiles.controller.ts +++ b/src/profiles/profiles.controller.ts @@ -17,7 +17,7 @@ import { } from '@nestjs/common'; import { Request } from 'express'; import { FileInterceptor } from '@nestjs/platform-express'; -import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; +import { ApiTags, ApiOperation, ApiBearerAuth, ApiResponse } from '@nestjs/swagger'; import { CacheInterceptor } from '@nestjs/cache-manager'; import { ProfilesService } from './profiles.service'; import { CreateProfileDto } from './dto/create-profile.dto'; @@ -31,6 +31,22 @@ export class ProfilesController { constructor(private readonly profilesService: ProfilesService) {} @ApiOperation({ summary: 'Get tipping info for a creator profile' }) + @ApiResponse({ + status: 200, + description: 'Tipping info retrieved successfully', + schema: { + example: { + walletAddress: 'GABC123XYZ456DEF789GHI012JKL345MNO678PQR890STU123VWX456YZ', + displayName: 'John Doe', + bio: 'Content creator and developer', + avatarUrl: 'https://example.com/avatar.jpg', + socialLinks: { + twitter: 'https://twitter.com/johndoe', + github: 'https://github.com/johndoe', + }, + }, + }, + }) @Get(':username/tipping-info') async getTippingInfo( @Param('username') username: string, @@ -39,12 +55,51 @@ export class ProfilesController { } @ApiOperation({ summary: 'Get public profile by username' }) + @ApiResponse({ + status: 200, + description: 'Profile retrieved successfully', + schema: { + example: { + id: '123e4567-e89b-12d3-a456-426614174000', + username: 'johndoe', + email: 'user@example.com', + displayName: 'John Doe', + bio: 'Content creator and developer', + avatarUrl: 'https://example.com/avatar.jpg', + socialLinks: { + twitter: 'https://twitter.com/johndoe', + github: 'https://github.com/johndoe', + }, + createdAt: '2024-01-01T00:00:00Z', + }, + }, + }) @Get(':username') async getProfile(@Param('username') username: string): Promise { return this.profilesService.getProfile(username); } @ApiOperation({ summary: 'Search profiles by query' }) + @ApiResponse({ + status: 200, + description: 'Profiles retrieved successfully', + schema: { + example: [ + { + id: '123e4567-e89b-12d3-a456-426614174000', + username: 'johndoe', + displayName: 'John Doe', + avatarUrl: 'https://example.com/avatar.jpg', + }, + { + id: '987f6543-e21b-43d3-a456-426614174000', + username: 'janedoe', + displayName: 'Jane Doe', + avatarUrl: 'https://example.com/avatar2.jpg', + }, + ], + }, + }) @Get() async searchProfiles(@Query('q') query: string): Promise { if (!query) { @@ -54,6 +109,38 @@ export class ProfilesController { } @ApiOperation({ summary: 'Get creator analytics dashboard (cached 5 min)' }) + @ApiResponse({ + status: 200, + description: 'Analytics retrieved successfully', + schema: { + example: { + summary: { + totalTipsReceived: 150, + totalAmountReceived: 1250.5, + averageTipAmount: 8.33, + largestTipAmount: 100, + }, + byAsset: [ + { asset: 'XLM', totalAmount: 1000, tipCount: 120 }, + { asset: 'USDC', totalAmount: 250.5, tipCount: 30 }, + ], + timeSeries: [ + { date: '2024-01-01', count: 10, totalAmount: 85, asset: 'XLM' }, + { date: '2024-01-02', count: 15, totalAmount: 120, asset: 'XLM' }, + ], + topSupporters: [ + { + walletAddress: 'GDEF789GHI012JKL345MNO678PQR890STU123VWX456YZA123BCD456EFG', + totalAmount: 500, + tipCount: 50, + lastTipAt: '2024-01-15T00:00:00Z', + }, + ], + period: '30d', + generatedAt: '2024-01-15T00:00:00Z', + }, + }, + }) @ApiBearerAuth() @UseGuards(JwtAuthGuard) @UseInterceptors(CacheInterceptor) @@ -67,6 +154,26 @@ export class ProfilesController { } @ApiOperation({ summary: 'Update authenticated user profile' }) + @ApiResponse({ + status: 200, + description: 'Profile updated successfully', + schema: { + example: { + id: '123e4567-e89b-12d3-a456-426614174000', + username: 'johndoe', + email: 'user@example.com', + displayName: 'John Doe', + bio: 'Content creator and developer passionate about open source', + avatarUrl: 'https://example.com/avatar.jpg', + socialLinks: { + twitter: 'https://twitter.com/johndoe', + github: 'https://github.com/johndoe', + }, + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-15T00:00:00Z', + }, + }, + }) @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Put('me') @@ -78,6 +185,23 @@ export class ProfilesController { } @ApiOperation({ summary: 'Update social links on profile' }) + @ApiResponse({ + status: 200, + description: 'Social links updated successfully', + schema: { + example: { + id: '123e4567-e89b-12d3-a456-426614174000', + username: 'johndoe', + socialLinks: { + twitter: 'https://twitter.com/johndoe', + github: 'https://github.com/johndoe', + youtube: 'https://youtube.com/@johndoe', + website: 'https://johndoe.com', + }, + updatedAt: '2024-01-15T00:00:00Z', + }, + }, + }) @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Patch('me/social-links') @@ -89,6 +213,15 @@ export class ProfilesController { } @ApiOperation({ summary: 'Upload profile avatar image' }) + @ApiResponse({ + status: 200, + description: 'Avatar uploaded successfully', + schema: { + example: { + avatarUrl: 'https://example.com/avatars/123e4567-e89b-12d3-a456-426614174000.jpg', + }, + }, + }) @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Post('me/avatar') diff --git a/src/shared/dto/pagination.dto.ts b/src/shared/dto/pagination.dto.ts index 4932b87..58a07b2 100644 --- a/src/shared/dto/pagination.dto.ts +++ b/src/shared/dto/pagination.dto.ts @@ -3,7 +3,7 @@ import { Type } from 'class-transformer'; import { ApiPropertyOptional } from '@nestjs/swagger'; export class PaginationDto { - @ApiPropertyOptional({ description: 'Page number', default: 1, minimum: 1 }) + @ApiPropertyOptional({ description: 'Page number', default: 1, minimum: 1, example: 1 }) @IsOptional() @Type(() => Number) @IsInt() @@ -15,6 +15,7 @@ export class PaginationDto { default: 20, minimum: 1, maximum: 100, + example: 20, }) @IsOptional() @Type(() => Number) diff --git a/src/stellar/stellar.controller.ts b/src/stellar/stellar.controller.ts index 9f683d8..7ca9685 100644 --- a/src/stellar/stellar.controller.ts +++ b/src/stellar/stellar.controller.ts @@ -8,7 +8,7 @@ import { Post, Body, } from '@nestjs/common'; -import { ApiTags, ApiOperation } from '@nestjs/swagger'; +import { ApiTags, ApiOperation, ApiResponse, ApiBody } from '@nestjs/swagger'; import { StellarService } from './stellar.service'; @ApiTags('stellar') @@ -19,6 +19,18 @@ export class StellarController { constructor(private readonly stellarService: StellarService) {} @ApiOperation({ summary: 'Get XLM and token balances for a wallet' }) + @ApiResponse({ + status: 200, + description: 'Balances retrieved successfully', + schema: { + example: { + balances: [ + { asset: 'XLM', balance: '1000.5' }, + { asset: 'USDC', balance: '500.25', issuer: 'GA5ZSEJYB37JRC5BVHU4MWOOF7N2FDJQNEBVGTBRDPEKGBQ6776PQJJI' }, + ], + }, + }, + }) @Get('balance') async getBalance( @Query('walletAddress') walletAddress: string, @@ -33,6 +45,19 @@ export class StellarController { } @ApiOperation({ summary: 'Get Stellar account details' }) + @ApiResponse({ + status: 200, + description: 'Account details retrieved successfully', + schema: { + example: { + address: 'GABC123XYZ456DEF789GHI012JKL345MNO678PQR890STU123VWX456YZ', + exists: true, + sequenceNumber: '1234567890', + subentryCount: 5, + network: 'public', + }, + }, + }) @Get('account') async getAccount(@Query('walletAddress') walletAddress: string): Promise<{ address: string; @@ -51,6 +76,31 @@ export class StellarController { } @ApiOperation({ summary: 'Verify a Stellar payment transaction' }) + @ApiBody({ + schema: { + type: 'object', + properties: { + transactionHash: { + type: 'string', + example: 'abc123def4567890123456789012345678901234567890123456789012345678', + }, + }, + required: ['transactionHash'], + }, + }) + @ApiResponse({ + status: 200, + description: 'Payment verification result', + schema: { + example: { + verified: true, + from: 'GDEF789GHI012JKL345MNO678PQR890STU123VWX456YZA123BCD456EFG', + to: 'GABC123XYZ456DEF789GHI012JKL345MNO678PQR890STU123VWX456YZ', + amount: 10.5, + asset: 'XLM', + }, + }, + }) @Post('verify-payment') async verifyPayment( @Body('transactionHash') transactionHash: string, diff --git a/src/tips/dto/create-tip.dto.ts b/src/tips/dto/create-tip.dto.ts index 9599620..1e19b53 100644 --- a/src/tips/dto/create-tip.dto.ts +++ b/src/tips/dto/create-tip.dto.ts @@ -8,24 +8,25 @@ import { import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; export class CreateTipDto { - @ApiProperty({ description: 'Stellar wallet address of the tip recipient' }) + @ApiProperty({ description: 'Stellar wallet address of the tip recipient', example: 'GABC123XYZ456DEF789GHI012JKL345MNO678PQR890STU123VWX456YZ' }) @IsString() @IsNotEmpty() receiverWallet: string; @ApiPropertyOptional({ description: 'Stellar wallet address of the tip sender', + example: 'GDEF789GHI012JKL345MNO678PQR890STU123VWX456YZA123BCD456EFG', }) @IsString() @IsOptional() senderWallet?: string; - @ApiProperty({ description: 'Tip amount', minimum: 0.0000001 }) + @ApiProperty({ description: 'Tip amount', minimum: 0.0000001, example: 10.5 }) @IsNumber() @Min(0.0000001) amount: number; - @ApiPropertyOptional({ description: 'Optional message with the tip' }) + @ApiPropertyOptional({ description: 'Optional message with the tip', example: 'Great content! Keep it up!' }) @IsString() @IsOptional() message?: string; @@ -33,6 +34,7 @@ export class CreateTipDto { @ApiPropertyOptional({ description: 'Asset type: XLM or USDC', default: 'XLM', + example: 'XLM', }) @IsString() @IsOptional() @@ -40,6 +42,7 @@ export class CreateTipDto { @ApiPropertyOptional({ description: 'Asset issuer address (required for USDC)', + example: 'GA5ZSEJYB37JRC5BVHU4MWOOF7N2FDJQNEBVGTBRDPEKGBQ6776PQJJI', }) @IsString() @IsOptional() @@ -47,6 +50,7 @@ export class CreateTipDto { @ApiPropertyOptional({ description: 'Stellar transaction hash for on-chain verification', + example: 'abc123def4567890123456789012345678901234567890123456789012345678', }) @IsString() @IsOptional() diff --git a/src/tips/tips.controller.ts b/src/tips/tips.controller.ts index 3e1b775..80469b4 100644 --- a/src/tips/tips.controller.ts +++ b/src/tips/tips.controller.ts @@ -9,7 +9,7 @@ import { Req, } from '@nestjs/common'; import { Request } from 'express'; -import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; +import { ApiTags, ApiOperation, ApiBearerAuth, ApiResponse, ApiBody } from '@nestjs/swagger'; import { TipsService, TipFilterOptions } from './tips.service'; import { CreateTipDto } from './dto/create-tip.dto'; import { Tip } from '../entities/tip.entity'; @@ -22,6 +22,24 @@ export class TipsController { constructor(private readonly tipsService: TipsService) {} @ApiOperation({ summary: 'Create a new tip' }) + @ApiResponse({ + status: 201, + description: 'Tip created successfully', + schema: { + example: { + id: '123e4567-e89b-12d3-a456-426614174000', + receiverWallet: 'GABC123XYZ456DEF789GHI012JKL345MNO678PQR890STU123VWX456YZ', + senderWallet: 'GDEF789GHI012JKL345MNO678PQR890STU123VWX456YZA123BCD456EFG', + amount: 10.5, + message: 'Great content! Keep it up!', + asset: 'XLM', + assetIssuer: null, + transactionHash: 'abc123def4567890123456789012345678901234567890123456789012345678', + status: 'confirmed', + createdAt: '2024-01-15T00:00:00Z', + }, + }, + }) @Post() @TipCreationThrottle() async createTip(@Body() createTipDto: CreateTipDto): Promise { @@ -34,12 +52,56 @@ export class TipsController { } @ApiOperation({ summary: 'Get a tip by ID' }) + @ApiResponse({ + status: 200, + description: 'Tip retrieved successfully', + schema: { + example: { + id: '123e4567-e89b-12d3-a456-426614174000', + receiverWallet: 'GABC123XYZ456DEF789GHI012JKL345MNO678PQR890STU123VWX456YZ', + senderWallet: 'GDEF789GHI012JKL345MNO678PQR890STU123VWX456YZA123BCD456EFG', + amount: 10.5, + message: 'Great content! Keep it up!', + asset: 'XLM', + assetIssuer: null, + transactionHash: 'abc123def4567890123456789012345678901234567890123456789012345678', + status: 'confirmed', + createdAt: '2024-01-15T00:00:00Z', + }, + }, + }) @Get(':id') async getTip(@Param('id') id: string): Promise { return this.tipsService.getTipById(id); } @ApiOperation({ summary: 'Get tips received by the authenticated user' }) + @ApiResponse({ + status: 200, + description: 'Tips retrieved successfully', + schema: { + example: { + data: [ + { + id: '123e4567-e89b-12d3-a456-426614174000', + receiverWallet: 'GABC123XYZ456DEF789GHI012JKL345MNO678PQR890STU123VWX456YZ', + senderWallet: 'GDEF789GHI012JKL345MNO678PQR890STU123VWX456YZA123BCD456EFG', + amount: 10.5, + message: 'Great content!', + asset: 'XLM', + status: 'confirmed', + createdAt: '2024-01-15T00:00:00Z', + }, + ], + total: 150, + page: 1, + limit: 20, + totalPages: 8, + hasNextPage: true, + hasPreviousPage: false, + }, + }, + }) @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Get('my/received') @@ -79,6 +141,32 @@ export class TipsController { } @ApiOperation({ summary: 'Get tips sent by the authenticated user' }) + @ApiResponse({ + status: 200, + description: 'Tips retrieved successfully', + schema: { + example: { + data: [ + { + id: '123e4567-e89b-12d3-a456-426614174000', + receiverWallet: 'GABC123XYZ456DEF789GHI012JKL345MNO678PQR890STU123VWX456YZ', + senderWallet: 'GDEF789GHI012JKL345MNO678PQR890STU123VWX456YZA123BCD456EFG', + amount: 10.5, + message: 'Great content!', + asset: 'XLM', + status: 'confirmed', + createdAt: '2024-01-15T00:00:00Z', + }, + ], + total: 75, + page: 1, + limit: 20, + totalPages: 4, + hasNextPage: true, + hasPreviousPage: false, + }, + }, + }) @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Get('my/sent') @@ -118,6 +206,32 @@ export class TipsController { } @ApiOperation({ summary: 'Get tips by wallet address' }) + @ApiResponse({ + status: 200, + description: 'Tips retrieved successfully', + schema: { + example: { + data: [ + { + id: '123e4567-e89b-12d3-a456-426614174000', + receiverWallet: 'GABC123XYZ456DEF789GHI012JKL345MNO678PQR890STU123VWX456YZ', + senderWallet: 'GDEF789GHI012JKL345MNO678PQR890STU123VWX456YZA123BCD456EFG', + amount: 10.5, + message: 'Great content!', + asset: 'XLM', + status: 'confirmed', + createdAt: '2024-01-15T00:00:00Z', + }, + ], + total: 200, + page: 1, + limit: 20, + totalPages: 10, + hasNextPage: true, + hasPreviousPage: false, + }, + }, + }) @Get('wallet/:walletAddress') async getTipsByWallet( @Param('walletAddress') walletAddress: string, @@ -154,6 +268,26 @@ export class TipsController { } @ApiOperation({ summary: 'Get tip statistics for the authenticated user' }) + @ApiResponse({ + status: 200, + description: 'Statistics retrieved successfully', + schema: { + example: [ + { + totalAmount: '1250.5', + totalTips: '150', + asset: 'XLM', + assetIssuer: null, + }, + { + totalAmount: '500', + totalTips: '50', + asset: 'USDC', + assetIssuer: 'GA5ZSEJYB37JRC5BVHU4MWOOF7N2FDJQNEBVGTBRDPEKGBQ6776PQJJI', + }, + ], + }, + }) @ApiBearerAuth() @UseGuards(JwtAuthGuard) @Get('my/stats') @@ -169,6 +303,37 @@ export class TipsController { } @ApiOperation({ summary: 'Confirm a tip with a transaction hash' }) + @ApiBody({ + schema: { + type: 'object', + properties: { + transactionHash: { + type: 'string', + example: 'abc123def4567890123456789012345678901234567890123456789012345678', + }, + }, + required: ['transactionHash'], + }, + }) + @ApiResponse({ + status: 200, + description: 'Tip confirmed successfully', + schema: { + example: { + id: '123e4567-e89b-12d3-a456-426614174000', + receiverWallet: 'GABC123XYZ456DEF789GHI012JKL345MNO678PQR890STU123VWX456YZ', + senderWallet: 'GDEF789GHI012JKL345MNO678PQR890STU123VWX456YZA123BCD456EFG', + amount: 10.5, + message: 'Great content! Keep it up!', + asset: 'XLM', + assetIssuer: null, + transactionHash: 'abc123def4567890123456789012345678901234567890123456789012345678', + status: 'confirmed', + createdAt: '2024-01-15T00:00:00Z', + updatedAt: '2024-01-15T00:05:00Z', + }, + }, + }) @Post(':id/confirm') @TipCreationThrottle() async confirmTip( From 133e4dbaf10b8b9a79d275f4c9e1720f689d241f Mon Sep 17 00:00:00 2001 From: JACOB STANLEY Date: Sun, 21 Jun 2026 00:33:45 +0100 Subject: [PATCH 2/2] https://github.com/StellarTips/StellarTip-Backend.git --- docs/API_VERSIONING.md | 191 ++++++++++++++++++ src/app.module.ts | 3 +- src/auth/auth.controller.ts | 2 + src/main.ts | 20 +- src/notifications/notifications.controller.ts | 2 + src/profiles/profiles.controller.ts | 2 + .../middleware/version-redirect.middleware.ts | 39 ++++ src/stellar/stellar.controller.ts | 2 + src/tips/tips.controller.ts | 2 + 9 files changed, 258 insertions(+), 5 deletions(-) create mode 100644 docs/API_VERSIONING.md create mode 100644 src/shared/middleware/version-redirect.middleware.ts diff --git a/docs/API_VERSIONING.md b/docs/API_VERSIONING.md new file mode 100644 index 0000000..cb928e3 --- /dev/null +++ b/docs/API_VERSIONING.md @@ -0,0 +1,191 @@ +# API Versioning Strategy + +## Overview + +The StellarTip API implements URI-based versioning to ensure backward compatibility and enable future API evolution without breaking existing clients. + +## Versioning Scheme + +### URI-Based Versioning + +All API endpoints are prefixed with a version identifier in the URL path: + +``` +https://api.stellartip.com/v1/{endpoint} +``` + +**Example:** +- Versioned endpoint: `GET /v1/auth/login` +- Unversioned (backward compatible): `GET /auth/login` → defaults to v1 + +### Current Version: v1.0.0 + +The current stable version is `v1.0.0`. All business logic endpoints are versioned under `/v1`. + +## Implementation Details + +### NestJS Configuration + +API versioning is configured in `src/main.ts` using NestJS's built-in versioning support: + +```typescript +app.enableVersioning({ + type: VersioningType.URI, + defaultVersion: '1', +}); +``` + +The `defaultVersion: '1'` setting ensures backward compatibility - requests without a version prefix automatically default to v1. + +### Controller Versioning + +Business logic controllers are decorated with `@Version('1')` to specify their API version: + +```typescript +@ApiTags('auth') +@Controller('auth') +@Version('1') +export class AuthController { + // ... +} +``` + +### Infrastructure Endpoints (Not Versioned) + +The following endpoints are NOT versioned as they are infrastructure-related: +- **Health endpoints**: `/health/*` - Health checks and monitoring +- **Root endpoint**: `/` - API welcome message +- **Swagger docs**: `/api/docs` - API documentation + +These remain accessible without version prefix and are excluded from versioning. + +## Backward Compatibility + +### Default Version Behavior + +The API uses `defaultVersion: '1'` which provides seamless backward compatibility: + +- **Versioned requests**: `GET /v1/auth/login` → Works as expected +- **Unversioned requests**: `GET /auth/login` → Automatically treated as v1 (no redirect needed) + +This approach is simpler and more efficient than using redirects, as it avoids the overhead of HTTP redirects while maintaining full backward compatibility. + +## Migration Guide for Clients + +### Recommended Approach + +1. **Update your base URL:** + ```javascript + // Old (still works due to backward compatibility) + const BASE_URL = 'https://api.stellartip.com'; + + // New (recommended) + const BASE_URL = 'https://api.stellartip.com/v1'; + ``` + +2. **Test your application:** Both versioned and unversioned endpoints should work identically. + +3. **Monitor usage:** Track which version your clients are using to plan future deprecations. + +### Timeline + +- **Phase 1 (Current):** Both versioned (`/v1/*`) and unversioned endpoints work. Unversioned requests automatically default to v1. +- **Phase 2 (Future):** Unversioned endpoints will be deprecated. Clients will receive deprecation warnings. +- **Phase 3 (Future):** Unversioned endpoints will be removed. Only `/v1` endpoints will be supported. + +## Versioning Best Practices + +### When to Create a New Version + +Create a new API version when: + +1. **Breaking Changes:** Any change that breaks existing client functionality + - Removing or renaming fields + - Changing data types + - Modifying required parameters + - Changing authentication mechanisms + +2. **Major Behavioral Changes:** Significant changes in how endpoints work + - Different response formats + - Changed business logic + - Altered error handling + +### Non-Breaking Changes (No New Version Needed) + +These changes can be made within the current version: + +- Adding new optional fields +- Adding new endpoints +- Bug fixes that don't change behavior +- Performance improvements +- Documentation updates + +### Future Versioning + +When v2 is needed: + +1. Create new controller classes or methods with `@Version('2')` +2. Maintain v1 controllers for backward compatibility +3. Update documentation to highlight differences +4. Set a deprecation timeline for v1 + +## Testing Versioned Endpoints + +### Using cURL + +```bash +# Versioned endpoint (recommended) +curl https://api.stellartip.com/v1/auth/login \ + -H "Content-Type: application/json" \ + -d '{"email":"user@example.com","password":"password"}' + +# Unversioned (backward compatible, defaults to v1) +curl https://api.stellartip.com/auth/login \ + -H "Content-Type: application/json" \ + -d '{"email":"user@example.com","password":"password"}' + +# Infrastructure endpoint (not versioned) +curl https://api.stellartip.com/health +``` + +### Using Swagger Documentation + +The Swagger UI is available at `/api/docs` and automatically reflects the versioned endpoints. All examples in the documentation use the `/v1` prefix for business logic endpoints. + +## Monitoring and Logging + +### Version Metrics + +The API should track: +- Request counts per version (v1 vs unversioned) +- Error rates per version +- Response times per version + +This data helps determine when to deprecate older versions. + +### Deprecation Warnings + +When a version is deprecated, responses will include: + +```http +HTTP/1.1 200 OK +X-API-Deprecation: true +X-API-Sunset-Date: 2025-12-31 +X-API-Recommended-Version: v2 +``` + +## Support and Questions + +For questions about API versioning or migration assistance: +- Check the Swagger documentation at `/api/docs` +- Review this document for common scenarios +- Contact the development team for specific concerns + +## Changelog + +### v1.0.0 (Current) +- Initial API versioning implementation +- All business logic endpoints moved to `/v1` prefix +- Infrastructure endpoints (health, root, docs) remain unversioned +- Backward compatibility maintained through defaultVersion setting +- Swagger documentation updated with versioning information diff --git a/src/app.module.ts b/src/app.module.ts index c44e7c2..f3d4716 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -55,4 +55,5 @@ import { SharedModule } from './shared/shared.module'; }, ], }) -export class AppModule {} +export class AppModule implements NestModule { + configure(consumer: Mi \ No newline at end of file diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index a969926..906fc13 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -8,6 +8,7 @@ import { Req, HttpException, HttpStatus, + Version, } from '@nestjs/common'; import { Request } from 'express'; import { ApiTags, ApiOperation, ApiBearerAuth, ApiBody, ApiResponse } from '@nestjs/swagger'; @@ -20,6 +21,7 @@ import { User } from '../entities/user.entity'; @ApiTags('auth') @Controller('auth') +@Version('1') export class AuthController { constructor(private readonly authService: AuthService) {} diff --git a/src/main.ts b/src/main.ts index f170f51..4a9e213 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,6 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; -import { ValidationPipe } from '@nestjs/common'; +import { ValidationPipe, VersioningType } from '@nestjs/common'; import { DataSource } from 'typeorm'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import * as compression from 'compression'; @@ -18,6 +18,13 @@ async function bootstrap(): Promise { // Security headers and CORS configureSecurity(app); + // API Versioning with URI prefix (e.g., /v1/auth/login) + // defaultVersion: '1' provides backward compatibility for unversioned requests + app.enableVersioning({ + type: VersioningType.URI, + defaultVersion: '1', + }); + // Response compression app.use(compression()); @@ -38,9 +45,14 @@ async function bootstrap(): Promise { .setTitle('StellarTip API') .setDescription( 'Decentralized micro-tipping platform on the Stellar blockchain. ' + - 'Tip creators with XLM or USDC — no intermediaries, just Stellar.', + 'Tip creators with XLM or USDC — no intermediaries, just Stellar.\n\n' + + '## API Versioning\n' + + 'This API uses URI-based versioning. All endpoints are prefixed with `/v1`.\n' + + 'Example: `GET /v1/auth/login`\n\n' + + '## Backward Compatibility\n' + + 'For backward compatibility, requests without a version prefix will default to v1.', ) - .setVersion('0.1.0') + .setVersion('1.0.0') .addBearerAuth() .addTag('auth', 'Authentication endpoints') .addTag('profiles', 'Creator profile management') @@ -48,7 +60,7 @@ async function bootstrap(): Promise { .addTag('stellar', 'Stellar blockchain interaction') .addTag('notifications', 'In-app notifications') .addTag('health', 'Health check and monitoring') - .addServer('http://localhost:3000', 'Local development') + .addServer('http://localhost:3000/v1', 'Local development (v1)') .build(); const document = SwaggerModule.createDocument(app, config); diff --git a/src/notifications/notifications.controller.ts b/src/notifications/notifications.controller.ts index 6fb57a3..1cb8ac5 100644 --- a/src/notifications/notifications.controller.ts +++ b/src/notifications/notifications.controller.ts @@ -6,6 +6,7 @@ import { Query, UseGuards, Req, + Version, } from '@nestjs/common'; import { Request } from 'express'; import { ApiTags, ApiOperation, ApiBearerAuth, ApiResponse } from '@nestjs/swagger'; @@ -14,6 +15,7 @@ import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { Notification } from '../entities/notification.entity'; @ApiTags('notifications') +@Version('1') @Controller('notifications') export class NotificationsController { constructor(private readonly notificationsService: NotificationsService) {} diff --git a/src/profiles/profiles.controller.ts b/src/profiles/profiles.controller.ts index d4b521a..c2fd037 100644 --- a/src/profiles/profiles.controller.ts +++ b/src/profiles/profiles.controller.ts @@ -14,6 +14,7 @@ import { ParseFilePipe, MaxFileSizeValidator, FileTypeValidator, + Version, } from '@nestjs/common'; import { Request } from 'express'; import { FileInterceptor } from '@nestjs/platform-express'; @@ -26,6 +27,7 @@ import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { User } from '../entities/user.entity'; @ApiTags('profiles') +@Version('1') @Controller('profiles') export class ProfilesController { constructor(private readonly profilesService: ProfilesService) {} diff --git a/src/shared/middleware/version-redirect.middleware.ts b/src/shared/middleware/version-redirect.middleware.ts new file mode 100644 index 0000000..9dba9da --- /dev/null +++ b/src/shared/middleware/version-redirect.middleware.ts @@ -0,0 +1,39 @@ +import { Injectable, NestMiddleware, RequestMethod } from '@nestjs/common'; +import { Request, Response, NextFunction } from 'express'; + +/** + * Middleware to handle backward compatibility for API versioning. + * + * This middleware redirects requests without a version prefix to the v1 version. + * For example, GET /auth/login redirects to GET /v1/auth/login + * + * This ensures backward compatibility for existing clients while encouraging + * migration to the versioned endpoints. + */ +@Injectable() +export class VersionRedirectMiddleware implements NestMiddleware { + use(req: Request, res: Response, next: NextFunction): void { + // Skip if the path already starts with /v1 or /api + if (req.path.startsWith('/v1') || req.path.startsWith('/api') || req.path === '/health') { + return next(); + } + + // Skip for static files and other non-API routes + if (req.path.includes('.')) { + return next(); + } + + // Redirect to v1 version for API routes + // Only redirect for common API paths to avoid affecting other routes + const apiPaths = ['/auth', '/profiles', '/tips', '/stellar', '/notifications', '/app']; + const shouldRedirect = apiPaths.some(path => req.path.startsWith(path)); + + if (shouldRedirect) { + // Return 301 permanent redirect to encourage clients to update + // Or use 307 for temporary redirect if you want to maintain current behavior + return res.redirect(301, `/v1${req.path}`); + } + + next(); + } +} diff --git a/src/stellar/stellar.controller.ts b/src/stellar/stellar.controller.ts index 7ca9685..9031e34 100644 --- a/src/stellar/stellar.controller.ts +++ b/src/stellar/stellar.controller.ts @@ -7,11 +7,13 @@ import { HttpStatus, Post, Body, + Version, } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse, ApiBody } from '@nestjs/swagger'; import { StellarService } from './stellar.service'; @ApiTags('stellar') +@Version('1') @Controller('stellar') export class StellarController { private readonly logger = new Logger(StellarController.name); diff --git a/src/tips/tips.controller.ts b/src/tips/tips.controller.ts index 80469b4..a1379a3 100644 --- a/src/tips/tips.controller.ts +++ b/src/tips/tips.controller.ts @@ -7,6 +7,7 @@ import { Query, UseGuards, Req, + Version, } from '@nestjs/common'; import { Request } from 'express'; import { ApiTags, ApiOperation, ApiBearerAuth, ApiResponse, ApiBody } from '@nestjs/swagger'; @@ -17,6 +18,7 @@ import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { TipCreationThrottle } from '../config/throttle.config'; @ApiTags('tips') +@Version('1') @Controller('tips') export class TipsController { constructor(private readonly tipsService: TipsService) {}