diff --git a/app/__init__.py b/app/__init__.py index 3993d0d..6677b06 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -3,15 +3,17 @@ from flask_sqlalchemy import SQLAlchemy from flask_login import LoginManager from flask_migrate import Migrate from flask_mail import Mail +from flask_wtf.csrf import CSRFProtect from config import config import os import logging from logging.handlers import RotatingFileHandler -db = SQLAlchemy() +db = SQLAlchemy() login_manager = LoginManager() -migrate = Migrate() -mail = Mail() +migrate = Migrate() +mail = Mail() +csrf = CSRFProtect() # initialized here; .init_app() called in create_app() def create_app(config_name='default'): @@ -22,6 +24,7 @@ def create_app(config_name='default'): login_manager.init_app(app) migrate.init_app(app, db) mail.init_app(app) + csrf.init_app(app) # enables CSRF protection for all web routes login_manager.login_view = 'auth.login' login_manager.login_message = 'Please log in to access this page.' @@ -69,6 +72,8 @@ def create_app(config_name='default'): # Register csrf_token() as an app-wide Jinja2 global so templates that # render manual forms (no WTForms object) can still inject the CSRF token. + # CSRFProtect (initialized above) is the authoritative guard for web routes; + # the /api/v1 blueprint is exempted below via csrf.exempt(). from flask_wtf.csrf import generate_csrf app.jinja_env.globals['csrf_token'] = generate_csrf app.jinja_env.globals['enumerate'] = enumerate @@ -131,8 +136,12 @@ def create_app(config_name='default'): app.register_blueprint(customers.bp) app.register_blueprint(scheduled_reports.bp) - # ── Mobile API ───────────────────────────────────────────── - from app.api import register_api + # ── Mobile API (Phase 7) ───────────────────────────────────────────────── + # The /api/v1 blueprint uses JWT Bearer tokens — no CSRF cookies needed. + # Exempt it from CSRFProtect before registering so POST /api/v1/* routes + # are never rejected for a missing CSRF token. + from app.api import register_api, api_bp + csrf.exempt(api_bp) register_api(app) # ── Error handler: 413 Request Entity Too Large ─────────────────────── diff --git a/app/api/__init__.py b/app/api/__init__.py index 5871bef..653b2aa 100644 --- a/app/api/__init__.py +++ b/app/api/__init__.py @@ -57,4 +57,4 @@ def register_api(app): # api_bp.register_blueprint(notifications_bp) # api_bp.register_blueprint(photos_bp) - app.register_blueprint(api_bp) + app.register_blueprint(api_bp) \ No newline at end of file diff --git a/app/api/auth.py b/app/api/auth.py index c514b2c..c20012f 100644 --- a/app/api/auth.py +++ b/app/api/auth.py @@ -31,8 +31,6 @@ GET /api/v1/auth/me import logging from flask import Blueprint, request, g -from flask_wtf.csrf import csrf_exempt - from app import db from app.models.user import User from app.models.api_token import RefreshToken, DeviceToken @@ -61,7 +59,6 @@ def _user_payload(user: User) -> dict: # ── Login ───────────────────────────────────────────────────────────────────── @bp.route('/auth/login', methods=['POST']) -@csrf_exempt def login(): """ Authenticate with username + password. @@ -137,7 +134,6 @@ def login(): # ── Refresh ─────────────────────────────────────────────────────────────────── @bp.route('/auth/refresh', methods=['POST']) -@csrf_exempt def refresh(): """ Exchange a valid refresh token for a new access token. @@ -207,7 +203,6 @@ def refresh(): # ── Logout ──────────────────────────────────────────────────────────────────── @bp.route('/auth/logout', methods=['POST']) -@csrf_exempt @jwt_required def logout(): """ @@ -261,7 +256,6 @@ def me(): # ── Device token registration ───────────────────────────────────────────────── @bp.route('/devices/register', methods=['POST']) -@csrf_exempt @jwt_required def register_device(): """ @@ -316,4 +310,4 @@ def register_device(): logger.info('API DEVICE REGISTERED | user=%s | device_id=%s | apns_token=...%s', g.api_user.username, device_id, apns_token[-6:]) - return api_ok({'registered': True}) + return api_ok({'registered': True}) \ No newline at end of file