diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx index a84316f2..1bcc3613 100644 --- a/src/app/(auth)/login/page.tsx +++ b/src/app/(auth)/login/page.tsx @@ -81,7 +81,6 @@ export default function LoginPage() { autoComplete="username" autoCapitalize="none" spellCheck={false} - placeholder="you@example.com or yourname" disabled={isLoading} className={cn(errors.identifier && 'border-destructive focus-visible:ring-destructive')} {...register('identifier')} diff --git a/src/app/api/auth/sign-in-by-identifier/route.ts b/src/app/api/auth/sign-in-by-identifier/route.ts index 00cdcea4..4a618658 100644 --- a/src/app/api/auth/sign-in-by-identifier/route.ts +++ b/src/app/api/auth/sign-in-by-identifier/route.ts @@ -93,6 +93,14 @@ export async function POST(req: NextRequest) { 'x-forwarded-for': req.headers.get('x-forwarded-for') ?? ip, 'user-agent': req.headers.get('user-agent') ?? '', cookie: req.headers.get('cookie') ?? '', + // CRITICAL: forward Origin + Referer so better-auth's CSRF check + // passes. Without these the internal call lands as a cross-origin + // request with no Origin → 403 MISSING_OR_NULL_ORIGIN, and the + // user sees a generic "Invalid credentials" toast even though + // the password is right. (Bug surfaced 2026-05-13 testing on + // 192.168.1.17:3000 from an iPad.) + ...(req.headers.get('origin') ? { origin: req.headers.get('origin')! } : {}), + ...(req.headers.get('referer') ? { referer: req.headers.get('referer')! } : {}), }, body: forwardBody, }); diff --git a/src/components/layout/user-menu.tsx b/src/components/layout/user-menu.tsx index bddd1d2a..173540d9 100644 --- a/src/components/layout/user-menu.tsx +++ b/src/components/layout/user-menu.tsx @@ -15,7 +15,7 @@ import { useRouter } from 'next/navigation'; import { useQueryClient } from '@tanstack/react-query'; -import { Moon, Sun, LogOut, User, Settings, Bell, Check, Building2 } from 'lucide-react'; +import { LogOut, User, Settings, Bell, Check, Building2 } from 'lucide-react'; import { type ReactNode } from 'react'; import { useUIStore } from '@/stores/ui-store'; @@ -51,17 +51,10 @@ export function UserMenu({ trigger, align = 'end', user, ports }: UserMenuProps) const currentPortId = useUIStore((s) => s.currentPortId); const currentPortSlug = useUIStore((s) => s.currentPortSlug); const setPort = useUIStore((s) => s.setPort); - const darkMode = useUIStore((s) => s.darkMode); - const toggleDarkMode = useUIStore((s) => s.toggleDarkMode); const base = currentPortSlug ? `/${currentPortSlug}` : ''; const showPortSwitcher = ports && ports.length > 1; - function handleToggleDarkMode() { - toggleDarkMode(); - document.documentElement.classList.toggle('dark'); - } - function handlePortChange(port: Port) { if (port.id === currentPortId) return; setPort(port.id, port.slug); @@ -132,20 +125,6 @@ export function UserMenu({ trigger, align = 'end', user, ports }: UserMenuProps) Notification preferences - - {darkMode ? ( - <> - - Light Mode - - ) : ( - <> - - Dark Mode - - )} - - router.push('/api/auth/sign-out')} diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx index 4662e1d6..2b6bac97 100644 --- a/src/components/ui/dialog.tsx +++ b/src/components/ui/dialog.tsx @@ -51,7 +51,12 @@ const DialogContent = React.forwardRef< 'max-h-dvh overflow-y-auto sm:max-h-[calc(100dvh-2rem)]', 'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0', 'sm:top-[50%] sm:right-auto sm:bottom-auto sm:left-[50%] sm:max-w-lg sm:translate-x-[-50%] sm:translate-y-[-50%] sm:border sm:rounded-lg', - 'sm:data-[state=closed]:zoom-out-95 sm:data-[state=open]:zoom-in-95 sm:data-[state=closed]:slide-out-to-left-1/2 sm:data-[state=closed]:slide-out-to-top-[48%] sm:data-[state=open]:slide-in-from-left-1/2 sm:data-[state=open]:slide-in-from-top-[48%]', + // Desktop animation: subtle centered fade + zoom (no slide-from- + // corner so the dialog appears in place rather than flying in + // from top-right). The base fade-in/out classes above provide + // the opacity transition; zoom-95 adds a 5% scale-in for + // depth without feeling jarring. + 'sm:data-[state=closed]:zoom-out-95 sm:data-[state=open]:zoom-in-95', className, )} {...props} diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx index 584f52a7..17a6ba0d 100644 --- a/src/components/ui/sonner.tsx +++ b/src/components/ui/sonner.tsx @@ -1,16 +1,16 @@ 'use client'; -import { useTheme } from 'next-themes'; import { Toaster as Sonner } from 'sonner'; type ToasterProps = React.ComponentProps; +// Dark mode is disabled across the app — hardcode the light theme so +// sonner doesn't pick up a `prefers-color-scheme: dark` system hint and +// render against a dark background that the rest of the UI doesn't use. const Toaster = ({ ...props }: ToasterProps) => { - const { theme = 'system' } = useTheme(); - return ( void; setPort: (portId: string, portSlug: string) => void; - toggleDarkMode: () => void; } export const useUIStore = create()( @@ -17,10 +15,8 @@ export const useUIStore = create()( sidebarCollapsed: false, currentPortId: null, currentPortSlug: null, - darkMode: false, toggleSidebar: () => set((s) => ({ sidebarCollapsed: !s.sidebarCollapsed })), setPort: (portId, portSlug) => set({ currentPortId: portId, currentPortSlug: portSlug }), - toggleDarkMode: () => set((s) => ({ darkMode: !s.darkMode })), }), { name: 'pn-crm-ui', @@ -30,7 +26,6 @@ export const useUIStore = create()( // the previous session before the URL-derived effect runs. partialize: (state) => ({ sidebarCollapsed: state.sidebarCollapsed, - darkMode: state.darkMode, }), }, ),