Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OpenAPI based local API #12

Merged
merged 10 commits into from
Oct 9, 2024
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
Loading