feat: add workspace onboarding templates
This commit is contained in:
@@ -0,0 +1,632 @@
|
||||
"""Workspace onboarding templates and apply helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
from typing import Any, Dict, Iterable, List, Optional, Tuple
|
||||
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models.domain import Domain
|
||||
from app.models.mail_source import MailSource
|
||||
from app.models.setting import Setting
|
||||
from app.models.workspace import Workspace
|
||||
from app.services.workspace_audit import record_workspace_audit_log, sanitize_audit_details
|
||||
from app.services.workspaces import normalize_workspace_slug, workspace_domain_query
|
||||
from app.utils.domain_validator import validate_domain_config
|
||||
|
||||
ONBOARDING_SCHEMA_VERSION = "dmarq.workspace_onboarding.v1"
|
||||
|
||||
SAFE_NOTIFICATION_DEFAULTS = {
|
||||
"notifications.apprise_enabled",
|
||||
"notifications.min_send_interval_minutes",
|
||||
"notifications.redact_pii_enabled",
|
||||
"notifications.alert_new_sources_enabled",
|
||||
"notifications.alert_compliance_drop_enabled",
|
||||
"notifications.alert_compliance_drop_points",
|
||||
"notifications.alert_failure_threshold_enabled",
|
||||
"notifications.alert_failure_threshold_count",
|
||||
"notifications.alert_missing_reports_enabled",
|
||||
"notifications.alert_missing_reports_days",
|
||||
"notifications.summary_daily_enabled",
|
||||
"notifications.summary_weekly_enabled",
|
||||
"notifications.summary_send_hour_utc",
|
||||
"notifications.summary_weekday_utc",
|
||||
}
|
||||
|
||||
NOTIFICATION_SETTING_META = {
|
||||
"notifications.apprise_enabled": ("Send notifications through Apprise", "boolean"),
|
||||
"notifications.min_send_interval_minutes": ("Minimum minutes between notifications", "integer"),
|
||||
"notifications.redact_pii_enabled": ("Redact PII in outbound notifications", "boolean"),
|
||||
"notifications.alert_new_sources_enabled": ("Alert when new senders appear", "boolean"),
|
||||
"notifications.alert_compliance_drop_enabled": ("Alert on compliance drops", "boolean"),
|
||||
"notifications.alert_compliance_drop_points": ("Compliance drop threshold", "integer"),
|
||||
"notifications.alert_failure_threshold_enabled": ("Alert on high failure volume", "boolean"),
|
||||
"notifications.alert_failure_threshold_count": ("Daily failure-count threshold", "integer"),
|
||||
"notifications.alert_missing_reports_enabled": ("Alert when reports stop arriving", "boolean"),
|
||||
"notifications.alert_missing_reports_days": ("Days without reports before alerting", "integer"),
|
||||
"notifications.summary_daily_enabled": ("Send daily DMARC summaries", "boolean"),
|
||||
"notifications.summary_weekly_enabled": ("Send weekly DMARC summaries", "boolean"),
|
||||
"notifications.summary_send_hour_utc": ("UTC hour for scheduled summaries", "integer"),
|
||||
"notifications.summary_weekday_utc": ("UTC weekday for weekly summaries", "integer"),
|
||||
}
|
||||
|
||||
WORKSPACE_ONBOARDING_TEMPLATES: List[Dict[str, Any]] = [
|
||||
{
|
||||
"id": "standard_monitoring",
|
||||
"name": "Standard DMARC Monitoring",
|
||||
"description": (
|
||||
"Create one monitored domain, one disabled IMAP mail source, "
|
||||
"and safe notification defaults."
|
||||
),
|
||||
"variables": [
|
||||
{"name": "domain", "required": True, "example": "example.com"},
|
||||
{"name": "workspace_name", "required": False, "example": "Example Client"},
|
||||
{"name": "report_mailbox", "required": False, "example": "dmarc@example.com"},
|
||||
{"name": "imap_server", "required": False, "example": "imap.example.com"},
|
||||
{"name": "imap_username", "required": False, "example": "dmarc@example.com"},
|
||||
{"name": "imap_password", "required": False, "secret": True},
|
||||
],
|
||||
"domains": [
|
||||
{
|
||||
"name": "{domain}",
|
||||
"description": "Primary DMARC domain for {workspace_name}",
|
||||
"dkim_selectors": ["google", "selector1"],
|
||||
}
|
||||
],
|
||||
"mail_sources": [
|
||||
{
|
||||
"name": "{workspace_name} DMARC inbox",
|
||||
"method": "IMAP",
|
||||
"server": "{imap_server}",
|
||||
"port": 993,
|
||||
"username": "{imap_username}",
|
||||
"password": "{imap_password}",
|
||||
"folder": "INBOX",
|
||||
"use_ssl": True,
|
||||
"polling_interval": 60,
|
||||
"enabled": False,
|
||||
}
|
||||
],
|
||||
"notification_defaults": {
|
||||
"notifications.apprise_enabled": "false",
|
||||
"notifications.redact_pii_enabled": "true",
|
||||
"notifications.alert_new_sources_enabled": "true",
|
||||
"notifications.alert_compliance_drop_enabled": "true",
|
||||
"notifications.alert_compliance_drop_points": "10",
|
||||
"notifications.alert_failure_threshold_enabled": "true",
|
||||
"notifications.alert_failure_threshold_count": "100",
|
||||
"notifications.alert_missing_reports_enabled": "true",
|
||||
"notifications.alert_missing_reports_days": "2",
|
||||
"notifications.summary_weekly_enabled": "true",
|
||||
"notifications.summary_send_hour_utc": "8",
|
||||
"notifications.summary_weekday_utc": "0",
|
||||
},
|
||||
"checklist": [
|
||||
{
|
||||
"id": "verify-domain-dns",
|
||||
"category": "domains",
|
||||
"title": "Verify DMARC, SPF, and DKIM DNS posture",
|
||||
"description": (
|
||||
"Open the domain DNS view and confirm DMARC, SPF, and each "
|
||||
"configured DKIM selector resolve."
|
||||
),
|
||||
},
|
||||
{
|
||||
"id": "connect-mail-source",
|
||||
"category": "mail_sources",
|
||||
"title": "Connect the DMARC report inbox",
|
||||
"description": (
|
||||
"Add credentials or complete OAuth, run a connection test, "
|
||||
"then enable polling."
|
||||
),
|
||||
},
|
||||
{
|
||||
"id": "run-initial-import",
|
||||
"category": "mail_sources",
|
||||
"title": "Run an initial DMARC import",
|
||||
"description": (
|
||||
"Trigger a manual import for the mail source and confirm " "reports are parsed."
|
||||
),
|
||||
},
|
||||
{
|
||||
"id": "configure-notification-target",
|
||||
"category": "notifications",
|
||||
"title": "Configure and test notification delivery",
|
||||
"description": (
|
||||
"Add Apprise targets, send a test notification, and confirm "
|
||||
"alert thresholds match the client."
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "dns_only_assessment",
|
||||
"name": "DNS Posture Assessment",
|
||||
"description": (
|
||||
"Create monitored domains and a validation checklist without " "mailbox ingestion."
|
||||
),
|
||||
"variables": [
|
||||
{"name": "domain", "required": True, "example": "example.com"},
|
||||
{"name": "workspace_name", "required": False, "example": "Example Client"},
|
||||
],
|
||||
"domains": [
|
||||
{
|
||||
"name": "{domain}",
|
||||
"description": "DNS posture assessment for {workspace_name}",
|
||||
"dkim_selectors": [],
|
||||
}
|
||||
],
|
||||
"mail_sources": [],
|
||||
"notification_defaults": {
|
||||
"notifications.apprise_enabled": "false",
|
||||
"notifications.alert_missing_reports_enabled": "false",
|
||||
"notifications.summary_weekly_enabled": "false",
|
||||
},
|
||||
"checklist": [
|
||||
{
|
||||
"id": "verify-domain-dns",
|
||||
"category": "domains",
|
||||
"title": "Verify published authentication records",
|
||||
"description": (
|
||||
"Review DMARC, SPF, DKIM, MTA-STS, and BIMI posture before "
|
||||
"recommending changes."
|
||||
),
|
||||
},
|
||||
{
|
||||
"id": "document-report-inbox",
|
||||
"category": "mail_sources",
|
||||
"title": "Document the future DMARC report inbox",
|
||||
"description": (
|
||||
"Confirm where aggregate reports are delivered before " "enabling ingestion."
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class _SafeFormatDict(dict):
|
||||
def __missing__(self, key):
|
||||
return "{" + key + "}"
|
||||
|
||||
|
||||
def _render_value(value: Any, variables: Dict[str, Any]) -> Any:
|
||||
if isinstance(value, str):
|
||||
return value.format_map(
|
||||
_SafeFormatDict({k: "" if v is None else v for k, v in variables.items()})
|
||||
)
|
||||
if isinstance(value, list):
|
||||
return [_render_value(item, variables) for item in value]
|
||||
if isinstance(value, dict):
|
||||
return {key: _render_value(item, variables) for key, item in value.items()}
|
||||
return value
|
||||
|
||||
|
||||
def _clean_optional_strings(value: Any) -> Any:
|
||||
if isinstance(value, dict):
|
||||
return {key: _clean_optional_strings(item) for key, item in value.items()}
|
||||
if isinstance(value, list):
|
||||
return [_clean_optional_strings(item) for item in value]
|
||||
if isinstance(value, str):
|
||||
stripped = value.strip()
|
||||
return stripped or None
|
||||
return value
|
||||
|
||||
|
||||
def _template_by_id(template_id: str) -> Dict[str, Any]:
|
||||
for template in WORKSPACE_ONBOARDING_TEMPLATES:
|
||||
if template["id"] == template_id:
|
||||
return copy.deepcopy(template)
|
||||
raise ValueError(f"Unknown onboarding template: {template_id}")
|
||||
|
||||
|
||||
def list_onboarding_templates() -> List[Dict[str, Any]]:
|
||||
"""Return static workspace onboarding templates."""
|
||||
return copy.deepcopy(WORKSPACE_ONBOARDING_TEMPLATES)
|
||||
|
||||
|
||||
def public_onboarding_plan(plan: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Return an API-safe plan with secret-like values redacted."""
|
||||
return sanitize_audit_details(plan)
|
||||
|
||||
|
||||
def _workspace_context(
|
||||
workspace: Dict[str, Any],
|
||||
variables: Optional[Dict[str, Any]],
|
||||
) -> Tuple[str, str, Dict[str, Any]]:
|
||||
context = {str(key): value for key, value in (variables or {}).items()}
|
||||
workspace_name = str(workspace.get("name") or context.get("workspace_name") or "").strip()
|
||||
slug = normalize_workspace_slug(str(workspace.get("slug") or workspace_name))
|
||||
context.setdefault("workspace_name", workspace_name or slug)
|
||||
context.setdefault("domain", "")
|
||||
context.setdefault("report_mailbox", "")
|
||||
context.setdefault("imap_server", "")
|
||||
context.setdefault("imap_username", context.get("report_mailbox", ""))
|
||||
context.setdefault("imap_password", "")
|
||||
return slug, workspace_name, context
|
||||
|
||||
|
||||
def _required_variable_errors(template: Dict[str, Any], variables: Dict[str, Any]) -> List[str]:
|
||||
errors = []
|
||||
for variable in template.get("variables", []):
|
||||
name = variable["name"]
|
||||
if variable.get("required") and not str(variables.get(name) or "").strip():
|
||||
errors.append(f"template variable is required: {name}")
|
||||
return errors
|
||||
|
||||
|
||||
def _render_sections(
|
||||
template: Dict[str, Any],
|
||||
variables: Dict[str, Any],
|
||||
*,
|
||||
domains: Optional[List[Dict[str, Any]]],
|
||||
mail_sources: Optional[List[Dict[str, Any]]],
|
||||
notification_defaults: Optional[Dict[str, Any]],
|
||||
) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]], Dict[str, Any]]:
|
||||
rendered_domains = _clean_optional_strings(
|
||||
_render_value(domains if domains is not None else template.get("domains", []), variables)
|
||||
)
|
||||
rendered_sources = _clean_optional_strings(
|
||||
_render_value(
|
||||
mail_sources if mail_sources is not None else template.get("mail_sources", []),
|
||||
variables,
|
||||
)
|
||||
)
|
||||
rendered_notifications = _clean_optional_strings(
|
||||
_render_value(
|
||||
(
|
||||
notification_defaults
|
||||
if notification_defaults is not None
|
||||
else template.get("notification_defaults", {})
|
||||
),
|
||||
variables,
|
||||
)
|
||||
)
|
||||
return rendered_domains, rendered_sources, rendered_notifications
|
||||
|
||||
|
||||
def _validated_domains(
|
||||
rendered_domains: Iterable[Dict[str, Any]],
|
||||
) -> Tuple[List[Dict[str, Any]], List[str]]:
|
||||
errors = []
|
||||
validated = []
|
||||
for item in rendered_domains:
|
||||
name = str(item.get("name") or "").strip().strip(".").lower()
|
||||
validation = validate_domain_config(
|
||||
{"name": name, "description": item.get("description") or ""}
|
||||
)
|
||||
if not validation["valid"]:
|
||||
errors.append(f"invalid domain {name or '<empty>'}: {validation['errors']}")
|
||||
continue
|
||||
selectors = [
|
||||
str(selector).strip()
|
||||
for selector in item.get("dkim_selectors") or []
|
||||
if str(selector).strip()
|
||||
]
|
||||
validated.append(
|
||||
{
|
||||
"name": name,
|
||||
"description": item.get("description"),
|
||||
"dkim_selectors": selectors,
|
||||
}
|
||||
)
|
||||
return validated, errors
|
||||
|
||||
|
||||
def _validated_mail_sources(
|
||||
rendered_sources: Iterable[Dict[str, Any]],
|
||||
) -> Tuple[List[Dict[str, Any]], List[str]]:
|
||||
errors = []
|
||||
validated = []
|
||||
for item in rendered_sources:
|
||||
name = str(item.get("name") or "").strip()
|
||||
method = str(item.get("method") or "IMAP").strip().upper()
|
||||
if not name:
|
||||
errors.append("mail source name is required")
|
||||
continue
|
||||
if method not in {"IMAP", "POP3", "GMAIL_API", "M365_GRAPH"}:
|
||||
errors.append(f"unsupported mail source method: {method}")
|
||||
continue
|
||||
validated.append(
|
||||
{
|
||||
"name": name,
|
||||
"method": method,
|
||||
"server": item.get("server"),
|
||||
"port": int(item.get("port") or 993),
|
||||
"username": item.get("username"),
|
||||
"password": item.get("password"),
|
||||
"use_ssl": bool(item.get("use_ssl", True)),
|
||||
"folder": item.get("folder") or "INBOX",
|
||||
"polling_interval": int(item.get("polling_interval") or 60),
|
||||
"enabled": bool(item.get("enabled", False)),
|
||||
"gmail_client_id": item.get("gmail_client_id"),
|
||||
"gmail_client_secret": item.get("gmail_client_secret"),
|
||||
"m365_tenant_id": item.get("m365_tenant_id") or "common",
|
||||
"m365_client_id": item.get("m365_client_id"),
|
||||
"m365_client_secret": item.get("m365_client_secret"),
|
||||
"m365_mailbox": item.get("m365_mailbox"),
|
||||
"m365_folder_id": item.get("m365_folder_id"),
|
||||
}
|
||||
)
|
||||
return validated, errors
|
||||
|
||||
|
||||
def _validated_notification_defaults(
|
||||
rendered_notifications: Dict[str, Any],
|
||||
) -> Tuple[Dict[str, str], List[str]]:
|
||||
errors = []
|
||||
safe_notifications = {}
|
||||
for key, value in (rendered_notifications or {}).items():
|
||||
if key not in SAFE_NOTIFICATION_DEFAULTS:
|
||||
errors.append(f"unsupported notification default: {key}")
|
||||
continue
|
||||
safe_notifications[key] = "" if value is None else str(value)
|
||||
return safe_notifications, errors
|
||||
|
||||
|
||||
def _validated_sections(
|
||||
rendered_domains: Iterable[Dict[str, Any]],
|
||||
rendered_sources: Iterable[Dict[str, Any]],
|
||||
rendered_notifications: Dict[str, Any],
|
||||
) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]], Dict[str, str], List[str]]:
|
||||
domains, domain_errors = _validated_domains(rendered_domains)
|
||||
sources, source_errors = _validated_mail_sources(rendered_sources)
|
||||
notifications, notification_errors = _validated_notification_defaults(rendered_notifications)
|
||||
return domains, sources, notifications, domain_errors + source_errors + notification_errors
|
||||
|
||||
|
||||
def build_onboarding_plan( # pylint: disable=too-many-locals
|
||||
*,
|
||||
template_id: str,
|
||||
workspace: Dict[str, Any],
|
||||
variables: Optional[Dict[str, Any]] = None,
|
||||
domains: Optional[List[Dict[str, Any]]] = None,
|
||||
mail_sources: Optional[List[Dict[str, Any]]] = None,
|
||||
notification_defaults: Optional[Dict[str, Any]] = None,
|
||||
overwrite_existing: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
"""Render and validate a workspace onboarding plan."""
|
||||
template = _template_by_id(template_id)
|
||||
slug, workspace_name, variables = _workspace_context(workspace, variables)
|
||||
errors: List[str] = []
|
||||
if not slug:
|
||||
errors.append("workspace.slug or workspace.name is required")
|
||||
if not workspace_name:
|
||||
errors.append("workspace.name is required")
|
||||
errors.extend(_required_variable_errors(template, variables))
|
||||
rendered_domains, rendered_sources, rendered_notifications = _render_sections(
|
||||
template,
|
||||
variables,
|
||||
domains=domains,
|
||||
mail_sources=mail_sources,
|
||||
notification_defaults=notification_defaults,
|
||||
)
|
||||
validated_domains, validated_sources, safe_notifications, section_errors = _validated_sections(
|
||||
rendered_domains, rendered_sources, rendered_notifications
|
||||
)
|
||||
errors.extend(section_errors)
|
||||
|
||||
return {
|
||||
"schema_version": ONBOARDING_SCHEMA_VERSION,
|
||||
"template_id": template_id,
|
||||
"template_name": template["name"],
|
||||
"workspace": {
|
||||
"slug": slug,
|
||||
"name": workspace_name,
|
||||
"description": workspace.get("description"),
|
||||
},
|
||||
"domains": validated_domains,
|
||||
"mail_sources": validated_sources,
|
||||
"notification_defaults": safe_notifications,
|
||||
"checklist": copy.deepcopy(template.get("checklist", [])),
|
||||
"overwrite_existing": overwrite_existing,
|
||||
"errors": errors,
|
||||
}
|
||||
|
||||
|
||||
def _ensure_workspace(db: Session, workspace_plan: Dict[str, Any]) -> Tuple[Workspace, str]:
|
||||
workspace = db.query(Workspace).filter(Workspace.slug == workspace_plan["slug"]).first()
|
||||
if workspace:
|
||||
return workspace, "existing"
|
||||
workspace = Workspace(
|
||||
slug=workspace_plan["slug"],
|
||||
name=workspace_plan["name"],
|
||||
description=workspace_plan.get("description"),
|
||||
active=True,
|
||||
)
|
||||
db.add(workspace)
|
||||
db.flush()
|
||||
return workspace, "created"
|
||||
|
||||
|
||||
def _apply_domains(
|
||||
db: Session,
|
||||
*,
|
||||
workspace: Workspace,
|
||||
domain_plans: Iterable[Dict[str, Any]],
|
||||
) -> List[Dict[str, Any]]:
|
||||
results: List[Dict[str, Any]] = []
|
||||
for item in domain_plans:
|
||||
existing = workspace_domain_query(db, workspace).filter(Domain.name == item["name"]).first()
|
||||
if existing:
|
||||
results.append({"name": item["name"], "status": "existing", "id": existing.id})
|
||||
continue
|
||||
owned_elsewhere = (
|
||||
db.query(Domain)
|
||||
.filter(Domain.name == item["name"], Domain.workspace_id != workspace.id)
|
||||
.first()
|
||||
)
|
||||
if owned_elsewhere:
|
||||
results.append(
|
||||
{
|
||||
"name": item["name"],
|
||||
"status": "conflict",
|
||||
"message": "Domain is already owned by another workspace",
|
||||
}
|
||||
)
|
||||
continue
|
||||
domain = Domain(
|
||||
workspace_id=workspace.id,
|
||||
name=item["name"],
|
||||
description=item.get("description"),
|
||||
dkim_selectors=",".join(item.get("dkim_selectors") or []) or None,
|
||||
active=True,
|
||||
verified=False,
|
||||
)
|
||||
db.add(domain)
|
||||
db.flush()
|
||||
results.append({"name": domain.name, "status": "created", "id": domain.id})
|
||||
return results
|
||||
|
||||
|
||||
def _apply_mail_sources(
|
||||
db: Session,
|
||||
*,
|
||||
workspace: Workspace,
|
||||
source_plans: Iterable[Dict[str, Any]],
|
||||
) -> List[Dict[str, Any]]:
|
||||
results: List[Dict[str, Any]] = []
|
||||
for item in source_plans:
|
||||
existing = (
|
||||
db.query(MailSource)
|
||||
.filter(MailSource.workspace_id == workspace.id, MailSource.name == item["name"])
|
||||
.first()
|
||||
)
|
||||
if existing:
|
||||
results.append({"name": item["name"], "status": "existing", "id": existing.id})
|
||||
continue
|
||||
source = MailSource(
|
||||
workspace_id=workspace.id,
|
||||
name=item["name"],
|
||||
method=item["method"],
|
||||
server=item.get("server"),
|
||||
port=item.get("port") or 993,
|
||||
username=item.get("username"),
|
||||
password=item.get("password"),
|
||||
use_ssl=item.get("use_ssl", True),
|
||||
folder=item.get("folder") or "INBOX",
|
||||
polling_interval=item.get("polling_interval") or 60,
|
||||
enabled=item.get("enabled", False),
|
||||
gmail_client_id=item.get("gmail_client_id"),
|
||||
gmail_client_secret=item.get("gmail_client_secret"),
|
||||
m365_tenant_id=item.get("m365_tenant_id") or "common",
|
||||
m365_client_id=item.get("m365_client_id"),
|
||||
m365_client_secret=item.get("m365_client_secret"),
|
||||
m365_mailbox=item.get("m365_mailbox"),
|
||||
m365_folder_id=item.get("m365_folder_id"),
|
||||
)
|
||||
db.add(source)
|
||||
db.flush()
|
||||
results.append({"name": source.name, "status": "created", "id": source.id})
|
||||
return results
|
||||
|
||||
|
||||
def _apply_notification_defaults(
|
||||
db: Session,
|
||||
*,
|
||||
defaults: Dict[str, str],
|
||||
overwrite_existing: bool,
|
||||
) -> List[Dict[str, Any]]:
|
||||
results: List[Dict[str, Any]] = []
|
||||
for key, value in defaults.items():
|
||||
row = db.query(Setting).filter(Setting.key == key).first()
|
||||
if row and not overwrite_existing:
|
||||
results.append({"key": key, "status": "existing"})
|
||||
continue
|
||||
if row:
|
||||
row.value = value
|
||||
results.append({"key": key, "status": "updated"})
|
||||
continue
|
||||
description, value_type = NOTIFICATION_SETTING_META.get(
|
||||
key, ("Workspace onboarding default", "string")
|
||||
)
|
||||
db.add(
|
||||
Setting(
|
||||
key=key,
|
||||
value=value,
|
||||
description=description,
|
||||
value_type=value_type,
|
||||
category="notifications",
|
||||
)
|
||||
)
|
||||
results.append({"key": key, "status": "created"})
|
||||
return results
|
||||
|
||||
|
||||
def apply_onboarding_plan(
|
||||
db: Session,
|
||||
*,
|
||||
plan: Dict[str, Any],
|
||||
auth_context: Optional[Dict[str, Any]],
|
||||
request=None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Apply a validated onboarding plan and return an API-safe result."""
|
||||
if plan.get("errors"):
|
||||
raise ValueError("Cannot apply an invalid onboarding plan")
|
||||
try:
|
||||
workspace, workspace_status = _ensure_workspace(db, plan["workspace"])
|
||||
domain_results = _apply_domains(db, workspace=workspace, domain_plans=plan["domains"])
|
||||
source_results = _apply_mail_sources(
|
||||
db,
|
||||
workspace=workspace,
|
||||
source_plans=plan["mail_sources"],
|
||||
)
|
||||
notification_results = _apply_notification_defaults(
|
||||
db,
|
||||
defaults=plan["notification_defaults"],
|
||||
overwrite_existing=bool(plan.get("overwrite_existing")),
|
||||
)
|
||||
conflicts = [item for item in domain_results if item["status"] == "conflict"]
|
||||
if conflicts:
|
||||
db.rollback()
|
||||
return {
|
||||
"applied": False,
|
||||
"workspace": plan["workspace"],
|
||||
"errors": [item["message"] for item in conflicts],
|
||||
"results": {"domains": domain_results},
|
||||
}
|
||||
record_workspace_audit_log(
|
||||
db,
|
||||
workspace=workspace,
|
||||
action="workspace.onboarding_applied",
|
||||
entity_type="workspace",
|
||||
entity_id=workspace.id,
|
||||
entity_name=workspace.slug,
|
||||
details={
|
||||
"template_id": plan["template_id"],
|
||||
"workspace_status": workspace_status,
|
||||
"domains": domain_results,
|
||||
"mail_sources": source_results,
|
||||
"notification_defaults": notification_results,
|
||||
"checklist_ids": [item["id"] for item in plan.get("checklist", [])],
|
||||
},
|
||||
auth_context=auth_context,
|
||||
request=request,
|
||||
)
|
||||
db.commit()
|
||||
except IntegrityError:
|
||||
db.rollback()
|
||||
raise
|
||||
|
||||
db.refresh(workspace)
|
||||
return public_onboarding_plan(
|
||||
{
|
||||
"applied": True,
|
||||
"schema_version": plan["schema_version"],
|
||||
"template_id": plan["template_id"],
|
||||
"workspace": {
|
||||
"id": workspace.id,
|
||||
"slug": workspace.slug,
|
||||
"name": workspace.name,
|
||||
"status": workspace_status,
|
||||
},
|
||||
"results": {
|
||||
"domains": domain_results,
|
||||
"mail_sources": source_results,
|
||||
"notification_defaults": notification_results,
|
||||
},
|
||||
"checklist": plan.get("checklist", []),
|
||||
}
|
||||
)
|
||||
Reference in New Issue
Block a user