mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-16 09:56:36 +00:00
Merge branch 'dev' into feat/login-page-customization
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
|
||||
7
src/lib/getSevenDaysAgo.ts
Normal file
7
src/lib/getSevenDaysAgo.ts
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
export function constructShareLink(
|
||||
token: string
|
||||
) {
|
||||
export function constructShareLink(token: string) {
|
||||
return `${window.location.origin}/s/${token!}`;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
69
src/lib/wireguard.ts
Normal 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`;
|
||||
}
|
||||
Reference in New Issue
Block a user