feat(auth): B-2.3 emails FR + DictIA branding (SMTP Resend)

Rebrand src/services/email.py IN PLACE: French + DictIA + brand gradient
(#0062ff/#00bdd8/#00c896) — replaces legacy "Speakr" / #2563eb. Greetings now
use user.name with fallback to user.username. Subjects:
"Vérifiez votre courriel — DictIA" + "Réinitialiser votre mot de passe — DictIA".
SMTP_FROM_NAME defaults to DictIA. Footer points to info@dictia.ca with the
Loi 25 tagline.

Refonte 4 auth templates IN PLACE pour étendre marketing/base.html : check_email,
forgot_password, reset_password, verify_success. Tokens DictIA (brand-navy,
brand-bg, grad-bg, shadow-cta), French copy, WCAG patterns (label for,
focus-visible:outline-2, role=alert, aria-required, text-brand-navy/70 minimum,
NBSP français pour Loi 25 / 24 heures / 1 heure / 8 caractères).

Translate inline French flash messages in src/api/auth.py for /verify-email,
/resend-verification, /forgot-password, /reset-password. Anti-enumeration fix:
forgot_password no longer flashes the cooldown remaining (would leak account
existence) — silently skips resend, generic flash unchanged. Cooldown logic
in src/services/email.py UNCHANGED (60s — verified by test).

config/env.email.example: defaults to Resend SMTP at the top + adds Resend
to the provider examples list (preserves Gmail/SendGrid/Mailgun/SES/M365).

Tests: tests/test_email_service_dictia.py — 12 tests covering DictIA branding,
French copy, display-name fallback, anti-enumeration parity (forgot_password
returns identical message for known/unknown emails), 60s cooldown, SMTP-not-
configured returns False (no exception), check_email.html extends marketing/base
(no var(--text-primary) leaks). Includes Windows manual driver
(_run_email_service_dictia_windows.py) since pytest cannot collect on Windows
native (fcntl POSIX-only).

NO new dependency added (no resend SDK — SMTP via existing _send_email).
NO new route added or removed.
NO src/auth_extended/ created.
NO change to itsdangerous-based token logic.
templates/auth/**/*.html already in tailwind.config.js content array (B-2.2).

Verified locally on Windows manual driver: 12/12 PASS B-2.3, 9/9 PASS regression
on B-2.2 signup, 9/9 PASS regression on B-2.1 ConsentLog.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Allison
2026-04-27 23:02:20 -04:00
parent 3b324ad0b9
commit 37639a7d09
10 changed files with 666 additions and 520 deletions

View File

@@ -1,127 +1,61 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex, nofollow, noarchive, nosnippet, noimageindex">
<title>{{ title }} - DictIA</title>
<link rel="icon" href="{{ url_for('static', filename='img/favicon.ico') }}" type="image/svg+xml">
<script src="{{ url_for('static', filename='vendor/js/tailwind.min.js') }}"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='vendor/css/fontawesome.min.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
{% extends 'marketing/base.html' %}
{% include 'includes/loading_overlay.html' %}
{% block title %}{% if action == 'password_reset' %}Vérifiez votre courriel — DictIA{% else %}Confirmez votre courriel — DictIA{% endif %}{% endblock %}
{% block description %}Un courriel vous a été envoyé. Suivez le lien pour activer votre compte DictIA.{% endblock %}
<script>
function applyTheme() {
if (!document.documentElement) return;
const savedMode = localStorage.getItem('darkMode');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (savedMode === 'true' || (savedMode === null && prefersDark)) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
const savedScheme = localStorage.getItem('colorScheme') || 'blue';
const isDark = document.documentElement.classList.contains('dark');
const themePrefix = isDark ? 'theme-dark-' : 'theme-light-';
const themeClasses = ['blue', 'emerald', 'purple', 'rose', 'amber', 'teal'];
themeClasses.forEach(theme => {
document.documentElement.classList.remove(`theme-light-${theme}`);
document.documentElement.classList.remove(`theme-dark-${theme}`);
});
if (savedScheme !== 'blue') {
document.documentElement.classList.add(themePrefix + savedScheme);
}
}
applyTheme();
</script>
</head>
<body class="bg-[var(--bg-primary)] text-[var(--text-primary)]">
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-6 flex flex-col min-h-screen">
<header class="flex justify-between items-center mb-6 pb-4 border-b border-[var(--border-primary)]">
<h1 class="text-3xl font-bold text-[var(--text-primary)]">
<a href="{{ url_for('recordings.index') }}" class="flex items-center">
<img src="{{ url_for('static', filename='img/logo-dictia.png') }}" alt="DictIA" class="h-14 w-14 mr-3">
DictIA
</a>
</h1>
</header>
{% block content %}
<section class="min-h-[calc(100vh-62px)] bg-brand-bg py-16 px-4" aria-labelledby="check-email-title">
<div class="max-w-md mx-auto bg-white p-8 rounded-[18px] border border-brand-border shadow-cta text-center">
<div class="mx-auto mb-6 w-16 h-16 rounded-full grad-bg flex items-center justify-center text-white text-2xl" aria-hidden="true">&#x2709;</div>
<main class="flex-grow flex items-center justify-center">
<div class="w-full max-w-md bg-[var(--bg-secondary)] p-8 rounded-xl shadow-lg border border-[var(--border-primary)]">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="mb-4 p-3 rounded-lg {% if category == 'success' %}bg-[var(--bg-success-light)] text-[var(--text-success-strong)]{% elif category == 'danger' %}bg-[var(--bg-danger-light)] text-[var(--text-danger-strong)]{% elif category == 'warning' %}bg-[var(--bg-warning-light)] text-[var(--text-warning-strong)]{% else %}bg-[var(--bg-info-light)] text-[var(--text-info-strong)]{% endif %}">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
<h1 id="check-email-title" class="text-2xl font-black text-brand-navy mb-2">
{% if action == 'password_reset' %}Vérifiez votre courriel
{% elif action == 'verification_required' %}Vérification requise
{% else %}Confirmez votre courriel{% endif %}
</h1>
<div class="text-center">
<div class="mb-6">
<div class="w-20 h-20 mx-auto bg-[var(--bg-info-light)] rounded-full flex items-center justify-center">
<i class="fas fa-envelope text-[var(--text-info-strong)] text-3xl"></i>
</div>
</div>
<p class="text-sm text-brand-navy/70 mb-6">
{% if action == 'password_reset' %}
{{ "Si un compte DictIA existe pour <strong>" ~ email ~ "</strong>, vous recevrez un courriel avec un lien pour réinitialiser votre mot de passe. Le lien expire dans 1&nbsp;heure." | safe }}
{% elif action == 'verification_required' %}
{{ "Vérifiez votre boîte de réception à <strong>" ~ email ~ "</strong>. Si vous ne recevez rien, demandez un nouveau courriel ci-dessous." | safe }}
{% else %}
{{ "Nous avons envoyé un lien de vérification à <strong>" ~ email ~ "</strong>. Cliquez dessus pour activer votre compte. Le lien expire dans 24&nbsp;heures." | safe }}
{% endif %}
</p>
{% if action == 'verification' %}
<h2 class="text-2xl font-semibold text-[var(--text-primary)] mb-4">Check Your Email</h2>
<p class="text-[var(--text-secondary)] mb-2">We've sent a verification link to:</p>
<p class="text-[var(--text-primary)] font-medium mb-6">{{ email }}</p>
<p class="text-[var(--text-muted)] text-sm mb-6">
Click the link in the email to verify your account. The link will expire in 24 hours.
</p>
{% elif action == 'verification_required' %}
<h2 class="text-2xl font-semibold text-[var(--text-primary)] mb-4">Email Verification Required</h2>
<p class="text-[var(--text-secondary)] mb-2">Please verify your email address:</p>
<p class="text-[var(--text-primary)] font-medium mb-6">{{ email }}</p>
<p class="text-[var(--text-muted)] text-sm mb-6">
Check your inbox for a verification email. If you haven't received it, you can request a new one.
</p>
{% elif action == 'password_reset' %}
<h2 class="text-2xl font-semibold text-[var(--text-primary)] mb-4">Check Your Email</h2>
<p class="text-[var(--text-secondary)] mb-2">If an account exists with this email:</p>
<p class="text-[var(--text-primary)] font-medium mb-6">{{ email }}</p>
<p class="text-[var(--text-muted)] text-sm mb-6">
We've sent a password reset link. The link will expire in 1 hour.
</p>
{% endif %}
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div role="alert" class="mb-3 p-3 rounded-lg text-sm
{% if category == 'danger' %}bg-red-50 text-red-900 border border-red-200
{% elif category == 'warning' %}bg-amber-50 text-amber-900 border border-amber-200
{% elif category == 'success' %}bg-green-50 text-green-900 border border-green-200
{% else %}bg-blue-50 text-blue-900 border border-blue-200{% endif %}">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% if show_resend and (action == 'verification' or action == 'verification_required') %}
<div class="mb-6">
<form method="POST" action="{{ url_for('auth.resend_verification') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="email" value="{{ email }}">
<button type="submit" class="text-[var(--text-accent)] hover:underline text-sm">
<i class="fas fa-redo mr-1"></i> Resend verification email
</button>
</form>
</div>
{% endif %}
{% if show_resend and action != 'password_reset' %}
<form method="POST" action="{{ url_for('auth.resend_verification') }}" class="mb-4">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="email" value="{{ email }}">
<button type="submit" class="w-full grad-bg text-white font-semibold py-3 rounded-[0.75rem] shadow-cta hover:shadow-cta-hover transition focus-visible:outline-2 focus-visible:outline-brand-b1 focus-visible:outline-offset-2">
Renvoyer le lien de vérification
</button>
</form>
{% endif %}
<div class="pt-4 border-t border-[var(--border-secondary)]">
<a href="{{ url_for('auth.login') }}" class="text-[var(--text-accent)] hover:underline">
<i class="fas fa-arrow-left mr-1"></i> Back to Login
</a>
</div>
</div>
</div>
</main>
<p class="text-xs text-brand-navy/70 mt-4">
Vous ne recevez rien&nbsp;? Vérifiez vos pourriels (spam) ou
<a href="mailto:info@dictia.ca" class="grad-text font-semibold">contactez le support</a>.
</p>
<footer class="text-center py-4 mt-8 text-xs text-[var(--text-light)] border-t border-[var(--border-primary)]">
<div>&copy; {{ now.year }} InnovA AI &middot; <a href="/politique-confidentialite" class="underline hover:text-[var(--text-primary)]">Politique de confidentialit&eacute;</a> &middot; <a href="/conditions-utilisation" class="underline hover:text-[var(--text-primary)]">Conditions d'utilisation</a></div>
</footer>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (window.AppLoader) {
AppLoader.waitForReady();
}
});
</script>
</body>
</html>
<p class="mt-6 text-sm">
<a href="{{ url_for('auth.login') }}" class="grad-text font-semibold">&larr; Retour à la connexion</a>
</p>
</div>
</section>
{% endblock %}

View File

@@ -1,105 +1,46 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex, nofollow, noarchive, nosnippet, noimageindex">
<title>{{ title }} - DictIA</title>
<link rel="icon" href="{{ url_for('static', filename='img/favicon.ico') }}" type="image/svg+xml">
<script src="{{ url_for('static', filename='vendor/js/tailwind.min.js') }}"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='vendor/css/fontawesome.min.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
{% extends 'marketing/base.html' %}
{% include 'includes/loading_overlay.html' %}
{% block title %}Mot de passe oublié — DictIA{% endblock %}
{% block description %}Recevez un lien sécurisé pour réinitialiser le mot de passe de votre compte DictIA.{% endblock %}
<script>
function applyTheme() {
if (!document.documentElement) return;
const savedMode = localStorage.getItem('darkMode');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (savedMode === 'true' || (savedMode === null && prefersDark)) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
const savedScheme = localStorage.getItem('colorScheme') || 'blue';
const isDark = document.documentElement.classList.contains('dark');
const themePrefix = isDark ? 'theme-dark-' : 'theme-light-';
const themeClasses = ['blue', 'emerald', 'purple', 'rose', 'amber', 'teal'];
themeClasses.forEach(theme => {
document.documentElement.classList.remove(`theme-light-${theme}`);
document.documentElement.classList.remove(`theme-dark-${theme}`);
});
if (savedScheme !== 'blue') {
document.documentElement.classList.add(themePrefix + savedScheme);
}
}
applyTheme();
</script>
</head>
<body class="bg-[var(--bg-primary)] text-[var(--text-primary)]">
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-6 flex flex-col min-h-screen">
<header class="flex justify-between items-center mb-6 pb-4 border-b border-[var(--border-primary)]">
<h1 class="text-3xl font-bold text-[var(--text-primary)]">
<a href="{{ url_for('recordings.index') }}" class="flex items-center">
<img src="{{ url_for('static', filename='img/logo-dictia.png') }}" alt="DictIA" class="h-14 w-14 mr-3">
DictIA
</a>
</h1>
</header>
{% block content %}
<section class="min-h-[calc(100vh-62px)] bg-brand-bg py-16 px-4" aria-labelledby="forgot-title">
<div class="max-w-md mx-auto bg-white p-8 rounded-[18px] border border-brand-border shadow-cta">
<h1 id="forgot-title" class="text-3xl font-black text-brand-navy mb-2">Mot de passe oublié</h1>
<p class="text-sm text-brand-navy/70 mb-6">{{ "Entrez votre adresse courriel. Si un compte existe, nous vous enverrons un lien sécurisé pour réinitialiser votre mot de passe (valide 1&nbsp;heure)." | safe }}</p>
<main class="flex-grow flex items-center justify-center">
<div class="w-full max-w-md bg-[var(--bg-secondary)] p-8 rounded-xl shadow-lg border border-[var(--border-primary)]">
<h2 class="text-2xl font-semibold text-[var(--text-primary)] mb-2 text-center">Forgot Password</h2>
<p class="text-[var(--text-muted)] text-sm text-center mb-6">
Enter your email address and we'll send you a link to reset your password.
</p>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div role="alert" class="mb-3 p-3 rounded-lg text-sm
{% if category == 'danger' %}bg-red-50 text-red-900 border border-red-200
{% elif category == 'warning' %}bg-amber-50 text-amber-900 border border-amber-200
{% elif category == 'success' %}bg-green-50 text-green-900 border border-green-200
{% else %}bg-blue-50 text-blue-900 border border-blue-200{% endif %}">
{{ message | safe }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="mb-4 p-3 rounded-lg {% if category == 'success' %}bg-[var(--bg-success-light)] text-[var(--text-success-strong)]{% elif category == 'danger' %}bg-[var(--bg-danger-light)] text-[var(--text-danger-strong)]{% elif category == 'warning' %}bg-[var(--bg-warning-light)] text-[var(--text-warning-strong)]{% else %}bg-[var(--bg-info-light)] text-[var(--text-info-strong)]{% endif %}">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST" action="{{ url_for('auth.forgot_password') }}" class="space-y-4" novalidate>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<form method="POST" action="{{ url_for('auth.forgot_password') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div>
<label for="email" class="block text-sm font-medium text-brand-navy mb-1">Courriel <span class="text-red-600" aria-hidden="true">*</span></label>
<input type="email" id="email" name="email" autocomplete="email" required aria-required="true"
class="w-full px-3 py-2 border border-brand-border rounded-[0.5rem] text-brand-navy focus-visible:outline-2 focus-visible:outline-brand-b1 focus-visible:outline-offset-2"
placeholder="vous@cabinet.qc.ca">
</div>
<div class="mb-6">
<label for="email" class="block text-sm font-medium text-[var(--text-secondary)] mb-1">Email Address</label>
<input type="email" id="email" name="email" required
class="mt-1 block w-full rounded-md border-[var(--border-secondary)] shadow-sm focus:border-[var(--border-focus)] focus:ring-[var(--ring-focus)] focus:ring-opacity-50 bg-[var(--bg-input)] text-[var(--text-primary)]"
placeholder="Enter your email address">
</div>
<button type="submit" class="w-full grad-bg text-white font-semibold py-3 rounded-[0.75rem] shadow-cta hover:shadow-cta-hover transition focus-visible:outline-2 focus-visible:outline-brand-b1 focus-visible:outline-offset-2">
Recevoir un lien de réinitialisation
</button>
</form>
<div class="flex flex-col space-y-4">
<button type="submit" class="w-full py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-[var(--text-button)] bg-[var(--bg-button)] hover:bg-[var(--bg-button-hover)] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[var(--border-focus)]">
<i class="fas fa-paper-plane mr-2"></i> Send Reset Link
</button>
<div class="text-center text-sm text-[var(--text-muted)]">
<span>Remember your password?</span>
<a href="{{ url_for('auth.login') }}" class="font-medium text-[var(--text-accent)] hover:underline">Back to Login</a>
</div>
</div>
</form>
</div>
</main>
<footer class="text-center py-4 mt-8 text-xs text-[var(--text-light)] border-t border-[var(--border-primary)]">
<div>&copy; {{ now.year }} InnovA AI &middot; <a href="/politique-confidentialite" class="underline hover:text-[var(--text-primary)]">Politique de confidentialit&eacute;</a> &middot; <a href="/conditions-utilisation" class="underline hover:text-[var(--text-primary)]">Conditions d'utilisation</a></div>
</footer>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (window.AppLoader) {
AppLoader.waitForReady();
}
});
</script>
</body>
</html>
<p class="text-center text-sm text-brand-navy/70 mt-6">
<a href="{{ url_for('auth.login') }}" class="grad-text font-semibold">&larr; Retour à la connexion</a>
</p>
</div>
</section>
{% endblock %}

View File

@@ -1,114 +1,54 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex, nofollow, noarchive, nosnippet, noimageindex">
<title>{{ title }} - DictIA</title>
<link rel="icon" href="{{ url_for('static', filename='img/favicon.ico') }}" type="image/svg+xml">
<script src="{{ url_for('static', filename='vendor/js/tailwind.min.js') }}"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='vendor/css/fontawesome.min.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
{% extends 'marketing/base.html' %}
{% include 'includes/loading_overlay.html' %}
{% block title %}Nouveau mot de passe — DictIA{% endblock %}
{% block description %}Définissez un nouveau mot de passe pour votre compte DictIA. Lien sécurisé valide 1 heure.{% endblock %}
<script>
function applyTheme() {
if (!document.documentElement) return;
const savedMode = localStorage.getItem('darkMode');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (savedMode === 'true' || (savedMode === null && prefersDark)) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
const savedScheme = localStorage.getItem('colorScheme') || 'blue';
const isDark = document.documentElement.classList.contains('dark');
const themePrefix = isDark ? 'theme-dark-' : 'theme-light-';
const themeClasses = ['blue', 'emerald', 'purple', 'rose', 'amber', 'teal'];
themeClasses.forEach(theme => {
document.documentElement.classList.remove(`theme-light-${theme}`);
document.documentElement.classList.remove(`theme-dark-${theme}`);
});
if (savedScheme !== 'blue') {
document.documentElement.classList.add(themePrefix + savedScheme);
}
}
applyTheme();
</script>
</head>
<body class="bg-[var(--bg-primary)] text-[var(--text-primary)]">
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-6 flex flex-col min-h-screen">
<header class="flex justify-between items-center mb-6 pb-4 border-b border-[var(--border-primary)]">
<h1 class="text-3xl font-bold text-[var(--text-primary)]">
<a href="{{ url_for('recordings.index') }}" class="flex items-center">
<img src="{{ url_for('static', filename='img/logo-dictia.png') }}" alt="DictIA" class="h-14 w-14 mr-3">
DictIA
</a>
</h1>
</header>
{% block content %}
<section class="min-h-[calc(100vh-62px)] bg-brand-bg py-16 px-4" aria-labelledby="reset-title">
<div class="max-w-md mx-auto bg-white p-8 rounded-[18px] border border-brand-border shadow-cta">
<h1 id="reset-title" class="text-3xl font-black text-brand-navy mb-2">Nouveau mot de passe</h1>
<p class="text-sm text-brand-navy/70 mb-6">Choisissez un mot de passe robuste pour sécuriser votre compte DictIA.</p>
<main class="flex-grow flex items-center justify-center">
<div class="w-full max-w-md bg-[var(--bg-secondary)] p-8 rounded-xl shadow-lg border border-[var(--border-primary)]">
<h2 class="text-2xl font-semibold text-[var(--text-primary)] mb-2 text-center">Reset Password</h2>
<p class="text-[var(--text-muted)] text-sm text-center mb-6">
Enter your new password below.
</p>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div role="alert" class="mb-3 p-3 rounded-lg text-sm
{% if category == 'danger' %}bg-red-50 text-red-900 border border-red-200
{% elif category == 'warning' %}bg-amber-50 text-amber-900 border border-amber-200
{% elif category == 'success' %}bg-green-50 text-green-900 border border-green-200
{% else %}bg-blue-50 text-blue-900 border border-blue-200{% endif %}">
{{ message | safe }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="mb-4 p-3 rounded-lg {% if category == 'success' %}bg-[var(--bg-success-light)] text-[var(--text-success-strong)]{% elif category == 'danger' %}bg-[var(--bg-danger-light)] text-[var(--text-danger-strong)]{% elif category == 'warning' %}bg-[var(--bg-warning-light)] text-[var(--text-warning-strong)]{% else %}bg-[var(--bg-info-light)] text-[var(--text-info-strong)]{% endif %}">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST" action="{{ url_for('auth.reset_password', token=token) }}" class="space-y-4" novalidate>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<form method="POST" action="{{ url_for('auth.reset_password', token=token) }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div>
<label for="password" class="block text-sm font-medium text-brand-navy mb-1">Nouveau mot de passe <span class="text-red-600" aria-hidden="true">*</span></label>
<input type="password" id="password" name="password" autocomplete="new-password" minlength="8" required aria-required="true" aria-describedby="password-help"
class="w-full px-3 py-2 border border-brand-border rounded-[0.5rem] text-brand-navy focus-visible:outline-2 focus-visible:outline-brand-b1 focus-visible:outline-offset-2"
placeholder="••••••••">
<p id="password-help" class="text-xs text-brand-navy/70 mt-1">{{ "8&nbsp;caractères minimum, dont une majuscule, une minuscule, un chiffre et un caractère spécial." | safe }}</p>
</div>
<div class="mb-4">
<label for="password" class="block text-sm font-medium text-[var(--text-secondary)] mb-1">New Password</label>
<input type="password" id="password" name="password" required
class="mt-1 block w-full rounded-md border-[var(--border-secondary)] shadow-sm focus:border-[var(--border-focus)] focus:ring-[var(--ring-focus)] focus:ring-opacity-50 bg-[var(--bg-input)] text-[var(--text-primary)]"
placeholder="Enter your new password">
<p class="text-xs text-[var(--text-muted)] mt-1">Password must be at least 8 characters long.</p>
</div>
<div>
<label for="confirm_password" class="block text-sm font-medium text-brand-navy mb-1">Confirmer le mot de passe <span class="text-red-600" aria-hidden="true">*</span></label>
<input type="password" id="confirm_password" name="confirm_password" autocomplete="new-password" minlength="8" required aria-required="true"
class="w-full px-3 py-2 border border-brand-border rounded-[0.5rem] text-brand-navy focus-visible:outline-2 focus-visible:outline-brand-b1 focus-visible:outline-offset-2"
placeholder="••••••••">
</div>
<div class="mb-6">
<label for="confirm_password" class="block text-sm font-medium text-[var(--text-secondary)] mb-1">Confirm Password</label>
<input type="password" id="confirm_password" name="confirm_password" required
class="mt-1 block w-full rounded-md border-[var(--border-secondary)] shadow-sm focus:border-[var(--border-focus)] focus:ring-[var(--ring-focus)] focus:ring-opacity-50 bg-[var(--bg-input)] text-[var(--text-primary)]"
placeholder="Confirm your new password">
</div>
<button type="submit" class="w-full grad-bg text-white font-semibold py-3 rounded-[0.75rem] shadow-cta hover:shadow-cta-hover transition focus-visible:outline-2 focus-visible:outline-brand-b1 focus-visible:outline-offset-2">
Définir mon nouveau mot de passe
</button>
</form>
<div class="flex flex-col space-y-4">
<button type="submit" class="w-full py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-[var(--text-button)] bg-[var(--bg-button)] hover:bg-[var(--bg-button-hover)] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[var(--border-focus)]">
<i class="fas fa-key mr-2"></i> Reset Password
</button>
<div class="text-center text-sm text-[var(--text-muted)]">
<a href="{{ url_for('auth.login') }}" class="font-medium text-[var(--text-accent)] hover:underline">
<i class="fas fa-arrow-left mr-1"></i> Back to Login
</a>
</div>
</div>
</form>
</div>
</main>
<footer class="text-center py-4 mt-8 text-xs text-[var(--text-light)] border-t border-[var(--border-primary)]">
<div>&copy; {{ now.year }} InnovA AI &middot; <a href="/politique-confidentialite" class="underline hover:text-[var(--text-primary)]">Politique de confidentialit&eacute;</a> &middot; <a href="/conditions-utilisation" class="underline hover:text-[var(--text-primary)]">Conditions d'utilisation</a></div>
</footer>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (window.AppLoader) {
AppLoader.waitForReady();
}
});
</script>
</body>
</html>
<p class="text-center text-sm text-brand-navy/70 mt-6">
<a href="{{ url_for('auth.login') }}" class="grad-text font-semibold">&larr; Retour à la connexion</a>
</p>
</div>
</section>
{% endblock %}

View File

@@ -1,85 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex, nofollow, noarchive, nosnippet, noimageindex">
<title>{{ title }} - DictIA</title>
<link rel="icon" href="{{ url_for('static', filename='img/favicon.ico') }}" type="image/svg+xml">
<script src="{{ url_for('static', filename='vendor/js/tailwind.min.js') }}"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='vendor/css/fontawesome.min.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
{% extends 'marketing/base.html' %}
{% include 'includes/loading_overlay.html' %}
{% block title %}Courriel vérifié — DictIA{% endblock %}
{% block description %}Votre courriel a été vérifié. Vous pouvez maintenant vous connecter à votre compte DictIA.{% endblock %}
<script>
function applyTheme() {
if (!document.documentElement) return;
const savedMode = localStorage.getItem('darkMode');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (savedMode === 'true' || (savedMode === null && prefersDark)) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
const savedScheme = localStorage.getItem('colorScheme') || 'blue';
const isDark = document.documentElement.classList.contains('dark');
const themePrefix = isDark ? 'theme-dark-' : 'theme-light-';
const themeClasses = ['blue', 'emerald', 'purple', 'rose', 'amber', 'teal'];
themeClasses.forEach(theme => {
document.documentElement.classList.remove(`theme-light-${theme}`);
document.documentElement.classList.remove(`theme-dark-${theme}`);
});
if (savedScheme !== 'blue') {
document.documentElement.classList.add(themePrefix + savedScheme);
}
}
applyTheme();
</script>
</head>
<body class="bg-[var(--bg-primary)] text-[var(--text-primary)]">
<div class="container mx-auto px-4 sm:px-6 lg:px-8 py-6 flex flex-col min-h-screen">
<header class="flex justify-between items-center mb-6 pb-4 border-b border-[var(--border-primary)]">
<h1 class="text-3xl font-bold text-[var(--text-primary)]">
<a href="{{ url_for('recordings.index') }}" class="flex items-center">
<img src="{{ url_for('static', filename='img/logo-dictia.png') }}" alt="DictIA" class="h-14 w-14 mr-3">
DictIA
</a>
</h1>
</header>
{% block content %}
<section class="min-h-[calc(100vh-62px)] bg-brand-bg py-16 px-4" aria-labelledby="verify-success-title">
<div class="max-w-md mx-auto bg-white p-8 rounded-[18px] border border-brand-border shadow-cta text-center">
<div class="mx-auto mb-6 w-16 h-16 rounded-full bg-green-100 text-green-700 flex items-center justify-center text-3xl font-black" aria-hidden="true">&check;</div>
<main class="flex-grow flex items-center justify-center">
<div class="w-full max-w-md bg-[var(--bg-secondary)] p-8 rounded-xl shadow-lg border border-[var(--border-primary)]">
<div class="text-center">
<div class="mb-6">
<div class="w-20 h-20 mx-auto bg-[var(--bg-success-light)] rounded-full flex items-center justify-center">
<i class="fas fa-check text-[var(--text-success-strong)] text-3xl"></i>
</div>
</div>
<h1 id="verify-success-title" class="text-2xl font-black text-brand-navy mb-2">Votre courriel a été vérifié</h1>
<p class="text-sm text-brand-navy/70 mb-6">
Vous pouvez maintenant vous connecter à votre compte DictIA et commencer à transcrire en toute conformité Loi&nbsp;25.
</p>
<h2 class="text-2xl font-semibold text-[var(--text-primary)] mb-4">Email Verified!</h2>
<p class="text-[var(--text-secondary)] mb-6">
Your email address has been successfully verified. You can now log in to your account.
</p>
<a href="{{ url_for('auth.login') }}"
class="inline-block w-full grad-bg text-white font-semibold py-3 rounded-[0.75rem] shadow-cta hover:shadow-cta-hover transition focus-visible:outline-2 focus-visible:outline-brand-b1 focus-visible:outline-offset-2">
Se connecter
</a>
<a href="{{ url_for('auth.login') }}" class="inline-block w-full py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-[var(--text-button)] bg-[var(--bg-button)] hover:bg-[var(--bg-button-hover)] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[var(--border-focus)] transition-colors duration-200">
<i class="fas fa-sign-in-alt mr-2"></i> Continue to Login
</a>
</div>
</div>
</main>
<footer class="text-center py-4 mt-8 text-xs text-[var(--text-light)] border-t border-[var(--border-primary)]">
<div>&copy; {{ now.year }} InnovA AI &middot; <a href="/politique-confidentialite" class="underline hover:text-[var(--text-primary)]">Politique de confidentialit&eacute;</a> &middot; <a href="/conditions-utilisation" class="underline hover:text-[var(--text-primary)]">Conditions d'utilisation</a></div>
</footer>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (window.AppLoader) {
AppLoader.waitForReady();
}
});
</script>
</body>
</html>
<p class="text-xs text-brand-navy/70 mt-6">
Une question&nbsp;? Écrivez-nous à
<a href="mailto:info@dictia.ca" class="grad-text font-semibold">info@dictia.ca</a>.
</p>
</div>
</section>
{% endblock %}