Add guest links for temporary shared access
Handing a dog sitter the ability to log a pee meant handing them the account password: permanent, total control, revocable only by changing it. Settings → Guest access now mints a URL that does the one thing instead. A link is a session, not an account. Opening /guest/<token> inserts an ordinary session row against the owner's user_id, tagged with the link it came from, so every data path downstream — sync, photos, the profile — stays scoped by user_id exactly as before and needed no changes at all. Only the capability checks differ by role, which is what kept this from touching the sync contract. Redemption is a plain GET so tapping the link in a message works, and the 303 to / leaves the token out of the address bar, bookmarks and the PWA start URL. What a guest cannot change is enforced in the upsert, not in the UI. The WHERE clause gains a logged_by_share test: an owner (empty share id) may change anything, a guest only rows carrying their own link's id. A sitter can fix up their own entries and cannot rewrite or delete one of the owner's, including everything logged before this existed, since those rows carry the empty id too. Deletes come along free, being tombstones. The test is on the link id rather than its label because two links can easily both be "Sitter", and the id is also why /api/me hands the guest its share id: the client needs it to know what to grey out. The exercise library is the owner's on the same reasoning — a guest trains against it but the server drops any exercise a guest sends. Attribution is stamped from the session on insert and left out of DO UPDATE SET, so it is decided once by whoever logged the event and survives every later edit. It never comes off the wire, so it cannot be forged — a guest re-POSTs the owner's whole event list on every sync, but those rows already exist and keep their stored values. Expiry is a date the owner picks; the link dies at the end of that day in their own timezone, which the client computes because the server has no way to know it. Sessions are capped at the link's own end, and every request re-checks the link is live rather than trusting the session row, so revoking kicks a guest out on their next request instead of whenever their session happens to lapse. Only the token hash is stored, as with session tokens, so the URL is shown once at creation and cannot be read back. The client side follows from that. A guest opening someone else's entry gets the edit dialog read-only rather than a form that would silently discard what they typed, and mergeSynced takes the server's copy for anything they may not change — otherwise a refused write would sit in their cache forever showing an edit that never happened. An ended link wipes their cached copy of someone else's history and says so, rather than offering a sign-in form they have no password for.
This commit is contained in:
@@ -49,7 +49,7 @@ puppy-tracker/
|
||||
│ ├── go.mod
|
||||
│ ├── go.sum
|
||||
│ ├── main.go # SQLite store, LWW sync, static file serving
|
||||
│ ├── auth.go # accounts, sessions, invite-gated registration
|
||||
│ ├── auth.go # accounts, sessions, invite-gated registration, guest links
|
||||
│ ├── reminders.go # reminder rules, the evaluation loop, push subscriptions
|
||||
│ ├── webpush.go # VAPID + RFC 8291/8188 message encryption
|
||||
│ ├── pedigree.go # SKK lookup, background crawl, per-dog cache
|
||||
@@ -100,6 +100,54 @@ events, profile and photos.
|
||||
when you pass `-secure-cookies` (enable it behind a TLS proxy), so passwords
|
||||
aren't sent in the clear.
|
||||
|
||||
## Guest links
|
||||
|
||||
A dog sitter needs to log a pee; they do not need your password. **Settings →
|
||||
Guest access** mints a link that does exactly the first thing.
|
||||
|
||||
- **It is a session, not an account.** Opening `/guest/<token>` mints an ordinary
|
||||
session row against *your* `user_id`, tagged with the link it came from. Every
|
||||
data path downstream — sync, photos, the profile — is scoped by `user_id` as
|
||||
before, so a guest simply is you as far as the data is concerned. Only the
|
||||
capability checks differ.
|
||||
- **What a guest gets.** The whole app to read: every panel, every chart, all
|
||||
history. They can log new events freely, and edit or delete the ones they
|
||||
logged themselves. What they don't get is anything under Settings that belongs
|
||||
to the account — the puppy profile, the pedigree id, reminders, other guest
|
||||
links, and deleting the account. Those routes are behind `requireOwner` and
|
||||
403 for a guest; the client hides the matching UI. The two device-local
|
||||
preferences (dark mode, confetti) stay, since they are the guest's own browser
|
||||
and not your account.
|
||||
- **A guest cannot change your logs.** The upsert in `Store.sync` only lets a
|
||||
guest update rows carrying their own link's id, so a sitter can fix up their
|
||||
own entries and cannot rewrite or delete a single one of yours — including
|
||||
everything logged before guest links existed. The check is on the link *id*,
|
||||
not its label, because two links can easily both be called "Sitter". You keep
|
||||
full control either way and can edit anything on your own account, theirs
|
||||
included. The exercise library is the owner's for the same reason: a guest
|
||||
logs training sessions against it but the server drops any exercise a guest
|
||||
sends. In the app a guest opening someone else's entry gets a read-only view
|
||||
rather than a form that would throw away what they typed.
|
||||
- **It expires, and you can revoke it.** You pick the last day the link should
|
||||
work; it stops at the end of that day in your own timezone. Sessions minted
|
||||
from a link are capped at the link's own expiry, so one can never outlive it,
|
||||
and every request re-checks that the link is still live — so revoking kicks
|
||||
whoever is already using it out on their very next request, not whenever their
|
||||
session happens to lapse. Revoking also deletes those session rows outright.
|
||||
- **The URL is shown once.** Only a hash of the token is stored, exactly as with
|
||||
session tokens, so a leaked database yields no working links — and the app
|
||||
cannot show you the URL again later. Settings lists each live link by label,
|
||||
expiry and when it was last used.
|
||||
- **Events say who logged them.** An event created through a link carries that
|
||||
link's label (badged in the History log) and its id (which is what authorises
|
||||
changes). The server stamps both from the session on insert and never reads
|
||||
them off the wire, so neither can be forged; both are left out of the update
|
||||
path, so a later edit by anyone keeps the original attribution.
|
||||
- **A link is a bearer token — serve over HTTPS.** Anyone holding the URL can
|
||||
redeem it until it expires. Send it over something private, and run behind TLS
|
||||
(`-secure-cookies`) as above. When a link ends, the guest's browser drops its
|
||||
cached copy of your history rather than keeping it around.
|
||||
|
||||
## Reminders
|
||||
|
||||
Opt-in push notifications for the two things that are easy to lose track of:
|
||||
|
||||
Reference in New Issue
Block a user