feat: Added QR login
fix: Fixed webview for windows builds
This commit is contained in:
parent
01b91aa48c
commit
4739b89933
26 changed files with 545 additions and 83 deletions
|
|
@ -20,6 +20,7 @@ import EventEmitter from "node:events";
|
|||
import { ErrorLike } from "bun";
|
||||
import { appPath, getErrorMessage } from "../utils";
|
||||
import { DrizzleSqliteDODatabase } from "drizzle-orm/durable-sqlite";
|
||||
import { ensureDir } from "fs-extra";
|
||||
|
||||
export const config = new Conf<SettingsType>({
|
||||
projectName: projectPackage.name,
|
||||
|
|
@ -48,11 +49,9 @@ console.log("App Directory is ", process.env.APPDIR);
|
|||
const fileCookieStore = new FileCookieStore(path.join(path.dirname(config.path), 'cookies.json'));
|
||||
console.log("Cookie Jar Path Located At: ", fileCookieStore.filePath);
|
||||
export const jar = new CookieJar(fileCookieStore);
|
||||
await fs.mkdir(config.get('downloadPath'), { recursive: true });
|
||||
let sqlite: Database;
|
||||
export let db: DrizzleSqliteDODatabase<typeof schema>;
|
||||
await reloadDatabase();
|
||||
migrate(db!, { migrationsFolder: appPath("./drizzle") });
|
||||
const emulatorsSqlite = new Database(appPath(`./vendors/es-de/emulators.${os.platform()}.${os.arch()}.sqlite`), { readonly: true });
|
||||
export const emulatorsDb = drizzle(emulatorsSqlite, { schema: emulatorSchema });
|
||||
export const taskQueue = new TaskQueue();
|
||||
|
|
@ -73,7 +72,7 @@ events.addListener('activegameexit', ({ error }) =>
|
|||
events.emit('notification', { message: getErrorMessage(error), type: 'error' });
|
||||
}
|
||||
});
|
||||
console.log("Logging In to Romm");
|
||||
config.onDidChange('downloadPath', () => reloadDatabase());
|
||||
|
||||
export async function cleanup ()
|
||||
{
|
||||
|
|
@ -85,8 +84,10 @@ export async function cleanup ()
|
|||
|
||||
export async function reloadDatabase ()
|
||||
{
|
||||
await ensureDir(config.get('downloadPath'));
|
||||
sqlite = new Database(path.join(config.get('downloadPath'), 'db.sqlite'), { create: true, readwrite: true });
|
||||
db = drizzle(sqlite, { schema });
|
||||
migrate(db!, { migrationsFolder: appPath("./drizzle") });
|
||||
}
|
||||
|
||||
interface AppEventMap
|
||||
|
|
|
|||
|
|
@ -1,32 +1,47 @@
|
|||
import Elysia, { status } from "elysia";
|
||||
import { config, db, jar } from "./app";
|
||||
import Elysia, { sse, status } from "elysia";
|
||||
import { config, jar, taskQueue } from "./app";
|
||||
import z from "zod";
|
||||
import { client } from "@clients/romm/client.gen";
|
||||
import { loginApiLoginPost } from "@clients/romm";
|
||||
import { loginApiLoginPost, logoutApiLogoutPost } from "@clients/romm";
|
||||
import secrets from '../api/secrets';
|
||||
import { LoginJob } from "./jobs/login-job";
|
||||
|
||||
export default new Elysia()
|
||||
.post('/login', async ({ body: { host, username, password } }) =>
|
||||
.post('/login/remote/start', async () =>
|
||||
{
|
||||
if (config.has('rommAddress') && config.has('rommUser'))
|
||||
if (taskQueue.hasActiveOfType(LoginJob))
|
||||
{
|
||||
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));
|
||||
}
|
||||
return status("Conflict", "Login Already Active");
|
||||
}
|
||||
|
||||
config.set('rommAddress', host);
|
||||
config.set('rommUser', username);
|
||||
const job = new LoginJob();
|
||||
taskQueue.enqueue("login", job);
|
||||
return status("OK");
|
||||
})
|
||||
.get('/login/remote/status', async function* ()
|
||||
{
|
||||
const job = taskQueue.findJob("login");
|
||||
if (job)
|
||||
{
|
||||
const loginJob = job.job as LoginJob;
|
||||
yield sse({ data: { endsAt: loginJob.endsAt, url: loginJob.url } });
|
||||
await taskQueue.waitForJob('login');
|
||||
yield sse({ data: {} });
|
||||
}
|
||||
|
||||
await secrets.set({ service: 'gameflow', name: 'romm', value: password });
|
||||
await login();
|
||||
|
||||
return status(200);
|
||||
}, { body: z.object({ host: z.url(), username: z.string(), password: z.string() }) })
|
||||
yield sse({ data: {} });
|
||||
})
|
||||
.post('/login/remote/cancel', async () =>
|
||||
{
|
||||
const job = taskQueue.findJob("login");
|
||||
if (job)
|
||||
{
|
||||
job.abort("cancel");
|
||||
await taskQueue.waitForJob('login');
|
||||
}
|
||||
return {};
|
||||
})
|
||||
.post('/login', async ({ body }) => tryLoginAndSave(body), { body: z.object({ host: z.url(), username: z.string(), password: z.string() }) })
|
||||
.get('/login', async () =>
|
||||
{
|
||||
const credentials = await secrets.get({ service: 'gameflow', name: 'romm' });
|
||||
|
|
@ -54,6 +69,31 @@ async function updateClient ()
|
|||
});
|
||||
}
|
||||
|
||||
export async function tryLoginAndSave ({ host, username, password }: { host: string, username: string, password: string; })
|
||||
{
|
||||
if (config.has('rommAddress') && config.has('rommUser'))
|
||||
{
|
||||
await logout();
|
||||
const oldRommAddress = config.get('rommAddress');
|
||||
if (oldRommAddress)
|
||||
{
|
||||
const cookies = await jar.getCookies(oldRommAddress);
|
||||
await Promise.all(cookies.map(c => jar.store.removeCookie(c.domain, c.path, c.key)));
|
||||
}
|
||||
}
|
||||
|
||||
const response = await login({ rommAddress: host, rommUser: username, rommPassword: password });
|
||||
if (response?.code === 200)
|
||||
{
|
||||
config.set('rommAddress', host);
|
||||
config.set('rommUser', username);
|
||||
|
||||
await secrets.set({ service: 'gameflow', name: 'romm', value: password });
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
export async function logout ()
|
||||
{
|
||||
if (!config.has('rommAddress'))
|
||||
|
|
@ -66,11 +106,12 @@ export async function logout ()
|
|||
console.log("Logging Out of ROMM");
|
||||
try
|
||||
{
|
||||
await loginApiLoginPost({
|
||||
await logoutApiLogoutPost({
|
||||
baseUrl: rommAddress, headers: {
|
||||
'cookie': await jar.getCookieString(rommAddress)
|
||||
}
|
||||
});
|
||||
await jar.store.removeCookie(new URL(rommAddress).host, null, "romm_session");
|
||||
} catch (error)
|
||||
{
|
||||
console.error("Failed to logout of ROMM ", error);
|
||||
|
|
@ -78,20 +119,39 @@ export async function logout ()
|
|||
}
|
||||
}
|
||||
|
||||
export async function login ()
|
||||
export async function login (data?: { rommAddress?: string, rommUser?: string, rommPassword?: string; })
|
||||
{
|
||||
if (!config.has('rommAddress') || !config.has('rommUser'))
|
||||
const address = data?.rommAddress ?? config.get('rommAddress');
|
||||
const user = data?.rommUser ?? config.get('rommUser');
|
||||
const password = data?.rommPassword ?? await secrets.get({ service: 'gameflow', name: "romm" });
|
||||
|
||||
if (!address || !user)
|
||||
{
|
||||
return;
|
||||
console.warn("Romm not setup");
|
||||
return status(404);
|
||||
}
|
||||
const rommAddress = config.get('rommAddress');
|
||||
const rommUser = config.get('rommUser');
|
||||
if (rommAddress && rommUser)
|
||||
{
|
||||
console.log("Logging In to ROMM");
|
||||
const password = await secrets.get({ service: 'gameflow', name: "romm" });
|
||||
if (password === null)
|
||||
{
|
||||
return status(404, "No Found Password");
|
||||
}
|
||||
|
||||
const loginResponse = await loginApiLoginPost({ baseUrl: rommAddress, auth: `${rommUser}:${password}` });
|
||||
loginResponse.response.headers.getSetCookie().map(c => jar.setCookie(c, rommAddress));
|
||||
await updateClient();
|
||||
if (loginResponse.response.status === 200)
|
||||
{
|
||||
loginResponse.response.headers.getSetCookie().map(c => jar.setCookie(c, rommAddress));
|
||||
await updateClient();
|
||||
return status(200, loginResponse.response.statusText);
|
||||
} else
|
||||
{
|
||||
console.error("Could not Login to Romm: ", loginResponse.response.statusText);
|
||||
return status(loginResponse.response.status, loginResponse.response.statusText);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -59,10 +59,6 @@ export default new Elysia()
|
|||
}
|
||||
|
||||
return processImage(coverBlob.cover, query);
|
||||
/*return sharp(coverBlob.cover)
|
||||
.resize({ width, height, withoutEnlargement: true })
|
||||
.blur(blur)
|
||||
.toBuffer();*/
|
||||
}, {
|
||||
params: z.object({ id: z.coerce.number() }),
|
||||
query: z.object({ blur: z.coerce.number().optional(), width: z.coerce.number().optional(), height: z.coerce.number().optional() })
|
||||
|
|
@ -73,13 +69,6 @@ export default new Elysia()
|
|||
{
|
||||
const rommAdress = config.get('rommAddress');
|
||||
return processImage(`${rommAdress}/${path}`, query);
|
||||
|
||||
/*
|
||||
const rommFetch = await fetch(`${rommAdress}/${path}`);
|
||||
return sharp(await rommFetch.arrayBuffer())
|
||||
.resize({ width, height, withoutEnlargement: true })
|
||||
.blur(blur)
|
||||
.toBuffer();*/
|
||||
}
|
||||
return status('Not Found');
|
||||
}, { query: z.object({ blur: z.coerce.number().optional(), width: z.coerce.number().optional(), height: z.coerce.number().optional() }) })
|
||||
|
|
@ -94,8 +83,6 @@ export default new Elysia()
|
|||
}
|
||||
|
||||
return processImage(screenshot.content, query);
|
||||
//return sharp(screenshot.content).resize({ width, height, withoutEnlargement: true }).blur(blur).toBuffer();
|
||||
//return screenshot.content;
|
||||
}
|
||||
|
||||
return status(404);
|
||||
|
|
|
|||
61
src/bun/api/jobs/login-job.ts
Normal file
61
src/bun/api/jobs/login-job.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import Elysia, { status } from "elysia";
|
||||
import { IJob, JobContext } from "../task-queue";
|
||||
import { LOGIN_PORT, SERVER_URL } from "@/shared/constants";
|
||||
import { host, localIp } from "@/bun/utils/host";
|
||||
import cors from "@elysiajs/cors";
|
||||
import { tryLoginAndSave } from "../auth";
|
||||
import z from "zod";
|
||||
import { config } from "../app";
|
||||
|
||||
export class LoginJob implements IJob
|
||||
{
|
||||
endsAt: Date;
|
||||
url: string;
|
||||
|
||||
constructor()
|
||||
{
|
||||
this.endsAt = new Date();
|
||||
this.url = `http://${localIp}:${LOGIN_PORT}/`;
|
||||
}
|
||||
|
||||
async start (context: JobContext): Promise<any>
|
||||
{
|
||||
const loginServer = new Elysia({ serve: { hostname: localIp, port: LOGIN_PORT } })
|
||||
.use(cors())
|
||||
.get(`/`, ({ headers }) => process.env.PUBLIC_ACCESS ? fetch(`${SERVER_URL(host)}/auth/qr/`, { headers: headers as any }) : Bun.file(`./dist/auth/qr/index.html`))
|
||||
.get(`/*`, ({ path, headers }) => process.env.PUBLIC_ACCESS ? fetch(`${SERVER_URL(host)}/auth/qr/${path}`, { headers: headers as any }) : Bun.file(`./dist/${path}`))
|
||||
.get('/status', () => ({ expires_at: this.endsAt, max_time: 300000 }))
|
||||
.post('/cancel', () => context.abort("cancel"))
|
||||
.get('/defaults', () => ({ host: config.get('rommAddress'), username: config.get('rommUser') ?? '' }))
|
||||
.post(`/login`, async ({ body }) =>
|
||||
{
|
||||
const response = await tryLoginAndSave(body as any);
|
||||
if (response?.code === 200)
|
||||
{
|
||||
context.abort("success");
|
||||
return status("Accepted");
|
||||
} else
|
||||
{
|
||||
return response;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
loginServer.listen({});
|
||||
await new Promise((resolve, reject) =>
|
||||
{
|
||||
this.endsAt = new Date(new Date().getTime() + 300000);
|
||||
context.abortSignal.addEventListener('abort', () => reject());
|
||||
setTimeout(() => { reject('timeout'); }, 300000); // auto close after 5 minutes
|
||||
});
|
||||
} catch
|
||||
{
|
||||
} finally
|
||||
{
|
||||
await loginServer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import z from "zod";
|
||||
import { SettingsSchema } from "@shared/constants";
|
||||
import { LOGIN_PORT, SettingsSchema } from "@shared/constants";
|
||||
import Elysia, { status } from "elysia";
|
||||
import { config, customEmulators, db, emulatorsDb, taskQueue } from "./app";
|
||||
import * as appSchema from './schema/app';
|
||||
|
|
@ -103,7 +103,7 @@ export const settings = new Elysia({ prefix: '/api/settings' })
|
|||
const oldDownloadPath = config.get('downloadPath');
|
||||
if (!existsSync(oldDownloadPath))
|
||||
{
|
||||
return status("Not Found", "Old downlod path doesn't exist");
|
||||
return status("Not Found", "Old download path doesn't exist");
|
||||
}
|
||||
|
||||
async function isDirEmpty (dirname: string)
|
||||
|
|
@ -121,7 +121,7 @@ export const settings = new Elysia({ prefix: '/api/settings' })
|
|||
|
||||
if (existsSync(path) && !isDirEmpty(path))
|
||||
{
|
||||
return status("Conflict", "New location alaready exists and is not empty");
|
||||
return status("Conflict", "New location already exists and is not empty");
|
||||
}
|
||||
|
||||
await move(oldDownloadPath, path);
|
||||
|
|
|
|||
|
|
@ -138,6 +138,7 @@ export interface IPublicJob
|
|||
state?: string;
|
||||
status: JobStatus;
|
||||
job: any;
|
||||
abort: (reason?: any) => void;
|
||||
}
|
||||
|
||||
export class JobContext implements IPublicJob
|
||||
|
|
@ -177,7 +178,7 @@ export class JobContext implements IPublicJob
|
|||
} catch (error)
|
||||
{
|
||||
console.error(error);
|
||||
this.events.emit('error', { id: this.m_id, error });
|
||||
this.events.emit('error', { id: this.m_id, job: this.m_job, error });
|
||||
this.error = error;
|
||||
} finally
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue