diff --git a/apps/client/src/features/workspace/components/members/components/invite-action-menu.tsx b/apps/client/src/features/workspace/components/members/components/invite-action-menu.tsx index 045047af..5472a9c2 100644 --- a/apps/client/src/features/workspace/components/members/components/invite-action-menu.tsx +++ b/apps/client/src/features/workspace/components/members/components/invite-action-menu.tsx @@ -5,8 +5,10 @@ import { modals } from "@mantine/modals"; import { useResendInvitationMutation, useRevokeInvitationMutation, + useGetInviteLink, } from "@/features/workspace/queries/workspace-query.ts"; import { useTranslation } from "react-i18next"; +import { notifications } from "@mantine/notifications"; interface Props { invitationId: string; @@ -15,6 +17,17 @@ export default function InviteActionMenu({ invitationId }: Props) { const { t } = useTranslation(); const resendInvitationMutation = useResendInvitationMutation(); const revokeInvitationMutation = useRevokeInvitationMutation(); + const { data: inviteLink, error, } = useGetInviteLink(invitationId); + + const onCopyLink = async () => { + if (error) { + notifications.show({ message: error.message, color: "red" }) + } else { + navigator.clipboard.writeText(inviteLink.inviteLink) + notifications.show({ message: "Invite link copied to clipboard!"}) + } + } + const onResend = async () => { await resendInvitationMutation.mutateAsync({ invitationId }); @@ -58,6 +71,8 @@ export default function InviteActionMenu({ invitationId }: Props) { {t("Resend invitation")} + Copy invite link + Resend invitation { + return useQuery({ + queryKey:["inviteLink",invitationId], + queryFn: () => getInviteLink({ invitationId }), + }) +} + export function useCreateInvitationMutation() { const queryClient = useQueryClient(); diff --git a/apps/client/src/features/workspace/services/workspace-service.ts b/apps/client/src/features/workspace/services/workspace-service.ts index e1bc2071..dd8bf8a4 100644 --- a/apps/client/src/features/workspace/services/workspace-service.ts +++ b/apps/client/src/features/workspace/services/workspace-service.ts @@ -5,6 +5,7 @@ import { IInvitation, IWorkspace, IAcceptInvite, + IInvitationLink, } from "../types/workspace.types"; import { IPagination, QueryParams } from "@/lib/types.ts"; @@ -53,6 +54,13 @@ export async function acceptInvitation(data: IAcceptInvite): Promise { await api.post("/workspace/invites/accept", data); } +export async function getInviteLink(data: { + invitationId: string; +}): Promise { + const req = await api.post("/workspace/invites/link", data); + return req.data; +} + export async function resendInvitation(data: { invitationId: string; }): Promise { diff --git a/apps/client/src/features/workspace/types/workspace.types.ts b/apps/client/src/features/workspace/types/workspace.types.ts index 5f424be5..faec5590 100644 --- a/apps/client/src/features/workspace/types/workspace.types.ts +++ b/apps/client/src/features/workspace/types/workspace.types.ts @@ -28,6 +28,10 @@ export interface IInvitation { createdAt: Date; } +export interface IInvitationLink { + inviteLink: string; +} + export interface IAcceptInvite { invitationId: string; name: string; diff --git a/apps/server/src/core/workspace/controllers/workspace.controller.ts b/apps/server/src/core/workspace/controllers/workspace.controller.ts index bb69e7d5..557f71e6 100644 --- a/apps/server/src/core/workspace/controllers/workspace.controller.ts +++ b/apps/server/src/core/workspace/controllers/workspace.controller.ts @@ -237,4 +237,30 @@ export class WorkspaceController { secure: this.environmentService.isHttps(), }); } + + @HttpCode(HttpStatus.OK) + @Post('invites/link') + async getInviteLink( + @Body() inviteDto: InvitationIdDto, + @AuthUser() user: User, + @AuthWorkspace() workspace: Workspace, + ) { + if (this.environmentService.isCloud()) { + throw new ForbiddenException(); + } + + const ability = this.workspaceAbility.createForUser(user, workspace); + if ( + ability.cannot(WorkspaceCaslAction.Manage, WorkspaceCaslSubject.Member) + ) { + throw new ForbiddenException(); + } + const inviteLink = + await this.workspaceInvitationService.getInvitationLinkById( + inviteDto.invitationId, + workspace.id, + ); + + return { inviteLink }; + } } diff --git a/apps/server/src/core/workspace/services/workspace-invitation.service.ts b/apps/server/src/core/workspace/services/workspace-invitation.service.ts index fa42208c..35a1f143 100644 --- a/apps/server/src/core/workspace/services/workspace-invitation.service.ts +++ b/apps/server/src/core/workspace/services/workspace-invitation.service.ts @@ -71,6 +71,21 @@ export class WorkspaceInvitationService { return invitation; } + async getInvitationTokenById(invitationId: string, workspaceId: string) { + const invitation = await this.db + .selectFrom('workspaceInvitations') + .select(['token']) + .where('id', '=', invitationId) + .where('workspaceId', '=', workspaceId) + .executeTakeFirst(); + + if (!invitation) { + throw new NotFoundException('Invitation not found'); + } + + return invitation; + } + async createInvitation( inviteUserDto: InviteUserDto, workspaceId: string, @@ -256,7 +271,6 @@ export class WorkspaceInvitationService { invitationId: string, workspaceId: string, ): Promise { - // const invitation = await this.db .selectFrom('workspaceInvitations') .selectAll() @@ -292,13 +306,28 @@ export class WorkspaceInvitationService { .execute(); } + async getInvitationLinkById( + invitationId: string, + workspaceId: string, + ): Promise { + const token = await this.getInvitationTokenById(invitationId, workspaceId); + return this.buildInviteLink(invitationId, token.token); + } + + async buildInviteLink( + invitationId: string, + inviteToken: string, + ): Promise { + return `${this.environmentService.getAppUrl()}/invites/${invitationId}?token=${inviteToken}`; + } + async sendInvitationMail( invitationId: string, inviteeEmail: string, inviteToken: string, invitedByName: string, ): Promise { - const inviteLink = `${this.environmentService.getAppUrl()}/invites/${invitationId}?token=${inviteToken}`; + const inviteLink = await this.buildInviteLink(invitationId, inviteToken); const emailTemplate = InvitationEmail({ inviteLink,