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
41 changes: 38 additions & 3 deletions admin/src/api/client.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,44 @@
// Demo mode interceptor — loaded async but awaited before first API call
const demoReady: Promise<void> | null =
// Can be activated at build time (VITE_DEMO_MODE) or at runtime (admin/admin login)
let demoReady: Promise<void> | null =
import.meta.env.VITE_DEMO_MODE === "true"
? import("./demo/index").then(({ setupDemoInterceptor }) => setupDemoInterceptor())
? import("./demo/index").then(({ setupDemoInterceptor }) =>
setupDemoInterceptor(),
)
: null;

let demoActive = import.meta.env.VITE_DEMO_MODE === "true";

// Activate demo mode at runtime (e.g. admin/admin login on production)
export function activateDemoMode(): Promise<void> {
if (demoActive) return demoReady!;
demoActive = true;
demoReady = import("./demo/index").then(({ setupDemoInterceptor }) =>
setupDemoInterceptor(),
);
return demoReady;
}

export function isDemoActive(): boolean {
return demoActive;
}

// Auto-activate demo if page reloads with a demo token in localStorage
function checkDemoToken(): boolean {
const token = localStorage.getItem("admin_token");
if (!token) return false;
try {
const payload = JSON.parse(atob(token.split(".")[1]));
return payload.demo === true;
} catch {
return false;
}
}

if (!demoActive && checkDemoToken()) {
activateDemoMode();
}

// Base API client
const BASE_URL = "";

Expand Down Expand Up @@ -89,7 +124,7 @@ export const api = {
// SSE helper with generic type support (fetch-based to send JWT)
export function createSSE<T = unknown>(endpoint: string, onMessage: (data: T) => void) {
// In demo mode, use polling via fetch instead of real SSE
if (import.meta.env.VITE_DEMO_MODE === "true") {
if (demoActive) {
let stopped = false;
const poll = async () => {
if (demoReady) await demoReady;
Expand Down
13 changes: 7 additions & 6 deletions admin/src/composables/useClaudeCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
type CcContextFile,
type CcSessionSummary,
} from '@/api/claudeCode'
import { isDemoActive } from '@/api/client'

export interface CcMessage {
role: 'user' | 'assistant'
Expand Down Expand Up @@ -46,7 +47,7 @@ let ws: ReturnType<typeof createClaudeCodeWs> | null = null
let reconnectTimer: ReturnType<typeof setTimeout> | null = null
let reconnectAttempts = 0
const MAX_RECONNECT_ATTEMPTS = 5
const isDemo = import.meta.env.VITE_DEMO_MODE === 'true'
const isDemo = () => import.meta.env.VITE_DEMO_MODE === 'true' || isDemoActive()

function handleEvent(event: CcWsEvent) {
error.value = null
Expand Down Expand Up @@ -170,7 +171,7 @@ function _autoSaveTranscript() {
}

function connect() {
if (isDemo) {
if (isDemo()) {
isConnected.value = true
return
}
Expand Down Expand Up @@ -264,7 +265,7 @@ function newSession() {
}
_resetState()
// Reconnect if WS dropped
if (!ws && !isDemo) {
if (!ws && !isDemo()) {
connect()
}
}
Expand All @@ -284,7 +285,7 @@ function sendMessage(prompt: string) {
currentToolBlocks.value = []
error.value = null

if (isDemo) {
if (isDemo()) {
_demoSimulate()
return
}
Expand Down Expand Up @@ -361,7 +362,7 @@ async function loadSession(sessionId: string) {
if (!isActive.value) {
isActive.value = true
}
if (!ws && !isDemo) {
if (!ws && !isDemo()) {
connect()
}
} catch {
Expand All @@ -370,7 +371,7 @@ async function loadSession(sessionId: string) {
}

function abort() {
if (isDemo) {
if (isDemo()) {
isProcessing.value = false
_flushMessage()
return
Expand Down
47 changes: 34 additions & 13 deletions admin/src/stores/auth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { activateDemoMode, isDemoActive } from '@/api/client'

export type UserRole = 'admin' | 'user' | 'web' | 'guest'

Expand Down Expand Up @@ -85,9 +86,15 @@ export const useAuthStore = defineStore('auth', () => {
role: payload.role,
workspace_id: payload.workspace_id || 1,
}
// Fetch deployment mode and permissions on init
fetchDeploymentMode()
fetchPermissions()
if (payload.demo) {
// Demo session — use demo interceptor, don't hit backend
deploymentMode.value = 'full'
activateDemoMode().then(() => fetchPermissions())
} else {
// Real session — fetch from backend
fetchDeploymentMode()
fetchPermissions()
}
} catch {
token.value = null
localStorage.removeItem('admin_token')
Expand All @@ -100,21 +107,40 @@ export const useAuthStore = defineStore('auth', () => {
const payload = btoa(JSON.stringify({
sub: username,
user_id: 0,
role: import.meta.env.VITE_DEMO_ROLE || 'admin',
role: 'admin',
workspace_id: 1,
exp: Math.floor(Date.now() / 1000) + 86400, // 24 hours
iat: Math.floor(Date.now() / 1000),
dev: true
demo: true,
}))
const signature = btoa('dev-signature')
const signature = btoa('demo-signature')
return `${header}.${payload}.${signature}`
}

async function loginAsDemo(username: string): Promise<boolean> {
await activateDemoMode()
const devToken = createDevToken(username)
token.value = devToken
localStorage.setItem('admin_token', devToken)
user.value = { id: 0, username, role: 'admin' as UserRole, workspace_id: 1 }
deploymentMode.value = 'full'
// Fetch permissions from demo interceptor
await fetchPermissions()
localStorage.removeItem('chat-fullscreen')
error.value = null
return true
}

async function login(username: string, password: string): Promise<boolean> {
isLoading.value = true
error.value = null

try {
// admin/admin always enters demo mode (offline, mock data)
if (username === 'admin' && password === 'admin') {
return await loginAsDemo(username)
}

const response = await fetch('/admin/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
Expand Down Expand Up @@ -150,14 +176,9 @@ export const useAuthStore = defineStore('auth', () => {
return true
} catch (e) {
// In dev mode, allow login without backend
if ((isDev || isDemo) && ((username === 'admin' && password === 'admin') || (username === 'demo' && password === 'demo'))) {
if (isDev || isDemo) {
console.warn('⚠️ Dev/Demo mode: Backend unavailable, using mock authentication')
const devToken = createDevToken(username)
token.value = devToken
localStorage.setItem('admin_token', devToken)
user.value = { id: 0, username, role: (import.meta.env.VITE_DEMO_ROLE || 'admin') as UserRole, workspace_id: 1 }
error.value = null
return true
return await loginAsDemo(username)
}

error.value = 'Connection error - Backend not running'
Expand Down
Loading