Skip to content

Commit 159b7c3

Browse files
author
Antonio Maiolo
authored
feat(app): add lock app functionality to settings (#63)
1 parent d0a0da6 commit 159b7c3

6 files changed

Lines changed: 98 additions & 4 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"think-app": patch
3+
---
4+
5+
Add lock app functionality to settings page

app/src/pages/SettingsPage.tsx

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
44
import { Button } from "@/components/ui/button";
55
import { Input } from "@/components/ui/input";
66
import { Progress } from "@/components/ui/progress";
7-
import { Check, Circle, Loader2, Monitor, Sun, Moon, AlertTriangle } from "lucide-react";
7+
import { Check, Circle, Loader2, Monitor, Sun, Moon, AlertTriangle, LogOut } from "lucide-react";
88
import { Theme, setTheme, getTheme } from "@/hooks/useSystemTheme";
99
import { apiFetch } from "@/lib/api";
1010
import { ModelSelector } from "@/components/ModelSelector";
@@ -67,6 +67,10 @@ export default function SettingsPage({ onNameChange }: SettingsPageProps) {
6767
const [staleEmbeddingsCount, setStaleEmbeddingsCount] = useState(0);
6868
const [showReembedDialog, setShowReembedDialog] = useState(false);
6969

70+
// Lock app state
71+
const [showLockDialog, setShowLockDialog] = useState(false);
72+
const [locking, setLocking] = useState(false);
73+
7074
// Use the reembed job hook for background re-embedding
7175
const reembedJob = useReembedJob({
7276
onComplete: (processed, failed) => {
@@ -309,6 +313,20 @@ export default function SettingsPage({ onNameChange }: SettingsPageProps) {
309313
setTheme(newTheme);
310314
};
311315

316+
const handleLock = async () => {
317+
setLocking(true);
318+
try {
319+
const res = await apiFetch("/api/auth/logout", { method: "POST" });
320+
if (res.ok) {
321+
window.location.reload();
322+
}
323+
} catch (err) {
324+
console.error("Failed to lock:", err);
325+
} finally {
326+
setLocking(false);
327+
}
328+
};
329+
312330
// Progress from the job hook
313331
const progressPercent = reembedJob.progress;
314332

@@ -567,6 +585,26 @@ export default function SettingsPage({ onNameChange }: SettingsPageProps) {
567585
</Button>
568586
</CardContent>
569587
</Card>
588+
589+
{/* Security Section */}
590+
<Card>
591+
<CardHeader>
592+
<CardTitle className="text-base">Security</CardTitle>
593+
</CardHeader>
594+
<CardContent>
595+
<Button
596+
variant="outline"
597+
onClick={() => setShowLockDialog(true)}
598+
className="w-full"
599+
>
600+
<LogOut className="h-4 w-4 mr-2" />
601+
Lock App
602+
</Button>
603+
<p className="text-xs text-muted-foreground mt-2 text-center">
604+
Lock the app and require password to access
605+
</p>
606+
</CardContent>
607+
</Card>
570608
</div>
571609

572610
{/* Settings change warning dialog - rendered via portal for full-screen overlay */}
@@ -685,6 +723,39 @@ export default function SettingsPage({ onNameChange }: SettingsPageProps) {
685723
</div>,
686724
document.body
687725
)}
726+
727+
{/* Lock app confirmation dialog */}
728+
{showLockDialog && createPortal(
729+
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-[100]">
730+
<div className="bg-background border rounded-lg shadow-xl max-w-md w-full mx-4 p-6">
731+
<div className="flex items-start gap-3 mb-4">
732+
<LogOut className="h-6 w-6 text-muted-foreground flex-shrink-0 mt-0.5" />
733+
<div>
734+
<h3 className="font-semibold text-lg">Lock App?</h3>
735+
<p className="text-sm text-muted-foreground mt-1">
736+
You'll need to enter your password to unlock.
737+
</p>
738+
</div>
739+
</div>
740+
741+
<div className="flex flex-col gap-2">
742+
<Button onClick={handleLock} disabled={locking} className="w-full">
743+
{locking && <Loader2 className="h-4 w-4 animate-spin mr-2" />}
744+
Lock
745+
</Button>
746+
<Button
747+
variant="ghost"
748+
onClick={() => setShowLockDialog(false)}
749+
disabled={locking}
750+
className="w-full"
751+
>
752+
Cancel
753+
</Button>
754+
</div>
755+
</div>
756+
</div>,
757+
document.body
758+
)}
688759
</>
689760
);
690761
}

backend/app/db/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .core import init_db, is_db_initialized, db_exists, DB_PATH
1+
from .core import init_db, is_db_initialized, db_exists, reset_db_connection, DB_PATH
22
from .crud import (
33
create_memory,
44
get_memories,
@@ -28,6 +28,7 @@
2828
"init_db",
2929
"is_db_initialized",
3030
"db_exists",
31+
"reset_db_connection",
3132
"DB_PATH",
3233
"create_memory",
3334
"get_memories",

backend/app/db/core.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,16 @@ def is_db_initialized() -> bool:
125125
return _engine is not None
126126

127127

128+
def reset_db_connection():
129+
"""Reset database connection and clear encryption key (logout)."""
130+
global _engine, _session_maker, _db_key
131+
if _engine is not None:
132+
_engine.dispose()
133+
_engine = None
134+
_session_maker = None
135+
_db_key = None
136+
137+
128138
def db_exists() -> bool:
129139
"""Check if the database file exists (password was set)."""
130140
return DB_PATH.exists()

backend/app/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ async def lifespan(app: FastAPI):
6161
)
6262

6363
# Paths that don't require unlock
64-
PUBLIC_PATHS = {"/health", "/api/auth/status", "/api/auth/setup", "/api/auth/unlock"}
64+
PUBLIC_PATHS = {"/health", "/api/auth/status", "/api/auth/setup", "/api/auth/unlock", "/api/auth/logout"}
6565

6666

6767
@app.middleware("http")

backend/app/routes/auth.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from fastapi import APIRouter, HTTPException
22

33
from ..config import reload_settings
4-
from ..db import init_db, is_db_initialized, db_exists
4+
from ..db import init_db, is_db_initialized, db_exists, reset_db_connection
55
from ..services.secrets import derive_db_key, set_api_key, get_api_key, delete_api_key
66
from ..schemas import SetPasswordRequest, UnlockRequest, ApiKeyRequest
77

@@ -47,6 +47,13 @@ async def unlock(request: UnlockRequest):
4747
return {"success": True}
4848

4949

50+
@router.post("/auth/logout")
51+
async def logout():
52+
"""Lock the database (logout)."""
53+
reset_db_connection()
54+
return {"success": True}
55+
56+
5057
@router.post("/settings/api-key")
5158
async def save_api_key_endpoint(request: ApiKeyRequest):
5259
"""Save an API key to the encrypted database."""

0 commit comments

Comments
 (0)