Internal
Public Access
Implement comprehensive email verification and admin approval system for user registration. Users must verify their email before logging in, and admins must approve new users before they can access the system. Major features: - Email verification with UUID tokens (24hr expiry, one-time use) - Admin approval workflow via Django admin interface - Custom authentication backend enforcing verification and approval - Password change functionality for authenticated users - Enhanced profile page with proper styling and theme support - Collect first name, last name, and timezone during registration - Profile link added to header navigation Email notifications: - Verification email sent after registration - Admin notification when users need approval - Approval notification sent to users Security features: - Cryptographically secure UUID tokens - Token expiration and one-time use enforcement - Email enumeration protection - Custom JWT token validation for API access - Existing users auto-approved via data migration Templates added: - Email templates (verification, approval, admin notification) - Web templates (password change, verification pages) - Enhanced profile page with dark/light mode support 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
88 lines
5.2 KiB
HTML
88 lines
5.2 KiB
HTML
{% extends 'base.html' %}
|
|
|
|
{% block title %}Change Password - KeepItGoing{% endblock %}
|
|
|
|
{% block body %}
|
|
<div class="app-layout" style="display: flex; min-height: 100vh;">
|
|
<!-- Simple header for non-dashboard pages -->
|
|
<div style="position: fixed; top: 0; left: 0; right: 0; background: var(--color-bg-secondary, #fff); border-bottom: 1px solid var(--color-border, #e5e7eb); padding: 1rem 2rem; z-index: 100; display: flex; justify-content: space-between; align-items: center;">
|
|
<a href="{% url 'dashboard' %}" style="font-size: 1.25rem; font-weight: 600; text-decoration: none; color: var(--color-text, #111);">KeepItGoing</a>
|
|
<div style="display: flex; gap: 1rem; align-items: center;">
|
|
<span style="color: var(--color-text-secondary, #666);">{{ user.email }}</span>
|
|
<a href="{% url 'profile' %}" style="color: var(--color-primary, #3B82F6); text-decoration: none;">Profile</a>
|
|
<a href="{% url 'logout' %}" style="color: var(--color-text-secondary, #666); text-decoration: none;">Logout</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Main content -->
|
|
<div style="flex: 1; padding: 5rem 2rem 2rem; max-width: 600px; margin: 0 auto; width: 100%;">
|
|
<h1 style="font-size: 2rem; font-weight: 700; margin-bottom: 2rem; color: var(--color-text, #111);">Change Password</h1>
|
|
|
|
{% if success %}
|
|
<div style="background-color: #d1fae5; border: 1px solid #6ee7b7; color: #065f46; padding: 1rem; border-radius: 0.5rem; margin-bottom: 1.5rem;">
|
|
<strong>✓ Success!</strong><br>
|
|
{{ success }}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<form method="post" style="background: var(--color-bg-secondary, #fff); padding: 2rem; border-radius: 0.5rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
|
|
{% csrf_token %}
|
|
|
|
<div style="margin-bottom: 1.5rem;">
|
|
<label for="old_password" style="display: block; margin-bottom: 0.5rem; font-weight: 500; color: var(--color-text, #111);">Current Password</label>
|
|
<input type="password"
|
|
id="old_password"
|
|
name="old_password"
|
|
required
|
|
style="width: 100%; padding: 0.75rem; border: 1px solid var(--color-border, #d1d5db); border-radius: 0.375rem; font-size: 1rem;">
|
|
{% if errors.old_password %}
|
|
<p style="color: #dc2626; margin-top: 0.25rem; font-size: 0.875rem;">{{ errors.old_password }}</p>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div style="margin-bottom: 1.5rem;">
|
|
<label for="new_password" style="display: block; margin-bottom: 0.5rem; font-weight: 500; color: var(--color-text, #111);">New Password</label>
|
|
<input type="password"
|
|
id="new_password"
|
|
name="new_password"
|
|
required
|
|
minlength="8"
|
|
style="width: 100%; padding: 0.75rem; border: 1px solid var(--color-border, #d1d5db); border-radius: 0.375rem; font-size: 1rem;">
|
|
<p style="color: var(--color-text-secondary, #666); margin-top: 0.25rem; font-size: 0.875rem;">Must be at least 8 characters</p>
|
|
{% if errors.new_password %}
|
|
<p style="color: #dc2626; margin-top: 0.25rem; font-size: 0.875rem;">{{ errors.new_password }}</p>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div style="margin-bottom: 1.5rem;">
|
|
<label for="confirm_password" style="display: block; margin-bottom: 0.5rem; font-weight: 500; color: var(--color-text, #111);">Confirm New Password</label>
|
|
<input type="password"
|
|
id="confirm_password"
|
|
name="confirm_password"
|
|
required
|
|
minlength="8"
|
|
style="width: 100%; padding: 0.75rem; border: 1px solid var(--color-border, #d1d5db); border-radius: 0.375rem; font-size: 1rem;">
|
|
{% if errors.confirm_password %}
|
|
<p style="color: #dc2626; margin-top: 0.25rem; font-size: 0.875rem;">{{ errors.confirm_password }}</p>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div style="display: flex; gap: 1rem;">
|
|
<button type="submit"
|
|
style="flex: 1; background-color: #3B82F6; color: white; padding: 0.75rem 1.5rem; border: none; border-radius: 0.375rem; font-size: 1rem; font-weight: 600; cursor: pointer;">
|
|
Change Password
|
|
</button>
|
|
<a href="{% url 'profile' %}"
|
|
style="flex: 1; background-color: var(--color-bg, #f3f4f6); color: var(--color-text, #111); padding: 0.75rem 1.5rem; border: none; border-radius: 0.375rem; font-size: 1rem; font-weight: 600; text-decoration: none; text-align: center; display: flex; align-items: center; justify-content: center;">
|
|
Cancel
|
|
</a>
|
|
</div>
|
|
</form>
|
|
|
|
<div style="margin-top: 2rem; text-align: center;">
|
|
<a href="{% url 'dashboard' %}" style="color: var(--color-text-secondary, #666); text-decoration: none; font-size: 0.875rem;">← Back to Dashboard</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|