feat: implement mandatory login on startup and set roman password
Some checks failed
Release Binaries / release (push) Has been cancelled

This commit is contained in:
Gemini AI
2025-12-29 01:25:43 +04:00
Unverified
parent 8474be8559
commit dc21ade84e
5 changed files with 295 additions and 103 deletions

View File

@@ -481,6 +481,8 @@ if (isMac) {
} }
app.whenReady().then(() => { app.whenReady().then(() => {
clearGuestUsers()
logoutActiveUser()
ensureDefaultUsers() ensureDefaultUsers()
applyUserEnvToCli() applyUserEnvToCli()
startCli() startCli()

View File

@@ -111,19 +111,30 @@ function migrateLegacyData(targetDir: string) {
export function ensureDefaultUsers(): UserRecord { export function ensureDefaultUsers(): UserRecord {
const store = readStore() const store = readStore()
if (store.users.length > 0) {
const active = store.users.find((u) => u.id === store.activeUserId) ?? store.users[0] // If roman exists, ensure his password is updated to the new required one if it matches the old default
if (!store.activeUserId) { const roman = store.users.find(u => u.name === "roman")
store.activeUserId = active.id if (roman && roman.salt && roman.passwordHash) {
const oldDefaultHash = hashPassword("q1w2e3r4", roman.salt)
if (roman.passwordHash === oldDefaultHash) {
console.log("[UserStore] Updating roman's password to new default")
const newSalt = generateSalt()
roman.salt = newSalt
roman.passwordHash = hashPassword("!@#$q1w2e3r4", newSalt)
roman.updatedAt = nowIso()
writeStore(store) writeStore(store)
} }
}
if (store.users.length > 0) {
const active = store.users.find((u) => u.id === store.activeUserId) ?? store.users[0]
return active return active
} }
const existingIds = new Set<string>() const existingIds = new Set<string>()
const userId = ensureUniqueId("roman", existingIds) const userId = ensureUniqueId("roman", existingIds)
const salt = generateSalt() const salt = generateSalt()
const passwordHash = hashPassword("q1w2e3r4", salt) const passwordHash = hashPassword("!@#$q1w2e3r4", salt)
const record: UserRecord = { const record: UserRecord = {
id: userId, id: userId,
name: "roman", name: "roman",
@@ -134,7 +145,6 @@ export function ensureDefaultUsers(): UserRecord {
} }
store.users.push(record) store.users.push(record)
store.activeUserId = record.id
writeStore(store) writeStore(store)
const userDir = getUserDir(record.id) const userDir = getUserDir(record.id)
@@ -153,6 +163,13 @@ export function getActiveUser(): UserRecord | null {
return store.users.find((user) => user.id === store.activeUserId) ?? null return store.users.find((user) => user.id === store.activeUserId) ?? null
} }
export function logoutActiveUser() {
const store = readStore()
store.activeUserId = undefined
writeStore(store)
console.log("[UserStore] Active user logged out")
}
export function setActiveUser(userId: string) { export function setActiveUser(userId: string) {
const store = readStore() const store = readStore()
const user = store.users.find((u) => u.id === userId) const user = store.users.find((u) => u.id === userId)

View File

@@ -11,6 +11,8 @@ import { RemoteAccessOverlay } from "./components/remote-access-overlay"
import { InstanceMetadataProvider } from "./lib/contexts/instance-metadata-context" import { InstanceMetadataProvider } from "./lib/contexts/instance-metadata-context"
import { initMarkdown } from "./lib/markdown" import { initMarkdown } from "./lib/markdown"
import QwenOAuthCallback from "./pages/QwenOAuthCallback" import QwenOAuthCallback from "./pages/QwenOAuthCallback"
import LoginView from "./components/auth/LoginView"
import { isLoggedIn, initializeUserContext } from "./lib/user-context"
import { useTheme } from "./lib/theme" import { useTheme } from "./lib/theme"
import { useCommands } from "./lib/hooks/use-commands" import { useCommands } from "./lib/hooks/use-commands"
@@ -391,100 +393,110 @@ const App: Component = () => {
</div> </div>
</Dialog.Portal> </Dialog.Portal>
</Dialog> </Dialog>
<div class="h-screen w-screen flex flex-col"> <Show
<Show when={isLoggedIn()}
when={shouldShowFolderSelection()} fallback={<LoginView onLoginSuccess={() => initializeUserContext()} />}
fallback={ >
<> <div class="h-screen w-screen flex flex-col">
<InstanceTabs <Show
instances={instances()} when={shouldShowFolderSelection()}
activeInstanceId={activeInstanceId()} fallback={
onSelect={setActiveInstanceId} <>
onClose={handleCloseInstance} <InstanceTabs
onNew={handleNewInstanceRequest} instances={instances()}
onOpenRemoteAccess={() => setRemoteAccessOpen(true)} activeInstanceId={activeInstanceId()}
/> onSelect={setActiveInstanceId}
onClose={handleCloseInstance}
onNew={handleNewInstanceRequest}
onOpenRemoteAccess={() => setRemoteAccessOpen(true)}
/>
<For each={Array.from(instances().values())}> <For each={Array.from(instances().values())}>
{(instance) => { {(instance) => {
const isActiveInstance = () => activeInstanceId() === instance.id const isActiveInstance = () => activeInstanceId() === instance.id
const isVisible = () => isActiveInstance() && !showFolderSelection() const isVisible = () => isActiveInstance() && !showFolderSelection()
return ( return (
<div class="flex-1 min-h-0 overflow-hidden" style={{ display: isVisible() ? "flex" : "none" }}> <div class="flex-1 min-h-0 overflow-hidden" style={{ display: isVisible() ? "flex" : "none" }}>
<InstanceMetadataProvider instance={instance}> <InstanceMetadataProvider instance={instance}>
<InstanceShell <InstanceShell
instance={instance} instance={instance}
escapeInDebounce={escapeInDebounce()} escapeInDebounce={escapeInDebounce()}
paletteCommands={paletteCommands} paletteCommands={paletteCommands}
onCloseSession={(sessionId) => handleCloseSession(instance.id, sessionId)} onCloseSession={(sessionId) => handleCloseSession(instance.id, sessionId)}
onNewSession={() => handleNewSession(instance.id)} onNewSession={() => handleNewSession(instance.id)}
handleSidebarAgentChange={(sessionId, agent) => handleSidebarAgentChange(instance.id, sessionId, agent)} handleSidebarAgentChange={(sessionId, agent) => handleSidebarAgentChange(instance.id, sessionId, agent)}
handleSidebarModelChange={(sessionId, model) => handleSidebarModelChange(instance.id, sessionId, model)} handleSidebarModelChange={(sessionId, model) => handleSidebarModelChange(instance.id, sessionId, model)}
onExecuteCommand={executeCommand} onExecuteCommand={executeCommand}
tabBarOffset={instanceTabBarHeight()} tabBarOffset={instanceTabBarHeight()}
/> />
</InstanceMetadataProvider> </InstanceMetadataProvider>
</div> </div>
) )
}} }}
</For> </For>
</> </>
} }
> >
<FolderSelectionView <FolderSelectionView
onSelectFolder={handleSelectFolder} onSelectFolder={handleSelectFolder}
isLoading={isSelectingFolder()} isLoading={isSelectingFolder()}
advancedSettingsOpen={isAdvancedSettingsOpen()} advancedSettingsOpen={isAdvancedSettingsOpen()}
onAdvancedSettingsOpen={() => setIsAdvancedSettingsOpen(true)} onAdvancedSettingsOpen={() => setIsAdvancedSettingsOpen(true)}
onAdvancedSettingsClose={() => setIsAdvancedSettingsOpen(false)} onAdvancedSettingsClose={() => setIsAdvancedSettingsOpen(false)}
onOpenRemoteAccess={() => setRemoteAccessOpen(true)} onOpenRemoteAccess={() => setRemoteAccessOpen(true)}
/> />
</Show> </Show>
<Show when={showFolderSelection()}> <Show when={showFolderSelection()}>
<div class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center"> <div class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center">
<div class="w-full h-full relative"> <div class="w-full h-full relative">
<button <button
onClick={() => { onClick={() => {
setShowFolderSelection(false) setShowFolderSelection(false)
setShowFolderSelectionOnStart(false) setShowFolderSelectionOnStart(false)
setIsAdvancedSettingsOpen(false) setIsAdvancedSettingsOpen(false)
clearLaunchError() clearLaunchError()
}} }}
class="absolute top-4 right-4 z-10 p-2 bg-white dark:bg-gray-800 rounded-lg shadow-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors" class="absolute top-4 right-4 z-10 p-2 bg-white dark:bg-gray-800 rounded-lg shadow-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
title="Close (Esc)" title="Close (Esc)"
> >
<svg class="w-5 h-5 text-gray-600 dark:text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-5 h-5 text-gray-600 dark:text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg> </svg>
</button> </button>
<FolderSelectionView <FolderSelectionView
onSelectFolder={handleSelectFolder} onSelectFolder={handleSelectFolder}
isLoading={isSelectingFolder()} isLoading={isSelectingFolder()}
advancedSettingsOpen={isAdvancedSettingsOpen()} advancedSettingsOpen={isAdvancedSettingsOpen()}
onAdvancedSettingsOpen={() => setIsAdvancedSettingsOpen(true)} onAdvancedSettingsOpen={() => setIsAdvancedSettingsOpen(true)}
onAdvancedSettingsClose={() => setIsAdvancedSettingsOpen(false)} onAdvancedSettingsClose={() => setIsAdvancedSettingsOpen(false)}
/> />
</div>
</div> </div>
</div> </Show>
</Show>
<RemoteAccessOverlay open={remoteAccessOpen()} onClose={() => setRemoteAccessOpen(false)} /> <RemoteAccessOverlay open={remoteAccessOpen()} onClose={() => setRemoteAccessOpen(false)} />
<AlertDialog /> <AlertDialog />
<Toaster <Toaster
position="top-right" position="top-right"
gutter={16} gutter={8}
toastOptions={{ containerClassName=""
duration: 8000, containerStyle={{}}
className: "bg-transparent border-none shadow-none p-0", toastOptions={{
}} className: "",
/> duration: 5000,
</div> style: {
background: "#363636",
color: "#fff",
},
}}
/>
</Show>
</> </>
) )
} }

View File

@@ -0,0 +1,161 @@
import { Component, createSignal, onMount, For, Show } from "solid-js"
import { Lock, User, LogIn, ShieldCheck, Cpu } from "lucide-solid"
import toast from "solid-toast"
interface UserRecord {
id: string
name: string
isGuest?: boolean
}
interface LoginViewProps {
onLoginSuccess: (user: UserRecord) => void
}
const LoginView: Component<LoginViewProps> = (props) => {
const [users, setUsers] = createSignal<UserRecord[]>([])
const [selectedUserId, setSelectedUserId] = createSignal<string>("")
const [password, setPassword] = createSignal("")
const [isLoggingIn, setIsLoggingIn] = createSignal(false)
const [isLoadingUsers, setIsLoadingUsers] = createSignal(true)
onMount(async () => {
try {
const ipcRenderer = (window as any).electron?.ipcRenderer
if (ipcRenderer) {
const userList = await ipcRenderer.invoke("users:list")
setUsers(userList)
if (userList.length > 0) {
setSelectedUserId(userList[0].id)
}
}
} catch (error) {
console.error("Failed to fetch users:", error)
toast.error("Failed to load user list")
} finally {
setIsLoadingUsers(false)
}
})
const handleLogin = async (e: Event) => {
e.preventDefault()
if (!selectedUserId()) return
setIsLoggingIn(true)
try {
const ipcRenderer = (window as any).electron?.ipcRenderer
if (ipcRenderer) {
const result = await ipcRenderer.invoke("users:login", {
id: selectedUserId(),
password: password(),
})
if (result.success) {
toast.success(`Welcome back, ${result.user.name}!`)
props.onLoginSuccess(result.user)
} else {
toast.error("Invalid password")
}
}
} catch (error) {
console.error("Login failed:", error)
toast.error("Login failed. Please try again.")
} finally {
setIsLoggingIn(false)
}
}
return (
<div class="fixed inset-0 z-[9999] flex items-center justify-center bg-[#0a0a0a]">
{/* Dynamic Background */}
<div class="absolute inset-0 overflow-hidden pointer-events-none opacity-20">
<div class="absolute -top-[10%] -left-[10%] w-[40%] h-[40%] bg-blue-500/20 blur-[120px] rounded-full animate-pulse" />
<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 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">
{/* Logo & Header */}
<div class="flex flex-col items-center mb-10">
<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-full h-full bg-[#141414] rounded-2xl flex items-center justify-center">
<Cpu class="w-10 h-10 text-white" />
</div>
</div>
<h1 class="text-3xl font-bold text-white tracking-tight mb-2">NomadArch</h1>
<p class="text-gray-400 text-sm">Secure Neural Access Point</p>
</div>
<form onSubmit={handleLogin} class="space-y-6">
{/* User Selection */}
<div class="space-y-2">
<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-blue-500 transition-colors" />
</div>
<select
value={selectedUserId()}
onInput={(e) => setSelectedUserId(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-pointer"
>
<Show when={isLoadingUsers()}>
<option>Loading identities...</option>
</Show>
<For each={users()}>
{(user) => (
<option value={user.id}>
{user.name} {user.isGuest ? "(Guest)" : ""}
</option>
)}
</For>
</select>
<div class="absolute inset-y-0 right-0 pr-4 flex items-center pointer-events-none">
<LogIn class="w-4 h-4 text-gray-600 transform rotate-90" />
</div>
</div>
</div>
{/* Password Input */}
<div class="space-y-2">
<label class="text-xs font-semibold text-gray-500 uppercase tracking-wider ml-1">Access Key</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-blue-500 transition-colors" />
</div>
<input
type="password"
placeholder="Enter password..."
value={password()}
onInput={(e) => setPassword(e.currentTarget.value)}
required
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>
<button
type="submit"
disabled={isLoggingIn() || !selectedUserId()}
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"
>
<Show when={isLoggingIn()} fallback={
<>
<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" />
<span>Decrypting...</span>
</Show>
</button>
</form>
<div class="mt-8 text-center text-xs text-gray-600">
Powered by Antigravity OS v4.5 | Encrypted Connection
</div>
</div>
</div>
)
}
export default LoginView

View File

@@ -1,20 +1,23 @@
/** import { createSignal } from "solid-js"
* User Context utilities for frontend
* Handles active user ID and passes it in API requests
*/
// Storage key for active user // Storage key for active user
const ACTIVE_USER_KEY = "codenomad_active_user_id" const ACTIVE_USER_KEY = "codenomad_active_user_id"
const [isLoggedIn, setLoggedIn] = createSignal(false)
export { isLoggedIn, setLoggedIn }
/** /**
* Set the active user ID * Set the active user ID
*/ */
export function setActiveUserId(userId: string | null): void { export function setActiveUserId(userId: string | null): void {
if (userId) { if (userId) {
localStorage.setItem(ACTIVE_USER_KEY, userId) localStorage.setItem(ACTIVE_USER_KEY, userId)
setLoggedIn(true)
console.log(`[UserContext] Active user set to: ${userId}`) console.log(`[UserContext] Active user set to: ${userId}`)
} else { } else {
localStorage.removeItem(ACTIVE_USER_KEY) localStorage.removeItem(ACTIVE_USER_KEY)
setLoggedIn(false)
console.log(`[UserContext] Active user cleared`) console.log(`[UserContext] Active user cleared`)
} }
} }
@@ -72,25 +75,22 @@ export async function initializeUserContext(): Promise<void> {
setActiveUserId(activeUser.id) setActiveUserId(activeUser.id)
console.log(`[UserContext] Initialized with user: ${activeUser.id} (${activeUser.name})`) console.log(`[UserContext] Initialized with user: ${activeUser.id} (${activeUser.name})`)
} else { } else {
setLoggedIn(false)
console.log(`[UserContext] No active user from IPC`) console.log(`[UserContext] No active user from IPC`)
} }
} else { } else {
// Web mode - try to get from localStorage or use default // Web mode - try to get from localStorage or use default
const existingId = getActiveUserId() const existingId = getActiveUserId()
if (existingId) { if (existingId) {
setLoggedIn(true)
console.log(`[UserContext] Using cached user ID: ${existingId}`) console.log(`[UserContext] Using cached user ID: ${existingId}`)
} else { } else {
// Set a default user ID for web mode setLoggedIn(false)
const defaultUserId = "default" console.log(`[UserContext] Web mode - no active user`)
setActiveUserId(defaultUserId)
console.log(`[UserContext] Web mode - using default user ID`)
} }
} }
} catch (error) { } catch (error) {
console.error(`[UserContext] Failed to initialize:`, error) console.error(`[UserContext] Failed to initialize:`, error)
// Fall back to default setLoggedIn(false)
if (!getActiveUserId()) {
setActiveUserId("default")
}
} }
} }