Ajoute la réinitialisation de mot de passe par email (SMTP)
Un lien "Mot de passe oublié ?" (login.html) mène à /forgot-password : si l'adresse saisie correspond à un compte, un jeton à haute entropie (secrets.token_urlsafe, valable 1h) est généré et envoyé par email via smtplib (auth/send_email.py, aucune dépendance ajoutée) — configuré uniquement par variables d'environnement (SMTP_HOST/PORT/USER/PASSWORD/ FROM, voir .env.example et docker-compose.prod.yml), n'importe quel serveur SMTP existant convient (Mailcow compris). Seul le hash SHA-256 du jeton est stocké (auth/password_reset.py, table _password_reset_tokens) : un jeton envoyé par email reste inutilisable même en cas de fuite de la base. Le même message générique s'affiche que l'adresse corresponde à un compte ou non, pour ne jamais permettre à ce formulaire de servir à deviner quelles adresses sont déjà inscrites. Un échec d'envoi (SMTP non configuré) est journalisé côté serveur seulement, jamais révélé à l'utilisateur. /reset-password/<token> vérifie le jeton (non expiré, non déjà utilisé), applique les mêmes règles de mot de passe fort qu'à l'inscription (même schéma visuel), puis consomme le jeton et remet à zéro le compteur anti-bruteforce du compte (auth/set_password.py) — une identité prouvée par email est une voie de récupération légitime même pour un compte verrouillé. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
9efe119936
commit
d5a84413d1
@@ -0,0 +1,26 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Mot de passe oublié — Forge Engine{% endblock %}
|
||||
{% block content %}
|
||||
<section class="section">
|
||||
<div class="container" style="max-width:420px;">
|
||||
<h1 class="title">Mot de passe oublié</h1>
|
||||
|
||||
{% if sent %}
|
||||
<div class="notification is-success">Si un compte existe avec cette adresse, un email vient de t'être envoyé avec un lien pour choisir un nouveau mot de passe.</div>
|
||||
<p class="hint mt-3"><a href="{{ url_for('login') }}">Retour à la connexion</a></p>
|
||||
{% else %}
|
||||
<p class="hint mb-4">Indique ton adresse email : si un compte y est associé, un lien de réinitialisation te sera envoyé.</p>
|
||||
<form method="post" action="{{ url_for('forgot_password') }}" autocomplete="off">
|
||||
<div class="field">
|
||||
<label class="label">Adresse email</label>
|
||||
<div class="control"><input class="input" type="email" name="email" required autofocus></div>
|
||||
</div>
|
||||
<div class="field mt-4">
|
||||
<button type="submit" class="button primary is-fullwidth">Envoyer le lien</button>
|
||||
</div>
|
||||
</form>
|
||||
<p class="hint mt-3"><a href="{{ url_for('login') }}">Retour à la connexion</a></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -23,7 +23,8 @@
|
||||
<button type="submit" class="button primary is-fullwidth">Continuer</button>
|
||||
</div>
|
||||
</form>
|
||||
<p class="hint mt-3">Pas encore de compte ? <a href="{{ url_for('register') }}">S'inscrire</a></p>
|
||||
<p class="hint mt-3"><a href="{{ url_for('forgot_password') }}">Mot de passe oublié ?</a></p>
|
||||
<p class="hint mt-1">Pas encore de compte ? <a href="{{ url_for('register') }}">S'inscrire</a></p>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Nouveau mot de passe — Forge Engine{% endblock %}
|
||||
{% block content %}
|
||||
<section class="section">
|
||||
<div class="container" style="max-width:480px;">
|
||||
<h1 class="title">Choisir un nouveau mot de passe</h1>
|
||||
|
||||
{% if invalid %}
|
||||
<div class="notification is-danger">Ce lien de réinitialisation est invalide ou a expiré.</div>
|
||||
<p class="hint mt-3"><a href="{{ url_for('forgot_password') }}">Redemander un lien</a></p>
|
||||
{% else %}
|
||||
{% if error %}
|
||||
<div class="notification is-danger">{{ error }}</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" autocomplete="off">
|
||||
<div class="field">
|
||||
<label class="label">Nouveau mot de passe</label>
|
||||
<div class="control"><input class="input" type="password" id="password" name="password" required autocomplete="new-password"></div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div class="passwordStrengthBar"><div class="passwordStrengthFill" id="pwFill"></div></div>
|
||||
<ul class="passwordChecklist" id="pwChecklist">
|
||||
<li data-check="longueur">Au moins 8 caractères</li>
|
||||
<li data-check="majuscule">Une majuscule</li>
|
||||
<li data-check="minuscule">Une minuscule</li>
|
||||
<li data-check="chiffre">Un chiffre</li>
|
||||
<li data-check="special">Un caractère spécial (ex. ! ? # -)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Confirmer le mot de passe</label>
|
||||
<div class="control"><input class="input" type="password" name="password_confirm" required autocomplete="new-password"></div>
|
||||
</div>
|
||||
|
||||
<div class="field mt-4">
|
||||
<button type="submit" class="button primary is-fullwidth">Réinitialiser mon mot de passe</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% if not invalid %}
|
||||
<script>
|
||||
var MIN_SCORE_REQUIRED = {{ min_score_required }};
|
||||
var PW_CHECKS = {
|
||||
longueur: function (p) { return p.length >= 8; },
|
||||
majuscule: function (p) { return /[A-Z]/.test(p); },
|
||||
minuscule: function (p) { return /[a-z]/.test(p); },
|
||||
chiffre: function (p) { return /\d/.test(p); },
|
||||
special: function (p) { return /[^A-Za-z0-9]/.test(p); },
|
||||
};
|
||||
(function () {
|
||||
var input = document.getElementById('password');
|
||||
var fill = document.getElementById('pwFill');
|
||||
var items = document.querySelectorAll('#pwChecklist li');
|
||||
if (!input) return;
|
||||
function update() {
|
||||
var value = input.value || '';
|
||||
var score = 0;
|
||||
items.forEach(function (li) {
|
||||
var ok = PW_CHECKS[li.dataset.check](value);
|
||||
li.classList.toggle('ok', ok);
|
||||
if (ok) score++;
|
||||
});
|
||||
var pct = (score / items.length) * 100;
|
||||
fill.style.width = pct + '%';
|
||||
fill.className = 'passwordStrengthFill' +
|
||||
(score >= items.length ? ' strong' : score >= MIN_SCORE_REQUIRED ? ' good' : score >= 2 ? ' medium' : ' weak');
|
||||
}
|
||||
input.addEventListener('input', update);
|
||||
update();
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user