From 32be5c7f23be10a22345363aaf4e85b5cb3128da Mon Sep 17 00:00:00 2001 From: Tanya Date: Fri, 2 Jan 2026 13:28:07 -0500 Subject: [PATCH] feat: Enhance auth database setup and environment variable loading This commit improves the setup of the authentication database by adding a new function to create necessary tables for both frontends. It also ensures that environment variables are loaded from a `.env` file before any database operations, enhancing configuration management. Additionally, minor updates are made to related scripts for better clarity and functionality. --- MONOREPO_MIGRATION.md | 1 + backend/app.py | 18 +++- backend/worker.py | 190 ++++++++++++++++++++++++++++++++++ scripts/drop_auth_database.sh | 1 + scripts/fix_admin_password.py | 1 + 5 files changed, 208 insertions(+), 3 deletions(-) diff --git a/MONOREPO_MIGRATION.md b/MONOREPO_MIGRATION.md index 5e83067..66c1d68 100644 --- a/MONOREPO_MIGRATION.md +++ b/MONOREPO_MIGRATION.md @@ -123,3 +123,4 @@ npm run dev:viewer 4. Update CI/CD pipelines if applicable 5. Archive or remove the old `punimtag-viewer` repository + diff --git a/backend/app.py b/backend/app.py index 149b513..d23da80 100644 --- a/backend/app.py +++ b/backend/app.py @@ -61,12 +61,22 @@ def start_worker() -> None: if "cursor" in python_executable.lower() or not python_executable.startswith("/usr"): python_executable = "/usr/bin/python3" - # Ensure PYTHONPATH is set correctly + # Ensure PYTHONPATH is set correctly and pass DATABASE_URL_AUTH explicitly + # Load .env file to get DATABASE_URL_AUTH if not already in environment + from dotenv import load_dotenv + env_file = project_root / ".env" + if env_file.exists(): + load_dotenv(dotenv_path=env_file) + worker_env = { **{k: v for k, v in os.environ.items()}, "PYTHONPATH": str(project_root), } + # Explicitly ensure DATABASE_URL_AUTH is passed to worker subprocess + if "DATABASE_URL_AUTH" in os.environ: + worker_env["DATABASE_URL_AUTH"] = os.environ["DATABASE_URL_AUTH"] + _worker_process = subprocess.Popen( [ python_executable, @@ -769,11 +779,13 @@ async def lifespan(app: FastAPI): ensure_face_excluded_column(inspector) ensure_role_permissions_table(inspector) - # Note: Auth database schema and tables are managed by the frontend - # Only check/update if the database exists (don't create it) + # Setup auth database tables for both frontends (viewer and admin) if auth_engine is not None: try: ensure_auth_user_is_active_column() + # Import and call worker's setup function to create all auth tables + from backend.worker import setup_auth_database_tables + setup_auth_database_tables() except Exception as auth_exc: # Auth database might not exist yet - that's okay, frontend will handle it print(f"â„šī¸ Auth database not available: {auth_exc}") diff --git a/backend/worker.py b/backend/worker.py index 40fda03..cf46260 100644 --- a/backend/worker.py +++ b/backend/worker.py @@ -4,19 +4,206 @@ from __future__ import annotations import signal import sys +from pathlib import Path from typing import NoReturn import uuid +from dotenv import load_dotenv + +# Load environment variables from .env file before importing anything that needs them +# Path calculation: backend/worker.py -> backend/ -> punimtag/ -> .env +env_path = Path(__file__).parent.parent / ".env" +env_path = env_path.resolve() # Make absolute path +if env_path.exists(): + load_dotenv(dotenv_path=env_path, override=True) from rq import Worker from redis import Redis from backend.services.tasks import import_photos_task, process_faces_task +from backend.db.session import auth_engine +from sqlalchemy import text # Redis connection for RQ redis_conn = Redis(host="localhost", port=6379, db=0, decode_responses=False) +def setup_auth_database_tables() -> None: + """Create all necessary tables in the auth database for both frontends.""" + if auth_engine is None: + print("[Worker] âš ī¸ Auth database not configured (DATABASE_URL_AUTH not set), skipping table creation") + return + + try: + print("[Worker] đŸ—ƒī¸ Setting up auth database tables...") + + with auth_engine.connect() as conn: + # Create users table + conn.execute(text(""" + CREATE TABLE IF NOT EXISTS users ( + id SERIAL PRIMARY KEY, + email VARCHAR(255) UNIQUE NOT NULL, + name VARCHAR(255) NOT NULL, + password_hash VARCHAR(255) NOT NULL, + is_admin BOOLEAN DEFAULT FALSE, + has_write_access BOOLEAN DEFAULT FALSE, + email_verified BOOLEAN DEFAULT FALSE, + email_confirmation_token VARCHAR(255) UNIQUE, + email_confirmation_token_expiry TIMESTAMP, + password_reset_token VARCHAR(255) UNIQUE, + password_reset_token_expiry TIMESTAMP, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + """)) + + # Add missing columns if table already exists + for col_def in [ + "ALTER TABLE users ADD COLUMN IF NOT EXISTS is_admin BOOLEAN DEFAULT FALSE", + "ALTER TABLE users ADD COLUMN IF NOT EXISTS has_write_access BOOLEAN DEFAULT FALSE", + "ALTER TABLE users ADD COLUMN IF NOT EXISTS email_verified BOOLEAN DEFAULT FALSE", + "ALTER TABLE users ADD COLUMN IF NOT EXISTS email_confirmation_token VARCHAR(255)", + "ALTER TABLE users ADD COLUMN IF NOT EXISTS email_confirmation_token_expiry TIMESTAMP", + "ALTER TABLE users ADD COLUMN IF NOT EXISTS password_reset_token VARCHAR(255)", + "ALTER TABLE users ADD COLUMN IF NOT EXISTS password_reset_token_expiry TIMESTAMP", + "ALTER TABLE users ADD COLUMN IF NOT EXISTS is_active BOOLEAN DEFAULT TRUE", + ]: + try: + conn.execute(text(col_def)) + except Exception: + pass # Column might already exist or error is expected + + # Create unique indexes for nullable columns + conn.execute(text(""" + CREATE UNIQUE INDEX IF NOT EXISTS users_email_confirmation_token_key + ON users(email_confirmation_token) + WHERE email_confirmation_token IS NOT NULL; + """)) + conn.execute(text(""" + CREATE UNIQUE INDEX IF NOT EXISTS users_password_reset_token_key + ON users(password_reset_token) + WHERE password_reset_token IS NOT NULL; + """)) + + # Create pending_identifications table + conn.execute(text(""" + CREATE TABLE IF NOT EXISTS pending_identifications ( + id SERIAL PRIMARY KEY, + face_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + first_name VARCHAR(255) NOT NULL, + last_name VARCHAR(255) NOT NULL, + middle_name VARCHAR(255), + maiden_name VARCHAR(255), + date_of_birth DATE, + status VARCHAR(50) DEFAULT 'pending', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE + ); + """)) + + # Create indexes for pending_identifications + conn.execute(text("CREATE INDEX IF NOT EXISTS idx_pending_identifications_face_id ON pending_identifications(face_id);")) + conn.execute(text("CREATE INDEX IF NOT EXISTS idx_pending_identifications_user_id ON pending_identifications(user_id);")) + conn.execute(text("CREATE INDEX IF NOT EXISTS idx_pending_identifications_status ON pending_identifications(status);")) + + # Create pending_photos table + conn.execute(text(""" + CREATE TABLE IF NOT EXISTS pending_photos ( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL, + filename VARCHAR(255) NOT NULL, + original_filename VARCHAR(255) NOT NULL, + file_path VARCHAR(512) NOT NULL, + file_size INTEGER NOT NULL, + mime_type VARCHAR(100) NOT NULL, + status VARCHAR(50) DEFAULT 'pending', + submitted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + reviewed_at TIMESTAMP, + reviewed_by INTEGER, + rejection_reason TEXT, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE + ); + """)) + + # Create indexes for pending_photos + conn.execute(text("CREATE INDEX IF NOT EXISTS idx_pending_photos_user_id ON pending_photos(user_id);")) + conn.execute(text("CREATE INDEX IF NOT EXISTS idx_pending_photos_status ON pending_photos(status);")) + conn.execute(text("CREATE INDEX IF NOT EXISTS idx_pending_photos_submitted_at ON pending_photos(submitted_at);")) + + # Create inappropriate_photo_reports table + conn.execute(text(""" + CREATE TABLE IF NOT EXISTS inappropriate_photo_reports ( + id SERIAL PRIMARY KEY, + photo_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + status VARCHAR(50) DEFAULT 'pending', + reported_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + reviewed_at TIMESTAMP, + reviewed_by INTEGER, + review_notes TEXT, + report_comment TEXT, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + CONSTRAINT uq_photo_user_report UNIQUE (photo_id, user_id) + ); + """)) + + # Create indexes for inappropriate_photo_reports + conn.execute(text("CREATE INDEX IF NOT EXISTS idx_inappropriate_photo_reports_photo_id ON inappropriate_photo_reports(photo_id);")) + conn.execute(text("CREATE INDEX IF NOT EXISTS idx_inappropriate_photo_reports_user_id ON inappropriate_photo_reports(user_id);")) + conn.execute(text("CREATE INDEX IF NOT EXISTS idx_inappropriate_photo_reports_status ON inappropriate_photo_reports(status);")) + conn.execute(text("CREATE INDEX IF NOT EXISTS idx_inappropriate_photo_reports_reported_at ON inappropriate_photo_reports(reported_at);")) + + # Create pending_linkages table + conn.execute(text(""" + CREATE TABLE IF NOT EXISTS pending_linkages ( + id SERIAL PRIMARY KEY, + photo_id INTEGER NOT NULL, + tag_id INTEGER, + tag_name VARCHAR(255), + user_id INTEGER NOT NULL, + status VARCHAR(50) DEFAULT 'pending', + notes TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE + ); + """)) + + # Create indexes for pending_linkages + conn.execute(text("CREATE INDEX IF NOT EXISTS idx_pending_linkages_photo_id ON pending_linkages(photo_id);")) + conn.execute(text("CREATE INDEX IF NOT EXISTS idx_pending_linkages_tag_id ON pending_linkages(tag_id);")) + conn.execute(text("CREATE INDEX IF NOT EXISTS idx_pending_linkages_user_id ON pending_linkages(user_id);")) + conn.execute(text("CREATE INDEX IF NOT EXISTS idx_pending_linkages_status ON pending_linkages(status);")) + + # Create photo_favorites table + conn.execute(text(""" + CREATE TABLE IF NOT EXISTS photo_favorites ( + id SERIAL PRIMARY KEY, + photo_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + favorited_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + CONSTRAINT uq_photo_user_favorite UNIQUE (photo_id, user_id) + ); + """)) + + # Create indexes for photo_favorites + conn.execute(text("CREATE INDEX IF NOT EXISTS idx_photo_favorites_photo_id ON photo_favorites(photo_id);")) + conn.execute(text("CREATE INDEX IF NOT EXISTS idx_photo_favorites_user_id ON photo_favorites(user_id);")) + conn.execute(text("CREATE INDEX IF NOT EXISTS idx_photo_favorites_favorited_at ON photo_favorites(favorited_at);")) + + conn.commit() + + print("[Worker] ✅ Auth database tables created/verified successfully") + except Exception as e: + print(f"[Worker] âš ī¸ Failed to create auth database tables: {e}") + print("[Worker] Tables may already exist or database may not be accessible") + # Don't exit - worker can still function without auth tables + + def main() -> NoReturn: """Worker entrypoint - starts RQ worker to process background jobs.""" def _handle_sigterm(_signum, _frame): @@ -31,6 +218,9 @@ def main() -> NoReturn: print(f"[Worker] Starting worker: {worker_name}") print(f"[Worker] Listening on queue: default") + # Setup auth database tables for both frontends + setup_auth_database_tables() + # Check if Redis is accessible try: redis_conn.ping() diff --git a/scripts/drop_auth_database.sh b/scripts/drop_auth_database.sh index de4c960..7f46226 100755 --- a/scripts/drop_auth_database.sh +++ b/scripts/drop_auth_database.sh @@ -13,3 +13,4 @@ else echo "âš ī¸ Failed to drop auth database (it may not exist)" fi + diff --git a/scripts/fix_admin_password.py b/scripts/fix_admin_password.py index 8e47517..5ab7c7a 100644 --- a/scripts/fix_admin_password.py +++ b/scripts/fix_admin_password.py @@ -50,3 +50,4 @@ if __name__ == "__main__": success = fix_admin_password() sys.exit(0 if success else 1) +