05/02/2026 updated code for security 6
This commit is contained in:
@@ -191,6 +191,7 @@ def login():
|
|||||||
|
|
||||||
|
|
||||||
@auth_bp.route('/logout', methods=['POST'])
|
@auth_bp.route('/logout', methods=['POST'])
|
||||||
|
@limiter.limit('60 per minute')
|
||||||
def logout():
|
def logout():
|
||||||
"""Blacklist both the access token (from header) and refresh token (from body)."""
|
"""Blacklist both the access token (from header) and refresh token (from body)."""
|
||||||
auth_header = request.headers.get('Authorization', '')
|
auth_header = request.headers.get('Authorization', '')
|
||||||
@@ -230,6 +231,7 @@ def refresh():
|
|||||||
# ── MFA / TOTP endpoints ─────────────────────────────────────────────────────
|
# ── MFA / TOTP endpoints ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
@auth_bp.route('/mfa/setup', methods=['GET'])
|
@auth_bp.route('/mfa/setup', methods=['GET'])
|
||||||
|
@limiter.limit('10 per minute')
|
||||||
@require_jwt
|
@require_jwt
|
||||||
def mfa_setup():
|
def mfa_setup():
|
||||||
"""Generate a new TOTP secret and return QR code (as base64 PNG data URI)."""
|
"""Generate a new TOTP secret and return QR code (as base64 PNG data URI)."""
|
||||||
@@ -261,6 +263,7 @@ def mfa_setup():
|
|||||||
|
|
||||||
|
|
||||||
@auth_bp.route('/mfa/enable', methods=['POST'])
|
@auth_bp.route('/mfa/enable', methods=['POST'])
|
||||||
|
@limiter.limit('10 per minute')
|
||||||
@require_jwt
|
@require_jwt
|
||||||
def mfa_enable():
|
def mfa_enable():
|
||||||
"""Enable MFA after verifying the first TOTP code. Returns one-time backup codes."""
|
"""Enable MFA after verifying the first TOTP code. Returns one-time backup codes."""
|
||||||
@@ -306,6 +309,7 @@ def mfa_enable():
|
|||||||
|
|
||||||
|
|
||||||
@auth_bp.route('/mfa/disable', methods=['POST'])
|
@auth_bp.route('/mfa/disable', methods=['POST'])
|
||||||
|
@limiter.limit('10 per minute')
|
||||||
@require_jwt
|
@require_jwt
|
||||||
def mfa_disable():
|
def mfa_disable():
|
||||||
"""Disable MFA after verifying the current TOTP code or a backup code."""
|
"""Disable MFA after verifying the current TOTP code or a backup code."""
|
||||||
@@ -420,6 +424,7 @@ def mfa_verify():
|
|||||||
|
|
||||||
|
|
||||||
@auth_bp.route('/mfa/status', methods=['GET'])
|
@auth_bp.route('/mfa/status', methods=['GET'])
|
||||||
|
@limiter.limit('60 per minute')
|
||||||
@require_jwt
|
@require_jwt
|
||||||
def mfa_status():
|
def mfa_status():
|
||||||
user = db.session.get(User, g.current_user_id)
|
user = db.session.get(User, g.current_user_id)
|
||||||
@@ -432,6 +437,7 @@ def mfa_status():
|
|||||||
|
|
||||||
|
|
||||||
@auth_bp.route('/mfa/backup-codes/regenerate', methods=['POST'])
|
@auth_bp.route('/mfa/backup-codes/regenerate', methods=['POST'])
|
||||||
|
@limiter.limit('5 per minute')
|
||||||
@require_jwt
|
@require_jwt
|
||||||
def mfa_backup_codes_regenerate():
|
def mfa_backup_codes_regenerate():
|
||||||
"""
|
"""
|
||||||
@@ -474,6 +480,7 @@ def mfa_backup_codes_regenerate():
|
|||||||
|
|
||||||
|
|
||||||
@auth_bp.route('/me', methods=['GET'])
|
@auth_bp.route('/me', methods=['GET'])
|
||||||
|
@limiter.limit('60 per minute')
|
||||||
@require_jwt
|
@require_jwt
|
||||||
def me():
|
def me():
|
||||||
"""Return basic profile info for the authenticated user."""
|
"""Return basic profile info for the authenticated user."""
|
||||||
@@ -532,6 +539,7 @@ def audit_log():
|
|||||||
# ── Account management ────────────────────────────────────────────────────────
|
# ── Account management ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@auth_bp.route('/change-password', methods=['POST'])
|
@auth_bp.route('/change-password', methods=['POST'])
|
||||||
|
@limiter.limit('5 per minute')
|
||||||
@require_jwt
|
@require_jwt
|
||||||
def change_password():
|
def change_password():
|
||||||
"""
|
"""
|
||||||
@@ -622,6 +630,7 @@ def change_password():
|
|||||||
|
|
||||||
|
|
||||||
@auth_bp.route('/account', methods=['DELETE'])
|
@auth_bp.route('/account', methods=['DELETE'])
|
||||||
|
@limiter.limit('3 per minute')
|
||||||
@require_jwt
|
@require_jwt
|
||||||
def delete_account():
|
def delete_account():
|
||||||
"""
|
"""
|
||||||
@@ -673,6 +682,7 @@ def delete_account():
|
|||||||
# ── Account Recovery ──────────────────────────────────────────────────────────
|
# ── Account Recovery ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@auth_bp.route('/recovery/setup', methods=['POST'])
|
@auth_bp.route('/recovery/setup', methods=['POST'])
|
||||||
|
@limiter.limit('10 per minute')
|
||||||
@require_jwt
|
@require_jwt
|
||||||
def recovery_setup():
|
def recovery_setup():
|
||||||
"""
|
"""
|
||||||
@@ -710,6 +720,7 @@ def recovery_setup():
|
|||||||
|
|
||||||
|
|
||||||
@auth_bp.route('/recovery/status', methods=['GET'])
|
@auth_bp.route('/recovery/status', methods=['GET'])
|
||||||
|
@limiter.limit('60 per minute')
|
||||||
@require_jwt
|
@require_jwt
|
||||||
def recovery_status():
|
def recovery_status():
|
||||||
"""Return whether the user has a recovery code configured."""
|
"""Return whether the user has a recovery code configured."""
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
from flask import Blueprint, request, jsonify, g
|
from flask import Blueprint, request, jsonify, g
|
||||||
from app import db
|
from app import db, limiter
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
from app.models.emergency_access import EmergencyAccess
|
from app.models.emergency_access import EmergencyAccess
|
||||||
from app.models.audit_log import AuditLog
|
from app.models.audit_log import AuditLog
|
||||||
@@ -15,6 +15,7 @@ def _client_ip():
|
|||||||
|
|
||||||
|
|
||||||
@emergency_bp.route('', methods=['GET'])
|
@emergency_bp.route('', methods=['GET'])
|
||||||
|
@limiter.limit('30 per minute')
|
||||||
@require_jwt
|
@require_jwt
|
||||||
def list_emergency():
|
def list_emergency():
|
||||||
"""Return emergency access records both as grantor and as grantee."""
|
"""Return emergency access records both as grantor and as grantee."""
|
||||||
@@ -45,6 +46,7 @@ def _ea_as_grantee(ea: EmergencyAccess) -> dict:
|
|||||||
|
|
||||||
|
|
||||||
@emergency_bp.route('', methods=['POST'])
|
@emergency_bp.route('', methods=['POST'])
|
||||||
|
@limiter.limit('10 per minute')
|
||||||
@require_jwt
|
@require_jwt
|
||||||
def create_emergency():
|
def create_emergency():
|
||||||
"""Grantor creates an emergency access invitation for a trusted contact."""
|
"""Grantor creates an emergency access invitation for a trusted contact."""
|
||||||
@@ -101,6 +103,7 @@ def create_emergency():
|
|||||||
|
|
||||||
|
|
||||||
@emergency_bp.route('/<int:ea_id>', methods=['DELETE'])
|
@emergency_bp.route('/<int:ea_id>', methods=['DELETE'])
|
||||||
|
@limiter.limit('10 per minute')
|
||||||
@require_jwt
|
@require_jwt
|
||||||
def delete_emergency(ea_id):
|
def delete_emergency(ea_id):
|
||||||
"""Grantor removes an emergency access grant."""
|
"""Grantor removes an emergency access grant."""
|
||||||
@@ -125,6 +128,7 @@ def delete_emergency(ea_id):
|
|||||||
|
|
||||||
|
|
||||||
@emergency_bp.route('/<int:ea_id>/accept', methods=['POST'])
|
@emergency_bp.route('/<int:ea_id>/accept', methods=['POST'])
|
||||||
|
@limiter.limit('10 per minute')
|
||||||
@require_jwt
|
@require_jwt
|
||||||
def accept_emergency(ea_id):
|
def accept_emergency(ea_id):
|
||||||
"""Grantee accepts an emergency access invitation."""
|
"""Grantee accepts an emergency access invitation."""
|
||||||
@@ -157,6 +161,7 @@ def accept_emergency(ea_id):
|
|||||||
|
|
||||||
|
|
||||||
@emergency_bp.route('/<int:ea_id>/provide', methods=['POST'])
|
@emergency_bp.route('/<int:ea_id>/provide', methods=['POST'])
|
||||||
|
@limiter.limit('10 per minute')
|
||||||
@require_jwt
|
@require_jwt
|
||||||
def provide_vault(ea_id):
|
def provide_vault(ea_id):
|
||||||
"""
|
"""
|
||||||
@@ -194,6 +199,7 @@ def provide_vault(ea_id):
|
|||||||
|
|
||||||
|
|
||||||
@emergency_bp.route('/<int:ea_id>/request', methods=['POST'])
|
@emergency_bp.route('/<int:ea_id>/request', methods=['POST'])
|
||||||
|
@limiter.limit('10 per minute')
|
||||||
@require_jwt
|
@require_jwt
|
||||||
def request_access(ea_id):
|
def request_access(ea_id):
|
||||||
"""Grantee initiates an access request, starting the wait-period clock."""
|
"""Grantee initiates an access request, starting the wait-period clock."""
|
||||||
@@ -226,6 +232,7 @@ def request_access(ea_id):
|
|||||||
|
|
||||||
|
|
||||||
@emergency_bp.route('/<int:ea_id>/deny', methods=['POST'])
|
@emergency_bp.route('/<int:ea_id>/deny', methods=['POST'])
|
||||||
|
@limiter.limit('10 per minute')
|
||||||
@require_jwt
|
@require_jwt
|
||||||
def deny_access(ea_id):
|
def deny_access(ea_id):
|
||||||
"""Grantor denies a pending access request (resets to ready)."""
|
"""Grantor denies a pending access request (resets to ready)."""
|
||||||
@@ -253,6 +260,7 @@ def deny_access(ea_id):
|
|||||||
|
|
||||||
|
|
||||||
@emergency_bp.route('/<int:ea_id>/vault', methods=['GET'])
|
@emergency_bp.route('/<int:ea_id>/vault', methods=['GET'])
|
||||||
|
@limiter.limit('10 per minute')
|
||||||
@require_jwt
|
@require_jwt
|
||||||
def get_emergency_vault(ea_id):
|
def get_emergency_vault(ea_id):
|
||||||
"""
|
"""
|
||||||
@@ -294,4 +302,3 @@ def get_emergency_vault(ea_id):
|
|||||||
'enc_vault': ea.enc_vault,
|
'enc_vault': ea.enc_vault,
|
||||||
'grantor_public_key': grantor.sharing_public_key if grantor else None,
|
'grantor_public_key': grantor.sharing_public_key if grantor else None,
|
||||||
}), 200
|
}), 200
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from flask import Blueprint, request, jsonify, g
|
from flask import Blueprint, request, jsonify, g
|
||||||
from app import db
|
from app import db, limiter
|
||||||
from app.models.folder import Folder
|
from app.models.folder import Folder
|
||||||
from app.models.audit_log import AuditLog
|
from app.models.audit_log import AuditLog
|
||||||
from app.services.auth_service import require_jwt
|
from app.services.auth_service import require_jwt
|
||||||
@@ -13,6 +13,7 @@ def _client_ip():
|
|||||||
|
|
||||||
|
|
||||||
@folders_bp.route('', methods=['GET'])
|
@folders_bp.route('', methods=['GET'])
|
||||||
|
@limiter.limit('120 per minute')
|
||||||
@require_jwt
|
@require_jwt
|
||||||
def list_folders():
|
def list_folders():
|
||||||
folders = Folder.query.filter_by(user_id=g.current_user_id).order_by(Folder.name.asc()).all()
|
folders = Folder.query.filter_by(user_id=g.current_user_id).order_by(Folder.name.asc()).all()
|
||||||
@@ -20,6 +21,7 @@ def list_folders():
|
|||||||
|
|
||||||
|
|
||||||
@folders_bp.route('', methods=['POST'])
|
@folders_bp.route('', methods=['POST'])
|
||||||
|
@limiter.limit('30 per minute')
|
||||||
@require_jwt
|
@require_jwt
|
||||||
def create_folder():
|
def create_folder():
|
||||||
data = request.get_json(silent=True) or {}
|
data = request.get_json(silent=True) or {}
|
||||||
@@ -43,6 +45,7 @@ def create_folder():
|
|||||||
|
|
||||||
|
|
||||||
@folders_bp.route('/<int:folder_id>', methods=['PUT'])
|
@folders_bp.route('/<int:folder_id>', methods=['PUT'])
|
||||||
|
@limiter.limit('30 per minute')
|
||||||
@require_jwt
|
@require_jwt
|
||||||
def update_folder(folder_id):
|
def update_folder(folder_id):
|
||||||
folder = Folder.query.filter_by(id=folder_id, user_id=g.current_user_id).first()
|
folder = Folder.query.filter_by(id=folder_id, user_id=g.current_user_id).first()
|
||||||
@@ -68,6 +71,7 @@ def update_folder(folder_id):
|
|||||||
|
|
||||||
|
|
||||||
@folders_bp.route('/<int:folder_id>', methods=['DELETE'])
|
@folders_bp.route('/<int:folder_id>', methods=['DELETE'])
|
||||||
|
@limiter.limit('30 per minute')
|
||||||
@require_jwt
|
@require_jwt
|
||||||
def delete_folder(folder_id):
|
def delete_folder(folder_id):
|
||||||
folder = Folder.query.filter_by(id=folder_id, user_id=g.current_user_id).first()
|
folder = Folder.query.filter_by(id=folder_id, user_id=g.current_user_id).first()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from flask import Blueprint, request, jsonify, g
|
from flask import Blueprint, request, jsonify, g
|
||||||
from app import db
|
from app import db, limiter
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
from app.models.shared_item import SharedItem
|
from app.models.shared_item import SharedItem
|
||||||
from app.models.audit_log import AuditLog
|
from app.models.audit_log import AuditLog
|
||||||
@@ -15,6 +15,7 @@ def _client_ip():
|
|||||||
# ── Sharing keypair management ────────────────────────────────────────────────
|
# ── Sharing keypair management ────────────────────────────────────────────────
|
||||||
|
|
||||||
@sharing_bp.route('/keys', methods=['GET'])
|
@sharing_bp.route('/keys', methods=['GET'])
|
||||||
|
@limiter.limit('60 per minute')
|
||||||
@require_jwt
|
@require_jwt
|
||||||
def get_my_keys():
|
def get_my_keys():
|
||||||
"""Return current user's encrypted sharing private key (to decrypt client-side)."""
|
"""Return current user's encrypted sharing private key (to decrypt client-side)."""
|
||||||
@@ -30,6 +31,7 @@ def get_my_keys():
|
|||||||
|
|
||||||
|
|
||||||
@sharing_bp.route('/keys', methods=['POST'])
|
@sharing_bp.route('/keys', methods=['POST'])
|
||||||
|
@limiter.limit('10 per minute')
|
||||||
@require_jwt
|
@require_jwt
|
||||||
def store_my_keys():
|
def store_my_keys():
|
||||||
"""Store ECDH keypair. Public key plaintext; private key encrypted with vault key."""
|
"""Store ECDH keypair. Public key plaintext; private key encrypted with vault key."""
|
||||||
@@ -61,6 +63,7 @@ def store_my_keys():
|
|||||||
|
|
||||||
|
|
||||||
@sharing_bp.route('/public-key', methods=['GET'])
|
@sharing_bp.route('/public-key', methods=['GET'])
|
||||||
|
@limiter.limit('30 per minute')
|
||||||
@require_jwt
|
@require_jwt
|
||||||
def get_public_key():
|
def get_public_key():
|
||||||
"""Look up another user's ECDH public key by email (needed to create a share)."""
|
"""Look up another user's ECDH public key by email (needed to create a share)."""
|
||||||
@@ -84,6 +87,7 @@ def get_public_key():
|
|||||||
# ── Outgoing shares ───────────────────────────────────────────────────────────
|
# ── Outgoing shares ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@sharing_bp.route('', methods=['GET'])
|
@sharing_bp.route('', methods=['GET'])
|
||||||
|
@limiter.limit('60 per minute')
|
||||||
@require_jwt
|
@require_jwt
|
||||||
def list_outgoing():
|
def list_outgoing():
|
||||||
"""List all items the current user has shared with others."""
|
"""List all items the current user has shared with others."""
|
||||||
@@ -103,6 +107,7 @@ def list_outgoing():
|
|||||||
|
|
||||||
|
|
||||||
@sharing_bp.route('', methods=['POST'])
|
@sharing_bp.route('', methods=['POST'])
|
||||||
|
@limiter.limit('30 per minute')
|
||||||
@require_jwt
|
@require_jwt
|
||||||
def create_share():
|
def create_share():
|
||||||
"""
|
"""
|
||||||
@@ -164,6 +169,7 @@ def create_share():
|
|||||||
|
|
||||||
|
|
||||||
@sharing_bp.route('/<int:share_id>', methods=['DELETE'])
|
@sharing_bp.route('/<int:share_id>', methods=['DELETE'])
|
||||||
|
@limiter.limit('30 per minute')
|
||||||
@require_jwt
|
@require_jwt
|
||||||
def delete_share(share_id):
|
def delete_share(share_id):
|
||||||
share = SharedItem.query.filter_by(id=share_id, owner_id=g.current_user_id).first()
|
share = SharedItem.query.filter_by(id=share_id, owner_id=g.current_user_id).first()
|
||||||
@@ -190,6 +196,7 @@ def delete_share(share_id):
|
|||||||
# ── Inbox (received shares) ───────────────────────────────────────────────────
|
# ── Inbox (received shares) ───────────────────────────────────────────────────
|
||||||
|
|
||||||
@sharing_bp.route('/inbox', methods=['GET'])
|
@sharing_bp.route('/inbox', methods=['GET'])
|
||||||
|
@limiter.limit('60 per minute')
|
||||||
@require_jwt
|
@require_jwt
|
||||||
def inbox():
|
def inbox():
|
||||||
"""List all items shared with the current user."""
|
"""List all items shared with the current user."""
|
||||||
@@ -216,6 +223,7 @@ def inbox():
|
|||||||
|
|
||||||
|
|
||||||
@sharing_bp.route('/inbox/<int:share_id>/accept', methods=['POST'])
|
@sharing_bp.route('/inbox/<int:share_id>/accept', methods=['POST'])
|
||||||
|
@limiter.limit('30 per minute')
|
||||||
@require_jwt
|
@require_jwt
|
||||||
def accept_share(share_id):
|
def accept_share(share_id):
|
||||||
"""Mark a received share as accepted (links recipient_id if not already set)."""
|
"""Mark a received share as accepted (links recipient_id if not already set)."""
|
||||||
|
|||||||
+8
-1
@@ -1,5 +1,5 @@
|
|||||||
from flask import Blueprint, request, jsonify, g
|
from flask import Blueprint, request, jsonify, g
|
||||||
from app import db
|
from app import db, limiter
|
||||||
from app.models.vault_item import VaultItem, ItemType
|
from app.models.vault_item import VaultItem, ItemType
|
||||||
from app.models.audit_log import AuditLog
|
from app.models.audit_log import AuditLog
|
||||||
from app.services.auth_service import require_jwt
|
from app.services.auth_service import require_jwt
|
||||||
@@ -15,6 +15,7 @@ def _client_ip():
|
|||||||
|
|
||||||
|
|
||||||
@vault_bp.route('', methods=['GET'])
|
@vault_bp.route('', methods=['GET'])
|
||||||
|
@limiter.limit('120 per minute')
|
||||||
@require_jwt
|
@require_jwt
|
||||||
def list_items():
|
def list_items():
|
||||||
items = VaultItem.query.filter_by(user_id=g.current_user_id).order_by(
|
items = VaultItem.query.filter_by(user_id=g.current_user_id).order_by(
|
||||||
@@ -24,6 +25,7 @@ def list_items():
|
|||||||
|
|
||||||
|
|
||||||
@vault_bp.route('', methods=['POST'])
|
@vault_bp.route('', methods=['POST'])
|
||||||
|
@limiter.limit('60 per minute')
|
||||||
@require_jwt
|
@require_jwt
|
||||||
def create_item():
|
def create_item():
|
||||||
data = request.get_json(silent=True) or {}
|
data = request.get_json(silent=True) or {}
|
||||||
@@ -72,6 +74,7 @@ def create_item():
|
|||||||
|
|
||||||
|
|
||||||
@vault_bp.route('/<int:item_id>', methods=['GET'])
|
@vault_bp.route('/<int:item_id>', methods=['GET'])
|
||||||
|
@limiter.limit('120 per minute')
|
||||||
@require_jwt
|
@require_jwt
|
||||||
def get_item(item_id):
|
def get_item(item_id):
|
||||||
item = VaultItem.query.filter_by(id=item_id, user_id=g.current_user_id).first()
|
item = VaultItem.query.filter_by(id=item_id, user_id=g.current_user_id).first()
|
||||||
@@ -81,6 +84,7 @@ def get_item(item_id):
|
|||||||
|
|
||||||
|
|
||||||
@vault_bp.route('/<int:item_id>', methods=['PUT'])
|
@vault_bp.route('/<int:item_id>', methods=['PUT'])
|
||||||
|
@limiter.limit('60 per minute')
|
||||||
@require_jwt
|
@require_jwt
|
||||||
def update_item(item_id):
|
def update_item(item_id):
|
||||||
item = VaultItem.query.filter_by(id=item_id, user_id=g.current_user_id).first()
|
item = VaultItem.query.filter_by(id=item_id, user_id=g.current_user_id).first()
|
||||||
@@ -129,6 +133,7 @@ def update_item(item_id):
|
|||||||
|
|
||||||
|
|
||||||
@vault_bp.route('/<int:item_id>', methods=['DELETE'])
|
@vault_bp.route('/<int:item_id>', methods=['DELETE'])
|
||||||
|
@limiter.limit('60 per minute')
|
||||||
@require_jwt
|
@require_jwt
|
||||||
def delete_item(item_id):
|
def delete_item(item_id):
|
||||||
item = VaultItem.query.filter_by(id=item_id, user_id=g.current_user_id).first()
|
item = VaultItem.query.filter_by(id=item_id, user_id=g.current_user_id).first()
|
||||||
@@ -153,6 +158,7 @@ def delete_item(item_id):
|
|||||||
# ── Import / Export ───────────────────────────────────────────────────────────
|
# ── Import / Export ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@vault_bp.route('/export', methods=['GET'])
|
@vault_bp.route('/export', methods=['GET'])
|
||||||
|
@limiter.limit('10 per minute')
|
||||||
@require_jwt
|
@require_jwt
|
||||||
def export_items():
|
def export_items():
|
||||||
"""
|
"""
|
||||||
@@ -178,6 +184,7 @@ def export_items():
|
|||||||
|
|
||||||
|
|
||||||
@vault_bp.route('/import', methods=['POST'])
|
@vault_bp.route('/import', methods=['POST'])
|
||||||
|
@limiter.limit('10 per minute')
|
||||||
@require_jwt
|
@require_jwt
|
||||||
def import_items():
|
def import_items():
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user