mirror of
https://github.com/fosrl/pangolin.git
synced 2026-09-11 21:41:41 +02:00
Compare commits
44 Commits
6496763aae
..
1.15.4
| Author | SHA1 | Date | |
|---|---|---|---|
| 843b13ed57 | |||
| be89e5ca55 | |||
| 333625f199 | |||
| dbfd715381 | |||
| f1d989964e | |||
| b701629498 | |||
| 8250946325 | |||
| 71f63d8e6f | |||
| dd5e834db0 | |||
| 970ecb52f0 | |||
| 62ea1b40e1 | |||
| 3b0fd5c592 | |||
| b7616026dd | |||
| 16ad60b89a | |||
| db7971d2f7 | |||
| f3f8bd3125 | |||
| 516fd0ee8f | |||
| 8d6700d493 | |||
| 9d4ace9b3e | |||
| 2800655e33 | |||
| 91eecee11d | |||
| 899e5aa395 | |||
| d5820c4902 | |||
| a91c002274 | |||
| 4d142b93dd | |||
| 04dcf57ff3 | |||
| 975550c755 | |||
| a964a80d85 | |||
| 22c3b8f116 | |||
| c4b1831cfe | |||
| cdb6813384 | |||
| b14b68d83c | |||
| 3c2f930e6b | |||
| ca9c7ce555 | |||
| c2e95a0607 | |||
| 2767ee9e80 | |||
| d998a8087f | |||
| fdce016921 | |||
| c73d70933b | |||
| e9d0ad6e37 | |||
| a35586f762 | |||
| f527c30923 | |||
| 94e70219cf | |||
| 69ecc22318 |
@@ -525,41 +525,10 @@ jobs:
|
|||||||
VERIFIED_INDEX_KEYLESS=false
|
VERIFIED_INDEX_KEYLESS=false
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# If index verification fails, attempt to verify child platform manifests
|
# Check if verification succeeded
|
||||||
if [ "${VERIFIED_INDEX}" != "true" ] || [ "${VERIFIED_INDEX_KEYLESS}" != "true" ]; then
|
if [ "${VERIFIED_INDEX}" != "true" ] && [ "${VERIFIED_INDEX_KEYLESS}" != "true" ]; then
|
||||||
echo "Index verification not available; attempting child manifest verification for ${BASE_IMAGE}:${IMAGE_TAG}"
|
echo "⚠️ WARNING: Verification not available for ${BASE_IMAGE}:${IMAGE_TAG}"
|
||||||
CHILD_VERIFIED=false
|
echo "This may be due to registry propagation delays. Continuing anyway."
|
||||||
|
|
||||||
for ARCH in arm64 amd64; do
|
|
||||||
CHILD_TAG="${IMAGE_TAG}-${ARCH}"
|
|
||||||
echo "Resolving child digest for ${BASE_IMAGE}:${CHILD_TAG}"
|
|
||||||
CHILD_DIGEST="$(skopeo inspect --retry-times 3 docker://${BASE_IMAGE}:${CHILD_TAG} | jq -r '.Digest' || true)"
|
|
||||||
if [ -n "${CHILD_DIGEST}" ] && [ "${CHILD_DIGEST}" != "null" ]; then
|
|
||||||
CHILD_REF="${BASE_IMAGE}@${CHILD_DIGEST}"
|
|
||||||
echo "==> cosign verify (public key) child ${CHILD_REF}"
|
|
||||||
if retry_verify "cosign verify --key env://COSIGN_PUBLIC_KEY '${CHILD_REF}' -o text"; then
|
|
||||||
CHILD_VERIFIED=true
|
|
||||||
echo "Public key verification succeeded for child ${CHILD_REF}"
|
|
||||||
else
|
|
||||||
echo "Public key verification failed for child ${CHILD_REF}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> cosign verify (keyless policy) child ${CHILD_REF}"
|
|
||||||
if retry_verify "cosign verify --certificate-oidc-issuer '${issuer}' --certificate-identity-regexp '${id_regex}' '${CHILD_REF}' -o text"; then
|
|
||||||
CHILD_VERIFIED=true
|
|
||||||
echo "Keyless verification succeeded for child ${CHILD_REF}"
|
|
||||||
else
|
|
||||||
echo "Keyless verification failed for child ${CHILD_REF}"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "No child digest found for ${BASE_IMAGE}:${CHILD_TAG}; skipping"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ "${CHILD_VERIFIED}" != "true" ]; then
|
|
||||||
echo "Failed to verify index and no child manifests verified for ${BASE_IMAGE}:${IMAGE_TAG}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
) || TAG_FAILED=true
|
) || TAG_FAILED=true
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -1154,7 +1154,7 @@
|
|||||||
"actionDeleteClient": "Client löschen",
|
"actionDeleteClient": "Client löschen",
|
||||||
"actionArchiveClient": "Client archivieren",
|
"actionArchiveClient": "Client archivieren",
|
||||||
"actionUnarchiveClient": "Client dearchivieren",
|
"actionUnarchiveClient": "Client dearchivieren",
|
||||||
"actionBlockClient": "Klient sperren",
|
"actionBlockClient": "Client sperren",
|
||||||
"actionUnblockClient": "Client entsperren",
|
"actionUnblockClient": "Client entsperren",
|
||||||
"actionUpdateClient": "Client aktualisieren",
|
"actionUpdateClient": "Client aktualisieren",
|
||||||
"actionListClients": "Clients auflisten",
|
"actionListClients": "Clients auflisten",
|
||||||
@@ -2532,10 +2532,10 @@
|
|||||||
"archiveClientQuestion": "Sind Sie sicher, dass Sie diesen Client archivieren möchten?",
|
"archiveClientQuestion": "Sind Sie sicher, dass Sie diesen Client archivieren möchten?",
|
||||||
"archiveClientMessage": "Der Client wird archiviert und aus der Liste Ihrer aktiven Clients entfernt.",
|
"archiveClientMessage": "Der Client wird archiviert und aus der Liste Ihrer aktiven Clients entfernt.",
|
||||||
"archiveClientConfirm": "Client archivieren",
|
"archiveClientConfirm": "Client archivieren",
|
||||||
"blockClient": "Klient sperren",
|
"blockClient": "Client sperren",
|
||||||
"blockClientQuestion": "Sind Sie sicher, dass Sie diesen Client blockieren möchten?",
|
"blockClientQuestion": "Sind Sie sicher, dass Sie diesen Client blockieren möchten?",
|
||||||
"blockClientMessage": "Das Gerät wird gezwungen, die Verbindung zu trennen, wenn es gerade verbunden ist. Sie können das Gerät später entsperren.",
|
"blockClientMessage": "Das Gerät wird gezwungen, die Verbindung zu trennen, wenn es gerade verbunden ist. Sie können das Gerät später entsperren.",
|
||||||
"blockClientConfirm": "Klient sperren",
|
"blockClientConfirm": "Client sperren",
|
||||||
"active": "Aktiv",
|
"active": "Aktiv",
|
||||||
"usernameOrEmail": "Benutzername oder E-Mail",
|
"usernameOrEmail": "Benutzername oder E-Mail",
|
||||||
"selectYourOrganization": "Wählen Sie Ihre Organisation",
|
"selectYourOrganization": "Wählen Sie Ihre Organisation",
|
||||||
|
|||||||
+1
-1
@@ -2060,7 +2060,7 @@
|
|||||||
"machineClientsBannerDescription": "Machine clients are for servers and automated systems that are not associated with a specific user. They authenticate with an ID and secret, and can run with Pangolin CLI, Olm CLI, or Olm as a container.",
|
"machineClientsBannerDescription": "Machine clients are for servers and automated systems that are not associated with a specific user. They authenticate with an ID and secret, and can run with Pangolin CLI, Olm CLI, or Olm as a container.",
|
||||||
"machineClientsBannerPangolinCLI": "Pangolin CLI",
|
"machineClientsBannerPangolinCLI": "Pangolin CLI",
|
||||||
"machineClientsBannerOlmCLI": "Olm CLI",
|
"machineClientsBannerOlmCLI": "Olm CLI",
|
||||||
"machineClientsBannerOlmContainer": "Olm Container",
|
"machineClientsBannerOlmContainer": "Container",
|
||||||
"clientsTableUserClients": "User",
|
"clientsTableUserClients": "User",
|
||||||
"clientsTableMachineClients": "Machine",
|
"clientsTableMachineClients": "Machine",
|
||||||
"licenseTableValidUntil": "Valid Until",
|
"licenseTableValidUntil": "Valid Until",
|
||||||
|
|||||||
+1
-1
@@ -15,7 +15,7 @@
|
|||||||
"dev:check": "npx tsc --noEmit && npm run format:check",
|
"dev:check": "npx tsc --noEmit && npm run format:check",
|
||||||
"dev:setup": "cp config/config.example.yml config/config.yml && npm run set:oss && npm run set:sqlite && npm run db:sqlite:generate && npm run db:sqlite:push",
|
"dev:setup": "cp config/config.example.yml config/config.yml && npm run set:oss && npm run set:sqlite && npm run db:sqlite:generate && npm run db:sqlite:push",
|
||||||
"db:generate": "drizzle-kit generate --config=./drizzle.config.ts",
|
"db:generate": "drizzle-kit generate --config=./drizzle.config.ts",
|
||||||
"db:push": "npx tsx server/db/pg/migrate.ts",
|
"db:push": "npx tsx server/db/migrate.ts",
|
||||||
"db:studio": "drizzle-kit studio --config=./drizzle.config.ts",
|
"db:studio": "drizzle-kit studio --config=./drizzle.config.ts",
|
||||||
"db:clear-migrations": "rm -rf server/migrations",
|
"db:clear-migrations": "rm -rf server/migrations",
|
||||||
"set:oss": "echo 'export const build = \"oss\" as \"saas\" | \"enterprise\" | \"oss\";' > server/build.ts && cp tsconfig.oss.json tsconfig.json",
|
"set:oss": "echo 'export const build = \"oss\" as \"saas\" | \"enterprise\" | \"oss\";' > server/build.ts && cp tsconfig.oss.json tsconfig.json",
|
||||||
|
|||||||
@@ -142,7 +142,8 @@ export const resources = pgTable("resources", {
|
|||||||
}).default("forced"), // "forced" = always show, "automatic" = only when down
|
}).default("forced"), // "forced" = always show, "automatic" = only when down
|
||||||
maintenanceTitle: text("maintenanceTitle"),
|
maintenanceTitle: text("maintenanceTitle"),
|
||||||
maintenanceMessage: text("maintenanceMessage"),
|
maintenanceMessage: text("maintenanceMessage"),
|
||||||
maintenanceEstimatedTime: text("maintenanceEstimatedTime")
|
maintenanceEstimatedTime: text("maintenanceEstimatedTime"),
|
||||||
|
postAuthPath: text("postAuthPath")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const targets = pgTable("targets", {
|
export const targets = pgTable("targets", {
|
||||||
|
|||||||
@@ -162,7 +162,8 @@ export const resources = sqliteTable("resources", {
|
|||||||
}).default("forced"), // "forced" = always show, "automatic" = only when down
|
}).default("forced"), // "forced" = always show, "automatic" = only when down
|
||||||
maintenanceTitle: text("maintenanceTitle"),
|
maintenanceTitle: text("maintenanceTitle"),
|
||||||
maintenanceMessage: text("maintenanceMessage"),
|
maintenanceMessage: text("maintenanceMessage"),
|
||||||
maintenanceEstimatedTime: text("maintenanceEstimatedTime")
|
maintenanceEstimatedTime: text("maintenanceEstimatedTime"),
|
||||||
|
postAuthPath: text("postAuthPath")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const targets = sqliteTable("targets", {
|
export const targets = sqliteTable("targets", {
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ export const sandboxLimitSet: LimitSet = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const freeLimitSet: LimitSet = {
|
export const freeLimitSet: LimitSet = {
|
||||||
[FeatureId.USERS]: { value: 5, description: "Starter limit" },
|
[FeatureId.SITES]: { value: 5, description: "Basic limit" },
|
||||||
[FeatureId.SITES]: { value: 5, description: "Starter limit" },
|
[FeatureId.USERS]: { value: 5, description: "Basic limit" },
|
||||||
[FeatureId.DOMAINS]: { value: 5, description: "Starter limit" },
|
[FeatureId.DOMAINS]: { value: 5, description: "Basic limit" },
|
||||||
[FeatureId.REMOTE_EXIT_NODES]: { value: 1, description: "Starter limit" },
|
[FeatureId.REMOTE_EXIT_NODES]: { value: 1, description: "Basic limit" },
|
||||||
};
|
};
|
||||||
|
|
||||||
export const tier1LimitSet: LimitSet = {
|
export const tier1LimitSet: LimitSet = {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import path from "path";
|
|||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
// This is a placeholder value replaced by the build process
|
// This is a placeholder value replaced by the build process
|
||||||
export const APP_VERSION = "1.15.3";
|
export const APP_VERSION = "1.15.4";
|
||||||
|
|
||||||
export const __FILENAME = fileURLToPath(import.meta.url);
|
export const __FILENAME = fileURLToPath(import.meta.url);
|
||||||
export const __DIRNAME = path.dirname(__FILENAME);
|
export const __DIRNAME = path.dirname(__FILENAME);
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* Normalizes a post-authentication path for safe use when building redirect URLs.
|
||||||
|
* Returns a path that starts with / and does not allow open redirects (no //, no :).
|
||||||
|
*/
|
||||||
|
export function normalizePostAuthPath(path: string | null | undefined): string | null {
|
||||||
|
if (path == null || typeof path !== "string") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const trimmed = path.trim();
|
||||||
|
if (trimmed === "") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Reject protocol-relative (//) or scheme (:) to avoid open redirect
|
||||||
|
if (trimmed.includes("//") || trimmed.includes(":")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ import { CreateOrgIdpResponse } from "@server/routers/orgIdp/types";
|
|||||||
import { isSubscribed } from "#private/lib/isSubscribed";
|
import { isSubscribed } from "#private/lib/isSubscribed";
|
||||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
import privateConfig from "#private/lib/config";
|
import privateConfig from "#private/lib/config";
|
||||||
|
import { build } from "@server/build";
|
||||||
|
|
||||||
const paramsSchema = z.strictObject({ orgId: z.string().nonempty() });
|
const paramsSchema = z.strictObject({ orgId: z.string().nonempty() });
|
||||||
|
|
||||||
@@ -122,12 +123,14 @@ export async function createOrgOidcIdp(
|
|||||||
|
|
||||||
let { autoProvision } = parsedBody.data;
|
let { autoProvision } = parsedBody.data;
|
||||||
|
|
||||||
const subscribed = await isSubscribed(
|
if (build == "saas") { // this is not paywalled with a ee license because this whole endpoint is restricted
|
||||||
orgId,
|
const subscribed = await isSubscribed(
|
||||||
tierMatrix.deviceApprovals
|
orgId,
|
||||||
);
|
tierMatrix.deviceApprovals
|
||||||
if (!subscribed) {
|
);
|
||||||
autoProvision = false;
|
if (!subscribed) {
|
||||||
|
autoProvision = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = config.getRawConfig().server.secret!;
|
const key = config.getRawConfig().server.secret!;
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import config from "@server/lib/config";
|
|||||||
import { isSubscribed } from "#private/lib/isSubscribed";
|
import { isSubscribed } from "#private/lib/isSubscribed";
|
||||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
import privateConfig from "#private/lib/config";
|
import privateConfig from "#private/lib/config";
|
||||||
|
import { build } from "@server/build";
|
||||||
|
|
||||||
const paramsSchema = z
|
const paramsSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -127,12 +128,15 @@ export async function updateOrgOidcIdp(
|
|||||||
|
|
||||||
let { autoProvision } = parsedBody.data;
|
let { autoProvision } = parsedBody.data;
|
||||||
|
|
||||||
const subscribed = await isSubscribed(
|
if (build == "saas") {
|
||||||
orgId,
|
// this is not paywalled with a ee license because this whole endpoint is restricted
|
||||||
tierMatrix.deviceApprovals
|
const subscribed = await isSubscribed(
|
||||||
);
|
orgId,
|
||||||
if (!subscribed) {
|
tierMatrix.deviceApprovals
|
||||||
autoProvision = false;
|
);
|
||||||
|
if (!subscribed) {
|
||||||
|
autoProvision = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if IDP exists and is of type OIDC
|
// Check if IDP exists and is of type OIDC
|
||||||
|
|||||||
@@ -797,7 +797,7 @@ async function notAllowed(
|
|||||||
) {
|
) {
|
||||||
let loginPage: LoginPage | null = null;
|
let loginPage: LoginPage | null = null;
|
||||||
if (orgId) {
|
if (orgId) {
|
||||||
const subscribed = await isSubscribed(
|
const subscribed = await isSubscribed( // this is fine because the org login page is only a saas feature
|
||||||
orgId,
|
orgId,
|
||||||
tierMatrix.loginPageDomain
|
tierMatrix.loginPageDomain
|
||||||
);
|
);
|
||||||
@@ -854,7 +854,7 @@ async function headerAuthChallenged(
|
|||||||
) {
|
) {
|
||||||
let loginPage: LoginPage | null = null;
|
let loginPage: LoginPage | null = null;
|
||||||
if (orgId) {
|
if (orgId) {
|
||||||
const subscribed = await isSubscribed(orgId, tierMatrix.loginPageDomain);
|
const subscribed = await isSubscribed(orgId, tierMatrix.loginPageDomain); // this is fine because the org login page is only a saas feature
|
||||||
if (subscribed) {
|
if (subscribed) {
|
||||||
loginPage = await getOrgLoginPage(orgId);
|
loginPage = await getOrgLoginPage(orgId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToke
|
|||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
import stoi from "@server/lib/stoi";
|
import stoi from "@server/lib/stoi";
|
||||||
import { logAccessAudit } from "#dynamic/lib/logAccessAudit";
|
import { logAccessAudit } from "#dynamic/lib/logAccessAudit";
|
||||||
|
import { normalizePostAuthPath } from "@server/lib/normalizePostAuthPath";
|
||||||
|
|
||||||
const authWithAccessTokenBodySchema = z.strictObject({
|
const authWithAccessTokenBodySchema = z.strictObject({
|
||||||
accessToken: z.string(),
|
accessToken: z.string(),
|
||||||
@@ -164,10 +165,16 @@ export async function authWithAccessToken(
|
|||||||
requestIp: req.ip
|
requestIp: req.ip
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let redirectUrl = `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`;
|
||||||
|
const postAuthPath = normalizePostAuthPath(resource.postAuthPath);
|
||||||
|
if (postAuthPath) {
|
||||||
|
redirectUrl = redirectUrl + postAuthPath;
|
||||||
|
}
|
||||||
|
|
||||||
return response<AuthWithAccessTokenResponse>(res, {
|
return response<AuthWithAccessTokenResponse>(res, {
|
||||||
data: {
|
data: {
|
||||||
session: token,
|
session: token,
|
||||||
redirectUrl: `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`
|
redirectUrl
|
||||||
},
|
},
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ const createHttpResourceSchema = z
|
|||||||
http: z.boolean(),
|
http: z.boolean(),
|
||||||
protocol: z.enum(["tcp", "udp"]),
|
protocol: z.enum(["tcp", "udp"]),
|
||||||
domainId: z.string(),
|
domainId: z.string(),
|
||||||
stickySession: z.boolean().optional()
|
stickySession: z.boolean().optional(),
|
||||||
|
postAuthPath: z.string().nullable().optional()
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
@@ -188,7 +189,7 @@ async function createHttpResource(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { name, domainId } = parsedBody.data;
|
const { name, domainId, postAuthPath } = parsedBody.data;
|
||||||
const subdomain = parsedBody.data.subdomain;
|
const subdomain = parsedBody.data.subdomain;
|
||||||
const stickySession = parsedBody.data.stickySession;
|
const stickySession = parsedBody.data.stickySession;
|
||||||
|
|
||||||
@@ -255,7 +256,8 @@ async function createHttpResource(
|
|||||||
http: true,
|
http: true,
|
||||||
protocol: "tcp",
|
protocol: "tcp",
|
||||||
ssl: true,
|
ssl: true,
|
||||||
stickySession: stickySession
|
stickySession: stickySession,
|
||||||
|
postAuthPath: postAuthPath
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export type GetResourceAuthInfoResponse = {
|
|||||||
whitelist: boolean;
|
whitelist: boolean;
|
||||||
skipToIdpId: number | null;
|
skipToIdpId: number | null;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
|
postAuthPath: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getResourceAuthInfo(
|
export async function getResourceAuthInfo(
|
||||||
@@ -147,7 +148,8 @@ export async function getResourceAuthInfo(
|
|||||||
url,
|
url,
|
||||||
whitelist: resource.emailWhitelistEnabled,
|
whitelist: resource.emailWhitelistEnabled,
|
||||||
skipToIdpId: resource.skipToIdpId,
|
skipToIdpId: resource.skipToIdpId,
|
||||||
orgId: resource.orgId
|
orgId: resource.orgId,
|
||||||
|
postAuthPath: resource.postAuthPath ?? null
|
||||||
},
|
},
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
|
|||||||
@@ -55,7 +55,8 @@ const updateHttpResourceBodySchema = z
|
|||||||
maintenanceModeType: z.enum(["forced", "automatic"]).optional(),
|
maintenanceModeType: z.enum(["forced", "automatic"]).optional(),
|
||||||
maintenanceTitle: z.string().max(255).nullable().optional(),
|
maintenanceTitle: z.string().max(255).nullable().optional(),
|
||||||
maintenanceMessage: z.string().max(2000).nullable().optional(),
|
maintenanceMessage: z.string().max(2000).nullable().optional(),
|
||||||
maintenanceEstimatedTime: z.string().max(100).nullable().optional()
|
maintenanceEstimatedTime: z.string().max(100).nullable().optional(),
|
||||||
|
postAuthPath: z.string().nullable().optional()
|
||||||
})
|
})
|
||||||
.refine((data) => Object.keys(data).length > 0, {
|
.refine((data) => Object.keys(data).length > 0, {
|
||||||
error: "At least one field must be provided for update"
|
error: "At least one field must be provided for update"
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import m10 from "./scriptsPg/1.13.0";
|
|||||||
import m11 from "./scriptsPg/1.14.0";
|
import m11 from "./scriptsPg/1.14.0";
|
||||||
import m12 from "./scriptsPg/1.15.0";
|
import m12 from "./scriptsPg/1.15.0";
|
||||||
import m13 from "./scriptsPg/1.15.3";
|
import m13 from "./scriptsPg/1.15.3";
|
||||||
|
import m14 from "./scriptsPg/1.15.4";
|
||||||
|
|
||||||
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
||||||
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
||||||
@@ -36,7 +37,8 @@ const migrations = [
|
|||||||
{ version: "1.13.0", run: m10 },
|
{ version: "1.13.0", run: m10 },
|
||||||
{ version: "1.14.0", run: m11 },
|
{ version: "1.14.0", run: m11 },
|
||||||
{ version: "1.15.0", run: m12 },
|
{ version: "1.15.0", run: m12 },
|
||||||
{ version: "1.15.3", run: m13 }
|
{ version: "1.15.3", run: m13 },
|
||||||
|
{ version: "1.15.4", run: m14 }
|
||||||
// Add new migrations here as they are created
|
// Add new migrations here as they are created
|
||||||
] as {
|
] as {
|
||||||
version: string;
|
version: string;
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import m31 from "./scriptsSqlite/1.13.0";
|
|||||||
import m32 from "./scriptsSqlite/1.14.0";
|
import m32 from "./scriptsSqlite/1.14.0";
|
||||||
import m33 from "./scriptsSqlite/1.15.0";
|
import m33 from "./scriptsSqlite/1.15.0";
|
||||||
import m34 from "./scriptsSqlite/1.15.3";
|
import m34 from "./scriptsSqlite/1.15.3";
|
||||||
|
import m35 from "./scriptsSqlite/1.15.4";
|
||||||
|
|
||||||
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
||||||
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
||||||
@@ -70,7 +71,8 @@ const migrations = [
|
|||||||
{ version: "1.13.0", run: m31 },
|
{ version: "1.13.0", run: m31 },
|
||||||
{ version: "1.14.0", run: m32 },
|
{ version: "1.14.0", run: m32 },
|
||||||
{ version: "1.15.0", run: m33 },
|
{ version: "1.15.0", run: m33 },
|
||||||
{ version: "1.15.3", run: m34 }
|
{ version: "1.15.3", run: m34 },
|
||||||
|
{ version: "1.15.4", run: m35 }
|
||||||
// Add new migrations here as they are created
|
// Add new migrations here as they are created
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { db } from "@server/db/pg/driver";
|
||||||
|
import { sql } from "drizzle-orm";
|
||||||
|
import { __DIRNAME } from "@server/lib/consts";
|
||||||
|
|
||||||
|
const version = "1.15.4";
|
||||||
|
|
||||||
|
export default async function migration() {
|
||||||
|
console.log(`Running setup script ${version}...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db.execute(sql`BEGIN`);
|
||||||
|
|
||||||
|
await db.execute(
|
||||||
|
sql`ALTER TABLE "resources" ADD COLUMN "postAuthPath" text;`
|
||||||
|
);
|
||||||
|
|
||||||
|
await db.execute(sql`COMMIT`);
|
||||||
|
console.log("Migrated database");
|
||||||
|
} catch (e) {
|
||||||
|
await db.execute(sql`ROLLBACK`);
|
||||||
|
console.log("Unable to migrate database");
|
||||||
|
console.log(e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`${version} migration complete`);
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { __DIRNAME, APP_PATH } from "@server/lib/consts";
|
||||||
|
import Database from "better-sqlite3";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
const version = "1.15.4";
|
||||||
|
|
||||||
|
export default async function migration() {
|
||||||
|
console.log(`Running setup script ${version}...`);
|
||||||
|
|
||||||
|
const location = path.join(APP_PATH, "db", "db.sqlite");
|
||||||
|
const db = new Database(location);
|
||||||
|
|
||||||
|
try {
|
||||||
|
db.transaction(() => {
|
||||||
|
db.prepare(
|
||||||
|
`ALTER TABLE 'resources' ADD 'postAuthPath' text;`
|
||||||
|
).run();
|
||||||
|
})();
|
||||||
|
|
||||||
|
console.log(`Migrated database`);
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Failed to migrate db:", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`${version} migration complete`);
|
||||||
|
}
|
||||||
@@ -61,7 +61,7 @@ import {
|
|||||||
import { FeatureId } from "@server/lib/billing/features";
|
import { FeatureId } from "@server/lib/billing/features";
|
||||||
|
|
||||||
// Plan tier definitions matching the mockup
|
// Plan tier definitions matching the mockup
|
||||||
type PlanId = "starter" | "home" | "team" | "business" | "enterprise";
|
type PlanId = "basic" | "home" | "team" | "business" | "enterprise";
|
||||||
|
|
||||||
type PlanOption = {
|
type PlanOption = {
|
||||||
id: PlanId;
|
id: PlanId;
|
||||||
@@ -73,8 +73,8 @@ type PlanOption = {
|
|||||||
|
|
||||||
const planOptions: PlanOption[] = [
|
const planOptions: PlanOption[] = [
|
||||||
{
|
{
|
||||||
id: "starter",
|
id: "basic",
|
||||||
name: "Starter",
|
name: "Basic",
|
||||||
price: "Free",
|
price: "Free",
|
||||||
tierType: null
|
tierType: null
|
||||||
},
|
},
|
||||||
@@ -109,10 +109,10 @@ const planOptions: PlanOption[] = [
|
|||||||
|
|
||||||
// Tier limits mapping derived from limit sets
|
// Tier limits mapping derived from limit sets
|
||||||
const tierLimits: Record<
|
const tierLimits: Record<
|
||||||
Tier | "starter",
|
Tier | "basic",
|
||||||
{ users: number; sites: number; domains: number; remoteNodes: number }
|
{ users: number; sites: number; domains: number; remoteNodes: number }
|
||||||
> = {
|
> = {
|
||||||
starter: {
|
basic: {
|
||||||
users: freeLimitSet[FeatureId.USERS]?.value ?? 0,
|
users: freeLimitSet[FeatureId.USERS]?.value ?? 0,
|
||||||
sites: freeLimitSet[FeatureId.SITES]?.value ?? 0,
|
sites: freeLimitSet[FeatureId.SITES]?.value ?? 0,
|
||||||
domains: freeLimitSet[FeatureId.DOMAINS]?.value ?? 0,
|
domains: freeLimitSet[FeatureId.DOMAINS]?.value ?? 0,
|
||||||
@@ -183,7 +183,7 @@ export default function BillingPage() {
|
|||||||
// Confirmation dialog state
|
// Confirmation dialog state
|
||||||
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
||||||
const [pendingTier, setPendingTier] = useState<{
|
const [pendingTier, setPendingTier] = useState<{
|
||||||
tier: Tier | "starter";
|
tier: Tier | "basic";
|
||||||
action: "upgrade" | "downgrade";
|
action: "upgrade" | "downgrade";
|
||||||
planName: string;
|
planName: string;
|
||||||
price: string;
|
price: string;
|
||||||
@@ -402,8 +402,8 @@ export default function BillingPage() {
|
|||||||
pendingTier.action === "upgrade" ||
|
pendingTier.action === "upgrade" ||
|
||||||
pendingTier.action === "downgrade"
|
pendingTier.action === "downgrade"
|
||||||
) {
|
) {
|
||||||
// If downgrading to starter (free tier), go to Stripe portal
|
// If downgrading to basic (free tier), go to Stripe portal
|
||||||
if (pendingTier.tier === "starter") {
|
if (pendingTier.tier === "basic") {
|
||||||
handleModifySubscription();
|
handleModifySubscription();
|
||||||
} else if (hasSubscription) {
|
} else if (hasSubscription) {
|
||||||
handleChangeTier(pendingTier.tier);
|
handleChangeTier(pendingTier.tier);
|
||||||
@@ -417,7 +417,7 @@ export default function BillingPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const showTierConfirmation = (
|
const showTierConfirmation = (
|
||||||
tier: Tier | "starter",
|
tier: Tier | "basic",
|
||||||
action: "upgrade" | "downgrade",
|
action: "upgrade" | "downgrade",
|
||||||
planName: string,
|
planName: string,
|
||||||
price: string
|
price: string
|
||||||
@@ -432,9 +432,9 @@ export default function BillingPage() {
|
|||||||
|
|
||||||
// Get current plan ID from tier
|
// Get current plan ID from tier
|
||||||
const getCurrentPlanId = (): PlanId => {
|
const getCurrentPlanId = (): PlanId => {
|
||||||
if (!hasSubscription || !currentTier) return "starter";
|
if (!hasSubscription || !currentTier) return "basic";
|
||||||
const plan = planOptions.find((p) => p.tierType === currentTier);
|
const plan = planOptions.find((p) => p.tierType === currentTier);
|
||||||
return plan?.id || "starter";
|
return plan?.id || "basic";
|
||||||
};
|
};
|
||||||
|
|
||||||
const currentPlanId = getCurrentPlanId();
|
const currentPlanId = getCurrentPlanId();
|
||||||
@@ -451,8 +451,8 @@ export default function BillingPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (plan.id === currentPlanId) {
|
if (plan.id === currentPlanId) {
|
||||||
// If it's the starter plan (starter with no subscription), show as current but disabled
|
// If it's the basic plan (basic with no subscription), show as current but disabled
|
||||||
if (plan.id === "starter" && !hasSubscription) {
|
if (plan.id === "basic" && !hasSubscription) {
|
||||||
return {
|
return {
|
||||||
label: "Current Plan",
|
label: "Current Plan",
|
||||||
action: () => {},
|
action: () => {},
|
||||||
@@ -484,10 +484,10 @@ export default function BillingPage() {
|
|||||||
plan.name,
|
plan.name,
|
||||||
plan.price + (" " + plan.priceDetail || "")
|
plan.price + (" " + plan.priceDetail || "")
|
||||||
);
|
);
|
||||||
} else if (plan.id === "starter") {
|
} else if (plan.id === "basic") {
|
||||||
// Show confirmation for downgrading to starter (free tier)
|
// Show confirmation for downgrading to basic (free tier)
|
||||||
showTierConfirmation(
|
showTierConfirmation(
|
||||||
"starter",
|
"basic",
|
||||||
"downgrade",
|
"downgrade",
|
||||||
plan.name,
|
plan.name,
|
||||||
plan.price
|
plan.price
|
||||||
@@ -566,7 +566,7 @@ export default function BillingPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Check if downgrading to a tier would violate current usage limits
|
// Check if downgrading to a tier would violate current usage limits
|
||||||
const checkLimitViolations = (targetTier: Tier | "starter"): Array<{
|
const checkLimitViolations = (targetTier: Tier | "basic"): Array<{
|
||||||
feature: string;
|
feature: string;
|
||||||
currentUsage: number;
|
currentUsage: number;
|
||||||
newLimit: number;
|
newLimit: number;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import type {
|
|||||||
import { CheckOrgUserAccessResponse } from "@server/routers/org";
|
import { CheckOrgUserAccessResponse } from "@server/routers/org";
|
||||||
import OrgPolicyRequired from "@app/components/OrgPolicyRequired";
|
import OrgPolicyRequired from "@app/components/OrgPolicyRequired";
|
||||||
import { isOrgSubscribed } from "@app/lib/api/isOrgSubscribed";
|
import { isOrgSubscribed } from "@app/lib/api/isOrgSubscribed";
|
||||||
|
import { normalizePostAuthPath } from "@server/lib/normalizePostAuthPath";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
@@ -108,6 +109,11 @@ export default async function ResourceAuthPage(props: {
|
|||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalizedPostAuthPath = normalizePostAuthPath(authInfo.postAuthPath);
|
||||||
|
if (normalizedPostAuthPath) {
|
||||||
|
redirectUrl = new URL(authInfo.url).origin + normalizedPostAuthPath;
|
||||||
|
}
|
||||||
|
|
||||||
const hasAuth =
|
const hasAuth =
|
||||||
authInfo.password ||
|
authInfo.password ||
|
||||||
authInfo.pincode ||
|
authInfo.pincode ||
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export const MachineClientsBanner = ({ orgId }: MachineClientsBannerProps) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href="https://docs.pangolin.net/manage/clients/install-client#docker"
|
href="https://docs.pangolin.net/manage/clients/install-client#docker-pangolin-cli"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ export type CommandItem = string | { title: string; command: string };
|
|||||||
|
|
||||||
const PLATFORMS = [
|
const PLATFORMS = [
|
||||||
"unix",
|
"unix",
|
||||||
"windows",
|
|
||||||
"docker",
|
"docker",
|
||||||
"kubernetes",
|
"kubernetes",
|
||||||
"podman",
|
"podman",
|
||||||
"nixos"
|
"nixos",
|
||||||
|
"windows"
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
type Platform = (typeof PLATFORMS)[number];
|
type Platform = (typeof PLATFORMS)[number];
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { Button } from "./ui/button";
|
|||||||
|
|
||||||
export type CommandItem = string | { title: string; command: string };
|
export type CommandItem = string | { title: string; command: string };
|
||||||
|
|
||||||
const PLATFORMS = ["unix", "windows", "docker"] as const;
|
const PLATFORMS = ["unix", "docker", "windows"] as const;
|
||||||
|
|
||||||
type Platform = (typeof PLATFORMS)[number];
|
type Platform = (typeof PLATFORMS)[number];
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ export function OlmInstallCommands({
|
|||||||
All: [
|
All: [
|
||||||
{
|
{
|
||||||
title: t("install"),
|
title: t("install"),
|
||||||
command: `curl -fsSL https://static.pangolin.net/get-cli.sh | bash`
|
command: `curl -fsSL https://static.pangolin.net/get-cli.sh | sudo bash`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("run"),
|
title: t("run"),
|
||||||
@@ -51,24 +51,12 @@ export function OlmInstallCommands({
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
windows: {
|
|
||||||
x64: [
|
|
||||||
{
|
|
||||||
title: t("install"),
|
|
||||||
command: `curl -o olm.exe -L "https://github.com/fosrl/olm/releases/download/${version}/olm_windows_installer.exe"`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("run"),
|
|
||||||
command: `olm.exe --id ${id} --secret ${secret} --endpoint ${endpoint}`
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
docker: {
|
docker: {
|
||||||
"Docker Compose": [
|
"Docker Compose": [
|
||||||
`services:
|
`services:
|
||||||
olm:
|
pangolin-cli:
|
||||||
image: fosrl/olm
|
image: fosrl/pangolin-cli
|
||||||
container_name: olm
|
container_name: pangolin-cli
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
network_mode: host
|
network_mode: host
|
||||||
cap_add:
|
cap_add:
|
||||||
@@ -77,11 +65,24 @@ export function OlmInstallCommands({
|
|||||||
- /dev/net/tun:/dev/net/tun
|
- /dev/net/tun:/dev/net/tun
|
||||||
environment:
|
environment:
|
||||||
- PANGOLIN_ENDPOINT=${endpoint}
|
- PANGOLIN_ENDPOINT=${endpoint}
|
||||||
- OLM_ID=${id}
|
- CLIENT_ID=${id}
|
||||||
- OLM_SECRET=${secret}`
|
- CLIENT_SECRET=${secret}`
|
||||||
],
|
],
|
||||||
"Docker Run": [
|
"Docker Run": [
|
||||||
`docker run -dit --network host --cap-add NET_ADMIN --device /dev/net/tun:/dev/net/tun fosrl/olm --id ${id} --secret ${secret} --endpoint ${endpoint}`
|
`docker run -dit --network host --cap-add NET_ADMIN --device /dev/net/tun:/dev/net/tun fosrl/pangolin-cli up client --id ${id} --secret ${secret} --endpoint ${endpoint} --attach`
|
||||||
|
]
|
||||||
|
},
|
||||||
|
windows: {
|
||||||
|
x64: [
|
||||||
|
{
|
||||||
|
title: t("install"),
|
||||||
|
command: `# Download and run the installer to install Olm first\n
|
||||||
|
curl -o olm.exe -L "https://github.com/fosrl/olm/releases/download/${version}/olm_windows_installer.exe"`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("run"),
|
||||||
|
command: `olm.exe --id ${id} --secret ${secret} --endpoint ${endpoint}`
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user