Files
headplane/app/routes/auth/login/action.ts
T
2026-03-07 17:10:34 -05:00

103 lines
3.1 KiB
TypeScript

import { redirect } from "react-router";
import { isDataWithApiError } from "~/server/headscale/api/error-client";
import log from "~/utils/log";
import type { Route } from "./+types/page";
export async function loginAction({ request, context }: Route.LoaderArgs) {
const formData = await request.formData();
const apiKey = formData.has("api_key") ? String(formData.get("api_key")) : undefined;
if (apiKey === undefined) {
log.warn("auth", "Request made without API key");
log.warn(
"auth",
"If this is unexpected, ensure your reverse proxy (if applicable) is configured correctly",
);
return {
success: false,
message: "Missing API key. Please enter your API key.",
};
}
if (apiKey.length === 0) {
log.warn("auth", "Request made with empty API key");
log.warn(
"auth",
"If this is unexpected, ensure your reverse proxy (if applicable) is configured correctly",
);
return {
success: false,
message: "API key cannot be empty. Please enter a valid API key.",
};
}
const api = context.hsApi.getRuntimeClient(apiKey);
try {
const apiKeys = await api.getApiKeys();
// We don't need to check for 0 API keys because this request cannot
// be authenticated correctly without an API key
//
// 0.28.0 pointlessly added asterisks to the prefixes of API keys, which is
// the dumbest thing I've ever seen.
const lookup = apiKeys.find((key) => apiKey.startsWith(key.prefix.replaceAll("*", "")));
if (!lookup) {
return {
success: false,
message: "API key was not found in the Headscale database",
};
}
if (lookup.expiration === null || lookup.expiration === undefined) {
log.error("auth", "Got an API key without an expiration");
return {
success: false,
message: "API key is malformed (missing expiration). Please generate a new API key.",
};
}
const expiry = new Date(lookup.expiration);
if (expiry.getTime() < Date.now()) {
return {
success: false,
message: "API key has expired",
};
}
return redirect("/machines", {
headers: {
"Set-Cookie": await context.auth.createApiKeySession(
apiKey,
`${lookup.prefix}...`,
expiry.getTime() - Date.now(),
),
},
});
} catch (error) {
// Check if this is a React Router DataWithResponseInit wrapping a Headscale API error
if (isDataWithApiError(error)) {
const apiError = error.data;
// TODO: What in gods name is wrong with the headscale API?
if (
apiError.statusCode === 401 ||
apiError.statusCode === 403 ||
(apiError.statusCode === 500 && apiError.rawData.trim() === "Unauthorized")
) {
return {
success: false,
message: "API key is invalid (it may be incorrect or expired)",
};
}
}
log.error("auth", "Error while validating API key: %s", error);
log.debug("auth", "Error details: %o", error);
return {
success: false,
message: "Error while validating API key (see logs for details)",
};
}
}