bugfix: invitation link copy option disapearing fixed. route problem for unauthorizrd users opening invitation link fixed.
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
107
frontend/src/lib/hooks/useOrganizationInviteLinkCopy.ts
Normal file
107
frontend/src/lib/hooks/useOrganizationInviteLinkCopy.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user