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
4 changes: 3 additions & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
"typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js",
"migration:generate": "npm run typeorm -- migration:generate -d src/database/data-source.ts",
"migration:run": "npm run typeorm -- migration:run -d src/database/data-source.ts",
"migration:revert": "npm run typeorm -- migration:revert -d src/database/data-source.ts"
"migration:revert": "npm run typeorm -- migration:revert -d src/database/data-source.ts",
"seed:demo": "SEED_DEMO_DATA=true ts-node -r tsconfig-paths/register src/database/seeds/demo-seed.ts"
},
"dependencies": {
"@nestjs/common": "^11.0.1",
Expand All @@ -31,6 +32,7 @@
"@nestjs/swagger": "^8.1.0",
"@nestjs/terminus": "^11.1.1",
"@nestjs/typeorm": "^11.0.0",
"helmet": "^8.1.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"ioredis": "^5.11.0",
Expand Down
5 changes: 4 additions & 1 deletion backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { PortfolioModule } from './portfolio/portfolio.module';
import { AvailabilityModule } from './availability/availability.module';
import { AvatarModule } from './avatar/avatar.module';
import { AdminModule } from './admin/admin.module';
import { MonitoringModule } from './monitoring/monitoring.module';
import { MetricsMiddleware } from './monitoring/metrics.middleware';

@Module({
imports: [
Expand All @@ -29,12 +31,13 @@ import { AdminModule } from './admin/admin.module';
PortfolioModule,
AvailabilityModule,
AvatarModule,
MonitoringModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer): void {
consumer.apply(HttpLoggerMiddleware).forRoutes('*');
consumer.apply(HttpLoggerMiddleware, MetricsMiddleware).forRoutes('*');
}
}
60 changes: 60 additions & 0 deletions backend/src/database/seeds/demo-seed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import 'reflect-metadata';
import { AppDataSource } from '../../database/data-source';
import { User } from '../../users/entities/user.entity';
import { Role } from '../../users/entities/role.entity';

async function seedDemo() {
if (process.env.SEED_DEMO_DATA !== 'true') {
console.log('SEED_DEMO_DATA is not true; skipping demo seed.');
return;
}

await AppDataSource.initialize();
const roleRepo = AppDataSource.getRepository(Role);
const userRepo = AppDataSource.getRepository(User);

const mentorRole =
(await roleRepo.findOne({ where: { name: 'mentor' } })) ??
(await roleRepo.save(roleRepo.create({ name: 'mentor', description: 'Demo mentor role' })));
const menteeRole =
(await roleRepo.findOne({ where: { name: 'mentee' } })) ??
(await roleRepo.save(roleRepo.create({ name: 'mentee', description: 'Demo mentee role' })));

for (let i = 1; i <= 5; i++) {
const mentorWallet = `demo_mentor_${i}@example.com`;
const menteeWallet = `demo_mentee_${i}@example.com`;

const mentorExists = await userRepo.findOne({ where: { walletAddress: mentorWallet } });
if (!mentorExists) {
await userRepo.save(
userRepo.create({
walletAddress: mentorWallet,
displayName: `Demo Mentor ${i}`,
username: `demo_mentor_${i}`,
roles: [mentorRole],
}),
);
}

const menteeExists = await userRepo.findOne({ where: { walletAddress: menteeWallet } });
if (!menteeExists) {
await userRepo.save(
userRepo.create({
walletAddress: menteeWallet,
displayName: `Demo Mentee ${i}`,
username: `demo_mentee_${i}`,
roles: [menteeRole],
}),
);
}
}

console.log('Demo mentor/mentee accounts seeded.');
await AppDataSource.destroy();
}

seedDemo().catch(async (e) => {
console.error(e);
if (AppDataSource.isInitialized) await AppDataSource.destroy();
process.exit(1);
});
13 changes: 11 additions & 2 deletions backend/src/health/health.controller.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
import { Controller, Get } from '@nestjs/common';
import { HealthCheck, HealthCheckService } from '@nestjs/terminus';
import { RedisHealthIndicator } from '../redis/redis.health';
import { DataSource } from 'typeorm';

@Controller('health')
export class HealthController {
constructor(
private readonly health: HealthCheckService,
private readonly redis: RedisHealthIndicator,
private readonly dataSource: DataSource,
) {}

@Get()
@HealthCheck()
check() {
return this.health.check([() => this.redis.isHealthy('redis')]);
async check() {
return this.health.check([
async () => {
await this.dataSource.query('SELECT 1');
return { database: { status: 'up' } };
},
() => this.redis.isHealthy('redis'),
async () => ({ memory: { status: 'up', heapUsed: process.memoryUsage().heapUsed } }),
]);
}
}
29 changes: 28 additions & 1 deletion backend/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { HttpStatus, ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
import { DataSource } from 'typeorm';
import { ApiValidationException } from './common/exceptions/api-exceptions';
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
import helmet from 'helmet';

async function bootstrap() {
const app = await NestFactory.create(AppModule, {
Expand All @@ -15,6 +15,33 @@ async function bootstrap() {
? ['error', 'warn']
: ['log', 'error', 'warn', 'debug', 'verbose'],
});
app.disable('x-powered-by');
app.set('trust proxy', process.env.TRUST_PROXY === 'true');

const allowedOrigins =
process.env.CORS_ORIGINS?.split(',').map((o) => o.trim()).filter(Boolean) ?? [];
app.enableCors({
origin: (origin, callback) => {
if (!origin) return callback(null, true);
if (process.env.NODE_ENV !== 'production' && /^https?:\/\/localhost(:\d+)?$/.test(origin)) {
return callback(null, true);
}
if (allowedOrigins.includes(origin)) return callback(null, true);
return callback(new Error('Not allowed by CORS'), false);
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Authorization', 'Content-Type', 'Accept'],
optionsSuccessStatus: 204,
});
app.use(
helmet({
hsts: process.env.NODE_ENV === 'production' ? { maxAge: 31_536_000 } : false,
frameguard: { action: 'deny' },
noSniff: true,
xssFilter: true,
}),
);

app.useGlobalPipes(
new ValidationPipe({
Expand Down
13 changes: 13 additions & 0 deletions backend/src/monitoring/metrics.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Controller, Get, Header } from '@nestjs/common';
import { MetricsService } from './metrics.service';

@Controller()
export class MetricsController {
constructor(private readonly metrics: MetricsService) {}

@Get('metrics')
@Header('Content-Type', 'text/plain; version=0.0.4')
getMetrics() {
return this.metrics.toPrometheus();
}
}
13 changes: 13 additions & 0 deletions backend/src/monitoring/metrics.middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { MetricsService } from './metrics.service';

@Injectable()
export class MetricsMiddleware implements NestMiddleware {
constructor(private readonly metrics: MetricsService) {}

use(_req: Request, _res: Response, next: NextFunction) {
this.metrics.recordRequest();
next();
}
}
32 changes: 32 additions & 0 deletions backend/src/monitoring/metrics.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Injectable } from '@nestjs/common';

@Injectable()
export class MetricsService {
private requestsTotal = 0;
private jwtVerificationFailures = 0;

recordRequest() {
this.requestsTotal += 1;
}

recordJwtVerificationFailure() {
this.jwtVerificationFailures += 1;
}

toPrometheus(): string {
return [
'# HELP http_requests_total Total HTTP requests handled',
'# TYPE http_requests_total counter',
`http_requests_total ${this.requestsTotal}`,
'# HELP http_request_duration_seconds HTTP request duration histogram placeholder',
'# TYPE http_request_duration_seconds histogram',
'http_request_duration_seconds_bucket{le="1"} 0',
'http_request_duration_seconds_bucket{le="+Inf"} 0',
'http_request_duration_seconds_count 0',
'http_request_duration_seconds_sum 0',
'# HELP jwt_verification_failures_total JWT verification failures',
'# TYPE jwt_verification_failures_total counter',
`jwt_verification_failures_total ${this.jwtVerificationFailures}`,
].join('\n');
}
}
10 changes: 10 additions & 0 deletions backend/src/monitoring/monitoring.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { MetricsController } from './metrics.controller';
import { MetricsService } from './metrics.service';

@Module({
controllers: [MetricsController],
providers: [MetricsService],
exports: [MetricsService],
})
export class MonitoringModule {}
12 changes: 11 additions & 1 deletion contract/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,11 @@ impl EscrowContract {
}
s.state = SessionState::Approved;
env.storage().persistent().set(&DataKey::Session(session_id), &s);
env.events().publish((Symbol::new(&env, "SessionApproved"),), session_id);
}

pub fn approve_session(env: Env, session_id: u64, token_id: Address) {
Self::approve(env, session_id, token_id);
}

pub fn refund(env: Env, session_id: u64, token_id: Address) {
Expand All @@ -219,6 +224,11 @@ impl EscrowContract {
s.state = SessionState::Refunded;
env.storage().persistent().set(&DataKey::Session(session_id), &s);
env.events().publish((symbol_short!("REFUNDED"), session_id), s.amount);
env.events().publish((Symbol::new(&env, "SessionRefunded"),), session_id);
}

pub fn refund_session(env: Env, session_id: u64, token_id: Address) {
Self::refund(env, session_id, token_id);
}

pub fn auto_refund(env: Env, session_id: u64, token_id: Address) {
Expand Down Expand Up @@ -606,4 +616,4 @@ impl SkillSyncEscrow {
}

#[cfg(test)]
mod test;
mod test;
Loading
Loading