""" ChillTrack API — Firebase auth helpers. Auth pattern follows RentNow-Admin-API/app.py and NeoFix-API/main.py: - Firebase service account JSON lives in the `FIREBASE` env var (json.loads it). - Firestore is the datastore (unlike the Realtime-DB repos), so init mirrors RentNow's firestore.client(database_id=os.environ.get("FIRESTORE_DB_NAME", "(default)")). - Firebase ID token in `Authorization: Bearer `. - 401 = missing/invalid token, 403 = signed in but wrong role. """ import os import json from functools import wraps from flask import request, jsonify import firebase_admin from firebase_admin import credentials, auth, firestore AUDIT_COLLECTION = "ctk_audit" def init_firebase(): """Initializes the Firebase Admin app exactly once and returns the Firestore client.""" if firebase_admin._apps: return firestore.client(database_id=os.environ.get("FIRESTORE_DB_NAME", "(default)")) raw = os.environ.get("FIREBASE") if not raw: print("FATAL: The 'FIREBASE' environment variable is not set.") exit(1) try: cred_json = json.loads(raw) cred = credentials.Certificate(cred_json) storage_bucket = os.environ.get("FIREBASE_STORAGE_BUCKET", "") firebase_admin.initialize_app(cred, {"storageBucket": storage_bucket} if storage_bucket else {}) print("Firebase Admin SDK initialized successfully.") except Exception as e: print(f"FATAL: Error initializing Firebase: {e}") exit(1) return firestore.client(database_id=os.environ.get("FIRESTORE_DB_NAME", "(default)")) def require_auth(fn): """Verifies the Firebase ID token and attaches the decoded token to request.user.""" @wraps(fn) def wrapper(*args, **kwargs): header = request.headers.get("Authorization", "") token = header[7:].strip() if header.startswith("Bearer ") else "" if not token: return jsonify({"error": "Missing bearer token."}), 401 try: decoded_token = auth.verify_id_token(token) except Exception as e: print(f"Token verification failed: {e}") return jsonify({"error": "Invalid or expired token."}), 401 request.user = decoded_token return fn(*args, **kwargs) return wrapper def require_role(role): """Decorator factory: require_auth, plus request.user['role'] must equal `role`.""" def decorator(fn): @wraps(fn) @require_auth def wrapper(*args, **kwargs): if request.user.get("role") != role: return jsonify({"error": f"'{role}' access required."}), 403 return fn(*args, **kwargs) return wrapper return decorator def audit_log(db, actor, action, entity, entity_id, metadata=None): """Writes an audit trail doc. actor is {uid, email}. Never raises — logs and swallows.""" try: db.collection(AUDIT_COLLECTION).add({ "actor": actor, "action": action, "entity": entity, "entity_id": entity_id, "metadata": metadata or {}, "ts": firestore.SERVER_TIMESTAMP, }) except Exception as e: print(f"audit_log failed: {e}")