From cb384765fb625c43633f2e7b13434255c30ab462 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Tue, 18 Aug 2026 13:46:02 +0300 Subject: [PATCH] feat(security): implement dynamic tactical license gate, instant key revocation, and clean hardcoded credentials --- apps/api/scripts/evaluate_geocoding.ts | 2 +- apps/api/scripts/load_test_geocoding.ts | 2 +- apps/api/scripts/test_autocomplete.ts | 2 +- apps/api/scripts/test_did_you_mean.ts | 2 +- apps/api/scripts/test_failed_searches.ts | 2 +- apps/api/scripts/test_poi_gates.ts | 2 +- apps/api/scripts/test_relative_queries.ts | 2 +- apps/api/src/auth/auth.module.ts | 9 +- apps/api/src/auth/auth.service.ts | 25 +- apps/api/src/auth/tenant.controller.ts | 43 ++- apps/api/src/tactical/tactical.controller.ts | 16 + apps/dashboard/js/app.js | 34 ++- apps/dashboard/src/pages/DashboardHome.tsx | 26 +- apps/web/public/map-demo.html | 4 +- apps/web/src/App.tsx | 2 +- apps/web/src/components/MapComponent.tsx | 2 +- apps/web/src/pages/TacticalDefenseView.tsx | 302 ++++++++++++++++++- 17 files changed, 439 insertions(+), 38 deletions(-) diff --git a/apps/api/scripts/evaluate_geocoding.ts b/apps/api/scripts/evaluate_geocoding.ts index a0b17b3..5a36742 100644 --- a/apps/api/scripts/evaluate_geocoding.ts +++ b/apps/api/scripts/evaluate_geocoding.ts @@ -4,7 +4,7 @@ import axios from 'axios'; // Constants const API_URL = process.env.TEST_API_URL || 'https://map-saas.intaleqapp.com/api/geocoding/search'; -const API_KEY = process.env.TEST_API_KEY || 'zP9vL5mK2nQ8xR7jT4wS1yB6hG3fV0cX'; +const API_KEY = process.env.TEST_API_KEY || process.env.MAP_API_KEY || ''; let DATA_FILE = path.join(__dirname, '../../../data/golden_tests/queries_syria.json'); if (!fs.existsSync(DATA_FILE) && fs.existsSync('/data/data/golden_tests/queries_syria.json')) { DATA_FILE = '/data/data/golden_tests/queries_syria.json'; // Docker environment fallback diff --git a/apps/api/scripts/load_test_geocoding.ts b/apps/api/scripts/load_test_geocoding.ts index 727e8e5..1d6cab3 100644 --- a/apps/api/scripts/load_test_geocoding.ts +++ b/apps/api/scripts/load_test_geocoding.ts @@ -1,7 +1,7 @@ import axios from 'axios'; const API_URL = process.env.TEST_API_URL || 'http://localhost:3200/api/geocoding/search'; -const API_KEY = process.env.TEST_API_KEY || 'zP9vL5mK2nQ8xR7jT4wS1yB6hG3fV0cX'; +const API_KEY = process.env.TEST_API_KEY || process.env.MAP_API_KEY || ''; const TOTAL_REQUESTS = 5000; const CONCURRENCY = 50; // Number of parallel requests diff --git a/apps/api/scripts/test_autocomplete.ts b/apps/api/scripts/test_autocomplete.ts index c009f0c..d539adb 100644 --- a/apps/api/scripts/test_autocomplete.ts +++ b/apps/api/scripts/test_autocomplete.ts @@ -1,7 +1,7 @@ import axios from 'axios'; const API_URL = process.env.TEST_API_URL || 'http://localhost:3200/api/geocoding/autocomplete'; -const API_KEY = process.env.TEST_API_KEY || 'zP9vL5mK2nQ8xR7jT4wS1yB6hG3fV0cX'; +const API_KEY = process.env.TEST_API_KEY || process.env.MAP_API_KEY || ''; const testQueries = [ "مستش", diff --git a/apps/api/scripts/test_did_you_mean.ts b/apps/api/scripts/test_did_you_mean.ts index c599efd..9e775f1 100644 --- a/apps/api/scripts/test_did_you_mean.ts +++ b/apps/api/scripts/test_did_you_mean.ts @@ -1,7 +1,7 @@ import axios from 'axios'; const API_URL = process.env.TEST_API_URL || 'http://localhost:3200/api/geocoding/search'; -const API_KEY = process.env.TEST_API_KEY || 'zP9vL5mK2nQ8xR7jT4wS1yB6hG3fV0cX'; +const API_KEY = process.env.TEST_API_KEY || process.env.MAP_API_KEY || ''; const testQueries = [ "السمساني", // Should suggest "الشميساني" diff --git a/apps/api/scripts/test_failed_searches.ts b/apps/api/scripts/test_failed_searches.ts index 9267cc6..d1618d0 100644 --- a/apps/api/scripts/test_failed_searches.ts +++ b/apps/api/scripts/test_failed_searches.ts @@ -2,7 +2,7 @@ import axios from 'axios'; import { Client } from 'pg'; const API_URL = process.env.TEST_API_URL || 'http://localhost:3200/api/geocoding/search'; -const API_KEY = process.env.TEST_API_KEY || 'zP9vL5mK2nQ8xR7jT4wS1yB6hG3fV0cX'; +const API_KEY = process.env.TEST_API_KEY || process.env.MAP_API_KEY || ''; const DB_URL = process.env.DATABASE_URL || 'postgresql://mapuser:TestMapPass123!@localhost:5432/mapdb'; async function testFailedLogging() { diff --git a/apps/api/scripts/test_poi_gates.ts b/apps/api/scripts/test_poi_gates.ts index e221871..4c82387 100644 --- a/apps/api/scripts/test_poi_gates.ts +++ b/apps/api/scripts/test_poi_gates.ts @@ -1,7 +1,7 @@ import axios from 'axios'; const API_URL = process.env.TEST_API_URL || 'http://localhost:3200/api/geocoding/search'; -const API_KEY = process.env.TEST_API_KEY || 'zP9vL5mK2nQ8xR7jT4wS1yB6hG3fV0cX'; +const API_KEY = process.env.TEST_API_KEY || process.env.MAP_API_KEY || ''; const testQueries = [ "سيتي مول", diff --git a/apps/api/scripts/test_relative_queries.ts b/apps/api/scripts/test_relative_queries.ts index ae8c615..9dfe9f6 100644 --- a/apps/api/scripts/test_relative_queries.ts +++ b/apps/api/scripts/test_relative_queries.ts @@ -2,7 +2,7 @@ import axios from 'axios'; const API_URL = process.env.TEST_API_URL || 'http://localhost:3200/api/geocoding/search'; const AUTO_URL = process.env.TEST_AUTO_URL || 'http://localhost:3200/api/geocoding/autocomplete'; -const API_KEY = process.env.TEST_API_KEY || 'zP9vL5mK2nQ8xR7jT4wS1yB6hG3fV0cX'; +const API_KEY = process.env.TEST_API_KEY || process.env.MAP_API_KEY || ''; const testQueries = [ "قرب مستشفى", diff --git a/apps/api/src/auth/auth.module.ts b/apps/api/src/auth/auth.module.ts index e25b676..8fdc402 100644 --- a/apps/api/src/auth/auth.module.ts +++ b/apps/api/src/auth/auth.module.ts @@ -30,9 +30,10 @@ export class AuthModule implements OnModuleInit { * دمج مفتاح الأمان الافتراضي من الإعدادات لمنع توقف الرقابة الحالية */ async onModuleInit() { - const defaultKey = this.configService.get('MAP_API_KEY') || 'zP9vL5mK2nQ8xR7jT4wS1yB6hG3fV0cX'; - await this.authService.seedDefaultKey('Default System', 'admin@intaleq.xyz', defaultKey); - await this.authService.seedDefaultKey('Default Fallback', 'support@intaleq.xyz', 'intaleq_secret_2026'); - console.log('✅ System API Keys seeded successfully'); + const defaultKey = this.configService.get('MAP_API_KEY'); + if (defaultKey) { + await this.authService.seedDefaultKey('Default System', 'admin@intaleq.xyz', defaultKey); + console.log('✅ System API Key from environment seeded successfully'); + } } } diff --git a/apps/api/src/auth/auth.service.ts b/apps/api/src/auth/auth.service.ts index 732de76..651334a 100644 --- a/apps/api/src/auth/auth.service.ts +++ b/apps/api/src/auth/auth.service.ts @@ -1,4 +1,4 @@ -import { Injectable, UnauthorizedException, Logger, NotFoundException, ConflictException } from '@nestjs/common'; +import { Injectable, UnauthorizedException, Logger, NotFoundException, ConflictException, ForbiddenException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { createHash } from 'crypto'; @@ -126,6 +126,21 @@ export class AuthService { } } + /** + * Delete / Revoke an API key for a tenant with strict IDOR verification + */ + async deleteApiKey(tenantId: string, keyId: string): Promise { + const key = await this.apiKeyRepository.findOne({ where: { id: keyId } }); + if (!key) { + throw new NotFoundException('API Key not found'); + } + if (key.tenantId !== tenantId) { + throw new ForbiddenException('Access denied: You cannot delete an API key belonging to another tenant'); + } + await this.redisService.del(`auth:apikey:${key.key}`); + await this.apiKeyRepository.delete(keyId); + } + /** * Fetch all API keys for a specific tenant */ @@ -141,16 +156,16 @@ export class AuthService { */ async createApiKey(tenantId: string, name: string, rateLimit?: number, allowedOrigins?: string[]): Promise { const existingKeysCount = await this.apiKeyRepository.count({ where: { tenantId } }); - if (existingKeysCount >= 1) { - throw new ConflictException('Limit reached: Only 1 API key allowed per developer currently.'); + if (existingKeysCount >= 5) { + throw new ConflictException('Limit reached: Maximum 5 API keys allowed per tenant.'); } - const key = `in_${createHash('md5').update(Math.random().toString()).digest('hex').substring(0, 24)}`; + const key = `in_${createHash('sha256').update(tenantId + Date.now().toString() + Math.random().toString()).digest('hex').substring(0, 28)}`; const apiKey = this.apiKeyRepository.create({ key, secretHash: this.hashSecret(key), - name, + name: name || 'Production Key', tenantId, rateLimit: rateLimit || 100, allowedOrigins: allowedOrigins || [], diff --git a/apps/api/src/auth/tenant.controller.ts b/apps/api/src/auth/tenant.controller.ts index 26a85a2..00f2c16 100644 --- a/apps/api/src/auth/tenant.controller.ts +++ b/apps/api/src/auth/tenant.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Post, Body, Param, UseGuards, Req, ForbiddenException } from '@nestjs/common'; +import { Controller, Get, Post, Delete, 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'; @@ -30,6 +30,21 @@ export class TenantController { ); } + @Post('keys/:tenantId') + @ApiOperation({ summary: 'Create a new API key for specified tenant (IDOR Protected)' }) + async createKeyForTenant( + @Req() req: any, + @Param('tenantId') tenantId: string, + @Body() dto: CreateKeyDto + ) { + // IDOR Protection: Tenant can only create keys for their own tenant ID + if (req.tenant.id !== tenantId) { + throw new ForbiddenException('Access denied: Cannot create API keys for another tenant.'); + } + + return this.createKey(req, dto); + } + @Get('keys') @ApiOperation({ summary: 'Get all API keys for the authenticated tenant' }) async getKeys(@Req() req: any) { @@ -37,9 +52,35 @@ export class TenantController { return this.authService.getApiKeys(tenantId); } + @Get('keys/:tenantId') + @ApiOperation({ summary: 'Get all API keys for specified tenant (IDOR Protected)' }) + async getKeysForTenant( + @Req() req: any, + @Param('tenantId') tenantId: string + ) { + // IDOR Protection: Tenant can only access their own keys + if (req.tenant.id !== tenantId) { + throw new ForbiddenException('Access denied: Cannot view API keys for another tenant.'); + } + + return this.authService.getApiKeys(req.tenant.id); + } + + @Delete('keys/:keyId') + @ApiOperation({ summary: 'Revoke / Delete an API key (IDOR Protected)' }) + async deleteKey( + @Req() req: any, + @Param('keyId') keyId: string + ) { + const tenantId = req.tenant.id; + await this.authService.deleteApiKey(tenantId, keyId); + return { success: true, message: 'API key revoked successfully' }; + } + @Get('me') @ApiOperation({ summary: 'Get current authenticated tenant info' }) async getMe(@Req() req: any) { return req.tenant; } } + diff --git a/apps/api/src/tactical/tactical.controller.ts b/apps/api/src/tactical/tactical.controller.ts index 367146d..7182fed 100644 --- a/apps/api/src/tactical/tactical.controller.ts +++ b/apps/api/src/tactical/tactical.controller.ts @@ -6,6 +6,7 @@ import { HttpStatus, Post, Query, + Req, UseGuards, } from '@nestjs/common'; import { ApiHeader, ApiOperation, ApiTags } from '@nestjs/swagger'; @@ -26,6 +27,21 @@ import { TacticalService } from './tactical.service'; export class TacticalController { constructor(private readonly tacticalService: TacticalService) {} + @Get('verify-license') + @ApiOperation({ summary: 'Verify tactical clearance and military license' }) + async verifyLicense(@Req() req: any) { + const tenant = req.tenant; + const apiKey = req.apiKey; + return { + valid: true, + tenantName: tenant?.name || 'Authorized Tactical Operator', + plan: tenant?.plan || 'ENTERPRISE', + keyName: apiKey?.name || 'Tactical Defense Key', + rateLimit: req.rateLimit || 1000, + timestamp: new Date().toISOString(), + }; + } + @Get('line-of-sight') @ApiOperation({ summary: 'Calculate Tactical Line of Sight & Intervisibility (تبادل الرؤية العسكري)', diff --git a/apps/dashboard/js/app.js b/apps/dashboard/js/app.js index b866b32..b61e794 100644 --- a/apps/dashboard/js/app.js +++ b/apps/dashboard/js/app.js @@ -328,9 +328,14 @@ const app = { - +
+ + +
`; @@ -339,6 +344,29 @@ const app = { lucide.createIcons(); }, + deleteKey: async (keyId, keyName) => { + if (!confirm(`Are you sure you want to revoke the API key "${keyName || 'Production Key'}"? This action cannot be undone.`)) { + return; + } + + try { + const res = await fetch(`/api/auth/management/keys/${keyId}`, { + method: 'DELETE', + headers: auth.getAuthHeader() + }); + + if (res.ok) { + await app.fetchData(); + } else { + const data = await res.json().catch(() => ({})); + alert(data.message || 'Failed to revoke API key'); + } + } catch (e) { + console.error('Delete key failed:', e); + alert('Failed to revoke API key'); + } + }, + toggleKeyVisibility: (id) => { app.state.showKeys[id] = !app.state.showKeys[id]; app.renderKeysTable(); diff --git a/apps/dashboard/src/pages/DashboardHome.tsx b/apps/dashboard/src/pages/DashboardHome.tsx index 06c157b..e95ef84 100644 --- a/apps/dashboard/src/pages/DashboardHome.tsx +++ b/apps/dashboard/src/pages/DashboardHome.tsx @@ -6,6 +6,7 @@ import { Eye, EyeOff, RefreshCw, + Trash2, ShieldCheck, Globe, Zap, @@ -53,7 +54,7 @@ const DashboardHome = ({ tenant, keys, loading, onRefresh }: DashboardHomeProps) const createKey = async (name: string, limit: number) => { if (!tenant) return; try { - await axios.post(`/api/auth/management/keys/${tenant.id}`, { + await axios.post(`/api/auth/management/keys`, { name, rateLimit: limit }); @@ -65,6 +66,18 @@ const DashboardHome = ({ tenant, keys, loading, onRefresh }: DashboardHomeProps) } }; + const deleteKey = async (keyId: string, keyName: string) => { + if (!window.confirm(`Are you sure you want to revoke the API key "${keyName || 'Production Key'}"? This action cannot be undone.`)) { + return; + } + try { + await axios.delete(`/api/auth/management/keys/${keyId}`); + onRefresh(); + } catch (e) { + alert('Failed to revoke key'); + } + }; + return (
{/* Hero Section */} @@ -264,9 +277,14 @@ const DashboardHome = ({ tenant, keys, loading, onRefresh }: DashboardHomeProps)
- +
+ + +
))} diff --git a/apps/web/public/map-demo.html b/apps/web/public/map-demo.html index 5277170..bc0817d 100644 --- a/apps/web/public/map-demo.html +++ b/apps/web/public/map-demo.html @@ -346,8 +346,8 @@ CONFIG ─────────────────────────────────────────── */ const API_BASE = window.location.origin; // Dynamically use the current host - const API_KEY = 'zP9vL5mK2nQ8xR7jT4wS1yB6hG3fV0cX'; - const HEADERS = { 'x-api-key': API_KEY }; + const API_KEY = new URLSearchParams(window.location.search).get('api_key') || localStorage.getItem('map_api_key') || ''; + const HEADERS = API_KEY ? { 'x-api-key': API_KEY } : {}; const ROUTES = { LEVANT: { diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 910ec7c..1172ea0 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -5,7 +5,7 @@ import { decodePolyline } from './utils/polyline'; import WeatherPanel from './components/WeatherPanel'; import { LineOfSightTool } from './components/LineOfSightTool'; -const DEFAULT_API_KEY = (import.meta as any).env.VITE_API_KEY || 'zP9vL5mK2nQ8xR7jT4wS1yB6hG3fV0cX'; +const DEFAULT_API_KEY = (import.meta as any).env.VITE_API_KEY || ''; function App() { const [map, setMap] = useState(null); diff --git a/apps/web/src/components/MapComponent.tsx b/apps/web/src/components/MapComponent.tsx index af8c489..6ae174e 100644 --- a/apps/web/src/components/MapComponent.tsx +++ b/apps/web/src/components/MapComponent.tsx @@ -183,7 +183,7 @@ const MapComponent: React.FC = ({ const fetchWeather = async () => { try { const apiUrl = (import.meta as any).env.VITE_API_URL || '/api'; - const apiKey = (import.meta as any).env.VITE_API_KEY || 'zP9vL5mK2nQ8xR7jT4wS1yB6hG3fV0cX'; + const apiKey = (import.meta as any).env.VITE_API_KEY || ''; const regionParam = currentRegion ? `?region=${encodeURIComponent(currentRegion)}` : ''; const citiesRes = await fetch(`${apiUrl}/weather/cities${regionParam}`, { diff --git a/apps/web/src/pages/TacticalDefenseView.tsx b/apps/web/src/pages/TacticalDefenseView.tsx index 04385bf..5b53552 100644 --- a/apps/web/src/pages/TacticalDefenseView.tsx +++ b/apps/web/src/pages/TacticalDefenseView.tsx @@ -32,7 +32,13 @@ import { Compass, Check, EyeOff, - Sliders + Sliders, + Lock, + Unlock, + Key, + ShieldAlert, + ShieldCheck, + LogOut } from 'lucide-react'; import { calculateLineOfSight, @@ -78,6 +84,66 @@ export const TacticalDefenseView: React.FC = () => { const mapContainer = useRef(null); const map = useRef(null); + // Tactical License & Access Clearance State + const [authKey, setAuthKey] = useState(() => { + return new URLSearchParams(window.location.search).get('key') || + new URLSearchParams(window.location.search).get('api_key') || + sessionStorage.getItem('tactical_api_key') || ''; + }); + const [authStatus, setAuthStatus] = useState<'validating' | 'authorized' | 'unauthorized'>('validating'); + const [authInfo, setAuthInfo] = useState<{ tenantName?: string; plan?: string; keyName?: string } | null>(null); + const [authError, setAuthError] = useState(''); + const [inputKey, setInputKey] = useState(''); + const [isVerifying, setIsVerifying] = useState(false); + + const verifyTacticalLicense = async (keyCandidate: string) => { + if (!keyCandidate || !keyCandidate.trim()) { + setAuthStatus('unauthorized'); + return; + } + setIsVerifying(true); + setAuthError(''); + try { + const apiUrl = (import.meta as any).env.VITE_API_URL || '/api'; + const res = await fetch(`${apiUrl}/tactical/verify-license`, { + headers: { 'x-api-key': keyCandidate.trim() } + }); + if (res.ok) { + const data = await res.json(); + setAuthInfo(data); + setAuthKey(keyCandidate.trim()); + sessionStorage.setItem('tactical_api_key', keyCandidate.trim()); + setAuthStatus('authorized'); + } else { + sessionStorage.removeItem('tactical_api_key'); + setAuthStatus('unauthorized'); + setAuthError('مفتاح التصريح التكتيكي غير صالح، أو منتهي الصلاحية، أو تم إلغاؤه من قبل الإدارة.'); + } + } catch (e) { + sessionStorage.removeItem('tactical_api_key'); + setAuthStatus('unauthorized'); + setAuthError('تعذر التحقق من خادم التخويل التكتيكي. يرجى التأكد من الاتصال.'); + } finally { + setIsVerifying(false); + } + }; + + useEffect(() => { + if (authKey) { + verifyTacticalLicense(authKey); + } else { + setAuthStatus('unauthorized'); + } + }, []); + + const handleRevokeOrLock = () => { + sessionStorage.removeItem('tactical_api_key'); + setAuthKey(''); + setAuthStatus('unauthorized'); + setAuthInfo(null); + setAuthError(''); + }; + const [mode, setMode] = useState('terrain'); const [cursorPos, setCursorPos] = useState({ lat: 31.95, lng: 35.93, elev: 850 }); const [activePlacement, setActivePlacement] = useState(null); @@ -983,11 +1049,11 @@ export const TacticalDefenseView: React.FC = () => { setArtilleryLoading(true); try { const apiUrl = (import.meta as any).env.VITE_API_URL || '/api'; - const apiKey = (import.meta as any).env.VITE_API_KEY || 'zP9vL5mK2nQ8xR7jT4wS1yB6hG3fV0cX'; + const keyToSend = authKey || (import.meta as any).env.VITE_API_KEY || ''; const res = await fetch(`${apiUrl}/tactical/artillery-fire-mission`, { method: 'POST', - headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey }, + headers: { 'Content-Type': 'application/json', 'x-api-key': keyToSend }, body: JSON.stringify({ gunLat: gunPos[0], gunLng: gunPos[1], @@ -1026,10 +1092,10 @@ export const TacticalDefenseView: React.FC = () => { setHlzLoading(true); try { const apiUrl = (import.meta as any).env.VITE_API_URL || '/api'; - const apiKey = (import.meta as any).env.VITE_API_KEY || 'zP9vL5mK2nQ8xR7jT4wS1yB6hG3fV0cX'; + const keyToSend = authKey || (import.meta as any).env.VITE_API_KEY || ''; const res = await fetch(`${apiUrl}/tactical/hlz-assessment?lat=${hlzCenter[0]}&lng=${hlzCenter[1]}&radius=4000`, { - headers: { 'x-api-key': apiKey } + headers: { 'x-api-key': keyToSend } }); const data = await res.json(); setHlzResult(data); @@ -1199,11 +1265,57 @@ ${terrainResult.tacticalRecommendations.map((r, i) => `${i + 1}. ${r}`).join('\n ))} - {/* Realtime Cursor HUD */} -
-
LAT: {cursorPos.lat}
-
LNG: {cursorPos.lng}
-
ELEV: {cursorPos.elev}m
+ {/* Realtime Cursor HUD & Clearance Status */} +
+
+
LAT: {cursorPos.lat}
+
LNG: {cursorPos.lng}
+
ELEV: {cursorPos.elev}m
+
+ + {authStatus === 'authorized' && ( +
+
+ + تصريح: {authInfo?.tenantName || 'Enterprise'} +
+ + +
+ )}
@@ -2781,6 +2893,176 @@ ${terrainResult.tacticalRecommendations.map((r, i) => `${i + 1}. ${r}`).join('\n {/* Tactical Map Container */}
+ + {/* Tactical License Authorization Modal Overlay */} + {authStatus !== 'authorized' && ( +
+
+ {/* Top Tactical Glowing Badge */} +
+ {authStatus === 'validating' ? ( + + ) : ( + + )} +
+ +

+ منظومة القيادة والسيطرة والتحليل التكتيكي +

+
+ بوابة التخويل الأمني العسكري — يتطلب استخدام أدوات التحليل الطبوغرافي ورمايات المدفعية وحقول الألغام مفتاح تصريح معتمد (Enterprise Key). +
+ + {authError && ( +
+ + {authError} +
+ )} + +
{ + e.preventDefault(); + verifyTacticalLicense(inputKey); + }} + style={{ width: '100%', display: 'flex', flexDirection: 'column', gap: 14 }} + > +
+ +
+ setInputKey(e.target.value)} + placeholder="in_xxxxxxxxxxxxxxxxxxxxxxxxxxxx" + autoFocus + style={{ + width: '100%', + background: 'rgba(15, 23, 42, 0.8)', + border: '1px solid rgba(255, 255, 255, 0.2)', + borderRadius: 10, + padding: '12px 14px', + color: '#ffffff', + fontSize: '0.9rem', + fontFamily: 'monospace', + outline: 'none', + boxSizing: 'border-box' + }} + /> + +
+
+ + +
+ +
+ منصة الخرائط والأنظمة الذكية + + ← العودة إلى الخريطة العامة + +
+
+
+ )}
); };