feat: per-user account management
This commit is contained in:
parent
0bf0094dbd
commit
de34fd841c
9 changed files with 229 additions and 12 deletions
|
|
@ -4,8 +4,8 @@
|
|||
import { type DefaultError, type InfiniteData, infiniteQueryOptions, queryOptions, type UseMutationOptions } from '@tanstack/react-query';
|
||||
|
||||
import { client } from '../client.gen';
|
||||
import { browseFilesystem, cancelDoctor, createBackupSchedule, createNotificationDestination, createRepository, createVolume, deleteBackupSchedule, deleteNotificationDestination, deleteRepository, deleteSnapshot, deleteSnapshots, deleteSsoInvitation, deleteSsoProvider, deleteVolume, devPanelExec, downloadResticPassword, dumpSnapshot, getAdminUsers, getBackupSchedule, getBackupScheduleForVolume, getDevPanel, getMirrorCompatibility, getNotificationDestination, getPublicSsoProviders, getRegistrationStatus, getRepository, getRepositoryStats, getScheduleMirrors, getScheduleNotifications, getSnapshotDetails, getSsoSettings, getStatus, getSystemInfo, getUpdates, getUserDeletionImpact, getVolume, healthCheckVolume, listBackupSchedules, listFiles, listNotificationDestinations, listRcloneRemotes, listRepositories, listSnapshotFiles, listSnapshots, listVolumes, mountVolume, type Options, refreshSnapshots, reorderBackupSchedules, restoreSnapshot, runBackupNow, runForget, setRegistrationStatus, startDoctor, stopBackup, tagSnapshots, testConnection, testNotificationDestination, unlockRepository, unmountVolume, updateBackupSchedule, updateNotificationDestination, updateRepository, updateScheduleMirrors, updateScheduleNotifications, updateSsoProviderAutoLinking, updateVolume } from '../sdk.gen';
|
||||
import type { BrowseFilesystemData, BrowseFilesystemResponse, CancelDoctorData, CancelDoctorResponse, CreateBackupScheduleData, CreateBackupScheduleResponse, CreateNotificationDestinationData, CreateNotificationDestinationResponse, CreateRepositoryData, CreateRepositoryResponse, CreateVolumeData, CreateVolumeResponse, DeleteBackupScheduleData, DeleteBackupScheduleResponse, DeleteNotificationDestinationData, DeleteNotificationDestinationResponse, DeleteRepositoryData, DeleteRepositoryResponse, DeleteSnapshotData, DeleteSnapshotResponse, DeleteSnapshotsData, DeleteSnapshotsResponse, DeleteSsoInvitationData, DeleteSsoProviderData, DeleteVolumeData, DeleteVolumeResponse, DevPanelExecData, DevPanelExecResponse, DownloadResticPasswordData, DownloadResticPasswordResponse, DumpSnapshotData, DumpSnapshotResponse, GetAdminUsersData, GetAdminUsersResponse, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponse, GetBackupScheduleResponse, GetDevPanelData, GetDevPanelResponse, GetMirrorCompatibilityData, GetMirrorCompatibilityResponse, GetNotificationDestinationData, GetNotificationDestinationResponse, GetPublicSsoProvidersData, GetPublicSsoProvidersResponse, GetRegistrationStatusData, GetRegistrationStatusResponse, GetRepositoryData, GetRepositoryResponse, GetRepositoryStatsData, GetRepositoryStatsResponse, GetScheduleMirrorsData, GetScheduleMirrorsResponse, GetScheduleNotificationsData, GetScheduleNotificationsResponse, GetSnapshotDetailsData, GetSnapshotDetailsResponse, GetSsoSettingsData, GetSsoSettingsResponse, GetStatusData, GetStatusResponse, GetSystemInfoData, GetSystemInfoResponse, GetUpdatesData, GetUpdatesResponse, GetUserDeletionImpactData, GetUserDeletionImpactResponse, GetVolumeData, GetVolumeResponse, HealthCheckVolumeData, HealthCheckVolumeResponse, ListBackupSchedulesData, ListBackupSchedulesResponse, ListFilesData, ListFilesResponse, ListNotificationDestinationsData, ListNotificationDestinationsResponse, ListRcloneRemotesData, ListRcloneRemotesResponse, ListRepositoriesData, ListRepositoriesResponse, ListSnapshotFilesData, ListSnapshotFilesResponse, ListSnapshotsData, ListSnapshotsResponse, ListVolumesData, ListVolumesResponse, MountVolumeData, MountVolumeResponse, RefreshSnapshotsData, RefreshSnapshotsResponse, ReorderBackupSchedulesData, ReorderBackupSchedulesResponse, RestoreSnapshotData, RestoreSnapshotResponse, RunBackupNowData, RunBackupNowResponse, RunForgetData, RunForgetResponse, SetRegistrationStatusData, SetRegistrationStatusResponse, StartDoctorData, StartDoctorResponse, StopBackupData, StopBackupResponse, TagSnapshotsData, TagSnapshotsResponse, TestConnectionData, TestConnectionResponse, TestNotificationDestinationData, TestNotificationDestinationResponse, UnlockRepositoryData, UnlockRepositoryResponse, UnmountVolumeData, UnmountVolumeResponse, UpdateBackupScheduleData, UpdateBackupScheduleResponse, UpdateNotificationDestinationData, UpdateNotificationDestinationResponse, UpdateRepositoryData, UpdateRepositoryResponse, UpdateScheduleMirrorsData, UpdateScheduleMirrorsResponse, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponse, UpdateSsoProviderAutoLinkingData, UpdateVolumeData, UpdateVolumeResponse } from '../types.gen';
|
||||
import { browseFilesystem, cancelDoctor, createBackupSchedule, createNotificationDestination, createRepository, createVolume, deleteBackupSchedule, deleteNotificationDestination, deleteRepository, deleteSnapshot, deleteSnapshots, deleteSsoInvitation, deleteSsoProvider, deleteUserAccount, deleteVolume, devPanelExec, downloadResticPassword, dumpSnapshot, getAdminUsers, getBackupSchedule, getBackupScheduleForVolume, getDevPanel, getMirrorCompatibility, getNotificationDestination, getPublicSsoProviders, getRegistrationStatus, getRepository, getRepositoryStats, getScheduleMirrors, getScheduleNotifications, getSnapshotDetails, getSsoSettings, getStatus, getSystemInfo, getUpdates, getUserDeletionImpact, getVolume, healthCheckVolume, listBackupSchedules, listFiles, listNotificationDestinations, listRcloneRemotes, listRepositories, listSnapshotFiles, listSnapshots, listVolumes, mountVolume, type Options, refreshSnapshots, reorderBackupSchedules, restoreSnapshot, runBackupNow, runForget, setRegistrationStatus, startDoctor, stopBackup, tagSnapshots, testConnection, testNotificationDestination, unlockRepository, unmountVolume, updateBackupSchedule, updateNotificationDestination, updateRepository, updateScheduleMirrors, updateScheduleNotifications, updateSsoProviderAutoLinking, updateVolume } from '../sdk.gen';
|
||||
import type { BrowseFilesystemData, BrowseFilesystemResponse, CancelDoctorData, CancelDoctorResponse, CreateBackupScheduleData, CreateBackupScheduleResponse, CreateNotificationDestinationData, CreateNotificationDestinationResponse, CreateRepositoryData, CreateRepositoryResponse, CreateVolumeData, CreateVolumeResponse, DeleteBackupScheduleData, DeleteBackupScheduleResponse, DeleteNotificationDestinationData, DeleteNotificationDestinationResponse, DeleteRepositoryData, DeleteRepositoryResponse, DeleteSnapshotData, DeleteSnapshotResponse, DeleteSnapshotsData, DeleteSnapshotsResponse, DeleteSsoInvitationData, DeleteSsoProviderData, DeleteUserAccountData, DeleteVolumeData, DeleteVolumeResponse, DevPanelExecData, DevPanelExecResponse, DownloadResticPasswordData, DownloadResticPasswordResponse, DumpSnapshotData, DumpSnapshotResponse, GetAdminUsersData, GetAdminUsersResponse, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponse, GetBackupScheduleResponse, GetDevPanelData, GetDevPanelResponse, GetMirrorCompatibilityData, GetMirrorCompatibilityResponse, GetNotificationDestinationData, GetNotificationDestinationResponse, GetPublicSsoProvidersData, GetPublicSsoProvidersResponse, GetRegistrationStatusData, GetRegistrationStatusResponse, GetRepositoryData, GetRepositoryResponse, GetRepositoryStatsData, GetRepositoryStatsResponse, GetScheduleMirrorsData, GetScheduleMirrorsResponse, GetScheduleNotificationsData, GetScheduleNotificationsResponse, GetSnapshotDetailsData, GetSnapshotDetailsResponse, GetSsoSettingsData, GetSsoSettingsResponse, GetStatusData, GetStatusResponse, GetSystemInfoData, GetSystemInfoResponse, GetUpdatesData, GetUpdatesResponse, GetUserDeletionImpactData, GetUserDeletionImpactResponse, GetVolumeData, GetVolumeResponse, HealthCheckVolumeData, HealthCheckVolumeResponse, ListBackupSchedulesData, ListBackupSchedulesResponse, ListFilesData, ListFilesResponse, ListNotificationDestinationsData, ListNotificationDestinationsResponse, ListRcloneRemotesData, ListRcloneRemotesResponse, ListRepositoriesData, ListRepositoriesResponse, ListSnapshotFilesData, ListSnapshotFilesResponse, ListSnapshotsData, ListSnapshotsResponse, ListVolumesData, ListVolumesResponse, MountVolumeData, MountVolumeResponse, RefreshSnapshotsData, RefreshSnapshotsResponse, ReorderBackupSchedulesData, ReorderBackupSchedulesResponse, RestoreSnapshotData, RestoreSnapshotResponse, RunBackupNowData, RunBackupNowResponse, RunForgetData, RunForgetResponse, SetRegistrationStatusData, SetRegistrationStatusResponse, StartDoctorData, StartDoctorResponse, StopBackupData, StopBackupResponse, TagSnapshotsData, TagSnapshotsResponse, TestConnectionData, TestConnectionResponse, TestNotificationDestinationData, TestNotificationDestinationResponse, UnlockRepositoryData, UnlockRepositoryResponse, UnmountVolumeData, UnmountVolumeResponse, UpdateBackupScheduleData, UpdateBackupScheduleResponse, UpdateNotificationDestinationData, UpdateNotificationDestinationResponse, UpdateRepositoryData, UpdateRepositoryResponse, UpdateScheduleMirrorsData, UpdateScheduleMirrorsResponse, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponse, UpdateSsoProviderAutoLinkingData, UpdateVolumeData, UpdateVolumeResponse } from '../types.gen';
|
||||
|
||||
export type QueryKey<TOptions extends Options> = [
|
||||
Pick<TOptions, 'baseUrl' | 'body' | 'headers' | 'path' | 'query'> & {
|
||||
|
|
@ -163,6 +163,23 @@ export const getAdminUsersOptions = (options?: Options<GetAdminUsersData>) => qu
|
|||
queryKey: getAdminUsersQueryKey(options)
|
||||
});
|
||||
|
||||
/**
|
||||
* Delete an account linked to a user
|
||||
*/
|
||||
export const deleteUserAccountMutation = (options?: Partial<Options<DeleteUserAccountData>>): UseMutationOptions<unknown, DefaultError, Options<DeleteUserAccountData>> => {
|
||||
const mutationOptions: UseMutationOptions<unknown, DefaultError, Options<DeleteUserAccountData>> = {
|
||||
mutationFn: async (fnOptions) => {
|
||||
const { data } = await deleteUserAccount({
|
||||
...options,
|
||||
...fnOptions,
|
||||
throwOnError: true
|
||||
});
|
||||
return data;
|
||||
}
|
||||
};
|
||||
return mutationOptions;
|
||||
};
|
||||
|
||||
export const getUserDeletionImpactQueryKey = (options: Options<GetUserDeletionImpactData>) => createQueryKey('getUserDeletionImpact', options);
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export {
|
|||
deleteSnapshots,
|
||||
deleteSsoInvitation,
|
||||
deleteSsoProvider,
|
||||
deleteUserAccount,
|
||||
deleteVolume,
|
||||
devPanelExec,
|
||||
downloadResticPassword,
|
||||
|
|
@ -113,6 +114,9 @@ export type {
|
|||
DeleteSsoProviderData,
|
||||
DeleteSsoProviderErrors,
|
||||
DeleteSsoProviderResponses,
|
||||
DeleteUserAccountData,
|
||||
DeleteUserAccountErrors,
|
||||
DeleteUserAccountResponses,
|
||||
DeleteVolumeData,
|
||||
DeleteVolumeResponse,
|
||||
DeleteVolumeResponses,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import type { Client, Options as Options2, TDataShape } from './client';
|
||||
import { client } from './client.gen';
|
||||
import type { BrowseFilesystemData, BrowseFilesystemResponses, CancelDoctorData, CancelDoctorErrors, CancelDoctorResponses, CreateBackupScheduleData, CreateBackupScheduleResponses, CreateNotificationDestinationData, CreateNotificationDestinationResponses, CreateRepositoryData, CreateRepositoryResponses, CreateVolumeData, CreateVolumeResponses, DeleteBackupScheduleData, DeleteBackupScheduleResponses, DeleteNotificationDestinationData, DeleteNotificationDestinationErrors, DeleteNotificationDestinationResponses, DeleteRepositoryData, DeleteRepositoryResponses, DeleteSnapshotData, DeleteSnapshotResponses, DeleteSnapshotsData, DeleteSnapshotsResponses, DeleteSsoInvitationData, DeleteSsoInvitationErrors, DeleteSsoInvitationResponses, DeleteSsoProviderData, DeleteSsoProviderErrors, DeleteSsoProviderResponses, DeleteVolumeData, DeleteVolumeResponses, DevPanelExecData, DevPanelExecErrors, DevPanelExecResponses, DownloadResticPasswordData, DownloadResticPasswordResponses, DumpSnapshotData, DumpSnapshotResponses, GetAdminUsersData, GetAdminUsersResponses, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponses, GetBackupScheduleResponses, GetDevPanelData, GetDevPanelResponses, GetMirrorCompatibilityData, GetMirrorCompatibilityResponses, GetNotificationDestinationData, GetNotificationDestinationErrors, GetNotificationDestinationResponses, GetPublicSsoProvidersData, GetPublicSsoProvidersResponses, GetRegistrationStatusData, GetRegistrationStatusResponses, GetRepositoryData, GetRepositoryResponses, GetRepositoryStatsData, GetRepositoryStatsResponses, GetScheduleMirrorsData, GetScheduleMirrorsResponses, GetScheduleNotificationsData, GetScheduleNotificationsResponses, GetSnapshotDetailsData, GetSnapshotDetailsResponses, GetSsoSettingsData, GetSsoSettingsResponses, GetStatusData, GetStatusResponses, GetSystemInfoData, GetSystemInfoResponses, GetUpdatesData, GetUpdatesResponses, GetUserDeletionImpactData, GetUserDeletionImpactResponses, GetVolumeData, GetVolumeErrors, GetVolumeResponses, HealthCheckVolumeData, HealthCheckVolumeErrors, HealthCheckVolumeResponses, ListBackupSchedulesData, ListBackupSchedulesResponses, ListFilesData, ListFilesResponses, ListNotificationDestinationsData, ListNotificationDestinationsResponses, ListRcloneRemotesData, ListRcloneRemotesResponses, ListRepositoriesData, ListRepositoriesResponses, ListSnapshotFilesData, ListSnapshotFilesResponses, ListSnapshotsData, ListSnapshotsResponses, ListVolumesData, ListVolumesResponses, MountVolumeData, MountVolumeResponses, RefreshSnapshotsData, RefreshSnapshotsResponses, ReorderBackupSchedulesData, ReorderBackupSchedulesResponses, RestoreSnapshotData, RestoreSnapshotResponses, RunBackupNowData, RunBackupNowResponses, RunForgetData, RunForgetResponses, SetRegistrationStatusData, SetRegistrationStatusResponses, StartDoctorData, StartDoctorErrors, StartDoctorResponses, StopBackupData, StopBackupErrors, StopBackupResponses, TagSnapshotsData, TagSnapshotsResponses, TestConnectionData, TestConnectionResponses, TestNotificationDestinationData, TestNotificationDestinationErrors, TestNotificationDestinationResponses, UnlockRepositoryData, UnlockRepositoryResponses, UnmountVolumeData, UnmountVolumeResponses, UpdateBackupScheduleData, UpdateBackupScheduleResponses, UpdateNotificationDestinationData, UpdateNotificationDestinationErrors, UpdateNotificationDestinationResponses, UpdateRepositoryData, UpdateRepositoryErrors, UpdateRepositoryResponses, UpdateScheduleMirrorsData, UpdateScheduleMirrorsResponses, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponses, UpdateSsoProviderAutoLinkingData, UpdateSsoProviderAutoLinkingErrors, UpdateSsoProviderAutoLinkingResponses, UpdateVolumeData, UpdateVolumeErrors, UpdateVolumeResponses } from './types.gen';
|
||||
import type { BrowseFilesystemData, BrowseFilesystemResponses, CancelDoctorData, CancelDoctorErrors, CancelDoctorResponses, CreateBackupScheduleData, CreateBackupScheduleResponses, CreateNotificationDestinationData, CreateNotificationDestinationResponses, CreateRepositoryData, CreateRepositoryResponses, CreateVolumeData, CreateVolumeResponses, DeleteBackupScheduleData, DeleteBackupScheduleResponses, DeleteNotificationDestinationData, DeleteNotificationDestinationErrors, DeleteNotificationDestinationResponses, DeleteRepositoryData, DeleteRepositoryResponses, DeleteSnapshotData, DeleteSnapshotResponses, DeleteSnapshotsData, DeleteSnapshotsResponses, DeleteSsoInvitationData, DeleteSsoInvitationErrors, DeleteSsoInvitationResponses, DeleteSsoProviderData, DeleteSsoProviderErrors, DeleteSsoProviderResponses, DeleteUserAccountData, DeleteUserAccountErrors, DeleteUserAccountResponses, DeleteVolumeData, DeleteVolumeResponses, DevPanelExecData, DevPanelExecErrors, DevPanelExecResponses, DownloadResticPasswordData, DownloadResticPasswordResponses, DumpSnapshotData, DumpSnapshotResponses, GetAdminUsersData, GetAdminUsersResponses, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponses, GetBackupScheduleResponses, GetDevPanelData, GetDevPanelResponses, GetMirrorCompatibilityData, GetMirrorCompatibilityResponses, GetNotificationDestinationData, GetNotificationDestinationErrors, GetNotificationDestinationResponses, GetPublicSsoProvidersData, GetPublicSsoProvidersResponses, GetRegistrationStatusData, GetRegistrationStatusResponses, GetRepositoryData, GetRepositoryResponses, GetRepositoryStatsData, GetRepositoryStatsResponses, GetScheduleMirrorsData, GetScheduleMirrorsResponses, GetScheduleNotificationsData, GetScheduleNotificationsResponses, GetSnapshotDetailsData, GetSnapshotDetailsResponses, GetSsoSettingsData, GetSsoSettingsResponses, GetStatusData, GetStatusResponses, GetSystemInfoData, GetSystemInfoResponses, GetUpdatesData, GetUpdatesResponses, GetUserDeletionImpactData, GetUserDeletionImpactResponses, GetVolumeData, GetVolumeErrors, GetVolumeResponses, HealthCheckVolumeData, HealthCheckVolumeErrors, HealthCheckVolumeResponses, ListBackupSchedulesData, ListBackupSchedulesResponses, ListFilesData, ListFilesResponses, ListNotificationDestinationsData, ListNotificationDestinationsResponses, ListRcloneRemotesData, ListRcloneRemotesResponses, ListRepositoriesData, ListRepositoriesResponses, ListSnapshotFilesData, ListSnapshotFilesResponses, ListSnapshotsData, ListSnapshotsResponses, ListVolumesData, ListVolumesResponses, MountVolumeData, MountVolumeResponses, RefreshSnapshotsData, RefreshSnapshotsResponses, ReorderBackupSchedulesData, ReorderBackupSchedulesResponses, RestoreSnapshotData, RestoreSnapshotResponses, RunBackupNowData, RunBackupNowResponses, RunForgetData, RunForgetResponses, SetRegistrationStatusData, SetRegistrationStatusResponses, StartDoctorData, StartDoctorErrors, StartDoctorResponses, StopBackupData, StopBackupErrors, StopBackupResponses, TagSnapshotsData, TagSnapshotsResponses, TestConnectionData, TestConnectionResponses, TestNotificationDestinationData, TestNotificationDestinationErrors, TestNotificationDestinationResponses, UnlockRepositoryData, UnlockRepositoryResponses, UnmountVolumeData, UnmountVolumeResponses, UpdateBackupScheduleData, UpdateBackupScheduleResponses, UpdateNotificationDestinationData, UpdateNotificationDestinationErrors, UpdateNotificationDestinationResponses, UpdateRepositoryData, UpdateRepositoryErrors, UpdateRepositoryResponses, UpdateScheduleMirrorsData, UpdateScheduleMirrorsResponses, UpdateScheduleNotificationsData, UpdateScheduleNotificationsResponses, UpdateSsoProviderAutoLinkingData, UpdateSsoProviderAutoLinkingErrors, UpdateSsoProviderAutoLinkingResponses, UpdateVolumeData, UpdateVolumeErrors, UpdateVolumeResponses } from './types.gen';
|
||||
|
||||
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<TData, ThrowOnError> & {
|
||||
/**
|
||||
|
|
@ -61,6 +61,11 @@ export const deleteSsoInvitation = <ThrowOnError extends boolean = false>(option
|
|||
*/
|
||||
export const getAdminUsers = <ThrowOnError extends boolean = false>(options?: Options<GetAdminUsersData, ThrowOnError>) => (options?.client ?? client).get<GetAdminUsersResponses, unknown, ThrowOnError>({ url: '/api/v1/auth/admin-users', ...options });
|
||||
|
||||
/**
|
||||
* Delete an account linked to a user
|
||||
*/
|
||||
export const deleteUserAccount = <ThrowOnError extends boolean = false>(options: Options<DeleteUserAccountData, ThrowOnError>) => (options.client ?? client).delete<DeleteUserAccountResponses, DeleteUserAccountErrors, ThrowOnError>({ url: '/api/v1/auth/admin-users/{userId}/accounts/{accountId}', ...options });
|
||||
|
||||
/**
|
||||
* Get impact of deleting a user
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -167,6 +167,10 @@ export type GetAdminUsersResponses = {
|
|||
offset: number;
|
||||
total: number;
|
||||
users: Array<{
|
||||
accounts: Array<{
|
||||
id: string;
|
||||
providerId: string;
|
||||
}>;
|
||||
banned: boolean;
|
||||
email: string;
|
||||
id: string;
|
||||
|
|
@ -178,6 +182,34 @@ export type GetAdminUsersResponses = {
|
|||
|
||||
export type GetAdminUsersResponse = GetAdminUsersResponses[keyof GetAdminUsersResponses];
|
||||
|
||||
export type DeleteUserAccountData = {
|
||||
body?: never;
|
||||
path: {
|
||||
userId: string;
|
||||
accountId: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/api/v1/auth/admin-users/{userId}/accounts/{accountId}';
|
||||
};
|
||||
|
||||
export type DeleteUserAccountErrors = {
|
||||
/**
|
||||
* Forbidden
|
||||
*/
|
||||
403: unknown;
|
||||
/**
|
||||
* Cannot delete the last account
|
||||
*/
|
||||
409: unknown;
|
||||
};
|
||||
|
||||
export type DeleteUserAccountResponses = {
|
||||
/**
|
||||
* Account deleted successfully
|
||||
*/
|
||||
200: unknown;
|
||||
};
|
||||
|
||||
export type GetUserDeletionImpactData = {
|
||||
body?: never;
|
||||
path: {
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ export function decodeLoginError(error?: string): LoginErrorCode | null {
|
|||
export function getLoginErrorDescription(errorCode: LoginErrorCode | null): string | null {
|
||||
switch (errorCode) {
|
||||
case "ACCOUNT_LINK_REQUIRED":
|
||||
return "Your account exists but is not linked to this organization SSO provider. Sign in with username/password first, then link SSO from settings.";
|
||||
return "Your account exists but is not linked to this SSO provider. Sign in with username/password first, then enable auto linking in your provider settings or contact your administrator.";
|
||||
case "EMAIL_NOT_VERIFIED":
|
||||
return "Your identity provider did not mark your email as verified.";
|
||||
case "INVITE_REQUIRED":
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useMutation, useQuery, useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { Shield, ShieldAlert, UserCheck, Trash2, Search, AlertTriangle, Ban } from "lucide-react";
|
||||
import { Shield, ShieldAlert, UserCheck, Trash2, Search, AlertTriangle, Ban, KeyRound } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { authClient } from "~/client/lib/auth-client";
|
||||
|
|
@ -17,12 +17,21 @@ import {
|
|||
DialogTitle,
|
||||
} from "~/client/components/ui/dialog";
|
||||
import { CreateUserDialog } from "./create-user-dialog";
|
||||
import { getAdminUsersOptions, getUserDeletionImpactOptions } from "~/client/api-client/@tanstack/react-query.gen";
|
||||
import {
|
||||
getAdminUsersOptions,
|
||||
getUserDeletionImpactOptions,
|
||||
deleteUserAccountMutation,
|
||||
} from "~/client/api-client/@tanstack/react-query.gen";
|
||||
|
||||
export function UserManagement({ currentUser }: { currentUser: { id: string } | undefined | null }) {
|
||||
const [search, setSearch] = useState("");
|
||||
const [userToDelete, setUserToDelete] = useState<string | null>(null);
|
||||
const [userToBan, setUserToBan] = useState<{ id: string; name: string; isBanned: boolean } | null>(null);
|
||||
const [userToManageAccounts, setUserToManageAccounts] = useState<{
|
||||
id: string;
|
||||
name: string;
|
||||
accounts: { id: string; providerId: string }[];
|
||||
} | null>(null);
|
||||
|
||||
const { data: deletionImpact, isLoading: isLoadingImpact } = useQuery({
|
||||
...getUserDeletionImpactOptions({ path: { userId: userToDelete ?? "" } }),
|
||||
|
|
@ -74,6 +83,23 @@ export function UserManagement({ currentUser }: { currentUser: { id: string } |
|
|||
},
|
||||
});
|
||||
|
||||
const deleteAccount = useMutation({
|
||||
...deleteUserAccountMutation(),
|
||||
onSuccess: (_data, variables) => {
|
||||
toast.success("Account removed successfully");
|
||||
setUserToManageAccounts((prev) => {
|
||||
if (!prev) return null;
|
||||
return {
|
||||
...prev,
|
||||
accounts: prev.accounts.filter((a) => a.id !== variables.path.accountId),
|
||||
};
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error("Failed to remove account", { description: error.message });
|
||||
},
|
||||
});
|
||||
|
||||
const normalizedSearch = search.toLowerCase();
|
||||
const filteredUsers = data.users.filter((user) => {
|
||||
const name = user.name?.toLowerCase() ?? "";
|
||||
|
|
@ -135,12 +161,12 @@ export function UserManagement({ currentUser }: { currentUser: { id: string } |
|
|||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className={cn("flex justify-end gap-2", { hidden: user.id === currentUser?.id })}>
|
||||
<div className={cn("flex justify-end gap-2")}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
title="Demote to User"
|
||||
className={cn({ hidden: user.role !== "admin" })}
|
||||
className={cn({ hidden: user.role !== "admin" || user.id === currentUser?.id })}
|
||||
onClick={() => setRoleMutation.mutate({ userId: user.id, role: "user" })}
|
||||
>
|
||||
<ShieldAlert className="h-4 w-4" />
|
||||
|
|
@ -149,7 +175,7 @@ export function UserManagement({ currentUser }: { currentUser: { id: string } |
|
|||
variant="ghost"
|
||||
size="icon"
|
||||
title="Promote to Admin"
|
||||
className={cn({ hidden: user.role === "admin" })}
|
||||
className={cn({ hidden: user.role === "admin" || user.id === currentUser?.id })}
|
||||
onClick={() => setRoleMutation.mutate({ userId: user.id, role: "admin" })}
|
||||
>
|
||||
<Shield className="h-4 w-4" />
|
||||
|
|
@ -159,7 +185,7 @@ export function UserManagement({ currentUser }: { currentUser: { id: string } |
|
|||
variant="ghost"
|
||||
size="icon"
|
||||
title="Unban user"
|
||||
className={cn({ hidden: !user.banned })}
|
||||
className={cn({ hidden: !user.banned || user.id === currentUser?.id })}
|
||||
onClick={() => setUserToBan({ id: user.id, name: user.name ?? user.email, isBanned: true })}
|
||||
>
|
||||
<UserCheck className="h-4 w-4" />
|
||||
|
|
@ -168,15 +194,36 @@ export function UserManagement({ currentUser }: { currentUser: { id: string } |
|
|||
variant="ghost"
|
||||
size="icon"
|
||||
title="Ban user"
|
||||
className={cn({ hidden: !!user.banned })}
|
||||
className={cn({ hidden: !!user.banned || user.id === currentUser?.id })}
|
||||
onClick={() => setUserToBan({ id: user.id, name: user.name ?? user.email, isBanned: false })}
|
||||
>
|
||||
<Ban className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<Button variant="ghost" size="icon" title="Delete User" onClick={() => setUserToDelete(user.id)}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
title="Delete User"
|
||||
className={cn({ hidden: user.id === currentUser?.id })}
|
||||
onClick={() => setUserToDelete(user.id)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
title="Manage Accounts"
|
||||
onClick={() =>
|
||||
setUserToManageAccounts({
|
||||
id: user.id,
|
||||
name: user.name ?? user.email,
|
||||
accounts: user.accounts,
|
||||
})
|
||||
}
|
||||
>
|
||||
<KeyRound className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
|
@ -271,6 +318,45 @@ export function UserManagement({ currentUser }: { currentUser: { id: string } |
|
|||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={Boolean(userToManageAccounts)} onOpenChange={(open) => !open && setUserToManageAccounts(null)}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Manage Accounts</DialogTitle>
|
||||
<DialogDescription>Linked authentication accounts for {userToManageAccounts?.name}.</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-2">
|
||||
{userToManageAccounts?.accounts.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground text-center py-4">No accounts linked.</p>
|
||||
) : null}
|
||||
{userToManageAccounts?.accounts.map((account) => (
|
||||
<div key={account.id} className="flex items-center justify-between p-3 border rounded-md">
|
||||
<span className="text-sm font-medium">{account.providerId}</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
title="Remove account"
|
||||
disabled={deleteAccount.isPending}
|
||||
onClick={() =>
|
||||
deleteAccount.mutate({
|
||||
path: { userId: userToManageAccounts.id, accountId: account.id },
|
||||
})
|
||||
}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setUserToManageAccounts(null)}>
|
||||
Close
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {
|
|||
deleteSsoInvitationDto,
|
||||
updateSsoProviderAutoLinkingBody,
|
||||
updateSsoProviderAutoLinkingDto,
|
||||
deleteUserAccountDto,
|
||||
} from "./auth.dto";
|
||||
import { authService } from "./auth.service";
|
||||
import { requireAdmin, requireAuth } from "./auth.middleware";
|
||||
|
|
@ -100,6 +101,9 @@ export const authController = new Hono()
|
|||
query: { limit: 100 },
|
||||
});
|
||||
|
||||
const userIds = usersData.users.map((u) => u.id);
|
||||
const accountsByUser = await authService.getUserAccounts(userIds);
|
||||
|
||||
return c.json<AdminUsersDto>({
|
||||
users: usersData.users.map((adminUser) => ({
|
||||
id: adminUser.id,
|
||||
|
|
@ -107,12 +111,25 @@ export const authController = new Hono()
|
|||
email: adminUser.email,
|
||||
role: adminUser.role ?? "user",
|
||||
banned: Boolean(adminUser.banned),
|
||||
accounts: accountsByUser[adminUser.id] ?? [],
|
||||
})),
|
||||
total: usersData.total,
|
||||
limit: "limit" in usersData ? (usersData.limit ?? 100) : 100,
|
||||
offset: "offset" in usersData ? (usersData.offset ?? 0) : 0,
|
||||
});
|
||||
})
|
||||
.delete("/admin-users/:userId/accounts/:accountId", requireAuth, requireAdmin, deleteUserAccountDto, async (c) => {
|
||||
const userId = c.req.param("userId");
|
||||
const accountId = c.req.param("accountId");
|
||||
|
||||
const result = await authService.deleteUserAccount(userId, accountId);
|
||||
|
||||
if (result.lastAccount) {
|
||||
return c.json({ message: "Cannot delete the last account of a user" }, 409);
|
||||
}
|
||||
|
||||
return c.json({ success: true });
|
||||
})
|
||||
.get("/deletion-impact/:userId", requireAuth, requireAdmin, getUserDeletionImpactDto, async (c) => {
|
||||
const userId = c.req.param("userId");
|
||||
const impact = await authService.getUserDeletionImpact(userId);
|
||||
|
|
|
|||
|
|
@ -73,6 +73,10 @@ export const adminUsersResponse = type({
|
|||
email: "string",
|
||||
role: "string",
|
||||
banned: "boolean",
|
||||
accounts: type({
|
||||
id: "string",
|
||||
providerId: "string",
|
||||
}).array(),
|
||||
}).array(),
|
||||
total: "number",
|
||||
limit: "number",
|
||||
|
|
@ -173,6 +177,23 @@ export const deleteSsoInvitationDto = describeRoute({
|
|||
},
|
||||
});
|
||||
|
||||
export const deleteUserAccountDto = describeRoute({
|
||||
description: "Delete an account linked to a user",
|
||||
operationId: "deleteUserAccount",
|
||||
tags: ["Auth"],
|
||||
responses: {
|
||||
200: {
|
||||
description: "Account deleted successfully",
|
||||
},
|
||||
403: {
|
||||
description: "Forbidden",
|
||||
},
|
||||
409: {
|
||||
description: "Cannot delete the last account",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const updateSsoProviderAutoLinkingBody = type({
|
||||
enabled: "boolean",
|
||||
});
|
||||
|
|
|
|||
|
|
@ -152,6 +152,41 @@ export class AuthService {
|
|||
async deleteSsoInvitation(invitationId: string): Promise<void> {
|
||||
await db.delete(invitation).where(eq(invitation.id, invitationId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch accounts for a list of users, keyed by userId
|
||||
*/
|
||||
async getUserAccounts(userIds: string[]): Promise<Record<string, { id: string; providerId: string }[]>> {
|
||||
if (userIds.length === 0) return {};
|
||||
|
||||
const accounts = await db
|
||||
.select({ id: account.id, providerId: account.providerId, userId: account.userId })
|
||||
.from(account)
|
||||
.where(inArray(account.userId, userIds));
|
||||
|
||||
const grouped: Record<string, { id: string; providerId: string }[]> = {};
|
||||
for (const row of accounts) {
|
||||
if (!grouped[row.userId]) {
|
||||
grouped[row.userId] = [];
|
||||
}
|
||||
grouped[row.userId].push({ id: row.id, providerId: row.providerId });
|
||||
}
|
||||
return grouped;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a single account for a user, refusing if it is the last one
|
||||
*/
|
||||
async deleteUserAccount(userId: string, accountId: string): Promise<{ lastAccount: boolean }> {
|
||||
const userAccounts = await db.select({ id: account.id }).from(account).where(eq(account.userId, userId));
|
||||
|
||||
if (userAccounts.length <= 1) {
|
||||
return { lastAccount: true };
|
||||
}
|
||||
|
||||
await db.delete(account).where(and(eq(account.id, accountId), eq(account.userId, userId)));
|
||||
return { lastAccount: false };
|
||||
}
|
||||
}
|
||||
|
||||
export const authService = new AuthService();
|
||||
|
|
|
|||
Loading…
Reference in a new issue