Compare commits

..

7 Commits

Author SHA1 Message Date
dependabot[bot]
af4c1e926b Bump aws-actions/configure-aws-credentials from 5 to 6
Bumps [aws-actions/configure-aws-credentials](https://github.com/aws-actions/configure-aws-credentials) from 5 to 6.
- [Release notes](https://github.com/aws-actions/configure-aws-credentials/releases)
- [Changelog](https://github.com/aws-actions/configure-aws-credentials/blob/main/CHANGELOG.md)
- [Commits](https://github.com/aws-actions/configure-aws-credentials/compare/v5...v6)

---
updated-dependencies:
- dependency-name: aws-actions/configure-aws-credentials
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-05 05:44:50 +00:00
Owen
b4c01349d1 Merge branch 'dev' 2026-02-04 21:44:07 -08:00
Owen
e4d4c62833 Dont create newt sites with exit node or subnet 2026-02-02 18:19:13 -08:00
Owen
20ae903d7f Subscribed limits for domains is higher 2026-02-02 16:46:48 -08:00
MoweME
b0566d3c6f fix(i18n): correct German site terminology
Updates the German translation to use "Standort" (site) instead of "Seite" (page) for consistency with the site context.
2026-01-29 10:01:30 -08:00
MoweME
5dda8c384f fix(i18n): correct German translation strings
Corrects mistranslation of device timestamp labels and fixes product name reference in site tunnel settings.
2026-01-29 10:01:30 -08:00
Owen
cb569ff14d Properly insert PANGOLIN_SETUP_TOKEN into db
Fixes #2361
2026-01-28 15:03:31 -08:00
38 changed files with 767 additions and 783 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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",

View File

@@ -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",

View File

@@ -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

View File

@@ -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,

View File

@@ -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,

View File

@@ -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: {

View File

@@ -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) ||

View File

@@ -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") {

View File

@@ -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

View File

@@ -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;
} }

View File

@@ -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>

View File

@@ -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

View File

@@ -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>
); );
} }

View File

@@ -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);

View File

@@ -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 (
<> <>

View File

@@ -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>

View File

@@ -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"
} }
/> />
</> </>

View File

@@ -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"
} }
/> />
</> </>

View File

@@ -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,

View File

@@ -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

View File

@@ -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>

View File

@@ -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}
/> />
); );
} }

View File

@@ -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}
/> />
)} )}

View File

@@ -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 = {

View File

@@ -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) {
&nbsp; &nbsp;
{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>

View File

@@ -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>

View File

@@ -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);

View File

@@ -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();
} }
} }

View File

@@ -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>

View File

@@ -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: ""
} }
}); });

View File

@@ -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}
</> </>
); );

View File

@@ -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);

View File

@@ -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"];
}, []); }, []);

View File

@@ -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}`
} }
] ]
}, },

View File

@@ -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();
} }