mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-11 05:11:49 +01:00
Fix reused invite refresh pending state
This commit is contained in:
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
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user