from flask import Flask, render_template from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate from flask_login import LoginManager from flask_wtf.csrf import CSRFProtect from flask_limiter import Limiter from flask_limiter.util import get_remote_address from flask_cors import CORS from .config import config db = SQLAlchemy() migrate = Migrate() login_manager = LoginManager() csrf = CSRFProtect() limiter = Limiter(key_func=get_remote_address) def create_app(config_name: str = 'development') -> Flask: app = Flask(__name__) app.config.from_object(config[config_name]) # Extensions db.init_app(app) migrate.init_app(app, db) login_manager.init_app(app) csrf.init_app(app) limiter.init_app(app) # Restrict CORS to the configured origin (locked to production domain in prod) cors_origins = app.config.get('CORS_ORIGINS', '*') CORS(app, resources={r'/api/*': {'origins': cors_origins}}) # Inject static asset version into every template for cache-busting. # Usage in templates: {{ url_for('static', filename='css/app.css') }}?v={{ sv }} app.jinja_env.globals['sv'] = app.config.get('STATIC_VERSION', '1') # Attach security headers to every response @app.after_request def set_security_headers(response): # Strict-Transport-Security: enforce HTTPS for 1 year, include subdomains response.headers['Strict-Transport-Security'] = ( 'max-age=31536000; includeSubDomains' ) # Prevent clickjacking response.headers['X-Frame-Options'] = 'DENY' # Prevent MIME-type sniffing response.headers['X-Content-Type-Options'] = 'nosniff' # Control referrer information leakage response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin' # Permissions policy — disable features the app does not use response.headers['Permissions-Policy'] = ( 'geolocation=(), camera=(), microphone=()' ) # CSP via HTTP header (authoritative — overrides the meta tag for all resources) response.headers['Content-Security-Policy'] = ( "default-src 'self'; " "script-src 'self'; " "style-src 'self'; " "img-src 'self' data:; " "font-src 'self'; " "connect-src 'self'; " "frame-ancestors 'none';" ) return response # Ensure all models are imported so SQLAlchemy knows about them from .models.user import User from .models.folder import Folder from .models.vault_item import VaultItem from .models.token_blacklist import TokenBlacklist from .models.shared_item import SharedItem from .models.emergency_access import EmergencyAccess from .models.audit_log import AuditLog @login_manager.user_loader def load_user(user_id): return User.query.get(int(user_id)) # Blueprints from .routes.auth import auth_bp from .routes.vault import vault_bp from .routes.folders import folders_bp from .routes.sharing import sharing_bp from .routes.emergency import emergency_bp app.register_blueprint(auth_bp, url_prefix='/api/auth') app.register_blueprint(vault_bp, url_prefix='/api/vault') app.register_blueprint(folders_bp, url_prefix='/api/folders') app.register_blueprint(sharing_bp, url_prefix='/api/sharing') app.register_blueprint(emergency_bp, url_prefix='/api/emergency') # Exempt all API blueprints from CSRF — JWT bearer tokens make CSRF irrelevant csrf.exempt(auth_bp) csrf.exempt(vault_bp) csrf.exempt(folders_bp) csrf.exempt(sharing_bp) csrf.exempt(emergency_bp) # Page-serving routes @app.route('/') @app.route('/login') def login_page(): return render_template('auth/login.html') @app.route('/register') def register_page(): return render_template('auth/register.html') @app.route('/vault') def vault_page(): return render_template('vault/index.html') @app.route('/recover') def recover_page(): return render_template('auth/recover.html') return app