From 451aed89833f1c0f7467063e9f90265d4ddc5e30 Mon Sep 17 00:00:00 2001 From: Nico <47644445+nicotsx@users.noreply.github.com> Date: Tue, 20 Jan 2026 22:28:22 +0100 Subject: [PATCH] Multi users (#381) * feat(db): add support for multiple users and organizations * feat: backfill entities with new organization id * refactor: filter all backend queries to surface only organization specific entities * refactor: each org has its own restic password * test: ensure organization is created * chore: pr feedbacks * refactor: filter by org id in all places * refactor: download restic password from stored db password * refactor(navigation): use volume id in urls instead of name * feat: disable registrations * refactor(auth): bubble up auth error to hono * refactor: use async local storage for cleaner context sharing * refactor: enable user registration vs disabling it * test: multi-org isolation * chore: final cleanup --- .env.example | 2 +- .env.test | 1 + AGENTS.md | 12 +- .../api-client/@tanstack/react-query.gen.ts | 56 +- app/client/api-client/index.ts | 8 + app/client/api-client/sdk.gen.ts | 46 +- app/client/api-client/types.gen.ts | 788 +++++++- app/client/components/volume-file-browser.tsx | 10 +- app/client/lib/auth-client.ts | 10 +- app/client/lib/constants.ts | 1 + app/client/modules/auth/routes/onboarding.tsx | 3 +- .../components/create-schedule-form.tsx | 2 +- .../backups/components/schedule-summary.tsx | 2 +- .../repositories/routes/snapshot-details.tsx | 2 +- .../modules/settings/routes/settings.tsx | 57 +- .../volumes/components/healthchecks-card.tsx | 4 +- .../modules/volumes/routes/create-volume.tsx | 2 +- .../modules/volumes/routes/volume-details.tsx | 20 +- app/client/modules/volumes/routes/volumes.tsx | 2 +- app/client/modules/volumes/tabs/files.tsx | 2 +- app/client/modules/volumes/tabs/info.tsx | 4 +- app/context.ts | 1 + app/drizzle/0034_slippery_mongu.sql | 44 + app/drizzle/0035_default-admin-role.sql | 1 + app/drizzle/0036_create-default-org.sql | 9 + app/drizzle/0037_create-default-member.sql | 10 + app/drizzle/0038_shallow_pride.sql | 5 + app/drizzle/0039_backfill-entities-org-id.sql | 15 + app/drizzle/0040_tidy_maelstrom.sql | 95 + app/drizzle/0041_motionless_storm.sql | 29 + app/drizzle/0042_watery_liz_osborn.sql | 1 + app/drizzle/meta/0034_snapshot.json | 1550 ++++++++++++++++ app/drizzle/meta/0035_snapshot.json | 1550 ++++++++++++++++ app/drizzle/meta/0036_snapshot.json | 1550 ++++++++++++++++ app/drizzle/meta/0037_snapshot.json | 1550 ++++++++++++++++ app/drizzle/meta/0038_snapshot.json | 1626 ++++++++++++++++ app/drizzle/meta/0039_snapshot.json | 1626 ++++++++++++++++ app/drizzle/meta/0040_snapshot.json | 1626 ++++++++++++++++ app/drizzle/meta/0041_snapshot.json | 1620 ++++++++++++++++ app/drizzle/meta/0042_snapshot.json | 1628 +++++++++++++++++ app/drizzle/meta/_journal.json | 63 + .../auth-middlewares/convert-legacy-user.ts | 4 +- app/lib/auth-middlewares/only-one-user.ts | 16 +- app/lib/auth.ts | 90 +- app/routes.ts | 2 +- app/server/__tests__/isolation.test.ts | 207 +++ app/server/core/config.ts | 25 +- app/server/core/request-context.ts | 24 + app/server/db/schema.ts | 283 ++- app/server/jobs/auto-remount.ts | 5 +- app/server/jobs/backup-execution.ts | 35 +- app/server/jobs/cleanup-dangling.ts | 15 +- app/server/jobs/healthchecks.ts | 11 +- app/server/jobs/repository-healthchecks.ts | 5 +- app/server/modules/auth/auth.middleware.ts | 35 +- .../__tests__/backups.patterns.test.ts | 3 + .../backups/__tests__/backups.service.test.ts | 3 + .../modules/backups/backups.controller.ts | 8 - app/server/modules/backups/backups.service.ts | 85 +- app/server/modules/lifecycle/migrations.ts | 8 +- .../migrations/00001-retag-snapshots.ts | 2 +- .../00002-isolate-restic-passwords.ts | 159 ++ app/server/modules/lifecycle/startup.ts | 30 +- .../notifications/notifications.service.ts | 19 +- .../repositories/repositories.controller.ts | 7 - .../repositories/repositories.service.ts | 77 +- .../modules/system/system.controller.ts | 55 +- app/server/modules/system/system.dto.ts | 47 +- app/server/modules/system/system.service.ts | 32 +- .../modules/volumes/volume.controller.ts | 42 +- app/server/modules/volumes/volume.service.ts | 102 +- app/server/utils/crypto.ts | 24 +- app/server/utils/restic.ts | 101 +- app/test/helpers/auth.ts | 46 +- app/test/helpers/backup.ts | 4 + app/test/helpers/organization.ts | 35 + app/test/helpers/repository.ts | 4 + app/test/helpers/volume.ts | 4 + app/test/setup.ts | 1 + docker-compose.yml | 5 + 80 files changed, 16874 insertions(+), 419 deletions(-) create mode 100644 app/drizzle/0034_slippery_mongu.sql create mode 100644 app/drizzle/0035_default-admin-role.sql create mode 100644 app/drizzle/0036_create-default-org.sql create mode 100644 app/drizzle/0037_create-default-member.sql create mode 100644 app/drizzle/0038_shallow_pride.sql create mode 100644 app/drizzle/0039_backfill-entities-org-id.sql create mode 100644 app/drizzle/0040_tidy_maelstrom.sql create mode 100644 app/drizzle/0041_motionless_storm.sql create mode 100644 app/drizzle/0042_watery_liz_osborn.sql create mode 100644 app/drizzle/meta/0034_snapshot.json create mode 100644 app/drizzle/meta/0035_snapshot.json create mode 100644 app/drizzle/meta/0036_snapshot.json create mode 100644 app/drizzle/meta/0037_snapshot.json create mode 100644 app/drizzle/meta/0038_snapshot.json create mode 100644 app/drizzle/meta/0039_snapshot.json create mode 100644 app/drizzle/meta/0040_snapshot.json create mode 100644 app/drizzle/meta/0041_snapshot.json create mode 100644 app/drizzle/meta/0042_snapshot.json create mode 100644 app/server/__tests__/isolation.test.ts create mode 100644 app/server/core/request-context.ts create mode 100644 app/server/modules/lifecycle/migrations/00002-isolate-restic-passwords.ts create mode 100644 app/test/helpers/organization.ts diff --git a/.env.example b/.env.example index 7ce4dab1..8303e218 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,6 @@ SERVER_IP=localhost DATABASE_URL=./data/zerobyte.db -RESTIC_PASS_FILE=./data/restic.pass RESTIC_CACHE_DIR=./data/restic/cache ZEROBYTE_REPOSITORIES_DIR=./data/repositories ZEROBYTE_VOLUMES_DIR=./data/volumes +APP_SECRET= diff --git a/.env.test b/.env.test index a8c805e0..04a34a89 100644 --- a/.env.test +++ b/.env.test @@ -1 +1,2 @@ DATABASE_URL=:memory: +APP_SECRET=8b9acd4456dd5db0a4a3c4f4e1240b2c3ae08bb59690167197425e4a25dd9a69 diff --git a/AGENTS.md b/AGENTS.md index 9d09ac2c..2e9509f3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -75,14 +75,13 @@ bun run gen:api-client ### Code Quality ```bash -# Format and lint (Biome) -bunx biome check --write . +# Format and lint -# Format only -bunx biome format --write . +# Format +bunx oxfmt format --write -# Lint only -bunx biome lint . +# Lint +bun run lint ``` ## Architecture @@ -186,7 +185,6 @@ Zerobyte is a wrapper around Restic for backup operations. Key integration point - `buildRepoUrl()` - Constructs repository URLs for different backends - `buildEnv()` - Sets environment variables (credentials, cache dir) -- `ensurePassfile()` - Manages encryption password file - Type-safe parsing of Restic JSON output using ArkType schemas **Rclone Integration** (`app/server/modules/repositories/`): diff --git a/app/client/api-client/@tanstack/react-query.gen.ts b/app/client/api-client/@tanstack/react-query.gen.ts index 342f4a36..f29ecb61 100644 --- a/app/client/api-client/@tanstack/react-query.gen.ts +++ b/app/client/api-client/@tanstack/react-query.gen.ts @@ -21,6 +21,7 @@ import { getBackupScheduleForVolume, getMirrorCompatibility, getNotificationDestination, + getRegistrationStatus, getRepository, getScheduleMirrors, getScheduleNotifications, @@ -44,6 +45,7 @@ import { restoreSnapshot, runBackupNow, runForget, + setRegistrationStatus, stopBackup, tagSnapshots, testConnection, @@ -91,6 +93,8 @@ import type { GetMirrorCompatibilityResponse, GetNotificationDestinationData, GetNotificationDestinationResponse, + GetRegistrationStatusData, + GetRegistrationStatusResponse, GetRepositoryData, GetRepositoryResponse, GetScheduleMirrorsData, @@ -135,6 +139,8 @@ import type { RunBackupNowResponse, RunForgetData, RunForgetResponse, + SetRegistrationStatusData, + SetRegistrationStatusResponse, StopBackupData, StopBackupResponse, TagSnapshotsData, @@ -1259,8 +1265,56 @@ export const getUpdatesOptions = (options?: Options) => queryKey: getUpdatesQueryKey(options), }); +export const getRegistrationStatusQueryKey = (options?: Options) => + createQueryKey("getRegistrationStatus", options); + /** - * Download the Restic password file for backup recovery. Requires password re-authentication. + * Get the current registration status for new users + */ +export const getRegistrationStatusOptions = (options?: Options) => + queryOptions< + GetRegistrationStatusResponse, + DefaultError, + GetRegistrationStatusResponse, + ReturnType + >({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getRegistrationStatus({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: getRegistrationStatusQueryKey(options), + }); + +/** + * Update the registration status for new users. Requires global admin role. + */ +export const setRegistrationStatusMutation = ( + options?: Partial>, +): UseMutationOptions> => { + const mutationOptions: UseMutationOptions< + SetRegistrationStatusResponse, + DefaultError, + Options + > = { + mutationFn: async (fnOptions) => { + const { data } = await setRegistrationStatus({ + ...options, + ...fnOptions, + throwOnError: true, + }); + return data; + }, + }; + return mutationOptions; +}; + +/** + * Download the organization's Restic password for backup recovery. Requires organization owner or admin role and password re-authentication. */ export const downloadResticPasswordMutation = ( options?: Partial>, diff --git a/app/client/api-client/index.ts b/app/client/api-client/index.ts index bee3869e..38a8eb3b 100644 --- a/app/client/api-client/index.ts +++ b/app/client/api-client/index.ts @@ -18,6 +18,7 @@ export { getBackupScheduleForVolume, getMirrorCompatibility, getNotificationDestination, + getRegistrationStatus, getRepository, getScheduleMirrors, getScheduleNotifications, @@ -41,6 +42,7 @@ export { restoreSnapshot, runBackupNow, runForget, + setRegistrationStatus, stopBackup, tagSnapshots, testConnection, @@ -108,6 +110,9 @@ export type { GetNotificationDestinationErrors, GetNotificationDestinationResponse, GetNotificationDestinationResponses, + GetRegistrationStatusData, + GetRegistrationStatusResponse, + GetRegistrationStatusResponses, GetRepositoryData, GetRepositoryResponse, GetRepositoryResponses, @@ -176,6 +181,9 @@ export type { RunForgetData, RunForgetResponse, RunForgetResponses, + SetRegistrationStatusData, + SetRegistrationStatusResponse, + SetRegistrationStatusResponses, StopBackupData, StopBackupErrors, StopBackupResponse, diff --git a/app/client/api-client/sdk.gen.ts b/app/client/api-client/sdk.gen.ts index 835080bd..7e570675 100644 --- a/app/client/api-client/sdk.gen.ts +++ b/app/client/api-client/sdk.gen.ts @@ -39,6 +39,8 @@ import type { GetNotificationDestinationData, GetNotificationDestinationErrors, GetNotificationDestinationResponses, + GetRegistrationStatusData, + GetRegistrationStatusResponses, GetRepositoryData, GetRepositoryResponses, GetScheduleMirrorsData, @@ -85,6 +87,8 @@ import type { RunBackupNowResponses, RunForgetData, RunForgetResponses, + SetRegistrationStatusData, + SetRegistrationStatusResponses, StopBackupData, StopBackupErrors, StopBackupResponses, @@ -179,7 +183,7 @@ export const testConnection = ( */ export const deleteVolume = (options: Options) => (options.client ?? client).delete({ - url: "/api/v1/volumes/{name}", + url: "/api/v1/volumes/{id}", ...options, }); @@ -188,7 +192,7 @@ export const deleteVolume = (options: Opti */ export const getVolume = (options: Options) => (options.client ?? client).get({ - url: "/api/v1/volumes/{name}", + url: "/api/v1/volumes/{id}", ...options, }); @@ -197,7 +201,7 @@ export const getVolume = (options: Options */ export const updateVolume = (options: Options) => (options.client ?? client).put({ - url: "/api/v1/volumes/{name}", + url: "/api/v1/volumes/{id}", ...options, headers: { "Content-Type": "application/json", @@ -210,7 +214,7 @@ export const updateVolume = (options: Opti */ export const mountVolume = (options: Options) => (options.client ?? client).post({ - url: "/api/v1/volumes/{name}/mount", + url: "/api/v1/volumes/{id}/mount", ...options, }); @@ -221,7 +225,7 @@ export const unmountVolume = ( options: Options, ) => (options.client ?? client).post({ - url: "/api/v1/volumes/{name}/unmount", + url: "/api/v1/volumes/{id}/unmount", ...options, }); @@ -232,7 +236,7 @@ export const healthCheckVolume = ( options: Options, ) => (options.client ?? client).post({ - url: "/api/v1/volumes/{name}/health-check", + url: "/api/v1/volumes/{id}/health-check", ...options, }); @@ -241,7 +245,7 @@ export const healthCheckVolume = ( */ export const listFiles = (options: Options) => (options.client ?? client).get({ - url: "/api/v1/volumes/{name}/files", + url: "/api/v1/volumes/{id}/files", ...options, }); @@ -708,7 +712,33 @@ export const getUpdates = (options?: Optio }); /** - * Download the Restic password file for backup recovery. Requires password re-authentication. + * Get the current registration status for new users + */ +export const getRegistrationStatus = ( + options?: Options, +) => + (options?.client ?? client).get({ + url: "/api/v1/system/registration-status", + ...options, + }); + +/** + * Update the registration status for new users. Requires global admin role. + */ +export const setRegistrationStatus = ( + options?: Options, +) => + (options?.client ?? client).put({ + url: "/api/v1/system/registration-status", + ...options, + headers: { + "Content-Type": "application/json", + ...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, diff --git a/app/client/api-client/types.gen.ts b/app/client/api-client/types.gen.ts index 3cab252d..ebb93f62 100644 --- a/app/client/api-client/types.gen.ts +++ b/app/client/api-client/types.gen.ts @@ -313,10 +313,10 @@ export type TestConnectionResponse = TestConnectionResponses[keyof TestConnectio export type DeleteVolumeData = { body?: never; path: { - name: string; + id: string; }; query?: never; - url: "/api/v1/volumes/{name}"; + url: "/api/v1/volumes/{id}"; }; export type DeleteVolumeResponses = { @@ -333,10 +333,10 @@ export type DeleteVolumeResponse = DeleteVolumeResponses[keyof DeleteVolumeRespo export type GetVolumeData = { body?: never; path: { - name: string; + id: string; }; query?: never; - url: "/api/v1/volumes/{name}"; + url: "/api/v1/volumes/{id}"; }; export type GetVolumeErrors = { @@ -485,10 +485,10 @@ export type UpdateVolumeData = { name?: string; }; path: { - name: string; + id: string; }; query?: never; - url: "/api/v1/volumes/{name}"; + url: "/api/v1/volumes/{id}"; }; export type UpdateVolumeErrors = { @@ -574,10 +574,10 @@ export type UpdateVolumeResponse = UpdateVolumeResponses[keyof UpdateVolumeRespo export type MountVolumeData = { body?: never; path: { - name: string; + id: string; }; query?: never; - url: "/api/v1/volumes/{name}/mount"; + url: "/api/v1/volumes/{id}/mount"; }; export type MountVolumeResponses = { @@ -595,10 +595,10 @@ export type MountVolumeResponse = MountVolumeResponses[keyof MountVolumeResponse export type UnmountVolumeData = { body?: never; path: { - name: string; + id: string; }; query?: never; - url: "/api/v1/volumes/{name}/unmount"; + url: "/api/v1/volumes/{id}/unmount"; }; export type UnmountVolumeResponses = { @@ -616,10 +616,10 @@ export type UnmountVolumeResponse = UnmountVolumeResponses[keyof UnmountVolumeRe export type HealthCheckVolumeData = { body?: never; path: { - name: string; + id: string; }; query?: never; - url: "/api/v1/volumes/{name}/health-check"; + url: "/api/v1/volumes/{id}/health-check"; }; export type HealthCheckVolumeErrors = { @@ -644,7 +644,7 @@ export type HealthCheckVolumeResponse = HealthCheckVolumeResponses[keyof HealthC export type ListFilesData = { body?: never; path: { - name: string; + id: string; }; query?: { /** @@ -652,7 +652,7 @@ export type ListFilesData = { */ path?: string; }; - url: "/api/v1/volumes/{name}/files"; + url: "/api/v1/volumes/{id}/files"; }; export type ListFilesResponses = { @@ -725,8 +725,18 @@ export type ListRepositoriesResponses = { secretAccessKey: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { accessKeyId: string; @@ -736,8 +746,18 @@ export type ListRepositoriesResponses = { secretAccessKey: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { accountKey: string; @@ -746,9 +766,19 @@ export type ListRepositoriesResponses = { container: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; endpointSuffix?: string; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "gcs"; @@ -757,17 +787,37 @@ export type ListRepositoriesResponses = { projectId: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "local"; name: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; path?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "rclone"; @@ -775,18 +825,38 @@ export type ListRepositoriesResponses = { remote: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "rest"; url: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; password?: string; path?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; username?: string; } | { @@ -799,9 +869,19 @@ export type ListRepositoriesResponses = { skipHostKeyCheck?: boolean; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; knownHosts?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; }; createdAt: number; id: string; @@ -828,8 +908,18 @@ export type CreateRepositoryData = { secretAccessKey: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { accessKeyId: string; @@ -839,8 +929,18 @@ export type CreateRepositoryData = { secretAccessKey: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { accountKey: string; @@ -849,9 +949,19 @@ export type CreateRepositoryData = { container: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; endpointSuffix?: string; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "gcs"; @@ -860,17 +970,37 @@ export type CreateRepositoryData = { projectId: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "local"; name: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; path?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "rclone"; @@ -878,18 +1008,38 @@ export type CreateRepositoryData = { remote: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "rest"; url: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; password?: string; path?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; username?: string; } | { @@ -902,9 +1052,19 @@ export type CreateRepositoryData = { skipHostKeyCheck?: boolean; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; knownHosts?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; }; name: string; compressionMode?: "auto" | "max" | "off"; @@ -993,8 +1153,18 @@ export type GetRepositoryResponses = { secretAccessKey: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { accessKeyId: string; @@ -1004,8 +1174,18 @@ export type GetRepositoryResponses = { secretAccessKey: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { accountKey: string; @@ -1014,9 +1194,19 @@ export type GetRepositoryResponses = { container: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; endpointSuffix?: string; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "gcs"; @@ -1025,17 +1215,37 @@ export type GetRepositoryResponses = { projectId: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "local"; name: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; path?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "rclone"; @@ -1043,18 +1253,38 @@ export type GetRepositoryResponses = { remote: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "rest"; url: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; password?: string; path?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; username?: string; } | { @@ -1067,9 +1297,19 @@ export type GetRepositoryResponses = { skipHostKeyCheck?: boolean; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; knownHosts?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; }; createdAt: number; id: string; @@ -1123,8 +1363,18 @@ export type UpdateRepositoryResponses = { secretAccessKey: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { accessKeyId: string; @@ -1134,8 +1384,18 @@ export type UpdateRepositoryResponses = { secretAccessKey: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { accountKey: string; @@ -1144,9 +1404,19 @@ export type UpdateRepositoryResponses = { container: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; endpointSuffix?: string; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "gcs"; @@ -1155,17 +1425,37 @@ export type UpdateRepositoryResponses = { projectId: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "local"; name: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; path?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "rclone"; @@ -1173,18 +1463,38 @@ export type UpdateRepositoryResponses = { remote: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "rest"; url: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; password?: string; path?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; username?: string; } | { @@ -1197,9 +1507,19 @@ export type UpdateRepositoryResponses = { skipHostKeyCheck?: boolean; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; knownHosts?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; }; createdAt: number; id: string; @@ -1470,8 +1790,18 @@ export type ListBackupSchedulesResponses = { secretAccessKey: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { accessKeyId: string; @@ -1481,8 +1811,18 @@ export type ListBackupSchedulesResponses = { secretAccessKey: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { accountKey: string; @@ -1491,9 +1831,19 @@ export type ListBackupSchedulesResponses = { container: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; endpointSuffix?: string; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "gcs"; @@ -1502,17 +1852,37 @@ export type ListBackupSchedulesResponses = { projectId: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "local"; name: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; path?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "rclone"; @@ -1520,18 +1890,38 @@ export type ListBackupSchedulesResponses = { remote: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "rest"; url: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; password?: string; path?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; username?: string; } | { @@ -1544,9 +1934,19 @@ export type ListBackupSchedulesResponses = { skipHostKeyCheck?: boolean; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; knownHosts?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; }; createdAt: number; id: string; @@ -1762,8 +2162,18 @@ export type GetBackupScheduleResponses = { secretAccessKey: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { accessKeyId: string; @@ -1773,8 +2183,18 @@ export type GetBackupScheduleResponses = { secretAccessKey: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { accountKey: string; @@ -1783,9 +2203,19 @@ export type GetBackupScheduleResponses = { container: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; endpointSuffix?: string; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "gcs"; @@ -1794,17 +2224,37 @@ export type GetBackupScheduleResponses = { projectId: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "local"; name: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; path?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "rclone"; @@ -1812,18 +2262,38 @@ export type GetBackupScheduleResponses = { remote: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "rest"; url: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; password?: string; path?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; username?: string; } | { @@ -1836,9 +2306,19 @@ export type GetBackupScheduleResponses = { skipHostKeyCheck?: boolean; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; knownHosts?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; }; createdAt: number; id: string; @@ -2035,8 +2515,18 @@ export type GetBackupScheduleForVolumeResponses = { secretAccessKey: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { accessKeyId: string; @@ -2046,8 +2536,18 @@ export type GetBackupScheduleForVolumeResponses = { secretAccessKey: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { accountKey: string; @@ -2056,9 +2556,19 @@ export type GetBackupScheduleForVolumeResponses = { container: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; endpointSuffix?: string; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "gcs"; @@ -2067,17 +2577,37 @@ export type GetBackupScheduleForVolumeResponses = { projectId: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "local"; name: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; path?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "rclone"; @@ -2085,18 +2615,38 @@ export type GetBackupScheduleForVolumeResponses = { remote: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "rest"; url: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; password?: string; path?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; username?: string; } | { @@ -2109,9 +2659,19 @@ export type GetBackupScheduleForVolumeResponses = { skipHostKeyCheck?: boolean; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; knownHosts?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; }; createdAt: number; id: string; @@ -2516,8 +3076,18 @@ export type GetScheduleMirrorsResponses = { secretAccessKey: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { accessKeyId: string; @@ -2527,8 +3097,18 @@ export type GetScheduleMirrorsResponses = { secretAccessKey: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { accountKey: string; @@ -2537,9 +3117,19 @@ export type GetScheduleMirrorsResponses = { container: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; endpointSuffix?: string; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "gcs"; @@ -2548,17 +3138,37 @@ export type GetScheduleMirrorsResponses = { projectId: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "local"; name: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; path?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "rclone"; @@ -2566,18 +3176,38 @@ export type GetScheduleMirrorsResponses = { remote: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "rest"; url: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; password?: string; path?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; username?: string; } | { @@ -2590,9 +3220,19 @@ export type GetScheduleMirrorsResponses = { skipHostKeyCheck?: boolean; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; knownHosts?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; }; createdAt: number; id: string; @@ -2646,8 +3286,18 @@ export type UpdateScheduleMirrorsResponses = { secretAccessKey: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { accessKeyId: string; @@ -2657,8 +3307,18 @@ export type UpdateScheduleMirrorsResponses = { secretAccessKey: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { accountKey: string; @@ -2667,9 +3327,19 @@ export type UpdateScheduleMirrorsResponses = { container: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; endpointSuffix?: string; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "gcs"; @@ -2678,17 +3348,37 @@ export type UpdateScheduleMirrorsResponses = { projectId: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "local"; name: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; path?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "rclone"; @@ -2696,18 +3386,38 @@ export type UpdateScheduleMirrorsResponses = { remote: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; } | { backend: "rest"; url: string; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; password?: string; path?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; username?: string; } | { @@ -2720,9 +3430,19 @@ export type UpdateScheduleMirrorsResponses = { skipHostKeyCheck?: boolean; cacert?: string; customPassword?: string; + downloadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; insecureTls?: boolean; isExistingRepository?: boolean; knownHosts?: string; + uploadLimit?: { + unit?: "Gbps" | "Kbps" | "Mbps"; + value?: number; + enabled?: boolean; + }; }; createdAt: number; id: string; @@ -3415,6 +4135,44 @@ export type GetUpdatesResponses = { export type GetUpdatesResponse = GetUpdatesResponses[keyof GetUpdatesResponses]; +export type GetRegistrationStatusData = { + body?: never; + path?: never; + query?: never; + url: "/api/v1/system/registration-status"; +}; + +export type GetRegistrationStatusResponses = { + /** + * Registration status + */ + 200: { + enabled: boolean; + }; +}; + +export type GetRegistrationStatusResponse = GetRegistrationStatusResponses[keyof GetRegistrationStatusResponses]; + +export type SetRegistrationStatusData = { + body?: { + enabled: boolean; + }; + path?: never; + query?: never; + url: "/api/v1/system/registration-status"; +}; + +export type SetRegistrationStatusResponses = { + /** + * Registration status updated + */ + 200: { + enabled: boolean; + }; +}; + +export type SetRegistrationStatusResponse = SetRegistrationStatusResponses[keyof SetRegistrationStatusResponses]; + export type DownloadResticPasswordData = { body?: { password: string; @@ -3426,7 +4184,7 @@ export type DownloadResticPasswordData = { export type DownloadResticPasswordResponses = { /** - * Restic password file content + * Organization's Restic password */ 200: string; }; diff --git a/app/client/components/volume-file-browser.tsx b/app/client/components/volume-file-browser.tsx index fc8826f5..9b1dbf83 100644 --- a/app/client/components/volume-file-browser.tsx +++ b/app/client/components/volume-file-browser.tsx @@ -6,7 +6,7 @@ import { useFileBrowser } from "../hooks/use-file-browser"; import { parseError } from "../lib/errors"; type VolumeFileBrowserProps = { - volumeName: string; + volumeId: string; enabled?: boolean; withCheckboxes?: boolean; selectedPaths?: Set; @@ -18,7 +18,7 @@ type VolumeFileBrowserProps = { }; export const VolumeFileBrowser = ({ - volumeName, + volumeId, enabled = true, withCheckboxes = false, selectedPaths, @@ -31,7 +31,7 @@ export const VolumeFileBrowser = ({ const queryClient = useQueryClient(); const { data, isLoading, error } = useQuery({ - ...listFilesOptions({ path: { name: volumeName } }), + ...listFilesOptions({ path: { id: volumeId } }), enabled, }); @@ -41,7 +41,7 @@ export const VolumeFileBrowser = ({ fetchFolder: async (path) => { return await queryClient.ensureQueryData( listFilesOptions({ - path: { name: volumeName }, + path: { id: volumeId }, query: { path }, }), ); @@ -49,7 +49,7 @@ export const VolumeFileBrowser = ({ prefetchFolder: (path) => { void queryClient.prefetchQuery( listFilesOptions({ - path: { name: volumeName }, + path: { id: volumeId }, query: { path }, }), ); diff --git a/app/client/lib/auth-client.ts b/app/client/lib/auth-client.ts index 8dfd12a5..66974411 100644 --- a/app/client/lib/auth-client.ts +++ b/app/client/lib/auth-client.ts @@ -1,8 +1,14 @@ import { createAuthClient } from "better-auth/react"; -import { twoFactorClient, usernameClient } from "better-auth/client/plugins"; +import { twoFactorClient, usernameClient, adminClient, organizationClient } from "better-auth/client/plugins"; import { inferAdditionalFields } from "better-auth/client/plugins"; import type { auth } from "~/lib/auth"; export const authClient = createAuthClient({ - plugins: [inferAdditionalFields(), usernameClient(), twoFactorClient()], + plugins: [ + inferAdditionalFields(), + usernameClient(), + adminClient(), + organizationClient(), + twoFactorClient(), + ], }); diff --git a/app/client/lib/constants.ts b/app/client/lib/constants.ts index 801c5768..fc2c499a 100644 --- a/app/client/lib/constants.ts +++ b/app/client/lib/constants.ts @@ -1 +1,2 @@ export const REPOSITORY_BASE = "/var/lib/zerobyte/repositories"; +export const REGISTRATION_ENABLED_KEY = "registrations_enabled"; diff --git a/app/client/modules/auth/routes/onboarding.tsx b/app/client/modules/auth/routes/onboarding.tsx index 6290b598..f3d9ad28 100644 --- a/app/client/modules/auth/routes/onboarding.tsx +++ b/app/client/modules/auth/routes/onboarding.tsx @@ -86,7 +86,8 @@ export default function OnboardingPage() { void navigate("/download-recovery-key"); } else if (error) { console.error(error); - toast.error("Failed to create admin user", { description: error.message }); + const errorMessage = error.message ?? "Unknown error"; + toast.error("Failed to create admin user", { description: errorMessage }); } }; diff --git a/app/client/modules/backups/components/create-schedule-form.tsx b/app/client/modules/backups/components/create-schedule-form.tsx index bf78889d..1978c608 100644 --- a/app/client/modules/backups/components/create-schedule-form.tsx +++ b/app/client/modules/backups/components/create-schedule-form.tsx @@ -372,7 +372,7 @@ export const CreateScheduleForm = ({ initialValues, formId, onSubmit, volume }: {
{schedule.name} - + {schedule.volume.name} diff --git a/app/client/modules/repositories/routes/snapshot-details.tsx b/app/client/modules/repositories/routes/snapshot-details.tsx index 2d740d1b..dd31f9a8 100644 --- a/app/client/modules/repositories/routes/snapshot-details.tsx +++ b/app/client/modules/repositories/routes/snapshot-details.tsx @@ -136,7 +136,7 @@ export default function SnapshotDetailsPage({ loaderData }: Route.ComponentProps Volume:

{backupSchedule?.volume.name} diff --git a/app/client/modules/settings/routes/settings.tsx b/app/client/modules/settings/routes/settings.tsx index f06054da..45731e60 100644 --- a/app/client/modules/settings/routes/settings.tsx +++ b/app/client/modules/settings/routes/settings.tsx @@ -1,9 +1,13 @@ -import { useMutation } from "@tanstack/react-query"; -import { Download, KeyRound, User, X } from "lucide-react"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { Download, KeyRound, User, X, Users } from "lucide-react"; import { useState } from "react"; import { useNavigate } from "react-router"; import { toast } from "sonner"; -import { downloadResticPasswordMutation } from "~/client/api-client/@tanstack/react-query.gen"; +import { + downloadResticPasswordMutation, + setRegistrationStatusMutation, + getRegistrationStatusOptions, +} 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 { @@ -17,6 +21,7 @@ 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 { authClient } from "~/client/lib/auth-client"; import { appContext } from "~/context"; import { TwoFactorSection } from "../components/two-factor-section"; @@ -50,6 +55,25 @@ export default function Settings({ loaderData }: Route.ComponentProps) { const [isChangingPassword, setIsChangingPassword] = useState(false); const navigate = useNavigate(); + const isAdmin = loaderData.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 handleLogout = async () => { await authClient.signOut({ @@ -145,6 +169,33 @@ export default function Settings({ loaderData }: Route.ComponentProps) { return ( + {isAdmin && ( +

+ + + System Settings + + Manage system-wide settings +
+ )} + {isAdmin && ( + +
+
+ +

When enabled, new users can sign up

+
+ updateRegistrationStatusMutation.mutate({ body: { enabled: checked } })} + disabled={registrationStatusQuery.isLoading || updateRegistrationStatusMutation.isPending} + /> +
+
+ )}
diff --git a/app/client/modules/volumes/components/healthchecks-card.tsx b/app/client/modules/volumes/components/healthchecks-card.tsx index 86a3c88e..74157b70 100644 --- a/app/client/modules/volumes/components/healthchecks-card.tsx +++ b/app/client/modules/volumes/components/healthchecks-card.tsx @@ -60,7 +60,7 @@ export const HealthchecksCard = ({ volume }: Props) => { - toggleAutoRemount.mutate({ path: { name: volume.name }, body: { autoRemount: !volume.autoRemount } }) + toggleAutoRemount.mutate({ path: { id: volume.shortId }, body: { autoRemount: !volume.autoRemount } }) } disabled={toggleAutoRemount.isPending} enabledLabel="Enabled" @@ -74,7 +74,7 @@ export const HealthchecksCard = ({ volume }: Props) => { variant="outline" className="mt-4" loading={healthcheck.isPending} - onClick={() => healthcheck.mutate({ path: { name: volume.name } })} + onClick={() => healthcheck.mutate({ path: { id: volume.shortId } })} > Run Health Check diff --git a/app/client/modules/volumes/routes/create-volume.tsx b/app/client/modules/volumes/routes/create-volume.tsx index 46bbec06..8d5bbaca 100644 --- a/app/client/modules/volumes/routes/create-volume.tsx +++ b/app/client/modules/volumes/routes/create-volume.tsx @@ -33,7 +33,7 @@ export default function CreateVolume() { ...createVolumeMutation(), onSuccess: (data) => { toast.success("Volume created successfully"); - void navigate(`/volumes/${data.name}`); + void navigate(`/volumes/${data.shortId}`); }, }); diff --git a/app/client/modules/volumes/routes/volume-details.tsx b/app/client/modules/volumes/routes/volume-details.tsx index f2b3da4a..f5faeb81 100644 --- a/app/client/modules/volumes/routes/volume-details.tsx +++ b/app/client/modules/volumes/routes/volume-details.tsx @@ -41,12 +41,12 @@ const getVolumeStatusVariant = (status: VolumeStatus): "success" | "neutral" | " }; export const handle = { - breadcrumb: (match: Route.MetaArgs) => [{ label: "Volumes", href: "/volumes" }, { label: match.params.name }], + breadcrumb: (match: Route.MetaArgs) => [{ label: "Volumes", href: "/volumes" }, { label: match.params.id }], }; export function meta({ params }: Route.MetaArgs) { return [ - { title: `Zerobyte - ${params.name}` }, + { title: `Zerobyte - ${params.id}` }, { name: "description", content: "View and manage volume details, configuration, and files.", @@ -55,19 +55,19 @@ export function meta({ params }: Route.MetaArgs) { } export const clientLoader = async ({ params }: Route.ClientLoaderArgs) => { - const volume = await getVolume({ path: { name: params.name } }); + const volume = await getVolume({ path: { id: params.id } }); if (volume.data) return volume.data; }; export default function VolumeDetails({ loaderData }: Route.ComponentProps) { - const { name } = useParams<{ name: string }>(); + const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams(); const activeTab = searchParams.get("tab") || "info"; const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const { data } = useQuery({ - ...getVolumeOptions({ path: { name: name ?? "" } }), + ...getVolumeOptions({ path: { id: id ?? "" } }), initialData: loaderData, }); @@ -110,10 +110,10 @@ export default function VolumeDetails({ loaderData }: Route.ComponentProps) { const handleConfirmDelete = () => { setShowDeleteConfirm(false); - deleteVol.mutate({ path: { name: name ?? "" } }); + deleteVol.mutate({ path: { id: id ?? "" } }); }; - if (!name) { + if (!id) { return
Volume not found
; } @@ -139,7 +139,7 @@ export default function VolumeDetails({ loaderData }: Route.ComponentProps) {