mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-13 14:16:38 +00:00
Compare commits
33 Commits
crowdin_de
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c30f6db31 | ||
|
|
f021b73458 | ||
|
|
74f4751bcc | ||
|
|
e5bce4e180 | ||
|
|
9b0e7b381c | ||
|
|
90afe5a7ac | ||
|
|
b24de85157 | ||
|
|
eda43dffe1 | ||
|
|
82c9a1eb70 | ||
|
|
a3d4553d14 | ||
|
|
1cc5f59f66 | ||
|
|
4e2d88efdd | ||
|
|
4975cabb2c | ||
|
|
225591094f | ||
|
|
82f88f2cd3 | ||
|
|
99e6bd31b6 | ||
|
|
5c50590d7b | ||
|
|
072c89e704 | ||
|
|
dbdff6812d | ||
|
|
42b9d5158d | ||
|
|
2ba225299e | ||
|
|
cc841d5640 | ||
|
|
fa0818d3fa | ||
|
|
dec358c4cd | ||
|
|
e98f873f81 | ||
|
|
e9a2a7e752 | ||
|
|
91b7ceb2cf | ||
|
|
d4b830b9bb | ||
|
|
14d6ff25a7 | ||
|
|
1f62f305ce | ||
|
|
cebcf3e337 | ||
|
|
4cfcc64481 | ||
|
|
1a2069a6d9 |
@@ -2681,5 +2681,6 @@
|
|||||||
"approvalsEmptyStateStep2Title": "Enable Device Approvals",
|
"approvalsEmptyStateStep2Title": "Enable Device Approvals",
|
||||||
"approvalsEmptyStateStep2Description": "Edit a role and enable the 'Require Device Approvals' option. Users with this role will need admin approval for new devices.",
|
"approvalsEmptyStateStep2Description": "Edit a role and enable the 'Require Device Approvals' option. Users with this role will need admin approval for new devices.",
|
||||||
"approvalsEmptyStatePreviewDescription": "Preview: When enabled, pending device requests will appear here for review",
|
"approvalsEmptyStatePreviewDescription": "Preview: When enabled, pending device requests will appear here for review",
|
||||||
"approvalsEmptyStateButtonText": "Manage Roles"
|
"approvalsEmptyStateButtonText": "Manage Roles",
|
||||||
|
"domainErrorTitle": "We are having trouble verifying your domain"
|
||||||
}
|
}
|
||||||
|
|||||||
3948
package-lock.json
generated
3948
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
37
package.json
37
package.json
@@ -33,7 +33,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@asteasolutions/zod-to-openapi": "8.4.1",
|
"@asteasolutions/zod-to-openapi": "8.4.1",
|
||||||
"@aws-sdk/client-s3": "3.989.0",
|
"@aws-sdk/client-s3": "3.1004.0",
|
||||||
"@faker-js/faker": "10.3.0",
|
"@faker-js/faker": "10.3.0",
|
||||||
"@headlessui/react": "2.2.9",
|
"@headlessui/react": "2.2.9",
|
||||||
"@hookform/resolvers": "5.2.2",
|
"@hookform/resolvers": "5.2.2",
|
||||||
@@ -80,16 +80,16 @@
|
|||||||
"d3": "7.9.0",
|
"d3": "7.9.0",
|
||||||
"drizzle-orm": "0.45.1",
|
"drizzle-orm": "0.45.1",
|
||||||
"express": "5.2.1",
|
"express": "5.2.1",
|
||||||
"express-rate-limit": "8.2.1",
|
"express-rate-limit": "8.3.0",
|
||||||
"glob": "13.0.6",
|
"glob": "13.0.6",
|
||||||
"helmet": "8.1.0",
|
"helmet": "8.1.0",
|
||||||
"http-errors": "2.0.1",
|
"http-errors": "2.0.1",
|
||||||
"input-otp": "1.4.2",
|
"input-otp": "1.4.2",
|
||||||
"ioredis": "5.9.3",
|
"ioredis": "5.10.0",
|
||||||
"jmespath": "0.16.0",
|
"jmespath": "0.16.0",
|
||||||
"js-yaml": "4.1.1",
|
"js-yaml": "4.1.1",
|
||||||
"jsonwebtoken": "9.0.3",
|
"jsonwebtoken": "9.0.3",
|
||||||
"lucide-react": "0.563.0",
|
"lucide-react": "0.577.0",
|
||||||
"maxmind": "5.0.5",
|
"maxmind": "5.0.5",
|
||||||
"moment": "2.30.1",
|
"moment": "2.30.1",
|
||||||
"next": "15.5.12",
|
"next": "15.5.12",
|
||||||
@@ -99,20 +99,21 @@
|
|||||||
"node-cache": "5.1.2",
|
"node-cache": "5.1.2",
|
||||||
"nodemailer": "8.0.1",
|
"nodemailer": "8.0.1",
|
||||||
"oslo": "1.2.1",
|
"oslo": "1.2.1",
|
||||||
"pg": "8.19.0",
|
"pg": "8.20.0",
|
||||||
"posthog-node": "5.26.0",
|
"posthog-node": "5.28.0",
|
||||||
"qrcode.react": "4.2.0",
|
"qrcode.react": "4.2.0",
|
||||||
"react": "19.2.4",
|
"react": "19.2.4",
|
||||||
"react-day-picker": "9.13.2",
|
"react-day-picker": "9.14.0",
|
||||||
"react-dom": "19.2.4",
|
"react-dom": "19.2.4",
|
||||||
"react-easy-sort": "1.8.0",
|
"react-easy-sort": "1.8.0",
|
||||||
"react-hook-form": "7.71.2",
|
"react-hook-form": "7.71.2",
|
||||||
"react-icons": "5.5.0",
|
"react-icons": "5.6.0",
|
||||||
"recharts": "2.15.4",
|
"recharts": "2.15.4",
|
||||||
"reodotdev": "1.0.0",
|
"reodotdev": "1.1.0",
|
||||||
|
"resend": "6.9.2",
|
||||||
"semver": "7.7.4",
|
"semver": "7.7.4",
|
||||||
"sshpk": "^1.18.0",
|
"sshpk": "^1.18.0",
|
||||||
"stripe": "20.3.1",
|
"stripe": "20.4.1",
|
||||||
"swagger-ui-express": "5.0.1",
|
"swagger-ui-express": "5.0.1",
|
||||||
"tailwind-merge": "3.5.0",
|
"tailwind-merge": "3.5.0",
|
||||||
"topojson-client": "3.1.0",
|
"topojson-client": "3.1.0",
|
||||||
@@ -130,10 +131,10 @@
|
|||||||
"zod-validation-error": "5.0.0"
|
"zod-validation-error": "5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@dotenvx/dotenvx": "1.52.0",
|
"@dotenvx/dotenvx": "1.54.1",
|
||||||
"@esbuild-plugins/tsconfig-paths": "0.1.2",
|
"@esbuild-plugins/tsconfig-paths": "0.1.2",
|
||||||
"@react-email/preview-server": "5.2.8",
|
"@react-email/preview-server": "5.2.8",
|
||||||
"@tailwindcss/postcss": "4.1.18",
|
"@tailwindcss/postcss": "4.2.1",
|
||||||
"@tanstack/react-query-devtools": "5.91.3",
|
"@tanstack/react-query-devtools": "5.91.3",
|
||||||
"@types/better-sqlite3": "7.6.13",
|
"@types/better-sqlite3": "7.6.13",
|
||||||
"@types/cookie-parser": "1.4.10",
|
"@types/cookie-parser": "1.4.10",
|
||||||
@@ -145,10 +146,10 @@
|
|||||||
"@types/jmespath": "0.15.2",
|
"@types/jmespath": "0.15.2",
|
||||||
"@types/js-yaml": "4.0.9",
|
"@types/js-yaml": "4.0.9",
|
||||||
"@types/jsonwebtoken": "9.0.10",
|
"@types/jsonwebtoken": "9.0.10",
|
||||||
"@types/node": "25.2.3",
|
"@types/node": "25.3.5",
|
||||||
"@types/nodemailer": "7.0.11",
|
"@types/nodemailer": "7.0.11",
|
||||||
"@types/nprogress": "0.2.3",
|
"@types/nprogress": "0.2.3",
|
||||||
"@types/pg": "8.16.0",
|
"@types/pg": "8.18.0",
|
||||||
"@types/react": "19.2.14",
|
"@types/react": "19.2.14",
|
||||||
"@types/react-dom": "19.2.3",
|
"@types/react-dom": "19.2.3",
|
||||||
"@types/semver": "7.7.1",
|
"@types/semver": "7.7.1",
|
||||||
@@ -166,10 +167,14 @@
|
|||||||
"postcss": "8.5.6",
|
"postcss": "8.5.6",
|
||||||
"prettier": "3.8.1",
|
"prettier": "3.8.1",
|
||||||
"react-email": "5.2.8",
|
"react-email": "5.2.8",
|
||||||
"tailwindcss": "4.1.18",
|
"tailwindcss": "4.2.1",
|
||||||
"tsc-alias": "1.8.16",
|
"tsc-alias": "1.8.16",
|
||||||
"tsx": "4.21.0",
|
"tsx": "4.21.0",
|
||||||
"typescript": "5.9.3",
|
"typescript": "5.9.3",
|
||||||
"typescript-eslint": "8.55.0"
|
"typescript-eslint": "8.56.1"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"esbuild": "0.27.3",
|
||||||
|
"dompurify": "3.3.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -328,6 +328,14 @@ export const approvals = pgTable("approvals", {
|
|||||||
.notNull()
|
.notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const bannedEmails = pgTable("bannedEmails", {
|
||||||
|
email: varchar("email", { length: 255 }).primaryKey(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const bannedIps = pgTable("bannedIps", {
|
||||||
|
ip: varchar("ip", { length: 255 }).primaryKey(),
|
||||||
|
});
|
||||||
|
|
||||||
export type Approval = InferSelectModel<typeof approvals>;
|
export type Approval = InferSelectModel<typeof approvals>;
|
||||||
export type Limit = InferSelectModel<typeof limits>;
|
export type Limit = InferSelectModel<typeof limits>;
|
||||||
export type Account = InferSelectModel<typeof account>;
|
export type Account = InferSelectModel<typeof account>;
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ export const domains = pgTable("domains", {
|
|||||||
tries: integer("tries").notNull().default(0),
|
tries: integer("tries").notNull().default(0),
|
||||||
certResolver: varchar("certResolver"),
|
certResolver: varchar("certResolver"),
|
||||||
customCertResolver: varchar("customCertResolver"),
|
customCertResolver: varchar("customCertResolver"),
|
||||||
preferWildcardCert: boolean("preferWildcardCert")
|
preferWildcardCert: boolean("preferWildcardCert"),
|
||||||
|
errorMessage: text("errorMessage")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const dnsRecords = pgTable("dnsRecords", {
|
export const dnsRecords = pgTable("dnsRecords", {
|
||||||
|
|||||||
@@ -318,6 +318,15 @@ export const approvals = sqliteTable("approvals", {
|
|||||||
.notNull()
|
.notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export const bannedEmails = sqliteTable("bannedEmails", {
|
||||||
|
email: text("email").primaryKey()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const bannedIps = sqliteTable("bannedIps", {
|
||||||
|
ip: text("ip").primaryKey()
|
||||||
|
});
|
||||||
|
|
||||||
export type Approval = InferSelectModel<typeof approvals>;
|
export type Approval = InferSelectModel<typeof approvals>;
|
||||||
export type Limit = InferSelectModel<typeof limits>;
|
export type Limit = InferSelectModel<typeof limits>;
|
||||||
export type Account = InferSelectModel<typeof account>;
|
export type Account = InferSelectModel<typeof account>;
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ export const domains = sqliteTable("domains", {
|
|||||||
failed: integer("failed", { mode: "boolean" }).notNull().default(false),
|
failed: integer("failed", { mode: "boolean" }).notNull().default(false),
|
||||||
tries: integer("tries").notNull().default(0),
|
tries: integer("tries").notNull().default(0),
|
||||||
certResolver: text("certResolver"),
|
certResolver: text("certResolver"),
|
||||||
preferWildcardCert: integer("preferWildcardCert", { mode: "boolean" })
|
preferWildcardCert: integer("preferWildcardCert", { mode: "boolean" }),
|
||||||
|
errorMessage: text("errorMessage")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const dnsRecords = sqliteTable("dnsRecords", {
|
export const dnsRecords = sqliteTable("dnsRecords", {
|
||||||
|
|||||||
@@ -85,9 +85,7 @@ export async function deleteOrgById(
|
|||||||
deletedNewtIds.push(deletedNewt.newtId);
|
deletedNewtIds.push(deletedNewt.newtId);
|
||||||
await trx
|
await trx
|
||||||
.delete(newtSessions)
|
.delete(newtSessions)
|
||||||
.where(
|
.where(eq(newtSessions.newtId, deletedNewt.newtId));
|
||||||
eq(newtSessions.newtId, deletedNewt.newtId)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,33 +119,38 @@ export async function deleteOrgById(
|
|||||||
eq(clientSitesAssociationsCache.clientId, client.clientId)
|
eq(clientSitesAssociationsCache.clientId, client.clientId)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await trx.delete(resources).where(eq(resources.orgId, orgId));
|
||||||
|
|
||||||
const allOrgDomains = await trx
|
const allOrgDomains = await trx
|
||||||
.select()
|
.select()
|
||||||
.from(orgDomains)
|
.from(orgDomains)
|
||||||
.innerJoin(domains, eq(domains.domainId, orgDomains.domainId))
|
.innerJoin(domains, eq(orgDomains.domainId, domains.domainId))
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(orgDomains.orgId, orgId),
|
eq(orgDomains.orgId, orgId),
|
||||||
eq(domains.configManaged, false)
|
eq(domains.configManaged, false)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
logger.info(`Found ${allOrgDomains.length} domains to delete`);
|
||||||
const domainIdsToDelete: string[] = [];
|
const domainIdsToDelete: string[] = [];
|
||||||
for (const orgDomain of allOrgDomains) {
|
for (const orgDomain of allOrgDomains) {
|
||||||
const domainId = orgDomain.domains.domainId;
|
const domainId = orgDomain.domains.domainId;
|
||||||
const orgCount = await trx
|
const [orgCount] = await trx
|
||||||
.select({ count: sql<number>`count(*)` })
|
.select({ count: count() })
|
||||||
.from(orgDomains)
|
.from(orgDomains)
|
||||||
.where(eq(orgDomains.domainId, domainId));
|
.where(eq(orgDomains.domainId, domainId));
|
||||||
if (orgCount[0].count === 1) {
|
logger.info(`Found ${orgCount.count} orgs using domain ${domainId}`);
|
||||||
|
if (orgCount.count === 1) {
|
||||||
domainIdsToDelete.push(domainId);
|
domainIdsToDelete.push(domainId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
logger.info(`Found ${domainIdsToDelete.length} domains to delete`);
|
||||||
if (domainIdsToDelete.length > 0) {
|
if (domainIdsToDelete.length > 0) {
|
||||||
await trx
|
await trx
|
||||||
.delete(domains)
|
.delete(domains)
|
||||||
.where(inArray(domains.domainId, domainIdsToDelete));
|
.where(inArray(domains.domainId, domainIdsToDelete));
|
||||||
}
|
}
|
||||||
await trx.delete(resources).where(eq(resources.orgId, orgId));
|
|
||||||
|
|
||||||
await usageService.add(orgId, FeatureId.ORGINIZATIONS, -1, trx); // here we are decreasing the org count BEFORE deleting the org because we need to still be able to get the org to get the billing org inside of here
|
await usageService.add(orgId, FeatureId.ORGINIZATIONS, -1, trx); // here we are decreasing the org count BEFORE deleting the org because we need to still be able to get the org to get the billing org inside of here
|
||||||
|
|
||||||
@@ -231,15 +234,13 @@ export function sendTerminationMessages(result: DeleteOrgByIdResult): void {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
for (const olmId of result.olmsToTerminate) {
|
for (const olmId of result.olmsToTerminate) {
|
||||||
sendTerminateClient(
|
sendTerminateClient(0, OlmErrorCodes.TERMINATED_REKEYED, olmId).catch(
|
||||||
0,
|
(error) => {
|
||||||
OlmErrorCodes.TERMINATED_REKEYED,
|
logger.error(
|
||||||
olmId
|
"Failed to send termination message to olm:",
|
||||||
).catch((error) => {
|
error
|
||||||
logger.error(
|
);
|
||||||
"Failed to send termination message to olm:",
|
}
|
||||||
error
|
);
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { NextFunction, Request, Response } from "express";
|
import { NextFunction, Request, Response } from "express";
|
||||||
import { db, users } from "@server/db";
|
import { bannedEmails, bannedIps, db, users } from "@server/db";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import { email, z } from "zod";
|
import { email, z } from "zod";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
@@ -65,6 +65,30 @@ export async function signup(
|
|||||||
skipVerificationEmail
|
skipVerificationEmail
|
||||||
} = parsedBody.data;
|
} = parsedBody.data;
|
||||||
|
|
||||||
|
const [bannedEmail] = await db
|
||||||
|
.select()
|
||||||
|
.from(bannedEmails)
|
||||||
|
.where(eq(bannedEmails.email, email))
|
||||||
|
.limit(1);
|
||||||
|
if (bannedEmail) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.FORBIDDEN, "Signup blocked. Do not attempt to continue to use this service.")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.ip) {
|
||||||
|
const [bannedIp] = await db
|
||||||
|
.select()
|
||||||
|
.from(bannedIps)
|
||||||
|
.where(eq(bannedIps.ip, req.ip))
|
||||||
|
.limit(1);
|
||||||
|
if (bannedIp) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.FORBIDDEN, "Signup blocked. Do not attempt to continue to use this service.")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const passwordHash = await hashPassword(password);
|
const passwordHash = await hashPassword(password);
|
||||||
const userId = generateId(15);
|
const userId = generateId(15);
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ async function queryDomains(orgId: string, limit: number, offset: number) {
|
|||||||
tries: domains.tries,
|
tries: domains.tries,
|
||||||
configManaged: domains.configManaged,
|
configManaged: domains.configManaged,
|
||||||
certResolver: domains.certResolver,
|
certResolver: domains.certResolver,
|
||||||
preferWildcardCert: domains.preferWildcardCert
|
preferWildcardCert: domains.preferWildcardCert,
|
||||||
|
errorMessage: domains.errorMessage
|
||||||
})
|
})
|
||||||
.from(orgDomains)
|
.from(orgDomains)
|
||||||
.where(eq(orgDomains.orgId, orgId))
|
.where(eq(orgDomains.orgId, orgId))
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ export async function updateHolePunch(
|
|||||||
destinations: destinations
|
destinations: destinations
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// logger.error(error); // FIX THIS
|
logger.error(error);
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.INTERNAL_SERVER_ERROR,
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
@@ -339,10 +339,10 @@ export async function updateAndGenerateEndpointDestinations(
|
|||||||
handleSiteEndpointChange(newt.siteId, updatedSite.endpoint!);
|
handleSiteEndpointChange(newt.siteId, updatedSite.endpoint!);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!updatedSite || !updatedSite.subnet) {
|
// if (!updatedSite || !updatedSite.subnet) {
|
||||||
logger.warn(`Site not found: ${newt.siteId}`);
|
// logger.warn(`Site not found: ${newt.siteId}`);
|
||||||
throw new Error("Site not found");
|
// throw new Error("Site not found");
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Find all clients that connect to this site
|
// Find all clients that connect to this site
|
||||||
// const sitesClientPairs = await db
|
// const sitesClientPairs = await db
|
||||||
|
|||||||
@@ -42,6 +42,13 @@ export async function buildSiteConfigurationForOlmClient(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!site.publicKey || site.publicKey == "") { // the site is not ready to accept new peers
|
||||||
|
logger.warn(
|
||||||
|
`Site ${site.siteId} has no public key, skipping`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// if (site.lastHolePunch && now - site.lastHolePunch > 6 && relay) {
|
// if (site.lastHolePunch && now - site.lastHolePunch > 6 && relay) {
|
||||||
// logger.warn(
|
// logger.warn(
|
||||||
// `Site ${site.siteId} last hole punch is too old, skipping`
|
// `Site ${site.siteId} last hole punch is too old, skipping`
|
||||||
|
|||||||
@@ -223,6 +223,20 @@ async function createHttpResource(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prevent creating resource with same domain as dashboard
|
||||||
|
const dashboardUrl = config.getRawConfig().app.dashboard_url;
|
||||||
|
if (dashboardUrl) {
|
||||||
|
const dashboardHost = new URL(dashboardUrl).hostname;
|
||||||
|
if (fullDomain === dashboardHost) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.CONFLICT,
|
||||||
|
"Resource domain cannot be the same as the dashboard domain"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (build != "oss") {
|
if (build != "oss") {
|
||||||
const existingLoginPages = await db
|
const existingLoginPages = await db
|
||||||
.select()
|
.select()
|
||||||
|
|||||||
@@ -353,6 +353,20 @@ async function updateHttpResource(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prevent updating resource with same domain as dashboard
|
||||||
|
const dashboardUrl = config.getRawConfig().app.dashboard_url;
|
||||||
|
if (dashboardUrl) {
|
||||||
|
const dashboardHost = new URL(dashboardUrl).hostname;
|
||||||
|
if (fullDomain === dashboardHost) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.CONFLICT,
|
||||||
|
"Resource domain cannot be the same as the dashboard domain"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (build != "oss") {
|
if (build != "oss") {
|
||||||
const existingLoginPages = await db
|
const existingLoginPages = await db
|
||||||
.select()
|
.select()
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ export default async function DomainSettingsPage({
|
|||||||
failed={domain.failed}
|
failed={domain.failed}
|
||||||
verified={domain.verified}
|
verified={domain.verified}
|
||||||
type={domain.type}
|
type={domain.type}
|
||||||
|
errorMessage={domain.errorMessage}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DNSRecordsTable records={dnsRecords} type={domain.type} />
|
<DNSRecordsTable records={dnsRecords} type={domain.type} />
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger
|
TooltipTrigger
|
||||||
} from "@app/components/ui/tooltip";
|
} from "@app/components/ui/tooltip";
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
@@ -65,6 +66,7 @@ import { build } from "@server/build";
|
|||||||
import { Resource } from "@server/db";
|
import { Resource } from "@server/db";
|
||||||
import { isTargetValid } from "@server/lib/validators";
|
import { isTargetValid } from "@server/lib/validators";
|
||||||
import { ListTargetsResponse } from "@server/routers/target";
|
import { ListTargetsResponse } from "@server/routers/target";
|
||||||
|
import { ListRemoteExitNodesResponse } from "@server/routers/remoteExitNode/types";
|
||||||
import { ArrayElement } from "@server/types/ArrayElement";
|
import { ArrayElement } from "@server/types/ArrayElement";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
@@ -81,6 +83,7 @@ import {
|
|||||||
CircleCheck,
|
CircleCheck,
|
||||||
CircleX,
|
CircleX,
|
||||||
Info,
|
Info,
|
||||||
|
InfoIcon,
|
||||||
Plus,
|
Plus,
|
||||||
Settings,
|
Settings,
|
||||||
SquareArrowOutUpRight
|
SquareArrowOutUpRight
|
||||||
@@ -210,6 +213,13 @@ export default function Page() {
|
|||||||
orgQueries.sites({ orgId: orgId as string })
|
orgQueries.sites({ orgId: orgId as string })
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [remoteExitNodes, setRemoteExitNodes] = useState<
|
||||||
|
ListRemoteExitNodesResponse["remoteExitNodes"]
|
||||||
|
>([]);
|
||||||
|
const [loadingExitNodes, setLoadingExitNodes] = useState(
|
||||||
|
build === "saas"
|
||||||
|
);
|
||||||
|
|
||||||
const [createLoading, setCreateLoading] = useState(false);
|
const [createLoading, setCreateLoading] = useState(false);
|
||||||
const [showSnippets, setShowSnippets] = useState(false);
|
const [showSnippets, setShowSnippets] = useState(false);
|
||||||
const [niceId, setNiceId] = useState<string>("");
|
const [niceId, setNiceId] = useState<string>("");
|
||||||
@@ -224,6 +234,27 @@ export default function Page() {
|
|||||||
useState<LocalTarget | null>(null);
|
useState<LocalTarget | null>(null);
|
||||||
const [healthCheckDialogOpen, setHealthCheckDialogOpen] = useState(false);
|
const [healthCheckDialogOpen, setHealthCheckDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (build !== "saas") return;
|
||||||
|
|
||||||
|
const fetchExitNodes = async () => {
|
||||||
|
try {
|
||||||
|
const res = await api.get<
|
||||||
|
AxiosResponse<ListRemoteExitNodesResponse>
|
||||||
|
>(`/org/${orgId}/remote-exit-nodes`);
|
||||||
|
if (res && res.status === 200) {
|
||||||
|
setRemoteExitNodes(res.data.data.remoteExitNodes);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to fetch remote exit nodes:", e);
|
||||||
|
} finally {
|
||||||
|
setLoadingExitNodes(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchExitNodes();
|
||||||
|
}, [orgId]);
|
||||||
|
|
||||||
const [isAdvancedMode, setIsAdvancedMode] = useState(() => {
|
const [isAdvancedMode, setIsAdvancedMode] = useState(() => {
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
const saved = localStorage.getItem("create-advanced-mode");
|
const saved = localStorage.getItem("create-advanced-mode");
|
||||||
@@ -289,15 +320,25 @@ export default function Page() {
|
|||||||
},
|
},
|
||||||
...(!env.flags.allowRawResources
|
...(!env.flags.allowRawResources
|
||||||
? []
|
? []
|
||||||
: [
|
: build === "saas" && remoteExitNodes.length === 0
|
||||||
{
|
? []
|
||||||
id: "raw" as ResourceType,
|
: [
|
||||||
title: t("resourceRaw"),
|
{
|
||||||
description: build == "saas" ? t("resourceRawDescriptionCloud") : t("resourceRawDescription")
|
id: "raw" as ResourceType,
|
||||||
}
|
title: t("resourceRaw"),
|
||||||
])
|
description:
|
||||||
|
build == "saas"
|
||||||
|
? t("resourceRawDescriptionCloud")
|
||||||
|
: t("resourceRawDescription")
|
||||||
|
}
|
||||||
|
])
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// In saas mode with no exit nodes, force HTTP
|
||||||
|
const showTypeSelector =
|
||||||
|
build !== "saas" ||
|
||||||
|
(!loadingExitNodes && remoteExitNodes.length > 0);
|
||||||
|
|
||||||
const baseForm = useForm({
|
const baseForm = useForm({
|
||||||
resolver: zodResolver(baseResourceFormSchema),
|
resolver: zodResolver(baseResourceFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -559,7 +600,7 @@ export default function Page() {
|
|||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: t("resourceErrorCreate"),
|
title: t("resourceErrorCreate"),
|
||||||
description: t("resourceErrorCreateMessageDescription")
|
description: formatAxiosError(e, t("resourceErrorCreateMessageDescription"))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -984,34 +1025,35 @@ export default function Page() {
|
|||||||
</SettingsSectionTitle>
|
</SettingsSectionTitle>
|
||||||
</SettingsSectionHeader>
|
</SettingsSectionHeader>
|
||||||
<SettingsSectionBody>
|
<SettingsSectionBody>
|
||||||
{resourceTypes.length > 1 && (
|
{showTypeSelector &&
|
||||||
<>
|
resourceTypes.length > 1 && (
|
||||||
<div className="mb-2">
|
<>
|
||||||
<span className="text-sm font-medium">
|
<div className="mb-2">
|
||||||
{t("type")}
|
<span className="text-sm font-medium">
|
||||||
</span>
|
{t("type")}
|
||||||
</div>
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<StrategySelect
|
<StrategySelect
|
||||||
options={resourceTypes}
|
options={resourceTypes}
|
||||||
defaultValue="http"
|
defaultValue="http"
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
baseForm.setValue(
|
baseForm.setValue(
|
||||||
"http",
|
"http",
|
||||||
value === "http"
|
value === "http"
|
||||||
);
|
);
|
||||||
// Update method default when switching resource type
|
// Update method default when switching resource type
|
||||||
addTargetForm.setValue(
|
addTargetForm.setValue(
|
||||||
"method",
|
"method",
|
||||||
value === "http"
|
value === "http"
|
||||||
? "http"
|
? "http"
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
cols={2}
|
cols={2}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<SettingsSectionForm>
|
<SettingsSectionForm>
|
||||||
<Form {...baseForm}>
|
<Form {...baseForm}>
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ const CredenzaContent = ({ className, children, ...props }: CredenzaProps) => {
|
|||||||
return (
|
return (
|
||||||
<CredenzaContent
|
<CredenzaContent
|
||||||
className={cn(
|
className={cn(
|
||||||
"overflow-y-auto max-h-[100dvh] md:max-h-screen md:top-[clamp(1.5rem,12vh,200px)] md:translate-y-0",
|
"overflow-y-auto max-h-[100dvh] md:max-h-[calc(100vh-clamp(3rem,24vh,400px))] md:top-[clamp(1.5rem,12vh,200px)] md:translate-y-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -10,17 +10,20 @@ import {
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { Badge } from "./ui/badge";
|
import { Badge } from "./ui/badge";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
|
import { AlertTriangle } from "lucide-react";
|
||||||
|
|
||||||
type DomainInfoCardProps = {
|
type DomainInfoCardProps = {
|
||||||
failed: boolean;
|
failed: boolean;
|
||||||
verified: boolean;
|
verified: boolean;
|
||||||
type: string | null;
|
type: string | null;
|
||||||
|
errorMessage?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function DomainInfoCard({
|
export default function DomainInfoCard({
|
||||||
failed,
|
failed,
|
||||||
verified,
|
verified,
|
||||||
type
|
type,
|
||||||
|
errorMessage
|
||||||
}: DomainInfoCardProps) {
|
}: DomainInfoCardProps) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const env = useEnvContext();
|
const env = useEnvContext();
|
||||||
@@ -39,6 +42,7 @@ export default function DomainInfoCard({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
<Alert>
|
<Alert>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
<InfoSections cols={3}>
|
<InfoSections cols={3}>
|
||||||
@@ -79,5 +83,19 @@ export default function DomainInfoCard({
|
|||||||
</InfoSections>
|
</InfoSections>
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
{errorMessage && (failed || !verified) && (
|
||||||
|
<Alert variant={failed ? "destructive" : "warning"}>
|
||||||
|
<AlertTriangle className="h-4 w-4" />
|
||||||
|
<AlertTitle>
|
||||||
|
{failed
|
||||||
|
? t("domainErrorTitle", { fallback: "Domain Error" })
|
||||||
|
: t("domainPendingErrorTitle", { fallback: "Verification Issue" })}
|
||||||
|
</AlertTitle>
|
||||||
|
<AlertDescription className="font-mono text-xs break-all">
|
||||||
|
{errorMessage}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,12 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger
|
DropdownMenuTrigger
|
||||||
} from "./ui/dropdown-menu";
|
} from "./ui/dropdown-menu";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger
|
||||||
|
} from "./ui/tooltip";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
export type DomainRow = {
|
export type DomainRow = {
|
||||||
@@ -39,6 +45,7 @@ export type DomainRow = {
|
|||||||
configManaged: boolean;
|
configManaged: boolean;
|
||||||
certResolver: string;
|
certResolver: string;
|
||||||
preferWildcardCert: boolean;
|
preferWildcardCert: boolean;
|
||||||
|
errorMessage?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -175,7 +182,7 @@ export default function DomainsTable({ domains, orgId }: Props) {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const { verified, failed, type } = row.original;
|
const { verified, failed, type, errorMessage } = row.original;
|
||||||
if (verified) {
|
if (verified) {
|
||||||
return type == "wildcard" ? (
|
return type == "wildcard" ? (
|
||||||
<Badge variant="outlinePrimary">{t("manual")}</Badge>
|
<Badge variant="outlinePrimary">{t("manual")}</Badge>
|
||||||
@@ -183,12 +190,44 @@ export default function DomainsTable({ domains, orgId }: Props) {
|
|||||||
<Badge variant="green">{t("verified")}</Badge>
|
<Badge variant="green">{t("verified")}</Badge>
|
||||||
);
|
);
|
||||||
} else if (failed) {
|
} else if (failed) {
|
||||||
|
if (errorMessage) {
|
||||||
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Badge variant="red" className="cursor-help">
|
||||||
|
{t("failed", { fallback: "Failed" })}
|
||||||
|
</Badge>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="max-w-xs">
|
||||||
|
<p className="break-words">{errorMessage}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Badge variant="red">
|
<Badge variant="red">
|
||||||
{t("failed", { fallback: "Failed" })}
|
{t("failed", { fallback: "Failed" })}
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
if (errorMessage) {
|
||||||
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Badge variant="yellow" className="cursor-help">
|
||||||
|
{t("pending")}
|
||||||
|
</Badge>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="max-w-xs">
|
||||||
|
<p className="break-words">{errorMessage}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
return <Badge variant="yellow">{t("pending")}</Badge>;
|
return <Badge variant="yellow">{t("pending")}</Badge>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user