Google Sign in added
This commit is contained in:
1026
api-dashboard/package-lock.json
generated
1026
api-dashboard/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,7 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"firebase": "^12.16.0",
|
||||
"react": "^19.2.7",
|
||||
"react-dom": "^19.2.7"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import logo from './assets/logo.png';
|
||||
import { auth, googleProvider } from './firebase';
|
||||
import { signInWithPopup } from 'firebase/auth';
|
||||
import './App.css';
|
||||
|
||||
interface DeveloperApiKey {
|
||||
@@ -32,11 +34,15 @@ function App() {
|
||||
// Auth State
|
||||
const [session, setSession] = useState<UserSession | null>(null);
|
||||
const [loginType, setLoginType] = useState<'CUSTOMER' | 'SYSTEM'>('SYSTEM');
|
||||
const [loginMobile, setLoginMobile] = useState('');
|
||||
const [loginPin, setLoginPin] = useState('');
|
||||
const [loginEmail, setLoginEmail] = useState('');
|
||||
const [loginPassword, setLoginPassword] = useState('');
|
||||
|
||||
// First-time registration flow states (Student Google login)
|
||||
const [showCustomerMobilePrompt, setShowCustomerMobilePrompt] = useState(false);
|
||||
const [customerPendingEmail, setCustomerPendingEmail] = useState('');
|
||||
const [customerPendingName, setCustomerPendingName] = useState('');
|
||||
const [customerMobileNumber, setCustomerMobileNumber] = useState('');
|
||||
|
||||
// Key Management State
|
||||
const [keys, setKeys] = useState<DeveloperApiKey[]>([]);
|
||||
const [newKeyName, setNewKeyName] = useState('');
|
||||
@@ -102,56 +108,105 @@ function App() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleCustomerGoogleSignIn = async () => {
|
||||
setLoading(true);
|
||||
clearMessages();
|
||||
try {
|
||||
const result = await signInWithPopup(auth, googleProvider);
|
||||
const user = result.user;
|
||||
if (user.email) {
|
||||
const checkRes = await fetch(`${BACKEND_URL}/api/auth/check-email`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email: user.email }),
|
||||
});
|
||||
const checkData = await checkRes.json();
|
||||
|
||||
if (checkData.exists) {
|
||||
await executeCustomerFirebaseLogin(user.email, user.displayName || "Google User");
|
||||
} else {
|
||||
setCustomerPendingEmail(user.email);
|
||||
setCustomerPendingName(user.displayName || "Google User");
|
||||
setShowCustomerMobilePrompt(true);
|
||||
}
|
||||
} else {
|
||||
throw new Error('No email found in Google Account.');
|
||||
}
|
||||
} catch (err: any) {
|
||||
showNotification('error', err.message || 'Failed to authenticate with Google');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const executeCustomerFirebaseLogin = async (email: string, name: string, mobileNumber?: string) => {
|
||||
const loginRes = await fetch(`${BACKEND_URL}/api/auth/firebase-login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, name, mobileNumber }),
|
||||
});
|
||||
const loginData = await loginRes.json();
|
||||
if (!loginRes.ok || !loginData.success) {
|
||||
throw new Error(loginData.message || 'Google Canteen Login failed');
|
||||
}
|
||||
|
||||
const sessionData: UserSession = {
|
||||
token: loginData.token,
|
||||
id: loginData.user.id,
|
||||
name: loginData.user.name || 'Customer',
|
||||
emailOrMobile: loginData.user.mobileNumber,
|
||||
roleOrType: 'CUSTOMER',
|
||||
};
|
||||
|
||||
setSession(sessionData);
|
||||
localStorage.setItem('developer_session', JSON.stringify(sessionData));
|
||||
showNotification('success', `Welcome back, ${sessionData.name}!`);
|
||||
setShowCustomerMobilePrompt(false);
|
||||
};
|
||||
|
||||
const handleCustomerMobileSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!/^[0-9]{10}$/.test(customerMobileNumber)) {
|
||||
showNotification('error', 'Please enter a valid 10-digit mobile number');
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
clearMessages();
|
||||
try {
|
||||
await executeCustomerFirebaseLogin(customerPendingEmail, customerPendingName, customerMobileNumber);
|
||||
} catch (err: any) {
|
||||
showNotification('error', err.message || 'Registration failed');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
clearMessages();
|
||||
|
||||
try {
|
||||
if (loginType === 'SYSTEM') {
|
||||
const res = await fetch(`${BACKEND_URL}/api/system/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email: loginEmail, password: loginPassword }),
|
||||
});
|
||||
const res = await fetch(`${BACKEND_URL}/api/system/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email: loginEmail, password: loginPassword }),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || 'System Login failed');
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || 'System Login failed');
|
||||
|
||||
const sessionData: UserSession = {
|
||||
token: data.token,
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
emailOrMobile: data.email,
|
||||
roleOrType: data.role,
|
||||
};
|
||||
const sessionData: UserSession = {
|
||||
token: data.token,
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
emailOrMobile: data.email,
|
||||
roleOrType: data.role,
|
||||
};
|
||||
|
||||
setSession(sessionData);
|
||||
localStorage.setItem('developer_session', JSON.stringify(sessionData));
|
||||
showNotification('success', `Welcome back, ${data.name}!`);
|
||||
} else {
|
||||
// Customer login
|
||||
const res = await fetch(`${BACKEND_URL}/api/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ mobileNumber: loginMobile, pin: loginPin }),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
if (!res.ok || !data.success) throw new Error(data.message || 'Customer Login failed');
|
||||
|
||||
const sessionData: UserSession = {
|
||||
token: data.token,
|
||||
id: data.user.id,
|
||||
name: data.user.name || 'Customer',
|
||||
emailOrMobile: data.user.mobileNumber,
|
||||
roleOrType: 'CUSTOMER',
|
||||
};
|
||||
|
||||
setSession(sessionData);
|
||||
localStorage.setItem('developer_session', JSON.stringify(sessionData));
|
||||
showNotification('success', `Welcome back, ${sessionData.name}!`);
|
||||
}
|
||||
setSession(sessionData);
|
||||
localStorage.setItem('developer_session', JSON.stringify(sessionData));
|
||||
showNotification('success', `Welcome back, ${data.name}!`);
|
||||
} catch (err: any) {
|
||||
showNotification('error', err.message || 'An error occurred during authentication');
|
||||
} finally {
|
||||
@@ -517,77 +572,128 @@ function App() {
|
||||
className={loginType === 'CUSTOMER' ? 'active' : ''}
|
||||
onClick={() => { setLoginType('CUSTOMER'); clearMessages(); }}
|
||||
>
|
||||
Customer Portal
|
||||
Student Portal
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleLogin} className="login-form">
|
||||
{loginType === 'SYSTEM' ? (
|
||||
<>
|
||||
<div className="input-group">
|
||||
<label htmlFor="username">Username</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
required
|
||||
placeholder="e.g. admin"
|
||||
value={loginEmail}
|
||||
onChange={(e) => setLoginEmail(e.target.value)}
|
||||
/>
|
||||
{loginType === 'SYSTEM' ? (
|
||||
<form onSubmit={handleLogin} className="login-form">
|
||||
<div className="input-group">
|
||||
<label htmlFor="username">Username</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
required
|
||||
placeholder="e.g. admin"
|
||||
value={loginEmail}
|
||||
onChange={(e) => setLoginEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="input-group">
|
||||
<label htmlFor="password">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
required
|
||||
placeholder="••••••••"
|
||||
value={loginPassword}
|
||||
onChange={(e) => setLoginPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" className="btn-primary w-full" disabled={loading}>
|
||||
{loading ? 'Authenticating...' : 'Sign In to Dashboard'}
|
||||
</button>
|
||||
</form>
|
||||
) : (
|
||||
<div className="customer-login-container">
|
||||
{showCustomerMobilePrompt ? (
|
||||
<form onSubmit={handleCustomerMobileSubmit} className="login-form">
|
||||
<div className="input-group">
|
||||
<label htmlFor="customerMobile">10-Digit Mobile Number</label>
|
||||
<input
|
||||
type="tel"
|
||||
id="customerMobile"
|
||||
required
|
||||
pattern="[0-9]{10}"
|
||||
placeholder="9876543210"
|
||||
value={customerMobileNumber}
|
||||
onChange={(e) => setCustomerMobileNumber(e.target.value.replace(/[^0-9]/g, '').slice(0, 10))}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" className="btn-primary w-full" disabled={loading || customerMobileNumber.length !== 10}>
|
||||
{loading ? 'Registering...' : 'Register & Enter Dashboard'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-revoke w-full"
|
||||
onClick={() => { setShowCustomerMobilePrompt(false); clearMessages(); }}
|
||||
disabled={loading}
|
||||
style={{ marginTop: '0.5rem', border: '1px solid var(--color-border)', color: 'var(--color-text-secondary)' }}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</form>
|
||||
) : (
|
||||
<div className="google-signin-wrapper" style={{ marginTop: '0.5rem', display: 'flex', justifyContent: 'center' }}>
|
||||
<button
|
||||
type="button"
|
||||
className="google-login-btn"
|
||||
onClick={handleCustomerGoogleSignIn}
|
||||
disabled={loading}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '12px',
|
||||
width: '100%',
|
||||
padding: '12px',
|
||||
backgroundColor: '#ffffff',
|
||||
border: '1px solid #cbd5e1',
|
||||
borderRadius: '12px',
|
||||
color: '#1e293b',
|
||||
fontWeight: '700',
|
||||
cursor: 'pointer',
|
||||
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)'
|
||||
}}
|
||||
>
|
||||
{loading ? (
|
||||
<span>Connecting...</span>
|
||||
) : (
|
||||
<>
|
||||
<svg className="google-icon" viewBox="0 0 24 24" width="20" height="20">
|
||||
<path
|
||||
fill="#4285F4"
|
||||
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
||||
/>
|
||||
<path
|
||||
fill="#34A853"
|
||||
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
||||
/>
|
||||
<path
|
||||
fill="#FBBC05"
|
||||
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
||||
/>
|
||||
<path
|
||||
fill="#EA4335"
|
||||
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
||||
/>
|
||||
</svg>
|
||||
<span>Sign in with Google</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<div className="input-group">
|
||||
<label htmlFor="password">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
required
|
||||
placeholder="••••••••"
|
||||
value={loginPassword}
|
||||
onChange={(e) => setLoginPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="input-group">
|
||||
<label htmlFor="mobile">10-Digit Mobile Number</label>
|
||||
<input
|
||||
type="tel"
|
||||
id="mobile"
|
||||
required
|
||||
pattern="[0-9]{10}"
|
||||
placeholder="9876543210"
|
||||
value={loginMobile}
|
||||
onChange={(e) => setLoginMobile(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="input-group">
|
||||
<label htmlFor="pin">6-Digit Security PIN</label>
|
||||
<input
|
||||
type="password"
|
||||
id="pin"
|
||||
required
|
||||
pattern="[0-9]{6}"
|
||||
placeholder="••••••"
|
||||
value={loginPin}
|
||||
maxLength={6}
|
||||
onChange={(e) => setLoginPin(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<button type="submit" className="btn-primary w-full" disabled={loading}>
|
||||
{loading ? 'Authenticating...' : 'Sign In to Dashboard'}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="demo-credentials-box">
|
||||
<h4>💡 Dev Sandbox Access:</h4>
|
||||
{loginType === 'SYSTEM' ? (
|
||||
<p>Default Admin: <strong>admin</strong> / Password: <strong>admin123</strong></p>
|
||||
) : (
|
||||
<p>Create / use any customer login registered on the POS client. (e.g. mobile <strong>9876543210</strong>, pin <strong>123456</strong>)</p>
|
||||
<p>Sign in with your Google account. First-time users will be asked for a mobile number to complete registration.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
16
api-dashboard/src/firebase.ts
Normal file
16
api-dashboard/src/firebase.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { initializeApp } from "firebase/app";
|
||||
import { getAuth, GoogleAuthProvider } from "firebase/auth";
|
||||
|
||||
const firebaseConfig = {
|
||||
apiKey: "AIzaSyDOEiKtH-gs2nAx8Di45wJf8CY5nPm4xPE",
|
||||
authDomain: "tillo-c8de9.firebaseapp.com",
|
||||
projectId: "tillo-c8de9",
|
||||
storageBucket: "tillo-c8de9.firebasestorage.app",
|
||||
messagingSenderId: "200333262782",
|
||||
appId: "1:200333262782:web:0d988352ee6a44d7a943ef",
|
||||
measurementId: "G-YB01EQT2VW"
|
||||
};
|
||||
|
||||
const app = initializeApp(firebaseConfig);
|
||||
export const auth = getAuth(app);
|
||||
export const googleProvider = new GoogleAuthProvider();
|
||||
Reference in New Issue
Block a user