mirror of
https://github.com/tale/headplane.git
synced 2026-07-26 15:58:14 +00:00
fix: actually fix type errors
This commit is contained in:
@@ -4,7 +4,7 @@ import { isRouteErrorResponse, useRevalidator } from "react-router";
|
||||
import { isConnectionError } from "~/server/headscale/api/error-client";
|
||||
import cn from "~/utils/cn";
|
||||
|
||||
import Button from "./Button";
|
||||
import Button from "./button";
|
||||
import { ErrorBanner } from "./error-banner";
|
||||
|
||||
interface PageErrorProps {
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import { Outlet, redirect } from "react-router";
|
||||
|
||||
import { ErrorBanner } from "~/components/error-banner";
|
||||
import { pruneEphemeralNodes } from "~/server/db/pruner";
|
||||
import { isDataUnauthorizedError } from "~/server/headscale/api/error-client";
|
||||
import log from "~/utils/log";
|
||||
|
||||
import type { Route } from "./+types/dashboard";
|
||||
|
||||
export async function loader({ request, context, ...rest }: Route.LoaderArgs) {
|
||||
const principal = await context.auth.require(request);
|
||||
const apiKey = context.auth.getHeadscaleApiKey(principal, context.oidc?.apiKey);
|
||||
const api = context.hsApi.getRuntimeClient(apiKey);
|
||||
|
||||
// MARK: The session should stay valid if Headscale isn't healthy
|
||||
const healthy = await api.isHealthy();
|
||||
if (healthy) {
|
||||
try {
|
||||
await api.getApiKeys();
|
||||
await pruneEphemeralNodes({ context, request, ...rest });
|
||||
} catch (error) {
|
||||
if (isDataUnauthorizedError(error)) {
|
||||
const displayName =
|
||||
principal.kind === "oidc" ? principal.profile.name : principal.displayName;
|
||||
log.warn("auth", "Logging out %s due to expired API key", displayName);
|
||||
return redirect("/login", {
|
||||
headers: {
|
||||
"Set-Cookie": await context.auth.destroySession(request),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
healthy,
|
||||
};
|
||||
}
|
||||
|
||||
export default function Layout() {
|
||||
return (
|
||||
<main className="container mt-4 mb-24 overscroll-contain">
|
||||
<Outlet />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
||||
return (
|
||||
<div className="mx-auto my-24 w-fit overscroll-contain">
|
||||
<ErrorBanner className="max-w-2xl" error={error} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import Button from "~/components/Button";
|
||||
import Button from "~/components/button";
|
||||
import Code from "~/components/Code";
|
||||
import Dialog, { DialogPanel } from "~/components/Dialog";
|
||||
import Input from "~/components/Input";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Button from "~/components/Button";
|
||||
import Button from "~/components/button";
|
||||
import Dialog, { DialogPanel } from "~/components/Dialog";
|
||||
import Text from "~/components/Text";
|
||||
import Title from "~/components/Title";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Split } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import Button from "~/components/Button";
|
||||
import Button from "~/components/button";
|
||||
import Chip from "~/components/Chip";
|
||||
import Dialog, { DialogPanel } from "~/components/Dialog";
|
||||
import Input from "~/components/Input";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import Button from "~/components/Button";
|
||||
import Button from "~/components/button";
|
||||
import Code from "~/components/Code";
|
||||
import Dialog, { DialogPanel } from "~/components/Dialog";
|
||||
import Input from "~/components/Input";
|
||||
|
||||
@@ -48,11 +48,14 @@ export async function loader({ request, params, context }: Route.LoaderArgs) {
|
||||
const nodes = nodesSnap.data;
|
||||
const users = usersSnap.data;
|
||||
const node = nodes.find((node) => node.id === params.id);
|
||||
if (node == null) {
|
||||
throw data(null, { status: 404 });
|
||||
}
|
||||
|
||||
const lookup = await context.agents?.lookup([node.nodeKey]);
|
||||
const [enhancedNode] = mapNodes([node], lookup);
|
||||
const tags = [...node.tags].toSorted();
|
||||
const supportsNodeOwnerChange = !context.hsApi.clientHelpers.isAtleast("0.28.0-beta.1");
|
||||
const supportsNodeOwnerChange = !context.hsApi.clientHelpers.isAtleast("0.28.0");
|
||||
|
||||
return {
|
||||
agent: context.agents?.agentID(),
|
||||
|
||||
@@ -46,7 +46,7 @@ export async function loader({ request, context }: Route.LoaderArgs) {
|
||||
|
||||
const stats = await context.agents?.lookup(nodes.map((node) => node.nodeKey));
|
||||
const populatedNodes = mapNodes(nodes, stats);
|
||||
const supportsNodeOwnerChange = !context.hsApi.clientHelpers.isAtleast("0.28.0-beta.1");
|
||||
const supportsNodeOwnerChange = !context.hsApi.clientHelpers.isAtleast("0.28.0");
|
||||
|
||||
return {
|
||||
agent: context.agents?.agentID(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Button from "~/components/Button";
|
||||
import Button from "~/components/button";
|
||||
import Dialog, { DialogPanel } from "~/components/Dialog";
|
||||
import Text from "~/components/Text";
|
||||
import Title from "~/components/Title";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import Button from "~/components/Button";
|
||||
import Button from "~/components/button";
|
||||
import Dialog, { DialogPanel } from "~/components/Dialog";
|
||||
import Input from "~/components/Input";
|
||||
import Text from "~/components/Text";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import Button from "~/components/Button";
|
||||
import Button from "~/components/button";
|
||||
import Dialog, { DialogPanel } from "~/components/Dialog";
|
||||
import Input from "~/components/Input";
|
||||
import Text from "~/components/Text";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import Button from "~/components/Button";
|
||||
import Button from "~/components/button";
|
||||
import Dialog, { DialogPanel } from "~/components/Dialog";
|
||||
import Input from "~/components/Input";
|
||||
import Text from "~/components/Text";
|
||||
|
||||
@@ -1,47 +1,46 @@
|
||||
import { useState } from 'react';
|
||||
import Button from '~/components/Button';
|
||||
import Card from '~/components/Card';
|
||||
import Code from '~/components/Code';
|
||||
import Input from '~/components/Input';
|
||||
import { useState } from "react";
|
||||
|
||||
import Button from "~/components/button";
|
||||
import Card from "~/components/Card";
|
||||
import Code from "~/components/Code";
|
||||
import Input from "~/components/Input";
|
||||
|
||||
interface UserPromptProps {
|
||||
hostname: string;
|
||||
hostname: string;
|
||||
}
|
||||
|
||||
export default function UserPrompt({ hostname }: UserPromptProps) {
|
||||
const [username, setUsername] = useState('');
|
||||
const [username, setUsername] = useState("");
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center h-screen">
|
||||
<Card>
|
||||
<Card.Title>Enter Username</Card.Title>
|
||||
<Card.Text className="mb-4">
|
||||
Enter the username you want to use to connect to{' '}
|
||||
<Code>{hostname}</Code>
|
||||
{'. '}
|
||||
WebSSH follows the Headscale ACLs, so only permitted usernames will be
|
||||
able to connect.
|
||||
</Card.Text>
|
||||
<Input
|
||||
labelHidden
|
||||
type="text"
|
||||
label="Username"
|
||||
placeholder="Username"
|
||||
className="mb-2"
|
||||
onChange={setUsername}
|
||||
/>
|
||||
<Button
|
||||
variant="heavy"
|
||||
className="w-full"
|
||||
onPress={() => {
|
||||
// We can't use the navigate hook here as we need to do a
|
||||
// full page reload to ensure the SSH connection is established
|
||||
window.location.href = `${__PREFIX__}/ssh?hostname=${hostname}&username=${username}`;
|
||||
}}
|
||||
>
|
||||
Connect
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="flex h-screen items-center justify-center">
|
||||
<Card>
|
||||
<Card.Title>Enter Username</Card.Title>
|
||||
<Card.Text className="mb-4">
|
||||
Enter the username you want to use to connect to <Code>{hostname}</Code>
|
||||
{". "}
|
||||
WebSSH follows the Headscale ACLs, so only permitted usernames will be able to connect.
|
||||
</Card.Text>
|
||||
<Input
|
||||
labelHidden
|
||||
type="text"
|
||||
label="Username"
|
||||
placeholder="Username"
|
||||
className="mb-2"
|
||||
onChange={setUsername}
|
||||
/>
|
||||
<Button
|
||||
variant="heavy"
|
||||
className="w-full"
|
||||
onPress={() => {
|
||||
// We can't use the navigate hook here as we need to do a
|
||||
// full page reload to ensure the SSH connection is established
|
||||
window.location.href = `${__PREFIX__}/ssh?hostname=${hostname}&username=${username}`;
|
||||
}}
|
||||
>
|
||||
Connect
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import log from "~/utils/log";
|
||||
|
||||
import type { HeadplaneConfig } from "../config/config-schema";
|
||||
import type { RuntimeApiClient } from "../headscale/api/endpoints";
|
||||
|
||||
import { isDataUnauthorizedError } from "../headscale/api/error-client";
|
||||
|
||||
export type OidcConfig = NonNullable<HeadplaneConfig["oidc"]>;
|
||||
@@ -228,7 +227,7 @@ async function discoveryCoalesce(
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
log.warn("oidc", "Failed to reach OIDC provider for discovery, will retry on next request");
|
||||
log.warn("auth", "Failed to reach OIDC provider for discovery, will retry on next request");
|
||||
discoveryFailed = true;
|
||||
metadata = {
|
||||
issuer: config.issuer,
|
||||
|
||||
+28
-32
@@ -3,46 +3,42 @@
|
||||
// is static and logger is later modified in `app/server/index.ts` to
|
||||
// disable debug logging if the `HEADPLANE_DEBUG_LOG` specifies as such.
|
||||
|
||||
const levels = ['info', 'warn', 'error', 'debug'] as const;
|
||||
type Category = 'server' | 'config' | 'agent' | 'api' | 'auth';
|
||||
const levels = ["info", "warn", "error", "debug"] as const;
|
||||
type Category = "server" | "config" | "agent" | "api" | "auth" | "sse";
|
||||
|
||||
export interface Logger
|
||||
extends Record<
|
||||
(typeof levels)[number],
|
||||
(category: Category, message: string, ...args: unknown[]) => void
|
||||
> {
|
||||
debugEnabled: boolean;
|
||||
export interface Logger extends Record<
|
||||
(typeof levels)[number],
|
||||
(category: Category, message: string, ...args: unknown[]) => void
|
||||
> {
|
||||
debugEnabled: boolean;
|
||||
}
|
||||
|
||||
const logLevels = getLogLevels();
|
||||
export default {
|
||||
debugEnabled: logLevels.includes('debug'),
|
||||
debug: (..._: Parameters<Logger['debug']>) => {},
|
||||
...Object.fromEntries(
|
||||
logLevels.map((level) => [
|
||||
level,
|
||||
(category: Category, message: string, ...args: unknown[]) => {
|
||||
const date = new Date().toISOString();
|
||||
console.log(
|
||||
`${date} [${category}] ${level.toUpperCase()}: ${message}`,
|
||||
...args,
|
||||
);
|
||||
},
|
||||
]),
|
||||
),
|
||||
debugEnabled: logLevels.includes("debug"),
|
||||
debug: (..._: Parameters<Logger["debug"]>) => {},
|
||||
...Object.fromEntries(
|
||||
logLevels.map((level) => [
|
||||
level,
|
||||
(category: Category, message: string, ...args: unknown[]) => {
|
||||
const date = new Date().toISOString();
|
||||
console.log(`${date} [${category}] ${level.toUpperCase()}: ${message}`, ...args);
|
||||
},
|
||||
]),
|
||||
),
|
||||
} as Logger;
|
||||
|
||||
function getLogLevels() {
|
||||
const debugLog = process.env.HEADPLANE_DEBUG_LOG;
|
||||
if (debugLog == null) {
|
||||
return ['info', 'warn', 'error'];
|
||||
}
|
||||
const debugLog = process.env.HEADPLANE_DEBUG_LOG;
|
||||
if (debugLog == null) {
|
||||
return ["info", "warn", "error"];
|
||||
}
|
||||
|
||||
const normalized = debugLog.trim().toLowerCase();
|
||||
const truthyValues = ['1', 'true', 'yes', 'on'];
|
||||
if (!truthyValues.includes(normalized)) {
|
||||
return ['info', 'warn', 'error'];
|
||||
}
|
||||
const normalized = debugLog.trim().toLowerCase();
|
||||
const truthyValues = ["1", "true", "yes", "on"];
|
||||
if (!truthyValues.includes(normalized)) {
|
||||
return ["info", "warn", "error"];
|
||||
}
|
||||
|
||||
return ['info', 'warn', 'error', 'debug'];
|
||||
return ["info", "warn", "error", "debug"];
|
||||
}
|
||||
|
||||
+19
-24
@@ -1,26 +1,21 @@
|
||||
{
|
||||
"include": [
|
||||
"**/*",
|
||||
"**/.server/**/*",
|
||||
"**/.client/**/*",
|
||||
".react-router/types/**/*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||
"types": ["node", "vite/client"],
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "react-jsx",
|
||||
"rootDirs": [".", "./.react-router/types"],
|
||||
"paths": {
|
||||
"~/*": ["./app/*"]
|
||||
},
|
||||
"esModuleInterop": true,
|
||||
"verbatimModuleSyntax": false,
|
||||
"noEmit": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true
|
||||
}
|
||||
"include": ["**/*", "**/.server/**/*", "**/.client/**/*", ".react-router/types/**/*"],
|
||||
"compilerOptions": {
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2023"],
|
||||
"types": ["node", "vite/client"],
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "react-jsx",
|
||||
"rootDirs": [".", "./.react-router/types"],
|
||||
"paths": {
|
||||
"~/*": ["./app/*"]
|
||||
},
|
||||
"esModuleInterop": true,
|
||||
"verbatimModuleSyntax": false,
|
||||
"noEmit": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user