85 lines
3.2 KiB
TypeScript
85 lines
3.2 KiB
TypeScript
// The public API of the branding module (plan item AT2.6, spec §10.3): an organization's own brand
|
|
// -- its display name, two colours and two logos -- as the end customers of the app see it. It is a
|
|
// layer over the app brand (config/src/brand.ts) and never replaces it: layout, type and the neutral
|
|
// tokens stay the app's, so one tenant cannot break the app's accessibility.
|
|
//
|
|
// The data is `organizations.branding`, a column of the tenancy module's own table; this module
|
|
// writes it through the tenancy module's entity and owns no table of its own.
|
|
|
|
import { LOGO_UPLOAD_LIMIT_BYTES } from "@conusai-kit/brand/logo/check"
|
|
import { procedure } from "@conusai-kit/core"
|
|
import { z } from "zod"
|
|
import { LOGO_SIZES } from "./logo.js"
|
|
import { brandViewSchema, LOGO_VARIANTS } from "./view.js"
|
|
|
|
export { LOGO_SIZES, type LogoChoice, type LogoSize, logoUrl, resolveLogo, tenantLogo } from "./logo.js"
|
|
export { type BrandView, brandViewSchema, LOGO_VARIANTS, type LogoVariant } from "./view.js"
|
|
|
|
/** The audit event of every change: one per call, whether a colour, a name or a logo. */
|
|
export const BRAND_CHANGED = "branding.brand_changed"
|
|
|
|
const inOrg = { org: z.string() }
|
|
|
|
export const brandGet = procedure("branding.brand.get")
|
|
.kind("read")
|
|
.scope("org")
|
|
.permission("branding.brand.read")
|
|
.input(z.object(inOrg))
|
|
.output(brandViewSchema)
|
|
.contract()
|
|
|
|
export const brandSet = procedure("branding.brand.set")
|
|
.kind("write")
|
|
.scope("org")
|
|
.permission("branding.brand.write")
|
|
.input(
|
|
z.object({
|
|
...inOrg,
|
|
// Absent leaves a field as it is; null clears it; a value sets it.
|
|
displayName: z.string().max(60).nullable().optional(),
|
|
primary: z.string().max(32).nullable().optional(),
|
|
accent: z.string().max(32).nullable().optional(),
|
|
}),
|
|
)
|
|
.output(brandViewSchema)
|
|
.audit(BRAND_CHANGED)
|
|
.contract()
|
|
|
|
export const logoSet = procedure("branding.logo.set")
|
|
.kind("effect")
|
|
.scope("org")
|
|
.permission("branding.brand.write")
|
|
.input(
|
|
z.object({
|
|
...inOrg,
|
|
variant: z.enum(LOGO_VARIANTS),
|
|
contentType: z.string().max(64),
|
|
/** The image, standard base64: a PNG or WebP up to the upload limit (it is resized), an SVG up to the stored one. */
|
|
body: z.string().max(Math.ceil((LOGO_UPLOAD_LIMIT_BYTES * 4) / 3) + 4),
|
|
}),
|
|
)
|
|
.output(brandViewSchema)
|
|
.audit(BRAND_CHANGED)
|
|
.contract()
|
|
|
|
// Removing a logo deletes its stored files, which only an effect may do, so it is not a field of `brand.set`.
|
|
export const logoClear = procedure("branding.logo.clear")
|
|
.kind("effect")
|
|
.scope("org")
|
|
.permission("branding.brand.write")
|
|
.input(z.object({ ...inOrg, variant: z.enum(LOGO_VARIANTS) }))
|
|
.output(brandViewSchema)
|
|
.audit(BRAND_CHANGED)
|
|
.contract()
|
|
|
|
export { tenantStyle } from "./style.js"
|
|
|
|
export const logoGet = procedure("branding.logo.get")
|
|
.kind("effect")
|
|
.scope("org")
|
|
.permission("branding.brand.read")
|
|
// `size` picks the 1x or the 2x rendition of a raster logo (2x when absent); an SVG is one file and has one size.
|
|
.input(z.object({ ...inOrg, variant: z.enum(LOGO_VARIANTS), size: z.enum(LOGO_SIZES).optional() }))
|
|
.output(z.object({ contentType: z.string(), body: z.string() }))
|
|
.contract()
|