229 lines
8.1 KiB
TypeScript
229 lines
8.1 KiB
TypeScript
import Elysia, { sse, status } from "elysia";
|
|
import { config, events, jar, taskQueue } from "./app";
|
|
import z from "zod";
|
|
import { client } from "@clients/romm/client.gen";
|
|
import { loginApiLoginPost, logoutApiLogoutPost } from "@clients/romm";
|
|
import secrets from '../api/secrets';
|
|
import { LoginJob } from "./jobs/login-job";
|
|
import TwitchLoginJob from "./jobs/twitch-login-job";
|
|
|
|
export default new Elysia()
|
|
.post('/login/twitch', async ({ body: { openInBrowser } }) =>
|
|
{
|
|
if (taskQueue.hasActiveOfType(TwitchLoginJob))
|
|
{
|
|
return status("Conflict", `Twitch Authentication already in progress`);
|
|
}
|
|
|
|
if (!process.env.TWITCH_CLIENT_ID)
|
|
{
|
|
return status("Not Found", "Twitch Client ID not set");
|
|
}
|
|
|
|
return taskQueue.enqueue(TwitchLoginJob.id, new TwitchLoginJob(process.env.TWITCH_CLIENT_ID, openInBrowser ?? false));
|
|
},
|
|
{ body: z.object({ openInBrowser: z.boolean().optional() }) })
|
|
.post('/logout/twitch', async () =>
|
|
{
|
|
if (!process.env.TWITCH_CLIENT_ID)
|
|
{
|
|
return status("Not Found", "Twitch Client ID not set");
|
|
}
|
|
|
|
const res = await fetch('https://id.twitch.tv/oauth2/revoke', {
|
|
method: "POST", headers: {
|
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
},
|
|
body: new URLSearchParams({
|
|
client_id: process.env.TWITCH_CLIENT_ID
|
|
})
|
|
});
|
|
|
|
await secrets.delete({ service: 'gamflow_twitch', name: 'access_token' });
|
|
await secrets.delete({ service: 'gamflow_twitch', name: 'refresh_token' });
|
|
await secrets.delete({ service: 'gamflow_twitch', name: 'expires_in' });
|
|
|
|
return status(res.status, res.statusText);
|
|
})
|
|
.get('/login/twitch', async () =>
|
|
{
|
|
const access_token = await secrets.get({ service: 'gamflow_twitch', name: 'access_token' });
|
|
if (!access_token)
|
|
{
|
|
return status('Not Found', "Not Logged In");
|
|
}
|
|
|
|
const res = await fetch('https://id.twitch.tv/oauth2/validate', { headers: { Authorization: `OAuth ${access_token}` } });
|
|
if (res.ok)
|
|
{
|
|
return await res.json() as { login: string, expires_in: number; client_id: string, user_id: string; };
|
|
}
|
|
|
|
if (!process.env.TWITCH_CLIENT_ID)
|
|
{
|
|
return status("Not Found", "Twitch Client ID not set");
|
|
}
|
|
|
|
const refresh_token = await secrets.get({ service: 'gamflow_twitch', name: "refresh_token" });
|
|
if (!refresh_token)
|
|
{
|
|
return status("Not Found", "Refresh Token Not Found");
|
|
}
|
|
|
|
// refresh token
|
|
const refreshResponse = await fetch('https://id.twitch.tv/oauth2/token', {
|
|
method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({
|
|
client_id: process.env.TWITCH_CLIENT_ID,
|
|
access_token,
|
|
grant_type: "refresh_token",
|
|
refresh_token
|
|
})
|
|
});
|
|
|
|
if (refreshResponse.ok)
|
|
{
|
|
const data: {
|
|
access_token: string,
|
|
refresh_token: string,
|
|
token_type: string;
|
|
expires_in: number;
|
|
} = await refreshResponse.json();
|
|
|
|
await secrets.set({ service: 'gamflow_twitch', name: 'access_token', value: data.access_token });
|
|
await secrets.set({ service: 'gamflow_twitch', name: 'refresh_token', value: data.refresh_token });
|
|
await secrets.set({ service: 'gamflow_twitch', name: 'expires_in', value: new Date(new Date().getTime() + data.expires_in).toString() });
|
|
|
|
events.emit('notification', { message: "Twitch Refresh Successful", type: 'success' });
|
|
|
|
const res = await fetch('https://id.twitch.tv/oauth2/validate', { headers: { Authorization: `OAuth ${data.access_token}` } });
|
|
if (res.ok)
|
|
{
|
|
return await res.json() as { login: string, expires_in: number; client_id: string, user_id: string; };
|
|
}
|
|
}
|
|
|
|
return status(400, res.statusText);
|
|
})
|
|
.post('/login/romm', async () =>
|
|
{
|
|
if (taskQueue.hasActiveOfType(LoginJob))
|
|
{
|
|
return status("Conflict", "Login Already Active");
|
|
}
|
|
|
|
return taskQueue.enqueue(LoginJob.id, new LoginJob());
|
|
})
|
|
.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' });
|
|
return { hasPassword: !!credentials };
|
|
}, { response: z.object({ hasPassword: z.boolean() }) })
|
|
.post('/logout', async () =>
|
|
{
|
|
await secrets.delete({ service: 'gameflow', name: 'romm' });
|
|
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);
|
|
}, { response: z.any() });
|
|
|
|
async function updateClient ()
|
|
{
|
|
client.setConfig({
|
|
baseUrl: config.get('rommAddress'), headers: {
|
|
cookie: await jar.getCookieString(config.get('rommAddress') ?? '')
|
|
}
|
|
});
|
|
}
|
|
|
|
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'))
|
|
{
|
|
return;
|
|
}
|
|
const rommAddress = config.get('rommAddress');
|
|
if (rommAddress)
|
|
{
|
|
console.log("Logging Out of ROMM");
|
|
try
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function login (data?: { rommAddress?: string, rommUser?: string, rommPassword?: string; })
|
|
{
|
|
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)
|
|
{
|
|
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");
|
|
if (password === null)
|
|
{
|
|
return status(404, "No Found Password");
|
|
}
|
|
|
|
const loginResponse = await loginApiLoginPost({ baseUrl: rommAddress, auth: `${rommUser}:${password}` });
|
|
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);
|
|
}
|
|
|
|
}
|
|
}
|
|
|