Implement Google OAuth flow and enhance login functionality
- Updated the Google Sign-In integration to utilize the new OAuth callback mechanism. - Added a redirect flow for Google authentication, improving user experience. - Enhanced error handling and user feedback during the login process. - Removed hardcoded Google client ID in favor of environment variable usage. - Introduced a new component for handling OAuth callbacks and updated the App component to manage authentication states accordingly. - Improved API route for processing OAuth callbacks, including token exchange and user verification.
This commit is contained in:
@@ -14,6 +14,7 @@ import UploadIcon from '@mui/icons-material/Upload';
|
|||||||
import AuthService from './services/AuthService';
|
import AuthService from './services/AuthService';
|
||||||
import DataViewer from './components/DataViewer';
|
import DataViewer from './components/DataViewer';
|
||||||
import Login from './components/Login';
|
import Login from './components/Login';
|
||||||
|
import OAuthCallback from './components/OAuthCallback';
|
||||||
|
|
||||||
const theme = createTheme({
|
const theme = createTheme({
|
||||||
palette: {
|
palette: {
|
||||||
@@ -230,6 +231,10 @@ class App extends Component {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
isOAuthCallback = () => {
|
||||||
|
return window.location.pathname === '/auth/callback';
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { isAuthenticated, user, loading, currentView, documentStatus, processingStatus, snackbar } = this.state;
|
const { isAuthenticated, user, loading, currentView, documentStatus, processingStatus, snackbar } = this.state;
|
||||||
|
|
||||||
@@ -448,7 +453,9 @@ class App extends Component {
|
|||||||
|
|
||||||
<Box sx={{ height: 'calc(100vh - 64px)', display: 'flex', flexDirection: 'column' }}>
|
<Box sx={{ height: 'calc(100vh - 64px)', display: 'flex', flexDirection: 'column' }}>
|
||||||
<Container maxWidth={false} sx={{ mt: 4, flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', width: '100%' }}>
|
<Container maxWidth={false} sx={{ mt: 4, flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', width: '100%' }}>
|
||||||
{isAuthenticated ? (
|
{this.isOAuthCallback() ? (
|
||||||
|
<OAuthCallback />
|
||||||
|
) : isAuthenticated ? (
|
||||||
<DataViewer
|
<DataViewer
|
||||||
user={user}
|
user={user}
|
||||||
onUpdateExportData={this.updateExportData}
|
onUpdateExportData={this.updateExportData}
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ class Login extends Component {
|
|||||||
error: null,
|
error: null,
|
||||||
loading: false,
|
loading: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Flags to track FedCM attempts and success
|
||||||
|
this.fedcmAttempted = false;
|
||||||
|
this.fedcmSucceeded = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@@ -34,11 +38,17 @@ class Login extends Component {
|
|||||||
initializeGoogleSignIn = () => {
|
initializeGoogleSignIn = () => {
|
||||||
if (window.google && window.google.accounts) {
|
if (window.google && window.google.accounts) {
|
||||||
try {
|
try {
|
||||||
|
// Note: Removed debug logging to avoid deprecated method warnings
|
||||||
|
|
||||||
|
console.log('REACT_APP_GOOGLE_CLIENT_ID', process.env.REACT_APP_GOOGLE_CLIENT_ID);
|
||||||
|
console.log('Current origin for Google auth:', window.location.origin);
|
||||||
|
console.log('User agent:', navigator.userAgent);
|
||||||
|
|
||||||
window.google.accounts.id.initialize({
|
window.google.accounts.id.initialize({
|
||||||
client_id: process.env.REACT_APP_GOOGLE_CLIENT_ID || 'your_google_client_id_here',
|
client_id: process.env.REACT_APP_GOOGLE_CLIENT_ID,
|
||||||
callback: this.handleGoogleResponse,
|
callback: this.handleGoogleResponse,
|
||||||
auto_select: false,
|
auto_select: false,
|
||||||
cancel_on_tap_outside: true,
|
cancel_on_tap_outside: false,
|
||||||
});
|
});
|
||||||
console.log('✅ Google Sign-In initialized');
|
console.log('✅ Google Sign-In initialized');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -48,6 +58,9 @@ class Login extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
handleGoogleResponse = (response) => {
|
handleGoogleResponse = (response) => {
|
||||||
|
// Mark FedCM as successful if we get here
|
||||||
|
this.fedcmSucceeded = true;
|
||||||
|
|
||||||
this.setState({ loading: true, error: null });
|
this.setState({ loading: true, error: null });
|
||||||
this.props.onLogin(response)
|
this.props.onLogin(response)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -70,6 +83,11 @@ class Login extends Component {
|
|||||||
errorMessage = '🚫 Zugriff verweigert: Ihre E-Mail-Adresse ist nicht autorisiert. Versuchen Sie, sich mit einem anderen Google-Konto anzumelden.';
|
errorMessage = '🚫 Zugriff verweigert: Ihre E-Mail-Adresse ist nicht autorisiert. Versuchen Sie, sich mit einem anderen Google-Konto anzumelden.';
|
||||||
} else if (error.message.includes('No authorized users configured')) {
|
} else if (error.message.includes('No authorized users configured')) {
|
||||||
errorMessage = '🔒 Kein Zugriff: Derzeit sind keine Benutzer autorisiert. Wenden Sie sich an den Administrator.';
|
errorMessage = '🔒 Kein Zugriff: Derzeit sind keine Benutzer autorisiert. Wenden Sie sich an den Administrator.';
|
||||||
|
} else if (error.message.includes('Not signed in with the identity provider') ||
|
||||||
|
error.message.includes('NetworkError') ||
|
||||||
|
error.message.includes('FedCM')) {
|
||||||
|
// FedCM failed, offer redirect option
|
||||||
|
errorMessage = '🔄 Schnelle Anmeldung nicht verfügbar. Versuchen Sie die Standard-Anmeldung.';
|
||||||
} else {
|
} else {
|
||||||
// Show the actual error message from the server
|
// Show the actual error message from the server
|
||||||
errorMessage = `❌ Anmeldefehler: ${error.message}`;
|
errorMessage = `❌ Anmeldefehler: ${error.message}`;
|
||||||
@@ -92,29 +110,105 @@ class Login extends Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear any previous error
|
// Clear any previous error and start loading
|
||||||
this.setState({ error: null, loading: false });
|
this.setState({ error: null, loading: true });
|
||||||
|
|
||||||
|
// Try FedCM first (seamless for users already signed in to Google)
|
||||||
|
console.log('🎯 Trying FedCM first for optimal UX...');
|
||||||
|
this.tryFedCMFirst();
|
||||||
|
};
|
||||||
|
|
||||||
|
tryFedCMFirst = () => {
|
||||||
if (window.google && window.google.accounts && window.google.accounts.id) {
|
if (window.google && window.google.accounts && window.google.accounts.id) {
|
||||||
try {
|
try {
|
||||||
window.google.accounts.id.prompt();
|
console.log('✅ Trying FedCM for seamless sign-in...');
|
||||||
} catch (error) {
|
|
||||||
console.error('Google prompt error:', error);
|
// Listen for the specific FedCM errors that indicate no Google session
|
||||||
this.setState({
|
const originalConsoleError = console.error;
|
||||||
error: 'Google-Anmeldung konnte nicht geladen werden. Die Seite wird aktualisiert, um es erneut zu versuchen.',
|
let errorIntercepted = false;
|
||||||
loading: true
|
|
||||||
|
console.error = (...args) => {
|
||||||
|
const errorMessage = args.join(' ');
|
||||||
|
if (!errorIntercepted && (
|
||||||
|
errorMessage.includes('Not signed in with the identity provider') ||
|
||||||
|
errorMessage.includes('FedCM get() rejects with NetworkError') ||
|
||||||
|
errorMessage.includes('Error retrieving a token')
|
||||||
|
)) {
|
||||||
|
errorIntercepted = true;
|
||||||
|
console.error = originalConsoleError; // Restore immediately
|
||||||
|
console.log('🔄 FedCM failed (user not signed in to Google), using redirect...');
|
||||||
|
this.redirectToGoogleOAuth();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
originalConsoleError.apply(console, args);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Try FedCM
|
||||||
|
window.google.accounts.id.prompt((notification) => {
|
||||||
|
console.log('🔍 FedCM notification:', notification);
|
||||||
|
console.error = originalConsoleError; // Restore console.error
|
||||||
|
// If we get here without error, FedCM is working
|
||||||
});
|
});
|
||||||
setTimeout(() => window.location.reload(), 2000);
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('FedCM initialization error, falling back to redirect:', error);
|
||||||
|
this.redirectToGoogleOAuth();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.setState({
|
// Google Identity Services not loaded, go straight to redirect
|
||||||
error: 'Google-Anmeldung nicht geladen. Die Seite wird aktualisiert, um es erneut zu versuchen.',
|
console.log('📋 GSI not loaded, using redirect flow...');
|
||||||
loading: true
|
this.redirectToGoogleOAuth();
|
||||||
});
|
|
||||||
setTimeout(() => window.location.reload(), 2000);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
redirectToGoogleOAuth = () => {
|
||||||
|
try {
|
||||||
|
// Generate a random state parameter for security
|
||||||
|
const state = this.generateRandomString(32);
|
||||||
|
sessionStorage.setItem('oauth_state', state);
|
||||||
|
|
||||||
|
// Build the Google OAuth2 authorization URL
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
client_id: process.env.REACT_APP_GOOGLE_CLIENT_ID,
|
||||||
|
redirect_uri: window.location.origin + '/auth/callback',
|
||||||
|
response_type: 'code',
|
||||||
|
scope: 'openid email profile',
|
||||||
|
state: state,
|
||||||
|
access_type: 'online',
|
||||||
|
prompt: 'select_account'
|
||||||
|
});
|
||||||
|
|
||||||
|
const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
|
||||||
|
|
||||||
|
console.log('🔗 Redirecting to Google OAuth:', authUrl);
|
||||||
|
|
||||||
|
// Redirect to Google OAuth
|
||||||
|
window.location.href = authUrl;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Redirect OAuth error:', error);
|
||||||
|
this.setState({
|
||||||
|
error: 'Google-Anmeldung konnte nicht gestartet werden.',
|
||||||
|
loading: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
generateRandomString = (length) => {
|
||||||
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||||
|
let result = '';
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
handleUseRedirect = () => {
|
||||||
|
console.log('🔄 User chose redirect flow');
|
||||||
|
this.setState({ error: null, loading: true });
|
||||||
|
this.redirectToGoogleOAuth();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -157,6 +251,20 @@ class Login extends Component {
|
|||||||
{loading ? 'Anmeldung läuft...' : 'Mit Google anmelden'}
|
{loading ? 'Anmeldung läuft...' : 'Mit Google anmelden'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
{error && error.includes('Standard-Anmeldung') && (
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
size="large"
|
||||||
|
startIcon={<GoogleIcon />}
|
||||||
|
onClick={this.handleUseRedirect}
|
||||||
|
disabled={loading}
|
||||||
|
sx={{ py: 1.5, mt: 2 }}
|
||||||
|
>
|
||||||
|
Standard Google-Anmeldung verwenden
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<Typography variant="caption" display="block" textAlign="center" sx={{ mt: 2 }}>
|
<Typography variant="caption" display="block" textAlign="center" sx={{ mt: 2 }}>
|
||||||
|
|||||||
133
client/src/components/OAuthCallback.js
Normal file
133
client/src/components/OAuthCallback.js
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import { Box, CircularProgress, Typography, Alert } from '@mui/material';
|
||||||
|
|
||||||
|
class OAuthCallback extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
loading: true,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.handleOAuthCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOAuthCallback = async () => {
|
||||||
|
try {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const code = urlParams.get('code');
|
||||||
|
const state = urlParams.get('state');
|
||||||
|
const error = urlParams.get('error');
|
||||||
|
|
||||||
|
// Check for OAuth errors
|
||||||
|
if (error) {
|
||||||
|
throw new Error(`OAuth error: ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify state parameter for security
|
||||||
|
const storedState = sessionStorage.getItem('oauth_state');
|
||||||
|
if (!state || state !== storedState) {
|
||||||
|
throw new Error('Invalid state parameter - possible CSRF attack');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear stored state
|
||||||
|
sessionStorage.removeItem('oauth_state');
|
||||||
|
|
||||||
|
if (!code) {
|
||||||
|
throw new Error('No authorization code received');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🔑 Authorization code received, exchanging for tokens...');
|
||||||
|
|
||||||
|
// Exchange authorization code for tokens via our backend
|
||||||
|
const response = await fetch('/api/auth/google/callback', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
code: code,
|
||||||
|
redirect_uri: window.location.origin + '/auth/callback'
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success && data.token) {
|
||||||
|
console.log('✅ OAuth callback successful');
|
||||||
|
|
||||||
|
// Store the JWT token
|
||||||
|
localStorage.setItem('token', data.token);
|
||||||
|
|
||||||
|
// Redirect to main app
|
||||||
|
window.location.href = '/';
|
||||||
|
} else {
|
||||||
|
throw new Error(data.message || 'Authentication failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('OAuth callback error:', error);
|
||||||
|
this.setState({
|
||||||
|
loading: false,
|
||||||
|
error: error.message || 'Authentication failed'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { loading, error } = this.state;
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
display="flex"
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
minHeight="60vh"
|
||||||
|
flexDirection="column"
|
||||||
|
>
|
||||||
|
<Alert severity="error" sx={{ mb: 2, maxWidth: 400 }}>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
Anmeldung fehlgeschlagen
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2">
|
||||||
|
{error}
|
||||||
|
</Typography>
|
||||||
|
</Alert>
|
||||||
|
<Typography variant="body2" color="textSecondary">
|
||||||
|
<a href="/" style={{ color: 'inherit' }}>
|
||||||
|
Zurück zur Anmeldung
|
||||||
|
</a>
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
display="flex"
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
minHeight="60vh"
|
||||||
|
flexDirection="column"
|
||||||
|
>
|
||||||
|
<CircularProgress size={60} sx={{ mb: 2 }} />
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
Anmeldung wird verarbeitet...
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="textSecondary">
|
||||||
|
Sie werden automatisch weitergeleitet.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OAuthCallback;
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "concurrently \"npm run dev:frontend\" \"npm run dev:backend\"",
|
"dev": "concurrently \"npm run dev:frontend\" \"npm run dev:backend\"",
|
||||||
"dev:frontend": "webpack serve --mode development --config webpack.config.js",
|
"dev:frontend": "webpack serve --mode development --config webpack.config.js",
|
||||||
"dev:backend": "nodemon src/index.js",
|
"dev:backend": "nodemon --exitcrash src/index.js",
|
||||||
"build": "webpack --config webpack.prod.config.js",
|
"build": "webpack --config webpack.prod.config.js",
|
||||||
"build:prod": "npm run build && npm run start:prod",
|
"build:prod": "npm run build && npm run start:prod",
|
||||||
"start": "npm run build && node src/index.js",
|
"start": "npm run build && node src/index.js",
|
||||||
|
|||||||
@@ -76,6 +76,98 @@ router.post('/google', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Google OAuth callback (redirect flow)
|
||||||
|
router.post('/google/callback', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { code, redirect_uri } = req.body;
|
||||||
|
|
||||||
|
console.log('🔄 Processing OAuth callback with authorization code');
|
||||||
|
|
||||||
|
if (!code) {
|
||||||
|
console.log('❌ No authorization code provided');
|
||||||
|
return res.status(400).json({ error: 'Authorization code is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exchange authorization code for tokens
|
||||||
|
const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({
|
||||||
|
client_id: process.env.GOOGLE_CLIENT_ID,
|
||||||
|
client_secret: process.env.GOOGLE_CLIENT_SECRET,
|
||||||
|
code: code,
|
||||||
|
grant_type: 'authorization_code',
|
||||||
|
redirect_uri: redirect_uri,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!tokenResponse.ok) {
|
||||||
|
const errorData = await tokenResponse.text();
|
||||||
|
console.log('❌ Token exchange failed:', errorData);
|
||||||
|
return res.status(400).json({ error: 'Failed to exchange authorization code' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokens = await tokenResponse.json();
|
||||||
|
console.log('🎯 Received tokens from Google');
|
||||||
|
|
||||||
|
// Use the ID token to get user info
|
||||||
|
const ticket = await client.verifyIdToken({
|
||||||
|
idToken: tokens.id_token,
|
||||||
|
audience: process.env.GOOGLE_CLIENT_ID,
|
||||||
|
});
|
||||||
|
|
||||||
|
const payload = ticket.getPayload();
|
||||||
|
const googleId = payload['sub'];
|
||||||
|
const email = payload['email'];
|
||||||
|
const name = payload['name'];
|
||||||
|
const picture = payload['picture'];
|
||||||
|
|
||||||
|
console.log(`👤 OAuth callback verified for: ${email}`);
|
||||||
|
|
||||||
|
// Check if email is authorized
|
||||||
|
const authorized = await isEmailAuthorized(email);
|
||||||
|
console.log(`🔒 Email authorization check for ${email}: ${authorized ? 'ALLOWED' : 'DENIED'}`);
|
||||||
|
|
||||||
|
if (!authorized) {
|
||||||
|
console.log(`❌ Access denied for ${email}`);
|
||||||
|
return res.status(403).json({
|
||||||
|
error: 'Access denied',
|
||||||
|
message: 'Your email address is not authorized to access this application'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create user object
|
||||||
|
const user = {
|
||||||
|
id: googleId,
|
||||||
|
email,
|
||||||
|
name,
|
||||||
|
picture,
|
||||||
|
google_id: googleId,
|
||||||
|
};
|
||||||
|
console.log('✅ User object created from OAuth callback');
|
||||||
|
|
||||||
|
// Generate JWT token
|
||||||
|
const jwtToken = generateToken(user);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
token: jwtToken,
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
picture: user.picture,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('OAuth callback error:', error);
|
||||||
|
res.status(401).json({ error: 'OAuth authentication failed' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Verify JWT token
|
// Verify JWT token
|
||||||
router.get('/verify', authenticateToken, async (req, res) => {
|
router.get('/verify', authenticateToken, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ module.exports = {
|
|||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
template: './client/public/index.html',
|
template: './client/public/index.html',
|
||||||
templateParameters: {
|
templateParameters: {
|
||||||
REACT_APP_GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID || 'your_google_client_id_here',
|
REACT_APP_GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
new webpack.DefinePlugin({
|
new webpack.DefinePlugin({
|
||||||
|
|||||||
Reference in New Issue
Block a user