Compare commits

..

4 Commits

Author SHA1 Message Date
miloschwartz
58ac499f30 add safeRead 2026-02-21 16:38:51 -08:00
miloschwartz
f07f0092ad testing with local font 2026-02-21 14:34:38 -08:00
miloschwartz
218a4893b6 hide address on sites and clients 2026-02-20 22:47:56 -08:00
miloschwartz
266bf261aa update note in migration 2026-02-20 22:45:37 -08:00
35 changed files with 157 additions and 52 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

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

View File

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

24
server/db/pg/safeRead.ts Normal file
View File

@@ -0,0 +1,24 @@
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,4 +1,5 @@
export * from "./driver";
export * from "./safeRead";
export * from "./schema/schema";
export * from "./schema/privateSchema";
export * from "./migrate";

View File

@@ -0,0 +1,11 @@
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,6 +46,8 @@ export class UsageService {
return null;
}
let orgIdToUse = await this.getBillingOrg(orgId, transaction);
// Truncate value to 11 decimal places
value = this.truncateValue(value);
@@ -57,7 +59,6 @@ export class UsageService {
try {
let usage;
if (transaction) {
const orgIdToUse = await this.getBillingOrg(orgId, transaction);
usage = await this.internalAddUsage(
orgIdToUse,
featureId,
@@ -66,7 +67,6 @@ 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 ${orgId}/${featureId}, retrying attempt ${attempt}/${maxRetries} after ${delay.toFixed(0)}ms`
`Deadlock detected for ${orgIdToUse}/${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 ${orgId}/${featureId} after ${attempt} attempts:`,
`Failed to add usage for ${orgIdToUse}/${featureId} after ${attempt} attempts:`,
error
);
break;
@@ -169,7 +169,7 @@ export class UsageService {
return;
}
const orgIdToUse = await this.getBillingOrg(orgId);
let 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> {
const orgIdToUse = await this.getBillingOrg(orgId);
let 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;
}
const orgIdToUse = await this.getBillingOrg(orgId, trx);
let orgIdToUse = await this.getBillingOrg(orgId, trx);
const usageId = `${orgIdToUse}-${featureId}`;
@@ -382,7 +382,7 @@ export class UsageService {
return false;
}
const orgIdToUse = await this.getBillingOrg(orgId, trx);
let 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,8 +78,7 @@ export async function getOrgTierData(
if (
subscription.type === "tier1" ||
subscription.type === "tier2" ||
subscription.type === "tier3" ||
subscription.type === "enterprise"
subscription.type === "tier3"
) {
tier = subscription.type;
active = true;

View File

@@ -197,6 +197,7 @@ 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 orgs
// set authDaemonMode to "site" for all site-resources
try {
db.transaction(() => {})();

View File

@@ -1,6 +1,7 @@
import type { Metadata } from "next";
import "./globals.css";
import { Geist, Inter, Manrope, Open_Sans } from "next/font/google";
import localFont from "next/font/local";
import { APP_FONT } from "./font-config";
import { ThemeProvider } from "@app/providers/ThemeProvider";
import EnvProvider from "@app/providers/EnvProvider";
import { pullEnv } from "@app/lib/pullEnv";
@@ -24,6 +25,7 @@ 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"}`,
@@ -32,10 +34,79 @@ export const metadata: Metadata = {
export const dynamic = "force-dynamic";
const font = Inter({
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({
subsets: ["latin"]
});
const fontClassName = inter.className;
export default async function RootLayout({
children
}: Readonly<{
@@ -79,16 +150,16 @@ export default async function RootLayout({
return (
<html suppressHydrationWarning lang={locale}>
<body className={`${font.className} h-screen-safe overflow-hidden`}>
<body className={`${fontClassName} 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={4}>
<InfoSections cols={3}>
<InfoSection>
<InfoSectionTitle>{t("name")}</InfoSectionTitle>
<InfoSectionContent>{client.name}</InfoSectionContent>
@@ -55,12 +55,6 @@ 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={4}>
<InfoSections cols={3}>
<InfoSection>
<InfoSectionTitle>{t("identifier")}</InfoSectionTitle>
<InfoSectionContent>{site.niceId}</InfoSectionContent>
@@ -68,15 +68,6 @@ 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 == "enterprise") &&
(subRes.data.data.tier == "tier1" || subRes.data.data.tier == "tier2" || subRes.data.data.tier == "tier3") &&
subRes.data.data.active;
} catch {}
}

View File

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