From 718c7f50992656a97434ce5041e716145ec3a5c8 Mon Sep 17 00:00:00 2001
From: blackhao <13851610112@163.com>
Date: Wed, 10 Dec 2025 21:22:14 -0600
Subject: set keys
---
backend/app/auth/models.py | 5 +-
backend/app/auth/routes.py | 47 ++++++++
frontend/index.html | 4 +-
frontend/public/webicon.png | Bin 0 -> 77075 bytes
frontend/src/components/LeftSidebar.tsx | 193 +++++++++++++++++++++++++++++++-
5 files changed, 240 insertions(+), 9 deletions(-)
create mode 100644 frontend/public/webicon.png
diff --git a/backend/app/auth/models.py b/backend/app/auth/models.py
index 76c33fa..8477ba2 100644
--- a/backend/app/auth/models.py
+++ b/backend/app/auth/models.py
@@ -1,5 +1,5 @@
import os
-from sqlalchemy import Column, Integer, String, DateTime, create_engine
+from sqlalchemy import Column, Integer, String, DateTime, Text, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from datetime import datetime
@@ -23,6 +23,9 @@ class User(Base):
hashed_password = Column(String(255), nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)
is_active = Column(Integer, default=1)
+ # API Keys (stored encrypted in production, plain for simplicity here)
+ openai_api_key = Column(Text, nullable=True)
+ gemini_api_key = Column(Text, nullable=True)
def init_db():
diff --git a/backend/app/auth/routes.py b/backend/app/auth/routes.py
index 7f07c2a..3c906b5 100644
--- a/backend/app/auth/routes.py
+++ b/backend/app/auth/routes.py
@@ -2,6 +2,7 @@ from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from typing import Optional
+from pydantic import BaseModel
from .models import User, get_db
from .utils import (
@@ -212,6 +213,52 @@ async def get_me(current_user: User = Depends(get_current_user)):
return current_user
+@router.get("/api-keys")
+async def get_api_keys(current_user: User = Depends(get_current_user)):
+ """
+ Get current user's API keys (masked for security).
+ """
+ def mask_key(key: str | None) -> str:
+ if not key:
+ return ""
+ if len(key) <= 8:
+ return "*" * len(key)
+ return key[:4] + "*" * (len(key) - 8) + key[-4:]
+
+ return {
+ "openai_api_key": mask_key(current_user.openai_api_key),
+ "gemini_api_key": mask_key(current_user.gemini_api_key),
+ "has_openai_key": bool(current_user.openai_api_key),
+ "has_gemini_key": bool(current_user.gemini_api_key),
+ }
+
+
+class ApiKeysUpdate(BaseModel):
+ openai_api_key: Optional[str] = None
+ gemini_api_key: Optional[str] = None
+
+
+@router.post("/api-keys")
+async def update_api_keys(
+ keys: ApiKeysUpdate,
+ current_user: User = Depends(get_current_user),
+ db: Session = Depends(get_db)
+):
+ """
+ Update current user's API keys.
+ Pass empty string to clear a key, or omit to keep unchanged.
+ """
+ if keys.openai_api_key is not None:
+ current_user.openai_api_key = keys.openai_api_key if keys.openai_api_key else None
+
+ if keys.gemini_api_key is not None:
+ current_user.gemini_api_key = keys.gemini_api_key if keys.gemini_api_key else None
+
+ db.commit()
+
+ return {"message": "API keys updated successfully"}
+
+
@router.post("/logout")
async def logout():
"""
diff --git a/frontend/index.html b/frontend/index.html
index 072a57e..a91e052 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -2,9 +2,9 @@
-
+
- frontend
+ ContextFlow
diff --git a/frontend/public/webicon.png b/frontend/public/webicon.png
new file mode 100644
index 0000000..0fc5a9a
Binary files /dev/null and b/frontend/public/webicon.png differ
diff --git a/frontend/src/components/LeftSidebar.tsx b/frontend/src/components/LeftSidebar.tsx
index 1a111bf..d929dcc 100644
--- a/frontend/src/components/LeftSidebar.tsx
+++ b/frontend/src/components/LeftSidebar.tsx
@@ -2,7 +2,7 @@ import React, { useEffect, useMemo, useRef, useState, useCallback } from 'react'
import { useReactFlow } from 'reactflow';
import {
Folder, FileText, Archive, ChevronLeft, ChevronRight, Trash2, MessageSquare,
- MoreVertical, Download, Upload, Plus, RefreshCw, Edit3, Loader2, LogOut, User
+ MoreVertical, Download, Upload, Plus, RefreshCw, Edit3, Loader2, LogOut, User, Settings, Key, X, Eye, EyeOff
} from 'lucide-react';
import useFlowStore, { type FSItem, type BlueprintDocument, type FileMeta } from '../store/flowStore';
import { useAuthStore } from '../store/authStore';
@@ -54,6 +54,60 @@ const LeftSidebar: React.FC = ({ isOpen, onToggle }) => {
const [openaiPurpose, setOpenaiPurpose] = useState('user_data');
const [fileSearch, setFileSearch] = useState('');
+ // User Settings Modal State
+ const [showUserSettings, setShowUserSettings] = useState(false);
+ const [openaiApiKey, setOpenaiApiKey] = useState('');
+ const [geminiApiKey, setGeminiApiKey] = useState('');
+ const [showOpenaiKey, setShowOpenaiKey] = useState(false);
+ const [showGeminiKey, setShowGeminiKey] = useState(false);
+ const [savingKeys, setSavingKeys] = useState(false);
+ const [keysMessage, setKeysMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
+ const { getAuthHeader } = useAuthStore();
+
+ // Load API keys when settings modal opens
+ useEffect(() => {
+ if (showUserSettings) {
+ fetch('/api/auth/api-keys', {
+ headers: { ...getAuthHeader() },
+ })
+ .then(res => res.json())
+ .then(data => {
+ setOpenaiApiKey(data.openai_api_key || '');
+ setGeminiApiKey(data.gemini_api_key || '');
+ })
+ .catch(() => {});
+ }
+ }, [showUserSettings, getAuthHeader]);
+
+ const handleSaveApiKeys = async () => {
+ setSavingKeys(true);
+ setKeysMessage(null);
+ try {
+ const res = await fetch('/api/auth/api-keys', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json', ...getAuthHeader() },
+ body: JSON.stringify({
+ openai_api_key: openaiApiKey.includes('*') ? undefined : openaiApiKey,
+ gemini_api_key: geminiApiKey.includes('*') ? undefined : geminiApiKey,
+ }),
+ });
+ if (res.ok) {
+ setKeysMessage({ type: 'success', text: 'API keys saved successfully!' });
+ // Reload masked keys
+ const data = await fetch('/api/auth/api-keys', {
+ headers: { ...getAuthHeader() },
+ }).then(r => r.json());
+ setOpenaiApiKey(data.openai_api_key || '');
+ setGeminiApiKey(data.gemini_api_key || '');
+ } else {
+ setKeysMessage({ type: 'error', text: 'Failed to save API keys' });
+ }
+ } catch {
+ setKeysMessage({ type: 'error', text: 'Network error' });
+ }
+ setSavingKeys(false);
+ };
+
const handleDragStart = (e: React.DragEvent, archiveId: string) => {
e.dataTransfer.setData('archiveId', archiveId);
e.dataTransfer.effectAllowed = 'copy';
@@ -459,15 +513,15 @@ const LeftSidebar: React.FC = ({ isOpen, onToggle }) => {
{user && (
)}
+ {/* User Settings Modal */}
+ {showUserSettings && (
+ setShowUserSettings(false)}>
+
e.stopPropagation()}
+ >
+ {/* Header */}
+
+
User Settings
+
+
+
+ {/* Content */}
+
+ {/* User Info */}
+
+
+
+
+
+
{user?.username}
+
{user?.email}
+
+
+
+ {/* API Keys Section */}
+
+
+ API Keys
+
+
+ {/* OpenAI API Key */}
+
+
+
+ setOpenaiApiKey(e.target.value)}
+ placeholder="sk-..."
+ className={`w-full px-3 py-2 pr-10 rounded-lg text-sm ${
+ isDark
+ ? 'bg-gray-700 border-gray-600 text-white placeholder-gray-500'
+ : 'bg-white border-gray-300 text-gray-900 placeholder-gray-400'
+ } border focus:outline-none focus:ring-2 focus:ring-blue-500`}
+ />
+
+
+
+
+ {/* Gemini API Key */}
+
+
+
+ setGeminiApiKey(e.target.value)}
+ placeholder="AI..."
+ className={`w-full px-3 py-2 pr-10 rounded-lg text-sm ${
+ isDark
+ ? 'bg-gray-700 border-gray-600 text-white placeholder-gray-500'
+ : 'bg-white border-gray-300 text-gray-900 placeholder-gray-400'
+ } border focus:outline-none focus:ring-2 focus:ring-blue-500`}
+ />
+
+
+
+
+ {/* Save Button */}
+
+
+ {/* Message */}
+ {keysMessage && (
+
+ {keysMessage.text}
+
+ )}
+
+
+
+ {/* Footer */}
+
+
+
+
+
+ )}
+
{/* Tabs */}