Skip to content

Commit

Permalink
Merge pull request #12 from elek-io/local-api
Browse files Browse the repository at this point in the history
OpenAPI based local API
  • Loading branch information
Nils-Kolvenbach authored Oct 9, 2024
2 parents 59f47b9 + fadeaad commit 6551560
Show file tree
Hide file tree
Showing 14 changed files with 404 additions and 451 deletions.
5 changes: 5 additions & 0 deletions .changeset/empty-cars-cough.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@elek-io/client': minor
---

Added local API for developers to be able to recieve Project data locally e.g. for usage in websites during a build step. The local API is based on the OpenAPI specification. Meaning there is a swagger UI available locally when enabled in the users profile. The API can be used to query the data manually or use the [OpenAPI Generator CLI](https://openapi-generator.tech/) like so: `openapi-generator-cli generate -i ./openapi.json -g typescript-fetch -o ./src/api-client --openapi-normalizer SET_TAGS_FOR_ALL_OPERATIONS=elek-io`, where `SET_TAGS_FOR_ALL_OPERATIONS=elek-io` merges the tagged APIs into one for ease of use.
111 changes: 93 additions & 18 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"dependencies": {
"@electron-toolkit/preload": "3.0.1",
"@electron-toolkit/utils": "3.0.0",
"@elek-io/core": "0.11.0",
"@elek-io/core": "0.14.0",
"@fontsource-variable/montserrat": "5.0.19",
"@fontsource/roboto": "5.0.13",
"@hookform/resolvers": "3.9.0",
Expand Down
125 changes: 39 additions & 86 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,20 @@ Sentry.init({

class Main {
public readonly customFileProtocol: string = 'elek-io-local-file';
private allowedOriginsToLoadInternal: string[] = [];
private allowedOriginsToLoadExternal: string[] = [
private allowedHostnamesToLoadInternal: string[] = [];
private allowedHostnamesToLoadExternal: string[] = [
this.customFileProtocol,
'https://elek.io',
'https://api.elek.io',
'https://github.com',
'localhost',
'elek.io',
'api.elek.io',
'github.com',
];
private core: ElekIoCore | null = null;

constructor() {
// Allow the vite dev server to do HMR in development
if (app.isPackaged === false && process.env['ELECTRON_RENDERER_URL']) {
this.allowedOriginsToLoadInternal.push(
this.allowedHostnamesToLoadInternal.push(
process.env['ELECTRON_RENDERER_URL']
);
}
Expand Down Expand Up @@ -75,6 +76,11 @@ class Main {
this.core = new ElekIoCore({
log: { level: app.isPackaged ? 'info' : 'debug' },
});
const user = await this.core.user.get();

if (user && user.localApi.isEnabled) {
this.core.api.start(user.localApi.port);
}

this.registerCustomFileProtocol();

Expand Down Expand Up @@ -122,13 +128,13 @@ class Main {
const parsedUrl = new URL(url);

if (
this.allowedOriginsToLoadExternal.includes(parsedUrl.origin) === false
this.allowedHostnamesToLoadExternal.includes(parsedUrl.hostname) ===
false
) {
Sentry.captureException(
new SecurityError(
`Prevented navigation to untrusted, external origin "${parsedUrl}" from "${webContents.getURL()}"`
)
);
const errorMessage = `Prevented navigation to untrusted, external URL "${parsedUrl.toString()}" from "${webContents.getURL()}"`;
Sentry.captureException(new SecurityError(errorMessage));
this.core?.logger.error(errorMessage);

return { action: 'deny' };
}

Expand All @@ -143,27 +149,13 @@ class Main {
* Creates a new window with security in mind and loads the frontend
*/
private async createWindow(): Promise<BrowserWindow> {
if (!this.core) {
throw new Error(
'Trying to create a new window but Core is not initialized.'
);
}

const initialWindowSize = this.getInitialWindowSize();
const user = await this.core.user.get();

const options: BrowserWindowConstructorOptions = {
width: initialWindowSize.width,
height: initialWindowSize.height,
};

if (user?.window) {
options.width = user.window.width;
options.height = user.window.height;
options.x = user.window.position.x;
options.y = user.window.position.y;
}

// Overwrite webPreferences to always load the correct preload script
// and explicitly enable security features - although Electron > v28 should set these by default
options.webPreferences = {
Expand All @@ -186,8 +178,6 @@ class Main {
this.onWindowWebContentsWillNavigate(window, event, urlToLoad)
);

window.on('close', (event) => this.onWindowClose(window, event));

if (app.isPackaged) {
// Client is in production
// Load the static index.html directly
Expand Down Expand Up @@ -250,61 +240,15 @@ class Main {
const parsedUrl = new URL(urlToLoad);

if (
this.allowedOriginsToLoadInternal.includes(parsedUrl.origin) === false
this.allowedHostnamesToLoadInternal.includes(parsedUrl.origin) === false
) {
event.preventDefault();
Sentry.captureException(
new SecurityError(
`Prevented navigation to untrusted, internal origin "${urlToLoad}" from "${window.webContents.getURL()}"`
)
);
const errorMessage = `Prevented navigation to untrusted, internal URL "${parsedUrl.toString()}" from "${window.webContents.getURL()}"`;
Sentry.captureException(new SecurityError(errorMessage));
this.core?.logger.error(errorMessage);
}
}

/**
* Saves the window size and position inside the users file before the window is closed
*/
private async onWindowClose(
window: BrowserWindow,
event: Electron.Event
): Promise<void> {
event.preventDefault();

if (!this.core) {
Sentry.captureException(
new Error('Trying to close the window but Core is not initialized.')
);
window.destroy();
return;
}

const user = await this.core.user.get();
const width = window.getSize()[0];
const height = window.getSize()[1];
const x = window.getPosition()[0];
const y = window.getPosition()[1];

if (!user || !width || !height || !x || !y) {
window.destroy();
return;
}

await this.core.user.set({
...user,
window: {
width,
height,
position: {
x,
y,
},
},
});

window.destroy();
return;
}

/**
* Registers a custom file protocol to load files from the local file system
*/
Expand Down Expand Up @@ -355,17 +299,26 @@ class Main {
ipcMain.handle('electron:dialog:showSaveDialog', async (_event, args) => {
return await dialog.showSaveDialog(window, args);
});
ipcMain.handle('core:logger:debug', async (_event, args) => {
return await core.logger.debug(args[0]);
ipcMain.handle('core:api:start', async (_event, args) => {
return await core.api.start(args[0]);
});
ipcMain.handle('core:api:isRunning', async () => {
return await core.api.isRunning();
});
ipcMain.handle('core:api:stop', async () => {
return await core.api.stop();
});
ipcMain.handle('core:logger:debug', (_event, args) => {
return core.logger.debug(args[0]);
});
ipcMain.handle('core:logger:info', async (_event, args) => {
return await core.logger.info(args[0]);
ipcMain.handle('core:logger:info', (_event, args) => {
return core.logger.info(args[0]);
});
ipcMain.handle('core:logger:warn', async (_event, args) => {
return await core.logger.warn(args[0]);
ipcMain.handle('core:logger:warn', (_event, args) => {
return core.logger.warn(args[0]);
});
ipcMain.handle('core:logger:error', async (_event, args) => {
return await core.logger.error(args[0]);
ipcMain.handle('core:logger:error', (_event, args) => {
return core.logger.error(args[0]);
});
ipcMain.handle('core:logger:read', async (_event, args) => {
return await core.logger.read(args[0]);
Expand Down
Loading

0 comments on commit 6551560

Please sign in to comment.