diff --git a/packages/db/prisma/migrations/20260509000000_add_license_table/migration.sql b/packages/db/prisma/migrations/20260509000000_add_license_table/migration.sql index af5d848e7..44074dd1d 100644 --- a/packages/db/prisma/migrations/20260509000000_add_license_table/migration.sql +++ b/packages/db/prisma/migrations/20260509000000_add_license_table/migration.sql @@ -1,6 +1,3 @@ --- AlterTable -ALTER TABLE "Org" ADD COLUMN "trialUsedAt" TIMESTAMP(3); - -- CreateTable CREATE TABLE "License" ( "id" TEXT NOT NULL, diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index 7e1af6be7..785f1df93 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -293,10 +293,6 @@ model Org { chats Chat[] license License? - - /// Set the first time this instance is seen to be on a trial subscription. - /// Never cleared. Used to gate the "Start trial" CTA in the UI. - trialUsedAt DateTime? } model License { diff --git a/packages/shared/src/entitlements.ts b/packages/shared/src/entitlements.ts index 8ba587b43..97d743990 100644 --- a/packages/shared/src/entitlements.ts +++ b/packages/shared/src/entitlements.ts @@ -88,12 +88,12 @@ const getValidOfflineLicense = (): getValidOfflineLicense | null => { return payload; } -// If the license hasn't successfully synced with Lighthouse for this long, -// the locally-cached state is no longer trusted. This guards against an -// operator blocking egress to prevent the license row from hearing about -// a canceled or past-due subscription. 7 days absorbs week-long transient -// outages (weekends, firewall rollouts) without punishing legitimate -// customers. +// If the license hasn't successfully synced with Lighthouse for this long, +// the locally-cached state is no longer trusted. This guards against an +// operator blocking egress to prevent the license row from hearing about +// a canceled or past-due subscription. 7 days absorbs week-long transient +// outages (weekends, firewall rollouts) without punishing legitimate +// customers. export const STALE_ONLINE_LICENSE_THRESHOLD_MS = 7 * 24 * 60 * 60 * 1000; // Surface a UI warning (banner + "refreshed" timestamp color) when the @@ -116,6 +116,13 @@ const getValidOnlineLicense = (_license: License | null): License | null => { return null; } +export const isValidLicenseActive = (_license: License | null): boolean => { + return ( + getValidOfflineLicense() !== null || + getValidOnlineLicense(_license) !== null + ); +} + export const isAnonymousAccessAvailable = (_license: License | null): boolean => { const offlineKey = getValidOfflineLicense(); if (offlineKey) { diff --git a/packages/shared/src/index.server.ts b/packages/shared/src/index.server.ts index 8be1b35c2..1c45eb3cf 100644 --- a/packages/shared/src/index.server.ts +++ b/packages/shared/src/index.server.ts @@ -5,6 +5,7 @@ export { hasEntitlement as _hasEntitlement, getEntitlements as _getEntitlements, isAnonymousAccessAvailable as _isAnonymousAccessAvailable, + isValidLicenseActive as _isValidLicenseActive, getSeatCap, getOfflineLicenseMetadata, STALE_ONLINE_LICENSE_THRESHOLD_MS, diff --git a/packages/web/src/__mocks__/prisma.ts b/packages/web/src/__mocks__/prisma.ts index 1cc8ea3e9..12afc2120 100644 --- a/packages/web/src/__mocks__/prisma.ts +++ b/packages/web/src/__mocks__/prisma.ts @@ -21,7 +21,6 @@ export const MOCK_ORG: Org = { memberApprovalRequired: false, inviteLinkEnabled: false, inviteLinkId: null, - trialUsedAt: null, } export const MOCK_API_KEY: ApiKey = { diff --git a/packages/web/src/app/(app)/@sidebar/components/defaultSidebar/index.tsx b/packages/web/src/app/(app)/@sidebar/components/defaultSidebar/index.tsx index c215af57c..c8702a4ee 100644 --- a/packages/web/src/app/(app)/@sidebar/components/defaultSidebar/index.tsx +++ b/packages/web/src/app/(app)/@sidebar/components/defaultSidebar/index.tsx @@ -6,14 +6,13 @@ import { getConnectionStats } from "@/actions"; import { getOrgAccountRequests } from "@/features/userManagement/actions"; import { isServiceError } from "@/lib/utils"; import { ServiceErrorException } from "@/lib/serviceError"; -import { __unsafePrisma } from "@/prisma"; -import { SINGLE_TENANT_ORG_ID } from "@/lib/constants"; import { OrgRole } from "@prisma/client"; import { SidebarBase } from "@/app/(app)/@sidebar/components/sidebarBase"; import { Nav } from "./nav"; import { ChatHistory } from "./chatHistory"; -import { withAuth } from "@/middleware/withAuth"; +import { getAuthContext, withAuth } from "@/middleware/withAuth"; import { sew } from "@/middleware/sew"; +import { isValidLicenseActive } from "@/lib/entitlements"; const SIDEBAR_CHAT_LIMIT = 30; @@ -27,15 +26,13 @@ export async function DefaultSidebar() { throw new ServiceErrorException(chatHistory); } + const licenseActive = await isValidLicenseActive(); + + const authContext = await getAuthContext(); + const isOwner = !isServiceError(authContext) && authContext.role === OrgRole.OWNER; + const isSettingsNotificationVisible = await (async () => { - if (!session) { - return false; - } - const membership = await __unsafePrisma.userToOrg.findUnique({ - where: { orgId_userId: { orgId: SINGLE_TENANT_ORG_ID, userId: session.user.id } }, - select: { role: true }, - }); - if (membership?.role !== OrgRole.OWNER) { + if (!isOwner) { return false; } const connectionStats = await getConnectionStats(); @@ -49,6 +46,8 @@ export async function DefaultSidebar() { } >