From 2c7824814922f587756183a32992adeaa6a2c76f Mon Sep 17 00:00:00 2001 From: Stu Alexandere Date: Sun, 21 Dec 2025 18:04:27 +0000 Subject: [PATCH] Added first user setup mode Removed default user details from defaults, you can use ./go and it will ask you for your user. --- .env.default | 7 +- backends/advanced/compose/backend.yml | 6 +- backends/advanced/config/config.defaults.yaml | 2 - .../src/advanced_omi_backend/app_factory.py | 17 +- .../advanced/src/advanced_omi_backend/auth.py | 23 +- .../routers/api_router.py | 2 + .../routers/modules/setup_routes.py | 128 +++++++ backends/advanced/webui/src/App.tsx | 2 + .../src/components/auth/ProtectedRoute.tsx | 7 +- .../webui/src/contexts/AuthContext.tsx | 28 +- .../advanced/webui/src/pages/LoginPage.tsx | 7 +- .../advanced/webui/src/pages/SetupPage.tsx | 321 ++++++++++++++++++ backends/advanced/webui/src/services/api.ts | 10 + clear.sh | 130 +++++++ go.sh | 154 +++++++++ 15 files changed, 828 insertions(+), 16 deletions(-) create mode 100644 backends/advanced/src/advanced_omi_backend/routers/modules/setup_routes.py create mode 100644 backends/advanced/webui/src/pages/SetupPage.tsx create mode 100755 clear.sh create mode 100755 go.sh diff --git a/.env.default b/.env.default index 05df89ef..e12cc5a6 100644 --- a/.env.default +++ b/.env.default @@ -10,8 +10,13 @@ COMPOSE_PROJECT_NAME=chronicle # ========================================== # AUTHENTICATION & SECURITY # ========================================== -# Run ./quick-start.sh to generate secure credentials +# REQUIRED: Run ./quick-start.sh to generate a secure AUTH_SECRET_KEY +# This is needed for JWT token signing and must be set before backend starts AUTH_SECRET_KEY= + +# Admin account setup (two options): +# Option 1: Set ADMIN_PASSWORD here for automatic admin creation on startup (good for Docker/CI) +# Option 2: Leave ADMIN_PASSWORD empty to create admin via web UI setup screen at /setup ADMIN_NAME=admin ADMIN_EMAIL=admin@example.com ADMIN_PASSWORD= diff --git a/backends/advanced/compose/backend.yml b/backends/advanced/compose/backend.yml index 10d8ae0a..f64933a0 100644 --- a/backends/advanced/compose/backend.yml +++ b/backends/advanced/compose/backend.yml @@ -23,10 +23,10 @@ services: - MYCELIA_URL=${MYCELIA_URL:-http://mycelia-backend:5173} healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8000/readiness"] - interval: 30s - timeout: 30s + interval: 5s + timeout: 10s retries: 5 - start_period: 5s + start_period: 10s restart: unless-stopped networks: - chronicle-network diff --git a/backends/advanced/config/config.defaults.yaml b/backends/advanced/config/config.defaults.yaml index e5dbe937..f8854ed2 100644 --- a/backends/advanced/config/config.defaults.yaml +++ b/backends/advanced/config/config.defaults.yaml @@ -11,8 +11,6 @@ wizard_completed: false # Authentication Configuration auth: secret_key: '' # Auto-generated on first run if empty - admin_name: admin - admin_email: admin@example.com admin_password_hash: '' # Set via wizard or environment variable # Speech Detection Settings diff --git a/backends/advanced/src/advanced_omi_backend/app_factory.py b/backends/advanced/src/advanced_omi_backend/app_factory.py index a8ba4cc9..f3c3f4ee 100644 --- a/backends/advanced/src/advanced_omi_backend/app_factory.py +++ b/backends/advanced/src/advanced_omi_backend/app_factory.py @@ -24,6 +24,7 @@ import advanced_omi_backend.settings_manager as settings_manager_module from advanced_omi_backend.auth import ( bearer_backend, + check_admin_exists, cookie_backend, create_admin_user_if_needed, current_superuser, @@ -100,11 +101,21 @@ async def lifespan(app: FastAPI): application_logger.error(f"Failed to initialize settings manager: {e}") raise - # Create admin user if needed + # Create admin user if needed (supports both env var and web UI setup) try: - await create_admin_user_if_needed() + import os + if os.getenv("ADMIN_PASSWORD"): + # Backward compatibility: Use environment variable for auto-creation + await create_admin_user_if_needed() + else: + # Web UI setup mode: Check if admin exists + admin_exists = await check_admin_exists() + if not admin_exists: + application_logger.info("⚠️ No admin user exists - setup via web UI required at /setup") + else: + application_logger.info("✅ Admin user already exists (setup completed)") except Exception as e: - application_logger.error(f"Failed to create admin user: {e}") + application_logger.error(f"Failed to check/create admin user: {e}") # Don't raise here as this is not critical for startup # Sync admin user with Mycelia OAuth (if using Mycelia memory provider) diff --git a/backends/advanced/src/advanced_omi_backend/auth.py b/backends/advanced/src/advanced_omi_backend/auth.py index 7c68d0b4..843d5160 100644 --- a/backends/advanced/src/advanced_omi_backend/auth.py +++ b/backends/advanced/src/advanced_omi_backend/auth.py @@ -46,8 +46,8 @@ def _verify_configured(var_name: str, *, optional: bool = False) -> Optional[str SECRET_KEY = _verify_configured("AUTH_SECRET_KEY") COOKIE_SECURE = _verify_configured("COOKIE_SECURE", optional=True) == "true" -# Admin user configuration -ADMIN_PASSWORD = _verify_configured("ADMIN_PASSWORD") +# Admin user configuration (optional - can use web UI setup if not set) +ADMIN_PASSWORD = _verify_configured("ADMIN_PASSWORD", optional=True) ADMIN_EMAIL = _verify_configured("ADMIN_EMAIL", optional=True) or "admin@example.com" @@ -200,6 +200,25 @@ def get_accessible_user_ids(user: User) -> list[str] | None: return [str(user.id)] # Can only access own data +async def check_admin_exists() -> bool: + """ + Check if any admin user exists in the database. + + Returns: + True if at least one superuser exists, False otherwise + """ + try: + # Import User model here to avoid circular import + from advanced_omi_backend.users import User + + # Query for any user with is_superuser=True + admin = await User.find_one({"is_superuser": True}) + return admin is not None + except Exception as e: + logger.error(f"Failed to check admin existence: {e}") + raise + + async def create_admin_user_if_needed(): """Create admin user during startup if it doesn't exist and credentials are provided.""" if not ADMIN_PASSWORD: diff --git a/backends/advanced/src/advanced_omi_backend/routers/api_router.py b/backends/advanced/src/advanced_omi_backend/routers/api_router.py index e6abfe48..cf9652cd 100644 --- a/backends/advanced/src/advanced_omi_backend/routers/api_router.py +++ b/backends/advanced/src/advanced_omi_backend/routers/api_router.py @@ -21,6 +21,7 @@ user_router, ) from .modules.health_routes import router as health_router +from .modules.setup_routes import router as setup_router logger = logging.getLogger(__name__) audio_logger = logging.getLogger("audio_processing") @@ -29,6 +30,7 @@ router = APIRouter(prefix="/api", tags=["api"]) # Include all sub-routers +router.include_router(setup_router) # Setup routes (public, no auth required) router.include_router(audio_router) router.include_router(user_router) router.include_router(chat_router) diff --git a/backends/advanced/src/advanced_omi_backend/routers/modules/setup_routes.py b/backends/advanced/src/advanced_omi_backend/routers/modules/setup_routes.py new file mode 100644 index 00000000..edb2fdd6 --- /dev/null +++ b/backends/advanced/src/advanced_omi_backend/routers/modules/setup_routes.py @@ -0,0 +1,128 @@ +""" +Setup routes for first-time admin account creation. + +Provides public endpoints for checking setup status and creating the initial admin user. +""" + +import logging +from typing import Optional + +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel, EmailStr, Field, field_validator + +from advanced_omi_backend.auth import check_admin_exists, get_user_manager +from advanced_omi_backend.users import UserCreate, get_user_db + +logger = logging.getLogger(__name__) + +router = APIRouter(prefix="/setup", tags=["setup"]) + + +class SetupStatusResponse(BaseModel): + """Response model for setup status check.""" + requires_setup: bool = Field(..., description="Whether initial admin setup is required") + + +class AdminCreateRequest(BaseModel): + """Request model for creating the first admin user.""" + display_name: str = Field(..., min_length=1, description="Administrator's display name") + email: EmailStr = Field(..., description="Administrator's email address") + password: str = Field(..., min_length=8, description="Administrator's password (minimum 8 characters)") + confirm_password: str = Field(..., description="Password confirmation") + + @field_validator('confirm_password') + @classmethod + def passwords_match(cls, v: str, info) -> str: + """Validate that password and confirm_password match.""" + if 'password' in info.data and v != info.data['password']: + raise ValueError('Passwords do not match') + return v + + +class AdminCreateResponse(BaseModel): + """Response model for successful admin creation.""" + message: str + user_id: str + email: str + + +@router.get("/status", response_model=SetupStatusResponse) +async def get_setup_status(): + """ + Check if initial admin setup is required. + + Public endpoint (no authentication required). + Returns whether an admin user already exists. + """ + try: + admin_exists = await check_admin_exists() + return SetupStatusResponse(requires_setup=not admin_exists) + except Exception as e: + logger.error(f"Failed to check setup status: {e}") + raise HTTPException(status_code=500, detail="Failed to check setup status") + + +@router.post("/create-admin", response_model=AdminCreateResponse, status_code=201) +async def create_admin(request: AdminCreateRequest): + """ + Create the first admin user. + + Public endpoint (no authentication required). + Can only be used once - fails if an admin already exists. + + Args: + request: Admin creation request with display_name, email, password, and confirm_password + + Returns: + AdminCreateResponse with success message and user details + + Raises: + 409 Conflict: Admin already exists + 400 Bad Request: Validation errors + """ + try: + # Atomic check: verify no admin exists before proceeding + admin_exists = await check_admin_exists() + if admin_exists: + logger.warning("Attempted to create admin when one already exists") + raise HTTPException( + status_code=409, + detail="Admin user already exists. Setup has already been completed." + ) + + # Get user database and manager + user_db_gen = get_user_db() + user_db = await user_db_gen.__anext__() + user_manager_gen = get_user_manager(user_db) + user_manager = await user_manager_gen.__anext__() + + # Create admin user with UserManager (handles password hashing) + admin_create = UserCreate( + email=request.email, + password=request.password, + is_superuser=True, + is_verified=True, + display_name=request.display_name, + ) + + admin_user = await user_manager.create(admin_create) + + logger.info( + f"✅ Created admin user via web setup: {admin_user.user_id} ({admin_user.email})" + ) + + return AdminCreateResponse( + message="Admin user created successfully", + user_id=str(admin_user.id), + email=admin_user.email + ) + + except HTTPException: + # Re-raise HTTP exceptions (like 409 Conflict) + raise + except Exception as e: + logger.error(f"Failed to create admin user: {e}", exc_info=True) + raise HTTPException( + status_code=500, + detail="Failed to create admin user. Please try again." + ) diff --git a/backends/advanced/webui/src/App.tsx b/backends/advanced/webui/src/App.tsx index 56f31462..e28371c6 100644 --- a/backends/advanced/webui/src/App.tsx +++ b/backends/advanced/webui/src/App.tsx @@ -4,6 +4,7 @@ import { ThemeProvider } from './contexts/ThemeContext' import { RecordingProvider } from './contexts/RecordingContext' import Layout from './components/layout/Layout' import LoginPage from './pages/LoginPage' +import SetupPage from './pages/SetupPage' import Chat from './pages/Chat' import ConversationsRouter from './pages/ConversationsRouter' import MemoriesRouter from './pages/MemoriesRouter' @@ -31,6 +32,7 @@ function App() { + } /> } /> diff --git a/backends/advanced/webui/src/components/auth/ProtectedRoute.tsx b/backends/advanced/webui/src/components/auth/ProtectedRoute.tsx index c03eb0e3..ef2eacbf 100644 --- a/backends/advanced/webui/src/components/auth/ProtectedRoute.tsx +++ b/backends/advanced/webui/src/components/auth/ProtectedRoute.tsx @@ -8,7 +8,7 @@ interface ProtectedRouteProps { } export default function ProtectedRoute({ children, adminOnly = false }: ProtectedRouteProps) { - const { user, token, isLoading, isAdmin } = useAuth() + const { user, token, isLoading, isAdmin, setupRequired } = useAuth() if (isLoading) { return ( @@ -18,6 +18,11 @@ export default function ProtectedRoute({ children, adminOnly = false }: Protecte ) } + // Redirect to setup if required + if (setupRequired === true) { + return + } + if (!token || !user) { return } diff --git a/backends/advanced/webui/src/contexts/AuthContext.tsx b/backends/advanced/webui/src/contexts/AuthContext.tsx index 97a5b42c..9b355adb 100644 --- a/backends/advanced/webui/src/contexts/AuthContext.tsx +++ b/backends/advanced/webui/src/contexts/AuthContext.tsx @@ -1,5 +1,5 @@ import { createContext, useContext, useState, useEffect, ReactNode } from 'react' -import { authApi } from '../services/api' +import { authApi, setupApi } from '../services/api' import { getStorageKey } from '../utils/storage' interface User { @@ -18,6 +18,8 @@ interface AuthContextType { logout: () => void isLoading: boolean isAdmin: boolean + setupRequired: boolean | null // null = checking, true/false = determined + checkSetupStatus: () => Promise } const AuthContext = createContext(undefined) @@ -26,16 +28,36 @@ export function AuthProvider({ children }: { children: ReactNode }) { const [user, setUser] = useState(null) const [token, setToken] = useState(localStorage.getItem(getStorageKey('token'))) const [isLoading, setIsLoading] = useState(true) + const [setupRequired, setSetupRequired] = useState(null) // Check if user is admin const isAdmin = user?.is_superuser || false + // Function to check setup status + const checkSetupStatus = async (): Promise => { + try { + const response = await setupApi.getSetupStatus() + const required = response.data.requires_setup + setSetupRequired(required) + return required + } catch (error) { + console.error('❌ AuthContext: Failed to check setup status:', error) + setSetupRequired(false) // Assume setup not required on error to avoid blocking login + return false + } + } + useEffect(() => { const initAuth = async () => { console.log('🔐 AuthContext: Initializing authentication...') + + // First, check setup status + console.log('🔐 AuthContext: Checking setup status...') + await checkSetupStatus() + const savedToken = localStorage.getItem(getStorageKey('token')) console.log('🔐 AuthContext: Saved token exists:', !!savedToken) - + if (savedToken) { try { console.log('🔐 AuthContext: Verifying token with API call...') @@ -108,7 +130,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { } return ( - + {children} ) diff --git a/backends/advanced/webui/src/pages/LoginPage.tsx b/backends/advanced/webui/src/pages/LoginPage.tsx index 8924bd8c..a70c5399 100644 --- a/backends/advanced/webui/src/pages/LoginPage.tsx +++ b/backends/advanced/webui/src/pages/LoginPage.tsx @@ -11,7 +11,12 @@ export default function LoginPage() { const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState('') - const { user, login } = useAuth() + const { user, login, setupRequired } = useAuth() + + // Redirect to setup if required + if (setupRequired === true) { + return + } // Redirect if already logged in if (user) { diff --git a/backends/advanced/webui/src/pages/SetupPage.tsx b/backends/advanced/webui/src/pages/SetupPage.tsx new file mode 100644 index 00000000..76dbf7fb --- /dev/null +++ b/backends/advanced/webui/src/pages/SetupPage.tsx @@ -0,0 +1,321 @@ +import React, { useState, useEffect } from 'react' +import { Navigate, useNavigate } from 'react-router-dom' +import { useAuth } from '../contexts/AuthContext' +import { setupApi } from '../services/api' +import { Brain, Eye, EyeOff } from 'lucide-react' + +export default function SetupPage() { + const [displayName, setDisplayName] = useState('') + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [confirmPassword, setConfirmPassword] = useState('') + const [showPassword, setShowPassword] = useState(false) + const [showConfirmPassword, setShowConfirmPassword] = useState(false) + const [isLoading, setIsLoading] = useState(false) + const [isCheckingStatus, setIsCheckingStatus] = useState(true) + const [error, setError] = useState('') + const [fieldErrors, setFieldErrors] = useState>({}) + + const { user, login, checkSetupStatus } = useAuth() + const navigate = useNavigate() + + // Check setup status on mount + useEffect(() => { + const checkInitialSetupStatus = async () => { + try { + const response = await setupApi.getSetupStatus() + if (!response.data.requires_setup) { + // Setup already completed, redirect to login + navigate('/login', { replace: true }) + } + } catch (err) { + setError('Unable to check setup status. Please refresh the page.') + } finally { + setIsCheckingStatus(false) + } + } + + checkInitialSetupStatus() + }, [navigate]) + + // Redirect if already logged in + if (user) { + return + } + + // Show loading while checking setup status + if (isCheckingStatus) { + return ( +
+
+
+ Checking setup status... +
+
+ ) + } + + const validateForm = (): boolean => { + const errors: Record = {} + + if (!displayName.trim()) { + errors.displayName = 'Name is required' + } + + if (!email.trim()) { + errors.email = 'Email is required' + } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { + errors.email = 'Invalid email format' + } + + if (!password) { + errors.password = 'Password is required' + } else if (password.length < 8) { + errors.password = 'Password must be at least 8 characters' + } + + if (!confirmPassword) { + errors.confirmPassword = 'Please confirm your password' + } else if (password !== confirmPassword) { + errors.confirmPassword = 'Passwords do not match' + } + + setFieldErrors(errors) + return Object.keys(errors).length === 0 + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setIsLoading(true) + setError('') + setFieldErrors({}) + + // Client-side validation + if (!validateForm()) { + setIsLoading(false) + return + } + + try { + // Create admin user + await setupApi.createAdmin({ + display_name: displayName, + email, + password, + confirm_password: confirmPassword, + }) + + // Auto-login with the credentials + const loginResult = await login(email, password) + if (loginResult.success) { + // Refresh setup status (setup is now complete) + await checkSetupStatus() + + // Stop loading before navigation + setIsLoading(false) + + // Navigate to dashboard + navigate('/', { replace: true }) + } else { + setIsLoading(false) + setError('Admin created successfully, but auto-login failed. Please login manually.') + setTimeout(() => navigate('/login', { replace: true }), 2000) + } + } catch (err: any) { + setIsLoading(false) + + // Handle different error responses + if (err.response?.status === 409) { + setError('Setup has already been completed by another user. Redirecting to login...') + setTimeout(() => navigate('/login', { replace: true }), 2000) + } else if (err.response?.status === 400) { + const detail = err.response?.data?.detail + if (typeof detail === 'string') { + setError(detail) + } else { + setError('Validation error. Please check your inputs.') + } + } else if (err.message?.includes('Network') || err.code === 'ERR_NETWORK') { + setError('Unable to connect to server. Please check your connection and try again.') + } else { + setError('Setup failed. Please try again.') + } + } + } + + return ( +
+ {/* Decorative background elements */} +
+
+
+
+ +
+ {/* Logo & Header */} +
+
+ +
+

+ Welcome to Chronicle +

+

+ First-Time Setup +

+

+ Create your administrator account to get started +

+
+ + {/* Setup Form */} +
+
+ {/* Display Name Input */} +
+ + setDisplayName(e.target.value)} + className={`input ${fieldErrors.displayName ? 'border-error-500 dark:border-error-500' : ''}`} + placeholder="Administrator" + /> + {fieldErrors.displayName && ( +

{fieldErrors.displayName}

+ )} +
+ + {/* Email Input */} +
+ + setEmail(e.target.value)} + className={`input ${fieldErrors.email ? 'border-error-500 dark:border-error-500' : ''}`} + placeholder="admin@example.com" + /> + {fieldErrors.email && ( +

{fieldErrors.email}

+ )} +
+ + {/* Password Input */} +
+ +
+ setPassword(e.target.value)} + className={`input pr-10 ${fieldErrors.password ? 'border-error-500 dark:border-error-500' : ''}`} + placeholder="Minimum 8 characters" + /> + +
+ {fieldErrors.password && ( +

{fieldErrors.password}

+ )} +
+ + {/* Confirm Password Input */} +
+ +
+ setConfirmPassword(e.target.value)} + className={`input pr-10 ${fieldErrors.confirmPassword ? 'border-error-500 dark:border-error-500' : ''}`} + placeholder="Re-enter your password" + /> + +
+ {fieldErrors.confirmPassword && ( +

{fieldErrors.confirmPassword}

+ )} +
+ + {/* Error Message */} + {error && ( +
+

+ {error} +

+
+ )} + + {/* Submit Button */} + +
+
+ + {/* Footer */} +
+

+ Chronicle Dashboard v1.0 +

+
+
+
+ ) +} diff --git a/backends/advanced/webui/src/services/api.ts b/backends/advanced/webui/src/services/api.ts index 8d777e3d..9310f0fa 100644 --- a/backends/advanced/webui/src/services/api.ts +++ b/backends/advanced/webui/src/services/api.ts @@ -106,6 +106,16 @@ export const authApi = { getMe: () => api.get('/users/me'), } +export const setupApi = { + getSetupStatus: () => api.get('/api/setup/status'), + createAdmin: (setupData: { + display_name: string + email: string + password: string + confirm_password: string + }) => api.post('/api/setup/create-admin', setupData), +} + export const conversationsApi = { getAll: () => api.get('/api/conversations'), getById: (id: string) => api.get(`/api/conversations/${id}`), diff --git a/clear.sh b/clear.sh new file mode 100755 index 00000000..13741680 --- /dev/null +++ b/clear.sh @@ -0,0 +1,130 @@ +#!/bin/bash +set -e + +# Chronicle Admin Reset Script +# Removes admin users from database and clears auth variables for fresh setup + +echo "🧹 Chronicle Admin Reset" +echo "========================================" + +# Check we're in the right directory +if [ ! -f "docker-compose.yml" ] || [ ! -f "docker-compose.infra.yml" ]; then + echo "❌ Error: Must be run from the GOLD directory" + echo " cd to the directory containing docker-compose.yml" + exit 1 +fi +echo "" +echo "⚠️ WARNING: This will:" +echo " - Remove ALL admin users from the database" +echo " - Clear AUTH_SECRET_KEY from .env" +echo " - Clear ADMIN_PASSWORD from .env" +echo " - Allow you to run ./go.sh for a fresh setup" +echo "" +read -p "Are you sure? (yes/no): " -r +echo "" + +if [[ ! $REPLY =~ ^[Yy][Ee][Ss]$ ]]; then + echo "❌ Aborted" + exit 0 +fi + +# Get database name - check where backend actually loads it from +# Priority: backends/advanced/.env > root .env > .env.default > hardcoded default +if [ -f backends/advanced/.env ]; then + MONGODB_DATABASE=$(grep "^MONGODB_DATABASE=" backends/advanced/.env | cut -d'=' -f2) +fi + +if [ -z "$MONGODB_DATABASE" ] && [ -f .env ]; then + MONGODB_DATABASE=$(grep "^MONGODB_DATABASE=" .env | cut -d'=' -f2) +fi + +if [ -z "$MONGODB_DATABASE" ] && [ -f .env.default ]; then + MONGODB_DATABASE=$(grep "^MONGODB_DATABASE=" .env.default | cut -d'=' -f2) +fi + +# Final fallback to backend's hardcoded default +if [ -z "$MONGODB_DATABASE" ]; then + MONGODB_DATABASE="friend-lite" +fi + +echo "📦 Database: ${MONGODB_DATABASE}" +echo "" + +# Check if MongoDB is running +echo "🔍 Checking MongoDB connection..." +if ! docker ps | grep -q "mongo"; then + echo "⚠️ MongoDB container is not running" + echo " Starting MongoDB..." + docker compose -f docker-compose.infra.yml up -d mongo + echo " Waiting for MongoDB to be ready..." + sleep 5 +fi + +# Remove admin users from MongoDB +echo "🗑️ Removing admin users from database..." +docker exec -i mongo mongosh "${MONGODB_DATABASE}" --quiet --eval ' +const beforeCount = db.users.countDocuments({ is_superuser: true }); +const result = db.users.deleteMany({ is_superuser: true }); +const afterCount = db.users.countDocuments({ is_superuser: true }); +print("✅ Removed " + result.deletedCount + " admin user(s). Remaining admins: " + afterCount); +' || echo "⚠️ MongoDB operation may have failed - check if container is running" + +echo "" +echo "🔐 Clearing auth variables from .env files..." + +# Function to clear auth variables from a file +clear_auth_vars() { + local file=$1 + local cleared=false + + if [ -f "$file" ]; then + # Clear AUTH_SECRET_KEY + if grep -q "^AUTH_SECRET_KEY=" "$file"; then + if [[ "$OSTYPE" == "darwin"* ]]; then + sed -i '' "s|^AUTH_SECRET_KEY=.*|AUTH_SECRET_KEY=|" "$file" + else + sed -i "s|^AUTH_SECRET_KEY=.*|AUTH_SECRET_KEY=|" "$file" + fi + echo " ✅ AUTH_SECRET_KEY cleared from $file" + cleared=true + fi + + # Clear ADMIN_PASSWORD + if grep -q "^ADMIN_PASSWORD=" "$file"; then + if [[ "$OSTYPE" == "darwin"* ]]; then + sed -i '' "s|^ADMIN_PASSWORD=.*|ADMIN_PASSWORD=|" "$file" + else + sed -i "s|^ADMIN_PASSWORD=.*|ADMIN_PASSWORD=|" "$file" + fi + echo " ✅ ADMIN_PASSWORD cleared from $file" + cleared=true + fi + fi +} + +# Clear from root .env +clear_auth_vars ".env" + +# Clear from backends/advanced/.env (this is what the backend actually uses!) +clear_auth_vars "backends/advanced/.env" + +if [ ! -f .env ] && [ ! -f backends/advanced/.env ]; then + echo " ⚠️ No .env files found (will be created by go.sh)" +fi + +echo "" +echo "🔄 Restarting backend to invalidate active sessions..." +docker compose restart backend 2>/dev/null || echo " ⚠️ Backend not running (that's ok)" + +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "✅ Admin reset complete!" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "" +echo "🚀 Next steps:" +echo " 1. Clear your browser cache/localStorage (Cmd+Shift+R or hard refresh)" +echo " 2. Visit the web UI - you'll be redirected to /setup" +echo " 3. Create a new admin account" +echo "" +echo "💡 Or run ./go.sh to restart everything fresh" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" diff --git a/go.sh b/go.sh new file mode 100755 index 00000000..27fdb218 --- /dev/null +++ b/go.sh @@ -0,0 +1,154 @@ +#!/bin/bash +set -e + +# Chronicle Quick Start - Web UI Setup Flow +# This script starts Chronicle and opens the web UI setup screen to create an admin account + +echo "🚀 Chronicle Quick Start - Web UI Setup" +echo "========================================" + +# Check we're in the right directory +if [ ! -f "docker-compose.yml" ] || [ ! -f "docker-compose.infra.yml" ]; then + echo "❌ Error: Must be run from the GOLD directory" + echo " cd to the directory containing docker-compose.yml" + exit 1 +fi + +# Check if .env exists, if not create from defaults +if [ ! -f .env ]; then + echo "📝 Creating .env from .env.default..." + cp .env.default .env +fi + +# Generate AUTH_SECRET_KEY if not set (check all .env files) +SECRET_KEY="" + +# Check if any .env file has a valid AUTH_SECRET_KEY +for env_file in backends/advanced/.env .env; do + if [ -f "$env_file" ] && grep -q "^AUTH_SECRET_KEY=.\+" "$env_file" 2>/dev/null; then + SECRET_KEY=$(grep "^AUTH_SECRET_KEY=" "$env_file" | cut -d'=' -f2) + echo "✅ AUTH_SECRET_KEY already set in $env_file" + break + fi +done + +# Generate if not found +if [ -z "$SECRET_KEY" ]; then + echo "🔐 Generating secure AUTH_SECRET_KEY..." + SECRET_KEY=$(openssl rand -base64 32) + echo "✅ AUTH_SECRET_KEY generated" +fi + +# Ensure it's set in backends/advanced/.env (the one backend actually uses) +if [ -f backends/advanced/.env ]; then + if grep -q "^AUTH_SECRET_KEY=" backends/advanced/.env; then + if [[ "$OSTYPE" == "darwin"* ]]; then + sed -i '' "s|^AUTH_SECRET_KEY=.*|AUTH_SECRET_KEY=${SECRET_KEY}|" backends/advanced/.env + else + sed -i "s|^AUTH_SECRET_KEY=.*|AUTH_SECRET_KEY=${SECRET_KEY}|" backends/advanced/.env + fi + else + echo "AUTH_SECRET_KEY=${SECRET_KEY}" >> backends/advanced/.env + fi +fi + +# Also set in root .env for consistency +if [ -f .env ]; then + if grep -q "^AUTH_SECRET_KEY=" .env; then + if [[ "$OSTYPE" == "darwin"* ]]; then + sed -i '' "s|^AUTH_SECRET_KEY=.*|AUTH_SECRET_KEY=${SECRET_KEY}|" .env + else + sed -i "s|^AUTH_SECRET_KEY=.*|AUTH_SECRET_KEY=${SECRET_KEY}|" .env + fi + else + echo "AUTH_SECRET_KEY=${SECRET_KEY}" >> .env + fi +fi + +# Ensure ADMIN_PASSWORD is empty in all .env files (to trigger web UI setup) +for env_file in .env backends/advanced/.env; do + if [ -f "$env_file" ] && grep -q "^ADMIN_PASSWORD=.\+" "$env_file" 2>/dev/null; then + echo "⚠️ ADMIN_PASSWORD is set in $env_file - clearing it to enable web UI setup..." + if [[ "$OSTYPE" == "darwin"* ]]; then + sed -i '' "s|^ADMIN_PASSWORD=.*|ADMIN_PASSWORD=|" "$env_file" + else + sed -i "s|^ADMIN_PASSWORD=.*|ADMIN_PASSWORD=|" "$env_file" + fi + fi +done + +echo "" +echo "🐳 Starting Docker services..." + +# Check if infrastructure is already running +if docker ps --filter "name=^mongo$" --filter "status=running" -q | grep -q .; then + echo " ✅ Infrastructure already running (reusing existing)" +else + echo " Starting infrastructure (MongoDB, Redis, Qdrant)..." + docker compose -f docker-compose.infra.yml up -d + echo " Waiting for infrastructure to be ready..." + sleep 3 +fi + +echo " Starting application services..." +# Clean up any orphaned containers from previous runs +docker compose down 2>/dev/null || true +docker compose up -d --build + +echo "" +echo "⏳ Waiting for backend to be ready..." +MAX_WAIT=60 +WAITED=0 +BACKEND_PORT=$(grep "^BACKEND_PORT=" .env | cut -d'=' -f2 || echo "8000") + +while [ $WAITED -lt $MAX_WAIT ]; do + if curl -s "http://localhost:${BACKEND_PORT}/health" > /dev/null 2>&1; then + echo "✅ Backend is ready!" + break + fi + sleep 2 + WAITED=$((WAITED + 2)) + echo " Waiting... (${WAITED}s/${MAX_WAIT}s)" +done + +if [ $WAITED -ge $MAX_WAIT ]; then + echo "❌ Backend failed to start within ${MAX_WAIT} seconds" + echo " Check logs with: docker compose logs backend" + exit 1 +fi + +echo "" +echo "✅ Chronicle is running!" +echo "" +echo "📱 Opening web UI setup screen..." +echo " You'll be prompted to create your admin account" +echo "" + +# Get webui port +WEBUI_PORT=$(grep "^WEBUI_PORT=" .env | cut -d'=' -f2 || echo "3000") + +# Open browser to setup page +if command -v open > /dev/null; then + # macOS + open "http://localhost:${WEBUI_PORT}/setup" +elif command -v xdg-open > /dev/null; then + # Linux + xdg-open "http://localhost:${WEBUI_PORT}/setup" +elif command -v start > /dev/null; then + # Windows + start "http://localhost:${WEBUI_PORT}/setup" +else + echo " Please open your browser to: http://localhost:${WEBUI_PORT}/setup" +fi + +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "📋 Quick Reference:" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo " Web UI Setup: http://localhost:${WEBUI_PORT}/setup" +echo " Web Dashboard: http://localhost:${WEBUI_PORT}" +echo " Backend API: http://localhost:${BACKEND_PORT}" +echo "" +echo " View logs: docker compose logs -f" +echo " Stop services: docker compose down" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"