mirror of
https://github.com/tale/headplane.git
synced 2026-08-28 07:57:03 +00:00
feat: update to the v8 middleware api
This commit is contained in:
@@ -1,11 +1,15 @@
|
||||
import { redirect } from "react-router";
|
||||
|
||||
import { authContext, headscaleContext } from "~/server/context";
|
||||
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 auth = context.get(authContext);
|
||||
const headscale = context.get(headscaleContext);
|
||||
|
||||
const formData = await request.formData();
|
||||
const apiKey = formData.has("api_key") ? String(formData.get("api_key")) : undefined;
|
||||
|
||||
@@ -35,7 +39,7 @@ export async function loginAction({ request, context }: Route.LoaderArgs) {
|
||||
|
||||
// Build a client with the candidate API key the user just submitted, so the
|
||||
// GET /api/v1/apikey call below validates the key against Headscale itself.
|
||||
const api = context.headscale.client(apiKey);
|
||||
const api = headscale.client(apiKey);
|
||||
try {
|
||||
const apiKeys = await api.apiKeys.list();
|
||||
|
||||
@@ -70,7 +74,7 @@ export async function loginAction({ request, context }: Route.LoaderArgs) {
|
||||
|
||||
return redirect("/machines", {
|
||||
headers: {
|
||||
"Set-Cookie": await context.auth.createApiKeySession(
|
||||
"Set-Cookie": await auth.createApiKeySession(
|
||||
apiKey,
|
||||
`${lookup.prefix}...`,
|
||||
expiry.getTime() - Date.now(),
|
||||
|
||||
@@ -7,6 +7,7 @@ import Card from "~/components/card";
|
||||
import Code from "~/components/code";
|
||||
import Input from "~/components/input";
|
||||
import Link from "~/components/link";
|
||||
import { appConfigContext, authContext, oidcContext } from "~/server/context";
|
||||
import { useLiveData } from "~/utils/live-data";
|
||||
|
||||
import type { Route } from "./+types/page";
|
||||
@@ -16,15 +17,19 @@ import Logout from "./logout";
|
||||
import { OidcErrorNotice } from "./oidc-error";
|
||||
|
||||
export async function loader({ request, context, url }: Route.LoaderArgs) {
|
||||
const auth = context.get(authContext);
|
||||
const config = context.get(appConfigContext);
|
||||
const oidc = context.get(oidcContext);
|
||||
|
||||
try {
|
||||
await context.auth.require(request);
|
||||
await auth.require(request);
|
||||
return redirect("/machines");
|
||||
} catch {}
|
||||
|
||||
const qp = url.searchParams;
|
||||
const urlState = qp.get("s") ?? undefined;
|
||||
|
||||
const oidcService = context.oidc.state === "enabled" ? context.oidc.value : undefined;
|
||||
const oidcService = oidc.state === "enabled" ? oidc.value : undefined;
|
||||
const oidcStatus = oidcService
|
||||
? await oidcService.discover().then(
|
||||
(r) => (r.ok ? oidcService.status() : oidcService.status()),
|
||||
@@ -34,7 +39,7 @@ export async function loader({ request, context, url }: Route.LoaderArgs) {
|
||||
|
||||
if (
|
||||
oidcService &&
|
||||
context.config.oidc?.disable_api_key_login &&
|
||||
config.oidc?.disable_api_key_login &&
|
||||
oidcStatus?.state === "ready" &&
|
||||
urlState !== "logout"
|
||||
) {
|
||||
@@ -45,7 +50,7 @@ export async function loader({ request, context, url }: Route.LoaderArgs) {
|
||||
const oidcErrorCodes = oidcStatus?.state === "error" ? [oidcStatus.error.code] : [];
|
||||
|
||||
return {
|
||||
isCookieSecureEnabled: context.config.server.cookie_secure,
|
||||
isCookieSecureEnabled: config.server.cookie_secure,
|
||||
isOidcConnectorEnabled,
|
||||
oidcErrorCodes,
|
||||
urlState,
|
||||
|
||||
+12
-12
@@ -1,34 +1,34 @@
|
||||
import { type ActionFunctionArgs, redirect } from "react-router";
|
||||
|
||||
import type { AppContext } from "~/server/context";
|
||||
import { appConfigContext, authContext, oidcContext } from "~/server/context";
|
||||
|
||||
export async function loader() {
|
||||
return redirect("/machines");
|
||||
}
|
||||
|
||||
export async function action({ request, context }: ActionFunctionArgs<AppContext>) {
|
||||
let principal: Awaited<ReturnType<typeof context.auth.require>> | undefined;
|
||||
export async function action({ request, context }: ActionFunctionArgs) {
|
||||
const auth = context.get(authContext);
|
||||
const config = context.get(appConfigContext);
|
||||
const oidc = context.get(oidcContext);
|
||||
|
||||
let principal: Awaited<ReturnType<typeof auth.require>> | undefined;
|
||||
try {
|
||||
principal = await context.auth.require(request);
|
||||
principal = await auth.require(request);
|
||||
} catch {
|
||||
return redirect("/login");
|
||||
}
|
||||
|
||||
// When API key is disabled, we need to explicitly redirect
|
||||
// with a logout state to prevent auto login again.
|
||||
let url = context.config.oidc?.disable_api_key_login ? "/login?s=logout" : "/login";
|
||||
let url = config.oidc?.disable_api_key_login ? "/login?s=logout" : "/login";
|
||||
|
||||
// For OIDC sessions, redirect to the provider's RP-initiated logout
|
||||
// endpoint when explicitly enabled, so the upstream IdP session is also
|
||||
// ended. Disabled by default because the post_logout_redirect_uri must be
|
||||
// pre-registered on the IdP — turning this on without registering it would
|
||||
// strand users on the IdP's error page.
|
||||
if (
|
||||
principal?.kind === "oidc" &&
|
||||
context.oidc.state === "enabled" &&
|
||||
context.config.oidc?.use_end_session
|
||||
) {
|
||||
const service = context.oidc.value;
|
||||
if (principal?.kind === "oidc" && oidc.state === "enabled" && config.oidc?.use_end_session) {
|
||||
const service = oidc.value;
|
||||
const status = service.status();
|
||||
if (status.state !== "ready") {
|
||||
// Trigger discovery if it hasn't happened yet so we can find the
|
||||
@@ -44,7 +44,7 @@ export async function action({ request, context }: ActionFunctionArgs<AppContext
|
||||
|
||||
return redirect(url, {
|
||||
headers: {
|
||||
"Set-Cookie": await context.auth.destroySession(request),
|
||||
"Set-Cookie": await auth.destroySession(request),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { data, redirect } from "react-router";
|
||||
|
||||
import {
|
||||
appConfigContext,
|
||||
authContext,
|
||||
headscaleApiKeyContext,
|
||||
headscaleContext,
|
||||
oidcContext,
|
||||
} from "~/server/context";
|
||||
import { findHeadscaleUserBySubject } from "~/server/web/headscale-identity";
|
||||
import { Roles } from "~/server/web/roles";
|
||||
import log from "~/utils/log";
|
||||
@@ -8,16 +15,22 @@ import { createOidcStateCookie } from "~/utils/oidc-state";
|
||||
import type { Route } from "./+types/oidc-callback";
|
||||
|
||||
export async function loader({ request, context, url }: Route.LoaderArgs) {
|
||||
if (context.oidc.state !== "enabled") {
|
||||
throw data(`OIDC is unavailable: ${context.oidc.reason}`, { status: 501 });
|
||||
const auth = context.get(authContext);
|
||||
const config = context.get(appConfigContext);
|
||||
const headscale = context.get(headscaleContext);
|
||||
const headscaleApiKey = context.get(headscaleApiKeyContext);
|
||||
const oidc = context.get(oidcContext);
|
||||
|
||||
if (oidc.state !== "enabled") {
|
||||
throw data(`OIDC is unavailable: ${oidc.reason}`, { status: 501 });
|
||||
}
|
||||
const service = context.oidc.value;
|
||||
const service = oidc.value;
|
||||
|
||||
if (url.searchParams.toString().length === 0) {
|
||||
return redirect("/login?s=error_no_query");
|
||||
}
|
||||
|
||||
const cookie = createOidcStateCookie(context.config);
|
||||
const cookie = createOidcStateCookie(config);
|
||||
const oidcCookieState = await cookie.parse(request.headers.get("Cookie"));
|
||||
|
||||
if (oidcCookieState == null) {
|
||||
@@ -53,7 +66,7 @@ export async function loader({ request, context, url }: Route.LoaderArgs) {
|
||||
? identity.role
|
||||
: undefined;
|
||||
|
||||
const userId = await context.auth.findOrCreateUser(
|
||||
const userId = await auth.findOrCreateUser(
|
||||
identity.subject,
|
||||
{
|
||||
name: identity.name,
|
||||
@@ -61,7 +74,7 @@ export async function loader({ request, context, url }: Route.LoaderArgs) {
|
||||
picture: identity.picture,
|
||||
},
|
||||
{
|
||||
initialRole: claimedRole ?? context.config.oidc?.default_role,
|
||||
initialRole: claimedRole ?? config.oidc?.default_role,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -69,11 +82,11 @@ export async function loader({ request, context, url }: Route.LoaderArgs) {
|
||||
// Looks up the Headscale user that matches this OIDC identity. We use
|
||||
// the configured admin API key here — not a per-request one — because
|
||||
// there is no per-request key yet (the session is being created).
|
||||
const hsApi = context.headscale.client(context.headscaleApiKey!);
|
||||
const hsApi = headscale.client(headscaleApiKey!);
|
||||
const hsUsers = await hsApi.users.list();
|
||||
const hsUser = findHeadscaleUserBySubject(hsUsers, identity.subject, identity.email);
|
||||
if (hsUser) {
|
||||
await context.auth.linkHeadscaleUser(userId, hsUser.id);
|
||||
await auth.linkHeadscaleUser(userId, hsUser.id);
|
||||
}
|
||||
} catch (error) {
|
||||
log.warn("auth", "Failed to link Headscale user: %s", String(error));
|
||||
@@ -81,11 +94,11 @@ export async function loader({ request, context, url }: Route.LoaderArgs) {
|
||||
|
||||
// Only persist the id_token when RP-initiated logout is enabled — otherwise
|
||||
// we'd be storing a credential we never use.
|
||||
const idToken = context.config.oidc?.use_end_session ? identity.idToken : undefined;
|
||||
const idToken = config.oidc?.use_end_session ? identity.idToken : undefined;
|
||||
|
||||
return redirect("/", {
|
||||
headers: {
|
||||
"Set-Cookie": await context.auth.createOidcSession(
|
||||
"Set-Cookie": await auth.createOidcSession(
|
||||
userId,
|
||||
{
|
||||
name: identity.name,
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
import { data, redirect } from "react-router";
|
||||
|
||||
import { appConfigContext, authContext, oidcContext } from "~/server/context";
|
||||
import { createOidcStateCookie } from "~/utils/oidc-state";
|
||||
|
||||
import type { Route } from "./+types/oidc-start";
|
||||
|
||||
export async function loader({ request, context }: Route.LoaderArgs) {
|
||||
const auth = context.get(authContext);
|
||||
const config = context.get(appConfigContext);
|
||||
const oidc = context.get(oidcContext);
|
||||
|
||||
try {
|
||||
await context.auth.require(request);
|
||||
await auth.require(request);
|
||||
return redirect("/");
|
||||
} catch {}
|
||||
|
||||
if (context.oidc.state !== "enabled") {
|
||||
throw data(`OIDC is unavailable: ${context.oidc.reason}`, { status: 501 });
|
||||
if (oidc.state !== "enabled") {
|
||||
throw data(`OIDC is unavailable: ${oidc.reason}`, { status: 501 });
|
||||
}
|
||||
const service = context.oidc.value;
|
||||
const service = oidc.value;
|
||||
|
||||
const result = await service.startFlow();
|
||||
if (!result.ok) {
|
||||
@@ -21,7 +26,7 @@ export async function loader({ request, context }: Route.LoaderArgs) {
|
||||
}
|
||||
|
||||
const { url, flowState } = result.value;
|
||||
const cookie = createOidcStateCookie(context.config);
|
||||
const cookie = createOidcStateCookie(config);
|
||||
|
||||
return redirect(url, {
|
||||
status: 302,
|
||||
|
||||
Reference in New Issue
Block a user