feat: email verification, merchant/store password reset, seed gap fix
Some checks failed
CI / ruff (push) Successful in 10s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has been cancelled

- Add EmailVerificationToken and UserPasswordResetToken models with migration
- Add email verification flow: verify-email page route, resend-verification API
- Block login for unverified users (EmailNotVerifiedException in auth_service)
- Add forgot-password/reset-password endpoints for merchant and store auth
- Add "Forgot Password?" links to merchant and store login pages
- Send welcome email with verification link on merchant creation
- Seed email_verification and merchant_password_reset email templates
- Fix db-reset Makefile to run all init-prod seed scripts
- Add UserAuthService to satisfy architecture validation rules
- Add 52 new tests (unit + integration) with full coverage

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-18 23:22:46 +01:00
parent a8b29750a5
commit d9fc52d47a
30 changed files with 2574 additions and 29 deletions

View File

@@ -86,18 +86,48 @@
<hr class="my-8" />
<p class="mt-4">
<a class="text-sm font-medium text-purple-600 dark:text-purple-400 hover:underline"
href="#">
Forgot your password?
</a>
</p>
<p class="mt-2">
<a class="text-sm font-medium text-gray-600 dark:text-gray-400 hover:underline"
href="/">
&larr; Back to Platform
</a>
</p>
<!-- Forgot Password Form -->
<div x-show="showForgotPassword" x-transition>
<h2 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">Reset Password</h2>
<p class="mb-4 text-sm text-gray-600 dark:text-gray-400">Enter your email address and we'll send you a link to reset your password.</p>
<form @submit.prevent="handleForgotPassword">
<label class="block text-sm">
<span class="text-gray-700 dark:text-gray-400">Email</span>
<input x-model="forgotPasswordEmail"
:disabled="forgotPasswordLoading"
class="block w-full mt-1 text-sm dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:text-gray-300 dark:focus:shadow-outline-gray form-input"
placeholder="you@example.com"
type="email"
required />
</label>
<button type="submit" :disabled="forgotPasswordLoading"
class="block w-full px-4 py-2 mt-4 text-sm font-medium leading-5 text-center text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple disabled:opacity-50">
<span x-show="!forgotPasswordLoading">Send Reset Link</span>
<span x-show="forgotPasswordLoading">Sending...</span>
</button>
</form>
<p class="mt-4">
<a @click.prevent="showForgotPassword = false"
class="text-sm font-medium text-purple-600 dark:text-purple-400 hover:underline cursor-pointer">
&larr; Back to Login
</a>
</p>
</div>
<div x-show="!showForgotPassword">
<p class="mt-4">
<a @click.prevent="showForgotPassword = true"
class="text-sm font-medium text-purple-600 dark:text-purple-400 hover:underline cursor-pointer">
Forgot your password?
</a>
</p>
<p class="mt-2">
<a class="text-sm font-medium text-gray-600 dark:text-gray-400 hover:underline"
href="/">
&larr; Back to Platform
</a>
</p>
</div>
</div>
</div>
</div>

View File

@@ -119,18 +119,48 @@
<hr class="my-8" />
<p class="mt-4">
<a class="text-sm font-medium text-purple-600 dark:text-purple-400 hover:underline"
href="#">
Forgot your password?
</a>
</p>
<p class="mt-2">
<a class="text-sm font-medium text-gray-600 dark:text-gray-400 hover:underline"
href="/">
← Back to Platform
</a>
</p>
<!-- Forgot Password Form -->
<div x-show="showForgotPassword" x-transition>
<h2 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">Reset Password</h2>
<p class="mb-4 text-sm text-gray-600 dark:text-gray-400">Enter your email address and we'll send you a link to reset your password.</p>
<form @submit.prevent="handleForgotPassword">
<label class="block text-sm">
<span class="text-gray-700 dark:text-gray-400">Email</span>
<input x-model="forgotPasswordEmail"
:disabled="forgotPasswordLoading"
class="block w-full mt-1 text-sm dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:text-gray-300 dark:focus:shadow-outline-gray form-input"
placeholder="you@example.com"
type="email"
required />
</label>
<button type="submit" :disabled="forgotPasswordLoading"
class="block w-full px-4 py-2 mt-4 text-sm font-medium leading-5 text-center text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple disabled:opacity-50">
<span x-show="!forgotPasswordLoading">Send Reset Link</span>
<span x-show="forgotPasswordLoading">Sending...</span>
</button>
</form>
<p class="mt-4">
<a @click.prevent="showForgotPassword = false"
class="text-sm font-medium text-purple-600 dark:text-purple-400 hover:underline cursor-pointer">
&larr; Back to Login
</a>
</p>
</div>
<div x-show="!showForgotPassword">
<p class="mt-4">
<a @click.prevent="showForgotPassword = true"
class="text-sm font-medium text-purple-600 dark:text-purple-400 hover:underline cursor-pointer">
Forgot your password?
</a>
</p>
<p class="mt-2">
<a class="text-sm font-medium text-gray-600 dark:text-gray-400 hover:underline"
href="/">
&larr; Back to Platform
</a>
</p>
</div>
</div>
</div>
</div>