Spaces:
Sleeping
Sleeping
File size: 6,002 Bytes
67d3f72 23654e5 d038974 23654e5 d038974 23654e5 d038974 b55f151 f8b1f63 ebb7d3d f8b1f63 b55f151 23654e5 df450f1 e60b22c df450f1 23654e5 d038974 23654e5 df450f1 e60b22c df450f1 23654e5 ebb7d3d 23654e5 d038974 23654e5 d038974 23654e5 d038974 23654e5 d038974 23654e5 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 |
"""
Participatory Planning Application
Copyright (c) 2024-2025 Marcos Thadeu Queiroz Magalhães (thadillo@gmail.com)
Licensed under MIT License - See LICENSE file for details
"""
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from dotenv import load_dotenv
import os
db = SQLAlchemy()
limiter = Limiter(
key_func=get_remote_address,
default_limits=["200 per day", "50 per hour"],
storage_uri="memory://"
)
def create_app():
load_dotenv()
app = Flask(__name__)
# Secret key validation with fail-fast in production
flask_secret_key = os.getenv('FLASK_SECRET_KEY')
flask_env = os.getenv('FLASK_ENV', 'production')
if not flask_secret_key:
if flask_env == 'production':
raise RuntimeError(
"FLASK_SECRET_KEY must be set in production! "
"Generate one with: python -c \"import secrets; print(secrets.token_hex(32))\""
)
else:
# Development: Generate random secret (not persistent)
import secrets
flask_secret_key = secrets.token_hex(32)
app.logger.warning("⚠️ No FLASK_SECRET_KEY set - using random key for development")
app.logger.warning("⚠️ Sessions will be invalidated on restart!")
elif flask_secret_key == 'dev-secret-key-change-in-production':
raise RuntimeError(
"FLASK_SECRET_KEY is using the default insecure value! "
"Change it in .env file to a secure random value."
)
app.config['SECRET_KEY'] = flask_secret_key
# Session configuration for iframe embedding (HF Spaces)
app.config['SESSION_COOKIE_SECURE'] = True # Required for HTTPS
app.config['SESSION_COOKIE_HTTPONLY'] = True # Security
app.config['SESSION_COOKIE_SAMESITE'] = 'None' # Allow in iframes
app.config['SESSION_COOKIE_PARTITIONED'] = True # Safari compatibility
app.config['PERMANENT_SESSION_LIFETIME'] = 86400 # 24 hours
# Use custom database path if set (for HF Spaces), otherwise use instance folder
db_path = os.getenv('DATABASE_PATH')
if db_path:
# Absolute path for Hugging Face Spaces
app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{db_path}'
else:
# Relative path for local development
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///participatory_planner.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
# SQLite-specific settings to reduce locking issues
app.config['SQLALCHEMY_ENGINE_OPTIONS'] = {
'connect_args': {
'timeout': 60, # Increase timeout to 60 seconds for HuggingFace
'check_same_thread': False # Allow multi-threaded access
},
'pool_pre_ping': True, # Verify connections before using
'pool_recycle': 3600, # Recycle connections every hour
}
db.init_app(app)
limiter.init_app(app)
# Enable WAL mode for SQLite to reduce locking
with app.app_context():
from sqlalchemy import event
from sqlalchemy.engine import Engine
@event.listens_for(Engine, "connect")
def set_sqlite_pragma(dbapi_conn, connection_record):
cursor = dbapi_conn.cursor()
cursor.execute("PRAGMA journal_mode=WAL") # Write-Ahead Logging
cursor.execute("PRAGMA synchronous=NORMAL") # Balance safety/performance
cursor.execute("PRAGMA busy_timeout=60000") # 60 second timeout for HuggingFace
cursor.close()
# Import models
from app.models import models
# Import and register blueprints
from app.routes import auth, submissions, admin
app.register_blueprint(auth.bp)
app.register_blueprint(submissions.bp)
app.register_blueprint(admin.bp)
# Add Partitioned attribute to session cookies for Safari compatibility
@app.after_request
def add_partitioned_cookie(response):
"""Add Partitioned attribute to cookies for Safari in iframes"""
# Get the Set-Cookie headers
set_cookie = response.headers.get('Set-Cookie')
if set_cookie and 'session=' in set_cookie:
# Add Partitioned attribute if SameSite=None is present
if 'SameSite=None' in set_cookie and 'Partitioned' not in set_cookie:
response.headers['Set-Cookie'] = set_cookie + '; Partitioned'
return response
# Create tables
with app.app_context():
db.create_all()
# Initialize with admin token if not exists (SECURE VERSION)
from app.models.models import Token
import secrets
# Check if any admin token exists
existing_admin = Token.query.filter_by(type='admin').first()
if not existing_admin:
# Get admin token from environment or generate secure random token
admin_token_value = os.getenv('ADMIN_TOKEN')
if not admin_token_value:
# Generate secure random token
admin_token_value = secrets.token_urlsafe(16)
app.logger.warning("=" * 80)
app.logger.warning("🔐 ADMIN TOKEN GENERATED (SAVE THIS - SHOWN ONLY ONCE):")
app.logger.warning(f" {admin_token_value}")
app.logger.warning("=" * 80)
print("\n" + "=" * 80)
print("🔐 ADMIN TOKEN GENERATED (SAVE THIS - SHOWN ONLY ONCE):")
print(f" {admin_token_value}")
print("=" * 80 + "\n")
else:
app.logger.info("Using ADMIN_TOKEN from environment variable")
admin_token = Token(
token=admin_token_value,
type='admin',
name='Administrator'
)
db.session.add(admin_token)
db.session.commit()
app.logger.info(f"Admin token created: {admin_token_value[:4]}...{admin_token_value[-4:]}")
return app
|