1use axum::{
2 extract::State,
3 response::{Html, IntoResponse, Json},
4 routing::{get, post},
5 Router,
6};
7use serde::{Deserialize, Serialize};
8use std::collections::{HashMap, HashSet};
9use std::sync::Arc;
10
11use crate::{error::ConsoleError, state::ConsoleState};
12
13const TEAM_MANAGEMENT_HTML: &str = r#"<!DOCTYPE html>
15<html lang="pt-BR">
16<head>
17 <meta charset="UTF-8">
18 <meta name="viewport" content="width=device-width, initial-scale=1.0">
19 <title>Team Management & RBAC - AVL Console</title>
20 <style>
21 * { margin: 0; padding: 0; box-sizing: border-box; }
22 body {
23 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
24 background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
25 min-height: 100vh;
26 padding: 20px;
27 }
28 .container {
29 max-width: 1600px;
30 margin: 0 auto;
31 background: white;
32 border-radius: 20px;
33 padding: 40px;
34 box-shadow: 0 20px 60px rgba(0,0,0,0.3);
35 }
36 h1 {
37 color: #f5576c;
38 margin-bottom: 10px;
39 font-size: 36px;
40 }
41 .subtitle {
42 color: #666;
43 margin-bottom: 30px;
44 font-size: 16px;
45 }
46 .tabs {
47 display: flex;
48 gap: 10px;
49 margin-bottom: 30px;
50 border-bottom: 2px solid #e9ecef;
51 }
52 .tab {
53 padding: 15px 30px;
54 background: none;
55 border: none;
56 border-bottom: 3px solid transparent;
57 cursor: pointer;
58 font-weight: 600;
59 color: #6c757d;
60 transition: all 0.2s;
61 font-size: 15px;
62 }
63 .tab.active {
64 color: #f5576c;
65 border-bottom-color: #f5576c;
66 }
67 .tab:hover {
68 color: #f5576c;
69 }
70 .tab-content {
71 display: none;
72 }
73 .tab-content.active {
74 display: block;
75 }
76 .action-bar {
77 display: flex;
78 justify-content: space-between;
79 align-items: center;
80 margin-bottom: 25px;
81 }
82 .search-box {
83 flex: 1;
84 max-width: 400px;
85 position: relative;
86 }
87 .search-box input {
88 width: 100%;
89 padding: 12px 40px 12px 15px;
90 border: 2px solid #dee2e6;
91 border-radius: 10px;
92 font-size: 14px;
93 }
94 .search-box input:focus {
95 outline: none;
96 border-color: #f5576c;
97 }
98 .search-icon {
99 position: absolute;
100 right: 15px;
101 top: 50%;
102 transform: translateY(-50%);
103 color: #adb5bd;
104 }
105 .btn {
106 padding: 12px 24px;
107 border: none;
108 border-radius: 10px;
109 font-weight: 600;
110 cursor: pointer;
111 transition: all 0.2s;
112 font-size: 14px;
113 }
114 .btn-primary {
115 background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
116 color: white;
117 }
118 .btn-primary:hover {
119 transform: translateY(-2px);
120 box-shadow: 0 6px 20px rgba(245, 87, 108, 0.4);
121 }
122 .btn-secondary {
123 background: white;
124 color: #f5576c;
125 border: 2px solid #f5576c;
126 }
127 .btn-secondary:hover {
128 background: #fff5f7;
129 }
130 .team-grid {
131 display: grid;
132 grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
133 gap: 20px;
134 }
135 .team-card {
136 background: white;
137 border: 2px solid #e9ecef;
138 border-radius: 16px;
139 padding: 25px;
140 transition: all 0.2s;
141 cursor: pointer;
142 }
143 .team-card:hover {
144 border-color: #f5576c;
145 box-shadow: 0 8px 24px rgba(245, 87, 108, 0.15);
146 transform: translateY(-4px);
147 }
148 .team-header {
149 display: flex;
150 align-items: center;
151 gap: 15px;
152 margin-bottom: 15px;
153 }
154 .team-icon {
155 width: 50px;
156 height: 50px;
157 border-radius: 12px;
158 background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
159 display: flex;
160 align-items: center;
161 justify-content: center;
162 font-size: 24px;
163 }
164 .team-info {
165 flex: 1;
166 }
167 .team-name {
168 font-weight: 600;
169 font-size: 18px;
170 color: #495057;
171 }
172 .team-members {
173 font-size: 13px;
174 color: #6c757d;
175 }
176 .team-description {
177 color: #6c757d;
178 font-size: 14px;
179 margin-bottom: 15px;
180 line-height: 1.5;
181 }
182 .team-roles {
183 display: flex;
184 flex-wrap: wrap;
185 gap: 6px;
186 }
187 .role-badge {
188 padding: 4px 10px;
189 border-radius: 6px;
190 font-size: 11px;
191 font-weight: 600;
192 background: #f8f9fa;
193 color: #495057;
194 }
195 .users-table {
196 width: 100%;
197 border-collapse: collapse;
198 background: white;
199 }
200 .users-table thead {
201 background: #f8f9fa;
202 }
203 .users-table th {
204 padding: 15px;
205 text-align: left;
206 font-size: 13px;
207 font-weight: 600;
208 color: #495057;
209 text-transform: uppercase;
210 letter-spacing: 0.5px;
211 }
212 .users-table td {
213 padding: 15px;
214 border-bottom: 1px solid #e9ecef;
215 font-size: 14px;
216 }
217 .users-table tbody tr:hover {
218 background: #f8f9fa;
219 }
220 .user-avatar {
221 width: 40px;
222 height: 40px;
223 border-radius: 50%;
224 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
225 display: flex;
226 align-items: center;
227 justify-content: center;
228 color: white;
229 font-weight: 600;
230 font-size: 16px;
231 }
232 .user-info {
233 display: flex;
234 align-items: center;
235 gap: 12px;
236 }
237 .user-details {
238 display: flex;
239 flex-direction: column;
240 }
241 .user-name {
242 font-weight: 600;
243 color: #495057;
244 }
245 .user-email {
246 font-size: 12px;
247 color: #6c757d;
248 }
249 .status-badge {
250 padding: 6px 12px;
251 border-radius: 8px;
252 font-size: 12px;
253 font-weight: 600;
254 }
255 .status-active {
256 background: #d4edda;
257 color: #155724;
258 }
259 .status-inactive {
260 background: #f8d7da;
261 color: #721c24;
262 }
263 .permissions-grid {
264 display: grid;
265 grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
266 gap: 15px;
267 }
268 .permission-card {
269 background: #f8f9fa;
270 border: 2px solid #e9ecef;
271 border-radius: 12px;
272 padding: 20px;
273 }
274 .permission-card.enabled {
275 border-color: #f5576c;
276 background: #fff5f7;
277 }
278 .permission-title {
279 font-weight: 600;
280 color: #495057;
281 margin-bottom: 8px;
282 display: flex;
283 align-items: center;
284 gap: 10px;
285 }
286 .permission-desc {
287 font-size: 13px;
288 color: #6c757d;
289 margin-bottom: 12px;
290 }
291 .toggle-switch {
292 position: relative;
293 width: 50px;
294 height: 26px;
295 }
296 .toggle-switch input {
297 opacity: 0;
298 width: 0;
299 height: 0;
300 }
301 .toggle-slider {
302 position: absolute;
303 cursor: pointer;
304 top: 0;
305 left: 0;
306 right: 0;
307 bottom: 0;
308 background-color: #ccc;
309 transition: 0.4s;
310 border-radius: 26px;
311 }
312 .toggle-slider:before {
313 position: absolute;
314 content: "";
315 height: 18px;
316 width: 18px;
317 left: 4px;
318 bottom: 4px;
319 background-color: white;
320 transition: 0.4s;
321 border-radius: 50%;
322 }
323 .toggle-switch input:checked + .toggle-slider {
324 background-color: #f5576c;
325 }
326 .toggle-switch input:checked + .toggle-slider:before {
327 transform: translateX(24px);
328 }
329 .modal {
330 display: none;
331 position: fixed;
332 top: 0;
333 left: 0;
334 right: 0;
335 bottom: 0;
336 background: rgba(0,0,0,0.6);
337 align-items: center;
338 justify-content: center;
339 z-index: 1000;
340 }
341 .modal.active {
342 display: flex;
343 }
344 .modal-content {
345 background: white;
346 border-radius: 20px;
347 padding: 40px;
348 max-width: 600px;
349 width: 90%;
350 max-height: 90vh;
351 overflow-y: auto;
352 }
353 .modal-header {
354 margin-bottom: 25px;
355 }
356 .modal-title {
357 font-size: 24px;
358 font-weight: 600;
359 color: #495057;
360 }
361 .form-group {
362 margin-bottom: 20px;
363 }
364 .form-group label {
365 display: block;
366 font-weight: 600;
367 color: #495057;
368 margin-bottom: 8px;
369 font-size: 14px;
370 }
371 .form-group input,
372 .form-group select,
373 .form-group textarea {
374 width: 100%;
375 padding: 12px;
376 border: 2px solid #dee2e6;
377 border-radius: 8px;
378 font-size: 14px;
379 }
380 .form-group input:focus,
381 .form-group select:focus,
382 .form-group textarea:focus {
383 outline: none;
384 border-color: #f5576c;
385 }
386 .form-actions {
387 display: flex;
388 gap: 10px;
389 justify-content: flex-end;
390 margin-top: 30px;
391 }
392 .audit-log {
393 background: #f8f9fa;
394 border-radius: 12px;
395 padding: 20px;
396 }
397 .log-entry {
398 padding: 15px;
399 border-bottom: 1px solid #e9ecef;
400 display: flex;
401 align-items: start;
402 gap: 15px;
403 }
404 .log-entry:last-child {
405 border-bottom: none;
406 }
407 .log-icon {
408 font-size: 24px;
409 }
410 .log-content {
411 flex: 1;
412 }
413 .log-action {
414 font-weight: 600;
415 color: #495057;
416 margin-bottom: 4px;
417 }
418 .log-details {
419 font-size: 13px;
420 color: #6c757d;
421 }
422 .log-time {
423 font-size: 12px;
424 color: #adb5bd;
425 }
426 </style>
427</head>
428<body>
429 <div class="container">
430 <h1>👥 Team Management & RBAC</h1>
431 <p class="subtitle">Enterprise-grade access control and team collaboration</p>
432
433 <div class="tabs">
434 <button class="tab active" onclick="switchTab('teams')">Teams</button>
435 <button class="tab" onclick="switchTab('users')">Users</button>
436 <button class="tab" onclick="switchTab('roles')">Roles & Permissions</button>
437 <button class="tab" onclick="switchTab('audit')">Audit Log</button>
438 </div>
439
440 <!-- Teams Tab -->
441 <div id="teams-tab" class="tab-content active">
442 <div class="action-bar">
443 <div class="search-box">
444 <input type="text" placeholder="Search teams..." onkeyup="searchTeams(this.value)">
445 <span class="search-icon">🔍</span>
446 </div>
447 <button class="btn btn-primary" onclick="openModal('newTeam')">➕ New Team</button>
448 </div>
449 <div class="team-grid" id="teamsGrid"></div>
450 </div>
451
452 <!-- Users Tab -->
453 <div id="users-tab" class="tab-content">
454 <div class="action-bar">
455 <div class="search-box">
456 <input type="text" placeholder="Search users..." onkeyup="searchUsers(this.value)">
457 <span class="search-icon">🔍</span>
458 </div>
459 <button class="btn btn-primary" onclick="openModal('newUser')">➕ Invite User</button>
460 </div>
461 <table class="users-table">
462 <thead>
463 <tr>
464 <th>User</th>
465 <th>Role</th>
466 <th>Teams</th>
467 <th>Status</th>
468 <th>Last Active</th>
469 <th>Actions</th>
470 </tr>
471 </thead>
472 <tbody id="usersTable"></tbody>
473 </table>
474 </div>
475
476 <!-- Roles Tab -->
477 <div id="roles-tab" class="tab-content">
478 <div class="action-bar">
479 <h2 style="color: #495057; margin: 0;">Configure Role Permissions</h2>
480 <select id="roleSelect" onchange="loadPermissions(this.value)" style="padding: 10px; border-radius: 8px; border: 2px solid #dee2e6;">
481 <option value="admin">Admin</option>
482 <option value="developer">Developer</option>
483 <option value="viewer">Viewer</option>
484 </select>
485 </div>
486 <div class="permissions-grid" id="permissionsGrid"></div>
487 </div>
488
489 <!-- Audit Log Tab -->
490 <div id="audit-tab" class="tab-content">
491 <div class="audit-log" id="auditLog"></div>
492 </div>
493 </div>
494
495 <!-- Modals -->
496 <div id="newTeamModal" class="modal">
497 <div class="modal-content">
498 <div class="modal-header">
499 <h2 class="modal-title">Create New Team</h2>
500 </div>
501 <form onsubmit="createTeam(event)">
502 <div class="form-group">
503 <label>Team Name</label>
504 <input type="text" name="name" required placeholder="Engineering, Design, Marketing...">
505 </div>
506 <div class="form-group">
507 <label>Description</label>
508 <textarea name="description" rows="3" placeholder="Brief description of the team's purpose"></textarea>
509 </div>
510 <div class="form-group">
511 <label>Default Role</label>
512 <select name="default_role">
513 <option value="developer">Developer</option>
514 <option value="viewer">Viewer</option>
515 </select>
516 </div>
517 <div class="form-actions">
518 <button type="button" class="btn btn-secondary" onclick="closeModal('newTeam')">Cancel</button>
519 <button type="submit" class="btn btn-primary">Create Team</button>
520 </div>
521 </form>
522 </div>
523 </div>
524
525 <div id="newUserModal" class="modal">
526 <div class="modal-content">
527 <div class="modal-header">
528 <h2 class="modal-title">Invite User</h2>
529 </div>
530 <form onsubmit="inviteUser(event)">
531 <div class="form-group">
532 <label>Email</label>
533 <input type="email" name="email" required placeholder="user@company.com">
534 </div>
535 <div class="form-group">
536 <label>Role</label>
537 <select name="role">
538 <option value="developer">Developer</option>
539 <option value="viewer">Viewer</option>
540 <option value="admin">Admin</option>
541 </select>
542 </div>
543 <div class="form-group">
544 <label>Teams</label>
545 <select name="teams" multiple style="height: 100px;">
546 <option value="team_1">Engineering</option>
547 <option value="team_2">Design</option>
548 <option value="team_3">Marketing</option>
549 </select>
550 </div>
551 <div class="form-actions">
552 <button type="button" class="btn btn-secondary" onclick="closeModal('newUser')">Cancel</button>
553 <button type="submit" class="btn btn-primary">Send Invitation</button>
554 </div>
555 </form>
556 </div>
557 </div>
558
559 <script>
560 let currentTab = 'teams';
561
562 async function loadTeams() {
563 try {
564 const response = await fetch('/teams/list');
565 const data = await response.json();
566 renderTeams(data.teams);
567 } catch (error) {
568 console.error('Failed to load teams:', error);
569 }
570 }
571
572 async function loadUsers() {
573 try {
574 const response = await fetch('/teams/users');
575 const data = await response.json();
576 renderUsers(data.users);
577 } catch (error) {
578 console.error('Failed to load users:', error);
579 }
580 }
581
582 async function loadAuditLog() {
583 try {
584 const response = await fetch('/teams/audit');
585 const data = await response.json();
586 renderAuditLog(data.logs);
587 } catch (error) {
588 console.error('Failed to load audit log:', error);
589 }
590 }
591
592 function renderTeams(teams) {
593 const grid = document.getElementById('teamsGrid');
594 grid.innerHTML = teams.map(team => `
595 <div class="team-card" onclick="viewTeam('${team.id}')">
596 <div class="team-header">
597 <div class="team-icon">${team.icon}</div>
598 <div class="team-info">
599 <div class="team-name">${team.name}</div>
600 <div class="team-members">${team.member_count} members</div>
601 </div>
602 </div>
603 <div class="team-description">${team.description}</div>
604 <div class="team-roles">
605 ${team.roles.map(role => `<span class="role-badge">${role}</span>`).join('')}
606 </div>
607 </div>
608 `).join('');
609 }
610
611 function renderUsers(users) {
612 const table = document.getElementById('usersTable');
613 table.innerHTML = users.map(user => `
614 <tr>
615 <td>
616 <div class="user-info">
617 <div class="user-avatar">${user.name.charAt(0)}</div>
618 <div class="user-details">
619 <div class="user-name">${user.name}</div>
620 <div class="user-email">${user.email}</div>
621 </div>
622 </div>
623 </td>
624 <td><span class="role-badge">${user.role}</span></td>
625 <td>${user.teams.join(', ')}</td>
626 <td><span class="status-badge status-${user.status}">${user.status}</span></td>
627 <td>${user.last_active}</td>
628 <td>
629 <button class="btn-secondary" style="padding: 6px 12px; font-size: 12px;" onclick="editUser('${user.id}')">Edit</button>
630 </td>
631 </tr>
632 `).join('');
633 }
634
635 function renderAuditLog(logs) {
636 const logDiv = document.getElementById('auditLog');
637 logDiv.innerHTML = logs.map(log => `
638 <div class="log-entry">
639 <div class="log-icon">${log.icon}</div>
640 <div class="log-content">
641 <div class="log-action">${log.action}</div>
642 <div class="log-details">${log.details}</div>
643 <div class="log-time">${log.time}</div>
644 </div>
645 </div>
646 `).join('');
647 }
648
649 function loadPermissions(role) {
650 const permissions = {
651 admin: [
652 {name: 'Manage Users', desc: 'Create, edit, and delete users', enabled: true},
653 {name: 'Manage Teams', desc: 'Create and configure teams', enabled: true},
654 {name: 'View Billing', desc: 'Access billing and invoices', enabled: true},
655 {name: 'Manage Database', desc: 'Full database access', enabled: true},
656 {name: 'Manage Storage', desc: 'Full storage access', enabled: true},
657 {name: 'View Logs', desc: 'Access system logs', enabled: true},
658 ],
659 developer: [
660 {name: 'Manage Users', desc: 'Create, edit, and delete users', enabled: false},
661 {name: 'Manage Teams', desc: 'Create and configure teams', enabled: false},
662 {name: 'View Billing', desc: 'Access billing and invoices', enabled: false},
663 {name: 'Manage Database', desc: 'Full database access', enabled: true},
664 {name: 'Manage Storage', desc: 'Full storage access', enabled: true},
665 {name: 'View Logs', desc: 'Access system logs', enabled: true},
666 ],
667 viewer: [
668 {name: 'Manage Users', desc: 'Create, edit, and delete users', enabled: false},
669 {name: 'Manage Teams', desc: 'Create and configure teams', enabled: false},
670 {name: 'View Billing', desc: 'Access billing and invoices', enabled: false},
671 {name: 'Manage Database', desc: 'Full database access', enabled: false},
672 {name: 'Manage Storage', desc: 'Full storage access', enabled: false},
673 {name: 'View Logs', desc: 'Access system logs', enabled: true},
674 ]
675 };
676
677 const grid = document.getElementById('permissionsGrid');
678 grid.innerHTML = permissions[role].map((perm, idx) => `
679 <div class="permission-card ${perm.enabled ? 'enabled' : ''}">
680 <div class="permission-title">
681 <span>${perm.name}</span>
682 <label class="toggle-switch" style="margin-left: auto;">
683 <input type="checkbox" ${perm.enabled ? 'checked' : ''} onchange="togglePermission('${role}', ${idx}, this.checked)">
684 <span class="toggle-slider"></span>
685 </label>
686 </div>
687 <div class="permission-desc">${perm.desc}</div>
688 </div>
689 `).join('');
690 }
691
692 function switchTab(tab) {
693 currentTab = tab;
694 document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
695 document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
696 event.target.classList.add('active');
697 document.getElementById(`${tab}-tab`).classList.add('active');
698
699 if (tab === 'teams') loadTeams();
700 else if (tab === 'users') loadUsers();
701 else if (tab === 'roles') loadPermissions('admin');
702 else if (tab === 'audit') loadAuditLog();
703 }
704
705 function openModal(modal) {
706 document.getElementById(`${modal}Modal`).classList.add('active');
707 }
708
709 function closeModal(modal) {
710 document.getElementById(`${modal}Modal`).classList.remove('active');
711 }
712
713 async function createTeam(e) {
714 e.preventDefault();
715 const formData = new FormData(e.target);
716 const data = Object.fromEntries(formData);
717
718 try {
719 await fetch('/teams/create', {
720 method: 'POST',
721 headers: {'Content-Type': 'application/json'},
722 body: JSON.stringify(data)
723 });
724 closeModal('newTeam');
725 loadTeams();
726 } catch (error) {
727 console.error('Failed to create team:', error);
728 }
729 }
730
731 async function inviteUser(e) {
732 e.preventDefault();
733 const formData = new FormData(e.target);
734 const data = Object.fromEntries(formData);
735
736 try {
737 await fetch('/teams/invite', {
738 method: 'POST',
739 headers: {'Content-Type': 'application/json'},
740 body: JSON.stringify(data)
741 });
742 closeModal('newUser');
743 loadUsers();
744 } catch (error) {
745 console.error('Failed to invite user:', error);
746 }
747 }
748
749 function searchTeams(query) {
750 // Implement search
751 }
752
753 function searchUsers(query) {
754 // Implement search
755 }
756
757 function viewTeam(id) {
758 // Implement team details view
759 }
760
761 function editUser(id) {
762 // Implement user edit
763 }
764
765 function togglePermission(role, idx, enabled) {
766 // Implement permission toggle
767 console.log(`Toggle ${role} permission ${idx}: ${enabled}`);
768 }
769
770 // Initialize
771 loadTeams();
772 </script>
773</body>
774</html>"#;
775
776#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
778#[serde(rename_all = "lowercase")]
779pub enum Role {
780 Admin,
781 Developer,
782 Viewer,
783 Custom(String),
784}
785
786impl Role {
787 pub fn default_permissions(&self) -> HashSet<Permission> {
788 match self {
789 Role::Admin => HashSet::from([
790 Permission::ManageUsers,
791 Permission::ManageTeams,
792 Permission::ViewBilling,
793 Permission::ManageDatabase,
794 Permission::ManageStorage,
795 Permission::ViewLogs,
796 Permission::ManageSettings,
797 ]),
798 Role::Developer => HashSet::from([
799 Permission::ManageDatabase,
800 Permission::ManageStorage,
801 Permission::ViewLogs,
802 ]),
803 Role::Viewer => HashSet::from([Permission::ViewLogs]),
804 Role::Custom(_) => HashSet::new(),
805 }
806 }
807}
808
809#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
811pub enum Permission {
812 ManageUsers,
813 ManageTeams,
814 ViewBilling,
815 ManageDatabase,
816 ManageStorage,
817 ViewLogs,
818 ManageSettings,
819}
820
821#[derive(Debug, Serialize, Deserialize, Clone)]
823pub struct Team {
824 pub id: String,
825 pub name: String,
826 pub description: String,
827 pub icon: String,
828 pub member_count: usize,
829 pub roles: Vec<String>,
830 pub created_at: String,
831}
832
833#[derive(Debug, Serialize, Deserialize, Clone)]
835pub struct User {
836 pub id: String,
837 pub name: String,
838 pub email: String,
839 pub role: Role,
840 pub teams: Vec<String>,
841 pub status: String,
842 pub last_active: String,
843 pub permissions: HashSet<Permission>,
844}
845
846#[derive(Debug, Serialize, Deserialize, Clone)]
848pub struct AuditLogEntry {
849 pub id: String,
850 pub action: String,
851 pub details: String,
852 pub icon: String,
853 pub user_id: String,
854 pub time: String,
855}
856
857#[derive(Debug, Serialize)]
859struct TeamsResponse {
860 teams: Vec<Team>,
861}
862
863#[derive(Debug, Serialize)]
865struct UsersResponse {
866 users: Vec<User>,
867}
868
869#[derive(Debug, Serialize)]
871struct AuditLogResponse {
872 logs: Vec<AuditLogEntry>,
873}
874
875async fn team_management_ui() -> impl IntoResponse {
877 Html(TEAM_MANAGEMENT_HTML)
878}
879
880async fn list_teams(
882 State(state): State<Arc<ConsoleState>>,
883) -> Result<Json<TeamsResponse>, ConsoleError> {
884 let teams = vec![
886 Team {
887 id: "team_1".to_string(),
888 name: "Engineering".to_string(),
889 description: "Core development team building AVL Platform features".to_string(),
890 icon: "⚙️".to_string(),
891 member_count: 12,
892 roles: vec!["Admin".to_string(), "Developer".to_string()],
893 created_at: "2024-01-15".to_string(),
894 },
895 Team {
896 id: "team_2".to_string(),
897 name: "Design".to_string(),
898 description: "UI/UX designers crafting beautiful experiences".to_string(),
899 icon: "🎨".to_string(),
900 member_count: 5,
901 roles: vec!["Developer".to_string(), "Viewer".to_string()],
902 created_at: "2024-02-01".to_string(),
903 },
904 Team {
905 id: "team_3".to_string(),
906 name: "Marketing".to_string(),
907 description: "Growth and customer success team".to_string(),
908 icon: "📊".to_string(),
909 member_count: 8,
910 roles: vec!["Viewer".to_string()],
911 created_at: "2024-02-15".to_string(),
912 },
913 ];
914
915 Ok(Json(TeamsResponse { teams }))
916}
917
918async fn list_users(
920 State(state): State<Arc<ConsoleState>>,
921) -> Result<Json<UsersResponse>, ConsoleError> {
922 let users = vec![
924 User {
925 id: "user_1".to_string(),
926 name: "Alice Johnson".to_string(),
927 email: "alice@company.com".to_string(),
928 role: Role::Admin,
929 teams: vec!["Engineering".to_string()],
930 status: "active".to_string(),
931 last_active: "2 minutes ago".to_string(),
932 permissions: Role::Admin.default_permissions(),
933 },
934 User {
935 id: "user_2".to_string(),
936 name: "Bob Smith".to_string(),
937 email: "bob@company.com".to_string(),
938 role: Role::Developer,
939 teams: vec!["Engineering".to_string(), "Design".to_string()],
940 status: "active".to_string(),
941 last_active: "1 hour ago".to_string(),
942 permissions: Role::Developer.default_permissions(),
943 },
944 User {
945 id: "user_3".to_string(),
946 name: "Carol White".to_string(),
947 email: "carol@company.com".to_string(),
948 role: Role::Viewer,
949 teams: vec!["Marketing".to_string()],
950 status: "active".to_string(),
951 last_active: "3 hours ago".to_string(),
952 permissions: Role::Viewer.default_permissions(),
953 },
954 ];
955
956 Ok(Json(UsersResponse { users }))
957}
958
959async fn get_audit_log(
961 State(state): State<Arc<ConsoleState>>,
962) -> Result<Json<AuditLogResponse>, ConsoleError> {
963 let logs = vec![
965 AuditLogEntry {
966 id: "log_1".to_string(),
967 action: "User Invited".to_string(),
968 details: "alice@company.com invited bob@company.com to Engineering team".to_string(),
969 icon: "✉️".to_string(),
970 user_id: "user_1".to_string(),
971 time: "2 hours ago".to_string(),
972 },
973 AuditLogEntry {
974 id: "log_2".to_string(),
975 action: "Permission Changed".to_string(),
976 details: "bob@company.com role changed from Viewer to Developer".to_string(),
977 icon: "🔐".to_string(),
978 user_id: "user_1".to_string(),
979 time: "5 hours ago".to_string(),
980 },
981 AuditLogEntry {
982 id: "log_3".to_string(),
983 action: "Team Created".to_string(),
984 details: "New team 'Design' created with 5 members".to_string(),
985 icon: "🎨".to_string(),
986 user_id: "user_1".to_string(),
987 time: "1 day ago".to_string(),
988 },
989 ];
990
991 Ok(Json(AuditLogResponse { logs }))
992}
993
994async fn create_team(
996 State(state): State<Arc<ConsoleState>>,
997 Json(payload): Json<Team>,
998) -> Result<Json<Team>, ConsoleError> {
999 Ok(Json(payload))
1001}
1002
1003async fn invite_user(
1005 State(state): State<Arc<ConsoleState>>,
1006 Json(payload): Json<User>,
1007) -> Result<Json<User>, ConsoleError> {
1008 Ok(Json(payload))
1010}
1011
1012pub fn has_permission(user: &User, permission: &Permission) -> bool {
1014 user.permissions.contains(permission)
1015}
1016
1017pub fn router(state: Arc<ConsoleState>) -> Router {
1019 Router::new()
1020 .route("/", get(team_management_ui))
1021 .route("/list", get(list_teams))
1022 .route("/users", get(list_users))
1023 .route("/audit", get(get_audit_log))
1024 .route("/create", post(create_team))
1025 .route("/invite", post(invite_user))
1026 .with_state(state)
1027}
1028
1029#[cfg(test)]
1030mod tests {
1031 use super::*;
1032
1033 #[test]
1034 fn test_admin_permissions() {
1035 let perms = Role::Admin.default_permissions();
1036 assert!(perms.contains(&Permission::ManageUsers));
1037 assert!(perms.contains(&Permission::ManageDatabase));
1038 }
1039
1040 #[test]
1041 fn test_developer_permissions() {
1042 let perms = Role::Developer.default_permissions();
1043 assert!(!perms.contains(&Permission::ManageUsers));
1044 assert!(perms.contains(&Permission::ManageDatabase));
1045 }
1046
1047 #[test]
1048 fn test_viewer_permissions() {
1049 let perms = Role::Viewer.default_permissions();
1050 assert!(!perms.contains(&Permission::ManageUsers));
1051 assert!(perms.contains(&Permission::ViewLogs));
1052 }
1053}