Skip to content
Merged
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
108 changes: 107 additions & 1 deletion backend/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
Query,
UseGuards,
Request,
UnauthorizedException,
} from '@nestjs/common';
import {
ApiBearerAuth,
Expand All @@ -17,19 +18,28 @@ import {
} from '@nestjs/swagger';
import { Throttle } from '@nestjs/throttler';
import { AuthService } from './auth.service';
import { TwoFactorService } from './two-factor.service';
import {
RegisterDto,
LoginDto,
GetNonceDto,
VerifySignatureDto,
LinkWalletDto,
} from './dto/auth.dto';
import {
VerifyTwoFactorDto,
LoginWithTwoFactorDto,
AdminDisableTwoFactorDto,
} from './dto/two-factor.dto';
import { JwtAuthGuard } from './guards/jwt-auth.guard';

@ApiTags('auth')
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
constructor(
private readonly authService: AuthService,
private readonly twoFactorService: TwoFactorService,
) {}

@Post('register')
@Throttle({ auth: { limit: 5, ttl: 15 * 60 * 1000 } })
Expand Down Expand Up @@ -99,4 +109,100 @@ export class AuthController {
) {
return this.authService.linkWallet(req.user.id, dto);
}

// --- Two-Factor Authentication Endpoints ---

@Post('2fa/enable')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOperation({
summary: 'Enable 2FA - generates secret and backup codes',
description:
'Returns a TOTP secret, otpauth:// URL for QR code generation, and backup codes. ' +
'Call POST /auth/2fa/verify with a valid token to activate.',
})
@ApiResponse({ status: 201, description: 'Secret and backup codes generated' })
@ApiResponse({ status: 400, description: '2FA already enabled' })
enable2fa(@Request() req: { user: { id: string } }) {
return this.twoFactorService.enable(req.user.id);
}

@Post('2fa/verify')
@UseGuards(JwtAuthGuard)
@HttpCode(HttpStatus.OK)
@ApiBearerAuth()
@ApiOperation({
summary: 'Verify and activate 2FA with a TOTP token',
description:
'After enabling, submit a token from your authenticator app to confirm setup.',
})
@ApiResponse({ status: 200, description: '2FA activated' })
@ApiResponse({ status: 401, description: 'Invalid token' })
verify2fa(
@Request() req: { user: { id: string } },
@Body() dto: VerifyTwoFactorDto,
) {
return this.twoFactorService.verify(req.user.id, dto.token);
}

@Post('2fa/validate')
@Throttle({ auth: { limit: 5, ttl: 15 * 60 * 1000 } })
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'Complete login with 2FA token',
description:
'When login returns requiresTwoFactor: true, call this endpoint with the userId and TOTP token.',
})
@ApiResponse({ status: 200, description: 'JWT returned on success' })
@ApiResponse({ status: 401, description: 'Invalid 2FA token' })
async validate2fa(
@Body('userId') userId: string,
@Body() dto: LoginWithTwoFactorDto,
) {
const valid = await this.twoFactorService.validateLogin(userId, dto.token);
if (!valid) {
throw new UnauthorizedException('Invalid 2FA token');
}
return this.twoFactorService.completeLogin(userId);
}

@Post('2fa/disable')
@UseGuards(JwtAuthGuard)
@HttpCode(HttpStatus.OK)
@ApiBearerAuth()
@ApiOperation({ summary: 'Disable 2FA for your account' })
@ApiResponse({ status: 200, description: '2FA disabled' })
@ApiResponse({ status: 400, description: '2FA not enabled' })
disable2fa(@Request() req: { user: { id: string } }) {
return this.twoFactorService.disable(req.user.id);
}

@Post('2fa/admin-disable')
@UseGuards(JwtAuthGuard)
@HttpCode(HttpStatus.OK)
@ApiBearerAuth()
@ApiOperation({
summary: 'Admin: disable 2FA for a locked account',
description: 'Requires ADMIN role',
})
@ApiResponse({ status: 200, description: '2FA disabled for target user' })
@ApiResponse({ status: 400, description: '2FA not enabled for user' })
adminDisable2fa(
@Request() req: { user: { id: string; role: string } },
@Body() dto: AdminDisableTwoFactorDto,
) {
if (req.user.role !== 'ADMIN') {
throw new UnauthorizedException('Admin access required');
}
return this.twoFactorService.adminDisable(dto.userId);
}

@Get('2fa/status')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: 'Check if 2FA is enabled for your account' })
@ApiResponse({ status: 200, description: '2FA status' })
get2faStatus(@Request() req: { user: { id: string } }) {
return this.twoFactorService.getStatus(req.user.id);
}
}
8 changes: 6 additions & 2 deletions backend/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';
// import { CacheModule } from '@nestjs/cache-manager';
import { JwtStrategy } from './strategies/jwt.strategy';
import { UserModule } from '../modules/user/user.module';
import { AuthService } from './auth.service';
import { TwoFactorService } from './two-factor.service';
import { AuthController } from './auth.controller';
import { User } from '../modules/user/entities/user.entity';

@Module({
imports: [
UserModule,
TypeOrmModule.forFeature([User]),
// CacheModule,
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.registerAsync({
Expand All @@ -28,7 +32,7 @@ import { AuthController } from './auth.controller';
}),
],
controllers: [AuthController],
providers: [AuthService, JwtStrategy],
exports: [AuthService, JwtModule, PassportModule],
providers: [AuthService, TwoFactorService, JwtStrategy],
exports: [AuthService, TwoFactorService, JwtModule, PassportModule],
})
export class AuthModule {}
10 changes: 10 additions & 0 deletions backend/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,16 @@ export class AuthService {
throw new UnauthorizedException('Invalid credentials');
}

// Check if 2FA is enabled
const fullUser = await this.userService.findByEmail(dto.email);
if (fullUser?.twoFactorEnabled) {
return {
requiresTwoFactor: true,
userId: user.id,
message: 'Please provide your 2FA token',
};
}

return {
accessToken: this.generateToken(user.id, user.email, user.role),
};
Expand Down
28 changes: 28 additions & 0 deletions backend/src/auth/dto/two-factor.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { IsString, Length, IsUUID } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';

export class VerifyTwoFactorDto {
@ApiProperty({
example: '123456',
description: '6-digit TOTP token from authenticator app',
})
@IsString()
@Length(6, 8) // 6 for TOTP, 8 for backup codes
token: string;
}

export class LoginWithTwoFactorDto {
@ApiProperty({
example: '123456',
description: '6-digit TOTP token or backup code',
})
@IsString()
@Length(6, 8)
token: string;
}

export class AdminDisableTwoFactorDto {
@ApiProperty({ description: 'User ID to disable 2FA for' })
@IsUUID()
userId: string;
}
Loading
Loading