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