diff --git a/backend/main.py b/backend/main.py index c5d3192..0c60ad5 100644 --- a/backend/main.py +++ b/backend/main.py @@ -105,39 +105,18 @@ COMPONENT_TEMPLATES = { def seed_database(): - """Create tables and insert preseed data if missing.""" + """Create tables and seed services (no default users — first-run setup creates them).""" Base.metadata.create_all(bind=engine) db = SessionLocal() try: - # Seed users - if not db.query(User).filter(User.username == "admin").first(): - db.add( - User( - username="admin", - password_hash=hash_password("admin123"), - display_name="Administrator", - role="admin", - ) - ) - if not db.query(User).filter(User.username == "viewer").first(): - db.add( - User( - username="viewer", - password_hash=hash_password("viewer123"), - display_name="Viewer", - role="viewer", - ) - ) - - # Seed services + components - for name, desc, sell, cost in [ - ("Atlas", "Core storage service", 4000.0, 2000.0), - ("Atlas+", "Premium storage + backup service", 5000.0, 2800.0), - ("Rif", "Core server service", 3500.0, 1700.0), - ("Rif+", "Premium server + backup service", 4500.0, 2200.0), - ]: - existing = db.query(Service).filter(Service.name == name).first() - if not existing: + # Seed services + components (only if no services exist yet) + if not db.query(Service).first(): + for name, desc, sell, cost in [ + ("Atlas", "Core storage service", 4000.0, 2000.0), + ("Atlas+", "Premium storage + backup service", 5000.0, 2800.0), + ("Rif", "Core server service", 3500.0, 1700.0), + ("Rif+", "Premium server + backup service", 4500.0, 2200.0), + ]: svc = Service( name=name, description=desc, @@ -146,9 +125,8 @@ def seed_database(): active=True, ) db.add(svc) - db.flush() # get the id + db.flush() - # Add component templates for cname, ucost, usell, qty, notes in COMPONENT_TEMPLATES.get(name, []): db.add( ServiceComponent( @@ -160,14 +138,26 @@ def seed_database(): notes=notes, ) ) - - db.commit() + db.commit() finally: db.close() seed_database() + +# --------------------------------------------------------------------------- +# Helper: check if setup is complete +# --------------------------------------------------------------------------- +def is_setup_complete(db: Session) -> bool: + """Returns True if at least one admin user exists.""" + return db.query(User).filter(User.role == "admin").first() is not None + + +def require_setup(db: Session = Depends(get_db)): + """Dependency that raises 404 if setup is not complete (caller handles redirect).""" + return is_setup_complete(db) + # --------------------------------------------------------------------------- # Helper: compute monthly P&L # --------------------------------------------------------------------------- @@ -552,9 +542,80 @@ def api_dashboard(db: Session = Depends(get_db)): # --------------------------------------------------------------------------- # Frontend Routes # --------------------------------------------------------------------------- +@app.get("/setup", response_class=HTMLResponse) +def setup_page(request: Request, db: Session = Depends(get_db)): + """Serve the first-time setup page.""" + if is_setup_complete(db): + return RedirectResponse(url="/login") + return templates.TemplateResponse(request, "setup.html") + + +class SetupInput(BaseModel): + admin_username: str + admin_password: str + admin_display: str = "Administrator" + + +@app.post("/api/setup") +def api_setup( + input: SetupInput, + db: Session = Depends(get_db), +): + """Create the first admin user (only works if no admin exists yet).""" + if is_setup_complete(db): + raise HTTPException(status_code=400, detail="Setup already completed") + + if not input.admin_username.strip() or len(input.admin_password) < 4: + raise HTTPException(status_code=400, detail="Username required, password min 4 chars") + + # Create admin + admin = User( + username=input.admin_username.strip(), + password_hash=hash_password(input.admin_password), + display_name=input.admin_display.strip() or input.admin_username.strip(), + role="admin", + ) + db.add(admin) + + # Create guest viewer + import secrets + guest_pass = secrets.token_hex(6) + viewer = User( + username="viewer", + password_hash=hash_password(guest_pass), + display_name="Viewer", + role="viewer", + ) + db.add(viewer) + db.commit() + db.refresh(admin) + db.refresh(viewer) + + # Auto-login as admin + token = create_access_token(admin.id, admin.role) + response = JSONResponse({ + "token": token, + "guest_password": guest_pass, + "user": { + "id": admin.id, + "username": admin.username, + "display_name": admin.display_name, + "role": admin.role, + }, + }) + response.set_cookie( + key="token", value=token, + httponly=True, max_age=86400, samesite="lax", + secure=False, path="/", + ) + return response + + @app.get("/login", response_class=HTMLResponse) -def login_page(request: Request): - """Serve the login page.""" +def login_page(request: Request, db: Session = Depends(get_db)): + """Serve the login page. Redirect to /setup if no admin exists.""" + if not is_setup_complete(db): + return RedirectResponse(url="/setup") return templates.TemplateResponse(request, "login.html") @@ -565,6 +626,8 @@ def dashboard_page( db: Session = Depends(get_db), ): """Serve the dashboard page.""" + if not is_setup_complete(db): + return RedirectResponse(url="/setup") if not current_user: return RedirectResponse(url="/login") current_month = datetime.now(timezone.utc).strftime("%Y-%m") @@ -589,6 +652,8 @@ def transactions_page( db: Session = Depends(get_db), ): """Serve the transactions page.""" + if not is_setup_complete(db): + return RedirectResponse(url="/setup") if not current_user: return RedirectResponse(url="/login") current_month = datetime.now(timezone.utc).strftime("%Y-%m") @@ -610,6 +675,8 @@ def services_page( db: Session = Depends(get_db), ): """Serve the services management page.""" + if not is_setup_complete(db): + return RedirectResponse(url="/setup") if not current_user: return RedirectResponse(url="/login") services = db.query(Service).order_by(Service.name).all() diff --git a/backend/templates/login.html b/backend/templates/login.html index 5bdfa3e..aa82d3b 100644 --- a/backend/templates/login.html +++ b/backend/templates/login.html @@ -5,7 +5,10 @@
Let's set up your admin account to get started.
++ 🔑 Password set — save it somewhere safe. +
+
+ Username: viewer
+ Password:
+