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

227 lines
6 KiB
Markdown

# 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:
```elixir
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:
```heex
<.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:
```heex
<.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:
```elixir
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()`:
```elixir
socket
|> put_flash(:info, t("Schedule created successfully."))
|> push_navigate(to: ~p"/schedules")
```
```elixir
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:
```heex
<%= @current_scope.user.email %>
<%= @current_scope.organization.name %>
```
Access in LiveView:
```elixir
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:
```heex
<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:
```heex
<.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
```elixir
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`:
```elixir
~p"/orgs/#{@current_scope.organization.slug}/devices"
```