feat: add parent PIN setup functionality and email notifications
All checks were successful
Gitea Actions Demo / build-and-push (push) Successful in 23s

- Implemented User model updates to include PIN and related fields.
- Created email sender utility for sending verification and reset emails.
- Developed ParentPinSetup component for setting up a parent PIN with verification code.
- Enhanced UserProfile and EntityEditForm components to support new features.
- Updated routing to include PIN setup and authentication checks.
- Added styles for new components and improved existing styles for consistency.
- Introduced loading states and error handling in various components.
This commit is contained in:
2026-01-27 14:47:49 -05:00
parent cd9070ec99
commit 3066d7d356
19 changed files with 852 additions and 257 deletions

View File

@@ -3,7 +3,7 @@ import secrets, jwt
from datetime import datetime, timedelta, timezone
from models.user import User
from flask import Blueprint, request, jsonify, current_app
from flask_mail import Mail, Message
from backend.utils.email_instance import email_sender
from tinydb import Query
import os
@@ -18,32 +18,15 @@ from db.db import users_db
logger = logging.getLogger(__name__)
auth_api = Blueprint('auth_api', __name__)
UserQuery = Query()
mail = Mail()
TOKEN_EXPIRY_MINUTES = 60*4
RESET_PASSWORD_TOKEN_EXPIRY_MINUTES = 10
def send_verification_email(to_email, token):
verify_url = f"{current_app.config['FRONTEND_URL']}/auth/verify?token={token}"
html_body = f'Click <a href="{verify_url}">here</a> to verify your account.'
msg = Message(
subject="Verify your account",
recipients=[to_email],
html=html_body,
sender=current_app.config['MAIL_DEFAULT_SENDER']
)
mail.send(msg)
email_sender.send_verification_email(to_email, token)
def send_reset_password_email(to_email, token):
reset_url = f"{current_app.config['FRONTEND_URL']}/auth/reset-password?token={token}"
html_body = f'Click <a href="{reset_url}">here</a> to reset your password.'
msg = Message(
subject="Reset your password",
recipients=[to_email],
html=html_body,
sender=current_app.config['MAIL_DEFAULT_SENDER']
)
mail.send(msg)
email_sender.send_reset_password_email(to_email, token)
@auth_api.route('/signup', methods=['POST'])
def signup():

View File

@@ -3,6 +3,11 @@ from models.user import User
from tinydb import Query
from db.db import users_db
import jwt
import random
import string
import smtplib
from backend.utils.email_instance import email_sender
from datetime import datetime, timedelta
user_api = Blueprint('user_api', __name__)
UserQuery = Query()
@@ -31,6 +36,25 @@ def get_profile():
'image_id': user.image_id
}), 200
@user_api.route('/user/profile', methods=['PUT'])
def update_profile():
user = get_current_user()
if not user:
return jsonify({'error': 'Unauthorized'}), 401
data = request.get_json()
# Only allow first_name, last_name, image_id to be updated
first_name = data.get('first_name')
last_name = data.get('last_name')
image_id = data.get('image_id')
if first_name is not None:
user.first_name = first_name
if last_name is not None:
user.last_name = last_name
if image_id is not None:
user.image_id = image_id
users_db.update(user.to_dict(), UserQuery.email == user.email)
return jsonify({'message': 'Profile updated'}), 200
@user_api.route('/user/image', methods=['PUT'])
def update_image():
user = get_current_user()
@@ -43,3 +67,82 @@ def update_image():
user.image_id = image_id
users_db.update(user.to_dict(), UserQuery.email == user.email)
return jsonify({'message': 'Image updated', 'image_id': image_id}), 200
@user_api.route('/user/check-pin', methods=['POST'])
def check_pin():
user = get_current_user()
if not user:
return jsonify({'error': 'Unauthorized'}), 401
data = request.get_json()
pin = data.get('pin')
if not pin:
return jsonify({'error': 'Missing pin'}), 400
if user.pin and pin == user.pin:
return jsonify({'valid': True}), 200
return jsonify({'valid': False}), 200
@user_api.route('/user/has-pin', methods=['GET'])
def has_pin():
user = get_current_user()
if not user:
return jsonify({'error': 'Unauthorized'}), 401
return jsonify({'has_pin': bool(user.pin)}), 200
@user_api.route('/user/request-pin-setup', methods=['POST'])
def request_pin_setup():
user = get_current_user()
if not user or not user.verified:
return jsonify({'error': 'Unauthorized'}), 401
# Generate 6-digit/character code
code = ''.join(random.choices(string.ascii_uppercase + string.digits, k=6))
user.pin_setup_code = code
user.pin_setup_code_created = datetime.utcnow().isoformat()
users_db.update(user.to_dict(), UserQuery.email == user.email)
# Send email
send_pin_setup_email(user.email, code)
return jsonify({'message': 'Verification code sent to your email.'}), 200
def send_pin_setup_email(email, code):
# Use the reusable email sender
email_sender.send_pin_setup_email(email, code)
@user_api.route('/user/verify-pin-setup', methods=['POST'])
def verify_pin_setup():
user = get_current_user()
if not user or not user.verified:
return jsonify({'error': 'Unauthorized'}), 401
data = request.get_json()
code = data.get('code')
if not code:
return jsonify({'error': 'Missing code'}), 400
if not user.pin_setup_code or not user.pin_setup_code_created:
return jsonify({'error': 'No code requested'}), 400
# Check expiry (10 min)
created = datetime.fromisoformat(user.pin_setup_code_created)
if datetime.utcnow() > created + timedelta(minutes=10):
return jsonify({'error': 'Code expired'}), 400
if code.strip().upper() != user.pin_setup_code.upper():
return jsonify({'error': 'Invalid code'}), 400
return jsonify({'message': 'Code verified'}), 200
@user_api.route('/user/set-pin', methods=['POST'])
def set_pin():
user = get_current_user()
if not user or not user.verified:
return jsonify({'error': 'Unauthorized'}), 401
data = request.get_json()
pin = data.get('pin')
if not pin or not pin.isdigit() or not (4 <= len(pin) <= 6):
return jsonify({'error': 'PIN must be 4-6 digits'}), 400
# Only allow if code was recently verified
if not user.pin_setup_code or not user.pin_setup_code_created:
return jsonify({'error': 'No code verified'}), 400
created = datetime.fromisoformat(user.pin_setup_code_created)
if datetime.utcnow() > created + timedelta(minutes=10):
return jsonify({'error': 'Code expired'}), 400
# Set pin, clear code
user.pin = pin
user.pin_setup_code = ''
user.pin_setup_code_created = None
users_db.update(user.to_dict(), UserQuery.email == user.email)
return jsonify({'message': 'Parent PIN set'}), 200