implement patches
This commit is contained in:
+58
-5
@@ -4,6 +4,7 @@ import time
|
||||
from flask import Blueprint, request, jsonify, g
|
||||
from app import db, limiter
|
||||
from app.models.user import User
|
||||
from app.models.audit_log import AuditLog
|
||||
from app.services.auth_service import (
|
||||
hash_auth_token,
|
||||
verify_auth_token,
|
||||
@@ -19,6 +20,11 @@ auth_bp = Blueprint('auth', __name__)
|
||||
EMAIL_RE = re.compile(r'^[^@\s]+@[^@\s]+\.[^@\s]+$')
|
||||
|
||||
|
||||
def _client_ip():
|
||||
"""Return the best-effort client IP from the request context."""
|
||||
return request.headers.get('X-Forwarded-For', request.remote_addr or '').split(',')[0].strip()
|
||||
|
||||
|
||||
@auth_bp.route('/register', methods=['POST'])
|
||||
@limiter.limit('10 per minute')
|
||||
def register():
|
||||
@@ -40,6 +46,16 @@ def register():
|
||||
master_hash = hash_auth_token(auth_hash)
|
||||
user = User(email=email, master_hash=master_hash, enc_key_salt=enc_key_salt)
|
||||
db.session.add(user)
|
||||
db.session.flush() # populate user.id before logging
|
||||
|
||||
AuditLog.log(
|
||||
user_id=user.id,
|
||||
action='auth.register',
|
||||
resource_type='user',
|
||||
resource_id=user.id,
|
||||
detail=f'New account registered: {email}',
|
||||
ip_address=_client_ip(),
|
||||
)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'message': 'Account created successfully'}), 201
|
||||
@@ -63,6 +79,15 @@ def login():
|
||||
|
||||
from datetime import datetime
|
||||
user.last_login = datetime.utcnow()
|
||||
|
||||
AuditLog.log(
|
||||
user_id=user.id,
|
||||
action='auth.login',
|
||||
resource_type='user',
|
||||
resource_id=user.id,
|
||||
detail=f'Successful login{" (MFA pending)" if user.totp_enabled else ""}',
|
||||
ip_address=_client_ip(),
|
||||
)
|
||||
db.session.commit()
|
||||
|
||||
# MFA gate: if enabled, issue a short-lived mfa_token instead of full tokens
|
||||
@@ -125,7 +150,7 @@ def refresh():
|
||||
@require_jwt
|
||||
def mfa_setup():
|
||||
"""Generate a new TOTP secret and return QR code (as base64 PNG data URI)."""
|
||||
user = User.query.get(g.current_user_id)
|
||||
user = db.session.get(User, g.current_user_id)
|
||||
if user.totp_enabled:
|
||||
return jsonify({'error': 'MFA is already enabled'}), 400
|
||||
|
||||
@@ -156,7 +181,7 @@ def mfa_setup():
|
||||
@require_jwt
|
||||
def mfa_enable():
|
||||
"""Enable MFA after verifying the first TOTP code."""
|
||||
user = User.query.get(g.current_user_id)
|
||||
user = db.session.get(User, g.current_user_id)
|
||||
if user.totp_enabled:
|
||||
return jsonify({'error': 'MFA is already enabled'}), 400
|
||||
|
||||
@@ -173,6 +198,15 @@ def mfa_enable():
|
||||
|
||||
user.totp_secret = secret
|
||||
user.totp_enabled = True
|
||||
|
||||
AuditLog.log(
|
||||
user_id=user.id,
|
||||
action='auth.mfa_enable',
|
||||
resource_type='user',
|
||||
resource_id=user.id,
|
||||
detail='TOTP two-factor authentication enabled',
|
||||
ip_address=_client_ip(),
|
||||
)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'message': 'MFA enabled successfully'}), 200
|
||||
@@ -182,7 +216,7 @@ def mfa_enable():
|
||||
@require_jwt
|
||||
def mfa_disable():
|
||||
"""Disable MFA after verifying the current TOTP code."""
|
||||
user = User.query.get(g.current_user_id)
|
||||
user = db.session.get(User, g.current_user_id)
|
||||
if not user.totp_enabled:
|
||||
return jsonify({'error': 'MFA is not enabled'}), 400
|
||||
|
||||
@@ -195,6 +229,15 @@ def mfa_disable():
|
||||
|
||||
user.totp_secret = None
|
||||
user.totp_enabled = False
|
||||
|
||||
AuditLog.log(
|
||||
user_id=user.id,
|
||||
action='auth.mfa_disable',
|
||||
resource_type='user',
|
||||
resource_id=user.id,
|
||||
detail='TOTP two-factor authentication disabled',
|
||||
ip_address=_client_ip(),
|
||||
)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'message': 'MFA disabled'}), 200
|
||||
@@ -216,7 +259,7 @@ def mfa_verify():
|
||||
except Exception:
|
||||
return jsonify({'error': 'Invalid or expired MFA token'}), 401
|
||||
|
||||
user = User.query.get(int(payload['sub']))
|
||||
user = db.session.get(User, int(payload['sub']))
|
||||
if not user or not user.totp_enabled:
|
||||
return jsonify({'error': 'MFA not configured for this account'}), 400
|
||||
|
||||
@@ -227,6 +270,16 @@ def mfa_verify():
|
||||
# One-time use: blacklist the mfa_token
|
||||
blacklist_token(mfa_token, 'mfa')
|
||||
|
||||
AuditLog.log(
|
||||
user_id=user.id,
|
||||
action='auth.mfa_verify',
|
||||
resource_type='user',
|
||||
resource_id=user.id,
|
||||
detail='MFA verification successful — session tokens issued',
|
||||
ip_address=_client_ip(),
|
||||
)
|
||||
db.session.commit()
|
||||
|
||||
tokens = generate_tokens(user.id)
|
||||
return jsonify({
|
||||
'access_token': tokens['access_token'],
|
||||
@@ -237,5 +290,5 @@ def mfa_verify():
|
||||
@auth_bp.route('/mfa/status', methods=['GET'])
|
||||
@require_jwt
|
||||
def mfa_status():
|
||||
user = User.query.get(g.current_user_id)
|
||||
user = db.session.get(User, g.current_user_id)
|
||||
return jsonify({'totp_enabled': user.totp_enabled}), 200
|
||||
|
||||
Reference in New Issue
Block a user