feat: separate global admin page (#595)
This commit is contained in:
parent
e6a8b83900
commit
f686c1aa16
21 changed files with 796 additions and 108 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, deleteUserAccount, deleteVolume, devPanelExec, downloadResticPassword, dumpSnapshot, getAdminUsers, getBackupProgress, 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, GetBackupProgressData, GetBackupProgressResponse, 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, getBackupProgress, getBackupSchedule, getBackupScheduleForVolume, getDevPanel, getMirrorCompatibility, getNotificationDestination, getOrgMembers, 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, removeOrgMember, reorderBackupSchedules, restoreSnapshot, runBackupNow, runForget, setRegistrationStatus, startDoctor, stopBackup, tagSnapshots, testConnection, testNotificationDestination, unlockRepository, unmountVolume, updateBackupSchedule, updateMemberRole, 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, GetBackupProgressData, GetBackupProgressResponse, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponse, GetBackupScheduleResponse, GetDevPanelData, GetDevPanelResponse, GetMirrorCompatibilityData, GetMirrorCompatibilityResponse, GetNotificationDestinationData, GetNotificationDestinationResponse, GetOrgMembersData, GetOrgMembersResponse, 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, RemoveOrgMemberData, 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, UpdateMemberRoleData, 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'> & {
|
||||
|
|
@ -198,6 +198,58 @@ export const getUserDeletionImpactOptions = (options: Options<GetUserDeletionImp
|
|||
queryKey: getUserDeletionImpactQueryKey(options)
|
||||
});
|
||||
|
||||
export const getOrgMembersQueryKey = (options?: Options<GetOrgMembersData>) => createQueryKey('getOrgMembers', options);
|
||||
|
||||
/**
|
||||
* Get members of the active organization
|
||||
*/
|
||||
export const getOrgMembersOptions = (options?: Options<GetOrgMembersData>) => queryOptions<GetOrgMembersResponse, DefaultError, GetOrgMembersResponse, ReturnType<typeof getOrgMembersQueryKey>>({
|
||||
queryFn: async ({ queryKey, signal }) => {
|
||||
const { data } = await getOrgMembers({
|
||||
...options,
|
||||
...queryKey[0],
|
||||
signal,
|
||||
throwOnError: true
|
||||
});
|
||||
return data;
|
||||
},
|
||||
queryKey: getOrgMembersQueryKey(options)
|
||||
});
|
||||
|
||||
/**
|
||||
* Update a member's role in the active organization
|
||||
*/
|
||||
export const updateMemberRoleMutation = (options?: Partial<Options<UpdateMemberRoleData>>): UseMutationOptions<unknown, DefaultError, Options<UpdateMemberRoleData>> => {
|
||||
const mutationOptions: UseMutationOptions<unknown, DefaultError, Options<UpdateMemberRoleData>> = {
|
||||
mutationFn: async (fnOptions) => {
|
||||
const { data } = await updateMemberRole({
|
||||
...options,
|
||||
...fnOptions,
|
||||
throwOnError: true
|
||||
});
|
||||
return data;
|
||||
}
|
||||
};
|
||||
return mutationOptions;
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove a member from the active organization
|
||||
*/
|
||||
export const removeOrgMemberMutation = (options?: Partial<Options<RemoveOrgMemberData>>): UseMutationOptions<unknown, DefaultError, Options<RemoveOrgMemberData>> => {
|
||||
const mutationOptions: UseMutationOptions<unknown, DefaultError, Options<RemoveOrgMemberData>> = {
|
||||
mutationFn: async (fnOptions) => {
|
||||
const { data } = await removeOrgMember({
|
||||
...options,
|
||||
...fnOptions,
|
||||
throwOnError: true
|
||||
});
|
||||
return data;
|
||||
}
|
||||
};
|
||||
return mutationOptions;
|
||||
};
|
||||
|
||||
export const listVolumesQueryKey = (options?: Options<ListVolumesData>) => createQueryKey('listVolumes', options);
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ export {
|
|||
getDevPanel,
|
||||
getMirrorCompatibility,
|
||||
getNotificationDestination,
|
||||
getOrgMembers,
|
||||
getPublicSsoProviders,
|
||||
getRegistrationStatus,
|
||||
getRepository,
|
||||
|
|
@ -52,6 +53,7 @@ export {
|
|||
mountVolume,
|
||||
type Options,
|
||||
refreshSnapshots,
|
||||
removeOrgMember,
|
||||
reorderBackupSchedules,
|
||||
restoreSnapshot,
|
||||
runBackupNow,
|
||||
|
|
@ -65,6 +67,7 @@ export {
|
|||
unlockRepository,
|
||||
unmountVolume,
|
||||
updateBackupSchedule,
|
||||
updateMemberRole,
|
||||
updateNotificationDestination,
|
||||
updateRepository,
|
||||
updateScheduleMirrors,
|
||||
|
|
@ -153,6 +156,9 @@ export type {
|
|||
GetNotificationDestinationErrors,
|
||||
GetNotificationDestinationResponse,
|
||||
GetNotificationDestinationResponses,
|
||||
GetOrgMembersData,
|
||||
GetOrgMembersResponse,
|
||||
GetOrgMembersResponses,
|
||||
GetPublicSsoProvidersData,
|
||||
GetPublicSsoProvidersResponse,
|
||||
GetPublicSsoProvidersResponses,
|
||||
|
|
@ -227,6 +233,9 @@ export type {
|
|||
RefreshSnapshotsData,
|
||||
RefreshSnapshotsResponse,
|
||||
RefreshSnapshotsResponses,
|
||||
RemoveOrgMemberData,
|
||||
RemoveOrgMemberErrors,
|
||||
RemoveOrgMemberResponses,
|
||||
ReorderBackupSchedulesData,
|
||||
ReorderBackupSchedulesResponse,
|
||||
ReorderBackupSchedulesResponses,
|
||||
|
|
@ -269,6 +278,9 @@ export type {
|
|||
UpdateBackupScheduleData,
|
||||
UpdateBackupScheduleResponse,
|
||||
UpdateBackupScheduleResponses,
|
||||
UpdateMemberRoleData,
|
||||
UpdateMemberRoleErrors,
|
||||
UpdateMemberRoleResponses,
|
||||
UpdateNotificationDestinationData,
|
||||
UpdateNotificationDestinationErrors,
|
||||
UpdateNotificationDestinationResponse,
|
||||
|
|
|
|||
|
|
@ -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, DeleteUserAccountData, DeleteUserAccountErrors, DeleteUserAccountResponses, DeleteVolumeData, DeleteVolumeResponses, DevPanelExecData, DevPanelExecErrors, DevPanelExecResponses, DownloadResticPasswordData, DownloadResticPasswordResponses, DumpSnapshotData, DumpSnapshotResponses, GetAdminUsersData, GetAdminUsersResponses, GetBackupProgressData, GetBackupProgressResponses, 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, GetBackupProgressData, GetBackupProgressResponses, GetBackupScheduleData, GetBackupScheduleForVolumeData, GetBackupScheduleForVolumeResponses, GetBackupScheduleResponses, GetDevPanelData, GetDevPanelResponses, GetMirrorCompatibilityData, GetMirrorCompatibilityResponses, GetNotificationDestinationData, GetNotificationDestinationErrors, GetNotificationDestinationResponses, GetOrgMembersData, GetOrgMembersResponses, 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, RemoveOrgMemberData, RemoveOrgMemberErrors, RemoveOrgMemberResponses, 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, UpdateMemberRoleData, UpdateMemberRoleErrors, UpdateMemberRoleResponses, 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> & {
|
||||
/**
|
||||
|
|
@ -71,6 +71,28 @@ export const deleteUserAccount = <ThrowOnError extends boolean = false>(options:
|
|||
*/
|
||||
export const getUserDeletionImpact = <ThrowOnError extends boolean = false>(options: Options<GetUserDeletionImpactData, ThrowOnError>) => (options.client ?? client).get<GetUserDeletionImpactResponses, unknown, ThrowOnError>({ url: '/api/v1/auth/deletion-impact/{userId}', ...options });
|
||||
|
||||
/**
|
||||
* Get members of the active organization
|
||||
*/
|
||||
export const getOrgMembers = <ThrowOnError extends boolean = false>(options?: Options<GetOrgMembersData, ThrowOnError>) => (options?.client ?? client).get<GetOrgMembersResponses, unknown, ThrowOnError>({ url: '/api/v1/auth/org-members', ...options });
|
||||
|
||||
/**
|
||||
* Update a member's role in the active organization
|
||||
*/
|
||||
export const updateMemberRole = <ThrowOnError extends boolean = false>(options: Options<UpdateMemberRoleData, ThrowOnError>) => (options.client ?? client).patch<UpdateMemberRoleResponses, UpdateMemberRoleErrors, ThrowOnError>({
|
||||
url: '/api/v1/auth/org-members/{memberId}/role',
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Remove a member from the active organization
|
||||
*/
|
||||
export const removeOrgMember = <ThrowOnError extends boolean = false>(options: Options<RemoveOrgMemberData, ThrowOnError>) => (options.client ?? client).delete<RemoveOrgMemberResponses, RemoveOrgMemberErrors, ThrowOnError>({ url: '/api/v1/auth/org-members/{memberId}', ...options });
|
||||
|
||||
/**
|
||||
* List all volumes
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -240,6 +240,89 @@ export type GetUserDeletionImpactResponses = {
|
|||
|
||||
export type GetUserDeletionImpactResponse = GetUserDeletionImpactResponses[keyof GetUserDeletionImpactResponses];
|
||||
|
||||
export type GetOrgMembersData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: '/api/v1/auth/org-members';
|
||||
};
|
||||
|
||||
export type GetOrgMembersResponses = {
|
||||
/**
|
||||
* List of organization members
|
||||
*/
|
||||
200: {
|
||||
members: Array<{
|
||||
createdAt: string;
|
||||
id: string;
|
||||
role: string;
|
||||
user: {
|
||||
email: string;
|
||||
name: string | null;
|
||||
};
|
||||
userId: string;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
|
||||
export type GetOrgMembersResponse = GetOrgMembersResponses[keyof GetOrgMembersResponses];
|
||||
|
||||
export type UpdateMemberRoleData = {
|
||||
body?: {
|
||||
role: 'admin' | 'member';
|
||||
};
|
||||
path: {
|
||||
memberId: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/api/v1/auth/org-members/{memberId}/role';
|
||||
};
|
||||
|
||||
export type UpdateMemberRoleErrors = {
|
||||
/**
|
||||
* Forbidden
|
||||
*/
|
||||
403: unknown;
|
||||
/**
|
||||
* Member not found
|
||||
*/
|
||||
404: unknown;
|
||||
};
|
||||
|
||||
export type UpdateMemberRoleResponses = {
|
||||
/**
|
||||
* Member role updated successfully
|
||||
*/
|
||||
200: unknown;
|
||||
};
|
||||
|
||||
export type RemoveOrgMemberData = {
|
||||
body?: never;
|
||||
path: {
|
||||
memberId: string;
|
||||
};
|
||||
query?: never;
|
||||
url: '/api/v1/auth/org-members/{memberId}';
|
||||
};
|
||||
|
||||
export type RemoveOrgMemberErrors = {
|
||||
/**
|
||||
* Forbidden
|
||||
*/
|
||||
403: unknown;
|
||||
/**
|
||||
* Member not found
|
||||
*/
|
||||
404: unknown;
|
||||
};
|
||||
|
||||
export type RemoveOrgMemberResponses = {
|
||||
/**
|
||||
* Member removed successfully
|
||||
*/
|
||||
200: unknown;
|
||||
};
|
||||
|
||||
export type ListVolumesData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Bell, CalendarClock, Database, HardDrive, Settings } from "lucide-react";
|
||||
import { Bell, CalendarClock, Database, HardDrive, Settings, ShieldCheck } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Sidebar,
|
||||
|
|
@ -10,6 +10,7 @@ import {
|
|||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarSeparator,
|
||||
useSidebar,
|
||||
} from "~/client/components/ui/sidebar";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "~/client/components/ui/tooltip";
|
||||
|
|
@ -49,7 +50,11 @@ const items = [
|
|||
},
|
||||
];
|
||||
|
||||
export function AppSidebar() {
|
||||
type Props = {
|
||||
isInstanceAdmin: boolean;
|
||||
};
|
||||
|
||||
export function AppSidebar({ isInstanceAdmin }: Props) {
|
||||
const { state, isMobile, setOpenMobile } = useSidebar();
|
||||
const { updates, hasUpdate } = useUpdates();
|
||||
const [showReleaseNotes, setShowReleaseNotes] = useState(false);
|
||||
|
|
@ -131,6 +136,63 @@ export function AppSidebar() {
|
|||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
{isInstanceAdmin && (
|
||||
<>
|
||||
<SidebarSeparator />
|
||||
<SidebarGroup>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<SidebarMenuButton asChild className="relative overflow-hidden">
|
||||
<Link
|
||||
to="/admin"
|
||||
onClick={() => isMobile && setOpenMobile(false)}
|
||||
activeProps={{ className: "bg-strong-accent/10" }}
|
||||
className="w-full flex items-center gap-2"
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<>
|
||||
{isActive && (
|
||||
<div
|
||||
className={cn("absolute left-0 top-0 h-full w-0.75 bg-strong-accent mr-2", {
|
||||
hidden: isCollapsed,
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
<ShieldCheck
|
||||
className={cn("transition-all duration-200", {
|
||||
"text-strong-accent": isActive,
|
||||
"ml-1": isActive && !isCollapsed,
|
||||
"text-muted-foreground": !isActive,
|
||||
})}
|
||||
/>
|
||||
<span
|
||||
className={cn({
|
||||
"text-white font-medium": isActive,
|
||||
"text-muted-foreground": !isActive,
|
||||
})}
|
||||
>
|
||||
Administration
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" className={cn({ hidden: !isCollapsed })}>
|
||||
<p>Administration</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</>
|
||||
)}
|
||||
</SidebarContent>
|
||||
<SidebarFooter className="p-4 border-r border-border/50">
|
||||
<OrganizationSwitcher />
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export function Layout({ loaderData }: Props) {
|
|||
|
||||
return (
|
||||
<SidebarProvider defaultOpen={loaderData.sidebarOpen}>
|
||||
<AppSidebar />
|
||||
<AppSidebar isInstanceAdmin={loaderData.user?.role === "admin"} />
|
||||
<div className="w-full relative flex flex-col min-h-screen md:h-screen md:overflow-hidden">
|
||||
<header className="z-50 bg-card-header border-b border-border/50 shrink-0 h-16.25">
|
||||
<div className="flex items-center h-full justify-between px-2 sm:px-8 mx-auto container gap-4">
|
||||
|
|
|
|||
|
|
@ -344,7 +344,7 @@ function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
|
|||
data-slot="sidebar-content"
|
||||
data-sidebar="content"
|
||||
className={cn(
|
||||
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
|
||||
"flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto overflow-x-hidden group-data-[collapsible=icon]:overflow-hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
100
app/client/modules/admin/routes/admin-page.tsx
Normal file
100
app/client/modules/admin/routes/admin-page.tsx
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import { useMutation, useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||
import { Users, Settings as SettingsIcon } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
getRegistrationStatusOptions,
|
||||
setRegistrationStatusMutation,
|
||||
} from "~/client/api-client/@tanstack/react-query.gen";
|
||||
import { Card, CardContent, CardDescription, CardTitle } from "~/client/components/ui/card";
|
||||
import { Label } from "~/client/components/ui/label";
|
||||
import { Switch } from "~/client/components/ui/switch";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/client/components/ui/tabs";
|
||||
import type { AppContext } from "~/context";
|
||||
import { UserManagement } from "~/client/modules/settings/components/user-management";
|
||||
|
||||
type Props = {
|
||||
appContext: AppContext;
|
||||
};
|
||||
|
||||
export function AdminPage({ appContext }: Props) {
|
||||
const { tab } = useSearch({ from: "/(dashboard)/admin/" });
|
||||
const activeTab = tab || "users";
|
||||
const navigate = useNavigate();
|
||||
|
||||
const registrationStatus = useSuspenseQuery({
|
||||
...getRegistrationStatusOptions(),
|
||||
});
|
||||
|
||||
const updateRegistrationStatusMutation = useMutation({
|
||||
...setRegistrationStatusMutation(),
|
||||
onSuccess: () => {
|
||||
toast.success("Registration settings updated");
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error("Failed to update registration settings", {
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const onTabChange = (value: string) => {
|
||||
void navigate({ to: ".", search: () => ({ tab: value }) });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Tabs value={activeTab} onValueChange={onTabChange} className="w-full">
|
||||
<TabsList>
|
||||
<TabsTrigger value="users">Users</TabsTrigger>
|
||||
<TabsTrigger value="system">System</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<div className="mt-2">
|
||||
<TabsContent value="users" className="mt-0">
|
||||
<Card className="p-0 gap-0">
|
||||
<div className="border-b border-border/50 bg-card-header p-6">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Users className="size-5" />
|
||||
User Management
|
||||
</CardTitle>
|
||||
<CardDescription className="mt-1.5">Manage users, roles and permissions</CardDescription>
|
||||
</div>
|
||||
<UserManagement currentUser={appContext.user} />
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="system" className="mt-0">
|
||||
<Card className="p-0 gap-0">
|
||||
<div className="border-b border-border/50 bg-card-header p-6">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<SettingsIcon className="size-5" />
|
||||
System Settings
|
||||
</CardTitle>
|
||||
<CardDescription className="mt-1.5">Manage system-wide settings</CardDescription>
|
||||
</div>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="enable-registrations" className="text-base">
|
||||
Enable new user registrations
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground max-w-2xl">When enabled, new users can sign up</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="enable-registrations"
|
||||
checked={registrationStatus.data.enabled}
|
||||
onCheckedChange={(checked) =>
|
||||
updateRegistrationStatusMutation.mutate({ body: { enabled: checked } })
|
||||
}
|
||||
disabled={updateRegistrationStatusMutation.isPending}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</div>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
173
app/client/modules/settings/components/org-members-section.tsx
Normal file
173
app/client/modules/settings/components/org-members-section.tsx
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
import { useMutation, useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { Shield, ShieldAlert, Trash2 } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
getOrgMembersOptions,
|
||||
removeOrgMemberMutation,
|
||||
updateMemberRoleMutation,
|
||||
} from "~/client/api-client/@tanstack/react-query.gen";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "~/client/components/ui/alert-dialog";
|
||||
import { Badge } from "~/client/components/ui/badge";
|
||||
import { Button } from "~/client/components/ui/button";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/client/components/ui/table";
|
||||
import { useOrganizationContext } from "~/client/hooks/use-org-context";
|
||||
import { cn } from "~/client/lib/utils";
|
||||
|
||||
export function OrgMembersSection() {
|
||||
const { activeMember } = useOrganizationContext();
|
||||
const [memberToRemove, setMemberToRemove] = useState<{ id: string; name: string } | null>(null);
|
||||
|
||||
const orgMembersQuery = useSuspenseQuery({
|
||||
...getOrgMembersOptions(),
|
||||
});
|
||||
|
||||
const updateRole = useMutation({
|
||||
...updateMemberRoleMutation(),
|
||||
onSuccess: () => {
|
||||
toast.success("Member role updated");
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error("Failed to update role", { description: error.message });
|
||||
},
|
||||
});
|
||||
|
||||
const removeMember = useMutation({
|
||||
...removeOrgMemberMutation(),
|
||||
onSuccess: () => {
|
||||
toast.success("Member removed from organization");
|
||||
setMemberToRemove(null);
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error("Failed to remove member", { description: error.message });
|
||||
},
|
||||
});
|
||||
|
||||
const members = orgMembersQuery.data.members;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Member</TableHead>
|
||||
<TableHead>Role</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow className={cn({ hidden: members.length > 0 })}>
|
||||
<TableCell colSpan={3} className="text-center text-sm text-muted-foreground">
|
||||
No members found.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{members.map((member) => {
|
||||
const isOwner = member.role === "owner";
|
||||
const isSelf = member.id === activeMember?.id;
|
||||
|
||||
return (
|
||||
<TableRow key={member.id}>
|
||||
<TableCell>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{member.user.name ?? member.user.email}</span>
|
||||
<span className="text-sm text-muted-foreground">{member.user.email}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">{member.role}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
title="Demote to Member"
|
||||
className={cn({ hidden: member.role !== "admin" || isSelf })}
|
||||
disabled={updateRole.isPending}
|
||||
onClick={() =>
|
||||
updateRole.mutate({
|
||||
path: { memberId: member.id },
|
||||
body: { role: "member" },
|
||||
})
|
||||
}
|
||||
>
|
||||
<ShieldAlert className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
title="Promote to Admin"
|
||||
className={cn({ hidden: member.role !== "member" || isSelf })}
|
||||
disabled={updateRole.isPending}
|
||||
onClick={() =>
|
||||
updateRole.mutate({
|
||||
path: { memberId: member.id },
|
||||
body: { role: "admin" },
|
||||
})
|
||||
}
|
||||
>
|
||||
<Shield className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<AlertDialog
|
||||
open={memberToRemove?.id === member.id}
|
||||
onOpenChange={(open) => !open && setMemberToRemove(null)}
|
||||
>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
title="Remove member"
|
||||
className={cn({ hidden: isOwner || isSelf })}
|
||||
onClick={() =>
|
||||
setMemberToRemove({
|
||||
id: member.id,
|
||||
name: member.user.name ?? member.user.email,
|
||||
})
|
||||
}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Remove member</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to remove <strong>{memberToRemove?.name}</strong> from this
|
||||
organization? They will lose access to all organization resources.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
disabled={removeMember.isPending}
|
||||
onClick={() => removeMember.mutate({ path: { memberId: memberToRemove!.id } })}
|
||||
>
|
||||
Remove
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -42,13 +42,10 @@ export function SsoSettingsSection() {
|
|||
const [inviteEmail, setInviteEmail] = useState("");
|
||||
const [inviteRole, setInviteRole] = useState<InvitationRole>("member");
|
||||
|
||||
const { data } = useSuspenseQuery({
|
||||
const ssoSettingsQuery = useSuspenseQuery({
|
||||
...getSsoSettingsOptions(),
|
||||
});
|
||||
|
||||
const providers = data.providers;
|
||||
const invitations = data.invitations;
|
||||
|
||||
const updateProviderAutoLinkingMutation = useMutation({
|
||||
...updateSsoProviderAutoLinkingMutation(),
|
||||
onSuccess: (_, v) => {
|
||||
|
|
@ -125,6 +122,9 @@ export function SsoSettingsSection() {
|
|||
},
|
||||
});
|
||||
|
||||
const providers = ssoSettingsQuery.data.providers;
|
||||
const invitations = ssoSettingsQuery.data.invitations;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-3">
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ export function CreateSsoProviderPage() {
|
|||
},
|
||||
onSuccess: () => {
|
||||
toast.success("SSO provider registered successfully");
|
||||
void navigate({ to: "/settings", search: { tab: "users" } });
|
||||
void navigate({ to: "/settings", search: { tab: "organization" } });
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error("Failed to register provider", { description: error.message });
|
||||
|
|
@ -242,7 +242,7 @@ export function CreateSsoProviderPage() {
|
|||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={() => void navigate({ to: "/settings", search: { tab: "users" } })}
|
||||
onClick={() => void navigate({ to: "/settings", search: { tab: "organization" } })}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,8 @@
|
|||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { Download, KeyRound, User, X, Users, Settings as SettingsIcon } from "lucide-react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { Download, KeyRound, User, X, Settings as SettingsIcon, Building2 } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
downloadResticPasswordMutation,
|
||||
setRegistrationStatusMutation,
|
||||
getRegistrationStatusOptions,
|
||||
} from "~/client/api-client/@tanstack/react-query.gen";
|
||||
import { downloadResticPasswordMutation } from "~/client/api-client/@tanstack/react-query.gen";
|
||||
import { Button } from "~/client/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardTitle } from "~/client/components/ui/card";
|
||||
import {
|
||||
|
|
@ -20,14 +16,14 @@ import {
|
|||
} from "~/client/components/ui/dialog";
|
||||
import { Input } from "~/client/components/ui/input";
|
||||
import { Label } from "~/client/components/ui/label";
|
||||
import { Switch } from "~/client/components/ui/switch";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/client/components/ui/tabs";
|
||||
import { authClient } from "~/client/lib/auth-client";
|
||||
import { type AppContext } from "~/context";
|
||||
import { TwoFactorSection } from "../components/two-factor-section";
|
||||
import { UserManagement } from "../components/user-management";
|
||||
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||
import { SsoSettingsSection } from "../components/sso/sso-settings-section";
|
||||
import { OrgMembersSection } from "../components/org-members-section";
|
||||
import { useOrganizationContext } from "~/client/hooks/use-org-context";
|
||||
|
||||
type Props = {
|
||||
appContext: AppContext;
|
||||
|
|
@ -45,25 +41,8 @@ export function SettingsPage({ appContext }: Props) {
|
|||
const activeTab = tab || "account";
|
||||
|
||||
const navigate = useNavigate();
|
||||
const isAdmin = appContext.user?.role === "admin";
|
||||
|
||||
const registrationStatusQuery = useQuery({
|
||||
...getRegistrationStatusOptions(),
|
||||
enabled: isAdmin,
|
||||
});
|
||||
|
||||
const updateRegistrationStatusMutation = useMutation({
|
||||
...setRegistrationStatusMutation(),
|
||||
onSuccess: () => {
|
||||
toast.success("Registration settings updated");
|
||||
void registrationStatusQuery.refetch();
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error("Failed to update registration settings", {
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
const { activeMember } = useOrganizationContext();
|
||||
const isOrgAdmin = activeMember?.role === "owner" || activeMember?.role === "admin";
|
||||
|
||||
const handleLogout = async () => {
|
||||
await authClient.signOut({
|
||||
|
|
@ -166,8 +145,7 @@ export function SettingsPage({ appContext }: Props) {
|
|||
<Tabs value={activeTab} onValueChange={onTabChange} className="w-full">
|
||||
<TabsList>
|
||||
<TabsTrigger value="account">Account</TabsTrigger>
|
||||
{isAdmin && <TabsTrigger value="users">Users & Authentication</TabsTrigger>}
|
||||
{isAdmin && <TabsTrigger value="system">System</TabsTrigger>}
|
||||
{isOrgAdmin && <TabsTrigger value="organization">Organization</TabsTrigger>}
|
||||
</TabsList>
|
||||
|
||||
<div className="mt-2">
|
||||
|
|
@ -307,17 +285,19 @@ export function SettingsPage({ appContext }: Props) {
|
|||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{isAdmin && (
|
||||
<TabsContent value="users" className="mt-0 space-y-4">
|
||||
{isOrgAdmin && (
|
||||
<TabsContent value="organization" className="mt-0 space-y-4">
|
||||
<Card className="p-0 gap-0">
|
||||
<div className="border-b border-border/50 bg-card-header p-6">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Users className="size-5" />
|
||||
User Management
|
||||
<Building2 className="size-5" />
|
||||
Members
|
||||
</CardTitle>
|
||||
<CardDescription className="mt-1.5">Manage users, roles and permissions</CardDescription>
|
||||
<CardDescription className="mt-1.5">Manage organization members and roles</CardDescription>
|
||||
</div>
|
||||
<UserManagement currentUser={appContext.user} />
|
||||
<CardContent className="p-6">
|
||||
<OrgMembersSection />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="p-0 gap-0">
|
||||
|
|
@ -334,38 +314,6 @@ export function SettingsPage({ appContext }: Props) {
|
|||
</Card>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
{isAdmin && (
|
||||
<TabsContent value="system" className="mt-0">
|
||||
<Card className="p-0 gap-0">
|
||||
<div className="border-b border-border/50 bg-card-header p-6">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<SettingsIcon className="size-5" />
|
||||
System Settings
|
||||
</CardTitle>
|
||||
<CardDescription className="mt-1.5">Manage system-wide settings</CardDescription>
|
||||
</div>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="enable-registrations" className="text-base">
|
||||
Enable new user registrations
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground max-w-2xl">When enabled, new users can sign up</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="enable-registrations"
|
||||
checked={registrationStatusQuery.data?.enabled ?? false}
|
||||
onCheckedChange={(checked) =>
|
||||
updateRegistrationStatusMutation.mutate({ body: { enabled: checked } })
|
||||
}
|
||||
disabled={registrationStatusQuery.isLoading || updateRegistrationStatusMutation.isPending}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
)}
|
||||
</div>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import { Route as dashboardSettingsIndexRouteImport } from './routes/(dashboard)
|
|||
import { Route as dashboardRepositoriesIndexRouteImport } from './routes/(dashboard)/repositories/index'
|
||||
import { Route as dashboardNotificationsIndexRouteImport } from './routes/(dashboard)/notifications/index'
|
||||
import { Route as dashboardBackupsIndexRouteImport } from './routes/(dashboard)/backups/index'
|
||||
import { Route as dashboardAdminIndexRouteImport } from './routes/(dashboard)/admin/index'
|
||||
import { Route as dashboardVolumesCreateRouteImport } from './routes/(dashboard)/volumes/create'
|
||||
import { Route as dashboardVolumesVolumeIdRouteImport } from './routes/(dashboard)/volumes/$volumeId'
|
||||
import { Route as dashboardRepositoriesCreateRouteImport } from './routes/(dashboard)/repositories/create'
|
||||
|
|
@ -96,6 +97,11 @@ const dashboardBackupsIndexRoute = dashboardBackupsIndexRouteImport.update({
|
|||
path: '/backups/',
|
||||
getParentRoute: () => dashboardRouteRoute,
|
||||
} as any)
|
||||
const dashboardAdminIndexRoute = dashboardAdminIndexRouteImport.update({
|
||||
id: '/admin/',
|
||||
path: '/admin/',
|
||||
getParentRoute: () => dashboardRouteRoute,
|
||||
} as any)
|
||||
const dashboardVolumesCreateRoute = dashboardVolumesCreateRouteImport.update({
|
||||
id: '/volumes/create',
|
||||
path: '/volumes/create',
|
||||
|
|
@ -190,6 +196,7 @@ export interface FileRoutesByFullPath {
|
|||
'/repositories/create': typeof dashboardRepositoriesCreateRoute
|
||||
'/volumes/$volumeId': typeof dashboardVolumesVolumeIdRoute
|
||||
'/volumes/create': typeof dashboardVolumesCreateRoute
|
||||
'/admin/': typeof dashboardAdminIndexRoute
|
||||
'/backups/': typeof dashboardBackupsIndexRoute
|
||||
'/notifications/': typeof dashboardNotificationsIndexRoute
|
||||
'/repositories/': typeof dashboardRepositoriesIndexRoute
|
||||
|
|
@ -216,6 +223,7 @@ export interface FileRoutesByTo {
|
|||
'/repositories/create': typeof dashboardRepositoriesCreateRoute
|
||||
'/volumes/$volumeId': typeof dashboardVolumesVolumeIdRoute
|
||||
'/volumes/create': typeof dashboardVolumesCreateRoute
|
||||
'/admin': typeof dashboardAdminIndexRoute
|
||||
'/backups': typeof dashboardBackupsIndexRoute
|
||||
'/notifications': typeof dashboardNotificationsIndexRoute
|
||||
'/repositories': typeof dashboardRepositoriesIndexRoute
|
||||
|
|
@ -245,6 +253,7 @@ export interface FileRoutesById {
|
|||
'/(dashboard)/repositories/create': typeof dashboardRepositoriesCreateRoute
|
||||
'/(dashboard)/volumes/$volumeId': typeof dashboardVolumesVolumeIdRoute
|
||||
'/(dashboard)/volumes/create': typeof dashboardVolumesCreateRoute
|
||||
'/(dashboard)/admin/': typeof dashboardAdminIndexRoute
|
||||
'/(dashboard)/backups/': typeof dashboardBackupsIndexRoute
|
||||
'/(dashboard)/notifications/': typeof dashboardNotificationsIndexRoute
|
||||
'/(dashboard)/repositories/': typeof dashboardRepositoriesIndexRoute
|
||||
|
|
@ -273,6 +282,7 @@ export interface FileRouteTypes {
|
|||
| '/repositories/create'
|
||||
| '/volumes/$volumeId'
|
||||
| '/volumes/create'
|
||||
| '/admin/'
|
||||
| '/backups/'
|
||||
| '/notifications/'
|
||||
| '/repositories/'
|
||||
|
|
@ -299,6 +309,7 @@ export interface FileRouteTypes {
|
|||
| '/repositories/create'
|
||||
| '/volumes/$volumeId'
|
||||
| '/volumes/create'
|
||||
| '/admin'
|
||||
| '/backups'
|
||||
| '/notifications'
|
||||
| '/repositories'
|
||||
|
|
@ -327,6 +338,7 @@ export interface FileRouteTypes {
|
|||
| '/(dashboard)/repositories/create'
|
||||
| '/(dashboard)/volumes/$volumeId'
|
||||
| '/(dashboard)/volumes/create'
|
||||
| '/(dashboard)/admin/'
|
||||
| '/(dashboard)/backups/'
|
||||
| '/(dashboard)/notifications/'
|
||||
| '/(dashboard)/repositories/'
|
||||
|
|
@ -434,6 +446,13 @@ declare module '@tanstack/react-router' {
|
|||
preLoaderRoute: typeof dashboardBackupsIndexRouteImport
|
||||
parentRoute: typeof dashboardRouteRoute
|
||||
}
|
||||
'/(dashboard)/admin/': {
|
||||
id: '/(dashboard)/admin/'
|
||||
path: '/admin'
|
||||
fullPath: '/admin/'
|
||||
preLoaderRoute: typeof dashboardAdminIndexRouteImport
|
||||
parentRoute: typeof dashboardRouteRoute
|
||||
}
|
||||
'/(dashboard)/volumes/create': {
|
||||
id: '/(dashboard)/volumes/create'
|
||||
path: '/volumes/create'
|
||||
|
|
@ -570,6 +589,7 @@ interface dashboardRouteRouteChildren {
|
|||
dashboardRepositoriesCreateRoute: typeof dashboardRepositoriesCreateRoute
|
||||
dashboardVolumesVolumeIdRoute: typeof dashboardVolumesVolumeIdRoute
|
||||
dashboardVolumesCreateRoute: typeof dashboardVolumesCreateRoute
|
||||
dashboardAdminIndexRoute: typeof dashboardAdminIndexRoute
|
||||
dashboardBackupsIndexRoute: typeof dashboardBackupsIndexRoute
|
||||
dashboardNotificationsIndexRoute: typeof dashboardNotificationsIndexRoute
|
||||
dashboardRepositoriesIndexRoute: typeof dashboardRepositoriesIndexRoute
|
||||
|
|
@ -592,6 +612,7 @@ const dashboardRouteRouteChildren: dashboardRouteRouteChildren = {
|
|||
dashboardRepositoriesCreateRoute: dashboardRepositoriesCreateRoute,
|
||||
dashboardVolumesVolumeIdRoute: dashboardVolumesVolumeIdRoute,
|
||||
dashboardVolumesCreateRoute: dashboardVolumesCreateRoute,
|
||||
dashboardAdminIndexRoute: dashboardAdminIndexRoute,
|
||||
dashboardBackupsIndexRoute: dashboardBackupsIndexRoute,
|
||||
dashboardNotificationsIndexRoute: dashboardNotificationsIndexRoute,
|
||||
dashboardRepositoriesIndexRoute: dashboardRepositoriesIndexRoute,
|
||||
|
|
|
|||
30
app/routes/(dashboard)/admin/index.tsx
Normal file
30
app/routes/(dashboard)/admin/index.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||
import { type } from "arktype";
|
||||
import { fetchUser } from "../route";
|
||||
import type { AppContext } from "~/context";
|
||||
import { AdminPage } from "~/client/modules/admin/routes/admin-page";
|
||||
import { getAdminUsersOptions } from "~/client/api-client/@tanstack/react-query.gen";
|
||||
|
||||
export const Route = createFileRoute("/(dashboard)/admin/")({
|
||||
validateSearch: type({ tab: "string?" }),
|
||||
component: RouteComponent,
|
||||
loader: async ({ context }) => {
|
||||
const authContext = await fetchUser();
|
||||
|
||||
if (authContext.user?.role !== "admin") {
|
||||
throw redirect({ to: "/settings" });
|
||||
}
|
||||
|
||||
await context.queryClient.ensureQueryData(getAdminUsersOptions());
|
||||
|
||||
return authContext as AppContext;
|
||||
},
|
||||
staticData: {
|
||||
breadcrumb: () => [{ label: "Administration" }],
|
||||
},
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const appContext = Route.useLoaderData();
|
||||
return <AdminPage appContext={appContext} />;
|
||||
}
|
||||
|
|
@ -3,21 +3,12 @@ import { fetchUser } from "../route";
|
|||
import type { AppContext } from "~/context";
|
||||
import { SettingsPage } from "~/client/modules/settings/routes/settings";
|
||||
import { type } from "arktype";
|
||||
import { getAdminUsersOptions, getSsoSettingsOptions } from "~/client/api-client/@tanstack/react-query.gen";
|
||||
|
||||
export const Route = createFileRoute("/(dashboard)/settings/")({
|
||||
component: RouteComponent,
|
||||
validateSearch: type({ tab: "string?" }),
|
||||
loader: async ({ context }) => {
|
||||
loader: async () => {
|
||||
const authContext = await fetchUser();
|
||||
|
||||
if (authContext.user?.role === "admin") {
|
||||
await Promise.all([
|
||||
context.queryClient.ensureQueryData(getSsoSettingsOptions()),
|
||||
context.queryClient.ensureQueryData(getAdminUsersOptions()),
|
||||
]);
|
||||
}
|
||||
|
||||
return authContext as AppContext;
|
||||
},
|
||||
staticData: {
|
||||
|
|
|
|||
|
|
@ -1,17 +1,16 @@
|
|||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||
import { CreateSsoProviderPage } from "~/client/modules/settings/routes/create-sso-provider";
|
||||
import { fetchUser } from "../../route";
|
||||
import { getOrganizationContext } from "~/server/lib/functions/organization-context";
|
||||
|
||||
export const Route = createFileRoute("/(dashboard)/settings/sso/new")({
|
||||
component: RouteComponent,
|
||||
loader: async () => {
|
||||
const authContext = await fetchUser();
|
||||
const orgContext = await getOrganizationContext();
|
||||
const role = orgContext.activeMember?.role;
|
||||
|
||||
if (authContext.user?.role !== "admin") {
|
||||
if (role !== "owner" && role !== "admin") {
|
||||
throw redirect({ to: "/settings" });
|
||||
}
|
||||
|
||||
return authContext;
|
||||
},
|
||||
staticData: {
|
||||
breadcrumb: () => [{ label: "Settings", href: "/settings" }, { label: "Register SSO Provider" }],
|
||||
|
|
|
|||
|
|
@ -124,12 +124,8 @@ export const auth = betterAuth({
|
|||
sso({
|
||||
trustEmailVerified: false,
|
||||
providersLimit: async (user: User) => {
|
||||
const existingUser = await db.query.usersTable.findFirst({
|
||||
columns: { role: true },
|
||||
where: { id: user.id },
|
||||
});
|
||||
|
||||
return existingUser?.role === "admin" ? 10 : 0;
|
||||
const isOrgAdmin = await authService.isOrgAdminAnywhere(user.id);
|
||||
return isOrgAdmin ? 10 : 0;
|
||||
},
|
||||
organizationProvisioning: {
|
||||
disabled: false,
|
||||
|
|
|
|||
|
|
@ -16,9 +16,14 @@ import {
|
|||
updateSsoProviderAutoLinkingBody,
|
||||
updateSsoProviderAutoLinkingDto,
|
||||
deleteUserAccountDto,
|
||||
getOrgMembersDto,
|
||||
type OrgMembersDto,
|
||||
updateMemberRoleBody,
|
||||
updateMemberRoleDto,
|
||||
removeOrgMemberDto,
|
||||
} from "./auth.dto";
|
||||
import { authService } from "./auth.service";
|
||||
import { requireAdmin, requireAuth } from "./auth.middleware";
|
||||
import { requireAdmin, requireAuth, requireOrgAdmin } from "./auth.middleware";
|
||||
import { auth } from "~/server/lib/auth";
|
||||
|
||||
export const authController = new Hono()
|
||||
|
|
@ -30,7 +35,7 @@ export const authController = new Hono()
|
|||
const providers = await authService.getPublicSsoProviders();
|
||||
return c.json<PublicSsoProvidersDto>(providers);
|
||||
})
|
||||
.get("/sso-settings", requireAuth, requireAdmin, getSsoSettingsDto, async (c) => {
|
||||
.get("/sso-settings", requireAuth, requireOrgAdmin, getSsoSettingsDto, async (c) => {
|
||||
const headers = c.req.raw.headers;
|
||||
const activeOrganizationId = c.get("organizationId");
|
||||
|
||||
|
|
@ -62,7 +67,7 @@ export const authController = new Hono()
|
|||
})),
|
||||
});
|
||||
})
|
||||
.delete("/sso-providers/:providerId", requireAuth, requireAdmin, deleteSsoProviderDto, async (c) => {
|
||||
.delete("/sso-providers/:providerId", requireAuth, requireOrgAdmin, deleteSsoProviderDto, async (c) => {
|
||||
const providerId = c.req.param("providerId");
|
||||
const organizationId = c.get("organizationId");
|
||||
|
||||
|
|
@ -77,7 +82,7 @@ export const authController = new Hono()
|
|||
.patch(
|
||||
"/sso-providers/:providerId/auto-linking",
|
||||
requireAuth,
|
||||
requireAdmin,
|
||||
requireOrgAdmin,
|
||||
updateSsoProviderAutoLinkingDto,
|
||||
validator("json", updateSsoProviderAutoLinkingBody),
|
||||
async (c) => {
|
||||
|
|
@ -94,7 +99,7 @@ export const authController = new Hono()
|
|||
return c.json({ success: true });
|
||||
},
|
||||
)
|
||||
.delete("/sso-invitations/:invitationId", requireAuth, requireAdmin, deleteSsoInvitationDto, async (c) => {
|
||||
.delete("/sso-invitations/:invitationId", requireAuth, requireOrgAdmin, deleteSsoInvitationDto, async (c) => {
|
||||
const invitationId = c.req.param("invitationId");
|
||||
const organizationId = c.get("organizationId");
|
||||
|
||||
|
|
@ -151,4 +156,49 @@ export const authController = new Hono()
|
|||
const userId = c.req.param("userId");
|
||||
const impact = await authService.getUserDeletionImpact(userId);
|
||||
return c.json<UserDeletionImpactDto>(impact);
|
||||
})
|
||||
.get("/org-members", requireAuth, requireOrgAdmin, getOrgMembersDto, async (c) => {
|
||||
const organizationId = c.get("organizationId");
|
||||
const result = await authService.getOrgMembers(organizationId);
|
||||
return c.json<OrgMembersDto>(result);
|
||||
})
|
||||
.patch(
|
||||
"/org-members/:memberId/role",
|
||||
requireAuth,
|
||||
requireOrgAdmin,
|
||||
updateMemberRoleDto,
|
||||
validator("json", updateMemberRoleBody),
|
||||
async (c) => {
|
||||
const memberId = c.req.param("memberId");
|
||||
const organizationId = c.get("organizationId");
|
||||
const { role } = c.req.valid("json");
|
||||
|
||||
const result = await authService.updateMemberRole(memberId, organizationId, role);
|
||||
|
||||
if (!result.found) {
|
||||
return c.json({ message: "Member not found" }, 404);
|
||||
}
|
||||
|
||||
if (result.isOwner) {
|
||||
return c.json({ message: "Cannot change the role of the organization owner" }, 403);
|
||||
}
|
||||
|
||||
return c.json({ success: true });
|
||||
},
|
||||
)
|
||||
.delete("/org-members/:memberId", requireAuth, requireOrgAdmin, removeOrgMemberDto, async (c) => {
|
||||
const memberId = c.req.param("memberId");
|
||||
const organizationId = c.get("organizationId");
|
||||
|
||||
const result = await authService.removeOrgMember(memberId, organizationId);
|
||||
|
||||
if (!result.found) {
|
||||
return c.json({ message: "Member not found" }, 404);
|
||||
}
|
||||
|
||||
if (result.isOwner) {
|
||||
return c.json({ message: "Cannot remove the organization owner" }, 403);
|
||||
}
|
||||
|
||||
return c.json({ success: true });
|
||||
});
|
||||
|
|
|
|||
|
|
@ -197,6 +197,75 @@ export const deleteUserAccountDto = describeRoute({
|
|||
},
|
||||
});
|
||||
|
||||
export const orgMembersResponse = type({
|
||||
members: type({
|
||||
id: "string",
|
||||
userId: "string",
|
||||
role: "string",
|
||||
createdAt: "string",
|
||||
user: {
|
||||
name: "string | null",
|
||||
email: "string",
|
||||
},
|
||||
}).array(),
|
||||
});
|
||||
|
||||
export type OrgMembersDto = typeof orgMembersResponse.infer;
|
||||
|
||||
export const getOrgMembersDto = describeRoute({
|
||||
description: "Get members of the active organization",
|
||||
operationId: "getOrgMembers",
|
||||
tags: ["Auth"],
|
||||
responses: {
|
||||
200: {
|
||||
description: "List of organization members",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(orgMembersResponse),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const updateMemberRoleBody = type({
|
||||
role: "'member' | 'admin'",
|
||||
});
|
||||
|
||||
export const updateMemberRoleDto = describeRoute({
|
||||
description: "Update a member's role in the active organization",
|
||||
operationId: "updateMemberRole",
|
||||
tags: ["Auth"],
|
||||
responses: {
|
||||
200: {
|
||||
description: "Member role updated successfully",
|
||||
},
|
||||
403: {
|
||||
description: "Forbidden",
|
||||
},
|
||||
404: {
|
||||
description: "Member not found",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const removeOrgMemberDto = describeRoute({
|
||||
description: "Remove a member from the active organization",
|
||||
operationId: "removeOrgMember",
|
||||
tags: ["Auth"],
|
||||
responses: {
|
||||
200: {
|
||||
description: "Member removed successfully",
|
||||
},
|
||||
403: {
|
||||
description: "Forbidden",
|
||||
},
|
||||
404: {
|
||||
description: "Member not found",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const updateSsoProviderAutoLinkingBody = type({
|
||||
enabled: "boolean",
|
||||
});
|
||||
|
|
|
|||
|
|
@ -212,6 +212,86 @@ export class AuthService {
|
|||
return grouped;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all members of an organization with their user data
|
||||
*/
|
||||
async getOrgMembers(organizationId: string) {
|
||||
const members = await db.query.member.findMany({
|
||||
where: { organizationId },
|
||||
with: { user: true },
|
||||
});
|
||||
|
||||
return {
|
||||
members: members.map((m) => ({
|
||||
id: m.id,
|
||||
userId: m.userId,
|
||||
role: m.role,
|
||||
createdAt: new Date(m.createdAt).toISOString(),
|
||||
user: {
|
||||
name: m.user.name,
|
||||
email: m.user.email,
|
||||
},
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a member's role in an organization.
|
||||
* Cannot change the role of an owner.
|
||||
*/
|
||||
async updateMemberRole(memberId: string, organizationId: string, role: "member" | "admin") {
|
||||
const targetMember = await db.query.member.findFirst({
|
||||
where: { AND: [{ id: memberId }, { organizationId }] },
|
||||
});
|
||||
|
||||
if (!targetMember) {
|
||||
return { found: false, isOwner: false } as const;
|
||||
}
|
||||
|
||||
if (targetMember.role === "owner") {
|
||||
return { found: true, isOwner: true } as const;
|
||||
}
|
||||
|
||||
await db.update(member).set({ role }).where(eq(member.id, memberId));
|
||||
|
||||
return { found: true, isOwner: false } as const;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a member from an organization.
|
||||
* Cannot remove an owner.
|
||||
*/
|
||||
async removeOrgMember(memberId: string, organizationId: string) {
|
||||
const targetMember = await db.query.member.findFirst({
|
||||
where: { AND: [{ id: memberId }, { organizationId }] },
|
||||
});
|
||||
|
||||
if (!targetMember) {
|
||||
return { found: false, isOwner: false } as const;
|
||||
}
|
||||
|
||||
if (targetMember.role === "owner") {
|
||||
return { found: true, isOwner: true } as const;
|
||||
}
|
||||
|
||||
await db.delete(member).where(eq(member.id, memberId));
|
||||
|
||||
return { found: true, isOwner: false } as const;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user is an owner or admin in any organization
|
||||
*/
|
||||
async isOrgAdminAnywhere(userId: string) {
|
||||
const membership = await db.query.member.findFirst({
|
||||
where: {
|
||||
AND: [{ userId }, { role: { in: ["owner", "admin"] } }],
|
||||
},
|
||||
});
|
||||
|
||||
return !!membership;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a single account for a user, refusing if it is the last one
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ const inviteOnlyMessage =
|
|||
"Access is invite-only. Ask an organization admin to send you an invitation before signing in with SSO.";
|
||||
|
||||
async function openSsoSettings(page: Page) {
|
||||
await gotoAndWaitForAppReady(page, "/settings?tab=users");
|
||||
await gotoAndWaitForAppReady(page, "/settings?tab=organization");
|
||||
await expect(page.getByText("Single Sign-On")).toBeVisible();
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue