File size: 3,685 Bytes
45dc2a1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
"""
Saved email "customizations" — a named header + footer + brand color a company designs once
and picks per campaign, instead of redesigning them on every send. Stored per org at
orgs/{org_id}/customizations/{id}. The frontend merges the chosen customization around the
message body into the final HTML (frontend/src/lib/emailFrame.ts), so sending itself is
unchanged and never needs to know which customization was used.
"""

from __future__ import annotations

import re
import uuid
from datetime import datetime, timezone

from backend import firestore_client as db
from backend import orgs

_COLOR_RE = re.compile(r"^#[0-9a-fA-F]{6}$")
_MAX_NAME = 80
_MAX_HTML = 20_000


def _now() -> str:
    return datetime.now(timezone.utc).isoformat()


def _prefix(org_id: str) -> str:
    return f"orgs/{org_id}/customizations/"


def _path(org_id: str, customization_id: str) -> str:
    return f"{_prefix(org_id)}{customization_id}"


def _clean(name: str, brand_color: str, logo_url: str, header_text: str, footer_html: str, footer_bg: str) -> dict:
    name = name.strip()
    if not name or len(name) > _MAX_NAME:
        raise ValueError(f"Name is required and must be at most {_MAX_NAME} characters")
    if not _COLOR_RE.match(brand_color):
        raise ValueError("Brand color must be a hex color like #c0392b")
    if not _COLOR_RE.match(footer_bg):
        raise ValueError("Footer background must be a hex color like #f4f4f4")
    logo_url = logo_url.strip()
    if logo_url and not logo_url.lower().startswith("https://"):
        raise ValueError("Logo URL must start with https://")
    if len(footer_html) > _MAX_HTML or len(header_text) > 200:
        raise ValueError("Header or footer is too long")
    return {
        "name": name,
        "brand_color": brand_color,
        "logo_url": logo_url,
        "header_text": header_text.strip(),
        "footer_html": footer_html,
        "footer_bg": footer_bg,
    }


def create_customization(org_id: str, actor_uid: str, name: str, brand_color: str, logo_url: str,
                         header_text: str, footer_html: str, footer_bg: str = "#f4f4f4") -> str:
    orgs.require_permission(org_id, actor_uid, "edit_emails")
    data = _clean(name, brand_color, logo_url, header_text, footer_html, footer_bg)
    customization_id = uuid.uuid4().hex
    db.set_doc(_path(org_id, customization_id), data | {"created_at": _now(), "updated_at": _now()})
    return customization_id


def update_customization(org_id: str, actor_uid: str, customization_id: str, name: str, brand_color: str,
                         logo_url: str, header_text: str, footer_html: str, footer_bg: str = "#f4f4f4") -> None:
    orgs.require_permission(org_id, actor_uid, "edit_emails")
    get_customization(org_id, customization_id)
    data = _clean(name, brand_color, logo_url, header_text, footer_html, footer_bg)
    db.update_doc(_path(org_id, customization_id), data | {"updated_at": _now()})


def delete_customization(org_id: str, actor_uid: str, customization_id: str) -> None:
    orgs.require_permission(org_id, actor_uid, "edit_emails")
    get_customization(org_id, customization_id)
    db.delete_doc(_path(org_id, customization_id))


def get_customization(org_id: str, customization_id: str) -> dict:
    doc = db.get_doc(_path(org_id, customization_id))
    if doc is None:
        raise orgs.NotFoundError(f"No customization {customization_id!r} in org {org_id!r}")
    return doc


def list_customizations(org_id: str, actor_uid: str) -> dict[str, dict]:
    orgs.require_permission(org_id, actor_uid)
    prefix = _prefix(org_id)
    return {path[len(prefix):]: data for path, data in db.query_prefix(prefix).items()}