feat: move to secure OS credential storage so that you never get logged out again

This commit is contained in:
Simeon Radivoev 2026-02-10 00:35:37 +02:00
parent d6e0a8350a
commit ef08fa6114
Signed by: simeonradivoev
GPG key ID: 7611A451D2A5D37A
15 changed files with 493 additions and 276 deletions

View file

@ -1,7 +1,97 @@
import z from "zod";
import { config } from "./settings";
import Elysia from "elysia";
import Elysia, { status } from "elysia";
import keytar from '@hackolade/keytar';
import { loginApiLoginPost } from "../../clients/romm";
import { CookieJar } from 'tough-cookie';
import FileCookieStore from 'tough-cookie-file-store';
import path from 'node:path';
const fileCookieStore = new FileCookieStore(path.join(path.dirname(config.path), 'cookies.json'));
console.log("Cookie Jar Path Located At: ", fileCookieStore.filePath);
const jar = new CookieJar(fileCookieStore);
await login();
export async function logout ()
{
if (!config.has('rommAddress'))
{
return;
}
const rommAddress = config.get('rommAddress');
if (rommAddress)
{
console.log("Logging Out of ROMM");
try
{
await loginApiLoginPost({
baseUrl: rommAddress, headers: {
'cookie': await jar.getCookieString(rommAddress)
}
});
} catch (error)
{
console.error("Failed to logout of ROMM ", error);
}
}
}
async function login ()
{
if (!config.has('rommAddress') || !config.has('rommUser'))
{
return;
}
const rommAddress = config.get('rommAddress');
const rommUser = config.get('rommUser');
if (rommAddress && rommUser)
{
console.log("Logging In to ROMM");
const password = await keytar.getPassword('romm', 'gameflow');
const loginResponse = await loginApiLoginPost({ baseUrl: rommAddress, auth: `${rommUser}:${password}` });
loginResponse.response.headers.getSetCookie().map(c => jar.setCookie(c, rommAddress));
}
}
export const romm = new Elysia({ prefix: "/romm" })
.post('/login', async ({ body: { host, username, password } }) =>
{
if (config.has('rommAddress') && config.has('rommUser'))
{
await logout();
const oldRommAddress = config.get('rommAddress');
if (oldRommAddress)
{
const cookies = await jar.getCookies(oldRommAddress);
cookies.map(c => jar.store.removeCookie(c.domain, c.path, c.key));
}
}
config.set('rommAddress', host);
config.set('rommUser', username);
await keytar.setPassword('romm', 'gameflow', password);
await login();
return status(200);
}, { body: z.object({ host: z.url(), username: z.string(), password: z.string() }) })
.get('/login', async () =>
{
const credentials = await keytar.getPassword('romm', 'gameflow');
return { hasPassword: !!credentials };
}, { response: z.object({ hasPassword: z.boolean() }) })
.post('/logout', async () =>
{
await keytar.deletePassword('romm', 'gameflow');
await logout();
const rommAddress = config.get('rommAddress');
if (rommAddress)
{
const cookies = await jar.getCookies(rommAddress);
cookies.map(c => jar.store.removeCookie(c.domain, c.path, c.key));
}
return status(200);
})
.all("/*", async ({ request, params, set }) =>
{
if (!config.has('rommAddress') && !config.get('rommAddress'))
@ -16,18 +106,32 @@ export const romm = new Elysia({ prefix: "/romm" })
url.port = rommUrl.port;
url.protocol = rommUrl.protocol;
// Forward headers (optional: remove host if needed)
// Forward headers (optional: remove host if needed)
const headers = new Headers(request.headers);
headers.delete('host');
headers.set("accept-encoding", "identity");
headers.set('cookie', await jar.getCookieString(rommUrl.href));
const rommResponse = await fetch(url, {
let rommResponse = await fetch(url, {
method: request.method,
headers,
body: await request.arrayBuffer(),
redirect: 'manual', // avoid ROMM redirects
});
/*
if (rommResponse.status === 403 && config.has('rommUser'))
{
await login();
headers.set('cookie', await jar.getCookieString(rommUrl.href));
rommResponse = await fetch(url, {
method: request.method,
headers,
body: await request.arrayBuffer(),
redirect: 'manual', // avoid ROMM redirects
});
}*/
set.status = rommResponse.status;
rommResponse.headers.forEach((value, key) =>
{
@ -35,4 +139,4 @@ export const romm = new Elysia({ prefix: "/romm" })
});
return new Response(rommResponse.body, { status: rommResponse.status });
});
}).on('stop', logout);

View file

@ -11,10 +11,10 @@ if (!Bun.env.PUBLIC_ACCESS)
bunServer = RunBunServer();
}
function cleanup ()
async function cleanup ()
{
bunServer?.stop();
api.apiServer.stop();
await api.apiServer.stop();
process.exit(0);
}
@ -25,7 +25,7 @@ try
});
webviewWorker.addEventListener('error', console.error);
await new Promise(resolve => webviewWorker.addEventListener('close', resolve));
cleanup();
await cleanup();
}
catch (error)
{

View file

@ -1,6 +1,7 @@
import { SERVER_PORT } from "../shared/constants";
import path from 'node:path';
import { host } from "./utils";
import appInfo from '../../package.json';
export function RunBunServer ()
{
@ -12,6 +13,19 @@ export function RunBunServer ()
"/": Bun.file("./dist/index.html"),
// Serve a file by lazily loading it into memory
"/favicon.ico": Bun.file("./dist/favicon.ico"),
"/.well-known/appspecific/com.chrome.devtools.json": new Response(
JSON.stringify({
name: appInfo.name,
version: appInfo.version,
debuggable: true,
}),
{
headers: {
"Content-Type": "application/json",
"Cache-Control": "no-cache",
},
}
)
},
fetch: async (req) =>
{