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
30 changed files with 142 additions and 36 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

@@ -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,7 +150,7 @@ 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" && (

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>