Add password reset flow with email-based token verification
Implement secure password reset functionality: - Add password_reset_tokens table for storing hashed reset tokens - Create email utility module (dev mode logs to console) - Add API endpoints: request, validate, and complete password reset - Add web routes and templates for forgot-password and reset-password - Security: 1-hour token expiry, single-use, rate limiting, session invalidation - Prevent email enumeration by always returning success message Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "components/forms.html" import text_input, button_primary, form_errors %}
|
||||
|
||||
{% block title %}Reset Password - CmdForge{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="min-h-[70vh] flex items-center justify-center px-4 py-12">
|
||||
<div class="w-full max-w-md">
|
||||
<div class="text-center mb-8">
|
||||
<a href="{{ url_for('web.home') }}" class="inline-flex items-center justify-center">
|
||||
<svg class="w-10 h-10 text-indigo-600" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<h1 class="mt-4 text-2xl font-bold text-gray-900">Reset your password</h1>
|
||||
<p class="mt-2 text-gray-600">
|
||||
Enter your email address and we'll send you a link to reset your password.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-8 shadow-sm">
|
||||
{% if success_message %}
|
||||
<div class="mb-6 p-4 bg-green-50 border border-green-200 rounded-lg">
|
||||
<div class="flex">
|
||||
<svg class="w-5 h-5 text-green-500 mr-3 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
<div>
|
||||
<p class="text-sm text-green-800">{{ success_message }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
{{ form_errors(errors) }}
|
||||
|
||||
<form action="{{ url_for('web.forgot_password') }}" method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
{{ text_input(
|
||||
name='email',
|
||||
label='Email address',
|
||||
type='email',
|
||||
placeholder='[email protected]',
|
||||
required=true,
|
||||
value=email or ''
|
||||
) }}
|
||||
|
||||
{{ button_primary('Send reset link', full_width=true) }}
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<p class="mt-6 text-center text-sm text-gray-600">
|
||||
Remember your password?
|
||||
<a href="{{ url_for('web.login') }}" class="text-indigo-600 hover:text-indigo-800 font-medium">
|
||||
Sign in
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,85 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "components/forms.html" import text_input, button_primary, form_errors %}
|
||||
|
||||
{% block title %}Set New Password - CmdForge{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="min-h-[70vh] flex items-center justify-center px-4 py-12">
|
||||
<div class="w-full max-w-md">
|
||||
<div class="text-center mb-8">
|
||||
<a href="{{ url_for('web.home') }}" class="inline-flex items-center justify-center">
|
||||
<svg class="w-10 h-10 text-indigo-600" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<h1 class="mt-4 text-2xl font-bold text-gray-900">Set new password</h1>
|
||||
{% if token_valid %}
|
||||
<p class="mt-2 text-gray-600">
|
||||
Enter your new password below.
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-8 shadow-sm">
|
||||
{% if success_message %}
|
||||
<div class="mb-6 p-4 bg-green-50 border border-green-200 rounded-lg">
|
||||
<div class="flex">
|
||||
<svg class="w-5 h-5 text-green-500 mr-3 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
<div>
|
||||
<p class="text-sm text-green-800">{{ success_message }}</p>
|
||||
<p class="mt-2 text-sm text-green-700">
|
||||
<a href="{{ url_for('web.login') }}" class="font-medium underline hover:text-green-800">
|
||||
Sign in with your new password
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% elif not token_valid %}
|
||||
{{ form_errors(errors) }}
|
||||
<div class="text-center">
|
||||
<p class="text-gray-600 mb-4">Need to reset your password?</p>
|
||||
<a href="{{ url_for('web.forgot_password') }}" class="inline-flex items-center justify-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
||||
Request new reset link
|
||||
</a>
|
||||
</div>
|
||||
{% else %}
|
||||
{{ form_errors(errors) }}
|
||||
|
||||
<form action="{{ url_for('web.reset_password') }}" method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="token" value="{{ token }}">
|
||||
|
||||
{{ text_input(
|
||||
name='new_password',
|
||||
label='New password',
|
||||
type='password',
|
||||
placeholder='At least 8 characters',
|
||||
required=true,
|
||||
help='Use a mix of letters, numbers, and symbols'
|
||||
) }}
|
||||
|
||||
{{ text_input(
|
||||
name='confirm_password',
|
||||
label='Confirm new password',
|
||||
type='password',
|
||||
placeholder='Confirm your new password',
|
||||
required=true
|
||||
) }}
|
||||
|
||||
{{ button_primary('Set new password', full_width=true) }}
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<p class="mt-6 text-center text-sm text-gray-600">
|
||||
Remember your password?
|
||||
<a href="{{ url_for('web.login') }}" class="text-indigo-600 hover:text-indigo-800 font-medium">
|
||||
Sign in
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user