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
12 changes: 4 additions & 8 deletions apps/backend/.env.example
Original file line number Diff line number Diff line change
@@ -1,11 +1,3 @@
DB_HOST=localhost
DB_PORT=5432
DB_USERNAME=lumenpulse_user
DB_PASSWORD=yourpassword
DB_DATABASE=lumenpulse_db
PYTHON_API_URL=http://localhost:8000
# Backend Configuration

# Database (matches docker-compose postgres service)
DB_HOST=localhost
DB_PORT=5432
Expand Down Expand Up @@ -35,6 +27,10 @@ REDIS_PORT=6379
# Default cache TTL in milliseconds (300000 = 5 minutes)
CACHE_TTL_MS=300000

# Two-Factor Authentication (TOTP)
# Generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
TOTP_ENCRYPTION_KEY=

# Webhook — shared secret for HMAC-SHA256 signature verification
# Must match WEBHOOK_SECRET set in the data-processing service
WEBHOOK_SECRET=your_webhook_secret_here
2 changes: 2 additions & 0 deletions apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,12 @@
"class-validator": "^0.14.3",
"dotenv": "^17.2.3",
"helmet": "^8.1.0",
"otplib": "^12.0.1",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"pg": "^8.17.2",
"prom-client": "^15.1.3",
"qrcode": "^1.5.4",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"socket.io": "^4.8.3",
Expand Down
133 changes: 124 additions & 9 deletions apps/backend/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ import { GetChallengeDto, VerifyChallengeDto } from './dto/auth.dto';
import { ForgotPasswordDto } from './dto/forgot-password.dto';
import { ResetPasswordDto } from './dto/reset-password.dto';
import { RefreshTokenDto, LogoutDto } from './dto/refresh-token.dto';
import {
TwoFactorGenerateResponseDto,
TwoFactorEnableDto,
TwoFactorVerifyDto,
TwoFactorDisableDto,
TwoFactorPendingResponseDto,
} from './dto/totp.dto';
import {
ApiTags,
ApiOperation,
Expand All @@ -49,18 +56,22 @@ export class AuthController {
@ApiOperation({ summary: 'Login with email and password' })
@ApiResponse({
status: 200,
description: 'Login successful',
description: 'Login successful or 2FA required',
schema: {
properties: {
access_token: {
type: 'string',
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
oneOf: [
{
properties: {
access_token: { type: 'string' },
refresh_token: { type: 'string' },
},
},
refresh_token: {
type: 'string',
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
{
properties: {
requiresTwoFactor: { type: 'boolean', example: true },
userId: { type: 'string' },
},
},
},
],
},
})
@ApiResponse({ status: 401, description: 'Invalid credentials' })
Expand All @@ -69,6 +80,17 @@ export class AuthController {
if (!user) {
throw new UnauthorizedException();
}

// Check if 2FA is enabled
const fullUser = await this.usersService.findById(user.id);
if (fullUser?.twoFactorEnabled) {
// Do NOT issue a session token yet
return {
requiresTwoFactor: true,
userId: user.id,
};
}

return this.authService.login(user);
}

Expand Down Expand Up @@ -230,6 +252,99 @@ export class AuthController {
};
}

@UseGuards(JwtAuthGuard)
@Post('2fa/generate')
@HttpCode(HttpStatus.OK)
@ApiBearerAuth('JWT-auth')
@ApiOperation({ summary: 'Generate TOTP secret and QR code for 2FA setup' })
@ApiResponse({
status: 200,
description: 'QR code and OTP auth URI generated',
type: TwoFactorGenerateResponseDto,
})
@ApiResponse({ status: 400, description: '2FA already enabled' })
@ApiResponse({ status: 401, description: 'Unauthorized' })
async generateTwoFactor(@Request() req: { user: { sub: string } }) {
return this.authService.generateTwoFactorSecret(req.user.sub);
}

@UseGuards(JwtAuthGuard)
@Post('2fa/enable')
@HttpCode(HttpStatus.OK)
@ApiBearerAuth('JWT-auth')
@ApiOperation({ summary: 'Verify TOTP and enable 2FA' })
@ApiResponse({
status: 200,
description: '2FA enabled successfully',
schema: {
properties: {
message: { type: 'string', example: '2FA enabled successfully' },
},
},
})
@ApiResponse({ status: 400, description: 'Invalid token or not pending' })
@ApiResponse({ status: 401, description: 'Unauthorized' })
async enableTwoFactor(
@Request() req: { user: { sub: string } },
@Body() body: TwoFactorEnableDto,
) {
return this.authService.enableTwoFactor(req.user.sub, body.token);
}

@Post('2fa/verify')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Verify TOTP during login (no auth required)' })
@ApiResponse({
status: 200,
description: '2FA verified, login successful',
schema: {
properties: {
access_token: { type: 'string' },
refresh_token: { type: 'string' },
},
},
})
@ApiResponse({
status: 400,
description: 'Invalid request or token',
})
async verifyTwoFactor(
@Body() body: TwoFactorVerifyDto,
@Request() req: ExpressRequest,
) {
const ipAddress = req.ip || req.connection?.remoteAddress;
// Note: Rate limiting should be applied to this endpoint in production
return this.authService.verifyTwoFactor(
body.userId,
body.token,
undefined,
ipAddress,
);
}

@UseGuards(JwtAuthGuard)
@Post('2fa/disable')
@HttpCode(HttpStatus.OK)
@ApiBearerAuth('JWT-auth')
@ApiOperation({ summary: 'Verify TOTP and disable 2FA' })
@ApiResponse({
status: 200,
description: '2FA disabled successfully',
schema: {
properties: {
message: { type: 'string', example: '2FA disabled successfully' },
},
},
})
@ApiResponse({ status: 400, description: 'Invalid token or 2FA not enabled' })
@ApiResponse({ status: 401, description: 'Unauthorized' })
async disableTwoFactor(
@Request() req: { user: { sub: string } },
@Body() body: TwoFactorDisableDto,
) {
return this.authService.disableTwoFactor(req.user.sub, body.token);
}

@Get('challenge')
@ApiOperation({ summary: 'Get authentication challenge for Stellar wallet' })
@ApiResponse({
Expand Down
Loading
Loading