CI / skip-ci-check (pull_request) Successful in 29s
CI / python-lint (pull_request) Successful in 31s
CI / docker-ci (pull_request) Successful in 32s
CI / secret-scan (pull_request) Successful in 37s
CI / e2e (pull_request) Successful in 4m32s
CI / viewer-unit (pull_request) Successful in 4m42s
CI / admin-unit (pull_request) Successful in 5m6s
Capture optional contact fields on identify/approve, add Select All and Approve Next 10 on User Identified Faces, and email admins when the pending queue grows (ADMIN_NOTIFY_EMAIL via existing noreply SMTP).
257 lines
12 KiB
Python
257 lines
12 KiB
Python
"""RQ worker entrypoint for PunimTag."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import signal
|
|
import sys
|
|
import uuid
|
|
from pathlib import Path
|
|
from typing import NoReturn
|
|
|
|
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 redis import Redis # noqa: E402
|
|
from rq import Worker # noqa: E402
|
|
from sqlalchemy import text # noqa: E402
|
|
|
|
from backend.db.session import auth_engine # noqa: E402
|
|
|
|
# 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,
|
|
email TEXT,
|
|
phone TEXT,
|
|
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
|
|
);
|
|
"""))
|
|
conn.execute(text(
|
|
"ALTER TABLE pending_identifications ADD COLUMN IF NOT EXISTS email TEXT"
|
|
))
|
|
conn.execute(text(
|
|
"ALTER TABLE pending_identifications ADD COLUMN IF NOT EXISTS phone TEXT"
|
|
))
|
|
|
|
# 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):
|
|
sys.exit(0)
|
|
|
|
signal.signal(signal.SIGTERM, _handle_sigterm)
|
|
signal.signal(signal.SIGINT, _handle_sigterm)
|
|
|
|
# Generate unique worker name to avoid conflicts
|
|
worker_name = f"punimtag-worker-{uuid.uuid4().hex[:8]}"
|
|
|
|
print(f"[Worker] Starting worker: {worker_name}")
|
|
print("[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()
|
|
print("[Worker] Redis connection successful")
|
|
except Exception as e:
|
|
print(f"[Worker] ❌ Redis connection failed: {e}")
|
|
sys.exit(1)
|
|
|
|
# Register tasks with worker
|
|
# Tasks are imported from services.tasks
|
|
worker = Worker(
|
|
["default"],
|
|
connection=redis_conn,
|
|
name=worker_name,
|
|
)
|
|
|
|
print("[Worker] ✅ Worker ready, waiting for jobs...")
|
|
|
|
# Start worker
|
|
worker.work()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
|
|
|