mirror of
https://github.com/R0m1k3/Presencia.git
synced 2026-10-11 17:27:42 +02:00
Sécurité - JWT : un JWT_SECRET d'exemple ou trop court est ignoré au profit d'un secret aléatoire conservé en base (avant, la valeur publique de docker-compose.yml permettait de forger un jeton admin). - Le compte est relu en base à chaque requête : désactivation, changement de rôle et réinitialisation du mot de passe prennent effet immédiatement. - Connexion : 10 échecs max par e-mail / 15 min, temps constant que l'e-mail existe ou non, mot de passe admin retiré des logs. - Validation des entrées (ids, dates, mois, longueurs, e-mail) : 400 au lieu de 500. Un admin ne peut plus se désactiver ni se rétrograder. - Mots de passe : 8 caractères minimum ; changement en libre-service. - Export : nom de fichier conforme RFC 5987 (un nom de société avec « — » faisait planter l'export). - esc() échappe aussi les guillemets (injection d'attributs HTML). - En-têtes CSP / X-Frame-Options / nosniff (Nginx + API), API et Postgres publiés sur 127.0.0.1 seulement, image backend non-root via npm ci. Optimisations - Export : une requête pour tout le mois au lieu d'une par cadre. - Saisie groupée : un seul INSERT (unnest), doublons dédupliqués. - Dates renvoyées en chaînes (plus de décalage d'un jour selon le TZ). - Planning : chaque clic met à jour l'affichage localement au lieu de recharger le mois ; les réponses de mois périmées sont ignorées. - Recherche Utilisateurs / Plannings filtrée localement (plus une requête par touche) ; statuts des sociétés chargés en parallèle. UI/UX - Mobile : toutes les destinations dans la barre basse (Sociétés et Ma saisie étaient inaccessibles à l'admin) ; tableaux affichés en cartes (les boutons d'action étaient hors écran). - Boutons désactivés pendant l'envoi, focus et Échap dans les dialogues, retour à l'écran de connexion quand la session est révoquée, erreurs d'export affichées au lieu d'un fichier JSON téléchargé, actions destructives signalées, confirmation avant désactivation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D5Bdayziw6tybgqETZoNSt
296 lines
12 KiB
HTML
296 lines
12 KiB
HTML
<!doctype html>
|
||
<html lang="fr">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>Presencia — Gestion des présences</title>
|
||
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' rx='22' fill='%233f6b54'/%3E%3C/svg%3E" />
|
||
<link rel="stylesheet" href="css/inter.css" />
|
||
<link rel="stylesheet" href="css/styles.css" />
|
||
<link rel="stylesheet" href="css/presencia.css" />
|
||
<link rel="stylesheet" href="css/phosphor.css" />
|
||
<link rel="stylesheet" href="css/app.css" />
|
||
</head>
|
||
<body>
|
||
|
||
<!-- ══════════════ LOGIN ══════════════ -->
|
||
<section id="view-login" class="login-split">
|
||
<div class="login-pitch">
|
||
<div class="brand-row">
|
||
<span class="brand-mark"></span>
|
||
<span class="brand-name">Presencia</span>
|
||
</div>
|
||
<div class="login-pitch-copy">
|
||
<h1>Les présences,<br />sans le tableur.</h1>
|
||
<p class="text-muted">Saisie par demi-journée, validation en deux temps, export mensuel par société. Chaque cadre remplit son mois en moins de deux minutes.</p>
|
||
</div>
|
||
<div class="login-pitch-feet">
|
||
<span>Validation cadre puis administrateur</span>
|
||
<span>Export PDF & Excel</span>
|
||
</div>
|
||
</div>
|
||
<div class="login-formside">
|
||
<form id="login-form" class="login-form" autocomplete="on">
|
||
<div>
|
||
<h3>Connexion</h3>
|
||
<p class="login-lede">Accès réservé — les comptes sont créés par un administrateur.</p>
|
||
</div>
|
||
<div class="field">
|
||
<label for="login-email">Adresse e-mail</label>
|
||
<input class="input" id="login-email" type="email" required autocomplete="username" placeholder="vous@societe.com" />
|
||
</div>
|
||
<div class="field">
|
||
<label for="login-password">Mot de passe</label>
|
||
<input class="input" id="login-password" type="password" required autocomplete="current-password" placeholder="••••••••" />
|
||
</div>
|
||
<div id="login-error" class="login-error" hidden></div>
|
||
<button type="submit" class="btn btn-primary btn-block">Se connecter</button>
|
||
</form>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ══════════════ APP ══════════════ -->
|
||
<div id="app" class="shell" hidden>
|
||
<aside class="side">
|
||
<div class="side-brand">
|
||
<span class="brand-mark small"></span>
|
||
<span class="brand-name small">Presencia</span>
|
||
</div>
|
||
<nav id="side-nav"></nav>
|
||
<div class="side-foot">
|
||
<div class="side-user">
|
||
<span id="user-initials" class="avatar"></span>
|
||
<span class="side-user-text">
|
||
<span id="user-name" class="side-user-name"></span>
|
||
<span id="user-meta" class="side-user-meta"></span>
|
||
</span>
|
||
</div>
|
||
<button id="my-password-btn" class="navitem"><i class="ni-ico ph ph-lock-simple"></i><span>Mon mot de passe</span></button>
|
||
<button id="logout-btn" class="navitem"><i class="ni-ico ph ph-sign-out"></i><span>Déconnexion</span></button>
|
||
</div>
|
||
</aside>
|
||
|
||
<main class="content">
|
||
<div class="mobile-only mobile-head">
|
||
<span class="brand-mark tiny"></span>
|
||
<span id="screen-title" class="mobile-title"></span>
|
||
<button id="my-password-btn-mobile" class="chipbtn chipbtn-icon" aria-label="Changer mon mot de passe" title="Changer mon mot de passe"><i class="ph ph-lock-simple"></i></button>
|
||
<button id="logout-btn-mobile" class="chipbtn" aria-label="Se déconnecter"><i class="ph ph-sign-out"></i>Quitter</button>
|
||
</div>
|
||
|
||
<!-- ─── DASHBOARD ─── -->
|
||
<section id="view-dashboard" class="view stack-20">
|
||
<div>
|
||
<h3 id="dash-greeting"></h3>
|
||
<p id="dash-subtitle" class="lede"></p>
|
||
</div>
|
||
<div id="dash-tiles" class="tile-grid"></div>
|
||
<div class="stack-8">
|
||
<h6>À faire maintenant</h6>
|
||
<div id="dash-actions" class="stack-6"></div>
|
||
</div>
|
||
<div id="dash-company-block" class="stack-8" hidden>
|
||
<h6>État du mois par société</h6>
|
||
<div class="card table-card">
|
||
<table class="table">
|
||
<thead><tr><th>Société</th><th>Cadres validés</th><th>Statut</th><th></th></tr></thead>
|
||
<tbody id="dash-company-rows"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ─── PLANNING ─── -->
|
||
<section id="view-planning" class="view stack-14" hidden>
|
||
<div class="planning-head">
|
||
<div class="planning-head-title">
|
||
<button id="back-to-plannings" class="btn btn-ghost back-link" hidden>← Tous les plannings</button>
|
||
<h3 id="planning-title"></h3>
|
||
<p id="planning-subtitle" class="lede"></p>
|
||
</div>
|
||
<div class="month-nav">
|
||
<button id="prev-month" class="btn btn-secondary btn-icon" aria-label="Mois précédent"><i class="ph ph-caret-left"></i></button>
|
||
<span id="month-label" class="month-label"></span>
|
||
<button id="next-month" class="btn btn-secondary btn-icon" aria-label="Mois suivant"><i class="ph ph-caret-right"></i></button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="lockbar">
|
||
<i id="lock-icon" class="ph ph-pencil-simple-line lockbar-icon"></i>
|
||
<span class="lockbar-text">
|
||
<span id="lock-title" class="lockbar-title"></span>
|
||
<span id="lock-detail" class="lockbar-detail"></span>
|
||
</span>
|
||
<span class="lockbar-actions">
|
||
<button id="reopen-month-btn" class="btn btn-secondary" hidden>Réouvrir le mois</button>
|
||
<button id="validate-month-btn" class="btn btn-primary" hidden>Valider mon mois</button>
|
||
</span>
|
||
</div>
|
||
|
||
<div class="toolrow">
|
||
<span class="toolrow-label">Saisie rapide</span>
|
||
<div id="brush-options" class="toolrow-items"></div>
|
||
</div>
|
||
|
||
<div class="toolrow">
|
||
<div id="view-options" class="toolrow-items"></div>
|
||
<span class="toolrow-right">
|
||
<button id="fill-workdays-btn" class="btn btn-secondary">Remplir les jours ouvrés</button>
|
||
<button id="clear-month-btn" class="btn btn-secondary btn-danger-ghost">Tout effacer</button>
|
||
</span>
|
||
</div>
|
||
|
||
<div id="brush-hint" class="brush-hint"></div>
|
||
|
||
<div id="planning-view" class="pv-grid">
|
||
<div class="cal-head">
|
||
<span>Lun</span><span>Mar</span><span>Mer</span><span>Jeu</span><span>Ven</span><span>Sam</span><span>Dim</span>
|
||
</div>
|
||
<div id="cal-grid" class="cal-grid"></div>
|
||
</div>
|
||
|
||
<div id="planning-summary" class="summary-row"></div>
|
||
</section>
|
||
|
||
<!-- ─── PLANNINGS (admin) ─── -->
|
||
<section id="view-plannings" class="view stack-14" hidden>
|
||
<div>
|
||
<h3>Plannings des cadres</h3>
|
||
<p id="plannings-subtitle" class="lede"></p>
|
||
</div>
|
||
<div class="filter-bar">
|
||
<span class="field">
|
||
<label for="pl-co">Société</label>
|
||
<select class="input" id="pl-co"><option value="all">Toutes les sociétés</option></select>
|
||
</span>
|
||
<span class="field">
|
||
<label for="pl-q">Rechercher un cadre</label>
|
||
<input class="input" id="pl-q" placeholder="Nom ou e-mail" />
|
||
</span>
|
||
</div>
|
||
<div id="cadre-rows" class="stack-6"></div>
|
||
</section>
|
||
|
||
<!-- ─── VALIDATION & EXPORT (admin) ─── -->
|
||
<section id="view-validation" class="view stack-14" hidden>
|
||
<div>
|
||
<h3>Validation & export</h3>
|
||
<p class="lede">Une société se valide quand tous ses cadres ont validé leur mois.</p>
|
||
</div>
|
||
<div class="filter-bar">
|
||
<span class="field">
|
||
<label for="va-co">Société</label>
|
||
<select class="input" id="va-co"></select>
|
||
</span>
|
||
<span class="field">
|
||
<label for="va-mo">Mois</label>
|
||
<select class="input" id="va-mo"></select>
|
||
</span>
|
||
</div>
|
||
|
||
<div class="card validation-card">
|
||
<div class="validation-head">
|
||
<span class="validation-head-text">
|
||
<span id="validation-company-name" class="validation-company"></span>
|
||
<span id="validation-status-text" class="validation-status"></span>
|
||
</span>
|
||
<span class="validation-head-actions">
|
||
<button id="export-pdf-btn" class="btn btn-secondary"><i class="ph ph-file-pdf"></i>PDF</button>
|
||
<button id="export-excel-btn" class="btn btn-secondary"><i class="ph ph-file-xls"></i>Excel</button>
|
||
<button id="validate-company-btn" class="btn btn-primary">Valider la société</button>
|
||
</span>
|
||
</div>
|
||
<div class="progress"><span id="validation-progress" class="progress-bar"></span></div>
|
||
<table class="table">
|
||
<thead><tr><th>Cadre</th><th>Demi-journées</th><th>Statut</th><th></th></tr></thead>
|
||
<tbody id="validation-rows"></tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ─── COMPANIES (admin) ─── -->
|
||
<section id="view-companies" class="view stack-14" hidden>
|
||
<div class="page-head">
|
||
<div>
|
||
<h3>Sociétés</h3>
|
||
<p id="companies-count" class="lede"></p>
|
||
</div>
|
||
<button id="add-company-btn" class="btn btn-primary"><i class="ph ph-plus"></i>Nouvelle société</button>
|
||
</div>
|
||
<div class="card table-card">
|
||
<table class="table">
|
||
<thead><tr><th>Nom</th><th>Cadres actifs</th><th>Créée le</th><th></th></tr></thead>
|
||
<tbody id="companies-rows"></tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ─── USERS (admin) ─── -->
|
||
<section id="view-users" class="view stack-14" hidden>
|
||
<div class="page-head">
|
||
<div>
|
||
<h3>Utilisateurs</h3>
|
||
<p class="lede">Aucune inscription publique — les comptes sont créés ici.</p>
|
||
</div>
|
||
<button id="add-user-btn" class="btn btn-primary"><i class="ph ph-plus"></i>Nouvel utilisateur</button>
|
||
</div>
|
||
<div class="filter-bar">
|
||
<span class="field">
|
||
<label for="us-co">Société</label>
|
||
<select class="input" id="us-co"><option value="all">Toutes les sociétés</option></select>
|
||
</span>
|
||
<span class="field">
|
||
<label for="us-q">Rechercher</label>
|
||
<input class="input" id="us-q" placeholder="Nom ou e-mail" />
|
||
</span>
|
||
</div>
|
||
<div class="card table-card">
|
||
<table class="table">
|
||
<thead><tr><th>Nom</th><th>E-mail</th><th>Rôle</th><th>Société</th><th>Statut</th><th></th></tr></thead>
|
||
<tbody id="users-rows"></tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ─── HISTORY ─── -->
|
||
<section id="view-history" class="view stack-14" hidden>
|
||
<div>
|
||
<h3>Historique</h3>
|
||
<p class="lede">Vos mois saisis et leurs totaux.</p>
|
||
</div>
|
||
<div class="card table-card">
|
||
<table class="table">
|
||
<thead><tr><th>Mois</th><th>Présent</th><th>Non-présent</th><th>Congé</th><th>RTT</th><th>Statut</th><th></th></tr></thead>
|
||
<tbody id="history-rows"></tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
</main>
|
||
|
||
<nav id="mobile-nav" class="mobile-only mobile-nav"></nav>
|
||
</div>
|
||
|
||
<!-- ══════════════ POPOVER ══════════════ -->
|
||
<div id="pop-backdrop" class="pop-backdrop" hidden></div>
|
||
<div id="pop" class="pop" hidden>
|
||
<div class="pop-head">
|
||
<span id="pop-title" class="pop-title"></span>
|
||
<span class="pop-hint">1–4 au clavier</span>
|
||
</div>
|
||
<div id="pop-options"></div>
|
||
<button id="pop-clear" class="pop-opt pop-opt-muted"><i class="dot st-none"></i>Effacer<span class="kbd">⌫</span></button>
|
||
<button id="pop-whole-day" class="pop-opt"><i class="ph ph-arrows-out-line-horizontal pop-ico"></i>Appliquer à la journée<span class="kbd">↵</span></button>
|
||
</div>
|
||
|
||
<!-- ══════════════ DIALOG ══════════════ -->
|
||
<div id="dialog-backdrop" class="dialog-backdrop" hidden>
|
||
<div id="dialog" class="dialog" role="dialog" aria-modal="true" aria-labelledby="dialog-title"></div>
|
||
</div>
|
||
|
||
<!-- ══════════════ TOAST ══════════════ -->
|
||
<div id="toast" class="toast" aria-live="polite" hidden></div>
|
||
|
||
<script src="js/app.js"></script>
|
||
</body>
|
||
</html>
|