Accounts people can create, and photos on a server with no bucket
A tenant had exactly the users somebody had created with a command on the
server. That is not a missing screen: a shop with an owner and four staff
either shared one password or raised a ticket per person, and a phone app
for the shop floor could not exist while there was one account to sign in
as.
Registration is by invitation, never open signup - the same line already
drawn around creating a company. The code carries the address and the role
and the request carries only a password, so a code that gets forwarded
cannot become somebody else's account, and a staff invitation cannot be
redeemed as an owner. Single use lives in the UPDATE and the account is
created in the same transaction.
Deactivating a member revokes their sessions in that transaction too. An
access token lives twelve hours, so without it "remove their access"
removed it sometime tomorrow. The session list and revoke that go with it
are the benefit of opaque tokens the product had been paying for and never
collecting: nothing could say what was signed in, let alone stop one.
Face images now work on a deployment with no object storage, which was
every local install and every self-hosted site - the arrivals feed said
"not storing customer photos" for every customer forever, on the screen
whose whole job is to show a face. Bounded to one row per visitor, so it
grows with the customer base and not with footfall; the bucket stays
primary wherever one exists.
Image.auth says whether a URL needs the session, because a browser img
cannot load one that does, a mobile image view can, and a webview can do
neither - the desktop client resolves those to a data URI in Go.
Found by running it, not by tests:
* UPDATE ... RETURNING gives the value AFTER the update, so the prune
read back empty keys, deleted nothing, and the table grew with
footfall exactly as if it were not there. The fake agreed with either
version; only the live Postgres test caught it.
* Trusting only the auth flag broke every shop card, because Sites.jsx
rebuilt a partial snapshot object and dropped it. A relative URL is
now sufficient on its own.
* ago() renders a future time as "just now", so a code valid for a week
read "expires just now".
Verified live against real Postgres: invite, preview, escalation refused,
register into a session, replay 404, staff forbidden, device revoked and
401 at once, last owner refused, and a 92,405-byte camera JPEG stored,
served to its owner, 401 with no session, 404 to another tenant, and
rendered in a browser.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HViLj9gYNRtSr7YVZmW5sn
This commit is contained in:
@@ -1,24 +1,45 @@
|
||||
import { useAuthedImage } from '../hooks.js'
|
||||
|
||||
// One camera picture, however this deployment stores them.
|
||||
// One picture from the API, however this deployment stores them.
|
||||
//
|
||||
// Two shapes arrive here and they need different handling, which is exactly
|
||||
// why it is one component rather than an <img> repeated on each screen:
|
||||
//
|
||||
// * An ABSOLUTE url is a presigned link to object storage. It carries its
|
||||
// own signature, so a plain <img src> loads it.
|
||||
// * A RELATIVE url is served by this server from its own database, for a
|
||||
// deployment with no bucket. An <img> cannot send an Authorization header,
|
||||
// so it has to be fetched with the session and handed over as an object
|
||||
// URL. Minting an unauthenticated link instead would put a photograph of
|
||||
// somebody's shop floor behind no session at all, which is the thing this
|
||||
// path exists to avoid.
|
||||
export default function Shot({ url, alt }) {
|
||||
const local = typeof url === 'string' && url.startsWith('/')
|
||||
// * A presigned link to object storage carries its own signature, so a plain
|
||||
// <img src> loads it.
|
||||
// * A picture this server holds itself - for a deployment with no bucket -
|
||||
// is served from an endpoint that requires the session. An <img> cannot
|
||||
// send an Authorization header, so it has to be fetched and handed over as
|
||||
// an object URL. Minting an unauthenticated link instead would put a
|
||||
// photograph of somebody's shop floor, or of a customer, behind no session
|
||||
// at all, which is the thing that path exists to avoid.
|
||||
//
|
||||
// Which one it is comes from the API's own `auth` flag, not from the shape of
|
||||
// the URL. Guessing by whether it starts with "/" is right today and stops
|
||||
// being right the first time object storage is served from this same host -
|
||||
// and the failure then is a photograph that silently will not load.
|
||||
export default function Shot({ image, url, alt }) {
|
||||
// `image` is the whole object from the API; `url` is the older call shape,
|
||||
// kept working so a screen that has not been updated still renders. The
|
||||
// fallback heuristic applies only when nothing told us.
|
||||
const src0 = image ? image.url : url
|
||||
// Either signal is enough, and that is not belt-and-braces. A RELATIVE url is
|
||||
// served by this server and always needs the session - there is no such thing
|
||||
// as a public one - so it is sufficient on its own, and a caller that rebuilds
|
||||
// an image object and loses `auth` cannot turn a working picture into a broken
|
||||
// one. (It did exactly that once: Sites.jsx returned `{url, at}` from its
|
||||
// snapshot picker, the flag went missing, and every shop card showed a broken
|
||||
// image.) The FLAG is what adds the case the URL cannot express: an absolute
|
||||
// link that still needs a bearer, which happens the first time object storage
|
||||
// is served from this same host.
|
||||
const needsAuth =
|
||||
(image && !!image.auth) ||
|
||||
(typeof src0 === 'string' && src0.startsWith('/'))
|
||||
|
||||
// Hooks cannot be called conditionally, so this always runs and simply has
|
||||
// nothing to do when the URL is already usable.
|
||||
const fetched = useAuthedImage(local ? url : null)
|
||||
const src = local ? fetched : url
|
||||
const fetched = useAuthedImage(needsAuth ? src0 : null)
|
||||
const src = needsAuth ? fetched : src0
|
||||
if (!src) return null
|
||||
return <img src={src} alt={alt} loading="lazy" />
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user