bugfix: invitation link copy option disapearing fixed. route problem for unauthorizrd users opening invitation link fixed.

This commit is contained in:
2026-05-17 00:02:13 +03:30
parent 627219df58
commit 2d43254af2
14 changed files with 561 additions and 205 deletions

View File

@@ -15,6 +15,17 @@ export const apiClient = axios.create({
timeout: 10000,
});
/** Invitation preview/accept must work with no cookies (public API, no JWT). */
function isPublicInvitationRequest(url: string | undefined): boolean {
if (!url) return false;
return (
url.includes('/staff/invitations/preview') ||
url.includes('/staff/invitations/accept') ||
url.includes('/organizations/invitations/preview') ||
url.includes('/organizations/invitations/accept')
);
}
// ❌ REMOVE request interceptor completely (no Authorization header)
// ✅ Response interceptor
@@ -23,7 +34,11 @@ apiClient.interceptors.response.use(
async (error: AxiosError) => {
const originalRequest = error.config as CustomAxiosRequestConfig;
if (error.response?.status === 401 && !originalRequest._retry) {
if (
error.response?.status === 401 &&
!originalRequest._retry &&
!isPublicInvitationRequest(originalRequest.url)
) {
originalRequest._retry = true;
try {

View File

@@ -63,6 +63,21 @@ export const staffApi = {
return response.data;
},
getInvitationLink: async (
membershipId: string,
): Promise<{
success: boolean;
data: {
membershipId: string;
invitationId: string;
email: string;
invitationUrl: string;
};
}> => {
const response = await apiClient.post(`/staff/members/${membershipId}/invitation-link`);
return response.data;
},
previewInvite: async (token: string): Promise<PreviewInviteResponse> => {
const response = await apiClient.get(`/staff/invitations/preview?token=${encodeURIComponent(token)}`);
return response.data;

View File

@@ -0,0 +1,107 @@
'use client';
import { useCallback, useEffect, useState } from 'react';
import {
organizationApi,
type OrganizationInvitationHistoryItemDto,
} from '@/lib/api/organization';
import {
canShareOrganizationInviteLink,
readOrganizationInviteLinks,
type StoredOrganizationInviteLink,
writeOrganizationInviteLinks,
} from '@/components/invitations/organizationInviteLinks';
export function useOrganizationInviteLinkCopy(organizationId: string | undefined) {
const [pendingInviteLinks, setPendingInviteLinks] = useState<
Record<string, StoredOrganizationInviteLink>
>({});
const [copiedId, setCopiedId] = useState<string | null>(null);
const [copyingInvitationId, setCopyingInvitationId] = useState<string | null>(null);
useEffect(() => {
if (!organizationId) return;
setPendingInviteLinks(readOrganizationInviteLinks(organizationId));
}, [organizationId]);
const storeInviteLink = useCallback(
(invitationId: string, ownerEmail: string, invitationUrl: string) => {
if (!organizationId) return;
setPendingInviteLinks((prev) => {
const next = {
...prev,
[invitationId]: {
invitationId,
ownerEmail: ownerEmail.trim().toLowerCase(),
invitationUrl,
},
};
writeOrganizationInviteLinks(organizationId, next);
return next;
});
},
[organizationId],
);
const pruneAcceptedLinks = useCallback(
(items: OrganizationInvitationHistoryItemDto[]) => {
if (!organizationId) return;
const acceptedIds = new Set(
items.filter((item) => item.acceptedAt || item.status === 'ACTIVE').map((item) => item.id),
);
setPendingInviteLinks((prev) => {
let changed = false;
const next = { ...prev };
for (const id of Object.keys(next)) {
if (acceptedIds.has(id)) {
delete next[id];
changed = true;
}
}
if (changed) {
writeOrganizationInviteLinks(organizationId, next);
}
return changed ? next : prev;
});
},
[organizationId],
);
const copyInvitationLink = useCallback(
async (
invitation: OrganizationInvitationHistoryItemDto,
options?: { onRegenerated?: () => void | Promise<void> },
): Promise<string | null> => {
if (!canShareOrganizationInviteLink(invitation)) return null;
setCopyingInvitationId(invitation.id);
try {
let invitationUrl = pendingInviteLinks[invitation.id]?.invitationUrl;
if (!invitationUrl || invitation.status === 'EXPIRED') {
const res = await organizationApi.getInvitationLink(invitation.id);
invitationUrl = res.data.invitationUrl;
storeInviteLink(invitation.id, invitation.ownerEmail, invitationUrl);
await options?.onRegenerated?.();
}
await navigator.clipboard.writeText(invitationUrl);
setCopiedId(invitation.id);
setTimeout(() => setCopiedId(null), 1500);
return invitationUrl;
} finally {
setCopyingInvitationId(null);
}
},
[pendingInviteLinks, storeInviteLink],
);
return {
pendingInviteLinks,
copiedId,
copyingInvitationId,
storeInviteLink,
copyInvitationLink,
pruneAcceptedLinks,
};
}