feat: custom flags allowlist

This commit is contained in:
Nicolas Meienberger 2026-03-04 19:32:08 +01:00
parent 0c20473e30
commit 157be2d6ab
14 changed files with 3868 additions and 3471 deletions

View file

@ -4,8 +4,8 @@
import { type DefaultError, type InfiniteData, infiniteQueryOptions, queryOptions, type UseMutationOptions } from '@tanstack/react-query'; import { type DefaultError, type InfiniteData, infiniteQueryOptions, queryOptions, type UseMutationOptions } from '@tanstack/react-query';
import { client } from '../client.gen'; 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, 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 { browseFilesystem, cancelDoctor, createBackupSchedule, createNotificationDestination, createRepository, createVolume, deleteBackupSchedule, deleteNotificationDestination, deleteRepository, deleteSnapshot, deleteSnapshots, deleteSsoInvitation, deleteSsoProvider, deleteUserAccount, deleteVolume, 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'; 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, 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> = [ export type QueryKey<TOptions extends Options> = [
Pick<TOptions, 'baseUrl' | 'body' | 'headers' | 'path' | 'query'> & { Pick<TOptions, 'baseUrl' | 'body' | 'headers' | 'path' | 'query'> & {
@ -58,93 +58,6 @@ export const getStatusOptions = (options?: Options<GetStatusData>) => queryOptio
queryKey: getStatusQueryKey(options) queryKey: getStatusQueryKey(options)
}); });
export const getPublicSsoProvidersQueryKey = (options?: Options<GetPublicSsoProvidersData>) => createQueryKey('getPublicSsoProviders', options);
/**
* Get public SSO providers for the instance
*/
export const getPublicSsoProvidersOptions = (options?: Options<GetPublicSsoProvidersData>) => queryOptions<GetPublicSsoProvidersResponse, DefaultError, GetPublicSsoProvidersResponse, ReturnType<typeof getPublicSsoProvidersQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await getPublicSsoProviders({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: getPublicSsoProvidersQueryKey(options)
});
export const getSsoSettingsQueryKey = (options?: Options<GetSsoSettingsData>) => createQueryKey('getSsoSettings', options);
/**
* Get SSO providers and invitations for the active organization
*/
export const getSsoSettingsOptions = (options?: Options<GetSsoSettingsData>) => queryOptions<GetSsoSettingsResponse, DefaultError, GetSsoSettingsResponse, ReturnType<typeof getSsoSettingsQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await getSsoSettings({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: getSsoSettingsQueryKey(options)
});
/**
* Delete an SSO provider
*/
export const deleteSsoProviderMutation = (options?: Partial<Options<DeleteSsoProviderData>>): UseMutationOptions<unknown, DefaultError, Options<DeleteSsoProviderData>> => {
const mutationOptions: UseMutationOptions<unknown, DefaultError, Options<DeleteSsoProviderData>> = {
mutationFn: async (fnOptions) => {
const { data } = await deleteSsoProvider({
...options,
...fnOptions,
throwOnError: true
});
return data;
}
};
return mutationOptions;
};
/**
* Update whether SSO sign-in can auto-link existing accounts by email
*/
export const updateSsoProviderAutoLinkingMutation = (options?: Partial<Options<UpdateSsoProviderAutoLinkingData>>): UseMutationOptions<unknown, DefaultError, Options<UpdateSsoProviderAutoLinkingData>> => {
const mutationOptions: UseMutationOptions<unknown, DefaultError, Options<UpdateSsoProviderAutoLinkingData>> = {
mutationFn: async (fnOptions) => {
const { data } = await updateSsoProviderAutoLinking({
...options,
...fnOptions,
throwOnError: true
});
return data;
}
};
return mutationOptions;
};
/**
* Delete an SSO invitation
*/
export const deleteSsoInvitationMutation = (options?: Partial<Options<DeleteSsoInvitationData>>): UseMutationOptions<unknown, DefaultError, Options<DeleteSsoInvitationData>> => {
const mutationOptions: UseMutationOptions<unknown, DefaultError, Options<DeleteSsoInvitationData>> = {
mutationFn: async (fnOptions) => {
const { data } = await deleteSsoInvitation({
...options,
...fnOptions,
throwOnError: true
});
return data;
}
};
return mutationOptions;
};
export const getAdminUsersQueryKey = (options?: Options<GetAdminUsersData>) => createQueryKey('getAdminUsers', options); export const getAdminUsersQueryKey = (options?: Options<GetAdminUsersData>) => createQueryKey('getAdminUsers', options);
/** /**
@ -250,6 +163,93 @@ export const removeOrgMemberMutation = (options?: Partial<Options<RemoveOrgMembe
return mutationOptions; return mutationOptions;
}; };
export const getPublicSsoProvidersQueryKey = (options?: Options<GetPublicSsoProvidersData>) => createQueryKey('getPublicSsoProviders', options);
/**
* Get public SSO providers for the instance
*/
export const getPublicSsoProvidersOptions = (options?: Options<GetPublicSsoProvidersData>) => queryOptions<GetPublicSsoProvidersResponse, DefaultError, GetPublicSsoProvidersResponse, ReturnType<typeof getPublicSsoProvidersQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await getPublicSsoProviders({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: getPublicSsoProvidersQueryKey(options)
});
export const getSsoSettingsQueryKey = (options?: Options<GetSsoSettingsData>) => createQueryKey('getSsoSettings', options);
/**
* Get SSO providers and invitations for the active organization
*/
export const getSsoSettingsOptions = (options?: Options<GetSsoSettingsData>) => queryOptions<GetSsoSettingsResponse, DefaultError, GetSsoSettingsResponse, ReturnType<typeof getSsoSettingsQueryKey>>({
queryFn: async ({ queryKey, signal }) => {
const { data } = await getSsoSettings({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: getSsoSettingsQueryKey(options)
});
/**
* Delete an SSO provider
*/
export const deleteSsoProviderMutation = (options?: Partial<Options<DeleteSsoProviderData>>): UseMutationOptions<unknown, DefaultError, Options<DeleteSsoProviderData>> => {
const mutationOptions: UseMutationOptions<unknown, DefaultError, Options<DeleteSsoProviderData>> = {
mutationFn: async (fnOptions) => {
const { data } = await deleteSsoProvider({
...options,
...fnOptions,
throwOnError: true
});
return data;
}
};
return mutationOptions;
};
/**
* Update whether SSO sign-in can auto-link existing accounts by email
*/
export const updateSsoProviderAutoLinkingMutation = (options?: Partial<Options<UpdateSsoProviderAutoLinkingData>>): UseMutationOptions<unknown, DefaultError, Options<UpdateSsoProviderAutoLinkingData>> => {
const mutationOptions: UseMutationOptions<unknown, DefaultError, Options<UpdateSsoProviderAutoLinkingData>> = {
mutationFn: async (fnOptions) => {
const { data } = await updateSsoProviderAutoLinking({
...options,
...fnOptions,
throwOnError: true
});
return data;
}
};
return mutationOptions;
};
/**
* Delete an SSO invitation
*/
export const deleteSsoInvitationMutation = (options?: Partial<Options<DeleteSsoInvitationData>>): UseMutationOptions<unknown, DefaultError, Options<DeleteSsoInvitationData>> => {
const mutationOptions: UseMutationOptions<unknown, DefaultError, Options<DeleteSsoInvitationData>> = {
mutationFn: async (fnOptions) => {
const { data } = await deleteSsoInvitation({
...options,
...fnOptions,
throwOnError: true
});
return data;
}
};
return mutationOptions;
};
export const listVolumesQueryKey = (options?: Options<ListVolumesData>) => createQueryKey('listVolumes', options); export const listVolumesQueryKey = (options?: Options<ListVolumesData>) => createQueryKey('listVolumes', options);
/** /**
@ -855,23 +855,6 @@ export const tagSnapshotsMutation = (options?: Partial<Options<TagSnapshotsData>
return mutationOptions; return mutationOptions;
}; };
/**
* Execute a restic command against a repository (dev panel only)
*/
export const devPanelExecMutation = (options?: Partial<Options<DevPanelExecData>>): UseMutationOptions<DevPanelExecResponse, DefaultError, Options<DevPanelExecData>> => {
const mutationOptions: UseMutationOptions<DevPanelExecResponse, DefaultError, Options<DevPanelExecData>> = {
mutationFn: async (fnOptions) => {
const { data } = await devPanelExec({
...options,
...fnOptions,
throwOnError: true
});
return data;
}
};
return mutationOptions;
};
export const listBackupSchedulesQueryKey = (options?: Options<ListBackupSchedulesData>) => createQueryKey('listBackupSchedules', options); export const listBackupSchedulesQueryKey = (options?: Options<ListBackupSchedulesData>) => createQueryKey('listBackupSchedules', options);
/** /**

View file

@ -191,7 +191,7 @@ export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn, SseFn>
*/ */
export type CreateClientConfig<T extends ClientOptions = ClientOptions> = ( export type CreateClientConfig<T extends ClientOptions = ClientOptions> = (
override?: Config<ClientOptions & T>, override?: Config<ClientOptions & T>,
) => Config<Required<ClientOptions> & T>; ) => Config<Required<ClientOptions> & T> | Promise<Config<Required<ClientOptions> & T>>;
export interface TDataShape { export interface TDataShape {
body?: unknown; body?: unknown;

View file

@ -97,7 +97,7 @@ interface Params {
const stripEmptySlots = (params: Params) => { const stripEmptySlots = (params: Params) => {
for (const [slot, value] of Object.entries(params)) { for (const [slot, value] of Object.entries(params)) {
if (value && typeof value === 'object' && !Object.keys(value).length) { if (value && typeof value === 'object' && !Array.isArray(value) && !Object.keys(value).length) {
delete params[slot as Slot]; delete params[slot as Slot];
} }
} }

View file

@ -24,38 +24,6 @@ export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends
*/ */
export const getStatus = <ThrowOnError extends boolean = false>(options?: Options<GetStatusData, ThrowOnError>) => (options?.client ?? client).get<GetStatusResponses, unknown, ThrowOnError>({ url: '/api/v1/auth/status', ...options }); export const getStatus = <ThrowOnError extends boolean = false>(options?: Options<GetStatusData, ThrowOnError>) => (options?.client ?? client).get<GetStatusResponses, unknown, ThrowOnError>({ url: '/api/v1/auth/status', ...options });
/**
* Get public SSO providers for the instance
*/
export const getPublicSsoProviders = <ThrowOnError extends boolean = false>(options?: Options<GetPublicSsoProvidersData, ThrowOnError>) => (options?.client ?? client).get<GetPublicSsoProvidersResponses, unknown, ThrowOnError>({ url: '/api/v1/auth/sso-providers', ...options });
/**
* Get SSO providers and invitations for the active organization
*/
export const getSsoSettings = <ThrowOnError extends boolean = false>(options?: Options<GetSsoSettingsData, ThrowOnError>) => (options?.client ?? client).get<GetSsoSettingsResponses, unknown, ThrowOnError>({ url: '/api/v1/auth/sso-settings', ...options });
/**
* Delete an SSO provider
*/
export const deleteSsoProvider = <ThrowOnError extends boolean = false>(options: Options<DeleteSsoProviderData, ThrowOnError>) => (options.client ?? client).delete<DeleteSsoProviderResponses, DeleteSsoProviderErrors, ThrowOnError>({ url: '/api/v1/auth/sso-providers/{providerId}', ...options });
/**
* Update whether SSO sign-in can auto-link existing accounts by email
*/
export const updateSsoProviderAutoLinking = <ThrowOnError extends boolean = false>(options: Options<UpdateSsoProviderAutoLinkingData, ThrowOnError>) => (options.client ?? client).patch<UpdateSsoProviderAutoLinkingResponses, UpdateSsoProviderAutoLinkingErrors, ThrowOnError>({
url: '/api/v1/auth/sso-providers/{providerId}/auto-linking',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
/**
* Delete an SSO invitation
*/
export const deleteSsoInvitation = <ThrowOnError extends boolean = false>(options: Options<DeleteSsoInvitationData, ThrowOnError>) => (options.client ?? client).delete<DeleteSsoInvitationResponses, DeleteSsoInvitationErrors, ThrowOnError>({ url: '/api/v1/auth/sso-invitations/{invitationId}', ...options });
/** /**
* List admin users for settings management * List admin users for settings management
*/ */
@ -93,6 +61,38 @@ export const updateMemberRole = <ThrowOnError extends boolean = false>(options:
*/ */
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 }); 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 });
/**
* Get public SSO providers for the instance
*/
export const getPublicSsoProviders = <ThrowOnError extends boolean = false>(options?: Options<GetPublicSsoProvidersData, ThrowOnError>) => (options?.client ?? client).get<GetPublicSsoProvidersResponses, unknown, ThrowOnError>({ url: '/api/v1/auth/sso-providers', ...options });
/**
* Get SSO providers and invitations for the active organization
*/
export const getSsoSettings = <ThrowOnError extends boolean = false>(options?: Options<GetSsoSettingsData, ThrowOnError>) => (options?.client ?? client).get<GetSsoSettingsResponses, unknown, ThrowOnError>({ url: '/api/v1/auth/sso-settings', ...options });
/**
* Delete an SSO provider
*/
export const deleteSsoProvider = <ThrowOnError extends boolean = false>(options: Options<DeleteSsoProviderData, ThrowOnError>) => (options.client ?? client).delete<DeleteSsoProviderResponses, DeleteSsoProviderErrors, ThrowOnError>({ url: '/api/v1/auth/sso-providers/{providerId}', ...options });
/**
* Update whether SSO sign-in can auto-link existing accounts by email
*/
export const updateSsoProviderAutoLinking = <ThrowOnError extends boolean = false>(options: Options<UpdateSsoProviderAutoLinkingData, ThrowOnError>) => (options.client ?? client).patch<UpdateSsoProviderAutoLinkingResponses, UpdateSsoProviderAutoLinkingErrors, ThrowOnError>({
url: '/api/v1/auth/sso-providers/{providerId}/auto-linking',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
/**
* Delete an SSO invitation
*/
export const deleteSsoInvitation = <ThrowOnError extends boolean = false>(options: Options<DeleteSsoInvitationData, ThrowOnError>) => (options.client ?? client).delete<DeleteSsoInvitationResponses, DeleteSsoInvitationErrors, ThrowOnError>({ url: '/api/v1/auth/sso-invitations/{invitationId}', ...options });
/** /**
* List all volumes * List all volumes
*/ */
@ -101,24 +101,24 @@ export const listVolumes = <ThrowOnError extends boolean = false>(options?: Opti
/** /**
* Create a new volume * Create a new volume
*/ */
export const createVolume = <ThrowOnError extends boolean = false>(options?: Options<CreateVolumeData, ThrowOnError>) => (options?.client ?? client).post<CreateVolumeResponses, unknown, ThrowOnError>({ export const createVolume = <ThrowOnError extends boolean = false>(options: Options<CreateVolumeData, ThrowOnError>) => (options.client ?? client).post<CreateVolumeResponses, unknown, ThrowOnError>({
url: '/api/v1/volumes', url: '/api/v1/volumes',
...options, ...options,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...options?.headers ...options.headers
} }
}); });
/** /**
* Test connection to backend * Test connection to backend
*/ */
export const testConnection = <ThrowOnError extends boolean = false>(options?: Options<TestConnectionData, ThrowOnError>) => (options?.client ?? client).post<TestConnectionResponses, unknown, ThrowOnError>({ export const testConnection = <ThrowOnError extends boolean = false>(options: Options<TestConnectionData, ThrowOnError>) => (options.client ?? client).post<TestConnectionResponses, unknown, ThrowOnError>({
url: '/api/v1/volumes/test-connection', url: '/api/v1/volumes/test-connection',
...options, ...options,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...options?.headers ...options.headers
} }
}); });
@ -177,12 +177,12 @@ export const listRepositories = <ThrowOnError extends boolean = false>(options?:
/** /**
* Create a new restic repository * Create a new restic repository
*/ */
export const createRepository = <ThrowOnError extends boolean = false>(options?: Options<CreateRepositoryData, ThrowOnError>) => (options?.client ?? client).post<CreateRepositoryResponses, unknown, ThrowOnError>({ export const createRepository = <ThrowOnError extends boolean = false>(options: Options<CreateRepositoryData, ThrowOnError>) => (options.client ?? client).post<CreateRepositoryResponses, unknown, ThrowOnError>({
url: '/api/v1/repositories', url: '/api/v1/repositories',
...options, ...options,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...options?.headers ...options.headers
} }
}); });
@ -319,12 +319,12 @@ export const listBackupSchedules = <ThrowOnError extends boolean = false>(option
/** /**
* Create a new backup schedule for a volume * Create a new backup schedule for a volume
*/ */
export const createBackupSchedule = <ThrowOnError extends boolean = false>(options?: Options<CreateBackupScheduleData, ThrowOnError>) => (options?.client ?? client).post<CreateBackupScheduleResponses, unknown, ThrowOnError>({ export const createBackupSchedule = <ThrowOnError extends boolean = false>(options: Options<CreateBackupScheduleData, ThrowOnError>) => (options.client ?? client).post<CreateBackupScheduleResponses, unknown, ThrowOnError>({
url: '/api/v1/backups', url: '/api/v1/backups',
...options, ...options,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...options?.headers ...options.headers
} }
}); });
@ -412,12 +412,12 @@ export const getMirrorCompatibility = <ThrowOnError extends boolean = false>(opt
/** /**
* Reorder backup schedules by providing an array of schedule short IDs in the desired order * Reorder backup schedules by providing an array of schedule short IDs in the desired order
*/ */
export const reorderBackupSchedules = <ThrowOnError extends boolean = false>(options?: Options<ReorderBackupSchedulesData, ThrowOnError>) => (options?.client ?? client).post<ReorderBackupSchedulesResponses, unknown, ThrowOnError>({ export const reorderBackupSchedules = <ThrowOnError extends boolean = false>(options: Options<ReorderBackupSchedulesData, ThrowOnError>) => (options.client ?? client).post<ReorderBackupSchedulesResponses, unknown, ThrowOnError>({
url: '/api/v1/backups/reorder', url: '/api/v1/backups/reorder',
...options, ...options,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...options?.headers ...options.headers
} }
}); });
@ -434,12 +434,12 @@ export const listNotificationDestinations = <ThrowOnError extends boolean = fals
/** /**
* Create a new notification destination * Create a new notification destination
*/ */
export const createNotificationDestination = <ThrowOnError extends boolean = false>(options?: Options<CreateNotificationDestinationData, ThrowOnError>) => (options?.client ?? client).post<CreateNotificationDestinationResponses, unknown, ThrowOnError>({ export const createNotificationDestination = <ThrowOnError extends boolean = false>(options: Options<CreateNotificationDestinationData, ThrowOnError>) => (options.client ?? client).post<CreateNotificationDestinationResponses, unknown, ThrowOnError>({
url: '/api/v1/notifications/destinations', url: '/api/v1/notifications/destinations',
...options, ...options,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...options?.headers ...options.headers
} }
}); });
@ -488,24 +488,24 @@ export const getRegistrationStatus = <ThrowOnError extends boolean = false>(opti
/** /**
* Update the registration status for new users. Requires global admin role. * Update the registration status for new users. Requires global admin role.
*/ */
export const setRegistrationStatus = <ThrowOnError extends boolean = false>(options?: Options<SetRegistrationStatusData, ThrowOnError>) => (options?.client ?? client).put<SetRegistrationStatusResponses, unknown, ThrowOnError>({ export const setRegistrationStatus = <ThrowOnError extends boolean = false>(options: Options<SetRegistrationStatusData, ThrowOnError>) => (options.client ?? client).put<SetRegistrationStatusResponses, unknown, ThrowOnError>({
url: '/api/v1/system/registration-status', url: '/api/v1/system/registration-status',
...options, ...options,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...options?.headers ...options.headers
} }
}); });
/** /**
* Download the organization's Restic password for backup recovery. Requires organization owner or admin role and password re-authentication. * Download the organization's Restic password for backup recovery. Requires organization owner or admin role and password re-authentication.
*/ */
export const downloadResticPassword = <ThrowOnError extends boolean = false>(options?: Options<DownloadResticPasswordData, ThrowOnError>) => (options?.client ?? client).post<DownloadResticPasswordResponses, unknown, ThrowOnError>({ export const downloadResticPassword = <ThrowOnError extends boolean = false>(options: Options<DownloadResticPasswordData, ThrowOnError>) => (options.client ?? client).post<DownloadResticPasswordResponses, unknown, ThrowOnError>({
url: '/api/v1/system/restic-password', url: '/api/v1/system/restic-password',
...options, ...options,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...options?.headers ...options.headers
} }
}); });

File diff suppressed because it is too large Load diff

View file

@ -18,7 +18,7 @@ export const AdvancedSection = ({ form }: AdvancedSectionProps) => {
<FormControl> <FormControl>
<Textarea <Textarea
{...field} {...field}
placeholder="--iexclude-larger-than 500M&#10;--no-scan&#10;--read-concurrency 8" placeholder="--exclude-larger-than 500M&#10;--no-scan&#10;--read-concurrency 8"
className="font-mono text-sm min-h-24" className="font-mono text-sm min-h-24"
/> />
</FormControl> </FormControl>

View file

@ -4,6 +4,7 @@ import { useForm } from "react-hook-form";
import { useScrollToFormError } from "~/client/hooks/use-scroll-to-form-error"; import { useScrollToFormError } from "~/client/hooks/use-scroll-to-form-error";
import { Form } from "~/client/components/ui/form"; import { Form } from "~/client/components/ui/form";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "~/client/components/ui/collapsible";
import type { BackupSchedule, Volume } from "~/client/lib/types"; import type { BackupSchedule, Volume } from "~/client/lib/types";
import { AdvancedSection } from "./advanced-section"; import { AdvancedSection } from "./advanced-section";
import { BasicInfoSection } from "./basic-info-section"; import { BasicInfoSection } from "./basic-info-section";
@ -175,15 +176,14 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }:
</CardContent> </CardContent>
</Card> </Card>
<Card className="min-w-0"> <Card className="min-w-0 @container">
<CardHeader>
<CardTitle>Advanced</CardTitle>
<CardDescription>
Pass additional flags directly to the restic backup command. Use with caution.
</CardDescription>
</CardHeader>
<CardContent> <CardContent>
<AdvancedSection form={form} /> <Collapsible>
<CollapsibleTrigger>Advanced</CollapsibleTrigger>
<CollapsibleContent className="pb-4 space-y-4">
<AdvancedSection form={form} />
</CollapsibleContent>
</Collapsible>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>

View file

@ -192,6 +192,7 @@ export function ScheduleDetailsPage(props: Props) {
updateSchedule.mutate({ updateSchedule.mutate({
path: { shortId: schedule.shortId }, path: { shortId: schedule.shortId },
body: { body: {
name: schedule.name,
repositoryId: schedule.repositoryId, repositoryId: schedule.repositoryId,
enabled, enabled,
cronExpression: schedule.cronExpression, cronExpression: schedule.cronExpression,
@ -200,6 +201,7 @@ export function ScheduleDetailsPage(props: Props) {
excludePatterns: schedule.excludePatterns || [], excludePatterns: schedule.excludePatterns || [],
excludeIfPresent: schedule.excludeIfPresent || [], excludeIfPresent: schedule.excludeIfPresent || [],
oneFileSystem: schedule.oneFileSystem, oneFileSystem: schedule.oneFileSystem,
customResticParams: schedule.customResticParams || [],
}, },
}); });
}; };

View file

@ -1 +1 @@
ALTER TABLE `backup_schedules_table` ADD `custom_restic_params` text DEFAULT '[]'; ALTER TABLE `backup_schedules_table` ADD `custom_restic_params` text DEFAULT '[]';

View file

@ -1,8 +1,8 @@
{ {
"version": "7", "version": "7",
"dialect": "sqlite", "dialect": "sqlite",
"id": "38a5fef6-d38e-4bf0-b9ab-0723db3741da", "id": "1e14036e-978e-400a-bff0-fe379595fcbd",
"prevIds": ["3a308c54-d950-464f-9490-fee06985fbeb"], "prevIds": ["60a53b0c-395a-4960-9b48-1740388bd2dc"],
"ddl": [ "ddl": [
{ {
"name": "account", "name": "account",
@ -48,6 +48,10 @@
"name": "sessions_table", "name": "sessions_table",
"entityType": "tables" "entityType": "tables"
}, },
{
"name": "sso_provider",
"entityType": "tables"
},
{ {
"name": "two_factor", "name": "two_factor",
"entityType": "tables" "entityType": "tables"
@ -1154,6 +1158,116 @@
"entityType": "columns", "entityType": "columns",
"table": "sessions_table" "table": "sessions_table"
}, },
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "sso_provider"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "provider_id",
"entityType": "columns",
"table": "sso_provider"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "organization_id",
"entityType": "columns",
"table": "sso_provider"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "user_id",
"entityType": "columns",
"table": "sso_provider"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "issuer",
"entityType": "columns",
"table": "sso_provider"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "domain",
"entityType": "columns",
"table": "sso_provider"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": "false",
"generated": null,
"name": "auto_link_matching_emails",
"entityType": "columns",
"table": "sso_provider"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "oidc_config",
"entityType": "columns",
"table": "sso_provider"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "saml_config",
"entityType": "columns",
"table": "sso_provider"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": "(unixepoch() * 1000)",
"generated": null,
"name": "created_at",
"entityType": "columns",
"table": "sso_provider"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": "(unixepoch() * 1000)",
"generated": null,
"name": "updated_at",
"entityType": "columns",
"table": "sso_provider"
},
{ {
"type": "text", "type": "text",
"notNull": false, "notNull": false,
@ -1699,6 +1813,28 @@
"entityType": "fks", "entityType": "fks",
"table": "sessions_table" "table": "sessions_table"
}, },
{
"columns": ["organization_id"],
"tableTo": "organization",
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_sso_provider_organization_id_organization_id_fk",
"entityType": "fks",
"table": "sso_provider"
},
{
"columns": ["user_id"],
"tableTo": "users_table",
"columnsTo": ["id"],
"onUpdate": "NO ACTION",
"onDelete": "SET NULL",
"nameExplicit": false,
"name": "fk_sso_provider_user_id_users_table_id_fk",
"entityType": "fks",
"table": "sso_provider"
},
{ {
"columns": ["user_id"], "columns": ["user_id"],
"tableTo": "users_table", "tableTo": "users_table",
@ -1798,6 +1934,13 @@
"table": "sessions_table", "table": "sessions_table",
"entityType": "pks" "entityType": "pks"
}, },
{
"columns": ["id"],
"nameExplicit": false,
"name": "sso_provider_pk",
"table": "sso_provider",
"entityType": "pks"
},
{ {
"columns": ["id"], "columns": ["id"],
"nameExplicit": false, "nameExplicit": false,
@ -2019,6 +2162,13 @@
"entityType": "uniques", "entityType": "uniques",
"table": "sessions_table" "table": "sessions_table"
}, },
{
"columns": ["provider_id"],
"nameExplicit": false,
"name": "sso_provider_provider_id_unique",
"entityType": "uniques",
"table": "sso_provider"
},
{ {
"columns": ["username"], "columns": ["username"],
"nameExplicit": false, "nameExplicit": false,

View file

@ -10,6 +10,7 @@ import { generateShortId } from "~/server/utils/id";
import { getOrganizationId } from "~/server/core/request-context"; import { getOrganizationId } from "~/server/core/request-context";
import { calculateNextRun } from "./backup.helpers"; import { calculateNextRun } from "./backup.helpers";
import { asShortId, type ShortId } from "~/server/utils/branded"; import { asShortId, type ShortId } from "~/server/utils/branded";
import { validateCustomResticParams } from "~/server/utils/restic/helpers/validate-custom-params";
const listSchedules = async () => { const listSchedules = async () => {
const organizationId = getOrganizationId(); const organizationId = getOrganizationId();
@ -119,6 +120,11 @@ const createSchedule = async (data: CreateBackupScheduleBody) => {
throw new NotFoundError("Repository not found"); throw new NotFoundError("Repository not found");
} }
if (data.customResticParams && data.customResticParams.length > 0) {
const paramError = validateCustomResticParams(data.customResticParams);
if (paramError) throw new BadRequestError(paramError);
}
const nextBackupAt = calculateNextRun(data.cronExpression); const nextBackupAt = calculateNextRun(data.cronExpression);
const [newSchedule] = await db const [newSchedule] = await db
@ -167,6 +173,11 @@ const updateSchedule = async (scheduleIdOrShortId: number | string, data: Update
throw new BadRequestError("Invalid cron expression"); throw new BadRequestError("Invalid cron expression");
} }
if (data.customResticParams && data.customResticParams.length > 0) {
const paramError = validateCustomResticParams(data.customResticParams);
if (paramError) throw new BadRequestError(paramError);
}
if (data.name) { if (data.name) {
const existingName = await db.query.backupSchedulesTable.findFirst({ const existingName = await db.query.backupSchedulesTable.findFirst({
where: { where: {

View file

@ -18,6 +18,7 @@ import { addCommonArgs } from "../helpers/add-common-args";
import { buildEnv } from "../helpers/build-env"; import { buildEnv } from "../helpers/build-env";
import { buildRepoUrl } from "../helpers/build-repo-url"; import { buildRepoUrl } from "../helpers/build-repo-url";
import { cleanupTemporaryKeys } from "../helpers/cleanup-temporary-keys"; import { cleanupTemporaryKeys } from "../helpers/cleanup-temporary-keys";
import { validateCustomResticParams } from "../helpers/validate-custom-params";
export const backup = async ( export const backup = async (
config: RepositoryConfig, config: RepositoryConfig,
@ -87,6 +88,10 @@ export const backup = async (
} }
if (options.customResticParams && options.customResticParams.length > 0) { if (options.customResticParams && options.customResticParams.length > 0) {
const validationError = validateCustomResticParams(options.customResticParams);
if (validationError) {
throw new Error(`Invalid customResticParams: ${validationError}`);
}
for (const param of options.customResticParams) { for (const param of options.customResticParams) {
const tokens = param.trim().split(/\s+/).filter(Boolean); const tokens = param.trim().split(/\s+/).filter(Boolean);
args.push(...tokens); args.push(...tokens);

View file

@ -0,0 +1,71 @@
const DENIED_FLAGS = new Set<string>([
"--password-command",
"--password-file",
"--password",
"-p",
"--repository",
"--repository-file",
"-r",
"--option",
"-o",
"--key-hint",
"--tls-client-cert",
"--cacert",
"--repo",
]);
const ALLOWED_FLAGS = new Set<string>([
"--verbose",
"-v",
"--no-scan",
"--exclude-larger-than",
"--skip-if-unchanged",
"--exclude-caches",
"--force",
"--use-fs-snapshot",
"--read-concurrency",
"--ignore-ctime",
"--ignore-inode",
"--with-atime",
"--no-cache",
"--cleanup-cache",
"--cache-dir",
"--limit-upload",
"--limit-download",
"--pack-size",
"--dry-run",
"-n",
"--no-lock",
"--json",
]);
function extractFlagName(token: string): string | null {
if (!token.startsWith("-")) {
return null;
}
const eqIdx = token.indexOf("=");
return eqIdx === -1 ? token : token.slice(0, eqIdx);
}
export function validateCustomResticParams(params: string[]): string | null {
for (const param of params) {
const tokens = param.trim().split(/\s+/).filter(Boolean);
for (const token of tokens) {
const flag = extractFlagName(token);
if (flag === null) {
continue;
}
if (DENIED_FLAGS.has(flag)) {
return `Flag "${flag}" is not permitted in customResticParams`;
}
if (!ALLOWED_FLAGS.has(flag)) {
return `Unknown or unsupported flag "${flag}" in customResticParams. Permitted flags: ${[...ALLOWED_FLAGS].join(", ")}`;
}
}
}
return null;
}

View file

@ -38,9 +38,5 @@ export default defineConfig({
server: { server: {
host: "0.0.0.0", host: "0.0.0.0",
port: 3000, port: 3000,
watch: {
usePolling: true,
ignored: ["**/data/**", "**/*.db", "**/*.db-journal", "**/*.db-wal", "**/*.db-shm"],
},
}, },
}); });