118 lines
4.1 KiB
Python
118 lines
4.1 KiB
Python
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' https://api.pwnedpasswords.com; "
|
|
"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 |