3833015626
## What Invitation lookups did not filter on `expiresAt`, so expired invitations were still treated as active. This aligns them with the sibling `findInvitationsByEmail`, which already applied that filter. - `WorkspaceInvitationService.getOneWorkspaceInvitation` - added `deletedAt IS NULL` and `expiresAt > now` (also converted to a typed `findOne` so the column references are checked). - `AuthService.findInvitationForSignInUp` - added `expiresAt > now` (it already filtered `deletedAt`). - `throwIfOnboardingInvitationLimitReached` - expired tokens no longer count toward the onboarding invitation limit. - `createWorkspaceInvitation` - deletes the expired token for that email before issuing a replacement, so re-invites don't accumulate stale rows. ## Why Without the filter, an expired pending invitation behaved as if it were still active: - On sign-up with a personal invite token, an expired invitation still granted access to the workspace. - Re-inviting an email whose invitation had lapsed reported `INVITATION_ALREADY_EXIST` instead of sending a fresh invite. - Expired onboarding invitations still consumed quota, so the limit could be hit by invitations nobody could use. Once expired tokens are ignored on read, a re-invite would leave the old row behind, so `createWorkspaceInvitation` now removes it. The delete is scoped to the same workspace, invitation token types, that exact email, and `expiresAt <= now`, so it can only remove tokens that are already unusable. Closes twentyhq/private-issues#503 ## Tests Integration suites added, run against a real database: - `auth/sign-up/failing-sign-up-with-expired-invitation` - expired personal invitation is rejected (snapshot asserts the specific `FORBIDDEN` error). - `auth/sign-up/successful-sign-up-with-valid-invitation` - positive control: a valid invitation still grants access, so the rejection above cannot pass for an unrelated reason. - `expired-workspace-invitation` - re-invite over an expired invitation succeeds and leaves exactly one (fresh) token; a valid invitation is still reported as already existing. Each assertion was verified to fail when its corresponding filter is removed. Unit tests (`workspace-invitation.service.spec.ts`, `auth.service.spec.ts`), typecheck, and lint all pass. Not included: invitations that expire and are never re-invited still linger, since no cron reaps invitation tokens today.
50 lines
1.3 KiB
TypeScript
50 lines
1.3 KiB
TypeScript
import { AppTokenType } from 'src/engine/core-modules/app-token/app-token.entity';
|
|
import { SEED_APPLE_WORKSPACE_ID } from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
|
|
|
|
export const seedWorkspaceInvitation = ({
|
|
email,
|
|
value,
|
|
expiresAt,
|
|
workspaceId = SEED_APPLE_WORKSPACE_ID,
|
|
}: {
|
|
email: string;
|
|
value: string;
|
|
expiresAt: Date;
|
|
workspaceId?: string;
|
|
}) =>
|
|
testDataSource.query(
|
|
`INSERT INTO core."appToken" ("workspaceId", "type", "value", "expiresAt", "context")
|
|
VALUES ($1, $2, $3, $4, $5::jsonb)`,
|
|
[
|
|
workspaceId,
|
|
AppTokenType.InvitationToken,
|
|
value,
|
|
expiresAt.toISOString(),
|
|
JSON.stringify({ email }),
|
|
],
|
|
);
|
|
|
|
export const findWorkspaceInvitationsByEmail = ({
|
|
email,
|
|
workspaceId = SEED_APPLE_WORKSPACE_ID,
|
|
}: {
|
|
email: string;
|
|
workspaceId?: string;
|
|
}): Promise<{ value: string; expiresAt: string }[]> =>
|
|
testDataSource.query(
|
|
`SELECT "value", "expiresAt" FROM core."appToken" WHERE "workspaceId" = $1 AND context->>'email' = $2`,
|
|
[workspaceId, email],
|
|
);
|
|
|
|
export const deleteWorkspaceInvitationsByEmail = ({
|
|
email,
|
|
workspaceId = SEED_APPLE_WORKSPACE_ID,
|
|
}: {
|
|
email: string;
|
|
workspaceId?: string;
|
|
}) =>
|
|
testDataSource.query(
|
|
`DELETE FROM core."appToken" WHERE "workspaceId" = $1 AND context->>'email' = $2`,
|
|
[workspaceId, email],
|
|
);
|