Fix reused invite refresh pending state

This commit is contained in:
Dotta committed 2026-04-13 09:23:31 -05:00
1 parent 4c146ab6e7
commit 2551a53c49
2 files changed
+127 -14

No files matched your search

@@ -15,11 +15,17 @@ import { errorHandler } from "../middleware/index.js";
function createSelectChain(rows: unknown[]) {
const query = {
then(resolve: (value: unknown[]) => unknown) {
return Promise.resolve(rows).then(resolve);
},
leftJoin() {
return query;
},
orderBy() {
return query;
},
where() {
return Promise.resolve(rows);
return query;
},
};
return {
@@ -40,10 +46,13 @@ function createDbStub(...selectResponses: unknown[][]) {
};
}
function createApp(db: Record<string, unknown>) {
function createApp(
db: Record<string, unknown>,
actor: Record<string, unknown> = { type: "anon" },
) {
const app = express();
app.use((req, _res, next) => {
(req as any).actor = { type: "anon" };
(req as any).actor = actor;
next();
});
app.use(
@@ -202,4 +211,60 @@ describe("GET /invites/:token", () => {
expect(res.body.joinRequestType).toBe("human");
expect(res.body.companyName).toBe("Acme Robotics");
});
it("falls back to a reusable human join request when the accepted invite reused an existing queue entry", async () => {
const invite = {
id: "invite-2",
companyId: "company-1",
inviteType: "company_join",
allowedJoinTypes: "human",
tokenHash: "hash",
defaultsPayload: null,
expiresAt: new Date("2027-03-07T00:10:00.000Z"),
invitedByUserId: null,
revokedAt: null,
acceptedAt: new Date("2026-03-07T00:05:00.000Z"),
createdAt: new Date("2026-03-07T00:00:00.000Z"),
updatedAt: new Date("2026-03-07T00:05:00.000Z"),
};
const app = createApp(
createDbStub(
[invite],
[],
[{ email: "jane@example.com" }],
[
{
id: "join-1",
requestType: "human",
status: "pending_approval",
requestingUserId: "user-1",
requestEmailSnapshot: "jane@example.com",
},
],
[
{
name: "Acme Robotics",
brandColor: "#114488",
logoAssetId: "logo-1",
},
],
[
{
companyId: "company-1",
objectKey: "company-1/assets/companies/logo-1",
contentType: "image/png",
byteSize: 3,
originalFilename: "logo.png",
},
],
),
{ type: "board", userId: "user-1", source: "session" },
);
const res = await request(app).get("/api/invites/pcp_invite_test");
expect(res.status).toBe(200);
expect(res.body.joinRequestStatus).toBe("pending_approval");
expect(res.body.joinRequestType).toBe("human");
});
});
+59 -11
View File
@@ -1788,6 +1788,57 @@ async function resolveActorEmail(db: Db, req: Request): Promise<string | null> {
return user?.email ?? null;
}
async function resolveAcceptedInviteJoinRequest(
db: Db,
req: Request,
invite: typeof invites.$inferSelect | null,
) {
if (!invite?.acceptedAt) return null;
const directJoinRequest = await db
.select({
requestType: joinRequests.requestType,
status: joinRequests.status,
requestingUserId: joinRequests.requestingUserId,
requestEmailSnapshot: joinRequests.requestEmailSnapshot,
})
.from(joinRequests)
.where(eq(joinRequests.inviteId, invite.id))
.then((rows) => rows[0] ?? null);
if (directJoinRequest) return directJoinRequest;
if (!invite.companyId) return null;
const actorRequestingUserId = isLocalImplicit(req)
? "local-board"
: req.actor.userId ?? null;
const actorEmail = await resolveActorEmail(db, req);
if (!actorRequestingUserId && !actorEmail) return null;
return findReusableHumanJoinRequest(
await db
.select({
id: joinRequests.id,
requestType: joinRequests.requestType,
status: joinRequests.status,
requestingUserId: joinRequests.requestingUserId,
requestEmailSnapshot: joinRequests.requestEmailSnapshot,
})
.from(joinRequests)
.where(
and(
eq(joinRequests.companyId, invite.companyId),
eq(joinRequests.requestType, "human"),
),
)
.orderBy(desc(joinRequests.createdAt)),
{
requestingUserId: actorRequestingUserId,
requestEmailSnapshot: actorEmail,
},
);
}
function grantsFromDefaults(
defaultsPayload: Record<string, unknown> | null | undefined,
key: "human" | "agent"
@@ -2529,16 +2580,7 @@ export function accessRoutes(
.from(invites)
.where(eq(invites.tokenHash, hashToken(token)))
.then((rows) => rows[0] ?? null);
const inviteJoinRequest = invite?.acceptedAt
? await db
.select({
requestType: joinRequests.requestType,
status: joinRequests.status,
})
.from(joinRequests)
.where(eq(joinRequests.inviteId, invite.id))
.then((rows) => rows[0] ?? null)
: null;
const inviteJoinRequest = await resolveAcceptedInviteJoinRequest(db, req, invite);
if (
!invite ||
invite.revokedAt ||
@@ -2570,7 +2612,13 @@ export function accessRoutes(
.from(invites)
.where(eq(invites.tokenHash, hashToken(token)))
.then((rows) => rows[0] ?? null);
if (!invite || invite.revokedAt || invite.acceptedAt || inviteExpired(invite)) {
const inviteJoinRequest = await resolveAcceptedInviteJoinRequest(db, req, invite);
if (
!invite ||
invite.revokedAt ||
inviteExpired(invite) ||
(invite.acceptedAt && !inviteJoinRequest)
) {
throw notFound("Invite not found");
}