Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
21 changes: 21 additions & 0 deletions src/api-keys/dto/api-key-query.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { IsOptional, IsBoolean } from 'class-validator';

Check failure on line 1 in src/api-keys/dto/api-key-query.dto.ts

View workflow job for this annotation

GitHub Actions / Test and Quality Checks

Definition for rule '@typescript-eslint/prefer-const' was not found
import { ApiPropertyOptional } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IntersectionType } from '@nestjs/swagger';
import { PaginationDto } from '../../common/dto';

export class ApiKeyFilterDto {
@ApiPropertyOptional({
description: 'Filter by active status',
example: true,
})
@IsOptional()
@Transform(({ value }) => value === 'true' || value === true)
@IsBoolean({ message: 'isActive must be a boolean' })
isActive?: boolean;

Check failure on line 15 in src/api-keys/dto/api-key-query.dto.ts

View workflow job for this annotation

GitHub Actions / Test and Quality Checks

Expected indentation of 4 spaces but found 2
}

export class ApiKeyQueryDto extends IntersectionType(
ApiKeyFilterDto,
PaginationDto,
) {}
24 changes: 13 additions & 11 deletions src/api-keys/dto/create-api-key.dto.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { IsString, IsNotEmpty, IsArray, IsOptional, IsInt, Min, ArrayMinSize } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsNotEmpty, IsArray, IsOptional, IsInt, Min, ArrayMinSize, MaxLength } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { ApiKeyScope } from '../enums/api-key-scope.enum';

export class CreateApiKeyDto {
@ApiProperty({
description: 'Friendly name for the API key',
example: 'Production Integration Key',
maxLength: 100,
})
@IsString()
@IsNotEmpty()
@IsString({ message: 'Name must be a string' })
@IsNotEmpty({ message: 'Name is required' })
@MaxLength(100, { message: 'Name must not exceed 100 characters' })
name: string;

@ApiProperty({
Expand All @@ -17,18 +19,18 @@ export class CreateApiKeyDto {
enum: ApiKeyScope,
isArray: true,
})
@IsArray()
@ArrayMinSize(1)
@IsString({ each: true })
@IsArray({ message: 'Scopes must be an array' })
@ArrayMinSize(1, { message: 'At least one scope is required' })
@IsString({ each: true, message: 'Each scope must be a string' })
scopes: string[];

@ApiProperty({
@ApiPropertyOptional({
description: 'Rate limit (requests per minute) for this key. If not provided, uses global default.',
example: 100,
required: false,
minimum: 1,
})
@IsOptional()
@IsInt()
@Min(1)
@IsInt({ message: 'Rate limit must be an integer' })
@Min(1, { message: 'Rate limit must be at least 1' })
rateLimit?: number;
}
4 changes: 4 additions & 0 deletions src/api-keys/dto/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './create-api-key.dto';
export * from './update-api-key.dto';
export * from './api-key-response.dto';
export * from './api-key-query.dto';
51 changes: 28 additions & 23 deletions src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { Controller, Post, Body, Req, Res, Get, UseGuards, HttpCode, HttpStatus, Put } from '@nestjs/common';
import { Controller, Post, Body, Req, Get, UseGuards, HttpCode, HttpStatus, Put, Param } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LocalAuthGuard } from './guards/local-auth.guard';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { CreateUserDto } from '../users/dto/create-user.dto';
import { LoginDto } from './dto/login.dto';
import {
LoginDto,
LoginWeb3Dto,
RefreshTokenDto,
ForgotPasswordDto,
ResetPasswordDto,
VerifyEmailParamsDto,
} from './dto';
import { ErrorResponseDto } from '../common/errors/error.dto';
import { ApiTags, ApiOperation, ApiResponse, ApiBody } from '@nestjs/swagger';
import { Request, Response } from 'express';
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
import { Request } from 'express';

@ApiTags('auth')
@Controller('auth')
Expand All @@ -23,7 +29,7 @@
}

@Post('login')
@ApiOperation({ summary: 'Login user' })
@ApiOperation({ summary: 'Login user with email and password' })
@ApiResponse({ status: 200, description: 'Login successful.' })
@ApiResponse({ status: 401, description: 'Invalid credentials.', type: ErrorResponseDto })
@HttpCode(HttpStatus.OK)
Expand All @@ -36,21 +42,20 @@
@ApiResponse({ status: 200, description: 'Web3 login successful.' })
@ApiResponse({ status: 401, description: 'Invalid signature.', type: ErrorResponseDto })
@HttpCode(HttpStatus.OK)
async web3Login(@Body() loginDto: LoginDto) {
const credentials = {
async web3Login(@Body() loginDto: LoginWeb3Dto) {
return this.authService.login({
walletAddress: loginDto.walletAddress,
signature: loginDto.signature,
};
return this.authService.login(credentials);
});
}

@Post('refresh-token')
@ApiOperation({ summary: 'Refresh access token' })
@ApiResponse({ status: 200, description: 'Token refreshed successfully.' })
@ApiResponse({ status: 401, description: 'Invalid refresh token.', type: ErrorResponseDto })
@HttpCode(HttpStatus.OK)
async refreshToken(@Body('refreshToken') refreshToken: string) {
return this.authService.refreshToken(refreshToken);
async refreshToken(@Body() refreshTokenDto: RefreshTokenDto) {
return this.authService.refreshToken(refreshTokenDto.refreshToken);
}

@Post('logout')
Expand All @@ -59,7 +64,7 @@
@ApiResponse({ status: 200, description: 'Logged out successfully.' })
@HttpCode(HttpStatus.OK)
async logout(@Req() req: Request) {
const user = req['user'] as any;

Check warning on line 67 in src/auth/auth.controller.ts

View workflow job for this annotation

GitHub Actions / Test and Quality Checks

Unexpected any. Specify a different type
return this.authService.logout(user.id);
}

Expand All @@ -67,27 +72,27 @@
@ApiOperation({ summary: 'Request password reset' })
@ApiResponse({ status: 200, description: 'Password reset email sent.' })
@HttpCode(HttpStatus.OK)
async forgotPassword(@Body('email') email: string) {
return this.authService.forgotPassword(email);
async forgotPassword(@Body() forgotPasswordDto: ForgotPasswordDto) {
return this.authService.forgotPassword(forgotPasswordDto.email);
}

@Put('reset-password')
@ApiOperation({ summary: 'Reset password' })
@ApiOperation({ summary: 'Reset password with token' })
@ApiResponse({ status: 200, description: 'Password reset successfully.' })
@ApiResponse({ status: 400, description: 'Invalid or expired reset token.', type: ErrorResponseDto })
@HttpCode(HttpStatus.OK)
async resetPassword(
@Body('token') token: string,
@Body('newPassword') newPassword: string,
) {
return this.authService.resetPassword(token, newPassword);
async resetPassword(@Body() resetPasswordDto: ResetPasswordDto) {
return this.authService.resetPassword(
resetPasswordDto.token,
resetPasswordDto.newPassword,
);
}

@Get('verify-email/:token')
@ApiOperation({ summary: 'Verify email address' })
@ApiResponse({ status: 200, description: 'Email verified successfully.' })
@ApiResponse({ status: 400, description: 'Invalid or expired verification token.', type: ErrorResponseDto })
async verifyEmail(@Body('token') token: string) {
return this.authService.verifyEmail(token);
async verifyEmail(@Param() params: VerifyEmailParamsDto) {
return this.authService.verifyEmail(params.token);
}
}
}
74 changes: 74 additions & 0 deletions src/auth/dto/auth-response.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';

export class TokenPairDto {
@ApiProperty({
description: 'JWT access token',
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
})
accessToken: string;

@ApiProperty({
description: 'JWT refresh token',
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
})
refreshToken: string;

@ApiProperty({
description: 'Token expiration time in seconds',
example: 3600,
})
expiresIn: number;

@ApiProperty({
description: 'Token type',
example: 'Bearer',
})
tokenType: string;
}

export class AuthUserDto {
@ApiProperty({ example: 'user_abc123' })
id: string;

@ApiProperty({ example: 'john.doe@example.com' })
email: string;

@ApiPropertyOptional({ example: 'John' })
firstName?: string;

@ApiPropertyOptional({ example: 'Doe' })
lastName?: string;

@ApiPropertyOptional({ example: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e' })
walletAddress?: string;

@ApiProperty({ example: ['user'] })
roles: string[];
}

export class LoginResponseDto {
@ApiProperty({ type: AuthUserDto })
user: AuthUserDto;

@ApiProperty({ type: TokenPairDto })
tokens: TokenPairDto;
}

export class RegisterResponseDto {
@ApiProperty({ type: AuthUserDto })
user: AuthUserDto;

@ApiProperty({
description: 'Message about email verification',
example: 'Please check your email to verify your account',
})
message: string;
}

export class MessageResponseDto {
@ApiProperty({
description: 'Response message',
example: 'Operation completed successfully',
})
message: string;
}
12 changes: 12 additions & 0 deletions src/auth/dto/forgot-password.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { IsEmail, IsNotEmpty } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';

export class ForgotPasswordDto {
@ApiProperty({
description: 'Email address to send password reset link',
example: 'john.doe@example.com',
})
@IsEmail({}, { message: 'Please provide a valid email address' })
@IsNotEmpty({ message: 'Email is required' })
email: string;
}
6 changes: 6 additions & 0 deletions src/auth/dto/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export * from './login.dto';
export * from './refresh-token.dto';
export * from './forgot-password.dto';
export * from './reset-password.dto';
export * from './verify-email-params.dto';
export * from './auth-response.dto';
94 changes: 73 additions & 21 deletions src/auth/dto/login.dto.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,88 @@
import { IsEmail, IsString, IsOptional } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsString, IsNotEmpty, ValidateIf } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsEthereumAddress } from '../../common/validators';

export class LoginDto {
/**
* DTO for email/password login
*/
export class LoginEmailDto {
@ApiProperty({
example: 'john.doe@example.com',
description: 'User email address',
example: 'john.doe@example.com',
})
@IsEmail()
@IsOptional()
email?: string;
@IsEmail({}, { message: 'Please provide a valid email address' })
@IsNotEmpty({ message: 'Email is required' })
email: string;

@ApiProperty({
description: 'User password',
example: 'SecureP@ss123',
minLength: 8,
})
@IsString({ message: 'Password must be a string' })
@IsNotEmpty({ message: 'Password is required' })
password: string;
}

/**
* DTO for Web3 wallet login
*/
export class LoginWeb3Dto {
@ApiProperty({
description: 'Ethereum wallet address',
example: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
description: 'Wallet address for Web3 authentication',
})
@IsString()
@IsOptional()
walletAddress?: string;
@IsEthereumAddress({ message: 'Invalid Ethereum wallet address' })
@IsNotEmpty({ message: 'Wallet address is required' })
walletAddress: string;

@ApiProperty({
example: 'securePassword123',
description: 'User password (required if using email)',
description: 'Signature from wallet for authentication',
example: '0x...',
})
@IsString({ message: 'Signature must be a string' })
@IsNotEmpty({ message: 'Signature is required' })
signature: string;
}

/**
* Combined DTO for backward compatibility - supports both email and Web3 login
* Uses conditional validation based on which fields are provided
*/
export class LoginDto {
@ApiPropertyOptional({
description: 'User email address (required for email login)',
example: 'john.doe@example.com',
})
@ValidateIf((o) => !o.walletAddress)
@IsEmail({}, { message: 'Please provide a valid email address' })
@IsNotEmpty({ message: 'Email is required when not using Web3 login' })
email?: string;

@ApiPropertyOptional({
description: 'User password (required for email login)',
example: 'SecureP@ss123',
})
@IsString()
@IsOptional()
@ValidateIf((o) => !o.walletAddress)
@IsString({ message: 'Password must be a string' })
@IsNotEmpty({ message: 'Password is required when not using Web3 login' })
password?: string;

@ApiProperty({
example: 'signature_from_wallet',
description: 'Signature from wallet for Web3 authentication',
@ApiPropertyOptional({
description: 'Ethereum wallet address (required for Web3 login)',
example: '0x742d35Cc6634C0532925a3b844Bc454e4438f44e',
})
@ValidateIf((o) => !o.email)
@IsEthereumAddress({ message: 'Invalid Ethereum wallet address' })
@IsNotEmpty({ message: 'Wallet address is required for Web3 login' })
walletAddress?: string;

@ApiPropertyOptional({
description: 'Signature from wallet (required for Web3 login)',
example: '0x...',
})
@IsString()
@IsOptional()
@ValidateIf((o) => !o.email)
@IsString({ message: 'Signature must be a string' })
@IsNotEmpty({ message: 'Signature is required for Web3 login' })
signature?: string;
}
}
13 changes: 13 additions & 0 deletions src/auth/dto/refresh-token.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { IsString, IsNotEmpty, IsJWT } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';

export class RefreshTokenDto {
@ApiProperty({
description: 'JWT refresh token',
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
})
@IsString({ message: 'Refresh token must be a string' })
@IsNotEmpty({ message: 'Refresh token is required' })
@IsJWT({ message: 'Invalid refresh token format' })
refreshToken: string;
}
Loading
Loading