feat: implement mandatory login on startup and set roman password
Some checks failed
Release Binaries / release (push) Has been cancelled
Some checks failed
Release Binaries / release (push) Has been cancelled
This commit is contained in:
@@ -481,6 +481,8 @@ if (isMac) {
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
clearGuestUsers()
|
||||
logoutActiveUser()
|
||||
ensureDefaultUsers()
|
||||
applyUserEnvToCli()
|
||||
startCli()
|
||||
|
||||
@@ -111,19 +111,30 @@ function migrateLegacyData(targetDir: string) {
|
||||
|
||||
export function ensureDefaultUsers(): UserRecord {
|
||||
const store = readStore()
|
||||
if (store.users.length > 0) {
|
||||
const active = store.users.find((u) => u.id === store.activeUserId) ?? store.users[0]
|
||||
if (!store.activeUserId) {
|
||||
store.activeUserId = active.id
|
||||
|
||||
// If roman exists, ensure his password is updated to the new required one if it matches the old default
|
||||
const roman = store.users.find(u => u.name === "roman")
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
if (store.users.length > 0) {
|
||||
const active = store.users.find((u) => u.id === store.activeUserId) ?? store.users[0]
|
||||
return active
|
||||
}
|
||||
|
||||
const existingIds = new Set<string>()
|
||||
const userId = ensureUniqueId("roman", existingIds)
|
||||
const salt = generateSalt()
|
||||
const passwordHash = hashPassword("q1w2e3r4", salt)
|
||||
const passwordHash = hashPassword("!@#$q1w2e3r4", salt)
|
||||
const record: UserRecord = {
|
||||
id: userId,
|
||||
name: "roman",
|
||||
@@ -134,7 +145,6 @@ export function ensureDefaultUsers(): UserRecord {
|
||||
}
|
||||
|
||||
store.users.push(record)
|
||||
store.activeUserId = record.id
|
||||
writeStore(store)
|
||||
|
||||
const userDir = getUserDir(record.id)
|
||||
@@ -153,6 +163,13 @@ export function getActiveUser(): UserRecord | 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) {
|
||||
const store = readStore()
|
||||
const user = store.users.find((u) => u.id === userId)
|
||||
|
||||
@@ -11,6 +11,8 @@ import { RemoteAccessOverlay } from "./components/remote-access-overlay"
|
||||
import { InstanceMetadataProvider } from "./lib/contexts/instance-metadata-context"
|
||||
import { initMarkdown } from "./lib/markdown"
|
||||
import QwenOAuthCallback from "./pages/QwenOAuthCallback"
|
||||
import LoginView from "./components/auth/LoginView"
|
||||
import { isLoggedIn, initializeUserContext } from "./lib/user-context"
|
||||
|
||||
import { useTheme } from "./lib/theme"
|
||||
import { useCommands } from "./lib/hooks/use-commands"
|
||||
@@ -391,100 +393,110 @@ const App: Component = () => {
|
||||
</div>
|
||||
</Dialog.Portal>
|
||||
</Dialog>
|
||||
<div class="h-screen w-screen flex flex-col">
|
||||
<Show
|
||||
when={shouldShowFolderSelection()}
|
||||
fallback={
|
||||
<>
|
||||
<InstanceTabs
|
||||
instances={instances()}
|
||||
activeInstanceId={activeInstanceId()}
|
||||
onSelect={setActiveInstanceId}
|
||||
onClose={handleCloseInstance}
|
||||
onNew={handleNewInstanceRequest}
|
||||
onOpenRemoteAccess={() => setRemoteAccessOpen(true)}
|
||||
/>
|
||||
<Show
|
||||
when={isLoggedIn()}
|
||||
fallback={<LoginView onLoginSuccess={() => initializeUserContext()} />}
|
||||
>
|
||||
<div class="h-screen w-screen flex flex-col">
|
||||
<Show
|
||||
when={shouldShowFolderSelection()}
|
||||
fallback={
|
||||
<>
|
||||
<InstanceTabs
|
||||
instances={instances()}
|
||||
activeInstanceId={activeInstanceId()}
|
||||
onSelect={setActiveInstanceId}
|
||||
onClose={handleCloseInstance}
|
||||
onNew={handleNewInstanceRequest}
|
||||
onOpenRemoteAccess={() => setRemoteAccessOpen(true)}
|
||||
/>
|
||||
|
||||
<For each={Array.from(instances().values())}>
|
||||
{(instance) => {
|
||||
const isActiveInstance = () => activeInstanceId() === instance.id
|
||||
const isVisible = () => isActiveInstance() && !showFolderSelection()
|
||||
return (
|
||||
<div class="flex-1 min-h-0 overflow-hidden" style={{ display: isVisible() ? "flex" : "none" }}>
|
||||
<InstanceMetadataProvider instance={instance}>
|
||||
<InstanceShell
|
||||
instance={instance}
|
||||
escapeInDebounce={escapeInDebounce()}
|
||||
paletteCommands={paletteCommands}
|
||||
onCloseSession={(sessionId) => handleCloseSession(instance.id, sessionId)}
|
||||
onNewSession={() => handleNewSession(instance.id)}
|
||||
handleSidebarAgentChange={(sessionId, agent) => handleSidebarAgentChange(instance.id, sessionId, agent)}
|
||||
handleSidebarModelChange={(sessionId, model) => handleSidebarModelChange(instance.id, sessionId, model)}
|
||||
onExecuteCommand={executeCommand}
|
||||
tabBarOffset={instanceTabBarHeight()}
|
||||
/>
|
||||
</InstanceMetadataProvider>
|
||||
<For each={Array.from(instances().values())}>
|
||||
{(instance) => {
|
||||
const isActiveInstance = () => activeInstanceId() === instance.id
|
||||
const isVisible = () => isActiveInstance() && !showFolderSelection()
|
||||
return (
|
||||
<div class="flex-1 min-h-0 overflow-hidden" style={{ display: isVisible() ? "flex" : "none" }}>
|
||||
<InstanceMetadataProvider instance={instance}>
|
||||
<InstanceShell
|
||||
instance={instance}
|
||||
escapeInDebounce={escapeInDebounce()}
|
||||
paletteCommands={paletteCommands}
|
||||
onCloseSession={(sessionId) => handleCloseSession(instance.id, sessionId)}
|
||||
onNewSession={() => handleNewSession(instance.id)}
|
||||
handleSidebarAgentChange={(sessionId, agent) => handleSidebarAgentChange(instance.id, sessionId, agent)}
|
||||
handleSidebarModelChange={(sessionId, model) => handleSidebarModelChange(instance.id, sessionId, model)}
|
||||
onExecuteCommand={executeCommand}
|
||||
tabBarOffset={instanceTabBarHeight()}
|
||||
/>
|
||||
</InstanceMetadataProvider>
|
||||
|
||||
</div>
|
||||
)
|
||||
</div>
|
||||
)
|
||||
|
||||
}}
|
||||
</For>
|
||||
}}
|
||||
</For>
|
||||
|
||||
</>
|
||||
}
|
||||
>
|
||||
<FolderSelectionView
|
||||
onSelectFolder={handleSelectFolder}
|
||||
isLoading={isSelectingFolder()}
|
||||
advancedSettingsOpen={isAdvancedSettingsOpen()}
|
||||
onAdvancedSettingsOpen={() => setIsAdvancedSettingsOpen(true)}
|
||||
onAdvancedSettingsClose={() => setIsAdvancedSettingsOpen(false)}
|
||||
onOpenRemoteAccess={() => setRemoteAccessOpen(true)}
|
||||
/>
|
||||
</Show>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<FolderSelectionView
|
||||
onSelectFolder={handleSelectFolder}
|
||||
isLoading={isSelectingFolder()}
|
||||
advancedSettingsOpen={isAdvancedSettingsOpen()}
|
||||
onAdvancedSettingsOpen={() => setIsAdvancedSettingsOpen(true)}
|
||||
onAdvancedSettingsClose={() => setIsAdvancedSettingsOpen(false)}
|
||||
onOpenRemoteAccess={() => setRemoteAccessOpen(true)}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
<Show when={showFolderSelection()}>
|
||||
<div class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center">
|
||||
<div class="w-full h-full relative">
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowFolderSelection(false)
|
||||
setShowFolderSelectionOnStart(false)
|
||||
setIsAdvancedSettingsOpen(false)
|
||||
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"
|
||||
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">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
<FolderSelectionView
|
||||
onSelectFolder={handleSelectFolder}
|
||||
isLoading={isSelectingFolder()}
|
||||
advancedSettingsOpen={isAdvancedSettingsOpen()}
|
||||
onAdvancedSettingsOpen={() => setIsAdvancedSettingsOpen(true)}
|
||||
onAdvancedSettingsClose={() => setIsAdvancedSettingsOpen(false)}
|
||||
/>
|
||||
<Show when={showFolderSelection()}>
|
||||
<div class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center">
|
||||
<div class="w-full h-full relative">
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowFolderSelection(false)
|
||||
setShowFolderSelectionOnStart(false)
|
||||
setIsAdvancedSettingsOpen(false)
|
||||
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"
|
||||
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">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
<FolderSelectionView
|
||||
onSelectFolder={handleSelectFolder}
|
||||
isLoading={isSelectingFolder()}
|
||||
advancedSettingsOpen={isAdvancedSettingsOpen()}
|
||||
onAdvancedSettingsOpen={() => setIsAdvancedSettingsOpen(true)}
|
||||
onAdvancedSettingsClose={() => setIsAdvancedSettingsOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
|
||||
<RemoteAccessOverlay open={remoteAccessOpen()} onClose={() => setRemoteAccessOpen(false)} />
|
||||
<RemoteAccessOverlay open={remoteAccessOpen()} onClose={() => setRemoteAccessOpen(false)} />
|
||||
|
||||
<AlertDialog />
|
||||
<AlertDialog />
|
||||
|
||||
<Toaster
|
||||
position="top-right"
|
||||
gutter={16}
|
||||
toastOptions={{
|
||||
duration: 8000,
|
||||
className: "bg-transparent border-none shadow-none p-0",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Toaster
|
||||
position="top-right"
|
||||
gutter={8}
|
||||
containerClassName=""
|
||||
containerStyle={{}}
|
||||
toastOptions={{
|
||||
className: "",
|
||||
duration: 5000,
|
||||
style: {
|
||||
background: "#363636",
|
||||
color: "#fff",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Show>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
161
packages/ui/src/components/auth/LoginView.tsx
Normal file
161
packages/ui/src/components/auth/LoginView.tsx
Normal 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
|
||||
@@ -1,20 +1,23 @@
|
||||
/**
|
||||
* User Context utilities for frontend
|
||||
* Handles active user ID and passes it in API requests
|
||||
*/
|
||||
import { createSignal } from "solid-js"
|
||||
|
||||
// Storage key for active user
|
||||
const ACTIVE_USER_KEY = "codenomad_active_user_id"
|
||||
|
||||
const [isLoggedIn, setLoggedIn] = createSignal(false)
|
||||
|
||||
export { isLoggedIn, setLoggedIn }
|
||||
|
||||
/**
|
||||
* Set the active user ID
|
||||
*/
|
||||
export function setActiveUserId(userId: string | null): void {
|
||||
if (userId) {
|
||||
localStorage.setItem(ACTIVE_USER_KEY, userId)
|
||||
setLoggedIn(true)
|
||||
console.log(`[UserContext] Active user set to: ${userId}`)
|
||||
} else {
|
||||
localStorage.removeItem(ACTIVE_USER_KEY)
|
||||
setLoggedIn(false)
|
||||
console.log(`[UserContext] Active user cleared`)
|
||||
}
|
||||
}
|
||||
@@ -72,25 +75,22 @@ export async function initializeUserContext(): Promise<void> {
|
||||
setActiveUserId(activeUser.id)
|
||||
console.log(`[UserContext] Initialized with user: ${activeUser.id} (${activeUser.name})`)
|
||||
} else {
|
||||
setLoggedIn(false)
|
||||
console.log(`[UserContext] No active user from IPC`)
|
||||
}
|
||||
} else {
|
||||
// Web mode - try to get from localStorage or use default
|
||||
const existingId = getActiveUserId()
|
||||
if (existingId) {
|
||||
setLoggedIn(true)
|
||||
console.log(`[UserContext] Using cached user ID: ${existingId}`)
|
||||
} else {
|
||||
// Set a default user ID for web mode
|
||||
const defaultUserId = "default"
|
||||
setActiveUserId(defaultUserId)
|
||||
console.log(`[UserContext] Web mode - using default user ID`)
|
||||
setLoggedIn(false)
|
||||
console.log(`[UserContext] Web mode - no active user`)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[UserContext] Failed to initialize:`, error)
|
||||
// Fall back to default
|
||||
if (!getActiveUserId()) {
|
||||
setActiveUserId("default")
|
||||
}
|
||||
setLoggedIn(false)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user