Wire desktop dev key login

This commit is contained in:
Sreeram Sreedhar 2026-06-22 15:21:03 -07:00
parent 00b91becfd
commit 5bd0682213
6 changed files with 139 additions and 13 deletions

View file

@ -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<AppInfo | null>(null)
const [session, setSession] = useState<AuthSession | null>(null)
useEffect(() => {
invoke<AppInfo>("app_info")
.then(setInfo)
.catch(() => setInfo(null))
getSession()
.then(setSession)
.catch(() => setSession(null))
}, [])
async function signOut() {
await clearSession()
router.replace("/login")
}
return (
<div className="mx-auto w-full max-w-2xl p-8">
<h1 className="mb-6 font-semibold text-2xl">Settings</h1>
<Card className="mb-4">
<CardHeader>
<CardTitle className="text-base">Account</CardTitle>
<CardDescription>
Keychain-backed desktop session for API requests.
</CardDescription>
</CardHeader>
<CardContent className="space-y-3 text-sm">
<Row
label="User"
value={session?.email ?? session?.userId ?? "..."}
/>
<Row label="API" value={session?.apiUrl ?? "..."} />
<Button variant="outline" onClick={signOut}>
Sign out
</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base">About</CardTitle>

View file

@ -17,7 +17,12 @@ export const metadata: Metadata = {
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<html
lang="en"
className="dark"
style={{ colorScheme: "dark" }}
suppressHydrationWarning
>
<body className={`${font.variable} antialiased`} suppressHydrationWarning>
<Providers>{children}</Providers>
</body>

View file

@ -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<string | null>(null)
const [isSubmitting, setIsSubmitting] = useState(false)
async function onSubmit(event: FormEvent<HTMLFormElement>) {
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 (
<div className="flex h-screen flex-col">
{/* Keep the frameless window draggable from the top edge. */}
<div data-tauri-drag-region className="h-10 shrink-0" />
<div className="flex flex-1 items-center justify-center p-8">
<div className="w-full max-w-sm text-center">
<div className="w-full max-w-sm">
<h1 className="font-semibold text-2xl">Supermemory</h1>
<p className="mt-2 text-muted-foreground text-sm">
Sign in to access your memories.
</p>
<Button className="mt-6 w-full" onClick={() => router.replace("/")}>
<Button className="mt-6 w-full" disabled>
Sign in with browser
</Button>
<p className="mt-3 text-muted-foreground text-xs">
Browser-based sign-in arrives in a later phase.
</p>
{desktopDevAuthEnabled ? (
<form className="mt-6 space-y-3 text-left" onSubmit={onSubmit}>
<div className="space-y-2">
<Label htmlFor="api-key">Development API key</Label>
<Input
id="api-key"
value={token}
onChange={(event) => setToken(event.target.value)}
placeholder="sm_..."
type="password"
autoComplete="off"
/>
</div>
{error ? (
<p className="text-destructive text-sm">{error}</p>
) : null}
<Button
type="submit"
className="w-full"
disabled={!token.trim() || isSubmitting}
>
{isSubmitting ? "Checking..." : "Continue with key"}
</Button>
</form>
) : null}
</div>
</div>
</div>

View file

@ -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<AuthStatus>("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 (
<div className="flex min-h-screen items-center justify-center text-muted-foreground text-sm">
Loading…
Loading...
</div>
)
}

29
apps/desktop/lib/auth.ts Normal file
View file

@ -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<string | null>("auth_get_token")
}
export async function getSession() {
return invoke<AuthSession>("auth_whoami")
}
export async function storeToken(token: string) {
await invoke("auth_store_token", { token })
return getSession()
}
export async function clearSession() {
await invoke("auth_clear")
}

View file

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