204 lines
9.6 KiB
PHP
204 lines
9.6 KiB
PHP
<?php
|
||
/**
|
||
* Écran de connexion — SEULE vue à ne pas passer par layouts/header.php.
|
||
*
|
||
* Elle s'affiche avant qu'une session existe, donc avant tout contrôle d'accès,
|
||
* et ne peut pas afficher la barre de navigation qui en dépend. Elle porte donc
|
||
* son propre <head> : c'est le TROISIÈME endroit où incrémenter le `?v=` des
|
||
* assets, avec layouts/header.php et layouts/footer.php.
|
||
*
|
||
* Depuis la V7.11, ce n'est plus un sélecteur de rôle mais une AUTHENTIFICATION :
|
||
* l'utilisateur présente le jeton que la DSI lui a remis, et c'est son compte qui
|
||
* détermine son rôle et son périmètre.
|
||
*
|
||
* La V7.12 ajoute un second facteur pour les comptes Service Enfance et DSI.
|
||
* D'où les quatre états de cet écran, portés par $etape :
|
||
* « jeton » — saisie du jeton ;
|
||
* « definition » — premier accès d'un compte privilégié : choisir son mot de passe ;
|
||
* « verification » — accès suivants : le saisir ;
|
||
* « changement » — session ouverte, le titulaire remplace son mot de passe.
|
||
* Un seul formulaire est affiché à la fois : les étapes sont exclusives.
|
||
*/
|
||
/** @var string $csrfToken */
|
||
/** @var string $error */
|
||
/** @var string $notice */
|
||
/** @var string $etape */
|
||
/** @var string $titulaire */
|
||
/** @var int $longueurMinimale */
|
||
/** @var array|null $currentProfile */
|
||
/** @var string $homeUrl */
|
||
/** @var string $cspNonce */
|
||
|
||
$titres = [
|
||
'jeton' => 'Connexion',
|
||
'definition' => 'Choisissez votre mot de passe',
|
||
'verification' => 'Mot de passe',
|
||
'changement' => 'Changer de mot de passe',
|
||
];
|
||
$titre = $titres[$etape] ?? 'Connexion';
|
||
?>
|
||
<!doctype html>
|
||
<html lang="fr">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title><?= htmlspecialchars($titre) ?> — PTA</title>
|
||
<link rel="stylesheet" href="assets/style.css?v=7.24">
|
||
</head>
|
||
<body class="role-page-body">
|
||
<main class="role-page-shell">
|
||
<section class="role-intro">
|
||
<p class="eyebrow">PTA</p>
|
||
<h1><?= htmlspecialchars($titre) ?></h1>
|
||
|
||
<?php if ($etape === 'jeton'): ?>
|
||
<p>Saisissez le jeton d’accès qui vous a été remis. Il détermine votre rôle, les pages visibles et les actions autorisées.</p>
|
||
<?php elseif ($etape === 'definition'): ?>
|
||
<p>
|
||
Votre compte <strong><?= htmlspecialchars($titulaire) ?></strong> engage l’application :
|
||
il demande un second secret, connu de vous seul. Choisissez-le maintenant, il vous sera demandé à chaque connexion.
|
||
</p>
|
||
<?php elseif ($etape === 'verification'): ?>
|
||
<p>Jeton reconnu. Saisissez le mot de passe de <strong><?= htmlspecialchars($titulaire) ?></strong> pour ouvrir la session.</p>
|
||
<?php else: ?>
|
||
<p>Votre mot de passe actuel vous est demandé : sans lui, un poste laissé ouvert suffirait à confisquer votre compte.</p>
|
||
<?php endif; ?>
|
||
|
||
<?php if ($currentProfile && $etape === 'jeton'): ?>
|
||
<div class="role-current-profile">
|
||
Session ouverte : <strong><?= htmlspecialchars((string) $currentProfile['role_label']) ?></strong>
|
||
— <?= htmlspecialchars((string) $currentProfile['label']) ?>
|
||
<a class="button button-primary role-resume-link" href="<?= htmlspecialchars($homeUrl) ?>">Reprendre</a>
|
||
</div>
|
||
<?php endif; ?>
|
||
<?php if ($notice !== ''): ?>
|
||
<div class="message message-success"><?= htmlspecialchars($notice) ?></div>
|
||
<?php endif; ?>
|
||
<?php if ($error !== ''): ?>
|
||
<div class="message message-error"><?= htmlspecialchars($error) ?></div>
|
||
<?php endif; ?>
|
||
</section>
|
||
|
||
<?php if ($etape === 'jeton'): ?>
|
||
<form method="post" class="role-selection-form role-token-form" id="role-token-form">
|
||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrfToken) ?>">
|
||
<label class="field">
|
||
<span>Jeton d’accès</span>
|
||
<?php /* autocomplete="off" : un jeton n'a pas à être retenu par le navigateur d'un poste partagé. */ ?>
|
||
<input
|
||
type="text"
|
||
name="token"
|
||
id="role-token-input"
|
||
class="role-token-input"
|
||
placeholder="PTA-XXXXXX-XXXXXX-XXXXXX-XXXXXX"
|
||
autocomplete="off"
|
||
autocapitalize="characters"
|
||
spellcheck="false"
|
||
required
|
||
autofocus>
|
||
</label>
|
||
<div class="role-form-actions">
|
||
<button type="submit" class="button button-primary">Continuer</button>
|
||
</div>
|
||
</form>
|
||
|
||
<?php elseif ($etape === 'definition' || $etape === 'verification'): ?>
|
||
<form method="post" class="role-selection-form role-token-form">
|
||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrfToken) ?>">
|
||
<input type="hidden" name="etape" value="mot_de_passe">
|
||
|
||
<label class="field">
|
||
<span><?= $etape === 'definition' ? 'Nouveau mot de passe' : 'Mot de passe' ?></span>
|
||
<input
|
||
type="password"
|
||
name="mot_de_passe"
|
||
autocomplete="<?= $etape === 'definition' ? 'new-password' : 'current-password' ?>"
|
||
minlength="<?= (int) $longueurMinimale ?>"
|
||
required
|
||
autofocus>
|
||
</label>
|
||
|
||
<?php if ($etape === 'definition'): ?>
|
||
<label class="field">
|
||
<span>Confirmation</span>
|
||
<input type="password" name="mot_de_passe_confirmation" autocomplete="new-password" minlength="<?= (int) $longueurMinimale ?>" required>
|
||
</label>
|
||
<p class="hint">
|
||
Au moins <?= (int) $longueurMinimale ?> caractères. Aucune règle de composition :
|
||
une phrase dont vous vous souvenez protège mieux qu’un mot court hérissé de symboles.
|
||
Il ne sera affiché nulle part — la DSI elle-même ne peut que l’effacer, jamais le lire.
|
||
</p>
|
||
<?php endif; ?>
|
||
|
||
<div class="role-form-actions">
|
||
<button type="submit" class="button button-primary"><?= $etape === 'definition' ? 'Enregistrer et se connecter' : 'Se connecter' ?></button>
|
||
</div>
|
||
<p class="hint role-step-back">
|
||
<a href="role.php?action=reset&csrf_token=<?= htmlspecialchars(urlencode($csrfToken), ENT_QUOTES) ?>">Recommencer avec un autre jeton</a>
|
||
</p>
|
||
</form>
|
||
|
||
<?php else: ?>
|
||
<form method="post" class="role-selection-form role-token-form">
|
||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrfToken) ?>">
|
||
<input type="hidden" name="etape" value="changement">
|
||
|
||
<label class="field">
|
||
<span>Mot de passe actuel</span>
|
||
<input type="password" name="mot_de_passe_actuel" autocomplete="current-password" required autofocus>
|
||
</label>
|
||
<label class="field">
|
||
<span>Nouveau mot de passe</span>
|
||
<input type="password" name="mot_de_passe" autocomplete="new-password" minlength="<?= (int) $longueurMinimale ?>" required>
|
||
</label>
|
||
<label class="field">
|
||
<span>Confirmation</span>
|
||
<input type="password" name="mot_de_passe_confirmation" autocomplete="new-password" minlength="<?= (int) $longueurMinimale ?>" required>
|
||
</label>
|
||
<p class="hint">Au moins <?= (int) $longueurMinimale ?> caractères.</p>
|
||
|
||
<div class="role-form-actions">
|
||
<button type="submit" class="button button-primary">Changer le mot de passe</button>
|
||
</div>
|
||
<p class="hint role-step-back">
|
||
<a href="<?= htmlspecialchars($homeUrl) ?>">Revenir à l’application</a>
|
||
</p>
|
||
</form>
|
||
<?php endif; ?>
|
||
|
||
<aside class="role-security-note">
|
||
<?php if ($etape === 'changement'): ?>
|
||
<strong>Un mot de passe oublié ne se retrouve pas :</strong> la DSI l’efface, et vous en choisissez un nouveau à votre connexion suivante, avec votre jeton.
|
||
<?php else: ?>
|
||
<strong>Jeton perdu ou compromis ?</strong> Contactez la DSI : elle en génère un nouveau, ce qui révoque immédiatement l’ancien.
|
||
Un jeton vaut pour une personne — il ne se prête pas, puisque toutes les actions sont journalisées à son nom.
|
||
Les comptes Service Enfance et DSI y ajoutent un mot de passe, que vous seul connaissez.
|
||
<?php endif; ?>
|
||
</aside>
|
||
</main>
|
||
<script nonce="<?= htmlspecialchars($cspNonce, ENT_QUOTES) ?>">
|
||
(() => {
|
||
const input = document.querySelector('#role-token-input');
|
||
if (!input) return;
|
||
|
||
/**
|
||
* Met en forme la saisie au fil de la frappe : majuscules et suppression des
|
||
* espaces. Les jetons sont recopiés depuis un courriel ou un papier, et un
|
||
* copier-coller ramène presque toujours une espace ou un retour à la ligne.
|
||
* Le serveur normalise de toute façon (CompteModel::normaliser) ; ceci n'est
|
||
* qu'un confort de saisie — et cela ne concerne QUE le jeton, jamais le mot
|
||
* de passe, dont chaque caractère compte tel qu'il est frappé.
|
||
*/
|
||
input.addEventListener('input', () => {
|
||
const position = input.selectionStart;
|
||
const nettoye = input.value.toUpperCase().replace(/\s+/g, '');
|
||
if (nettoye !== input.value) {
|
||
input.value = nettoye;
|
||
input.setSelectionRange(position, position);
|
||
}
|
||
});
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|