mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-09 03:18:04 +00:00
Wire desktop dev key login
This commit is contained in:
parent
00b91becfd
commit
5bd0682213
6 changed files with 139 additions and 13 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
29
apps/desktop/lib/auth.ts
Normal 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")
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue