The app reads its Base44 app id and backend URL from VITE_BASE44_APP_ID and VITE_BASE44_BACKEND_URL, but neither was set anywhere, so Vite compiled both defaults to undefined and app-params resolved them to null at runtime. On base44's own host these arrive as query params; on a custom domain they never do. A null serverUrl skipped the SDK's default (it only applies to undefined), making every API baseURL relative — requests hit our own nginx, which answered with index.html, and the app rendered against HTML and died. Deploys reported success while serving an empty page. appBaseUrl was never passed either, so login/logout redirects were relative too and bounced between /login and the SPA fallback. redirectToLogin also omits app_id, which base44.app/login 404s on when the request comes from our domain rather than a *.base44.app one, so login now goes through loginWithProvider. Set both vars as build args in the Dockerfile — they must be present at build time, since runtime env vars in Dokploy arrive after Vite has already inlined them. Guard the build against an empty app id and a missing dist/index.html. Surface failures instead of hiding them: an ErrorBoundary around the tree, and a diagnostic panel for auth errors that previously fell through to a crash. In nginx, serve a real 404 for missing /assets/ files. Falling through to the SPA rule returned index.html as text/html, which the browser refuses to run as a module — another silent blank page. Also add /health and stop the SPA fallback from being cached under the requested path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
158 lines
4.9 KiB
JavaScript
158 lines
4.9 KiB
JavaScript
import React, { createContext, useState, useContext, useEffect } from 'react';
|
|
import { base44 } from '@/api/base44Client';
|
|
import { appParams } from '@/lib/app-params';
|
|
import { createAxiosClient } from '@base44/sdk/dist/utils/axios-client';
|
|
|
|
const AuthContext = createContext();
|
|
|
|
export const AuthProvider = ({ children }) => {
|
|
const [user, setUser] = useState(null);
|
|
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
|
const [isLoadingAuth, setIsLoadingAuth] = useState(true);
|
|
const [isLoadingPublicSettings, setIsLoadingPublicSettings] = useState(true);
|
|
const [authError, setAuthError] = useState(null);
|
|
const [appPublicSettings, setAppPublicSettings] = useState(null); // Contains only { id, public_settings }
|
|
|
|
useEffect(() => {
|
|
checkAppState();
|
|
}, []);
|
|
|
|
const checkAppState = async () => {
|
|
try {
|
|
setIsLoadingPublicSettings(true);
|
|
setAuthError(null);
|
|
|
|
// First, check app public settings (with token if available)
|
|
// This will tell us if auth is required, user not registered, etc.
|
|
const appClient = createAxiosClient({
|
|
baseURL: `${appParams.serverUrl}/api/apps/public`,
|
|
headers: {
|
|
'X-App-Id': appParams.appId
|
|
},
|
|
token: appParams.token, // Include token if available
|
|
interceptResponses: true
|
|
});
|
|
|
|
try {
|
|
const publicSettings = await appClient.get(`/prod/public-settings/by-id/${appParams.appId}`);
|
|
setAppPublicSettings(publicSettings);
|
|
|
|
// If we got the app public settings successfully, check if user is authenticated
|
|
if (appParams.token) {
|
|
await checkUserAuth();
|
|
} else {
|
|
setIsLoadingAuth(false);
|
|
setIsAuthenticated(false);
|
|
}
|
|
setIsLoadingPublicSettings(false);
|
|
} catch (appError) {
|
|
console.error('App state check failed:', appError);
|
|
|
|
// Handle app-level errors
|
|
if (appError.status === 403 && appError.data?.extra_data?.reason) {
|
|
const reason = appError.data.extra_data.reason;
|
|
if (reason === 'auth_required') {
|
|
setAuthError({
|
|
type: 'auth_required',
|
|
message: 'Authentication required'
|
|
});
|
|
} else if (reason === 'user_not_registered') {
|
|
setAuthError({
|
|
type: 'user_not_registered',
|
|
message: 'User not registered for this app'
|
|
});
|
|
} else {
|
|
setAuthError({
|
|
type: reason,
|
|
message: appError.message
|
|
});
|
|
}
|
|
} else {
|
|
setAuthError({
|
|
type: 'unknown',
|
|
message: appError.message || 'Failed to load app'
|
|
});
|
|
}
|
|
setIsLoadingPublicSettings(false);
|
|
setIsLoadingAuth(false);
|
|
}
|
|
} catch (error) {
|
|
console.error('Unexpected error:', error);
|
|
setAuthError({
|
|
type: 'unknown',
|
|
message: error.message || 'An unexpected error occurred'
|
|
});
|
|
setIsLoadingPublicSettings(false);
|
|
setIsLoadingAuth(false);
|
|
}
|
|
};
|
|
|
|
const checkUserAuth = async () => {
|
|
try {
|
|
// Now check if the user is authenticated
|
|
setIsLoadingAuth(true);
|
|
const currentUser = await base44.auth.me();
|
|
setUser(currentUser);
|
|
setIsAuthenticated(true);
|
|
setIsLoadingAuth(false);
|
|
} catch (error) {
|
|
console.error('User auth check failed:', error);
|
|
setIsLoadingAuth(false);
|
|
setIsAuthenticated(false);
|
|
|
|
// If user auth fails, it might be an expired token
|
|
if (error.status === 401 || error.status === 403) {
|
|
setAuthError({
|
|
type: 'auth_required',
|
|
message: 'Authentication required'
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
const logout = (shouldRedirect = true) => {
|
|
setUser(null);
|
|
setIsAuthenticated(false);
|
|
|
|
if (shouldRedirect) {
|
|
// Use the SDK's logout method which handles token cleanup and redirect
|
|
base44.auth.logout(window.location.href);
|
|
} else {
|
|
// Just remove the token without redirect
|
|
base44.auth.logout();
|
|
}
|
|
};
|
|
|
|
const navigateToLogin = () => {
|
|
// redirectToLogin() posts to `${appBaseUrl}/login` with no app_id, so Base44
|
|
// cannot tell which app is asking when the request comes from our own domain
|
|
// instead of a *.base44.app one. loginWithProvider() hits the same login flow
|
|
// but carries app_id in the query, which is what a self-hosted build needs.
|
|
base44.auth.loginWithProvider('google', window.location.href);
|
|
};
|
|
|
|
return (
|
|
<AuthContext.Provider value={{
|
|
user,
|
|
isAuthenticated,
|
|
isLoadingAuth,
|
|
isLoadingPublicSettings,
|
|
authError,
|
|
appPublicSettings,
|
|
logout,
|
|
navigateToLogin,
|
|
checkAppState
|
|
}}>
|
|
{children}
|
|
</AuthContext.Provider>
|
|
);
|
|
};
|
|
|
|
export const useAuth = () => {
|
|
const context = useContext(AuthContext);
|
|
if (!context) {
|
|
throw new Error('useAuth must be used within an AuthProvider');
|
|
}
|
|
return context;
|
|
};
|