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 */}