From 5895bc3ae3d64fcbe1952fe365e132ea0ac2d8b6 Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Sat, 17 Jan 2026 15:24:47 +0100 Subject: [PATCH] refactor: filter all backend queries to surface only organization specific entities --- app/drizzle/0040_tidy_maelstrom.sql | 95 + app/drizzle/meta/0040_snapshot.json | 1626 +++++++++++++++++ app/drizzle/meta/_journal.json | 7 + app/server/db/schema.ts | 16 +- app/server/jobs/auto-remount.ts | 2 +- app/server/jobs/backup-execution.ts | 36 +- app/server/jobs/cleanup-dangling.ts | 10 +- app/server/jobs/healthchecks.ts | 4 +- app/server/jobs/repository-healthchecks.ts | 2 +- .../modules/backups/backups.controller.ts | 26 +- app/server/modules/backups/backups.service.ts | 81 +- app/server/modules/lifecycle/startup.ts | 14 +- .../notifications/notifications.controller.ts | 12 +- .../notifications/notifications.service.ts | 24 +- .../repositories/repositories.controller.ts | 26 +- .../repositories/repositories.service.ts | 64 +- .../modules/volumes/volume.controller.ts | 14 +- app/server/modules/volumes/volume.service.ts | 32 +- 18 files changed, 1934 insertions(+), 157 deletions(-) create mode 100644 app/drizzle/0040_tidy_maelstrom.sql create mode 100644 app/drizzle/meta/0040_snapshot.json diff --git a/app/drizzle/0040_tidy_maelstrom.sql b/app/drizzle/0040_tidy_maelstrom.sql new file mode 100644 index 00000000..51e76f76 --- /dev/null +++ b/app/drizzle/0040_tidy_maelstrom.sql @@ -0,0 +1,95 @@ +PRAGMA foreign_keys=OFF;--> statement-breakpoint +CREATE TABLE `__new_backup_schedules_table` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `short_id` text NOT NULL, + `name` text NOT NULL, + `volume_id` integer NOT NULL, + `repository_id` text NOT NULL, + `enabled` integer DEFAULT true NOT NULL, + `cron_expression` text NOT NULL, + `retention_policy` text, + `exclude_patterns` text DEFAULT '[]', + `exclude_if_present` text DEFAULT '[]', + `include_patterns` text DEFAULT '[]', + `last_backup_at` integer, + `last_backup_status` text, + `last_backup_error` text, + `next_backup_at` integer, + `one_file_system` integer DEFAULT false NOT NULL, + `sort_order` integer DEFAULT 0 NOT NULL, + `created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, + `updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, + `organization_id` text NOT NULL, + FOREIGN KEY (`volume_id`) REFERENCES `volumes_table`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`repository_id`) REFERENCES `repositories_table`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`organization_id`) REFERENCES `organization`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +INSERT INTO `__new_backup_schedules_table`("id", "short_id", "name", "volume_id", "repository_id", "enabled", "cron_expression", "retention_policy", "exclude_patterns", "exclude_if_present", "include_patterns", "last_backup_at", "last_backup_status", "last_backup_error", "next_backup_at", "one_file_system", "sort_order", "created_at", "updated_at", "organization_id") SELECT "id", "short_id", "name", "volume_id", "repository_id", "enabled", "cron_expression", "retention_policy", "exclude_patterns", "exclude_if_present", "include_patterns", "last_backup_at", "last_backup_status", "last_backup_error", "next_backup_at", "one_file_system", "sort_order", "created_at", "updated_at", "organization_id" FROM `backup_schedules_table`;--> statement-breakpoint +DROP TABLE `backup_schedules_table`;--> statement-breakpoint +ALTER TABLE `__new_backup_schedules_table` RENAME TO `backup_schedules_table`;--> statement-breakpoint +PRAGMA foreign_keys=ON;--> statement-breakpoint +CREATE UNIQUE INDEX `backup_schedules_table_short_id_unique` ON `backup_schedules_table` (`short_id`);--> statement-breakpoint +CREATE UNIQUE INDEX `backup_schedules_table_name_unique` ON `backup_schedules_table` (`name`);--> statement-breakpoint +CREATE TABLE `__new_notification_destinations_table` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `name` text NOT NULL, + `enabled` integer DEFAULT true NOT NULL, + `type` text NOT NULL, + `config` text NOT NULL, + `created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, + `updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, + `organization_id` text NOT NULL, + FOREIGN KEY (`organization_id`) REFERENCES `organization`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +INSERT INTO `__new_notification_destinations_table`("id", "name", "enabled", "type", "config", "created_at", "updated_at", "organization_id") SELECT "id", "name", "enabled", "type", "config", "created_at", "updated_at", "organization_id" FROM `notification_destinations_table`;--> statement-breakpoint +DROP TABLE `notification_destinations_table`;--> statement-breakpoint +ALTER TABLE `__new_notification_destinations_table` RENAME TO `notification_destinations_table`;--> statement-breakpoint +CREATE UNIQUE INDEX `notification_destinations_table_name_unique` ON `notification_destinations_table` (`name`);--> statement-breakpoint +CREATE TABLE `__new_repositories_table` ( + `id` text PRIMARY KEY NOT NULL, + `short_id` text NOT NULL, + `name` text NOT NULL, + `type` text NOT NULL, + `config` text NOT NULL, + `compression_mode` text DEFAULT 'auto', + `status` text DEFAULT 'unknown', + `last_checked` integer, + `last_error` text, + `upload_limit_enabled` integer DEFAULT false NOT NULL, + `upload_limit_value` real DEFAULT 1 NOT NULL, + `upload_limit_unit` text DEFAULT 'Mbps' NOT NULL, + `download_limit_enabled` integer DEFAULT false NOT NULL, + `download_limit_value` real DEFAULT 1 NOT NULL, + `download_limit_unit` text DEFAULT 'Mbps' NOT NULL, + `created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, + `updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, + `organization_id` text NOT NULL, + FOREIGN KEY (`organization_id`) REFERENCES `organization`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +INSERT INTO `__new_repositories_table`("id", "short_id", "name", "type", "config", "compression_mode", "status", "last_checked", "last_error", "upload_limit_enabled", "upload_limit_value", "upload_limit_unit", "download_limit_enabled", "download_limit_value", "download_limit_unit", "created_at", "updated_at", "organization_id") SELECT "id", "short_id", "name", "type", "config", "compression_mode", "status", "last_checked", "last_error", "upload_limit_enabled", "upload_limit_value", "upload_limit_unit", "download_limit_enabled", "download_limit_value", "download_limit_unit", "created_at", "updated_at", "organization_id" FROM `repositories_table`;--> statement-breakpoint +DROP TABLE `repositories_table`;--> statement-breakpoint +ALTER TABLE `__new_repositories_table` RENAME TO `repositories_table`;--> statement-breakpoint +CREATE UNIQUE INDEX `repositories_table_short_id_unique` ON `repositories_table` (`short_id`);--> statement-breakpoint +CREATE TABLE `__new_volumes_table` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `short_id` text NOT NULL, + `name` text NOT NULL, + `type` text NOT NULL, + `status` text DEFAULT 'unmounted' NOT NULL, + `last_error` text, + `last_health_check` integer DEFAULT (unixepoch() * 1000) NOT NULL, + `created_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, + `updated_at` integer DEFAULT (unixepoch() * 1000) NOT NULL, + `config` text NOT NULL, + `auto_remount` integer DEFAULT true NOT NULL, + `organization_id` text NOT NULL, + FOREIGN KEY (`organization_id`) REFERENCES `organization`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +INSERT INTO `__new_volumes_table`("id", "short_id", "name", "type", "status", "last_error", "last_health_check", "created_at", "updated_at", "config", "auto_remount", "organization_id") SELECT "id", "short_id", "name", "type", "status", "last_error", "last_health_check", "created_at", "updated_at", "config", "auto_remount", "organization_id" FROM `volumes_table`;--> statement-breakpoint +DROP TABLE `volumes_table`;--> statement-breakpoint +ALTER TABLE `__new_volumes_table` RENAME TO `volumes_table`;--> statement-breakpoint +CREATE UNIQUE INDEX `volumes_table_short_id_unique` ON `volumes_table` (`short_id`); \ No newline at end of file diff --git a/app/drizzle/meta/0040_snapshot.json b/app/drizzle/meta/0040_snapshot.json new file mode 100644 index 00000000..2ab7325a --- /dev/null +++ b/app/drizzle/meta/0040_snapshot.json @@ -0,0 +1,1626 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "6e25d040-091e-47ba-b3be-07dc56a22846", + "prevId": "62f2a91a-1f9e-46c0-b35a-0b9a14b98d5e", + "tables": { + "account": { + "name": "account", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "account_user_id_users_table_id_fk": { + "name": "account_user_id_users_table_id_fk", + "tableFrom": "account", + "tableTo": "users_table", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "app_metadata": { + "name": "app_metadata", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "backup_schedule_mirrors_table": { + "name": "backup_schedule_mirrors_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "schedule_id": { + "name": "schedule_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "last_copy_at": { + "name": "last_copy_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_copy_status": { + "name": "last_copy_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_copy_error": { + "name": "last_copy_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "backup_schedule_mirrors_table_schedule_id_repository_id_unique": { + "name": "backup_schedule_mirrors_table_schedule_id_repository_id_unique", + "columns": [ + "schedule_id", + "repository_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "backup_schedule_mirrors_table_schedule_id_backup_schedules_table_id_fk": { + "name": "backup_schedule_mirrors_table_schedule_id_backup_schedules_table_id_fk", + "tableFrom": "backup_schedule_mirrors_table", + "tableTo": "backup_schedules_table", + "columnsFrom": [ + "schedule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_schedule_mirrors_table_repository_id_repositories_table_id_fk": { + "name": "backup_schedule_mirrors_table_repository_id_repositories_table_id_fk", + "tableFrom": "backup_schedule_mirrors_table", + "tableTo": "repositories_table", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "backup_schedule_notifications_table": { + "name": "backup_schedule_notifications_table", + "columns": { + "schedule_id": { + "name": "schedule_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "destination_id": { + "name": "destination_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "notify_on_start": { + "name": "notify_on_start", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "notify_on_success": { + "name": "notify_on_success", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "notify_on_warning": { + "name": "notify_on_warning", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "notify_on_failure": { + "name": "notify_on_failure", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": {}, + "foreignKeys": { + "backup_schedule_notifications_table_schedule_id_backup_schedules_table_id_fk": { + "name": "backup_schedule_notifications_table_schedule_id_backup_schedules_table_id_fk", + "tableFrom": "backup_schedule_notifications_table", + "tableTo": "backup_schedules_table", + "columnsFrom": [ + "schedule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_schedule_notifications_table_destination_id_notification_destinations_table_id_fk": { + "name": "backup_schedule_notifications_table_destination_id_notification_destinations_table_id_fk", + "tableFrom": "backup_schedule_notifications_table", + "tableTo": "notification_destinations_table", + "columnsFrom": [ + "destination_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "backup_schedule_notifications_table_schedule_id_destination_id_pk": { + "columns": [ + "schedule_id", + "destination_id" + ], + "name": "backup_schedule_notifications_table_schedule_id_destination_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "backup_schedules_table": { + "name": "backup_schedules_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "short_id": { + "name": "short_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "volume_id": { + "name": "volume_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "retention_policy": { + "name": "retention_policy", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "exclude_patterns": { + "name": "exclude_patterns", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'[]'" + }, + "exclude_if_present": { + "name": "exclude_if_present", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'[]'" + }, + "include_patterns": { + "name": "include_patterns", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'[]'" + }, + "last_backup_at": { + "name": "last_backup_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_backup_status": { + "name": "last_backup_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_backup_error": { + "name": "last_backup_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "next_backup_at": { + "name": "next_backup_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "one_file_system": { + "name": "one_file_system", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "backup_schedules_table_short_id_unique": { + "name": "backup_schedules_table_short_id_unique", + "columns": [ + "short_id" + ], + "isUnique": true + }, + "backup_schedules_table_name_unique": { + "name": "backup_schedules_table_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "backup_schedules_table_volume_id_volumes_table_id_fk": { + "name": "backup_schedules_table_volume_id_volumes_table_id_fk", + "tableFrom": "backup_schedules_table", + "tableTo": "volumes_table", + "columnsFrom": [ + "volume_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_schedules_table_repository_id_repositories_table_id_fk": { + "name": "backup_schedules_table_repository_id_repositories_table_id_fk", + "tableFrom": "backup_schedules_table", + "tableTo": "repositories_table", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "backup_schedules_table_organization_id_organization_id_fk": { + "name": "backup_schedules_table_organization_id_organization_id_fk", + "tableFrom": "backup_schedules_table", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "invitation": { + "name": "invitation", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "invitation_organizationId_idx": { + "name": "invitation_organizationId_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + "email" + ], + "isUnique": false + } + }, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_users_table_id_fk": { + "name": "invitation_inviter_id_users_table_id_fk", + "tableFrom": "invitation", + "tableTo": "users_table", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "member": { + "name": "member", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "member_organizationId_idx": { + "name": "member_organizationId_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "member_userId_idx": { + "name": "member_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_users_table_id_fk": { + "name": "member_user_id_users_table_id_fk", + "tableFrom": "member", + "tableTo": "users_table", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notification_destinations_table": { + "name": "notification_destinations_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "notification_destinations_table_name_unique": { + "name": "notification_destinations_table_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "notification_destinations_table_organization_id_organization_id_fk": { + "name": "notification_destinations_table_organization_id_organization_id_fk", + "tableFrom": "notification_destinations_table", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "organization": { + "name": "organization", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "columns": [ + "slug" + ], + "isUnique": true + }, + "organization_slug_uidx": { + "name": "organization_slug_uidx", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "repositories_table": { + "name": "repositories_table", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "short_id": { + "name": "short_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "compression_mode": { + "name": "compression_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'auto'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'unknown'" + }, + "last_checked": { + "name": "last_checked", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "upload_limit_enabled": { + "name": "upload_limit_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "upload_limit_value": { + "name": "upload_limit_value", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "upload_limit_unit": { + "name": "upload_limit_unit", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'Mbps'" + }, + "download_limit_enabled": { + "name": "download_limit_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "download_limit_value": { + "name": "download_limit_value", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "download_limit_unit": { + "name": "download_limit_unit", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'Mbps'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "repositories_table_short_id_unique": { + "name": "repositories_table_short_id_unique", + "columns": [ + "short_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "repositories_table_organization_id_organization_id_fk": { + "name": "repositories_table_organization_id_organization_id_fk", + "tableFrom": "repositories_table", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions_table": { + "name": "sessions_table", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "sessions_table_token_unique": { + "name": "sessions_table_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "sessionsTable_userId_idx": { + "name": "sessionsTable_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_table_user_id_users_table_id_fk": { + "name": "sessions_table_user_id_users_table_id_fk", + "tableFrom": "sessions_table", + "tableTo": "users_table", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "two_factor": { + "name": "two_factor", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "backup_codes": { + "name": "backup_codes", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "twoFactor_secret_idx": { + "name": "twoFactor_secret_idx", + "columns": [ + "secret" + ], + "isUnique": false + }, + "twoFactor_userId_idx": { + "name": "twoFactor_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "two_factor_user_id_users_table_id_fk": { + "name": "two_factor_user_id_users_table_id_fk", + "tableFrom": "two_factor", + "tableTo": "users_table", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users_table": { + "name": "users_table", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "has_downloaded_restic_password": { + "name": "has_downloaded_restic_password", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_username": { + "name": "display_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "two_factor_enabled": { + "name": "two_factor_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "banned": { + "name": "banned", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "users_table_username_unique": { + "name": "users_table_username_unique", + "columns": [ + "username" + ], + "isUnique": true + }, + "users_table_email_unique": { + "name": "users_table_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "verification": { + "name": "verification", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + "identifier" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "volumes_table": { + "name": "volumes_table", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "short_id": { + "name": "short_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unmounted'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_health_check": { + "name": "last_health_check", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch() * 1000)" + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auto_remount": { + "name": "auto_remount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "volumes_table_short_id_unique": { + "name": "volumes_table_short_id_unique", + "columns": [ + "short_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "volumes_table_organization_id_organization_id_fk": { + "name": "volumes_table_organization_id_organization_id_fk", + "tableFrom": "volumes_table", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/app/drizzle/meta/_journal.json b/app/drizzle/meta/_journal.json index d0144604..c90456c4 100644 --- a/app/drizzle/meta/_journal.json +++ b/app/drizzle/meta/_journal.json @@ -281,6 +281,13 @@ "when": 1768658359845, "tag": "0039_backfill-entities-org-id", "breakpoints": true + }, + { + "idx": 40, + "version": "6", + "when": 1768659604809, + "tag": "0040_tidy_maelstrom", + "breakpoints": true } ] } \ No newline at end of file diff --git a/app/server/db/schema.ts b/app/server/db/schema.ts index 77ffbe45..4ae84513 100644 --- a/app/server/db/schema.ts +++ b/app/server/db/schema.ts @@ -185,7 +185,9 @@ export const volumesTable = sqliteTable("volumes_table", { .default(sql`(unixepoch() * 1000)`), config: text("config", { mode: "json" }).$type().notNull(), autoRemount: int("auto_remount", { mode: "boolean" }).notNull().default(true), - organizationId: text("organization_id").references(() => organization.id, { onDelete: "cascade" }), + organizationId: text("organization_id") + .notNull() + .references(() => organization.id, { onDelete: "cascade" }), }); export type Volume = typeof volumesTable.$inferSelect; export type VolumeInsert = typeof volumesTable.$inferInsert; @@ -215,7 +217,9 @@ export const repositoriesTable = sqliteTable("repositories_table", { updatedAt: int("updated_at", { mode: "number" }) .notNull() .default(sql`(unixepoch() * 1000)`), - organizationId: text("organization_id").references(() => organization.id, { onDelete: "cascade" }), + organizationId: text("organization_id") + .notNull() + .references(() => organization.id, { onDelete: "cascade" }), }); export type Repository = typeof repositoriesTable.$inferSelect; export type RepositoryInsert = typeof repositoriesTable.$inferInsert; @@ -259,7 +263,9 @@ export const backupSchedulesTable = sqliteTable("backup_schedules_table", { updatedAt: int("updated_at", { mode: "number" }) .notNull() .default(sql`(unixepoch() * 1000)`), - organizationId: text("organization_id").references(() => organization.id, { onDelete: "cascade" }), + organizationId: text("organization_id") + .notNull() + .references(() => organization.id, { onDelete: "cascade" }), }); export type BackupScheduleInsert = typeof backupSchedulesTable.$inferInsert; @@ -280,7 +286,9 @@ export const notificationDestinationsTable = sqliteTable("notification_destinati updatedAt: int("updated_at", { mode: "number" }) .notNull() .default(sql`(unixepoch() * 1000)`), - organizationId: text("organization_id").references(() => organization.id, { onDelete: "cascade" }), + organizationId: text("organization_id") + .notNull() + .references(() => organization.id, { onDelete: "cascade" }), }); export type NotificationDestination = typeof notificationDestinationsTable.$inferSelect; diff --git a/app/server/jobs/auto-remount.ts b/app/server/jobs/auto-remount.ts index 6c86af9e..2c97b130 100644 --- a/app/server/jobs/auto-remount.ts +++ b/app/server/jobs/auto-remount.ts @@ -16,7 +16,7 @@ export class VolumeAutoRemountJob extends Job { for (const volume of volumes) { if (volume.autoRemount) { try { - await volumeService.mountVolume(volume.name); + await volumeService.mountVolume(volume.name, volume.organizationId); } catch (err) { logger.error(`Failed to auto-remount volume ${volume.name}:`, err); } diff --git a/app/server/jobs/backup-execution.ts b/app/server/jobs/backup-execution.ts index c38947a6..5ad48b7b 100644 --- a/app/server/jobs/backup-execution.ts +++ b/app/server/jobs/backup-execution.ts @@ -1,26 +1,38 @@ import { Job } from "../core/scheduler"; import { backupsService } from "../modules/backups/backups.service"; import { logger } from "../utils/logger"; +import { db } from "../db/db"; export class BackupExecutionJob extends Job { async run() { logger.debug("Checking for backup schedules to execute..."); - const scheduleIds = await backupsService.getSchedulesToExecute(); + const organizations = await db.query.organization.findMany({}); - if (scheduleIds.length === 0) { + let totalExecuted = 0; + + for (const org of organizations) { + const scheduleIds = await backupsService.getSchedulesToExecute(org.id); + + if (scheduleIds.length === 0) { + continue; + } + + logger.info(`Found ${scheduleIds.length} backup schedule(s) to execute for organization ${org.name}`); + + for (const scheduleId of scheduleIds) { + backupsService.executeBackup(scheduleId, org.id).catch((err) => { + logger.error(`Error executing backup for schedule ${scheduleId}:`, err); + }); + } + + totalExecuted += scheduleIds.length; + } + + if (totalExecuted === 0) { logger.debug("No backup schedules to execute"); - return { done: true, timestamp: new Date(), executed: 0 }; } - logger.info(`Found ${scheduleIds.length} backup schedule(s) to execute`); - - for (const scheduleId of scheduleIds) { - backupsService.executeBackup(scheduleId).catch((err) => { - logger.error(`Error executing backup for schedule ${scheduleId}:`, err); - }); - } - - return { done: true, timestamp: new Date(), executed: scheduleIds.length }; + return { done: true, timestamp: new Date(), executed: totalExecuted }; } } diff --git a/app/server/jobs/cleanup-dangling.ts b/app/server/jobs/cleanup-dangling.ts index 858d8565..b82501f8 100644 --- a/app/server/jobs/cleanup-dangling.ts +++ b/app/server/jobs/cleanup-dangling.ts @@ -8,10 +8,18 @@ import { logger } from "../utils/logger"; import { executeUnmount } from "../modules/backends/utils/backend-utils"; import { toMessage } from "../utils/errors"; import { VOLUME_MOUNT_BASE } from "../core/constants"; +import { db } from "../db/db"; export class CleanupDanglingMountsJob extends Job { async run() { - const allVolumes = await volumeService.listVolumes(); + const organizations = await db.query.organization.findMany({}); + + const allVolumes = []; + for (const org of organizations) { + const volumes = await volumeService.listVolumes(org.id); + allVolumes.push(...volumes); + } + const allSystemMounts = await readMountInfo(); for (const mount of allSystemMounts) { diff --git a/app/server/jobs/healthchecks.ts b/app/server/jobs/healthchecks.ts index 07b02062..fec45ade 100644 --- a/app/server/jobs/healthchecks.ts +++ b/app/server/jobs/healthchecks.ts @@ -14,9 +14,9 @@ export class VolumeHealthCheckJob extends Job { }); for (const volume of volumes) { - const { status } = await volumeService.checkHealth(volume.name); + const { status } = await volumeService.checkHealth(volume.name, volume.organizationId); if (status === "error" && volume.autoRemount) { - await volumeService.mountVolume(volume.name); + await volumeService.mountVolume(volume.name, volume.organizationId); } } diff --git a/app/server/jobs/repository-healthchecks.ts b/app/server/jobs/repository-healthchecks.ts index 1e600c9f..db130320 100644 --- a/app/server/jobs/repository-healthchecks.ts +++ b/app/server/jobs/repository-healthchecks.ts @@ -15,7 +15,7 @@ export class RepositoryHealthCheckJob extends Job { for (const repository of repositories) { try { - await repositoriesService.checkHealth(repository.id); + await repositoriesService.checkHealth(repository.id, repository.organizationId); } catch (error) { logger.error(`Health check failed for repository ${repository.name}:`, error); } diff --git a/app/server/modules/backups/backups.controller.ts b/app/server/modules/backups/backups.controller.ts index fefeacb7..d6418bc7 100644 --- a/app/server/modules/backups/backups.controller.ts +++ b/app/server/modules/backups/backups.controller.ts @@ -46,27 +46,27 @@ import { requireAuth } from "../auth/auth.middleware"; export const backupScheduleController = new Hono() .use(requireAuth) .get("/", listBackupSchedulesDto, async (c) => { - const schedules = await backupsService.listSchedules(); + const schedules = await backupsService.listSchedules(c.get("organizationId")); return c.json(schedules, 200); }) .get("/:scheduleId", getBackupScheduleDto, async (c) => { const scheduleId = c.req.param("scheduleId"); - const schedule = await backupsService.getSchedule(Number(scheduleId)); + const schedule = await backupsService.getSchedule(Number(scheduleId), c.get("organizationId")); return c.json(schedule, 200); }) .get("/volume/:volumeId", getBackupScheduleForVolumeDto, async (c) => { const volumeId = c.req.param("volumeId"); - const schedule = await backupsService.getScheduleForVolume(Number(volumeId)); + const schedule = await backupsService.getScheduleForVolume(Number(volumeId), c.get("organizationId")); return c.json(schedule, 200); }) .post("/", createBackupScheduleDto, validator("json", createBackupScheduleBody), async (c) => { const body = c.req.valid("json"); - const schedule = await backupsService.createSchedule(body); + const schedule = await backupsService.createSchedule(body, c.get("organizationId")); return c.json(schedule, 201); }) @@ -74,21 +74,21 @@ export const backupScheduleController = new Hono() const scheduleId = c.req.param("scheduleId"); const body = c.req.valid("json"); - const schedule = await backupsService.updateSchedule(Number(scheduleId), body); + const schedule = await backupsService.updateSchedule(Number(scheduleId), body, c.get("organizationId")); return c.json(schedule, 200); }) .delete("/:scheduleId", deleteBackupScheduleDto, async (c) => { const scheduleId = c.req.param("scheduleId"); - await backupsService.deleteSchedule(Number(scheduleId)); + await backupsService.deleteSchedule(Number(scheduleId), c.get("organizationId")); return c.json({ success: true }, 200); }) .post("/:scheduleId/run", runBackupNowDto, async (c) => { const scheduleId = c.req.param("scheduleId"); - backupsService.executeBackup(Number(scheduleId), true).catch((err) => { + backupsService.executeBackup(Number(scheduleId), c.get("organizationId"), true).catch((err) => { console.error(`Error executing manual backup for schedule ${scheduleId}:`, err); }); @@ -97,14 +97,14 @@ export const backupScheduleController = new Hono() .post("/:scheduleId/stop", stopBackupDto, async (c) => { const scheduleId = c.req.param("scheduleId"); - await backupsService.stopBackup(Number(scheduleId)); + await backupsService.stopBackup(Number(scheduleId), c.get("organizationId")); return c.json({ success: true }, 200); }) .post("/:scheduleId/forget", runForgetDto, async (c) => { const scheduleId = c.req.param("scheduleId"); - await backupsService.runForget(Number(scheduleId)); + await backupsService.runForget(Number(scheduleId), c.get("organizationId")); return c.json({ success: true }, 200); }) @@ -128,27 +128,27 @@ export const backupScheduleController = new Hono() ) .get("/:scheduleId/mirrors", getScheduleMirrorsDto, async (c) => { const scheduleId = Number.parseInt(c.req.param("scheduleId"), 10); - const mirrors = await backupsService.getMirrors(scheduleId); + const mirrors = await backupsService.getMirrors(scheduleId, c.get("organizationId")); return c.json(mirrors, 200); }) .put("/:scheduleId/mirrors", updateScheduleMirrorsDto, validator("json", updateScheduleMirrorsBody), async (c) => { const scheduleId = Number.parseInt(c.req.param("scheduleId"), 10); const body = c.req.valid("json"); - const mirrors = await backupsService.updateMirrors(scheduleId, body); + const mirrors = await backupsService.updateMirrors(scheduleId, c.get("organizationId"), body); return c.json(mirrors, 200); }) .get("/:scheduleId/mirrors/compatibility", getMirrorCompatibilityDto, async (c) => { const scheduleId = Number.parseInt(c.req.param("scheduleId"), 10); - const compatibility = await backupsService.getMirrorCompatibility(scheduleId); + const compatibility = await backupsService.getMirrorCompatibility(scheduleId, c.get("organizationId")); return c.json(compatibility, 200); }) .post("/reorder", reorderBackupSchedulesDto, validator("json", reorderBackupSchedulesBody), async (c) => { const body = c.req.valid("json"); - await backupsService.reorderSchedules(body.scheduleIds); + await backupsService.reorderSchedules(body.scheduleIds, c.get("organizationId")); return c.json({ success: true }, 200); }); diff --git a/app/server/modules/backups/backups.service.ts b/app/server/modules/backups/backups.service.ts index d18e507b..7f6247b4 100644 --- a/app/server/modules/backups/backups.service.ts +++ b/app/server/modules/backups/backups.service.ts @@ -56,8 +56,9 @@ const processPattern = (pattern: string, volumePath: string): string => { return pattern; }; -const listSchedules = async () => { +const listSchedules = async (organizationId: string) => { const schedules = await db.query.backupSchedulesTable.findMany({ + where: eq(backupSchedulesTable.organizationId, organizationId), with: { volume: true, repository: true, @@ -67,9 +68,9 @@ const listSchedules = async () => { return schedules; }; -const getSchedule = async (scheduleId: number) => { +const getSchedule = async (scheduleId: number, organizationId: string) => { const schedule = await db.query.backupSchedulesTable.findFirst({ - where: eq(backupSchedulesTable.id, scheduleId), + where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)), with: { volume: true, repository: true, @@ -83,13 +84,13 @@ const getSchedule = async (scheduleId: number) => { return schedule; }; -const createSchedule = async (data: CreateBackupScheduleBody) => { +const createSchedule = async (data: CreateBackupScheduleBody, organizationId: string) => { if (!cron.validate(data.cronExpression)) { throw new BadRequestError("Invalid cron expression"); } const existingName = await db.query.backupSchedulesTable.findFirst({ - where: eq(backupSchedulesTable.name, data.name), + where: and(eq(backupSchedulesTable.name, data.name), eq(backupSchedulesTable.organizationId, organizationId)), }); if (existingName) { @@ -97,7 +98,7 @@ const createSchedule = async (data: CreateBackupScheduleBody) => { } const volume = await db.query.volumesTable.findFirst({ - where: eq(volumesTable.id, data.volumeId), + where: and(eq(volumesTable.id, data.volumeId), eq(volumesTable.organizationId, organizationId)), }); if (!volume) { @@ -105,7 +106,7 @@ const createSchedule = async (data: CreateBackupScheduleBody) => { } const repository = await db.query.repositoriesTable.findFirst({ - where: eq(repositoriesTable.id, data.repositoryId), + where: and(eq(repositoriesTable.id, data.repositoryId), eq(repositoriesTable.organizationId, organizationId)), }); if (!repository) { @@ -129,6 +130,7 @@ const createSchedule = async (data: CreateBackupScheduleBody) => { oneFileSystem: data.oneFileSystem, nextBackupAt: nextBackupAt, shortId: generateShortId(), + organizationId, }) .returning(); @@ -139,9 +141,9 @@ const createSchedule = async (data: CreateBackupScheduleBody) => { return newSchedule; }; -const updateSchedule = async (scheduleId: number, data: UpdateBackupScheduleBody) => { +const updateSchedule = async (scheduleId: number, data: UpdateBackupScheduleBody, organizationId: string) => { const schedule = await db.query.backupSchedulesTable.findFirst({ - where: eq(backupSchedulesTable.id, scheduleId), + where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)), }); if (!schedule) { @@ -154,7 +156,7 @@ const updateSchedule = async (scheduleId: number, data: UpdateBackupScheduleBody if (data.name) { const existingName = await db.query.backupSchedulesTable.findFirst({ - where: and(eq(backupSchedulesTable.name, data.name), ne(backupSchedulesTable.id, scheduleId)), + where: and(eq(backupSchedulesTable.name, data.name), ne(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)), }); if (existingName) { @@ -163,7 +165,7 @@ const updateSchedule = async (scheduleId: number, data: UpdateBackupScheduleBody } const repository = await db.query.repositoriesTable.findFirst({ - where: eq(repositoriesTable.id, data.repositoryId), + where: and(eq(repositoriesTable.id, data.repositoryId), eq(repositoriesTable.organizationId, organizationId)), }); if (!repository) { @@ -186,9 +188,9 @@ const updateSchedule = async (scheduleId: number, data: UpdateBackupScheduleBody return updated; }; -const deleteSchedule = async (scheduleId: number) => { +const deleteSchedule = async (scheduleId: number, organizationId: string) => { const schedule = await db.query.backupSchedulesTable.findFirst({ - where: eq(backupSchedulesTable.id, scheduleId), + where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)), }); if (!schedule) { @@ -198,9 +200,9 @@ const deleteSchedule = async (scheduleId: number) => { await db.delete(backupSchedulesTable).where(eq(backupSchedulesTable.id, scheduleId)); }; -const executeBackup = async (scheduleId: number, manual = false) => { +const executeBackup = async (scheduleId: number, organizationId: string, manual = false) => { const schedule = await db.query.backupSchedulesTable.findFirst({ - where: eq(backupSchedulesTable.id, scheduleId), + where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)), }); if (!schedule) { @@ -319,12 +321,12 @@ const executeBackup = async (scheduleId: number, manual = false) => { } if (schedule.retentionPolicy) { - void runForget(schedule.id).catch((error) => { + void runForget(schedule.id, organizationId).catch((error) => { logger.error(`Failed to run retention policy for schedule ${scheduleId}: ${toMessage(error)}`); }); } - void copyToMirrors(scheduleId, repository, schedule.retentionPolicy).catch((error) => { + void copyToMirrors(scheduleId, organizationId, repository, schedule.retentionPolicy).catch((error) => { logger.error(`Background mirror copy failed for schedule ${scheduleId}: ${toMessage(error)}`); }); @@ -407,12 +409,13 @@ const executeBackup = async (scheduleId: number, manual = false) => { } }; -const getSchedulesToExecute = async () => { +const getSchedulesToExecute = async (organizationId: string) => { const now = Date.now(); const schedules = await db.query.backupSchedulesTable.findMany({ where: and( eq(backupSchedulesTable.enabled, true), or(ne(backupSchedulesTable.lastBackupStatus, "in_progress"), isNull(backupSchedulesTable.lastBackupStatus)), + eq(backupSchedulesTable.organizationId, organizationId) ), }); @@ -427,18 +430,18 @@ const getSchedulesToExecute = async () => { return schedulesToRun; }; -const getScheduleForVolume = async (volumeId: number) => { +const getScheduleForVolume = async (volumeId: number, organizationId: string) => { const schedule = await db.query.backupSchedulesTable.findFirst({ - where: eq(backupSchedulesTable.volumeId, volumeId), + where: and(eq(backupSchedulesTable.volumeId, volumeId), eq(backupSchedulesTable.organizationId, organizationId)), with: { volume: true, repository: true }, }); return schedule ?? null; }; -const stopBackup = async (scheduleId: number) => { +const stopBackup = async (scheduleId: number, organizationId: string) => { const schedule = await db.query.backupSchedulesTable.findFirst({ - where: eq(backupSchedulesTable.id, scheduleId), + where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)), }); if (!schedule) { @@ -466,9 +469,9 @@ const stopBackup = async (scheduleId: number) => { } }; -const runForget = async (scheduleId: number, repositoryId?: string) => { +const runForget = async (scheduleId: number, organizationId: string, repositoryId?: string) => { const schedule = await db.query.backupSchedulesTable.findFirst({ - where: eq(backupSchedulesTable.id, scheduleId), + where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)), }); if (!schedule) { @@ -480,7 +483,7 @@ const runForget = async (scheduleId: number, repositoryId?: string) => { } const repository = await db.query.repositoriesTable.findFirst({ - where: eq(repositoriesTable.id, repositoryId ?? schedule.repositoryId), + where: and(eq(repositoriesTable.id, repositoryId ?? schedule.repositoryId), eq(repositoriesTable.organizationId, organizationId)), }); if (!repository) { @@ -499,9 +502,9 @@ const runForget = async (scheduleId: number, repositoryId?: string) => { logger.info(`Retention policy applied successfully for schedule ${scheduleId}`); }; -const getMirrors = async (scheduleId: number) => { +const getMirrors = async (scheduleId: number, organizationId: string) => { const schedule = await db.query.backupSchedulesTable.findFirst({ - where: eq(backupSchedulesTable.id, scheduleId), + where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)), }); if (!schedule) { @@ -516,9 +519,9 @@ const getMirrors = async (scheduleId: number) => { return mirrors; }; -const updateMirrors = async (scheduleId: number, data: UpdateScheduleMirrorsBody) => { +const updateMirrors = async (scheduleId: number, organizationId: string, data: UpdateScheduleMirrorsBody) => { const schedule = await db.query.backupSchedulesTable.findFirst({ - where: eq(backupSchedulesTable.id, scheduleId), + where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)), with: { repository: true }, }); @@ -532,7 +535,7 @@ const updateMirrors = async (scheduleId: number, data: UpdateScheduleMirrorsBody } const repo = await db.query.repositoriesTable.findFirst({ - where: eq(repositoriesTable.id, mirror.repositoryId), + where: and(eq(repositoriesTable.id, mirror.repositoryId), eq(repositoriesTable.organizationId, organizationId)), }); if (!repo) { @@ -577,16 +580,17 @@ const updateMirrors = async (scheduleId: number, data: UpdateScheduleMirrorsBody ); } - return getMirrors(scheduleId); + return getMirrors(scheduleId, organizationId); }; const copyToMirrors = async ( scheduleId: number, + organizationId: string, sourceRepository: { id: string; config: (typeof repositoriesTable.$inferSelect)["config"] }, retentionPolicy: (typeof backupSchedulesTable.$inferSelect)["retentionPolicy"], ) => { const schedule = await db.query.backupSchedulesTable.findFirst({ - where: eq(backupSchedulesTable.id, scheduleId), + where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)), }); if (!schedule) { @@ -630,7 +634,7 @@ const copyToMirrors = async ( } if (retentionPolicy) { - void runForget(scheduleId, mirror.repository.id).catch((error) => { + void runForget(scheduleId, organizationId, mirror.repository.id).catch((error) => { logger.error( `Failed to run retention policy for mirror repository ${mirror.repository.name}: ${toMessage(error)}`, ); @@ -670,9 +674,9 @@ const copyToMirrors = async ( } }; -const getMirrorCompatibility = async (scheduleId: number) => { +const getMirrorCompatibility = async (scheduleId: number, organizationId: string) => { const schedule = await db.query.backupSchedulesTable.findFirst({ - where: eq(backupSchedulesTable.id, scheduleId), + where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)), with: { repository: true }, }); @@ -680,7 +684,9 @@ const getMirrorCompatibility = async (scheduleId: number) => { throw new NotFoundError("Backup schedule not found"); } - const allRepositories = await db.query.repositoriesTable.findMany(); + const allRepositories = await db.query.repositoriesTable.findMany({ + where: eq(repositoriesTable.organizationId, organizationId), + }); const repos = allRepositories.filter((repo) => repo.id !== schedule.repositoryId); const compatibility = await Promise.all( @@ -690,13 +696,14 @@ const getMirrorCompatibility = async (scheduleId: number) => { return compatibility; }; -const reorderSchedules = async (scheduleIds: number[]) => { +const reorderSchedules = async (scheduleIds: number[], organizationId: string) => { const uniqueIds = new Set(scheduleIds); if (uniqueIds.size !== scheduleIds.length) { throw new BadRequestError("Duplicate schedule IDs in reorder request"); } const existingSchedules = await db.query.backupSchedulesTable.findMany({ + where: eq(backupSchedulesTable.organizationId, organizationId), columns: { id: true }, }); const existingIds = new Set(existingSchedules.map((s) => s.id)); diff --git a/app/server/modules/lifecycle/startup.ts b/app/server/modules/lifecycle/startup.ts index f7e5fadb..1708d942 100644 --- a/app/server/modules/lifecycle/startup.ts +++ b/app/server/modules/lifecycle/startup.ts @@ -20,7 +20,7 @@ const ensureLatestConfigurationSchema = async () => { const volumes = await db.query.volumesTable.findMany({}); for (const volume of volumes) { - await volumeService.updateVolume(volume.name, volume).catch((err) => { + await volumeService.updateVolume(volume.name, volume, volume.organizationId).catch((err) => { logger.error(`Failed to update volume ${volume.name}: ${err}`); }); } @@ -28,7 +28,7 @@ const ensureLatestConfigurationSchema = async () => { const repositories = await db.query.repositoriesTable.findMany({}); for (const repo of repositories) { - await repositoriesService.updateRepository(repo.id, {}).catch((err) => { + await repositoriesService.updateRepository(repo.id, repo.organizationId, {}).catch((err) => { logger.error(`Failed to update repository ${repo.name}: ${err}`); }); } @@ -36,9 +36,11 @@ const ensureLatestConfigurationSchema = async () => { const notifications = await db.query.notificationDestinationsTable.findMany({}); for (const notification of notifications) { - await notificationsService.updateDestination(notification.id, notification).catch((err) => { - logger.error(`Failed to update notification destination ${notification.id}: ${err}`); - }); + await notificationsService + .updateDestination(notification.id, notification.organizationId, notification) + .catch((err) => { + logger.error(`Failed to update notification destination ${notification.id}: ${err}`); + }); } }; @@ -67,7 +69,7 @@ export const startup = async () => { }); for (const volume of volumes) { - await volumeService.mountVolume(volume.name).catch((err) => { + await volumeService.mountVolume(volume.name, volume.organizationId).catch((err) => { logger.error(`Error auto-remounting volume ${volume.name} on startup: ${err.message}`); }); } diff --git a/app/server/modules/notifications/notifications.controller.ts b/app/server/modules/notifications/notifications.controller.ts index 42a4b0e7..b1d23f83 100644 --- a/app/server/modules/notifications/notifications.controller.ts +++ b/app/server/modules/notifications/notifications.controller.ts @@ -22,32 +22,32 @@ import { requireAuth } from "../auth/auth.middleware"; export const notificationsController = new Hono() .use(requireAuth) .get("/destinations", listDestinationsDto, async (c) => { - const destinations = await notificationsService.listDestinations(); + const destinations = await notificationsService.listDestinations(c.get("organizationId")); return c.json(destinations, 200); }) .post("/destinations", createDestinationDto, validator("json", createDestinationBody), async (c) => { const body = c.req.valid("json"); - const destination = await notificationsService.createDestination(body.name, body.config); + const destination = await notificationsService.createDestination(body.name, body.config, c.get("organizationId")); return c.json(destination, 201); }) .get("/destinations/:id", getDestinationDto, async (c) => { const id = Number.parseInt(c.req.param("id"), 10); - const destination = await notificationsService.getDestination(id); + const destination = await notificationsService.getDestination(id, c.get("organizationId")); return c.json(destination, 200); }) .patch("/destinations/:id", updateDestinationDto, validator("json", updateDestinationBody), async (c) => { const id = Number.parseInt(c.req.param("id"), 10); const body = c.req.valid("json"); - const destination = await notificationsService.updateDestination(id, body); + const destination = await notificationsService.updateDestination(id, c.get("organizationId"), body); return c.json(destination, 200); }) .delete("/destinations/:id", deleteDestinationDto, async (c) => { const id = Number.parseInt(c.req.param("id"), 10); - await notificationsService.deleteDestination(id); + await notificationsService.deleteDestination(id, c.get("organizationId")); return c.json({ message: "Notification destination deleted" }, 200); }) .post("/destinations/:id/test", testDestinationDto, async (c) => { const id = Number.parseInt(c.req.param("id"), 10); - const result = await notificationsService.testDestination(id); + const result = await notificationsService.testDestination(id, c.get("organizationId")); return c.json(result, 200); }); diff --git a/app/server/modules/notifications/notifications.service.ts b/app/server/modules/notifications/notifications.service.ts index 395056ff..aab17716 100644 --- a/app/server/modules/notifications/notifications.service.ts +++ b/app/server/modules/notifications/notifications.service.ts @@ -15,16 +15,17 @@ import { notificationConfigSchema, type NotificationConfig, type NotificationEve import { toMessage } from "../../utils/errors"; import { type } from "arktype"; -const listDestinations = async () => { +const listDestinations = async (organizationId: string) => { const destinations = await db.query.notificationDestinationsTable.findMany({ + where: eq(notificationDestinationsTable.organizationId, organizationId), orderBy: (destinations, { asc }) => [asc(destinations.name)], }); return destinations; }; -const getDestination = async (id: number) => { +const getDestination = async (id: number, organizationId: string) => { const destination = await db.query.notificationDestinationsTable.findFirst({ - where: eq(notificationDestinationsTable.id, id), + where: and(eq(notificationDestinationsTable.id, id), eq(notificationDestinationsTable.organizationId, organizationId)), }); if (!destination) { @@ -132,11 +133,11 @@ async function decryptSensitiveFields(config: NotificationConfig): Promise { +const createDestination = async (name: string, config: NotificationConfig, organizationId: string) => { const slug = slugify(name, { lower: true, strict: true }); const existing = await db.query.notificationDestinationsTable.findFirst({ - where: eq(notificationDestinationsTable.name, slug), + where: and(eq(notificationDestinationsTable.name, slug), eq(notificationDestinationsTable.organizationId, organizationId)), }); if (existing) { @@ -151,6 +152,7 @@ const createDestination = async (name: string, config: NotificationConfig) => { name: slug, type: config.type, config: encryptedConfig, + organizationId, }) .returning(); @@ -163,9 +165,10 @@ const createDestination = async (name: string, config: NotificationConfig) => { const updateDestination = async ( id: number, + organizationId: string, updates: { name?: string; enabled?: boolean; config?: NotificationConfig }, ) => { - const existing = await getDestination(id); + const existing = await getDestination(id, organizationId); if (!existing) { throw new NotFoundError("Notification destination not found"); @@ -179,7 +182,7 @@ const updateDestination = async ( const slug = slugify(updates.name, { lower: true, strict: true }); const conflict = await db.query.notificationDestinationsTable.findFirst({ - where: and(eq(notificationDestinationsTable.name, slug), ne(notificationDestinationsTable.id, id)), + where: and(eq(notificationDestinationsTable.name, slug), ne(notificationDestinationsTable.id, id), eq(notificationDestinationsTable.organizationId, organizationId)), }); if (conflict) { @@ -214,12 +217,13 @@ const updateDestination = async ( return updated; }; -const deleteDestination = async (id: number) => { +const deleteDestination = async (id: number, organizationId: string) => { + await getDestination(id, organizationId); await db.delete(notificationDestinationsTable).where(eq(notificationDestinationsTable.id, id)); }; -const testDestination = async (id: number) => { - const destination = await getDestination(id); +const testDestination = async (id: number, organizationId: string) => { + const destination = await getDestination(id, organizationId); if (!destination.enabled) { throw new ConflictError("Cannot test disabled notification destination"); diff --git a/app/server/modules/repositories/repositories.controller.ts b/app/server/modules/repositories/repositories.controller.ts index b36d8c25..83c0f249 100644 --- a/app/server/modules/repositories/repositories.controller.ts +++ b/app/server/modules/repositories/repositories.controller.ts @@ -42,13 +42,13 @@ import { requireAuth } from "../auth/auth.middleware"; export const repositoriesController = new Hono() .use(requireAuth) .get("/", listRepositoriesDto, async (c) => { - const repositories = await repositoriesService.listRepositories(); + const repositories = await repositoriesService.listRepositories(c.get("organizationId")); return c.json(repositories, 200); }) .post("/", createRepositoryDto, validator("json", createRepositoryBody), async (c) => { const body = c.req.valid("json"); - const res = await repositoriesService.createRepository(body.name, body.config, body.compressionMode); + const res = await repositoriesService.createRepository(body.name, body.config, c.get("organizationId"), body.compressionMode); return c.json({ message: "Repository created", repository: res.repository }, 201); }) @@ -69,13 +69,13 @@ export const repositoriesController = new Hono() }) .get("/:id", getRepositoryDto, async (c) => { const { id } = c.req.param(); - const res = await repositoriesService.getRepository(id); + const res = await repositoriesService.getRepository(id, c.get("organizationId")); return c.json(res.repository, 200); }) .delete("/:id", deleteRepositoryDto, async (c) => { const { id } = c.req.param(); - await repositoriesService.deleteRepository(id); + await repositoriesService.deleteRepository(id, c.get("organizationId")); return c.json({ message: "Repository deleted" }, 200); }) @@ -83,7 +83,7 @@ export const repositoriesController = new Hono() const { id } = c.req.param(); const { backupId } = c.req.valid("query"); - const res = await repositoriesService.listSnapshots(id, backupId); + const res = await repositoriesService.listSnapshots(id, c.get("organizationId"), backupId); const snapshots = res.map((snapshot) => { const { summary } = snapshot; @@ -108,7 +108,7 @@ export const repositoriesController = new Hono() }) .get("/:id/snapshots/:snapshotId", getSnapshotDetailsDto, async (c) => { const { id, snapshotId } = c.req.param(); - const snapshot = await repositoriesService.getSnapshotDetails(id, snapshotId); + const snapshot = await repositoriesService.getSnapshotDetails(id, c.get("organizationId"), snapshotId); let duration = 0; if (snapshot.summary) { @@ -137,7 +137,7 @@ export const repositoriesController = new Hono() const { path } = c.req.valid("query"); const decodedPath = path ? decodeURIComponent(path) : undefined; - const result = await repositoriesService.listSnapshotFiles(id, snapshotId, decodedPath); + const result = await repositoriesService.listSnapshotFiles(id, c.get("organizationId"), snapshotId, decodedPath); c.header("Cache-Control", "max-age=300, stale-while-revalidate=600"); @@ -148,21 +148,21 @@ export const repositoriesController = new Hono() const { id } = c.req.param(); const { snapshotId, ...options } = c.req.valid("json"); - const result = await repositoriesService.restoreSnapshot(id, snapshotId, options); + const result = await repositoriesService.restoreSnapshot(id, c.get("organizationId"), snapshotId, options); return c.json(result, 200); }) .post("/:id/doctor", doctorRepositoryDto, async (c) => { const { id } = c.req.param(); - const result = await repositoriesService.doctorRepository(id); + const result = await repositoriesService.doctorRepository(id, c.get("organizationId")); return c.json(result, 200); }) .delete("/:id/snapshots/:snapshotId", deleteSnapshotDto, async (c) => { const { id, snapshotId } = c.req.param(); - await repositoriesService.deleteSnapshot(id, snapshotId); + await repositoriesService.deleteSnapshot(id, c.get("organizationId"), snapshotId); return c.json({ message: "Snapshot deleted" }, 200); }) @@ -170,7 +170,7 @@ export const repositoriesController = new Hono() const { id } = c.req.param(); const { snapshotIds } = c.req.valid("json"); - await repositoriesService.deleteSnapshots(id, snapshotIds); + await repositoriesService.deleteSnapshots(id, c.get("organizationId"), snapshotIds); return c.json({ message: "Snapshots deleted" }, 200); }) @@ -178,7 +178,7 @@ export const repositoriesController = new Hono() const { id } = c.req.param(); const { snapshotIds, ...tags } = c.req.valid("json"); - await repositoriesService.tagSnapshots(id, snapshotIds, tags); + await repositoriesService.tagSnapshots(id, c.get("organizationId"), snapshotIds, tags); return c.json({ message: "Snapshots tagged" }, 200); }) @@ -186,7 +186,7 @@ export const repositoriesController = new Hono() const { id } = c.req.param(); const body = c.req.valid("json"); - const res = await repositoriesService.updateRepository(id, body); + const res = await repositoriesService.updateRepository(id, c.get("organizationId"), body); return c.json(res.repository, 200); }); diff --git a/app/server/modules/repositories/repositories.service.ts b/app/server/modules/repositories/repositories.service.ts index e647259a..bd8cb336 100644 --- a/app/server/modules/repositories/repositories.service.ts +++ b/app/server/modules/repositories/repositories.service.ts @@ -1,5 +1,5 @@ import crypto from "node:crypto"; -import { eq, or } from "drizzle-orm"; +import { and, eq, or } from "drizzle-orm"; import { InternalServerError, NotFoundError } from "http-errors-enhanced"; import { db } from "../../db/db"; import { repositoriesTable } from "../../db/schema"; @@ -17,14 +17,19 @@ import { } from "~/schemas/restic"; import { type } from "arktype"; -const findRepository = async (idOrShortId: string) => { +const findRepository = async (idOrShortId: string, organizationId: string) => { return await db.query.repositoriesTable.findFirst({ - where: or(eq(repositoriesTable.id, idOrShortId), eq(repositoriesTable.shortId, idOrShortId)), + where: and( + or(eq(repositoriesTable.id, idOrShortId), eq(repositoriesTable.shortId, idOrShortId)), + eq(repositoriesTable.organizationId, organizationId) + ), }); }; -const listRepositories = async () => { - const repositories = await db.query.repositoriesTable.findMany({}); +const listRepositories = async (organizationId: string) => { + const repositories = await db.query.repositoriesTable.findMany({ + where: eq(repositoriesTable.organizationId, organizationId), + }); return repositories; }; @@ -67,7 +72,7 @@ const encryptConfig = async (config: RepositoryConfig): Promise { +const createRepository = async (name: string, config: RepositoryConfig, organizationId: string, compressionMode?: CompressionMode) => { const id = crypto.randomUUID(); const shortId = generateShortId(); @@ -88,6 +93,7 @@ const createRepository = async (name: string, config: RepositoryConfig, compress config: encryptedConfig, compressionMode: compressionMode ?? "auto", status: "unknown", + organizationId, }) .returning(); @@ -124,8 +130,8 @@ const createRepository = async (name: string, config: RepositoryConfig, compress throw new InternalServerError(`Failed to initialize repository: ${errorMessage}`); }; -const getRepository = async (id: string) => { - const repository = await findRepository(id); +const getRepository = async (id: string, organizationId: string) => { + const repository = await findRepository(id, organizationId); if (!repository) { throw new NotFoundError("Repository not found"); @@ -134,8 +140,8 @@ const getRepository = async (id: string) => { return { repository }; }; -const deleteRepository = async (id: string) => { - const repository = await findRepository(id); +const deleteRepository = async (id: string, organizationId: string) => { + const repository = await findRepository(id, organizationId); if (!repository) { throw new NotFoundError("Repository not found"); @@ -157,8 +163,8 @@ const deleteRepository = async (id: string) => { * * @returns List of snapshots */ -const listSnapshots = async (id: string, backupId?: string) => { - const repository = await findRepository(id); +const listSnapshots = async (id: string, organizationId: string, backupId?: string) => { + const repository = await findRepository(id, organizationId); if (!repository) { throw new NotFoundError("Repository not found"); @@ -188,8 +194,8 @@ const listSnapshots = async (id: string, backupId?: string) => { } }; -const listSnapshotFiles = async (id: string, snapshotId: string, path?: string) => { - const repository = await findRepository(id); +const listSnapshotFiles = async (id: string, organizationId: string, snapshotId: string, path?: string) => { + const repository = await findRepository(id, organizationId); if (!repository) { throw new NotFoundError("Repository not found"); @@ -233,6 +239,7 @@ const listSnapshotFiles = async (id: string, snapshotId: string, path?: string) const restoreSnapshot = async ( id: string, + organizationId: string, snapshotId: string, options?: { include?: string[]; @@ -243,7 +250,7 @@ const restoreSnapshot = async ( overwrite?: OverwriteMode; }, ) => { - const repository = await findRepository(id); + const repository = await findRepository(id, organizationId); if (!repository) { throw new NotFoundError("Repository not found"); @@ -266,8 +273,8 @@ const restoreSnapshot = async ( } }; -const getSnapshotDetails = async (id: string, snapshotId: string) => { - const repository = await findRepository(id); +const getSnapshotDetails = async (id: string, organizationId: string, snapshotId: string) => { + const repository = await findRepository(id, organizationId); if (!repository) { throw new NotFoundError("Repository not found"); @@ -295,8 +302,8 @@ const getSnapshotDetails = async (id: string, snapshotId: string) => { return snapshot; }; -const checkHealth = async (repositoryId: string) => { - const repository = await findRepository(repositoryId); +const checkHealth = async (repositoryId: string, organizationId: string) => { + const repository = await findRepository(repositoryId, organizationId); if (!repository) { throw new NotFoundError("Repository not found"); @@ -321,8 +328,8 @@ const checkHealth = async (repositoryId: string) => { } }; -const doctorRepository = async (id: string) => { - const repository = await findRepository(id); +const doctorRepository = async (id: string, organizationId: string) => { + const repository = await findRepository(id, organizationId); if (!repository) { throw new NotFoundError("Repository not found"); @@ -410,8 +417,8 @@ const doctorRepository = async (id: string) => { }; }; -const deleteSnapshot = async (id: string, snapshotId: string) => { - const repository = await findRepository(id); +const deleteSnapshot = async (id: string, organizationId: string, snapshotId: string) => { + const repository = await findRepository(id, organizationId); if (!repository) { throw new NotFoundError("Repository not found"); @@ -427,8 +434,8 @@ const deleteSnapshot = async (id: string, snapshotId: string) => { } }; -const deleteSnapshots = async (id: string, snapshotIds: string[]) => { - const repository = await findRepository(id); +const deleteSnapshots = async (id: string, organizationId: string, snapshotIds: string[]) => { + const repository = await findRepository(id, organizationId); if (!repository) { throw new NotFoundError("Repository not found"); @@ -448,10 +455,11 @@ const deleteSnapshots = async (id: string, snapshotIds: string[]) => { const tagSnapshots = async ( id: string, + organizationId: string, snapshotIds: string[], tags: { add?: string[]; remove?: string[]; set?: string[] }, ) => { - const repository = await findRepository(id); + const repository = await findRepository(id, organizationId); if (!repository) { throw new NotFoundError("Repository not found"); @@ -469,8 +477,8 @@ const tagSnapshots = async ( } }; -const updateRepository = async (id: string, updates: { name?: string; compressionMode?: CompressionMode }) => { - const existing = await findRepository(id); +const updateRepository = async (id: string, organizationId: string, updates: { name?: string; compressionMode?: CompressionMode }) => { + const existing = await findRepository(id, organizationId); if (!existing) { throw new NotFoundError("Repository not found"); diff --git a/app/server/modules/volumes/volume.controller.ts b/app/server/modules/volumes/volume.controller.ts index 3763966a..4da378b5 100644 --- a/app/server/modules/volumes/volume.controller.ts +++ b/app/server/modules/volumes/volume.controller.ts @@ -52,13 +52,13 @@ export const volumeController = new Hono() }) .delete("/:name", deleteVolumeDto, async (c) => { const { name } = c.req.param(); - await volumeService.deleteVolume(name); + await volumeService.deleteVolume(name, c.get("organizationId")); return c.json({ message: "Volume deleted" }, 200); }) .get("/:name", getVolumeDto, async (c) => { const { name } = c.req.param(); - const res = await volumeService.getVolume(name); + const res = await volumeService.getVolume(name, c.get("organizationId")); const response = { volume: { @@ -77,7 +77,7 @@ export const volumeController = new Hono() .put("/:name", updateVolumeDto, validator("json", updateVolumeBody), async (c) => { const { name } = c.req.param(); const body = c.req.valid("json"); - const res = await volumeService.updateVolume(name, body); + const res = await volumeService.updateVolume(name, body, c.get("organizationId")); const response = { ...res.volume, @@ -88,26 +88,26 @@ export const volumeController = new Hono() }) .post("/:name/mount", mountVolumeDto, async (c) => { const { name } = c.req.param(); - const { error, status } = await volumeService.mountVolume(name); + const { error, status } = await volumeService.mountVolume(name, c.get("organizationId")); return c.json({ error, status }, error ? 500 : 200); }) .post("/:name/unmount", unmountVolumeDto, async (c) => { const { name } = c.req.param(); - const { error, status } = await volumeService.unmountVolume(name); + const { error, status } = await volumeService.unmountVolume(name, c.get("organizationId")); return c.json({ error, status }, error ? 500 : 200); }) .post("/:name/health-check", healthCheckDto, async (c) => { const { name } = c.req.param(); - const { error, status } = await volumeService.checkHealth(name); + const { error, status } = await volumeService.checkHealth(name, c.get("organizationId")); return c.json({ error, status }, 200); }) .get("/:name/files", listFilesDto, async (c) => { const { name } = c.req.param(); const subPath = c.req.query("path"); - const result = await volumeService.listFiles(name, subPath); + const result = await volumeService.listFiles(name, c.get("organizationId"), subPath); const response = { files: result.files, diff --git a/app/server/modules/volumes/volume.service.ts b/app/server/modules/volumes/volume.service.ts index 45f0975f..5eb5415b 100644 --- a/app/server/modules/volumes/volume.service.ts +++ b/app/server/modules/volumes/volume.service.ts @@ -54,7 +54,7 @@ const createVolume = async (name: string, backendConfig: BackendConfig, organiza const slug = slugify(name, { lower: true, strict: true }); const existing = await db.query.volumesTable.findFirst({ - where: eq(volumesTable.name, slug), + where: and(eq(volumesTable.name, slug), eq(volumesTable.organizationId, organizationId)), }); if (existing) { @@ -90,9 +90,9 @@ const createVolume = async (name: string, backendConfig: BackendConfig, organiza return { volume: created, status: 201 }; }; -const deleteVolume = async (name: string) => { +const deleteVolume = async (name: string, organizationId: string) => { const volume = await db.query.volumesTable.findFirst({ - where: eq(volumesTable.name, name), + where: and(eq(volumesTable.name, name), eq(volumesTable.organizationId, organizationId)), }); if (!volume) { @@ -104,9 +104,9 @@ const deleteVolume = async (name: string) => { await db.delete(volumesTable).where(eq(volumesTable.name, name)); }; -const mountVolume = async (name: string) => { +const mountVolume = async (name: string, organizationId: string) => { const volume = await db.query.volumesTable.findFirst({ - where: eq(volumesTable.name, name), + where: and(eq(volumesTable.name, name), eq(volumesTable.organizationId, organizationId)), }); if (!volume) { @@ -128,9 +128,9 @@ const mountVolume = async (name: string) => { return { error, status }; }; -const unmountVolume = async (name: string) => { +const unmountVolume = async (name: string, organizationId: string) => { const volume = await db.query.volumesTable.findFirst({ - where: eq(volumesTable.name, name), + where: and(eq(volumesTable.name, name), eq(volumesTable.organizationId, organizationId)), }); if (!volume) { @@ -149,9 +149,9 @@ const unmountVolume = async (name: string) => { return { error, status }; }; -const getVolume = async (name: string) => { +const getVolume = async (name: string, organizationId: string) => { const volume = await db.query.volumesTable.findFirst({ - where: eq(volumesTable.name, name), + where: and(eq(volumesTable.name, name), eq(volumesTable.organizationId, organizationId)), }); if (!volume) { @@ -169,9 +169,9 @@ const getVolume = async (name: string) => { return { volume, statfs }; }; -const updateVolume = async (name: string, volumeData: UpdateVolumeBody) => { +const updateVolume = async (name: string, volumeData: UpdateVolumeBody, organizationId: string) => { const existing = await db.query.volumesTable.findFirst({ - where: eq(volumesTable.name, name), + where: and(eq(volumesTable.name, name), eq(volumesTable.organizationId, organizationId)), }); if (!existing) { @@ -183,7 +183,7 @@ const updateVolume = async (name: string, volumeData: UpdateVolumeBody) => { const newSlug = slugify(volumeData.name, { lower: true, strict: true }); const conflict = await db.query.volumesTable.findFirst({ - where: and(eq(volumesTable.name, newSlug), ne(volumesTable.id, existing.id)), + where: and(eq(volumesTable.name, newSlug), ne(volumesTable.id, existing.id), eq(volumesTable.organizationId, organizationId)), }); if (conflict) { @@ -272,9 +272,9 @@ const testConnection = async (backendConfig: BackendConfig) => { }; }; -const checkHealth = async (name: string) => { +const checkHealth = async (name: string, organizationId: string) => { const volume = await db.query.volumesTable.findFirst({ - where: eq(volumesTable.name, name), + where: and(eq(volumesTable.name, name), eq(volumesTable.organizationId, organizationId)), }); if (!volume) { @@ -296,9 +296,9 @@ const checkHealth = async (name: string) => { return { status, error }; }; -const listFiles = async (name: string, subPath?: string) => { +const listFiles = async (name: string, organizationId: string, subPath?: string) => { const volume = await db.query.volumesTable.findFirst({ - where: eq(volumesTable.name, name), + where: and(eq(volumesTable.name, name), eq(volumesTable.organizationId, organizationId)), }); if (!volume) {