From 092004d07783025ac35536fa735d1bffbc071398 Mon Sep 17 00:00:00 2001 From: ShaerWare Date: Thu, 2 Apr 2026 22:21:36 +0500 Subject: [PATCH] feat: admin/admin login enters demo mode on production MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Runtime demo activation — admin/admin credentials bypass backend and activate client-side demo interceptor with mock data. Real admin user disabled on both server and local. Co-Authored-By: Claude Opus 4.6 (1M context) --- admin/src/api/client.ts | 41 ++++++++++++++++++++-- admin/src/composables/useClaudeCode.ts | 13 +++---- admin/src/stores/auth.ts | 47 +++++++++++++++++++------- 3 files changed, 79 insertions(+), 22 deletions(-) diff --git a/admin/src/api/client.ts b/admin/src/api/client.ts index a1b3d184..dd5b0f8f 100644 --- a/admin/src/api/client.ts +++ b/admin/src/api/client.ts @@ -1,9 +1,44 @@ // Demo mode interceptor — loaded async but awaited before first API call -const demoReady: Promise | null = +// Can be activated at build time (VITE_DEMO_MODE) or at runtime (admin/admin login) +let demoReady: Promise | 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 { + 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 = ""; @@ -89,7 +124,7 @@ export const api = { // SSE helper with generic type support (fetch-based to send JWT) export function createSSE(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; diff --git a/admin/src/composables/useClaudeCode.ts b/admin/src/composables/useClaudeCode.ts index db5f9d42..9dc93bb2 100644 --- a/admin/src/composables/useClaudeCode.ts +++ b/admin/src/composables/useClaudeCode.ts @@ -6,6 +6,7 @@ import { type CcContextFile, type CcSessionSummary, } from '@/api/claudeCode' +import { isDemoActive } from '@/api/client' export interface CcMessage { role: 'user' | 'assistant' @@ -46,7 +47,7 @@ let ws: ReturnType | null = null let reconnectTimer: ReturnType | 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 @@ -170,7 +171,7 @@ function _autoSaveTranscript() { } function connect() { - if (isDemo) { + if (isDemo()) { isConnected.value = true return } @@ -264,7 +265,7 @@ function newSession() { } _resetState() // Reconnect if WS dropped - if (!ws && !isDemo) { + if (!ws && !isDemo()) { connect() } } @@ -284,7 +285,7 @@ function sendMessage(prompt: string) { currentToolBlocks.value = [] error.value = null - if (isDemo) { + if (isDemo()) { _demoSimulate() return } @@ -361,7 +362,7 @@ async function loadSession(sessionId: string) { if (!isActive.value) { isActive.value = true } - if (!ws && !isDemo) { + if (!ws && !isDemo()) { connect() } } catch { @@ -370,7 +371,7 @@ async function loadSession(sessionId: string) { } function abort() { - if (isDemo) { + if (isDemo()) { isProcessing.value = false _flushMessage() return diff --git a/admin/src/stores/auth.ts b/admin/src/stores/auth.ts index cb028817..65f90b1f 100644 --- a/admin/src/stores/auth.ts +++ b/admin/src/stores/auth.ts @@ -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' @@ -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') @@ -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 { + 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 { 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' }, @@ -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'