diff --git a/app/init_wizard.py b/app/init_wizard.py new file mode 100644 index 0000000..6d385db --- /dev/null +++ b/app/init_wizard.py @@ -0,0 +1,107 @@ +""" +HarborForge initialization via AbstractWizard. + +On startup, reads config from AbstractWizard and creates: +- Admin user (if not exists) +- Default project (if configured) +""" +import os +import logging +import httpx +from sqlalchemy.orm import Session + +from app.models import models +from app.api.deps import get_password_hash + +logger = logging.getLogger("harborforge.init") + +WIZARD_URL = os.getenv("WIZARD_URL", "http://wizard:8080") +WIZARD_CONFIG = os.getenv("WIZARD_CONFIG", "harborforge.json") + + +def fetch_wizard_config() -> dict | None: + """Fetch initialization config from AbstractWizard.""" + url = f"{WIZARD_URL}/api/v1/config/{WIZARD_CONFIG}" + try: + resp = httpx.get(url, timeout=10) + if resp.status_code == 200: + data = resp.json() + # AbstractWizard wraps in {"data": ...} + return data.get("data", data) + elif resp.status_code == 404: + logger.info("No wizard config found at %s, skipping initialization", url) + return None + else: + logger.warning("Wizard returned %d: %s", resp.status_code, resp.text) + return None + except httpx.ConnectError: + logger.info("AbstractWizard not available at %s, skipping", WIZARD_URL) + return None + except Exception as e: + logger.warning("Failed to fetch wizard config: %s", e) + return None + + +def init_admin_user(db: Session, admin_cfg: dict) -> None: + """Create admin user if not exists.""" + username = admin_cfg.get("username", "admin") + existing = db.query(models.User).filter(models.User.username == username).first() + if existing: + logger.info("Admin user '%s' already exists (id=%d), skipping", username, existing.id) + return + + password = admin_cfg.get("password", "changeme") + user = models.User( + username=username, + email=admin_cfg.get("email", f"{username}@harborforge.local"), + full_name=admin_cfg.get("full_name", "Admin"), + hashed_password=get_password_hash(password), + is_admin=True, + is_active=True, + ) + db.add(user) + db.commit() + db.refresh(user) + logger.info("Created admin user '%s' (id=%d)", username, user.id) + + +def init_default_project(db: Session, project_cfg: dict, admin_user_id: int) -> None: + """Create default project if configured and not exists.""" + name = project_cfg.get("name", "Default") + existing = db.query(models.Project).filter(models.Project.name == name).first() + if existing: + logger.info("Project '%s' already exists (id=%d), skipping", name, existing.id) + return + + project = models.Project( + name=name, + description=project_cfg.get("description", ""), + owner_id=admin_user_id, + ) + db.add(project) + db.commit() + db.refresh(project) + logger.info("Created default project '%s' (id=%d)", name, project.id) + + +def run_init(db: Session) -> None: + """Main initialization entry point.""" + config = fetch_wizard_config() + if not config: + return + + logger.info("Running HarborForge initialization from AbstractWizard") + + # Admin user + admin_cfg = config.get("admin") + if admin_cfg: + init_admin_user(db, admin_cfg) + + # Default project + project_cfg = config.get("default_project") + if project_cfg: + admin = db.query(models.User).filter(models.User.is_admin == True).first() + if admin: + init_default_project(db, project_cfg, admin.id) + + logger.info("Initialization complete") diff --git a/app/main.py b/app/main.py index fb15eb6..f43e6d4 100644 --- a/app/main.py +++ b/app/main.py @@ -46,6 +46,14 @@ app.include_router(misc_router) # Run database migration on startup @app.on_event("startup") def startup(): - from app.core.config import Base, engine + from app.core.config import Base, engine, SessionLocal from app.models import webhook, apikey, activity, milestone, notification, worklog Base.metadata.create_all(bind=engine) + + # Initialize from AbstractWizard (admin user, default project, etc.) + from app.init_wizard import run_init + db = SessionLocal() + try: + run_init(db) + finally: + db.close()