fix(guests): drop a stored identity when a spent invite names someone else
A guest coming back through their own already-redeemed link is the ordinary #1265 case, and the identity the device holds is theirs. The same link opened on a shared device that holds another guest's identity is not: the redemption 409s, ensureIdentity() falls through to the stored identity, and the visitor's likes are filed under the previous person. The two cases were indistinguishable client-side, so the 409/410 body now carries the invite's guest_id. On a mismatch the stored identity is cleared and the visitor is asked who they are. A response without guest_id keeps the previous behaviour.
This commit is contained in:
@@ -350,8 +350,12 @@ router.post('/:slug/guest/redeem', verifyGalleryAccess, async (req, res) => {
|
||||
.where({ token: inviteToken, event_id: event.id })
|
||||
.first();
|
||||
if (!invite) return { error: 'not_found' };
|
||||
if (invite.revoked_at) return { error: 'revoked' };
|
||||
if (invite.redeemed_at) return { error: 'already_redeemed' };
|
||||
// Spent and revoked invites name their guest, so the client can tell
|
||||
// "this guest came back through their own link" (keep the identity the
|
||||
// device holds) from "someone else's link on a device that holds
|
||||
// another guest's identity" (drop it) — see GuestIdentityContext.
|
||||
if (invite.revoked_at) return { error: 'revoked', guestId: invite.guest_id };
|
||||
if (invite.redeemed_at) return { error: 'already_redeemed', guestId: invite.guest_id };
|
||||
|
||||
const guest = await trx('gallery_guests')
|
||||
.where({ id: invite.guest_id, is_deleted: false })
|
||||
@@ -380,7 +384,9 @@ router.post('/:slug/guest/redeem', verifyGalleryAccess, async (req, res) => {
|
||||
already_redeemed: 409,
|
||||
guest_missing: 404,
|
||||
};
|
||||
return res.status(statusMap[result.error] || 400).json({ error: result.error });
|
||||
const body = { error: result.error };
|
||||
if (result.guestId != null) body.guest_id = Number(result.guestId);
|
||||
return res.status(statusMap[result.error] || 400).json(body);
|
||||
}
|
||||
|
||||
const token = signGuestToken({
|
||||
|
||||
@@ -187,7 +187,27 @@ export const GuestIdentityProvider: React.FC<GuestIdentityProviderProps> = ({
|
||||
const newUrl = window.location.pathname + (newSearch ? `?${newSearch}` : '') + window.location.hash;
|
||||
window.history.replaceState({}, '', newUrl);
|
||||
} catch (error) {
|
||||
// Silently fail invalid invites; user will fall back to normal prompt.
|
||||
// A spent (409) or revoked (410) invite is the normal way a guest comes
|
||||
// back through their own emailed link, and the identity this device
|
||||
// holds is then theirs — keep it. But the same link opened on a shared
|
||||
// device that holds SOMEONE ELSE's identity must not quietly act as
|
||||
// that someone: the server names the invite's guest on those two
|
||||
// responses precisely so the two cases can be told apart. On a
|
||||
// mismatch the stored identity is dropped, so the visitor is asked
|
||||
// who they are instead of having their likes filed under the previous
|
||||
// person. A response without guest_id (older backend) keeps today's
|
||||
// behaviour.
|
||||
const status = (error as { response?: { status?: number; data?: { guest_id?: unknown } } })
|
||||
.response;
|
||||
const invitedGuestId = status?.data?.guest_id;
|
||||
if ((status?.status === 409 || status?.status === 410) && typeof invitedGuestId === 'number') {
|
||||
const stored = getGuestIdentity(slug);
|
||||
if (stored && stored.id !== invitedGuestId) {
|
||||
clearGuestIdentity(slug);
|
||||
setIdentity(null);
|
||||
}
|
||||
}
|
||||
// Otherwise fail silently; the visitor falls back to the normal prompt.
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('Failed to redeem invite token', error);
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user