mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-04 09:46:40 +00:00
Compare commits
18 Commits
1.14.0-rc.
...
1.14.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e9f18bf24 | ||
|
|
ab3be26790 | ||
|
|
5c67a1cb12 | ||
|
|
e28ab19ed4 | ||
|
|
59f8334cfd | ||
|
|
718bec4bbc | ||
|
|
2d731cb24b | ||
|
|
1905936950 | ||
|
|
c362bc673c | ||
|
|
4da0a752ef | ||
|
|
221ee6a1c2 | ||
|
|
2e60ecec87 | ||
|
|
71386d3b05 | ||
|
|
89a7e2e4dc | ||
|
|
27440700a5 | ||
|
|
b5019cef12 | ||
|
|
7e48cbe1aa | ||
|
|
4b2c570e73 |
11
.github/workflows/cicd.yml
vendored
11
.github/workflows/cicd.yml
vendored
@@ -99,7 +99,7 @@ jobs:
|
|||||||
id: check-rc
|
id: check-rc
|
||||||
run: |
|
run: |
|
||||||
TAG=${{ env.TAG }}
|
TAG=${{ env.TAG }}
|
||||||
if [[ "$TAG" == *".rc."* ]]; then
|
if [[ "$TAG" == *"-rc."* ]]; then
|
||||||
echo "IS_RC=true" >> $GITHUB_ENV
|
echo "IS_RC=true" >> $GITHUB_ENV
|
||||||
else
|
else
|
||||||
echo "IS_RC=false" >> $GITHUB_ENV
|
echo "IS_RC=false" >> $GITHUB_ENV
|
||||||
@@ -171,7 +171,7 @@ jobs:
|
|||||||
id: check-rc
|
id: check-rc
|
||||||
run: |
|
run: |
|
||||||
TAG=${{ env.TAG }}
|
TAG=${{ env.TAG }}
|
||||||
if [[ "$TAG" == *".rc."* ]]; then
|
if [[ "$TAG" == *"-rc."* ]]; then
|
||||||
echo "IS_RC=true" >> $GITHUB_ENV
|
echo "IS_RC=true" >> $GITHUB_ENV
|
||||||
else
|
else
|
||||||
echo "IS_RC=false" >> $GITHUB_ENV
|
echo "IS_RC=false" >> $GITHUB_ENV
|
||||||
@@ -219,7 +219,7 @@ jobs:
|
|||||||
id: check-rc
|
id: check-rc
|
||||||
run: |
|
run: |
|
||||||
TAG=${{ env.TAG }}
|
TAG=${{ env.TAG }}
|
||||||
if [[ "$TAG" == *".rc."* ]]; then
|
if [[ "$TAG" == *"-rc."* ]]; then
|
||||||
echo "IS_RC=true" >> $GITHUB_ENV
|
echo "IS_RC=true" >> $GITHUB_ENV
|
||||||
else
|
else
|
||||||
echo "IS_RC=false" >> $GITHUB_ENV
|
echo "IS_RC=false" >> $GITHUB_ENV
|
||||||
@@ -322,13 +322,18 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Login to GHCR
|
- name: Login to GHCR
|
||||||
|
env:
|
||||||
|
REGISTRY_AUTH_FILE: ${{ runner.temp }}/containers/auth.json
|
||||||
run: |
|
run: |
|
||||||
|
mkdir -p "$(dirname "$REGISTRY_AUTH_FILE")"
|
||||||
skopeo login ghcr.io -u "${{ github.actor }}" -p "${{ secrets.GITHUB_TOKEN }}"
|
skopeo login ghcr.io -u "${{ github.actor }}" -p "${{ secrets.GITHUB_TOKEN }}"
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Copy tag from Docker Hub to GHCR
|
- name: Copy tag from Docker Hub to GHCR
|
||||||
# Mirror the already-built image (all architectures) to GHCR so we can sign it
|
# Mirror the already-built image (all architectures) to GHCR so we can sign it
|
||||||
# Wait a bit for both architectures to be available in Docker Hub manifest
|
# Wait a bit for both architectures to be available in Docker Hub manifest
|
||||||
|
env:
|
||||||
|
REGISTRY_AUTH_FILE: ${{ runner.temp }}/containers/auth.json
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
TAG=${{ env.TAG }}
|
TAG=${{ env.TAG }}
|
||||||
|
|||||||
2
.github/workflows/mirror.yaml
vendored
2
.github/workflows/mirror.yaml
vendored
@@ -45,7 +45,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
skopeo list-tags --retry-times 3 docker://"${SOURCE_IMAGE}" \
|
skopeo list-tags --retry-times 3 docker://"${SOURCE_IMAGE}" \
|
||||||
| jq -r '.Tags[]' | sort -u > src-tags.txt
|
| jq -r '.Tags[]' | grep -v -e '-arm64' -e '-amd64' | sort -u > src-tags.txt
|
||||||
echo "Found source tags: $(wc -l < src-tags.txt)"
|
echo "Found source tags: $(wc -l < src-tags.txt)"
|
||||||
head -n 20 src-tags.txt || true
|
head -n 20 src-tags.txt || true
|
||||||
|
|
||||||
|
|||||||
@@ -2349,6 +2349,7 @@
|
|||||||
"enterConfirmation": "Enter confirmation",
|
"enterConfirmation": "Enter confirmation",
|
||||||
"blueprintViewDetails": "Details",
|
"blueprintViewDetails": "Details",
|
||||||
"defaultIdentityProvider": "Default Identity Provider",
|
"defaultIdentityProvider": "Default Identity Provider",
|
||||||
|
"defaultIdentityProviderDescription": "When a default identity provider is selected, the user will be automatically redirected to the provider for authentication.",
|
||||||
"editInternalResourceDialogNetworkSettings": "Network Settings",
|
"editInternalResourceDialogNetworkSettings": "Network Settings",
|
||||||
"editInternalResourceDialogAccessPolicy": "Access Policy",
|
"editInternalResourceDialogAccessPolicy": "Access Policy",
|
||||||
"editInternalResourceDialogAddRoles": "Add Roles",
|
"editInternalResourceDialogAddRoles": "Add Roles",
|
||||||
|
|||||||
@@ -134,13 +134,15 @@ export const resources = pgTable("resources", {
|
|||||||
proxyProtocol: boolean("proxyProtocol").notNull().default(false),
|
proxyProtocol: boolean("proxyProtocol").notNull().default(false),
|
||||||
proxyProtocolVersion: integer("proxyProtocolVersion").default(1),
|
proxyProtocolVersion: integer("proxyProtocolVersion").default(1),
|
||||||
|
|
||||||
maintenanceModeEnabled: boolean("maintenanceModeEnabled").notNull().default(false),
|
maintenanceModeEnabled: boolean("maintenanceModeEnabled")
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
maintenanceModeType: text("maintenanceModeType", {
|
maintenanceModeType: text("maintenanceModeType", {
|
||||||
enum: ["forced", "automatic"]
|
enum: ["forced", "automatic"]
|
||||||
}).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")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const targets = pgTable("targets", {
|
export const targets = pgTable("targets", {
|
||||||
@@ -223,8 +225,8 @@ export const siteResources = pgTable("siteResources", {
|
|||||||
enabled: boolean("enabled").notNull().default(true),
|
enabled: boolean("enabled").notNull().default(true),
|
||||||
alias: varchar("alias"),
|
alias: varchar("alias"),
|
||||||
aliasAddress: varchar("aliasAddress"),
|
aliasAddress: varchar("aliasAddress"),
|
||||||
tcpPortRangeString: varchar("tcpPortRangeString"),
|
tcpPortRangeString: varchar("tcpPortRangeString").notNull().default("*"),
|
||||||
udpPortRangeString: varchar("udpPortRangeString"),
|
udpPortRangeString: varchar("udpPortRangeString").notNull().default("*"),
|
||||||
disableIcmp: boolean("disableIcmp").notNull().default(false)
|
disableIcmp: boolean("disableIcmp").notNull().default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -464,13 +466,22 @@ export const resourceHeaderAuth = pgTable("resourceHeaderAuth", {
|
|||||||
headerAuthHash: varchar("headerAuthHash").notNull()
|
headerAuthHash: varchar("headerAuthHash").notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const resourceHeaderAuthExtendedCompatibility = pgTable("resourceHeaderAuthExtendedCompatibility", {
|
export const resourceHeaderAuthExtendedCompatibility = pgTable(
|
||||||
headerAuthExtendedCompatibilityId: serial("headerAuthExtendedCompatibilityId").primaryKey(),
|
"resourceHeaderAuthExtendedCompatibility",
|
||||||
|
{
|
||||||
|
headerAuthExtendedCompatibilityId: serial(
|
||||||
|
"headerAuthExtendedCompatibilityId"
|
||||||
|
).primaryKey(),
|
||||||
resourceId: integer("resourceId")
|
resourceId: integer("resourceId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||||
extendedCompatibilityIsActivated: boolean("extendedCompatibilityIsActivated").notNull().default(true),
|
extendedCompatibilityIsActivated: boolean(
|
||||||
});
|
"extendedCompatibilityIsActivated"
|
||||||
|
)
|
||||||
|
.notNull()
|
||||||
|
.default(true)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const resourceAccessToken = pgTable("resourceAccessToken", {
|
export const resourceAccessToken = pgTable("resourceAccessToken", {
|
||||||
accessTokenId: varchar("accessTokenId").primaryKey(),
|
accessTokenId: varchar("accessTokenId").primaryKey(),
|
||||||
@@ -872,7 +883,9 @@ export type ResourceSession = InferSelectModel<typeof resourceSessions>;
|
|||||||
export type ResourcePincode = InferSelectModel<typeof resourcePincode>;
|
export type ResourcePincode = InferSelectModel<typeof resourcePincode>;
|
||||||
export type ResourcePassword = InferSelectModel<typeof resourcePassword>;
|
export type ResourcePassword = InferSelectModel<typeof resourcePassword>;
|
||||||
export type ResourceHeaderAuth = InferSelectModel<typeof resourceHeaderAuth>;
|
export type ResourceHeaderAuth = InferSelectModel<typeof resourceHeaderAuth>;
|
||||||
export type ResourceHeaderAuthExtendedCompatibility = InferSelectModel<typeof resourceHeaderAuthExtendedCompatibility>;
|
export type ResourceHeaderAuthExtendedCompatibility = InferSelectModel<
|
||||||
|
typeof resourceHeaderAuthExtendedCompatibility
|
||||||
|
>;
|
||||||
export type ResourceOtp = InferSelectModel<typeof resourceOtp>;
|
export type ResourceOtp = InferSelectModel<typeof resourceOtp>;
|
||||||
export type ResourceAccessToken = InferSelectModel<typeof resourceAccessToken>;
|
export type ResourceAccessToken = InferSelectModel<typeof resourceAccessToken>;
|
||||||
export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>;
|
export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>;
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import {
|
import { db, loginPage, LoginPage, loginPageOrg, Org, orgs } from "@server/db";
|
||||||
db, loginPage, LoginPage, loginPageOrg, Org, orgs,
|
|
||||||
} from "@server/db";
|
|
||||||
import {
|
import {
|
||||||
Resource,
|
Resource,
|
||||||
ResourcePassword,
|
ResourcePassword,
|
||||||
@@ -27,7 +25,7 @@ export type ResourceWithAuth = {
|
|||||||
pincode: ResourcePincode | null;
|
pincode: ResourcePincode | null;
|
||||||
password: ResourcePassword | null;
|
password: ResourcePassword | null;
|
||||||
headerAuth: ResourceHeaderAuth | null;
|
headerAuth: ResourceHeaderAuth | null;
|
||||||
headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null
|
headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null;
|
||||||
org: Org;
|
org: Org;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -59,12 +57,12 @@ export async function getResourceByDomain(
|
|||||||
)
|
)
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
resourceHeaderAuthExtendedCompatibility,
|
resourceHeaderAuthExtendedCompatibility,
|
||||||
eq(resourceHeaderAuthExtendedCompatibility.resourceId, resources.resourceId)
|
eq(
|
||||||
|
resourceHeaderAuthExtendedCompatibility.resourceId,
|
||||||
|
resources.resourceId
|
||||||
)
|
)
|
||||||
.innerJoin(
|
|
||||||
orgs,
|
|
||||||
eq(orgs.orgId, resources.orgId)
|
|
||||||
)
|
)
|
||||||
|
.innerJoin(orgs, eq(orgs.orgId, resources.orgId))
|
||||||
.where(eq(resources.fullDomain, domain))
|
.where(eq(resources.fullDomain, domain))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
@@ -77,7 +75,8 @@ export async function getResourceByDomain(
|
|||||||
pincode: result.resourcePincode,
|
pincode: result.resourcePincode,
|
||||||
password: result.resourcePassword,
|
password: result.resourcePassword,
|
||||||
headerAuth: result.resourceHeaderAuth,
|
headerAuth: result.resourceHeaderAuth,
|
||||||
headerAuthExtendedCompatibility: result.resourceHeaderAuthExtendedCompatibility,
|
headerAuthExtendedCompatibility:
|
||||||
|
result.resourceHeaderAuthExtendedCompatibility,
|
||||||
org: result.orgs
|
org: result.orgs
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,10 +147,14 @@ export const resources = sqliteTable("resources", {
|
|||||||
onDelete: "set null"
|
onDelete: "set null"
|
||||||
}),
|
}),
|
||||||
headers: text("headers"), // comma-separated list of headers to add to the request
|
headers: text("headers"), // comma-separated list of headers to add to the request
|
||||||
proxyProtocol: integer("proxyProtocol", { mode: "boolean" }).notNull().default(false),
|
proxyProtocol: integer("proxyProtocol", { mode: "boolean" })
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
proxyProtocolVersion: integer("proxyProtocolVersion").default(1),
|
proxyProtocolVersion: integer("proxyProtocolVersion").default(1),
|
||||||
|
|
||||||
maintenanceModeEnabled: integer("maintenanceModeEnabled", { mode: "boolean" })
|
maintenanceModeEnabled: integer("maintenanceModeEnabled", {
|
||||||
|
mode: "boolean"
|
||||||
|
})
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(false),
|
.default(false),
|
||||||
maintenanceModeType: text("maintenanceModeType", {
|
maintenanceModeType: text("maintenanceModeType", {
|
||||||
@@ -158,8 +162,7 @@ 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")
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const targets = sqliteTable("targets", {
|
export const targets = sqliteTable("targets", {
|
||||||
@@ -250,9 +253,9 @@ export const siteResources = sqliteTable("siteResources", {
|
|||||||
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
||||||
alias: text("alias"),
|
alias: text("alias"),
|
||||||
aliasAddress: text("aliasAddress"),
|
aliasAddress: text("aliasAddress"),
|
||||||
tcpPortRangeString: text("tcpPortRangeString"),
|
tcpPortRangeString: text("tcpPortRangeString").notNull().default("*"),
|
||||||
udpPortRangeString: text("udpPortRangeString"),
|
udpPortRangeString: text("udpPortRangeString").notNull().default("*"),
|
||||||
disableIcmp: integer("disableIcmp", { mode: "boolean" })
|
disableIcmp: integer("disableIcmp", { mode: "boolean" }).notNull().default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const clientSiteResources = sqliteTable("clientSiteResources", {
|
export const clientSiteResources = sqliteTable("clientSiteResources", {
|
||||||
@@ -637,15 +640,25 @@ export const resourceHeaderAuth = sqliteTable("resourceHeaderAuth", {
|
|||||||
headerAuthHash: text("headerAuthHash").notNull()
|
headerAuthHash: text("headerAuthHash").notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const resourceHeaderAuthExtendedCompatibility = sqliteTable("resourceHeaderAuthExtendedCompatibility", {
|
export const resourceHeaderAuthExtendedCompatibility = sqliteTable(
|
||||||
headerAuthExtendedCompatibilityId: integer("headerAuthExtendedCompatibilityId").primaryKey({
|
"resourceHeaderAuthExtendedCompatibility",
|
||||||
|
{
|
||||||
|
headerAuthExtendedCompatibilityId: integer(
|
||||||
|
"headerAuthExtendedCompatibilityId"
|
||||||
|
).primaryKey({
|
||||||
autoIncrement: true
|
autoIncrement: true
|
||||||
}),
|
}),
|
||||||
resourceId: integer("resourceId")
|
resourceId: integer("resourceId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||||
extendedCompatibilityIsActivated: integer("extendedCompatibilityIsActivated", {mode: "boolean"}).notNull().default(true)
|
extendedCompatibilityIsActivated: integer(
|
||||||
});
|
"extendedCompatibilityIsActivated",
|
||||||
|
{ mode: "boolean" }
|
||||||
|
)
|
||||||
|
.notNull()
|
||||||
|
.default(true)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const resourceAccessToken = sqliteTable("resourceAccessToken", {
|
export const resourceAccessToken = sqliteTable("resourceAccessToken", {
|
||||||
accessTokenId: text("accessTokenId").primaryKey(),
|
accessTokenId: text("accessTokenId").primaryKey(),
|
||||||
@@ -932,7 +945,9 @@ export type ResourceSession = InferSelectModel<typeof resourceSessions>;
|
|||||||
export type ResourcePincode = InferSelectModel<typeof resourcePincode>;
|
export type ResourcePincode = InferSelectModel<typeof resourcePincode>;
|
||||||
export type ResourcePassword = InferSelectModel<typeof resourcePassword>;
|
export type ResourcePassword = InferSelectModel<typeof resourcePassword>;
|
||||||
export type ResourceHeaderAuth = InferSelectModel<typeof resourceHeaderAuth>;
|
export type ResourceHeaderAuth = InferSelectModel<typeof resourceHeaderAuth>;
|
||||||
export type ResourceHeaderAuthExtendedCompatibility = InferSelectModel<typeof resourceHeaderAuthExtendedCompatibility>;
|
export type ResourceHeaderAuthExtendedCompatibility = InferSelectModel<
|
||||||
|
typeof resourceHeaderAuthExtendedCompatibility
|
||||||
|
>;
|
||||||
export type ResourceOtp = InferSelectModel<typeof resourceOtp>;
|
export type ResourceOtp = InferSelectModel<typeof resourceOtp>;
|
||||||
export type ResourceAccessToken = InferSelectModel<typeof resourceAccessToken>;
|
export type ResourceAccessToken = InferSelectModel<typeof resourceAccessToken>;
|
||||||
export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>;
|
export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>;
|
||||||
|
|||||||
3
server/lib/blueprints/MaintenanceSchema.ts
Normal file
3
server/lib/blueprints/MaintenanceSchema.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const MaintenanceSchema = z.object({});
|
||||||
@@ -1,4 +1,14 @@
|
|||||||
import { db, newts, blueprints, Blueprint, Site, siteResources, roleSiteResources, userSiteResources, clientSiteResources } from "@server/db";
|
import {
|
||||||
|
db,
|
||||||
|
newts,
|
||||||
|
blueprints,
|
||||||
|
Blueprint,
|
||||||
|
Site,
|
||||||
|
siteResources,
|
||||||
|
roleSiteResources,
|
||||||
|
userSiteResources,
|
||||||
|
clientSiteResources
|
||||||
|
} from "@server/db";
|
||||||
import { Config, ConfigSchema } from "./types";
|
import { Config, ConfigSchema } from "./types";
|
||||||
import { ProxyResourcesResults, updateProxyResources } from "./proxyResources";
|
import { ProxyResourcesResults, updateProxyResources } from "./proxyResources";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
@@ -134,7 +144,8 @@ export async function applyBlueprint({
|
|||||||
userSiteResources.siteResourceId,
|
userSiteResources.siteResourceId,
|
||||||
result.oldSiteResource.siteResourceId
|
result.oldSiteResource.siteResourceId
|
||||||
)
|
)
|
||||||
).then((rows) => rows.map((row) => row.userId));
|
)
|
||||||
|
.then((rows) => rows.map((row) => row.userId));
|
||||||
|
|
||||||
const existingClientIds = await trx
|
const existingClientIds = await trx
|
||||||
.select()
|
.select()
|
||||||
@@ -144,13 +155,19 @@ export async function applyBlueprint({
|
|||||||
clientSiteResources.siteResourceId,
|
clientSiteResources.siteResourceId,
|
||||||
result.oldSiteResource.siteResourceId
|
result.oldSiteResource.siteResourceId
|
||||||
)
|
)
|
||||||
).then((rows) => rows.map((row) => row.clientId));
|
)
|
||||||
|
.then((rows) => rows.map((row) => row.clientId));
|
||||||
|
|
||||||
// delete the existing site resource
|
// delete the existing site resource
|
||||||
await trx
|
await trx
|
||||||
.delete(siteResources)
|
.delete(siteResources)
|
||||||
.where(
|
.where(
|
||||||
and(eq(siteResources.siteResourceId, result.oldSiteResource.siteResourceId))
|
and(
|
||||||
|
eq(
|
||||||
|
siteResources.siteResourceId,
|
||||||
|
result.oldSiteResource.siteResourceId
|
||||||
|
)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
await rebuildClientAssociationsFromSiteResource(
|
await rebuildClientAssociationsFromSiteResource(
|
||||||
@@ -161,7 +178,7 @@ export async function applyBlueprint({
|
|||||||
const [insertedSiteResource] = await trx
|
const [insertedSiteResource] = await trx
|
||||||
.insert(siteResources)
|
.insert(siteResources)
|
||||||
.values({
|
.values({
|
||||||
...result.newSiteResource,
|
...result.newSiteResource
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
@@ -174,7 +191,8 @@ export async function applyBlueprint({
|
|||||||
await trx.insert(roleSiteResources).values(
|
await trx.insert(roleSiteResources).values(
|
||||||
existingRoleIds.map((roleId) => ({
|
existingRoleIds.map((roleId) => ({
|
||||||
roleId,
|
roleId,
|
||||||
siteResourceId: insertedSiteResource!.siteResourceId
|
siteResourceId:
|
||||||
|
insertedSiteResource!.siteResourceId
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -183,7 +201,8 @@ export async function applyBlueprint({
|
|||||||
await trx.insert(userSiteResources).values(
|
await trx.insert(userSiteResources).values(
|
||||||
existingUserIds.map((userId) => ({
|
existingUserIds.map((userId) => ({
|
||||||
userId,
|
userId,
|
||||||
siteResourceId: insertedSiteResource!.siteResourceId
|
siteResourceId:
|
||||||
|
insertedSiteResource!.siteResourceId
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -192,7 +211,8 @@ export async function applyBlueprint({
|
|||||||
await trx.insert(clientSiteResources).values(
|
await trx.insert(clientSiteResources).values(
|
||||||
existingClientIds.map((clientId) => ({
|
existingClientIds.map((clientId) => ({
|
||||||
clientId,
|
clientId,
|
||||||
siteResourceId: insertedSiteResource!.siteResourceId
|
siteResourceId:
|
||||||
|
insertedSiteResource!.siteResourceId
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -201,7 +221,6 @@ export async function applyBlueprint({
|
|||||||
insertedSiteResource,
|
insertedSiteResource,
|
||||||
trx
|
trx
|
||||||
);
|
);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
const [newSite] = await trx
|
const [newSite] = await trx
|
||||||
.select()
|
.select()
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import {
|
|||||||
domains,
|
domains,
|
||||||
orgDomains,
|
orgDomains,
|
||||||
Resource,
|
Resource,
|
||||||
resourceHeaderAuth, resourceHeaderAuthExtendedCompatibility,
|
resourceHeaderAuth,
|
||||||
|
resourceHeaderAuthExtendedCompatibility,
|
||||||
resourcePincode,
|
resourcePincode,
|
||||||
resourceRules,
|
resourceRules,
|
||||||
resourceWhitelist,
|
resourceWhitelist,
|
||||||
@@ -30,7 +31,8 @@ import {pickPort} from "@server/routers/target/helpers";
|
|||||||
import { resourcePassword } from "@server/db";
|
import { resourcePassword } from "@server/db";
|
||||||
import { hashPassword } from "@server/auth/password";
|
import { hashPassword } from "@server/auth/password";
|
||||||
import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators";
|
import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators";
|
||||||
import {get} from "http";
|
import { isLicensedOrSubscribed } from "../isLicencedOrSubscribed";
|
||||||
|
import { build } from "@server/build";
|
||||||
|
|
||||||
export type ProxyResourcesResults = {
|
export type ProxyResourcesResults = {
|
||||||
proxyResource: Resource;
|
proxyResource: Resource;
|
||||||
@@ -209,6 +211,16 @@ export async function updateProxyResources(
|
|||||||
resource = existingResource;
|
resource = existingResource;
|
||||||
} else {
|
} else {
|
||||||
// Update existing resource
|
// Update existing resource
|
||||||
|
|
||||||
|
const isLicensed = await isLicensedOrSubscribed(orgId);
|
||||||
|
if (build == "enterprise" && !isLicensed) {
|
||||||
|
logger.warn(
|
||||||
|
"Server is not licensed! Clearing set maintenance screen values"
|
||||||
|
);
|
||||||
|
// null the maintenance mode fields if not licensed
|
||||||
|
resourceData.maintenance = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
[resource] = await trx
|
[resource] = await trx
|
||||||
.update(resources)
|
.update(resources)
|
||||||
.set({
|
.set({
|
||||||
@@ -233,7 +245,14 @@ export async function updateProxyResources(
|
|||||||
: false,
|
: false,
|
||||||
headers: headers || null,
|
headers: headers || null,
|
||||||
applyRules:
|
applyRules:
|
||||||
resourceData.rules && resourceData.rules.length > 0
|
resourceData.rules && resourceData.rules.length > 0,
|
||||||
|
maintenanceModeEnabled:
|
||||||
|
resourceData.maintenance?.enabled,
|
||||||
|
maintenanceModeType: resourceData.maintenance?.type,
|
||||||
|
maintenanceTitle: resourceData.maintenance?.title,
|
||||||
|
maintenanceMessage: resourceData.maintenance?.message,
|
||||||
|
maintenanceEstimatedTime:
|
||||||
|
resourceData.maintenance?.["estimated-time"]
|
||||||
})
|
})
|
||||||
.where(
|
.where(
|
||||||
eq(resources.resourceId, existingResource.resourceId)
|
eq(resources.resourceId, existingResource.resourceId)
|
||||||
@@ -303,8 +322,13 @@ export async function updateProxyResources(
|
|||||||
const headerAuthPassword =
|
const headerAuthPassword =
|
||||||
resourceData.auth?.["basic-auth"]?.password;
|
resourceData.auth?.["basic-auth"]?.password;
|
||||||
const headerAuthExtendedCompatibility =
|
const headerAuthExtendedCompatibility =
|
||||||
resourceData.auth?.["basic-auth"]?.extendedCompatibility;
|
resourceData.auth?.["basic-auth"]
|
||||||
if (headerAuthUser && headerAuthPassword && headerAuthExtendedCompatibility !== null) {
|
?.extendedCompatibility;
|
||||||
|
if (
|
||||||
|
headerAuthUser &&
|
||||||
|
headerAuthPassword &&
|
||||||
|
headerAuthExtendedCompatibility !== null
|
||||||
|
) {
|
||||||
const headerAuthHash = await hashPassword(
|
const headerAuthHash = await hashPassword(
|
||||||
Buffer.from(
|
Buffer.from(
|
||||||
`${headerAuthUser}:${headerAuthPassword}`
|
`${headerAuthUser}:${headerAuthPassword}`
|
||||||
@@ -315,9 +339,12 @@ export async function updateProxyResources(
|
|||||||
resourceId: existingResource.resourceId,
|
resourceId: existingResource.resourceId,
|
||||||
headerAuthHash
|
headerAuthHash
|
||||||
}),
|
}),
|
||||||
trx.insert(resourceHeaderAuthExtendedCompatibility).values({
|
trx
|
||||||
|
.insert(resourceHeaderAuthExtendedCompatibility)
|
||||||
|
.values({
|
||||||
resourceId: existingResource.resourceId,
|
resourceId: existingResource.resourceId,
|
||||||
extendedCompatibilityIsActivated: headerAuthExtendedCompatibility
|
extendedCompatibilityIsActivated:
|
||||||
|
headerAuthExtendedCompatibility
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -622,6 +649,15 @@ export async function updateProxyResources(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isLicensed = await isLicensedOrSubscribed(orgId);
|
||||||
|
if (build == "enterprise" && !isLicensed) {
|
||||||
|
logger.warn(
|
||||||
|
"Server is not licensed! Clearing set maintenance screen values"
|
||||||
|
);
|
||||||
|
// null the maintenance mode fields if not licensed
|
||||||
|
resourceData.maintenance = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
// Create new resource
|
// Create new resource
|
||||||
const [newResource] = await trx
|
const [newResource] = await trx
|
||||||
.insert(resources)
|
.insert(resources)
|
||||||
@@ -643,7 +679,13 @@ export async function updateProxyResources(
|
|||||||
ssl: resourceSsl,
|
ssl: resourceSsl,
|
||||||
headers: headers || null,
|
headers: headers || null,
|
||||||
applyRules:
|
applyRules:
|
||||||
resourceData.rules && resourceData.rules.length > 0
|
resourceData.rules && resourceData.rules.length > 0,
|
||||||
|
maintenanceModeEnabled: resourceData.maintenance?.enabled,
|
||||||
|
maintenanceModeType: resourceData.maintenance?.type,
|
||||||
|
maintenanceTitle: resourceData.maintenance?.title,
|
||||||
|
maintenanceMessage: resourceData.maintenance?.message,
|
||||||
|
maintenanceEstimatedTime:
|
||||||
|
resourceData.maintenance?.["estimated-time"]
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
@@ -674,9 +716,14 @@ export async function updateProxyResources(
|
|||||||
const headerAuthUser = resourceData.auth?.["basic-auth"]?.user;
|
const headerAuthUser = resourceData.auth?.["basic-auth"]?.user;
|
||||||
const headerAuthPassword =
|
const headerAuthPassword =
|
||||||
resourceData.auth?.["basic-auth"]?.password;
|
resourceData.auth?.["basic-auth"]?.password;
|
||||||
const headerAuthExtendedCompatibility = resourceData.auth?.["basic-auth"]?.extendedCompatibility;
|
const headerAuthExtendedCompatibility =
|
||||||
|
resourceData.auth?.["basic-auth"]?.extendedCompatibility;
|
||||||
|
|
||||||
if (headerAuthUser && headerAuthPassword && headerAuthExtendedCompatibility !== null) {
|
if (
|
||||||
|
headerAuthUser &&
|
||||||
|
headerAuthPassword &&
|
||||||
|
headerAuthExtendedCompatibility !== null
|
||||||
|
) {
|
||||||
const headerAuthHash = await hashPassword(
|
const headerAuthHash = await hashPassword(
|
||||||
Buffer.from(
|
Buffer.from(
|
||||||
`${headerAuthUser}:${headerAuthPassword}`
|
`${headerAuthUser}:${headerAuthPassword}`
|
||||||
@@ -688,10 +735,13 @@ export async function updateProxyResources(
|
|||||||
resourceId: newResource.resourceId,
|
resourceId: newResource.resourceId,
|
||||||
headerAuthHash
|
headerAuthHash
|
||||||
}),
|
}),
|
||||||
trx.insert(resourceHeaderAuthExtendedCompatibility).values({
|
trx
|
||||||
|
.insert(resourceHeaderAuthExtendedCompatibility)
|
||||||
|
.values({
|
||||||
resourceId: newResource.resourceId,
|
resourceId: newResource.resourceId,
|
||||||
extendedCompatibilityIsActivated: headerAuthExtendedCompatibility
|
extendedCompatibilityIsActivated:
|
||||||
}),
|
headerAuthExtendedCompatibility
|
||||||
|
})
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { portRangeStringSchema } from "@server/lib/ip";
|
import { portRangeStringSchema } from "@server/lib/ip";
|
||||||
|
import { MaintenanceSchema } from "#dynamic/lib/blueprints/MaintenanceSchema";
|
||||||
|
|
||||||
export const SiteSchema = z.object({
|
export const SiteSchema = z.object({
|
||||||
name: z.string().min(1).max(100),
|
name: z.string().min(1).max(100),
|
||||||
@@ -53,11 +54,13 @@ export const AuthSchema = z.object({
|
|||||||
// pincode has to have 6 digits
|
// pincode has to have 6 digits
|
||||||
pincode: z.number().min(100000).max(999999).optional(),
|
pincode: z.number().min(100000).max(999999).optional(),
|
||||||
password: z.string().min(1).optional(),
|
password: z.string().min(1).optional(),
|
||||||
"basic-auth": z.object({
|
"basic-auth": z
|
||||||
|
.object({
|
||||||
user: z.string().min(1),
|
user: z.string().min(1),
|
||||||
password: z.string().min(1),
|
password: z.string().min(1),
|
||||||
extendedCompatibility: z.boolean().default(true)
|
extendedCompatibility: z.boolean().default(true)
|
||||||
}).optional(),
|
})
|
||||||
|
.optional(),
|
||||||
"sso-enabled": z.boolean().optional().default(false),
|
"sso-enabled": z.boolean().optional().default(false),
|
||||||
"sso-roles": z
|
"sso-roles": z
|
||||||
.array(z.string())
|
.array(z.string())
|
||||||
@@ -156,7 +159,8 @@ export const ResourceSchema = z
|
|||||||
"host-header": z.string().optional(),
|
"host-header": z.string().optional(),
|
||||||
"tls-server-name": z.string().optional(),
|
"tls-server-name": z.string().optional(),
|
||||||
headers: z.array(HeaderSchema).optional(),
|
headers: z.array(HeaderSchema).optional(),
|
||||||
rules: z.array(RuleSchema).optional()
|
rules: z.array(RuleSchema).optional(),
|
||||||
|
maintenance: MaintenanceSchema.optional()
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(resource) => {
|
(resource) => {
|
||||||
|
|||||||
@@ -318,10 +318,7 @@ export function doCidrsOverlap(cidr1: string, cidr2: string): boolean {
|
|||||||
const range2 = cidrToRange(cidr2);
|
const range2 = cidrToRange(cidr2);
|
||||||
|
|
||||||
// Overlap if the ranges intersect
|
// Overlap if the ranges intersect
|
||||||
return (
|
return range1.start <= range2.end && range2.start <= range1.end;
|
||||||
range1.start <= range2.end &&
|
|
||||||
range2.start <= range1.end
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getNextAvailableClientSubnet(
|
export async function getNextAvailableClientSubnet(
|
||||||
|
|||||||
@@ -216,7 +216,10 @@ export const configSchema = z
|
|||||||
.default(["newt", "wireguard", "local"]),
|
.default(["newt", "wireguard", "local"]),
|
||||||
allow_raw_resources: z.boolean().optional().default(true),
|
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")
|
pp_transport_prefix: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.default("pp-transport-v")
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.prefault({}),
|
.prefault({}),
|
||||||
|
|||||||
@@ -475,9 +475,9 @@ export async function getTraefikConfig(
|
|||||||
// RECEIVE BANDWIDTH ENDPOINT.
|
// RECEIVE BANDWIDTH ENDPOINT.
|
||||||
|
|
||||||
// TODO: HOW TO HANDLE ^^^^^^ BETTER
|
// TODO: HOW TO HANDLE ^^^^^^ BETTER
|
||||||
const anySitesOnline = (
|
const anySitesOnline = targets.some(
|
||||||
targets
|
(target) => target.site.online
|
||||||
).some((target) => target.site.online);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
targets
|
targets
|
||||||
@@ -603,9 +603,9 @@ export async function getTraefikConfig(
|
|||||||
loadBalancer: {
|
loadBalancer: {
|
||||||
servers: (() => {
|
servers: (() => {
|
||||||
// Check if any sites are online
|
// Check if any sites are online
|
||||||
const anySitesOnline = (
|
const anySitesOnline = targets.some(
|
||||||
targets
|
(target) => target.site.online
|
||||||
).some((target) => target.site.online);
|
);
|
||||||
|
|
||||||
return targets
|
return targets
|
||||||
.filter((target) => {
|
.filter((target) => {
|
||||||
|
|||||||
22
server/private/lib/blueprints/MaintenanceSchema.ts
Normal file
22
server/private/lib/blueprints/MaintenanceSchema.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of a proprietary work.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2025 Fossorial, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This file is licensed under the Fossorial Commercial License.
|
||||||
|
* You may not use this file except in compliance with the License.
|
||||||
|
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||||
|
*
|
||||||
|
* This file is not licensed under the AGPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const MaintenanceSchema = z.object({
|
||||||
|
enabled: z.boolean().optional(),
|
||||||
|
type: z.enum(["forced", "automatic"]).optional(),
|
||||||
|
title: z.string().max(255).nullable().optional(),
|
||||||
|
message: z.string().max(2000).nullable().optional(),
|
||||||
|
"estimated-time": z.string().max(100).nullable().optional()
|
||||||
|
});
|
||||||
@@ -23,10 +23,10 @@ import {
|
|||||||
} from "@server/lib/checkOrgAccessPolicy";
|
} from "@server/lib/checkOrgAccessPolicy";
|
||||||
import { UserType } from "@server/types/UserTypes";
|
import { UserType } from "@server/types/UserTypes";
|
||||||
|
|
||||||
export async function enforceResourceSessionLength(
|
export function enforceResourceSessionLength(
|
||||||
resourceSession: ResourceSession,
|
resourceSession: ResourceSession,
|
||||||
org: Org
|
org: Org
|
||||||
): Promise<{ valid: boolean; error?: string }> {
|
): { valid: boolean; error?: string } {
|
||||||
if (org.maxSessionLengthHours) {
|
if (org.maxSessionLengthHours) {
|
||||||
const sessionIssuedAt = resourceSession.issuedAt; // may be null
|
const sessionIssuedAt = resourceSession.issuedAt; // may be null
|
||||||
const maxSessionLengthHours = org.maxSessionLengthHours;
|
const maxSessionLengthHours = org.maxSessionLengthHours;
|
||||||
|
|||||||
@@ -71,9 +71,9 @@ export async function getTraefikConfig(
|
|||||||
siteTypes: string[],
|
siteTypes: string[],
|
||||||
filterOutNamespaceDomains = false,
|
filterOutNamespaceDomains = false,
|
||||||
generateLoginPageRouters = false,
|
generateLoginPageRouters = false,
|
||||||
allowRawResources = true
|
allowRawResources = true,
|
||||||
|
allowMaintenancePage = true
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
|
|
||||||
// Get resources with their targets and sites in a single optimized query
|
// Get resources with their targets and sites in a single optimized query
|
||||||
// Start from sites on this exit node, then join to targets and resources
|
// Start from sites on this exit node, then join to targets and resources
|
||||||
const resourcesWithTargetsAndSites = await db
|
const resourcesWithTargetsAndSites = await db
|
||||||
@@ -435,8 +435,7 @@ export async function getTraefikConfig(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const availableServers = targets.filter(
|
const availableServers = targets.filter((target) => {
|
||||||
(target) => {
|
|
||||||
if (!target.enabled) return false;
|
if (!target.enabled) return false;
|
||||||
|
|
||||||
if (!target.site.online) return false;
|
if (!target.site.online) return false;
|
||||||
@@ -444,8 +443,7 @@ export async function getTraefikConfig(
|
|||||||
if (target.health == "unhealthy") return false;
|
if (target.health == "unhealthy") return false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const hasHealthyServers = availableServers.length > 0;
|
const hasHealthyServers = availableServers.length > 0;
|
||||||
|
|
||||||
@@ -794,9 +792,9 @@ export async function getTraefikConfig(
|
|||||||
loadBalancer: {
|
loadBalancer: {
|
||||||
servers: (() => {
|
servers: (() => {
|
||||||
// Check if any sites are online
|
// Check if any sites are online
|
||||||
const anySitesOnline = (
|
const anySitesOnline = targets.some(
|
||||||
targets
|
(target) => target.site.online
|
||||||
).some((target) => target.site.online);
|
);
|
||||||
|
|
||||||
return targets
|
return targets
|
||||||
.filter((target) => {
|
.filter((target) => {
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import {
|
|||||||
ResourceHeaderAuthExtendedCompatibility,
|
ResourceHeaderAuthExtendedCompatibility,
|
||||||
orgs,
|
orgs,
|
||||||
requestAuditLog,
|
requestAuditLog,
|
||||||
|
Org
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import {
|
import {
|
||||||
resources,
|
resources,
|
||||||
@@ -79,6 +80,7 @@ import { maxmindLookup } from "@server/db/maxmind";
|
|||||||
import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
|
import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
|
||||||
import semver from "semver";
|
import semver from "semver";
|
||||||
import { maxmindAsnLookup } from "@server/db/maxmindAsn";
|
import { maxmindAsnLookup } from "@server/db/maxmindAsn";
|
||||||
|
import { checkOrgAccessPolicy } from "@server/lib/checkOrgAccessPolicy";
|
||||||
|
|
||||||
// Zod schemas for request validation
|
// Zod schemas for request validation
|
||||||
const getResourceByDomainParamsSchema = z.strictObject({
|
const getResourceByDomainParamsSchema = z.strictObject({
|
||||||
@@ -94,6 +96,12 @@ const getUserOrgRoleParamsSchema = z.strictObject({
|
|||||||
orgId: z.string().min(1, "Organization ID is required")
|
orgId: z.string().min(1, "Organization ID is required")
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const getUserOrgSessionVerifySchema = z.strictObject({
|
||||||
|
userId: z.string().min(1, "User ID is required"),
|
||||||
|
orgId: z.string().min(1, "Organization ID is required"),
|
||||||
|
sessionId: z.string().min(1, "Session ID is required")
|
||||||
|
});
|
||||||
|
|
||||||
const getRoleResourceAccessParamsSchema = z.strictObject({
|
const getRoleResourceAccessParamsSchema = z.strictObject({
|
||||||
roleId: z
|
roleId: z
|
||||||
.string()
|
.string()
|
||||||
@@ -178,6 +186,7 @@ export type ResourceWithAuth = {
|
|||||||
password: ResourcePassword | null;
|
password: ResourcePassword | null;
|
||||||
headerAuth: ResourceHeaderAuth | null;
|
headerAuth: ResourceHeaderAuth | null;
|
||||||
headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null;
|
headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null;
|
||||||
|
org: Org
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UserSessionWithUser = {
|
export type UserSessionWithUser = {
|
||||||
@@ -503,8 +512,12 @@ hybridRouter.get(
|
|||||||
)
|
)
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
resourceHeaderAuthExtendedCompatibility,
|
resourceHeaderAuthExtendedCompatibility,
|
||||||
eq(resourceHeaderAuthExtendedCompatibility.resourceId, resources.resourceId)
|
eq(
|
||||||
|
resourceHeaderAuthExtendedCompatibility.resourceId,
|
||||||
|
resources.resourceId
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
.innerJoin(orgs, eq(orgs.orgId, resources.orgId))
|
||||||
.where(eq(resources.fullDomain, domain))
|
.where(eq(resources.fullDomain, domain))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
@@ -538,7 +551,9 @@ hybridRouter.get(
|
|||||||
pincode: result.resourcePincode,
|
pincode: result.resourcePincode,
|
||||||
password: result.resourcePassword,
|
password: result.resourcePassword,
|
||||||
headerAuth: result.resourceHeaderAuth,
|
headerAuth: result.resourceHeaderAuth,
|
||||||
headerAuthExtendedCompatibility: result.resourceHeaderAuthExtendedCompatibility
|
headerAuthExtendedCompatibility:
|
||||||
|
result.resourceHeaderAuthExtendedCompatibility,
|
||||||
|
org: result.orgs
|
||||||
};
|
};
|
||||||
|
|
||||||
return response<ResourceWithAuth>(res, {
|
return response<ResourceWithAuth>(res, {
|
||||||
@@ -818,6 +833,69 @@ hybridRouter.get(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Get user organization role
|
||||||
|
hybridRouter.get(
|
||||||
|
"/user/:userId/org/:orgId/session/:sessionId/verify",
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const parsedParams = getUserOrgSessionVerifySchema.safeParse(
|
||||||
|
req.params
|
||||||
|
);
|
||||||
|
if (!parsedParams.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedParams.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { userId, orgId, sessionId } = parsedParams.data;
|
||||||
|
const remoteExitNode = req.remoteExitNode;
|
||||||
|
|
||||||
|
if (!remoteExitNode || !remoteExitNode.exitNodeId) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
"Remote exit node not found"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await checkExitNodeOrg(remoteExitNode.exitNodeId, orgId)) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.UNAUTHORIZED,
|
||||||
|
"User is not authorized to access this organization"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessPolicy = await checkOrgAccessPolicy({
|
||||||
|
orgId,
|
||||||
|
userId,
|
||||||
|
sessionId
|
||||||
|
});
|
||||||
|
|
||||||
|
return response(res, {
|
||||||
|
data: accessPolicy,
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "User org access policy retrieved successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to get user org role"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Check if role has access to resource
|
// Check if role has access to resource
|
||||||
hybridRouter.get(
|
hybridRouter.get(
|
||||||
"/role/:roleId/resource/:resourceId/access",
|
"/role/:roleId/resource/:resourceId/access",
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ async function query(query: Q) {
|
|||||||
// throw an error
|
// throw an error
|
||||||
throw createHttpError(
|
throw createHttpError(
|
||||||
HttpCode.BAD_REQUEST,
|
HttpCode.BAD_REQUEST,
|
||||||
|
// todo: is this even possible?
|
||||||
`Too many distinct countries. Please narrow your query.`
|
`Too many distinct countries. Please narrow your query.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -219,15 +219,17 @@ async function queryUniqueFilterAttributes(
|
|||||||
.limit(DISTINCT_LIMIT + 1)
|
.limit(DISTINCT_LIMIT + 1)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (
|
// TODO: for stuff like the paths this is too restrictive so lets just show some of the paths and the user needs to
|
||||||
uniqueActors.length > DISTINCT_LIMIT ||
|
// refine the time range to see what they need to see
|
||||||
uniqueLocations.length > DISTINCT_LIMIT ||
|
// if (
|
||||||
uniqueHosts.length > DISTINCT_LIMIT ||
|
// uniqueActors.length > DISTINCT_LIMIT ||
|
||||||
uniquePaths.length > DISTINCT_LIMIT ||
|
// uniqueLocations.length > DISTINCT_LIMIT ||
|
||||||
uniqueResources.length > DISTINCT_LIMIT
|
// uniqueHosts.length > DISTINCT_LIMIT ||
|
||||||
) {
|
// uniquePaths.length > DISTINCT_LIMIT ||
|
||||||
throw new Error("Too many distinct filter attributes to retrieve. Please refine your time range.");
|
// uniqueResources.length > DISTINCT_LIMIT
|
||||||
}
|
// ) {
|
||||||
|
// throw new Error("Too many distinct filter attributes to retrieve. Please refine your time range.");
|
||||||
|
// }
|
||||||
|
|
||||||
return {
|
return {
|
||||||
actors: uniqueActors
|
actors: uniqueActors
|
||||||
@@ -307,10 +309,12 @@ export async function queryRequestAuditLogs(
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(error);
|
logger.error(error);
|
||||||
// if the message is "Too many distinct filter attributes to retrieve. Please refine your time range.", return a 400 and the message
|
// if the message is "Too many distinct filter attributes to retrieve. Please refine your time range.", return a 400 and the message
|
||||||
if (error instanceof Error && error.message === "Too many distinct filter attributes to retrieve. Please refine your time range.") {
|
if (
|
||||||
return next(
|
error instanceof Error &&
|
||||||
createHttpError(HttpCode.BAD_REQUEST, error.message)
|
error.message ===
|
||||||
);
|
"Too many distinct filter attributes to retrieve. Please refine your time range."
|
||||||
|
) {
|
||||||
|
return next(createHttpError(HttpCode.BAD_REQUEST, error.message));
|
||||||
}
|
}
|
||||||
return next(
|
return next(
|
||||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
|
|||||||
@@ -62,7 +62,26 @@ export async function exchangeSession(
|
|||||||
cleanHost = cleanHost.slice(0, -1 * matched.length);
|
cleanHost = cleanHost.slice(0, -1 * matched.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
const clientIp = requestIp?.split(":")[0];
|
const clientIp = requestIp
|
||||||
|
? (() => {
|
||||||
|
if (requestIp.startsWith("[") && requestIp.includes("]")) {
|
||||||
|
const ipv6Match = requestIp.match(/\[(.*?)\]/);
|
||||||
|
if (ipv6Match) {
|
||||||
|
return ipv6Match[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}/;
|
||||||
|
if (ipv4Pattern.test(requestIp)) {
|
||||||
|
const lastColonIndex = requestIp.lastIndexOf(":");
|
||||||
|
if (lastColonIndex !== -1) {
|
||||||
|
return requestIp.substring(0, lastColonIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestIp;
|
||||||
|
})()
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const [resource] = await db
|
const [resource] = await db
|
||||||
.select()
|
.select()
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ import {
|
|||||||
LoginPage,
|
LoginPage,
|
||||||
Org,
|
Org,
|
||||||
Resource,
|
Resource,
|
||||||
ResourceHeaderAuth, ResourceHeaderAuthExtendedCompatibility,
|
ResourceHeaderAuth,
|
||||||
|
ResourceHeaderAuthExtendedCompatibility,
|
||||||
ResourcePassword,
|
ResourcePassword,
|
||||||
ResourcePincode,
|
ResourcePincode,
|
||||||
ResourceRule,
|
ResourceRule,
|
||||||
@@ -39,6 +40,8 @@ import {
|
|||||||
} from "#dynamic/lib/checkOrgAccessPolicy";
|
} from "#dynamic/lib/checkOrgAccessPolicy";
|
||||||
import { logRequestAudit } from "./logRequestAudit";
|
import { logRequestAudit } from "./logRequestAudit";
|
||||||
import cache from "@server/lib/cache";
|
import cache from "@server/lib/cache";
|
||||||
|
import semver from "semver";
|
||||||
|
import { APP_VERSION } from "@server/lib/consts";
|
||||||
|
|
||||||
const verifyResourceSessionSchema = z.object({
|
const verifyResourceSessionSchema = z.object({
|
||||||
sessions: z.record(z.string(), z.string()).optional(),
|
sessions: z.record(z.string(), z.string()).optional(),
|
||||||
@@ -50,7 +53,8 @@ const verifyResourceSessionSchema = z.object({
|
|||||||
path: z.string(),
|
path: z.string(),
|
||||||
method: z.string(),
|
method: z.string(),
|
||||||
tls: z.boolean(),
|
tls: z.boolean(),
|
||||||
requestIp: z.string().optional()
|
requestIp: z.string().optional(),
|
||||||
|
badgerVersion: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type VerifyResourceSessionSchema = z.infer<
|
export type VerifyResourceSessionSchema = z.infer<
|
||||||
@@ -69,6 +73,7 @@ export type VerifyUserResponse = {
|
|||||||
headerAuthChallenged?: boolean;
|
headerAuthChallenged?: boolean;
|
||||||
redirectUrl?: string;
|
redirectUrl?: string;
|
||||||
userData?: BasicUserData;
|
userData?: BasicUserData;
|
||||||
|
pangolinVersion?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function verifyResourceSession(
|
export async function verifyResourceSession(
|
||||||
@@ -97,7 +102,8 @@ export async function verifyResourceSession(
|
|||||||
requestIp,
|
requestIp,
|
||||||
path,
|
path,
|
||||||
headers,
|
headers,
|
||||||
query
|
query,
|
||||||
|
badgerVersion
|
||||||
} = parsedBody.data;
|
} = parsedBody.data;
|
||||||
|
|
||||||
// Extract HTTP Basic Auth credentials if present
|
// Extract HTTP Basic Auth credentials if present
|
||||||
@@ -105,7 +111,15 @@ export async function verifyResourceSession(
|
|||||||
|
|
||||||
const clientIp = requestIp
|
const clientIp = requestIp
|
||||||
? (() => {
|
? (() => {
|
||||||
logger.debug("Request IP:", { requestIp });
|
const isNewerBadger =
|
||||||
|
badgerVersion &&
|
||||||
|
semver.valid(badgerVersion) &&
|
||||||
|
semver.gte(badgerVersion, "1.3.1");
|
||||||
|
|
||||||
|
if (isNewerBadger) {
|
||||||
|
return requestIp;
|
||||||
|
}
|
||||||
|
|
||||||
if (requestIp.startsWith("[") && requestIp.includes("]")) {
|
if (requestIp.startsWith("[") && requestIp.includes("]")) {
|
||||||
// if brackets are found, extract the IPv6 address from between the brackets
|
// if brackets are found, extract the IPv6 address from between the brackets
|
||||||
const ipv6Match = requestIp.match(/\[(.*?)\]/);
|
const ipv6Match = requestIp.match(/\[(.*?)\]/);
|
||||||
@@ -114,12 +128,17 @@ export async function verifyResourceSession(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ivp4
|
// Check if it looks like IPv4 (contains dots and matches IPv4 pattern)
|
||||||
// split at last colon
|
// IPv4 format: x.x.x.x where x is 0-255
|
||||||
|
const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}/;
|
||||||
|
if (ipv4Pattern.test(requestIp)) {
|
||||||
const lastColonIndex = requestIp.lastIndexOf(":");
|
const lastColonIndex = requestIp.lastIndexOf(":");
|
||||||
if (lastColonIndex !== -1) {
|
if (lastColonIndex !== -1) {
|
||||||
return requestIp.substring(0, lastColonIndex);
|
return requestIp.substring(0, lastColonIndex);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return as is
|
||||||
return requestIp;
|
return requestIp;
|
||||||
})()
|
})()
|
||||||
: undefined;
|
: undefined;
|
||||||
@@ -130,9 +149,7 @@ export async function verifyResourceSession(
|
|||||||
? await getCountryCodeFromIp(clientIp)
|
? await getCountryCodeFromIp(clientIp)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const ipAsn = clientIp
|
const ipAsn = clientIp ? await getAsnFromIp(clientIp) : undefined;
|
||||||
? await getAsnFromIp(clientIp)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
let cleanHost = host;
|
let cleanHost = host;
|
||||||
// if the host ends with :port, strip it
|
// if the host ends with :port, strip it
|
||||||
@@ -178,7 +195,13 @@ export async function verifyResourceSession(
|
|||||||
cache.set(resourceCacheKey, resourceData, 5);
|
cache.set(resourceCacheKey, resourceData, 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { resource, pincode, password, headerAuth, headerAuthExtendedCompatibility } = resourceData;
|
const {
|
||||||
|
resource,
|
||||||
|
pincode,
|
||||||
|
password,
|
||||||
|
headerAuth,
|
||||||
|
headerAuthExtendedCompatibility
|
||||||
|
} = resourceData;
|
||||||
|
|
||||||
if (!resource) {
|
if (!resource) {
|
||||||
logger.debug(`Resource not found ${cleanHost}`);
|
logger.debug(`Resource not found ${cleanHost}`);
|
||||||
@@ -474,8 +497,7 @@ export async function verifyResourceSession(
|
|||||||
|
|
||||||
return notAllowed(res);
|
return notAllowed(res);
|
||||||
}
|
}
|
||||||
}
|
} else if (headerAuth) {
|
||||||
else if (headerAuth) {
|
|
||||||
// if there are no other auth methods we need to return unauthorized if nothing is provided
|
// if there are no other auth methods we need to return unauthorized if nothing is provided
|
||||||
if (
|
if (
|
||||||
!sso &&
|
!sso &&
|
||||||
@@ -713,7 +735,11 @@ export async function verifyResourceSession(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If headerAuthExtendedCompatibility is activated but no clientHeaderAuth provided, force client to challenge
|
// If headerAuthExtendedCompatibility is activated but no clientHeaderAuth provided, force client to challenge
|
||||||
if (headerAuthExtendedCompatibility && headerAuthExtendedCompatibility.extendedCompatibilityIsActivated && !clientHeaderAuth){
|
if (
|
||||||
|
headerAuthExtendedCompatibility &&
|
||||||
|
headerAuthExtendedCompatibility.extendedCompatibilityIsActivated &&
|
||||||
|
!clientHeaderAuth
|
||||||
|
) {
|
||||||
return headerAuthChallenged(res, redirectPath, resource.orgId);
|
return headerAuthChallenged(res, redirectPath, resource.orgId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -825,7 +851,7 @@ async function notAllowed(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
data: { valid: false, redirectUrl },
|
data: { valid: false, redirectUrl, pangolinVersion: APP_VERSION },
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
message: "Access denied",
|
message: "Access denied",
|
||||||
@@ -839,8 +865,8 @@ function allowed(res: Response, userData?: BasicUserData) {
|
|||||||
const data = {
|
const data = {
|
||||||
data:
|
data:
|
||||||
userData !== undefined && userData !== null
|
userData !== undefined && userData !== null
|
||||||
? { valid: true, ...userData }
|
? { valid: true, ...userData, pangolinVersion: APP_VERSION }
|
||||||
: { valid: true },
|
: { valid: true, pangolinVersion: APP_VERSION },
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
message: "Access allowed",
|
message: "Access allowed",
|
||||||
@@ -879,7 +905,12 @@ async function headerAuthChallenged(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
data: { headerAuthChallenged: true, valid: false, redirectUrl },
|
data: {
|
||||||
|
headerAuthChallenged: true,
|
||||||
|
valid: false,
|
||||||
|
redirectUrl,
|
||||||
|
pangolinVersion: APP_VERSION
|
||||||
|
},
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
message: "Access denied",
|
message: "Access denied",
|
||||||
|
|||||||
@@ -56,12 +56,12 @@ async function getLatestOlmVersion(): Promise<string | null> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tags = await response.json();
|
let tags = await response.json();
|
||||||
if (!Array.isArray(tags) || tags.length === 0) {
|
if (!Array.isArray(tags) || tags.length === 0) {
|
||||||
logger.warn("No tags found for Olm repository");
|
logger.warn("No tags found for Olm repository");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
tags = tags.filter((version) => !version.name.includes("rc"));
|
||||||
const latestVersion = tags[0].name;
|
const latestVersion = tags[0].name;
|
||||||
|
|
||||||
olmVersionCache.set("latestOlmVersion", latestVersion);
|
olmVersionCache.set("latestOlmVersion", latestVersion);
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export async function getConfig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// clean up the public key - keep only valid base64 characters (A-Z, a-z, 0-9, +, /, =)
|
// clean up the public key - keep only valid base64 characters (A-Z, a-z, 0-9, +, /, =)
|
||||||
const cleanedPublicKey = publicKey.replace(/[^A-Za-z0-9+/=]/g, '');
|
const cleanedPublicKey = publicKey.replace(/[^A-Za-z0-9+/=]/g, "");
|
||||||
|
|
||||||
const exitNode = await createExitNode(cleanedPublicKey, reachableAt);
|
const exitNode = await createExitNode(cleanedPublicKey, reachableAt);
|
||||||
|
|
||||||
|
|||||||
@@ -858,7 +858,6 @@ authenticated.put(
|
|||||||
blueprints.applyJSONBlueprint
|
blueprints.applyJSONBlueprint
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
authenticated.get(
|
authenticated.get(
|
||||||
"/org/:orgId/blueprint/:blueprintId",
|
"/org/:orgId/blueprint/:blueprintId",
|
||||||
verifyApiKeyOrgAccess,
|
verifyApiKeyOrgAccess,
|
||||||
@@ -866,7 +865,6 @@ authenticated.get(
|
|||||||
blueprints.getBlueprint
|
blueprints.getBlueprint
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
authenticated.get(
|
authenticated.get(
|
||||||
"/org/:orgId/blueprints",
|
"/org/:orgId/blueprints",
|
||||||
verifyApiKeyOrgAccess,
|
verifyApiKeyOrgAccess,
|
||||||
|
|||||||
@@ -21,8 +21,7 @@ export async function pickOrgDefaults(
|
|||||||
// const subnet = await getNextAvailableOrgSubnet();
|
// const subnet = await getNextAvailableOrgSubnet();
|
||||||
// Just hard code the subnet for now for everyone
|
// Just hard code the subnet for now for everyone
|
||||||
const subnet = config.getRawConfig().orgs.subnet_group;
|
const subnet = config.getRawConfig().orgs.subnet_group;
|
||||||
const utilitySubnet =
|
const utilitySubnet = config.getRawConfig().orgs.utility_subnet_group;
|
||||||
config.getRawConfig().orgs.utility_subnet_group;
|
|
||||||
|
|
||||||
return response<PickOrgDefaultsResponse>(res, {
|
return response<PickOrgDefaultsResponse>(res, {
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import {Request, Response, NextFunction} from "express";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
import {
|
||||||
db,
|
db,
|
||||||
resourceHeaderAuth, resourceHeaderAuthExtendedCompatibility,
|
resourceHeaderAuth,
|
||||||
|
resourceHeaderAuthExtendedCompatibility,
|
||||||
resourcePassword,
|
resourcePassword,
|
||||||
resourcePincode,
|
resourcePincode,
|
||||||
resources
|
resources
|
||||||
@@ -125,7 +126,8 @@ export async function getResourceAuthInfo(
|
|||||||
const pincode = result?.resourcePincode;
|
const pincode = result?.resourcePincode;
|
||||||
const password = result?.resourcePassword;
|
const password = result?.resourcePassword;
|
||||||
const headerAuth = result?.resourceHeaderAuth;
|
const headerAuth = result?.resourceHeaderAuth;
|
||||||
const headerAuthExtendedCompatibility = result?.resourceHeaderAuthExtendedCompatibility;
|
const headerAuthExtendedCompatibility =
|
||||||
|
result?.resourceHeaderAuthExtendedCompatibility;
|
||||||
|
|
||||||
const url = `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`;
|
const url = `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`;
|
||||||
|
|
||||||
@@ -138,7 +140,8 @@ export async function getResourceAuthInfo(
|
|||||||
password: password !== null,
|
password: password !== null,
|
||||||
pincode: pincode !== null,
|
pincode: pincode !== null,
|
||||||
headerAuth: headerAuth !== null,
|
headerAuth: headerAuth !== null,
|
||||||
headerAuthExtendedCompatibility: headerAuthExtendedCompatibility !== null,
|
headerAuthExtendedCompatibility:
|
||||||
|
headerAuthExtendedCompatibility !== null,
|
||||||
sso: resource.sso,
|
sso: resource.sso,
|
||||||
blockAccess: resource.blockAccess,
|
blockAccess: resource.blockAccess,
|
||||||
url,
|
url,
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {db, resourceHeaderAuth, resourceHeaderAuthExtendedCompatibility} from "@server/db";
|
import {
|
||||||
|
db,
|
||||||
|
resourceHeaderAuth,
|
||||||
|
resourceHeaderAuthExtendedCompatibility
|
||||||
|
} from "@server/db";
|
||||||
import {
|
import {
|
||||||
resources,
|
resources,
|
||||||
userResources,
|
userResources,
|
||||||
@@ -109,7 +113,8 @@ function queryResources(accessibleResourceIds: number[], orgId: string) {
|
|||||||
domainId: resources.domainId,
|
domainId: resources.domainId,
|
||||||
niceId: resources.niceId,
|
niceId: resources.niceId,
|
||||||
headerAuthId: resourceHeaderAuth.headerAuthId,
|
headerAuthId: resourceHeaderAuth.headerAuthId,
|
||||||
headerAuthExtendedCompatibilityId: resourceHeaderAuthExtendedCompatibility.headerAuthExtendedCompatibilityId,
|
headerAuthExtendedCompatibilityId:
|
||||||
|
resourceHeaderAuthExtendedCompatibility.headerAuthExtendedCompatibilityId,
|
||||||
targetId: targets.targetId,
|
targetId: targets.targetId,
|
||||||
targetIp: targets.ip,
|
targetIp: targets.ip,
|
||||||
targetPort: targets.port,
|
targetPort: targets.port,
|
||||||
@@ -133,7 +138,10 @@ function queryResources(accessibleResourceIds: number[], orgId: string) {
|
|||||||
)
|
)
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
resourceHeaderAuthExtendedCompatibility,
|
resourceHeaderAuthExtendedCompatibility,
|
||||||
eq(resourceHeaderAuthExtendedCompatibility.resourceId, resources.resourceId)
|
eq(
|
||||||
|
resourceHeaderAuthExtendedCompatibility.resourceId,
|
||||||
|
resources.resourceId
|
||||||
|
)
|
||||||
)
|
)
|
||||||
.leftJoin(targets, eq(targets.resourceId, resources.resourceId))
|
.leftJoin(targets, eq(targets.resourceId, resources.resourceId))
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {db, resourceHeaderAuth, resourceHeaderAuthExtendedCompatibility} from "@server/db";
|
import {
|
||||||
|
db,
|
||||||
|
resourceHeaderAuth,
|
||||||
|
resourceHeaderAuthExtendedCompatibility
|
||||||
|
} from "@server/db";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
@@ -74,10 +78,19 @@ export async function setResourceHeaderAuth(
|
|||||||
await trx
|
await trx
|
||||||
.delete(resourceHeaderAuth)
|
.delete(resourceHeaderAuth)
|
||||||
.where(eq(resourceHeaderAuth.resourceId, resourceId));
|
.where(eq(resourceHeaderAuth.resourceId, resourceId));
|
||||||
await trx.delete(resourceHeaderAuthExtendedCompatibility).where(eq(resourceHeaderAuthExtendedCompatibility.resourceId, resourceId));
|
await trx
|
||||||
|
.delete(resourceHeaderAuthExtendedCompatibility)
|
||||||
|
.where(
|
||||||
|
eq(
|
||||||
|
resourceHeaderAuthExtendedCompatibility.resourceId,
|
||||||
|
resourceId
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
if (user && password && extendedCompatibility !== null) {
|
if (user && password && extendedCompatibility !== null) {
|
||||||
const headerAuthHash = await hashPassword(Buffer.from(`${user}:${password}`).toString("base64"));
|
const headerAuthHash = await hashPassword(
|
||||||
|
Buffer.from(`${user}:${password}`).toString("base64")
|
||||||
|
);
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
trx
|
trx
|
||||||
@@ -85,11 +98,13 @@ export async function setResourceHeaderAuth(
|
|||||||
.values({ resourceId, headerAuthHash }),
|
.values({ resourceId, headerAuthHash }),
|
||||||
trx
|
trx
|
||||||
.insert(resourceHeaderAuthExtendedCompatibility)
|
.insert(resourceHeaderAuthExtendedCompatibility)
|
||||||
.values({resourceId, extendedCompatibilityIsActivated: extendedCompatibility})
|
.values({
|
||||||
|
resourceId,
|
||||||
|
extendedCompatibilityIsActivated:
|
||||||
|
extendedCompatibility
|
||||||
|
})
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
|
|||||||
@@ -7,4 +7,4 @@ export type GetMaintenanceInfoResponse = {
|
|||||||
maintenanceTitle: string | null;
|
maintenanceTitle: string | null;
|
||||||
maintenanceMessage: string | null;
|
maintenanceMessage: string | null;
|
||||||
maintenanceEstimatedTime: string | null;
|
maintenanceEstimatedTime: string | null;
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -343,7 +343,9 @@ async function updateHttpResource(
|
|||||||
|
|
||||||
const isLicensed = await isLicensedOrSubscribed(resource.orgId);
|
const isLicensed = await isLicensedOrSubscribed(resource.orgId);
|
||||||
if (build == "enterprise" && !isLicensed) {
|
if (build == "enterprise" && !isLicensed) {
|
||||||
logger.warn("Server is not licensed! Clearing set maintenance screen values");
|
logger.warn(
|
||||||
|
"Server is not licensed! Clearing set maintenance screen values"
|
||||||
|
);
|
||||||
// null the maintenance mode fields if not licensed
|
// null the maintenance mode fields if not licensed
|
||||||
updateData.maintenanceModeEnabled = undefined;
|
updateData.maintenanceModeEnabled = undefined;
|
||||||
updateData.maintenanceModeType = undefined;
|
updateData.maintenanceModeType = undefined;
|
||||||
|
|||||||
@@ -39,12 +39,12 @@ async function getLatestNewtVersion(): Promise<string | null> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tags = await response.json();
|
let tags = await response.json();
|
||||||
if (!Array.isArray(tags) || tags.length === 0) {
|
if (!Array.isArray(tags) || tags.length === 0) {
|
||||||
logger.warn("No tags found for Newt repository");
|
logger.warn("No tags found for Newt repository");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
tags = tags.filter((version) => !version.name.includes("rc"));
|
||||||
const latestVersion = tags[0].name;
|
const latestVersion = tags[0].name;
|
||||||
|
|
||||||
cache.set("latestNewtVersion", latestVersion);
|
cache.set("latestNewtVersion", latestVersion);
|
||||||
|
|||||||
@@ -11,7 +11,11 @@ import {
|
|||||||
userSiteResources
|
userSiteResources
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { getUniqueSiteResourceName } from "@server/db/names";
|
import { getUniqueSiteResourceName } from "@server/db/names";
|
||||||
import { getNextAvailableAliasAddress, isIpInCidr, portRangeStringSchema } from "@server/lib/ip";
|
import {
|
||||||
|
getNextAvailableAliasAddress,
|
||||||
|
isIpInCidr,
|
||||||
|
portRangeStringSchema
|
||||||
|
} from "@server/lib/ip";
|
||||||
import { rebuildClientAssociationsFromSiteResource } from "@server/lib/rebuildClientAssociations";
|
import { rebuildClientAssociationsFromSiteResource } from "@server/lib/rebuildClientAssociations";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
@@ -69,7 +73,10 @@ const createSiteResourceSchema = z
|
|||||||
const domainRegex =
|
const domainRegex =
|
||||||
/^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/;
|
/^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/;
|
||||||
const isValidDomain = domainRegex.test(data.destination);
|
const isValidDomain = domainRegex.test(data.destination);
|
||||||
const isValidAlias = data.alias !== undefined && data.alias !== null && data.alias.trim() !== "";
|
const isValidAlias =
|
||||||
|
data.alias !== undefined &&
|
||||||
|
data.alias !== null &&
|
||||||
|
data.alias.trim() !== "";
|
||||||
|
|
||||||
return isValidDomain && isValidAlias; // require the alias to be set in the case of domain
|
return isValidDomain && isValidAlias; // require the alias to be set in the case of domain
|
||||||
}
|
}
|
||||||
@@ -182,7 +189,9 @@ export async function createSiteResource(
|
|||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (!org) {
|
if (!org) {
|
||||||
return next(createHttpError(HttpCode.NOT_FOUND, "Organization not found"));
|
return next(
|
||||||
|
createHttpError(HttpCode.NOT_FOUND, "Organization not found")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!org.subnet || !org.utilitySubnet) {
|
if (!org.subnet || !org.utilitySubnet) {
|
||||||
@@ -195,10 +204,13 @@ export async function createSiteResource(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Only check if destination is an IP address
|
// Only check if destination is an IP address
|
||||||
const isIp = z.union([z.ipv4(), z.ipv6()]).safeParse(destination).success;
|
const isIp = z
|
||||||
|
.union([z.ipv4(), z.ipv6()])
|
||||||
|
.safeParse(destination).success;
|
||||||
if (
|
if (
|
||||||
isIp &&
|
isIp &&
|
||||||
(isIpInCidr(destination, org.subnet) || isIpInCidr(destination, org.utilitySubnet))
|
(isIpInCidr(destination, org.subnet) ||
|
||||||
|
isIpInCidr(destination, org.utilitySubnet))
|
||||||
) {
|
) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
|
|||||||
@@ -88,9 +88,7 @@ export async function deleteSiteResource(
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info(
|
logger.info(`Deleted site resource ${siteResourceId}`);
|
||||||
`Deleted site resource ${siteResourceId}`
|
|
||||||
);
|
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
data: { message: "Site resource deleted successfully" },
|
data: { message: "Site resource deleted successfully" },
|
||||||
|
|||||||
@@ -204,7 +204,9 @@ export async function updateSiteResource(
|
|||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (!org) {
|
if (!org) {
|
||||||
return next(createHttpError(HttpCode.NOT_FOUND, "Organization not found"));
|
return next(
|
||||||
|
createHttpError(HttpCode.NOT_FOUND, "Organization not found")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!org.subnet || !org.utilitySubnet) {
|
if (!org.subnet || !org.utilitySubnet) {
|
||||||
@@ -217,10 +219,13 @@ export async function updateSiteResource(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Only check if destination is an IP address
|
// Only check if destination is an IP address
|
||||||
const isIp = z.union([z.ipv4(), z.ipv6()]).safeParse(destination).success;
|
const isIp = z
|
||||||
|
.union([z.ipv4(), z.ipv6()])
|
||||||
|
.safeParse(destination).success;
|
||||||
if (
|
if (
|
||||||
isIp &&
|
isIp &&
|
||||||
(isIpInCidr(destination!, org.subnet) || isIpInCidr(destination!, org.utilitySubnet))
|
(isIpInCidr(destination!, org.subnet) ||
|
||||||
|
isIpInCidr(destination!, org.utilitySubnet))
|
||||||
) {
|
) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
@@ -295,7 +300,7 @@ export async function updateSiteResource(
|
|||||||
const [insertedSiteResource] = await trx
|
const [insertedSiteResource] = await trx
|
||||||
.insert(siteResources)
|
.insert(siteResources)
|
||||||
.values({
|
.values({
|
||||||
...existingSiteResource,
|
...existingSiteResource
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
@@ -517,9 +522,14 @@ export async function handleMessagingForUpdatedSiteResource(
|
|||||||
site: { siteId: number; orgId: string },
|
site: { siteId: number; orgId: string },
|
||||||
trx: Transaction
|
trx: Transaction
|
||||||
) {
|
) {
|
||||||
|
logger.debug(
|
||||||
logger.debug("handleMessagingForUpdatedSiteResource: existingSiteResource is: ", existingSiteResource);
|
"handleMessagingForUpdatedSiteResource: existingSiteResource is: ",
|
||||||
logger.debug("handleMessagingForUpdatedSiteResource: updatedSiteResource is: ", updatedSiteResource);
|
existingSiteResource
|
||||||
|
);
|
||||||
|
logger.debug(
|
||||||
|
"handleMessagingForUpdatedSiteResource: updatedSiteResource is: ",
|
||||||
|
updatedSiteResource
|
||||||
|
);
|
||||||
|
|
||||||
const { mergedAllClients } =
|
const { mergedAllClients } =
|
||||||
await rebuildClientAssociationsFromSiteResource(
|
await rebuildClientAssociationsFromSiteResource(
|
||||||
|
|||||||
@@ -60,11 +60,11 @@ export default async function migration() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await db.execute(
|
await db.execute(
|
||||||
sql`ALTER TABLE "siteResources" ADD COLUMN "tcpPortRangeString" varchar;`
|
sql`ALTER TABLE "siteResources" ADD COLUMN "tcpPortRangeString" varchar NOT NULL DEFAULT '*';`
|
||||||
);
|
);
|
||||||
|
|
||||||
await db.execute(
|
await db.execute(
|
||||||
sql`ALTER TABLE "siteResources" ADD COLUMN "udpPortRangeString" varchar;`
|
sql`ALTER TABLE "siteResources" ADD COLUMN "udpPortRangeString" varchar NOT NULL DEFAULT '*';`
|
||||||
);
|
);
|
||||||
|
|
||||||
await db.execute(
|
await db.execute(
|
||||||
|
|||||||
@@ -73,16 +73,18 @@ export default async function migration() {
|
|||||||
).run();
|
).run();
|
||||||
|
|
||||||
db.prepare(
|
db.prepare(
|
||||||
`ALTER TABLE 'siteResources' ADD 'tcpPortRangeString' text;`
|
`ALTER TABLE 'siteResources' ADD 'tcpPortRangeString' text DEFAULT '*' NOT NULL;`
|
||||||
).run();
|
).run();
|
||||||
|
|
||||||
db.prepare(
|
db.prepare(
|
||||||
`ALTER TABLE 'siteResources' ADD 'udpPortRangeString' text;`
|
`ALTER TABLE 'siteResources' ADD 'udpPortRangeString' text DEFAULT '*' NOT NULL;`
|
||||||
).run();
|
).run();
|
||||||
|
|
||||||
db.prepare(
|
db.prepare(
|
||||||
`ALTER TABLE 'siteResources' ADD 'disableIcmp' integer;`
|
`ALTER TABLE 'siteResources' ADD 'disableIcmp' integer NOT NULL DEFAULT false;`
|
||||||
).run();
|
).run();
|
||||||
|
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
db.pragma("foreign_keys = ON");
|
db.pragma("foreign_keys = ON");
|
||||||
|
|||||||
@@ -71,7 +71,9 @@ export default async function GeneralSettingsPage({
|
|||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<OrgInfoCard />
|
<OrgInfoCard />
|
||||||
<HorizontalTabs items={navItems}>{children}</HorizontalTabs>
|
<HorizontalTabs items={navItems}>
|
||||||
|
{children}
|
||||||
|
</HorizontalTabs>
|
||||||
</div>
|
</div>
|
||||||
</OrgUserProvider>
|
</OrgUserProvider>
|
||||||
</OrgProvider>
|
</OrgProvider>
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export default async function ClientResourcesPage(
|
|||||||
niceId: siteResource.niceId,
|
niceId: siteResource.niceId,
|
||||||
tcpPortRangeString: siteResource.tcpPortRangeString || null,
|
tcpPortRangeString: siteResource.tcpPortRangeString || null,
|
||||||
udpPortRangeString: siteResource.udpPortRangeString || null,
|
udpPortRangeString: siteResource.udpPortRangeString || null,
|
||||||
disableIcmp: siteResource.disableIcmp || false,
|
disableIcmp: siteResource.disableIcmp || false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import { SwitchInput } from "@app/components/SwitchInput";
|
|||||||
import { Tag, TagInput } from "@app/components/tags/tag-input";
|
import { Tag, TagInput } from "@app/components/tags/tag-input";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
import { CheckboxWithLabel } from "@app/components/ui/checkbox";
|
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@@ -184,9 +183,6 @@ export default function ResourceAuthenticationPage() {
|
|||||||
|
|
||||||
const [ssoEnabled, setSsoEnabled] = useState(resource.sso);
|
const [ssoEnabled, setSsoEnabled] = useState(resource.sso);
|
||||||
|
|
||||||
const [autoLoginEnabled, setAutoLoginEnabled] = useState(
|
|
||||||
resource.skipToIdpId !== null && resource.skipToIdpId !== undefined
|
|
||||||
);
|
|
||||||
const [selectedIdpId, setSelectedIdpId] = useState<number | null>(
|
const [selectedIdpId, setSelectedIdpId] = useState<number | null>(
|
||||||
resource.skipToIdpId || null
|
resource.skipToIdpId || null
|
||||||
);
|
);
|
||||||
@@ -243,19 +239,8 @@ export default function ResourceAuthenticationPage() {
|
|||||||
text: w.email
|
text: w.email
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
if (autoLoginEnabled && !selectedIdpId && orgIdps.length > 0) {
|
|
||||||
setSelectedIdpId(orgIdps[0].idpId);
|
|
||||||
}
|
|
||||||
hasInitializedRef.current = true;
|
hasInitializedRef.current = true;
|
||||||
}, [
|
}, [pageLoading, resourceRoles, resourceUsers, whitelist, orgIdps]);
|
||||||
pageLoading,
|
|
||||||
resourceRoles,
|
|
||||||
resourceUsers,
|
|
||||||
whitelist,
|
|
||||||
autoLoginEnabled,
|
|
||||||
selectedIdpId,
|
|
||||||
orgIdps
|
|
||||||
]);
|
|
||||||
|
|
||||||
const [, submitUserRolesForm, loadingSaveUsersRoles] = useActionState(
|
const [, submitUserRolesForm, loadingSaveUsersRoles] = useActionState(
|
||||||
onSubmitUsersRoles,
|
onSubmitUsersRoles,
|
||||||
@@ -269,16 +254,6 @@ export default function ResourceAuthenticationPage() {
|
|||||||
const data = usersRolesForm.getValues();
|
const data = usersRolesForm.getValues();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Validate that an IDP is selected if auto login is enabled
|
|
||||||
if (autoLoginEnabled && !selectedIdpId) {
|
|
||||||
toast({
|
|
||||||
variant: "destructive",
|
|
||||||
title: t("error"),
|
|
||||||
description: t("selectIdpRequired")
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const jobs = [
|
const jobs = [
|
||||||
api.post(`/resource/${resource.resourceId}/roles`, {
|
api.post(`/resource/${resource.resourceId}/roles`, {
|
||||||
roleIds: data.roles.map((i) => parseInt(i.id))
|
roleIds: data.roles.map((i) => parseInt(i.id))
|
||||||
@@ -288,7 +263,7 @@ export default function ResourceAuthenticationPage() {
|
|||||||
}),
|
}),
|
||||||
api.post(`/resource/${resource.resourceId}`, {
|
api.post(`/resource/${resource.resourceId}`, {
|
||||||
sso: ssoEnabled,
|
sso: ssoEnabled,
|
||||||
skipToIdpId: autoLoginEnabled ? selectedIdpId : null
|
skipToIdpId: selectedIdpId
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -296,7 +271,7 @@ export default function ResourceAuthenticationPage() {
|
|||||||
|
|
||||||
updateResource({
|
updateResource({
|
||||||
sso: ssoEnabled,
|
sso: ssoEnabled,
|
||||||
skipToIdpId: autoLoginEnabled ? selectedIdpId : null
|
skipToIdpId: selectedIdpId
|
||||||
});
|
});
|
||||||
|
|
||||||
updateAuthInfo({
|
updateAuthInfo({
|
||||||
@@ -619,59 +594,24 @@ export default function ResourceAuthenticationPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{ssoEnabled && allIdps.length > 0 && (
|
{ssoEnabled && allIdps.length > 0 && (
|
||||||
<>
|
<div className="space-y-2">
|
||||||
<div className="space-y-2 mb-3">
|
<label className="text-sm font-medium">
|
||||||
<CheckboxWithLabel
|
{t("defaultIdentityProvider")}
|
||||||
label={t(
|
</label>
|
||||||
"autoLoginExternalIdp"
|
<Select
|
||||||
)}
|
onValueChange={(value) => {
|
||||||
checked={autoLoginEnabled}
|
if (value === "none") {
|
||||||
onCheckedChange={(
|
setSelectedIdpId(null);
|
||||||
checked
|
|
||||||
) => {
|
|
||||||
setAutoLoginEnabled(
|
|
||||||
checked as boolean
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
checked &&
|
|
||||||
allIdps.length > 0
|
|
||||||
) {
|
|
||||||
setSelectedIdpId(
|
|
||||||
allIdps[0].id
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
setSelectedIdpId(
|
setSelectedIdpId(
|
||||||
null
|
parseInt(value)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
{t(
|
|
||||||
"autoLoginExternalIdpDescription"
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{autoLoginEnabled && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-sm font-medium">
|
|
||||||
{t(
|
|
||||||
"defaultIdentityProvider"
|
|
||||||
)}
|
|
||||||
</label>
|
|
||||||
<Select
|
|
||||||
onValueChange={(
|
|
||||||
value
|
|
||||||
) =>
|
|
||||||
setSelectedIdpId(
|
|
||||||
parseInt(value)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
value={
|
value={
|
||||||
selectedIdpId
|
selectedIdpId
|
||||||
? selectedIdpId.toString()
|
? selectedIdpId.toString()
|
||||||
: undefined
|
: "none"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-full mt-1">
|
<SelectTrigger className="w-full mt-1">
|
||||||
@@ -682,25 +622,25 @@ export default function ResourceAuthenticationPage() {
|
|||||||
/>
|
/>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{allIdps.map(
|
<SelectItem value="none">
|
||||||
(idp) => (
|
{t("none")}
|
||||||
|
</SelectItem>
|
||||||
|
{allIdps.map((idp) => (
|
||||||
<SelectItem
|
<SelectItem
|
||||||
key={
|
key={idp.id}
|
||||||
idp.id
|
|
||||||
}
|
|
||||||
value={idp.id.toString()}
|
value={idp.id.toString()}
|
||||||
>
|
>
|
||||||
{
|
{idp.text}
|
||||||
idp.text
|
|
||||||
}
|
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
)
|
))}
|
||||||
)}
|
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{t(
|
||||||
|
"defaultIdentityProviderDescription"
|
||||||
)}
|
)}
|
||||||
</>
|
</p>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -118,8 +118,7 @@ export default function ResourceRules(props: {
|
|||||||
const [countrySelectValue, setCountrySelectValue] = useState("");
|
const [countrySelectValue, setCountrySelectValue] = useState("");
|
||||||
const [openAddRuleCountrySelect, setOpenAddRuleCountrySelect] =
|
const [openAddRuleCountrySelect, setOpenAddRuleCountrySelect] =
|
||||||
useState(false);
|
useState(false);
|
||||||
const [openAddRuleAsnSelect, setOpenAddRuleAsnSelect] =
|
const [openAddRuleAsnSelect, setOpenAddRuleAsnSelect] = useState(false);
|
||||||
useState(false);
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const { env } = useEnvContext();
|
const { env } = useEnvContext();
|
||||||
@@ -542,7 +541,11 @@ export default function ResourceRules(props: {
|
|||||||
updateRule(row.original.ruleId, {
|
updateRule(row.original.ruleId, {
|
||||||
match: value,
|
match: value,
|
||||||
value:
|
value:
|
||||||
value === "COUNTRY" ? "US" : value === "ASN" ? "AS15169" : row.original.value
|
value === "COUNTRY"
|
||||||
|
? "US"
|
||||||
|
: value === "ASN"
|
||||||
|
? "AS15169"
|
||||||
|
: row.original.value
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -559,9 +562,7 @@ export default function ResourceRules(props: {
|
|||||||
</SelectItem>
|
</SelectItem>
|
||||||
)}
|
)}
|
||||||
{isMaxmindAsnAvailable && (
|
{isMaxmindAsnAvailable && (
|
||||||
<SelectItem value="ASN">
|
<SelectItem value="ASN">{RuleMatch.ASN}</SelectItem>
|
||||||
{RuleMatch.ASN}
|
|
||||||
</SelectItem>
|
|
||||||
)}
|
)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
@@ -654,9 +655,7 @@ export default function ResourceRules(props: {
|
|||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="min-w-[200px] p-0">
|
<PopoverContent className="min-w-[200px] p-0">
|
||||||
<Command>
|
<Command>
|
||||||
<CommandInput
|
<CommandInput placeholder="Search ASNs or enter custom..." />
|
||||||
placeholder="Search ASNs or enter custom..."
|
|
||||||
/>
|
|
||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandEmpty>
|
<CommandEmpty>
|
||||||
No ASN found. Enter a custom ASN below.
|
No ASN found. Enter a custom ASN below.
|
||||||
@@ -665,7 +664,9 @@ export default function ResourceRules(props: {
|
|||||||
{MAJOR_ASNS.map((asn) => (
|
{MAJOR_ASNS.map((asn) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={asn.code}
|
key={asn.code}
|
||||||
value={asn.name + " " + asn.code}
|
value={
|
||||||
|
asn.name + " " + asn.code
|
||||||
|
}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
updateRule(
|
updateRule(
|
||||||
row.original.ruleId,
|
row.original.ruleId,
|
||||||
@@ -1088,19 +1089,25 @@ export default function ResourceRules(props: {
|
|||||||
?.name +
|
?.name +
|
||||||
" (" +
|
" (" +
|
||||||
field.value +
|
field.value +
|
||||||
")" || field.value
|
")" ||
|
||||||
|
field.value
|
||||||
: "Select ASN"}
|
: "Select ASN"}
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-full p-0">
|
<PopoverContent className="w-full p-0">
|
||||||
<Command>
|
<Command>
|
||||||
<CommandInput
|
<CommandInput placeholder="Search ASNs or enter custom..." />
|
||||||
placeholder="Search ASNs or enter custom..."
|
|
||||||
/>
|
|
||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandEmpty>
|
<CommandEmpty>
|
||||||
No ASN found. Use the custom input below.
|
No
|
||||||
|
ASN
|
||||||
|
found.
|
||||||
|
Use
|
||||||
|
the
|
||||||
|
custom
|
||||||
|
input
|
||||||
|
below.
|
||||||
</CommandEmpty>
|
</CommandEmpty>
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
{MAJOR_ASNS.map(
|
{MAJOR_ASNS.map(
|
||||||
@@ -1112,7 +1119,9 @@ export default function ResourceRules(props: {
|
|||||||
asn.code
|
asn.code
|
||||||
}
|
}
|
||||||
value={
|
value={
|
||||||
asn.name + " " + asn.code
|
asn.name +
|
||||||
|
" " +
|
||||||
|
asn.code
|
||||||
}
|
}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
field.onChange(
|
field.onChange(
|
||||||
@@ -1138,6 +1147,7 @@ export default function ResourceRules(props: {
|
|||||||
{
|
{
|
||||||
asn.code
|
asn.code
|
||||||
}
|
}
|
||||||
|
|
||||||
)
|
)
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
)
|
)
|
||||||
@@ -1148,14 +1158,32 @@ export default function ResourceRules(props: {
|
|||||||
<div className="border-t p-2">
|
<div className="border-t p-2">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Enter custom ASN (e.g., AS15169)"
|
placeholder="Enter custom ASN (e.g., AS15169)"
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(
|
||||||
if (e.key === "Enter") {
|
e
|
||||||
const value = e.currentTarget.value
|
) => {
|
||||||
|
if (
|
||||||
|
e.key ===
|
||||||
|
"Enter"
|
||||||
|
) {
|
||||||
|
const value =
|
||||||
|
e.currentTarget.value
|
||||||
.toUpperCase()
|
.toUpperCase()
|
||||||
.replace(/^AS/, "");
|
.replace(
|
||||||
if (/^\d+$/.test(value)) {
|
/^AS/,
|
||||||
field.onChange("AS" + value);
|
""
|
||||||
setOpenAddRuleAsnSelect(false);
|
);
|
||||||
|
if (
|
||||||
|
/^\d+$/.test(
|
||||||
|
value
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
field.onChange(
|
||||||
|
"AS" +
|
||||||
|
value
|
||||||
|
);
|
||||||
|
setOpenAddRuleAsnSelect(
|
||||||
|
false
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -756,7 +756,9 @@ WantedBy=default.target`
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="md:col-start-1 md:col-span-2">
|
<FormItem className="md:col-start-1 md:col-span-2">
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{t("siteAddress")}
|
{t(
|
||||||
|
"siteAddress"
|
||||||
|
)}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
@@ -909,7 +911,7 @@ WantedBy=default.target`
|
|||||||
? "squareOutlinePrimary"
|
? "squareOutlinePrimary"
|
||||||
: "squareOutline"
|
: "squareOutline"
|
||||||
}
|
}
|
||||||
className={`flex-1 min-w-[120px] ${platform === os ? "bg-primary/10" : ""} shadow-none`}
|
className={`flex-1 min-w-30 ${platform === os ? "bg-primary/10" : ""} shadow-none`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPlatform(os);
|
setPlatform(os);
|
||||||
}}
|
}}
|
||||||
@@ -940,7 +942,7 @@ WantedBy=default.target`
|
|||||||
? "squareOutlinePrimary"
|
? "squareOutlinePrimary"
|
||||||
: "squareOutline"
|
: "squareOutline"
|
||||||
}
|
}
|
||||||
className={`flex-1 min-w-[120px] ${architecture === arch ? "bg-primary/10" : ""} shadow-none`}
|
className={`flex-1 min-w-30 ${architecture === arch ? "bg-primary/10" : ""} shadow-none`}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setArchitecture(
|
setArchitecture(
|
||||||
arch
|
arch
|
||||||
|
|||||||
@@ -87,8 +87,6 @@ export default async function OrgAuthPage(props: {
|
|||||||
redirect(env.app.dashboardUrl);
|
redirect(env.app.dashboardUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(user, forceLogin);
|
|
||||||
|
|
||||||
if (user && !forceLogin) {
|
if (user && !forceLogin) {
|
||||||
let redirectToken: string | undefined;
|
let redirectToken: string | undefined;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -66,4 +66,3 @@ export const ClientDownloadBanner = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default ClientDownloadBanner;
|
export default ClientDownloadBanner;
|
||||||
|
|
||||||
|
|||||||
@@ -99,9 +99,7 @@ export default function ClientResourcesTable({
|
|||||||
siteId: number
|
siteId: number
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
await api
|
await api.delete(`/site-resource/${resourceId}`).then(() => {
|
||||||
.delete(`/site-resource/${resourceId}`)
|
|
||||||
.then(() => {
|
|
||||||
startTransition(() => {
|
startTransition(() => {
|
||||||
router.refresh();
|
router.refresh();
|
||||||
setIsDeleteModalOpen(false);
|
setIsDeleteModalOpen(false);
|
||||||
|
|||||||
@@ -87,7 +87,12 @@ const isValidPortRangeString = (val: string | undefined | null): boolean => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (startPort < 1 || startPort > 65535 || endPort < 1 || endPort > 65535) {
|
if (
|
||||||
|
startPort < 1 ||
|
||||||
|
startPort > 65535 ||
|
||||||
|
endPort < 1 ||
|
||||||
|
endPort > 65535
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +136,10 @@ const getPortModeFromString = (val: string | undefined | null): PortMode => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Helper to get the port string for API from mode and custom value
|
// Helper to get the port string for API from mode and custom value
|
||||||
const getPortStringFromMode = (mode: PortMode, customValue: string): string | undefined => {
|
const getPortStringFromMode = (
|
||||||
|
mode: PortMode,
|
||||||
|
customValue: string
|
||||||
|
): string | undefined => {
|
||||||
if (mode === "all") return "*";
|
if (mode === "all") return "*";
|
||||||
if (mode === "blocked") return "";
|
if (mode === "blocked") return "";
|
||||||
return customValue;
|
return customValue;
|
||||||
@@ -1097,8 +1105,7 @@ export default function CreateInternalResourceDialog({
|
|||||||
size="sm"
|
size="sm"
|
||||||
tags={
|
tags={
|
||||||
form.getValues()
|
form.getValues()
|
||||||
.roles ||
|
.roles || []
|
||||||
[]
|
|
||||||
}
|
}
|
||||||
setTags={(
|
setTags={(
|
||||||
newRoles
|
newRoles
|
||||||
@@ -1154,8 +1161,7 @@ export default function CreateInternalResourceDialog({
|
|||||||
)}
|
)}
|
||||||
tags={
|
tags={
|
||||||
form.getValues()
|
form.getValues()
|
||||||
.users ||
|
.users || []
|
||||||
[]
|
|
||||||
}
|
}
|
||||||
size="sm"
|
size="sm"
|
||||||
setTags={(
|
setTags={(
|
||||||
@@ -1245,9 +1251,7 @@ export default function CreateInternalResourceDialog({
|
|||||||
restrictTagsToAutocompleteOptions={
|
restrictTagsToAutocompleteOptions={
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
sortTags={
|
sortTags={true}
|
||||||
true
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|||||||
@@ -95,4 +95,3 @@ export const DismissableBanner = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default DismissableBanner;
|
export default DismissableBanner;
|
||||||
|
|
||||||
|
|||||||
@@ -501,13 +501,19 @@ export default function EditInternalResourceDialog({
|
|||||||
// ]);
|
// ]);
|
||||||
|
|
||||||
await queryClient.invalidateQueries(
|
await queryClient.invalidateQueries(
|
||||||
resourceQueries.siteResourceRoles({ siteResourceId: resource.id })
|
resourceQueries.siteResourceRoles({
|
||||||
|
siteResourceId: resource.id
|
||||||
|
})
|
||||||
);
|
);
|
||||||
await queryClient.invalidateQueries(
|
await queryClient.invalidateQueries(
|
||||||
resourceQueries.siteResourceUsers({ siteResourceId: resource.id })
|
resourceQueries.siteResourceUsers({
|
||||||
|
siteResourceId: resource.id
|
||||||
|
})
|
||||||
);
|
);
|
||||||
await queryClient.invalidateQueries(
|
await queryClient.invalidateQueries(
|
||||||
resourceQueries.siteResourceClients({ siteResourceId: resource.id })
|
resourceQueries.siteResourceClients({
|
||||||
|
siteResourceId: resource.id
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
|
|||||||
@@ -330,7 +330,7 @@ export default function ExitNodesTable({
|
|||||||
isRefreshing={isRefreshing}
|
isRefreshing={isRefreshing}
|
||||||
columnVisibility={{
|
columnVisibility={{
|
||||||
type: false,
|
type: false,
|
||||||
address: false,
|
address: false
|
||||||
}}
|
}}
|
||||||
enableColumnVisibility={true}
|
enableColumnVisibility={true}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -116,10 +116,12 @@ export function LayoutSidebar({
|
|||||||
isCollapsed={isSidebarCollapsed}
|
isCollapsed={isSidebarCollapsed}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={cn(
|
<div
|
||||||
|
className={cn(
|
||||||
"w-full border-b border-border",
|
"w-full border-b border-border",
|
||||||
isSidebarCollapsed && "mb-2"
|
isSidebarCollapsed && "mb-2"
|
||||||
)} />
|
)}
|
||||||
|
/>
|
||||||
<div className="flex-1 overflow-y-auto relative">
|
<div className="flex-1 overflow-y-auto relative">
|
||||||
<div className="px-2 pt-1">
|
<div className="px-2 pt-1">
|
||||||
{!isAdminPage && user.serverAdmin && (
|
{!isAdminPage && user.serverAdmin && (
|
||||||
|
|||||||
@@ -120,7 +120,9 @@ export default function LoginForm({
|
|||||||
const focusInput = () => {
|
const focusInput = () => {
|
||||||
// Try using the ref first
|
// Try using the ref first
|
||||||
if (otpContainerRef.current) {
|
if (otpContainerRef.current) {
|
||||||
const hiddenInput = otpContainerRef.current.querySelector('input') as HTMLInputElement;
|
const hiddenInput = otpContainerRef.current.querySelector(
|
||||||
|
"input"
|
||||||
|
) as HTMLInputElement;
|
||||||
if (hiddenInput) {
|
if (hiddenInput) {
|
||||||
hiddenInput.focus();
|
hiddenInput.focus();
|
||||||
return;
|
return;
|
||||||
@@ -128,17 +130,23 @@ export default function LoginForm({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: query the DOM
|
// Fallback: query the DOM
|
||||||
const otpContainer = document.querySelector('[data-slot="input-otp"]');
|
const otpContainer = document.querySelector(
|
||||||
|
'[data-slot="input-otp"]'
|
||||||
|
);
|
||||||
if (!otpContainer) return;
|
if (!otpContainer) return;
|
||||||
|
|
||||||
const hiddenInput = otpContainer.querySelector('input') as HTMLInputElement;
|
const hiddenInput = otpContainer.querySelector(
|
||||||
|
"input"
|
||||||
|
) as HTMLInputElement;
|
||||||
if (hiddenInput) {
|
if (hiddenInput) {
|
||||||
hiddenInput.focus();
|
hiddenInput.focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Last resort: click the first slot
|
// Last resort: click the first slot
|
||||||
const firstSlot = otpContainer.querySelector('[data-slot="input-otp-slot"]') as HTMLElement;
|
const firstSlot = otpContainer.querySelector(
|
||||||
|
'[data-slot="input-otp-slot"]'
|
||||||
|
) as HTMLElement;
|
||||||
if (firstSlot) {
|
if (firstSlot) {
|
||||||
firstSlot.click();
|
firstSlot.click();
|
||||||
}
|
}
|
||||||
@@ -508,7 +516,10 @@ export default function LoginForm({
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<div ref={otpContainerRef} className="flex justify-center">
|
<div
|
||||||
|
ref={otpContainerRef}
|
||||||
|
className="flex justify-center"
|
||||||
|
>
|
||||||
<InputOTP
|
<InputOTP
|
||||||
maxLength={6}
|
maxLength={6}
|
||||||
{...field}
|
{...field}
|
||||||
|
|||||||
@@ -11,9 +11,7 @@ type MachineClientsBannerProps = {
|
|||||||
orgId: string;
|
orgId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MachineClientsBanner = ({
|
export const MachineClientsBanner = ({ orgId }: MachineClientsBannerProps) => {
|
||||||
orgId
|
|
||||||
}: MachineClientsBannerProps) => {
|
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -57,4 +55,3 @@ export const MachineClientsBanner = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default MachineClientsBanner;
|
export default MachineClientsBanner;
|
||||||
|
|
||||||
|
|||||||
@@ -39,4 +39,3 @@ export default function OrgInfoCard({}: OrgInfoCardProps) {
|
|||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -119,4 +119,3 @@ export default async function OrgLoginPage({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export function OrgSelectionForm() {
|
|||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
{t("orgAuthWhatsThis")}{" "}
|
{t("orgAuthWhatsThis")}{" "}
|
||||||
<Link
|
<Link
|
||||||
href="https://docs.pangolin.net/manage/identity-providers/add-an-idp"
|
href="https://docs.pangolin.net/manage/organizations/org-id"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="underline"
|
className="underline"
|
||||||
|
|||||||
@@ -51,4 +51,3 @@ export const PrivateResourcesBanner = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default PrivateResourcesBanner;
|
export default PrivateResourcesBanner;
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,10 @@ export default function ProductUpdates({
|
|||||||
|
|
||||||
const data = useQueries({
|
const data = useQueries({
|
||||||
queries: [
|
queries: [
|
||||||
productUpdatesQueries.list(env.app.notifications.product_updates),
|
productUpdatesQueries.list(
|
||||||
|
env.app.notifications.product_updates,
|
||||||
|
env.app.version
|
||||||
|
),
|
||||||
productUpdatesQueries.latestVersion(
|
productUpdatesQueries.latestVersion(
|
||||||
env.app.notifications.new_releases
|
env.app.notifications.new_releases
|
||||||
)
|
)
|
||||||
@@ -189,7 +192,7 @@ function ProductUpdatesListPopup({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative z-1 cursor-pointer block group",
|
"relative z-1 cursor-pointer block group",
|
||||||
"rounded-md border border-primary/30 bg-gradient-to-br dark:from-primary/20 from-primary/20 via-background to-background p-2 py-3 w-full flex flex-col gap-2 text-sm",
|
"rounded-md border border-primary/30 bg-linear-to-br dark:from-primary/20 from-primary/20 via-background to-background p-2 py-3 w-full flex flex-col gap-2 text-sm",
|
||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out",
|
||||||
"data-closed:opacity-0 data-closed:translate-y-full"
|
"data-closed:opacity-0 data-closed:translate-y-full"
|
||||||
)}
|
)}
|
||||||
@@ -343,7 +346,7 @@ function NewVersionAvailable({
|
|||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative z-2 group cursor-pointer block",
|
"relative z-2 group cursor-pointer block",
|
||||||
"rounded-md border border-primary/30 bg-gradient-to-br dark:from-primary/20 from-primary/20 via-background to-background p-2 py-3 w-full flex flex-col gap-2 text-sm",
|
"rounded-md border border-primary/30 bg-linear-to-br dark:from-primary/20 from-primary/20 via-background to-background p-2 py-3 w-full flex flex-col gap-2 text-sm",
|
||||||
"transition duration-300 ease-in-out",
|
"transition duration-300 ease-in-out",
|
||||||
"data-closed:opacity-0 data-closed:translate-y-full"
|
"data-closed:opacity-0 data-closed:translate-y-full"
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -20,4 +20,3 @@ export const ProxyResourcesBanner = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default ProxyResourcesBanner;
|
export default ProxyResourcesBanner;
|
||||||
|
|
||||||
|
|||||||
@@ -82,11 +82,14 @@ export default function SetResourceHeaderAuthForm({
|
|||||||
async function onSubmit(data: SetHeaderAuthFormValues) {
|
async function onSubmit(data: SetHeaderAuthFormValues) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
api.post<AxiosResponse<Resource>>(`/resource/${resourceId}/header-auth`, {
|
api.post<AxiosResponse<Resource>>(
|
||||||
|
`/resource/${resourceId}/header-auth`,
|
||||||
|
{
|
||||||
user: data.user,
|
user: data.user,
|
||||||
password: data.password,
|
password: data.password,
|
||||||
extendedCompatibility: data.extendedCompatibility
|
extendedCompatibility: data.extendedCompatibility
|
||||||
})
|
}
|
||||||
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast({
|
toast({
|
||||||
title: t("resourceHeaderAuthSetup"),
|
title: t("resourceHeaderAuthSetup"),
|
||||||
@@ -100,10 +103,10 @@ export default function SetResourceHeaderAuthForm({
|
|||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: t('resourceErrorHeaderAuthSetup'),
|
title: t("resourceErrorHeaderAuthSetup"),
|
||||||
description: formatAxiosError(
|
description: formatAxiosError(
|
||||||
e,
|
e,
|
||||||
t('resourceErrorHeaderAuthSetupDescription')
|
t("resourceErrorHeaderAuthSetupDescription")
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@@ -180,10 +183,16 @@ export default function SetResourceHeaderAuthForm({
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<SwitchInput
|
<SwitchInput
|
||||||
id="header-auth-compatibility-toggle"
|
id="header-auth-compatibility-toggle"
|
||||||
label={t("headerAuthCompatibility")}
|
label={t(
|
||||||
info={t('headerAuthCompatibilityInfo')}
|
"headerAuthCompatibility"
|
||||||
|
)}
|
||||||
|
info={t(
|
||||||
|
"headerAuthCompatibilityInfo"
|
||||||
|
)}
|
||||||
checked={field.value}
|
checked={field.value}
|
||||||
onCheckedChange={field.onChange}
|
onCheckedChange={
|
||||||
|
field.onChange
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|||||||
@@ -91,10 +91,10 @@ export default function SetResourcePasswordForm({
|
|||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: t('resourceErrorPasswordSetup'),
|
title: t("resourceErrorPasswordSetup"),
|
||||||
description: formatAxiosError(
|
description: formatAxiosError(
|
||||||
e,
|
e,
|
||||||
t('resourceErrorPasswordSetupDescription')
|
t("resourceErrorPasswordSetupDescription")
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -97,10 +97,10 @@ export default function SetResourcePincodeForm({
|
|||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: t('resourceErrorPincodeSetup'),
|
title: t("resourceErrorPincodeSetup"),
|
||||||
description: formatAxiosError(
|
description: formatAxiosError(
|
||||||
e,
|
e,
|
||||||
t('resourceErrorPincodeSetupDescription')
|
t("resourceErrorPincodeSetupDescription")
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -37,4 +37,3 @@ export const SitesBanner = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default SitesBanner;
|
export default SitesBanner;
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ import {Label} from "./ui/label";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Info } from "lucide-react";
|
import { Info } from "lucide-react";
|
||||||
import { info } from "winston";
|
import { info } from "winston";
|
||||||
import {Popover, PopoverContent, PopoverTrigger} from "@/components/ui/popover";
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
|
||||||
interface SwitchComponentProps {
|
interface SwitchComponentProps {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -49,7 +53,8 @@ export function SwitchInput({
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
{label && <Label htmlFor={id}>{label}</Label>}
|
{label && <Label htmlFor={id}>{label}</Label>}
|
||||||
{info && <Popover>
|
{info && (
|
||||||
|
<Popover>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
{defaultTrigger}
|
{defaultTrigger}
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
@@ -60,7 +65,8 @@ export function SwitchInput({
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>}
|
</Popover>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{description && (
|
{description && (
|
||||||
<span className="text-muted-foreground text-sm">
|
<span className="text-muted-foreground text-sm">
|
||||||
|
|||||||
@@ -276,14 +276,15 @@ function AuthPageSettings({
|
|||||||
<>
|
<>
|
||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
<SettingsSectionHeader>
|
<SettingsSectionHeader>
|
||||||
<SettingsSectionTitle>{t("customDomain")}</SettingsSectionTitle>
|
<SettingsSectionTitle>
|
||||||
|
{t("customDomain")}
|
||||||
|
</SettingsSectionTitle>
|
||||||
<SettingsSectionDescription>
|
<SettingsSectionDescription>
|
||||||
{t("authPageDescription")}
|
{t("authPageDescription")}
|
||||||
</SettingsSectionDescription>
|
</SettingsSectionDescription>
|
||||||
</SettingsSectionHeader>
|
</SettingsSectionHeader>
|
||||||
<SettingsSectionBody>
|
<SettingsSectionBody>
|
||||||
<SettingsSectionForm>
|
<SettingsSectionForm>
|
||||||
|
|
||||||
<PaidFeaturesAlert />
|
<PaidFeaturesAlert />
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
|
|||||||
@@ -58,12 +58,7 @@ export default function IdpLoginButtons({
|
|||||||
|
|
||||||
let redirectToUrl: string | undefined;
|
let redirectToUrl: string | undefined;
|
||||||
try {
|
try {
|
||||||
console.log(
|
console.log("generating", idpId, redirect || "/", orgId);
|
||||||
"generating",
|
|
||||||
idpId,
|
|
||||||
redirect || "/",
|
|
||||||
orgId
|
|
||||||
);
|
|
||||||
const safeRedirect = cleanRedirect(redirect || "/");
|
const safeRedirect = cleanRedirect(redirect || "/");
|
||||||
const response = await generateOidcUrlProxy(
|
const response = await generateOidcUrlProxy(
|
||||||
idpId,
|
idpId,
|
||||||
|
|||||||
@@ -288,7 +288,10 @@ export function DataTable<TData, TValue>({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (persistPageSize && pagination.pageSize !== pageSize) {
|
if (persistPageSize && pagination.pageSize !== pageSize) {
|
||||||
// Only store if user has actually changed it from initial value
|
// Only store if user has actually changed it from initial value
|
||||||
if (hasUserChangedPageSize.current && pagination.pageSize !== initialPageSize.current) {
|
if (
|
||||||
|
hasUserChangedPageSize.current &&
|
||||||
|
pagination.pageSize !== initialPageSize.current
|
||||||
|
) {
|
||||||
setStoredPageSize(pagination.pageSize, tableId);
|
setStoredPageSize(pagination.pageSize, tableId);
|
||||||
}
|
}
|
||||||
setPageSize(pagination.pageSize);
|
setPageSize(pagination.pageSize);
|
||||||
@@ -298,7 +301,9 @@ export function DataTable<TData, TValue>({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Persist column visibility to localStorage when it changes (but not on initial mount)
|
// Persist column visibility to localStorage when it changes (but not on initial mount)
|
||||||
if (shouldPersistColumnVisibility) {
|
if (shouldPersistColumnVisibility) {
|
||||||
const hasChanged = JSON.stringify(columnVisibility) !== JSON.stringify(initialColumnVisibilityState.current);
|
const hasChanged =
|
||||||
|
JSON.stringify(columnVisibility) !==
|
||||||
|
JSON.stringify(initialColumnVisibilityState.current);
|
||||||
if (hasChanged) {
|
if (hasChanged) {
|
||||||
// Mark as user-initiated change and persist
|
// Mark as user-initiated change and persist
|
||||||
hasUserChangedColumnVisibility.current = true;
|
hasUserChangedColumnVisibility.current = true;
|
||||||
|
|||||||
@@ -41,12 +41,13 @@ export type LatestVersionResponse = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const productUpdatesQueries = {
|
export const productUpdatesQueries = {
|
||||||
list: (enabled: boolean) =>
|
list: (enabled: boolean, version?: string) =>
|
||||||
queryOptions({
|
queryOptions({
|
||||||
queryKey: ["PRODUCT_UPDATES"] as const,
|
queryKey: ["PRODUCT_UPDATES"] as const,
|
||||||
queryFn: async ({ signal }) => {
|
queryFn: async ({ signal }) => {
|
||||||
const sp = new URLSearchParams({
|
const sp = new URLSearchParams({
|
||||||
build
|
build,
|
||||||
|
...(version ? { version } : {})
|
||||||
});
|
});
|
||||||
const data = await remote.get<ResponseT<ProductUpdate[]>>(
|
const data = await remote.get<ResponseT<ProductUpdate[]>>(
|
||||||
`/product-updates?${sp.toString()}`,
|
`/product-updates?${sp.toString()}`,
|
||||||
|
|||||||
Reference in New Issue
Block a user