From 21d9d01885b6522621b33f3e44df86f90b711bb8 Mon Sep 17 00:00:00 2001 From: Keith Smith Date: Mon, 22 Dec 2025 18:13:38 -0700 Subject: [PATCH] Add email verification, admin approval, and user profile enhancements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- config/settings/base.py | 11 + tasks/serializers.py | 9 +- templates/base.html | 1 + templates/emails/account_approved.html | 34 ++ templates/emails/account_approved.txt | 14 + templates/emails/admin_new_user.html | 53 +++ templates/emails/admin_new_user.txt | 14 + templates/emails/verify_email.html | 39 ++ templates/emails/verify_email.txt | 14 + templates/users/change_password.html | 87 +++++ templates/users/login.html | 14 +- templates/users/profile.html | 136 ++++--- templates/users/register.html | 31 +- templates/users/register_success.html | 40 ++ templates/users/resend_verification.html | 55 +++ templates/users/verify_email.html | 69 ++++ users/admin.py | 56 ++- users/backends.py | 45 +++ ...r_approved_at_user_approved_by_and_more.py | 87 +++++ users/models.py | 41 ++ users/serializers.py | 2 +- users/urls.py | 12 +- users/utils.py | 128 +++++++ users/views.py | 350 +++++++++++++++++- 24 files changed, 1269 insertions(+), 73 deletions(-) create mode 100644 templates/emails/account_approved.html create mode 100644 templates/emails/account_approved.txt create mode 100644 templates/emails/admin_new_user.html create mode 100644 templates/emails/admin_new_user.txt create mode 100644 templates/emails/verify_email.html create mode 100644 templates/emails/verify_email.txt create mode 100644 templates/users/change_password.html create mode 100644 templates/users/register_success.html create mode 100644 templates/users/resend_verification.html create mode 100644 templates/users/verify_email.html create mode 100644 users/backends.py create mode 100644 users/migrations/0002_user_approved_at_user_approved_by_and_more.py create mode 100644 users/utils.py diff --git a/config/settings/base.py b/config/settings/base.py index 1ee4f32..278894c 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -68,6 +68,11 @@ WSGI_APPLICATION = 'config.wsgi.application' # Custom user model AUTH_USER_MODEL = 'users.User' +# Authentication backends +AUTHENTICATION_BACKENDS = [ + 'users.backends.EmailVerifiedApprovedBackend', +] + # Password validation AUTH_PASSWORD_VALIDATORS = [ { @@ -147,3 +152,9 @@ USAGE_LIMITS_ENABLED = SAAS_MODE LOGIN_URL = '/login/' LOGIN_REDIRECT_URL = '/' LOGOUT_REDIRECT_URL = '/login/' + +# Email verification settings +EMAIL_VERIFICATION_TOKEN_EXPIRY_HOURS = int( + os.environ.get('EMAIL_VERIFICATION_TOKEN_EXPIRY_HOURS', 24) +) +SITE_DOMAIN = os.environ.get('SITE_DOMAIN', 'localhost:8000') diff --git a/tasks/serializers.py b/tasks/serializers.py index caa7784..d123a2c 100644 --- a/tasks/serializers.py +++ b/tasks/serializers.py @@ -125,13 +125,14 @@ class TaskSyncSerializer(serializers.ModelSerializer): """Serializer for tasks in sync responses - uses sync_id as id.""" id = serializers.UUIDField(source='sync_id', read_only=True) + sync_id = serializers.UUIDField(read_only=True) tag_sync_ids = serializers.SerializerMethodField() parent_sync_id = serializers.UUIDField(source='parent.sync_id', read_only=True, allow_null=True) class Meta: model = Task fields = [ - 'id', 'parent', 'parent_sync_id', + 'id', 'sync_id', 'parent', 'parent_sync_id', 'title', 'description', 'status', 'priority', 'due_date', 'due_time', 'reminder_at', 'completed_at', 'recurrence', 'recurrence_rule', 'recurrence_end_date', 'tag_sync_ids', @@ -147,10 +148,11 @@ class TagSyncSerializer(serializers.ModelSerializer): """Serializer for tags in sync responses - uses sync_id as id.""" id = serializers.UUIDField(source='sync_id', read_only=True) + sync_id = serializers.UUIDField(read_only=True) class Meta: model = Tag - fields = ['id', 'name', 'description', 'color', 'icon', 'sort_order', + fields = ['id', 'sync_id', 'name', 'description', 'color', 'icon', 'sort_order', 'is_archived', 'is_deleted', 'created_at', 'updated_at'] @@ -158,11 +160,12 @@ class TimeEntrySyncSerializer(serializers.ModelSerializer): """Serializer for time entries in sync responses - uses sync_id as id.""" id = serializers.UUIDField(source='sync_id', read_only=True) + sync_id = serializers.UUIDField(read_only=True) task = serializers.UUIDField(source='task.sync_id', read_only=True) class Meta: model = TimeEntry - fields = ['id', 'task', 'started_at', 'ended_at', 'notes', + fields = ['id', 'sync_id', 'task', 'started_at', 'ended_at', 'notes', 'is_deleted', 'created_at', 'updated_at'] diff --git a/templates/base.html b/templates/base.html index 52a5b32..39b79b6 100644 --- a/templates/base.html +++ b/templates/base.html @@ -35,6 +35,7 @@
{{ user.email }} + Profile Logout
diff --git a/templates/emails/account_approved.html b/templates/emails/account_approved.html new file mode 100644 index 0000000..0fbec7d --- /dev/null +++ b/templates/emails/account_approved.html @@ -0,0 +1,34 @@ + + + + + + + +
+

Your Account Has Been Approved!

+ +

Hi {{ user.first_name|default:user.username }},

+ +

Great news! Your KeepItGoing account has been approved by our team.

+ +

You can now log in and start using KeepItGoing to manage your tasks and stay organized.

+ +
+ + Log In Now + +
+ +

We're excited to have you on board!

+ +
+ +

+ If you have any questions, please don't hesitate to reach out to our support team. +

+
+ + diff --git a/templates/emails/account_approved.txt b/templates/emails/account_approved.txt new file mode 100644 index 0000000..a583c63 --- /dev/null +++ b/templates/emails/account_approved.txt @@ -0,0 +1,14 @@ +Your Account Has Been Approved! + +Hi {{ user.first_name|default:user.username }}, + +Great news! Your KeepItGoing account has been approved by our team. + +You can now log in and start using KeepItGoing to manage your tasks and stay organized. + +Log in here: {{ login_url }} + +We're excited to have you on board! + +--- +KeepItGoing diff --git a/templates/emails/admin_new_user.html b/templates/emails/admin_new_user.html new file mode 100644 index 0000000..4a538dc --- /dev/null +++ b/templates/emails/admin_new_user.html @@ -0,0 +1,53 @@ + + + + + + + +
+

New User Registration

+ +

A new user has registered and verified their email address:

+ +
+ + + + + + + + + + {% if user.first_name or user.last_name %} + + + + + {% endif %} + + + + +
Email:{{ user.email }}
Username:{{ user.username }}
Name:{{ user.first_name }} {{ user.last_name }}
Registered:{{ user.created_at|date:"F j, Y, g:i a" }}
+
+ +

Please review and approve this user in the admin panel:

+ +
+ + Review in Admin Panel + +
+ +
+ +

+ This is an automated notification from KeepItGoing. +

+
+ + diff --git a/templates/emails/admin_new_user.txt b/templates/emails/admin_new_user.txt new file mode 100644 index 0000000..2c62a42 --- /dev/null +++ b/templates/emails/admin_new_user.txt @@ -0,0 +1,14 @@ +New User Registration + +A new user has registered and verified their email address: + +Email: {{ user.email }} +Username: {{ user.username }} +{% if user.first_name or user.last_name %}Name: {{ user.first_name }} {{ user.last_name }} +{% endif %}Registered: {{ user.created_at|date:"F j, Y, g:i a" }} + +Please review and approve this user in the admin panel: +{{ admin_url }} + +--- +KeepItGoing (automated notification) diff --git a/templates/emails/verify_email.html b/templates/emails/verify_email.html new file mode 100644 index 0000000..aeaa908 --- /dev/null +++ b/templates/emails/verify_email.html @@ -0,0 +1,39 @@ + + + + + + + +
+

Verify Your Email Address

+ +

Hi{% if user.first_name %} {{ user.first_name }}{% endif %},

+ +

Thank you for registering with KeepItGoing! Please verify your email address by clicking the button below:

+ +
+ + Verify Email Address + +
+ +

Or copy and paste this link into your browser:

+

+ {{ verification_url }} +

+ +

+ This link will expire in {{ expiry_hours }} hours. +

+ +
+ +

+ If you didn't create an account with KeepItGoing, you can safely ignore this email. +

+
+ + diff --git a/templates/emails/verify_email.txt b/templates/emails/verify_email.txt new file mode 100644 index 0000000..68ea0d2 --- /dev/null +++ b/templates/emails/verify_email.txt @@ -0,0 +1,14 @@ +Verify Your Email Address + +Hi{% if user.first_name %} {{ user.first_name }}{% endif %}, + +Thank you for registering with KeepItGoing! Please verify your email address by visiting this link: + +{{ verification_url }} + +This link will expire in {{ expiry_hours }} hours. + +If you didn't create an account with KeepItGoing, you can safely ignore this email. + +--- +KeepItGoing diff --git a/templates/users/change_password.html b/templates/users/change_password.html new file mode 100644 index 0000000..34d1ec1 --- /dev/null +++ b/templates/users/change_password.html @@ -0,0 +1,87 @@ +{% extends 'base.html' %} + +{% block title %}Change Password - KeepItGoing{% endblock %} + +{% block body %} +
+ +
+ KeepItGoing +
+ {{ user.email }} + Profile + Logout +
+
+ + +
+

Change Password

+ + {% if success %} +
+ ✓ Success!
+ {{ success }} +
+ {% endif %} + +
+ {% csrf_token %} + +
+ + + {% if errors.old_password %} +

{{ errors.old_password }}

+ {% endif %} +
+ +
+ + +

Must be at least 8 characters

+ {% if errors.new_password %} +

{{ errors.new_password }}

+ {% endif %} +
+ +
+ + + {% if errors.confirm_password %} +

{{ errors.confirm_password }}

+ {% endif %} +
+ +
+ + + Cancel + +
+
+ +
+ ← Back to Dashboard +
+
+
+{% endblock %} diff --git a/templates/users/login.html b/templates/users/login.html index d76945d..f4820e4 100644 --- a/templates/users/login.html +++ b/templates/users/login.html @@ -7,14 +7,24 @@

Login

{% if error %} -
{{ error }}
+
+ {{ error }} + {% if can_resend %} +

+ + Resend verification email + +

+ {% endif %} +
{% endif %}
{% csrf_token %}
- +
diff --git a/templates/users/profile.html b/templates/users/profile.html index 3d77b1f..fe947d8 100644 --- a/templates/users/profile.html +++ b/templates/users/profile.html @@ -3,75 +3,125 @@ {% block title %}Profile - KeepItGoing{% endblock %} {% block content %} -