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."""