Commit graph

19 commits

Author SHA1 Message Date
ca842e3add
feat(admin): beacon monitor reassign, token regen, assigned_by display, user monitor management
Uncommitted work from previous session:
- Add list_users_select/0 helper for admin select dropdowns
- Add regenerate_token/1 context function
- Preload assigned_by association on monitor queries
- Add admin nav link to beacon monitors
- Fix JS event targets (remove redundant target: @myself)
- Add reassign user form to admin monitor show page
- Add regenerate token button to admin monitor show page
- Show assigned_by admin on monitor detail page
- Add beacon monitors section to user management edit page
- Add unassign monitor capability from user management
- Add full test coverage for all new functionality
- Fix pre-existing /account route warning (redirect → /users/settings)
2026-07-22 16:27:21 -05:00
Graham McInitre
49ade78766 fix: wire pending_edits_query as data_provider for contact edit review table
The LiveTable on /admin/contact-edits used the bare ContactEdit schema
as its data source, which caused three symptoms:
- '0 pending' counter but stale approved/rejected edits still visible
- Blank contact/submitted-by cells (select_columns stripped preloaded
  associations, cell renderers received flat maps with no :contact/:user)
- Approve/reject didn't remove the row from the table

Fix: assign {Radio, :pending_edits_query, []} as the data_provider in
mount so handle_params threads it to stream_resources. The query variant
of list_resources preserves preloaded associations and includes the
WHERE status = :pending filter.

Added two tests that verify the table rendering and edit removal.
2026-07-22 08:54:46 -05:00
Graham McInitre
255c99cb36 fix: unwrap Repo.transaction return values for ecto_sql 3.14, fix sandbox deadlock in rover_planning tests
- accounts.ex: Repo.transaction in ecto_sql 3.14 wraps all return values in {:ok, ...},
  causing callers to receive {:ok, {:error, ...}} and {:ok, {:ok, ...}} instead of
  direct tuples. Unwrap in update_user_email and update_user_and_delete_all_tokens.
- rover_planning_test.exs: switch to async: false + Oban.Testing manual mode so
  RoverPathProfileWorker jobs don't deadlock on sandbox connections inside
  Repo.transaction. Introduced create_and_complete_mission!/2 and run_backfill!/1
  helpers that separate transaction lifecycle from worker execution.
2026-07-21 10:24:06 -05:00
Graham McInitre
15c4ba11a1 fix: Repo.transact -> Repo.transaction (crash on email change + token revocation) 2026-07-16 07:26:49 -05:00
5f154a20f3
refactor: apply functional programming patterns to accounts + map_live
- accounts.ex: replace if with with-chain in get_user_by_email_and_password
  for explicit pattern matching on User struct + valid_password? result.
  Collapse nested case into with in create_api_token.

- map_live.ex: extract connected? PubSub subscription block from mount into
  named subscribe_if_connected/1 function (cleaner than inline if with _ =).
  Deduplicate identical select_time + set_selected_time handlers into shared
  handle_set_time/2 helper. Extract toggle_grid + toggle_radar into shared
  handle_toggle/3 helper (functions-as-values pattern for assign key).
2026-07-01 17:56:06 -05:00
6bd4361ed1
updates 2026-06-16 12:38:08 -05:00
9db2cd20f5
fix: resolve 7 pre-existing test failures and ScoreCache DateTime bug
- CacheTest: fix sweep timing race by using negative TTL (-1)
  instead of positive TTL (1) for already-expired entries
- ScoreCache: replace ETS match-spec DateTime comparisons with
  :ets.foldl + DateTime.compare — DateTime structs are maps in
  Elixir >= 1.15 and ETS can't compare maps with :< / :> guards
- Accounts: drop unsupported returning: true on delete_all,
  return [] for expired tokens list
- Backtest: catch ArgumentError from String.to_existing_atom
  for unknown feature names, preserving the helpful Mix.Error
- ContactLive IndexTest: invalidate monthly_bars cache before
  assertion so test data is visible
- RoverLocationsLive MapTest: invalidate cached points before
  assertion
- StatusLiveTest: add DB cleanup in setup to reduce test
  interference; parameterize NARR candidate coordinates
2026-06-10 13:50:24 -05:00
d67186176f
fix: resolve all dialyzer type errors across project (46→0 project errors)
Type spec fixes:
- duct_usable_* return boolean→float (delegate to duct_usable_for_band)
- sanitize/1 spec includes :unicode error tuples
- match_delete/1 broadened from :ets.match_spec()
- telemetry_event local type replaces :telemetry.event/0
- wgrib2 parse_lon_val_segment corrected to tuple spec
- preloaded Ecto assoc types in get_mission/get_contact! specs

Unmatched returns:
- _ = prefix on Task.start, Oban.insert, Repo.query!, PubSub.subscribe,
  :ets.new, and if-expression returns across 18 files

Pattern match fixes:
- markdown: restructure acc!=[] guard as direct pattern match
- path_compute/pskr/skewt_location_resolver: remove dead clauses
- calibrate.aprs_144: remove unreachable format_float catch-all
- unused.ex: suppress MapSet.union no_opaque

Also: remove unused unicode_util_compat from mix.lock
2026-06-08 17:51:13 -05:00
15f4175cce
perf: fix remaining medium/low severity inefficiencies
- Radio.group_reciprocals: replace O(n²) nested Enum.filter with O(n)
  Enum.group_by using a hash key (sorted station pair + band + hour)
- contact_live/index.ex: cache monthly bars Repo.all(Contact) query
  with 60s TTL
- ScoreCache.valid_times: replace :ets.select with :ets.foldl to
  avoid intermediate list allocation
- Accounts.list_users: add limit(100) to unbounded query
- Accounts.backfill_missing_home_qth: batch individual Oban.insert
  calls into single Oban.insert_all
2026-06-01 15:39:25 -05:00
cc3dc41a6b
Fix all medium findings and split contact_live_test.exs
- Fix N+1 queries in radio.ex (preload :user)
- Add encryption_salt to session options
- Consolidate token deletion to single query
- Wrap gunzip in try/rescue for corrupt files
- Add Cache.match_delete/1 to encapsulate ETS access
- Precompute sec_i_factor_ref as module attribute
- Guard EXLA.Backend config to dev/test only
- Split 3505-line contact_live_test.exs into 4 files
- Replace Process.sleep with :sys.get_state synchronization
- Replace Process.alive? with DOM output assertions
- Clarify router.ex comment for non-live_session routes
- Update findings.md to reflect fixes
2026-05-29 15:53:04 -05:00
a3b97f9e98
fix(security): mass-assignment on owner edit + 3 minor hardening fixes
- Radio.normalize_proposed/1 now Map.take/2 the allowed-edit keys
  before normalization. Owner / admin / pending-edit submit paths all
  funnel through this boundary, so a crafted form can no longer
  mass-assign user_id, flagged_invalid, inserted_at, etc. on a
  contact via String.to_existing_atom -> Ecto.Changeset.change.
- Accounts.revoke_api_token/2 rescues Ecto.Query.CastError and
  returns {:error, :not_found} so a malformed UUID in
  DELETE /api/v1/me/api-tokens/:id renders the API's clean 404
  problem+json instead of a 500.
- ProfilesFile.read_etf decodes with :erlang.binary_to_term(bin, [:safe]).
  Defense-in-depth against tampered on-disk profiles (atom-table
  exhaustion via untrusted ETF).
- Regression tests: contact_edit_test.exs covers the rejected
  mass-assignment fields; accounts_api_token_test.exs covers the
  malformed-UUID path.
2026-05-11 19:04:16 -05:00
c6d2c48264
feat: secure /api/v1 REST API for regular-user actions
Adds bearer-token authenticated REST API at /api/v1 covering every
action a non-admin user can perform on the website: contact + beacon
submission, beacon-monitor management, propagation queries, profile
read/update, and self-service API token issuance/revocation.

Security: SHA-256-hashed bearer tokens (mwp_ prefix, plaintext shown
once at creation), RFC 9457 problem+json error responses, RFC 9651
RateLimit-* headers backed by an ETS bucket (600/min per token,
60/min per anonymous IP, 30/min on /auth/tokens), private-contact
filtering by viewer.

Docs at docs/api/README.md (prose reference) and docs/api/openapi.yaml
(OpenAPI 3.1 spec covering every endpoint, response, and schema).

Tests: 124 new tests across schema, plug, error renderer, rate
limiter, fallback, and every controller. 16/17 API modules at 100%
line coverage; FallbackController at 87.5% (one defmodule line, an
Erlang-cover artifact for action_fallback-only modules).
2026-05-09 08:59:54 -05:00
9796f06ba2
feat(accounts): auto-prefill home QTH from QRZ on register + backfill on boot 2026-04-25 17:03:01 -05:00
771299c951
feat(accounts): add Home QTH section to user settings page 2026-04-25 16:51:54 -05:00
7a7b30f7bf
chore(specs): add @spec to public API in accounts/release/backtest 2026-04-21 13:32:42 -05:00
00aad3cb98
feat(auth): self-service password reset
Add a "Forgot your password?" flow off the login page. A 24-hour
reset_password token is emailed on request, the landing page lets the
user pick a new password, and all other tokens for the user are
revoked on success.

The request endpoint returns the same flash regardless of whether the
email matches a user so that attackers can't enumerate accounts.
2026-04-18 08:42:35 -05:00
fc245367e3
User profiles at /u/:callsign, flexible band input, assorted UX cleanup
Profile page:
  * New MicrowavepropWeb.UserProfileLive at /u/:callsign is a public
    page showing a user's contacts and beacons. Resolves case-
    insensitively so /u/w5isp and /u/W5ISP are the same thing; unknown
    callsigns redirect to /. Uses daisyUI card / stats / table
    components with an avatar-placeholder initial and hero icons.
  * Accounts.get_user_by_callsign/1 (case-insensitive) plus
    Radio.list_contacts_for_user/1 and Beacons.list_beacons_for_user/1
    back the page. Beacons list includes both approved and pending so
    owners see their drafts.
  * The top nav bar and the three LiveView sidebars (MapLive,
    WeatherMapLive, ContactMapLive) now render the logged-in callsign
    as a navigate link to /u/:callsign instead of a static label.
  * Nine new tests cover the lookup, the LiveView render, and the
    ownership-scoped queries.

Flexible band input:
  * New Microwaveprop.Radio.BandResolver module converts any of:
    ADIF wavelength labels ("33cm", "1.25cm", "6mm", case/whitespace
    insensitive), numeric frequency strings ("903.100", "10368.000"),
    and canonical MHz integers into the one of the site's known bands.
    Returns the nearest allowed band for numeric inputs >= 900 MHz,
    nil otherwise.
  * 902 MHz is added to Contact.@allowed_bands, ContactEdit.@allowed_bands,
    AdifImport.@allowed_bands, and the BandResolver list so "33cm"
    round-trips end-to-end.
  * AdifImport and CsvImport now delegate band resolution to
    BandResolver, and Radio.create_contact/2 normalizes the :band attr
    on the way in so the manual form and any API callers benefit too.
    CsvImport's "invalid band" tests previously used 99999 MHz which
    the new resolver snaps to the nearest allowed band; swapped to
    "notaband" which is truly unresolvable.

Contacts and beacons list UX:
  * Remove the "Submitted" column from /contacts — it duplicated info
    already visible on the detail page and was pushing the real
    columns off narrow viewports. submitted_cell/1 and its three
    column-specific tests go with it.
  * Hide the Lat / Lon columns from /beacons — six decimal places of
    coordinates weren't useful next to the grid square and took a
    disproportionate amount of row width.
2026-04-12 16:13:08 -05:00
ee9275e0b9 Add /beacons CRUD and /users admin page
Beacons:
- Scaffolded with phx.gen.live then reworked so reads are public
  and mutations go through a live_session gated by the new
  :require_admin on_mount hook in UserAuth
- Beacon schema stores frequency (MHz), callsign, grid, lat/lon,
  power (W), and height above ground (m); grid auto-derives from
  lat/lon when left blank via Maidenhead.from_latlon
- Adds Maidenhead.from_latlon/3 so we can compute grids locally
  instead of hitting an external API

Users admin page:
- /users and /users/:id/edit (admin-only) for listing, editing
  (callsign/name/email/is_admin), and deleting other users
- Adds Accounts.list_users, admin_update_user, delete_user, and
  a dedicated admin_changeset on the User schema
- Nav gains a "Users" link for admins and a "Beacons" link for
  everyone
2026-04-08 12:01:45 -05:00
1d86e287b2 Add password auth with callsign + email confirmation
Generated Accounts context, User schema, and controllers via
phx.gen.auth. Adapted it for password-only login with required
email confirmation:

- Users have callsign (unique, uppercased), name, email, password
- Registration form fields: callsign, name, email, password, confirm
- Magic-link login path removed; login is email + password only
- After register, a confirmation email is sent and login is blocked
  until the account is confirmed via the token URL
- Confirmation link logs the user in on first use
- SMTP2GO configured as the outgoing mailer in k8s prod
2026-04-08 10:21:40 -05:00