mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-11 12:22:26 +00:00
Compare commits
7 Commits
dev
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af4c1e926b | ||
|
|
b4c01349d1 | ||
|
|
e4d4c62833 | ||
|
|
20ae903d7f | ||
|
|
b0566d3c6f | ||
|
|
5dda8c384f | ||
|
|
cb569ff14d |
4
.github/workflows/cicd.yml
vendored
4
.github/workflows/cicd.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
|||||||
permissions: write-all
|
permissions: write-all
|
||||||
steps:
|
steps:
|
||||||
- name: Configure AWS credentials
|
- name: Configure AWS credentials
|
||||||
uses: aws-actions/configure-aws-credentials@v5
|
uses: aws-actions/configure-aws-credentials@v6
|
||||||
with:
|
with:
|
||||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||||
role-duration-seconds: 3600
|
role-duration-seconds: 3600
|
||||||
@@ -576,7 +576,7 @@ jobs:
|
|||||||
permissions: write-all
|
permissions: write-all
|
||||||
steps:
|
steps:
|
||||||
- name: Configure AWS credentials
|
- name: Configure AWS credentials
|
||||||
uses: aws-actions/configure-aws-credentials@v5
|
uses: aws-actions/configure-aws-credentials@v6
|
||||||
with:
|
with:
|
||||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||||
role-duration-seconds: 3600
|
role-duration-seconds: 3600
|
||||||
|
|||||||
2
.github/workflows/restart-runners.yml
vendored
2
.github/workflows/restart-runners.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
permissions: write-all
|
permissions: write-all
|
||||||
steps:
|
steps:
|
||||||
- name: Configure AWS credentials
|
- name: Configure AWS credentials
|
||||||
uses: aws-actions/configure-aws-credentials@v5
|
uses: aws-actions/configure-aws-credentials@v6
|
||||||
with:
|
with:
|
||||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||||
role-duration-seconds: 3600
|
role-duration-seconds: 3600
|
||||||
|
|||||||
6
.github/workflows/saas.yml
vendored
6
.github/workflows/saas.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
|||||||
permissions: write-all
|
permissions: write-all
|
||||||
steps:
|
steps:
|
||||||
- name: Configure AWS credentials
|
- name: Configure AWS credentials
|
||||||
uses: aws-actions/configure-aws-credentials@v5
|
uses: aws-actions/configure-aws-credentials@v6
|
||||||
with:
|
with:
|
||||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||||
role-duration-seconds: 3600
|
role-duration-seconds: 3600
|
||||||
@@ -69,7 +69,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Configure AWS credentials
|
- name: Configure AWS credentials
|
||||||
uses: aws-actions/configure-aws-credentials@v5
|
uses: aws-actions/configure-aws-credentials@v6
|
||||||
with:
|
with:
|
||||||
role-to-assume: arn:aws:iam::${{ secrets.aws_account_id }}:role/${{ secrets.AWS_ROLE_NAME }}
|
role-to-assume: arn:aws:iam::${{ secrets.aws_account_id }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||||
role-duration-seconds: 3600
|
role-duration-seconds: 3600
|
||||||
@@ -110,7 +110,7 @@ jobs:
|
|||||||
permissions: write-all
|
permissions: write-all
|
||||||
steps:
|
steps:
|
||||||
- name: Configure AWS credentials
|
- name: Configure AWS credentials
|
||||||
uses: aws-actions/configure-aws-credentials@v5
|
uses: aws-actions/configure-aws-credentials@v6
|
||||||
with:
|
with:
|
||||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||||
role-duration-seconds: 3600
|
role-duration-seconds: 3600
|
||||||
|
|||||||
@@ -97,7 +97,7 @@
|
|||||||
"siteGeneralDescription": "Allgemeine Einstellungen für diesen Standort konfigurieren",
|
"siteGeneralDescription": "Allgemeine Einstellungen für diesen Standort konfigurieren",
|
||||||
"siteSettingDescription": "Standorteinstellungen konfigurieren",
|
"siteSettingDescription": "Standorteinstellungen konfigurieren",
|
||||||
"siteSetting": "{siteName} Einstellungen",
|
"siteSetting": "{siteName} Einstellungen",
|
||||||
"siteNewtTunnel": "Neuer Standort (empfohlen)",
|
"siteNewtTunnel": "Newt Standort (empfohlen)",
|
||||||
"siteNewtTunnelDescription": "Einfachster Weg, einen Einstiegspunkt in jedes Netzwerk zu erstellen. Keine zusätzliche Einrichtung.",
|
"siteNewtTunnelDescription": "Einfachster Weg, einen Einstiegspunkt in jedes Netzwerk zu erstellen. Keine zusätzliche Einrichtung.",
|
||||||
"siteWg": "Einfacher WireGuard Tunnel",
|
"siteWg": "Einfacher WireGuard Tunnel",
|
||||||
"siteWgDescription": "Verwende jeden WireGuard-Client, um einen Tunnel einzurichten. Manuelles NAT-Setup erforderlich.",
|
"siteWgDescription": "Verwende jeden WireGuard-Client, um einen Tunnel einzurichten. Manuelles NAT-Setup erforderlich.",
|
||||||
@@ -107,7 +107,7 @@
|
|||||||
"siteSeeAll": "Alle Standorte anzeigen",
|
"siteSeeAll": "Alle Standorte anzeigen",
|
||||||
"siteTunnelDescription": "Legen Sie fest, wie Sie sich mit dem Standort verbinden möchten",
|
"siteTunnelDescription": "Legen Sie fest, wie Sie sich mit dem Standort verbinden möchten",
|
||||||
"siteNewtCredentials": "Zugangsdaten",
|
"siteNewtCredentials": "Zugangsdaten",
|
||||||
"siteNewtCredentialsDescription": "So wird sich die Seite mit dem Server authentifizieren",
|
"siteNewtCredentialsDescription": "So wird sich der Standort mit dem Server authentifizieren",
|
||||||
"remoteNodeCredentialsDescription": "So wird sich der entfernte Node mit dem Server authentifizieren",
|
"remoteNodeCredentialsDescription": "So wird sich der entfernte Node mit dem Server authentifizieren",
|
||||||
"siteCredentialsSave": "Anmeldedaten speichern",
|
"siteCredentialsSave": "Anmeldedaten speichern",
|
||||||
"siteCredentialsSaveDescription": "Du kannst das nur einmal sehen. Stelle sicher, dass du es an einen sicheren Ort kopierst.",
|
"siteCredentialsSaveDescription": "Du kannst das nur einmal sehen. Stelle sicher, dass du es an einen sicheren Ort kopierst.",
|
||||||
@@ -2503,7 +2503,7 @@
|
|||||||
"deviceModel": "Gerätemodell",
|
"deviceModel": "Gerätemodell",
|
||||||
"serialNumber": "Seriennummer",
|
"serialNumber": "Seriennummer",
|
||||||
"hostname": "Hostname",
|
"hostname": "Hostname",
|
||||||
"firstSeen": "Erster Blick",
|
"firstSeen": "Zuerst gesehen",
|
||||||
"lastSeen": "Zuletzt gesehen",
|
"lastSeen": "Zuletzt gesehen",
|
||||||
"biometricsEnabled": "Biometrie aktiviert",
|
"biometricsEnabled": "Biometrie aktiviert",
|
||||||
"diskEncrypted": "Festplatte verschlüsselt",
|
"diskEncrypted": "Festplatte verschlüsselt",
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
"siteDescription": "Create and manage sites to enable connectivity to private networks",
|
"siteDescription": "Create and manage sites to enable connectivity to private networks",
|
||||||
"sitesBannerTitle": "Connect Any Network",
|
"sitesBannerTitle": "Connect Any Network",
|
||||||
"sitesBannerDescription": "A site is a connection to a remote network that allows Pangolin to provide access to resources, whether public or private, to users anywhere. Install the site network connector (Newt) anywhere you can run a binary or container to establish the connection.",
|
"sitesBannerDescription": "A site is a connection to a remote network that allows Pangolin to provide access to resources, whether public or private, to users anywhere. Install the site network connector (Newt) anywhere you can run a binary or container to establish the connection.",
|
||||||
"sitesBannerButtonText": "Install Site Connector",
|
"sitesBannerButtonText": "Install Site",
|
||||||
"approvalsBannerTitle": "Approve or Deny Device Access",
|
"approvalsBannerTitle": "Approve or Deny Device Access",
|
||||||
"approvalsBannerDescription": "Review and approve or deny device access requests from users. When device approvals are required, users must get admin approval before their devices can connect to your organization's resources.",
|
"approvalsBannerDescription": "Review and approve or deny device access requests from users. When device approvals are required, users must get admin approval before their devices can connect to your organization's resources.",
|
||||||
"approvalsBannerButtonText": "Learn More",
|
"approvalsBannerButtonText": "Learn More",
|
||||||
@@ -79,8 +79,8 @@
|
|||||||
"siteConfirmCopy": "I have copied the config",
|
"siteConfirmCopy": "I have copied the config",
|
||||||
"searchSitesProgress": "Search sites...",
|
"searchSitesProgress": "Search sites...",
|
||||||
"siteAdd": "Add Site",
|
"siteAdd": "Add Site",
|
||||||
"siteInstallNewt": "Install Site",
|
"siteInstallNewt": "Install Newt",
|
||||||
"siteInstallNewtDescription": "Install the site connector for your system",
|
"siteInstallNewtDescription": "Get Newt running on your system",
|
||||||
"WgConfiguration": "WireGuard Configuration",
|
"WgConfiguration": "WireGuard Configuration",
|
||||||
"WgConfigurationDescription": "Use the following configuration to connect to the network",
|
"WgConfigurationDescription": "Use the following configuration to connect to the network",
|
||||||
"operatingSystem": "Operating System",
|
"operatingSystem": "Operating System",
|
||||||
@@ -1545,8 +1545,8 @@
|
|||||||
"addressDescription": "The internal address of the client. Must fall within the organization's subnet.",
|
"addressDescription": "The internal address of the client. Must fall within the organization's subnet.",
|
||||||
"selectSites": "Select sites",
|
"selectSites": "Select sites",
|
||||||
"sitesDescription": "The client will have connectivity to the selected sites",
|
"sitesDescription": "The client will have connectivity to the selected sites",
|
||||||
"clientInstallOlm": "Install Machine Client",
|
"clientInstallOlm": "Install Olm",
|
||||||
"clientInstallOlmDescription": "Install the machine client for your system",
|
"clientInstallOlmDescription": "Get Olm running on your system",
|
||||||
"clientOlmCredentials": "Credentials",
|
"clientOlmCredentials": "Credentials",
|
||||||
"clientOlmCredentialsDescription": "This is how the client will authenticate with the server",
|
"clientOlmCredentialsDescription": "This is how the client will authenticate with the server",
|
||||||
"olmEndpoint": "Endpoint",
|
"olmEndpoint": "Endpoint",
|
||||||
@@ -2247,7 +2247,6 @@
|
|||||||
"actionLogsDescription": "View a history of actions performed in this organization",
|
"actionLogsDescription": "View a history of actions performed in this organization",
|
||||||
"accessLogsDescription": "View access auth requests for resources in this organization",
|
"accessLogsDescription": "View access auth requests for resources in this organization",
|
||||||
"licenseRequiredToUse": "An Enterprise license is required to use this feature.",
|
"licenseRequiredToUse": "An Enterprise license is required to use this feature.",
|
||||||
"ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature.",
|
|
||||||
"certResolver": "Certificate Resolver",
|
"certResolver": "Certificate Resolver",
|
||||||
"certResolverDescription": "Select the certificate resolver to use for this resource.",
|
"certResolverDescription": "Select the certificate resolver to use for this resource.",
|
||||||
"selectCertResolver": "Select Certificate Resolver",
|
"selectCertResolver": "Select Certificate Resolver",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { db, orgs, requestAuditLog } from "@server/db";
|
import { db, orgs, requestAuditLog } from "@server/db";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { and, eq, lt, sql } from "drizzle-orm";
|
import { and, eq, lt } from "drizzle-orm";
|
||||||
import cache from "@server/lib/cache";
|
import cache from "@server/lib/cache";
|
||||||
import { calculateCutoffTimestamp } from "@server/lib/cleanupLogs";
|
import { calculateCutoffTimestamp } from "@server/lib/cleanupLogs";
|
||||||
import { stripPortFromHost } from "@server/lib/ip";
|
import { stripPortFromHost } from "@server/lib/ip";
|
||||||
@@ -67,27 +67,17 @@ async function flushAuditLogs() {
|
|||||||
const logsToWrite = auditLogBuffer.splice(0, auditLogBuffer.length);
|
const logsToWrite = auditLogBuffer.splice(0, auditLogBuffer.length);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use a transaction to ensure all inserts succeed or fail together
|
// Batch insert logs in groups of 25 to avoid overwhelming the database
|
||||||
// This prevents index corruption from partial writes
|
const BATCH_DB_SIZE = 25;
|
||||||
await db.transaction(async (tx) => {
|
for (let i = 0; i < logsToWrite.length; i += BATCH_DB_SIZE) {
|
||||||
// Batch insert logs in groups of 25 to avoid overwhelming the database
|
const batch = logsToWrite.slice(i, i + BATCH_DB_SIZE);
|
||||||
const BATCH_DB_SIZE = 25;
|
await db.insert(requestAuditLog).values(batch);
|
||||||
for (let i = 0; i < logsToWrite.length; i += BATCH_DB_SIZE) {
|
}
|
||||||
const batch = logsToWrite.slice(i, i + BATCH_DB_SIZE);
|
|
||||||
await tx.insert(requestAuditLog).values(batch);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
logger.debug(`Flushed ${logsToWrite.length} audit logs to database`);
|
logger.debug(`Flushed ${logsToWrite.length} audit logs to database`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error flushing audit logs:", error);
|
logger.error("Error flushing audit logs:", error);
|
||||||
// On transaction error, put logs back at the front of the buffer to retry
|
// On error, we lose these logs - consider a fallback strategy if needed
|
||||||
// but only if buffer isn't too large
|
// (e.g., write to file, or put back in buffer with retry limit)
|
||||||
if (auditLogBuffer.length < MAX_BUFFER_SIZE - logsToWrite.length) {
|
|
||||||
auditLogBuffer.unshift(...logsToWrite);
|
|
||||||
logger.info(`Re-queued ${logsToWrite.length} audit logs for retry`);
|
|
||||||
} else {
|
|
||||||
logger.error(`Buffer full, dropped ${logsToWrite.length} audit logs`);
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
isFlushInProgress = false;
|
isFlushInProgress = false;
|
||||||
// If buffer filled up while we were flushing, flush again
|
// If buffer filled up while we were flushing, flush again
|
||||||
|
|||||||
@@ -56,29 +56,19 @@ async function query(clientId?: number, niceId?: string, orgId?: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PostureData = {
|
type PostureData = {
|
||||||
biometricsEnabled?: boolean | null | "-";
|
biometricsEnabled?: boolean | null;
|
||||||
diskEncrypted?: boolean | null | "-";
|
diskEncrypted?: boolean | null;
|
||||||
firewallEnabled?: boolean | null | "-";
|
firewallEnabled?: boolean | null;
|
||||||
autoUpdatesEnabled?: boolean | null | "-";
|
autoUpdatesEnabled?: boolean | null;
|
||||||
tpmAvailable?: boolean | null | "-";
|
tpmAvailable?: boolean | null;
|
||||||
windowsAntivirusEnabled?: boolean | null | "-";
|
windowsAntivirusEnabled?: boolean | null;
|
||||||
macosSipEnabled?: boolean | null | "-";
|
macosSipEnabled?: boolean | null;
|
||||||
macosGatekeeperEnabled?: boolean | null | "-";
|
macosGatekeeperEnabled?: boolean | null;
|
||||||
macosFirewallStealthMode?: boolean | null | "-";
|
macosFirewallStealthMode?: boolean | null;
|
||||||
linuxAppArmorEnabled?: boolean | null | "-";
|
linuxAppArmorEnabled?: boolean | null;
|
||||||
linuxSELinuxEnabled?: boolean | null | "-";
|
linuxSELinuxEnabled?: boolean | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
function maskPostureDataWithPlaceholder(posture: PostureData): PostureData {
|
|
||||||
const masked: PostureData = {};
|
|
||||||
for (const key of Object.keys(posture) as (keyof PostureData)[]) {
|
|
||||||
if (posture[key] !== undefined && posture[key] !== null) {
|
|
||||||
(masked as Record<keyof PostureData, "-">)[key] = "-";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return masked;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPlatformPostureData(
|
function getPlatformPostureData(
|
||||||
platform: string | null | undefined,
|
platform: string | null | undefined,
|
||||||
fingerprint: typeof currentFingerprint.$inferSelect | null
|
fingerprint: typeof currentFingerprint.$inferSelect | null
|
||||||
@@ -294,11 +284,9 @@ export async function getClient(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isUserDevice = client.user !== null && client.user !== undefined;
|
|
||||||
|
|
||||||
// Replace name with device name if OLM exists
|
// Replace name with device name if OLM exists
|
||||||
let clientName = client.clients.name;
|
let clientName = client.clients.name;
|
||||||
if (client.olms && isUserDevice) {
|
if (client.olms) {
|
||||||
const model = client.currentFingerprint?.deviceModel || null;
|
const model = client.currentFingerprint?.deviceModel || null;
|
||||||
clientName = getUserDeviceName(model, client.clients.name);
|
clientName = getUserDeviceName(model, client.clients.name);
|
||||||
}
|
}
|
||||||
@@ -306,34 +294,32 @@ export async function getClient(
|
|||||||
// Build fingerprint data if available
|
// Build fingerprint data if available
|
||||||
const fingerprintData = client.currentFingerprint
|
const fingerprintData = client.currentFingerprint
|
||||||
? {
|
? {
|
||||||
username: client.currentFingerprint.username || null,
|
username: client.currentFingerprint.username || null,
|
||||||
hostname: client.currentFingerprint.hostname || null,
|
hostname: client.currentFingerprint.hostname || null,
|
||||||
platform: client.currentFingerprint.platform || null,
|
platform: client.currentFingerprint.platform || null,
|
||||||
osVersion: client.currentFingerprint.osVersion || null,
|
osVersion: client.currentFingerprint.osVersion || null,
|
||||||
kernelVersion:
|
kernelVersion:
|
||||||
client.currentFingerprint.kernelVersion || null,
|
client.currentFingerprint.kernelVersion || null,
|
||||||
arch: client.currentFingerprint.arch || null,
|
arch: client.currentFingerprint.arch || null,
|
||||||
deviceModel: client.currentFingerprint.deviceModel || null,
|
deviceModel: client.currentFingerprint.deviceModel || null,
|
||||||
serialNumber: client.currentFingerprint.serialNumber || null,
|
serialNumber: client.currentFingerprint.serialNumber || null,
|
||||||
firstSeen: client.currentFingerprint.firstSeen || null,
|
firstSeen: client.currentFingerprint.firstSeen || null,
|
||||||
lastSeen: client.currentFingerprint.lastSeen || null
|
lastSeen: client.currentFingerprint.lastSeen || null
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
// Build posture data if available (platform-specific)
|
// Build posture data if available (platform-specific)
|
||||||
// Licensed: real values; not licensed: same keys but values set to "-"
|
// Only return posture data if org is licensed/subscribed
|
||||||
const rawPosture = getPlatformPostureData(
|
let postureData: PostureData | null = null;
|
||||||
client.currentFingerprint?.platform || null,
|
|
||||||
client.currentFingerprint
|
|
||||||
);
|
|
||||||
const isOrgLicensed = await isLicensedOrSubscribed(
|
const isOrgLicensed = await isLicensedOrSubscribed(
|
||||||
client.clients.orgId
|
client.clients.orgId
|
||||||
);
|
);
|
||||||
const postureData: PostureData | null = rawPosture
|
if (isOrgLicensed) {
|
||||||
? isOrgLicensed
|
postureData = getPlatformPostureData(
|
||||||
? rawPosture
|
client.currentFingerprint?.platform || null,
|
||||||
: maskPostureDataWithPlaceholder(rawPosture)
|
client.currentFingerprint
|
||||||
: null;
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const data: GetClientResponse = {
|
const data: GetClientResponse = {
|
||||||
...client.clients,
|
...client.clients,
|
||||||
|
|||||||
@@ -320,10 +320,7 @@ export async function listClients(
|
|||||||
// Merge clients with their site associations and replace name with device name
|
// Merge clients with their site associations and replace name with device name
|
||||||
const clientsWithSites = clientsList.map((client) => {
|
const clientsWithSites = clientsList.map((client) => {
|
||||||
const model = client.deviceModel || null;
|
const model = client.deviceModel || null;
|
||||||
let newName = client.name;
|
const newName = getUserDeviceName(model, client.name);
|
||||||
if (filter === "user") {
|
|
||||||
newName = getUserDeviceName(model, client.name);
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
...client,
|
...client,
|
||||||
name: newName,
|
name: newName,
|
||||||
|
|||||||
@@ -117,8 +117,6 @@ export const handleOlmPingMessage: MessageHandler = async (context) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isUserDevice = olm.userId !== null && olm.userId !== undefined;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// get the client
|
// get the client
|
||||||
const [client] = await db
|
const [client] = await db
|
||||||
@@ -221,9 +219,7 @@ export const handleOlmPingMessage: MessageHandler = async (context) => {
|
|||||||
logger.error("Error handling ping message", { error });
|
logger.error("Error handling ping message", { error });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isUserDevice) {
|
await handleFingerprintInsertion(olm, fingerprint, postures);
|
||||||
await handleFingerprintInsertion(olm, fingerprint, postures);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
message: {
|
message: {
|
||||||
|
|||||||
@@ -53,11 +53,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
postures
|
postures
|
||||||
});
|
});
|
||||||
|
|
||||||
const isUserDevice = olm.userId !== null && olm.userId !== undefined;
|
await handleFingerprintInsertion(olm, fingerprint, postures);
|
||||||
|
|
||||||
if (isUserDevice) {
|
|
||||||
await handleFingerprintInsertion(olm, fingerprint, postures);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(olmVersion && olm.version !== olmVersion) ||
|
(olmVersion && olm.version !== olmVersion) ||
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import { hashPassword } from "@server/auth/password";
|
|||||||
import { isValidIP } from "@server/lib/validators";
|
import { isValidIP } from "@server/lib/validators";
|
||||||
import { isIpInCidr } from "@server/lib/ip";
|
import { isIpInCidr } from "@server/lib/ip";
|
||||||
import { verifyExitNodeOrgAccess } from "#dynamic/lib/exitNodes";
|
import { verifyExitNodeOrgAccess } from "#dynamic/lib/exitNodes";
|
||||||
import { build } from "@server/build";
|
|
||||||
|
|
||||||
const createSiteParamsSchema = z.strictObject({
|
const createSiteParamsSchema = z.strictObject({
|
||||||
orgId: z.string()
|
orgId: z.string()
|
||||||
@@ -259,7 +258,19 @@ export async function createSite(
|
|||||||
let newSite: Site;
|
let newSite: Site;
|
||||||
|
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
if (type == "wireguard" || type == "newt") {
|
if (type == "newt") {
|
||||||
|
[newSite] = await trx
|
||||||
|
.insert(sites)
|
||||||
|
.values({
|
||||||
|
orgId,
|
||||||
|
name,
|
||||||
|
niceId,
|
||||||
|
address: updatedAddress || null,
|
||||||
|
type,
|
||||||
|
dockerSocketEnabled: true
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
} else if (type == "wireguard") {
|
||||||
// we are creating a site with an exit node (tunneled)
|
// we are creating a site with an exit node (tunneled)
|
||||||
if (!subnet) {
|
if (!subnet) {
|
||||||
return next(
|
return next(
|
||||||
@@ -311,11 +322,9 @@ export async function createSite(
|
|||||||
exitNodeId,
|
exitNodeId,
|
||||||
name,
|
name,
|
||||||
niceId,
|
niceId,
|
||||||
address: updatedAddress || null,
|
|
||||||
subnet,
|
subnet,
|
||||||
type,
|
type,
|
||||||
dockerSocketEnabled: type == "newt",
|
pubKey: pubKey || null
|
||||||
...(pubKey && type == "wireguard" && { pubKey })
|
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
} else if (type == "local") {
|
} else if (type == "local") {
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import {
|
|||||||
import { Input } from "@app/components/ui/input";
|
import { Input } from "@app/components/ui/input";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
|
||||||
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";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
@@ -52,7 +51,6 @@ export default function Page() {
|
|||||||
>("role");
|
>("role");
|
||||||
const { isUnlocked } = useLicenseStatusContext();
|
const { isUnlocked } = useLicenseStatusContext();
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const { isPaidUser } = usePaidStatus();
|
|
||||||
|
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
||||||
@@ -808,7 +806,7 @@ export default function Page() {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={createLoading || !isPaidUser}
|
disabled={createLoading}
|
||||||
loading={createLoading}
|
loading={createLoading}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// log any issues with the form
|
// log any issues with the form
|
||||||
|
|||||||
@@ -1,8 +1,18 @@
|
|||||||
|
import { pullEnv } from "@app/lib/pullEnv";
|
||||||
|
import { build } from "@server/build";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
interface LayoutProps {
|
interface LayoutProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
params: Promise<{}>;
|
params: Promise<{}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function Layout(props: LayoutProps) {
|
export default async function Layout(props: LayoutProps) {
|
||||||
|
const env = pullEnv();
|
||||||
|
|
||||||
|
if (build !== "saas" && !env.flags.useOrgOnlyIdp) {
|
||||||
|
redirect("/");
|
||||||
|
}
|
||||||
|
|
||||||
return props.children;
|
return props.children;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -195,27 +195,29 @@ export default function CredentialsPage() {
|
|||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
<SettingsSectionFooter>
|
{build !== "oss" && (
|
||||||
<Button
|
<SettingsSectionFooter>
|
||||||
variant="outline"
|
<Button
|
||||||
onClick={() => {
|
variant="outline"
|
||||||
setShouldDisconnect(false);
|
onClick={() => {
|
||||||
setModalOpen(true);
|
setShouldDisconnect(false);
|
||||||
}}
|
setModalOpen(true);
|
||||||
disabled={isSecurityFeatureDisabled()}
|
}}
|
||||||
>
|
disabled={isSecurityFeatureDisabled()}
|
||||||
{t("regenerateCredentialsButton")}
|
>
|
||||||
</Button>
|
{t("regenerateCredentialsButton")}
|
||||||
<Button
|
</Button>
|
||||||
onClick={() => {
|
<Button
|
||||||
setShouldDisconnect(true);
|
onClick={() => {
|
||||||
setModalOpen(true);
|
setShouldDisconnect(true);
|
||||||
}}
|
setModalOpen(true);
|
||||||
disabled={isSecurityFeatureDisabled()}
|
}}
|
||||||
>
|
disabled={isSecurityFeatureDisabled()}
|
||||||
{t("remoteExitNodeRegenerateAndDisconnect")}
|
>
|
||||||
</Button>
|
{t("remoteExitNodeRegenerateAndDisconnect")}
|
||||||
</SettingsSectionFooter>
|
</Button>
|
||||||
|
</SettingsSectionFooter>
|
||||||
|
)}
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
</SettingsContainer>
|
</SettingsContainer>
|
||||||
|
|
||||||
|
|||||||
@@ -61,9 +61,7 @@ export default function CredentialsPage() {
|
|||||||
const isEnterpriseNotLicensed = build === "enterprise" && !isUnlocked();
|
const isEnterpriseNotLicensed = build === "enterprise" && !isUnlocked();
|
||||||
const isSaasNotSubscribed =
|
const isSaasNotSubscribed =
|
||||||
build === "saas" && !subscription?.isSubscribed();
|
build === "saas" && !subscription?.isSubscribed();
|
||||||
return (
|
return isEnterpriseNotLicensed || isSaasNotSubscribed;
|
||||||
isEnterpriseNotLicensed || isSaasNotSubscribed || build === "oss"
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConfirmRegenerate = async () => {
|
const handleConfirmRegenerate = async () => {
|
||||||
@@ -183,27 +181,29 @@ export default function CredentialsPage() {
|
|||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
<SettingsSectionFooter>
|
{build !== "oss" && (
|
||||||
<Button
|
<SettingsSectionFooter>
|
||||||
variant="outline"
|
<Button
|
||||||
onClick={() => {
|
variant="outline"
|
||||||
setShouldDisconnect(false);
|
onClick={() => {
|
||||||
setModalOpen(true);
|
setShouldDisconnect(false);
|
||||||
}}
|
setModalOpen(true);
|
||||||
disabled={isSecurityFeatureDisabled()}
|
}}
|
||||||
>
|
disabled={isSecurityFeatureDisabled()}
|
||||||
{t("regenerateCredentialsButton")}
|
>
|
||||||
</Button>
|
{t("regenerateCredentialsButton")}
|
||||||
<Button
|
</Button>
|
||||||
onClick={() => {
|
<Button
|
||||||
setShouldDisconnect(true);
|
onClick={() => {
|
||||||
setModalOpen(true);
|
setShouldDisconnect(true);
|
||||||
}}
|
setModalOpen(true);
|
||||||
disabled={isSecurityFeatureDisabled()}
|
}}
|
||||||
>
|
disabled={isSecurityFeatureDisabled()}
|
||||||
{t("clientRegenerateAndDisconnect")}
|
>
|
||||||
</Button>
|
{t("clientRegenerateAndDisconnect")}
|
||||||
</SettingsSectionFooter>
|
</Button>
|
||||||
|
</SettingsSectionFooter>
|
||||||
|
)}
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|
||||||
<OlmInstallCommands
|
<OlmInstallCommands
|
||||||
|
|||||||
@@ -28,15 +28,7 @@ import { createApiClient, formatAxiosError } from "@app/lib/api";
|
|||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useState, useEffect, useTransition } from "react";
|
import { useState, useEffect, useTransition } from "react";
|
||||||
import {
|
import { Check, Ban, Shield, ShieldOff, Clock, CheckCircle2, XCircle } from "lucide-react";
|
||||||
Check,
|
|
||||||
Ban,
|
|
||||||
Shield,
|
|
||||||
ShieldOff,
|
|
||||||
Clock,
|
|
||||||
CheckCircle2,
|
|
||||||
XCircle
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { FaApple, FaWindows, FaLinux } from "react-icons/fa";
|
import { FaApple, FaWindows, FaLinux } from "react-icons/fa";
|
||||||
import { SiAndroid } from "react-icons/si";
|
import { SiAndroid } from "react-icons/si";
|
||||||
@@ -119,13 +111,13 @@ function getPlatformFieldConfig(
|
|||||||
osVersion: { show: true, labelKey: "iosVersion" },
|
osVersion: { show: true, labelKey: "iosVersion" },
|
||||||
kernelVersion: { show: false, labelKey: "kernelVersion" },
|
kernelVersion: { show: false, labelKey: "kernelVersion" },
|
||||||
arch: { show: true, labelKey: "architecture" },
|
arch: { show: true, labelKey: "architecture" },
|
||||||
deviceModel: { show: true, labelKey: "deviceModel" }
|
deviceModel: { show: true, labelKey: "deviceModel" },
|
||||||
},
|
},
|
||||||
android: {
|
android: {
|
||||||
osVersion: { show: true, labelKey: "androidVersion" },
|
osVersion: { show: true, labelKey: "androidVersion" },
|
||||||
kernelVersion: { show: true, labelKey: "kernelVersion" },
|
kernelVersion: { show: true, labelKey: "kernelVersion" },
|
||||||
arch: { show: true, labelKey: "architecture" },
|
arch: { show: true, labelKey: "architecture" },
|
||||||
deviceModel: { show: true, labelKey: "deviceModel" }
|
deviceModel: { show: true, labelKey: "deviceModel" },
|
||||||
},
|
},
|
||||||
unknown: {
|
unknown: {
|
||||||
osVersion: { show: true, labelKey: "osVersion" },
|
osVersion: { show: true, labelKey: "osVersion" },
|
||||||
@@ -141,6 +133,7 @@ function getPlatformFieldConfig(
|
|||||||
return configs[normalizedPlatform] || configs.unknown;
|
return configs[normalizedPlatform] || configs.unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default function GeneralPage() {
|
export default function GeneralPage() {
|
||||||
const { client, updateClient } = useClientContext();
|
const { client, updateClient } = useClientContext();
|
||||||
const { isPaidUser } = usePaidStatus();
|
const { isPaidUser } = usePaidStatus();
|
||||||
@@ -430,8 +423,7 @@ export default function GeneralPage() {
|
|||||||
{t(
|
{t(
|
||||||
fieldConfig
|
fieldConfig
|
||||||
.osVersion
|
.osVersion
|
||||||
?.labelKey ||
|
?.labelKey || "osVersion"
|
||||||
"osVersion"
|
|
||||||
)}
|
)}
|
||||||
</InfoSectionTitle>
|
</InfoSectionTitle>
|
||||||
<InfoSectionContent>
|
<InfoSectionContent>
|
||||||
@@ -567,231 +559,217 @@ export default function GeneralPage() {
|
|||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<SettingsSection>
|
{/* Device Security Section */}
|
||||||
<SettingsSectionHeader>
|
{build !== "oss" && (
|
||||||
<SettingsSectionTitle>
|
<SettingsSection>
|
||||||
{t("deviceSecurity")}
|
<SettingsSectionHeader>
|
||||||
</SettingsSectionTitle>
|
<SettingsSectionTitle>
|
||||||
<SettingsSectionDescription>
|
{t("deviceSecurity")}
|
||||||
{t("deviceSecurityDescription")}
|
</SettingsSectionTitle>
|
||||||
</SettingsSectionDescription>
|
<SettingsSectionDescription>
|
||||||
</SettingsSectionHeader>
|
{t("deviceSecurityDescription")}
|
||||||
|
</SettingsSectionDescription>
|
||||||
|
</SettingsSectionHeader>
|
||||||
|
|
||||||
<SettingsSectionBody>
|
<SettingsSectionBody>
|
||||||
<PaidFeaturesAlert />
|
{client.posture && Object.keys(client.posture).length > 0 ? (
|
||||||
{client.posture &&
|
<>
|
||||||
Object.keys(client.posture).length > 0 ? (
|
{!isPaidUser && <PaidFeaturesAlert />}
|
||||||
<>
|
<InfoSections cols={3}>
|
||||||
<InfoSections cols={3}>
|
{client.posture.biometricsEnabled !== null &&
|
||||||
{client.posture.biometricsEnabled !== null &&
|
client.posture.biometricsEnabled !== undefined && (
|
||||||
client.posture.biometricsEnabled !==
|
<InfoSection>
|
||||||
undefined && (
|
<InfoSectionTitle>
|
||||||
<InfoSection>
|
{t("biometricsEnabled")}
|
||||||
<InfoSectionTitle>
|
</InfoSectionTitle>
|
||||||
{t("biometricsEnabled")}
|
<InfoSectionContent>
|
||||||
</InfoSectionTitle>
|
{isPaidUser
|
||||||
<InfoSectionContent>
|
? formatPostureValue(
|
||||||
{isPaidUser
|
client.posture.biometricsEnabled
|
||||||
? formatPostureValue(
|
)
|
||||||
client.posture
|
: "-"}
|
||||||
.biometricsEnabled
|
</InfoSectionContent>
|
||||||
)
|
</InfoSection>
|
||||||
: "-"}
|
)}
|
||||||
</InfoSectionContent>
|
|
||||||
</InfoSection>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{client.posture.diskEncrypted !== null &&
|
{client.posture.diskEncrypted !== null &&
|
||||||
client.posture.diskEncrypted !==
|
client.posture.diskEncrypted !== undefined && (
|
||||||
undefined && (
|
<InfoSection>
|
||||||
<InfoSection>
|
<InfoSectionTitle>
|
||||||
<InfoSectionTitle>
|
{t("diskEncrypted")}
|
||||||
{t("diskEncrypted")}
|
</InfoSectionTitle>
|
||||||
</InfoSectionTitle>
|
<InfoSectionContent>
|
||||||
<InfoSectionContent>
|
{isPaidUser
|
||||||
{isPaidUser
|
? formatPostureValue(
|
||||||
? formatPostureValue(
|
client.posture.diskEncrypted
|
||||||
client.posture
|
)
|
||||||
.diskEncrypted
|
: "-"}
|
||||||
)
|
</InfoSectionContent>
|
||||||
: "-"}
|
</InfoSection>
|
||||||
</InfoSectionContent>
|
)}
|
||||||
</InfoSection>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{client.posture.firewallEnabled !== null &&
|
{client.posture.firewallEnabled !== null &&
|
||||||
client.posture.firewallEnabled !==
|
client.posture.firewallEnabled !== undefined && (
|
||||||
undefined && (
|
<InfoSection>
|
||||||
<InfoSection>
|
<InfoSectionTitle>
|
||||||
<InfoSectionTitle>
|
{t("firewallEnabled")}
|
||||||
{t("firewallEnabled")}
|
</InfoSectionTitle>
|
||||||
</InfoSectionTitle>
|
<InfoSectionContent>
|
||||||
<InfoSectionContent>
|
{isPaidUser
|
||||||
{isPaidUser
|
? formatPostureValue(
|
||||||
? formatPostureValue(
|
client.posture.firewallEnabled
|
||||||
client.posture
|
)
|
||||||
.firewallEnabled
|
: "-"}
|
||||||
)
|
</InfoSectionContent>
|
||||||
: "-"}
|
</InfoSection>
|
||||||
</InfoSectionContent>
|
)}
|
||||||
</InfoSection>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{client.posture.autoUpdatesEnabled !== null &&
|
{client.posture.autoUpdatesEnabled !== null &&
|
||||||
client.posture.autoUpdatesEnabled !==
|
client.posture.autoUpdatesEnabled !== undefined && (
|
||||||
undefined && (
|
<InfoSection>
|
||||||
<InfoSection>
|
<InfoSectionTitle>
|
||||||
<InfoSectionTitle>
|
{t("autoUpdatesEnabled")}
|
||||||
{t("autoUpdatesEnabled")}
|
</InfoSectionTitle>
|
||||||
</InfoSectionTitle>
|
<InfoSectionContent>
|
||||||
<InfoSectionContent>
|
{isPaidUser
|
||||||
{isPaidUser
|
? formatPostureValue(
|
||||||
? formatPostureValue(
|
client.posture.autoUpdatesEnabled
|
||||||
client.posture
|
)
|
||||||
.autoUpdatesEnabled
|
: "-"}
|
||||||
)
|
</InfoSectionContent>
|
||||||
: "-"}
|
</InfoSection>
|
||||||
</InfoSectionContent>
|
)}
|
||||||
</InfoSection>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{client.posture.tpmAvailable !== null &&
|
{client.posture.tpmAvailable !== null &&
|
||||||
client.posture.tpmAvailable !==
|
client.posture.tpmAvailable !== undefined && (
|
||||||
undefined && (
|
<InfoSection>
|
||||||
<InfoSection>
|
<InfoSectionTitle>
|
||||||
<InfoSectionTitle>
|
{t("tpmAvailable")}
|
||||||
{t("tpmAvailable")}
|
</InfoSectionTitle>
|
||||||
</InfoSectionTitle>
|
<InfoSectionContent>
|
||||||
<InfoSectionContent>
|
{isPaidUser
|
||||||
{isPaidUser
|
? formatPostureValue(
|
||||||
? formatPostureValue(
|
client.posture.tpmAvailable
|
||||||
client.posture
|
)
|
||||||
.tpmAvailable
|
: "-"}
|
||||||
)
|
</InfoSectionContent>
|
||||||
: "-"}
|
</InfoSection>
|
||||||
</InfoSectionContent>
|
)}
|
||||||
</InfoSection>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{client.posture.windowsAntivirusEnabled !==
|
{client.posture.windowsAntivirusEnabled !== null &&
|
||||||
null &&
|
client.posture.windowsAntivirusEnabled !== undefined && (
|
||||||
client.posture.windowsAntivirusEnabled !==
|
<InfoSection>
|
||||||
undefined && (
|
<InfoSectionTitle>
|
||||||
<InfoSection>
|
{t("windowsAntivirusEnabled")}
|
||||||
<InfoSectionTitle>
|
</InfoSectionTitle>
|
||||||
{t("windowsAntivirusEnabled")}
|
<InfoSectionContent>
|
||||||
</InfoSectionTitle>
|
{isPaidUser
|
||||||
<InfoSectionContent>
|
? formatPostureValue(
|
||||||
{isPaidUser
|
client.posture
|
||||||
? formatPostureValue(
|
.windowsAntivirusEnabled
|
||||||
client.posture
|
)
|
||||||
.windowsAntivirusEnabled
|
: "-"}
|
||||||
)
|
</InfoSectionContent>
|
||||||
: "-"}
|
</InfoSection>
|
||||||
</InfoSectionContent>
|
)}
|
||||||
</InfoSection>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{client.posture.macosSipEnabled !== null &&
|
{client.posture.macosSipEnabled !== null &&
|
||||||
client.posture.macosSipEnabled !==
|
client.posture.macosSipEnabled !== undefined && (
|
||||||
undefined && (
|
<InfoSection>
|
||||||
<InfoSection>
|
<InfoSectionTitle>
|
||||||
<InfoSectionTitle>
|
{t("macosSipEnabled")}
|
||||||
{t("macosSipEnabled")}
|
</InfoSectionTitle>
|
||||||
</InfoSectionTitle>
|
<InfoSectionContent>
|
||||||
<InfoSectionContent>
|
{isPaidUser
|
||||||
{isPaidUser
|
? formatPostureValue(
|
||||||
? formatPostureValue(
|
client.posture.macosSipEnabled
|
||||||
client.posture
|
)
|
||||||
.macosSipEnabled
|
: "-"}
|
||||||
)
|
</InfoSectionContent>
|
||||||
: "-"}
|
</InfoSection>
|
||||||
</InfoSectionContent>
|
)}
|
||||||
</InfoSection>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{client.posture.macosGatekeeperEnabled !==
|
{client.posture.macosGatekeeperEnabled !== null &&
|
||||||
null &&
|
client.posture.macosGatekeeperEnabled !==
|
||||||
client.posture.macosGatekeeperEnabled !==
|
undefined && (
|
||||||
undefined && (
|
<InfoSection>
|
||||||
<InfoSection>
|
<InfoSectionTitle>
|
||||||
<InfoSectionTitle>
|
{t("macosGatekeeperEnabled")}
|
||||||
{t("macosGatekeeperEnabled")}
|
</InfoSectionTitle>
|
||||||
</InfoSectionTitle>
|
<InfoSectionContent>
|
||||||
<InfoSectionContent>
|
{isPaidUser
|
||||||
{isPaidUser
|
? formatPostureValue(
|
||||||
? formatPostureValue(
|
client.posture
|
||||||
client.posture
|
.macosGatekeeperEnabled
|
||||||
.macosGatekeeperEnabled
|
)
|
||||||
)
|
: "-"}
|
||||||
: "-"}
|
</InfoSectionContent>
|
||||||
</InfoSectionContent>
|
</InfoSection>
|
||||||
</InfoSection>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
{client.posture.macosFirewallStealthMode !==
|
{client.posture.macosFirewallStealthMode !== null &&
|
||||||
null &&
|
client.posture.macosFirewallStealthMode !==
|
||||||
client.posture.macosFirewallStealthMode !==
|
undefined && (
|
||||||
undefined && (
|
<InfoSection>
|
||||||
<InfoSection>
|
<InfoSectionTitle>
|
||||||
<InfoSectionTitle>
|
{t("macosFirewallStealthMode")}
|
||||||
{t("macosFirewallStealthMode")}
|
</InfoSectionTitle>
|
||||||
</InfoSectionTitle>
|
<InfoSectionContent>
|
||||||
<InfoSectionContent>
|
{isPaidUser
|
||||||
{isPaidUser
|
? formatPostureValue(
|
||||||
? formatPostureValue(
|
client.posture
|
||||||
client.posture
|
.macosFirewallStealthMode
|
||||||
.macosFirewallStealthMode
|
)
|
||||||
)
|
: "-"}
|
||||||
: "-"}
|
</InfoSectionContent>
|
||||||
</InfoSectionContent>
|
</InfoSection>
|
||||||
</InfoSection>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
{client.posture.linuxAppArmorEnabled !== null &&
|
{client.posture.linuxAppArmorEnabled !== null &&
|
||||||
client.posture.linuxAppArmorEnabled !==
|
client.posture.linuxAppArmorEnabled !==
|
||||||
undefined && (
|
undefined && (
|
||||||
<InfoSection>
|
<InfoSection>
|
||||||
<InfoSectionTitle>
|
<InfoSectionTitle>
|
||||||
{t("linuxAppArmorEnabled")}
|
{t("linuxAppArmorEnabled")}
|
||||||
</InfoSectionTitle>
|
</InfoSectionTitle>
|
||||||
<InfoSectionContent>
|
<InfoSectionContent>
|
||||||
{isPaidUser
|
{isPaidUser
|
||||||
? formatPostureValue(
|
? formatPostureValue(
|
||||||
client.posture
|
client.posture
|
||||||
.linuxAppArmorEnabled
|
.linuxAppArmorEnabled
|
||||||
)
|
)
|
||||||
: "-"}
|
: "-"}
|
||||||
</InfoSectionContent>
|
</InfoSectionContent>
|
||||||
</InfoSection>
|
</InfoSection>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{client.posture.linuxSELinuxEnabled !== null &&
|
{client.posture.linuxSELinuxEnabled !== null &&
|
||||||
client.posture.linuxSELinuxEnabled !==
|
client.posture.linuxSELinuxEnabled !==
|
||||||
undefined && (
|
undefined && (
|
||||||
<InfoSection>
|
<InfoSection>
|
||||||
<InfoSectionTitle>
|
<InfoSectionTitle>
|
||||||
{t("linuxSELinuxEnabled")}
|
{t("linuxSELinuxEnabled")}
|
||||||
</InfoSectionTitle>
|
</InfoSectionTitle>
|
||||||
<InfoSectionContent>
|
<InfoSectionContent>
|
||||||
{isPaidUser
|
{isPaidUser
|
||||||
? formatPostureValue(
|
? formatPostureValue(
|
||||||
client.posture
|
client.posture
|
||||||
.linuxSELinuxEnabled
|
.linuxSELinuxEnabled
|
||||||
)
|
)
|
||||||
: "-"}
|
: "-"}
|
||||||
</InfoSectionContent>
|
</InfoSectionContent>
|
||||||
</InfoSection>
|
</InfoSection>
|
||||||
)}
|
)}
|
||||||
</InfoSections>
|
</InfoSections>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-muted-foreground">
|
<div className="text-muted-foreground">
|
||||||
{t("noData")}
|
{t("noData")}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
)}
|
||||||
</SettingsContainer>
|
</SettingsContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ export interface AuthPageProps {
|
|||||||
export default async function AuthPage(props: AuthPageProps) {
|
export default async function AuthPage(props: AuthPageProps) {
|
||||||
const orgId = (await props.params).orgId;
|
const orgId = (await props.params).orgId;
|
||||||
|
|
||||||
|
// custom auth branding is only available in enterprise and saas
|
||||||
|
if (build === "oss") {
|
||||||
|
redirect(`/${orgId}/settings/general/`);
|
||||||
|
}
|
||||||
|
|
||||||
let subscriptionStatus: GetOrgTierResponse | null = null;
|
let subscriptionStatus: GetOrgTierResponse | null = null;
|
||||||
try {
|
try {
|
||||||
const subRes = await getCachedSubscription(orgId);
|
const subRes = await getCachedSubscription(orgId);
|
||||||
|
|||||||
@@ -55,12 +55,14 @@ export default async function GeneralSettingsPage({
|
|||||||
{
|
{
|
||||||
title: t("security"),
|
title: t("security"),
|
||||||
href: `/{orgId}/settings/general/security`
|
href: `/{orgId}/settings/general/security`
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("authPage"),
|
|
||||||
href: `/{orgId}/settings/general/auth-page`
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
if (build !== "oss") {
|
||||||
|
navItems.push({
|
||||||
|
title: t("authPage"),
|
||||||
|
href: `/{orgId}/settings/general/auth-page`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -3,7 +3,12 @@ import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
|||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
import { useOrgContext } from "@app/hooks/useOrgContext";
|
import { useOrgContext } from "@app/hooks/useOrgContext";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { useState, useRef, useActionState, type ComponentRef } from "react";
|
import {
|
||||||
|
useState,
|
||||||
|
useRef,
|
||||||
|
useActionState,
|
||||||
|
type ComponentRef
|
||||||
|
} from "react";
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@@ -105,7 +110,7 @@ export default function SecurityPage() {
|
|||||||
return (
|
return (
|
||||||
<SettingsContainer>
|
<SettingsContainer>
|
||||||
<LogRetentionSectionForm org={org.org} />
|
<LogRetentionSectionForm org={org.org} />
|
||||||
<SecuritySettingsSectionForm org={org.org} />
|
{build !== "oss" && <SecuritySettingsSectionForm org={org.org} />}
|
||||||
</SettingsContainer>
|
</SettingsContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -238,120 +243,144 @@ function LogRetentionSectionForm({ org }: SectionFormProps) {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PaidFeaturesAlert />
|
{build !== "oss" && (
|
||||||
|
<>
|
||||||
|
<PaidFeaturesAlert />
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="settingsLogRetentionDaysAccess"
|
name="settingsLogRetentionDaysAccess"
|
||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
const isDisabled = !isPaidUser;
|
const isDisabled = !isPaidUser;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{t("logRetentionAccessLabel")}
|
{t(
|
||||||
</FormLabel>
|
"logRetentionAccessLabel"
|
||||||
<FormControl>
|
|
||||||
<Select
|
|
||||||
value={field.value.toString()}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
if (!isDisabled) {
|
|
||||||
field.onChange(
|
|
||||||
parseInt(
|
|
||||||
value,
|
|
||||||
10
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={isDisabled}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue
|
|
||||||
placeholder={t(
|
|
||||||
"selectLogRetention"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{LOG_RETENTION_OPTIONS.map(
|
|
||||||
(option) => (
|
|
||||||
<SelectItem
|
|
||||||
key={
|
|
||||||
option.value
|
|
||||||
}
|
|
||||||
value={option.value.toString()}
|
|
||||||
>
|
|
||||||
{t(
|
|
||||||
option.label
|
|
||||||
)}
|
|
||||||
</SelectItem>
|
|
||||||
)
|
|
||||||
)}
|
)}
|
||||||
</SelectContent>
|
</FormLabel>
|
||||||
</Select>
|
<FormControl>
|
||||||
</FormControl>
|
<Select
|
||||||
<FormMessage />
|
value={field.value.toString()}
|
||||||
</FormItem>
|
onValueChange={(
|
||||||
);
|
value
|
||||||
}}
|
) => {
|
||||||
/>
|
if (
|
||||||
<FormField
|
!isDisabled
|
||||||
control={form.control}
|
) {
|
||||||
name="settingsLogRetentionDaysAction"
|
field.onChange(
|
||||||
render={({ field }) => {
|
parseInt(
|
||||||
const isDisabled = !isPaidUser;
|
value,
|
||||||
|
10
|
||||||
return (
|
)
|
||||||
<FormItem>
|
);
|
||||||
<FormLabel>
|
}
|
||||||
{t("logRetentionActionLabel")}
|
}}
|
||||||
</FormLabel>
|
disabled={
|
||||||
<FormControl>
|
isDisabled
|
||||||
<Select
|
}
|
||||||
value={field.value.toString()}
|
>
|
||||||
onValueChange={(value) => {
|
<SelectTrigger>
|
||||||
if (!isDisabled) {
|
<SelectValue
|
||||||
field.onChange(
|
placeholder={t(
|
||||||
parseInt(
|
"selectLogRetention"
|
||||||
value,
|
|
||||||
10
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={isDisabled}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue
|
|
||||||
placeholder={t(
|
|
||||||
"selectLogRetention"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{LOG_RETENTION_OPTIONS.map(
|
|
||||||
(option) => (
|
|
||||||
<SelectItem
|
|
||||||
key={
|
|
||||||
option.value
|
|
||||||
}
|
|
||||||
value={option.value.toString()}
|
|
||||||
>
|
|
||||||
{t(
|
|
||||||
option.label
|
|
||||||
)}
|
)}
|
||||||
</SelectItem>
|
/>
|
||||||
)
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{LOG_RETENTION_OPTIONS.map(
|
||||||
|
(
|
||||||
|
option
|
||||||
|
) => (
|
||||||
|
<SelectItem
|
||||||
|
key={
|
||||||
|
option.value
|
||||||
|
}
|
||||||
|
value={option.value.toString()}
|
||||||
|
>
|
||||||
|
{t(
|
||||||
|
option.label
|
||||||
|
)}
|
||||||
|
</SelectItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="settingsLogRetentionDaysAction"
|
||||||
|
render={({ field }) => {
|
||||||
|
const isDisabled = !isPaidUser;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t(
|
||||||
|
"logRetentionActionLabel"
|
||||||
)}
|
)}
|
||||||
</SelectContent>
|
</FormLabel>
|
||||||
</Select>
|
<FormControl>
|
||||||
</FormControl>
|
<Select
|
||||||
<FormMessage />
|
value={field.value.toString()}
|
||||||
</FormItem>
|
onValueChange={(
|
||||||
);
|
value
|
||||||
}}
|
) => {
|
||||||
/>
|
if (
|
||||||
|
!isDisabled
|
||||||
|
) {
|
||||||
|
field.onChange(
|
||||||
|
parseInt(
|
||||||
|
value,
|
||||||
|
10
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={
|
||||||
|
isDisabled
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue
|
||||||
|
placeholder={t(
|
||||||
|
"selectLogRetention"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{LOG_RETENTION_OPTIONS.map(
|
||||||
|
(
|
||||||
|
option
|
||||||
|
) => (
|
||||||
|
<SelectItem
|
||||||
|
key={
|
||||||
|
option.value
|
||||||
|
}
|
||||||
|
value={option.value.toString()}
|
||||||
|
>
|
||||||
|
{t(
|
||||||
|
option.label
|
||||||
|
)}
|
||||||
|
</SelectItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</SettingsSectionForm>
|
</SettingsSectionForm>
|
||||||
@@ -711,7 +740,7 @@ function SecuritySettingsSectionForm({ org }: SectionFormProps) {
|
|||||||
type="submit"
|
type="submit"
|
||||||
form="security-settings-section-form"
|
form="security-settings-section-form"
|
||||||
loading={loadingSave}
|
loading={loadingSave}
|
||||||
disabled={loadingSave || !isPaidUser}
|
disabled={loadingSave}
|
||||||
>
|
>
|
||||||
{t("saveSettings")}
|
{t("saveSettings")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import { Alert, AlertDescription } from "@app/components/ui/alert";
|
|||||||
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
|
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { useStoredPageSize } from "@app/hooks/useStoredPageSize";
|
import { useStoredPageSize } from "@app/hooks/useStoredPageSize";
|
||||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
|
||||||
|
|
||||||
export default function GeneralPage() {
|
export default function GeneralPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -210,8 +209,7 @@ export default function GeneralPage() {
|
|||||||
console.log("Date range changed:", { startDate, endDate, page, size });
|
console.log("Date range changed:", { startDate, endDate, page, size });
|
||||||
if (
|
if (
|
||||||
(build == "saas" && !subscription?.subscribed) ||
|
(build == "saas" && !subscription?.subscribed) ||
|
||||||
(build == "enterprise" && !isUnlocked()) ||
|
(build == "enterprise" && !isUnlocked())
|
||||||
build === "oss"
|
|
||||||
) {
|
) {
|
||||||
console.log(
|
console.log(
|
||||||
"Access denied: subscription inactive or license locked"
|
"Access denied: subscription inactive or license locked"
|
||||||
@@ -613,7 +611,21 @@ export default function GeneralPage() {
|
|||||||
description={t("accessLogsDescription")}
|
description={t("accessLogsDescription")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PaidFeaturesAlert />
|
{build == "saas" && !subscription?.subscribed ? (
|
||||||
|
<Alert variant="info" className="mb-6">
|
||||||
|
<AlertDescription>
|
||||||
|
{t("subscriptionRequiredToUse")}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{build == "enterprise" && !isUnlocked() ? (
|
||||||
|
<Alert variant="info" className="mb-6">
|
||||||
|
<AlertDescription>
|
||||||
|
{t("licenseRequiredToUse")}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<LogDataTable
|
<LogDataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
@@ -644,8 +656,7 @@ export default function GeneralPage() {
|
|||||||
renderExpandedRow={renderExpandedRow}
|
renderExpandedRow={renderExpandedRow}
|
||||||
disabled={
|
disabled={
|
||||||
(build == "saas" && !subscription?.subscribed) ||
|
(build == "saas" && !subscription?.subscribed) ||
|
||||||
(build == "enterprise" && !isUnlocked()) ||
|
(build == "enterprise" && !isUnlocked())
|
||||||
build === "oss"
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
import { ColumnFilter } from "@app/components/ColumnFilter";
|
import { ColumnFilter } from "@app/components/ColumnFilter";
|
||||||
import { DateTimeValue } from "@app/components/DateTimePicker";
|
import { DateTimeValue } from "@app/components/DateTimePicker";
|
||||||
import { LogDataTable } from "@app/components/LogDataTable";
|
import { LogDataTable } from "@app/components/LogDataTable";
|
||||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
|
||||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||||
import { Alert, AlertDescription } from "@app/components/ui/alert";
|
import { Alert, AlertDescription } from "@app/components/ui/alert";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
@@ -93,9 +92,6 @@ export default function GeneralPage() {
|
|||||||
|
|
||||||
// Trigger search with default values on component mount
|
// Trigger search with default values on component mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (build === "oss") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const defaultRange = getDefaultDateRange();
|
const defaultRange = getDefaultDateRange();
|
||||||
queryDateTime(
|
queryDateTime(
|
||||||
defaultRange.startDate,
|
defaultRange.startDate,
|
||||||
@@ -465,7 +461,21 @@ export default function GeneralPage() {
|
|||||||
description={t("actionLogsDescription")}
|
description={t("actionLogsDescription")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PaidFeaturesAlert />
|
{build == "saas" && !subscription?.subscribed ? (
|
||||||
|
<Alert variant="info" className="mb-6">
|
||||||
|
<AlertDescription>
|
||||||
|
{t("subscriptionRequiredToUse")}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{build == "enterprise" && !isUnlocked() ? (
|
||||||
|
<Alert variant="info" className="mb-6">
|
||||||
|
<AlertDescription>
|
||||||
|
{t("licenseRequiredToUse")}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<LogDataTable
|
<LogDataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
@@ -498,8 +508,7 @@ export default function GeneralPage() {
|
|||||||
renderExpandedRow={renderExpandedRow}
|
renderExpandedRow={renderExpandedRow}
|
||||||
disabled={
|
disabled={
|
||||||
(build == "saas" && !subscription?.subscribed) ||
|
(build == "saas" && !subscription?.subscribed) ||
|
||||||
(build == "enterprise" && !isUnlocked()) ||
|
(build == "enterprise" && !isUnlocked())
|
||||||
build === "oss"
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import Link from "next/link";
|
|||||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||||
import { useEffect, useState, useTransition } from "react";
|
import { useEffect, useState, useTransition } from "react";
|
||||||
import { useStoredPageSize } from "@app/hooks/useStoredPageSize";
|
import { useStoredPageSize } from "@app/hooks/useStoredPageSize";
|
||||||
import { build } from "@server/build";
|
|
||||||
|
|
||||||
export default function GeneralPage() {
|
export default function GeneralPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -111,9 +110,6 @@ export default function GeneralPage() {
|
|||||||
|
|
||||||
// Trigger search with default values on component mount
|
// Trigger search with default values on component mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (build === "oss") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const defaultRange = getDefaultDateRange();
|
const defaultRange = getDefaultDateRange();
|
||||||
queryDateTime(
|
queryDateTime(
|
||||||
defaultRange.startDate,
|
defaultRange.startDate,
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ import {
|
|||||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||||
import { GetResourceResponse } from "@server/routers/resource/getResource";
|
import { GetResourceResponse } from "@server/routers/resource/getResource";
|
||||||
import type { ResourceContextType } from "@app/contexts/resourceContext";
|
import type { ResourceContextType } from "@app/contexts/resourceContext";
|
||||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
|
||||||
|
|
||||||
type MaintenanceSectionFormProps = {
|
type MaintenanceSectionFormProps = {
|
||||||
resource: GetResourceResponse;
|
resource: GetResourceResponse;
|
||||||
@@ -79,7 +78,6 @@ function MaintenanceSectionForm({
|
|||||||
const api = createApiClient({ env });
|
const api = createApiClient({ env });
|
||||||
const { isUnlocked } = useLicenseStatusContext();
|
const { isUnlocked } = useLicenseStatusContext();
|
||||||
const subscription = useSubscriptionStatusContext();
|
const subscription = useSubscriptionStatusContext();
|
||||||
const { isPaidUser } = usePaidStatus();
|
|
||||||
|
|
||||||
const MaintenanceFormSchema = z.object({
|
const MaintenanceFormSchema = z.object({
|
||||||
maintenanceModeEnabled: z.boolean().optional(),
|
maintenanceModeEnabled: z.boolean().optional(),
|
||||||
@@ -163,7 +161,7 @@ function MaintenanceSectionForm({
|
|||||||
const isEnterpriseNotLicensed = build === "enterprise" && !isUnlocked();
|
const isEnterpriseNotLicensed = build === "enterprise" && !isUnlocked();
|
||||||
const isSaasNotSubscribed =
|
const isSaasNotSubscribed =
|
||||||
build === "saas" && !subscription?.isSubscribed();
|
build === "saas" && !subscription?.isSubscribed();
|
||||||
return isEnterpriseNotLicensed || isSaasNotSubscribed || build === "oss";
|
return isEnterpriseNotLicensed || isSaasNotSubscribed;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!resource.http) {
|
if (!resource.http) {
|
||||||
@@ -415,7 +413,7 @@ function MaintenanceSectionForm({
|
|||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
loading={maintenanceSaveLoading}
|
loading={maintenanceSaveLoading}
|
||||||
disabled={maintenanceSaveLoading || !isPaidUser }
|
disabled={maintenanceSaveLoading}
|
||||||
form="maintenance-settings-form"
|
form="maintenance-settings-form"
|
||||||
>
|
>
|
||||||
{t("saveSettings")}
|
{t("saveSettings")}
|
||||||
@@ -741,10 +739,12 @@ export default function GeneralForm() {
|
|||||||
</SettingsSectionFooter>
|
</SettingsSectionFooter>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|
||||||
<MaintenanceSectionForm
|
{build !== "oss" && (
|
||||||
resource={resource}
|
<MaintenanceSectionForm
|
||||||
updateResource={updateResource}
|
resource={resource}
|
||||||
/>
|
updateResource={updateResource}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</SettingsContainer>
|
</SettingsContainer>
|
||||||
|
|
||||||
<Credenza
|
<Credenza
|
||||||
|
|||||||
@@ -72,9 +72,7 @@ export default function CredentialsPage() {
|
|||||||
const isEnterpriseNotLicensed = build === "enterprise" && !isUnlocked();
|
const isEnterpriseNotLicensed = build === "enterprise" && !isUnlocked();
|
||||||
const isSaasNotSubscribed =
|
const isSaasNotSubscribed =
|
||||||
build === "saas" && !subscription?.isSubscribed();
|
build === "saas" && !subscription?.isSubscribed();
|
||||||
return (
|
return isEnterpriseNotLicensed || isSaasNotSubscribed;
|
||||||
isEnterpriseNotLicensed || isSaasNotSubscribed || build === "oss"
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fetch site defaults for wireguard sites to show in obfuscated config
|
// Fetch site defaults for wireguard sites to show in obfuscated config
|
||||||
@@ -271,27 +269,29 @@ export default function CredentialsPage() {
|
|||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
<SettingsSectionFooter>
|
{build !== "oss" && (
|
||||||
<Button
|
<SettingsSectionFooter>
|
||||||
variant="outline"
|
<Button
|
||||||
onClick={() => {
|
variant="outline"
|
||||||
setShouldDisconnect(false);
|
onClick={() => {
|
||||||
setModalOpen(true);
|
setShouldDisconnect(false);
|
||||||
}}
|
setModalOpen(true);
|
||||||
disabled={isSecurityFeatureDisabled()}
|
}}
|
||||||
>
|
disabled={isSecurityFeatureDisabled()}
|
||||||
{t("regenerateCredentialsButton")}
|
>
|
||||||
</Button>
|
{t("regenerateCredentialsButton")}
|
||||||
<Button
|
</Button>
|
||||||
onClick={() => {
|
<Button
|
||||||
setShouldDisconnect(true);
|
onClick={() => {
|
||||||
setModalOpen(true);
|
setShouldDisconnect(true);
|
||||||
}}
|
setModalOpen(true);
|
||||||
disabled={isSecurityFeatureDisabled()}
|
}}
|
||||||
>
|
disabled={isSecurityFeatureDisabled()}
|
||||||
{t("siteRegenerateAndDisconnect")}
|
>
|
||||||
</Button>
|
{t("siteRegenerateAndDisconnect")}
|
||||||
</SettingsSectionFooter>
|
</Button>
|
||||||
|
</SettingsSectionFooter>
|
||||||
|
)}
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|
||||||
<NewtSiteInstallCommands
|
<NewtSiteInstallCommands
|
||||||
@@ -383,14 +383,16 @@ export default function CredentialsPage() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
<SettingsSectionFooter>
|
{build === "enterprise" && (
|
||||||
<Button
|
<SettingsSectionFooter>
|
||||||
onClick={() => setModalOpen(true)}
|
<Button
|
||||||
disabled={isSecurityFeatureDisabled()}
|
onClick={() => setModalOpen(true)}
|
||||||
>
|
disabled={isSecurityFeatureDisabled()}
|
||||||
{t("siteRegenerateAndDisconnect")}
|
>
|
||||||
</Button>
|
{t("siteRegenerateAndDisconnect")}
|
||||||
</SettingsSectionFooter>
|
</Button>
|
||||||
|
</SettingsSectionFooter>
|
||||||
|
)}
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
)}
|
)}
|
||||||
</SettingsContainer>
|
</SettingsContainer>
|
||||||
|
|||||||
@@ -7,35 +7,22 @@ import { cache } from "react";
|
|||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
searchParams: Promise<{ code?: string; user?: string }>;
|
searchParams: Promise<{ code?: string }>;
|
||||||
};
|
};
|
||||||
|
|
||||||
function deviceRedirectSearchParams(params: {
|
|
||||||
code?: string;
|
|
||||||
user?: string;
|
|
||||||
}): string {
|
|
||||||
const search = new URLSearchParams();
|
|
||||||
if (params.code) search.set("code", params.code);
|
|
||||||
if (params.user) search.set("user", params.user);
|
|
||||||
const q = search.toString();
|
|
||||||
return q ? `?${q}` : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function DeviceLoginPage({ searchParams }: Props) {
|
export default async function DeviceLoginPage({ searchParams }: Props) {
|
||||||
const user = await verifySession({ forceLogin: true });
|
const user = await verifySession({ forceLogin: true });
|
||||||
|
|
||||||
const params = await searchParams;
|
const params = await searchParams;
|
||||||
const code = params.code || "";
|
const code = params.code || "";
|
||||||
const defaultUser = params.user;
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
const redirectDestination = `/auth/login/device${deviceRedirectSearchParams({ code, user: params.user })}`;
|
const redirectDestination = code
|
||||||
const loginUrl = new URL("/auth/login", "http://x");
|
? `/auth/login/device?code=${encodeURIComponent(code)}`
|
||||||
loginUrl.searchParams.set("forceLogin", "true");
|
: "/auth/login/device";
|
||||||
loginUrl.searchParams.set("redirect", redirectDestination);
|
redirect(
|
||||||
if (defaultUser) loginUrl.searchParams.set("user", defaultUser);
|
`/auth/login?forceLogin=true&redirect=${encodeURIComponent(redirectDestination)}`
|
||||||
console.log("loginUrl", loginUrl.pathname + loginUrl.search);
|
);
|
||||||
redirect(loginUrl.pathname + loginUrl.search);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const userName = user
|
const userName = user
|
||||||
@@ -50,7 +37,6 @@ export default async function DeviceLoginPage({ searchParams }: Props) {
|
|||||||
userEmail={user?.email || ""}
|
userEmail={user?.email || ""}
|
||||||
userName={userName}
|
userName={userName}
|
||||||
initialCode={code}
|
initialCode={code}
|
||||||
userQueryParam={defaultUser}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,8 +72,6 @@ export default async function Page(props: {
|
|||||||
searchParams.redirect = redirectUrl;
|
searchParams.redirect = redirectUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultUser = searchParams.user as string | undefined;
|
|
||||||
|
|
||||||
// Only use SmartLoginForm if NOT (OSS build OR org-only IdP enabled)
|
// Only use SmartLoginForm if NOT (OSS build OR org-only IdP enabled)
|
||||||
const useSmartLogin =
|
const useSmartLogin =
|
||||||
build === "saas" || (build === "enterprise" && env.flags.useOrgOnlyIdp);
|
build === "saas" || (build === "enterprise" && env.flags.useOrgOnlyIdp);
|
||||||
@@ -153,7 +151,6 @@ export default async function Page(props: {
|
|||||||
<SmartLoginForm
|
<SmartLoginForm
|
||||||
redirect={redirectUrl}
|
redirect={redirectUrl}
|
||||||
forceLogin={forceLogin}
|
forceLogin={forceLogin}
|
||||||
defaultUser={defaultUser}
|
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -168,7 +165,6 @@ export default async function Page(props: {
|
|||||||
(build === "saas" || env.flags.useOrgOnlyIdp)
|
(build === "saas" || env.flags.useOrgOnlyIdp)
|
||||||
}
|
}
|
||||||
searchParams={searchParams}
|
searchParams={searchParams}
|
||||||
defaultUser={defaultUser}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -121,16 +121,24 @@ export const orgNavSections = (env?: Env): SidebarNavSection[] => [
|
|||||||
href: "/{orgId}/settings/access/roles",
|
href: "/{orgId}/settings/access/roles",
|
||||||
icon: <Users className="size-4 flex-none" />
|
icon: <Users className="size-4 flex-none" />
|
||||||
},
|
},
|
||||||
{
|
...(build === "saas" || env?.flags.useOrgOnlyIdp
|
||||||
title: "sidebarIdentityProviders",
|
? [
|
||||||
href: "/{orgId}/settings/idp",
|
{
|
||||||
icon: <Fingerprint className="size-4 flex-none" />
|
title: "sidebarIdentityProviders",
|
||||||
},
|
href: "/{orgId}/settings/idp",
|
||||||
{
|
icon: <Fingerprint className="size-4 flex-none" />
|
||||||
title: "sidebarApprovals",
|
}
|
||||||
href: "/{orgId}/settings/access/approvals",
|
]
|
||||||
icon: <UserCog className="size-4 flex-none" />
|
: []),
|
||||||
},
|
...(build !== "oss"
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
title: "sidebarApprovals",
|
||||||
|
href: "/{orgId}/settings/access/approvals",
|
||||||
|
icon: <UserCog className="size-4 flex-none" />
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []),
|
||||||
{
|
{
|
||||||
title: "sidebarShareableLinks",
|
title: "sidebarShareableLinks",
|
||||||
href: "/{orgId}/settings/share-links",
|
href: "/{orgId}/settings/share-links",
|
||||||
@@ -147,16 +155,20 @@ export const orgNavSections = (env?: Env): SidebarNavSection[] => [
|
|||||||
href: "/{orgId}/settings/logs/request",
|
href: "/{orgId}/settings/logs/request",
|
||||||
icon: <SquareMousePointer className="size-4 flex-none" />
|
icon: <SquareMousePointer className="size-4 flex-none" />
|
||||||
},
|
},
|
||||||
{
|
...(build != "oss"
|
||||||
title: "sidebarLogsAccess",
|
? [
|
||||||
href: "/{orgId}/settings/logs/access",
|
{
|
||||||
icon: <ScanEye className="size-4 flex-none" />
|
title: "sidebarLogsAccess",
|
||||||
},
|
href: "/{orgId}/settings/logs/access",
|
||||||
{
|
icon: <ScanEye className="size-4 flex-none" />
|
||||||
title: "sidebarLogsAction",
|
},
|
||||||
href: "/{orgId}/settings/logs/action",
|
{
|
||||||
icon: <Logs className="size-4 flex-none" />
|
title: "sidebarLogsAction",
|
||||||
}
|
href: "/{orgId}/settings/logs/action",
|
||||||
|
icon: <Logs className="size-4 flex-none" />
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: [])
|
||||||
];
|
];
|
||||||
|
|
||||||
const analytics = {
|
const analytics = {
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ import {
|
|||||||
import { Separator } from "./ui/separator";
|
import { Separator } from "./ui/separator";
|
||||||
import { InfoPopup } from "./ui/info-popup";
|
import { InfoPopup } from "./ui/info-popup";
|
||||||
import { ApprovalsEmptyState } from "./ApprovalsEmptyState";
|
import { ApprovalsEmptyState } from "./ApprovalsEmptyState";
|
||||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
|
||||||
|
|
||||||
export type ApprovalFeedProps = {
|
export type ApprovalFeedProps = {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
@@ -51,12 +50,9 @@ export function ApprovalFeed({
|
|||||||
Object.fromEntries(searchParams.entries())
|
Object.fromEntries(searchParams.entries())
|
||||||
);
|
);
|
||||||
|
|
||||||
const { isPaidUser } = usePaidStatus();
|
const { data, isFetching, refetch } = useQuery(
|
||||||
|
approvalQueries.listApprovals(orgId, filters)
|
||||||
const { data, isFetching, refetch } = useQuery({
|
);
|
||||||
...approvalQueries.listApprovals(orgId, filters),
|
|
||||||
enabled: isPaidUser
|
|
||||||
});
|
|
||||||
|
|
||||||
const approvals = data?.approvals ?? [];
|
const approvals = data?.approvals ?? [];
|
||||||
|
|
||||||
@@ -213,19 +209,19 @@ function ApprovalRequest({ approval, orgId, onSuccess }: ApprovalRequestProps) {
|
|||||||
|
|
||||||
{approval.type === "user_device" && (
|
{approval.type === "user_device" && (
|
||||||
<span className="inline-flex items-center gap-1">
|
<span className="inline-flex items-center gap-1">
|
||||||
{approval.deviceName ? (
|
{approval.deviceName ? (
|
||||||
<>
|
<>
|
||||||
{t("requestingNewDeviceApproval")}:{" "}
|
{t("requestingNewDeviceApproval")}:{" "}
|
||||||
{approval.niceId ? (
|
{approval.niceId ? (
|
||||||
<Link
|
<Link
|
||||||
href={`/${orgId}/settings/clients/user/${approval.niceId}/general`}
|
href={`/${orgId}/settings/clients/user/${approval.niceId}/general`}
|
||||||
className="text-primary hover:underline cursor-pointer"
|
className="text-primary hover:underline cursor-pointer"
|
||||||
>
|
>
|
||||||
{approval.deviceName}
|
{approval.deviceName}
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<span>{approval.deviceName}</span>
|
<span>{approval.deviceName}</span>
|
||||||
)}
|
)}
|
||||||
{approval.fingerprint && (
|
{approval.fingerprint && (
|
||||||
<InfoPopup>
|
<InfoPopup>
|
||||||
<div className="space-y-1 text-sm">
|
<div className="space-y-1 text-sm">
|
||||||
@@ -233,10 +229,7 @@ function ApprovalRequest({ approval, orgId, onSuccess }: ApprovalRequestProps) {
|
|||||||
{t("deviceInformation")}
|
{t("deviceInformation")}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-muted-foreground whitespace-pre-line">
|
<div className="text-muted-foreground whitespace-pre-line">
|
||||||
{formatFingerprintInfo(
|
{formatFingerprintInfo(approval.fingerprint, t)}
|
||||||
approval.fingerprint,
|
|
||||||
t
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</InfoPopup>
|
</InfoPopup>
|
||||||
|
|||||||
@@ -160,51 +160,56 @@ export default function CreateRoleForm({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
{build !== "oss" && (
|
||||||
|
<div>
|
||||||
|
<PaidFeaturesAlert />
|
||||||
|
|
||||||
<PaidFeaturesAlert />
|
<FormField
|
||||||
|
control={form.control}
|
||||||
<FormField
|
name="requireDeviceApproval"
|
||||||
control={form.control}
|
render={({ field }) => (
|
||||||
name="requireDeviceApproval"
|
<FormItem className="my-2">
|
||||||
render={({ field }) => (
|
<FormControl>
|
||||||
<FormItem className="my-2">
|
<CheckboxWithLabel
|
||||||
<FormControl>
|
{...field}
|
||||||
<CheckboxWithLabel
|
disabled={
|
||||||
{...field}
|
!isPaidUser
|
||||||
disabled={!isPaidUser}
|
}
|
||||||
value="on"
|
value="on"
|
||||||
checked={form.watch(
|
checked={form.watch(
|
||||||
"requireDeviceApproval"
|
"requireDeviceApproval"
|
||||||
)}
|
)}
|
||||||
onCheckedChange={(
|
onCheckedChange={(
|
||||||
checked
|
|
||||||
) => {
|
|
||||||
if (
|
|
||||||
checked !==
|
|
||||||
"indeterminate"
|
|
||||||
) {
|
|
||||||
form.setValue(
|
|
||||||
"requireDeviceApproval",
|
|
||||||
checked
|
checked
|
||||||
);
|
) => {
|
||||||
}
|
if (
|
||||||
}}
|
checked !==
|
||||||
label={t(
|
"indeterminate"
|
||||||
"requireDeviceApproval"
|
) {
|
||||||
)}
|
form.setValue(
|
||||||
/>
|
"requireDeviceApproval",
|
||||||
</FormControl>
|
checked
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
label={t(
|
||||||
|
"requireDeviceApproval"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
{t(
|
{t(
|
||||||
"requireDeviceApprovalDescription"
|
"requireDeviceApprovalDescription"
|
||||||
)}
|
)}
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</CredenzaBody>
|
</CredenzaBody>
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ type DashboardLoginFormProps = {
|
|||||||
searchParams?: {
|
searchParams?: {
|
||||||
[key: string]: string | string[] | undefined;
|
[key: string]: string | string[] | undefined;
|
||||||
};
|
};
|
||||||
defaultUser?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function DashboardLoginForm({
|
export default function DashboardLoginForm({
|
||||||
@@ -37,8 +36,7 @@ export default function DashboardLoginForm({
|
|||||||
idps,
|
idps,
|
||||||
forceLogin,
|
forceLogin,
|
||||||
showOrgLogin,
|
showOrgLogin,
|
||||||
searchParams,
|
searchParams
|
||||||
defaultUser
|
|
||||||
}: DashboardLoginFormProps) {
|
}: DashboardLoginFormProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { env } = useEnvContext();
|
const { env } = useEnvContext();
|
||||||
@@ -77,7 +75,6 @@ export default function DashboardLoginForm({
|
|||||||
redirect={redirect}
|
redirect={redirect}
|
||||||
idps={idps}
|
idps={idps}
|
||||||
forceLogin={forceLogin}
|
forceLogin={forceLogin}
|
||||||
defaultEmail={defaultUser}
|
|
||||||
onLogin={(redirectUrl) => {
|
onLogin={(redirectUrl) => {
|
||||||
if (redirectUrl) {
|
if (redirectUrl) {
|
||||||
const safe = cleanRedirect(redirectUrl);
|
const safe = cleanRedirect(redirectUrl);
|
||||||
|
|||||||
@@ -55,14 +55,12 @@ type DeviceLoginFormProps = {
|
|||||||
userEmail: string;
|
userEmail: string;
|
||||||
userName?: string;
|
userName?: string;
|
||||||
initialCode?: string;
|
initialCode?: string;
|
||||||
userQueryParam?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function DeviceLoginForm({
|
export default function DeviceLoginForm({
|
||||||
userEmail,
|
userEmail,
|
||||||
userName,
|
userName,
|
||||||
initialCode = "",
|
initialCode = ""
|
||||||
userQueryParam
|
|
||||||
}: DeviceLoginFormProps) {
|
}: DeviceLoginFormProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { env } = useEnvContext();
|
const { env } = useEnvContext();
|
||||||
@@ -221,12 +219,9 @@ export default function DeviceLoginForm({
|
|||||||
const currentSearch =
|
const currentSearch =
|
||||||
typeof window !== "undefined" ? window.location.search : "";
|
typeof window !== "undefined" ? window.location.search : "";
|
||||||
const redirectTarget = `/auth/login/device${currentSearch || ""}`;
|
const redirectTarget = `/auth/login/device${currentSearch || ""}`;
|
||||||
const loginUrl = new URL("/auth/login", "http://x");
|
router.push(
|
||||||
loginUrl.searchParams.set("forceLogin", "true");
|
`/auth/login?forceLogin=true&redirect=${encodeURIComponent(redirectTarget)}`
|
||||||
loginUrl.searchParams.set("redirect", redirectTarget);
|
);
|
||||||
if (userQueryParam)
|
|
||||||
loginUrl.searchParams.set("user", userQueryParam);
|
|
||||||
router.push(loginUrl.pathname + loginUrl.search);
|
|
||||||
router.refresh();
|
router.refresh();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -168,50 +168,56 @@ export default function EditRoleForm({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<PaidFeaturesAlert />
|
{build !== "oss" && (
|
||||||
|
<div>
|
||||||
|
<PaidFeaturesAlert />
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="requireDeviceApproval"
|
name="requireDeviceApproval"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="my-2">
|
<FormItem className="my-2">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<CheckboxWithLabel
|
<CheckboxWithLabel
|
||||||
{...field}
|
{...field}
|
||||||
disabled={!isPaidUser}
|
disabled={
|
||||||
value="on"
|
!isPaidUser
|
||||||
checked={form.watch(
|
}
|
||||||
"requireDeviceApproval"
|
value="on"
|
||||||
)}
|
checked={form.watch(
|
||||||
onCheckedChange={(
|
"requireDeviceApproval"
|
||||||
checked
|
)}
|
||||||
) => {
|
onCheckedChange={(
|
||||||
if (
|
|
||||||
checked !==
|
|
||||||
"indeterminate"
|
|
||||||
) {
|
|
||||||
form.setValue(
|
|
||||||
"requireDeviceApproval",
|
|
||||||
checked
|
checked
|
||||||
);
|
) => {
|
||||||
}
|
if (
|
||||||
}}
|
checked !==
|
||||||
label={t(
|
"indeterminate"
|
||||||
"requireDeviceApproval"
|
) {
|
||||||
)}
|
form.setValue(
|
||||||
/>
|
"requireDeviceApproval",
|
||||||
</FormControl>
|
checked
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
label={t(
|
||||||
|
"requireDeviceApproval"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
{t(
|
{t(
|
||||||
"requireDeviceApprovalDescription"
|
"requireDeviceApprovalDescription"
|
||||||
)}
|
)}
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</CredenzaBody>
|
</CredenzaBody>
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ type LoginFormProps = {
|
|||||||
idps?: LoginFormIDP[];
|
idps?: LoginFormIDP[];
|
||||||
orgId?: string;
|
orgId?: string;
|
||||||
forceLogin?: boolean;
|
forceLogin?: boolean;
|
||||||
defaultEmail?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function LoginForm({
|
export default function LoginForm({
|
||||||
@@ -62,8 +61,7 @@ export default function LoginForm({
|
|||||||
onLogin,
|
onLogin,
|
||||||
idps,
|
idps,
|
||||||
orgId,
|
orgId,
|
||||||
forceLogin,
|
forceLogin
|
||||||
defaultEmail
|
|
||||||
}: LoginFormProps) {
|
}: LoginFormProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@@ -118,7 +116,7 @@ export default function LoginForm({
|
|||||||
const form = useForm({
|
const form = useForm({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
email: defaultEmail ?? "",
|
email: "",
|
||||||
password: ""
|
password: ""
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,16 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { Card, CardContent } from "@app/components/ui/card";
|
import { Alert, AlertDescription } from "@app/components/ui/alert";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
|
||||||
import { ExternalLink, KeyRound, Sparkles } from "lucide-react";
|
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import Link from "next/link";
|
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||||
|
|
||||||
const bannerClassName =
|
|
||||||
"mb-6 border-primary/30 bg-linear-to-br from-primary/10 via-background to-background overflow-hidden";
|
|
||||||
const bannerContentClassName = "py-3 px-4";
|
|
||||||
const bannerRowClassName =
|
|
||||||
"flex items-center gap-2.5 text-sm text-muted-foreground";
|
|
||||||
|
|
||||||
export function PaidFeaturesAlert() {
|
export function PaidFeaturesAlert() {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
@@ -18,50 +10,19 @@ export function PaidFeaturesAlert() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{build === "saas" && !hasSaasSubscription ? (
|
{build === "saas" && !hasSaasSubscription ? (
|
||||||
<Card className={bannerClassName}>
|
<Alert variant="info" className="mb-6">
|
||||||
<CardContent className={bannerContentClassName}>
|
<AlertDescription>
|
||||||
<div className={bannerRowClassName}>
|
{t("subscriptionRequiredToUse")}
|
||||||
<KeyRound className="size-4 shrink-0 text-primary" />
|
</AlertDescription>
|
||||||
<span>{t("subscriptionRequiredToUse")}</span>
|
</Alert>
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{build === "enterprise" && !hasEnterpriseLicense ? (
|
{build === "enterprise" && !hasEnterpriseLicense ? (
|
||||||
<Card className={bannerClassName}>
|
<Alert variant="info" className="mb-6">
|
||||||
<CardContent className={bannerContentClassName}>
|
<AlertDescription>
|
||||||
<div className={bannerRowClassName}>
|
{t("licenseRequiredToUse")}
|
||||||
<KeyRound className="size-4 shrink-0 text-primary" />
|
</AlertDescription>
|
||||||
<span>{t("licenseRequiredToUse")}</span>
|
</Alert>
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{build === "oss" && !hasEnterpriseLicense ? (
|
|
||||||
<Card className={bannerClassName}>
|
|
||||||
<CardContent className={bannerContentClassName}>
|
|
||||||
<div className={bannerRowClassName}>
|
|
||||||
<KeyRound className="size-4 shrink-0 text-primary" />
|
|
||||||
<span>
|
|
||||||
{t.rich("ossEnterpriseEditionRequired", {
|
|
||||||
enterpriseEditionLink: (chunks) => (
|
|
||||||
<Link
|
|
||||||
href="https://docs.pangolin.net/self-host/enterprise-edition"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="inline-flex items-center gap-1 font-medium text-foreground underline"
|
|
||||||
>
|
|
||||||
{chunks}
|
|
||||||
<ExternalLink className="size-3.5 shrink-0" />
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import * as z from "zod";
|
import * as z from "zod";
|
||||||
@@ -42,7 +42,6 @@ const isValidEmail = (str: string): boolean => {
|
|||||||
type SmartLoginFormProps = {
|
type SmartLoginFormProps = {
|
||||||
redirect?: string;
|
redirect?: string;
|
||||||
forceLogin?: boolean;
|
forceLogin?: boolean;
|
||||||
defaultUser?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type ViewState =
|
type ViewState =
|
||||||
@@ -60,8 +59,7 @@ type ViewState =
|
|||||||
|
|
||||||
export default function SmartLoginForm({
|
export default function SmartLoginForm({
|
||||||
redirect,
|
redirect,
|
||||||
forceLogin,
|
forceLogin
|
||||||
defaultUser
|
|
||||||
}: SmartLoginFormProps) {
|
}: SmartLoginFormProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { lookup, loading, error } = useUserLookup();
|
const { lookup, loading, error } = useUserLookup();
|
||||||
@@ -74,18 +72,10 @@ export default function SmartLoginForm({
|
|||||||
const form = useForm<z.infer<typeof identifierSchema>>({
|
const form = useForm<z.infer<typeof identifierSchema>>({
|
||||||
resolver: zodResolver(identifierSchema),
|
resolver: zodResolver(identifierSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
identifier: defaultUser ?? ""
|
identifier: ""
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasAutoLookedUp = useRef(false);
|
|
||||||
useEffect(() => {
|
|
||||||
if (defaultUser?.trim() && !hasAutoLookedUp.current) {
|
|
||||||
hasAutoLookedUp.current = true;
|
|
||||||
void handleLookup({ identifier: defaultUser.trim() });
|
|
||||||
}
|
|
||||||
}, [defaultUser]);
|
|
||||||
|
|
||||||
const handleLookup = async (values: z.infer<typeof identifierSchema>) => {
|
const handleLookup = async (values: z.infer<typeof identifierSchema>) => {
|
||||||
const identifier = values.identifier.trim();
|
const identifier = values.identifier.trim();
|
||||||
const isEmail = isValidEmail(identifier);
|
const isEmail = isValidEmail(identifier);
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
|||||||
const approvalsRes = await api.get<{
|
const approvalsRes = await api.get<{
|
||||||
data: { approvals: Array<{ approvalId: number; clientId: number }> };
|
data: { approvals: Array<{ approvalId: number; clientId: number }> };
|
||||||
}>(`/org/${clientRow.orgId}/approvals?approvalState=pending&clientId=${clientRow.id}`);
|
}>(`/org/${clientRow.orgId}/approvals?approvalState=pending&clientId=${clientRow.id}`);
|
||||||
|
|
||||||
const approval = approvalsRes.data.data.approvals[0];
|
const approval = approvalsRes.data.data.approvals[0];
|
||||||
|
|
||||||
if (!approval) {
|
if (!approval) {
|
||||||
@@ -232,7 +232,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
|||||||
const approvalsRes = await api.get<{
|
const approvalsRes = await api.get<{
|
||||||
data: { approvals: Array<{ approvalId: number; clientId: number }> };
|
data: { approvals: Array<{ approvalId: number; clientId: number }> };
|
||||||
}>(`/org/${clientRow.orgId}/approvals?approvalState=pending&clientId=${clientRow.id}`);
|
}>(`/org/${clientRow.orgId}/approvals?approvalState=pending&clientId=${clientRow.id}`);
|
||||||
|
|
||||||
const approval = approvalsRes.data.data.approvals[0];
|
const approval = approvalsRes.data.data.approvals[0];
|
||||||
|
|
||||||
if (!approval) {
|
if (!approval) {
|
||||||
@@ -548,7 +548,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
{clientRow.approvalState === "pending" && (
|
{clientRow.approvalState === "pending" && build !== "oss" && (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => approveDevice(clientRow)}
|
onClick={() => approveDevice(clientRow)}
|
||||||
@@ -652,10 +652,17 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (build === "oss") {
|
||||||
|
return allOptions.filter((option) => option.value !== "pending" && option.value !== "denied");
|
||||||
|
}
|
||||||
|
|
||||||
return allOptions;
|
return allOptions;
|
||||||
}, [t]);
|
}, [t]);
|
||||||
|
|
||||||
const statusFilterDefaultValues = useMemo(() => {
|
const statusFilterDefaultValues = useMemo(() => {
|
||||||
|
if (build === "oss") {
|
||||||
|
return ["active"];
|
||||||
|
}
|
||||||
return ["active", "pending"];
|
return ["active", "pending"];
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -43,11 +43,11 @@ export function OlmInstallCommands({
|
|||||||
All: [
|
All: [
|
||||||
{
|
{
|
||||||
title: t("install"),
|
title: t("install"),
|
||||||
command: `curl -fsSL https://static.pangolin.net/get-cli.sh | bash`
|
command: `curl -fsSL https://static.pangolin.net/get-olm.sh | bash`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("run"),
|
title: t("run"),
|
||||||
command: `sudo pangolin up --id ${id} --secret ${secret} --endpoint ${endpoint} --attach`
|
command: `sudo olm --id ${id} --secret ${secret} --endpoint ${endpoint}`
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,6 +2,29 @@ import { NextRequest, NextResponse } from "next/server";
|
|||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
|
|
||||||
export function middleware(request: NextRequest) {
|
export function middleware(request: NextRequest) {
|
||||||
|
// If build is OSS, block access to private routes
|
||||||
|
if (build === "oss") {
|
||||||
|
const pathname = request.nextUrl.pathname;
|
||||||
|
|
||||||
|
// Define private route patterns that should be blocked in OSS build
|
||||||
|
const privateRoutes = [
|
||||||
|
"/settings/billing",
|
||||||
|
"/settings/remote-exit-nodes",
|
||||||
|
"/settings/idp",
|
||||||
|
"/auth/org"
|
||||||
|
];
|
||||||
|
|
||||||
|
// Check if current path matches any private route pattern
|
||||||
|
const isPrivateRoute = privateRoutes.some((route) =>
|
||||||
|
pathname.includes(route)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isPrivateRoute) {
|
||||||
|
// Return 404 to make it seem like the route doesn't exist
|
||||||
|
return new NextResponse(null, { status: 404 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.next();
|
return NextResponse.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user