2026-07-30 20:02:14 +00:00
|
|
|
import { app, BrowserWindow, dialog, ipcMain, session } from 'electron';
|
2026-07-14 11:31:11 +00:00
|
|
|
import path from 'path';
|
|
|
|
|
import fs from 'fs';
|
|
|
|
|
import https from 'https';
|
|
|
|
|
|
|
|
|
|
const AUTH_HOST = 'https://account.rokojori.com';
|
|
|
|
|
const PORT = 3000;
|
|
|
|
|
|
2026-07-18 06:37:47 +00:00
|
|
|
// ── Persistence helpers ────────────────────────────────────────────────────────
|
|
|
|
|
|
2026-07-14 11:31:11 +00:00
|
|
|
function tokenFile(): string {
|
|
|
|
|
return path.join( app.getPath( 'userData' ), 'tokens.json' );
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 06:37:47 +00:00
|
|
|
function emailFile(): string {
|
|
|
|
|
return path.join( app.getPath( 'userData' ), 'last-email.txt' );
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-14 11:31:11 +00:00
|
|
|
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 */ }
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 06:37:47 +00:00
|
|
|
function passwordFile(): string {
|
|
|
|
|
return path.join( app.getPath( 'userData' ), 'last-password.txt' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function loadLastEmail(): string {
|
|
|
|
|
try { return fs.readFileSync( emailFile(), 'utf-8' ).trim(); } catch { return ''; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function saveLastEmail( email: string ): void {
|
|
|
|
|
fs.writeFileSync( emailFile(), email, 'utf-8' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function loadLastPassword(): string {
|
|
|
|
|
try { return fs.readFileSync( passwordFile(), 'utf-8' ).trim(); } catch { return ''; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function saveLastPassword( password: string ): void {
|
|
|
|
|
fs.writeFileSync( passwordFile(), password, 'utf-8' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function clearCredentials(): void {
|
|
|
|
|
try { fs.unlinkSync( emailFile() ); } catch { /* already gone */ }
|
|
|
|
|
try { fs.unlinkSync( passwordFile() ); } catch { /* already gone */ }
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-30 20:02:14 +00:00
|
|
|
function localRecentsFile(): string {
|
|
|
|
|
return path.join( app.getPath( 'userData' ), 'local-recents.json' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function loadLocalRecents(): string[] {
|
|
|
|
|
try {
|
|
|
|
|
const raw = fs.readFileSync( localRecentsFile(), 'utf-8' );
|
|
|
|
|
return JSON.parse( raw ) as string[];
|
|
|
|
|
} catch {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function saveLocalRecents( recents: string[] ): void {
|
|
|
|
|
fs.writeFileSync( localRecentsFile(), JSON.stringify( recents ), 'utf-8' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function addLocalRecent( folderPath: string ): void {
|
|
|
|
|
const recents = loadLocalRecents().filter( r => r !== folderPath );
|
|
|
|
|
recents.unshift( folderPath );
|
|
|
|
|
saveLocalRecents( recents.slice( 0, 10 ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function removeLocalRecent( folderPath: string ): string[] {
|
|
|
|
|
const recents = loadLocalRecents().filter( r => r !== folderPath );
|
|
|
|
|
saveLocalRecents( recents );
|
|
|
|
|
return recents;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 06:37:47 +00:00
|
|
|
// ── Network ────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
2026-07-14 11:31:11 +00:00
|
|
|
function postJson( url: string, body: unknown ): Promise<unknown> {
|
|
|
|
|
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();
|
|
|
|
|
} );
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 06:37:47 +00:00
|
|
|
// ── Header injection ───────────────────────────────────────────────────────────
|
|
|
|
|
|
2026-07-14 11:31:11 +00:00
|
|
|
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 } );
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 06:37:47 +00:00
|
|
|
// ── Windows ────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
2026-07-14 11:31:11 +00:00
|
|
|
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; } );
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 06:37:47 +00:00
|
|
|
async function createMainWindow(): Promise<void> {
|
|
|
|
|
await session.defaultSession.clearStorageData( { storages: [ 'cookies' ] } );
|
|
|
|
|
|
2026-07-14 11:31:11 +00:00
|
|
|
mainWindow = new BrowserWindow( {
|
|
|
|
|
width: 1400,
|
|
|
|
|
height: 900,
|
|
|
|
|
webPreferences: {
|
|
|
|
|
nodeIntegration: false,
|
|
|
|
|
contextIsolation: true,
|
2026-07-30 20:02:14 +00:00
|
|
|
preload: path.join( __dirname, 'preload.js' ),
|
2026-07-14 11:31:11 +00:00
|
|
|
},
|
|
|
|
|
title: 'Roject',
|
|
|
|
|
} );
|
|
|
|
|
|
2026-07-14 20:27:03 +00:00
|
|
|
mainWindow.loadURL( `http://localhost:${PORT}/` );
|
2026-07-14 11:31:11 +00:00
|
|
|
|
|
|
|
|
const localBase = `http://localhost:${PORT}/`;
|
|
|
|
|
|
|
|
|
|
mainWindow.webContents.on( 'will-navigate', ( event, url ) => {
|
|
|
|
|
if ( !url.startsWith( localBase ) ) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
clearTokens();
|
|
|
|
|
currentTokens = null;
|
|
|
|
|
mainWindow?.close();
|
|
|
|
|
createLoginWindow();
|
|
|
|
|
}
|
|
|
|
|
} );
|
|
|
|
|
|
|
|
|
|
mainWindow.on( 'closed', () => { mainWindow = null; } );
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 06:37:47 +00:00
|
|
|
// ── Server ─────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
2026-07-14 11:31:11 +00:00
|
|
|
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();
|
|
|
|
|
process.env.ROJECT_ROOT = path.join( __dirname, '..', '..' );
|
2026-07-30 20:02:14 +00:00
|
|
|
process.env.ROJECT_ELECTRON = 'true';
|
2026-07-14 11:31:11 +00:00
|
|
|
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 );
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 06:37:47 +00:00
|
|
|
// ── App lifecycle ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
2026-07-14 11:31:11 +00:00
|
|
|
app.whenReady().then( () => {
|
|
|
|
|
registerHeaderInjector( () => currentTokens?.accessToken ?? null );
|
|
|
|
|
|
2026-07-18 06:37:47 +00:00
|
|
|
ipcMain.handle( 'auth:login', async ( _event, email: string, password: string, remember: boolean ) => {
|
2026-07-14 11:31:11 +00:00
|
|
|
try {
|
|
|
|
|
const result = await postJson( `${AUTH_HOST}/api/auth/login`, { email, password } ) as Record<string, unknown>;
|
|
|
|
|
if ( result.accessToken && result.refreshToken ) {
|
|
|
|
|
currentTokens = { accessToken: result.accessToken as string, refreshToken: result.refreshToken as string };
|
|
|
|
|
saveTokens( currentTokens );
|
2026-07-18 06:37:47 +00:00
|
|
|
if ( remember ) {
|
|
|
|
|
saveLastEmail( email );
|
|
|
|
|
saveLastPassword( password );
|
|
|
|
|
} else {
|
|
|
|
|
clearCredentials();
|
|
|
|
|
}
|
2026-07-14 11:31:11 +00:00
|
|
|
return { ok: true };
|
|
|
|
|
}
|
|
|
|
|
return { ok: false, error: ( result.error as string ) ?? 'Login failed' };
|
|
|
|
|
} catch ( err ) {
|
|
|
|
|
return { ok: false, error: String( err ) };
|
|
|
|
|
}
|
|
|
|
|
} );
|
|
|
|
|
|
2026-07-18 06:37:47 +00:00
|
|
|
ipcMain.handle( 'auth:last-email', () => loadLastEmail() );
|
|
|
|
|
ipcMain.handle( 'auth:last-password', () => loadLastPassword() );
|
|
|
|
|
ipcMain.handle( 'auth:clear-credentials', () => { clearCredentials(); } );
|
|
|
|
|
|
2026-07-30 20:02:14 +00:00
|
|
|
ipcMain.handle( 'local:open-folder', async () => {
|
|
|
|
|
const win = mainWindow ?? BrowserWindow.getFocusedWindow();
|
|
|
|
|
if ( !win ) return null;
|
|
|
|
|
const result = await dialog.showOpenDialog( win, { properties: [ 'openDirectory' ] } );
|
|
|
|
|
if ( result.canceled || result.filePaths.length === 0 ) return null;
|
|
|
|
|
const folderPath = result.filePaths[ 0 ];
|
|
|
|
|
addLocalRecent( folderPath );
|
|
|
|
|
return folderPath;
|
|
|
|
|
} );
|
|
|
|
|
|
|
|
|
|
ipcMain.handle( 'local:get-recents', () => loadLocalRecents() );
|
|
|
|
|
|
|
|
|
|
ipcMain.handle( 'local:remove-recent', ( _event, folderPath: string ) => {
|
|
|
|
|
return removeLocalRecent( folderPath );
|
|
|
|
|
} );
|
|
|
|
|
|
2026-07-14 11:31:11 +00:00
|
|
|
ipcMain.on( 'auth:login-success', () => {
|
2026-07-18 06:37:47 +00:00
|
|
|
// Create the main window first, close login only after it exists.
|
|
|
|
|
// Closing login before main is ready triggers window-all-closed → app quit.
|
|
|
|
|
createMainWindow().then( () => loginWindow?.close() );
|
2026-07-14 11:31:11 +00:00
|
|
|
} );
|
|
|
|
|
|
|
|
|
|
startExpressServer();
|
|
|
|
|
|
2026-07-18 06:37:47 +00:00
|
|
|
setTimeout( async () => {
|
2026-07-14 11:31:11 +00:00
|
|
|
currentTokens = loadTokens();
|
|
|
|
|
if ( currentTokens ) {
|
2026-07-18 06:37:47 +00:00
|
|
|
try {
|
|
|
|
|
const result = await postJson(
|
|
|
|
|
`${AUTH_HOST}/api/auth/refresh`,
|
|
|
|
|
{ refreshToken: currentTokens.refreshToken }
|
|
|
|
|
) as Record<string, unknown>;
|
|
|
|
|
if ( result.accessToken && result.refreshToken ) {
|
|
|
|
|
currentTokens = { accessToken: result.accessToken as string, refreshToken: result.refreshToken as string };
|
|
|
|
|
saveTokens( currentTokens );
|
|
|
|
|
createMainWindow();
|
|
|
|
|
} else {
|
|
|
|
|
clearTokens();
|
|
|
|
|
currentTokens = null;
|
|
|
|
|
createLoginWindow();
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
clearTokens();
|
|
|
|
|
currentTokens = null;
|
|
|
|
|
createLoginWindow();
|
|
|
|
|
}
|
2026-07-14 11:31:11 +00:00
|
|
|
} else {
|
|
|
|
|
createLoginWindow();
|
|
|
|
|
}
|
|
|
|
|
}, 500 );
|
|
|
|
|
|
|
|
|
|
app.on( 'activate', () => {
|
|
|
|
|
if ( BrowserWindow.getAllWindows().length === 0 ) {
|
2026-07-18 06:37:47 +00:00
|
|
|
if ( currentTokens ) void createMainWindow();
|
2026-07-14 11:31:11 +00:00
|
|
|
else createLoginWindow();
|
|
|
|
|
}
|
|
|
|
|
} );
|
|
|
|
|
} );
|
|
|
|
|
|
|
|
|
|
app.on( 'window-all-closed', () => {
|
|
|
|
|
if ( process.platform !== 'darwin' ) app.quit();
|
|
|
|
|
} );
|