mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 06:52:04 +00:00
fix(sharing): invitations need a fresh proof and end with sign out everywhere
Offering access to the record took a live session alone, and an invitation outlives the session that sent it: whoever held a stolen session could invite their own account and keep a way into the record after the owner signed everything out. POST /api/account/grants now takes the recent-proof gate at every level (a sign-in or re-proof within five minutes on the browser, with the second factor on an account that has one; a step-up elevation on a token, which no shipped client uses here), and the invite form opens the re-proof dialog and sends again. Sign out everywhere now withdraws every invitation the account offered that nobody has accepted. Accepted grants are left standing on purpose: each is a person the owner chose, often a carer who still needs the record, and a sign-out button must not quietly cut them off. The response lists them as grantsKept and the sessions card shows each with a one-click End access.
This commit is contained in:
1 parent
a4d27c618e
commit
a294d7f684
22 files changed
+585
-65
No files matched your search
@@ -386,6 +386,65 @@ describe("destroyOtherSessions — everything", () => {
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("withdraws invitations nobody accepted and leaves accepted grants standing", async () => {
|
||||
const prisma = getPrismaClient();
|
||||
const owner = await makeUser("sess-invites-owner");
|
||||
const carer = await makeUser("sess-invites-carer");
|
||||
const stranger = await makeUser("sess-invites-stranger");
|
||||
const other = await makeUser("sess-invites-other");
|
||||
const accepted = await prisma.accountGrant.create({
|
||||
data: {
|
||||
grantorId: owner.id,
|
||||
granteeId: carer.id,
|
||||
access: "READ",
|
||||
acceptedAt: new Date(),
|
||||
},
|
||||
});
|
||||
const pending = await prisma.accountGrant.create({
|
||||
data: { grantorId: owner.id, granteeId: stranger.id, access: "WRITE" },
|
||||
});
|
||||
// Somebody else's invitation to the owner is not the owner's to withdraw.
|
||||
const received = await prisma.accountGrant.create({
|
||||
data: { grantorId: other.id, granteeId: owner.id, access: "READ" },
|
||||
});
|
||||
|
||||
const result = await destroyOtherSessions(
|
||||
owner.id,
|
||||
{ kind: "accessToken", accessTokenHash: "none" },
|
||||
{ reach: "everything" },
|
||||
);
|
||||
|
||||
expect(result.pendingInvitesRevoked).toBe(1);
|
||||
const after = async (id: string) =>
|
||||
prisma.accountGrant.findUniqueOrThrow({ where: { id } });
|
||||
expect((await after(pending.id)).revokedAt).not.toBeNull();
|
||||
expect((await after(pending.id)).revokedBy).toBe("GRANTOR");
|
||||
expect((await after(accepted.id)).revokedAt).toBeNull();
|
||||
expect((await after(received.id)).revokedAt).toBeNull();
|
||||
});
|
||||
|
||||
it("sign-ins leaves pending invitations alone", async () => {
|
||||
const prisma = getPrismaClient();
|
||||
const owner = await makeUser("sess-invites-signins");
|
||||
const other = await makeUser("sess-invites-signins-2");
|
||||
const pending = await prisma.accountGrant.create({
|
||||
data: { grantorId: owner.id, granteeId: other.id, access: "READ" },
|
||||
});
|
||||
const result = await destroyOtherSessions(
|
||||
owner.id,
|
||||
{ kind: "accessToken", accessTokenHash: "none" },
|
||||
{ reach: "sign-ins" },
|
||||
);
|
||||
expect(result.pendingInvitesRevoked).toBe(0);
|
||||
expect(
|
||||
(
|
||||
await prisma.accountGrant.findUniqueOrThrow({
|
||||
where: { id: pending.id },
|
||||
})
|
||||
).revokedAt,
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("sign-ins leaves connections, tokens and share links alone", async () => {
|
||||
const prisma = getPrismaClient();
|
||||
const user = await makeUser("sess-signins-only");
|
||||
|
||||
Reference in new issue
Block a user