diff --git a/backend/app/alembic/env.py b/backend/app/alembic/env.py index 5db5c51..ccfa7cd 100644 --- a/backend/app/alembic/env.py +++ b/backend/app/alembic/env.py @@ -2,8 +2,9 @@ import sys from logging.config import fileConfig from pathlib import Path -from sqlalchemy import engine_from_config -from sqlalchemy import pool +from sqlalchemy import engine_from_config, pool, text, create_engine +from sqlalchemy.engine.url import make_url +from sqlalchemy.exc import OperationalError from alembic import context @@ -35,6 +36,51 @@ target_metadata = Base.metadata config.set_main_option("sqlalchemy.url", settings.database_url) +def ensure_database_exists(db_url: str) -> None: + """ + Ensure the target PostgreSQL database exists. + If connection to the target DB fails because it doesn't exist, connect to the + default 'postgres' database and create it. Safe to call multiple times. + """ + try: + # First, try connecting to the target database + test_engine = create_engine(db_url, poolclass=pool.NullPool) + with test_engine.connect() as conn: + conn.execute(text("SELECT 1")) + test_engine.dispose() + return + except OperationalError: + # Likely the database does not exist; proceed to create it + pass + + url = make_url(db_url) + # Only handle PostgreSQL here + if url.get_backend_name() != "postgresql": + return + + target_db = url.database + if not target_db: + return + + # Build admin URL pointing to the default 'postgres' database + admin_url = url.set(database="postgres") + + # CREATE DATABASE cannot run inside a transaction + admin_engine = create_engine(str(admin_url), isolation_level="AUTOCOMMIT", poolclass=pool.NullPool) + try: + with admin_engine.connect() as conn: + exists = conn.execute( + text("SELECT 1 FROM pg_database WHERE datname = :dbname"), + {"dbname": target_db}, + ).scalar() + if not exists: + # Quote the database name safely + dbname_quoted = '"' + target_db.replace('"', '""') + '"' + conn.execute(text(f"CREATE DATABASE {dbname_quoted}")) + finally: + admin_engine.dispose() + + def run_migrations_offline() -> None: """Run migrations in 'offline' mode. @@ -66,6 +112,9 @@ def run_migrations_online() -> None: and associate a connection with the context. """ + # Ensure the target database exists (handles first-run cases) + ensure_database_exists(settings.database_url) + connectable = engine_from_config( config.get_section(config.config_ini_section, {}), prefix="sqlalchemy.", diff --git a/frontend/src/components/auth/AuthGuard.tsx b/frontend/src/components/auth/AuthGuard.tsx index 43b4878..1e9bc9f 100644 --- a/frontend/src/components/auth/AuthGuard.tsx +++ b/frontend/src/components/auth/AuthGuard.tsx @@ -8,7 +8,7 @@ import { useEffect, useState } from 'react'; import { useRouter, usePathname } from 'next/navigation'; -import { useAuthStore } from '@/lib/stores/authStore'; +import { useAuth } from '@/lib/stores'; import { useMe } from '@/lib/api/hooks/useAuth'; import { AuthLoadingSkeleton } from '@/components/layout'; import config from '@/config/app.config'; @@ -50,7 +50,7 @@ interface AuthGuardProps { export function AuthGuard({ children, requireAdmin = false, fallback }: AuthGuardProps) { const router = useRouter(); const pathname = usePathname(); - const { isAuthenticated, isLoading: authLoading, user } = useAuthStore(); + const { isAuthenticated, isLoading: authLoading, user } = useAuth(); // Delayed loading state - only show skeleton after 150ms to avoid flicker on fast loads const [showLoading, setShowLoading] = useState(false); diff --git a/frontend/src/components/layout/Header.tsx b/frontend/src/components/layout/Header.tsx index 966be53..bf1a773 100644 --- a/frontend/src/components/layout/Header.tsx +++ b/frontend/src/components/layout/Header.tsx @@ -8,7 +8,7 @@ import Link from 'next/link'; import { usePathname } from 'next/navigation'; -import { useAuthStore } from '@/lib/stores/authStore'; +import { useAuth } from '@/lib/stores'; import { useLogout } from '@/lib/api/hooks/useAuth'; import { DropdownMenu, @@ -67,7 +67,7 @@ function NavLink({ } export function Header() { - const { user } = useAuthStore(); + const { user } = useAuth(); const { mutate: logout, isPending: isLoggingOut } = useLogout(); const handleLogout = () => {