mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-24 23:05:49 +00:00
121 lines
4.0 KiB
TypeScript
121 lines
4.0 KiB
TypeScript
import { createServer, type Server } from "node:http";
|
|
import { app, BrowserWindow, ipcMain } from "electron";
|
|
|
|
const DEV_CALLBACK_HOST = "127.0.0.1:43821";
|
|
const DEV_CALLBACK_ORIGIN = `http://${DEV_CALLBACK_HOST}`;
|
|
const CALLBACK_PATH = "/callback";
|
|
const LOOPBACK_CALLBACK_PATH = "/auth/callback";
|
|
const MAX_CALLBACK_URL_LENGTH = 8192;
|
|
const CALLBACK_PARAMETERS = new Set(["code", "error", "error_code", "error_description"]);
|
|
|
|
type AuthCallbackOptions = {
|
|
isDev: boolean;
|
|
focusApp: () => void;
|
|
};
|
|
|
|
function callbackHeaders(contentType: string) {
|
|
return {
|
|
"Cache-Control": "no-store",
|
|
"Content-Security-Policy":
|
|
"default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; frame-ancestors 'none'",
|
|
"Content-Type": contentType,
|
|
"Referrer-Policy": "no-referrer",
|
|
"X-Content-Type-Options": "nosniff",
|
|
};
|
|
}
|
|
|
|
export function createAuthCallbackController({ isDev, focusApp }: AuthCallbackOptions) {
|
|
const protocol = isDev ? "recordly-dev" : "recordly";
|
|
let pendingUrl: string | null = null;
|
|
let server: Server | null = null;
|
|
|
|
function parseCallback(rawUrl: string): URL | null {
|
|
if (rawUrl.length > MAX_CALLBACK_URL_LENGTH) return null;
|
|
try {
|
|
const url = new URL(rawUrl);
|
|
if (url.protocol !== `${protocol}:` || url.hostname !== "auth") return null;
|
|
if (url.pathname !== CALLBACK_PATH || url.username || url.password) return null;
|
|
return url;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function find(args: readonly string[]) {
|
|
return args.find((arg) => parseCallback(arg) !== null) ?? null;
|
|
}
|
|
|
|
function dispatch(rawUrl: string) {
|
|
const url = parseCallback(rawUrl);
|
|
if (!url) return false;
|
|
pendingUrl = url.href;
|
|
for (const window of BrowserWindow.getAllWindows()) {
|
|
if (!window.isDestroyed()) window.webContents.send("auth:callback", url.href);
|
|
}
|
|
if (app.isReady()) focusApp();
|
|
return true;
|
|
}
|
|
|
|
function startDevServer() {
|
|
if (!isDev || server) return;
|
|
server = createServer((request, response) => {
|
|
if (
|
|
request.method !== "GET" ||
|
|
request.headers.host !== DEV_CALLBACK_HOST ||
|
|
(request.url?.length ?? 0) > MAX_CALLBACK_URL_LENGTH
|
|
) {
|
|
response.writeHead(404, callbackHeaders("text/plain; charset=utf-8"));
|
|
response.end("Not found");
|
|
return;
|
|
}
|
|
|
|
const requestUrl = new URL(request.url ?? "/", DEV_CALLBACK_ORIGIN);
|
|
if (requestUrl.pathname !== LOOPBACK_CALLBACK_PATH) {
|
|
response.writeHead(404, callbackHeaders("text/plain; charset=utf-8"));
|
|
response.end("Not found");
|
|
return;
|
|
}
|
|
|
|
const appUrl = new URL(`${protocol}://auth${CALLBACK_PATH}`);
|
|
for (const key of CALLBACK_PARAMETERS) {
|
|
for (const value of requestUrl.searchParams.getAll(key)) {
|
|
appUrl.searchParams.append(key, value);
|
|
}
|
|
}
|
|
if (!appUrl.searchParams.has("code") && !appUrl.searchParams.has("error")) {
|
|
response.writeHead(400, callbackHeaders("text/plain; charset=utf-8"));
|
|
response.end("Invalid authentication callback");
|
|
return;
|
|
}
|
|
|
|
dispatch(appUrl.href);
|
|
response.writeHead(200, callbackHeaders("text/html; charset=utf-8"));
|
|
response.end(
|
|
'<!doctype html><meta charset="utf-8"><title>Signed into Recordly</title><style>body{margin:0;min-height:100vh;display:grid;place-items:center;background:#08090a;color:#ededef;font:16px "Helvetica Neue",Helvetica,Arial,sans-serif}.card{max-width:420px;padding:32px;text-align:center}h1{font-size:22px;font-weight:500}p{color:#8b8b8e;line-height:1.6}</style><main class="card"><h1>Signed into Recordly</h1><p>You can close this tab and return to the app.</p></main>',
|
|
);
|
|
});
|
|
server.on("error", (error) => {
|
|
console.error("[auth] Could not start local callback server", error);
|
|
server = null;
|
|
});
|
|
server.listen(43821, "127.0.0.1");
|
|
}
|
|
|
|
function close() {
|
|
server?.close();
|
|
server = null;
|
|
}
|
|
|
|
app.on("open-url", (event, url) => {
|
|
event.preventDefault();
|
|
dispatch(url);
|
|
});
|
|
|
|
ipcMain.handle("auth:get-pending-callback", () => pendingUrl);
|
|
ipcMain.handle("auth:ack-callback", (_, url: string) => {
|
|
if (pendingUrl === url) pendingUrl = null;
|
|
});
|
|
|
|
return { close, dispatch, find, protocol, startDevServer };
|
|
}
|