Commit Graph

3 Commits

Author SHA1 Message Date
Wanjohi
15f8d3eb34 fix(core): hold the account rules when two requests arrive together
Three rules here are enforced across a lookup and then a write, and each
was only as good as whatever stopped the two from interleaving. Nothing
did.

The connection cap counted with `select ... for update` over the
connections a user already had. That locks the rows it finds, and when
it finds none it locks nothing — there are no gap locks under read
committed — so several first-time links all counted zero and all
inserted. Six concurrent links against a cap of four produced six. The
count now happens under a lock on the account's own row, which is the
one thing every caller for that account is guaranteed to contend on.

Creating an account from a verified address looked the address up and
then inserted. Two tabs finishing the same sign-in both found nothing,
and the loser got the driver's constraint violation instead of the
account the winner had just made. The unique index is the thing that
actually arbitrates, so the loser now reads back what the winner wrote.
Claiming an address on an older account had the same shape and now gives
the same sentence a screen would have shown a moment earlier.

The tests run each call several times at once against a real database,
because run one at a time all three pass whether or not any of this
exists.
2026-09-05 09:32:16 +03:00
Wanjohi
1b61d2251e fix(core): hold the connection cap on the path a settings screen uses
Connecting a Steam account wrote the row itself, so the limit on how many one
person may connect was enforced on the sign-in path and nowhere else — and
this is the path the settings screen calls, which makes it the one that would
have gone over. It now resolves who is asking and hands over to the single
place the rule lives.

Two things fall out of that. A Steam account already connected to somebody
else is a conflict rather than a silent success returning the other person's
row id, and a Steam id of the wrong shape is refused before a lookup.
2026-09-05 00:03:53 +03:00
Wanjohi
da65cca4f2 feat(core): an account is an email address, and Steam is a connection
Signing in with Steam used to create the account. That made a second Steam
account a second person, and it made losing a Steam account lose everything
attached to it — the boxes, the team, the billing history.

Invert it. A user comes into existence by verifying an email address and
nothing else; a Steam account hangs off a user that already exists, capped at
four. Signing in with Steam resolves an account and refuses when there is
none, so the accounts made before this keep working — they already have the
connection this looks for — while nothing new is created behind a persona.

The cap lives here rather than in the schema because a unique index cannot
count the rows sharing a foreign key. The email column gains a partial unique
index instead, which is the constraint that can be expressed, and the address
is trimmed and lower-cased at the edge so two spellings are not two accounts.
2026-09-05 00:01:15 +03:00