From 1b638cf73985eed147482232e14c6cce87f3c1a0 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Wed, 15 Oct 2025 11:53:21 -0500 Subject: [PATCH] chore: add contributor guide and harden cache/csrf helpers --- AGENTS.md | 16 ++++++++++++++++ lib/aprsme/cache.ex | 14 ++++++++++---- lib/aprsme_web/plugs/api_csrf.ex | 30 +++++++++++------------------- 3 files changed, 37 insertions(+), 23 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..975e253 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,16 @@ +# Repository Guidelines + +## Project Structure & Module Organization +Elixir application code lives in `lib/`: `lib/aprsme` holds domain contexts, `lib/aprsme_web` contains LiveView, controllers, and templates, and `lib/mix` exposes project-specific Mix tasks. Front-end assets are in `assets/` (split into `js/`, `css/`, and component bundles under `assets/`). Database artifacts sit in `priv/repo/` with migrations and seeds, while `config/` provides environment-specific settings. Tests reside in `test/` with shared helpers in `test/support/`. Supporting tooling (Docker, scripts, CI manifests) lives under `scripts/`, `rel/`, and `k8s/`. + +## Build, Test, and Development Commands +Run `mix setup` once to fetch dependencies and prepare the Postgres database. Use `mix phx.server` (or `iex -S mix phx.server`) for local development. Execute `mix test` for the full ExUnit suite; `mix test.watch` keeps rerunning on file changes. For quality gates, `mix credo --strict`, `mix dialyzer`, and `mix sobelow` mirror CI checks. Before releases or deployments, build static assets with `MIX_ENV=prod mix assets.deploy` and create a release via `mix release`. + +## Coding Style & Naming Conventions +Follow idiomatic Elixir with 2-space indentation and modules in `PascalCase`. The top-level `.formatter.exs` enforces a 120-character line limit, imports Phoenix HTML formatting, and loads the Styler plugin—always run `mix format` before committing. Keep function names in `snake_case`, LiveView hooks under `assets/js` in `camelCase`, and CSS utilities organized by Tailwind conventions. Prefer contexts for new domain modules and colocate schemas with their contexts inside `lib/aprsme`. + +## Testing Guidelines +The project uses ExUnit with database sandboxing; aliases ensure `ecto.create` and `ecto.migrate` run before tests. Property-based tests live alongside unit tests using `StreamData`, and browser flows use Wallaby helpers in `test/support`. Name test files `_test.exs` and ensure async-friendly tests opt in with `use ExUnit.Case, async: true`. Check coverage locally with `mix coveralls.html` and review the generated report in `cover/`. + +## Commit & Pull Request Guidelines +Commit history favors short, imperative subjects with optional Conventional Commit prefixes (`fix:`, `refactor:`). Keep each commit focused, formatted, and passing the suite. PRs should outline the change, reference related issues, note DB or config impacts, and include screenshots or logs for UI-facing updates. Confirm that formatting, tests, and static analysis commands above complete successfully before requesting review. diff --git a/lib/aprsme/cache.ex b/lib/aprsme/cache.ex index a6ab023..edda431 100644 --- a/lib/aprsme/cache.ex +++ b/lib/aprsme/cache.ex @@ -20,7 +20,7 @@ defmodule Aprsme.Cache do """ def put(cache_name, key, value, opts \\ []) do if using_redis?() do - # Convert TTL from milliseconds to seconds for Redis + # Convert TTL from milliseconds to seconds for Redis, preserving sub-second values opts = convert_ttl_to_seconds(opts) Aprsme.RedisCache.put(cache_name, key, value, opts) else @@ -68,7 +68,10 @@ defmodule Aprsme.Cache do if using_redis?() do Aprsme.RedisCache.exists?(cache_name, key) else - Cachex.exists?(cache_name, key) + case Cachex.exists?(cache_name, key) do + {:ok, exists?} -> exists? + {:error, _reason} -> false + end end end @@ -94,9 +97,12 @@ defmodule Aprsme.Cache do nil -> opts + ttl_ms when is_integer(ttl_ms) and ttl_ms > 0 -> + ttl_seconds = Integer.ceil_div(ttl_ms, 1000) |> max(1) + Keyword.put(opts, :ttl, ttl_seconds) + ttl_ms when is_integer(ttl_ms) -> - # Convert milliseconds to seconds - Keyword.put(opts, :ttl, div(ttl_ms, 1000)) + Keyword.put(opts, :ttl, ttl_ms) _ -> opts diff --git a/lib/aprsme_web/plugs/api_csrf.ex b/lib/aprsme_web/plugs/api_csrf.ex index b0306a9..2c38aaa 100644 --- a/lib/aprsme_web/plugs/api_csrf.ex +++ b/lib/aprsme_web/plugs/api_csrf.ex @@ -45,28 +45,20 @@ defmodule AprsmeWeb.Plugs.ApiCSRF do end defp validate_csrf_token(conn, token) do - # Get session token for comparison - session_token = get_session(conn, "_csrf_token") + case get_session(conn, "_csrf_token") do + nil -> + reject_request(conn) - # Use Phoenix.Controller CSRF token validation - if session_token && valid_csrf_token?(conn, token) do - conn - else - reject_request(conn) - end - end - - defp valid_csrf_token?(conn, token) do - session_token = get_session(conn, "_csrf_token") - - if session_token do - # Use a simple comparison for now - Phoenix.Token.verify(conn, "_csrf_token", token, max_age: 86_400) == {:ok, session_token} - else - false + session_token -> + if Plug.CSRFProtection.verify_csrf_token(token, session_token) do + conn + else + reject_request(conn) + end end rescue - _ -> false + _ -> + reject_request(conn) end defp reject_request(conn) do