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:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
71
apps/frontend/src/lib/invitations.ts
Normal file
71
apps/frontend/src/lib/invitations.ts
Normal 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();
|
||||
}
|
||||
Reference in New Issue
Block a user