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

This commit is contained in:
miloschwartz
2025-12-17 11:41:17 -05:00
660 changed files with 19695 additions and 12803 deletions

View File

@@ -7,7 +7,10 @@ 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: "Device Login", regex: /^\/auth\/login\/device(\?code=[a-zA-Z0-9-]+)?$/ }
{
name: "Device Login",
regex: /^\/auth\/login\/device(\?code=[a-zA-Z0-9-]+)?$/
}
];
export function cleanRedirect(input: string, fallback?: string): string {

View File

@@ -1,21 +1,21 @@
export function parseDataSize(sizeStr: string): number {
if (typeof sizeStr !== 'string') return 0;
if (typeof sizeStr !== "string") return 0;
const match = /^\s*([\d.]+)\s*([KMGT]?B)\s*$/i.exec(sizeStr);
if (!match) return 0;
const match = /^\s*([\d.]+)\s*([KMGT]?B)\s*$/i.exec(sizeStr);
if (!match) return 0;
const [ , numStr, unitRaw ] = match;
const num = parseFloat(numStr);
if (isNaN(num)) return 0;
const [, numStr, unitRaw] = match;
const num = parseFloat(numStr);
if (isNaN(num)) return 0;
const unit = unitRaw.toUpperCase();
const multipliers = {
B: 1,
KB: 1024,
MB: 1024 ** 2,
GB: 1024 ** 3,
TB: 1024 ** 4,
} as const;
const unit = unitRaw.toUpperCase();
const multipliers = {
B: 1,
KB: 1024,
MB: 1024 ** 2,
GB: 1024 ** 3,
TB: 1024 ** 4
} as const;
return num * (multipliers[unit as keyof typeof multipliers] ?? 1);
}
return num * (multipliers[unit as keyof typeof multipliers] ?? 1);
}

View File

@@ -27,7 +27,9 @@ export class DockerManager {
async checkDockerSocket(): Promise<void> {
try {
const res = await this.api.post(`/site/${this.siteId}/docker/check`);
const res = await this.api.post(
`/site/${this.siteId}/docker/check`
);
console.log("Docker socket check response:", res);
} catch (error) {
console.error("Failed to check Docker socket:", error);
@@ -91,9 +93,7 @@ export class DockerManager {
};
try {
await this.api.post(
`/site/${this.siteId}/docker/trigger`
);
await this.api.post(`/site/${this.siteId}/docker/trigger`);
return await fetchContainerList();
} catch (error) {
console.error("Failed to trigger Docker containers:", error);
@@ -103,10 +103,10 @@ export class DockerManager {
async initializeDocker(): Promise<DockerState> {
console.log(`Initializing Docker for site ID: ${this.siteId}`);
// For now, assume Docker is enabled for newt sites
const isEnabled = true;
if (!isEnabled) {
return {
isEnabled: false,
@@ -118,7 +118,7 @@ export class DockerManager {
// Check and get Docker socket status
await this.checkDockerSocket();
const dockerStatus = await this.getDockerSocketStatus();
const isAvailable = dockerStatus?.isAvailable || false;
let containers: Container[] = [];

View File

@@ -0,0 +1,7 @@
export function getSevenDaysAgo() {
const today = new Date();
today.setHours(0, 0, 0, 0); // Set to midnight
const sevenDaysAgo = new Date(today);
sevenDaysAgo.setDate(today.getDate() - 7);
return sevenDaysAgo;
}

View File

@@ -1,29 +1,30 @@
export function parseHostTarget(input: string) {
try {
const normalized = input.match(/^(https?|h2c):\/\//) ? input : `http://${input}`;
const url = new URL(normalized);
try {
const normalized = input.match(/^(https?|h2c):\/\//)
? input
: `http://${input}`;
const url = new URL(normalized);
const protocol = url.protocol.replace(":", ""); // http | https | h2c
const host = url.hostname;
let defaultPort: number;
switch (protocol) {
case "https":
defaultPort = 443;
break;
case "h2c":
defaultPort = 80;
break;
default: // http
defaultPort = 80;
break;
const protocol = url.protocol.replace(":", ""); // http | https | h2c
const host = url.hostname;
let defaultPort: number;
switch (protocol) {
case "https":
defaultPort = 443;
break;
case "h2c":
defaultPort = 80;
break;
default: // http
defaultPort = 80;
break;
}
const port = url.port ? parseInt(url.port, 10) : defaultPort;
return { protocol, host, port };
} catch {
return null;
}
const port = url.port ? parseInt(url.port, 10) : defaultPort;
return { protocol, host, port };
} catch {
return null;
}
}

View File

@@ -89,11 +89,9 @@ export function pullEnv(): Env {
}
},
loginPage: {
titleText: process.env.LOGIN_PAGE_TITLE_TEXT as string,
subtitleText: process.env.LOGIN_PAGE_SUBTITLE_TEXT as string
},
signupPage: {
titleText: process.env.SIGNUP_PAGE_TITLE_TEXT as string,
subtitleText: process.env.SIGNUP_PAGE_SUBTITLE_TEXT as string
},
resourceAuthPage: {

View File

@@ -16,6 +16,7 @@ import { remote } from "./api";
import { durationToMs } from "./durationToMs";
import type { QueryRequestAnalyticsResponse } from "@server/routers/auditLogs";
import type { ListResourceNamesResponse } from "@server/routers/resource";
import type { ListDomainsResponse } from "@server/routers/domain";
export type ProductUpdate = {
link: string | null;
@@ -139,6 +140,17 @@ export const orgQueries = {
>(`/org/${orgId}/sites`, { signal });
return res.data.data.sites;
}
}),
domains: ({ orgId }: { orgId: string }) =>
queryOptions({
queryKey: ["ORG", orgId, "DOMAINS"] as const,
queryFn: async ({ signal, meta }) => {
const res = await meta!.api.get<
AxiosResponse<ListDomainsResponse>
>(`/org/${orgId}/domains`, { signal });
return res.data.data.domains;
}
})
};
@@ -168,17 +180,15 @@ export type LogAnalyticsFilters = z.TypeOf<typeof logAnalyticsFiltersSchema>;
export const logQueries = {
requestAnalytics: ({
orgId,
filters,
api
filters
}: {
orgId: string;
filters: LogAnalyticsFilters;
api: AxiosInstance;
}) =>
queryOptions({
queryKey: ["REQUEST_LOG_ANALYTICS", orgId, filters] as const,
queryFn: async ({ signal }) => {
const res = await api.get<
queryFn: async ({ signal, meta }) => {
const res = await meta!.api.get<
AxiosResponse<QueryRequestAnalyticsResponse>
>(`/org/${orgId}/logs/analytics`, {
params: filters,
@@ -228,11 +238,11 @@ export const resourceQueries = {
return res.data.data.clients;
}
}),
listNamesPerOrg: (orgId: string, api: AxiosInstance) =>
listNamesPerOrg: (orgId: string) =>
queryOptions({
queryKey: ["RESOURCES_NAMES", orgId] as const,
queryFn: async ({ signal }) => {
const res = await api.get<
queryFn: async ({ signal, meta }) => {
const res = await meta!.api.get<
AxiosResponse<ListResourceNamesResponse>
>(`/org/${orgId}/resource-names`, {
signal

View File

@@ -1,5 +1,3 @@
export function constructShareLink(
token: string
) {
export function constructShareLink(token: string) {
return `${window.location.origin}/s/${token!}`;
}

View File

@@ -1,63 +1,67 @@
export type DomainType = "organization" | "provided" | "provided-search";
export const SINGLE_LABEL_RE = /^[\p{L}\p{N}-]+$/u; // provided-search (no dots)
export const MULTI_LABEL_RE = /^[\p{L}\p{N}-]+(\.[\p{L}\p{N}-]+)*$/u; // ns/wildcard
export const SINGLE_LABEL_STRICT_RE = /^[\p{L}\p{N}](?:[\p{L}\p{N}-]*[\p{L}\p{N}])?$/u; // start/end alnum
export const SINGLE_LABEL_STRICT_RE =
/^[\p{L}\p{N}](?:[\p{L}\p{N}-]*[\p{L}\p{N}])?$/u; // start/end alnum
export function sanitizeInputRaw(input: string): string {
if (!input) return "";
return input
.toLowerCase()
.normalize("NFC") // normalize Unicode
.replace(/[^\p{L}\p{N}.-]/gu, ""); // allow Unicode letters, numbers, dot, hyphen
if (!input) return "";
return input
.toLowerCase()
.normalize("NFC") // normalize Unicode
.replace(/[^\p{L}\p{N}.-]/gu, ""); // allow Unicode letters, numbers, dot, hyphen
}
export function finalizeSubdomainSanitize(input: string): string {
if (!input) return "";
return input
.toLowerCase()
.normalize("NFC")
.replace(/[^\p{L}\p{N}.-]/gu, "") // allow Unicode
.replace(/\.{2,}/g, ".") // collapse multiple dots
.replace(/^-+|-+$/g, "") // strip leading/trailing hyphens
.replace(/^\.+|\.+$/g, "") // strip leading/trailing dots
.replace(/(\.-)|(-\.)/g, "."); // fix illegal dot-hyphen combos
if (!input) return "";
return input
.toLowerCase()
.normalize("NFC")
.replace(/[^\p{L}\p{N}.-]/gu, "") // allow Unicode
.replace(/\.{2,}/g, ".") // collapse multiple dots
.replace(/^-+|-+$/g, "") // strip leading/trailing hyphens
.replace(/^\.+|\.+$/g, "") // strip leading/trailing dots
.replace(/(\.-)|(-\.)/g, "."); // fix illegal dot-hyphen combos
}
export function validateByDomainType(subdomain: string, domainType: { type: "provided-search" | "organization"; domainType?: "ns" | "cname" | "wildcard" } ): boolean {
if (!domainType) return false;
if (domainType.type === "provided-search") {
return SINGLE_LABEL_RE.test(subdomain);
}
if (domainType.type === "organization") {
if (domainType.domainType === "cname") {
return subdomain === "";
} else if (domainType.domainType === "ns" || domainType.domainType === "wildcard") {
if (subdomain === "") return true;
if (!MULTI_LABEL_RE.test(subdomain)) return false;
const labels = subdomain.split(".");
return labels.every(l => l.length >= 1 && l.length <= 63 && SINGLE_LABEL_RE.test(l));
export function validateByDomainType(
subdomain: string,
domainType: {
type: "provided-search" | "organization";
domainType?: "ns" | "cname" | "wildcard";
}
}
return false;
): boolean {
if (!domainType) return false;
if (domainType.type === "provided-search") {
return SINGLE_LABEL_RE.test(subdomain);
}
if (domainType.type === "organization") {
if (domainType.domainType === "cname") {
return subdomain === "";
} else if (
domainType.domainType === "ns" ||
domainType.domainType === "wildcard"
) {
if (subdomain === "") return true;
if (!MULTI_LABEL_RE.test(subdomain)) return false;
const labels = subdomain.split(".");
return labels.every(
(l) =>
l.length >= 1 && l.length <= 63 && SINGLE_LABEL_RE.test(l)
);
}
}
return false;
}
export const isValidSubdomainStructure = (input: string): boolean => {
const regex = /^(?!-)([\p{L}\p{N}-]{1,63})(?<!-)$/u;
const regex = /^(?!-)([\p{L}\p{N}-]{1,63})(?<!-)$/u;
if (!input) return false;
if (input.includes("..")) return false;
if (!input) return false;
if (input.includes("..")) return false;
return input.split(".").every(label => regex.test(label));
return input.split(".").every((label) => regex.test(label));
};

View File

@@ -50,11 +50,9 @@ export type Env = {
};
};
loginPage: {
titleText?: string;
subtitleText?: string;
};
signupPage: {
titleText?: string;
subtitleText?: string;
};
resourceAuthPage: {

69
src/lib/wireguard.ts Normal file
View File

@@ -0,0 +1,69 @@
export function generateWireGuardConfig(
privateKey: string,
publicKey: string,
subnet: string,
address: string,
endpoint: string,
listenPort: string | number
): string {
const addressWithoutCidr = address.split("/")[0];
const port = typeof listenPort === "number" ? listenPort : listenPort;
return `[Interface]
Address = ${subnet}
ListenPort = 51820
PrivateKey = ${privateKey}
[Peer]
PublicKey = ${publicKey}
AllowedIPs = ${addressWithoutCidr}/32
Endpoint = ${endpoint}:${port}
PersistentKeepalive = 5`;
}
export function generateObfuscatedWireGuardConfig(options?: {
subnet?: string | null;
address?: string | null;
endpoint?: string | null;
listenPort?: number | string | null;
publicKey?: string | null;
}): string {
const obfuscate = (
value: string | null | undefined,
length: number = 20
): string => {
return value || "•".repeat(length);
};
const obfuscateKey = (value: string | null | undefined): string => {
return value || "•".repeat(44); // Base64 key length
};
const subnet = options?.subnet || obfuscate(null, 20);
const subnetWithCidr = subnet.includes("•")
? `${subnet}/32`
: subnet.includes("/")
? subnet
: `${subnet}/32`;
const address = options?.address
? options.address.split("/")[0]
: obfuscate(null, 20);
const endpoint = obfuscate(options?.endpoint, 20);
const listenPort = options?.listenPort
? typeof options.listenPort === "number"
? options.listenPort
: options.listenPort
: 51820;
const publicKey = obfuscateKey(options?.publicKey);
return `[Interface]
Address = ${subnetWithCidr}
ListenPort = 51820
PrivateKey = ${obfuscateKey(null)}
[Peer]
PublicKey = ${publicKey}
AllowedIPs = ${address}/32
Endpoint = ${endpoint}:${listenPort}
PersistentKeepalive = 5`;
}