- 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
227 lines
6 KiB
Markdown
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"
|
|
```
|