Compare commits

..

6 Commits

Author SHA1 Message Date
Owen Schwartz
63694032e8 Merge pull request #2511 from fosrl/dev
1.15.4-s.4
2026-02-20 20:45:23 -08:00
miloschwartz
eedf57af89 disable rybbit in saas 2026-02-19 17:54:40 -08:00
Owen
756f3f32ca Merge branch 'dev' 2026-02-17 21:57:49 -08:00
Owen
5987f6b2cd Allow enterprise 2026-02-17 21:55:57 -08:00
Owen
09a9457021 Fix transaction issue 2026-02-17 21:27:23 -08:00
Owen Schwartz
ca4643ec36 Merge pull request #2494 from fosrl/dev
1.15.4-s.3
2026-02-17 21:07:04 -08:00
35 changed files with 52 additions and 157 deletions

View File

@@ -3,14 +3,7 @@ import {
encodeHexLowerCase
} from "@oslojs/encoding";
import { sha256 } from "@oslojs/crypto/sha2";
import {
resourceSessions,
safeRead,
Session,
sessions,
User,
users
} from "@server/db";
import { resourceSessions, Session, sessions, User, users } from "@server/db";
import { db } from "@server/db";
import { eq, inArray } from "drizzle-orm";
import config from "@server/lib/config";
@@ -61,15 +54,11 @@ export async function validateSessionToken(
const sessionId = encodeHexLowerCase(
sha256(new TextEncoder().encode(token))
);
const result = await safeRead((db) =>
db
.select({ user: users, session: sessions })
.from(sessions)
.innerJoin(users, eq(sessions.userId, users.userId))
.where(eq(sessions.sessionId, sessionId))
);
const result = await db
.select({ user: users, session: sessions })
.from(sessions)
.innerJoin(users, eq(sessions.userId, users.userId))
.where(eq(sessions.sessionId, sessionId));
if (result.length < 1) {
return { session: null, user: null };
}

View File

@@ -1,7 +1,7 @@
import { encodeHexLowerCase } from "@oslojs/encoding";
import { sha256 } from "@oslojs/crypto/sha2";
import { resourceSessions, ResourceSession } from "@server/db";
import { db, safeRead } from "@server/db";
import { db } from "@server/db";
import { eq, and } from "drizzle-orm";
import config from "@server/lib/config";
@@ -66,17 +66,15 @@ export async function validateResourceSessionToken(
const sessionId = encodeHexLowerCase(
sha256(new TextEncoder().encode(token))
);
const result = await safeRead((db) =>
db
.select()
.from(resourceSessions)
.where(
and(
eq(resourceSessions.sessionId, sessionId),
eq(resourceSessions.resourceId, resourceId)
)
const result = await db
.select()
.from(resourceSessions)
.where(
and(
eq(resourceSessions.sessionId, sessionId),
eq(resourceSessions.resourceId, resourceId)
)
);
);
if (result.length < 1) {
return { resourceSession: null };

View File

@@ -1,5 +1,4 @@
export * from "./driver";
export * from "./safeRead";
export * from "./schema/schema";
export * from "./schema/privateSchema";
export * from "./migrate";

View File

@@ -1,24 +0,0 @@
import { db, primaryDb } from "./driver";
/**
* Runs a read query with replica fallback for Postgres.
* Executes the query against the replica first (when replicas exist).
* If the query throws or returns no data (null, undefined, or empty array),
* runs the same query against the primary.
*/
export async function safeRead<T>(
query: (d: typeof db | typeof primaryDb) => Promise<T>
): Promise<T> {
try {
const result = await query(db);
if (result === undefined || result === null) {
return query(primaryDb);
}
if (Array.isArray(result) && result.length === 0) {
return query(primaryDb);
}
return result;
} catch {
return query(primaryDb);
}
}

View File

@@ -1,5 +1,4 @@
export * from "./driver";
export * from "./safeRead";
export * from "./schema/schema";
export * from "./schema/privateSchema";
export * from "./migrate";

View File

@@ -1,11 +0,0 @@
import { db } from "./driver";
/**
* Runs a read query. For SQLite there is no replica/primary distinction,
* so the query is executed once against the database.
*/
export async function safeRead<T>(
query: (d: typeof db) => Promise<T>
): Promise<T> {
return query(db);
}

View File

@@ -46,8 +46,6 @@ export class UsageService {
return null;
}
let orgIdToUse = await this.getBillingOrg(orgId, transaction);
// Truncate value to 11 decimal places
value = this.truncateValue(value);
@@ -59,6 +57,7 @@ export class UsageService {
try {
let usage;
if (transaction) {
const orgIdToUse = await this.getBillingOrg(orgId, transaction);
usage = await this.internalAddUsage(
orgIdToUse,
featureId,
@@ -67,6 +66,7 @@ export class UsageService {
);
} else {
await db.transaction(async (trx) => {
const orgIdToUse = await this.getBillingOrg(orgId, trx);
usage = await this.internalAddUsage(
orgIdToUse,
featureId,
@@ -92,7 +92,7 @@ export class UsageService {
const delay = baseDelay + jitter;
logger.warn(
`Deadlock detected for ${orgIdToUse}/${featureId}, retrying attempt ${attempt}/${maxRetries} after ${delay.toFixed(0)}ms`
`Deadlock detected for ${orgId}/${featureId}, retrying attempt ${attempt}/${maxRetries} after ${delay.toFixed(0)}ms`
);
await new Promise((resolve) => setTimeout(resolve, delay));
@@ -100,7 +100,7 @@ export class UsageService {
}
logger.error(
`Failed to add usage for ${orgIdToUse}/${featureId} after ${attempt} attempts:`,
`Failed to add usage for ${orgId}/${featureId} after ${attempt} attempts:`,
error
);
break;
@@ -169,7 +169,7 @@ export class UsageService {
return;
}
let orgIdToUse = await this.getBillingOrg(orgId);
const orgIdToUse = await this.getBillingOrg(orgId);
try {
// Truncate value to 11 decimal places if provided
@@ -227,7 +227,7 @@ export class UsageService {
orgId: string,
featureId: FeatureId
): Promise<string | null> {
let orgIdToUse = await this.getBillingOrg(orgId);
const orgIdToUse = await this.getBillingOrg(orgId);
const cacheKey = `customer_${orgIdToUse}_${featureId}`;
const cached = cache.get<string>(cacheKey);
@@ -274,7 +274,7 @@ export class UsageService {
return null;
}
let orgIdToUse = await this.getBillingOrg(orgId, trx);
const orgIdToUse = await this.getBillingOrg(orgId, trx);
const usageId = `${orgIdToUse}-${featureId}`;
@@ -382,7 +382,7 @@ export class UsageService {
return false;
}
let orgIdToUse = await this.getBillingOrg(orgId, trx);
const orgIdToUse = await this.getBillingOrg(orgId, trx);
// This method should check the current usage against the limits set for the organization
// and kick out all of the sites on the org

View File

@@ -78,7 +78,8 @@ export async function getOrgTierData(
if (
subscription.type === "tier1" ||
subscription.type === "tier2" ||
subscription.type === "tier3"
subscription.type === "tier3" ||
subscription.type === "enterprise"
) {
tier = subscription.type;
active = true;

View File

@@ -197,7 +197,6 @@ export async function updateSiteBandwidth(
usageService
.checkLimitSet(
orgId,
FeatureId.EGRESS_DATA_MB,
bandwidthUsage
)

View File

@@ -14,7 +14,7 @@ export default async function migration() {
// all roles set hoemdir to true
// generate ca certs for all orgs?
// set authDaemonMode to "site" for all site-resources
// set authDaemonMode to "site" for all orgs
try {
db.transaction(() => {})();

View File

@@ -1,7 +1,6 @@
import type { Metadata } from "next";
import "./globals.css";
import localFont from "next/font/local";
import { APP_FONT } from "./font-config";
import { Geist, Inter, Manrope, Open_Sans } from "next/font/google";
import { ThemeProvider } from "@app/providers/ThemeProvider";
import EnvProvider from "@app/providers/EnvProvider";
import { pullEnv } from "@app/lib/pullEnv";
@@ -25,7 +24,6 @@ import { TanstackQueryProvider } from "@app/components/TanstackQueryProvider";
import { TailwindIndicator } from "@app/components/TailwindIndicator";
import { ViewportHeightFix } from "@app/components/ViewportHeightFix";
import StoreInternalRedirect from "@app/components/StoreInternalRedirect";
import { Inter } from "next/font/google";
export const metadata: Metadata = {
title: `Dashboard - ${process.env.BRANDING_APP_NAME || "Pangolin"}`,
@@ -34,79 +32,10 @@ export const metadata: Metadata = {
export const dynamic = "force-dynamic";
const ember = localFont({
src: [
{
path: "../../public/fonts/ember/AmazonEmber_Th.ttf",
weight: "100",
style: "normal"
},
{
path: "../../public/fonts/ember/AmazonEmber_ThIt.ttf",
weight: "100",
style: "italic"
},
{
path: "../../public/fonts/ember/AmazonEmber_Lt.ttf",
weight: "300",
style: "normal"
},
{
path: "../../public/fonts/ember/AmazonEmber_LtIt.ttf",
weight: "300",
style: "italic"
},
{
path: "../../public/fonts/ember/AmazonEmber_Rg.ttf",
weight: "400",
style: "normal"
},
{
path: "../../public/fonts/ember/AmazonEmber_RgIt.ttf",
weight: "400",
style: "italic"
},
{
path: "../../public/fonts/ember/Amazon-Ember-Medium.ttf",
weight: "500",
style: "normal"
},
{
path: "../../public/fonts/ember/Amazon-Ember-MediumItalic.ttf",
weight: "500",
style: "italic"
},
{
path: "../../public/fonts/ember/AmazonEmber_Bd.ttf",
weight: "700",
style: "normal"
},
{
path: "../../public/fonts/ember/AmazonEmber_BdIt.ttf",
weight: "700",
style: "italic"
},
{
path: "../../public/fonts/ember/AmazonEmber_He.ttf",
weight: "800",
style: "normal"
},
{
path: "../../public/fonts/ember/AmazonEmber_HeIt.ttf",
weight: "800",
style: "italic"
}
],
variable: "--font-ember",
display: "swap"
});
const inter = Inter({
const font = Inter({
subsets: ["latin"]
});
const fontClassName = inter.className;
export default async function RootLayout({
children
}: Readonly<{
@@ -150,16 +79,16 @@ export default async function RootLayout({
return (
<html suppressHydrationWarning lang={locale}>
<body className={`${fontClassName} h-screen-safe overflow-hidden`}>
<body className={`${font.className} h-screen-safe overflow-hidden`}>
<StoreInternalRedirect />
<TopLoader />
{build === "saas" && (
{/* build === "saas" && (
<Script
src="https://rybbit.fossorial.io/api/script.js"
data-site-id="fe1ff2a33287"
strategy="afterInteractive"
/>
)}
)*/}
<ViewportHeightFix />
<NextIntlClientProvider>
<ThemeProvider

View File

@@ -26,7 +26,7 @@ export default function SiteInfoCard({}: ClientInfoCardProps) {
return (
<Alert>
<AlertDescription>
<InfoSections cols={3}>
<InfoSections cols={4}>
<InfoSection>
<InfoSectionTitle>{t("name")}</InfoSectionTitle>
<InfoSectionContent>{client.name}</InfoSectionContent>
@@ -55,6 +55,12 @@ export default function SiteInfoCard({}: ClientInfoCardProps) {
)}
</InfoSectionContent>
</InfoSection>
<InfoSection>
<InfoSectionTitle>{t("address")}</InfoSectionTitle>
<InfoSectionContent>
{client.subnet.split("/")[0]}
</InfoSectionContent>
</InfoSection>
</InfoSections>
</AlertDescription>
</Alert>

View File

@@ -33,7 +33,7 @@ export default function SiteInfoCard({}: SiteInfoCardProps) {
return (
<Alert>
<AlertDescription>
<InfoSections cols={3}>
<InfoSections cols={4}>
<InfoSection>
<InfoSectionTitle>{t("identifier")}</InfoSectionTitle>
<InfoSectionContent>{site.niceId}</InfoSectionContent>
@@ -68,6 +68,15 @@ export default function SiteInfoCard({}: SiteInfoCardProps) {
{getConnectionTypeString(site.type)}
</InfoSectionContent>
</InfoSection>
{site.type == "newt" && (
<InfoSection>
<InfoSectionTitle>Address</InfoSectionTitle>
<InfoSectionContent>
{site.address?.split("/")[0]}
</InfoSectionContent>
</InfoSection>
)}
</InfoSections>
</AlertDescription>
</Alert>

View File

@@ -20,7 +20,7 @@ export const isOrgSubscribed = cache(async (orgId: string) => {
try {
const subRes = await getCachedSubscription(orgId);
subscribed =
(subRes.data.data.tier == "tier1" || subRes.data.data.tier == "tier2" || subRes.data.data.tier == "tier3") &&
(subRes.data.data.tier == "tier1" || subRes.data.data.tier == "tier2" || subRes.data.data.tier == "tier3" || subRes.data.data.tier == "enterprise") &&
subRes.data.data.active;
} catch {}
}

View File

@@ -42,7 +42,8 @@ export function SubscriptionStatusProvider({
if (
subscription.type == "tier1" ||
subscription.type == "tier2" ||
subscription.type == "tier3"
subscription.type == "tier3" ||
subscription.type == "enterprise"
) {
return {
tier: subscription.type,
@@ -61,7 +62,7 @@ export function SubscriptionStatusProvider({
const isSubscribed = () => {
const { tier, active } = getTier();
return (
(tier == "tier1" || tier == "tier2" || tier == "tier3") &&
(tier == "tier1" || tier == "tier2" || tier == "tier3" || tier == "enterprise") &&
active
);
};