2026-04-16-3 add secur ,billing ,documents ,
This commit is contained in:
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user