diff --git a/app/client/api-client/@tanstack/react-query.gen.ts b/app/client/api-client/@tanstack/react-query.gen.ts index 182d913b..1b730c22 100644 --- a/app/client/api-client/@tanstack/react-query.gen.ts +++ b/app/client/api-client/@tanstack/react-query.gen.ts @@ -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, 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 { 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, 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 = [ Pick & { @@ -58,93 +58,6 @@ export const getStatusOptions = (options?: Options) => queryOptio queryKey: getStatusQueryKey(options) }); -export const getPublicSsoProvidersQueryKey = (options?: Options) => createQueryKey('getPublicSsoProviders', options); - -/** - * Get public SSO providers for the instance - */ -export const getPublicSsoProvidersOptions = (options?: Options) => queryOptions>({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await getPublicSsoProviders({ - ...options, - ...queryKey[0], - signal, - throwOnError: true - }); - return data; - }, - queryKey: getPublicSsoProvidersQueryKey(options) -}); - -export const getSsoSettingsQueryKey = (options?: Options) => createQueryKey('getSsoSettings', options); - -/** - * Get SSO providers and invitations for the active organization - */ -export const getSsoSettingsOptions = (options?: Options) => queryOptions>({ - 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>): UseMutationOptions> => { - const mutationOptions: UseMutationOptions> = { - 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>): UseMutationOptions> => { - const mutationOptions: UseMutationOptions> = { - mutationFn: async (fnOptions) => { - const { data } = await updateSsoProviderAutoLinking({ - ...options, - ...fnOptions, - throwOnError: true - }); - return data; - } - }; - return mutationOptions; -}; - -/** - * Delete an SSO invitation - */ -export const deleteSsoInvitationMutation = (options?: Partial>): UseMutationOptions> => { - const mutationOptions: UseMutationOptions> = { - mutationFn: async (fnOptions) => { - const { data } = await deleteSsoInvitation({ - ...options, - ...fnOptions, - throwOnError: true - }); - return data; - } - }; - return mutationOptions; -}; - export const getAdminUsersQueryKey = (options?: Options) => createQueryKey('getAdminUsers', options); /** @@ -250,6 +163,93 @@ export const removeOrgMemberMutation = (options?: Partial) => createQueryKey('getPublicSsoProviders', options); + +/** + * Get public SSO providers for the instance + */ +export const getPublicSsoProvidersOptions = (options?: Options) => queryOptions>({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getPublicSsoProviders({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: getPublicSsoProvidersQueryKey(options) +}); + +export const getSsoSettingsQueryKey = (options?: Options) => createQueryKey('getSsoSettings', options); + +/** + * Get SSO providers and invitations for the active organization + */ +export const getSsoSettingsOptions = (options?: Options) => queryOptions>({ + 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>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + 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>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await updateSsoProviderAutoLinking({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + +/** + * Delete an SSO invitation + */ +export const deleteSsoInvitationMutation = (options?: Partial>): UseMutationOptions> => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (fnOptions) => { + const { data } = await deleteSsoInvitation({ + ...options, + ...fnOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; + export const listVolumesQueryKey = (options?: Options) => createQueryKey('listVolumes', options); /** @@ -855,23 +855,6 @@ export const tagSnapshotsMutation = (options?: Partial return mutationOptions; }; -/** - * Execute a restic command against a repository (dev panel only) - */ -export const devPanelExecMutation = (options?: Partial>): UseMutationOptions> => { - const mutationOptions: UseMutationOptions> = { - mutationFn: async (fnOptions) => { - const { data } = await devPanelExec({ - ...options, - ...fnOptions, - throwOnError: true - }); - return data; - } - }; - return mutationOptions; -}; - export const listBackupSchedulesQueryKey = (options?: Options) => createQueryKey('listBackupSchedules', options); /** diff --git a/app/client/api-client/client/types.gen.ts b/app/client/api-client/client/types.gen.ts index 92f86372..701c3630 100644 --- a/app/client/api-client/client/types.gen.ts +++ b/app/client/api-client/client/types.gen.ts @@ -191,7 +191,7 @@ export type Client = CoreClient */ export type CreateClientConfig = ( override?: Config, -) => Config & T>; +) => Config & T> | Promise & T>>; export interface TDataShape { body?: unknown; diff --git a/app/client/api-client/core/params.gen.ts b/app/client/api-client/core/params.gen.ts index 5c5cc4a3..598f2b72 100644 --- a/app/client/api-client/core/params.gen.ts +++ b/app/client/api-client/core/params.gen.ts @@ -97,7 +97,7 @@ interface Params { const stripEmptySlots = (params: 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]; } } diff --git a/app/client/api-client/sdk.gen.ts b/app/client/api-client/sdk.gen.ts index 3f2d16ee..efddf861 100644 --- a/app/client/api-client/sdk.gen.ts +++ b/app/client/api-client/sdk.gen.ts @@ -24,38 +24,6 @@ export type Options(options?: Options) => (options?.client ?? client).get({ url: '/api/v1/auth/status', ...options }); -/** - * Get public SSO providers for the instance - */ -export const getPublicSsoProviders = (options?: Options) => (options?.client ?? client).get({ url: '/api/v1/auth/sso-providers', ...options }); - -/** - * Get SSO providers and invitations for the active organization - */ -export const getSsoSettings = (options?: Options) => (options?.client ?? client).get({ url: '/api/v1/auth/sso-settings', ...options }); - -/** - * Delete an SSO provider - */ -export const deleteSsoProvider = (options: Options) => (options.client ?? client).delete({ url: '/api/v1/auth/sso-providers/{providerId}', ...options }); - -/** - * Update whether SSO sign-in can auto-link existing accounts by email - */ -export const updateSsoProviderAutoLinking = (options: Options) => (options.client ?? client).patch({ - url: '/api/v1/auth/sso-providers/{providerId}/auto-linking', - ...options, - headers: { - 'Content-Type': 'application/json', - ...options.headers - } -}); - -/** - * Delete an SSO invitation - */ -export const deleteSsoInvitation = (options: Options) => (options.client ?? client).delete({ url: '/api/v1/auth/sso-invitations/{invitationId}', ...options }); - /** * List admin users for settings management */ @@ -93,6 +61,38 @@ export const updateMemberRole = (options: */ export const removeOrgMember = (options: Options) => (options.client ?? client).delete({ url: '/api/v1/auth/org-members/{memberId}', ...options }); +/** + * Get public SSO providers for the instance + */ +export const getPublicSsoProviders = (options?: Options) => (options?.client ?? client).get({ url: '/api/v1/auth/sso-providers', ...options }); + +/** + * Get SSO providers and invitations for the active organization + */ +export const getSsoSettings = (options?: Options) => (options?.client ?? client).get({ url: '/api/v1/auth/sso-settings', ...options }); + +/** + * Delete an SSO provider + */ +export const deleteSsoProvider = (options: Options) => (options.client ?? client).delete({ url: '/api/v1/auth/sso-providers/{providerId}', ...options }); + +/** + * Update whether SSO sign-in can auto-link existing accounts by email + */ +export const updateSsoProviderAutoLinking = (options: Options) => (options.client ?? client).patch({ + url: '/api/v1/auth/sso-providers/{providerId}/auto-linking', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * Delete an SSO invitation + */ +export const deleteSsoInvitation = (options: Options) => (options.client ?? client).delete({ url: '/api/v1/auth/sso-invitations/{invitationId}', ...options }); + /** * List all volumes */ @@ -101,24 +101,24 @@ export const listVolumes = (options?: Opti /** * Create a new volume */ -export const createVolume = (options?: Options) => (options?.client ?? client).post({ +export const createVolume = (options: Options) => (options.client ?? client).post({ url: '/api/v1/volumes', ...options, headers: { 'Content-Type': 'application/json', - ...options?.headers + ...options.headers } }); /** * Test connection to backend */ -export const testConnection = (options?: Options) => (options?.client ?? client).post({ +export const testConnection = (options: Options) => (options.client ?? client).post({ url: '/api/v1/volumes/test-connection', ...options, headers: { 'Content-Type': 'application/json', - ...options?.headers + ...options.headers } }); @@ -177,12 +177,12 @@ export const listRepositories = (options?: /** * Create a new restic repository */ -export const createRepository = (options?: Options) => (options?.client ?? client).post({ +export const createRepository = (options: Options) => (options.client ?? client).post({ url: '/api/v1/repositories', ...options, headers: { 'Content-Type': 'application/json', - ...options?.headers + ...options.headers } }); @@ -319,12 +319,12 @@ export const listBackupSchedules = (option /** * Create a new backup schedule for a volume */ -export const createBackupSchedule = (options?: Options) => (options?.client ?? client).post({ +export const createBackupSchedule = (options: Options) => (options.client ?? client).post({ url: '/api/v1/backups', ...options, headers: { 'Content-Type': 'application/json', - ...options?.headers + ...options.headers } }); @@ -412,12 +412,12 @@ export const getMirrorCompatibility = (opt /** * Reorder backup schedules by providing an array of schedule short IDs in the desired order */ -export const reorderBackupSchedules = (options?: Options) => (options?.client ?? client).post({ +export const reorderBackupSchedules = (options: Options) => (options.client ?? client).post({ url: '/api/v1/backups/reorder', ...options, headers: { 'Content-Type': 'application/json', - ...options?.headers + ...options.headers } }); @@ -434,12 +434,12 @@ export const listNotificationDestinations = (options?: Options) => (options?.client ?? client).post({ +export const createNotificationDestination = (options: Options) => (options.client ?? client).post({ url: '/api/v1/notifications/destinations', ...options, headers: { 'Content-Type': 'application/json', - ...options?.headers + ...options.headers } }); @@ -488,24 +488,24 @@ export const getRegistrationStatus = (opti /** * Update the registration status for new users. Requires global admin role. */ -export const setRegistrationStatus = (options?: Options) => (options?.client ?? client).put({ +export const setRegistrationStatus = (options: Options) => (options.client ?? client).put({ url: '/api/v1/system/registration-status', ...options, headers: { '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. */ -export const downloadResticPassword = (options?: Options) => (options?.client ?? client).post({ +export const downloadResticPassword = (options: Options) => (options.client ?? client).post({ url: '/api/v1/system/restic-password', ...options, headers: { 'Content-Type': 'application/json', - ...options?.headers + ...options.headers } }); diff --git a/app/client/api-client/types.gen.ts b/app/client/api-client/types.gen.ts index 1e8e5af4..19873cb1 100644 --- a/app/client/api-client/types.gen.ts +++ b/app/client/api-client/types.gen.ts @@ -23,138 +23,6 @@ export type GetStatusResponses = { export type GetStatusResponse = GetStatusResponses[keyof GetStatusResponses]; -export type GetPublicSsoProvidersData = { - body?: never; - path?: never; - query?: never; - url: '/api/v1/auth/sso-providers'; -}; - -export type GetPublicSsoProvidersResponses = { - /** - * List of public SSO providers - */ - 200: { - providers: Array<{ - organizationSlug: string; - providerId: string; - }>; - }; -}; - -export type GetPublicSsoProvidersResponse = GetPublicSsoProvidersResponses[keyof GetPublicSsoProvidersResponses]; - -export type GetSsoSettingsData = { - body?: never; - path?: never; - query?: never; - url: '/api/v1/auth/sso-settings'; -}; - -export type GetSsoSettingsResponses = { - /** - * SSO settings for the active organization - */ - 200: { - invitations: Array<{ - email: string; - expiresAt: string; - id: string; - role: string; - status: string; - }>; - providers: Array<{ - autoLinkMatchingEmails: boolean; - domain: string; - issuer: string; - organizationId: string | null; - providerId: string; - type: string; - }>; - }; -}; - -export type GetSsoSettingsResponse = GetSsoSettingsResponses[keyof GetSsoSettingsResponses]; - -export type DeleteSsoProviderData = { - body?: never; - path: { - providerId: string; - }; - query?: never; - url: '/api/v1/auth/sso-providers/{providerId}'; -}; - -export type DeleteSsoProviderErrors = { - /** - * Forbidden - */ - 403: unknown; - /** - * Provider not found - */ - 404: unknown; -}; - -export type DeleteSsoProviderResponses = { - /** - * SSO provider deleted successfully - */ - 200: unknown; -}; - -export type UpdateSsoProviderAutoLinkingData = { - body?: { - enabled: boolean; - }; - path: { - providerId: string; - }; - query?: never; - url: '/api/v1/auth/sso-providers/{providerId}/auto-linking'; -}; - -export type UpdateSsoProviderAutoLinkingErrors = { - /** - * Forbidden - */ - 403: unknown; - /** - * Provider not found - */ - 404: unknown; -}; - -export type UpdateSsoProviderAutoLinkingResponses = { - /** - * SSO provider auto-linking setting updated successfully - */ - 200: unknown; -}; - -export type DeleteSsoInvitationData = { - body?: never; - path: { - invitationId: string; - }; - query?: never; - url: '/api/v1/auth/sso-invitations/{invitationId}'; -}; - -export type DeleteSsoInvitationErrors = { - /** - * Forbidden - */ - 403: unknown; -}; - -export type DeleteSsoInvitationResponses = { - /** - * SSO invitation deleted successfully - */ - 200: unknown; -}; - export type GetAdminUsersData = { body?: never; path?: never; @@ -268,7 +136,7 @@ export type GetOrgMembersResponses = { export type GetOrgMembersResponse = GetOrgMembersResponses[keyof GetOrgMembersResponses]; export type UpdateMemberRoleData = { - body?: { + body: { role: 'admin' | 'member'; }; path: { @@ -323,6 +191,138 @@ export type RemoveOrgMemberResponses = { 200: unknown; }; +export type GetPublicSsoProvidersData = { + body?: never; + path?: never; + query?: never; + url: '/api/v1/auth/sso-providers'; +}; + +export type GetPublicSsoProvidersResponses = { + /** + * List of public SSO providers + */ + 200: { + providers: Array<{ + organizationSlug: string; + providerId: string; + }>; + }; +}; + +export type GetPublicSsoProvidersResponse = GetPublicSsoProvidersResponses[keyof GetPublicSsoProvidersResponses]; + +export type GetSsoSettingsData = { + body?: never; + path?: never; + query?: never; + url: '/api/v1/auth/sso-settings'; +}; + +export type GetSsoSettingsResponses = { + /** + * SSO settings for the active organization + */ + 200: { + invitations: Array<{ + email: string; + expiresAt: string; + id: string; + role: string; + status: string; + }>; + providers: Array<{ + autoLinkMatchingEmails: boolean; + domain: string; + issuer: string; + organizationId: string | null; + providerId: string; + type: string; + }>; + }; +}; + +export type GetSsoSettingsResponse = GetSsoSettingsResponses[keyof GetSsoSettingsResponses]; + +export type DeleteSsoProviderData = { + body?: never; + path: { + providerId: string; + }; + query?: never; + url: '/api/v1/auth/sso-providers/{providerId}'; +}; + +export type DeleteSsoProviderErrors = { + /** + * Forbidden + */ + 403: unknown; + /** + * Provider not found + */ + 404: unknown; +}; + +export type DeleteSsoProviderResponses = { + /** + * SSO provider deleted successfully + */ + 200: unknown; +}; + +export type UpdateSsoProviderAutoLinkingData = { + body: { + enabled: boolean; + }; + path: { + providerId: string; + }; + query?: never; + url: '/api/v1/auth/sso-providers/{providerId}/auto-linking'; +}; + +export type UpdateSsoProviderAutoLinkingErrors = { + /** + * Forbidden + */ + 403: unknown; + /** + * Provider not found + */ + 404: unknown; +}; + +export type UpdateSsoProviderAutoLinkingResponses = { + /** + * SSO provider auto-linking setting updated successfully + */ + 200: unknown; +}; + +export type DeleteSsoInvitationData = { + body?: never; + path: { + invitationId: string; + }; + query?: never; + url: '/api/v1/auth/sso-invitations/{invitationId}'; +}; + +export type DeleteSsoInvitationErrors = { + /** + * Forbidden + */ + 403: unknown; +}; + +export type DeleteSsoInvitationResponses = { + /** + * SSO invitation deleted successfully + */ + 200: unknown; +}; + export type ListVolumesData = { body?: never; path?: never; @@ -399,7 +399,7 @@ export type ListVolumesResponses = { export type ListVolumesResponse = ListVolumesResponses[keyof ListVolumesResponses]; export type CreateVolumeData = { - body?: { + body: { config: { backend: 'directory'; path: string; @@ -524,7 +524,7 @@ export type CreateVolumeResponses = { export type CreateVolumeResponse = CreateVolumeResponses[keyof CreateVolumeResponses]; export type TestConnectionData = { - body?: { + body: { config: { backend: 'directory'; path: string; @@ -703,7 +703,7 @@ export type GetVolumeResponses = { export type GetVolumeResponse = GetVolumeResponses[keyof GetVolumeResponses]; export type UpdateVolumeData = { - body?: { + body: { autoRemount?: boolean; config?: { backend: 'directory'; @@ -1168,7 +1168,7 @@ export type ListRepositoriesResponses = { export type ListRepositoriesResponse = ListRepositoriesResponses[keyof ListRepositoriesResponses]; export type CreateRepositoryData = { - body?: { + body: { config: { accessKeyId: string; backend: 'r2'; @@ -1588,7 +1588,7 @@ export type GetRepositoryResponses = { export type GetRepositoryResponse = GetRepositoryResponses[keyof GetRepositoryResponses]; export type UpdateRepositoryData = { - body?: { + body: { compressionMode?: 'auto' | 'max' | 'off'; config?: { accessKeyId: string; @@ -1986,7 +1986,7 @@ export type GetRepositoryStatsResponses = { export type GetRepositoryStatsResponse = GetRepositoryStatsResponses[keyof GetRepositoryStatsResponses]; export type DeleteSnapshotsData = { - body?: { + body: { snapshotIds: Array; }; path: { @@ -2208,7 +2208,7 @@ export type DumpSnapshotResponses = { export type DumpSnapshotResponse = DumpSnapshotResponses[keyof DumpSnapshotResponses]; export type RestoreSnapshotData = { - body?: { + body: { snapshotId: string; delete?: boolean; exclude?: Array; @@ -2315,7 +2315,7 @@ export type UnlockRepositoryResponses = { export type UnlockRepositoryResponse = UnlockRepositoryResponses[keyof UnlockRepositoryResponses]; export type TagSnapshotsData = { - body?: { + body: { snapshotIds: Array; add?: Array; remove?: Array; @@ -2340,7 +2340,7 @@ export type TagSnapshotsResponses = { export type TagSnapshotsResponse = TagSnapshotsResponses[keyof TagSnapshotsResponses]; export type DevPanelExecData = { - body?: { + body: { command: string; args?: Array; }; @@ -2381,6 +2381,7 @@ export type ListBackupSchedulesResponses = { 200: Array<{ createdAt: number; cronExpression: string; + customResticParams: Array | null; enabled: boolean; excludeIfPresent: Array | null; excludePatterns: Array | null; @@ -2651,12 +2652,13 @@ export type ListBackupSchedulesResponses = { export type ListBackupSchedulesResponse = ListBackupSchedulesResponses[keyof ListBackupSchedulesResponses]; export type CreateBackupScheduleData = { - body?: { + body: { cronExpression: string; enabled: boolean; name: string; repositoryId: string; volumeId: number | string; + customResticParams?: Array; excludeIfPresent?: Array; excludePatterns?: Array; includePatterns?: Array; @@ -2684,6 +2686,7 @@ export type CreateBackupScheduleResponses = { 201: { createdAt: number; cronExpression: string; + customResticParams: Array | null; enabled: boolean; excludeIfPresent: Array | null; excludePatterns: Array | null; @@ -2749,6 +2752,7 @@ export type GetBackupScheduleResponses = { 200: { createdAt: number; cronExpression: string; + customResticParams: Array | null; enabled: boolean; excludeIfPresent: Array | null; excludePatterns: Array | null; @@ -3019,9 +3023,10 @@ export type GetBackupScheduleResponses = { export type GetBackupScheduleResponse = GetBackupScheduleResponses[keyof GetBackupScheduleResponses]; export type UpdateBackupScheduleData = { - body?: { + body: { cronExpression: string; repositoryId: string; + customResticParams?: Array; enabled?: boolean; excludeIfPresent?: Array; excludePatterns?: Array; @@ -3053,6 +3058,7 @@ export type UpdateBackupScheduleResponses = { 200: { createdAt: number; cronExpression: string; + customResticParams: Array | null; enabled: boolean; excludeIfPresent: Array | null; excludePatterns: Array | null; @@ -3098,6 +3104,7 @@ export type GetBackupScheduleForVolumeResponses = { 200: { createdAt: number; cronExpression: string; + customResticParams: Array | null; enabled: boolean; excludeIfPresent: Array | null; excludePatterns: Array | null; @@ -3529,7 +3536,7 @@ export type GetScheduleNotificationsResponses = { export type GetScheduleNotificationsResponse = GetScheduleNotificationsResponses[keyof GetScheduleNotificationsResponses]; export type UpdateScheduleNotificationsData = { - body?: { + body: { assignments: Array<{ destinationId: number; notifyOnFailure: boolean; @@ -3837,7 +3844,7 @@ export type GetScheduleMirrorsResponses = { export type GetScheduleMirrorsResponse = GetScheduleMirrorsResponses[keyof GetScheduleMirrorsResponses]; export type UpdateScheduleMirrorsData = { - body?: { + body: { mirrors: Array<{ enabled: boolean; repositoryId: string; @@ -4070,7 +4077,7 @@ export type GetMirrorCompatibilityResponses = { export type GetMirrorCompatibilityResponse = GetMirrorCompatibilityResponses[keyof GetMirrorCompatibilityResponses]; export type ReorderBackupSchedulesData = { - body?: { + body: { scheduleShortIds: Array; }; path?: never; @@ -4202,7 +4209,7 @@ export type ListNotificationDestinationsResponses = { export type ListNotificationDestinationsResponse = ListNotificationDestinationsResponses[keyof ListNotificationDestinationsResponses]; export type CreateNotificationDestinationData = { - body?: { + body: { config: { apiToken: string; priority: -1 | 0 | 1; @@ -4466,7 +4473,7 @@ export type GetNotificationDestinationResponses = { export type GetNotificationDestinationResponse = GetNotificationDestinationResponses[keyof GetNotificationDestinationResponses]; export type UpdateNotificationDestinationData = { - body?: { + body: { config?: { apiToken: string; priority: -1 | 0 | 1; @@ -4721,7 +4728,7 @@ export type GetRegistrationStatusResponses = { export type GetRegistrationStatusResponse = GetRegistrationStatusResponses[keyof GetRegistrationStatusResponses]; export type SetRegistrationStatusData = { - body?: { + body: { enabled: boolean; }; path?: never; @@ -4741,7 +4748,7 @@ export type SetRegistrationStatusResponses = { export type SetRegistrationStatusResponse = SetRegistrationStatusResponses[keyof SetRegistrationStatusResponses]; export type DownloadResticPasswordData = { - body?: { + body: { password: string; }; path?: never; diff --git a/app/server/lib/auth.ts b/app/server/lib/auth.ts index a19959dd..a8df5ed9 100644 --- a/app/server/lib/auth.ts +++ b/app/server/lib/auth.ts @@ -83,14 +83,23 @@ export const auth = betterAuth({ return { data: user }; }, + after: async (user, ctx) => { + if (ssoIntegration.isSsoCallback(ctx)) { + await ssoIntegration.onUserCreated(user, ctx); + } + }, }, }, session: { create: { before: async (session, ctx) => { - const membership = - (await ssoIntegration.resolveOrgMembership(session.userId, ctx)) ?? - (await ensureDefaultOrg(session.userId)); + if (ssoIntegration.isSsoCallback(ctx)) { + const membership = await ssoIntegration.resolveOrgMembershipOrThrow(session.userId, ctx); + return { data: { ...session, activeOrganizationId: membership.organizationId } }; + } + + const membership = await ensureDefaultOrg(session.userId); + return { data: { ...session, activeOrganizationId: membership.organizationId } }; }, }, diff --git a/app/server/modules/sso/__tests__/sso.integration.test.ts b/app/server/modules/sso/__tests__/sso.integration.test.ts index 188075a3..142c0284 100644 --- a/app/server/modules/sso/__tests__/sso.integration.test.ts +++ b/app/server/modules/sso/__tests__/sso.integration.test.ts @@ -4,6 +4,7 @@ import { eq } from "drizzle-orm"; import { db } from "~/server/db/db"; import { account, invitation, member, organization, ssoProvider, usersTable } from "~/server/db/schema"; import { ssoIntegration } from "../sso.integration"; +import { ensureDefaultOrg } from "~/server/lib/auth/helpers/create-default-org"; function createMockSsoCallbackContext(providerId: string): GenericEndpointContext { return { @@ -280,4 +281,51 @@ describe("ssoIntegration.resolveOrgMembership", () => { const ctx = createMockSsoCallbackContext("oidc-org-b"); await expect(ssoIntegration.resolveOrgMembership(userId, ctx)).rejects.toThrow("invite-only"); }); + + test("keeps invited org assignment when session context loses providerId after user creation", async () => { + const userId = await createUser("alice@example.com", randomSlug("alice")); + const inviterId = await createUser("inviter@example.com", randomSlug("inviter")); + const invitedOrgId = randomId(); + + await db.insert(organization).values({ + id: invitedOrgId, + name: "Acme Corp", + slug: randomSlug("acme"), + createdAt: new Date(), + }); + + await db.insert(ssoProvider).values({ + id: randomId(), + providerId: "oidc-acme", + organizationId: invitedOrgId, + userId: inviterId, + issuer: "https://issuer.example.com", + domain: "example.com", + }); + + await db.insert(invitation).values({ + id: randomId(), + organizationId: invitedOrgId, + email: "alice@example.com", + role: "member", + status: "pending", + expiresAt: new Date(Date.now() + 60 * 60 * 1000), + createdAt: new Date(), + inviterId, + }); + + const user = await db.query.usersTable.findFirst({ where: { id: userId } }); + if (!user) { + throw new Error("Expected user to exist"); + } + + await ssoIntegration.onUserCreated(user, createMockSsoCallbackContext("oidc-acme")); + + const membership = await ensureDefaultOrg(userId); + + expect(membership.organizationId).toBe(invitedOrgId); + + const [updatedInvitation] = await db.select().from(invitation).where(eq(invitation.organizationId, invitedOrgId)); + expect(updatedInvitation.status).toBe("accepted"); + }); }); diff --git a/app/server/modules/sso/sso.integration.ts b/app/server/modules/sso/sso.integration.ts index 26940090..4e644d30 100644 --- a/app/server/modules/sso/sso.integration.ts +++ b/app/server/modules/sso/sso.integration.ts @@ -82,6 +82,21 @@ async function onUserCreate( user.hasDownloadedResticPassword = true; } +async function resolveOrgMembershipOrThrow(userId: string, ctx: GenericEndpointContext | null) { + const membership = await resolveOrgMembership(userId, ctx); + if (!membership) { + throw new APIError("BAD_REQUEST", { + message: "Unable to resolve organization membership for this SSO session", + }); + } + + return membership; +} + +async function onUserCreated(user: User, ctx: GenericEndpointContext | null) { + await resolveOrgMembershipOrThrow(user.id, ctx); +} + export const ssoIntegration = { plugin: sso({ trustEmailVerified: false, @@ -101,6 +116,10 @@ export const ssoIntegration = { onUserCreate, + onUserCreated, + + resolveOrgMembershipOrThrow, + resolveOrgMembership, resolveTrustedProviders: resolveTrustedProvidersForRequest,