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
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,8 @@ PUSHER_APP_ID=your_app_id
PUSHER_KEY=your_key
PUSHER_SECRET=your_secret
PUSHER_CLUSTER=your_cluster

# Health Check Configuration
HEALTH_HEAP_USED_WARN_MB=300
HEALTH_DISK_USED_WARN_PERCENT=85
HEALTH_ALLOWLIST=127.0.0.1,10.0.0.0/24
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,24 @@ All endpoints follow RESTful patterns and are versioned:
}
```

## 🏥 Health Checks

### Endpoints
- `GET /api/v1/health/live` → Always returns `{ status: "up" }` if the app is alive
- `GET /api/v1/health/ready` → Runs DB, Redis, memory, and disk checks

### Kubernetes Example
```yaml
livenessProbe:
httpGet:
path: /api/v1/health/live
port: 3000
readinessProbe:
httpGet:
path: /api/v1/health/ready
port: 3000
```

## 🔐 Authentication

### Authentication Flow
Expand Down
48 changes: 48 additions & 0 deletions env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Database Configuration
DATABASE_URL=postgresql://user:password@localhost:5432/starshop
DB_HOST=localhost
DB_PORT=5432
DB_USERNAME=user
DB_PASSWORD=password
DB_DATABASE=starshop
DB_SSL=false

# JWT Configuration
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
JWT_EXPIRATION_TIME=1h

# AWS S3 Configuration (Optional - for file uploads)
AWS_ACCESS_KEY_ID=your_access_key
AWS_SECRET_ACCESS_KEY=your_secret_key
AWS_REGION=us-east-1
AWS_S3_BUCKET=your_bucket_name

# Cloudinary Configuration (Optional - for image uploads)
CLOUDINARY_CLOUD_NAME=your_cloud_name
CLOUDINARY_API_KEY=your_api_key
CLOUDINARY_API_SECRET=your_api_secret

# Pusher Configuration (Optional - for real-time notifications)
PUSHER_APP_ID=your_app_id
PUSHER_KEY=your_key
PUSHER_SECRET=your_secret
PUSHER_CLUSTER=your_cluster

# Supabase Configuration (Optional)
SUPABASE_URL=your_supabase_url
SUPABASE_SERVICE_ROLE_KEY=your_service_role_key

# Redis Configuration (Optional)
REDIS_URL=redis://localhost:6379

# File Upload Configuration
FILE_UPLOAD_LIMIT=10

# Application Configuration
NODE_ENV=development
PORT=3000

# Health Check Configuration
HEALTH_HEAP_USED_WARN_MB=300
HEALTH_DISK_USED_WARN_PERCENT=85
HEALTH_ALLOWLIST=127.0.0.1,10.0.0.0/24
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"@nestjs/platform-express": "^11.1.0",
"@nestjs/schedule": "^6.0.0",
"@nestjs/swagger": "^11.2.0",
"@nestjs/terminus": "^11.0.0",
"@nestjs/typeorm": "^11.0.0",
"@supabase/supabase-js": "^2.46.1",
"@types/bcryptjs": "^2.4.6",
Expand All @@ -58,6 +59,7 @@
"express-async-handler": "^1.2.0",
"express-rate-limit": "^7.5.0",
"express-validator": "^7.2.0",
"ip-range-check": "^0.3.0",
"jsonwebtoken": "^9.0.2",
"multer": "^1.4.5-lts.2",
"multer-s3": "^3.0.1",
Expand Down
2 changes: 2 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { BuyerRequestsModule } from './modules/buyer-requests/buyer-requests.mod
import { OffersModule } from './modules/offers/offers.module';
import { SupabaseModule } from './modules/supabase/supabase.module';
import { StoresModule } from './modules/stores/stores.module';
import { HealthModule } from './health/health.module';

// Entities
import { User } from './modules/users/entities/user.entity';
Expand Down Expand Up @@ -85,6 +86,7 @@ import { Store } from './modules/stores/entities/store.entity';
OffersModule,
SupabaseModule,
StoresModule,
HealthModule,
],
})
export class AppModule {}
65 changes: 65 additions & 0 deletions src/health/health.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Controller, Get, UseGuards } from '@nestjs/common';
import { HealthCheck, HealthCheckService, TypeOrmHealthIndicator, MemoryHealthIndicator, DiskHealthIndicator } from '@nestjs/terminus';
import { PrismaClient } from '@prisma/client';
import { createClient } from 'redis';
import { HealthAllowlistGuard } from '../middleware/healthAllowlist.guard';

@Controller('health')
@UseGuards(HealthAllowlistGuard)
export class HealthController {
private prisma = new PrismaClient();
private redis = process.env.REDIS_URL ? createClient({ url: process.env.REDIS_URL }) : null;

constructor(
private health: HealthCheckService,
private db: TypeOrmHealthIndicator,
private memory: MemoryHealthIndicator,
private disk: DiskHealthIndicator,
) {}

@Get('live')
@HealthCheck()
checkLive() {
return { status: 'up' };
}

@Get('ready')
@HealthCheck()
async checkReady() {
const checks = [];

// Database check
checks.push(async () => this.db.pingCheck('database', { timeout: 300 }));

// Redis check (if enabled)
if (this.redis) {
checks.push(async () => {
try {
await this.redis.connect();
await this.redis.ping();
await this.redis.disconnect();
return { redis: { status: 'up' } };
} catch (e) {
throw new Error('Redis unavailable');
}
});
}

// Optional memory/disk checks
if (process.env.HEALTH_HEAP_USED_WARN_MB) {
checks.push(async () => this.memory.checkHeap(
'memory_heap',
parseInt(process.env.HEALTH_HEAP_USED_WARN_MB, 10) * 1024 * 1024,
));
}

if (process.env.HEALTH_DISK_USED_WARN_PERCENT) {
checks.push(async () => this.disk.checkStorage('disk', {
path: '/',
thresholdPercent: parseInt(process.env.HEALTH_DISK_USED_WARN_PERCENT, 10) / 100,
}));
}

return this.health.check(checks);
}
}
9 changes: 9 additions & 0 deletions src/health/health.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { TerminusModule } from '@nestjs/terminus';
import { HealthController } from './health.controller';

@Module({
imports: [TerminusModule],
controllers: [HealthController],
})
export class HealthModule {}
18 changes: 18 additions & 0 deletions src/middleware/healthAllowlist.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
import ipRangeCheck from 'ip-range-check';

@Injectable()
export class HealthAllowlistGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();

if (process.env.NODE_ENV === 'production' && process.env.HEALTH_ALLOWLIST) {
const allowlist = process.env.HEALTH_ALLOWLIST.split(',');
if (!ipRangeCheck(request.ip, allowlist)) {
throw new ForbiddenException('Forbidden');
}
}

return true;
}
}