- 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
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
.exfiles contain the LiveView module (mount, handle_params, handle_event, etc.).html.heexfiles 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
:navigateon 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 currentUserstruct (email, id,is_superuser, etc.).organization— the currentOrganization(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 truefor callback annotations - Set
active_pageinmount/3 - Use
handle_params/3for 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"