NobGit
public anord read

Ledger

Why work hard when you can work easier?

Languages

Repository composition by tracked source files.

TypeScript
TypeScript 86% CSS 10% SQL 3% Shell 1% HTML 0%
Create file Wiki Documentation
Clone
https://nobgit.com/orgs/anord/ledger.git
ssh://[email protected]:2222/orgs/anord/ledger.git

Commit

Fixing loading issue when putting Ledger behind domain

b71e6dd
Alex Nord <[email protected]> 3 months ago
apps/api/src/db/migrations/003_footer_links.sql |  2 +
 apps/api/src/db/seed.ts                         |  4 +-
 apps/api/src/http/routes/settings.ts            | 11 ++++-
 apps/api/src/services/settings.ts               | 59 ++++++++++++++++++++-----
 apps/api/src/services/setup.ts                  |  2 +-
 packages/shared/src/contracts.ts                |  1 +
 6 files changed, 64 insertions(+), 15 deletions(-)
 create mode 100644 apps/api/src/db/migrations/003_footer_links.sql

Diff

diff --git a/apps/api/src/db/migrations/003_footer_links.sql b/apps/api/src/db/migrations/003_footer_links.sql
new file mode 100644
index 0000000..f9214d6
--- /dev/null
+++ b/apps/api/src/db/migrations/003_footer_links.sql
@@ -0,0 +1,2 @@
+ALTER TABLE branding_settings
+ADD COLUMN IF NOT EXISTS footer_links JSONB NOT NULL DEFAULT '[]'::jsonb;
diff --git a/apps/api/src/db/seed.ts b/apps/api/src/db/seed.ts
index 5884d13..d505c8c 100644
--- a/apps/api/src/db/seed.ts
+++ b/apps/api/src/db/seed.ts
@@ -23,8 +23,8 @@ async function main() {
 
   await pool.query(
     `
-      INSERT INTO branding_settings (site_name, logo_url, brand_color, footer_text, public_knowledge_base_enabled)
-      SELECT 'Ledger', NULL, '#245cff', 'Built for fast, trusted answers.', true
+      INSERT INTO branding_settings (site_name, logo_url, brand_color, footer_text, public_knowledge_base_enabled, footer_links)
+      SELECT 'Ledger', NULL, '#245cff', 'Powered by Ledger made by ANord.cc', true, '[]'::jsonb
       WHERE NOT EXISTS (SELECT 1 FROM branding_settings)
     `
   );
diff --git a/apps/api/src/http/routes/settings.ts b/apps/api/src/http/routes/settings.ts
index d00f30f..f97e901 100644
--- a/apps/api/src/http/routes/settings.ts
+++ b/apps/api/src/http/routes/settings.ts
@@ -8,7 +8,13 @@ const brandingSchema = z.object({
   siteName: z.string().min(2),
   logoUrl: z.string().url().nullable(),
   brandColor: z.string().regex(/^#[0-9a-fA-F]{6}$/),
-  publicKnowledgeBaseEnabled: z.boolean()
+  publicKnowledgeBaseEnabled: z.boolean(),
+  footerLinks: z.array(
+    z.object({
+      label: z.string().min(1),
+      href: z.string().url()
+    })
+  ).default([])
 });
 
 export const settingsRouter = Router();
@@ -20,7 +26,8 @@ settingsRouter.get("/public", async (_req, res) => {
       siteName: branding.site_name,
       logoUrl: branding.logo_url,
       brandColor: branding.brand_color,
-      publicKnowledgeBaseEnabled: branding.public_knowledge_base_enabled
+      publicKnowledgeBaseEnabled: branding.public_knowledge_base_enabled,
+      footerLinks: branding.footer_links ?? []
     }
   });
 });
diff --git a/apps/api/src/services/settings.ts b/apps/api/src/services/settings.ts
index ef379f4..25b9d1c 100644
--- a/apps/api/src/services/settings.ts
+++ b/apps/api/src/services/settings.ts
@@ -3,18 +3,46 @@ import { pool } from "../db/pool.js";
 
 const FIXED_FOOTER = "Powered by Ledger made by ANord.cc";
 
+function parseFooterLinks(value: unknown) {
+  if (!Array.isArray(value)) {
+    return [] as Array<{ label: string; href: string }>;
+  }
+
+  return value
+    .map((item) => {
+      if (!item || typeof item !== "object") {
+        return null;
+      }
+
+      const label = "label" in item ? String(item.label ?? "").trim() : "";
+      const href = "href" in item ? String(item.href ?? "").trim() : "";
+      if (!label || !href) {
+        return null;
+      }
+
+      return { label, href };
+    })
+    .filter(Boolean) as Array<{ label: string; href: string }>;
+}
+
 export async function getBrandingSettings() {
   const result = await pool.query(`SELECT * FROM branding_settings ORDER BY created_at ASC LIMIT 1`);
-  return (
+  const row =
     result.rows[0] ?? {
       id: "default",
       site_name: "Ledger",
       logo_url: null,
       brand_color: "#245cff",
       footer_text: FIXED_FOOTER,
+      footer_links: [],
       public_knowledge_base_enabled: true
-    }
-  );
+    };
+
+  return {
+    ...row,
+    footer_text: FIXED_FOOTER,
+    footer_links: parseFooterLinks(row.footer_links)
+  };
 }
 
 export async function upsertBrandingSettings(input: {
@@ -22,6 +50,7 @@ export async function upsertBrandingSettings(input: {
   logoUrl: string | null;
   brandColor: string;
   publicKnowledgeBaseEnabled: boolean;
+  footerLinks: Array<{ label: string; href: string }>;
 }) {
   const existing = await getBrandingSettings();
   if (existing) {
@@ -29,7 +58,7 @@ export async function upsertBrandingSettings(input: {
       `
         UPDATE branding_settings
         SET site_name = $2, logo_url = $3, brand_color = $4, footer_text = $5,
-            public_knowledge_base_enabled = $6, updated_at = now()
+            public_knowledge_base_enabled = $6, footer_links = $7, updated_at = now()
         WHERE id = $1
         RETURNING *
       `,
@@ -39,16 +68,21 @@ export async function upsertBrandingSettings(input: {
         input.logoUrl,
         input.brandColor,
         FIXED_FOOTER,
-        input.publicKnowledgeBaseEnabled
+        input.publicKnowledgeBaseEnabled,
+        JSON.stringify(input.footerLinks)
       ]
     );
-    return result.rows[0];
+    return {
+      ...result.rows[0],
+      footer_text: FIXED_FOOTER,
+      footer_links: parseFooterLinks(result.rows[0].footer_links)
+    };
   }
 
   const created = await pool.query(
     `
-      INSERT INTO branding_settings (site_name, logo_url, brand_color, footer_text, public_knowledge_base_enabled)
-      VALUES ($1, $2, $3, $4, $5)
+      INSERT INTO branding_settings (site_name, logo_url, brand_color, footer_text, public_knowledge_base_enabled, footer_links)
+      VALUES ($1, $2, $3, $4, $5, $6)
       RETURNING *
     `,
     [
@@ -56,10 +90,15 @@ export async function upsertBrandingSettings(input: {
       input.logoUrl,
       input.brandColor,
       FIXED_FOOTER,
-      input.publicKnowledgeBaseEnabled
+      input.publicKnowledgeBaseEnabled,
+      JSON.stringify(input.footerLinks)
     ]
   );
-  return created.rows[0];
+  return {
+    ...created.rows[0],
+    footer_text: FIXED_FOOTER,
+    footer_links: parseFooterLinks(created.rows[0].footer_links)
+  };
 }
 
 export async function getAdminSettingsBundle() {
diff --git a/apps/api/src/services/setup.ts b/apps/api/src/services/setup.ts
index 39bbacd..4d66db1 100644
--- a/apps/api/src/services/setup.ts
+++ b/apps/api/src/services/setup.ts
@@ -48,7 +48,7 @@ export async function initializeLedger(input: SetupInput) {
       `
         UPDATE branding_settings
         SET site_name = $1, brand_color = $2, footer_text = $3,
-            public_knowledge_base_enabled = $4, updated_at = now()
+            public_knowledge_base_enabled = $4, footer_links = '[]'::jsonb, updated_at = now()
         WHERE id = (SELECT id FROM branding_settings ORDER BY created_at ASC LIMIT 1)
         RETURNING id
       `,
diff --git a/packages/shared/src/contracts.ts b/packages/shared/src/contracts.ts
index f47c04e..5b755a0 100644
--- a/packages/shared/src/contracts.ts
+++ b/packages/shared/src/contracts.ts
@@ -70,6 +70,7 @@ export interface BrandingSettings {
   logoUrl: string | null;
   brandColor: string;
   publicKnowledgeBaseEnabled: boolean;
+  footerLinks: Array<{ label: string; href: string }>;
 }
 
 export interface SmtpSettings {