diff --git a/README.md b/README.md index f4b12b7..8a01c83 100644 --- a/README.md +++ b/README.md @@ -50,10 +50,25 @@ AkashChat leverages the decentralized cloud infrastructure of Akash Network, off ## Getting Started +### Minimal Setup + +AkashChat can run with just an API key! The minimal configuration requires: + +- Node.js (v18 or higher) +- `API_KEY` environment variable + +Redis, Auth0, Database, and other features are **optional** and only needed for: +- **Redis**: Session management and caching (without it, uses simple cookie-based sessions) +- **Auth0 + Database**: Multi-user accounts with persistent chat history +- **Access Token**: Frontend access control for private instances + ### Prerequisites +For the full-featured setup: + - Node.js (v18 or higher) -- Redis server (for caching and session management) +- Redis server (optional, for session management and caching) +- PostgreSQL database (optional, for multi-user support with Auth0) - API keys/endpoints for the AI models you want to use ### Installation @@ -69,9 +84,17 @@ AkashChat leverages the decentralized cloud infrastructure of Akash Network, off npm install ``` -3. Create a `.env.local` file in the root directory with the following variables: +3. Create a `.env.local` file in the root directory. + + **Minimal configuration** (only required variables): + ```env + # API Configuration - REQUIRED + API_KEY=your_api_key_here + ``` + + **Full configuration** (all available options): ```env - # API Configuration + # API Configuration - REQUIRED API_KEY=your_api_key_here API_ENDPOINT=your_api_endpoint_here DEFAULT_MODEL=your_default_model_here @@ -118,23 +141,23 @@ AkashChat leverages the decentralized cloud infrastructure of Akash Network, off | Variable | Description | Required | Default | |----------|-------------|----------|---------| -| API_KEY | Authentication key for Akash AI API access | Yes | - | -| API_ENDPOINT | Base URL for Akash AI API | Yes | https://chatapi.akash.network/api/v1 | +| **API_KEY** | **Authentication key for Akash AI API access** | **Yes** | **-** | +| API_ENDPOINT | Base URL for Akash AI API | No | https://api.akashml.com/v1 | | DEFAULT_MODEL | Default AI model to use | No | Qwen-QwQ-32B | -| REDIS_URL | Connection URL for Redis | Yes | redis://localhost:6379 | -| CACHE_TTL | Cache time-to-live in seconds | No | 600 (10 minutes) | -| ACCESS_TOKEN | Token for API and frontend protection | No | - | +| REDIS_URL | Connection URL for Redis (for session management) | No | - | +| CACHE_TTL | Cache/session time-to-live in seconds | No | 600 (10 minutes) | +| ACCESS_TOKEN | Token for API and frontend protection (SHA-256 hashed automatically) | No | - | | WS_TRANSCRIPTION_URLS | Comma-separated WebSocket URLs for voice transcription | No | - | | WS_TRANSCRIPTION_MODEL | Model for voice transcription | No | mobiuslabsgmbh/faster-whisper-large-v3-turbo | | IMG_API_KEY | Authentication key for AkashGen image generation | No | - | | IMG_ENDPOINT | Endpoint for AkashGen image generation | No | - | | IMG_GEN_FN_MODEL | Model for AkashGen image generation | No | - | -| AUTH0_SECRET | Auth0 session secret for user authentication | No | - | +| AUTH0_SECRET | Auth0 session secret (required for multi-user mode) | No | - | | AUTH0_BASE_URL | Base URL for Auth0 configuration | No | - | | AUTH0_ISSUER_BASE_URL | Auth0 issuer base URL | No | - | | AUTH0_CLIENT_ID | Auth0 application client ID | No | - | | AUTH0_CLIENT_SECRET | Auth0 application client secret | No | - | -| DATABASE_URL | PostgreSQL connection URL for data persistence | Yes | - | +| DATABASE_URL | PostgreSQL connection URL (required for multi-user mode) | No | - | | LITELLM_BASE_URL | LiteLLM proxy base URL | No | - | | LITELLM_API_KEY | LiteLLM proxy API key | No | - | diff --git a/app/api/auth/[...auth0]/route.ts b/app/api/auth/[...auth0]/route.ts index dff6913..e67e8ce 100644 --- a/app/api/auth/[...auth0]/route.ts +++ b/app/api/auth/[...auth0]/route.ts @@ -1,3 +1,4 @@ +import { isAuth0Configured, isDevBypassEnabled } from '@/lib/auth'; import { handleAuth } from '@auth0/nextjs-auth0'; import { NextRequest, NextResponse } from 'next/server'; @@ -26,10 +27,6 @@ function getDevUser() { }; } -function isDevBypassEnabled() { - return process.env.NODE_ENV === 'development' && process.env.DEV_BYPASS_AUTH === 'true'; -} - function handleDevLogin(request: NextRequest): NextResponse { const returnTo = request.nextUrl.searchParams.get('returnTo') || '/'; const redirectUrl = new URL(returnTo, request.url); @@ -77,16 +74,36 @@ export async function GET(request: NextRequest, context: { params: Promise<{ aut const params = await context.params; const route = params.auth0?.[0]; - if (isDevBypassEnabled()) { + // Handle dev bypass or when Auth0 is not configured + if (isDevBypassEnabled() || !isAuth0Configured()) { switch (route) { case 'login': + if (!isAuth0Configured()) { + return NextResponse.json({ error: 'Authentication not configured' }, { status: 501 }); + } return handleDevLogin(request); case 'logout': + if (!isAuth0Configured()) { + return NextResponse.redirect(new URL('/', request.url)); + } return handleDevLogout(request); case 'me': + if (!isAuth0Configured()) { + return NextResponse.json(null, { status: 401 }); + } return handleDevMe(request); case 'callback': return NextResponse.redirect(new URL('/', request.url)); + case 'session': + case 'status': + if (!isAuth0Configured()) { + return NextResponse.json({ isAuthenticated: false }); + } + break; + default: + if (!isAuth0Configured()) { + return NextResponse.json({ error: 'Authentication not configured' }, { status: 501 }); + } } } diff --git a/app/api/auth/session/refresh/route.ts b/app/api/auth/session/refresh/route.ts index 7dcde9e..f15dddd 100644 --- a/app/api/auth/session/refresh/route.ts +++ b/app/api/auth/session/refresh/route.ts @@ -3,24 +3,21 @@ import crypto from 'crypto'; import { NextResponse } from 'next/server'; import { CACHE_TTL } from '@/app/config/api'; -import { checkApiAccessToken } from '@/lib/auth'; -import { validateSession, storeSession } from '@/lib/redis'; -import redis from '@/lib/redis'; +import redis, { validateSession, storeSession, isRedisAvailable } from '@/lib/redis'; const RATE_LIMIT_WINDOW = Math.floor(CACHE_TTL * 0.20 * 0.25); const MAX_REQUESTS = 3; async function isRateLimited(token: string): Promise { + if (!redis) {return false;} + const key = `ratelimit:refresh:${token}`; const now = Math.floor(Date.now() / 1000); try { await redis.zadd(key, now, now.toString()); - await redis.zremrangebyscore(key, 0, now - RATE_LIMIT_WINDOW); - const requestCount = await redis.zcard(key); - await redis.expire(key, RATE_LIMIT_WINDOW); return requestCount > MAX_REQUESTS; @@ -53,9 +50,9 @@ export async function POST(request: Request) { return NextResponse.json({ error: 'No session token found' }, { status: 401 }); } - const authCheckResponse = checkApiAccessToken(request); - if (authCheckResponse) { - return authCheckResponse; + // Skip session management if Redis is not available + if (!isRedisAvailable()) { + return NextResponse.json({ success: true }); } if (await isRateLimited(currentToken)) { @@ -64,7 +61,7 @@ export async function POST(request: Request) { { status: 429 } ); } - const ttl = await redis.ttl(`session:${currentToken}`); + const ttl = await redis!.ttl(`session:${currentToken}`); const isValid = await validateSession(currentToken); if (isValid && ttl > CACHE_TTL * 0.20) { diff --git a/app/api/auth/session/route.ts b/app/api/auth/session/route.ts index dc7deda..06b1fa6 100644 --- a/app/api/auth/session/route.ts +++ b/app/api/auth/session/route.ts @@ -4,7 +4,7 @@ import { NextResponse } from 'next/server'; import { CACHE_TTL } from '@/app/config/api'; import { checkApiAccessToken } from '@/lib/auth'; -import { storeSession } from '@/lib/redis'; +import { storeSession, validateSession, isRedisAvailable } from '@/lib/redis'; export async function GET(request: Request) { if (process.env.NODE_ENV === 'production') { @@ -27,12 +27,56 @@ export async function GET(request: Request) { return NextResponse.json({ error: 'Invalid request - invalid fetch mode' }, { status: 403 }); } } - + + // If Redis is not available, use a simple cookie flag instead + if (!isRedisAvailable()) { + const cookieHeader = request.headers.get('cookie'); + const hasAuthCookie = cookieHeader?.includes('session_token=validated'); + + // If already validated (has cookie), allow through + if (hasAuthCookie) { + return NextResponse.json({ success: true }); + } + + // No cookie, require access token + const authCheckResponse = checkApiAccessToken(request); + if (authCheckResponse) { + return authCheckResponse; + } + + // Access token valid, set persistent cookie + const response = NextResponse.json({ success: true }); + response.cookies.set('session_token', 'validated', { + httpOnly: process.env.NODE_ENV === 'production', + secure: process.env.NODE_ENV === 'production', + sameSite: 'strict', + maxAge: 24 * 60 * 60, + path: '/', + partitioned: process.env.NODE_ENV === 'production', + }); + return response; + } + + // Check if there's already a valid session cookie + const cookieHeader = request.headers.get('cookie'); + const existingToken = cookieHeader?.split(';') + .find(c => c.trim().startsWith('session_token=')) + ?.split('=')[1]; + + if (existingToken) { + const isValid = await validateSession(existingToken); + if (isValid) { + return NextResponse.json({ success: true }); + } + } + + // No valid session, require access token const authCheckResponse = checkApiAccessToken(request); if (authCheckResponse) { return authCheckResponse; } + // Create new session const sessionToken = crypto.randomBytes(32).toString('hex'); await storeSession(sessionToken); diff --git a/app/api/auth/status/route.ts b/app/api/auth/status/route.ts index 1855034..a017050 100644 --- a/app/api/auth/status/route.ts +++ b/app/api/auth/status/route.ts @@ -1,16 +1,17 @@ import { NextResponse } from 'next/server'; import { ACCESS_TOKEN } from '@/app/config/api'; +import { isAuth0Configured } from '@/lib/auth'; /** - * API endpoint to check if an access token is required for the application - * This allows the client to proactively check if token authentication is needed + * API endpoint to check auth configuration status */ export async function GET() { return NextResponse.json({ requiresAccessToken: !!ACCESS_TOKEN, - message: ACCESS_TOKEN - ? 'This application requires an access token to continue' + authEnabled: isAuth0Configured(), + message: ACCESS_TOKEN + ? 'This application requires an access token to continue' : 'No access token required for this application', }); } \ No newline at end of file diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index f325982..19626c6 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -1,13 +1,12 @@ import { createOpenAI } from '@ai-sdk/openai'; -import { getSession } from '@auth0/nextjs-auth0'; import { streamText, createDataStreamResponse, generateText, simulateReadableStream, Message } from 'ai'; -import { NextRequest, NextResponse } from 'next/server'; +import { NextRequest } from 'next/server'; import cl100k_base from "tiktoken/encoders/cl100k_base.json"; import { Tiktoken } from "tiktoken/lite"; import { apiEndpoint, apiKey, imgGenFnModel, DEFAULT_SYSTEM_PROMPT } from '@/app/config/api'; import { defaultModel } from '@/app/config/models'; -import { withSessionAuth } from '@/lib/auth'; +import { withSessionAuth, getOptionalSession } from '@/lib/auth'; import { getAvailableModelsForUser } from '@/lib/models'; import { checkTokenLimit, incrementTokenUsageWithMultiplier, getClientIP, getRateLimitConfigForUser, storeConversationTokens } from '@/lib/rate-limit'; import { LiteLLMService } from '@/lib/services/litellm-service'; @@ -103,7 +102,7 @@ function createOpenAIWithRateLimit(apiKey: string) { const openai = createOpenAIWithRateLimit(apiKey); async function handlePostRequest(req: NextRequest) { - const session = await getSession(req, NextResponse.next()); + const session = await getOptionalSession(req); const isAuthenticated = !!session?.user; let userApiKey: string | null = null; diff --git a/app/api/chats/load/route.ts b/app/api/chats/load/route.ts index 4ef7fa2..eed06e5 100644 --- a/app/api/chats/load/route.ts +++ b/app/api/chats/load/route.ts @@ -1,12 +1,16 @@ -import { getSession } from '@auth0/nextjs-auth0'; import { NextRequest, NextResponse } from 'next/server'; +import { getOptionalSession, isAuth0Configured } from '@/lib/auth'; import { createDatabaseService } from '@/lib/services/database-service'; import { createEncryptionService } from '@/lib/services/encryption-service'; export async function GET(request: NextRequest) { try { - const session = await getSession(request, new NextResponse()); + if (!isAuth0Configured()) { + return NextResponse.json({ chatSessions: [], count: 0 }); + } + + const session = await getOptionalSession(request); if (!session?.user?.sub) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } diff --git a/app/api/models/all/route.ts b/app/api/models/all/route.ts index 8c47eea..8d57064 100644 --- a/app/api/models/all/route.ts +++ b/app/api/models/all/route.ts @@ -1,8 +1,9 @@ -import { getSession } from '@auth0/nextjs-auth0'; import { NextRequest, NextResponse } from 'next/server'; import { apiEndpoint, apiKey } from '@/app/config/api'; +import { getOptionalSession } from '@/lib/auth'; import { getUserTier, getAllModels } from '@/lib/database'; +import { isDatabaseAvailable } from '@/lib/postgres'; // Models that are always available regardless of LiteLLM API status const ALWAYS_AVAILABLE_MODELS = ['AkashGen']; @@ -40,22 +41,11 @@ interface ModelWithAccess { export async function GET(req: NextRequest) { try { - // Check if user is authenticated - const session = await getSession(req, NextResponse.next()); + // Check if user is authenticated (optional - works without Auth0) + const session = await getOptionalSession(req); const userId = session?.user?.sub || null; - - // Get user's tier - let userTier = null; - if (userId) { - userTier = await getUserTier(userId); - } - - const userTierName = userTier?.name || 'permissionless'; - - // Get all models from database (across all tiers) - const allModels = await getAllModels(); - - // Check which models are currently available via LiteLLM API + + // Check which models are currently available via API let apiModels: any[] = []; try { const response = await fetch(apiEndpoint + '/models', { @@ -66,10 +56,58 @@ export async function GET(req: NextRequest) { const apiData = await response.json(); apiModels = apiData.data || []; } catch (error) { - console.warn('[API] Failed to fetch from LiteLLM API:', error); - // Continue with empty array - models will be marked as unavailable + console.warn('[API] Failed to fetch from API:', error); } - + + // If database is not available, return models directly from API + if (!isDatabaseAvailable()) { + const modelsFromApi = apiModels.map((apiModel: any) => ({ + id: apiModel.id, + model_id: apiModel.id, + api_id: apiModel.id, + name: apiModel.id.split('/').pop() || apiModel.id, + description: `${apiModel.id} model`, + tier_requirement: 'permissionless', + available: true, + temperature: 0.7, + top_p: 0.95, + token_limit: 128000, + owned_by: apiModel.owned_by, + display_order: 0, + user_has_access: true, + is_available_now: true, + action_button: 'start_chat' as const, + action_text: 'Start Chat' + })); + + return NextResponse.json({ + models: modelsFromApi, + user_tier: 'permissionless', + stats: { + total_models: modelsFromApi.length, + available_now: modelsFromApi.length, + user_accessible: modelsFromApi.length, + user_available: modelsFromApi.length, + by_tier: { + permissionless: modelsFromApi.length, + extended: 0, + pro: 0 + } + } + }); + } + + // Get user's tier + let userTier = null; + if (userId) { + userTier = await getUserTier(userId); + } + + const userTierName = userTier?.name || 'permissionless'; + + // Get all models from database (across all tiers) + const allModels = await getAllModels(); + // Create set of available API model IDs for quick lookup const availableApiIds = new Set(apiModels.map(model => model.id)); diff --git a/app/api/models/route.ts b/app/api/models/route.ts index 037619d..504abd4 100644 --- a/app/api/models/route.ts +++ b/app/api/models/route.ts @@ -1,12 +1,12 @@ -import { getSession } from '@auth0/nextjs-auth0'; import { NextRequest, NextResponse } from 'next/server'; +import { getOptionalSession } from '@/lib/auth'; import { getAvailableModelsForUser } from '@/lib/models'; export async function GET(req: NextRequest) { try { - // Check if user is authenticated - const session = await getSession(req, NextResponse.next()); + // Check if user is authenticated (optional - works without Auth0) + const session = await getOptionalSession(req); const userId = session?.user?.sub || null; // Get models based on user's tier (or anonymous/permissionless for non-logged-in users) diff --git a/app/api/rate-limit/status/route.ts b/app/api/rate-limit/status/route.ts index c56d767..7a51fdd 100644 --- a/app/api/rate-limit/status/route.ts +++ b/app/api/rate-limit/status/route.ts @@ -1,6 +1,6 @@ -import { getSession } from '@auth0/nextjs-auth0'; -import { NextRequest, NextResponse } from 'next/server'; +import { NextRequest } from 'next/server'; +import { getOptionalSession } from '@/lib/auth'; import { checkTokenLimit, getClientIP, getRateLimitConfigForUser, getConversationTokens } from '@/lib/rate-limit'; export async function GET(req: NextRequest) { @@ -21,8 +21,8 @@ export async function GET(req: NextRequest) { }); } - // Check authentication status - const session = await getSession(req, NextResponse.next()); + // Check authentication status (optional - works without Auth0) + const session = await getOptionalSession(req); const isAuthenticated = !!session?.user; // Determine rate limit identifier and config diff --git a/app/api/user/account/route.ts b/app/api/user/account/route.ts index cb48915..ec23f4a 100644 --- a/app/api/user/account/route.ts +++ b/app/api/user/account/route.ts @@ -1,11 +1,15 @@ -import { getSession } from '@auth0/nextjs-auth0'; import { NextRequest, NextResponse } from 'next/server'; +import { getOptionalSession, isAuth0Configured } from '@/lib/auth'; import { deleteAllUserData } from '@/lib/database'; export async function DELETE(req: NextRequest) { try { - const session = await getSession(req, NextResponse.next()); + if (!isAuth0Configured()) { + return NextResponse.json({ error: 'Authentication not configured' }, { status: 401 }); + } + + const session = await getOptionalSession(req); if (!session?.user) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } diff --git a/app/api/user/chat-history/route.ts b/app/api/user/chat-history/route.ts index 60b58f1..8d77ec5 100644 --- a/app/api/user/chat-history/route.ts +++ b/app/api/user/chat-history/route.ts @@ -1,11 +1,15 @@ -import { getSession } from '@auth0/nextjs-auth0'; import { NextRequest, NextResponse } from 'next/server'; +import { getOptionalSession, isAuth0Configured } from '@/lib/auth'; import { deleteAllUserChatHistory } from '@/lib/database'; export async function DELETE(req: NextRequest) { try { - const session = await getSession(req, NextResponse.next()); + if (!isAuth0Configured()) { + return NextResponse.json({ error: 'Authentication not configured' }, { status: 401 }); + } + + const session = await getOptionalSession(req); if (!session?.user) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } diff --git a/app/api/user/initialize/route.ts b/app/api/user/initialize/route.ts index cb485be..4ebd441 100644 --- a/app/api/user/initialize/route.ts +++ b/app/api/user/initialize/route.ts @@ -1,37 +1,20 @@ -import { getSession } from '@auth0/nextjs-auth0'; import { NextRequest, NextResponse } from 'next/server'; +import { getOptionalSession, isAuth0Configured, isDevBypassEnabled } from '@/lib/auth'; import { getUserPreferences, upsertUserPreferences, updateUserTier } from '@/lib/database'; import { LiteLLMService } from '@/lib/services/litellm-service'; -function isDevBypassEnabled() { - return process.env.NODE_ENV === 'development' && process.env.DEV_BYPASS_AUTH === 'true'; -} - export async function POST(req: NextRequest) { try { - if (isDevBypassEnabled()) { - const devUserId = process.env.DEV_USER_ID || 'dev-test-user'; - try { - const existingPreferences = await getUserPreferences(devUserId); - return NextResponse.json({ - success: true, - isNewUser: !existingPreferences, - message: existingPreferences - ? 'User initialized successfully' - : 'New user created and initialized with extended tier' - }); - } catch (error) { - console.log('[DEV MODE] User initialize - DB error (ignored):', error); - return NextResponse.json({ - success: true, - isNewUser: false, - message: 'User initialized successfully (dev mode)' - }); - } + if (isDevBypassEnabled() || !isAuth0Configured()) { + return NextResponse.json({ + success: true, + isNewUser: false, + message: 'User initialized successfully (no auth mode)' + }); } - const session = await getSession(req, NextResponse.next()); + const session = await getOptionalSession(req); if (!session?.user) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } diff --git a/app/api/user/verification-status/route.ts b/app/api/user/verification-status/route.ts index 8ed0a9d..43402a4 100644 --- a/app/api/user/verification-status/route.ts +++ b/app/api/user/verification-status/route.ts @@ -1,12 +1,8 @@ -import { getSession } from '@auth0/nextjs-auth0'; import { NextRequest, NextResponse } from 'next/server'; +import { getOptionalSession, isAuth0Configured, isDevBypassEnabled } from '@/lib/auth'; import { auth0Management } from '@/lib/auth0-management'; -function isDevBypassEnabled() { - return process.env.NODE_ENV === 'development' && process.env.DEV_BYPASS_AUTH === 'true'; -} - function getDevVerificationStatus() { return NextResponse.json({ emailVerified: true, @@ -28,11 +24,11 @@ function getDevVerificationStatus() { export async function GET(req: NextRequest) { try { - if (isDevBypassEnabled()) { + if (isDevBypassEnabled() || !isAuth0Configured()) { return getDevVerificationStatus(); } - const session = await getSession(req, NextResponse.next()); + const session = await getOptionalSession(req); if (!session?.user) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } @@ -78,8 +74,8 @@ export async function GET(req: NextRequest) { export async function POST(req: NextRequest) { try { - if (isDevBypassEnabled()) { - return NextResponse.json({ + if (isDevBypassEnabled() || !isAuth0Configured()) { + return NextResponse.json({ success: true, emailVerified: true, marketingConsent: true, @@ -87,7 +83,7 @@ export async function POST(req: NextRequest) { }); } - const session = await getSession(req, NextResponse.next()); + const session = await getOptionalSession(req); if (!session?.user) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } diff --git a/app/config/api.ts b/app/config/api.ts index 3b5e226..d4bed56 100644 --- a/app/config/api.ts +++ b/app/config/api.ts @@ -1,9 +1,10 @@ -export const apiEndpoint = process.env.API_ENDPOINT || 'https://chatapi.akash.network/api/v1'; +export const apiEndpoint = process.env.API_ENDPOINT || 'https://api.akashml.com/v1'; export const apiKey = process.env.API_KEY; export const imgApiKey = process.env.IMG_API_KEY; export const imgEndpoint = process.env.IMG_ENDPOINT export const imgGenFnModel = process.env.IMG_GEN_FN_MODEL export const CACHE_TTL = Number(process.env.CACHE_TTL) || 10 * 60; // 10 minutes +export const SESSION_TTL = Number(process.env.SESSION_TTL) || 24 * 60 * 60; // 24 hours export const ACCESS_TOKEN = process.env.ACCESS_TOKEN || null; export const DEFAULT_SYSTEM_PROMPT = `You are a skilled conversationalist who adapts naturally to what users need. Your responses match the situation—whether someone wants deep analysis, casual chat, emotional support, creative collaboration, or just needs to vent. Core Approach diff --git a/app/context/ChatContext.tsx b/app/context/ChatContext.tsx index 57ddb13..3d4473a 100644 --- a/app/context/ChatContext.tsx +++ b/app/context/ChatContext.tsx @@ -16,7 +16,7 @@ import { useEncryptedSettings, UserPreferences } from '@/hooks/use-encrypted-set import { Folder, useFolders } from '@/hooks/use-folders'; import { useRateLimit } from '@/hooks/use-rate-limit'; import { safeSetItem } from '@/lib/local-storage-manager'; -import { getAccessToken, storeAccessToken, processMessages } from '@/lib/utils'; +import { validateAccessToken, processMessages } from '@/lib/utils'; const SELECTED_MODEL_KEY = 'selectedModel'; const CURRENT_SYSTEM_PROMPT_KEY = 'currentSystemPrompt'; @@ -530,32 +530,32 @@ export const ChatProvider: React.FC<{ children: React.ReactNode }> = ({ children if (statusResponse.ok) { const { requiresAccessToken } = await statusResponse.json(); - // If an access token is required but not present, show the dialog - if (requiresAccessToken && !getAccessToken()) { - setIsAccessError(true); - setSessionInitialized(true); - return; + // If an access token is required, try to establish session + // If it fails (no valid session cookie), show the access token dialog + if (requiresAccessToken) { + const response = await fetch('/api/auth/session/'); + if (!response.ok) { + const data = await response.json(); + if (response.status === 403 && data.error === 'Access token required') { + setIsAccessError(true); + setSessionInitialized(true); + return; + } + } } } - const accessToken = getAccessToken(); - const response = await fetch('/api/auth/session/', accessToken ? { - headers: { - 'Authorization': `Bearer ${accessToken}` - } - } : {}); + // Try to establish session (works with or without access token requirement) + const response = await fetch('/api/auth/session/'); if (!response.ok) { const data = await response.json(); - if (response.status === 403 && data.error === 'Invalid Access token' ) { + if (response.status === 403 && (data.error === 'Access token required' || data.error === 'Invalid Access token')) { setIsAccessError(true); } else { throw new Error('Failed to initialize session'); } } setSessionInitialized(true); - - // Check and cleanup local storage if needed - // Storage cleanup is now handled by safeSetItem when errors occur } catch (error) { setSessionError('Unable to establish a secure session. Please try refreshing the page.'); } @@ -696,29 +696,17 @@ export const ChatProvider: React.FC<{ children: React.ReactNode }> = ({ children const handleAccessTokenSubmit = async () => { if (accessTokenInput.trim()) { try { - await storeAccessToken(accessTokenInput.trim()); + const isValid = await validateAccessToken(accessTokenInput.trim()); - // Try to validate the token with the server - const response = await fetch('/api/auth/session/', { - headers: { - 'Authorization': `Bearer ${getAccessToken()}` - } - }); - - if (!response.ok) { - const data = await response.json(); - if (response.status === 403 && data.error === 'Invalid Access token') { - setModelError('Access token is invalid. Please check and try again.'); - return; - } else { - throw new Error('Failed to validate access token'); - } + if (!isValid) { + setModelError('Access token is invalid. Please check and try again.'); + return; } - - // Token is valid + + // Token is valid and session created setIsAccessError(false); setModelError(null); - + setAccessTokenInput(''); setSessionInitialized(true); } catch (error) { setModelError('Failed to validate access token. Please try again.'); diff --git a/components/auth/user-menu.tsx b/components/auth/user-menu.tsx index 238aad3..95400da 100644 --- a/components/auth/user-menu.tsx +++ b/components/auth/user-menu.tsx @@ -12,6 +12,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; +import { useAuthStatus } from '@/hooks/use-auth-status'; import { cleanupUserDataOnLogout } from '@/lib/data-sync'; import { cn } from '@/lib/utils'; @@ -22,6 +23,7 @@ interface UserMenuProps { export function UserMenu({ className }: UserMenuProps) { const { user, isLoading } = useUser(); const { resetAllState } = useChatContext(); + const { authEnabled, isLoading: authLoading } = useAuthStatus(); const handleLogout = () => { @@ -39,13 +41,18 @@ export function UserMenu({ className }: UserMenuProps) { } }; - if (isLoading) { + if (isLoading || authLoading) { return (
); } + // Don't show sign in button if auth is not configured if (!user) { + if (!authEnabled) { + return null; + } + return (