|
1 | | - |
2 | 1 | import React, { useEffect } from 'react'; |
3 | 2 | import { useNavigate, useLocation } from 'react-router-dom'; |
4 | | -import { useMsal, useIsAuthenticated } from "@azure/msal-react"; |
5 | | -import { loginRequest } from "../authConfig"; |
6 | | -import MongoIcon from '../components/icons/MongoIcon'; |
| 3 | +import { useMsal, useIsAuthenticated } from '@azure/msal-react'; |
| 4 | +import { loginRequest } from '../authConfig'; |
7 | 5 | import { USE_MSAL_AUTH } from '../app.config'; |
8 | 6 | import { useAuth } from '../contexts/AuthContext'; |
9 | | -import { UserIcon, MicrosoftIcon } from '../components/icons/material-icons-imports'; |
10 | 7 |
|
11 | | -// --- UI Component (Shared) --- |
| 8 | +// ── Icons ──────────────────────────────────────────────────────────────────── |
| 9 | + |
| 10 | +const MicrosoftLogo = () => ( |
| 11 | + <svg width="16" height="16" viewBox="0 0 21 21" style={{ flexShrink: 0 }}> |
| 12 | + <rect x="1" y="1" width="9" height="9" fill="#f25022" /> |
| 13 | + <rect x="11" y="1" width="9" height="9" fill="#7fba00" /> |
| 14 | + <rect x="1" y="11" width="9" height="9" fill="#00a4ef" /> |
| 15 | + <rect x="11" y="11" width="9" height="9" fill="#ffb900" /> |
| 16 | + </svg> |
| 17 | +); |
| 18 | + |
| 19 | +const DevIcon = () => ( |
| 20 | + <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" style={{ flexShrink: 0 }}> |
| 21 | + <circle cx="8" cy="5.5" r="2.5" /> |
| 22 | + <path d="M2.5 13.5c0-3.038 2.462-5.5 5.5-5.5s5.5 2.462 5.5 5.5" /> |
| 23 | + </svg> |
| 24 | +); |
| 25 | + |
| 26 | +// ── Feature list ───────────────────────────────────────────────────────────── |
| 27 | + |
| 28 | +const FEATURES = [ |
| 29 | + { |
| 30 | + icon: ( |
| 31 | + <svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round"> |
| 32 | + <path d="M2 4h12M2 8h8M2 12h5" /> |
| 33 | + </svg> |
| 34 | + ), |
| 35 | + label: 'AI query generation', |
| 36 | + }, |
| 37 | + { |
| 38 | + icon: ( |
| 39 | + <svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.4"> |
| 40 | + <path d="M2 3h12v3H2zM2 7h12v3H2zM2 11h12v3H2z" /> |
| 41 | + </svg> |
| 42 | + ), |
| 43 | + label: 'Data explorer', |
| 44 | + }, |
| 45 | + { |
| 46 | + icon: ( |
| 47 | + <svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.4"> |
| 48 | + <path d="M3 2h7l3 3v9H3zM6 7h5M6 9h5M6 11h3" /> |
| 49 | + </svg> |
| 50 | + ), |
| 51 | + label: 'Audit log', |
| 52 | + }, |
| 53 | + { |
| 54 | + icon: ( |
| 55 | + <svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round"> |
| 56 | + <path d="M2 13h12M4 11V6M7 11V3M10 11V8M13 11V5" /> |
| 57 | + </svg> |
| 58 | + ), |
| 59 | + label: 'Analytics', |
| 60 | + }, |
| 61 | +]; |
| 62 | + |
| 63 | +// ── Shared UI ───────────────────────────────────────────────────────────────── |
| 64 | + |
12 | 65 | interface LoginUIProps { |
13 | | - onLogin: () => void; |
14 | | - buttonText: string; |
15 | | - ButtonIcon: React.ComponentType<React.SVGProps<SVGSVGElement>>; |
| 66 | + onLogin: () => void; |
| 67 | + buttonText: string; |
| 68 | + buttonIcon: React.ReactNode; |
| 69 | + note: string; |
16 | 70 | } |
17 | 71 |
|
18 | | -const LoginUI: React.FC<LoginUIProps> = ({ onLogin, buttonText, ButtonIcon }) => ( |
19 | | - <div className="min-h-screen bg-slate-50 dark:bg-slate-900 flex flex-col justify-center items-center p-4 text-slate-800 dark:text-slate-200"> |
20 | | - <div className="max-w-md w-full mx-auto bg-white dark:bg-slate-800/50 dark:ring-1 dark:ring-slate-700 rounded-xl shadow-lg dark:shadow-black/20 p-6 sm:p-8 text-center"> |
21 | | - <header className="flex flex-col items-center justify-center space-y-4 mb-8"> |
22 | | - <MongoIcon className="w-16 h-16 text-blue-500" /> |
23 | | - <div> |
24 | | - <h1 className="text-3xl sm:text-4xl font-bold bg-gradient-to-r from-violet-600 to-blue-600 bg-clip-text text-transparent">QueryPal</h1> |
25 | | - <p className="text-slate-500 dark:text-slate-400 mt-2">Your AI-Powered Database Assistant</p> |
26 | | - </div> |
27 | | - </header> |
28 | | - |
29 | | - <main className="space-y-6"> |
30 | | - <button |
31 | | - onClick={onLogin} |
32 | | - className="w-full flex justify-center items-center gap-3 px-6 py-3 border border-transparent text-base font-medium rounded-md text-white bg-violet-600 hover:bg-violet-700 disabled:bg-slate-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-violet-500 transition-all duration-200 transform hover:scale-[1.02] active:scale-[0.99]" |
33 | | - title={buttonText} |
34 | | - > |
35 | | - <ButtonIcon className="w-6 h-6" /> |
36 | | - {buttonText} |
37 | | - </button> |
38 | | - <p className="text-xs text-slate-500 dark:text-slate-400"> |
39 | | - {USE_MSAL_AUTH |
40 | | - ? "You will be redirected to the Microsoft login page for authentication." |
41 | | - : "Using developer sign-in. No credentials required." |
42 | | - } |
43 | | - </p> |
44 | | - </main> |
| 72 | +const LoginUI: React.FC<LoginUIProps> = ({ onLogin, buttonText, buttonIcon, note }) => ( |
| 73 | + <div style={{ |
| 74 | + minHeight: '100vh', |
| 75 | + background: 'var(--bg)', |
| 76 | + display: 'flex', |
| 77 | + flexDirection: 'column', |
| 78 | + alignItems: 'center', |
| 79 | + justifyContent: 'center', |
| 80 | + padding: '32px 20px', |
| 81 | + fontFamily: 'var(--font-body)', |
| 82 | + color: 'var(--fg)', |
| 83 | + }}> |
| 84 | + <div style={{ width: '100%', maxWidth: 380, display: 'flex', flexDirection: 'column', gap: 28 }}> |
| 85 | + |
| 86 | + {/* Brand */} |
| 87 | + <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 12 }}> |
| 88 | + <div style={{ |
| 89 | + width: 48, height: 48, borderRadius: 14, |
| 90 | + background: 'var(--fg)', color: 'var(--bg)', |
| 91 | + display: 'grid', placeItems: 'center', |
| 92 | + fontFamily: 'var(--font-display)', fontWeight: 700, |
| 93 | + fontSize: 26, letterSpacing: '-0.05em', |
| 94 | + boxShadow: '0 4px 16px rgba(0,0,0,0.12)', |
| 95 | + }}>Q</div> |
| 96 | + <div style={{ textAlign: 'center' }}> |
| 97 | + <div style={{ |
| 98 | + fontFamily: 'var(--font-display)', fontWeight: 600, |
| 99 | + fontSize: 26, letterSpacing: '-0.02em', color: 'var(--fg)', |
| 100 | + lineHeight: 1.1, |
| 101 | + }}>QueryPal</div> |
| 102 | + <div style={{ |
| 103 | + fontSize: 13, color: 'var(--muted)', marginTop: 5, |
| 104 | + fontFamily: 'var(--font-body)', |
| 105 | + }}> |
| 106 | + AI-powered queries for Azure Cosmos DB |
| 107 | + </div> |
45 | 108 | </div> |
46 | | - <footer className="text-center mt-8 text-slate-500 dark:text-slate-400 text-sm"> |
47 | | - <p>Powered by Microsoft Azure and Google Gemini. For internal use only.</p> |
48 | | - <p className="text-xs max-w-md mx-auto"> |
49 | | - AI features use the Google Gemini API. Your data is not used to train their models. See the <a href="https://ai.google.dev/gemini-api/terms" target="_blank" rel="noopener noreferrer" className="underline hover:text-slate-700 dark:hover:text-slate-200">Terms of Service</a>. |
50 | | - </p> |
51 | | - </footer> |
| 109 | + </div> |
| 110 | + |
| 111 | + {/* Card */} |
| 112 | + <div style={{ |
| 113 | + background: 'var(--panel)', |
| 114 | + border: '1px solid var(--border)', |
| 115 | + borderRadius: 'var(--radius-lg)', |
| 116 | + padding: '28px 28px 24px', |
| 117 | + boxShadow: '0 2px 12px rgba(0,0,0,0.06)', |
| 118 | + display: 'flex', flexDirection: 'column', gap: 20, |
| 119 | + }}> |
| 120 | + <div> |
| 121 | + <div style={{ |
| 122 | + fontFamily: 'var(--font-display)', fontWeight: 600, |
| 123 | + fontSize: 18, color: 'var(--fg)', letterSpacing: '-0.01em', |
| 124 | + }}> |
| 125 | + Welcome back |
| 126 | + </div> |
| 127 | + <div style={{ fontSize: 13, color: 'var(--muted)', marginTop: 5, lineHeight: 1.5 }}> |
| 128 | + Sign in to access your workspace and databases. |
| 129 | + </div> |
| 130 | + </div> |
| 131 | + |
| 132 | + {/* Sign in button */} |
| 133 | + <button |
| 134 | + onClick={onLogin} |
| 135 | + style={{ |
| 136 | + width: '100%', height: 44, |
| 137 | + display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 10, |
| 138 | + background: 'var(--accent)', color: '#fff', |
| 139 | + border: 'none', borderRadius: 9, |
| 140 | + fontSize: 13.5, fontWeight: 600, fontFamily: 'var(--font-body)', |
| 141 | + cursor: 'pointer', |
| 142 | + transition: 'opacity 0.15s, transform 0.1s', |
| 143 | + letterSpacing: '-0.01em', |
| 144 | + }} |
| 145 | + onMouseEnter={e => { (e.currentTarget as HTMLElement).style.opacity = '0.88'; }} |
| 146 | + onMouseLeave={e => { (e.currentTarget as HTMLElement).style.opacity = '1'; }} |
| 147 | + onMouseDown={e => { (e.currentTarget as HTMLElement).style.transform = 'scale(0.987)'; }} |
| 148 | + onMouseUp={e => { (e.currentTarget as HTMLElement).style.transform = 'scale(1)'; }} |
| 149 | + > |
| 150 | + {buttonIcon} |
| 151 | + {buttonText} |
| 152 | + </button> |
| 153 | + |
| 154 | + {/* Note */} |
| 155 | + <div style={{ |
| 156 | + fontSize: 11.5, color: 'var(--muted)', |
| 157 | + textAlign: 'center', lineHeight: 1.55, |
| 158 | + borderTop: '1px solid var(--border)', |
| 159 | + paddingTop: 16, |
| 160 | + }}> |
| 161 | + {note} |
| 162 | + </div> |
| 163 | + </div> |
| 164 | + |
| 165 | + {/* Feature chips */} |
| 166 | + <div style={{ |
| 167 | + display: 'flex', flexWrap: 'wrap', |
| 168 | + gap: 8, justifyContent: 'center', |
| 169 | + }}> |
| 170 | + {FEATURES.map(f => ( |
| 171 | + <span key={f.label} style={{ |
| 172 | + display: 'inline-flex', alignItems: 'center', gap: 5, |
| 173 | + padding: '4px 10px', |
| 174 | + background: 'var(--panel)', |
| 175 | + border: '1px solid var(--border)', |
| 176 | + borderRadius: 99, |
| 177 | + fontSize: 11.5, color: 'var(--muted)', |
| 178 | + fontFamily: 'var(--font-body)', |
| 179 | + }}> |
| 180 | + <span style={{ color: 'var(--accent)', display: 'flex' }}>{f.icon}</span> |
| 181 | + {f.label} |
| 182 | + </span> |
| 183 | + ))} |
| 184 | + </div> |
| 185 | + |
| 186 | + {/* Footer */} |
| 187 | + <div style={{ |
| 188 | + textAlign: 'center', |
| 189 | + fontSize: 11, color: 'var(--muted)', |
| 190 | + lineHeight: 1.6, |
| 191 | + }}> |
| 192 | + <div>Powered by Microsoft Azure & Google Gemini</div> |
| 193 | + <div> |
| 194 | + AI features are subject to the{' '} |
| 195 | + <a |
| 196 | + href="https://ai.google.dev/gemini-api/terms" |
| 197 | + target="_blank" |
| 198 | + rel="noopener noreferrer" |
| 199 | + style={{ color: 'var(--accent)', textDecoration: 'none' }} |
| 200 | + onMouseEnter={e => { (e.currentTarget as HTMLElement).style.textDecoration = 'underline'; }} |
| 201 | + onMouseLeave={e => { (e.currentTarget as HTMLElement).style.textDecoration = 'none'; }} |
| 202 | + > |
| 203 | + Gemini API Terms of Service |
| 204 | + </a> |
| 205 | + . For internal use only. |
| 206 | + </div> |
| 207 | + </div> |
52 | 208 | </div> |
| 209 | + </div> |
53 | 210 | ); |
54 | 211 |
|
| 212 | +// ── Redirecting state ───────────────────────────────────────────────────────── |
| 213 | + |
| 214 | +const RedirectingScreen = () => ( |
| 215 | + <div style={{ |
| 216 | + minHeight: '100vh', background: 'var(--bg)', |
| 217 | + display: 'flex', alignItems: 'center', justifyContent: 'center', |
| 218 | + fontFamily: 'var(--font-body)', color: 'var(--muted)', fontSize: 13, |
| 219 | + gap: 10, |
| 220 | + }}> |
| 221 | + <style>{`@keyframes qp-spin { to { transform: rotate(360deg); } }`}</style> |
| 222 | + <div style={{ |
| 223 | + width: 14, height: 14, borderRadius: '50%', |
| 224 | + border: '2px solid var(--border)', borderTopColor: 'var(--accent)', |
| 225 | + animation: 'qp-spin 0.7s linear infinite', |
| 226 | + }} /> |
| 227 | + Redirecting… |
| 228 | + </div> |
| 229 | +); |
55 | 230 |
|
56 | | -// --- Container Components --- |
| 231 | +// ── Container components ────────────────────────────────────────────────────── |
57 | 232 |
|
58 | 233 | const MsalLoginPage: React.FC = () => { |
59 | | - const { instance, accounts } = useMsal(); |
60 | | - const isAuthenticated = useIsAuthenticated(); |
61 | | - const navigate = useNavigate(); |
62 | | - const location = useLocation(); |
63 | | - |
64 | | - useEffect(() => { |
65 | | - console.log('MSAL Login Page - isAuthenticated:', isAuthenticated, 'accounts:', accounts); |
66 | | - if (isAuthenticated && accounts.length > 0) { |
67 | | - // Check if there's a saved location to redirect to |
68 | | - const from = location.state?.from?.pathname || '/hub'; |
69 | | - const search = location.state?.from?.search || ''; |
70 | | - const hash = location.state?.from?.hash || ''; |
71 | | - const redirectPath = from + search + hash; |
72 | | - |
73 | | - console.log('User is authenticated, navigating to:', redirectPath); |
74 | | - navigate(redirectPath, { replace: true }); |
75 | | - } |
76 | | - }, [isAuthenticated, accounts, navigate, location.state]); |
77 | | - |
78 | | - const handleLogin = () => { |
79 | | - console.log('Starting MSAL login redirect'); |
80 | | - instance.loginRedirect(loginRequest).catch(e => { |
81 | | - console.error('MSAL login error:', e); |
82 | | - }); |
83 | | - } |
| 234 | + const { instance, accounts } = useMsal(); |
| 235 | + const isAuthenticated = useIsAuthenticated(); |
| 236 | + const navigate = useNavigate(); |
| 237 | + const location = useLocation(); |
84 | 238 |
|
85 | | - // Show loading state if we're in the middle of processing authentication |
| 239 | + useEffect(() => { |
86 | 240 | if (isAuthenticated && accounts.length > 0) { |
87 | | - return ( |
88 | | - <div className="min-h-screen bg-slate-50 dark:bg-slate-900 flex flex-col justify-center items-center p-4 text-slate-800 dark:text-slate-200"> |
89 | | - <div>Redirecting…</div> |
90 | | - </div> |
91 | | - ); |
| 241 | + const from = location.state?.from?.pathname || '/hub'; |
| 242 | + const search = location.state?.from?.search || ''; |
| 243 | + const hash = location.state?.from?.hash || ''; |
| 244 | + navigate(from + search + hash, { replace: true }); |
92 | 245 | } |
| 246 | + }, [isAuthenticated, accounts, navigate, location.state]); |
| 247 | + |
| 248 | + if (isAuthenticated && accounts.length > 0) return <RedirectingScreen />; |
93 | 249 |
|
94 | | - return <LoginUI onLogin={handleLogin} buttonText="Sign In with Microsoft Entra ID" ButtonIcon={MicrosoftIcon} />; |
| 250 | + return ( |
| 251 | + <LoginUI |
| 252 | + onLogin={() => instance.loginRedirect(loginRequest).catch(console.error)} |
| 253 | + buttonText="Continue with Microsoft Entra ID" |
| 254 | + buttonIcon={<MicrosoftLogo />} |
| 255 | + note="You'll be redirected to Microsoft's secure sign-in page to authenticate." |
| 256 | + /> |
| 257 | + ); |
95 | 258 | }; |
96 | 259 |
|
97 | 260 | const BypassLoginPage: React.FC = () => { |
98 | | - const { login } = useAuth(); |
99 | | - const navigate = useNavigate(); |
100 | | - const location = useLocation(); |
101 | | - |
102 | | - const handleLogin = () => { |
103 | | - login(); |
104 | | - // Navigate using React Router instead of window.location |
105 | | - setTimeout(() => { |
106 | | - // Check if there's a saved location to redirect to |
107 | | - const from = location.state?.from?.pathname || '/hub'; |
108 | | - const search = location.state?.from?.search || ''; |
109 | | - const hash = location.state?.from?.hash || ''; |
110 | | - const redirectPath = from + search + hash; |
111 | | - |
112 | | - navigate(redirectPath, { replace: true }); |
113 | | - }, 100); |
114 | | - }; |
115 | | - |
116 | | - return <LoginUI onLogin={handleLogin} buttonText="Sign In as Developer" ButtonIcon={UserIcon} />; |
117 | | -} |
| 261 | + const { login } = useAuth(); |
| 262 | + const navigate = useNavigate(); |
| 263 | + const location = useLocation(); |
118 | 264 |
|
119 | | -// --- Main Exported Component (Router) --- |
| 265 | + const handleLogin = () => { |
| 266 | + login(); |
| 267 | + setTimeout(() => { |
| 268 | + const from = location.state?.from?.pathname || '/hub'; |
| 269 | + const search = location.state?.from?.search || ''; |
| 270 | + const hash = location.state?.from?.hash || ''; |
| 271 | + navigate(from + search + hash, { replace: true }); |
| 272 | + }, 100); |
| 273 | + }; |
120 | 274 |
|
121 | | -const LoginPage: React.FC = () => { |
122 | | - return USE_MSAL_AUTH ? <MsalLoginPage /> : <BypassLoginPage />; |
| 275 | + return ( |
| 276 | + <LoginUI |
| 277 | + onLogin={handleLogin} |
| 278 | + buttonText="Continue as Developer" |
| 279 | + buttonIcon={<DevIcon />} |
| 280 | + note="Developer sign-in bypass is active. No credentials required." |
| 281 | + /> |
| 282 | + ); |
123 | 283 | }; |
124 | 284 |
|
| 285 | +// ── Export ──────────────────────────────────────────────────────────────────── |
| 286 | + |
| 287 | +const LoginPage: React.FC = () => USE_MSAL_AUTH ? <MsalLoginPage /> : <BypassLoginPage />; |
| 288 | + |
125 | 289 | export default LoginPage; |
0 commit comments