117 lines
3.4 KiB
JavaScript
117 lines
3.4 KiB
JavaScript
|
|
import http from "http";
|
||
|
|
import { URL } from "url";
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Start a local HTTP server to receive OAuth callback
|
||
|
|
* @param {Function} onCallback - Called with query params when callback received
|
||
|
|
* @param {number} fixedPort - Optional fixed port number (default: random)
|
||
|
|
* @returns {Promise<{server: http.Server, port: number, close: Function}>}
|
||
|
|
*/
|
||
|
|
export function startLocalServer(onCallback, fixedPort = null) {
|
||
|
|
return new Promise((resolve, reject) => {
|
||
|
|
const server = http.createServer((req, res) => {
|
||
|
|
const url = new URL(req.url, `http://localhost`);
|
||
|
|
|
||
|
|
if (url.pathname === "/callback" || url.pathname === "/auth/callback") {
|
||
|
|
const params = Object.fromEntries(url.searchParams);
|
||
|
|
|
||
|
|
// Send success response to browser with auto-close attempt
|
||
|
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
||
|
|
res.end(`<!DOCTYPE html>
|
||
|
|
<html>
|
||
|
|
<head>
|
||
|
|
<meta charset="utf-8">
|
||
|
|
<title>Authentication Successful</title>
|
||
|
|
<style>
|
||
|
|
body { font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #f5f5f5; }
|
||
|
|
.container { text-align: center; padding: 2rem; background: white; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
||
|
|
.success { color: #22c55e; font-size: 3rem; }
|
||
|
|
h1 { margin: 1rem 0; }
|
||
|
|
p { color: #666; }
|
||
|
|
#countdown { font-weight: bold; }
|
||
|
|
</style>
|
||
|
|
</head>
|
||
|
|
<body>
|
||
|
|
<div class="container">
|
||
|
|
<div class="success">✓</div>
|
||
|
|
<h1>Authentication Successful</h1>
|
||
|
|
<p id="message">Closing in <span id="countdown">3</span> seconds...</p>
|
||
|
|
</div>
|
||
|
|
<script>
|
||
|
|
let count = 3;
|
||
|
|
const countdown = document.getElementById("countdown");
|
||
|
|
const message = document.getElementById("message");
|
||
|
|
const timer = setInterval(() => {
|
||
|
|
count--;
|
||
|
|
countdown.textContent = count;
|
||
|
|
if (count <= 0) {
|
||
|
|
clearInterval(timer);
|
||
|
|
window.close();
|
||
|
|
setTimeout(() => {
|
||
|
|
message.textContent = "Please close this tab manually.";
|
||
|
|
}, 500);
|
||
|
|
}
|
||
|
|
}, 1000);
|
||
|
|
</script>
|
||
|
|
</body>
|
||
|
|
</html>`);
|
||
|
|
|
||
|
|
// Call callback with params
|
||
|
|
onCallback(params);
|
||
|
|
} else {
|
||
|
|
res.writeHead(404);
|
||
|
|
res.end("Not found");
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// Listen on fixed port or find available port
|
||
|
|
const portToUse = fixedPort || 0;
|
||
|
|
server.listen(portToUse, "127.0.0.1", () => {
|
||
|
|
const { port } = server.address();
|
||
|
|
resolve({
|
||
|
|
server,
|
||
|
|
port,
|
||
|
|
close: () => server.close(),
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
server.on("error", (err) => {
|
||
|
|
if (err.code === "EADDRINUSE" && fixedPort) {
|
||
|
|
reject(new Error(`Port ${fixedPort} is already in use. Please close other applications using this port.`));
|
||
|
|
} else {
|
||
|
|
reject(err);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Wait for callback with timeout
|
||
|
|
* @param {number} timeoutMs - Timeout in milliseconds
|
||
|
|
* @returns {Promise<Object>} - Callback params
|
||
|
|
*/
|
||
|
|
export function waitForCallback(timeoutMs = 300000) {
|
||
|
|
return new Promise((resolve, reject) => {
|
||
|
|
let resolved = false;
|
||
|
|
|
||
|
|
const timeout = setTimeout(() => {
|
||
|
|
if (!resolved) {
|
||
|
|
resolved = true;
|
||
|
|
reject(new Error("Authentication timeout"));
|
||
|
|
}
|
||
|
|
}, timeoutMs);
|
||
|
|
|
||
|
|
const onCallback = (params) => {
|
||
|
|
if (!resolved) {
|
||
|
|
resolved = true;
|
||
|
|
clearTimeout(timeout);
|
||
|
|
resolve(params);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Return the callback function
|
||
|
|
resolve.__onCallback = onCallback;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|