1127 lines
33 KiB
HTML
1127 lines
33 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="fr">
|
|
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
<meta name="theme-color" content="#0D2B5E">
|
|
<title>CYBERUSGATE - Access</title>
|
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
|
<link rel="manifest" href="/site.webmanifest">
|
|
<link rel="manifest" href="manifest.json">
|
|
<style>
|
|
:root {
|
|
--blue-dark: #0D2B5E;
|
|
--blue-main: #1565C0;
|
|
--blue-light: #1E88E5;
|
|
--blue-pale: #E3F2FD;
|
|
--green: #2E7D32;
|
|
--green-light: #E8F5E9;
|
|
--red: #C62828;
|
|
--red-light: #FFEBEE;
|
|
--orange: #E65100;
|
|
--gray-1: #F5F6FA;
|
|
--gray-2: #E8ECF1;
|
|
--gray-3: #B0BEC5;
|
|
--gray-4: #607D8B;
|
|
--white: #FFFFFF;
|
|
--text-main: #1A2332;
|
|
--text-sub: #546E7A;
|
|
--radius: 14px;
|
|
--radius-sm: 8px;
|
|
--shadow: 0 2px 16px rgba(0, 0, 0, 0.10);
|
|
}
|
|
|
|
body.dark-mode {
|
|
--gray-1: #070c16;
|
|
--gray-2: #1a2333;
|
|
--gray-3: #4b5563;
|
|
--gray-4: #9ca3af;
|
|
--white: #111826;
|
|
--text-main: #f8fafc;
|
|
--text-sub: #94a3b8;
|
|
--blue-dark: #0f172a;
|
|
--blue-pale: rgba(21, 101, 192, 0.2);
|
|
--green-light: rgba(46, 125, 50, 0.2);
|
|
--red-light: rgba(198, 40, 40, 0.2);
|
|
--shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
|
|
}
|
|
|
|
* {
|
|
box-sizing: border-box;
|
|
margin: 0;
|
|
padding: 0;
|
|
-webkit-tap-highlight-color: transparent;
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
}
|
|
|
|
body {
|
|
background: var(--gray-1);
|
|
color: var(--text-main);
|
|
min-height: 100vh;
|
|
overflow-x: hidden;
|
|
transition: 0.3s;
|
|
}
|
|
|
|
/* SCREENS */
|
|
.screen {
|
|
display: none;
|
|
min-height: 100vh;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.screen.active {
|
|
display: flex;
|
|
animation: fadeIn 0.3s ease;
|
|
}
|
|
|
|
@keyframes fadeIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(10px);
|
|
}
|
|
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
/* ======= LOGIN ======= */
|
|
#screen-login {
|
|
background: linear-gradient(145deg, #0D2B5E 0%, #1565C0 100%);
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 24px;
|
|
}
|
|
|
|
.login-card {
|
|
background: var(--white);
|
|
border-radius: 20px;
|
|
padding: 32px 24px;
|
|
width: 100%;
|
|
max-width: 380px;
|
|
box-shadow: var(--shadow);
|
|
text-align: center;
|
|
}
|
|
|
|
.logo-box {
|
|
width: 64px;
|
|
height: 64px;
|
|
background: var(--blue-pale);
|
|
border-radius: 16px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin: 0 auto 16px;
|
|
}
|
|
|
|
.logo-box svg {
|
|
width: 32px;
|
|
height: 32px;
|
|
fill: var(--blue-main);
|
|
}
|
|
|
|
.login-card h1 {
|
|
font-size: 22px;
|
|
color: var(--text-main);
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.login-card p {
|
|
font-size: 14px;
|
|
color: var(--text-sub);
|
|
margin-bottom: 32px;
|
|
}
|
|
|
|
.role-btn {
|
|
width: 100%;
|
|
padding: 16px;
|
|
border-radius: 12px;
|
|
border: 2px solid var(--gray-2);
|
|
background: var(--white);
|
|
margin-bottom: 16px;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
text-align: left;
|
|
transition: 0.2s;
|
|
}
|
|
|
|
.role-btn:active {
|
|
transform: scale(0.98);
|
|
border-color: var(--blue-main);
|
|
background: var(--blue-pale);
|
|
}
|
|
|
|
.role-icon {
|
|
width: 48px;
|
|
height: 48px;
|
|
border-radius: 50%;
|
|
background: var(--gray-1);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 20px;
|
|
font-weight: bold;
|
|
color: var(--blue-main);
|
|
}
|
|
|
|
.role-text h3 {
|
|
font-size: 16px;
|
|
color: var(--text-main);
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.role-text p {
|
|
font-size: 12px;
|
|
color: var(--text-sub);
|
|
}
|
|
|
|
/* ======= APP HEADER & NAV ======= */
|
|
.app-header {
|
|
background: var(--blue-dark);
|
|
padding: 48px 20px 20px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 100;
|
|
}
|
|
|
|
.user-badge {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
.avatar {
|
|
width: 44px;
|
|
height: 44px;
|
|
border-radius: 50%;
|
|
background: var(--blue-light);
|
|
border: 2px solid rgba(255, 255, 255, 0.2);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: white;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.header-info h2 {
|
|
font-size: 16px;
|
|
color: white;
|
|
}
|
|
|
|
.header-info p {
|
|
font-size: 12px;
|
|
color: rgba(255, 255, 255, 0.7);
|
|
}
|
|
|
|
.logout-btn {
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border: none;
|
|
color: white;
|
|
padding: 8px 12px;
|
|
border-radius: 8px;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.nav-tabs {
|
|
display: flex;
|
|
background: var(--white);
|
|
border-bottom: 1px solid var(--gray-2);
|
|
position: sticky;
|
|
top: 112px;
|
|
z-index: 99;
|
|
}
|
|
|
|
.nav-tab {
|
|
flex: 1;
|
|
padding: 14px 4px;
|
|
border: none;
|
|
background: transparent;
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
color: var(--gray-3);
|
|
text-transform: uppercase;
|
|
cursor: pointer;
|
|
border-bottom: 3px solid transparent;
|
|
transition: 0.2s;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
|
|
.nav-tab svg {
|
|
width: 22px;
|
|
height: 22px;
|
|
stroke: currentColor;
|
|
fill: none;
|
|
stroke-width: 2;
|
|
}
|
|
|
|
.nav-tab.active {
|
|
color: var(--blue-main);
|
|
border-bottom-color: var(--blue-main);
|
|
}
|
|
|
|
.tab-content {
|
|
padding: 20px;
|
|
display: none;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
padding-bottom: 100px;
|
|
}
|
|
|
|
.tab-content.active {
|
|
display: flex;
|
|
animation: fadeIn 0.3s;
|
|
}
|
|
|
|
/* ======= UI COMPONENTS ======= */
|
|
.card {
|
|
background: var(--white);
|
|
border-radius: var(--radius);
|
|
padding: 20px;
|
|
box-shadow: var(--shadow);
|
|
}
|
|
|
|
.card-title {
|
|
font-size: 16px;
|
|
font-weight: 700;
|
|
margin-bottom: 16px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.btn {
|
|
padding: 14px;
|
|
border-radius: 8px;
|
|
font-size: 15px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
border: none;
|
|
text-align: center;
|
|
width: 100%;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
gap: 8px;
|
|
transition: 0.2s;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: var(--blue-main);
|
|
color: white;
|
|
}
|
|
|
|
.btn-primary:active {
|
|
background: var(--blue-dark);
|
|
transform: scale(0.98);
|
|
}
|
|
|
|
.btn-outline {
|
|
background: transparent;
|
|
border: 2px solid var(--blue-main);
|
|
color: var(--blue-main);
|
|
}
|
|
|
|
/* BADGE COMPONENT (USER) */
|
|
.virtual-badge {
|
|
background: linear-gradient(135deg, var(--blue-dark), var(--blue-main));
|
|
border-radius: 16px;
|
|
padding: 24px;
|
|
color: white;
|
|
text-align: center;
|
|
position: relative;
|
|
overflow: hidden;
|
|
box-shadow: 0 10px 24px rgba(21, 101, 192, 0.3);
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.virtual-badge .company {
|
|
font-size: 12px;
|
|
opacity: 0.8;
|
|
letter-spacing: 1px;
|
|
margin-bottom: 16px;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.virtual-badge .nfc-icon {
|
|
width: 80px;
|
|
height: 80px;
|
|
margin: 0 auto 16px;
|
|
border: 3px solid rgba(255, 255, 255, 0.3);
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
animation: pulse 2s infinite;
|
|
cursor: pointer;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0% {
|
|
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.4);
|
|
}
|
|
|
|
70% {
|
|
box-shadow: 0 0 0 20px rgba(255, 255, 255, 0);
|
|
}
|
|
|
|
100% {
|
|
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0);
|
|
}
|
|
}
|
|
|
|
.virtual-badge h3 {
|
|
font-size: 20px;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.virtual-badge p {
|
|
font-size: 14px;
|
|
opacity: 0.9;
|
|
}
|
|
|
|
/* ADMIN UI */
|
|
.stat-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 12px;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.stat-box {
|
|
background: var(--white);
|
|
padding: 16px;
|
|
border-radius: 12px;
|
|
box-shadow: var(--shadow);
|
|
border-left: 4px solid var(--blue-main);
|
|
}
|
|
|
|
.stat-box .num {
|
|
font-size: 24px;
|
|
font-weight: bold;
|
|
color: var(--text-main);
|
|
}
|
|
|
|
.stat-box .lbl {
|
|
font-size: 12px;
|
|
color: var(--text-sub);
|
|
margin-top: 4px;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.user-list-item {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 16px;
|
|
border-bottom: 1px solid var(--gray-2);
|
|
cursor: pointer;
|
|
}
|
|
|
|
.user-list-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.user-list-item .info h4 {
|
|
font-size: 15px;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.user-list-item .info p {
|
|
font-size: 12px;
|
|
color: var(--text-sub);
|
|
}
|
|
|
|
/* BADGE MANAGEMENT CARD */
|
|
.badge-mgmt-card {
|
|
border: 1px solid var(--gray-2);
|
|
border-radius: 12px;
|
|
padding: 16px;
|
|
margin-bottom: 12px;
|
|
background: var(--gray-1);
|
|
}
|
|
|
|
.badge-mgmt-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.badge-mgmt-header h4 {
|
|
font-size: 14px;
|
|
}
|
|
|
|
/* TOGGLE */
|
|
.toggle {
|
|
width: 44px;
|
|
height: 24px;
|
|
background: var(--gray-3);
|
|
border-radius: 12px;
|
|
position: relative;
|
|
cursor: pointer;
|
|
transition: 0.3s;
|
|
}
|
|
|
|
.toggle.on {
|
|
background: var(--green);
|
|
}
|
|
|
|
.toggle::after {
|
|
content: '';
|
|
position: absolute;
|
|
top: 2px;
|
|
left: 2px;
|
|
width: 20px;
|
|
height: 20px;
|
|
border-radius: 50%;
|
|
background: white;
|
|
transition: 0.3s;
|
|
}
|
|
|
|
.toggle.on::after {
|
|
left: 22px;
|
|
}
|
|
|
|
/* SELECT */
|
|
select.select-slot {
|
|
width: 100%;
|
|
padding: 10px;
|
|
border-radius: 8px;
|
|
border: 1px solid var(--gray-3);
|
|
background: var(--white);
|
|
color: var(--text-main);
|
|
font-size: 13px;
|
|
outline: none;
|
|
}
|
|
|
|
/* MODALS */
|
|
.modal-overlay {
|
|
display: none;
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, 0.7);
|
|
z-index: 1000;
|
|
align-items: center;
|
|
justify-content: center;
|
|
backdrop-filter: blur(3px);
|
|
opacity: 0;
|
|
transition: 0.3s;
|
|
}
|
|
|
|
.modal-overlay.open {
|
|
display: flex;
|
|
opacity: 1;
|
|
}
|
|
|
|
.modal-box {
|
|
background: var(--white);
|
|
border-radius: 20px;
|
|
padding: 24px;
|
|
width: 90%;
|
|
max-width: 360px;
|
|
transform: scale(0.9);
|
|
transition: 0.3s;
|
|
text-align: center;
|
|
}
|
|
|
|
.modal-overlay.open .modal-box {
|
|
transform: scale(1);
|
|
}
|
|
|
|
.qr-placeholder {
|
|
width: 200px;
|
|
height: 200px;
|
|
margin: 20px auto;
|
|
background: white;
|
|
border: 4px solid var(--blue-main);
|
|
border-radius: 12px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
position: relative;
|
|
}
|
|
|
|
.qr-placeholder img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: contain;
|
|
}
|
|
|
|
/* TOAST */
|
|
.toast {
|
|
position: fixed;
|
|
bottom: 30px;
|
|
left: 50%;
|
|
transform: translateX(-50%) translateY(20px);
|
|
background: #263238;
|
|
color: white;
|
|
padding: 14px 24px;
|
|
border-radius: 30px;
|
|
font-size: 14px;
|
|
font-weight: bold;
|
|
opacity: 0;
|
|
transition: 0.3s;
|
|
z-index: 9999;
|
|
pointer-events: none;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
.toast.show {
|
|
opacity: 1;
|
|
transform: translateX(-50%) translateY(0);
|
|
}
|
|
|
|
/* Biometric animation */
|
|
.bio-icon {
|
|
color: var(--blue-main);
|
|
margin-bottom: 16px;
|
|
animation: pulseBio 2s infinite;
|
|
}
|
|
|
|
@keyframes pulseBio {
|
|
0% {
|
|
transform: scale(1);
|
|
}
|
|
|
|
50% {
|
|
transform: scale(1.05);
|
|
}
|
|
|
|
100% {
|
|
transform: scale(1);
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body class="dark-mode">
|
|
|
|
<!-- ===== LOGIN SCREEN ===== -->
|
|
<div id="screen-login" class="screen active">
|
|
<div class="login-card">
|
|
<div class="logo-box">
|
|
<svg viewBox="0 0 24 24">
|
|
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
|
</svg>
|
|
</div>
|
|
<h1>CYBERUSGATE</h1>
|
|
<p>Système de contrôle d'accès</p>
|
|
|
|
<div class="role-btn" onclick="login('user')">
|
|
<div class="role-icon">AD</div>
|
|
<div class="role-text">
|
|
<h3>Espace Utilisateur</h3>
|
|
<p>Alice Dupont · Employée</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="role-btn" onclick="login('admin')" style="border-color: var(--orange);">
|
|
<div class="role-icon" style="color: var(--orange);">⚙️</div>
|
|
<div class="role-text">
|
|
<h3>Espace DSI / Admin</h3>
|
|
<p>Gestion Infrastructure</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ===== MAIN APP SCREEN ===== -->
|
|
<div id="screen-app" class="screen">
|
|
<div class="app-header">
|
|
<div class="user-badge">
|
|
<div class="avatar" id="header-avatar">AD</div>
|
|
<div class="header-info">
|
|
<h2 id="header-name">Alice Dupont</h2>
|
|
<p id="header-role">Utilisateur Interne</p>
|
|
</div>
|
|
</div>
|
|
<button class="logout-btn" onclick="logout()">Quitter</button>
|
|
</div>
|
|
|
|
<!-- USER NAV -->
|
|
<nav class="nav-tabs" id="nav-user" style="display:none;">
|
|
<button class="nav-tab active" onclick="switchTab('user-badge', this)">
|
|
<svg viewBox="0 0 24 24">
|
|
<rect x="2" y="5" width="20" height="14" rx="3" />
|
|
<path d="M8 12h.01M12 12h.01M16 12h.01" />
|
|
</svg>
|
|
Mon Badge
|
|
</button>
|
|
<button class="nav-tab" onclick="switchTab('user-add', this)">
|
|
<svg viewBox="0 0 24 24">
|
|
<path d="M3 3h7v7H3zM14 3h7v7h-7zM3 14h7v7H3zM21 14v7m-3-3h6" />
|
|
</svg>
|
|
Scanner QR
|
|
</button>
|
|
</nav>
|
|
|
|
<!-- ADMIN NAV -->
|
|
<nav class="nav-tabs" id="nav-admin" style="display:none;">
|
|
<button class="nav-tab active" onclick="switchTab('admin-dash', this)">
|
|
<svg viewBox="0 0 24 24">
|
|
<rect x="3" y="3" width="7" height="7" />
|
|
<rect x="14" y="3" width="7" height="7" />
|
|
<rect x="14" y="14" width="7" height="7" />
|
|
<rect x="3" y="14" width="7" height="7" />
|
|
</svg>
|
|
Infrastructure
|
|
</button>
|
|
<button class="nav-tab" onclick="switchTab('admin-users', this)">
|
|
<svg viewBox="0 0 24 24">
|
|
<path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2" />
|
|
<circle cx="9" cy="7" r="4" />
|
|
<path d="M23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75" />
|
|
</svg>
|
|
Badges
|
|
</button>
|
|
<button class="nav-tab" onclick="switchTab('admin-qr', this)">
|
|
<svg viewBox="0 0 24 24">
|
|
<path d="M12 4v16m8-8H4" />
|
|
</svg>
|
|
Générer QR
|
|
</button>
|
|
</nav>
|
|
|
|
<!-- ===============================
|
|
USER VIEWS
|
|
=============================== -->
|
|
|
|
<div id="tab-user-badge" class="tab-content">
|
|
<div id="user-badges-container">
|
|
<!-- Generated via JS -->
|
|
</div>
|
|
</div>
|
|
|
|
<div id="tab-user-add" class="tab-content">
|
|
<div class="card" style="text-align: center;">
|
|
<h2 style="margin-bottom:10px;">Ajouter un badge</h2>
|
|
<p style="font-size:14px; color:var(--text-sub); margin-bottom:24px;">Demandez à votre administrateur système de
|
|
générer un QR code d'invitation pour provisionner votre téléphone.</p>
|
|
|
|
<div
|
|
style="width:200px; height:200px; background:var(--gray-2); border-radius:16px; margin:0 auto 24px; display:flex; align-items:center; justify-content:center; border:2px dashed var(--gray-4);">
|
|
<svg viewBox="0 0 24 24" style="width:48px; fill:none; stroke:var(--gray-4); stroke-width:2;">
|
|
<path d="M4 4h6v6H4zM14 4h6v6h-6zM4 14h6v6H4z" />
|
|
<path d="M14 14l6 6M20 14l-6 6" />
|
|
</svg>
|
|
</div>
|
|
|
|
<button class="btn btn-primary" onclick="simulateScanQR()">
|
|
<svg viewBox="0 0 24 24" width="20" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M3 7V5a2 2 0 012-2h2M17 3h2a2 2 0 012 2v2M21 17v2a2 2 0 01-2 2h-2M7 21H5a2 2 0 01-2-2v-2" />
|
|
</svg>
|
|
Simuler le Scan du QR
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<!-- ===============================
|
|
ADMIN VIEWS
|
|
=============================== -->
|
|
|
|
<div id="tab-admin-dash" class="tab-content">
|
|
<div class="stat-grid">
|
|
<div class="stat-box">
|
|
<div class="num" id="stat-machines">190</div>
|
|
<div class="lbl">Équipements</div>
|
|
</div>
|
|
<div class="stat-box" style="border-color:var(--green);">
|
|
<div class="num" id="stat-users">30</div>
|
|
<div class="lbl">Utilisateurs</div>
|
|
</div>
|
|
<div class="stat-box" style="border-color:var(--orange);">
|
|
<div class="num" id="stat-badges">--</div>
|
|
<div class="lbl">Badges Actifs</div>
|
|
</div>
|
|
<div class="stat-box" style="border-color:var(--red);">
|
|
<div class="num" id="stat-invites">--</div>
|
|
<div class="lbl">QR Actifs</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-title">État du système</div>
|
|
<div style="font-size:13px; color:var(--text-sub); display:flex; flex-direction:column; gap:12px;">
|
|
<div style="display:flex; justify-content:space-between;"><span>Synchro Active Directory</span><span
|
|
style="color:var(--green);font-weight:bold;">Opérationnelle</span></div>
|
|
<div style="display:flex; justify-content:space-between;"><span>Chiffrement AES-256</span><span
|
|
style="color:var(--green);font-weight:bold;">Actif</span></div>
|
|
<div style="display:flex; justify-content:space-between;"><span>Contrôleurs de Domaine</span><span
|
|
style="color:var(--green);font-weight:bold;">En ligne</span></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="tab-admin-users" class="tab-content">
|
|
<div class="card" style="padding:0; overflow:hidden;">
|
|
<div style="padding:16px; background:var(--gray-2); font-weight:bold; font-size:14px;">Annuaire Utilisateurs
|
|
</div>
|
|
<div id="admin-user-list">
|
|
<!-- Generated via JS -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="tab-admin-qr" class="tab-content">
|
|
<div class="card">
|
|
<div class="card-title">Générateur d'accès sécurisé</div>
|
|
<p style="font-size:13px; color:var(--text-sub); margin-bottom:20px;">Générez un QR Code temporaire.
|
|
L'utilisateur devra le scanner avec son application pour provisionner son e-badge.</p>
|
|
|
|
<button class="btn btn-primary" onclick="generateInviteQR()">Créer une invitation NFC</button>
|
|
</div>
|
|
|
|
<h3 style="font-size:14px; margin-top:20px; margin-bottom:10px;">Invitations en attente</h3>
|
|
<div id="active-invites-list">
|
|
<!-- Generated via JS -->
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- ===== MODALS ===== -->
|
|
|
|
<!-- 2MFA / Biometric Overlay (Apple Pay Style) -->
|
|
<div class="modal-overlay" id="modal-biometric" onclick="if(event.target===this)cancelBiometric()">
|
|
<div class="modal-box" style="max-width:320px;">
|
|
<div class="bio-icon">
|
|
<svg viewBox="0 0 24 24" width="64" height="64" stroke="currentColor" fill="none" stroke-width="1.5">
|
|
<path d="M12 2a10 10 0 0 0-10 10c0 5.523 4.477 10 10 10s10-4.477 10-10A10 10 0 0 0 12 2z" />
|
|
<path d="M8 14s1.5 2 4 2 4-2 4-2" />
|
|
<line x1="9" y1="9" x2="9.01" y2="9" />
|
|
<line x1="15" y1="9" x2="15.01" y2="9" />
|
|
</svg>
|
|
</div>
|
|
<h2 style="margin-bottom:8px; font-size:18px;">Face ID / Touch ID</h2>
|
|
<p style="font-size:13px; color:var(--text-sub); margin-bottom:24px;">Confirmez votre identité pour libérer
|
|
l'accès NFC.</p>
|
|
|
|
<button class="btn btn-primary" onclick="confirmBiometric()" style="margin-bottom:12px;">
|
|
S'authentifier
|
|
</button>
|
|
<button class="btn btn-outline" onclick="cancelBiometric()">Annuler</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Admin Manage User Modal -->
|
|
<div class="modal-overlay" id="modal-manage-user" onclick="if(event.target===this)closeModal('modal-manage-user')">
|
|
<div class="modal-box" style="max-width:400px; text-align:left;">
|
|
<h2 id="manage-user-name" style="margin-bottom:4px;">Utilisateur</h2>
|
|
<p style="font-size:13px; color:var(--text-sub); margin-bottom:20px;">Gestion des badges et droits d'accès</p>
|
|
|
|
<div id="manage-user-badges" style="max-height:60vh; overflow-y:auto; margin-bottom:20px;">
|
|
<!-- Generated via JS -->
|
|
</div>
|
|
|
|
<button class="btn btn-outline" onclick="closeModal('modal-manage-user')">Fermer</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- QR Generator Modal -->
|
|
<div class="modal-overlay" id="modal-qr" onclick="if(event.target===this)closeModal('modal-qr')">
|
|
<div class="modal-box">
|
|
<h2>QR d'Invitation</h2>
|
|
<div class="qr-placeholder">
|
|
<svg width="150" height="150" viewBox="0 0 24 24" fill="var(--text-main)">
|
|
<path
|
|
d="M3 3h8v8H3zm2 2v4h4V5zm8-2h8v8h-8zm2 2v4h4V5zM3 13h8v8H3zm2 2v4h4v-4zm13-2h2v2h-2zm-2 2h2v2h-2zm2 2h2v2h-2zm-4 0h2v2h-2zm2 2h2v2h-2zm-4-4h2v2h-2zm2-2h2v2h-2z" />
|
|
</svg>
|
|
</div>
|
|
<p style="font-weight:bold; color:var(--green); margin-bottom:4px;" id="qr-expiry">Expire dans 14:59</p>
|
|
<p style="font-size:12px; color:var(--text-sub); margin-bottom:20px;" id="qr-creator">Créé par: --</p>
|
|
|
|
<button class="btn btn-outline" onclick="closeModal('modal-qr')">Fermer</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="toast" id="toast"></div>
|
|
|
|
<script>
|
|
// ===== DATABASE / STATE =====
|
|
const defaultDB = {
|
|
users: [
|
|
{ id: 'u1', name: 'Alice Dupont', email: 'alice@societe.fr', role: 'user', avatar: 'AD' },
|
|
{ id: 'u2', name: 'Administrateur', email: 'admin@societe.fr', role: 'admin', avatar: '⚙️' }
|
|
],
|
|
badges: [
|
|
{ id: 'b1', userId: 'u1', name: 'Pass Sécurité Principal', tech: 'NFC', active: true, timeSlot: '247' }
|
|
],
|
|
invitations: []
|
|
};
|
|
|
|
// Changed version key to ensure fresh start without previous AVM data
|
|
let db = JSON.parse(localStorage.getItem('sa_db_v4')) || defaultDB;
|
|
let currentUser = null;
|
|
|
|
function saveDb() {
|
|
localStorage.setItem('sa_db_v4', JSON.stringify(db));
|
|
}
|
|
|
|
// ===== PWA SETUP =====
|
|
if ('serviceWorker' in navigator) {
|
|
window.addEventListener('load', () => {
|
|
navigator.serviceWorker.register('sw.js').catch(err => console.log('SW error:', err));
|
|
});
|
|
}
|
|
|
|
// ===== CORE LOGIC =====
|
|
function showToast(msg) {
|
|
const t = document.getElementById('toast');
|
|
t.textContent = msg;
|
|
t.classList.add('show');
|
|
setTimeout(() => t.classList.remove('show'), 3000);
|
|
}
|
|
|
|
function login(roleType) {
|
|
currentUser = db.users.find(u => u.role === roleType);
|
|
document.getElementById('header-avatar').innerText = currentUser.avatar;
|
|
document.getElementById('header-name').innerText = currentUser.name;
|
|
document.getElementById('header-role').innerText = roleType === 'admin' ? "DSI / Infra" : "Utilisateur Interne";
|
|
|
|
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
|
|
document.getElementById('screen-app').classList.add('active');
|
|
|
|
if (roleType === 'user') {
|
|
document.getElementById('nav-user').style.display = 'flex';
|
|
document.getElementById('nav-admin').style.display = 'none';
|
|
switchTab('user-badge', document.querySelectorAll('#nav-user .nav-tab')[0]);
|
|
} else {
|
|
document.getElementById('nav-user').style.display = 'none';
|
|
document.getElementById('nav-admin').style.display = 'flex';
|
|
switchTab('admin-dash', document.querySelectorAll('#nav-admin .nav-tab')[0]);
|
|
}
|
|
}
|
|
|
|
function logout() {
|
|
currentUser = null;
|
|
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
|
|
document.getElementById('screen-login').classList.add('active');
|
|
}
|
|
|
|
function switchTab(tabId, btnElement) {
|
|
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
|
document.getElementById('tab-' + tabId).classList.add('active');
|
|
|
|
if (btnElement) {
|
|
const parent = btnElement.parentElement;
|
|
parent.querySelectorAll('.nav-tab').forEach(b => b.classList.remove('active'));
|
|
btnElement.classList.add('active');
|
|
}
|
|
|
|
// Refresh data based on tab
|
|
if (tabId === 'user-badge') renderUserBadges();
|
|
if (tabId === 'admin-dash') updateAdminDash();
|
|
if (tabId === 'admin-users') renderAdminUserList();
|
|
if (tabId === 'admin-qr') renderActiveInvites();
|
|
}
|
|
|
|
// ===== USER VIEWS & BIOMETRICS =====
|
|
function renderUserBadges() {
|
|
const container = document.getElementById('user-badges-container');
|
|
const myBadges = db.badges.filter(b => b.userId === currentUser.id);
|
|
|
|
if (myBadges.length === 0) {
|
|
container.innerHTML = `<div class="card" style="text-align:center; padding:40px 20px;"><p style="color:var(--text-sub);">Aucun badge actif.<br>Scannez un QR code pour en ajouter un.</p></div>`;
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = myBadges.map(b => `
|
|
<div class="virtual-badge" style="${!b.active ? 'filter: grayscale(1); opacity:0.7;' : ''}">
|
|
<div class="company">CYBERUSGATE - SECURE INFRA</div>
|
|
<div class="nfc-icon" onclick="${b.active ? 'triggerBiometricAuth()' : 'showToast(\'Ce badge a été désactivé par l\\\\\'administrateur\')'}">
|
|
<svg viewBox="0 0 24 24" width="40" stroke="currentColor" fill="none" stroke-width="2"><path d="M12 4C8 4 4 8 4 12s4 8 8 8 8-4 8-8"/><circle cx="12" cy="12" r="2" fill="currentColor"/></svg>
|
|
</div>
|
|
<h3>${b.name}</h3>
|
|
<p>ID: ${b.id} ${!b.active ? '<br><b style="color:#FFCDD2;">DÉSACTIVÉ</b>' : ''}</p>
|
|
<div style="font-size:11px; margin-top:10px; opacity:0.7;">Plage: ${b.timeSlot === '247' ? 'Accès 24/7' : 'Heures de bureau uniquement'}</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
// Intercept NFC with Biometric 2MFA Modal
|
|
function triggerBiometricAuth() {
|
|
if (navigator.vibrate) navigator.vibrate(50);
|
|
document.getElementById('modal-biometric').classList.add('open');
|
|
}
|
|
|
|
function cancelBiometric() {
|
|
document.getElementById('modal-biometric').classList.remove('open');
|
|
showToast("❌ Authentification annulée");
|
|
}
|
|
|
|
function confirmBiometric() {
|
|
// Close modal
|
|
document.getElementById('modal-biometric').classList.remove('open');
|
|
|
|
// Simulate successful biometric read and subsequent NFC transmission
|
|
if (navigator.vibrate) navigator.vibrate([50, 50, 100]);
|
|
showToast("✓ Identité confirmée. Transmission NFC en cours...");
|
|
|
|
// After short delay, show success of access
|
|
setTimeout(() => {
|
|
showToast("🟢 Accès Autorisé au lecteur");
|
|
if (navigator.vibrate) navigator.vibrate(200);
|
|
}, 1200);
|
|
}
|
|
|
|
function simulateScanQR() {
|
|
const validInvites = db.invitations.filter(i => Date.now() < i.expiresAt);
|
|
if (validInvites.length === 0) {
|
|
showToast("❌ Aucun QR code d'invitation actif trouvé.");
|
|
return;
|
|
}
|
|
|
|
const invite = validInvites.pop();
|
|
db.invitations = db.invitations.filter(i => i.id !== invite.id);
|
|
|
|
const newBadge = {
|
|
id: 'B' + Math.floor(Math.random() * 10000),
|
|
userId: currentUser.id,
|
|
name: 'Nouveau Badge Sécurisé',
|
|
tech: 'NFC',
|
|
active: true,
|
|
timeSlot: 'office'
|
|
};
|
|
|
|
db.badges.push(newBadge);
|
|
saveDb();
|
|
|
|
showToast(`✓ Badge ajouté avec succès ! (Généré par ${invite.createdBy})`);
|
|
switchTab('user-badge', document.querySelectorAll('#nav-user .nav-tab')[0]);
|
|
}
|
|
|
|
// ===== ADMIN VIEWS =====
|
|
function updateAdminDash() {
|
|
document.getElementById('stat-badges').innerText = db.badges.length;
|
|
const validInvites = db.invitations.filter(i => Date.now() < i.expiresAt);
|
|
document.getElementById('stat-invites').innerText = validInvites.length;
|
|
}
|
|
|
|
function renderAdminUserList() {
|
|
const list = document.getElementById('admin-user-list');
|
|
const normalUsers = db.users.filter(u => u.role === 'user');
|
|
|
|
list.innerHTML = normalUsers.map(u => {
|
|
const userBadges = db.badges.filter(b => b.userId === u.id);
|
|
return `
|
|
<div class="user-list-item" onclick="openManageUser('${u.id}')">
|
|
<div class="info">
|
|
<h4>${u.name}</h4>
|
|
<p>${u.email} · ${userBadges.length} badge(s)</p>
|
|
</div>
|
|
<svg viewBox="0 0 24 24" width="20" stroke="var(--gray-3)" fill="none"><path d="M9 18l6-6-6-6"/></svg>
|
|
</div>
|
|
`}).join('');
|
|
}
|
|
|
|
let managingUserId = null;
|
|
|
|
function openManageUser(userId) {
|
|
managingUserId = userId;
|
|
const user = db.users.find(u => u.id === userId);
|
|
document.getElementById('manage-user-name').innerText = user.name;
|
|
renderManageUserBadges();
|
|
document.getElementById('modal-manage-user').classList.add('open');
|
|
}
|
|
|
|
function renderManageUserBadges() {
|
|
const container = document.getElementById('manage-user-badges');
|
|
const userBadges = db.badges.filter(b => b.userId === managingUserId);
|
|
|
|
if (userBadges.length === 0) {
|
|
container.innerHTML = '<p style="font-size:13px; color:var(--text-sub);">Aucun badge attribué à cet utilisateur.</p>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = userBadges.map(b => `
|
|
<div class="badge-mgmt-card">
|
|
<div class="badge-mgmt-header">
|
|
<h4>${b.name} (${b.id})</h4>
|
|
<div class="toggle ${b.active ? 'on' : ''}" onclick="toggleBadgeStatus('${b.id}', this)"></div>
|
|
</div>
|
|
<div style="margin-top:12px;">
|
|
<label style="font-size:12px; font-weight:bold; color:var(--text-sub); display:block; margin-bottom:6px;">Plage d'accès autorisée</label>
|
|
<select class="select-slot" onchange="updateBadgeTimeSlot('${b.id}', this.value)">
|
|
<option value="247" ${b.timeSlot === '247' ? 'selected' : ''}>Accès Total (24/7)</option>
|
|
<option value="office" ${b.timeSlot === 'office' ? 'selected' : ''}>Heures de bureau (08h00 - 18h00)</option>
|
|
<option value="night" ${b.timeSlot === 'night' ? 'selected' : ''}>Astreinte Nuit (18h00 - 08h00)</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
function toggleBadgeStatus(badgeId, toggleElement) {
|
|
const badge = db.badges.find(b => b.id === badgeId);
|
|
badge.active = !badge.active;
|
|
toggleElement.classList.toggle('on', badge.active);
|
|
saveDb();
|
|
showToast(badge.active ? "Badge réactivé" : "Badge désactivé ! L'utilisateur ne peut plus entrer.");
|
|
}
|
|
|
|
function updateBadgeTimeSlot(badgeId, slotValue) {
|
|
const badge = db.badges.find(b => b.id === badgeId);
|
|
badge.timeSlot = slotValue;
|
|
saveDb();
|
|
showToast("Plage d'accès mise à jour.");
|
|
}
|
|
|
|
function closeModal(modalId) {
|
|
document.getElementById(modalId).classList.remove('open');
|
|
}
|
|
|
|
function generateInviteQR() {
|
|
const newInvite = {
|
|
id: 'QR-' + Math.floor(Math.random() * 10000),
|
|
createdBy: currentUser.name,
|
|
expiresAt: Date.now() + 15 * 60000 // 15 mins
|
|
};
|
|
db.invitations.push(newInvite);
|
|
saveDb();
|
|
|
|
document.getElementById('qr-creator').innerText = "Généré par: " + newInvite.createdBy;
|
|
document.getElementById('modal-qr').classList.add('open');
|
|
renderActiveInvites();
|
|
}
|
|
|
|
function renderActiveInvites() {
|
|
const container = document.getElementById('active-invites-list');
|
|
const validInvites = db.invitations.filter(i => Date.now() < i.expiresAt);
|
|
|
|
if (validInvites.length === 0) {
|
|
container.innerHTML = '<p style="font-size:13px; color:var(--text-sub);">Aucune invitation en cours.</p>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = validInvites.map(i => {
|
|
const minsLeft = Math.ceil((i.expiresAt - Date.now()) / 60000);
|
|
return `
|
|
<div style="background:var(--gray-2); padding:12px; border-radius:8px; margin-bottom:8px; display:flex; justify-content:space-between; align-items:center;">
|
|
<div>
|
|
<div style="font-weight:bold; font-size:14px;">${i.id}</div>
|
|
<div style="font-size:11px; color:var(--text-sub);">Par: ${i.createdBy}</div>
|
|
</div>
|
|
<div style="font-size:12px; color:var(--orange); font-weight:bold;">Expire dans ~${minsLeft} min</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
}
|
|
</script>
|
|
</body>
|
|
|
|
</html> |