mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 20:23:36 +00:00
feat: update session cookie options and enhance authentication flow
- Changed SameSite attribute for session cookies from 'strict' to 'lax' to allow cross-origin fetches, improving compatibility with various client requests. - Updated cookie clearing logic in the authentication route to use `res.cookie()` for better reliability in cross-origin environments. - Refactored the login view to implement a state machine for managing authentication phases, enhancing clarity and maintainability. - Introduced a new logged-out view to inform users of session expiration and provide options to log in or retry. - Added account and security sections to the settings view, allowing users to manage their account and security preferences more effectively.
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { LogOut, User } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { logout } from '@/lib/http-api-client';
|
||||
import { useAuthStore } from '@/store/auth-store';
|
||||
|
||||
export function AccountSection() {
|
||||
const navigate = useNavigate();
|
||||
const [isLoggingOut, setIsLoggingOut] = useState(false);
|
||||
|
||||
const handleLogout = async () => {
|
||||
setIsLoggingOut(true);
|
||||
try {
|
||||
await logout();
|
||||
// Reset auth state
|
||||
useAuthStore.getState().resetAuth();
|
||||
// Navigate to logged out page
|
||||
navigate({ to: '/logged-out' });
|
||||
} catch (error) {
|
||||
console.error('Logout failed:', error);
|
||||
setIsLoggingOut(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-2xl overflow-hidden',
|
||||
'border border-border/50',
|
||||
'bg-gradient-to-br from-card/80 via-card/70 to-card/80 backdrop-blur-xl',
|
||||
'shadow-sm'
|
||||
)}
|
||||
>
|
||||
<div className="p-6 border-b border-border/30 bg-gradient-to-r from-primary/5 via-transparent to-transparent">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-primary/20 to-primary/10 flex items-center justify-center border border-primary/20">
|
||||
<User className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold text-foreground tracking-tight">Account</h2>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground/80 ml-12">Manage your session and account.</p>
|
||||
</div>
|
||||
<div className="p-6 space-y-4">
|
||||
{/* Logout */}
|
||||
<div className="flex items-center justify-between gap-4 p-4 rounded-xl bg-muted/30 border border-border/30">
|
||||
<div className="flex items-center gap-3.5 min-w-0">
|
||||
<div className="w-11 h-11 rounded-xl bg-gradient-to-br from-muted/50 to-muted/30 border border-border/30 flex items-center justify-center shrink-0">
|
||||
<LogOut className="w-5 h-5 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="font-medium text-foreground">Log Out</p>
|
||||
<p className="text-xs text-muted-foreground/70 mt-0.5">
|
||||
End your current session and return to the login screen
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleLogout}
|
||||
disabled={isLoggingOut}
|
||||
data-testid="logout-button"
|
||||
className={cn(
|
||||
'shrink-0 gap-2',
|
||||
'transition-all duration-200 ease-out',
|
||||
'hover:scale-[1.02] active:scale-[0.98]'
|
||||
)}
|
||||
>
|
||||
<LogOut className="w-4 h-4" />
|
||||
{isLoggingOut ? 'Logging out...' : 'Log Out'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { AccountSection } from './account-section';
|
||||
@@ -1,6 +1,7 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { Project } from '@/lib/electron';
|
||||
import type { NavigationItem } from '../config/navigation';
|
||||
import { GLOBAL_NAV_ITEMS, PROJECT_NAV_ITEMS } from '../config/navigation';
|
||||
import type { SettingsViewId } from '../hooks/use-settings-view';
|
||||
|
||||
interface SettingsNavigationProps {
|
||||
@@ -10,8 +11,53 @@ interface SettingsNavigationProps {
|
||||
onNavigate: (sectionId: SettingsViewId) => void;
|
||||
}
|
||||
|
||||
function NavButton({
|
||||
item,
|
||||
isActive,
|
||||
onNavigate,
|
||||
}: {
|
||||
item: NavigationItem;
|
||||
isActive: boolean;
|
||||
onNavigate: (sectionId: SettingsViewId) => void;
|
||||
}) {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => onNavigate(item.id)}
|
||||
className={cn(
|
||||
'group w-full flex items-center gap-2.5 px-3 py-2.5 rounded-xl text-sm font-medium transition-all duration-200 ease-out text-left relative overflow-hidden',
|
||||
isActive
|
||||
? [
|
||||
'bg-gradient-to-r from-brand-500/15 via-brand-500/10 to-brand-600/5',
|
||||
'text-foreground',
|
||||
'border border-brand-500/25',
|
||||
'shadow-sm shadow-brand-500/5',
|
||||
]
|
||||
: [
|
||||
'text-muted-foreground hover:text-foreground',
|
||||
'hover:bg-accent/50',
|
||||
'border border-transparent hover:border-border/40',
|
||||
],
|
||||
'hover:scale-[1.01] active:scale-[0.98]'
|
||||
)}
|
||||
>
|
||||
{/* Active indicator bar */}
|
||||
{isActive && (
|
||||
<div className="absolute inset-y-0 left-0 w-0.5 bg-gradient-to-b from-brand-400 via-brand-500 to-brand-600 rounded-r-full" />
|
||||
)}
|
||||
<Icon
|
||||
className={cn(
|
||||
'w-4 h-4 shrink-0 transition-all duration-200',
|
||||
isActive ? 'text-brand-500' : 'group-hover:text-brand-400 group-hover:scale-110'
|
||||
)}
|
||||
/>
|
||||
<span className="truncate">{item.label}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingsNavigation({
|
||||
navItems,
|
||||
activeSection,
|
||||
currentProject,
|
||||
onNavigate,
|
||||
@@ -19,52 +65,53 @@ export function SettingsNavigation({
|
||||
return (
|
||||
<nav
|
||||
className={cn(
|
||||
'hidden lg:block w-52 shrink-0',
|
||||
'hidden lg:block w-52 shrink-0 overflow-y-auto',
|
||||
'border-r border-border/50',
|
||||
'bg-gradient-to-b from-card/80 via-card/60 to-card/40 backdrop-blur-xl'
|
||||
)}
|
||||
>
|
||||
<div className="sticky top-0 p-4 space-y-1.5">
|
||||
{navItems
|
||||
.filter((item) => item.id !== 'danger' || currentProject)
|
||||
.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const isActive = activeSection === item.id;
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => onNavigate(item.id)}
|
||||
className={cn(
|
||||
'group w-full flex items-center gap-2.5 px-3 py-2.5 rounded-xl text-sm font-medium transition-all duration-200 ease-out text-left relative overflow-hidden',
|
||||
isActive
|
||||
? [
|
||||
'bg-gradient-to-r from-brand-500/15 via-brand-500/10 to-brand-600/5',
|
||||
'text-foreground',
|
||||
'border border-brand-500/25',
|
||||
'shadow-sm shadow-brand-500/5',
|
||||
]
|
||||
: [
|
||||
'text-muted-foreground hover:text-foreground',
|
||||
'hover:bg-accent/50',
|
||||
'border border-transparent hover:border-border/40',
|
||||
],
|
||||
'hover:scale-[1.01] active:scale-[0.98]'
|
||||
)}
|
||||
>
|
||||
{/* Active indicator bar */}
|
||||
{isActive && (
|
||||
<div className="absolute inset-y-0 left-0 w-0.5 bg-gradient-to-b from-brand-400 via-brand-500 to-brand-600 rounded-r-full" />
|
||||
)}
|
||||
<Icon
|
||||
className={cn(
|
||||
'w-4 h-4 shrink-0 transition-all duration-200',
|
||||
isActive ? 'text-brand-500' : 'group-hover:text-brand-400 group-hover:scale-110'
|
||||
)}
|
||||
<div className="sticky top-0 p-4 space-y-1">
|
||||
{/* Global Settings Label */}
|
||||
<div className="px-3 py-2 text-xs font-semibold text-muted-foreground/70 uppercase tracking-wider">
|
||||
Global Settings
|
||||
</div>
|
||||
|
||||
{/* Global Settings Items */}
|
||||
<div className="space-y-1">
|
||||
{GLOBAL_NAV_ITEMS.map((item) => (
|
||||
<NavButton
|
||||
key={item.id}
|
||||
item={item}
|
||||
isActive={activeSection === item.id}
|
||||
onNavigate={onNavigate}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Project Settings - only show when a project is selected */}
|
||||
{currentProject && (
|
||||
<>
|
||||
{/* Divider */}
|
||||
<div className="my-4 border-t border-border/50" />
|
||||
|
||||
{/* Project Settings Label */}
|
||||
<div className="px-3 py-2 text-xs font-semibold text-muted-foreground/70 uppercase tracking-wider">
|
||||
Project Settings
|
||||
</div>
|
||||
|
||||
{/* Project Settings Items */}
|
||||
<div className="space-y-1">
|
||||
{PROJECT_NAV_ITEMS.map((item) => (
|
||||
<NavButton
|
||||
key={item.id}
|
||||
item={item}
|
||||
isActive={activeSection === item.id}
|
||||
onNavigate={onNavigate}
|
||||
/>
|
||||
<span className="truncate">{item.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
Workflow,
|
||||
Plug,
|
||||
MessageSquareText,
|
||||
User,
|
||||
Shield,
|
||||
} from 'lucide-react';
|
||||
import type { SettingsViewId } from '../hooks/use-settings-view';
|
||||
|
||||
@@ -20,8 +22,13 @@ export interface NavigationItem {
|
||||
icon: LucideIcon;
|
||||
}
|
||||
|
||||
// Navigation items for the settings side panel
|
||||
export const NAV_ITEMS: NavigationItem[] = [
|
||||
export interface NavigationGroup {
|
||||
label: string;
|
||||
items: NavigationItem[];
|
||||
}
|
||||
|
||||
// Global settings - always visible
|
||||
export const GLOBAL_NAV_ITEMS: NavigationItem[] = [
|
||||
{ id: 'api-keys', label: 'API Keys', icon: Key },
|
||||
{ id: 'providers', label: 'AI Providers', icon: Bot },
|
||||
{ id: 'mcp-servers', label: 'MCP Servers', icon: Plug },
|
||||
@@ -32,5 +39,14 @@ export const NAV_ITEMS: NavigationItem[] = [
|
||||
{ id: 'keyboard', label: 'Keyboard Shortcuts', icon: Settings2 },
|
||||
{ id: 'audio', label: 'Audio', icon: Volume2 },
|
||||
{ id: 'defaults', label: 'Feature Defaults', icon: FlaskConical },
|
||||
{ id: 'account', label: 'Account', icon: User },
|
||||
{ id: 'security', label: 'Security', icon: Shield },
|
||||
];
|
||||
|
||||
// Project-specific settings - only visible when a project is selected
|
||||
export const PROJECT_NAV_ITEMS: NavigationItem[] = [
|
||||
{ id: 'danger', label: 'Danger Zone', icon: Trash2 },
|
||||
];
|
||||
|
||||
// Legacy export for backwards compatibility
|
||||
export const NAV_ITEMS: NavigationItem[] = [...GLOBAL_NAV_ITEMS, ...PROJECT_NAV_ITEMS];
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Trash2, Folder, AlertTriangle, Shield, RotateCcw } from 'lucide-react';
|
||||
import { Trash2, Folder, AlertTriangle } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { Project } from '../shared/types';
|
||||
|
||||
interface DangerZoneSectionProps {
|
||||
project: Project | null;
|
||||
onDeleteClick: () => void;
|
||||
skipSandboxWarning: boolean;
|
||||
onResetSandboxWarning: () => void;
|
||||
}
|
||||
|
||||
export function DangerZoneSection({
|
||||
project,
|
||||
onDeleteClick,
|
||||
skipSandboxWarning,
|
||||
onResetSandboxWarning,
|
||||
}: DangerZoneSectionProps) {
|
||||
export function DangerZoneSection({ project, onDeleteClick }: DangerZoneSectionProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -32,43 +25,11 @@ export function DangerZoneSection({
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold text-foreground tracking-tight">Danger Zone</h2>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground/80 ml-12">
|
||||
Destructive actions and reset options.
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground/80 ml-12">Destructive project actions.</p>
|
||||
</div>
|
||||
<div className="p-6 space-y-4">
|
||||
{/* Sandbox Warning Reset */}
|
||||
{skipSandboxWarning && (
|
||||
<div className="flex items-center justify-between gap-4 p-4 rounded-xl bg-destructive/5 border border-destructive/10">
|
||||
<div className="flex items-center gap-3.5 min-w-0">
|
||||
<div className="w-11 h-11 rounded-xl bg-gradient-to-br from-destructive/15 to-destructive/10 border border-destructive/20 flex items-center justify-center shrink-0">
|
||||
<Shield className="w-5 h-5 text-destructive" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="font-medium text-foreground">Sandbox Warning Disabled</p>
|
||||
<p className="text-xs text-muted-foreground/70 mt-0.5">
|
||||
The sandbox environment warning is hidden on startup
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onResetSandboxWarning}
|
||||
data-testid="reset-sandbox-warning-button"
|
||||
className={cn(
|
||||
'shrink-0 gap-2',
|
||||
'transition-all duration-200 ease-out',
|
||||
'hover:scale-[1.02] active:scale-[0.98]'
|
||||
)}
|
||||
>
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Project Delete */}
|
||||
{project && (
|
||||
{project ? (
|
||||
<div className="flex items-center justify-between gap-4 p-4 rounded-xl bg-destructive/5 border border-destructive/10">
|
||||
<div className="flex items-center gap-3.5 min-w-0">
|
||||
<div className="w-11 h-11 rounded-xl bg-gradient-to-br from-brand-500/15 to-brand-600/10 border border-brand-500/20 flex items-center justify-center shrink-0">
|
||||
@@ -94,13 +55,8 @@ export function DangerZoneSection({
|
||||
Delete Project
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Empty state when nothing to show */}
|
||||
{!skipSandboxWarning && !project && (
|
||||
<p className="text-sm text-muted-foreground/60 text-center py-4">
|
||||
No danger zone actions available.
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground/60 text-center py-4">No project selected.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,6 +12,8 @@ export type SettingsViewId =
|
||||
| 'keyboard'
|
||||
| 'audio'
|
||||
| 'defaults'
|
||||
| 'account'
|
||||
| 'security'
|
||||
| 'danger';
|
||||
|
||||
interface UseSettingsViewOptions {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { SecuritySection } from './security-section';
|
||||
@@ -0,0 +1,71 @@
|
||||
import { Shield, AlertTriangle } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Label } from '@/components/ui/label';
|
||||
|
||||
interface SecuritySectionProps {
|
||||
skipSandboxWarning: boolean;
|
||||
onSkipSandboxWarningChange: (skip: boolean) => void;
|
||||
}
|
||||
|
||||
export function SecuritySection({
|
||||
skipSandboxWarning,
|
||||
onSkipSandboxWarningChange,
|
||||
}: SecuritySectionProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-2xl overflow-hidden',
|
||||
'border border-border/50',
|
||||
'bg-gradient-to-br from-card/80 via-card/70 to-card/80 backdrop-blur-xl',
|
||||
'shadow-sm'
|
||||
)}
|
||||
>
|
||||
<div className="p-6 border-b border-border/30 bg-gradient-to-r from-primary/5 via-transparent to-transparent">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-primary/20 to-primary/10 flex items-center justify-center border border-primary/20">
|
||||
<Shield className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold text-foreground tracking-tight">Security</h2>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground/80 ml-12">
|
||||
Configure security warnings and protections.
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 space-y-4">
|
||||
{/* Sandbox Warning Toggle */}
|
||||
<div className="flex items-center justify-between gap-4 p-4 rounded-xl bg-muted/30 border border-border/30">
|
||||
<div className="flex items-center gap-3.5 min-w-0">
|
||||
<div className="w-11 h-11 rounded-xl bg-gradient-to-br from-amber-500/15 to-amber-600/10 border border-amber-500/20 flex items-center justify-center shrink-0">
|
||||
<AlertTriangle className="w-5 h-5 text-amber-500" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<Label
|
||||
htmlFor="sandbox-warning-toggle"
|
||||
className="font-medium text-foreground cursor-pointer"
|
||||
>
|
||||
Show Sandbox Warning on Startup
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground/70 mt-0.5">
|
||||
Display a security warning when not running in a sandboxed environment
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
id="sandbox-warning-toggle"
|
||||
checked={!skipSandboxWarning}
|
||||
onCheckedChange={(checked) => onSkipSandboxWarningChange(!checked)}
|
||||
data-testid="sandbox-warning-toggle"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Info text */}
|
||||
<p className="text-xs text-muted-foreground/60 px-4">
|
||||
When enabled, you'll see a warning on app startup if you're not running in a
|
||||
containerized environment (like Docker). This helps remind you to use proper isolation
|
||||
when running AI agents.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user