05/02/2026 updated code for security 2
This commit is contained in:
+45
-3
@@ -115,7 +115,7 @@ def login():
|
||||
user.failed_login_count = (user.failed_login_count or 0) + 1
|
||||
if user.failed_login_count >= MAX_FAILED_LOGINS:
|
||||
from datetime import timedelta
|
||||
user.locked_until = datetime.utcnow() + timedelta(minutes=LOCKOUT_MINUTES)
|
||||
user.locked_until = datetime.now(timezone.utc).replace(tzinfo=None) + timedelta(minutes=LOCKOUT_MINUTES)
|
||||
AuditLog.log(
|
||||
user_id=user.id,
|
||||
action='auth.account_locked',
|
||||
@@ -139,7 +139,7 @@ def login():
|
||||
# Successful authentication — reset lockout state.
|
||||
user.failed_login_count = 0
|
||||
user.locked_until = None
|
||||
user.last_login = datetime.utcnow()
|
||||
user.last_login = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||
|
||||
AuditLog.log(
|
||||
user_id=user.id,
|
||||
@@ -455,17 +455,58 @@ def mfa_backup_codes_regenerate():
|
||||
@require_jwt
|
||||
def me():
|
||||
"""Return basic profile info for the authenticated user."""
|
||||
import json
|
||||
user = db.session.get(User, g.current_user_id)
|
||||
stored_codes = json.loads(user.mfa_backup_codes or '[]')
|
||||
return jsonify({
|
||||
'id': user.id,
|
||||
'email': user.email,
|
||||
'created_at': user.created_at.isoformat() if user.created_at else None,
|
||||
'last_login': user.last_login.isoformat() if user.last_login else None,
|
||||
'totp_enabled': user.totp_enabled,
|
||||
'backup_codes_remaining': len(stored_codes),
|
||||
'recovery_configured': bool(user.recovery_enc_salt),
|
||||
}), 200
|
||||
|
||||
|
||||
@auth_bp.route('/audit-log', methods=['GET'])
|
||||
@require_jwt
|
||||
@limiter.limit('30 per minute')
|
||||
def audit_log():
|
||||
"""
|
||||
Return the authenticated user's recent audit log entries.
|
||||
|
||||
Query params:
|
||||
limit — max entries to return (default 50, max 200)
|
||||
offset — pagination offset (default 0)
|
||||
|
||||
Sensitive field values are never logged — entries contain only action
|
||||
types, resource IDs, timestamps, and IP addresses.
|
||||
"""
|
||||
try:
|
||||
limit = min(int(request.args.get('limit', 50)), 200)
|
||||
offset = max(int(request.args.get('offset', 0)), 0)
|
||||
except (ValueError, TypeError):
|
||||
return jsonify({'error': 'limit and offset must be integers'}), 400
|
||||
|
||||
entries = (
|
||||
AuditLog.query
|
||||
.filter_by(user_id=g.current_user_id)
|
||||
.order_by(AuditLog.created_at.desc())
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
.all()
|
||||
)
|
||||
total = AuditLog.query.filter_by(user_id=g.current_user_id).count()
|
||||
|
||||
return jsonify({
|
||||
'total': total,
|
||||
'limit': limit,
|
||||
'offset': offset,
|
||||
'entries': [e.to_dict() for e in entries],
|
||||
}), 200
|
||||
|
||||
|
||||
# ── Account management ────────────────────────────────────────────────────────
|
||||
|
||||
@auth_bp.route('/change-password', methods=['POST'])
|
||||
@@ -864,4 +905,5 @@ def recovery_items():
|
||||
{'id': item.id, 'enc_data': item.enc_data, 'iv': item.iv}
|
||||
for item in items
|
||||
]
|
||||
}), 200
|
||||
}), 200
|
||||
|
||||
|
||||
Reference in New Issue
Block a user