source: lan-frontend/src/components/TopBar.jsx@ 2058e5c

Last change on this file since 2058e5c was 2058e5c, checked in by istevanoska <ilinastevanoska@…>, 6 months ago

Working / before login

  • Property mode set to 100644
File size: 16.5 KB
Line 
1import React, { useEffect, useMemo, useState } from "react";
2
3export default function TopBar({ onOpenAI, onRefresh, lastUpdated, onEnvChange }) {
4 const [open, setOpen] = useState(false);
5
6 // admin auth
7 const [adminKey, setAdminKey] = useState("");
8 const [adminSession, setAdminSession] = useState("");
9
10 // env + tokens
11 const [envs, setEnvs] = useState(["default"]);
12 const [selectedEnv, setSelectedEnv] = useState("default");
13 const [newEnvName, setNewEnvName] = useState("");
14 const [token, setToken] = useState("");
15
16 const [busy, setBusy] = useState(false);
17 const [error, setError] = useState("");
18
19 // helper: fetch env list after login
20 async function loadEnvironments(session) {
21 const r = await fetch("/api/admin/environments", {
22 headers: { "X-Admin-Session": session },
23 });
24 const data = await r.json();
25 if (!r.ok) throw new Error(data?.error || `HTTP ${r.status}`);
26
27 const list = data.environments?.length ? data.environments : ["default"];
28 setEnvs(list);
29
30 // ако тековниот не постои, стави првиот од листата
31 if (!list.includes(selectedEnv)) {
32 setSelectedEnv(list[0]);
33 onEnvChange?.(list[0]);
34 }
35 }
36
37 async function login() {
38 setError("");
39 setToken("");
40
41 if (!adminKey.trim()) {
42 setError("Внеси admin key.");
43 return;
44 }
45
46 setBusy(true);
47 try {
48 const r = await fetch("/api/admin/login", {
49 method: "POST",
50 headers: { "Content-Type": "application/json" },
51 body: JSON.stringify({ admin_key: adminKey.trim() }),
52 });
53 const data = await r.json();
54 if (!r.ok) throw new Error(data?.error || `HTTP ${r.status}`);
55
56 const session = data.session;
57 setAdminSession(session);
58 await loadEnvironments(session);
59 } catch (e) {
60 setError(String(e.message || e));
61 } finally {
62 setBusy(false);
63 }
64 }
65
66 function logout() {
67 setAdminSession("");
68 setAdminKey("");
69 setToken("");
70 setError("");
71 setEnvs(["default"]);
72 setSelectedEnv("default");
73 onEnvChange?.("default");
74 }
75
76 async function createEnvironment() {
77 setError("");
78 setToken("");
79 if (!newEnvName.trim()) {
80 setError("Внеси име за environment.");
81 return;
82 }
83
84 setBusy(true);
85 try {
86 const r = await fetch("/api/admin/environments", {
87 method: "POST",
88 headers: {
89 "Content-Type": "application/json",
90 "X-Admin-Session": adminSession,
91 },
92 body: JSON.stringify({ name: newEnvName.trim() }),
93 });
94
95 const data = await r.json();
96 if (!r.ok) throw new Error(data?.error || `HTTP ${r.status}`);
97
98 // reload list, select new env
99 await loadEnvironments(adminSession);
100 setSelectedEnv(newEnvName.trim());
101 onEnvChange?.(newEnvName.trim());
102 setNewEnvName("");
103 } catch (e) {
104 setError(String(e.message || e));
105 } finally {
106 setBusy(false);
107 }
108 }
109
110 async function generateToken() {
111 setError("");
112 setToken("");
113 if (!selectedEnv) {
114 setError("Избери environment.");
115 return;
116 }
117
118 setBusy(true);
119 try {
120 const r = await fetch("/api/admin/tokens", {
121 method: "POST",
122 headers: {
123 "Content-Type": "application/json",
124 "X-Admin-Session": adminSession,
125 },
126 body: JSON.stringify({ env: selectedEnv }),
127 });
128
129 const data = await r.json();
130 if (!r.ok) throw new Error(data?.error || `HTTP ${r.status}`);
131
132 setToken(data.token || "");
133 } catch (e) {
134 setError(String(e.message || e));
135 } finally {
136 setBusy(false);
137 }
138 }
139
140 async function copyToken() {
141 if (!token) return;
142 await navigator.clipboard.writeText(token);
143 }
144
145 function changeEnv(val) {
146 setSelectedEnv(val);
147 setToken("");
148 onEnvChange?.(val);
149 }
150
151 const prettyTime = useMemo(() => {
152 if (!lastUpdated) return "Loading…";
153 try {
154 return `Updated ${new Date(lastUpdated).toLocaleTimeString()}`;
155 } catch {
156 return "Updated";
157 }
158 }, [lastUpdated]);
159
160 return (
161 <>
162 <style>{`
163 /* ---- TopBar scoped theme ---- */
164 .tb-wrap{
165 position: sticky;
166 top: 0;
167 z-index: 20;
168 backdrop-filter: blur(10px);
169 background: linear-gradient(180deg, rgba(10,16,32,0.92), rgba(10,16,32,0.75));
170 border-bottom: 1px solid rgba(96,165,250,0.18);
171 }
172 .tb-inner{
173 max-width: 1200px;
174 margin: 0 auto;
175 padding: 14px 18px;
176 display: flex;
177 align-items: center;
178 justify-content: space-between;
179 gap: 14px;
180 }
181
182 .tb-brand{
183 display:flex;
184 align-items:center;
185 gap: 12px;
186 min-width: 260px;
187 }
188 .tb-badge{
189 width: 44px;
190 height: 44px;
191 border-radius: 14px;
192 display:flex;
193 align-items:center;
194 justify-content:center;
195 background:
196 radial-gradient(120px 80px at 30% 20%, rgba(96,165,250,0.55), transparent 60%),
197 linear-gradient(135deg, rgba(30,64,175,0.75), rgba(15,23,42,0.85));
198 border: 1px solid rgba(96,165,250,0.25);
199 box-shadow: 0 10px 25px rgba(0,0,0,0.25);
200 }
201 .tb-title{
202 margin:0;
203 font-size: 16px;
204 font-weight: 900;
205 letter-spacing: 0.4px;
206 color: rgba(255,255,255,0.95);
207 line-height: 1.2;
208 }
209 .tb-sub{
210 margin: 2px 0 0 0;
211 font-size: 12px;
212 color: rgba(191,219,254,0.85);
213 }
214 .tb-sub span{
215 color: rgba(96,165,250,0.95);
216 font-weight: 800;
217 }
218
219 .tb-actions{
220 display:flex;
221 align-items:center;
222 gap: 10px;
223 flex-wrap: wrap;
224 justify-content: flex-end;
225 }
226
227 .tb-pill{
228 display:inline-flex;
229 align-items:center;
230 gap: 8px;
231 padding: 8px 12px;
232 border-radius: 999px;
233 background: rgba(30,41,59,0.65);
234 border: 1px solid rgba(96,165,250,0.22);
235 color: rgba(255,255,255,0.9);
236 font-weight: 800;
237 font-size: 12px;
238 box-shadow: inset 0 1px 0 rgba(255,255,255,0.06);
239 }
240 .tb-pill .dot{
241 width: 8px;
242 height: 8px;
243 border-radius: 999px;
244 background: rgba(96,165,250,0.95);
245 box-shadow: 0 0 0 3px rgba(96,165,250,0.15);
246 }
247
248 .tb-btn{
249 appearance: none;
250 border: 1px solid rgba(96,165,250,0.22);
251 background: rgba(15,23,42,0.55);
252 color: rgba(255,255,255,0.9);
253 padding: 9px 12px;
254 border-radius: 12px;
255 font-weight: 800;
256 font-size: 13px;
257 cursor: pointer;
258 transition: transform .12s ease, background .12s ease, border-color .12s ease, opacity .12s ease;
259 box-shadow: 0 8px 20px rgba(0,0,0,0.18);
260 }
261 .tb-btn:hover{
262 transform: translateY(-1px);
263 background: rgba(30,41,59,0.65);
264 border-color: rgba(96,165,250,0.38);
265 }
266 .tb-btn:disabled{
267 opacity: .6;
268 cursor: not-allowed;
269 transform: none;
270 }
271 .tb-btn.primary{
272 background: linear-gradient(135deg, rgba(37,99,235,0.95), rgba(56,189,248,0.7));
273 border-color: rgba(147,197,253,0.35);
274 color: rgba(5,12,24,0.95);
275 }
276 .tb-btn.primary:hover{
277 background: linear-gradient(135deg, rgba(59,130,246,0.95), rgba(34,211,238,0.75));
278 }
279
280 /* ---- Modal ---- */
281 .tb-backdrop{
282 position: fixed;
283 inset: 0;
284 background: rgba(2,6,23,0.72);
285 backdrop-filter: blur(6px);
286 display:flex;
287 align-items: center;
288 justify-content: center;
289 padding: 20px;
290 z-index: 50;
291 }
292 .tb-modal{
293 width: min(860px, 96vw);
294 border-radius: 18px;
295 background:
296 radial-gradient(900px 420px at 20% 0%, rgba(96,165,250,0.18), transparent 60%),
297 radial-gradient(700px 350px at 80% 10%, rgba(34,211,238,0.10), transparent 55%),
298 rgba(10,16,32,0.92);
299 border: 1px solid rgba(96,165,250,0.22);
300 box-shadow: 0 30px 80px rgba(0,0,0,0.45);
301 overflow: hidden;
302 }
303 .tb-modal-head{
304 padding: 16px 18px;
305 display:flex;
306 align-items: center;
307 justify-content: space-between;
308 gap: 10px;
309 border-bottom: 1px solid rgba(96,165,250,0.18);
310 }
311 .tb-modal-title{
312 color: rgba(255,255,255,0.96);
313 font-weight: 900;
314 letter-spacing: .3px;
315 }
316 .tb-modal-sub{
317 color: rgba(191,219,254,0.8);
318 font-size: 12px;
319 margin-top: 2px;
320 }
321 .tb-grid{
322 padding: 16px;
323 display:grid;
324 gap: 12px;
325 }
326 .tb-card{
327 border-radius: 16px;
328 background: rgba(15,23,42,0.55);
329 border: 1px solid rgba(96,165,250,0.18);
330 padding: 14px;
331 }
332 .tb-card-head{
333 display:flex;
334 align-items:center;
335 justify-content: space-between;
336 margin-bottom: 10px;
337 }
338 .tb-card-title{
339 color: rgba(255,255,255,0.92);
340 font-weight: 900;
341 }
342 .tb-help{
343 color: rgba(148,163,184,0.95);
344 font-size: 12px;
345 margin-top: 8px;
346 line-height: 1.35;
347 }
348 .tb-input, .tb-select{
349 width: 100%;
350 padding: 10px 12px;
351 border-radius: 12px;
352 border: 1px solid rgba(96,165,250,0.18);
353 background: rgba(2,6,23,0.35);
354 color: rgba(255,255,255,0.92);
355 outline: none;
356 font-weight: 700;
357 }
358 .tb-input::placeholder{ color: rgba(148,163,184,0.75); }
359 .tb-row{
360 display:grid;
361 gap: 10px;
362 }
363 .tb-token{
364 display:flex;
365 align-items:center;
366 justify-content: space-between;
367 gap: 10px;
368 padding: 10px 12px;
369 border-radius: 14px;
370 background: rgba(2,6,23,0.38);
371 border: 1px dashed rgba(147,197,253,0.32);
372 color: rgba(255,255,255,0.92);
373 }
374 .tb-mono{
375 font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
376 font-weight: 800;
377 color: rgba(191,219,254,0.95);
378 }
379 .tb-error{
380 border-color: rgba(239,68,68,0.45) !important;
381 background: rgba(127,29,29,0.15) !important;
382 color: rgba(254,202,202,0.95);
383 }
384 `}</style>
385
386 {/* TOPBAR */}
387 <div className="tb-wrap">
388 <div className="tb-inner">
389 <div className="tb-brand">
390 <div className="tb-badge">🛡️</div>
391 <div>
392 <h1 className="tb-title">LAN Security Monitor</h1>
393 <p className="tb-sub">
394 Sysmon Edition • <span>{prettyTime}</span>
395 </p>
396 </div>
397 </div>
398
399 <div className="tb-actions">
400 <span className="tb-pill">
401 <span className="dot" />
402 Env: {selectedEnv}
403 </span>
404 <button className="tb-btn" onClick={onRefresh} disabled={busy}>Refresh</button>
405 <button className="tb-btn" onClick={() => setOpen(true)}>Admin</button>
406 <button className="tb-btn primary" onClick={onOpenAI}>AI Assistant</button>
407 </div>
408 </div>
409 </div>
410
411 {/* MODAL */}
412 {open && (
413 <div className="tb-backdrop" onMouseDown={() => setOpen(false)}>
414 <div className="tb-modal" onMouseDown={(e) => e.stopPropagation()}>
415 <div className="tb-modal-head">
416 <div>
417 <div className="tb-modal-title">Admin Panel</div>
418 <div className="tb-modal-sub">Login ➜ Environments ➜ Generate token</div>
419 </div>
420 <button className="tb-btn" onClick={() => setOpen(false)}>Close</button>
421 </div>
422
423 <div className="tb-grid">
424 {/* 1) LOGIN */}
425 {!adminSession ? (
426 <div className="tb-card">
427 <div className="tb-card-head">
428 <div className="tb-card-title">Step 1: Admin login</div>
429 </div>
430
431 <div className="tb-row">
432 <input
433 className="tb-input"
434 type="password"
435 value={adminKey}
436 onChange={(e) => setAdminKey(e.target.value)}
437 placeholder="Enter admin key"
438 />
439 <button className="tb-btn primary" onClick={login} disabled={busy}>
440 {busy ? "Logging in…" : "Login"}
441 </button>
442 <div className="tb-help">
443 Admin key е само за логирање. После тоа користиме <b>session token</b>.
444 </div>
445 </div>
446 </div>
447 ) : (
448 <>
449 {/* 2) ENV SELECT */}
450 <div className="tb-card">
451 <div className="tb-card-head">
452 <div className="tb-card-title">Step 2: Choose environment</div>
453 <button className="tb-btn" onClick={logout}>Logout</button>
454 </div>
455
456 <div className="tb-row">
457 <select className="tb-select" value={selectedEnv} onChange={(e) => changeEnv(e.target.value)}>
458 {envs.map((e) => (
459 <option key={e} value={e}>{e}</option>
460 ))}
461 </select>
462 <div className="tb-help">
463 Овој env ќе се користи за dashboard филтрирање + token generation.
464 </div>
465 </div>
466 </div>
467
468 {/* 3) CREATE ENV */}
469 <div className="tb-card">
470 <div className="tb-card-head">
471 <div className="tb-card-title">Create new environment</div>
472 </div>
473
474 <div className="tb-row">
475 <input
476 className="tb-input"
477 value={newEnvName}
478 onChange={(e) => setNewEnvName(e.target.value)}
479 placeholder="e.g. school-lab / staging"
480 />
481 <button className="tb-btn" onClick={createEnvironment} disabled={busy}>
482 {busy ? "Creating…" : "Create"}
483 </button>
484 </div>
485 </div>
486
487 {/* 4) TOKEN */}
488 <div className="tb-card">
489 <div className="tb-card-head">
490 <div className="tb-card-title">Step 3: Generate token (for clients)</div>
491 </div>
492
493 <div className="tb-row">
494 <button className="tb-btn primary" onClick={generateToken} disabled={busy}>
495 {busy ? "Generating…" : `Generate token for "${selectedEnv}"`}
496 </button>
497
498 {token && (
499 <>
500 <div className="tb-token">
501 <div>
502 <div style={{ fontWeight: 900, color: "rgba(255,255,255,0.92)" }}>Token</div>
503 <div className="tb-mono">
504 {token.slice(0, 12)}…{token.slice(-10)}
505 </div>
506 </div>
507 <button className="tb-btn" onClick={copyToken}>Copy</button>
508 </div>
509
510 <div className="tb-help">
511 Клиентот мора да праќа header: <b>X-Env-Token</b> со овој token на <b>/receive</b>.
512 </div>
513 </>
514 )}
515 </div>
516 </div>
517 </>
518 )}
519
520 {error && (
521 <div className="tb-card tb-error">
522 <div style={{ fontWeight: 900 }}>Error</div>
523 <div style={{ marginTop: 6 }}>{error}</div>
524 </div>
525 )}
526 </div>
527 </div>
528 </div>
529 )}
530 </>
531 );
532}
Note: See TracBrowser for help on using the repository browser.