From 4b40e7b8d61b26300acc089cdd83c9d4fd9680ca Mon Sep 17 00:00:00 2001 From: Owen Date: Fri, 24 Oct 2025 16:29:37 -0700 Subject: [PATCH] Restrict features --- messages/en-US.json | 21 +- server/db/pg/schema/schema.ts | 6 +- server/db/sqlite/schema/schema.ts | 6 +- server/index.ts | 9 +- server/lib/cleanupLogs.ts | 62 ++++ server/private/lib/logAccessAudit.ts | 38 ++- server/private/middlewares/logActionAudit.ts | 30 +- server/routers/badger/logRequestAudit.ts | 38 ++- server/routers/org/getOrg.ts | 6 +- server/routers/org/updateOrg.ts | 7 +- src/app/[orgId]/settings/general/page.tsx | 314 ++++++++++++++++-- src/app/[orgId]/settings/logs/access/page.tsx | 48 ++- src/app/[orgId]/settings/logs/action/page.tsx | 47 ++- src/app/navigation.tsx | 24 +- src/components/DataTablePagination.tsx | 15 +- src/components/LogDataTable.tsx | 36 +- 16 files changed, 622 insertions(+), 85 deletions(-) create mode 100644 server/lib/cleanupLogs.ts diff --git a/messages/en-US.json b/messages/en-US.json index 0a62ad7e..80875b96 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1925,5 +1925,22 @@ "sidebarLogsRequest": "Request Logs", "sidebarLogsAccess": "Access Logs", "sidebarLogsAction": "Action Logs", - "requestLogsDescription": "View detailed pre-request logs for resources in this organization" -} + "logRetention": "Log Retention", + "logRetentionDescription": "Manage how long different types of logs are retained for this organization or disable them", + "requestLogsDescription": "View detailed request logs for resources in this organization", + "logRetentionRequestLabel": "Request Log Retention", + "logRetentionRequestDescription": "How long to retain request logs", + "logRetentionAccessLabel": "Access Log Retention", + "logRetentionAccessDescription": "How long to retain access logs", + "logRetentionActionLabel": "Action Log Retention", + "logRetentionActionDescription": "How long to retain action logs", + "logRetentionDisabled": "Disabled", + "logRetention3Days": "3 days", + "logRetention7Days": "7 days", + "logRetention14Days": "14 days", + "logRetention30Days": "30 days", + "logRetentionForever": "Forever", + "actionLogsDescription": "View a history of actions performed in this organization", + "accessLogsDescription": "View access auth requests for resources in this organization", + "licenseRequiredToUse": "An Enterprise license is required to use this feature." +} \ No newline at end of file diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index 601290e0..36e31804 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -29,13 +29,13 @@ export const orgs = pgTable("orgs", { createdAt: text("createdAt"), settingsLogRetentionDaysRequest: integer("settingsLogRetentionDaysRequest") // where 0 = dont keep logs and -1 = keep forever .notNull() - .default(15), + .default(7), settingsLogRetentionDaysAccess: integer("settingsLogRetentionDaysAccess") .notNull() - .default(15), + .default(0), settingsLogRetentionDaysAction: integer("settingsLogRetentionDaysAction") .notNull() - .default(15), + .default(0) }); export const orgDomains = pgTable("orgDomains", { diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index ede2e754..2d3a142c 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -22,13 +22,13 @@ export const orgs = sqliteTable("orgs", { createdAt: text("createdAt"), settingsLogRetentionDaysRequest: integer("settingsLogRetentionDaysRequest") // where 0 = dont keep logs and -1 = keep forever .notNull() - .default(15), + .default(7), settingsLogRetentionDaysAccess: integer("settingsLogRetentionDaysAccess") .notNull() - .default(15), + .default(0), settingsLogRetentionDaysAction: integer("settingsLogRetentionDaysAction") .notNull() - .default(15) + .default(0) }); export const userDomains = sqliteTable("userDomains", { diff --git a/server/index.ts b/server/index.ts index 8b4b3728..b84728ef 100644 --- a/server/index.ts +++ b/server/index.ts @@ -5,6 +5,7 @@ import { runSetupFunctions } from "./setup"; import { createApiServer } from "./apiServer"; import { createNextServer } from "./nextServer"; import { createInternalServer } from "./internalServer"; +import { createIntegrationApiServer } from "./integrationApiServer"; import { ApiKey, ApiKeyOrg, @@ -13,13 +14,13 @@ import { User, UserOrg } from "@server/db"; -import { createIntegrationApiServer } from "./integrationApiServer"; import config from "@server/lib/config"; import { setHostMeta } from "@server/lib/hostMeta"; -import { initTelemetryClient } from "./lib/telemetry.js"; -import { TraefikConfigManager } from "./lib/traefik/TraefikConfigManager.js"; +import { initTelemetryClient } from "@server/lib/telemetry"; +import { TraefikConfigManager } from "@server/lib/traefik/TraefikConfigManager"; import { initCleanup } from "#dynamic/cleanup"; import license from "#dynamic/license/license"; +import { initLogCleanupInterval } from "@server/lib/cleanupLogs"; async function startServers() { await setHostMeta(); @@ -33,6 +34,8 @@ async function startServers() { initTelemetryClient(); + initLogCleanupInterval(); + // Start all servers const apiServer = createApiServer(); const internalServer = createInternalServer(); diff --git a/server/lib/cleanupLogs.ts b/server/lib/cleanupLogs.ts new file mode 100644 index 00000000..7cc96ff9 --- /dev/null +++ b/server/lib/cleanupLogs.ts @@ -0,0 +1,62 @@ +import { db, orgs } from "@server/db"; +import { cleanUpOldLogs as cleanUpOldAccessLogs } from "@server/private/lib/logAccessAudit"; +import { cleanUpOldLogs as cleanUpOldActionLogs } from "@server/private/middlewares/logActionAudit"; +import { cleanUpOldLogs as cleanUpOldRequestLogs } from "@server/routers/badger/logRequestAudit"; +import { gt, or } from "drizzle-orm"; + +export function initLogCleanupInterval() { + return setInterval( + async () => { + const orgsToClean = await db + .select({ + orgId: orgs.orgId, + settingsLogRetentionDaysAction: + orgs.settingsLogRetentionDaysAction, + settingsLogRetentionDaysAccess: + orgs.settingsLogRetentionDaysAccess, + settingsLogRetentionDaysRequest: + orgs.settingsLogRetentionDaysRequest + }) + .from(orgs) + .where( + or( + gt(orgs.settingsLogRetentionDaysAction, 0), + gt(orgs.settingsLogRetentionDaysAccess, 0), + gt(orgs.settingsLogRetentionDaysRequest, 0) + ) + ); + + for (const org of orgsToClean) { + const { + orgId, + settingsLogRetentionDaysAction, + settingsLogRetentionDaysAccess, + settingsLogRetentionDaysRequest + } = org; + + if (settingsLogRetentionDaysAction > 0) { + await cleanUpOldActionLogs( + orgId, + settingsLogRetentionDaysRequest + ); + } + + if (settingsLogRetentionDaysAccess > 0) { + await cleanUpOldAccessLogs( + orgId, + settingsLogRetentionDaysRequest + ); + } + + if (settingsLogRetentionDaysRequest > 0) { + await cleanUpOldRequestLogs( + orgId, + settingsLogRetentionDaysRequest + ); + } + } + }, + // 3 * 60 * 60 * 1000 + 60 * 1000 // for testing + ); // every 3 hours +} diff --git a/server/private/lib/logAccessAudit.ts b/server/private/lib/logAccessAudit.ts index 97aef0ee..cc56d94c 100644 --- a/server/private/lib/logAccessAudit.ts +++ b/server/private/lib/logAccessAudit.ts @@ -1,7 +1,8 @@ import { accessAuditLog, db, orgs } from "@server/db"; import { getCountryCodeForIp } from "@server/lib/geoip"; import logger from "@server/logger"; -import { eq } from "drizzle-orm"; +import { and, eq, lt } from "drizzle-orm"; +import cache from "@server/lib/cache"; async function getAccessDays(orgId: string): Promise { // check cache first @@ -23,11 +24,38 @@ async function getAccessDays(orgId: string): Promise { } // store the result in cache - cache.set(`org_${orgId}_accessDays`, org.settingsLogRetentionDaysAction); + cache.set( + `org_${orgId}_accessDays`, + org.settingsLogRetentionDaysAction, + 300 + ); return org.settingsLogRetentionDaysAction; } +export async function cleanUpOldLogs(orgId: string, retentionDays: number) { + const now = Math.floor(Date.now() / 1000); + + const cutoffTimestamp = now - retentionDays * 24 * 60 * 60; + + try { + const deleteResult = await db + .delete(accessAuditLog) + .where( + and( + lt(accessAuditLog.timestamp, cutoffTimestamp), + eq(accessAuditLog.orgId, orgId) + ) + ); + + logger.info( + `Cleaned up ${deleteResult.changes} access audit logs older than ${retentionDays} days` + ); + } catch (error) { + logger.error("Error cleaning up old action audit logs:", error); + } +} + export async function logAccessAudit(data: { action: boolean; type: string; @@ -40,6 +68,12 @@ export async function logAccessAudit(data: { requestIp?: string; }) { try { + const retentionDays = await getAccessDays(data.orgId); + if (retentionDays === 0) { + // do not log + return; + } + let actorType: string | undefined; let actor: string | undefined; let actorId: string | undefined; diff --git a/server/private/middlewares/logActionAudit.ts b/server/private/middlewares/logActionAudit.ts index af7759fc..3dd2f084 100644 --- a/server/private/middlewares/logActionAudit.ts +++ b/server/private/middlewares/logActionAudit.ts @@ -17,10 +17,9 @@ import logger from "@server/logger"; import HttpCode from "@server/types/HttpCode"; import { Request, Response, NextFunction } from "express"; import createHttpError from "http-errors"; -import NodeCache from "node-cache"; -import { eq } from "drizzle-orm"; +import { and, eq, lt } from "drizzle-orm"; +import cache from "@server/lib/cache"; -const cache = new NodeCache({ stdTTL: 300 }); // cache for 5 minutes async function getActionDays(orgId: string): Promise { // check cache first const cached = cache.get(`org_${orgId}_actionDays`); @@ -41,11 +40,34 @@ async function getActionDays(orgId: string): Promise { } // store the result in cache - cache.set(`org_${orgId}_actionDays`, org.settingsLogRetentionDaysAction); + cache.set(`org_${orgId}_actionDays`, org.settingsLogRetentionDaysAction, 300); return org.settingsLogRetentionDaysAction; } +export async function cleanUpOldLogs(orgId: string, retentionDays: number) { + const now = Math.floor(Date.now() / 1000); + + const cutoffTimestamp = now - retentionDays * 24 * 60 * 60; + + try { + const deleteResult = await db + .delete(actionAuditLog) + .where( + and( + lt(actionAuditLog.timestamp, cutoffTimestamp), + eq(actionAuditLog.orgId, orgId) + ) + ); + + logger.info( + `Cleaned up ${deleteResult.changes} action audit logs older than ${retentionDays} days` + ); + } catch (error) { + logger.error("Error cleaning up old action audit logs:", error); + } +} + export function logActionAudit(action: ActionsEnum) { return async function ( req: Request, diff --git a/server/routers/badger/logRequestAudit.ts b/server/routers/badger/logRequestAudit.ts index 77b97d96..b0754aed 100644 --- a/server/routers/badger/logRequestAudit.ts +++ b/server/routers/badger/logRequestAudit.ts @@ -1,7 +1,7 @@ import { db, orgs, requestAuditLog } from "@server/db"; import logger from "@server/logger"; -import { eq } from "drizzle-orm"; -import NodeCache from "node-cache"; +import { and, eq, lt } from "drizzle-orm"; +import cache from "@server/lib/cache"; /** @@ -24,7 +24,6 @@ Reasons: */ -const cache = new NodeCache({ stdTTL: 300 }); // cache for 5 minutes async function getRetentionDays(orgId: string): Promise { // check cache first const cached = cache.get(`org_${orgId}_retentionDays`); @@ -34,7 +33,8 @@ async function getRetentionDays(orgId: string): Promise { const [org] = await db .select({ - settingsLogRetentionDaysRequest: orgs.settingsLogRetentionDaysRequest + settingsLogRetentionDaysRequest: + orgs.settingsLogRetentionDaysRequest }) .from(orgs) .where(eq(orgs.orgId, orgId)) @@ -45,11 +45,38 @@ async function getRetentionDays(orgId: string): Promise { } // store the result in cache - cache.set(`org_${orgId}_retentionDays`, org.settingsLogRetentionDaysRequest); + cache.set( + `org_${orgId}_retentionDays`, + org.settingsLogRetentionDaysRequest, + 300 + ); return org.settingsLogRetentionDaysRequest; } +export async function cleanUpOldLogs(orgId: string, retentionDays: number) { + const now = Math.floor(Date.now() / 1000); + + const cutoffTimestamp = now - retentionDays * 24 * 60 * 60; + + try { + const deleteResult = await db + .delete(requestAuditLog) + .where( + and( + lt(requestAuditLog.timestamp, cutoffTimestamp), + eq(requestAuditLog.orgId, orgId) + ) + ); + + logger.info( + `Cleaned up ${deleteResult.changes} request audit logs older than ${retentionDays} days` + ); + } catch (error) { + logger.error("Error cleaning up old request audit logs:", error); + } +} + export async function logRequestAudit( data: { action: boolean; @@ -76,7 +103,6 @@ export async function logRequestAudit( } ) { try { - if (data.orgId) { const retentionDays = await getRetentionDays(data.orgId); if (retentionDays === 0) { diff --git a/server/routers/org/getOrg.ts b/server/routers/org/getOrg.ts index 35c1a5f7..2497f9a6 100644 --- a/server/routers/org/getOrg.ts +++ b/server/routers/org/getOrg.ts @@ -49,13 +49,13 @@ export async function getOrg( const { orgId } = parsedParams.data; - const org = await db + const [org] = await db .select() .from(orgs) .where(eq(orgs.orgId, orgId)) .limit(1); - if (org.length === 0) { + if (!org) { return next( createHttpError( HttpCode.NOT_FOUND, @@ -66,7 +66,7 @@ export async function getOrg( return response(res, { data: { - org: org[0] + org }, success: true, error: false, diff --git a/server/routers/org/updateOrg.ts b/server/routers/org/updateOrg.ts index 64075cab..a517ed17 100644 --- a/server/routers/org/updateOrg.ts +++ b/server/routers/org/updateOrg.ts @@ -18,7 +18,10 @@ const updateOrgParamsSchema = z const updateOrgBodySchema = z .object({ - name: z.string().min(1).max(255).optional() + name: z.string().min(1).max(255).optional(), + settingsLogRetentionDaysRequest: z.number().min(-1).optional(), + settingsLogRetentionDaysAccess: z.number().min(-1).optional(), + settingsLogRetentionDaysAction: z.number().min(-1).optional() }) .strict() .refine((data) => Object.keys(data).length > 0, { @@ -74,7 +77,7 @@ export async function updateOrg( const updatedOrg = await db .update(orgs) .set({ - name: parsedBody.data.name + ...parsedBody.data }) .where(eq(orgs.orgId, orgId)) .returning(); diff --git a/src/app/[orgId]/settings/general/page.tsx b/src/app/[orgId]/settings/general/page.tsx index b301fd32..fd5c00c2 100644 --- a/src/app/[orgId]/settings/general/page.tsx +++ b/src/app/[orgId]/settings/general/page.tsx @@ -42,15 +42,36 @@ import { import { useUserContext } from "@app/hooks/useUserContext"; import { useTranslations } from "next-intl"; import { build } from "@server/build"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger +} from "@app/components/ui/dropdown-menu"; +import { ChevronDown } from "lucide-react"; +import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; +import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext"; // Schema for general organization settings const GeneralFormSchema = z.object({ name: z.string(), - subnet: z.string().optional() + subnet: z.string().optional(), + settingsLogRetentionDaysRequest: z.number(), + settingsLogRetentionDaysAccess: z.number(), + settingsLogRetentionDaysAction: z.number() }); type GeneralFormValues = z.infer; +const LOG_RETENTION_OPTIONS = [ + { label: "logRetentionDisabled", value: 0 }, + { label: "logRetention3Days", value: 3 }, + { label: "logRetention7Days", value: 7 }, + { label: "logRetention14Days", value: 14 }, + { label: "logRetention30Days", value: 30 }, + { label: "logRetentionForever", value: -1 } +]; + export default function GeneralPage() { const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const { orgUser } = userOrgUserContext(); @@ -60,6 +81,8 @@ export default function GeneralPage() { const { user } = useUserContext(); const t = useTranslations(); const { env } = useEnvContext(); + const { isUnlocked } = useLicenseStatusContext(); + const subscription = useSubscriptionStatusContext(); const [loadingDelete, setLoadingDelete] = useState(false); const [loadingSave, setLoadingSave] = useState(false); @@ -69,7 +92,13 @@ export default function GeneralPage() { resolver: zodResolver(GeneralFormSchema), defaultValues: { name: org?.org.name, - subnet: org?.org.subnet || "" // Add default value for subnet + subnet: org?.org.subnet || "", // Add default value for subnet + settingsLogRetentionDaysRequest: + org.org.settingsLogRetentionDaysRequest ?? 15, + settingsLogRetentionDaysAccess: + org.org.settingsLogRetentionDaysAccess ?? 15, + settingsLogRetentionDaysAction: + org.org.settingsLogRetentionDaysAction ?? 15 }, mode: "onChange" }); @@ -131,8 +160,14 @@ export default function GeneralPage() { try { // Update organization await api.post(`/org/${org?.org.orgId}`, { - name: data.name + name: data.name, // subnet: data.subnet // Include subnet in the API request + settingsLogRetentionDaysRequest: + data.settingsLogRetentionDaysRequest, + settingsLogRetentionDaysAccess: + data.settingsLogRetentionDaysAccess, + settingsLogRetentionDaysAction: + data.settingsLogRetentionDaysAction }); // Also save auth page settings if they have unsaved changes @@ -159,6 +194,11 @@ export default function GeneralPage() { } } + const getLabelForValue = (value: number) => { + const option = LOG_RETENTION_OPTIONS.find((opt) => opt.value === value); + return option ? t(option.label) : `${value} days`; + }; + return ( -

- {t("orgQuestionRemove")} -

+

{t("orgQuestionRemove")}

{t("orgMessageRemove")}

} @@ -179,23 +217,24 @@ export default function GeneralPage() { string={org?.org.name || ""} title={t("orgDelete")} /> - - - - {t("orgGeneralSettings")} - - - {t("orgGeneralSettingsDescription")} - - - - -
- + + + + + + + {t("orgGeneralSettings")} + + + {t("orgGeneralSettingsDescription")} + + + + )} - - - - -
+
+
+
- {(build === "saas") && ( - - )} + + + + {t("logRetention")} + + + {t("logRetentionDescription")} + + + + {/* {build === "saas" && !subscription?.subscribed ? ( + + + {t("orgAuthPageDisabled")}{" "} + {t("subscriptionRequiredToUse")} + + + ) : null} */} + + + ( + + + {t("logRetentionRequestLabel")} + + + + + + + + {LOG_RETENTION_OPTIONS.map( + (option) => ( + + field.onChange( + option.value + ) + } + > + {t( + option.label + )} + + ) + )} + + + + + {t( + "logRetentionRequestDescription" + )} + + + + )} + /> + + {build != "oss" && ( + <> + ( + + + {t( + "logRetentionAccessLabel" + )} + + + + + + + + {LOG_RETENTION_OPTIONS.map( + ( + option + ) => ( + + field.onChange( + option.value + ) + } + > + {t( + option.label + )} + + ) + )} + + + + + {t( + "logRetentionAccessDescription" + )} + + + + )} + /> + ( + + + {t( + "logRetentionActionLabel" + )} + + + + + + + + {LOG_RETENTION_OPTIONS.map( + ( + option + ) => ( + + field.onChange( + option.value + ) + } + > + {t( + option.label + )} + + ) + )} + + + + + {t( + "logRetentionActionDescription" + )} + + + + )} + /> + + )} + + + + + + + {build === "saas" && } {/* Save Button */}
diff --git a/src/app/[orgId]/settings/logs/access/page.tsx b/src/app/[orgId]/settings/logs/access/page.tsx index 07e33824..4244222d 100644 --- a/src/app/[orgId]/settings/logs/access/page.tsx +++ b/src/app/[orgId]/settings/logs/access/page.tsx @@ -6,12 +6,21 @@ import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { useParams, useRouter, useSearchParams } from "next/navigation"; import { useTranslations } from "next-intl"; -import { getStoredPageSize, LogDataTable, setStoredPageSize } from "@app/components/LogDataTable"; +import { + getStoredPageSize, + LogDataTable, + setStoredPageSize +} from "@app/components/LogDataTable"; import { ColumnDef } from "@tanstack/react-table"; import { DateTimeValue } from "@app/components/DateTimePicker"; import { ArrowUpRight, Key, User } from "lucide-react"; import Link from "next/link"; import { ColumnFilter } from "@app/components/ColumnFilter"; +import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; +import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext"; +import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; +import { build } from "@server/build"; +import { Alert, AlertDescription } from "@app/components/ui/alert"; export default function GeneralPage() { const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); @@ -21,6 +30,8 @@ export default function GeneralPage() { const t = useTranslations(); const { env } = useEnvContext(); const { orgId } = useParams(); + const subscription = useSubscriptionStatusContext(); + const { isUnlocked } = useLicenseStatusContext(); const [rows, setRows] = useState([]); const [isRefreshing, setIsRefreshing] = useState(false); @@ -202,6 +213,16 @@ export default function GeneralPage() { } ) => { console.log("Date range changed:", { startDate, endDate, page, size }); + if ( + (build == "saas" && !subscription?.subscribed) || + (build == "enterprise" && !isUnlocked()) + ) { + console.log( + "Access denied: subscription inactive or license locked" + ); + return; + } + setIsLoading(true); try { @@ -583,6 +604,27 @@ export default function GeneralPage() { return ( <> + + + {build == "saas" && !subscription?.subscribed ? ( + + + {t("subscriptionRequiredToUse")} + + + ) : null} + + {build == "enterprise" && !isUnlocked() ? ( + + + {t("licenseRequiredToUse")} + + + ) : null} + ); diff --git a/src/app/[orgId]/settings/logs/action/page.tsx b/src/app/[orgId]/settings/logs/action/page.tsx index 14281646..d71cc3ff 100644 --- a/src/app/[orgId]/settings/logs/action/page.tsx +++ b/src/app/[orgId]/settings/logs/action/page.tsx @@ -6,11 +6,20 @@ import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { useParams, useRouter, useSearchParams } from "next/navigation"; import { useTranslations } from "next-intl"; -import { getStoredPageSize, LogDataTable, setStoredPageSize } from "@app/components/LogDataTable"; +import { + getStoredPageSize, + LogDataTable, + setStoredPageSize +} from "@app/components/LogDataTable"; import { ColumnDef } from "@tanstack/react-table"; import { DateTimeValue } from "@app/components/DateTimePicker"; import { Key, User } from "lucide-react"; import { ColumnFilter } from "@app/components/ColumnFilter"; +import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; +import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext"; +import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; +import { build } from "@server/build"; +import { Alert, AlertDescription } from "@app/components/ui/alert"; export default function GeneralPage() { const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); @@ -20,6 +29,8 @@ export default function GeneralPage() { const { env } = useEnvContext(); const { orgId } = useParams(); const searchParams = useSearchParams(); + const subscription = useSubscriptionStatusContext(); + const { isUnlocked } = useLicenseStatusContext(); const [rows, setRows] = useState([]); const [isRefreshing, setIsRefreshing] = useState(false); @@ -187,6 +198,15 @@ export default function GeneralPage() { } ) => { console.log("Date range changed:", { startDate, endDate, page, size }); + if ( + (build == "saas" && !subscription?.subscribed) || + (build == "enterprise" && !isUnlocked()) + ) { + console.log( + "Access denied: subscription inactive or license locked" + ); + return; + } setIsLoading(true); try { @@ -435,6 +455,27 @@ export default function GeneralPage() { return ( <> + + + {build == "saas" && !subscription?.subscribed ? ( + + + {t("subscriptionRequiredToUse")} + + + ) : null} + + {build == "enterprise" && !isUnlocked() ? ( + + + {t("licenseRequiredToUse")} + + + ) : null} + ); diff --git a/src/app/navigation.tsx b/src/app/navigation.tsx index ed95eba3..8776c788 100644 --- a/src/app/navigation.tsx +++ b/src/app/navigation.tsx @@ -123,16 +123,20 @@ export const orgNavSections = ( href: "/{orgId}/settings/logs/request", icon: }, - { - title: "sidebarLogsAccess", - href: "/{orgId}/settings/logs/access", - icon: - }, - { - title: "sidebarLogsAction", - href: "/{orgId}/settings/logs/action", - icon: - }, + ...(build != "oss" + ? [ + { + title: "sidebarLogsAccess", + href: "/{orgId}/settings/logs/access", + icon: + }, + { + title: "sidebarLogsAction", + href: "/{orgId}/settings/logs/action", + icon: + } + ] + : []) ] }, { diff --git a/src/components/DataTablePagination.tsx b/src/components/DataTablePagination.tsx index 5c7af9d4..70d64f0c 100644 --- a/src/components/DataTablePagination.tsx +++ b/src/components/DataTablePagination.tsx @@ -23,6 +23,7 @@ interface DataTablePaginationProps { totalCount?: number; isServerPagination?: boolean; isLoading?: boolean; + disabled?: boolean; } export function DataTablePagination({ @@ -31,7 +32,8 @@ export function DataTablePagination({ onPageChange, totalCount, isServerPagination = false, - isLoading = false + isLoading = false, + disabled = false }: DataTablePaginationProps) { const t = useTranslations(); @@ -96,8 +98,9 @@ export function DataTablePagination({