diff --git a/apps/desktop/app/(app)/settings/page.tsx b/apps/desktop/app/(app)/settings/page.tsx index 2ee7013d..bd9b18c2 100644 --- a/apps/desktop/app/(app)/settings/page.tsx +++ b/apps/desktop/app/(app)/settings/page.tsx @@ -8,8 +8,11 @@ import { CardHeader, CardTitle, } from "@ui/components/card" +import { Button } from "@ui/components/button" import Link from "next/link" +import { useRouter } from "next/navigation" import { useEffect, useState } from "react" +import { clearSession, getSession, type AuthSession } from "@/lib/auth" type AppInfo = { name: string @@ -18,18 +21,47 @@ type AppInfo = { } export default function SettingsPage() { + const router = useRouter() const [info, setInfo] = useState(null) + const [session, setSession] = useState(null) useEffect(() => { invoke("app_info") .then(setInfo) .catch(() => setInfo(null)) + getSession() + .then(setSession) + .catch(() => setSession(null)) }, []) + async function signOut() { + await clearSession() + router.replace("/login") + } + return (

Settings

+ + + Account + + Keychain-backed desktop session for API requests. + + + + + + + + + About diff --git a/apps/desktop/app/layout.tsx b/apps/desktop/app/layout.tsx index a8c52ddb..98cdf915 100644 --- a/apps/desktop/app/layout.tsx +++ b/apps/desktop/app/layout.tsx @@ -17,7 +17,12 @@ export const metadata: Metadata = { export default function RootLayout({ children }: { children: ReactNode }) { return ( - + {children} diff --git a/apps/desktop/app/login/page.tsx b/apps/desktop/app/login/page.tsx index 349baea1..34a9c57a 100644 --- a/apps/desktop/app/login/page.tsx +++ b/apps/desktop/app/login/page.tsx @@ -1,29 +1,74 @@ "use client" import { Button } from "@ui/components/button" +import { Input } from "@ui/components/input" +import { Label } from "@ui/components/label" import { useRouter } from "next/navigation" +import { type FormEvent, useState } from "react" +import { desktopDevAuthEnabled, storeToken } from "@/lib/auth" -// Phase 1 stub. Phase 3 wires "Sign in" to the system-browser OAuth flow -// (invoke('auth_begin_oauth')) + the session-token deep-link handoff. export default function LoginPage() { const router = useRouter() + const [token, setToken] = useState("") + const [error, setError] = useState(null) + const [isSubmitting, setIsSubmitting] = useState(false) + + async function onSubmit(event: FormEvent) { + event.preventDefault() + setError(null) + setIsSubmitting(true) + + try { + await storeToken(token) + router.replace("/") + } catch (err) { + setError(err instanceof Error ? err.message : "Could not sign in") + } finally { + setIsSubmitting(false) + } + } return (
{/* Keep the frameless window draggable from the top edge. */}
-
+

Supermemory

Sign in to access your memories.

-

Browser-based sign-in arrives in a later phase.

+ {desktopDevAuthEnabled ? ( +
+
+ + setToken(event.target.value)} + placeholder="sm_..." + type="password" + autoComplete="off" + /> +
+ {error ? ( +

{error}

+ ) : null} + +
+ ) : null}
diff --git a/apps/desktop/components/auth-guard.tsx b/apps/desktop/components/auth-guard.tsx index a7d43269..206a5229 100644 --- a/apps/desktop/components/auth-guard.tsx +++ b/apps/desktop/components/auth-guard.tsx @@ -1,16 +1,31 @@ "use client" import { useRouter } from "next/navigation" -import { type ReactNode, useEffect } from "react" +import { type ReactNode, useEffect, useState } from "react" +import { getSession } from "@/lib/auth" -// Phase 1 stub. Phase 2/3 replaces `useAuthStatus` with a real check of the -// keychain-backed token (invoke('auth_get_token')) and returns "unauthenticated" -// when it is missing. The state machine + redirect are wired now so later phases -// only swap the source of truth, not the guard's shape. type AuthStatus = "loading" | "authenticated" | "unauthenticated" function useAuthStatus(): AuthStatus { - return "authenticated" + const [status, setStatus] = useState("loading") + + useEffect(() => { + let cancelled = false + + getSession() + .then(() => { + if (!cancelled) setStatus("authenticated") + }) + .catch(() => { + if (!cancelled) setStatus("unauthenticated") + }) + + return () => { + cancelled = true + } + }, []) + + return status } export function AuthGuard({ children }: { children: ReactNode }) { @@ -26,7 +41,7 @@ export function AuthGuard({ children }: { children: ReactNode }) { if (status !== "authenticated") { return (
- Loading… + Loading...
) } diff --git a/apps/desktop/lib/auth.ts b/apps/desktop/lib/auth.ts new file mode 100644 index 00000000..ef217119 --- /dev/null +++ b/apps/desktop/lib/auth.ts @@ -0,0 +1,29 @@ +"use client" + +import { invoke } from "@tauri-apps/api/core" + +export type AuthSession = { + userId: string + email?: string + name?: string + apiUrl: string +} + +export const desktopDevAuthEnabled = process.env.NEXT_PUBLIC_DESKTOP_DEV === "1" + +export async function getStoredToken() { + return invoke("auth_get_token") +} + +export async function getSession() { + return invoke("auth_whoami") +} + +export async function storeToken(token: string) { + await invoke("auth_store_token", { token }) + return getSession() +} + +export async function clearSession() { + await invoke("auth_clear") +} diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 59dd4fbe..021e70a7 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev --port 3001", + "dev": "NEXT_PUBLIC_DESKTOP_DEV=1 next dev --port 3001", "build": "next build", "check-types": "tsc --noEmit", "tauri": "tauri"