Commit graph

22 commits

Author SHA1 Message Date
Claude
64f5e0b725 sync: allow syncing athletes other than the current session
require_athlete_access rejected any /sync/{athlete_id} request where
athlete_id didn't match the session's own Intervals.icu athlete id.
That's a local, unconditional block that has nothing to do with
whether the session's access token actually has access to that
athlete's data on Intervals.icu -- for example a coach account whose
API key can see multiple athletes' activities.

require_athlete_access is replaced by require_authenticated_session,
which only checks that the request carries a valid session; the
session's own access token is then used to call Intervals.icu for
whichever athlete_id was requested, and Intervals.icu's own API is
what actually authorizes (or 401s/403s) that per-athlete access.

Import status and the 'last synced' default were previously looked
up using the *session's* local athlete id even when browsing a
different athlete_id -- i.e. a coach browsing an athlete's
activities would see their own import history instead of that
athlete's. Both /sync/{athlete_id} and its import-visible handler
now resolve the local athlete row for the athlete_id being browsed
(local_athlete_id) and use that instead. If that athlete has never
logged in locally, everything is treated as not-yet-imported; actually
importing then fails with the existing 'no local athlete for
Intervals.icu athlete' error rather than silently doing the wrong
thing.

process_activity_with_client (session-derived client) is now used
instead of process_activity (which looks up the target athlete's own
stored token) for both single-activity and bulk import, since the
token doing the request is the session's, not necessarily the target
athlete's own.
2026-08-13 11:39:49 +02:00
Claude
4475d156c8 sync: import activities via AJAX with a per-row progress bar
'Alle sichtbaren importieren' used to POST a plain form, which
synchronously imported every visible activity server-side and only
then redirected via a meta refresh -- the browser just sat on a
blank reload for however long the whole batch took, with no
per-activity feedback.

Each activity row now carries data-athlete-id/data-activity-id plus
a progress bar and status line. Both the per-row 'Analysieren'
button and the 'Alle sichtbaren importieren' button now drive a
small JS loop that POSTs to the existing
/sync/{athlete}/activities/{activity}/import endpoint per activity
with Accept: application/json, animates that row's progress bar
while the request is in flight, and shows the resulting crossing
count (or an error) inline -- without a full page reload.

The endpoint itself now branches on the Accept header: JSON for the
AJAX path, the previous meta-refresh HTML for plain form submits
(so it still works without JavaScript). process_activity and
process_activity_with_client now return the crossing count so it
can be reported back to the row.
2026-08-13 11:39:49 +02:00
Jonas Rabenstein
0622edfdcd extend .gitignore 2026-08-13 05:15:43 +02:00
Jonas Rabenstein
da1d36739f web: use Form and Query from axum_extra
Otherwise Vec can not be deserialized.
2026-08-13 05:07:38 +02:00
Claude
84f5f24b6a dedupe HTML escaping into a shared html module
web_escape (main.rs) and escape_html (auth.rs, web.rs) were three
byte-identical copies of the same & < > " ' replacement chain. Moved
into src/html.rs as html::escape and used from all three call sites.
2026-08-13 04:12:13 +02:00
Claude
1a0ee71e6b sessions: expire after 1 day
Sessions previously had no expiry at all: a county_session cookie
was valid forever until an explicit /logout. Both session lookups
now reject rows older than 1 day (created_at-based, not sliding),
and db::delete_expired_sessions prunes expired rows so the table
doesn't grow unbounded; it's called during the existing OAuth-start
housekeeping alongside the oauth_states cleanup.
2026-08-13 04:12:13 +02:00
Claude
1473e726f3 webhook: compare secret in constant time
payload.secret != expected_secret short-circuits on the first
differing byte, which leaks timing information about how many
leading bytes of a guess are correct. Use subtle::ConstantTimeEq
instead.
2026-08-13 04:12:13 +02:00
Claude
85369069c3 remove remaining dead code left over from /dev endpoints
db::ensure_dev_athlete was never called (main.rs had its own,
inferior copy, removed in the previous commit). Config::dev_sync_days
and Config::require_api_key were only used by the now-removed
/dev/sync handlers.
2026-08-13 04:12:13 +02:00
Claude
1c3fd3d20f remove /dev/sync debug endpoints
/dev/sync/{api_key} and /dev/sync/{api_key}/{athlete_id} took a
personal Intervals.icu API key as a URL path segment, which leaks
into server/proxy access logs (and tower_http's TraceLayer spans),
browser history, and Referer headers. There's no legitimate reason
to keep a credential-in-URL debug backdoor around outside local
development, so it's gone along with its handlers.

This also removes main.rs's local ensure_dev_athlete, which
duplicated db::ensure_dev_athlete but used a racy check-then-insert
instead of an upsert (TOCTOU on concurrent dev syncs for the same
athlete).
2026-08-13 04:12:13 +02:00
Claude
5a49624435 perf: batch county-crossing detection into one query per activity
geo::crossings_between issued one DB round trip per consecutive GPS
sample pair (tens of thousands per multi-hour activity), even though
almost every segment stays inside a single county and produces no
crossing at all.

geo::crossings_for_track batches all segments of a track into a
single query via UNNEST ... WITH ORDINALITY. The per-segment geometry
logic (endpoint county lookup via the GiST index, boundary
intersection, ST_LineLocatePoint for the fractional position) is
unchanged, it now just runs once per activity instead of once per
segment.
2026-08-13 04:12:13 +02:00
Jonas Rabenstein
2ab16a68b7 merge 2026-08-13 03:54:14 +02:00
Jonas Rabenstein
961d6665b5 fmt 2026-08-13 03:04:13 +02:00
Jonas Rabenstein
859603399a sync 2026-08-13 02:43:17 +02:00
Jonas Rabenstein
3cfd5bdd6c add map and clicks 2026-08-12 17:57:25 +02:00
Jonas Rabenstein
45bcc13788 state 2026-08-12 17:49:27 +02:00
Jonas Rabenstein
eda8e76a20 fixups 2026-08-12 14:24:50 +02:00
Jonas Rabenstein
89091a03d3 dev api key 2026-08-12 14:02:12 +02:00
Jonas Rabenstein
06c837b244 fixes 2026-08-12 13:48:51 +02:00
Jonas Rabenstein
189803d24d fixup! fixup! fix nix develop psql setup 2026-08-12 13:38:35 +02:00
Jonas Rabenstein
f4405db550 fixup! fix nix develop psql setup 2026-08-12 13:37:37 +02:00
Jonas Rabenstein
53329182bd fix nix develop psql setup 2026-08-12 13:31:44 +02:00
Jonas Rabenstein
a9d22e8312 chatgpt 2026-08-12 13:24:01 +02:00