import { app, BrowserWindow, ipcMain, session } from 'electron'; import path from 'path'; import fs from 'fs'; import https from 'https'; const AUTH_HOST = 'https://account.rokojori.com'; const PORT = 3000; function tokenFile(): string { return path.join( app.getPath( 'userData' ), 'tokens.json' ); } interface Tokens { accessToken: string; refreshToken: string; } function loadTokens(): Tokens | null { try { const raw = fs.readFileSync( tokenFile(), 'utf-8' ); return JSON.parse( raw ) as Tokens; } catch { return null; } } function saveTokens( tokens: Tokens ): void { fs.writeFileSync( tokenFile(), JSON.stringify( tokens ), 'utf-8' ); } function clearTokens(): void { try { fs.unlinkSync( tokenFile() ); } catch { /* already gone */ } } function postJson( url: string, body: unknown ): Promise { return new Promise( ( resolve, reject ) => { const data = JSON.stringify( body ); const parsed = new URL( url ); const req = https.request( { hostname: parsed.hostname, path: parsed.pathname, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength( data ), }, }, ( res ) => { let raw = ''; res.on( 'data', ( chunk: string ) => { raw += chunk; } ); res.on( 'end', () => { try { resolve( JSON.parse( raw ) ); } catch { reject( new Error( `Non-JSON response: ${raw}` ) ); } } ); } ); req.on( 'error', reject ); req.write( data ); req.end(); } ); } function registerHeaderInjector( getToken: () => string | null ): void { session.defaultSession.webRequest.onBeforeSendHeaders( { urls: [ `http://localhost:${PORT}/*` ] }, ( details, callback ) => { const token = getToken(); const headers = { ...details.requestHeaders }; if ( token ) headers[ 'Authorization' ] = `Bearer ${token}`; callback( { requestHeaders: headers } ); } ); } let mainWindow: BrowserWindow | null = null; let loginWindow: BrowserWindow | null = null; let currentTokens: Tokens | null = null; function createLoginWindow(): void { loginWindow = new BrowserWindow( { width: 420, height: 520, resizable: false, webPreferences: { nodeIntegration: false, contextIsolation: true, preload: path.join( __dirname, 'preload.js' ), }, title: 'Roject — Sign in', } ); loginWindow.loadFile( path.join( __dirname, 'login.html' ) ); loginWindow.on( 'closed', () => { loginWindow = null; } ); } function createMainWindow(): void { mainWindow = new BrowserWindow( { width: 1400, height: 900, webPreferences: { nodeIntegration: false, contextIsolation: true, }, title: 'Roject', } ); mainWindow.loadURL( `http://localhost:${PORT}/` ); const localBase = `http://localhost:${PORT}/`; // Keep the main window on localhost — any navigation away means the user needs to re-auth mainWindow.webContents.on( 'will-navigate', ( event, url ) => { if ( !url.startsWith( localBase ) ) { event.preventDefault(); clearTokens(); currentTokens = null; mainWindow?.close(); createLoginWindow(); } } ); mainWindow.on( 'closed', () => { mainWindow = null; } ); } function loadEnv(): void { const envPath = path.join( __dirname, '..', '..', '.env' ); try { const lines = fs.readFileSync( envPath, 'utf-8' ).split( /\r?\n/ ); for ( const line of lines ) { const trimmed = line.trim(); if ( !trimmed || trimmed.startsWith( '#' ) ) continue; const eq = trimmed.indexOf( '=' ); if ( eq === -1 ) continue; const key = trimmed.slice( 0, eq ).trim(); const val = trimmed.slice( eq + 1 ).trim().replace( /^["']|["']$/g, '' ); if ( key && !( key in process.env ) ) process.env[ key ] = val; } } catch { /* no .env file — rely on inherited env */ } } function startExpressServer(): void { loadEnv(); // Tell the server where the project root is so __dirname-relative paths work when compiled. // build/electron/ → up two levels → project root. process.env.ROJECT_ROOT = path.join( __dirname, '..', '..' ); const serverPath = path.join( __dirname, '..', 'server', 'server', 'index.js' ); // eslint-disable-next-line @typescript-eslint/no-require-imports const { startServer } = require( serverPath ) as { startServer: ( port: number ) => void }; startServer( PORT ); } app.whenReady().then( () => { registerHeaderInjector( () => currentTokens?.accessToken ?? null ); ipcMain.handle( 'auth:login', async ( _event, email: string, password: string ) => { try { const result = await postJson( `${AUTH_HOST}/api/auth/login`, { email, password } ) as Record; if ( result.accessToken && result.refreshToken ) { currentTokens = { accessToken: result.accessToken as string, refreshToken: result.refreshToken as string }; saveTokens( currentTokens ); return { ok: true }; } return { ok: false, error: ( result.error as string ) ?? 'Login failed' }; } catch ( err ) { return { ok: false, error: String( err ) }; } } ); ipcMain.on( 'auth:login-success', () => { loginWindow?.close(); createMainWindow(); } ); startExpressServer(); // Give the server a moment to bind before loading the window setTimeout( () => { currentTokens = loadTokens(); if ( currentTokens ) { createMainWindow(); } else { createLoginWindow(); } }, 500 ); app.on( 'activate', () => { if ( BrowserWindow.getAllWindows().length === 0 ) { if ( currentTokens ) createMainWindow(); else createLoginWindow(); } } ); } ); app.on( 'window-all-closed', () => { if ( process.platform !== 'darwin' ) app.quit(); } );