feat: add user registration, password reset, and guest login to LoginView
Some checks failed
Release Binaries / release (push) Has been cancelled

This commit is contained in:
Gemini AI
2025-12-29 03:31:38 +04:00
Unverified
parent 626e00b78d
commit 92352c5936

View File

@@ -1,5 +1,5 @@
import { Component, createSignal, onMount, For, Show } from "solid-js" import { Component, createSignal, onMount, For, Show } from "solid-js"
import { Lock, User, LogIn, ShieldCheck, Cpu } from "lucide-solid" import { Lock, User, ShieldCheck, Cpu, UserPlus, KeyRound, ArrowLeft, Ghost } from "lucide-solid"
import toast from "solid-toast" import toast from "solid-toast"
import { isElectronHost } from "../../lib/runtime-env" import { isElectronHost } from "../../lib/runtime-env"
import { setActiveUserId } from "../../lib/user-context" import { setActiveUserId } from "../../lib/user-context"
@@ -14,108 +14,255 @@ interface LoginViewProps {
onLoginSuccess: (user: UserRecord) => void onLoginSuccess: (user: UserRecord) => void
} }
type ViewMode = "login" | "register" | "reset"
const LoginView: Component<LoginViewProps> = (props) => { const LoginView: Component<LoginViewProps> = (props) => {
const [users, setUsers] = createSignal<UserRecord[]>([]) const [users, setUsers] = createSignal<UserRecord[]>([])
const [username, setUsername] = createSignal("") const [username, setUsername] = createSignal("")
const [password, setPassword] = createSignal("") const [password, setPassword] = createSignal("")
const [isLoggingIn, setIsLoggingIn] = createSignal(false) const [confirmPassword, setConfirmPassword] = createSignal("")
const [isLoadingUsers, setIsLoadingUsers] = createSignal(true) const [newPassword, setNewPassword] = createSignal("")
const [isLoading, setIsLoading] = createSignal(false)
const [mode, setMode] = createSignal<ViewMode>("login")
const getApi = () => { const getApi = () => {
const api = (window as any).electronAPI const api = (window as any).electronAPI
console.log("[LoginView] getApi:", api ? Object.keys(api) : "null")
return api return api
} }
onMount(async () => { const loadUsers = async () => {
console.log("[LoginView] onMount, isElectronHost:", isElectronHost())
try { try {
if (isElectronHost()) { if (isElectronHost()) {
const api = getApi() const api = getApi()
if (api && api.listUsers) { if (api?.listUsers) {
const userList = await api.listUsers() const userList = await api.listUsers()
console.log("[LoginView] listUsers result:", userList)
if (userList && Array.isArray(userList)) { if (userList && Array.isArray(userList)) {
setUsers(userList) setUsers(userList)
if (userList.length > 0) { if (userList.length > 0 && !username()) {
setUsername(userList[0].name) setUsername(userList[0].name)
} }
} }
} else {
console.error("[LoginView] listUsers method not found on API")
toast.error("API bridge incomplete")
} }
} else {
setUsername("web-explorer")
} }
} catch (error) { } catch (error) {
console.error("Failed to fetch users:", error) console.error("Failed to fetch users:", error)
toast.error("Failed to load identities")
} finally {
setIsLoadingUsers(false)
} }
}) }
onMount(loadUsers)
const resetForm = () => {
setPassword("")
setConfirmPassword("")
setNewPassword("")
}
const handleLogin = async (e: Event) => { const handleLogin = async (e: Event) => {
e.preventDefault() e.preventDefault()
const name = username().trim() const name = username().trim()
console.log("[LoginView] handleLogin called, name:", name)
if (!name) { if (!name) {
toast.error("Identity required") toast.error("Identity required")
return return
} }
setIsLoggingIn(true) setIsLoading(true)
try { try {
console.log("[LoginView] isElectronHost:", isElectronHost())
if (isElectronHost()) { if (isElectronHost()) {
const api = getApi() const api = getApi()
if (!api || !api.listUsers || !api.loginUser) { if (!api?.listUsers || !api?.loginUser) {
toast.error("API bridge not ready") toast.error("API bridge not ready")
return return
} }
console.log("[LoginView] Fetching users...")
const userList = await api.listUsers() const userList = await api.listUsers()
if (!userList || !Array.isArray(userList)) { const user = userList?.find((u: UserRecord) => u.name.toLowerCase() === name.toLowerCase())
toast.error("Bridge failure: try restarting")
return
}
console.log("[LoginView] Users found:", userList.map((u: any) => u.name))
const user = userList.find((u: UserRecord) => u.name.toLowerCase() === name.toLowerCase())
if (!user) { if (!user) {
toast.error(`Identity "${name}" not found`) toast.error(`Identity "${name}" not found`)
return return
} }
console.log("[LoginView] Attempting login for:", user.id)
const result = await api.loginUser({ const result = await api.loginUser({
id: user.id, id: user.id,
password: password(), password: password(),
}) })
console.log("[LoginView] Login result:", JSON.stringify(result))
if (result?.success) { if (result?.success) {
console.log("[LoginView] SUCCESS! Calling onLoginSuccess with:", result.user)
toast.success(`Welcome back, ${result.user.name}!`) toast.success(`Welcome back, ${result.user.name}!`)
setActiveUserId(result.user.id) setActiveUserId(result.user.id)
props.onLoginSuccess(result.user) props.onLoginSuccess(result.user)
} else { } else {
console.log("[LoginView] FAILED - result.success is:", result?.success) toast.error("Invalid access key")
toast.error("Invalid key for this identity")
} }
} else { } else {
console.log("[LoginView] Web mode login")
toast.success("Web mode access granted") toast.success("Web mode access granted")
props.onLoginSuccess({ id: "web-user", name: "Web Explorer" }) props.onLoginSuccess({ id: "web-user", name: username() || "Web Explorer" })
} }
} catch (error) { } catch (error) {
console.error("Login failed:", error) console.error("Login failed:", error)
toast.error("Decryption failed: check your key") toast.error("Authentication failed")
} finally { } finally {
setIsLoggingIn(false) setIsLoading(false)
}
}
const handleGuestLogin = async () => {
setIsLoading(true)
try {
const api = getApi()
if (api?.createGuest) {
const guestUser = await api.createGuest()
if (guestUser?.id) {
toast.success(`Welcome, ${guestUser.name}!`)
setActiveUserId(guestUser.id)
props.onLoginSuccess(guestUser)
} else {
toast.error("Failed to create guest session")
}
} else {
// Web mode fallback
const guestId = `guest-${Date.now()}`
toast.success("Guest session started")
props.onLoginSuccess({ id: guestId, name: "Guest", isGuest: true })
}
} catch (error) {
console.error("Guest login failed:", error)
toast.error("Guest login failed")
} finally {
setIsLoading(false)
}
}
const handleRegister = async (e: Event) => {
e.preventDefault()
const name = username().trim()
if (!name) {
toast.error("Username required")
return
}
if (name.length < 3) {
toast.error("Username must be at least 3 characters")
return
}
if (!password()) {
toast.error("Password required")
return
}
if (password().length < 4) {
toast.error("Password must be at least 4 characters")
return
}
if (password() !== confirmPassword()) {
toast.error("Passwords do not match")
return
}
// Check if user already exists
const existingUser = users().find(u => u.name.toLowerCase() === name.toLowerCase())
if (existingUser) {
toast.error(`Identity "${name}" already exists`)
return
}
setIsLoading(true)
try {
const api = getApi()
if (!api?.createUser) {
toast.error("Registration unavailable")
return
}
const newUser = await api.createUser({
name: name,
password: password(),
})
if (newUser?.id) {
toast.success(`Identity "${name}" created successfully!`)
await loadUsers()
setMode("login")
setUsername(name)
resetForm()
} else {
toast.error("Failed to create identity")
}
} catch (error) {
console.error("Registration failed:", error)
toast.error("Registration failed")
} finally {
setIsLoading(false)
}
}
const handleResetPassword = async (e: Event) => {
e.preventDefault()
const name = username().trim()
if (!name) {
toast.error("Select an identity first")
return
}
if (!password()) {
toast.error("Current password required")
return
}
if (!newPassword()) {
toast.error("New password required")
return
}
if (newPassword().length < 4) {
toast.error("New password must be at least 4 characters")
return
}
const user = users().find(u => u.name.toLowerCase() === name.toLowerCase())
if (!user) {
toast.error(`Identity "${name}" not found`)
return
}
setIsLoading(true)
try {
const api = getApi()
// First verify current password
const verifyResult = await api.loginUser({
id: user.id,
password: password(),
})
if (!verifyResult?.success) {
toast.error("Current password is incorrect")
return
}
// Update password
const updateResult = await api.updateUser({
id: user.id,
password: newPassword(),
})
if (updateResult?.id) {
toast.success("Password updated successfully!")
setMode("login")
resetForm()
} else {
toast.error("Failed to update password")
}
} catch (error) {
console.error("Password reset failed:", error)
toast.error("Password reset failed")
} finally {
setIsLoading(false)
}
}
const switchMode = (newMode: ViewMode) => {
setMode(newMode)
resetForm()
if (newMode === "register") {
setUsername("")
} }
} }
@@ -127,21 +274,38 @@ const LoginView: Component<LoginViewProps> = (props) => {
<div class="absolute -bottom-[10%] -right-[10%] w-[40%] h-[40%] bg-purple-500/20 blur-[120px] rounded-full animate-pulse delay-700" /> <div class="absolute -bottom-[10%] -right-[10%] w-[40%] h-[40%] bg-purple-500/20 blur-[120px] rounded-full animate-pulse delay-700" />
</div> </div>
<div class="relative w-full max-w-md px-6 py-12 bg-[#141414]/80 backdrop-blur-xl border border-white/10 rounded-3xl shadow-2xl"> <div class="relative w-full max-w-md px-6 py-10 bg-[#141414]/80 backdrop-blur-xl border border-white/10 rounded-3xl shadow-2xl">
{/* Logo & Header */} {/* Logo & Header */}
<div class="flex flex-col items-center mb-10"> <div class="flex flex-col items-center mb-8">
<div class="w-20 h-20 mb-6 bg-gradient-to-br from-blue-500 via-indigo-600 to-purple-700 p-0.5 rounded-2xl shadow-lg transform rotate-3"> <div class="w-16 h-16 mb-4 bg-gradient-to-br from-blue-500 via-indigo-600 to-purple-700 p-0.5 rounded-2xl shadow-lg transform rotate-3">
<div class="w-full h-full bg-[#141414] rounded-2xl flex items-center justify-center"> <div class="w-full h-full bg-[#141414] rounded-2xl flex items-center justify-center">
<Cpu class="w-10 h-10 text-white" /> <Cpu class="w-8 h-8 text-white" />
</div> </div>
</div> </div>
<h1 class="text-3xl font-bold text-white tracking-tight mb-2">NomadArch</h1> <h1 class="text-2xl font-bold text-white tracking-tight mb-1">NomadArch</h1>
<p class="text-gray-400 text-sm">Secure Neural Access Point</p> <p class="text-gray-400 text-sm">
{mode() === "login" && "Secure Neural Access Point"}
{mode() === "register" && "Create New Identity"}
{mode() === "reset" && "Reset Access Key"}
</p>
</div> </div>
<form onSubmit={handleLogin} class="space-y-6"> {/* Back button for non-login modes */}
{/* User Selection */} <Show when={mode() !== "login"}>
<div class="space-y-2"> <button
type="button"
onClick={() => switchMode("login")}
class="flex items-center gap-2 text-gray-400 hover:text-white transition-colors mb-4"
>
<ArrowLeft class="w-4 h-4" />
<span class="text-sm">Back to login</span>
</button>
</Show>
{/* Login Form */}
<Show when={mode() === "login"}>
<form onSubmit={handleLogin} class="space-y-5">
<div class="space-y-1.5">
<label class="text-xs font-semibold text-gray-500 uppercase tracking-wider ml-1">Identity</label> <label class="text-xs font-semibold text-gray-500 uppercase tracking-wider ml-1">Identity</label>
<div class="relative group"> <div class="relative group">
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none"> <div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
@@ -152,19 +316,16 @@ const LoginView: Component<LoginViewProps> = (props) => {
placeholder="Username" placeholder="Username"
value={username()} value={username()}
onInput={(e) => setUsername(e.currentTarget.value)} onInput={(e) => setUsername(e.currentTarget.value)}
class="block w-full pl-12 pr-4 py-4 bg-[#1a1a1a] border border-white/5 rounded-2xl text-white appearance-none focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500/50 transition-all cursor-text" class="block w-full pl-12 pr-4 py-3.5 bg-[#1a1a1a] border border-white/5 rounded-2xl text-white focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all"
list="identity-suggestions" list="identity-suggestions"
/> />
<datalist id="identity-suggestions"> <datalist id="identity-suggestions">
<For each={users()}> <For each={users()}>{(user) => <option value={user.name} />}</For>
{(user) => <option value={user.name} />}
</For>
</datalist> </datalist>
</div> </div>
</div> </div>
{/* Password Input */} <div class="space-y-1.5">
<div class="space-y-2">
<label class="text-xs font-semibold text-gray-500 uppercase tracking-wider ml-1">Access Key</label> <label class="text-xs font-semibold text-gray-500 uppercase tracking-wider ml-1">Access Key</label>
<div class="relative group"> <div class="relative group">
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none"> <div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
@@ -172,33 +333,191 @@ const LoginView: Component<LoginViewProps> = (props) => {
</div> </div>
<input <input
type="password" type="password"
placeholder="Enter password..." placeholder="Password"
value={password()} value={password()}
onInput={(e) => setPassword(e.currentTarget.value)} onInput={(e) => setPassword(e.currentTarget.value)}
required class="block w-full pl-12 pr-4 py-3.5 bg-[#1a1a1a] border border-white/5 rounded-2xl text-white placeholder-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all font-mono"
class="block w-full pl-12 pr-4 py-4 bg-[#1a1a1a] border border-white/5 rounded-2xl text-white placeholder-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500/50 transition-all font-mono"
/> />
</div> </div>
</div> </div>
<button <button
type="submit" type="submit"
disabled={isLoggingIn() || !username()} disabled={isLoading() || !username()}
class="w-full flex items-center justify-center gap-3 py-4 bg-gradient-to-r from-blue-600 via-indigo-600 to-purple-600 hover:from-blue-500 hover:to-purple-500 text-white font-bold rounded-2xl shadow-xl shadow-blue-900/20 transform active:scale-[0.98] transition-all disabled:opacity-50 disabled:cursor-not-allowed group" class="w-full flex items-center justify-center gap-3 py-3.5 bg-gradient-to-r from-blue-600 via-indigo-600 to-purple-600 hover:from-blue-500 hover:to-purple-500 text-white font-bold rounded-2xl shadow-xl transform active:scale-[0.98] transition-all disabled:opacity-50 disabled:cursor-not-allowed"
> >
<Show when={isLoggingIn()} fallback={ <Show when={isLoading()} fallback={<><ShieldCheck class="w-5 h-5" /><span>Verify Identity</span></>}>
<>
<ShieldCheck class="w-5 h-5 group-hover:scale-110 transition-transform" />
<span>Verify Identity</span>
</>
}>
<div class="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" /> <div class="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
<span>Decrypting...</span> <span>Verifying...</span>
</Show> </Show>
</button> </button>
</form> </form>
<div class="mt-8 text-center text-xs text-gray-600"> <div class="mt-6 flex flex-col gap-4">
<button
type="button"
onClick={handleGuestLogin}
disabled={isLoading()}
class="w-full flex items-center justify-center gap-2 py-3 bg-[#1a1a1a] hover:bg-[#252525] border border-white/10 text-gray-300 hover:text-white font-medium rounded-2xl transition-all disabled:opacity-50"
>
<Ghost class="w-5 h-5" />
<span>Continue as Guest</span>
</button>
<div class="flex items-center justify-between text-sm">
<button
type="button"
onClick={() => switchMode("register")}
class="flex items-center gap-1.5 text-gray-400 hover:text-blue-400 transition-colors"
>
<UserPlus class="w-4 h-4" />
<span>Create Identity</span>
</button>
<button
type="button"
onClick={() => switchMode("reset")}
class="flex items-center gap-1.5 text-gray-400 hover:text-purple-400 transition-colors"
>
<KeyRound class="w-4 h-4" />
<span>Reset Password</span>
</button>
</div>
</div>
</Show>
{/* Register Form */}
<Show when={mode() === "register"}>
<form onSubmit={handleRegister} class="space-y-5">
<div class="space-y-1.5">
<label class="text-xs font-semibold text-gray-500 uppercase tracking-wider ml-1">Choose Username</label>
<div class="relative group">
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
<User class="w-5 h-5 text-gray-500 group-focus-within:text-green-500 transition-colors" />
</div>
<input
type="text"
placeholder="Enter username"
value={username()}
onInput={(e) => setUsername(e.currentTarget.value)}
class="block w-full pl-12 pr-4 py-3.5 bg-[#1a1a1a] border border-white/5 rounded-2xl text-white focus:outline-none focus:ring-2 focus:ring-green-500/50 transition-all"
/>
</div>
</div>
<div class="space-y-1.5">
<label class="text-xs font-semibold text-gray-500 uppercase tracking-wider ml-1">Choose Password</label>
<div class="relative group">
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
<Lock class="w-5 h-5 text-gray-500 group-focus-within:text-green-500 transition-colors" />
</div>
<input
type="password"
placeholder="Enter password"
value={password()}
onInput={(e) => setPassword(e.currentTarget.value)}
class="block w-full pl-12 pr-4 py-3.5 bg-[#1a1a1a] border border-white/5 rounded-2xl text-white placeholder-gray-600 focus:outline-none focus:ring-2 focus:ring-green-500/50 transition-all font-mono"
/>
</div>
</div>
<div class="space-y-1.5">
<label class="text-xs font-semibold text-gray-500 uppercase tracking-wider ml-1">Confirm Password</label>
<div class="relative group">
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
<Lock class="w-5 h-5 text-gray-500 group-focus-within:text-green-500 transition-colors" />
</div>
<input
type="password"
placeholder="Confirm password"
value={confirmPassword()}
onInput={(e) => setConfirmPassword(e.currentTarget.value)}
class="block w-full pl-12 pr-4 py-3.5 bg-[#1a1a1a] border border-white/5 rounded-2xl text-white placeholder-gray-600 focus:outline-none focus:ring-2 focus:ring-green-500/50 transition-all font-mono"
/>
</div>
</div>
<button
type="submit"
disabled={isLoading() || !username() || !password() || !confirmPassword()}
class="w-full flex items-center justify-center gap-3 py-3.5 bg-gradient-to-r from-green-600 via-emerald-600 to-teal-600 hover:from-green-500 hover:to-teal-500 text-white font-bold rounded-2xl shadow-xl transform active:scale-[0.98] transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
<Show when={isLoading()} fallback={<><UserPlus class="w-5 h-5" /><span>Create Identity</span></>}>
<div class="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
<span>Creating...</span>
</Show>
</button>
</form>
</Show>
{/* Reset Password Form */}
<Show when={mode() === "reset"}>
<form onSubmit={handleResetPassword} class="space-y-5">
<div class="space-y-1.5">
<label class="text-xs font-semibold text-gray-500 uppercase tracking-wider ml-1">Identity</label>
<div class="relative group">
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
<User class="w-5 h-5 text-gray-500 group-focus-within:text-purple-500 transition-colors" />
</div>
<input
type="text"
placeholder="Username"
value={username()}
onInput={(e) => setUsername(e.currentTarget.value)}
class="block w-full pl-12 pr-4 py-3.5 bg-[#1a1a1a] border border-white/5 rounded-2xl text-white focus:outline-none focus:ring-2 focus:ring-purple-500/50 transition-all"
list="identity-suggestions-reset"
/>
<datalist id="identity-suggestions-reset">
<For each={users()}>{(user) => <option value={user.name} />}</For>
</datalist>
</div>
</div>
<div class="space-y-1.5">
<label class="text-xs font-semibold text-gray-500 uppercase tracking-wider ml-1">Current Password</label>
<div class="relative group">
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
<Lock class="w-5 h-5 text-gray-500 group-focus-within:text-purple-500 transition-colors" />
</div>
<input
type="password"
placeholder="Enter current password"
value={password()}
onInput={(e) => setPassword(e.currentTarget.value)}
class="block w-full pl-12 pr-4 py-3.5 bg-[#1a1a1a] border border-white/5 rounded-2xl text-white placeholder-gray-600 focus:outline-none focus:ring-2 focus:ring-purple-500/50 transition-all font-mono"
/>
</div>
</div>
<div class="space-y-1.5">
<label class="text-xs font-semibold text-gray-500 uppercase tracking-wider ml-1">New Password</label>
<div class="relative group">
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
<KeyRound class="w-5 h-5 text-gray-500 group-focus-within:text-purple-500 transition-colors" />
</div>
<input
type="password"
placeholder="Enter new password"
value={newPassword()}
onInput={(e) => setNewPassword(e.currentTarget.value)}
class="block w-full pl-12 pr-4 py-3.5 bg-[#1a1a1a] border border-white/5 rounded-2xl text-white placeholder-gray-600 focus:outline-none focus:ring-2 focus:ring-purple-500/50 transition-all font-mono"
/>
</div>
</div>
<button
type="submit"
disabled={isLoading() || !username() || !password() || !newPassword()}
class="w-full flex items-center justify-center gap-3 py-3.5 bg-gradient-to-r from-purple-600 via-violet-600 to-fuchsia-600 hover:from-purple-500 hover:to-fuchsia-500 text-white font-bold rounded-2xl shadow-xl transform active:scale-[0.98] transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
<Show when={isLoading()} fallback={<><KeyRound class="w-5 h-5" /><span>Reset Password</span></>}>
<div class="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
<span>Resetting...</span>
</Show>
</button>
</form>
</Show>
<div class="mt-6 text-center text-xs text-gray-600">
Powered by Antigravity OS v4.5 | Encrypted Connection Powered by Antigravity OS v4.5 | Encrypted Connection
</div> </div>
</div> </div>