Merge branch 'dev' into feat/login-page-customization

This commit is contained in:
Fred KISSIE
2025-12-05 22:38:07 +01:00
275 changed files with 21920 additions and 6990 deletions

View File

@@ -1,22 +1,32 @@
import { cookies, headers } from "next/headers";
import { pullEnv } from "../pullEnv";
import { headers } from "next/headers";
export async function authCookieHeader() {
const env = pullEnv();
const allCookies = await cookies();
const cookieName = env.server.sessionCookieName;
const sessionId = allCookies.get(cookieName)?.value ?? null;
// all other headers
// this is needed to pass through x-forwarded-for, x-forwarded-proto, etc.
const otherHeaders = await headers();
const otherHeadersObject = Object.fromEntries(otherHeaders.entries());
return {
headers: {
Cookie: `${cookieName}=${sessionId}`,
...otherHeadersObject
},
cookie:
otherHeadersObject["cookie"] || otherHeadersObject["Cookie"],
host: otherHeadersObject["host"] || otherHeadersObject["Host"],
"user-agent":
otherHeadersObject["user-agent"] ||
otherHeadersObject["User-Agent"],
"x-forwarded-for":
otherHeadersObject["x-forwarded-for"] ||
otherHeadersObject["X-Forwarded-For"],
"x-forwarded-host":
otherHeadersObject["fx-forwarded-host"] ||
otherHeadersObject["Fx-Forwarded-Host"],
"x-forwarded-port":
otherHeadersObject["x-forwarded-port"] ||
otherHeadersObject["X-Forwarded-Port"],
"x-forwarded-proto":
otherHeadersObject["x-forwarded-proto"] ||
otherHeadersObject["X-Forwarded-Proto"],
"x-real-ip":
otherHeadersObject["x-real-ip"] ||
otherHeadersObject["X-Real-IP"]
}
};
}

View File

@@ -6,15 +6,21 @@ import { pullEnv } from "../pullEnv";
import { cache } from "react";
export const verifySession = cache(async function ({
skipCheckVerifyEmail
skipCheckVerifyEmail,
forceLogin
}: {
skipCheckVerifyEmail?: boolean;
forceLogin?: boolean;
} = {}): Promise<GetUserResponse | null> {
const env = pullEnv();
try {
const search = new URLSearchParams();
if (forceLogin) {
search.set("forceLogin", "true");
}
const res = await internal.get<AxiosResponse<GetUserResponse>>(
"/user",
`/user?${search.toString()}`,
await authCookieHeader()
);

View File

@@ -6,7 +6,8 @@ type PatternConfig = {
const patterns: PatternConfig[] = [
{ name: "Invite Token", regex: /^\/invite\?token=[a-zA-Z0-9-]+$/ },
{ name: "Setup", regex: /^\/setup$/ },
{ name: "Resource Auth Portal", regex: /^\/auth\/resource\/\d+$/ }
{ name: "Resource Auth Portal", regex: /^\/auth\/resource\/\d+$/ },
{ name: "Device Login", regex: /^\/auth\/login\/device(\?code=[a-zA-Z0-9-]+)?$/ }
];
export function cleanRedirect(input: string, fallback?: string): string {

View File

@@ -55,8 +55,6 @@ export function pullEnv(): Env {
process.env.FLAGS_DISABLE_BASIC_WIREGUARD_SITES === "true"
? true
: false,
enableClients:
process.env.FLAGS_ENABLE_CLIENTS === "true" ? true : false,
hideSupporterKey:
process.env.HIDE_SUPPORTER_KEY === "true" ? true : false,
usePangolinDns:
@@ -66,6 +64,10 @@ export function pullEnv(): Env {
branding: {
appName: process.env.BRANDING_APP_NAME as string,
background_image_path: process.env.BACKGROUND_IMAGE_PATH as string,
hideAuthLayoutFooter:
process.env.BRANDING_HIDE_AUTH_LAYOUT_FOOTER === "true"
? true
: false,
logo: {
lightPath: process.env.BRANDING_LOGO_LIGHT_PATH as string,
darkPath: process.env.BRANDING_LOGO_DARK_PATH as string,

View File

@@ -1,10 +1,19 @@
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
import { durationToMs } from "./durationToMs";
import { build } from "@server/build";
import { remote } from "./api";
import type { ListClientsResponse } from "@server/routers/client";
import type { ListRolesResponse } from "@server/routers/role";
import type { ListSitesResponse } from "@server/routers/site";
import type {
ListSiteResourceClientsResponse,
ListSiteResourceRolesResponse,
ListSiteResourceUsersResponse
} from "@server/routers/siteResource";
import type { ListUsersResponse } from "@server/routers/user";
import type ResponseT from "@server/types/Response";
import z from "zod";
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
import type { AxiosInstance, AxiosResponse } from "axios";
import z from "zod";
import { remote } from "./api";
import { durationToMs } from "./durationToMs";
import type { QueryRequestAnalyticsResponse } from "@server/routers/auditLogs";
import type { ListResourceNamesResponse } from "@server/routers/resource";
@@ -70,6 +79,69 @@ export const productUpdatesQueries = {
})
};
export const clientFilterSchema = z.object({
filter: z.enum(["machine", "user"]),
limit: z.int().prefault(1000).optional()
});
export const orgQueries = {
clients: ({
orgId,
filters
}: {
orgId: string;
filters: z.infer<typeof clientFilterSchema>;
}) =>
queryOptions({
queryKey: ["ORG", orgId, "CLIENTS", filters] as const,
queryFn: async ({ signal, meta }) => {
const sp = new URLSearchParams({
...filters,
limit: (filters.limit ?? 1000).toString()
});
const res = await meta!.api.get<
AxiosResponse<ListClientsResponse>
>(`/org/${orgId}/clients?${sp.toString()}`, { signal });
return res.data.data.clients;
}
}),
users: ({ orgId }: { orgId: string }) =>
queryOptions({
queryKey: ["ORG", orgId, "USERS"] as const,
queryFn: async ({ signal, meta }) => {
const res = await meta!.api.get<
AxiosResponse<ListUsersResponse>
>(`/org/${orgId}/users`, { signal });
return res.data.data.users;
}
}),
roles: ({ orgId }: { orgId: string }) =>
queryOptions({
queryKey: ["ORG", orgId, "ROLES"] as const,
queryFn: async ({ signal, meta }) => {
const res = await meta!.api.get<
AxiosResponse<ListRolesResponse>
>(`/org/${orgId}/roles`, { signal });
return res.data.data.roles;
}
}),
sites: ({ orgId }: { orgId: string }) =>
queryOptions({
queryKey: ["ORG", orgId, "SITES"] as const,
queryFn: async ({ signal, meta }) => {
const res = await meta!.api.get<
AxiosResponse<ListSitesResponse>
>(`/org/${orgId}/sites`, { signal });
return res.data.data.sites;
}
})
};
export const logAnalyticsFiltersSchema = z.object({
timeStart: z
.string()
@@ -124,6 +196,38 @@ export const logQueries = {
};
export const resourceQueries = {
resourceUsers: ({ resourceId }: { resourceId: number }) =>
queryOptions({
queryKey: ["RESOURCES", resourceId, "USERS"] as const,
queryFn: async ({ signal, meta }) => {
const res = await meta!.api.get<
AxiosResponse<ListSiteResourceUsersResponse>
>(`/site-resource/${resourceId}/users`, { signal });
return res.data.data.users;
}
}),
resourceRoles: ({ resourceId }: { resourceId: number }) =>
queryOptions({
queryKey: ["RESOURCES", resourceId, "ROLES"] as const,
queryFn: async ({ signal, meta }) => {
const res = await meta!.api.get<
AxiosResponse<ListSiteResourceRolesResponse>
>(`/site-resource/${resourceId}/roles`, { signal });
return res.data.data.roles;
}
}),
resourceClients: ({ resourceId }: { resourceId: number }) =>
queryOptions({
queryKey: ["RESOURCES", resourceId, "CLIENTS"] as const,
queryFn: async ({ signal, meta }) => {
const res = await meta!.api.get<
AxiosResponse<ListSiteResourceClientsResponse>
>(`/site-resource/${resourceId}/clients`, { signal });
return res.data.data.clients;
}
}),
listNamesPerOrg: (orgId: string, api: AxiosInstance) =>
queryOptions({
queryKey: ["RESOURCES_NAMES", orgId] as const,

View File

@@ -30,14 +30,14 @@ export type Env = {
allowRawResources: boolean;
disableLocalSites: boolean;
disableBasicWireguardSites: boolean;
enableClients: boolean;
hideSupporterKey: boolean;
usePangolinDns: boolean;
};
branding: {
appName?: string;
background_image_path?: string;
logo: {
hideAuthLayoutFooter?: boolean;
logo?: {
lightPath?: string;
darkPath?: string;
authPage?: {