diff --git a/.changeset/add-lock-app-functionality.md b/.changeset/add-lock-app-functionality.md new file mode 100644 index 0000000..004977d --- /dev/null +++ b/.changeset/add-lock-app-functionality.md @@ -0,0 +1,5 @@ +--- +"think-app": patch +--- + +Add lock app functionality to settings page diff --git a/app/src/pages/SettingsPage.tsx b/app/src/pages/SettingsPage.tsx index befa485..a5b85b2 100644 --- a/app/src/pages/SettingsPage.tsx +++ b/app/src/pages/SettingsPage.tsx @@ -4,7 +4,7 @@ import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Progress } from "@/components/ui/progress"; -import { Check, Circle, Loader2, Monitor, Sun, Moon, AlertTriangle } from "lucide-react"; +import { Check, Circle, Loader2, Monitor, Sun, Moon, AlertTriangle, LogOut } from "lucide-react"; import { Theme, setTheme, getTheme } from "@/hooks/useSystemTheme"; import { apiFetch } from "@/lib/api"; import { ModelSelector } from "@/components/ModelSelector"; @@ -67,6 +67,10 @@ export default function SettingsPage({ onNameChange }: SettingsPageProps) { const [staleEmbeddingsCount, setStaleEmbeddingsCount] = useState(0); const [showReembedDialog, setShowReembedDialog] = useState(false); + // Lock app state + const [showLockDialog, setShowLockDialog] = useState(false); + const [locking, setLocking] = useState(false); + // Use the reembed job hook for background re-embedding const reembedJob = useReembedJob({ onComplete: (processed, failed) => { @@ -309,6 +313,20 @@ export default function SettingsPage({ onNameChange }: SettingsPageProps) { setTheme(newTheme); }; + const handleLock = async () => { + setLocking(true); + try { + const res = await apiFetch("/api/auth/logout", { method: "POST" }); + if (res.ok) { + window.location.reload(); + } + } catch (err) { + console.error("Failed to lock:", err); + } finally { + setLocking(false); + } + }; + // Progress from the job hook const progressPercent = reembedJob.progress; @@ -567,6 +585,26 @@ export default function SettingsPage({ onNameChange }: SettingsPageProps) { + + {/* Security Section */} + + + Security + + + +

+ Lock the app and require password to access +

+
+
{/* Settings change warning dialog - rendered via portal for full-screen overlay */} @@ -685,6 +723,39 @@ export default function SettingsPage({ onNameChange }: SettingsPageProps) { , document.body )} + + {/* Lock app confirmation dialog */} + {showLockDialog && createPortal( +
+
+
+ +
+

Lock App?

+

+ You'll need to enter your password to unlock. +

+
+
+ +
+ + +
+
+
, + document.body + )} ); } diff --git a/backend/app/db/__init__.py b/backend/app/db/__init__.py index 5311cdd..b12fab1 100644 --- a/backend/app/db/__init__.py +++ b/backend/app/db/__init__.py @@ -1,4 +1,4 @@ -from .core import init_db, is_db_initialized, db_exists, DB_PATH +from .core import init_db, is_db_initialized, db_exists, reset_db_connection, DB_PATH from .crud import ( create_memory, get_memories, @@ -28,6 +28,7 @@ "init_db", "is_db_initialized", "db_exists", + "reset_db_connection", "DB_PATH", "create_memory", "get_memories", diff --git a/backend/app/db/core.py b/backend/app/db/core.py index b3a19ec..9fc3e95 100644 --- a/backend/app/db/core.py +++ b/backend/app/db/core.py @@ -125,6 +125,16 @@ def is_db_initialized() -> bool: return _engine is not None +def reset_db_connection(): + """Reset database connection and clear encryption key (logout).""" + global _engine, _session_maker, _db_key + if _engine is not None: + _engine.dispose() + _engine = None + _session_maker = None + _db_key = None + + def db_exists() -> bool: """Check if the database file exists (password was set).""" return DB_PATH.exists() diff --git a/backend/app/main.py b/backend/app/main.py index 37a1be8..a721446 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -61,7 +61,7 @@ async def lifespan(app: FastAPI): ) # Paths that don't require unlock -PUBLIC_PATHS = {"/health", "/api/auth/status", "/api/auth/setup", "/api/auth/unlock"} +PUBLIC_PATHS = {"/health", "/api/auth/status", "/api/auth/setup", "/api/auth/unlock", "/api/auth/logout"} @app.middleware("http") diff --git a/backend/app/routes/auth.py b/backend/app/routes/auth.py index 825b34a..4b0ede2 100644 --- a/backend/app/routes/auth.py +++ b/backend/app/routes/auth.py @@ -1,7 +1,7 @@ from fastapi import APIRouter, HTTPException from ..config import reload_settings -from ..db import init_db, is_db_initialized, db_exists +from ..db import init_db, is_db_initialized, db_exists, reset_db_connection from ..services.secrets import derive_db_key, set_api_key, get_api_key, delete_api_key from ..schemas import SetPasswordRequest, UnlockRequest, ApiKeyRequest @@ -47,6 +47,13 @@ async def unlock(request: UnlockRequest): return {"success": True} +@router.post("/auth/logout") +async def logout(): + """Lock the database (logout).""" + reset_db_connection() + return {"success": True} + + @router.post("/settings/api-key") async def save_api_key_endpoint(request: ApiKeyRequest): """Save an API key to the encrypted database."""