Store an organisation logo and contact, and use them on client surfaces (MAPPS-429) #509
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/MAPPS-429-org-contact-and-logo"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
MAPPS-429, server half. The SPA half (onboarding and settings collecting these, the form page rendering the logo) follows and needs this.
Re-scoped after David asked for ORGANISATION metadata rather than user identity, which is what unblocked the ticket: PMS-512 gave user names to bunyip and left mokosh unable to write them, and none of that applies to tenant-level fields.
Most of the storage already existed
TenantBrandinghas declaredlogo_url,support_phoneand friends since migration 002,UpdateTenantRequestalready acceptsbranding, andTenantResponsealready returns it. What it lacked was a way to put an image behindlogo_url, and a field for who a client should ask for.support_contact_name, distinct frombilling_contact_name(who an invoice goes to, routinely a different person).logo_mime, so the serving route answers with the right content type rather than sniffing bytes or encoding the format into a filename.Both are fields on the existing JSONB column, so no migration for either.
The logo
Storage follows PMS-483's ticket attachments: local disk under the shared upload root, in a
tenant-logos/subdirectory so a deployment still mounts one volume, one file per tenant, overwritten in place. A logo has no history worth keeping and versioning it only invites a cache-busting problem.The read side is public because it has to be. This image has to render in a client's browser on
/request-forms/{token}and in a client's MAIL CLIENT. Neither has a session and a mail client will never authenticate, so an authenticated route could not serve it at all. A company logo is the least private asset an MSP owns, and the route is keyed by a v4 tenant uuid.SVG is refused, along with everything outside PNG, JPEG, WebP and GIF: it is a script-capable document and this route serves bytes from the API origin to anonymous callers.
Writing is admin-gated like the rename. Upload writes the file before the branding pointer and delete clears the pointer before the file, so neither order leaves branding pointing at something that is not there.
Reading it back
Collecting metadata is only worth doing if something consumes it, so:
{{logo_html}}composed server-side into a complete element or an empty string. Same shape PMS-748 used for the abuse notice, for the same two reasons: no conditionals in the renderer, and an absent key ships literal braces to the client (MAPPS-425).New config: PUBLIC_API_BASE_URL
A mail client cannot resolve a relative
src, and it is the only caller that cannot (the SPA joins the same path with the API base it already resolved). Deliberately not derived fromBASE_URLorSPA_BASE_URL: on staging and production those are the apex and the SPA, while the logo is served by the API on a third host. Unset omits the logo rather than emitting a broken image.This needs a
~/dockerPR to take effect on staging and production, same asABUSE_CONTACT_EMAILdid. Opening that separately.Migration 103 adds the placeholder to the seeded template, matching the PMS-748 body verbatim, so a tenant that customised its template keeps its copy and one still holding the original 101 wording is left alone.
Testing
cargo fmt --all --check,cargo clippy --all-targets(clean), both migration guards, and the tenants suite (19 tests). New coverage:application/pdfandimage/svg+xmlare refused.MAPPS-429. Collecting organisation metadata is only worth doing if something reads it, so this is the reading half. The request-form contact line falls back to the organisation's contact when a form defines none. PMS-748 gave each definition an optional `contact_info` and otherwise wrote "Contact {MSP}, who sent it to you", which tells a client nothing actionable. A per-form line is for the request type that routes somewhere unusual; every other form should inherit the service-desk number the MSP set once. Same fallback on the client-facing form page. The email gains the logo. `{{logo_html}}` is composed server-side into a complete `<p><img></p>` or an empty string, the same shape PMS-748 used for the abuse notice and for the same two reasons: `render_template` has no conditionals, so an element that must sometimes disappear cannot live in the template, and a key that is sometimes absent ships literal braces to the client (MAPPS-425). That needs an absolute URL, which is what `PUBLIC_API_BASE_URL` is for. A mail client cannot resolve a relative `src`, and this is the only caller that cannot: the SPA joins the same path with the API base it already resolved. It is deliberately not derived from `BASE_URL` or `SPA_BASE_URL`, which are the apex and the SPA on every deployed environment, while the logo is served by the API on a third host. Unset omits the logo rather than emitting a broken image. Migration 103 adds the placeholder to the seeded template, matching the PMS-748 body verbatim so a customised template keeps its copy, and so does a tenant still holding the original 101 wording. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E48b8YHWYwmaKjFBy5esXp