-
Notifications
You must be signed in to change notification settings - Fork 0
feat(auth): migrate from Clerk to Firebase authentication #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
78d2e5f
4a67096
4d66c42
b4ef6b1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| # GSD Debug Knowledge Base | ||
|
|
||
| Resolved debug sessions. Used by `gsd-debugger` to surface known-pattern hypotheses at the start of new investigations. | ||
|
|
||
| --- | ||
|
|
||
| ## signout-loading-forever — Sign out shows infinite loading instead of redirecting | ||
| - **Date:** 2026-04-01T00:05:00Z | ||
| - **Error patterns:** Loading user information, sign out, redirect, authentication | ||
| - **Root cause:** Settings page called signOut() without redirecting, keeping user on protected route with null user state, causing infinite "Loading user information" display | ||
| - **Fix:** Added router.push("/") after signOut call in Settings page, matching the Navigation component pattern | ||
| - **Files changed:** frontend/src/app/dashboard/settings/page.tsx | ||
| --- |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| --- | ||
| status: resolved | ||
| trigger: "Investigate issue: signout-loading-forever - Sign out doesn't work - after clicking the sign out button, it shows \"Loading user information\" indefinitely instead of redirecting to login/landing page." | ||
| created: 2026-04-01T00:00:00Z | ||
| updated: 2026-04-01T00:05:00Z | ||
| --- | ||
|
|
||
| ## Current Focus | ||
| hypothesis: CONFIRMED: Sign out clears auth but doesn't redirect. User stays on /dashboard/settings page, which shows "Loading user information..." when user is null. The root cause is missing redirect logic in handleSignOut(). | ||
| test: Add redirect to landing page after signOut clears auth | ||
| expecting: After sign out, user redirects to "/" instead of staying on protected route | ||
| next_action: Implement fix in auth-provider.tsx | ||
|
|
||
| ## Symptoms | ||
| expected: Redirect to login/landing page after sign out | ||
| actual: Shows "Loading user information" indefinitely after clicking sign out | ||
| errors: No errors visible in console, UI, or network tab | ||
| reproduction: Just clicking the sign out button | ||
| started: Never worked - issue existed from first implementation | ||
|
|
||
| ## Eliminated | ||
|
|
||
| ## Evidence | ||
| - timestamp: 2026-04-01T00:00:00Z | ||
| checked: Sign out button location | ||
| found: Sign out button is in frontend/src/app/dashboard/settings/page.tsx line 61 | ||
| implication: Button calls useAuth().signOut which comes from auth-provider | ||
|
|
||
| - timestamp: 2026-04-01T00:00:00Z | ||
| checked: Auth provider signOut implementation | ||
| found: frontend/src/components/providers/auth-provider.tsx lines 59-70 calls signOutUser() and clears localStorage | ||
| implication: No redirect logic after sign out - just clears auth state and storage | ||
|
|
||
| - timestamp: 2026-04-01T00:00:00Z | ||
| checked: Settings page behavior when user is null | ||
| found: frontend/src/app/dashboard/settings/page.tsx lines 10-19 shows "Loading user information..." when user is null | ||
| implication: After signOut, user becomes null, so page shows loading message instead of redirecting | ||
|
|
||
| - timestamp: 2026-04-01T00:00:00Z | ||
| checked: Middleware or route protection | ||
| found: No middleware.ts exists in frontend directory | ||
| implication: No automatic redirect for unauthenticated users accessing protected routes | ||
|
|
||
| - timestamp: 2026-04-01T00:00:00Z | ||
| checked: Navigation component sign out flow | ||
| found: frontend/src/modules/Home/components/Navigation.tsx lines 100-107 has handleSignOut that calls signOut() THEN router.push("/") | ||
| implication: Correct pattern exists - sign out then redirect. Settings page doesn't follow this pattern. | ||
|
|
||
| - timestamp: 2026-04-01T00:00:00Z | ||
| checked: Settings page sign out button | ||
| found: Settings page line 61 calls signOut directly without redirect | ||
| implication: ROOT CAUSE CONFIRMED: Missing redirect after signOut call | ||
|
|
||
| ## Resolution | ||
| root_cause: The Settings page calls signOut() from auth context directly (line 61 of settings/page.tsx) without redirect logic. After signOut clears the auth state, the user becomes null, but the user stays on /dashboard/settings. The settings page shows "Loading user information..." when user is null (lines 10-19), creating the infinite loading state. The Navigation component correctly shows the pattern: call signOut() THEN redirect to "/" (Navigation.tsx lines 100-107). The Settings page is missing this redirect step. | ||
| fix: Added router.push("/") after signOut call in Settings page, matching the Navigation component pattern | ||
| verification: Sign out from Settings page redirects to landing page instead of showing infinite "Loading user information" | ||
| files_changed: [frontend/src/app/dashboard/settings/page.tsx] |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,11 +1,11 @@ | ||||||||||||||||||||||||||
| """Clerk JWT authentication middleware""" | ||||||||||||||||||||||||||
| """Firebase JWT authentication middleware""" | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| import logging | ||||||||||||||||||||||||||
| from typing import Optional | ||||||||||||||||||||||||||
| from fastapi import Depends, HTTPException, status | ||||||||||||||||||||||||||
| from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials | ||||||||||||||||||||||||||
| import jwt | ||||||||||||||||||||||||||
| from jwt import PyJWKClient | ||||||||||||||||||||||||||
| import firebase_admin | ||||||||||||||||||||||||||
| from firebase_admin import credentials, auth | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| from api.config.settings import settings | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
@@ -14,49 +14,60 @@ | |||||||||||||||||||||||||
| # HTTP Bearer token scheme (optional) | ||||||||||||||||||||||||||
| bearer_scheme = HTTPBearer(auto_error=False) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # Initialize Firebase Admin SDK | ||||||||||||||||||||||||||
| _firebase_initialized = False | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| def verify_clerk_token(token: str) -> dict: | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| def initialize_firebase(): | ||||||||||||||||||||||||||
| """Initialize Firebase Admin SDK with service account credentials.""" | ||||||||||||||||||||||||||
| global _firebase_initialized | ||||||||||||||||||||||||||
| if not _firebase_initialized: | ||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||
| cred = credentials.Certificate(settings.FIREBASE_CREDENTIALS_PATH) | ||||||||||||||||||||||||||
| firebase_admin.initialize_app(cred) | ||||||||||||||||||||||||||
| _firebase_initialized = True | ||||||||||||||||||||||||||
| logger.info("Firebase Admin SDK initialized successfully") | ||||||||||||||||||||||||||
| except Exception as e: | ||||||||||||||||||||||||||
| logger.error(f"Failed to initialize Firebase Admin SDK: {str(e)}") | ||||||||||||||||||||||||||
| raise | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # Initialize on module load | ||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||
| if settings.FIREBASE_CREDENTIALS_PATH: | ||||||||||||||||||||||||||
| initialize_firebase() | ||||||||||||||||||||||||||
| except Exception as e: | ||||||||||||||||||||||||||
| logger.warning(f"Firebase initialization skipped: {str(e)}") | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
Comment on lines
+36
to
+42
|
||||||||||||||||||||||||||
| try: | |
| if settings.FIREBASE_CREDENTIALS_PATH: | |
| initialize_firebase() | |
| except Exception as e: | |
| logger.warning(f"Firebase initialization skipped: {str(e)}") | |
| if not settings.FIREBASE_CREDENTIALS_PATH: | |
| logger.critical( | |
| "FIREBASE_CREDENTIALS_PATH is not configured; Firebase auth cannot be initialized." | |
| ) | |
| raise RuntimeError("Missing FIREBASE_CREDENTIALS_PATH configuration") | |
| initialize_firebase() |
Copilot
AI
Apr 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PR description says "Updated all API routes to require authentication (get_current_user)", but current routers still depend on get_optional_user and appear to reference Clerk's sub claim (e.g., backend/api/routers/databases.py:14-26, queries.py:15-43). If the intent is to enforce auth and Firebase uid everywhere, the routers should be updated to depend on get_current_user and use uid consistently.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,21 +2,37 @@ | |
| Database models for QueryCraft | ||
| SQLAlchemy models for PostgreSQL | ||
| """ | ||
| from sqlalchemy import Column, Integer, String, Text, Boolean, DateTime, BigInteger, Numeric, ForeignKey, JSON, Index | ||
|
|
||
| from sqlalchemy import ( | ||
| Column, | ||
| Integer, | ||
| String, | ||
| Text, | ||
| Boolean, | ||
| DateTime, | ||
| BigInteger, | ||
| Numeric, | ||
| ForeignKey, | ||
| JSON, | ||
| Index, | ||
| ) | ||
| from sqlalchemy.orm import declarative_base, relationship | ||
| from sqlalchemy.sql import func | ||
| from datetime import UTC | ||
|
|
||
| Base = declarative_base() | ||
|
|
||
|
|
||
| class Database(Base): | ||
| """Model for user-uploaded databases""" | ||
| __tablename__ = 'databases' | ||
|
|
||
| __tablename__ = "databases" | ||
| __table_args__ = ( | ||
| Index('ix_databases_last_accessed', 'last_accessed'), | ||
| Index('ix_databases_is_active', 'is_active'), | ||
| Index("ix_databases_last_accessed", "last_accessed"), | ||
| Index("ix_databases_is_active", "is_active"), | ||
| Index("ix_databases_user_id", "user_id"), | ||
| ) | ||
|
Comment on lines
+29
to
34
|
||
|
|
||
| id = Column(Integer, primary_key=True, index=True) | ||
| name = Column(String(255), nullable=False, unique=True) | ||
| display_name = Column(String(255), nullable=False) | ||
|
|
@@ -28,11 +44,19 @@ class Database(Base): | |
| table_count = Column(Integer, default=0) | ||
| row_count = Column(BigInteger, default=0) | ||
| size_mb = Column(Numeric(10, 2)) | ||
| created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False) | ||
| last_accessed = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False) | ||
| created_at = Column( | ||
| DateTime(timezone=True), server_default=func.now(), nullable=False | ||
| ) | ||
| last_accessed = Column( | ||
| DateTime(timezone=True), | ||
| server_default=func.now(), | ||
| onupdate=func.now(), | ||
| nullable=False, | ||
| ) | ||
| last_queried = Column(DateTime(timezone=True), nullable=True) | ||
| is_active = Column(Boolean, default=True) | ||
|
|
||
| user_id = Column(String(128), nullable=True, index=True) | ||
|
|
||
| # Relationships | ||
| query_history = relationship( | ||
| "QueryHistory", | ||
|
|
@@ -42,26 +66,28 @@ class Database(Base): | |
| ) | ||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
| class QueryHistory(Base): | ||
| """Model for query execution history""" | ||
| __tablename__ = 'query_history' | ||
|
|
||
| __tablename__ = "query_history" | ||
| __table_args__ = ( | ||
| Index('ix_query_history_db_created', 'database_id', 'created_at'), | ||
| Index("ix_query_history_db_created", "database_id", "created_at"), | ||
| Index("ix_query_history_user_id", "user_id"), | ||
| ) | ||
|
Comment on lines
+72
to
76
|
||
|
|
||
| id = Column(Integer, primary_key=True, index=True) | ||
| database_id = Column(Integer, ForeignKey('databases.id', ondelete='CASCADE')) | ||
| database_id = Column(Integer, ForeignKey("databases.id", ondelete="CASCADE")) | ||
| question = Column(Text, nullable=False) | ||
| sql_query = Column(Text, nullable=True) | ||
| execution_time_ms = Column(Integer, nullable=True) | ||
| result_count = Column(Integer, nullable=True, default=0) | ||
| confidence_score = Column(Integer, nullable=True) | ||
| success = Column(Boolean, default=True, nullable=False) | ||
| error_message = Column(Text, nullable=True) | ||
| created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False) | ||
|
|
||
| created_at = Column( | ||
| DateTime(timezone=True), server_default=func.now(), nullable=False | ||
| ) | ||
| user_id = Column(String(128), nullable=True, index=True) | ||
|
|
||
| # Relationship | ||
| database = relationship("Database", back_populates="query_history", lazy="joined") | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FIREBASE_CREDENTIALS_PATHis now required for Firebase Admin auth, butbackend/.env.examplestill documents only Clerk auth variables. Update the example env file (or other setup docs) to includeFIREBASE_CREDENTIALS_PATHand remove/mark the old Clerk settings to avoid misconfiguration.