feat(login): redesign login page with responsive split layout#34
Conversation
Add a modern split-panel login page inspired by contemporary SaaS designs. - Left panel: gradient branding section with hero text, feature highlights, and supported database badges (hidden on mobile) - Right panel: login form with preserved authentication functionality - Mobile: compact branding header with login form and database pills - All existing auth logic (local + OIDC) remains untouched Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Change mobile branding heading from h1 to h2 to maintain single h1 per page for proper accessibility hierarchy - Update LoginPage test to use getAllByText for 'LibreDB Studio' since the split-panel layout now renders the title in multiple locations Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
96fdd8b to
bcb6cc8
Compare
On desktop, the SSO provider's login page now opens as a separate window positioned on the right half of the screen, creating a side-by-side experience with the main login page on the left. On mobile, the flow gracefully falls back to a full-page redirect. Changes: - Add handleSSO function with responsive window management - Enrich OIDC card UI with ShieldCheck icon and security badges - Store popup mode flag via cookie for callback coordination - Auto-close SSO window on completion and verify auth state - Clean up oidc-mode cookie in both success and error paths Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
bcb6cc8 to
a002d58
Compare
There was a problem hiding this comment.
Pull request overview
Redesigns the login page into a responsive split-panel layout and adds a desktop popup-based OIDC SSO flow (with server-side callback support) while keeping existing local login behavior intact.
Changes:
- Updated
LoginFormUI to a split branding/form layout, including a new desktop popup SSO flow and mobile fallback redirect. - Added
oidc-modecookie handling in OIDC login + callback routes to support popup closing behavior. - Adjusted the login page component test to account for multiple rendered “LibreDB Studio” titles.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
src/app/login/login-form.tsx |
New split layout and popup SSO handling logic for OIDC mode. |
src/app/api/auth/oidc/login/route.ts |
Sets oidc-mode cookie for popup flows; closes window on popup-mode config error. |
src/app/api/auth/oidc/callback/route.ts |
Reads/clears oidc-mode and returns window.close() HTML for popup flows. |
tests/components/LoginPage.test.tsx |
Updates title assertion to handle multiple occurrences. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
src/app/login/login-form.tsx
Outdated
| fetch('/api/auth/me').then(res => res.json()).then(data => { | ||
| if (data.authenticated) { | ||
| router.push(data.role === 'admin' ? '/admin' : '/'); | ||
| } | ||
| }).catch(() => {}); |
There was a problem hiding this comment.
/api/auth/me returns { authenticated: true, user: session } (see src/app/api/auth/me/route.ts), but this code reads data.role. That will always be undefined, causing admins to be redirected to / after popup SSO. Read the role from data.user.role (and consider guarding for non-200 responses).
| fetch('/api/auth/me').then(res => res.json()).then(data => { | |
| if (data.authenticated) { | |
| router.push(data.role === 'admin' ? '/admin' : '/'); | |
| } | |
| }).catch(() => {}); | |
| fetch('/api/auth/me') | |
| .then(res => { | |
| if (!res.ok) { | |
| return null; | |
| } | |
| return res.json(); | |
| }) | |
| .then(data => { | |
| if (data && data.authenticated) { | |
| const role = data.user?.role; | |
| router.push(role === 'admin' ? '/admin' : '/'); | |
| } | |
| }) | |
| .catch(() => {}); |
src/app/login/login-form.tsx
Outdated
| // Listen for SSO window close | ||
| const checkSSO = setInterval(() => { | ||
| if (!ssoWindow || ssoWindow.closed) { | ||
| clearInterval(checkSSO); | ||
| setIsLoading(false); | ||
| router.refresh(); | ||
| // Check if auth succeeded | ||
| fetch('/api/auth/me').then(res => res.json()).then(data => { | ||
| if (data.authenticated) { | ||
| router.push(data.role === 'admin' ? '/admin' : '/'); | ||
| } | ||
| }).catch(() => {}); | ||
| } | ||
| }, 500); |
There was a problem hiding this comment.
The polling interval created by setInterval isn’t cleaned up if the component unmounts (e.g., navigation away while the popup is still open). That can lead to state updates on an unmounted component. Store the interval id (e.g., in a ref) and clear it in a useEffect cleanup, and/or stop polling when navigation occurs.
src/app/api/auth/oidc/login/route.ts
Outdated
| sameSite: 'lax', | ||
| maxAge: 300, | ||
| path: '/', | ||
| }); |
There was a problem hiding this comment.
oidc-mode is set as a standalone cookie and callback trusts it to decide whether to window.close(). If the user closes the popup before completing auth, the cookie can remain for up to 5 minutes; a subsequent non-popup OIDC flow in the same browser could then hit the callback and incorrectly return window.close() instead of redirecting. Consider binding the mode flag to the current OIDC state (e.g., store popup:<state> and validate it in the callback), and/or explicitly clear any stale oidc-mode cookie when mode !== popup.
| }); | |
| }); | |
| } else { | |
| // Clear any stale popup mode flag so non-popup flows are not misclassified | |
| cookieStore.set('oidc-mode', '', { | |
| httpOnly: true, | |
| secure: process.env.NODE_ENV === 'production', | |
| sameSite: 'lax', | |
| maxAge: 0, | |
| path: '/', | |
| }); |
| // Check if this was a popup login | ||
| const isPopup = cookieStore.get('oidc-mode')?.value === 'popup'; | ||
|
|
||
| // Clean up cookies | ||
| cookieStore.delete('oidc-state'); | ||
| cookieStore.delete('oidc-mode'); | ||
|
|
||
| if (isPopup) { | ||
| // Close popup and let the parent window detect the auth via /api/auth/me | ||
| return new NextResponse( | ||
| '<html><body><script>window.close();</script></body></html>', | ||
| { headers: { 'Content-Type': 'text/html' } } | ||
| ); | ||
| } |
There was a problem hiding this comment.
Popup handling is determined solely by cookieStore.get('oidc-mode') === 'popup'. To avoid incorrectly treating a non-popup callback as a popup (e.g., due to a stale cookie from an abandoned popup attempt), validate that the mode cookie corresponds to the current login attempt (tie it to the decrypted OIDC state), and fall back to the normal redirect when it doesn’t match.
Popup-based SSO flow (window.open + window.moveTo/resizeTo) is unreliable because modern browsers block window manipulation APIs. Revert to the proven full-page redirect approach while keeping all UI improvements (split-panel layout, responsive design, security badges). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace violet/purple/emerald gradient with zinc-950 base and blue accent glows to match the app's premium dark aesthetic. Update feature content to align with libredb.org website messaging. Changes: - Background: zinc-950 + subtle blue-950/cyan-950 gradient overlay - Accent color: blue-400 (matching app's primary) instead of emerald - Feature cards: glassmorphism (bg-white/[0.03], border-white/[0.05]) - Content: website-aligned copy (7+ DB engines, AI-native, zero install) - Mobile: blue-tinted branding icon matching desktop panel - Ambient orbs: blue/cyan instead of purple/emerald Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Mobile footer was showing only 5 databases while desktop panel listed all 7. Sync both lists to show the complete set. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Description
Redesigns the login page with a modern split-panel layout while preserving all existing authentication logic untouched.
Left panel (desktop only): Branding area with gradient background, feature highlights, and supported database badges.
Right panel: Login form — identical functionality, refined presentation.
Mobile: Left panel hidden, compact branding shown above the form with smooth responsive transition.
Type of Change
Changes Made
src/app/login/login-form.tsxhidden lg:flex), right login formwindow.openright half), falls back to full-page redirect on mobile<h1>to<h2>to avoid duplicate h1 tagssrc/app/api/auth/oidc/login/route.tsoidc-modecookie when?mode=popupis present, so the callback knows whether to close the window or redirectsrc/app/api/auth/oidc/callback/route.tsoidc-modecookie: returnswindow.close()in popup mode, redirects normally otherwiseoidc-modecookie in both success and error pathstests/components/LoginPage.test.tsxgetByText→getAllByTextfor "LibreDB Studio" since the split layout renders it in multiple locationsWhat is NOT changed
Testing
Test Environment
Test Results
Checklist