mirror of
https://github.com/fosrl/pangolin.git
synced 2026-08-26 05:55:31 +02:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f3474dac98 | |||
| 72d2c79793 | |||
| 23764feb4f | |||
| d2809fbfd1 | |||
| 7319bf84f7 | |||
| 9ec9908ed7 | |||
| fd0a0818c1 | |||
| 929acc5b1c | |||
| 71348f45b2 | |||
| 21eb4d2876 | |||
| 18270381c1 | |||
| 8e938a2723 | |||
| 197f8f7ba5 | |||
| 10b528642d | |||
| 60d6fff085 | |||
| c664b3da91 | |||
| 492282e758 | |||
| 47f4aefc25 | |||
| 7c0ff9ede7 | |||
| 44e81ea979 | |||
| 56dc10330a | |||
| eb8ad6a181 | |||
| 4edd2e4d32 | |||
| 813c3abe54 | |||
| 048e4fc73c | |||
| 923371e5b4 | |||
| 81430ba3d3 | |||
| e4aaadc9f9 | |||
| d04740fede | |||
| 4677a0d501 | |||
| b4463f0e1a | |||
| b7c0669c38 | |||
| 152d2fb1d6 | |||
| d374b4f66e | |||
| cb3f0b49a8 | |||
| 192542629f | |||
| 84d5a4b86c |
@@ -77,7 +77,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
with:
|
||||
registry: docker.io
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
@@ -149,7 +149,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
with:
|
||||
registry: docker.io
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
@@ -204,7 +204,7 @@ jobs:
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
with:
|
||||
registry: docker.io
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
@@ -407,7 +407,7 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
- name: Login to GitHub Container Registry (for cosign)
|
||||
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10.4.0
|
||||
- uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0
|
||||
with:
|
||||
days-before-stale: 14
|
||||
days-before-close: 14
|
||||
|
||||
+2213
-788
File diff suppressed because it is too large
Load Diff
Generated
+324
-281
File diff suppressed because it is too large
Load Diff
+5
-5
@@ -94,12 +94,12 @@
|
||||
"input-otp": "1.4.2",
|
||||
"ioredis": "5.11.0",
|
||||
"jmespath": "0.16.0",
|
||||
"js-yaml": "4.3.0",
|
||||
"js-yaml": "4.3.1",
|
||||
"jsonwebtoken": "9.0.3",
|
||||
"lucide-react": "1.17.0",
|
||||
"maxmind": "5.0.6",
|
||||
"moment": "2.30.1",
|
||||
"next": "16.2.11",
|
||||
"next": "16.3.1",
|
||||
"next-intl": "4.13.0",
|
||||
"next-themes": "0.4.6",
|
||||
"nextjs-toploader": "3.9.17",
|
||||
@@ -139,7 +139,7 @@
|
||||
"devDependencies": {
|
||||
"@dotenvx/dotenvx": "1.69.1",
|
||||
"@esbuild-plugins/tsconfig-paths": "0.1.2",
|
||||
"@react-email/ui": "^6.5.0",
|
||||
"@react-email/ui": "^6.9.2",
|
||||
"@tailwindcss/postcss": "4.3.0",
|
||||
"@tanstack/react-query-devtools": "5.100.14",
|
||||
"@types/better-sqlite3": "7.6.13",
|
||||
@@ -170,7 +170,7 @@
|
||||
"esbuild-node-externals": "1.22.0",
|
||||
"eslint": "10.4.0",
|
||||
"eslint-config-next": "16.2.6",
|
||||
"postcss": "8.5.15",
|
||||
"postcss": "8.5.23",
|
||||
"prettier": "3.8.3",
|
||||
"react-email": "6.5.0",
|
||||
"tailwindcss": "4.3.0",
|
||||
@@ -182,6 +182,6 @@
|
||||
"overrides": {
|
||||
"esbuild": "0.28.0",
|
||||
"dompurify": "3.4.0",
|
||||
"postcss": "8.5.15"
|
||||
"postcss": "8.5.23"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
db,
|
||||
primaryDb,
|
||||
newts,
|
||||
blueprints,
|
||||
Blueprint,
|
||||
@@ -80,93 +81,103 @@ export async function applyBlueprint({
|
||||
trx,
|
||||
siteId
|
||||
);
|
||||
});
|
||||
|
||||
// We need to update the targets on the newts from the successfully updated information
|
||||
for (const result of publicResourcesResults) {
|
||||
for (const target of result.targetsToUpdate) {
|
||||
const [site] = await trx
|
||||
.select()
|
||||
.from(sites)
|
||||
.innerJoin(newts, eq(sites.siteId, newts.siteId))
|
||||
.where(
|
||||
and(
|
||||
eq(sites.siteId, target.siteId),
|
||||
eq(sites.orgId, orgId),
|
||||
eq(sites.type, "newt"),
|
||||
isNotNull(sites.pubKey)
|
||||
)
|
||||
// Push updates to newts/clients only after the transaction has
|
||||
// committed. Doing this while the transaction is still open can
|
||||
// race with the writes (e.g. newts requesting config before the
|
||||
// new targets/resources are actually visible), leaving them out
|
||||
// of sync until manually toggled.
|
||||
|
||||
// We need to update the targets on the newts from the successfully updated information
|
||||
for (const result of publicResourcesResults) {
|
||||
for (const target of result.targetsToUpdate) {
|
||||
// read from the primary: this determines whether/how we push
|
||||
// the just-created target to the newt, so a lagging replica
|
||||
// returning stale or missing data here would silently skip
|
||||
// the push
|
||||
const [site] = await primaryDb
|
||||
.select()
|
||||
.from(sites)
|
||||
.innerJoin(newts, eq(sites.siteId, newts.siteId))
|
||||
.where(
|
||||
and(
|
||||
eq(sites.siteId, target.siteId),
|
||||
eq(sites.orgId, orgId),
|
||||
eq(sites.type, "newt"),
|
||||
isNotNull(sites.pubKey)
|
||||
)
|
||||
.limit(1);
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (site) {
|
||||
logger.debug(
|
||||
`Updating target ${target.targetId} on site ${site.sites.siteId}`
|
||||
if (site) {
|
||||
logger.debug(
|
||||
`Updating target ${target.targetId} on site ${site.sites.siteId}`
|
||||
);
|
||||
|
||||
// see if you can find a matching target health check from the healthchecksToUpdate array
|
||||
const matchingHealthcheck =
|
||||
result.healthchecksToUpdate.find(
|
||||
(hc) => hc.targetId === target.targetId
|
||||
);
|
||||
|
||||
// see if you can find a matching target health check from the healthchecksToUpdate array
|
||||
const matchingHealthcheck =
|
||||
result.healthchecksToUpdate.find(
|
||||
(hc) => hc.targetId === target.targetId
|
||||
);
|
||||
|
||||
if (["http", "tcp", "udp"].includes(target.mode)) {
|
||||
await addProxyTargets(
|
||||
site.newt.newtId,
|
||||
[target],
|
||||
matchingHealthcheck
|
||||
? [matchingHealthcheck]
|
||||
: [],
|
||||
result.proxyResource.mode === "udp"
|
||||
? "udp"
|
||||
: "tcp",
|
||||
site.newt.version
|
||||
);
|
||||
} else if (
|
||||
["ssh", "rdp", "vnc"].includes(target.mode)
|
||||
) {
|
||||
await sendBrowserGatewayTargets(
|
||||
site.newt.newtId,
|
||||
[target],
|
||||
site.newt.version
|
||||
);
|
||||
}
|
||||
if (["http", "tcp", "udp"].includes(target.mode)) {
|
||||
await addProxyTargets(
|
||||
site.newt.newtId,
|
||||
[target],
|
||||
matchingHealthcheck
|
||||
? [matchingHealthcheck]
|
||||
: [],
|
||||
result.proxyResource.mode === "udp"
|
||||
? "udp"
|
||||
: "tcp",
|
||||
site.newt.version
|
||||
);
|
||||
} else if (
|
||||
["ssh", "rdp", "vnc"].includes(target.mode)
|
||||
) {
|
||||
await sendBrowserGatewayTargets(
|
||||
site.newt.newtId,
|
||||
[target],
|
||||
site.newt.version
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Successfully updated public resources for org ${orgId}: ${JSON.stringify(publicResourcesResults)}`
|
||||
);
|
||||
logger.debug(
|
||||
`Successfully updated public resources for org ${orgId}: ${JSON.stringify(publicResourcesResults)}`
|
||||
);
|
||||
|
||||
// We need to update the targets on the newts from the successfully updated information
|
||||
for (const result of privateResourcesResults) {
|
||||
rebuildClientAssociationsFromSiteResource(
|
||||
result.newSiteResource
|
||||
// We need to update the targets on the newts from the successfully updated information
|
||||
for (const result of privateResourcesResults) {
|
||||
rebuildClientAssociationsFromSiteResource(
|
||||
result.newSiteResource
|
||||
)
|
||||
.then(() =>
|
||||
waitForSiteResourceRebuildIdle(
|
||||
result.newSiteResource.siteResourceId
|
||||
)
|
||||
)
|
||||
.then(() =>
|
||||
waitForSiteResourceRebuildIdle(
|
||||
result.newSiteResource.siteResourceId
|
||||
)
|
||||
.then(() =>
|
||||
handleMessagingForUpdatedSiteResource(
|
||||
result.oldSiteResource,
|
||||
result.newSiteResource,
|
||||
result.oldSites.map((s) => s.siteId),
|
||||
result.newSites.map((s) => s.siteId)
|
||||
)
|
||||
.then(() =>
|
||||
handleMessagingForUpdatedSiteResource(
|
||||
result.oldSiteResource,
|
||||
result.newSiteResource,
|
||||
result.oldSites.map((s) => s.siteId),
|
||||
result.newSites.map((s) => s.siteId)
|
||||
)
|
||||
)
|
||||
.catch((e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild and handle messaging for site resource ${result.newSiteResource.siteResourceId}. Error: ${e}`
|
||||
);
|
||||
});
|
||||
}
|
||||
)
|
||||
.catch((e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild and handle messaging for site resource ${result.newSiteResource.siteResourceId}. Error: ${e}`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Successfully updated private resources for org ${orgId}: ${JSON.stringify(privateResourcesResults)}`
|
||||
);
|
||||
});
|
||||
logger.debug(
|
||||
`Successfully updated private resources for org ${orgId}: ${JSON.stringify(privateResourcesResults)}`
|
||||
);
|
||||
|
||||
blueprintSucceeded = true;
|
||||
blueprintMessage = "Blueprint applied successfully";
|
||||
|
||||
@@ -6,7 +6,7 @@ import HttpCode from "@server/types/HttpCode";
|
||||
import { response } from "@server/lib/response";
|
||||
import { db } from "@server/db";
|
||||
import { passwordResetTokens, users } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { alphabet, generateRandomString, sha256 } from "oslo/crypto";
|
||||
import { createDate } from "oslo";
|
||||
import logger from "@server/logger";
|
||||
@@ -49,7 +49,12 @@ export async function requestPasswordReset(
|
||||
const existingUser = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.email, email));
|
||||
.where(
|
||||
and(
|
||||
eq(users.email, email),
|
||||
eq(users.type, UserType.Internal)
|
||||
)
|
||||
);
|
||||
|
||||
if (!existingUser || !existingUser.length) {
|
||||
await randomDelay(2000);
|
||||
|
||||
@@ -345,6 +345,9 @@ export default async function migration() {
|
||||
await db.execute(
|
||||
sql`ALTER TABLE "virtualApiKeys" ADD CONSTRAINT "virtualApiKeys_createdByUserId_user_id_fk" FOREIGN KEY ("createdByUserId") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;`
|
||||
);
|
||||
await db.execute(
|
||||
sql`ALTER TABLE "eventStreamingDestinations" ADD "sendAISessionLogs" boolean DEFAULT false NOT NULL;`
|
||||
);
|
||||
await db.execute(
|
||||
sql`CREATE INDEX "idx_ai_budget_breach_events_budget_created" ON "aiBudgetBreachEvents" USING btree ("budgetId","createdAt");`
|
||||
);
|
||||
|
||||
@@ -402,6 +402,9 @@ export default async function migration() {
|
||||
db.prepare(
|
||||
`ALTER TABLE 'siteResources' ADD 'requiresExitNodeConnection' integer DEFAULT false NOT NULL;`
|
||||
).run();
|
||||
db.prepare(
|
||||
`ALTER TABLE 'eventStreamingDestinations' ADD 'sendAISessionLogs' integer DEFAULT false NOT NULL;`
|
||||
).run();
|
||||
|
||||
const insertRoleAction = db.prepare(`
|
||||
INSERT INTO 'roleActions' ("roleId", "actionId", "orgId")
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { assertEquals } from "@test/assert";
|
||||
import { detectLocale } from "./detectLocale";
|
||||
|
||||
function runTests() {
|
||||
assertEquals(
|
||||
detectLocale("zh-TW,zh;q=0.9,en-US;q=0.8,en;q=0.7,ja;q=0.6"),
|
||||
"zh-TW",
|
||||
"An exact regional match should take precedence over a language fallback"
|
||||
);
|
||||
assertEquals(
|
||||
detectLocale("ZH-tw"),
|
||||
"zh-TW",
|
||||
"Locale matching should be case-insensitive"
|
||||
);
|
||||
assertEquals(
|
||||
detectLocale(" zh-TW ; q=1 , zh-CN;q=0.8 "),
|
||||
"zh-TW",
|
||||
"Whitespace and quality parameters should not prevent an exact match"
|
||||
);
|
||||
assertEquals(
|
||||
detectLocale("zh-CN,zh-TW;q=0.9"),
|
||||
"zh-CN",
|
||||
"Simplified Chinese should still match exactly"
|
||||
);
|
||||
assertEquals(
|
||||
detectLocale("zh"),
|
||||
"zh-CN",
|
||||
"A generic Chinese preference should retain the existing fallback"
|
||||
);
|
||||
assertEquals(
|
||||
detectLocale("en-GB,en;q=0.9"),
|
||||
"en-US",
|
||||
"An unsupported region should fall back to a supported locale for the language"
|
||||
);
|
||||
assertEquals(
|
||||
detectLocale("ja-JP,zh-TW;q=0.9"),
|
||||
"zh-TW",
|
||||
"The next preference should be used when a language is unsupported"
|
||||
);
|
||||
assertEquals(
|
||||
detectLocale("zh-CN;q=0.5,zh-TW;q=0.9"),
|
||||
"zh-TW",
|
||||
"Preferences should be evaluated by quality"
|
||||
);
|
||||
assertEquals(
|
||||
detectLocale("zh-TW;q=0,fr-FR;q=0.8"),
|
||||
"fr-FR",
|
||||
"Locales with zero quality should be excluded"
|
||||
);
|
||||
assertEquals(
|
||||
detectLocale("*,de-DE;q=0.8"),
|
||||
"de-DE",
|
||||
"A wildcard should not obscure a supported preference"
|
||||
);
|
||||
assertEquals(
|
||||
detectLocale("ja-JP"),
|
||||
undefined,
|
||||
"An unsupported language should not match"
|
||||
);
|
||||
assertEquals(
|
||||
detectLocale(""),
|
||||
undefined,
|
||||
"An empty Accept-Language header should not match"
|
||||
);
|
||||
|
||||
console.log("All locale detection tests passed!");
|
||||
}
|
||||
|
||||
try {
|
||||
runTests();
|
||||
} catch (error) {
|
||||
console.error("Locale detection test failed:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Locale, locales } from "./config";
|
||||
|
||||
export function detectLocale(acceptLanguage: string): Locale | undefined {
|
||||
const browserLocales = acceptLanguage
|
||||
.split(",")
|
||||
.map((entry, index) => {
|
||||
const [locale, ...parameters] = entry.trim().split(";");
|
||||
const qualityParameter = parameters.find((parameter) =>
|
||||
parameter.trim().toLowerCase().startsWith("q=")
|
||||
);
|
||||
const quality = qualityParameter
|
||||
? Number(qualityParameter.trim().slice(2))
|
||||
: 1;
|
||||
|
||||
return {
|
||||
locale: locale.trim().toLowerCase(),
|
||||
quality,
|
||||
index
|
||||
};
|
||||
})
|
||||
.filter(
|
||||
({ locale, quality }) =>
|
||||
locale && locale !== "*" && quality > 0 && quality <= 1
|
||||
)
|
||||
.sort(
|
||||
(left, right) =>
|
||||
right.quality - left.quality || left.index - right.index
|
||||
);
|
||||
|
||||
for (const { locale: browserLocale } of browserLocales) {
|
||||
const exactMatch = locales.find(
|
||||
(locale) => locale.toLowerCase() === browserLocale
|
||||
);
|
||||
if (exactMatch) {
|
||||
return exactMatch;
|
||||
}
|
||||
|
||||
const browserLanguage = browserLocale.split("-")[0];
|
||||
const languageMatch = locales.find(
|
||||
(locale) => locale.split("-")[0].toLowerCase() === browserLanguage
|
||||
);
|
||||
if (languageMatch) {
|
||||
return languageMatch;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { Locale, defaultLocale, locales } from "@/i18n/config";
|
||||
import { detectLocale } from "@/i18n/detectLocale";
|
||||
import { internal } from "@app/lib/api";
|
||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||
|
||||
@@ -47,12 +48,7 @@ export async function getUserLocale(): Promise<Locale> {
|
||||
const acceptLang = headerList.get("accept-language");
|
||||
|
||||
if (acceptLang) {
|
||||
const browserLang = acceptLang.split(",")[0];
|
||||
const matched = locales.find((locale) =>
|
||||
browserLang
|
||||
.toLowerCase()
|
||||
.startsWith(locale.split("-")[0].toLowerCase())
|
||||
);
|
||||
const matched = detectLocale(acceptLang);
|
||||
if (matched) {
|
||||
return matched;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user