import secrets, jwt from datetime import datetime, timedelta, timezone from flask import Blueprint, request, jsonify, current_app from flask_mail import Mail, Message from tinydb import Query from api.error_codes import MISSING_FIELDS, EMAIL_EXISTS, MISSING_TOKEN, INVALID_TOKEN, TOKEN_TIMESTAMP_MISSING, \ TOKEN_EXPIRED, ALREADY_VERIFIED, MISSING_EMAIL, USER_NOT_FOUND, MISSING_EMAIL_OR_PASSWORD, INVALID_CREDENTIALS, \ NOT_VERIFIED from db.db import users_db auth_api = Blueprint('auth_api', __name__) UserQuery = Query() mail = Mail() TOKEN_EXPIRY_MINUTES = 60*4 SECRET_KEY = "your-secret-key" # Use a secure key in production #SECRET_KEY = os.environ.get('SECRET_KEY') def send_verification_email(to_email, token): verify_url = f"{current_app.config['FRONTEND_URL']}/auth/verify?token={token}" msg = Message( subject="Verify your account", recipients=[to_email], body=f"Click to verify your account: {verify_url}", sender=current_app.config['MAIL_DEFAULT_SENDER'] ) mail.send(msg) @auth_api.route('/signup', methods=['POST']) def signup(): data = request.get_json() required_fields = ['first_name', 'last_name', 'email', 'password'] if not all(field in data for field in required_fields): return jsonify({'error': 'Missing required fields', 'code': MISSING_FIELDS}), 400 if users_db.search(UserQuery.email == data['email']): return jsonify({'error': 'Email already exists', 'code': EMAIL_EXISTS}), 400 token = secrets.token_urlsafe(32) now_iso = datetime.utcnow().isoformat() users_db.insert({ 'first_name': data['first_name'], 'last_name': data['last_name'], 'email': data['email'], 'password': data['password'], # Hash in production! 'verified': False, 'verify_token': token, 'verify_token_created': now_iso }) send_verification_email(data['email'], token) return jsonify({'message': 'User created, verification email sent'}), 201 @auth_api.route('/verify', methods=['GET']) def verify(): token = request.args.get('token') status = 'success' reason = '' code = '' if not token: status = 'error' reason = 'Missing token' code = MISSING_TOKEN else: user = users_db.get(Query().verify_token == token) if not user: status = 'error' reason = 'Invalid token' code = INVALID_TOKEN else: created_str = user.get('verify_token_created') if not created_str: status = 'error' reason = 'Token timestamp missing' code = TOKEN_TIMESTAMP_MISSING else: created_dt = datetime.fromisoformat(created_str).replace(tzinfo=timezone.utc) if datetime.now(timezone.utc) - created_dt > timedelta(minutes=TOKEN_EXPIRY_MINUTES): status = 'error' reason = 'Token expired' code = TOKEN_EXPIRED else: users_db.update({'verified': True, 'verify_token': None, 'verify_token_created': None}, Query().verify_token == token) http_status = 200 if status == 'success' else 400 return jsonify({'status': status, 'reason': reason, 'code': code}), http_status @auth_api.route('/resend-verify', methods=['POST']) def resend_verify(): data = request.get_json() email = data.get('email') if not email: return jsonify({'error': 'Missing email', 'code': MISSING_EMAIL}), 400 user = users_db.get(UserQuery.email == email) if not user: return jsonify({'error': 'User not found', 'code': USER_NOT_FOUND}), 404 if user.get('verified'): return jsonify({'error': 'Account already verified', 'code': ALREADY_VERIFIED}), 400 token = secrets.token_urlsafe(32) now_iso = datetime.utcnow().isoformat() users_db.update({ 'verify_token': token, 'verify_token_created': now_iso }, UserQuery.email == email) send_verification_email(email, token) return jsonify({'message': 'Verification email resent'}), 200 @auth_api.route('/login', methods=['POST']) def login(): data = request.get_json() email = data.get('email') password = data.get('password') if not email or not password: return jsonify({'error': 'Missing email or password', 'code': MISSING_EMAIL_OR_PASSWORD}), 400 user = users_db.get(UserQuery.email == email) if not user or user.get('password') != password: return jsonify({'error': 'Invalid credentials', 'code': INVALID_CREDENTIALS}), 401 if not user.get('verified'): return jsonify({'error': 'This account has not verified', 'code': NOT_VERIFIED}), 403 payload = { 'email': email, 'exp': datetime.utcnow() + timedelta(hours=24*7) } token = jwt.encode(payload, SECRET_KEY, algorithm='HS256') resp = jsonify({'message': 'Login successful'}) resp.set_cookie('token', token, httponly=True, secure=True, samesite='Strict') return resp, 200 @auth_api.route('/me', methods=['GET']) def me(): token = request.cookies.get('token') if not token: return jsonify({'error': 'Missing token', 'code': MISSING_TOKEN}), 401 try: payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256']) email = payload.get('email') user = users_db.get(UserQuery.email == email) if not user: return jsonify({'error': 'User not found', 'code': USER_NOT_FOUND}), 404 return jsonify({ 'email': user['email'], 'first_name': user['first_name'], 'last_name': user['last_name'], 'verified': user['verified'] }), 200 except jwt.ExpiredSignatureError: return jsonify({'error': 'Token expired', 'code': TOKEN_EXPIRED}), 401 except jwt.InvalidTokenError: return jsonify({'error': 'Invalid token', 'code': INVALID_TOKEN}), 401 @auth_api.route('/logout', methods=['POST']) def logout(): resp = jsonify({'message': 'Logged out'}) resp.set_cookie('token', '', expires=0, httponly=True, secure=True, samesite='Strict') return resp, 200