Skip to content

Commit 98fa97e

Browse files
authored
Merge pull request #115 from JerryIdoko/feature/jwt-authentication-160
Pull Request: JWT Authentication for Admin/Indexer Routes (#160, #84)
2 parents 7e80970 + 401ade7 commit 98fa97e

7 files changed

Lines changed: 129 additions & 4 deletions

File tree

.env.example

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,8 @@ DB_DATABASE=tradeflow
1212
SOROBAN_RPC_URL="https://soroban-testnet.stellar.org"
1313
POOL_ADDRESS="CC..." # Replace with your Pool Contract ID
1414
INDEXER_POLL_INTERVAL=5000 # Polling interval in ms
15-
WS_PORT=3001 # WebSocket server port
15+
WS_PORT=3001 # WebSocket server port
16+
17+
# Security Configuration
18+
JWT_SECRET="YOUR_SUPER_SECRET_KEY_CHANGE_ME"
19+
ADMIN_PASSWORD="admin_secure_password_123"

src/app.module.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Module } from '@nestjs/common';
1+
import { Module, NestModule, MiddlewareConsumer, RequestMethod } from '@nestjs/common';
22
import { APP_GUARD, APP_FILTER } from '@nestjs/core';
33
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
44
import { AppController } from './app.controller';
@@ -13,6 +13,7 @@ import { TokensModule } from './tokens/tokens.module';
1313
import { ThrottlerExceptionFilter } from './common/filters/throttler-exception.filter';
1414
import { AllExceptionsFilter } from './common/filters/all-exceptions.filter';
1515
import { OgModule } from './og/og.module';
16+
import { RequireJwtMiddleware } from './common/middleware/require-jwt.middleware';
1617

1718
@Module({
1819
imports: [PrismaModule, HealthModule, RiskModule, AuthModule, AnalyticsModule, SwapModule, TokensModule, OgModule],
@@ -33,4 +34,13 @@ import { OgModule } from './og/og.module';
3334
},
3435
],
3536
})
36-
export class AppModule {}
37+
export class AppModule implements NestModule {
38+
configure(consumer: MiddlewareConsumer) {
39+
consumer
40+
.apply(RequireJwtMiddleware)
41+
.forRoutes(
42+
{ path: 'api/v1/webhook/soroban', method: RequestMethod.POST },
43+
{ path: 'invoices', method: RequestMethod.POST }, // Database-mutating in AppController
44+
);
45+
}
46+
}

src/auth/admin.controller.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Controller, Post, Body, HttpException, HttpStatus } from '@nestjs/common';
2+
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
3+
import { AuthService } from './auth.service';
4+
5+
@ApiTags('admin')
6+
@Controller('api/v1/admin')
7+
export class AdminController {
8+
constructor(private readonly authService: AuthService) {}
9+
10+
@Post('login')
11+
@ApiOperation({ summary: 'Admin login for backend dashboard' })
12+
@ApiResponse({
13+
status: 200,
14+
description: 'Admin login successful',
15+
schema: { type: 'object', properties: { token: { type: 'string' } } }
16+
})
17+
@ApiResponse({ status: 401, description: 'Unauthorized' })
18+
async login(@Body() body: { password: string }) {
19+
if (!body.password) {
20+
throw new HttpException('Password is required', HttpStatus.BAD_REQUEST);
21+
}
22+
23+
const isValid = await this.authService.verifyAdminPassword(body.password);
24+
25+
if (!isValid) {
26+
throw new HttpException('Invalid admin password', HttpStatus.UNAUTHORIZED);
27+
}
28+
29+
const token = this.authService.generateAdminJWT();
30+
return { token };
31+
}
32+
}

src/auth/auth.module.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import { Module } from '@nestjs/common';
22
import { AuthController } from './auth.controller';
3+
import { AdminController } from './admin.controller';
4+
import { WebhookController } from './webhook.controller';
35
import { AuthService } from './auth.service';
46

57
@Module({
6-
controllers: [AuthController],
8+
controllers: [AuthController, AdminController, WebhookController],
79
providers: [AuthService],
10+
exports: [AuthService], // Export for middleware use
811
})
912
export class AuthModule {}

src/auth/auth.service.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,17 @@ import { Keypair } from '@stellar/stellar-sdk';
77
export class AuthService {
88
private readonly jwtSecret = process.env.JWT_SECRET || 'fallback-secret-key';
99
private readonly jwtExpiration = '1h';
10+
private readonly adminExpiration = '24h';
11+
private readonly adminPassword = process.env.ADMIN_PASSWORD || 'admin123';
1012

1113
generateNonce(): string {
1214
return crypto.randomBytes(16).toString('hex');
1315
}
1416

17+
async verifyAdminPassword(password: string): Promise<boolean> {
18+
return password === this.adminPassword;
19+
}
20+
1521
async verifySignature(publicKey: string, signature: string, nonce: string): Promise<boolean> {
1622
try {
1723
const message = `Sign in to TradeFlow with nonce: ${nonce}`;
@@ -40,6 +46,17 @@ export class AuthService {
4046
});
4147
}
4248

49+
generateAdminJWT(): string {
50+
const payload = {
51+
role: 'admin',
52+
iat: Math.floor(Date.now() / 1000),
53+
};
54+
55+
return jwt.sign(payload, this.jwtSecret, {
56+
expiresIn: this.adminExpiration,
57+
});
58+
}
59+
4360
verifyJWT(token: string): any {
4461
try {
4562
return jwt.verify(token, this.jwtSecret);

src/auth/webhook.controller.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { Controller, Post, Body, HttpCode, HttpStatus } from '@nestjs/common';
2+
import { ApiTags, ApiOperation, ApiResponse, ApiHeader } from '@nestjs/swagger';
3+
4+
@ApiTags('webhooks')
5+
@Controller('api/v1/webhook')
6+
export class WebhookController {
7+
8+
@Post('soroban')
9+
@HttpCode(HttpStatus.OK)
10+
@ApiOperation({
11+
summary: 'Stellar Soroban event webhook receiver',
12+
description: 'Receives and processes incoming smart contract events. Requires JWT authentication.'
13+
})
14+
@ApiHeader({
15+
name: 'Authorization',
16+
description: 'Bearer <JWT_TOKEN>',
17+
required: true
18+
})
19+
@ApiResponse({ status: 200, description: 'Event processed successfully' })
20+
@ApiResponse({ status: 401, description: 'Unauthorized' })
21+
async handleSorobanEvent(@Body() eventData: any) {
22+
console.log('--- Incoming Soroban Event Webhook ---');
23+
console.log('Payload:', JSON.stringify(eventData, null, 2));
24+
25+
// In a production scenario, logic to respond to specific events
26+
// would go here (e.g. updating internal state).
27+
28+
return {
29+
status: 'success',
30+
receivedAt: new Date().toISOString()
31+
};
32+
}
33+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Injectable, NestMiddleware, UnauthorizedException } from '@nestjs/common';
2+
import { Request, Response, NextFunction } from 'express';
3+
import { AuthService } from '../../auth/auth.service';
4+
5+
@Injectable()
6+
export class RequireJwtMiddleware implements NestMiddleware {
7+
constructor(private readonly authService: AuthService) {}
8+
9+
use(req: Request, res: Response, next: NextFunction) {
10+
const authHeader = req.headers.authorization;
11+
12+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
13+
throw new UnauthorizedException('Missing or invalid Authorization header');
14+
}
15+
16+
const token = authHeader.split(' ')[1];
17+
18+
try {
19+
const decoded = this.authService.verifyJWT(token);
20+
req['user'] = decoded;
21+
next();
22+
} catch (error) {
23+
throw new UnauthorizedException('Invalid or expired token');
24+
}
25+
}
26+
}

0 commit comments

Comments
 (0)