forked from baron/baron-sso
adminfront /api-keys 새로고침 404 해결
This commit is contained in:
@@ -74,8 +74,8 @@ ensure_frontend_dependencies() {
|
|||||||
ensure_frontend_dependencies
|
ensure_frontend_dependencies
|
||||||
|
|
||||||
if [ "$mode" = "production" ]; then
|
if [ "$mode" = "production" ]; then
|
||||||
echo "Running in production mode with Vite preview..."
|
echo "Running in production mode with custom static server..."
|
||||||
exec sh -c "npm run build && npm run preview -- --host 0.0.0.0"
|
exec sh -c "npm run build && node ./scripts/serve-prod.mjs"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Running in development mode..."
|
echo "Running in development mode..."
|
||||||
|
|||||||
153
adminfront/scripts/serve-prod.mjs
Normal file
153
adminfront/scripts/serve-prod.mjs
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import { createServer } from "node:http";
|
||||||
|
import { readFile, stat } from "node:fs/promises";
|
||||||
|
import { extname, join, normalize, resolve } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const rootDir = fileURLToPath(new URL("..", import.meta.url));
|
||||||
|
const distDir = resolve(
|
||||||
|
process.env.ADMINFRONT_BUILD_OUT_DIR ?? "/tmp/baron-sso-adminfront-dist",
|
||||||
|
);
|
||||||
|
const host = process.env.HOST ?? "0.0.0.0";
|
||||||
|
const port = Number(process.env.PORT ?? process.env.ADMINFRONT_PORT ?? 5173);
|
||||||
|
const backendTarget = new URL(
|
||||||
|
process.env.API_PROXY_TARGET || "http://localhost:3000",
|
||||||
|
);
|
||||||
|
|
||||||
|
const contentTypes = {
|
||||||
|
".css": "text/css; charset=utf-8",
|
||||||
|
".html": "text/html; charset=utf-8",
|
||||||
|
".js": "application/javascript; charset=utf-8",
|
||||||
|
".json": "application/json; charset=utf-8",
|
||||||
|
".map": "application/json; charset=utf-8",
|
||||||
|
".mjs": "application/javascript; charset=utf-8",
|
||||||
|
".svg": "image/svg+xml",
|
||||||
|
};
|
||||||
|
|
||||||
|
function getContentType(filePath) {
|
||||||
|
return contentTypes[extname(filePath).toLowerCase()] ?? "application/octet-stream";
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendJson(res, statusCode, body) {
|
||||||
|
res.writeHead(statusCode, {
|
||||||
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
|
"Cache-Control": "no-store",
|
||||||
|
});
|
||||||
|
res.end(JSON.stringify(body));
|
||||||
|
}
|
||||||
|
|
||||||
|
function toSafePath(pathname) {
|
||||||
|
const decoded = decodeURIComponent(pathname);
|
||||||
|
const relative = decoded.replace(/^\/+/, "");
|
||||||
|
const safe = normalize(relative).replace(/^(\.\.(?:[\\/]|$))+/, "");
|
||||||
|
return join(distDir, safe);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tryReadFile(filePath) {
|
||||||
|
try {
|
||||||
|
return await readFile(filePath);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function proxyToBackend(req, res, pathname, search) {
|
||||||
|
const target = new URL(pathname + search, backendTarget);
|
||||||
|
const headers = new Headers();
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(req.headers)) {
|
||||||
|
if (!value) continue;
|
||||||
|
if (key === "host" || key === "content-length" || key === "connection") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
headers.set(key, value.join(", "));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
headers.set(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasBody = !["GET", "HEAD"].includes(req.method ?? "GET");
|
||||||
|
const response = await fetch(target, {
|
||||||
|
method: req.method,
|
||||||
|
headers,
|
||||||
|
body: hasBody ? req : undefined,
|
||||||
|
duplex: hasBody ? "half" : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseHeaders = new Headers(response.headers);
|
||||||
|
responseHeaders.delete("content-length");
|
||||||
|
responseHeaders.delete("transfer-encoding");
|
||||||
|
responseHeaders.delete("connection");
|
||||||
|
|
||||||
|
res.writeHead(response.status, Object.fromEntries(responseHeaders.entries()));
|
||||||
|
|
||||||
|
if (req.method === "HEAD") {
|
||||||
|
res.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
|
res.end(Buffer.from(arrayBuffer));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function serveStatic(req, res, pathname) {
|
||||||
|
const indexPath = join(distDir, "index.html");
|
||||||
|
const filePath = toSafePath(pathname);
|
||||||
|
|
||||||
|
let resolvedPath = filePath;
|
||||||
|
try {
|
||||||
|
const fileStat = await stat(resolvedPath);
|
||||||
|
if (fileStat.isDirectory()) {
|
||||||
|
resolvedPath = join(resolvedPath, "index.html");
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
resolvedPath = indexPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
let body = await tryReadFile(resolvedPath);
|
||||||
|
if (!body) {
|
||||||
|
body = await tryReadFile(indexPath);
|
||||||
|
resolvedPath = indexPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!body) {
|
||||||
|
sendJson(res, 500, { error: "dist_not_found" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.writeHead(200, {
|
||||||
|
"Content-Type": getContentType(resolvedPath),
|
||||||
|
"Cache-Control": resolvedPath.endsWith("index.html")
|
||||||
|
? "no-cache"
|
||||||
|
: "public, max-age=31536000, immutable",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (req.method === "HEAD") {
|
||||||
|
res.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.end(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
createServer(async (req, res) => {
|
||||||
|
try {
|
||||||
|
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
||||||
|
const { pathname, search } = url;
|
||||||
|
|
||||||
|
if (pathname === "/api" || pathname.startsWith("/api/")) {
|
||||||
|
await proxyToBackend(req, res, pathname, search);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedPath = pathname === "/" ? "/index.html" : pathname;
|
||||||
|
await serveStatic(req, res, normalizedPath);
|
||||||
|
} catch (error) {
|
||||||
|
sendJson(res, 500, {
|
||||||
|
error: "internal_server_error",
|
||||||
|
message: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).listen(port, host, () => {
|
||||||
|
console.log(`Adminfront production server listening on http://${host}:${port}`);
|
||||||
|
});
|
||||||
@@ -10,6 +10,10 @@ import {
|
|||||||
} from "../../lib/adminApi";
|
} from "../../lib/adminApi";
|
||||||
import ApiKeyListPage from "./ApiKeyListPage";
|
import ApiKeyListPage from "./ApiKeyListPage";
|
||||||
|
|
||||||
|
vi.mock("../../lib/i18n", () => ({
|
||||||
|
t: (_key: string, fallback?: string) => fallback ?? "",
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock("../../lib/adminApi", () => ({
|
vi.mock("../../lib/adminApi", () => ({
|
||||||
fetchApiKeys: vi.fn(async () => ({
|
fetchApiKeys: vi.fn(async () => ({
|
||||||
items: [
|
items: [
|
||||||
@@ -102,4 +106,20 @@ describe("ApiKeyListPage", () => {
|
|||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
expect(fetchApiKeys).toHaveBeenCalled();
|
expect(fetchApiKeys).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("refresh button refetches the list without navigation", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
renderPage();
|
||||||
|
|
||||||
|
await screen.findByText("client-id-stable");
|
||||||
|
|
||||||
|
const refreshButton = screen.getByRole("button", { name: /새로고침/ });
|
||||||
|
expect(refreshButton).toHaveAttribute("type", "button");
|
||||||
|
|
||||||
|
await user.click(refreshButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(fetchApiKeys).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -172,6 +172,7 @@ function ApiKeyListPage() {
|
|||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => query.refetch()}
|
onClick={() => query.refetch()}
|
||||||
disabled={query.isFetching}
|
disabled={query.isFetching}
|
||||||
|
|||||||
Reference in New Issue
Block a user