feat: add complex user invitation and management features

- Implemented complex member and invitation schemas using Zod for validation.
- Created new API endpoints for inviting, accepting, revoking, and canceling complex user invitations.
- Developed handlers for managing complex users, including listing, inviting, revoking, and canceling invitations.
- Added frontend components for displaying and managing complex users and invitations.
- Introduced session storage management for pending invitations.
- Integrated Gravatar for user avatars based on email.
- Created database migration for complex invitations table.
This commit is contained in:
Jose Selesan
2026-04-22 15:30:04 -03:00
parent 5f2a7fd494
commit 6ad4fc9ee9
37 changed files with 1935 additions and 78 deletions

View File

@@ -1,12 +1,10 @@
import { sentinelClient } from '@better-auth/infra/client';
import { createAuthClient } from 'better-auth/react';
import * as api from './api';
import { sentinelClient } from "@better-auth/infra/client";
const apiBaseUrl = api.apiBaseUrl;
const plugins = [
sentinelClient()
]
const plugins = [sentinelClient()];
export const authClient = createAuthClient({
baseURL: apiBaseUrl,

View File

@@ -1,8 +1,15 @@
import type {
AcceptComplexInvitationsInput,
AcceptComplexInvitationsResponse,
CancelComplexInvitationResponse,
Complex,
ComplexUsersOverview,
ComplexWithRole,
CreateComplexPayload,
CreateComplexResponse,
InviteComplexUserInput,
InviteComplexUserResponse,
RevokeComplexUserInput,
SelectComplexInput,
UpdateComplexPayload,
UpdateComplexResponse,
@@ -49,3 +56,47 @@ export async function select(payload: SelectComplexInput) {
);
return response.data;
}
export async function listUsers(id: string) {
const response = await http.get<ComplexUsersOverview>(`/api/complexes/${id}/users`);
return response.data;
}
export async function inviteUser(id: string, payload: InviteComplexUserInput) {
const response = await http.post<InviteComplexUserResponse>(
`/api/complexes/${id}/users`,
JSON.stringify(payload),
{ headers: { 'Content-Type': 'application/json' } }
);
return response.data;
}
export async function revokeUser(id: string, payload: RevokeComplexUserInput) {
const response = await http.delete<{ ok: boolean }>(
`/api/complexes/${id}/users/${payload.userId}`
);
return response.data;
}
export async function acceptPendingInvitations(payload: AcceptComplexInvitationsInput = {}) {
const response = await http.post<AcceptComplexInvitationsResponse>(
'/api/complexes/accept-pending',
JSON.stringify(payload),
{ headers: { 'Content-Type': 'application/json' } }
);
return response.data;
}
export async function resendInvitation(id: string, invitationId: string) {
const response = await http.post<InviteComplexUserResponse>(
`/api/complexes/${id}/invitations/${invitationId}/resend`
);
return response.data;
}
export async function cancelInvitation(id: string, invitationId: string) {
const response = await http.delete<CancelComplexInvitationResponse>(
`/api/complexes/${id}/invitations/${invitationId}`
);
return response.data;
}

View File

@@ -0,0 +1,71 @@
const PENDING_INVITE_EMAIL_KEY = 'pending-invite-email';
const PENDING_INVITE_TOKEN_KEY = 'pending-invite-token';
export type PendingInvite = {
email: string | null;
token: string | null;
};
function getSessionStorage(): Storage | null {
if (typeof window === 'undefined') {
return null;
}
return window.sessionStorage;
}
export function setPendingInvite(invite: PendingInvite) {
const storage = getSessionStorage();
if (!storage) return;
clearPendingInvite();
if (invite.email) {
storage.setItem(PENDING_INVITE_EMAIL_KEY, invite.email);
}
if (invite.token) {
storage.setItem(PENDING_INVITE_TOKEN_KEY, invite.token);
}
}
export function getPendingInvite(): PendingInvite {
const storage = getSessionStorage();
if (!storage) {
return {
email: null,
token: null,
};
}
return {
email: storage.getItem(PENDING_INVITE_EMAIL_KEY),
token: storage.getItem(PENDING_INVITE_TOKEN_KEY),
};
}
export function clearPendingInvite() {
const storage = getSessionStorage();
if (!storage) return;
storage.removeItem(PENDING_INVITE_EMAIL_KEY);
storage.removeItem(PENDING_INVITE_TOKEN_KEY);
}
export async function acceptStoredPendingInvite(
acceptPendingInvitations: (payload: { inviteToken?: string }) => Promise<unknown>
) {
const pendingInvite = getPendingInvite();
if (!pendingInvite.token) {
await acceptPendingInvitations({});
clearPendingInvite();
return;
}
await acceptPendingInvitations({ inviteToken: pendingInvite.token });
clearPendingInvite();
}