feat(security): implement dynamic tactical license gate, instant key revocation, and clean hardcoded credentials
This commit is contained in:
@@ -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<any>(null);
|
||||
|
||||
@@ -183,7 +183,7 @@ const MapComponent: React.FC<MapProps> = ({
|
||||
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}`, {
|
||||
|
||||
@@ -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<HTMLDivElement>(null);
|
||||
const map = useRef<maplibregl.Map | null>(null);
|
||||
|
||||
// Tactical License & Access Clearance State
|
||||
const [authKey, setAuthKey] = useState<string>(() => {
|
||||
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<string>('');
|
||||
const [inputKey, setInputKey] = useState<string>('');
|
||||
const [isVerifying, setIsVerifying] = useState<boolean>(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<TacticalMode>('terrain');
|
||||
const [cursorPos, setCursorPos] = useState({ lat: 31.95, lng: 35.93, elev: 850 });
|
||||
const [activePlacement, setActivePlacement] = useState<string | null>(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
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Realtime Cursor HUD */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, fontSize: '0.72rem', fontFamily: 'monospace', color: '#94a3b8' }}>
|
||||
<div>LAT: <span style={{ color: '#38bdf8' }}>{cursorPos.lat}</span></div>
|
||||
<div>LNG: <span style={{ color: '#38bdf8' }}>{cursorPos.lng}</span></div>
|
||||
<div>ELEV: <span style={{ color: '#4ade80' }}>{cursorPos.elev}m</span></div>
|
||||
{/* Realtime Cursor HUD & Clearance Status */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 14, fontSize: '0.72rem', fontFamily: 'monospace', color: '#94a3b8' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<div>LAT: <span style={{ color: '#38bdf8' }}>{cursorPos.lat}</span></div>
|
||||
<div>LNG: <span style={{ color: '#38bdf8' }}>{cursorPos.lng}</span></div>
|
||||
<div>ELEV: <span style={{ color: '#4ade80' }}>{cursorPos.elev}m</span></div>
|
||||
</div>
|
||||
|
||||
{authStatus === 'authorized' && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, borderRight: '1px solid rgba(255,255,255,0.15)', paddingRight: 10 }}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 5,
|
||||
background: 'rgba(34, 197, 94, 0.15)',
|
||||
border: '1px solid rgba(34, 197, 94, 0.35)',
|
||||
color: '#4ade80',
|
||||
padding: '3px 8px',
|
||||
borderRadius: 6,
|
||||
fontSize: '0.7rem',
|
||||
fontWeight: 700,
|
||||
fontFamily: 'inherit'
|
||||
}}>
|
||||
<ShieldCheck size={13} />
|
||||
<span>تصريح: {authInfo?.tenantName || 'Enterprise'}</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleRevokeOrLock}
|
||||
title="قفل المنظومة التكتيكية / خروج"
|
||||
style={{
|
||||
background: 'rgba(239, 68, 68, 0.15)',
|
||||
border: '1px solid rgba(239, 68, 68, 0.3)',
|
||||
color: '#f87171',
|
||||
padding: '3px 8px',
|
||||
borderRadius: 6,
|
||||
fontSize: '0.7rem',
|
||||
fontWeight: 700,
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
fontFamily: 'inherit',
|
||||
transition: 'all 0.15s ease'
|
||||
}}
|
||||
>
|
||||
<Lock size={12} />
|
||||
<span>قفل</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -2781,6 +2893,176 @@ ${terrainResult.tacticalRecommendations.map((r, i) => `${i + 1}. ${r}`).join('\n
|
||||
{/* Tactical Map Container */}
|
||||
<div ref={mapContainer} style={{ flex: 1, height: '100%', position: 'relative' }} />
|
||||
</div>
|
||||
|
||||
{/* Tactical License Authorization Modal Overlay */}
|
||||
{authStatus !== 'authorized' && (
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
background: 'rgba(5, 8, 15, 0.94)',
|
||||
backdropFilter: 'blur(25px)',
|
||||
zIndex: 9999,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
direction: 'rtl',
|
||||
padding: 20
|
||||
}}>
|
||||
<div style={{
|
||||
width: '100%',
|
||||
maxWidth: 520,
|
||||
background: 'linear-gradient(180deg, rgba(15, 23, 42, 0.98), rgba(11, 19, 38, 0.98))',
|
||||
border: '1px solid rgba(56, 189, 248, 0.3)',
|
||||
borderRadius: 16,
|
||||
boxShadow: '0 0 50px rgba(14, 165, 233, 0.2), 0 25px 50px -12px rgba(0, 0, 0, 0.8)',
|
||||
padding: '32px 28px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
textAlign: 'center',
|
||||
position: 'relative'
|
||||
}}>
|
||||
{/* Top Tactical Glowing Badge */}
|
||||
<div style={{
|
||||
width: 64,
|
||||
height: 64,
|
||||
borderRadius: 20,
|
||||
background: 'linear-gradient(135deg, rgba(14, 165, 233, 0.2), rgba(79, 70, 229, 0.3))',
|
||||
border: '1px solid rgba(56, 189, 248, 0.4)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 18,
|
||||
boxShadow: '0 0 25px rgba(56, 189, 248, 0.35)'
|
||||
}}>
|
||||
{authStatus === 'validating' ? (
|
||||
<Radio size={32} color="#38bdf8" />
|
||||
) : (
|
||||
<ShieldAlert size={32} color="#38bdf8" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h2 style={{ fontSize: '1.35rem', fontWeight: 800, color: '#f8fafc', margin: '0 0 6px 0' }}>
|
||||
منظومة القيادة والسيطرة والتحليل التكتيكي
|
||||
</h2>
|
||||
<div style={{ fontSize: '0.82rem', color: '#94a3b8', marginBottom: 20, lineHeight: 1.5 }}>
|
||||
بوابة التخويل الأمني العسكري — يتطلب استخدام أدوات التحليل الطبوغرافي ورمايات المدفعية وحقول الألغام مفتاح تصريح معتمد (<span style={{ color: '#38bdf8', fontFamily: 'monospace' }}>Enterprise Key</span>).
|
||||
</div>
|
||||
|
||||
{authError && (
|
||||
<div style={{
|
||||
width: '100%',
|
||||
background: 'rgba(239, 68, 68, 0.12)',
|
||||
border: '1px solid rgba(239, 68, 68, 0.3)',
|
||||
borderRadius: 8,
|
||||
padding: '10px 14px',
|
||||
color: '#f87171',
|
||||
fontSize: '0.8rem',
|
||||
fontWeight: 600,
|
||||
marginBottom: 18,
|
||||
textAlign: 'right',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8
|
||||
}}>
|
||||
<AlertTriangle size={16} style={{ flexShrink: 0 }} />
|
||||
<span>{authError}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
verifyTacticalLicense(inputKey);
|
||||
}}
|
||||
style={{ width: '100%', display: 'flex', flexDirection: 'column', gap: 14 }}
|
||||
>
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<label style={{ display: 'block', fontSize: '0.78rem', fontWeight: 700, color: '#cbd5e1', marginBottom: 6 }}>
|
||||
أدخل مفتاح الترخيص والتصريح التكتيكي:
|
||||
</label>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<input
|
||||
type="password"
|
||||
value={inputKey}
|
||||
onChange={(e) => 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'
|
||||
}}
|
||||
/>
|
||||
<Key size={16} color="#64748b" style={{ position: 'absolute', left: 14, top: 14 }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isVerifying || !inputKey.trim()}
|
||||
style={{
|
||||
width: '100%',
|
||||
background: 'linear-gradient(135deg, #0284c7, #4f46e5)',
|
||||
border: 'none',
|
||||
borderRadius: 10,
|
||||
padding: '12px',
|
||||
color: '#ffffff',
|
||||
fontSize: '0.88rem',
|
||||
fontWeight: 800,
|
||||
cursor: isVerifying || !inputKey.trim() ? 'not-allowed' : 'pointer',
|
||||
opacity: isVerifying || !inputKey.trim() ? 0.6 : 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 8,
|
||||
boxShadow: '0 4px 14px rgba(2, 132, 199, 0.4)',
|
||||
transition: 'all 0.2s ease'
|
||||
}}
|
||||
>
|
||||
{isVerifying ? (
|
||||
<>
|
||||
<Activity size={16} />
|
||||
<span>جاري التحقق من الصلاحيات والتصريح...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ShieldCheck size={16} />
|
||||
<span>تفعيل الدخول والتحقق من الصلاحية</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div style={{ marginTop: 22, paddingTop: 16, borderTop: '1px solid rgba(255,255,255,0.08)', width: '100%', display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: '0.75rem', color: '#64748b' }}>
|
||||
<span>منصة الخرائط والأنظمة الذكية</span>
|
||||
<a
|
||||
href="/"
|
||||
style={{
|
||||
color: '#38bdf8',
|
||||
textDecoration: 'none',
|
||||
fontWeight: 600,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4
|
||||
}}
|
||||
>
|
||||
← العودة إلى الخريطة العامة
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user