2026-04-16-3 add secur ,billing ,documents ,

This commit is contained in:
Hamza-Ayed
2026-04-16 05:36:53 +03:00
parent 47a38d657e
commit 9f2a03a979
20 changed files with 882 additions and 256 deletions
+2 -2
View File
@@ -185,7 +185,7 @@ export class AuthService {
} else {
// 3. Create new if neither found
this.logger.log(`Creating new tenant for Firebase user: ${email} (${uid})`);
const isAdmin = email === 'hamzaaleghwairyeen@gmail.com' || email === 'hamzadoctor@gmail.com';
const isAdmin = email === 'hamzaaleghwairyeen@gmail.com' || email === 'hamzadoctor@gmail.com' || email === 'hamzaayedpython@gmail.com';
tenant = await this.tenantRepository.save({
firebaseUid: uid,
email,
@@ -197,7 +197,7 @@ export class AuthService {
}
} else {
// Auto-upgrade admins if they exist but are on lower plan
const isAdmin = email === 'hamzaaleghwairyeen@gmail.com' || email === 'hamzadoctor@gmail.com';
const isAdmin = email === 'hamzaaleghwairyeen@gmail.com' || email === 'hamzadoctor@gmail.com' || email === 'hamzaayedpython@gmail.com';
if (isAdmin && tenant.plan !== TenantPlan.ENTERPRISE) {
tenant.plan = TenantPlan.ENTERPRISE;
await this.tenantRepository.save(tenant);
@@ -3,6 +3,7 @@ import { ApiKey } from './api-key.entity';
export enum TenantPlan {
FREE = 'FREE',
STARTER = 'STARTER',
PRO = 'PRO',
ENTERPRISE = 'ENTERPRISE',
}
+5 -1
View File
@@ -1,4 +1,4 @@
import { Controller, Get, Post, Body, Param, UseGuards, Req } from '@nestjs/common';
import { Controller, Get, Post, Body, Param, UseGuards, Req, ForbiddenException } from '@nestjs/common';
import { AuthService } from './auth.service';
import { CreateKeyDto } from './dto/management/create-key.dto';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
@@ -17,6 +17,10 @@ export class TenantController {
@Req() req: any,
@Body() dto: CreateKeyDto
) {
if (req.user && req.user.email_verified === false) {
throw new ForbiddenException('You must verify your email address before creating an API key.');
}
const tenantId = req.tenant.id;
return this.authService.createApiKey(
tenantId,
+13 -2
View File
@@ -32,7 +32,18 @@ export class BillingController {
async checkout(@Req() req: any, @Body() body: { plan: string; provider: PaymentProvider }) {
const tenantId = req.tenant.id;
const plan = body.plan;
const amount = plan === 'PRO' ? 40 : 0; // Price logic
// Mapping prices to plans
const pricing = {
'STARTER': 29,
'PRO': 89,
};
const amount = pricing[plan] || 0;
if (amount === 0 && plan !== 'FREE') {
throw new BadRequestException('Invalid plan or price not configured');
}
if (body.provider === PaymentProvider.PAYMOB) {
const { paymentKey, orderId } = await this.paymobProvider.createPaymentKey(tenantId, amount, plan);
@@ -84,7 +95,7 @@ export class BillingController {
}
// Redirect back to dashboard with status
const targetUrl = `https://map-dashbord.intaleqapp.com/dashboard.html#billing?payment_status=${query.success === 'true' ? 'success' : 'failed'}&id=${query.id}`;
const targetUrl = `https://map-dashboard.intaleqapp.com/dashboard.html#billing?payment_status=${query.success === 'true' ? 'success' : 'failed'}&id=${query.id}`;
return `
<!DOCTYPE html>
+5 -4
View File
@@ -40,7 +40,7 @@ export class BillingService {
sub = await this.subscriptionRepository.save({
tenantId,
plan: 'FREE',
monthlyRequestLimit: 8000,
monthlyRequestLimit: 5000,
status: SubscriptionStatus.ACTIVE,
});
}
@@ -115,9 +115,10 @@ export class BillingService {
// Update Subscription
const limits = {
[TenantPlan.FREE]: 8000,
[TenantPlan.PRO]: 50000,
[TenantPlan.ENTERPRISE]: 1000000,
[TenantPlan.FREE]: 5000,
[TenantPlan.STARTER]: 25000,
[TenantPlan.PRO]: 100000,
[TenantPlan.ENTERPRISE]: 500000,
};
const sub = await this.getSubscription(tenantId);
+20
View File
@@ -0,0 +1,20 @@
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException, ForbiddenException } from '@nestjs/common';
import { TenantPlan } from '../../auth/entities/tenant.entity';
@Injectable()
export class AdminGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const tenant = request['tenant']; // Populated by ApiKeyGuard
if (!tenant) {
throw new UnauthorizedException('Tenant not found in request');
}
if (tenant.plan !== TenantPlan.ENTERPRISE) {
throw new ForbiddenException('Admin access required. Your tenant plan must be ENTERPRISE.');
}
return true;
}
}
@@ -6,6 +6,7 @@ import { JordanResearchService } from './jordan-research.service';
import { AdministrativeLinkingService } from './administrative-linking.service';
import { MapRefinementService } from './map-refinement.service';
import { ApiKeyGuard } from '../common/guards/api-key.guard';
import { AdminGuard } from '../common/guards/admin.guard';
import { TenantThrottlerGuard } from '../common/guards/rate-limiter.guard';
import { SearchQueryDto } from './dto/search-query.dto';
import { ReverseGeocodeDto } from './dto/reverse-geocode.dto';
@@ -47,6 +48,7 @@ export class GeocodingController {
}
@Delete('places')
@UseGuards(AdminGuard)
@ApiOperation({ summary: 'Delete a place by name or ID' })
@ApiQuery({ name: 'name', required: false })
@ApiQuery({ name: 'id', required: false, type: Number })
@@ -66,12 +68,14 @@ export class GeocodingController {
}
@Post('upsert-place')
@UseGuards(AdminGuard)
@ApiOperation({ summary: 'Add or Update a location (Automated Scraper)' })
async upsertPlace(@Body() placeData: any) {
return this.geocodingService.upsertPlace(placeData);
}
@Post('upsert-batch')
@UseGuards(AdminGuard)
@ApiOperation({ summary: 'Add or Update multiple locations in bulk' })
async upsertBatch(@Body() body: { places: any[] }) {
return this.geocodingService.upsertBatch(body.places);
@@ -90,6 +94,7 @@ export class GeocodingController {
}
@Post('import-boundaries')
@UseGuards(AdminGuard)
@ApiOperation({ summary: 'Import administrative boundaries from a local GeoJSON file on the server' })
@ApiQuery({ name: 'country', required: true })
@ApiQuery({ name: 'filePath', required: true })
@@ -107,6 +112,7 @@ export class GeocodingController {
}
@Post('admin/sync-neighborhoods')
@UseGuards(AdminGuard)
@ApiOperation({ summary: 'Sync neighborhood points from OSM for a bbox' })
@ApiQuery({ name: 'bbox', required: false })
@ApiQuery({ name: 'country', required: false, enum: ['jordan', 'syria', 'egypt'] })
@@ -115,6 +121,7 @@ export class GeocodingController {
}
@Post('admin/generate-voronoi')
@UseGuards(AdminGuard)
@ApiOperation({ summary: 'Generate Voronoi polygons for neighborhoods' })
@ApiQuery({ name: 'country', required: false, enum: ['jordan', 'syria', 'egypt'] })
async generateVoronoi(@Query('country') country?: string) {
@@ -122,6 +129,7 @@ export class GeocodingController {
}
@Post('admin/link-places')
@UseGuards(AdminGuard)
@ApiOperation({ summary: 'Link places to administrative hierarchy' })
@ApiQuery({ name: 'country', required: true, enum: ['jordan', 'syria', 'egypt'] })
async linkPlaces(@Query('country') country: 'jordan' | 'syria' | 'egypt') {
+9
View File
@@ -10,6 +10,15 @@ async function bootstrap() {
logger: ['error', 'warn', 'log', 'debug', 'verbose'],
});
// Trust proxy for correct rate limiting behind Nginx / Cloudflare
const httpAdapter = app.getHttpAdapter();
if (httpAdapter && httpAdapter.getInstance && typeof httpAdapter.getInstance().set === 'function') {
httpAdapter.getInstance().set('trust proxy', 1);
}
// Apply standard HTTP security headers
app.use(helmet());
// 1. Modern Security Headers & Permissive CORS for Production Dashboard
app.enableCors({
origin: true, // Reflect request origin
+5 -4
View File
@@ -18,12 +18,13 @@ export class UsageController {
// Limits
const limits = {
FREE: 8000,
PRO: 50000,
ENTERPRISE: 1000000,
FREE: 5000,
STARTER: 25000,
PRO: 100000,
ENTERPRISE: 500000,
};
const limit = limits[tenant.plan] || 8000;
const limit = limits[tenant.plan] || 5000;
return {
...summary,
+6 -4
View File
@@ -13,13 +13,15 @@ import { TenantPlan } from '../auth/entities/tenant.entity';
// Quota Limits per Plan
const QUOTA_LIMITS: Record<TenantPlan, number> = {
[TenantPlan.FREE]: 8000,
[TenantPlan.PRO]: 50000,
[TenantPlan.ENTERPRISE]: 1000000,
[TenantPlan.FREE]: 5000,
[TenantPlan.STARTER]: 25000,
[TenantPlan.PRO]: 100000,
[TenantPlan.ENTERPRISE]: 500000,
};
const RATE_LIMITS: Record<TenantPlan, number> = {
[TenantPlan.FREE]: 10,
[TenantPlan.FREE]: 5,
[TenantPlan.STARTER]: 100,
[TenantPlan.PRO]: 500,
[TenantPlan.ENTERPRISE]: 5000,
};