Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 33 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 | - |

Expand Down
27 changes: 22 additions & 5 deletions app/api/auth/[...auth0]/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isAuth0Configured, isDevBypassEnabled } from '@/lib/auth';
import { handleAuth } from '@auth0/nextjs-auth0';
import { NextRequest, NextResponse } from 'next/server';

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 });
}
}
}

Expand Down
17 changes: 7 additions & 10 deletions app/api/auth/session/refresh/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean> {
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;
Expand Down Expand Up @@ -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)) {
Expand All @@ -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) {
Expand Down
48 changes: 46 additions & 2 deletions app/api/auth/session/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand All @@ -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);
Expand Down
9 changes: 5 additions & 4 deletions app/api/auth/status/route.ts
Original file line number Diff line number Diff line change
@@ -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',
});
}
7 changes: 3 additions & 4 deletions app/api/chat/route.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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;
Expand Down
8 changes: 6 additions & 2 deletions app/api/chats/load/route.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
Expand Down
Loading