feat: Add OIDC authentication with Authentik integration

- Add OIDC login flow with Authentik provider
- Implement session-based auth with Redis store
- Add avatar display from OIDC claims
- Fix input field performance with react-textarea-autosize
- Stabilize callbacks to prevent unnecessary re-renders
- Fix history loading to skip empty session files
- Add 2-row default height for input textarea

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-18 06:07:22 +01:00
parent cfee1711dc
commit 1186cb1b5e
23 changed files with 2884 additions and 87 deletions

View File

@@ -1,7 +1,12 @@
import { createContext, useContext, useState, useCallback, useRef, useEffect, useMemo } from 'react';
const WS_URL = import.meta.env.VITE_WS_URL || 'ws://100.105.142.13:3001';
const API_URL = import.meta.env.VITE_API_URL || 'http://100.105.142.13:3001';
// Build WebSocket URL from current location
function getWsUrl() {
if (import.meta.env.VITE_WS_URL) return import.meta.env.VITE_WS_URL;
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
return `${protocol}//${window.location.host}/ws`;
}
const SESSIONS_STORAGE_KEY = 'claude-webui-sessions';
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
@@ -64,6 +69,10 @@ export function SessionProvider({ children }) {
// Current assistant message refs keyed by session ID
const currentAssistantMessages = useRef({});
// Ref to current sessions state (for stable callbacks)
const sessionsRef = useRef(sessions);
sessionsRef.current = sessions;
// Track if initial load is done (for auto-connecting restored sessions)
const initialLoadDone = useRef(false);
const sessionsToConnect = useRef([]);
@@ -263,6 +272,26 @@ export function SessionProvider({ children }) {
for (const toolMsg of toolUseBlocks) {
addMessage(sessionId, toolMsg);
}
// Extract usage stats from message if present
const usage = message.usage;
if (usage) {
const inputTokens = (usage.input_tokens || 0) + (usage.cache_read_input_tokens || 0);
const outputTokens = usage.output_tokens || 0;
const cacheReadTokens = usage.cache_read_input_tokens || 0;
const cacheCreationTokens = usage.cache_creation_input_tokens || 0;
updateSession(sessionId, (session) => ({
stats: {
...(session.stats || {}),
inputTokens: (session.stats?.inputTokens || 0) + inputTokens,
outputTokens: (session.stats?.outputTokens || 0) + outputTokens,
cacheReadTokens: (session.stats?.cacheReadTokens || 0) + cacheReadTokens,
cacheCreationTokens: (session.stats?.cacheCreationTokens || 0) + cacheCreationTokens,
numTurns: (session.stats?.numTurns || 0) + 1,
},
}));
}
break;
}
@@ -342,6 +371,7 @@ export function SessionProvider({ children }) {
case 'result': {
// Final result with stats
console.log(`[${sessionId}] Result event:`, JSON.stringify(event, null, 2));
const defaultStats = { totalCost: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0, numTurns: 0 };
updateSession(sessionId, (session) => ({
isProcessing: false,
@@ -542,7 +572,7 @@ export function SessionProvider({ children }) {
const connectSession = useCallback((sessionId) => {
if (wsRefs.current[sessionId]?.readyState === WebSocket.OPEN) return;
const ws = new WebSocket(WS_URL);
const ws = new WebSocket(getWsUrl());
wsRefs.current[sessionId] = ws;
ws.onopen = () => {
@@ -643,7 +673,7 @@ export function SessionProvider({ children }) {
// Start Claude session
const startClaudeSession = useCallback(async (sessionId) => {
const session = sessions[sessionId];
const session = sessionsRef.current[sessionId];
if (!session) return;
const ws = wsRefs.current[sessionId];
@@ -659,7 +689,8 @@ export function SessionProvider({ children }) {
if (session.resumeOnStart) {
try {
const res = await fetch(
`${API_URL}/api/history/${encodeURIComponent(session.project)}?host=${session.host}`
`/api/history/${encodeURIComponent(session.project)}?host=${session.host}`,
{ credentials: 'include' }
);
const data = await res.json();
if (data.messages && Array.isArray(data.messages)) {
@@ -680,7 +711,7 @@ export function SessionProvider({ children }) {
host: session.host,
}));
}
}, [sessions, connectSession, updateSession]);
}, [connectSession]);
// Stop Claude session
const stopClaudeSession = useCallback((sessionId) => {
@@ -693,7 +724,7 @@ export function SessionProvider({ children }) {
// Send message to session
const sendMessage = useCallback(async (sessionId, message, attachments = []) => {
const session = sessions[sessionId];
const session = sessionsRef.current[sessionId];
if (!session?.active) return;
const ws = wsRefs.current[sessionId];
@@ -708,9 +739,10 @@ export function SessionProvider({ children }) {
}
try {
const res = await fetch(`${API_URL}/api/upload/${session.claudeSessionId}`, {
const res = await fetch(`/api/upload/${session.claudeSessionId}`, {
method: 'POST',
body: formData,
credentials: 'include',
});
const data = await res.json();
uploadedFiles = data.files || [];
@@ -743,7 +775,7 @@ export function SessionProvider({ children }) {
type: 'user_message',
message: finalMessage,
}));
}, [sessions, updateSession, addMessage]);
}, [updateSession, addMessage]);
// Stop generation
const stopGeneration = useCallback((sessionId) => {