towerops/docs/CONVENTIONS.md
Graham McIntie 1590f78bdc fix: input validation, SSRF, API hardening, and cookie security
- LIKE wildcard injection: sanitize %, _ in search queries (devices, sites, gaiia)
- Jason.decode! → Jason.decode with error handling for untrusted input
- inspect() leak: replace with generic error messages, log details server-side
- SSRF protection: URL validator blocks private IPs, localhost, non-HTTP schemes
- SSRF validation added to HTTP monitoring executor and integration credentials
- GraphQL complexity limits: always applied, not just in prod
- GraphQL introspection: also check GET query params, not just body
- Stripe webhook: explicit nil/empty checks for signature and body
- Cookie security: secure flag for session (prod), http_only+secure for remember_me
- Honeybadger API key: read from env var with fallback
- String.to_integer → Integer.parse with fallback for URL params
- String.to_atom → whitelist map for HTTP methods
- Gaiia webhook: remove secret_len and expected signature from log
- Admin API: add rate limiting pipeline
- to_atom_keys: per-key fallback instead of all-or-nothing rescue
2026-03-14 14:48:59 -05:00

6 KiB

TowerOps Coding Conventions

Project-specific patterns and conventions for the TowerOps Phoenix LiveView application.

File Organization

LiveView Modules

LiveView pages live under lib/towerops_web/live/ organized by domain:

lib/towerops_web/live/
  schedule_live/
    index.ex            # List page module
    index.html.heex     # List page template
    form.ex             # Create/edit form module
    form.html.heex      # Form template
    show.ex             # Detail page module
    show.html.heex      # Detail page template
  • .ex files contain the LiveView module (mount, handle_params, handle_event, etc.)
  • .html.heex files contain the corresponding HEEx template
  • Module naming: ToweropsWeb.ScheduleLive.Index, ToweropsWeb.ScheduleLive.Form, etc.

Test Files

Tests mirror the lib/ structure under test/:

test/
  towerops/             # Context/business logic tests
  towerops_web/         # Web layer tests
  snmpkit/              # SNMP toolkit tests
  integration/          # Integration tests (SNMP, discovery)
  support/              # Test helpers and fixtures
    fixtures/           # Factory/fixture modules
  mix/                  # Mix task tests
  test_helper.exs

Navigation & Page Structure

active_page Assign

Every LiveView sets an active_page assign in mount/3 to highlight the correct nav item:

def mount(_params, _session, socket) do
  {:ok, assign(socket, :active_page, "schedules")}
end

Valid values: "dashboard", "devices", "sites", "network-map", "alerts", "maintenance", "schedules", "insights", "trace", "activity", "settings", "help"

The layout component (Layouts.authenticated) checks @active_page to apply the active class to nav links.

Breadcrumbs

Use the ToweropsWeb.Components.Breadcrumbs component (.breadcrumb) for hierarchical navigation:

<.breadcrumb items={[
  %{label: "Dashboard", navigate: ~p"/dashboard"},
  %{label: "Devices", navigate: ~p"/devices"},
  %{label: "My Device"}
]} />
  • Each item is a map with :label (required) and :navigate (optional)
  • Omit :navigate on the last item (current page)
  • The first item automatically gets a home icon

Forms

Standard Form Pattern

Forms use phx-change="validate" for real-time validation and phx-submit="save" for submission:

<.simple_form for={@form} phx-change="validate" phx-submit="save">
  <.input field={@form[:name]} label={t("Name")} />
  <:actions>
    <.button>{t("Save")}</.button>
  </:actions>
</.simple_form>

The LiveView handles both events:

def handle_event("validate", %{"schedule" => params}, socket) do
  changeset = Schedule.changeset(socket.assigns.schedule, params)
  {:noreply, assign(socket, :form, to_form(changeset, action: :validate))}
end

def handle_event("save", %{"schedule" => params}, socket) do
  # ... create or update logic
end

Flash Messages

Use put_flash/3 with :info for success and :error for failures. Always wrap messages in t():

socket
|> put_flash(:info, t("Schedule created successfully."))
|> push_navigate(to: ~p"/schedules")
socket
|> put_flash(:error, t("Could not save schedule."))

Translations (i18n)

All user-facing strings must use the t() macro (or domain-specific variants). Defined in ToweropsWeb.GettextHelpers:

Macro Domain Example
t/1 Common UI t("Save"), t("Cancel")
t_auth/1 Authentication t_auth("Log in")
t_equipment/1 Equipment t_equipment("Add Device")
t_admin/1 Admin t_admin("Impersonate User")
t_email/1 Emails t_email("Welcome!")

Interpolation: t("Welcome, %{name}!", name: user.name)

Error messages use the standard gettext/1 function.

current_scope Assign

The @current_scope assign is available in all authenticated views. It's a struct containing:

  • .user — the current User struct (email, id, is_superuser, etc.)
  • .organization — the current Organization (name, slug, settings)
  • .timezone — the user's timezone string (falls back to "UTC")
  • .impersonating? — boolean, true when a superuser is impersonating another user

Access in templates:

<%= @current_scope.user.email %>
<%= @current_scope.organization.name %>

Access in LiveView:

organization = socket.assigns.current_scope.organization
user = socket.assigns.current_scope.user

The layout displays an impersonation banner when @current_scope.impersonating? is true.

CSS & Styling

Tailwind CSS

All styling uses Tailwind utility classes. The project uses DaisyUI as a component library on top of Tailwind.

Dark Mode

Dark mode is supported via the dark: prefix:

<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">

Always provide dark mode variants for backgrounds, text colors, and borders.

Icons

Icons use Heroicons via the .icon component with hero-* names:

<.icon name="hero-plus" />
<.icon name="hero-pencil-square" class="size-5" />
<.icon name="hero-check" class="size-4 text-green-300" />
<.icon name="hero-home-mini" class="h-4 w-4" />

Use the mini variant (hero-*-mini) for small inline icons and the default (outline) for larger UI elements.

LiveView Conventions

Module Boilerplate

defmodule ToweropsWeb.ScheduleLive.Index do
  @moduledoc false
  use ToweropsWeb, :live_view

  alias Towerops.OnCall

  @impl true
  def mount(_params, _session, socket) do
    {:ok, assign(socket, :active_page, "schedules")}
  end

  @impl true
  def handle_params(params, _url, socket) do
    # ...
  end
end
  • Always use @impl true for callback annotations
  • Set active_page in mount/3
  • Use handle_params/3 for loading data based on URL params / live_action

Scoped Routes

Routes are scoped under the organization slug:

/orgs/:org_slug/devices
/orgs/:org_slug/schedules/:id

Use ~p sigil for path generation with @current_scope:

~p"/orgs/#{@current_scope.organization.slug}/devices"