Files
property-management-network/lib/branding.ts
Leon SerfatyandClaude Opus 4.8 c9968531e4 Consolidate audit-fixes branch: webhooks, integrations, and deploy hardening
Batch commit of the pending working tree on security/audit-fixes-2026-07.
Major areas:
- Outbound webhooks / Zapier: schema + signed delivery with retries, public
  v1 API (REST-hook subscribe/unsubscribe), settings UI, cron drain.
- Deploy hardening: email via SMTP2GO (Resend fully removed), verified DB TLS
  (DATABASE_SSL=require + DATABASE_CA), storage fails loud in production when
  Spaces is unconfigured instead of silently using ephemeral disk.
- Integrations & features (concurrent work): accounting (QuickBooks/Xero),
  e-signature (DocuSign/Dropbox Sign), PayPal, geocoding/maps, onboarding,
  expanded legal pages.
- DB migrations 0006–0009.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 13:42:34 -04:00

76 lines
2.2 KiB
TypeScript

import { eq } from "drizzle-orm"
import { db } from "@/lib/db"
import { profiles } from "@/lib/db/schema"
import { PLAN_LIMITS } from "@/lib/stripe/plans"
import type { Plan } from "@/types"
export type Branding = {
brandName: string | null
logoUrl: string | null
color: string | null
hidePoweredBy: boolean
/** True only when the owner's plan includes white-label (landlord/lifetime). */
enabled: boolean
}
const DEFAULT_BRANDING: Branding = {
brandName: null,
logoUrl: null,
color: null,
hidePoweredBy: false,
enabled: false,
}
/**
* Loads a landlord's white-label branding for a given owner user id.
*
* Branding only applies when that owner's plan includes white-label
* (landlord/lifetime via PLAN_LIMITS[plan].hasWhiteLabel). A downgraded user's
* stored branding is preserved in the DB but `enabled` returns false, so callers
* fall back to the default (unbranded) look. Fails safe to unbranded on error.
*/
export async function getBranding(ownerUserId: string): Promise<Branding> {
if (!ownerUserId) return DEFAULT_BRANDING
try {
const profile = await db.query.profiles.findFirst({
where: eq(profiles.id, ownerUserId),
columns: {
plan: true,
brand_name: true,
brand_logo_url: true,
brand_color: true,
hide_powered_by: true,
},
})
if (!profile) return DEFAULT_BRANDING
const plan = (profile.plan ?? "starter") as Plan
const enabled = PLAN_LIMITS[plan]?.hasWhiteLabel === true
if (!enabled) return DEFAULT_BRANDING
return {
brandName: profile.brand_name?.trim() || null,
logoUrl: profile.brand_logo_url?.trim() || null,
color: profile.brand_color?.trim() || null,
hidePoweredBy: profile.hide_powered_by === true,
enabled: true,
}
} catch {
return DEFAULT_BRANDING
}
}
/** Matches a 6-digit hex color like #RRGGBB (case-insensitive). */
export const HEX_COLOR_RE = /^#[0-9a-fA-F]{6}$/
/** Returns a normalized #RRGGBB hex string, or null if invalid/empty. */
export function normalizeHexColor(input: string | null | undefined): string | null {
if (!input) return null
const value = input.trim()
if (!value) return null
return HEX_COLOR_RE.test(value) ? value.toLowerCase() : null
}