diff --git a/.gitignore b/.gitignore index b9c9e80..6cae65c 100644 --- a/.gitignore +++ b/.gitignore @@ -82,6 +82,7 @@ $RECYCLE.BIN/ *~ .directory .env +.docs data/state.json *.tmp *.bak diff --git a/docker-compose.yml b/docker-compose.yml index 168bf34..a901684 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -30,17 +30,18 @@ services: restart: "no" dockflare: - image: alplat/dockflare:stable + build: ./dockflare + #image: alplat/dockflare:stable container_name: dockflare restart: unless-stopped ports: - - "5000:5000" - #labels: + - "5001:5000" + labels: # -- Cloudflare Tunnel Configuration (via DockFlare) OPTIONAL -- - #- dockflare.enable=true - #- dockflare.hostname=dockflare.domain.tld - #- dockflare.service=http://dockflare:5000 - #- dockflare.access.policy=default_tld + - dockflare.enable=true + - dockflare.hostname=df.dataverse.icu + - dockflare.service=http://dockflare:5000 + - dockflare.access.policy=bypass volumes: - dockflare_data:/app/data environment: diff --git a/dockflare/app/__init__.py b/dockflare/app/__init__.py index 7f36975..956792f 100644 --- a/dockflare/app/__init__.py +++ b/dockflare/app/__init__.py @@ -24,6 +24,7 @@ import json from flask import Flask from flask_wtf.csrf import CSRFProtect from flask_login import LoginManager +from authlib.integrations.flask_client import OAuth from .core.user import User import docker from docker.errors import APIError @@ -37,6 +38,8 @@ log_queue = queue.Queue(maxsize=config.MAX_LOG_QUEUE_SIZE) state_update_queue = queue.Queue(maxsize=50) log_formatter = logging.Formatter('%(asctime)s [%(levelname)s] %(message)s', datefmt='%H:%M:%S') +oauth = None + class QueueLogHandler(logging.Handler): def __init__(self, log_queue_instance): super().__init__() @@ -96,6 +99,9 @@ def create_app(): app_instance.secret_key = os.urandom(24) app_instance.config['PREFERRED_URL_SCHEME'] = 'http' app_instance.config['APP_VERSION'] = config.APP_VERSION + app_instance.config['SESSION_COOKIE_HTTPONLY'] = True + app_instance.config['SESSION_COOKIE_SAMESITE'] = 'Lax' + app_instance.config['PERMANENT_SESSION_LIFETIME'] = 86400 # Initialize CSRF Protection csrf = CSRFProtect(app_instance) @@ -103,18 +109,27 @@ def create_app(): # Initialize Flask-Login login_manager = LoginManager() login_manager.init_app(app_instance) - login_manager.login_view = 'auth.login' + login_manager.login_view = 'web.login' login_manager.login_message_category = "info" + # Initialize OAuth + global oauth + oauth = OAuth() + oauth.init_app(app_instance) + @login_manager.unauthorized_handler def unauthorized(): - """Handle unauthorized access - return JSON for API requests, redirect for web requests.""" from flask import request, jsonify, redirect, url_for - # Check if this is an API request if request.path.startswith('/api/'): return jsonify({"status": "error", "message": "authentication_required"}), 401 - # For web requests, redirect to login page - return redirect(url_for('auth.login')) + + oauth_providers = app_instance.config.get('OAUTH_PROVIDERS', []) + if oauth_providers and not app_instance.config.get('DISABLE_PASSWORD_LOGIN', False): + return redirect(url_for('web.login')) + elif oauth_providers: + return redirect(url_for('web.login')) + else: + return redirect(url_for('auth.login')) # Custom user loader that exempts API routes from authentication checks @login_manager.request_loader @@ -129,13 +144,16 @@ def create_app(): @login_manager.user_loader def load_user(user_id): - """Load user from the config for session management.""" if not app_instance.is_configured: return None stored_username = app_instance.config.get('DOCKFLARE_USERNAME') + authorized_oauth_users = app_instance.config.get('OAUTH_AUTHORIZED_USERS', []) + if user_id == stored_username: - return User(user_id) + return User(user_id, auth_method='password') + elif user_id in authorized_oauth_users: + return User(user_id, auth_method='oauth') return None @app_instance.context_processor @@ -159,6 +177,7 @@ def create_app(): with app_instance.app_context(): from .web import routes as web_routes app_instance.register_blueprint(web_routes.bp) + csrf.exempt(web_routes.auth_callback) logging.info("Web blueprint registered.") from .web.api_v2_routes import api_v2_bp diff --git a/dockflare/app/core/user.py b/dockflare/app/core/user.py index 3026639..d7668c9 100644 --- a/dockflare/app/core/user.py +++ b/dockflare/app/core/user.py @@ -16,7 +16,23 @@ # # dockflare/app/core/user.py from flask_login import UserMixin +from datetime import datetime +from flask import current_app + class User(UserMixin): - - def __init__(self, username): + def __init__(self, username, auth_method='password', session_data=None): self.id = username + self.auth_method = auth_method + self.session_data = session_data or {} + self.login_time = datetime.utcnow() + + @property + def is_oauth_user(self): + return self.auth_method == 'oauth' + + def is_session_valid(self, max_age_seconds=None): + if max_age_seconds is None: + max_age_seconds = current_app.config.get('OAUTH_SESSION_TIMEOUT', 86400) + + age = (datetime.utcnow() - self.login_time).total_seconds() + return age < max_age_seconds diff --git a/dockflare/app/main.py b/dockflare/app/main.py index fa2b228..cb8c1e3 100644 --- a/dockflare/app/main.py +++ b/dockflare/app/main.py @@ -231,6 +231,30 @@ def main_application_entrypoint(): f.write(updated_payload) config_loader.apply_config_to_app(app, config_data) + + from app import oauth + def register_oauth_providers(flask_app, oauth_instance): + providers = flask_app.config.get('OAUTH_PROVIDERS', []) + for provider in providers: + if not provider.get('enabled'): + continue + + try: + client_id = fernet.decrypt(provider['client_id'].encode()).decode() + client_secret = fernet.decrypt(provider['client_secret'].encode()).decode() + except Exception: + logging.error(f"Could not decrypt credentials for provider {provider['name']}. Skipping.") + continue + + oauth_instance.register( + name=provider['id'], + client_id=client_id, + client_secret=client_secret, + server_metadata_url=f"https://accounts.google.com/.well-known/openid-configuration", + client_kwargs={'scope': 'openid email profile'} + ) + + register_oauth_providers(app, oauth) logging.info("DockFlare is configured and in Operational Mode.") except Exception as e: logging.error(f"Failed to load or decrypt configuration: {e}. Starting in Pre-Flight mode.", exc_info=True) diff --git a/dockflare/app/templates/login.html b/dockflare/app/templates/login.html index e56120f..2746eee 100644 --- a/dockflare/app/templates/login.html +++ b/dockflare/app/templates/login.html @@ -19,16 +19,50 @@ {% endfor %} {% endif %} {% endwith %} + + {% if oauth_providers %} +