diff --git a/src/App.tsx b/src/App.tsx index f2e1957..318ed1e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,8 +8,21 @@ import { MatrixView } from './components/MatrixView'; import { UploadReload } from './components/UploadReload'; import { EditPanel } from './components/EditPanel'; import { getAllSyncedRows, saveRowToSupabase } from './lib/supabase'; +import { getStoredSession, signOut, type AuthSession } from './lib/auth'; +import { LoginPage } from './components/LoginPage'; export default function App() { + const [session, setSession] = useState(() => getStoredSession()); + + const handleSignOut = () => { + signOut(); + setSession(null); + }; + + if (!session) { + return setSession(getStoredSession())} />; + } + const [appState, setAppState] = useState({ headers: [], data: [], @@ -192,11 +205,13 @@ export default function App() { return (
- 0} + 0} hasUnsavedChanges={appState.hasUnsavedChanges} + userEmail={session.user.email} + onSignOut={handleSignOut} />
diff --git a/src/components/LoginPage.tsx b/src/components/LoginPage.tsx new file mode 100644 index 0000000..4545f66 --- /dev/null +++ b/src/components/LoginPage.tsx @@ -0,0 +1,89 @@ +import React, { useState } from 'react'; +import { Database, Loader2, LogIn } from 'lucide-react'; +import { signIn } from '../lib/auth'; + +interface LoginPageProps { + onLogin: () => void; +} + +export function LoginPage({ onLogin }: LoginPageProps) { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(null); + + try { + await signIn(email.trim(), password); + onLogin(); + } catch (err: any) { + setError(err.message || 'Login failed.'); + } finally { + setLoading(false); + } + }; + + return ( +
+
+
+
+ +
+

CRAZE Data Quality

+

Sign in with your CRAZE account

+
+ +
+ {error && ( +
+ {error} +
+ )} + +
+ + setEmail(e.target.value)} + placeholder="you@craze.de" + required + autoFocus + className="w-full bg-slate-900 border border-slate-700 rounded-md px-3 py-2.5 text-sm text-white placeholder-slate-500 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 transition-colors" + /> +
+ +
+ + setPassword(e.target.value)} + placeholder="••••••••" + required + className="w-full bg-slate-900 border border-slate-700 rounded-md px-3 py-2.5 text-sm text-white placeholder-slate-500 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 transition-colors" + /> +
+ + +
+ +

+ Access restricted to @craze.de accounts +

+
+
+ ); +} diff --git a/src/components/TopBar.tsx b/src/components/TopBar.tsx index cb1302c..dbc69c5 100644 --- a/src/components/TopBar.tsx +++ b/src/components/TopBar.tsx @@ -1,14 +1,16 @@ import React from 'react'; -import { Download, Database } from 'lucide-react'; +import { Download, Database, LogOut } from 'lucide-react'; interface TopBarProps { stats: any; onExport: () => void; hasData: boolean; hasUnsavedChanges: boolean; + userEmail?: string; + onSignOut?: () => void; } -export function TopBar({ stats, onExport, hasData, hasUnsavedChanges }: TopBarProps) { +export function TopBar({ stats, onExport, hasData, hasUnsavedChanges, userEmail, onSignOut }: TopBarProps) { return (
@@ -50,8 +52,8 @@ export function TopBar({ stats, onExport, hasData, hasUnsavedChanges }: TopBarPr )} + {userEmail && ( +
+ {userEmail} + +
+ )}
); diff --git a/src/lib/auth.ts b/src/lib/auth.ts new file mode 100644 index 0000000..e7f3822 --- /dev/null +++ b/src/lib/auth.ts @@ -0,0 +1,51 @@ +const SUPABASE_URL = 'https://hwithddwaapyhnfwcesj.supabase.co'; +const SUPABASE_ANON_KEY = 'sb_publishable_fGXkh0bSrAOqSk2jWKAzSg_NJD9YPCv'; +const ALLOWED_DOMAIN = '@craze.de'; +const SESSION_KEY = 'craze_auth_session'; + +export interface AuthSession { + access_token: string; + user: { id: string; email: string }; +} + +export async function signIn(email: string, password: string): Promise { + if (!email.toLowerCase().endsWith(ALLOWED_DOMAIN)) { + throw new Error(`Only ${ALLOWED_DOMAIN} email addresses are allowed.`); + } + + const response = await fetch(`${SUPABASE_URL}/auth/v1/token?grant_type=password`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'apikey': SUPABASE_ANON_KEY, + }, + body: JSON.stringify({ email, password }), + }); + + if (!response.ok) { + const err = await response.json().catch(() => ({})); + throw new Error(err.error_description || err.message || 'Invalid email or password.'); + } + + const data = await response.json(); + const session: AuthSession = { + access_token: data.access_token, + user: { id: data.user.id, email: data.user.email }, + }; + + localStorage.setItem(SESSION_KEY, JSON.stringify(session)); + return session; +} + +export function signOut(): void { + localStorage.removeItem(SESSION_KEY); +} + +export function getStoredSession(): AuthSession | null { + try { + const raw = localStorage.getItem(SESSION_KEY); + return raw ? (JSON.parse(raw) as AuthSession) : null; + } catch { + return null; + } +}