Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/add-lock-app-functionality.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"think-app": patch
---

Add lock app functionality to settings page
73 changes: 72 additions & 1 deletion app/src/pages/SettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -567,6 +585,26 @@ export default function SettingsPage({ onNameChange }: SettingsPageProps) {
</Button>
</CardContent>
</Card>

{/* Security Section */}
<Card>
<CardHeader>
<CardTitle className="text-base">Security</CardTitle>
</CardHeader>
<CardContent>
<Button
variant="outline"
onClick={() => setShowLockDialog(true)}
className="w-full"
>
<LogOut className="h-4 w-4 mr-2" />
Lock App
</Button>
<p className="text-xs text-muted-foreground mt-2 text-center">
Lock the app and require password to access
</p>
</CardContent>
</Card>
</div>

{/* Settings change warning dialog - rendered via portal for full-screen overlay */}
Expand Down Expand Up @@ -685,6 +723,39 @@ export default function SettingsPage({ onNameChange }: SettingsPageProps) {
</div>,
document.body
)}

{/* Lock app confirmation dialog */}
{showLockDialog && createPortal(
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-[100]">
<div className="bg-background border rounded-lg shadow-xl max-w-md w-full mx-4 p-6">
<div className="flex items-start gap-3 mb-4">
<LogOut className="h-6 w-6 text-muted-foreground flex-shrink-0 mt-0.5" />
<div>
<h3 className="font-semibold text-lg">Lock App?</h3>
<p className="text-sm text-muted-foreground mt-1">
You'll need to enter your password to unlock.
</p>
</div>
</div>

<div className="flex flex-col gap-2">
<Button onClick={handleLock} disabled={locking} className="w-full">
{locking && <Loader2 className="h-4 w-4 animate-spin mr-2" />}
Lock
</Button>
<Button
variant="ghost"
onClick={() => setShowLockDialog(false)}
disabled={locking}
className="w-full"
>
Cancel
</Button>
</div>
</div>
</div>,
document.body
)}
</>
);
}
3 changes: 2 additions & 1 deletion backend/app/db/__init__.py
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -28,6 +28,7 @@
"init_db",
"is_db_initialized",
"db_exists",
"reset_db_connection",
"DB_PATH",
"create_memory",
"get_memories",
Expand Down
10 changes: 10 additions & 0 deletions backend/app/db/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
2 changes: 1 addition & 1 deletion backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
9 changes: 8 additions & 1 deletion backend/app/routes/auth.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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."""
Expand Down