mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-12 19:03:42 -05:00
fix: refresh invite expiration when sharing link (#7198)
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
This commit is contained in:
@@ -929,7 +929,7 @@ checksums:
|
||||
environments/settings/general/from_your_organization: 4b7970431edb3d0f13c394dbd755a055
|
||||
environments/settings/general/invitation_sent_once_more: e6e5ea066810f9dcb65788aa4f05d6e2
|
||||
environments/settings/general/invite_deleted_successfully: 1c7dca6d0f6870d945288e38cfd2f943
|
||||
environments/settings/general/invited_on: 83476ce4bcdfc3ccf524d1cd91b758a8
|
||||
environments/settings/general/invite_expires_on: 6fd2356ad91a5f189070c43855904bb4
|
||||
environments/settings/general/invites_failed: 180ffb8db417050227cc2b2ea74b7aae
|
||||
environments/settings/general/leave_organization: e74132cb4a0dc98c41e61ea3b2dd268b
|
||||
environments/settings/general/leave_organization_description: 2d0cd65e4e78a9b2835cf88c4de407fb
|
||||
|
||||
@@ -990,7 +990,7 @@
|
||||
"from_your_organization": "von deiner Organisation",
|
||||
"invitation_sent_once_more": "Einladung nochmal gesendet.",
|
||||
"invite_deleted_successfully": "Einladung erfolgreich gelöscht",
|
||||
"invited_on": "Eingeladen am {date}",
|
||||
"invite_expires_on": "Einladung läuft ab am {date}",
|
||||
"invites_failed": "Einladungen fehlgeschlagen",
|
||||
"leave_organization": "Organisation verlassen",
|
||||
"leave_organization_description": "Du wirst diese Organisation verlassen und den Zugriff auf alle Umfragen und Antworten verlieren. Du kannst nur wieder beitreten, wenn Du erneut eingeladen wirst.",
|
||||
|
||||
@@ -990,7 +990,7 @@
|
||||
"from_your_organization": "from your organization",
|
||||
"invitation_sent_once_more": "Invitation sent once more.",
|
||||
"invite_deleted_successfully": "Invite deleted successfully",
|
||||
"invited_on": "Invited on {date}",
|
||||
"invite_expires_on": "Invite expires on {date}",
|
||||
"invites_failed": "Invites failed",
|
||||
"leave_organization": "Leave organization",
|
||||
"leave_organization_description": "You wil leave this organization and loose access to all surveys and responses. You can only rejoin if you are invited again.",
|
||||
@@ -1268,14 +1268,13 @@
|
||||
"darken_or_lighten_background_of_your_choice": "Darken or lighten background of your choice.",
|
||||
"date_format": "Date format",
|
||||
"days_before_showing_this_survey_again": "or more days to pass between the last shown survey and showing this survey.",
|
||||
"display_type": "Display type",
|
||||
"dropdown": "Dropdown",
|
||||
"delete_anyways": "Delete anyways",
|
||||
"delete_block": "Delete block",
|
||||
"delete_choice": "Delete choice",
|
||||
"disable_the_visibility_of_survey_progress": "Disable the visibility of survey progress.",
|
||||
"display_an_estimate_of_completion_time_for_survey": "Display an estimate of completion time for survey",
|
||||
"display_number_of_responses_for_survey": "Display number of responses for survey",
|
||||
"display_type": "Display type",
|
||||
"divide": "Divide /",
|
||||
"does_not_contain": "Does not contain",
|
||||
"does_not_end_with": "Does not end with",
|
||||
@@ -1283,6 +1282,7 @@
|
||||
"does_not_include_all_of": "Does not include all of",
|
||||
"does_not_include_one_of": "Does not include one of",
|
||||
"does_not_start_with": "Does not start with",
|
||||
"dropdown": "Dropdown",
|
||||
"duplicate_block": "Duplicate block",
|
||||
"duplicate_question": "Duplicate question",
|
||||
"edit_link": "Edit link",
|
||||
@@ -1415,11 +1415,11 @@
|
||||
"limit_the_maximum_file_size": "Limit the maximum file size for uploads.",
|
||||
"limit_upload_file_size_to": "Limit upload file size to",
|
||||
"link_survey_description": "Share a link to a survey page or embed it in a web page or email.",
|
||||
"list": "List",
|
||||
"load_segment": "Load segment",
|
||||
"logic_error_warning": "Changing will cause logic errors",
|
||||
"logic_error_warning_text": "Changing the question type will remove the logic conditions from this question",
|
||||
"logo_settings": "Logo settings",
|
||||
"list": "List",
|
||||
"long_answer": "Long answer",
|
||||
"long_answer_toggle_description": "Allow respondents to write longer, multi-line answers.",
|
||||
"lower_label": "Lower Label",
|
||||
@@ -3095,4 +3095,4 @@
|
||||
"usability_rating_description": "Measure perceived usability by asking users to rate their experience with your product using a standardized 10-question survey.",
|
||||
"usability_score_name": "System Usability Score (SUS)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -990,7 +990,7 @@
|
||||
"from_your_organization": "de tu organización",
|
||||
"invitation_sent_once_more": "Invitación enviada una vez más.",
|
||||
"invite_deleted_successfully": "Invitación eliminada correctamente",
|
||||
"invited_on": "Invitado el {date}",
|
||||
"invite_expires_on": "La invitación expira el {date}",
|
||||
"invites_failed": "Las invitaciones fallaron",
|
||||
"leave_organization": "Abandonar organización",
|
||||
"leave_organization_description": "Abandonarás esta organización y perderás acceso a todas las encuestas y respuestas. Solo podrás volver a unirte si te invitan de nuevo.",
|
||||
|
||||
@@ -990,7 +990,7 @@
|
||||
"from_your_organization": "de votre organisation",
|
||||
"invitation_sent_once_more": "Invitation envoyée une fois de plus.",
|
||||
"invite_deleted_successfully": "Invitation supprimée avec succès",
|
||||
"invited_on": "Invité le {date}",
|
||||
"invite_expires_on": "L'invitation expire le {date}",
|
||||
"invites_failed": "Invitations échouées",
|
||||
"leave_organization": "Quitter l'organisation",
|
||||
"leave_organization_description": "Vous quitterez cette organisation et perdrez l'accès à toutes les enquêtes et réponses. Vous ne pourrez revenir que si vous êtes de nouveau invité.",
|
||||
|
||||
@@ -990,7 +990,7 @@
|
||||
"from_your_organization": "a szervezetétől",
|
||||
"invitation_sent_once_more": "A meghívó még egyszer elküldve.",
|
||||
"invite_deleted_successfully": "A meghívó sikeresen törölve",
|
||||
"invited_on": "Meghívva ekkor: {date}",
|
||||
"invite_expires_on": "A meghívó lejár: {date}",
|
||||
"invites_failed": "A meghívás sikertelen",
|
||||
"leave_organization": "Szervezet elhagyása",
|
||||
"leave_organization_description": "Elhagyja ezt a szervezetet, és elveszíti az összes kérdőívhez és válaszhoz való hozzáférését. Csak akkor tud ismét csatlakozni, ha újra meghívják.",
|
||||
|
||||
@@ -990,7 +990,7 @@
|
||||
"from_your_organization": "あなたの組織から",
|
||||
"invitation_sent_once_more": "招待状を再度送信しました。",
|
||||
"invite_deleted_successfully": "招待を正常に削除しました",
|
||||
"invited_on": "{date}に招待",
|
||||
"invite_expires_on": "招待は{date}に期限切れ",
|
||||
"invites_failed": "招待に失敗しました",
|
||||
"leave_organization": "組織を離れる",
|
||||
"leave_organization_description": "この組織を離れ、すべてのフォームと回答へのアクセス権を失います。再度招待された場合にのみ再参加できます。",
|
||||
|
||||
@@ -990,7 +990,7 @@
|
||||
"from_your_organization": "vanuit uw organisatie",
|
||||
"invitation_sent_once_more": "Uitnodiging nogmaals verzonden.",
|
||||
"invite_deleted_successfully": "Uitnodiging succesvol verwijderd",
|
||||
"invited_on": "Uitgenodigd op {date}",
|
||||
"invite_expires_on": "Uitnodiging verloopt op {date}",
|
||||
"invites_failed": "Uitnodigingen zijn mislukt",
|
||||
"leave_organization": "Verlaat de organisatie",
|
||||
"leave_organization_description": "U verlaat deze organisatie en verliest de toegang tot alle enquêtes en reacties. Je kunt alleen weer meedoen als je opnieuw wordt uitgenodigd.",
|
||||
|
||||
@@ -990,7 +990,7 @@
|
||||
"from_your_organization": "da sua organização",
|
||||
"invitation_sent_once_more": "Convite enviado de novo.",
|
||||
"invite_deleted_successfully": "Convite deletado com sucesso",
|
||||
"invited_on": "Convidado em {date}",
|
||||
"invite_expires_on": "O convite expira em {date}",
|
||||
"invites_failed": "Convites falharam",
|
||||
"leave_organization": "Sair da organização",
|
||||
"leave_organization_description": "Você vai sair dessa organização e perder acesso a todas as pesquisas e respostas. Você só pode voltar se for convidado de novo.",
|
||||
|
||||
@@ -990,7 +990,7 @@
|
||||
"from_your_organization": "da sua organização",
|
||||
"invitation_sent_once_more": "Convite enviado mais uma vez.",
|
||||
"invite_deleted_successfully": "Convite eliminado com sucesso",
|
||||
"invited_on": "Convidado em {date}",
|
||||
"invite_expires_on": "O convite expira em {date}",
|
||||
"invites_failed": "Convites falharam",
|
||||
"leave_organization": "Sair da organização",
|
||||
"leave_organization_description": "Vai sair desta organização e perder o acesso a todos os inquéritos e respostas. Só pode voltar a juntar-se se for convidado novamente.",
|
||||
|
||||
@@ -990,7 +990,7 @@
|
||||
"from_your_organization": "din organizația ta",
|
||||
"invitation_sent_once_more": "Invitație trimisă din nou.",
|
||||
"invite_deleted_successfully": "Invitație ștearsă cu succes",
|
||||
"invited_on": "Invitat pe {date}",
|
||||
"invite_expires_on": "Invitația expiră pe {date}",
|
||||
"invites_failed": "Invitații eșuate",
|
||||
"leave_organization": "Părăsește organizația",
|
||||
"leave_organization_description": "Vei părăsi această organizație și vei pierde accesul la toate sondajele și răspunsurile. Poți să te alături din nou doar dacă ești invitat.",
|
||||
|
||||
@@ -990,7 +990,7 @@
|
||||
"from_your_organization": "из вашей организации",
|
||||
"invitation_sent_once_more": "Приглашение отправлено ещё раз.",
|
||||
"invite_deleted_successfully": "Приглашение успешно удалено",
|
||||
"invited_on": "Приглашён {date}",
|
||||
"invite_expires_on": "Приглашение истекает {date}",
|
||||
"invites_failed": "Не удалось отправить приглашения",
|
||||
"leave_organization": "Покинуть организацию",
|
||||
"leave_organization_description": "Вы покинете эту организацию и потеряете доступ ко всем опросам и ответам. Вы сможете вернуться только по новому приглашению.",
|
||||
|
||||
@@ -990,7 +990,7 @@
|
||||
"from_your_organization": "från din organisation",
|
||||
"invitation_sent_once_more": "Inbjudan skickad igen.",
|
||||
"invite_deleted_successfully": "Inbjudan borttagen",
|
||||
"invited_on": "Inbjuden den {date}",
|
||||
"invite_expires_on": "Inbjudan går ut den {date}",
|
||||
"invites_failed": "Inbjudningar misslyckades",
|
||||
"leave_organization": "Lämna organisation",
|
||||
"leave_organization_description": "Du kommer att lämna denna organisation och förlora åtkomst till alla enkäter och svar. Du kan endast återansluta om du blir inbjuden igen.",
|
||||
|
||||
@@ -990,7 +990,7 @@
|
||||
"from_your_organization": "来自你的组织",
|
||||
"invitation_sent_once_more": "再次发送邀请。",
|
||||
"invite_deleted_successfully": "邀请 删除 成功",
|
||||
"invited_on": "受邀于 {date}",
|
||||
"invite_expires_on": "邀请将于 {date} 过期",
|
||||
"invites_failed": "邀请失败",
|
||||
"leave_organization": "离开 组织",
|
||||
"leave_organization_description": "您将离开此组织,并失去对所有调查和响应的访问权限。只有再次被邀请后,您才能重新加入。",
|
||||
|
||||
@@ -990,7 +990,7 @@
|
||||
"from_your_organization": "來自您的組織",
|
||||
"invitation_sent_once_more": "已再次發送邀請。",
|
||||
"invite_deleted_successfully": "邀請已成功刪除",
|
||||
"invited_on": "邀請於 '{'date'}'",
|
||||
"invite_expires_on": "邀請將於 '{'date'}' 過期",
|
||||
"invites_failed": "邀請失敗",
|
||||
"leave_organization": "離開組織",
|
||||
"leave_organization_description": "您將離開此組織並失去對所有問卷和回應的存取權限。只有再次收到邀請,您才能重新加入。",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { OrganizationRole } from "@prisma/client";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { ZId, ZUuid } from "@formbricks/types/common";
|
||||
import { AuthenticationError, OperationNotAllowedError, ValidationError } from "@formbricks/types/errors";
|
||||
import { TOrganizationRole, ZOrganizationRole } from "@formbricks/types/memberships";
|
||||
@@ -23,7 +24,7 @@ import {
|
||||
getMembershipsByUserId,
|
||||
getOrganizationOwnerCount,
|
||||
} from "@/modules/organization/settings/teams/lib/membership";
|
||||
import { deleteInvite, getInvite, inviteUser, resendInvite } from "./lib/invite";
|
||||
import { deleteInvite, getInvite, inviteUser, refreshInviteExpiration, resendInvite } from "./lib/invite";
|
||||
|
||||
const ZDeleteInviteAction = z.object({
|
||||
inviteId: ZUuid,
|
||||
@@ -57,30 +58,57 @@ const ZCreateInviteTokenAction = z.object({
|
||||
inviteId: ZUuid,
|
||||
});
|
||||
|
||||
export const createInviteTokenAction = authenticatedActionClient
|
||||
.schema(ZCreateInviteTokenAction)
|
||||
.action(async ({ parsedInput, ctx }) => {
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId: await getOrganizationIdFromInviteId(parsedInput.inviteId),
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
],
|
||||
});
|
||||
export const createInviteTokenAction = authenticatedActionClient.schema(ZCreateInviteTokenAction).action(
|
||||
withAuditLogging(
|
||||
"updated",
|
||||
"invite",
|
||||
async ({
|
||||
parsedInput,
|
||||
ctx,
|
||||
}: {
|
||||
ctx: AuthenticatedActionClientCtx;
|
||||
parsedInput: z.infer<typeof ZCreateInviteTokenAction>;
|
||||
}) => {
|
||||
const organizationId = await getOrganizationIdFromInviteId(parsedInput.inviteId);
|
||||
|
||||
const invite = await getInvite(parsedInput.inviteId);
|
||||
if (!invite) {
|
||||
throw new ValidationError("Invite not found");
|
||||
await checkAuthorizationUpdated({
|
||||
userId: ctx.user.id,
|
||||
organizationId,
|
||||
access: [
|
||||
{
|
||||
type: "organization",
|
||||
roles: ["owner", "manager"],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Get old expiresAt for audit logging before update
|
||||
const oldInvite = await prisma.invite.findUnique({
|
||||
where: { id: parsedInput.inviteId },
|
||||
select: { email: true, expiresAt: true },
|
||||
});
|
||||
|
||||
if (!oldInvite) {
|
||||
throw new ValidationError("Invite not found");
|
||||
}
|
||||
|
||||
// Refresh the invitation expiration
|
||||
const updatedInvite = await refreshInviteExpiration(parsedInput.inviteId);
|
||||
|
||||
// Set audit context
|
||||
ctx.auditLoggingCtx.organizationId = organizationId;
|
||||
ctx.auditLoggingCtx.inviteId = parsedInput.inviteId;
|
||||
ctx.auditLoggingCtx.oldObject = { expiresAt: oldInvite.expiresAt };
|
||||
ctx.auditLoggingCtx.newObject = { expiresAt: updatedInvite.expiresAt };
|
||||
|
||||
const inviteToken = createInviteToken(parsedInput.inviteId, updatedInvite.email, {
|
||||
expiresIn: "7d",
|
||||
});
|
||||
|
||||
return { inviteToken: encodeURIComponent(inviteToken) };
|
||||
}
|
||||
const inviteToken = createInviteToken(parsedInput.inviteId, invite.email, {
|
||||
expiresIn: "7d",
|
||||
});
|
||||
|
||||
return { inviteToken: encodeURIComponent(inviteToken) };
|
||||
});
|
||||
)
|
||||
);
|
||||
|
||||
const ZDeleteMembershipAction = z.object({
|
||||
userId: ZId,
|
||||
@@ -191,6 +219,7 @@ export const resendInviteAction = authenticatedActionClient.schema(ZResendInvite
|
||||
invite?.creator?.name ?? "",
|
||||
updatedInvite.name ?? ""
|
||||
);
|
||||
|
||||
return updatedInvite;
|
||||
}
|
||||
)
|
||||
|
||||
@@ -80,6 +80,7 @@ export const MemberActions = ({ organization, member, invite, showDeleteButton }
|
||||
if (createInviteTokenResponse?.data) {
|
||||
setShareInviteToken(createInviteTokenResponse.data.inviteToken);
|
||||
setShowShareInviteModal(true);
|
||||
router.refresh();
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(createInviteTokenResponse);
|
||||
toast.error(errorMessage);
|
||||
@@ -99,6 +100,7 @@ export const MemberActions = ({ organization, member, invite, showDeleteButton }
|
||||
});
|
||||
if (resendInviteResponse?.data) {
|
||||
toast.success(t("environments.settings.general.invitation_sent_once_more"));
|
||||
router.refresh();
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(resendInviteResponse);
|
||||
toast.error(errorMessage);
|
||||
|
||||
@@ -47,8 +47,8 @@ export const MembersInfo = ({
|
||||
<Badge type="gray" text="Expired" size="tiny" data-testid="expired-badge" />
|
||||
) : (
|
||||
<TooltipRenderer
|
||||
tooltipContent={`${t("environments.settings.general.invited_on", {
|
||||
date: getFormattedDateTimeString(member.createdAt),
|
||||
tooltipContent={`${t("environments.settings.general.invite_expires_on", {
|
||||
date: getFormattedDateTimeString(member.expiresAt),
|
||||
})}`}>
|
||||
<Badge type="warning" text="Pending" size="tiny" />
|
||||
</TooltipRenderer>
|
||||
|
||||
@@ -9,7 +9,14 @@ import {
|
||||
} from "@formbricks/types/errors";
|
||||
import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
|
||||
import { TInvitee } from "../types/invites";
|
||||
import { deleteInvite, getInvite, getInvitesByOrganizationId, inviteUser, resendInvite } from "./invite";
|
||||
import {
|
||||
deleteInvite,
|
||||
getInvite,
|
||||
getInvitesByOrganizationId,
|
||||
inviteUser,
|
||||
refreshInviteExpiration,
|
||||
resendInvite,
|
||||
} from "./invite";
|
||||
|
||||
vi.mock("@formbricks/database", () => ({
|
||||
prisma: {
|
||||
@@ -46,32 +53,129 @@ const mockInvite: Invite = {
|
||||
teamIds: [],
|
||||
};
|
||||
|
||||
describe("resendInvite", () => {
|
||||
describe("refreshInviteExpiration", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
test("returns email and name if invite exists", async () => {
|
||||
vi.mocked(prisma.invite.findUnique).mockResolvedValue({ ...mockInvite, creator: {} });
|
||||
vi.mocked(prisma.invite.update).mockResolvedValue({ ...mockInvite, organizationId: "org-1" });
|
||||
const result = await resendInvite("invite-1");
|
||||
expect(result).toEqual({ email: mockInvite.email, name: mockInvite.name });
|
||||
|
||||
test("updates expiresAt to approximately 7 days from now", async () => {
|
||||
const now = Date.now();
|
||||
const expectedExpiresAt = new Date(now + 1000 * 60 * 60 * 24 * 7);
|
||||
|
||||
vi.mocked(prisma.invite.update).mockResolvedValue({
|
||||
...mockInvite,
|
||||
expiresAt: expectedExpiresAt,
|
||||
});
|
||||
|
||||
const result = await refreshInviteExpiration("invite-1");
|
||||
|
||||
expect(prisma.invite.update).toHaveBeenCalledWith({
|
||||
where: { id: "invite-1" },
|
||||
data: {
|
||||
expiresAt: expect.any(Date),
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.expiresAt.getTime()).toBeGreaterThanOrEqual(now + 1000 * 60 * 60 * 24 * 7 - 1000);
|
||||
expect(result.expiresAt.getTime()).toBeLessThanOrEqual(now + 1000 * 60 * 60 * 24 * 7 + 1000);
|
||||
});
|
||||
test("throws ResourceNotFoundError if invite not found", async () => {
|
||||
vi.mocked(prisma.invite.findUnique).mockResolvedValue(null);
|
||||
await expect(resendInvite("invite-1")).rejects.toThrow(ResourceNotFoundError);
|
||||
|
||||
test("throws ResourceNotFoundError if invite not found (P2025)", async () => {
|
||||
const prismaError = new Prisma.PrismaClientKnownRequestError("Record not found", {
|
||||
code: "P2025",
|
||||
clientVersion: "1.0.0",
|
||||
});
|
||||
vi.mocked(prisma.invite.update).mockRejectedValue(prismaError);
|
||||
await expect(refreshInviteExpiration("invite-1")).rejects.toThrow(ResourceNotFoundError);
|
||||
});
|
||||
test("throws DatabaseError on prisma error", async () => {
|
||||
|
||||
test("throws DatabaseError on other prisma errors", async () => {
|
||||
const prismaError = new Prisma.PrismaClientKnownRequestError("db", {
|
||||
code: "P2002",
|
||||
clientVersion: "1.0.0",
|
||||
});
|
||||
vi.mocked(prisma.invite.findUnique).mockRejectedValue(prismaError);
|
||||
vi.mocked(prisma.invite.update).mockRejectedValue(prismaError);
|
||||
await expect(refreshInviteExpiration("invite-1")).rejects.toThrow(DatabaseError);
|
||||
});
|
||||
|
||||
test("throws error if non-prisma error", async () => {
|
||||
const error = new Error("db");
|
||||
vi.mocked(prisma.invite.update).mockRejectedValue(error);
|
||||
await expect(refreshInviteExpiration("invite-1")).rejects.toThrow("db");
|
||||
});
|
||||
|
||||
test("returns full invite object with all fields", async () => {
|
||||
const updatedInvite = {
|
||||
...mockInvite,
|
||||
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7),
|
||||
};
|
||||
vi.mocked(prisma.invite.update).mockResolvedValue(updatedInvite);
|
||||
|
||||
const result = await refreshInviteExpiration("invite-1");
|
||||
|
||||
expect(result).toEqual(updatedInvite);
|
||||
expect(result.id).toBe("invite-1");
|
||||
expect(result.email).toBe("test@example.com");
|
||||
expect(result.name).toBe("Test User");
|
||||
});
|
||||
});
|
||||
|
||||
describe("resendInvite", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("returns email and name after updating expiration", async () => {
|
||||
const updatedInvite = {
|
||||
...mockInvite,
|
||||
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7),
|
||||
};
|
||||
vi.mocked(prisma.invite.update).mockResolvedValue(updatedInvite);
|
||||
|
||||
const result = await resendInvite("invite-1");
|
||||
|
||||
expect(result).toEqual({ email: mockInvite.email, name: mockInvite.name });
|
||||
expect(prisma.invite.update).toHaveBeenCalledWith({
|
||||
where: { id: "invite-1" },
|
||||
data: {
|
||||
expiresAt: expect.any(Date),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("calls refreshInviteExpiration helper", async () => {
|
||||
const updatedInvite = {
|
||||
...mockInvite,
|
||||
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7),
|
||||
};
|
||||
vi.mocked(prisma.invite.update).mockResolvedValue(updatedInvite);
|
||||
|
||||
await resendInvite("invite-1");
|
||||
|
||||
expect(prisma.invite.update).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("throws ResourceNotFoundError if invite not found", async () => {
|
||||
const prismaError = new Prisma.PrismaClientKnownRequestError("Record not found", {
|
||||
code: "P2025",
|
||||
clientVersion: "1.0.0",
|
||||
});
|
||||
vi.mocked(prisma.invite.update).mockRejectedValue(prismaError);
|
||||
await expect(resendInvite("invite-1")).rejects.toThrow(ResourceNotFoundError);
|
||||
});
|
||||
|
||||
test("throws DatabaseError on other prisma errors", async () => {
|
||||
const prismaError = new Prisma.PrismaClientKnownRequestError("db", {
|
||||
code: "P2002",
|
||||
clientVersion: "1.0.0",
|
||||
});
|
||||
vi.mocked(prisma.invite.update).mockRejectedValue(prismaError);
|
||||
await expect(resendInvite("invite-1")).rejects.toThrow(DatabaseError);
|
||||
});
|
||||
|
||||
test("throws error if prisma error", async () => {
|
||||
test("throws error if non-prisma error", async () => {
|
||||
const error = new Error("db");
|
||||
vi.mocked(prisma.invite.findUnique).mockRejectedValue(error);
|
||||
vi.mocked(prisma.invite.update).mockRejectedValue(error);
|
||||
await expect(resendInvite("invite-1")).rejects.toThrow("db");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,44 +13,21 @@ import { getMembershipByUserIdOrganizationId } from "@/lib/membership/service";
|
||||
import { validateInputs } from "@/lib/utils/validate";
|
||||
import { type InviteWithCreator, type TInvite, type TInvitee } from "../types/invites";
|
||||
|
||||
export const resendInvite = async (inviteId: string): Promise<Pick<Invite, "email" | "name">> => {
|
||||
export const refreshInviteExpiration = async (inviteId: string): Promise<Invite> => {
|
||||
try {
|
||||
const invite = await prisma.invite.findUnique({
|
||||
where: {
|
||||
id: inviteId,
|
||||
},
|
||||
select: {
|
||||
email: true,
|
||||
name: true,
|
||||
creator: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!invite) {
|
||||
throw new ResourceNotFoundError("Invite", inviteId);
|
||||
}
|
||||
|
||||
const updatedInvite = await prisma.invite.update({
|
||||
where: {
|
||||
id: inviteId,
|
||||
},
|
||||
where: { id: inviteId },
|
||||
data: {
|
||||
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
organizationId: true,
|
||||
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7), // 7 days
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
email: updatedInvite.email,
|
||||
name: updatedInvite.name,
|
||||
};
|
||||
return updatedInvite;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
if (error.code === "P2025") {
|
||||
throw new ResourceNotFoundError("Invite", inviteId);
|
||||
}
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
@@ -58,6 +35,16 @@ export const resendInvite = async (inviteId: string): Promise<Pick<Invite, "emai
|
||||
}
|
||||
};
|
||||
|
||||
export const resendInvite = async (inviteId: string): Promise<Pick<Invite, "email" | "name">> => {
|
||||
// Refresh expiration and return the updated invite (single query)
|
||||
const updatedInvite = await refreshInviteExpiration(inviteId);
|
||||
|
||||
return {
|
||||
email: updatedInvite.email,
|
||||
name: updatedInvite.name,
|
||||
};
|
||||
};
|
||||
|
||||
export const getInvitesByOrganizationId = reactCache(
|
||||
async (organizationId: string, page?: number): Promise<TInvite[]> => {
|
||||
validateInputs([organizationId, z.string()], [page, z.number().optional()]);
|
||||
|
||||
Reference in New Issue
Block a user