source: lan-frontend/src/components/TopBar.jsx@ 505f39a

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

Added OAuth/prototype

  • Property mode set to 100644
File size: 14.4 KB
Line 
1import React, { useEffect, useMemo, useState } from "react";
2import logo from "../assets/ChatGPT_Image_Jan_21__2026__01_20_04_PM-removebg-preview (1).png";
3
4export default function TopBar({
5 onOpenAI,
6 onRefresh,
7 lastUpdated,
8 onEnvChange,
9 selectedEnv: selectedEnvProp, // optional (ако сакаш да го контролира App)
10 me,
11 onLoggedOut, // optional callback (App loadMe)
12}) {
13 const [open, setOpen] = useState(false);
14
15 // env + tokens
16 const [envs, setEnvs] = useState(["default"]);
17 const [selectedEnv, setSelectedEnv] = useState(selectedEnvProp || "default");
18 const [newEnvName, setNewEnvName] = useState("");
19 const [token, setToken] = useState("");
20
21 const [busy, setBusy] = useState(false);
22 const [error, setError] = useState("");
23
24
25 // keep in sync ако App праќа selectedEnv prop
26 useEffect(() => {
27 if (selectedEnvProp && selectedEnvProp !== selectedEnv) {
28 setSelectedEnv(selectedEnvProp);
29 }
30 // eslint-disable-next-line react-hooks/exhaustive-deps
31 }, [selectedEnvProp]);
32
33 async function loadEnvironments() {
34 const r = await fetch("/api/admin/environments", {
35 credentials: "include",
36 });
37 const data = await r.json();
38 if (!r.ok) throw new Error(data?.error || `HTTP ${r.status}`);
39
40 const list = data.environments?.length ? data.environments : ["default"];
41 setEnvs(list);
42
43 if (!list.includes(selectedEnv)) {
44 setSelectedEnv(list[0]);
45 onEnvChange?.(list[0]);
46 }
47 }
48
49 async function createEnvironment() {
50 setError("");
51 setToken("");
52
53 if (!newEnvName.trim()) {
54 setError("Внеси име за environment.");
55 return;
56 }
57
58 setBusy(true);
59 try {
60 const r = await fetch("/api/admin/environments", {
61 method: "POST",
62 headers: { "Content-Type": "application/json" },
63 credentials: "include",
64 body: JSON.stringify({ name: newEnvName.trim() }),
65 });
66
67 const data = await r.json();
68 if (!r.ok) throw new Error(data?.error || `HTTP ${r.status}`);
69
70 await loadEnvironments();
71 setSelectedEnv(newEnvName.trim());
72 onEnvChange?.(newEnvName.trim());
73 setNewEnvName("");
74 } catch (e) {
75 setError(String(e.message || e));
76 } finally {
77 setBusy(false);
78 }
79 }
80
81 async function generateToken() {
82 setError("");
83 setToken("");
84 if (!selectedEnv) {
85 setError("Избери environment.");
86 return;
87 }
88
89 setBusy(true);
90 try {
91 const r = await fetch("/api/admin/tokens", {
92 method: "POST",
93 headers: { "Content-Type": "application/json" },
94 credentials: "include",
95 body: JSON.stringify({ env: selectedEnv }),
96 });
97
98 const data = await r.json();
99 if (!r.ok) throw new Error(data?.error || `HTTP ${r.status}`);
100
101 setToken(data.token || "");
102 } catch (e) {
103 setError(String(e.message || e));
104 } finally {
105 setBusy(false);
106 }
107 }
108
109 async function copyToken() {
110 if (!token) return;
111 await navigator.clipboard.writeText(token);
112 }
113
114 function changeEnv(val) {
115 setSelectedEnv(val);
116 setToken("");
117 onEnvChange?.(val);
118 }
119
120 async function logout() {
121 try {
122 await fetch("/api/auth/logout", { method: "POST", credentials: "include" });
123 } catch {}
124 onLoggedOut?.();
125 // reset local admin modal state
126 setOpen(false);
127 setToken("");
128 setError("");
129 }
130
131 const prettyTime = useMemo(() => {
132 if (!lastUpdated) return "Loading…";
133 try {
134 return `Updated ${new Date(lastUpdated).toLocaleTimeString()}`;
135 } catch {
136 return "Updated";
137 }
138 }, [lastUpdated]);
139
140 return (
141 <>
142 <style>{`
143 .tb-wrap{ position: sticky; top: 0; z-index: 20; backdrop-filter: blur(10px);
144 background: linear-gradient(180deg, rgba(10,16,32,0.92), rgba(10,16,32,0.75));
145 border-bottom: 1px solid rgba(96,165,250,0.18); }
146 .tb-inner{ max-width: 1200px; margin: 0 auto; padding: 14px 18px;
147 display: flex; align-items: center; justify-content: space-between; gap: 14px; }
148 .tb-brand{ display:flex; align-items:center; gap: 12px; min-width: 260px; }
149 .tb-badge{ width: 44px; height: 44px; border-radius: 14px; display:flex; align-items:center; justify-content:center;
150 background: radial-gradient(120px 80px at 30% 20%, rgba(96,165,250,0.55), transparent 60%),
151 linear-gradient(135deg, rgba(30,64,175,0.75), rgba(15,23,42,0.85));
152 border: 1px solid rgba(96,165,250,0.25); box-shadow: 0 10px 25px rgba(0,0,0,0.25); }
153 .tb-title{ margin:0; font-size: 16px; font-weight: 900; letter-spacing: 0.4px; color: rgba(255,255,255,0.95); line-height: 1.2; }
154 .tb-sub{ margin: 2px 0 0 0; font-size: 12px; color: rgba(191,219,254,0.85); }
155 .tb-sub span{ color: rgba(96,165,250,0.95); font-weight: 800; }
156 .tb-actions{ display:flex; align-items:center; gap: 10px; flex-wrap: wrap; justify-content: flex-end; }
157 .tb-pill{ display:inline-flex; align-items:center; gap: 8px; padding: 8px 12px; border-radius: 999px;
158 background: rgba(30,41,59,0.65); border: 1px solid rgba(96,165,250,0.22);
159 color: rgba(255,255,255,0.9); font-weight: 800; font-size: 12px; box-shadow: inset 0 1px 0 rgba(255,255,255,0.06); }
160 .tb-pill .dot{ width: 8px; height: 8px; border-radius: 999px; background: rgba(96,165,250,0.95);
161 box-shadow: 0 0 0 3px rgba(96,165,250,0.15); }
162 .tb-btn{ appearance: none; border: 1px solid rgba(96,165,250,0.22); background: rgba(15,23,42,0.55);
163 color: rgba(255,255,255,0.9); padding: 9px 12px; border-radius: 12px; font-weight: 800;
164 font-size: 13px; cursor: pointer; transition: transform .12s ease, background .12s ease, border-color .12s ease, opacity .12s ease;
165 box-shadow: 0 8px 20px rgba(0,0,0,0.18); }
166 .tb-btn:hover{ transform: translateY(-1px); background: rgba(30,41,59,0.65); border-color: rgba(96,165,250,0.38); }
167 .tb-btn:disabled{ opacity: .6; cursor: not-allowed; transform: none; }
168 .tb-btn.primary{ background: linear-gradient(135deg, rgba(37,99,235,0.95), rgba(56,189,248,0.7));
169 border-color: rgba(147,197,253,0.35); color: rgba(5,12,24,0.95); }
170 .tb-btn.primary:hover{ background: linear-gradient(135deg, rgba(59,130,246,0.95), rgba(34,211,238,0.75)); }
171
172 .tb-backdrop{ position: fixed; inset: 0; background: rgba(2,6,23,0.72); backdrop-filter: blur(6px);
173 display:flex; align-items: center; justify-content: center; padding: 20px; z-index: 50; }
174 .tb-modal{ width: min(860px, 96vw); border-radius: 18px;
175 background: radial-gradient(900px 420px at 20% 0%, rgba(96,165,250,0.18), transparent 60%),
176 radial-gradient(700px 350px at 80% 10%, rgba(34,211,238,0.10), transparent 55%),
177 rgba(10,16,32,0.92);
178 border: 1px solid rgba(96,165,250,0.22); box-shadow: 0 30px 80px rgba(0,0,0,0.45); overflow: hidden; }
179 .tb-modal-head{ padding: 16px 18px; display:flex; align-items: center; justify-content: space-between; gap: 10px;
180 border-bottom: 1px solid rgba(96,165,250,0.18); }
181 .tb-modal-title{ color: rgba(255,255,255,0.96); font-weight: 900; letter-spacing: .3px; }
182 .tb-modal-sub{ color: rgba(191,219,254,0.8); font-size: 12px; margin-top: 2px; }
183 .tb-grid{ padding: 16px; display:grid; gap: 12px; }
184 .tb-card{ border-radius: 16px; background: rgba(15,23,42,0.55); border: 1px solid rgba(96,165,250,0.18); padding: 14px; }
185 .tb-card-head{ display:flex; align-items:center; justify-content: space-between; margin-bottom: 10px; gap: 10px; }
186 .tb-card-title{ color: rgba(255,255,255,0.92); font-weight: 900; }
187 .tb-help{ color: rgba(148,163,184,0.95); font-size: 12px; margin-top: 8px; line-height: 1.35; }
188 .tb-input, .tb-select{ width: 100%; padding: 10px 12px; border-radius: 12px;
189 border: 1px solid rgba(96,165,250,0.18); background: rgba(2,6,23,0.35); color: rgba(255,255,255,0.92);
190 outline: none; font-weight: 700; }
191 .tb-input::placeholder{ color: rgba(148,163,184,0.75); }
192 .tb-row{ display:grid; gap: 10px; }
193 .tb-token{ display:flex; align-items:center; justify-content: space-between; gap: 10px; padding: 10px 12px;
194 border-radius: 14px; background: rgba(2,6,23,0.38); border: 1px dashed rgba(147,197,253,0.32);
195 color: rgba(255,255,255,0.92); }
196 .tb-mono{ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
197 font-weight: 800; color: rgba(191,219,254,0.95); }
198 .tb-error{ border-color: rgba(239,68,68,0.45) !important; background: rgba(127,29,29,0.15) !important;
199 color: rgba(254,202,202,0.95); }
200 `}</style>
201
202 <div className="tb-wrap">
203 <div className="tb-inner">
204 <div className="tb-brand">
205 <div className="tb-badge">🛡️</div>
206 <div>
207 <h1 className="tb-title">LAN Security Monitor</h1>
208 <p className="tb-sub">
209 Sysmon Edition • <span>{prettyTime}</span>
210 </p>
211 {me?.email && (
212 <p className="tb-sub" style={{ marginTop: 4, opacity: 0.9 }}>
213 Signed in as <span>{me.email}</span>
214 {me.role ? ` • ${me.role}` : ""}
215 </p>
216 )}
217 </div>
218 </div>
219 <div>
220 <img
221 src={logo}
222 alt="NETIntel logo"
223 className="netintel-logo"
224 width={150}
225 style={{marginTop: 10 }}
226 />
227 </div>
228
229
230
231 <div className="tb-actions">
232 <span className="tb-pill">
233 <span className="dot" />
234 Env: {selectedEnv}
235 </span>
236 <button className="tb-btn" onClick={onRefresh} disabled={busy}>
237 Refresh
238 </button>
239 <button
240 className="tb-btn"
241 onClick={async () => {
242 setOpen(true);
243 setError("");
244 setToken("");
245 try {
246 await loadEnvironments();
247 } catch (e) {
248 setError(String(e.message || e));
249 }
250 }}
251 >
252 Admin
253 </button>
254 <button className="tb-btn primary" onClick={onOpenAI}>
255 AI Assistant
256 </button>
257 </div>
258 </div>
259 </div>
260
261 {open && (
262 <div className="tb-backdrop" onMouseDown={() => setOpen(false)}>
263 <div className="tb-modal" onMouseDown={(e) => e.stopPropagation()}>
264 <div className="tb-modal-head">
265 <div>
266 <div className="tb-modal-title">Tenant Admin</div>
267 <div className="tb-modal-sub">
268 Environments ➜ Generate token (clients send X-Env-Token to /receive)
269 </div>
270 </div>
271 <div style={{ display: "flex", gap: 10 }}>
272 <button className="tb-btn" onClick={logout}>
273 Logout
274 </button>
275 <button className="tb-btn" onClick={() => setOpen(false)}>
276 Close
277 </button>
278 </div>
279 </div>
280
281 <div className="tb-grid">
282 <div className="tb-card">
283 <div className="tb-card-head">
284 <div className="tb-card-title">Choose environment</div>
285 </div>
286 <div className="tb-row">
287 <select
288 className="tb-select"
289 value={selectedEnv}
290 onChange={(e) => changeEnv(e.target.value)}
291 >
292 {envs.map((e) => (
293 <option key={e} value={e}>
294 {e}
295 </option>
296 ))}
297 </select>
298 <div className="tb-help">
299 Овој env ќе се користи за dashboard филтрирање + token generation.
300 </div>
301 </div>
302 </div>
303
304 <div className="tb-card">
305 <div className="tb-card-head">
306 <div className="tb-card-title">Create new environment</div>
307 </div>
308 <div className="tb-row">
309 <input
310 className="tb-input"
311 value={newEnvName}
312 onChange={(e) => setNewEnvName(e.target.value)}
313 placeholder="e.g. school-lab / staging"
314 />
315 <button className="tb-btn" onClick={createEnvironment} disabled={busy}>
316 {busy ? "Creating…" : "Create"}
317 </button>
318 </div>
319 </div>
320
321 <div className="tb-card">
322 <div className="tb-card-head">
323 <div className="tb-card-title">Generate token (for clients)</div>
324 </div>
325
326 <div className="tb-row">
327 <button className="tb-btn primary" onClick={generateToken} disabled={busy}>
328 {busy ? "Generating…" : `Generate token for "${selectedEnv}"`}
329 </button>
330
331 {token && (
332 <>
333 <div className="tb-token">
334 <div>
335 <div style={{ fontWeight: 900, color: "rgba(255,255,255,0.92)" }}>
336 Token
337 </div>
338 <div className="tb-mono">
339 {token.slice(0, 12)}…{token.slice(-10)}
340 </div>
341 </div>
342 <button className="tb-btn" onClick={copyToken}>
343 Copy
344 </button>
345 </div>
346
347 <div className="tb-help">
348 Клиентот мора да праќа header: <b>X-Env-Token</b> со овој token на <b>/receive</b>.
349 </div>
350 </>
351 )}
352 </div>
353 </div>
354
355 {error && (
356 <div className="tb-card tb-error">
357 <div style={{ fontWeight: 900 }}>Error</div>
358 <div style={{ marginTop: 6 }}>{error}</div>
359 </div>
360 )}
361 </div>
362 </div>
363 </div>
364 )}
365 </>
366 );
367}
Note: See TracBrowser for help on using the repository browser.