Two P3 multi-user hardening fixes from the 2026-06-13 full-eval.
Login timing oracle: both login paths (the UI server action and
POST /api/auth) returned immediately on an unknown email but ran
bcrypt.compare when the email matched a user, so response latency
revealed which emails have accounts. New verifyPasswordOrDummy() in
lib/auth runs bcrypt against a fixed dummy hash when there is no user,
so every attempt spends exactly one bcrypt; the two error branches in
each route collapse into one.
exerciseId ownership: exercises are per-user, but the workout
create / PATCH (set-replace) / add-sets and CSV import-save routes wrote
SetLogs from a client-supplied exerciseId with no ownership check —
letting a user attach another user's exercise to their own workout,
which leaks that exercise's name/notes on fetch and wires up a
cross-user onDelete: Cascade link. All four now reject unowned ids with
400 via the shared lib/exerciseOwnership helper; the pre-existing inline
checks in both programs routes are refactored onto the same helper.
App-code only — no schema, no API contract change, no data migration.