mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-02 08:33:36 +00:00
feat: add support for external server mode with Docker integration
- Introduced a new `docker-compose.dev-server.yml` for running the backend API in a container, enabling local Electron to connect to it. - Updated `dev.mjs` to include a new option for launching the Docker server container. - Enhanced the UI application to support external server mode, allowing session-based authentication and adjusting routing logic accordingly. - Added utility functions to check and cache the external server mode status for improved performance. - Updated various components to handle authentication and routing based on the server mode.
This commit is contained in:
@@ -131,6 +131,39 @@ export const isElectronMode = (): boolean => {
|
||||
return api?.isElectron === true || !!api?.getApiKey;
|
||||
};
|
||||
|
||||
// Cached external server mode flag
|
||||
let cachedExternalServerMode: boolean | null = null;
|
||||
|
||||
/**
|
||||
* Check if running in external server mode (Docker API)
|
||||
* In this mode, Electron uses session-based auth like web mode
|
||||
*/
|
||||
export const checkExternalServerMode = async (): Promise<boolean> => {
|
||||
if (cachedExternalServerMode !== null) {
|
||||
return cachedExternalServerMode;
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
const api = window.electronAPI as any;
|
||||
if (api?.isExternalServerMode) {
|
||||
try {
|
||||
cachedExternalServerMode = await api.isExternalServerMode();
|
||||
return cachedExternalServerMode;
|
||||
} catch (error) {
|
||||
logger.warn('Failed to check external server mode:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cachedExternalServerMode = false;
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get cached external server mode (synchronous, returns null if not yet checked)
|
||||
*/
|
||||
export const isExternalServerMode = (): boolean | null => cachedExternalServerMode;
|
||||
|
||||
/**
|
||||
* Initialize API key and server URL for Electron mode authentication.
|
||||
* In web mode, authentication uses HTTP-only cookies instead.
|
||||
|
||||
@@ -132,6 +132,9 @@ let saveWindowBoundsTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
// API key for CSRF protection
|
||||
let apiKey: string | null = null;
|
||||
|
||||
// Track if we're using an external server (Docker API mode)
|
||||
let isExternalServerMode = false;
|
||||
|
||||
/**
|
||||
* Get the relative path to API key file within userData
|
||||
*/
|
||||
@@ -688,14 +691,35 @@ app.whenReady().then(async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Generate or load API key for CSRF protection (before starting server)
|
||||
ensureApiKey();
|
||||
|
||||
try {
|
||||
// Find available ports (prevents conflicts with other apps using same ports)
|
||||
serverPort = await findAvailablePort(DEFAULT_SERVER_PORT);
|
||||
if (serverPort !== DEFAULT_SERVER_PORT) {
|
||||
logger.info('Default server port', DEFAULT_SERVER_PORT, 'in use, using port', serverPort);
|
||||
// Check if we should skip the embedded server (for Docker API mode)
|
||||
const skipEmbeddedServer = process.env.SKIP_EMBEDDED_SERVER === 'true';
|
||||
isExternalServerMode = skipEmbeddedServer;
|
||||
|
||||
if (skipEmbeddedServer) {
|
||||
// Use the default server port (Docker container runs on 3008)
|
||||
serverPort = DEFAULT_SERVER_PORT;
|
||||
logger.info('SKIP_EMBEDDED_SERVER=true, using external server at port', serverPort);
|
||||
|
||||
// Wait for external server to be ready
|
||||
logger.info('Waiting for external server...');
|
||||
await waitForServer(60); // Give Docker container more time to start
|
||||
logger.info('External server is ready');
|
||||
|
||||
// In external server mode, we don't set an API key here.
|
||||
// The renderer will detect external server mode and use session-based
|
||||
// auth like web mode, redirecting to /login where the user enters
|
||||
// the API key from the Docker container logs.
|
||||
logger.info('External server mode: using session-based authentication');
|
||||
} else {
|
||||
// Generate or load API key for CSRF protection (before starting server)
|
||||
ensureApiKey();
|
||||
|
||||
// Find available ports (prevents conflicts with other apps using same ports)
|
||||
serverPort = await findAvailablePort(DEFAULT_SERVER_PORT);
|
||||
if (serverPort !== DEFAULT_SERVER_PORT) {
|
||||
logger.info('Default server port', DEFAULT_SERVER_PORT, 'in use, using port', serverPort);
|
||||
}
|
||||
}
|
||||
|
||||
staticPort = await findAvailablePort(DEFAULT_STATIC_PORT);
|
||||
@@ -708,8 +732,10 @@ app.whenReady().then(async () => {
|
||||
await startStaticServer();
|
||||
}
|
||||
|
||||
// Start backend server
|
||||
await startServer();
|
||||
// Start backend server (unless using external server)
|
||||
if (!skipEmbeddedServer) {
|
||||
await startServer();
|
||||
}
|
||||
|
||||
// Create window
|
||||
createWindow();
|
||||
@@ -909,10 +935,20 @@ ipcMain.handle('server:getUrl', async () => {
|
||||
});
|
||||
|
||||
// Get API key for authentication
|
||||
// Returns null in external server mode to trigger session-based auth
|
||||
ipcMain.handle('auth:getApiKey', () => {
|
||||
if (isExternalServerMode) {
|
||||
return null;
|
||||
}
|
||||
return apiKey;
|
||||
});
|
||||
|
||||
// Check if running in external server mode (Docker API)
|
||||
// Used by renderer to determine auth flow
|
||||
ipcMain.handle('auth:isExternalServerMode', () => {
|
||||
return isExternalServerMode;
|
||||
});
|
||||
|
||||
// Window management - update minimum width based on sidebar state
|
||||
// Now uses a fixed small minimum since horizontal scrolling handles overflow
|
||||
ipcMain.handle('window:updateMinWidth', (_, _sidebarExpanded: boolean) => {
|
||||
|
||||
@@ -25,6 +25,9 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
// Get API key for authentication
|
||||
getApiKey: (): Promise<string | null> => ipcRenderer.invoke('auth:getApiKey'),
|
||||
|
||||
// Check if running in external server mode (Docker API)
|
||||
isExternalServerMode: (): Promise<boolean> => ipcRenderer.invoke('auth:isExternalServerMode'),
|
||||
|
||||
// Native dialogs - better UX than prompt()
|
||||
openDirectory: (): Promise<Electron.OpenDialogReturnValue> =>
|
||||
ipcRenderer.invoke('dialog:openDirectory'),
|
||||
|
||||
@@ -18,6 +18,8 @@ import {
|
||||
verifySession,
|
||||
checkSandboxEnvironment,
|
||||
getServerUrlSync,
|
||||
checkExternalServerMode,
|
||||
isExternalServerMode,
|
||||
} from '@/lib/http-api-client';
|
||||
import { Toaster } from 'sonner';
|
||||
import { ThemeOption, themeOptions } from '@/config/theme-options';
|
||||
@@ -188,13 +190,16 @@ function RootLayoutContent() {
|
||||
// Initialize API key for Electron mode
|
||||
await initApiKey();
|
||||
|
||||
// In Electron mode, we're always authenticated via header
|
||||
if (isElectronMode()) {
|
||||
// Check if running in external server mode (Docker API)
|
||||
const externalMode = await checkExternalServerMode();
|
||||
|
||||
// In Electron mode (but NOT external server mode), we're always authenticated via header
|
||||
if (isElectronMode() && !externalMode) {
|
||||
useAuthStore.getState().setAuthState({ isAuthenticated: true, authChecked: true });
|
||||
return;
|
||||
}
|
||||
|
||||
// In web mode, verify the session cookie is still valid
|
||||
// In web mode OR external server mode, verify the session cookie is still valid
|
||||
// by making a request to an authenticated endpoint
|
||||
const isValid = await verifySession();
|
||||
|
||||
@@ -235,17 +240,20 @@ function RootLayoutContent() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Routing rules (web mode):
|
||||
// Routing rules (web mode and external server mode):
|
||||
// - If not authenticated: force /login (even /setup is protected)
|
||||
// - If authenticated but setup incomplete: force /setup
|
||||
useEffect(() => {
|
||||
if (!setupHydrated) return;
|
||||
|
||||
// Check if we need session-based auth (web mode OR external server mode)
|
||||
const needsSessionAuth = !isElectronMode() || isExternalServerMode() === true;
|
||||
|
||||
// Wait for auth check to complete before enforcing any redirects
|
||||
if (!isElectronMode() && !authChecked) return;
|
||||
if (needsSessionAuth && !authChecked) return;
|
||||
|
||||
// Unauthenticated -> force /login
|
||||
if (!isElectronMode() && !isAuthenticated) {
|
||||
if (needsSessionAuth && !isAuthenticated) {
|
||||
if (location.pathname !== '/login') {
|
||||
navigate({ to: '/login' });
|
||||
}
|
||||
@@ -351,8 +359,11 @@ function RootLayoutContent() {
|
||||
);
|
||||
}
|
||||
|
||||
// Wait for auth check before rendering protected routes (web mode only)
|
||||
if (!isElectronMode() && !authChecked) {
|
||||
// Check if we need session-based auth (web mode OR external server mode)
|
||||
const needsSessionAuth = !isElectronMode() || isExternalServerMode() === true;
|
||||
|
||||
// Wait for auth check before rendering protected routes (web mode and external server mode)
|
||||
if (needsSessionAuth && !authChecked) {
|
||||
return (
|
||||
<main className="flex h-screen items-center justify-center" data-testid="app-container">
|
||||
<LoadingState message="Loading..." />
|
||||
@@ -360,9 +371,9 @@ function RootLayoutContent() {
|
||||
);
|
||||
}
|
||||
|
||||
// Redirect to login if not authenticated (web mode)
|
||||
// Redirect to login if not authenticated (web mode and external server mode)
|
||||
// Show loading state while navigation to login is in progress
|
||||
if (!isElectronMode() && !isAuthenticated) {
|
||||
if (needsSessionAuth && !isAuthenticated) {
|
||||
return (
|
||||
<main className="flex h-screen items-center justify-center" data-testid="app-container">
|
||||
<LoadingState message="Redirecting to login..." />
|
||||
|
||||
Reference in New Issue
Block a user