style: supprime les dégradés placeholder des tuiles

This commit is contained in:
Cedric Abonnel
2026-05-09 13:24:36 +02:00
parent 5a02014e48
commit ddb753fff8
3 changed files with 104 additions and 20 deletions
+98
View File
@@ -0,0 +1,98 @@
# Authentification — Lien magique
Mécanisme d'authentification par email sans mot de passe. L'utilisateur reçoit un lien à usage unique, valide un temps limité, qui ouvre une session PHP.
## Fichiers concernés
| Fichier | Rôle |
|---|---|
| `public/login/index.php` | Formulaire de demande + génération du token |
| `public/login/magic.php` | Consommation du token + ouverture de session |
| Table BDD `auth_magic_links` | Persistance des tokens |
## Configuration (`.env`)
| Variable | Défaut | Description |
|---|---|---|
| `MAGIC_LINK_TTL_MINUTES` | `30` | Durée de validité du lien |
| `MAGIC_COOLDOWN_MINUTES` | `5` | Délai minimal entre deux demandes pour le même email |
| `MAGIC_WINDOW_HOURS` | `12` | Fenêtre glissante pour le plafond |
| `MAGIC_MAX_PER_WINDOW` | `5` | Nombre maximal de liens émis par fenêtre |
## Schéma BDD — `auth_magic_links`
```sql
id UUID PRIMARY KEY (gen_random_uuid())
email TEXT NOT NULL
token TEXT NOT NULL UNIQUE
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
expires_at TIMESTAMPTZ NOT NULL
consumed_at TIMESTAMPTZ NULL -- NULL = non consommé
ip TEXT
user_agent TEXT
return_to TEXT NULL -- chemin relatif de redirection post-login
```
## Phase 1 — Demande du lien (`index.php`, POST)
```
[Formulaire email] → POST /login
├─ Validation CSRF
├─ Validation syntaxe email
├─ Purge BDD : DELETE liens expirés ou consommés pour cet email
├─ Cooldown : un lien < MAGIC_COOLDOWN_MINUTES → refus
├─ Plafond : >= MAGIC_MAX_PER_WINDOW liens dans MAGIC_WINDOW_HOURS → refus
├─ Génération token : random_bytes(32) → base64url (43 chars, URL-safe)
├─ INSERT auth_magic_links (expires_at = NOW() + TTL)
└─ [stub] Envoi email : /login/magic.php?token=<token>
```
Le token est un base64url sans padding (`+/` remplacés par `-_`, `=` supprimés) — il ne contient que des caractères `[A-Za-z0-9\-_]`.
L'envoi de l'email est actuellement un **stub** : le code construit `$magicUrl` mais l'appel SMTP n'est pas implémenté (`index.php:142`).
## Phase 2 — Consommation du lien (`magic.php`, GET)
```
[Clic sur le lien] → GET /login/magic.php?token=<token>
├─ Validation format token (regex [A-Za-z0-9\-\_])
├─ BEGIN TRANSACTION
│ ├─ SELECT ... FOR UPDATE (verrou anti double-consommation)
│ ├─ Token inconnu → 400
│ ├─ consumed_at non null → "Lien déjà utilisé"
│ ├─ expires_at dépassé → "Lien expiré"
│ ├─ UPDATE consumed_at = NOW()
│ └─ COMMIT
├─ session_start() + session_regenerate_id(true)
├─ $_SESSION['user_email'] = email
└─ Redirection 303 → return_to (validé : doit commencer par /)
```
Le `FOR UPDATE` garantit qu'un token ne peut pas être consommé deux fois en cas de double-clic ou de requêtes concurrentes.
## Sécurités notables
- **CSRF** sur le formulaire de demande
- **Rate limiting** double (cooldown + plafond glissant) par email
- **Token URL-safe** généré par CSPRNG (`random_bytes`)
- **Verrou transactionnel** (`FOR UPDATE`) à la consommation
- **`session_regenerate_id(true)`** — prévient la fixation de session
- **`return_to` filtré** : seuls les chemins relatifs (commençant par `/`) sont acceptés
## État actuel — ce qui reste à câbler
L'envoi SMTP n'est pas implémenté. Le token est correctement généré et persisté en BDD, mais le mail n'est pas envoyé. Voir `public/login/index.php:142` :
```php
$magicUrl = url('/login/magic.php') . '?token=' . urlencode($token);
/* envoyer_mail_smtp(...) ou mail(...) */
```
+1 -2
View File
@@ -143,7 +143,7 @@ if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') {
/* envoyer_mail_smtp(...) ou mail(...) */
// message utilisateur
$okMsg = "Un lien vient d’être envoyé. Vérifiez votre boîte de réception et le dossier spam/indésirables.";
$okMsg = 'Un lien vient d’être envoyé. Vérifiez votre boîte de réception et le dossier spam/indésirables.';
} catch (\Throwable $ex) {
if ($pdo->inTransaction()) {
@@ -198,4 +198,3 @@ ob_start();
$content = ob_get_clean();
$title = 'Connexion';
include BASE_PATH . '/templates/layout.php';
+5 -18
View File
@@ -2,24 +2,14 @@
require_once BASE_PATH . '/src/Parsedown.php';
$Parsedown = new Parsedown();
$coverGradients = [
'linear-gradient(135deg, #e0e7ff 0%, #c7d2fe 100%)',
'linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%)',
'linear-gradient(135deg, #fce7f3 0%, #fbcfe8 100%)',
'linear-gradient(135deg, #fef3c7 0%, #fde68a 100%)',
'linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%)',
'linear-gradient(135deg, #ede9fe 0%, #ddd6fe 100%)',
];
ob_start();
?>
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
<?php foreach ($posts as $i => $post): ?>
<?php foreach ($posts as $post): ?>
<?php
$html = $Parsedown->text($post['content']);
$preview = mb_strimwidth(strip_tags($html), 0, 120, '…');
$gradient = $coverGradients[$i % count($coverGradients)];
$postUrl = '/post/' . rawurlencode($post['slug']);
$isDraft = !$post['published'];
$isAvantPremiere = $post['published'] && strtotime((string)($post['published_at'] ?? '')) > time();
@@ -32,13 +22,10 @@ ob_start();
<?php elseif ($isAvantPremiere): ?>
<div class="premiere-ribbon">Avant-première</div>
<?php endif; ?>
<?php
$coverFile = $post['cover'] ?? '';
$coverStyle = $coverFile !== ''
? 'background-image: url(/file?uuid=' . rawurlencode($post['uuid']) . '&name=' . rawurlencode($coverFile) . ')'
: 'background: ' . $gradient;
?>
<div class="card-cover" style="<?= $coverStyle ?>"></div>
<?php $coverFile = $post['cover'] ?? ''; ?>
<?php if ($coverFile !== ''): ?>
<div class="card-cover" style="background-image: url('/file?uuid=<?= rawurlencode($post['uuid']) ?>&name=<?= rawurlencode($coverFile) ?>')"></div>
<?php endif; ?>
<div class="card-body d-flex flex-column">
<h2 class="card-title">
<?php if ($isLocked): ?>