feat: complete per-user integration config isolation and UI initialization
Some checks failed
Release Binaries / release (push) Has been cancelled
Some checks failed
Release Binaries / release (push) Has been cancelled
This commit is contained in:
@@ -9,10 +9,13 @@ import {
|
|||||||
import { Logger } from "../../logger"
|
import { Logger } from "../../logger"
|
||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { getUserIntegrationsDir } from "../../user-data"
|
import { getUserIntegrationsDir, getUserIdFromRequest } from "../../user-context"
|
||||||
|
|
||||||
const CONFIG_DIR = getUserIntegrationsDir()
|
// Helper to get config file path for a user
|
||||||
const CONFIG_FILE = path.join(CONFIG_DIR, "ollama-config.json")
|
function getConfigFile(userId?: string | null): string {
|
||||||
|
const configDir = getUserIntegrationsDir(userId || undefined)
|
||||||
|
return path.join(configDir, "ollama-config.json")
|
||||||
|
}
|
||||||
|
|
||||||
interface OllamaRouteDeps {
|
interface OllamaRouteDeps {
|
||||||
logger: Logger
|
logger: Logger
|
||||||
@@ -26,7 +29,8 @@ export async function registerOllamaRoutes(
|
|||||||
|
|
||||||
app.get('/api/ollama/config', async (request, reply) => {
|
app.get('/api/ollama/config', async (request, reply) => {
|
||||||
try {
|
try {
|
||||||
const config = getOllamaConfig()
|
const userId = getUserIdFromRequest(request)
|
||||||
|
const config = getOllamaConfig(userId)
|
||||||
return { config: { ...config, apiKey: config.apiKey ? '***' : undefined } }
|
return { config: { ...config, apiKey: config.apiKey ? '***' : undefined } }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ error }, "Failed to get Ollama config")
|
logger.error({ error }, "Failed to get Ollama config")
|
||||||
@@ -48,9 +52,10 @@ export async function registerOllamaRoutes(
|
|||||||
}
|
}
|
||||||
}, async (request, reply) => {
|
}, async (request, reply) => {
|
||||||
try {
|
try {
|
||||||
|
const userId = getUserIdFromRequest(request)
|
||||||
const { enabled, apiKey, endpoint } = request.body as any
|
const { enabled, apiKey, endpoint } = request.body as any
|
||||||
updateOllamaConfig({ enabled, apiKey, endpoint })
|
updateOllamaConfig({ enabled, apiKey, endpoint }, userId)
|
||||||
logger.info("Ollama Cloud configuration updated")
|
logger.info({ userId }, "Ollama Cloud configuration updated for user")
|
||||||
return { success: true, config: { enabled, endpoint, apiKey: apiKey ? '***' : undefined } }
|
return { success: true, config: { enabled, endpoint, apiKey: apiKey ? '***' : undefined } }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ error }, "Failed to update Ollama config")
|
logger.error({ error }, "Failed to update Ollama config")
|
||||||
@@ -60,7 +65,8 @@ export async function registerOllamaRoutes(
|
|||||||
|
|
||||||
app.post('/api/ollama/test', async (request, reply) => {
|
app.post('/api/ollama/test', async (request, reply) => {
|
||||||
try {
|
try {
|
||||||
const config = getOllamaConfig()
|
const userId = getUserIdFromRequest(request)
|
||||||
|
const config = getOllamaConfig(userId)
|
||||||
if (!config.enabled) {
|
if (!config.enabled) {
|
||||||
return reply.status(400).send({ error: "Ollama Cloud is not enabled" })
|
return reply.status(400).send({ error: "Ollama Cloud is not enabled" })
|
||||||
}
|
}
|
||||||
@@ -556,24 +562,27 @@ export async function registerOllamaRoutes(
|
|||||||
logger.info("Ollama Cloud routes registered")
|
logger.info("Ollama Cloud routes registered")
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOllamaConfig(): OllamaCloudConfig {
|
function getOllamaConfig(userId?: string | null): OllamaCloudConfig {
|
||||||
|
const configFile = getConfigFile(userId)
|
||||||
try {
|
try {
|
||||||
if (!fs.existsSync(CONFIG_FILE)) {
|
if (!fs.existsSync(configFile)) {
|
||||||
return { enabled: false, endpoint: "https://ollama.com" }
|
return { enabled: false, endpoint: "https://ollama.com" }
|
||||||
}
|
}
|
||||||
const data = fs.readFileSync(CONFIG_FILE, 'utf-8')
|
const data = fs.readFileSync(configFile, 'utf-8')
|
||||||
return JSON.parse(data)
|
return JSON.parse(data)
|
||||||
} catch {
|
} catch {
|
||||||
return { enabled: false, endpoint: "https://ollama.com" }
|
return { enabled: false, endpoint: "https://ollama.com" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateOllamaConfig(config: Partial<OllamaCloudConfig>): void {
|
function updateOllamaConfig(config: Partial<OllamaCloudConfig>, userId?: string | null): void {
|
||||||
|
const configFile = getConfigFile(userId)
|
||||||
|
const configDir = getUserIntegrationsDir(userId || undefined)
|
||||||
try {
|
try {
|
||||||
if (!fs.existsSync(CONFIG_DIR)) {
|
if (!fs.existsSync(configDir)) {
|
||||||
fs.mkdirSync(CONFIG_DIR, { recursive: true })
|
fs.mkdirSync(configDir, { recursive: true })
|
||||||
}
|
}
|
||||||
const current = getOllamaConfig()
|
const current = getOllamaConfig(userId)
|
||||||
|
|
||||||
// Only update apiKey if a new non-empty value is provided
|
// Only update apiKey if a new non-empty value is provided
|
||||||
const updated = {
|
const updated = {
|
||||||
@@ -583,8 +592,8 @@ function updateOllamaConfig(config: Partial<OllamaCloudConfig>): void {
|
|||||||
apiKey: config.apiKey || current.apiKey
|
apiKey: config.apiKey || current.apiKey
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.writeFileSync(CONFIG_FILE, JSON.stringify(updated, null, 2))
|
fs.writeFileSync(configFile, JSON.stringify(updated, null, 2))
|
||||||
console.log(`[Ollama] Config saved: enabled=${updated.enabled}, endpoint=${updated.endpoint}, hasApiKey=${!!updated.apiKey}`)
|
console.log(`[Ollama] Config saved for user ${userId || "default"}: enabled=${updated.enabled}, endpoint=${updated.endpoint}, hasApiKey=${!!updated.apiKey}`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to save Ollama config:", error)
|
console.error("Failed to save Ollama config:", error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { FastifyInstance } from "fastify"
|
import { FastifyInstance, FastifyRequest } from "fastify"
|
||||||
import { ZAIClient, ZAI_MODELS, type ZAIConfig, type ZAIChatRequest, type ZAIMessage } from "../../integrations/zai-api"
|
import { ZAIClient, ZAI_MODELS, type ZAIConfig, type ZAIChatRequest, type ZAIMessage } from "../../integrations/zai-api"
|
||||||
import { Logger } from "../../logger"
|
import { Logger } from "../../logger"
|
||||||
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs"
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs"
|
||||||
import { join } from "path"
|
import { join } from "path"
|
||||||
import { getUserIntegrationsDir } from "../../user-data"
|
import { getUserIntegrationsDir, getUserIdFromRequest } from "../../user-context"
|
||||||
import { CORE_TOOLS, executeTools, type ToolCall, type ToolResult } from "../../tools/executor"
|
import { CORE_TOOLS, executeTools, type ToolCall, type ToolResult } from "../../tools/executor"
|
||||||
import { getMcpManager } from "../../mcp/client"
|
import { getMcpManager } from "../../mcp/client"
|
||||||
|
|
||||||
@@ -11,27 +11,27 @@ interface ZAIRouteDeps {
|
|||||||
logger: Logger
|
logger: Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
const CONFIG_DIR = getUserIntegrationsDir()
|
|
||||||
const CONFIG_FILE = join(CONFIG_DIR, "zai-config.json")
|
|
||||||
|
|
||||||
// Maximum number of tool execution loops to prevent infinite recursion
|
// Maximum number of tool execution loops to prevent infinite recursion
|
||||||
const MAX_TOOL_LOOPS = 10
|
const MAX_TOOL_LOOPS = 10
|
||||||
|
|
||||||
|
// Helper to get config file path for a user
|
||||||
|
function getConfigFile(userId?: string | null): string {
|
||||||
|
const configDir = getUserIntegrationsDir(userId || undefined)
|
||||||
|
return join(configDir, "zai-config.json")
|
||||||
|
}
|
||||||
|
|
||||||
export async function registerZAIRoutes(
|
export async function registerZAIRoutes(
|
||||||
app: FastifyInstance,
|
app: FastifyInstance,
|
||||||
deps: ZAIRouteDeps
|
deps: ZAIRouteDeps
|
||||||
) {
|
) {
|
||||||
const logger = deps.logger.child({ component: "zai-routes" })
|
const logger = deps.logger.child({ component: "zai-routes" })
|
||||||
|
|
||||||
// Ensure config directory exists
|
// Get Z.AI configuration (per-user)
|
||||||
if (!existsSync(CONFIG_DIR)) {
|
|
||||||
mkdirSync(CONFIG_DIR, { recursive: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get Z.AI configuration
|
|
||||||
app.get('/api/zai/config', async (request, reply) => {
|
app.get('/api/zai/config', async (request, reply) => {
|
||||||
try {
|
try {
|
||||||
const config = getZAIConfig()
|
const userId = getUserIdFromRequest(request)
|
||||||
|
const config = getZAIConfig(userId)
|
||||||
|
logger.debug({ userId }, "Getting Z.AI config for user")
|
||||||
return { config: { ...config, apiKey: config.apiKey ? '***' : undefined } }
|
return { config: { ...config, apiKey: config.apiKey ? '***' : undefined } }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ error }, "Failed to get Z.AI config")
|
logger.error({ error }, "Failed to get Z.AI config")
|
||||||
@@ -39,12 +39,13 @@ export async function registerZAIRoutes(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Update Z.AI configuration
|
// Update Z.AI configuration (per-user)
|
||||||
app.post('/api/zai/config', async (request, reply) => {
|
app.post('/api/zai/config', async (request, reply) => {
|
||||||
try {
|
try {
|
||||||
|
const userId = getUserIdFromRequest(request)
|
||||||
const { enabled, apiKey, endpoint } = request.body as Partial<ZAIConfig>
|
const { enabled, apiKey, endpoint } = request.body as Partial<ZAIConfig>
|
||||||
updateZAIConfig({ enabled, apiKey, endpoint })
|
updateZAIConfig({ enabled, apiKey, endpoint }, userId)
|
||||||
logger.info("Z.AI configuration updated")
|
logger.info({ userId }, "Z.AI configuration updated for user")
|
||||||
return { success: true, config: { enabled, endpoint, apiKey: apiKey ? '***' : undefined } }
|
return { success: true, config: { enabled, endpoint, apiKey: apiKey ? '***' : undefined } }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ error }, "Failed to update Z.AI config")
|
logger.error({ error }, "Failed to update Z.AI config")
|
||||||
@@ -52,10 +53,11 @@ export async function registerZAIRoutes(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Test Z.AI connection
|
// Test Z.AI connection (per-user)
|
||||||
app.post('/api/zai/test', async (request, reply) => {
|
app.post('/api/zai/test', async (request, reply) => {
|
||||||
try {
|
try {
|
||||||
const config = getZAIConfig()
|
const userId = getUserIdFromRequest(request)
|
||||||
|
const config = getZAIConfig(userId)
|
||||||
if (!config.enabled) {
|
if (!config.enabled) {
|
||||||
return reply.status(400).send({ error: "Z.AI is not enabled" })
|
return reply.status(400).send({ error: "Z.AI is not enabled" })
|
||||||
}
|
}
|
||||||
@@ -80,10 +82,11 @@ export async function registerZAIRoutes(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Chat completion endpoint WITH MCP TOOL SUPPORT
|
// Chat completion endpoint WITH MCP TOOL SUPPORT (per-user)
|
||||||
app.post('/api/zai/chat', async (request, reply) => {
|
app.post('/api/zai/chat', async (request, reply) => {
|
||||||
try {
|
try {
|
||||||
const config = getZAIConfig()
|
const userId = getUserIdFromRequest(request)
|
||||||
|
const config = getZAIConfig(userId)
|
||||||
if (!config.enabled) {
|
if (!config.enabled) {
|
||||||
return reply.status(400).send({ error: "Z.AI is not enabled" })
|
return reply.status(400).send({ error: "Z.AI is not enabled" })
|
||||||
}
|
}
|
||||||
@@ -348,11 +351,12 @@ async function chatWithToolLoop(
|
|||||||
return lastResponse
|
return lastResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
function getZAIConfig(): ZAIConfig {
|
function getZAIConfig(userId?: string | null): ZAIConfig {
|
||||||
|
const configFile = getConfigFile(userId)
|
||||||
try {
|
try {
|
||||||
console.log(`[Z.AI] Looking for config at: ${CONFIG_FILE}`)
|
console.log(`[Z.AI] Looking for config at: ${configFile} (user: ${userId || "default"})`)
|
||||||
if (existsSync(CONFIG_FILE)) {
|
if (existsSync(configFile)) {
|
||||||
const data = readFileSync(CONFIG_FILE, 'utf-8')
|
const data = readFileSync(configFile, 'utf-8')
|
||||||
const parsed = JSON.parse(data)
|
const parsed = JSON.parse(data)
|
||||||
console.log(`[Z.AI] Config loaded from file, enabled: ${parsed.enabled}`)
|
console.log(`[Z.AI] Config loaded from file, enabled: ${parsed.enabled}`)
|
||||||
return parsed
|
return parsed
|
||||||
@@ -365,24 +369,27 @@ function getZAIConfig(): ZAIConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateZAIConfig(config: Partial<ZAIConfig>): void {
|
function updateZAIConfig(config: Partial<ZAIConfig>, userId?: string | null): void {
|
||||||
|
const configFile = getConfigFile(userId)
|
||||||
|
const configDir = getUserIntegrationsDir(userId || undefined)
|
||||||
|
|
||||||
// Ensure directory exists with proper error handling
|
// Ensure directory exists with proper error handling
|
||||||
try {
|
try {
|
||||||
if (!existsSync(CONFIG_DIR)) {
|
if (!existsSync(configDir)) {
|
||||||
console.log(`[Z.AI] Creating config directory: ${CONFIG_DIR}`)
|
console.log(`[Z.AI] Creating config directory: ${configDir}`)
|
||||||
mkdirSync(CONFIG_DIR, { recursive: true })
|
mkdirSync(configDir, { recursive: true })
|
||||||
}
|
}
|
||||||
} catch (mkdirError) {
|
} catch (mkdirError) {
|
||||||
console.error(`[Z.AI] Failed to create config directory:`, mkdirError)
|
console.error(`[Z.AI] Failed to create config directory:`, mkdirError)
|
||||||
throw new Error(`Failed to create config directory: ${mkdirError}`)
|
throw new Error(`Failed to create config directory: ${mkdirError}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const current = getZAIConfig()
|
const current = getZAIConfig(userId)
|
||||||
const updated = { ...current, ...config }
|
const updated = { ...current, ...config }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(`[Z.AI] Writing config to: ${CONFIG_FILE}`)
|
console.log(`[Z.AI] Writing config to: ${configFile} (user: ${userId || "default"})`)
|
||||||
writeFileSync(CONFIG_FILE, JSON.stringify(updated, null, 2), 'utf-8')
|
writeFileSync(configFile, JSON.stringify(updated, null, 2), 'utf-8')
|
||||||
console.log(`[Z.AI] Config saved successfully`)
|
console.log(`[Z.AI] Config saved successfully`)
|
||||||
} catch (writeError) {
|
} catch (writeError) {
|
||||||
console.error(`[Z.AI] Failed to write config file:`, writeError)
|
console.error(`[Z.AI] Failed to write config file:`, writeError)
|
||||||
|
|||||||
101
packages/server/src/user-context.ts
Normal file
101
packages/server/src/user-context.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
/**
|
||||||
|
* User Context Module
|
||||||
|
* Manages the active user context for per-user config isolation
|
||||||
|
*/
|
||||||
|
|
||||||
|
import path from "path"
|
||||||
|
import os from "os"
|
||||||
|
import { existsSync, mkdirSync } from "fs"
|
||||||
|
|
||||||
|
const CONFIG_ROOT = path.join(os.homedir(), ".config", "codenomad")
|
||||||
|
const USERS_ROOT = path.join(CONFIG_ROOT, "users")
|
||||||
|
|
||||||
|
// Active user ID (set by the main process or HTTP header)
|
||||||
|
let activeUserId: string | null = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the active user ID
|
||||||
|
*/
|
||||||
|
export function setActiveUserId(userId: string | null): void {
|
||||||
|
activeUserId = userId
|
||||||
|
console.log(`[UserContext] Active user set to: ${userId || "(none)"}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the active user ID
|
||||||
|
*/
|
||||||
|
export function getActiveUserId(): string | null {
|
||||||
|
return activeUserId
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the data root for a specific user
|
||||||
|
* Falls back to global config if no user is set
|
||||||
|
*/
|
||||||
|
export function getUserDataRoot(userId?: string): string {
|
||||||
|
const effectiveUserId = userId || activeUserId
|
||||||
|
|
||||||
|
if (effectiveUserId) {
|
||||||
|
const userDir = path.join(USERS_ROOT, effectiveUserId)
|
||||||
|
return userDir
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prioritize environment variable if set (from Electron)
|
||||||
|
const override = process.env.CODENOMAD_USER_DIR
|
||||||
|
if (override && override.trim().length > 0) {
|
||||||
|
return path.resolve(override)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to global config root
|
||||||
|
return CONFIG_ROOT
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the integrations directory for the current or specified user
|
||||||
|
*/
|
||||||
|
export function getUserIntegrationsDir(userId?: string): string {
|
||||||
|
const userRoot = getUserDataRoot(userId)
|
||||||
|
const integrationsDir = path.join(userRoot, "integrations")
|
||||||
|
|
||||||
|
// Ensure directory exists
|
||||||
|
if (!existsSync(integrationsDir)) {
|
||||||
|
try {
|
||||||
|
mkdirSync(integrationsDir, { recursive: true })
|
||||||
|
console.log(`[UserContext] Created integrations dir: ${integrationsDir}`)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`[UserContext] Failed to create integrations dir:`, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return integrationsDir
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the instances directory for the current or specified user
|
||||||
|
*/
|
||||||
|
export function getUserInstancesDir(userId?: string): string {
|
||||||
|
const userRoot = getUserDataRoot(userId)
|
||||||
|
return path.join(userRoot, "instances")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the config file path for a specific integration
|
||||||
|
*/
|
||||||
|
export function getIntegrationConfigPath(integrationId: string, userId?: string): string {
|
||||||
|
const integrationsDir = getUserIntegrationsDir(userId)
|
||||||
|
return path.join(integrationsDir, `${integrationId}-config.json`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract user ID from request headers
|
||||||
|
*/
|
||||||
|
export function getUserIdFromRequest(request: { headers?: Record<string, string | string[] | undefined> }): string | null {
|
||||||
|
const header = request.headers?.["x-user-id"]
|
||||||
|
if (typeof header === "string" && header.length > 0) {
|
||||||
|
return header
|
||||||
|
}
|
||||||
|
if (Array.isArray(header) && header.length > 0) {
|
||||||
|
return header[0]
|
||||||
|
}
|
||||||
|
return activeUserId
|
||||||
|
}
|
||||||
@@ -1,14 +1,8 @@
|
|||||||
import os from "os"
|
import { getUserDataRoot as getRoot, getUserInstancesDir as getInstances, getUserIntegrationsDir as getIntegrations } from "./user-context"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
|
||||||
const DEFAULT_ROOT = path.join(os.homedir(), ".config", "codenomad")
|
|
||||||
|
|
||||||
export function getUserDataRoot(): string {
|
export function getUserDataRoot(): string {
|
||||||
const override = process.env.CODENOMAD_USER_DIR
|
return getRoot()
|
||||||
if (override && override.trim().length > 0) {
|
|
||||||
return path.resolve(override)
|
|
||||||
}
|
|
||||||
return DEFAULT_ROOT
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getUserConfigPath(): string {
|
export function getUserConfigPath(): string {
|
||||||
@@ -16,11 +10,11 @@ export function getUserConfigPath(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getUserInstancesDir(): string {
|
export function getUserInstancesDir(): string {
|
||||||
return path.join(getUserDataRoot(), "instances")
|
return getInstances()
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getUserIntegrationsDir(): string {
|
export function getUserIntegrationsDir(): string {
|
||||||
return path.join(getUserDataRoot(), "integrations")
|
return getIntegrations()
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getOpencodeWorkspacesRoot(): string {
|
export function getOpencodeWorkspacesRoot(): string {
|
||||||
|
|||||||
@@ -100,6 +100,11 @@ const App: Component = () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
// Initialize user context from Electron IPC
|
||||||
|
import("./lib/user-context").then(({ initializeUserContext }) => {
|
||||||
|
initializeUserContext()
|
||||||
|
})
|
||||||
|
|
||||||
updateInstanceTabBarHeight()
|
updateInstanceTabBarHeight()
|
||||||
const handleResize = () => updateInstanceTabBarHeight()
|
const handleResize = () => updateInstanceTabBarHeight()
|
||||||
window.addEventListener("resize", handleResize)
|
window.addEventListener("resize", handleResize)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Component, createSignal, onMount, For, Show, createEffect, on } from "solid-js"
|
import { Component, createSignal, onMount, For, Show, createEffect, on } from "solid-js"
|
||||||
import { CheckCircle, XCircle, Loader, RefreshCw, Settings, AlertTriangle } from "lucide-solid"
|
import { CheckCircle, XCircle, Loader, RefreshCw, Settings, AlertTriangle } from "lucide-solid"
|
||||||
|
import { userFetch } from "../../lib/user-context"
|
||||||
|
|
||||||
interface ApiStatus {
|
interface ApiStatus {
|
||||||
id: string
|
id: string
|
||||||
@@ -28,7 +29,7 @@ const API_CHECKS: ApiStatusCheck[] = [
|
|||||||
checkEnabled: async () => true, // Always available
|
checkEnabled: async () => true, // Always available
|
||||||
testConnection: async () => {
|
testConnection: async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/opencode-zen/test")
|
const res = await userFetch("/api/opencode-zen/test")
|
||||||
if (!res.ok) return false
|
if (!res.ok) return false
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
return data.connected === true
|
return data.connected === true
|
||||||
@@ -43,7 +44,7 @@ const API_CHECKS: ApiStatusCheck[] = [
|
|||||||
icon: "🦙",
|
icon: "🦙",
|
||||||
checkEnabled: async () => {
|
checkEnabled: async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/ollama/config")
|
const res = await userFetch("/api/ollama/config")
|
||||||
if (!res.ok) return false
|
if (!res.ok) return false
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
return data.config?.enabled === true
|
return data.config?.enabled === true
|
||||||
@@ -53,7 +54,7 @@ const API_CHECKS: ApiStatusCheck[] = [
|
|||||||
},
|
},
|
||||||
testConnection: async () => {
|
testConnection: async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/ollama/test", { method: "POST" })
|
const res = await userFetch("/api/ollama/test", { method: "POST" })
|
||||||
if (!res.ok) return false
|
if (!res.ok) return false
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
return data.connected === true
|
return data.connected === true
|
||||||
@@ -68,7 +69,7 @@ const API_CHECKS: ApiStatusCheck[] = [
|
|||||||
icon: "🧠",
|
icon: "🧠",
|
||||||
checkEnabled: async () => {
|
checkEnabled: async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/zai/config")
|
const res = await userFetch("/api/zai/config")
|
||||||
if (!res.ok) return false
|
if (!res.ok) return false
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
return data.config?.enabled === true
|
return data.config?.enabled === true
|
||||||
@@ -78,7 +79,7 @@ const API_CHECKS: ApiStatusCheck[] = [
|
|||||||
},
|
},
|
||||||
testConnection: async () => {
|
testConnection: async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/zai/test", { method: "POST" })
|
const res = await userFetch("/api/zai/test", { method: "POST" })
|
||||||
if (!res.ok) return false
|
if (!res.ok) return false
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
return data.connected === true
|
return data.connected === true
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Button } from '@suid/material'
|
|||||||
import { Cloud, CheckCircle, XCircle, Loader } from 'lucide-solid'
|
import { Cloud, CheckCircle, XCircle, Loader } from 'lucide-solid'
|
||||||
import { instances } from '../../stores/instances'
|
import { instances } from '../../stores/instances'
|
||||||
import { fetchProviders } from '../../stores/session-api'
|
import { fetchProviders } from '../../stores/session-api'
|
||||||
|
import { userFetch } from '../../lib/user-context'
|
||||||
|
|
||||||
interface OllamaCloudConfig {
|
interface OllamaCloudConfig {
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
@@ -34,7 +35,7 @@ const OllamaCloudSettings: Component = () => {
|
|||||||
// Load config on mount
|
// Load config on mount
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/ollama/config')
|
const response = await userFetch('/api/ollama/config')
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
const maskedKey = typeof data.config?.apiKey === "string" && /^\*+$/.test(data.config.apiKey)
|
const maskedKey = typeof data.config?.apiKey === "string" && /^\*+$/.test(data.config.apiKey)
|
||||||
@@ -62,7 +63,7 @@ const OllamaCloudSettings: Component = () => {
|
|||||||
delete payload.apiKey
|
delete payload.apiKey
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch('/api/ollama/config', {
|
const response = await userFetch('/api/ollama/config', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(payload)
|
body: JSON.stringify(payload)
|
||||||
@@ -101,7 +102,7 @@ const OllamaCloudSettings: Component = () => {
|
|||||||
setConnectionStatus('testing')
|
setConnectionStatus('testing')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/ollama/test', {
|
const response = await userFetch('/api/ollama/test', {
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -140,7 +141,7 @@ const OllamaCloudSettings: Component = () => {
|
|||||||
const loadModels = async () => {
|
const loadModels = async () => {
|
||||||
setIsLoadingModels(true)
|
setIsLoadingModels(true)
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/ollama/models')
|
const response = await userFetch('/api/ollama/models')
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
// Handle different response formats
|
// Handle different response formats
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Component, createSignal, onMount, Show } from 'solid-js'
|
|||||||
import toast from 'solid-toast'
|
import toast from 'solid-toast'
|
||||||
import { Button } from '@suid/material'
|
import { Button } from '@suid/material'
|
||||||
import { Cpu, CheckCircle, XCircle, Loader, Key, ExternalLink } from 'lucide-solid'
|
import { Cpu, CheckCircle, XCircle, Loader, Key, ExternalLink } from 'lucide-solid'
|
||||||
|
import { userFetch } from '../../lib/user-context'
|
||||||
|
|
||||||
interface ZAIConfig {
|
interface ZAIConfig {
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
@@ -19,7 +20,7 @@ const ZAISettings: Component = () => {
|
|||||||
// Load config on mount
|
// Load config on mount
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/zai/config')
|
const response = await userFetch('/api/zai/config')
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
setConfig(data.config)
|
setConfig(data.config)
|
||||||
@@ -37,7 +38,7 @@ const ZAISettings: Component = () => {
|
|||||||
const saveConfig = async () => {
|
const saveConfig = async () => {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/zai/config', {
|
const response = await userFetch('/api/zai/config', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(config())
|
body: JSON.stringify(config())
|
||||||
@@ -66,7 +67,7 @@ const ZAISettings: Component = () => {
|
|||||||
setConnectionStatus('testing')
|
setConnectionStatus('testing')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/zai/test', {
|
const response = await userFetch('/api/zai/test', {
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -104,7 +105,7 @@ const ZAISettings: Component = () => {
|
|||||||
|
|
||||||
const loadModels = async () => {
|
const loadModels = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/zai/models')
|
const response = await userFetch('/api/zai/models')
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
setModels(data.models.map((m: any) => m.name))
|
setModels(data.models.map((m: any) => m.name))
|
||||||
|
|||||||
96
packages/ui/src/lib/user-context.ts
Normal file
96
packages/ui/src/lib/user-context.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
/**
|
||||||
|
* User Context utilities for frontend
|
||||||
|
* Handles active user ID and passes it in API requests
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Storage key for active user
|
||||||
|
const ACTIVE_USER_KEY = "codenomad_active_user_id"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the active user ID
|
||||||
|
*/
|
||||||
|
export function setActiveUserId(userId: string | null): void {
|
||||||
|
if (userId) {
|
||||||
|
localStorage.setItem(ACTIVE_USER_KEY, userId)
|
||||||
|
console.log(`[UserContext] Active user set to: ${userId}`)
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem(ACTIVE_USER_KEY)
|
||||||
|
console.log(`[UserContext] Active user cleared`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the active user ID
|
||||||
|
*/
|
||||||
|
export function getActiveUserId(): string | null {
|
||||||
|
return localStorage.getItem(ACTIVE_USER_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get headers with user ID for API requests
|
||||||
|
*/
|
||||||
|
export function getUserHeaders(): Record<string, string> {
|
||||||
|
const userId = getActiveUserId()
|
||||||
|
if (userId) {
|
||||||
|
return { "X-User-Id": userId }
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create fetch options with user headers
|
||||||
|
*/
|
||||||
|
export function withUserHeaders(options: RequestInit = {}): RequestInit {
|
||||||
|
const userHeaders = getUserHeaders()
|
||||||
|
return {
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
...options.headers,
|
||||||
|
...userHeaders,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch wrapper that automatically includes user headers
|
||||||
|
*/
|
||||||
|
export async function userFetch(url: string, options: RequestInit = {}): Promise<Response> {
|
||||||
|
return fetch(url, withUserHeaders(options))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize user context from Electron IPC
|
||||||
|
* Call this on app startup
|
||||||
|
*/
|
||||||
|
export async function initializeUserContext(): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Check if we're in Electron environment
|
||||||
|
const ipcRenderer = (window as any).electron?.ipcRenderer
|
||||||
|
if (ipcRenderer) {
|
||||||
|
const activeUser = await ipcRenderer.invoke("users:active")
|
||||||
|
if (activeUser?.id) {
|
||||||
|
setActiveUserId(activeUser.id)
|
||||||
|
console.log(`[UserContext] Initialized with user: ${activeUser.id} (${activeUser.name})`)
|
||||||
|
} else {
|
||||||
|
console.log(`[UserContext] No active user from IPC`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Web mode - try to get from localStorage or use default
|
||||||
|
const existingId = getActiveUserId()
|
||||||
|
if (existingId) {
|
||||||
|
console.log(`[UserContext] Using cached user ID: ${existingId}`)
|
||||||
|
} else {
|
||||||
|
// Set a default user ID for web mode
|
||||||
|
const defaultUserId = "default"
|
||||||
|
setActiveUserId(defaultUserId)
|
||||||
|
console.log(`[UserContext] Web mode - using default user ID`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[UserContext] Failed to initialize:`, error)
|
||||||
|
// Fall back to default
|
||||||
|
if (!getActiveUserId()) {
|
||||||
|
setActiveUserId("default")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user