Spaces:
Running
Running
| """ | |
| bootstrap_admin.py — one-off: create (or promote) the first Iris admin account. | |
| Because there is no public signup, the very first admin must be provisioned out-of-band. | |
| This script creates the Firebase Auth user, sets the custom claim {role: "admin"} (which | |
| the backend's require_auth reads), and writes the iris_staff profile doc. Run it once. | |
| It is idempotent: if the user already exists, it just (re)sets the password + admin claim. | |
| CREDENTIALS — the Firebase service account is read from either: | |
| * env var FIREBASE = the full service-account JSON string (same secret the app uses), OR | |
| * a path to a serviceAccount.json file passed with --service-account | |
| USAGE (PowerShell example): | |
| $env:FIREBASE = Get-Content serviceAccount.json -Raw | |
| python bootstrap_admin.py --email admin@example.com --password '<strong-password>' --name "Iris Admin" | |
| USAGE (bash example): | |
| export FIREBASE="$(cat serviceAccount.json)" | |
| python bootstrap_admin.py --email admin@example.com --password '<strong-password>' --name "Iris Admin" | |
| The password is taken from --password or the ADMIN_PASSWORD env var; never hardcode it here. | |
| """ | |
| import os | |
| import sys | |
| import json | |
| import argparse | |
| from datetime import datetime, timezone | |
| import firebase_admin | |
| from firebase_admin import credentials, auth, firestore | |
| def load_credentials(service_account_path: str): | |
| """Build a firebase_admin credential from the FIREBASE env var or a JSON file.""" | |
| if os.environ.get("FIREBASE"): | |
| return credentials.Certificate(json.loads(os.environ["FIREBASE"])) | |
| if service_account_path and os.path.exists(service_account_path): | |
| return credentials.Certificate(service_account_path) | |
| sys.exit("ERROR: provide credentials via the FIREBASE env var or --service-account <path>.") | |
| def main() -> None: | |
| parser = argparse.ArgumentParser(description="Create or promote the first Iris admin.") | |
| parser.add_argument("--email", required=True, help="Admin email address.") | |
| parser.add_argument("--password", default=os.environ.get("ADMIN_PASSWORD", "")) | |
| parser.add_argument("--name", default="Iris Admin") | |
| parser.add_argument("--service-account", default="serviceAccount.json", | |
| help="Path to a service account JSON (ignored if FIREBASE env is set).") | |
| args = parser.parse_args() | |
| if not args.password: | |
| sys.exit("ERROR: supply the password via --password or the ADMIN_PASSWORD env var.") | |
| cred = load_credentials(args.service_account) | |
| if not firebase_admin._apps: | |
| firebase_admin.initialize_app(cred) | |
| db = firestore.client() | |
| email = args.email.strip().lower() | |
| # Create the user, or update it if it already exists (idempotent). | |
| try: | |
| user = auth.create_user(email=email, password=args.password, display_name=args.name) | |
| print(f"Created user {email} (uid={user.uid}).") | |
| except auth.EmailAlreadyExistsError: | |
| user = auth.get_user_by_email(email) | |
| auth.update_user(user.uid, password=args.password, display_name=args.name, disabled=False) | |
| print(f"User {email} already existed (uid={user.uid}); password + profile updated.") | |
| # Grant the admin role via custom claim — this is what the backend checks. | |
| auth.set_custom_user_claims(user.uid, {"role": "admin"}) | |
| print("Set custom claim {role: 'admin'}.") | |
| # Write the staff profile doc the dashboard lists. | |
| db.collection("iris_staff").document(user.uid).set({ | |
| "uid": user.uid, | |
| "email": email, | |
| "name": args.name, | |
| "role": "admin", | |
| "disabled": False, | |
| "created_at": datetime.now(timezone.utc).isoformat(), | |
| "created_by": "bootstrap_admin.py", | |
| }, merge=True) | |
| print("Wrote iris_staff profile.") | |
| print("\nDone. Sign in to the dashboard with this email/password.") | |
| print("Note: if the user was already signed in somewhere, the new admin claim") | |
| print("takes effect after the next token refresh (re-login).") | |
| if __name__ == "__main__": | |
| main() | |