mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-10 20:02:26 +00:00
Merge branch 'dev' into feat-blueprint-ui-on-dashboard
This commit is contained in:
2
.github/workflows/cicd.yml
vendored
2
.github/workflows/cicd.yml
vendored
@@ -99,7 +99,7 @@ jobs:
|
||||
make go-build-release
|
||||
|
||||
- name: Upload artifacts from /install/bin
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: install-bin
|
||||
path: install/bin/
|
||||
|
||||
@@ -45,7 +45,10 @@ Pangolin is a self-hosted tunneled reverse proxy server with identity and contex
|
||||
|
||||
## Installation
|
||||
|
||||
Check out the [quick install guide](https://docs.pangolin.net/self-host/quick-install) for how to install and set up Pangolin.
|
||||
- Check out the [quick install guide](https://docs.pangolin.net/self-host/quick-install) for how to install and set up Pangolin.
|
||||
- Install from the [DigitalOcean marketplace](https://marketplace.digitalocean.com/apps/pangolin-ce-1?refcode=edf0480eeb81) for a one-click pre-configured installer.
|
||||
|
||||
<img src="public/screenshots/hero.png" />
|
||||
|
||||
## Deployment Options
|
||||
|
||||
|
||||
@@ -1813,6 +1813,7 @@
|
||||
"securityPolicyChangeConfirmMessage": "I confirm",
|
||||
"securityPolicyChangeWarningText": "This will affect all users in the organization",
|
||||
"authPageErrorUpdateMessage": "An error occurred while updating the auth page settings",
|
||||
"authPageErrorUpdate": "Unable to update auth page",
|
||||
"authPageUpdated": "Auth page updated successfully",
|
||||
"healthCheckNotAvailable": "Local",
|
||||
"rewritePath": "Rewrite Path",
|
||||
|
||||
607
package-lock.json
generated
607
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
32
package.json
32
package.json
@@ -67,7 +67,7 @@
|
||||
"arctic": "^3.7.0",
|
||||
"axios": "^1.12.2",
|
||||
"better-sqlite3": "11.7.0",
|
||||
"canvas-confetti": "1.9.3",
|
||||
"canvas-confetti": "1.9.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "2.1.1",
|
||||
"cmdk": "1.1.1",
|
||||
@@ -77,7 +77,7 @@
|
||||
"cors": "2.8.5",
|
||||
"crypto-js": "^4.2.0",
|
||||
"date-fns": "4.1.0",
|
||||
"drizzle-orm": "0.44.6",
|
||||
"drizzle-orm": "0.44.7",
|
||||
"eslint": "9.37.0",
|
||||
"eslint-config-next": "15.5.6",
|
||||
"express": "5.1.0",
|
||||
@@ -87,7 +87,7 @@
|
||||
"http-errors": "2.0.0",
|
||||
"i": "^0.3.7",
|
||||
"input-otp": "1.4.2",
|
||||
"ioredis": "5.8.1",
|
||||
"ioredis": "5.8.2",
|
||||
"jmespath": "^0.16.0",
|
||||
"js-yaml": "4.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
@@ -100,12 +100,12 @@
|
||||
"nextjs-toploader": "^3.9.17",
|
||||
"node-cache": "5.1.2",
|
||||
"node-fetch": "3.3.2",
|
||||
"nodemailer": "7.0.9",
|
||||
"nodemailer": "7.0.10",
|
||||
"npm": "^11.6.2",
|
||||
"nprogress": "^0.2.0",
|
||||
"oslo": "1.2.1",
|
||||
"pg": "^8.16.2",
|
||||
"posthog-node": "^5.9.5",
|
||||
"posthog-node": "^5.10.4",
|
||||
"qrcode.react": "4.2.0",
|
||||
"react": "19.2.0",
|
||||
"react-day-picker": "9.11.1",
|
||||
@@ -135,37 +135,37 @@
|
||||
"@dotenvx/dotenvx": "1.51.0",
|
||||
"@esbuild-plugins/tsconfig-paths": "0.1.2",
|
||||
"@faker-js/faker": "^10.1.0",
|
||||
"@react-email/preview-server": "4.3.1",
|
||||
"@tailwindcss/postcss": "^4.1.14",
|
||||
"@react-email/preview-server": "4.3.2",
|
||||
"@tailwindcss/postcss": "^4.1.16",
|
||||
"@types/better-sqlite3": "7.6.12",
|
||||
"@types/cookie-parser": "1.4.9",
|
||||
"@types/cookie-parser": "1.4.10",
|
||||
"@types/cors": "2.8.19",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/express": "5.0.3",
|
||||
"@types/express": "5.0.5",
|
||||
"@types/express-session": "^1.18.2",
|
||||
"@types/jmespath": "^0.15.2",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/node": "24.8.1",
|
||||
"@types/nodemailer": "7.0.2",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@types/pg": "8.15.5",
|
||||
"@types/node": "24.9.2",
|
||||
"@types/nodemailer": "7.0.3",
|
||||
"@types/pg": "8.15.6",
|
||||
"@types/react": "19.2.2",
|
||||
"@types/react-dom": "19.2.2",
|
||||
"@types/semver": "^7.7.1",
|
||||
"@types/swagger-ui-express": "^4.1.8",
|
||||
"@types/ws": "8.18.1",
|
||||
"@types/yargs": "17.0.33",
|
||||
"drizzle-kit": "0.31.5",
|
||||
"@types/yargs": "17.0.34",
|
||||
"drizzle-kit": "0.31.6",
|
||||
"esbuild": "0.25.11",
|
||||
"esbuild-node-externals": "1.18.0",
|
||||
"postcss": "^8",
|
||||
"react-email": "4.3.1",
|
||||
"react-email": "4.3.2",
|
||||
"tailwindcss": "^4.1.4",
|
||||
"tsc-alias": "1.8.16",
|
||||
"tsx": "4.20.6",
|
||||
"typescript": "^5",
|
||||
"typescript-eslint": "^8.46.0"
|
||||
"typescript-eslint": "^8.46.2"
|
||||
},
|
||||
"overrides": {
|
||||
"emblor": {
|
||||
|
||||
@@ -204,7 +204,8 @@ export const configSchema = z
|
||||
.optional()
|
||||
.default(["newt", "wireguard", "local"]),
|
||||
allow_raw_resources: z.boolean().optional().default(true),
|
||||
file_mode: z.boolean().optional().default(false)
|
||||
file_mode: z.boolean().optional().default(false),
|
||||
pp_transport_prefix: z.string().optional().default("pp-transport-v")
|
||||
})
|
||||
.optional()
|
||||
.default({}),
|
||||
|
||||
@@ -296,7 +296,6 @@ export async function getTraefikConfig(
|
||||
: {})
|
||||
};
|
||||
|
||||
|
||||
const additionalMiddlewares =
|
||||
config.getRawConfig().traefik.additional_middlewares || [];
|
||||
|
||||
@@ -587,6 +586,8 @@ export async function getTraefikConfig(
|
||||
...(protocol === "tcp" ? { rule: "HostSNI(`*`)" } : {})
|
||||
};
|
||||
|
||||
const ppPrefix = config.getRawConfig().traefik.pp_transport_prefix;
|
||||
|
||||
config_output[protocol].services[serviceName] = {
|
||||
loadBalancer: {
|
||||
servers: (() => {
|
||||
@@ -642,7 +643,7 @@ export async function getTraefikConfig(
|
||||
})(),
|
||||
...(resource.proxyProtocol && protocol == "tcp"
|
||||
? {
|
||||
serversTransport: `pp-transport-v${resource.proxyProtocolVersion || 1}`
|
||||
serversTransport: `${ppPrefix}${resource.proxyProtocolVersion || 1}@file` // TODO: does @file here cause issues?
|
||||
}
|
||||
: {}),
|
||||
...(resource.stickySession
|
||||
|
||||
@@ -61,7 +61,7 @@ export async function cleanUpOldLogs(orgId: string, retentionDays: number) {
|
||||
)
|
||||
);
|
||||
|
||||
logger.info(
|
||||
logger.debug(
|
||||
`Cleaned up ${deleteResult.changes} access audit logs older than ${retentionDays} days`
|
||||
);
|
||||
} catch (error) {
|
||||
|
||||
@@ -109,7 +109,7 @@ export async function getTraefikConfig(
|
||||
domainNamespaceId: domainNamespaces.domainNamespaceId,
|
||||
// Certificate
|
||||
certificateStatus: certificates.status,
|
||||
domainCertResolver: domains.certResolver,
|
||||
domainCertResolver: domains.certResolver
|
||||
})
|
||||
.from(sites)
|
||||
.innerJoin(targets, eq(targets.siteId, sites.siteId))
|
||||
@@ -214,7 +214,7 @@ export async function getTraefikConfig(
|
||||
rewritePath: row.rewritePath,
|
||||
rewritePathType: row.rewritePathType,
|
||||
priority: priority, // may be null, we fallback later
|
||||
domainCertResolver: row.domainCertResolver,
|
||||
domainCertResolver: row.domainCertResolver
|
||||
});
|
||||
}
|
||||
|
||||
@@ -330,29 +330,43 @@ export async function getTraefikConfig(
|
||||
wildCard = resource.fullDomain;
|
||||
}
|
||||
|
||||
const configDomain = config.getDomain(resource.domainId);
|
||||
|
||||
let certResolver: string, preferWildcardCert: boolean;
|
||||
if (!configDomain) {
|
||||
certResolver = config.getRawConfig().traefik.cert_resolver;
|
||||
preferWildcardCert =
|
||||
const globalDefaultResolver =
|
||||
config.getRawConfig().traefik.cert_resolver;
|
||||
const globalDefaultPreferWildcard =
|
||||
config.getRawConfig().traefik.prefer_wildcard_cert;
|
||||
|
||||
const domainCertResolver = resource.domainCertResolver;
|
||||
const preferWildcardCert = resource.preferWildcardCert;
|
||||
|
||||
let resolverName: string | undefined;
|
||||
let preferWildcard: boolean | undefined;
|
||||
// Handle both letsencrypt & custom cases
|
||||
if (domainCertResolver) {
|
||||
resolverName = domainCertResolver.trim();
|
||||
} else {
|
||||
certResolver = configDomain.cert_resolver;
|
||||
preferWildcardCert = configDomain.prefer_wildcard_cert;
|
||||
resolverName = globalDefaultResolver;
|
||||
}
|
||||
|
||||
if (
|
||||
preferWildcardCert !== undefined &&
|
||||
preferWildcardCert !== null
|
||||
) {
|
||||
preferWildcard = preferWildcardCert;
|
||||
} else {
|
||||
preferWildcard = globalDefaultPreferWildcard;
|
||||
}
|
||||
|
||||
tls = {
|
||||
certResolver: certResolver,
|
||||
...(preferWildcardCert
|
||||
certResolver: resolverName,
|
||||
...(preferWildcard
|
||||
? {
|
||||
domains: [
|
||||
{
|
||||
main: wildCard,
|
||||
},
|
||||
],
|
||||
main: wildCard
|
||||
}
|
||||
: {}),
|
||||
]
|
||||
}
|
||||
: {})
|
||||
};
|
||||
} else {
|
||||
// find a cert that matches the full domain, if not continue
|
||||
@@ -657,6 +671,8 @@ export async function getTraefikConfig(
|
||||
...(protocol === "tcp" ? { rule: "HostSNI(`*`)" } : {})
|
||||
};
|
||||
|
||||
const ppPrefix = config.getRawConfig().traefik.pp_transport_prefix;
|
||||
|
||||
config_output[protocol].services[serviceName] = {
|
||||
loadBalancer: {
|
||||
servers: (() => {
|
||||
@@ -712,7 +728,7 @@ export async function getTraefikConfig(
|
||||
})(),
|
||||
...(resource.proxyProtocol && protocol == "tcp" // proxy protocol only works for tcp
|
||||
? {
|
||||
serversTransport: `pp-transport-v${resource.proxyProtocolVersion || 1}`
|
||||
serversTransport: `${ppPrefix}${resource.proxyProtocolVersion || 1}@file` // TODO: does @file here cause issues?
|
||||
}
|
||||
: {}),
|
||||
...(resource.stickySession
|
||||
@@ -771,7 +787,8 @@ export async function getTraefikConfig(
|
||||
loadBalancer: {
|
||||
servers: [
|
||||
{
|
||||
url: `http://${config.getRawConfig().server
|
||||
url: `http://${
|
||||
config.getRawConfig().server
|
||||
.internal_hostname
|
||||
}:${config.getRawConfig().server.next_port}`
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ export async function cleanUpOldLogs(orgId: string, retentionDays: number) {
|
||||
)
|
||||
);
|
||||
|
||||
logger.info(
|
||||
logger.debug(
|
||||
`Cleaned up ${deleteResult.changes} action audit logs older than ${retentionDays} days`
|
||||
);
|
||||
} catch (error) {
|
||||
|
||||
@@ -69,7 +69,7 @@ export async function cleanUpOldLogs(orgId: string, retentionDays: number) {
|
||||
)
|
||||
);
|
||||
|
||||
logger.info(
|
||||
logger.debug(
|
||||
`Cleaned up ${deleteResult.changes} request audit logs older than ${retentionDays} days`
|
||||
);
|
||||
} catch (error) {
|
||||
|
||||
@@ -107,6 +107,26 @@ export default async function migration() {
|
||||
await db.execute(sql`ALTER TABLE "resources" ADD CONSTRAINT "resources_skipToIdpId_idp_idpId_fk" FOREIGN KEY ("skipToIdpId") REFERENCES "public"."idp"("idpId") ON DELETE set null ON UPDATE no action;`);
|
||||
await db.execute(sql`ALTER TABLE "orgs" DROP COLUMN "settings";`);
|
||||
|
||||
|
||||
// get all of the domains
|
||||
const domainsQuery = await db.execute(sql`SELECT "domainId", "baseDomain" FROM "domains"`);
|
||||
const domains = domainsQuery.rows as {
|
||||
domainId: string;
|
||||
baseDomain: string;
|
||||
}[];
|
||||
|
||||
for (const domain of domains) {
|
||||
// insert two records into the dnsRecords table for each domain
|
||||
await db.execute(sql`
|
||||
INSERT INTO "dnsRecords" ("domainId", "recordType", "baseDomain", "value", "verified")
|
||||
VALUES (${domain.domainId}, 'A', ${`*.${domain.baseDomain}`}, ${'Server IP Address'}, true)
|
||||
`);
|
||||
await db.execute(sql`
|
||||
INSERT INTO "dnsRecords" ("domainId", "recordType", "baseDomain", "value", "verified")
|
||||
VALUES (${domain.domainId}, 'A', ${domain.baseDomain}, ${'Server IP Address'}, true)
|
||||
`);
|
||||
}
|
||||
|
||||
await db.execute(sql`COMMIT`);
|
||||
console.log("Migrated database");
|
||||
} catch (e) {
|
||||
|
||||
@@ -15,7 +15,7 @@ export default async function migration() {
|
||||
|
||||
db.transaction(() => {
|
||||
db.prepare(
|
||||
`UPDATE 'resourceRules' SET 'match' = 'COUNTRY' WHERE 'match' = 'GEOIP'`
|
||||
`UPDATE 'resourceRules' SET match = 'COUNTRY' WHERE match = 'GEOIP'`
|
||||
).run();
|
||||
|
||||
db.prepare(
|
||||
@@ -195,6 +195,29 @@ export default async function migration() {
|
||||
db.prepare(
|
||||
`ALTER TABLE 'user' ADD 'lastPasswordChange' integer;`
|
||||
).run();
|
||||
|
||||
// get all of the domains
|
||||
const domains = db.prepare(`SELECT domainId, baseDomain from domains`).all() as {
|
||||
domainId: number;
|
||||
baseDomain: string;
|
||||
}[];
|
||||
|
||||
for (const domain of domains) {
|
||||
// insert two records into the dnsRecords table for each domain
|
||||
const insert = db.prepare(
|
||||
`INSERT INTO 'dnsRecords' (domainId, recordType, baseDomain, value, verified) VALUES (?, 'A', ?, ?, 1)`
|
||||
);
|
||||
insert.run(
|
||||
domain.domainId,
|
||||
`*.${domain.baseDomain}`,
|
||||
`Server IP Address`
|
||||
);
|
||||
insert.run(
|
||||
domain.domainId,
|
||||
`${domain.baseDomain}`,
|
||||
`Server IP Address`
|
||||
);
|
||||
}
|
||||
})();
|
||||
|
||||
db.pragma("foreign_keys = ON");
|
||||
|
||||
@@ -733,6 +733,10 @@ export default function ReverseProxyTargets(props: {
|
||||
setHttpsTlsLoading(true);
|
||||
setProxySettingsLoading(true);
|
||||
|
||||
for (const targetId of targetsToRemove) {
|
||||
await api.delete(`/target/${targetId}`);
|
||||
}
|
||||
|
||||
// Save targets
|
||||
for (const target of targets) {
|
||||
const data: any = {
|
||||
@@ -775,10 +779,6 @@ export default function ReverseProxyTargets(props: {
|
||||
}
|
||||
}
|
||||
|
||||
for (const targetId of targetsToRemove) {
|
||||
await api.delete(`/target/${targetId}`);
|
||||
}
|
||||
|
||||
if (resource.http) {
|
||||
// Gather all settings
|
||||
const stickySessionData = targetsSettingsForm.getValues();
|
||||
|
||||
@@ -126,9 +126,11 @@ export default function InviteUserForm({
|
||||
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
{t("type")}
|
||||
<span className="px-2 py-1 rounded-md bg-secondary"><CopyToClipboard text={string} /></span>
|
||||
{t("toConfirm")}
|
||||
<span>{t("type")}</span>
|
||||
<div className="px-2 py-1 rounded-md bg-secondary max-w-[250px] overflow-x-auto whitespace-nowrap scrollbar-thin scrollbar-thumb-muted scrollbar-track-transparent">
|
||||
<CopyToClipboard text={string} />
|
||||
</div>
|
||||
<span>{t("toConfirm")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user