From 1e2bce9538a89962fd5a187f930d0503e084a636 Mon Sep 17 00:00:00 2001 From: yusuf gundogdu Date: Tue, 3 Mar 2026 02:05:08 +0300 Subject: [PATCH 1/6] feat(login): redesign login page with responsive split layout 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 --- src/app/login/login-form.tsx | 357 +++++++++++++++++++++++------------ 1 file changed, 233 insertions(+), 124 deletions(-) diff --git a/src/app/login/login-form.tsx b/src/app/login/login-form.tsx index 6ed9af8..d44524a 100644 --- a/src/app/login/login-form.tsx +++ b/src/app/login/login-form.tsx @@ -6,7 +6,7 @@ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import { Label } from '@/components/ui/label'; -import { Database, ExternalLink, Lock, Mail, ShieldCheck, UserCheck } from 'lucide-react'; +import { Database, ExternalLink, Lock, Mail, ShieldCheck, UserCheck, Zap, Globe, Shield, BarChart3 } from 'lucide-react'; import { toast } from 'sonner'; import { Badge } from '@/components/ui/badge'; @@ -53,141 +53,250 @@ function LoginFormInner({ authProvider }: { authProvider: string }) { } }; + const features = [ + { icon: Globe, title: 'Multi-Database Support', desc: 'PostgreSQL, MySQL, SQLite, MongoDB, Redis & more' }, + { icon: Zap, title: 'AI-Powered Queries', desc: 'Natural language to SQL with intelligent suggestions' }, + { icon: Shield, title: 'Enterprise Security', desc: 'JWT auth, OIDC SSO, role-based access control' }, + { icon: BarChart3, title: 'Visual Schema Explorer', desc: 'Explore tables, relations, and data visually' }, + ]; + return ( -
- - -
-
-
-
- -
+
+ {/* Left Panel - Branding (hidden on mobile) */} +
+ {/* Gradient background */} +
+ + {/* Decorative grid pattern */} +
+ + {/* Floating gradient orbs */} +
+
+
+ + {/* Content */} +
+ {/* Top: Logo */} +
+
+
+ LibreDB Studio
-
- LibreDB Studio - - Secure database administration and management portal - -
- - - - {isOIDC ? ( - <> - {oidcError && ( -
- Authentication failed. Please try again. -
- )} - - - ) : ( - <> -
-
- -
- - setEmail(e.target.value)} - required - /> + + {/* Middle: Hero text + Features */} +
+
+

+ Your fastest path to + database mastery +

+

+ Open-source database studio to query, explore, and manage all your databases from a single, powerful interface. +

+
+ +
+ {features.map((feature) => ( +
+
+
-
-
- -
- - setPassword(e.target.value)} - required - /> +
+

{feature.title}

+

{feature.desc}

-
+ + {/* Bottom: DB badges */} +
+

Supported Databases

+
+ {['PostgreSQL', 'MySQL', 'SQLite', 'MongoDB', 'Redis', 'Oracle', 'SQL Server'].map((db) => ( + - {isLoading ? 'Authenticating...' : 'Sign In'} - - + {db} + + ))} +
+
+
+
-
-
- -
-
- Quick Access for Demo -
+ {/* Right Panel - Login Form */} +
+
+ {/* Mobile branding (visible only on mobile) */} +
+
+
+
+
+
+
+

LibreDB Studio

+

Open-source database management

+
+
-
- + + ) : ( + <> +
+
+ +
+ + setEmail(e.target.value)} + required + /> +
+
+
+ +
+ + setPassword(e.target.value)} + required + /> +
+
+ +
+ +
+
+ +
+
+ Quick Access for Demo +
- - admin@libredb.org - - - - + +
- - user@libredb.org - - -
- - )} - - - -

- Enterprise-grade security powered by LibreDB Studio Engine -

- - v{process.env.NEXT_PUBLIC_APP_VERSION} - -
- + + )} + + + +

+ Enterprise-grade security powered by LibreDB Studio Engine +

+ + v{process.env.NEXT_PUBLIC_APP_VERSION} + +
+ + + {/* Mobile feature pills */} +
+ {['PostgreSQL', 'MySQL', 'MongoDB', 'Redis', 'SQLite'].map((db) => ( + + {db} + + ))} +
+
+
); } From 96fdd8b97f26da346106789f6652335fd7168bab Mon Sep 17 00:00:00 2001 From: yusuf gundogdu Date: Wed, 4 Mar 2026 01:17:33 +0300 Subject: [PATCH 2/6] fix(login): resolve CI test failure and improve accessibility - 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 --- src/app/login/login-form.tsx | 2 +- tests/components/LoginPage.test.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/login/login-form.tsx b/src/app/login/login-form.tsx index d44524a..1da8d07 100644 --- a/src/app/login/login-form.tsx +++ b/src/app/login/login-form.tsx @@ -150,7 +150,7 @@ function LoginFormInner({ authProvider }: { authProvider: string }) {
-

LibreDB Studio

+

LibreDB Studio

Open-source database management

diff --git a/tests/components/LoginPage.test.tsx b/tests/components/LoginPage.test.tsx index 5606932..c948baa 100644 --- a/tests/components/LoginPage.test.tsx +++ b/tests/components/LoginPage.test.tsx @@ -47,8 +47,8 @@ describe('LoginPage', () => { }); test('renders LibreDB Studio title', () => { - const { getByText } = renderLogin(); - expect(getByText('LibreDB Studio')).not.toBeNull(); + const { getAllByText } = renderLogin(); + expect(getAllByText('LibreDB Studio').length).toBeGreaterThanOrEqual(1); }); test('renders quick access Admin and User buttons', () => { From a002d581e483c65cd17cddde91c14abb51e7dd2e Mon Sep 17 00:00:00 2001 From: yusuf gundogdu Date: Wed, 4 Mar 2026 01:54:58 +0300 Subject: [PATCH 3/6] feat(auth): enhance OIDC login UX with side-by-side SSO window 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 --- src/app/api/auth/oidc/callback/route.ts | 26 ++++++++- src/app/api/auth/oidc/login/route.ts | 20 +++++++ src/app/login/login-form.tsx | 73 +++++++++++++++++++++++-- 3 files changed, 114 insertions(+), 5 deletions(-) diff --git a/src/app/api/auth/oidc/callback/route.ts b/src/app/api/auth/oidc/callback/route.ts index 0655346..8252f9f 100644 --- a/src/app/api/auth/oidc/callback/route.ts +++ b/src/app/api/auth/oidc/callback/route.ts @@ -63,8 +63,20 @@ export async function GET(request: Request) { const username = claims.email || claims.preferred_username || claims.sub || role; await login(role, username); - // Clean up state cookie + // 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( + '', + { headers: { 'Content-Type': 'text/html' } } + ); + } // Redirect based on role return NextResponse.redirect( @@ -75,6 +87,18 @@ export async function GET(request: Request) { if (error instanceof Error && 'cause' in error) { console.error('OIDC error cause:', error.cause); } + + // Clean up popup cookie on error too + const cookieStore2 = await cookies(); + const isPopup = cookieStore2.get('oidc-mode')?.value === 'popup'; + cookieStore2.delete('oidc-mode'); + + if (isPopup) { + return new NextResponse( + '', + { headers: { 'Content-Type': 'text/html' } } + ); + } return NextResponse.redirect(`${origin}/login?error=oidc_failed`); } } diff --git a/src/app/api/auth/oidc/login/route.ts b/src/app/api/auth/oidc/login/route.ts index 4c61042..efdd39e 100644 --- a/src/app/api/auth/oidc/login/route.ts +++ b/src/app/api/auth/oidc/login/route.ts @@ -33,10 +33,30 @@ export async function GET(request: Request) { path: '/', }); + // Store popup mode flag so callback knows to close the window + const requestUrl = new URL(request.url); + if (requestUrl.searchParams.get('mode') === 'popup') { + cookieStore.set('oidc-mode', 'popup', { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + maxAge: 300, + path: '/', + }); + } + return NextResponse.redirect(url.toString()); } catch (error) { console.error('OIDC login error:', error); const origin = getPublicOrigin(request); + const requestUrl = new URL(request.url); + const isPopup = requestUrl.searchParams.get('mode') === 'popup'; + if (isPopup) { + return new NextResponse( + '', + { headers: { 'Content-Type': 'text/html' } } + ); + } return NextResponse.redirect(`${origin}/login?error=oidc_config`); } } diff --git a/src/app/login/login-form.tsx b/src/app/login/login-form.tsx index 1da8d07..c58ab1f 100644 --- a/src/app/login/login-form.tsx +++ b/src/app/login/login-form.tsx @@ -19,6 +19,50 @@ function LoginFormInner({ authProvider }: { authProvider: string }) { const searchParams = useSearchParams(); const oidcError = searchParams.get('error'); + const handleSSO = () => { + setIsLoading(true); + const isMobile = window.innerWidth < 768; + + if (isMobile) { + window.location.href = '/api/auth/oidc/login?mode=redirect'; + return; + } + + // Desktop: open SSO as a side-by-side window on the right half of the screen + const screenW = window.screen.availWidth; + const screenH = window.screen.availHeight; + const halfW = Math.round(screenW / 2); + + const ssoWindow = window.open( + '/api/auth/oidc/login?mode=popup', + 'sso-login', + `width=${halfW},height=${screenH},left=${halfW},top=0,toolbar=no,menubar=no,scrollbars=yes` + ); + + // Move main window to the left half + try { + window.moveTo(0, 0); + window.resizeTo(halfW, screenH); + } catch { + // Some browsers block window resize - that's fine + } + + // 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); + }; + const handleLogin = async (e?: React.FormEvent, directEmail?: string, directPassword?: string) => { if (e) e.preventDefault(); const loginEmail = directEmail || email; @@ -176,17 +220,38 @@ function LoginFormInner({ authProvider }: { authProvider: string }) { Authentication failed. Please try again.
)} + +
+
+ +
+
+

Single Sign-On

+

+ Sign in securely with your organization's identity provider +

+
+
+ + +
+
+ + Encrypted +
+
+ + OIDC Protected +
+
) : ( <> From 1d6bd63ec20e43c6bca21ffb3cee1be3839bb693 Mon Sep 17 00:00:00 2001 From: cevheri Date: Sat, 7 Mar 2026 22:39:14 +0300 Subject: [PATCH 4/6] refactor(login): remove popup SSO window in favor of standard redirect 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 --- src/app/api/auth/oidc/callback/route.ts | 25 +------------ src/app/api/auth/oidc/login/route.ts | 20 ---------- src/app/login/login-form.tsx | 49 ++----------------------- 3 files changed, 5 insertions(+), 89 deletions(-) diff --git a/src/app/api/auth/oidc/callback/route.ts b/src/app/api/auth/oidc/callback/route.ts index 8252f9f..cb35623 100644 --- a/src/app/api/auth/oidc/callback/route.ts +++ b/src/app/api/auth/oidc/callback/route.ts @@ -63,20 +63,8 @@ export async function GET(request: Request) { const username = claims.email || claims.preferred_username || claims.sub || role; await login(role, username); - // Check if this was a popup login - const isPopup = cookieStore.get('oidc-mode')?.value === 'popup'; - - // Clean up cookies + // Clean up state cookie 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( - '', - { headers: { 'Content-Type': 'text/html' } } - ); - } // Redirect based on role return NextResponse.redirect( @@ -88,17 +76,6 @@ export async function GET(request: Request) { console.error('OIDC error cause:', error.cause); } - // Clean up popup cookie on error too - const cookieStore2 = await cookies(); - const isPopup = cookieStore2.get('oidc-mode')?.value === 'popup'; - cookieStore2.delete('oidc-mode'); - - if (isPopup) { - return new NextResponse( - '', - { headers: { 'Content-Type': 'text/html' } } - ); - } return NextResponse.redirect(`${origin}/login?error=oidc_failed`); } } diff --git a/src/app/api/auth/oidc/login/route.ts b/src/app/api/auth/oidc/login/route.ts index efdd39e..4c61042 100644 --- a/src/app/api/auth/oidc/login/route.ts +++ b/src/app/api/auth/oidc/login/route.ts @@ -33,30 +33,10 @@ export async function GET(request: Request) { path: '/', }); - // Store popup mode flag so callback knows to close the window - const requestUrl = new URL(request.url); - if (requestUrl.searchParams.get('mode') === 'popup') { - cookieStore.set('oidc-mode', 'popup', { - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - sameSite: 'lax', - maxAge: 300, - path: '/', - }); - } - return NextResponse.redirect(url.toString()); } catch (error) { console.error('OIDC login error:', error); const origin = getPublicOrigin(request); - const requestUrl = new URL(request.url); - const isPopup = requestUrl.searchParams.get('mode') === 'popup'; - if (isPopup) { - return new NextResponse( - '', - { headers: { 'Content-Type': 'text/html' } } - ); - } return NextResponse.redirect(`${origin}/login?error=oidc_config`); } } diff --git a/src/app/login/login-form.tsx b/src/app/login/login-form.tsx index c58ab1f..4295c0d 100644 --- a/src/app/login/login-form.tsx +++ b/src/app/login/login-form.tsx @@ -19,50 +19,6 @@ function LoginFormInner({ authProvider }: { authProvider: string }) { const searchParams = useSearchParams(); const oidcError = searchParams.get('error'); - const handleSSO = () => { - setIsLoading(true); - const isMobile = window.innerWidth < 768; - - if (isMobile) { - window.location.href = '/api/auth/oidc/login?mode=redirect'; - return; - } - - // Desktop: open SSO as a side-by-side window on the right half of the screen - const screenW = window.screen.availWidth; - const screenH = window.screen.availHeight; - const halfW = Math.round(screenW / 2); - - const ssoWindow = window.open( - '/api/auth/oidc/login?mode=popup', - 'sso-login', - `width=${halfW},height=${screenH},left=${halfW},top=0,toolbar=no,menubar=no,scrollbars=yes` - ); - - // Move main window to the left half - try { - window.moveTo(0, 0); - window.resizeTo(halfW, screenH); - } catch { - // Some browsers block window resize - that's fine - } - - // 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); - }; - const handleLogin = async (e?: React.FormEvent, directEmail?: string, directPassword?: string) => { if (e) e.preventDefault(); const loginEmail = directEmail || email; @@ -235,7 +191,10 @@ function LoginFormInner({ authProvider }: { authProvider: string }) {