mirror of
https://github.com/tale/headplane.git
synced 2026-08-24 03:26:30 +00:00
787f8decbc
Throwing data() in React Router v7 actions goes to ErrorBoundary instead of returning to the component. Changed validation errors to return directly so the login form can display them properly.
111 lines
3.4 KiB
TypeScript
111 lines
3.4 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",
|
|
};
|
|
}
|
|
|
|
const expiresDays = Math.round((expiry.getTime() - Date.now()) / 1000 / 60 / 60 / 24);
|
|
|
|
return redirect("/machines", {
|
|
headers: {
|
|
"Set-Cookie": await context.sessions.createSession(
|
|
{
|
|
api_key: apiKey,
|
|
user: {
|
|
subject: "unknown-non-oauth",
|
|
name: `${lookup.prefix}...`,
|
|
email: `expires@${expiresDays.toString()}-days`,
|
|
},
|
|
},
|
|
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)",
|
|
};
|
|
}
|
|
}
|