chore: add contributor guide and harden cache/csrf helpers

This commit is contained in:
Graham McIntire 2025-10-15 11:53:21 -05:00
parent 751b8a5f1b
commit 1b638cf739
No known key found for this signature in database
3 changed files with 37 additions and 23 deletions

16
AGENTS.md Normal file
View file

@ -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 `<feature>_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.

View file

@ -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

View file

@ -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