diff --git a/app/client/modules/auth/routes/onboarding.tsx b/app/client/modules/auth/routes/onboarding.tsx index 2ff3b754..f3d9ad28 100644 --- a/app/client/modules/auth/routes/onboarding.tsx +++ b/app/client/modules/auth/routes/onboarding.tsx @@ -87,13 +87,7 @@ export default function OnboardingPage() { } else if (error) { console.error(error); const errorMessage = error.message ?? "Unknown error"; - if (errorMessage.includes("User registrations are currently disabled")) { - toast.error("User registrations are currently disabled", { - description: "Please contact an administrator for access.", - }); - } else { - toast.error("Failed to create admin user", { description: errorMessage }); - } + toast.error("Failed to create admin user", { description: errorMessage }); } }; diff --git a/app/server/core/request-context.ts b/app/server/core/request-context.ts new file mode 100644 index 00000000..112d4a31 --- /dev/null +++ b/app/server/core/request-context.ts @@ -0,0 +1,24 @@ +import { AsyncLocalStorage } from "node:async_hooks"; + +export type RequestContext = { + organizationId: string; + userId?: string; +}; + +const requestContextStorage = new AsyncLocalStorage(); + +export const withContext = (context: RequestContext, fn: () => T): T => { + return requestContextStorage.run(context, fn); +}; + +export const getRequestContext = (): RequestContext => { + const context = requestContextStorage.getStore(); + + if (!context?.organizationId) { + throw new Error("Organization context is missing"); + } + + return context; +}; + +export const getOrganizationId = () => getRequestContext().organizationId; diff --git a/app/server/jobs/auto-remount.ts b/app/server/jobs/auto-remount.ts index 2fde5589..af528d03 100644 --- a/app/server/jobs/auto-remount.ts +++ b/app/server/jobs/auto-remount.ts @@ -4,6 +4,7 @@ import { logger } from "../utils/logger"; import { db } from "../db/db"; import { eq } from "drizzle-orm"; import { volumesTable } from "../db/schema"; +import { withContext } from "../core/request-context"; export class VolumeAutoRemountJob extends Job { async run() { @@ -16,7 +17,9 @@ export class VolumeAutoRemountJob extends Job { for (const volume of volumes) { if (volume.autoRemount) { try { - await volumeService.mountVolume(volume.id, volume.organizationId); + await withContext({ organizationId: volume.organizationId }, async () => { + await volumeService.mountVolume(volume.id); + }); } 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 5ad48b7b..41a37594 100644 --- a/app/server/jobs/backup-execution.ts +++ b/app/server/jobs/backup-execution.ts @@ -2,6 +2,7 @@ import { Job } from "../core/scheduler"; import { backupsService } from "../modules/backups/backups.service"; import { logger } from "../utils/logger"; import { db } from "../db/db"; +import { withContext } from "../core/request-context"; export class BackupExecutionJob extends Job { async run() { @@ -12,21 +13,23 @@ export class BackupExecutionJob extends Job { let totalExecuted = 0; for (const org of organizations) { - const scheduleIds = await backupsService.getSchedulesToExecute(org.id); + await withContext({ organizationId: org.id }, async () => { + const scheduleIds = await backupsService.getSchedulesToExecute(); - if (scheduleIds.length === 0) { - continue; - } + if (scheduleIds.length === 0) { + return; + } - logger.info(`Found ${scheduleIds.length} backup schedule(s) to execute for organization ${org.name}`); + 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); - }); - } + for (const scheduleId of scheduleIds) { + backupsService.executeBackup(scheduleId).catch((err) => { + logger.error(`Error executing backup for schedule ${scheduleId}:`, err); + }); + } - totalExecuted += scheduleIds.length; + totalExecuted += scheduleIds.length; + }); } if (totalExecuted === 0) { diff --git a/app/server/jobs/cleanup-dangling.ts b/app/server/jobs/cleanup-dangling.ts index 84b312b4..ffa9d292 100644 --- a/app/server/jobs/cleanup-dangling.ts +++ b/app/server/jobs/cleanup-dangling.ts @@ -9,6 +9,7 @@ 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"; +import { withContext } from "../core/request-context"; export class CleanupDanglingMountsJob extends Job { async run() { @@ -20,7 +21,7 @@ export class CleanupDanglingMountsJob extends Job { const allVolumes = []; for (const org of organizations) { - const volumes = await volumeService.listVolumes(org.id); + const volumes = await withContext({ organizationId: org.id }, async () => volumeService.listVolumes()); allVolumes.push(...volumes); } diff --git a/app/server/jobs/healthchecks.ts b/app/server/jobs/healthchecks.ts index 62c0666f..c872ebae 100644 --- a/app/server/jobs/healthchecks.ts +++ b/app/server/jobs/healthchecks.ts @@ -4,6 +4,7 @@ import { logger } from "../utils/logger"; import { db } from "../db/db"; import { eq, or } from "drizzle-orm"; import { volumesTable } from "../db/schema"; +import { withContext } from "../core/request-context"; export class VolumeHealthCheckJob extends Job { async run() { @@ -14,10 +15,12 @@ export class VolumeHealthCheckJob extends Job { }); for (const volume of volumes) { - const { status } = await volumeService.checkHealth(volume.id, volume.organizationId); - if (status === "error" && volume.autoRemount) { - await volumeService.mountVolume(volume.id, volume.organizationId); - } + await withContext({ organizationId: volume.organizationId }, async () => { + const { status } = await volumeService.checkHealth(volume.id); + if (status === "error" && volume.autoRemount) { + await volumeService.mountVolume(volume.id); + } + }); } return { done: true, timestamp: new Date() }; diff --git a/app/server/jobs/repository-healthchecks.ts b/app/server/jobs/repository-healthchecks.ts index db130320..94b054c4 100644 --- a/app/server/jobs/repository-healthchecks.ts +++ b/app/server/jobs/repository-healthchecks.ts @@ -4,6 +4,7 @@ import { logger } from "../utils/logger"; import { db } from "../db/db"; import { eq, or } from "drizzle-orm"; import { repositoriesTable } from "../db/schema"; +import { withContext } from "../core/request-context"; export class RepositoryHealthCheckJob extends Job { async run() { @@ -15,7 +16,9 @@ export class RepositoryHealthCheckJob extends Job { for (const repository of repositories) { try { - await repositoriesService.checkHealth(repository.id, repository.organizationId); + await withContext({ organizationId: repository.organizationId }, async () => { + await repositoriesService.checkHealth(repository.id); + }); } catch (error) { logger.error(`Health check failed for repository ${repository.name}:`, error); } diff --git a/app/server/modules/auth/auth.middleware.ts b/app/server/modules/auth/auth.middleware.ts index b211c8b8..2067d1c0 100644 --- a/app/server/modules/auth/auth.middleware.ts +++ b/app/server/modules/auth/auth.middleware.ts @@ -3,6 +3,7 @@ import { auth } from "~/lib/auth"; import { db } from "~/server/db/db"; import { member } from "~/server/db/schema"; import { eq, and } from "drizzle-orm"; +import { withContext } from "~/server/core/request-context"; declare module "hono" { interface ContextVariableMap { @@ -35,7 +36,9 @@ export const requireAuth = createMiddleware(async (c, next) => { c.set("user", user); c.set("organizationId", activeOrganizationId); - await next(); + await withContext({ organizationId: activeOrganizationId, userId: user.id }, async () => { + await next(); + }); }); /** diff --git a/app/server/modules/backups/__tests__/backups.patterns.test.ts b/app/server/modules/backups/__tests__/backups.patterns.test.ts index 2a39d54e..811daae2 100644 --- a/app/server/modules/backups/__tests__/backups.patterns.test.ts +++ b/app/server/modules/backups/__tests__/backups.patterns.test.ts @@ -8,6 +8,7 @@ import { getVolumePath } from "../../volumes/helpers"; import { restic } from "~/server/utils/restic"; import path from "node:path"; import { TEST_ORG_ID } from "~/test/helpers/organization"; +import * as context from "~/server/core/request-context"; const backupMock = mock(() => Promise.resolve({ exitCode: 0, result: JSON.parse(generateBackupOutput()) })); @@ -15,6 +16,7 @@ beforeEach(() => { backupMock.mockClear(); spyOn(restic, "backup").mockImplementation(backupMock); spyOn(restic, "forget").mockImplementation(mock(() => Promise.resolve({ success: true }))); + spyOn(context, "getOrganizationId").mockReturnValue(TEST_ORG_ID); }); afterEach(() => { @@ -37,7 +39,7 @@ describe("executeBackup - include / exclude patterns", () => { }); // act - await backupsService.executeBackup(schedule.id, TEST_ORG_ID); + await backupsService.executeBackup(schedule.id); // assert expect(backupMock).toHaveBeenCalledWith( @@ -68,7 +70,7 @@ describe("executeBackup - include / exclude patterns", () => { }); // act - await backupsService.executeBackup(schedule.id, TEST_ORG_ID); + await backupsService.executeBackup(schedule.id); // assert expect(backupMock).toHaveBeenCalledWith( @@ -98,7 +100,7 @@ describe("executeBackup - include / exclude patterns", () => { }); // act - await backupsService.executeBackup(schedule.id, TEST_ORG_ID); + await backupsService.executeBackup(schedule.id); // assert expect(backupMock).toHaveBeenCalledWith( @@ -122,7 +124,7 @@ describe("executeBackup - include / exclude patterns", () => { }); // act - await backupsService.executeBackup(schedule.id, TEST_ORG_ID); + await backupsService.executeBackup(schedule.id); // assert expect(backupMock).toHaveBeenCalledWith( diff --git a/app/server/modules/backups/__tests__/backups.service.test.ts b/app/server/modules/backups/__tests__/backups.service.test.ts index e41b7e47..5f200b2a 100644 --- a/app/server/modules/backups/__tests__/backups.service.test.ts +++ b/app/server/modules/backups/__tests__/backups.service.test.ts @@ -7,12 +7,14 @@ import { generateBackupOutput } from "~/test/helpers/restic"; import { faker } from "@faker-js/faker"; import * as spawnModule from "~/server/utils/spawn"; import { TEST_ORG_ID } from "~/test/helpers/organization"; +import * as context from "~/server/core/request-context"; const resticBackupMock = mock(() => Promise.resolve({ exitCode: 0, summary: "", error: "" })); beforeEach(() => { resticBackupMock.mockClear(); spyOn(spawnModule, "safeSpawn").mockImplementation(resticBackupMock); + spyOn(context, "getOrganizationId").mockReturnValue(TEST_ORG_ID); }); afterEach(() => { @@ -36,10 +38,10 @@ describe("execute backup", () => { ); // act - await backupsService.executeBackup(schedule.id, TEST_ORG_ID); + await backupsService.executeBackup(schedule.id); // assert - const updatedSchedule = await backupsService.getSchedule(schedule.id, TEST_ORG_ID); + const updatedSchedule = await backupsService.getSchedule(schedule.id); expect(updatedSchedule.nextBackupAt).not.toBeNull(); const nextBackupAt = new Date(updatedSchedule.nextBackupAt ?? 0); @@ -60,7 +62,7 @@ describe("execute backup", () => { }); // act - await backupsService.executeBackup(schedule.id, TEST_ORG_ID); + await backupsService.executeBackup(schedule.id); // assert expect(resticBackupMock).not.toHaveBeenCalled(); @@ -81,7 +83,7 @@ describe("execute backup", () => { ); // act - await backupsService.executeBackup(schedule.id, TEST_ORG_ID, true); + await backupsService.executeBackup(schedule.id, true); // assert expect(resticBackupMock).toHaveBeenCalled(); @@ -102,9 +104,9 @@ describe("execute backup", () => { }); // act - void backupsService.executeBackup(schedule.id, TEST_ORG_ID); + void backupsService.executeBackup(schedule.id); await new Promise((resolve) => setTimeout(resolve, 10)); - await backupsService.executeBackup(schedule.id, TEST_ORG_ID); + await backupsService.executeBackup(schedule.id); // assert expect(resticBackupMock).toHaveBeenCalledTimes(1); @@ -124,10 +126,10 @@ describe("execute backup", () => { ); // act - await backupsService.executeBackup(schedule.id, TEST_ORG_ID); + await backupsService.executeBackup(schedule.id); // assert - const updatedSchedule = await backupsService.getSchedule(schedule.id, TEST_ORG_ID); + const updatedSchedule = await backupsService.getSchedule(schedule.id); expect(updatedSchedule.lastBackupStatus).toBe("warning"); }); @@ -145,10 +147,10 @@ describe("execute backup", () => { ); // act - await backupsService.executeBackup(schedule.id, TEST_ORG_ID); + await backupsService.executeBackup(schedule.id); // assert - const updatedSchedule = await backupsService.getSchedule(schedule.id, TEST_ORG_ID); + const updatedSchedule = await backupsService.getSchedule(schedule.id); expect(updatedSchedule.lastBackupStatus).toBe("error"); }); }); @@ -169,7 +171,7 @@ describe("getSchedulesToExecute", () => { }); // act - const schedulesToExecute = await backupsService.getSchedulesToExecute(TEST_ORG_ID); + const schedulesToExecute = await backupsService.getSchedulesToExecute(); // assert expect(schedulesToExecute).toContain(schedule.id); diff --git a/app/server/modules/backups/backups.controller.ts b/app/server/modules/backups/backups.controller.ts index d6418bc7..e7a3a97a 100644 --- a/app/server/modules/backups/backups.controller.ts +++ b/app/server/modules/backups/backups.controller.ts @@ -46,49 +46,44 @@ import { requireAuth } from "../auth/auth.middleware"; export const backupScheduleController = new Hono() .use(requireAuth) .get("/", listBackupSchedulesDto, async (c) => { - const schedules = await backupsService.listSchedules(c.get("organizationId")); + const schedules = await backupsService.listSchedules(); return c.json(schedules, 200); }) .get("/:scheduleId", getBackupScheduleDto, async (c) => { const scheduleId = c.req.param("scheduleId"); - - const schedule = await backupsService.getSchedule(Number(scheduleId), c.get("organizationId")); + const schedule = await backupsService.getSchedule(Number(scheduleId)); return c.json(schedule, 200); }) .get("/volume/:volumeId", getBackupScheduleForVolumeDto, async (c) => { const volumeId = c.req.param("volumeId"); - const schedule = await backupsService.getScheduleForVolume(Number(volumeId), c.get("organizationId")); + const schedule = await backupsService.getScheduleForVolume(Number(volumeId)); return c.json(schedule, 200); }) .post("/", createBackupScheduleDto, validator("json", createBackupScheduleBody), async (c) => { const body = c.req.valid("json"); - - const schedule = await backupsService.createSchedule(body, c.get("organizationId")); + const schedule = await backupsService.createSchedule(body); return c.json(schedule, 201); }) .patch("/:scheduleId", updateBackupScheduleDto, validator("json", updateBackupScheduleBody), async (c) => { const scheduleId = c.req.param("scheduleId"); const body = c.req.valid("json"); - - const schedule = await backupsService.updateSchedule(Number(scheduleId), body, c.get("organizationId")); + const schedule = await backupsService.updateSchedule(Number(scheduleId), body); return c.json(schedule, 200); }) .delete("/:scheduleId", deleteBackupScheduleDto, async (c) => { const scheduleId = c.req.param("scheduleId"); - - await backupsService.deleteSchedule(Number(scheduleId), c.get("organizationId")); + await backupsService.deleteSchedule(Number(scheduleId)); return c.json({ success: true }, 200); }) .post("/:scheduleId/run", runBackupNowDto, async (c) => { const scheduleId = c.req.param("scheduleId"); - - backupsService.executeBackup(Number(scheduleId), c.get("organizationId"), true).catch((err) => { + backupsService.executeBackup(Number(scheduleId), true).catch((err) => { console.error(`Error executing manual backup for schedule ${scheduleId}:`, err); }); @@ -96,15 +91,13 @@ export const backupScheduleController = new Hono() }) .post("/:scheduleId/stop", stopBackupDto, async (c) => { const scheduleId = c.req.param("scheduleId"); - - await backupsService.stopBackup(Number(scheduleId), c.get("organizationId")); + await backupsService.stopBackup(Number(scheduleId)); return c.json({ success: true }, 200); }) .post("/:scheduleId/forget", runForgetDto, async (c) => { const scheduleId = c.req.param("scheduleId"); - - await backupsService.runForget(Number(scheduleId), c.get("organizationId")); + await backupsService.runForget(Number(scheduleId)); return c.json({ success: true }, 200); }) @@ -128,27 +121,26 @@ 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, c.get("organizationId")); + const mirrors = await backupsService.getMirrors(scheduleId); 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, c.get("organizationId"), body); + const mirrors = await backupsService.updateMirrors(scheduleId, 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, c.get("organizationId")); + const compatibility = await backupsService.getMirrorCompatibility(scheduleId); return c.json(compatibility, 200); }) .post("/reorder", reorderBackupSchedulesDto, validator("json", reorderBackupSchedulesBody), async (c) => { const body = c.req.valid("json"); - - await backupsService.reorderSchedules(body.scheduleIds, c.get("organizationId")); + await backupsService.reorderSchedules(body.scheduleIds); 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 75e26eb2..d105f729 100644 --- a/app/server/modules/backups/backups.service.ts +++ b/app/server/modules/backups/backups.service.ts @@ -16,6 +16,7 @@ import { repoMutex } from "../../core/repository-mutex"; import { checkMirrorCompatibility, getIncompatibleMirrorError } from "~/server/utils/backend-compatibility"; import path from "node:path"; import { generateShortId } from "~/server/utils/id"; +import { getOrganizationId } from "~/server/core/request-context"; const runningBackups = new Map(); @@ -56,7 +57,8 @@ const processPattern = (pattern: string, volumePath: string): string => { return pattern; }; -const listSchedules = async (organizationId: string) => { +const listSchedules = async () => { + const organizationId = getOrganizationId(); const schedules = await db.query.backupSchedulesTable.findMany({ where: eq(backupSchedulesTable.organizationId, organizationId), with: { @@ -68,7 +70,8 @@ const listSchedules = async (organizationId: string) => { return schedules; }; -const getSchedule = async (scheduleId: number, organizationId: string) => { +const getSchedule = async (scheduleId: number) => { + const organizationId = getOrganizationId(); const schedule = await db.query.backupSchedulesTable.findFirst({ where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)), with: { @@ -84,7 +87,8 @@ const getSchedule = async (scheduleId: number, organizationId: string) => { return schedule; }; -const createSchedule = async (data: CreateBackupScheduleBody, organizationId: string) => { +const createSchedule = async (data: CreateBackupScheduleBody) => { + const organizationId = getOrganizationId(); if (!cron.validate(data.cronExpression)) { throw new BadRequestError("Invalid cron expression"); } @@ -141,7 +145,8 @@ const createSchedule = async (data: CreateBackupScheduleBody, organizationId: st return newSchedule; }; -const updateSchedule = async (scheduleId: number, data: UpdateBackupScheduleBody, organizationId: string) => { +const updateSchedule = async (scheduleId: number, data: UpdateBackupScheduleBody) => { + const organizationId = getOrganizationId(); const schedule = await db.query.backupSchedulesTable.findFirst({ where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)), }); @@ -188,7 +193,8 @@ const updateSchedule = async (scheduleId: number, data: UpdateBackupScheduleBody return updated; }; -const deleteSchedule = async (scheduleId: number, organizationId: string) => { +const deleteSchedule = async (scheduleId: number) => { + const organizationId = getOrganizationId(); const schedule = await db.query.backupSchedulesTable.findFirst({ where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)), }); @@ -202,7 +208,8 @@ const deleteSchedule = async (scheduleId: number, organizationId: string) => { .where(and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId))); }; -const executeBackup = async (scheduleId: number, organizationId: string, manual = false) => { +const executeBackup = async (scheduleId: number, manual = false) => { + const organizationId = getOrganizationId(); const schedule = await db.query.backupSchedulesTable.findFirst({ where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)), }); @@ -324,12 +331,12 @@ const executeBackup = async (scheduleId: number, organizationId: string, manual } if (schedule.retentionPolicy) { - void runForget(schedule.id, organizationId).catch((error) => { + void runForget(schedule.id).catch((error) => { logger.error(`Failed to run retention policy for schedule ${scheduleId}: ${toMessage(error)}`); }); } - void copyToMirrors(scheduleId, organizationId, repository, schedule.retentionPolicy).catch((error) => { + void copyToMirrors(scheduleId, repository, schedule.retentionPolicy).catch((error) => { logger.error(`Background mirror copy failed for schedule ${scheduleId}: ${toMessage(error)}`); }); @@ -412,13 +419,14 @@ const executeBackup = async (scheduleId: number, organizationId: string, manual } }; -const getSchedulesToExecute = async (organizationId: string) => { +const getSchedulesToExecute = async () => { + const organizationId = getOrganizationId(); 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) + eq(backupSchedulesTable.organizationId, organizationId), ), }); @@ -433,7 +441,8 @@ const getSchedulesToExecute = async (organizationId: string) => { return schedulesToRun; }; -const getScheduleForVolume = async (volumeId: number, organizationId: string) => { +const getScheduleForVolume = async (volumeId: number) => { + const organizationId = getOrganizationId(); const schedule = await db.query.backupSchedulesTable.findFirst({ where: and(eq(backupSchedulesTable.volumeId, volumeId), eq(backupSchedulesTable.organizationId, organizationId)), with: { volume: true, repository: true }, @@ -442,7 +451,8 @@ const getScheduleForVolume = async (volumeId: number, organizationId: string) => return schedule ?? null; }; -const stopBackup = async (scheduleId: number, organizationId: string) => { +const stopBackup = async (scheduleId: number) => { + const organizationId = getOrganizationId(); const schedule = await db.query.backupSchedulesTable.findFirst({ where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)), }); @@ -472,7 +482,8 @@ const stopBackup = async (scheduleId: number, organizationId: string) => { } }; -const runForget = async (scheduleId: number, organizationId: string, repositoryId?: string) => { +const runForget = async (scheduleId: number, repositoryId?: string) => { + const organizationId = getOrganizationId(); const schedule = await db.query.backupSchedulesTable.findFirst({ where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)), }); @@ -505,7 +516,8 @@ const runForget = async (scheduleId: number, organizationId: string, repositoryI logger.info(`Retention policy applied successfully for schedule ${scheduleId}`); }; -const getMirrors = async (scheduleId: number, organizationId: string) => { +const getMirrors = async (scheduleId: number) => { + const organizationId = getOrganizationId(); const schedule = await db.query.backupSchedulesTable.findFirst({ where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)), }); @@ -522,7 +534,8 @@ const getMirrors = async (scheduleId: number, organizationId: string) => { return mirrors; }; -const updateMirrors = async (scheduleId: number, organizationId: string, data: UpdateScheduleMirrorsBody) => { +const updateMirrors = async (scheduleId: number, data: UpdateScheduleMirrorsBody) => { + const organizationId = getOrganizationId(); const schedule = await db.query.backupSchedulesTable.findFirst({ where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)), with: { repository: true }, @@ -583,15 +596,15 @@ const updateMirrors = async (scheduleId: number, organizationId: string, data: U ); } - return getMirrors(scheduleId, organizationId); + return getMirrors(scheduleId); }; const copyToMirrors = async ( scheduleId: number, - organizationId: string, sourceRepository: { id: string; config: (typeof repositoriesTable.$inferSelect)["config"] }, retentionPolicy: (typeof backupSchedulesTable.$inferSelect)["retentionPolicy"], ) => { + const organizationId = getOrganizationId(); const schedule = await db.query.backupSchedulesTable.findFirst({ where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)), }); @@ -637,7 +650,7 @@ const copyToMirrors = async ( } if (retentionPolicy) { - void runForget(scheduleId, organizationId, mirror.repository.id).catch((error) => { + void runForget(scheduleId, mirror.repository.id).catch((error) => { logger.error( `Failed to run retention policy for mirror repository ${mirror.repository.name}: ${toMessage(error)}`, ); @@ -677,7 +690,8 @@ const copyToMirrors = async ( } }; -const getMirrorCompatibility = async (scheduleId: number, organizationId: string) => { +const getMirrorCompatibility = async (scheduleId: number) => { + const organizationId = getOrganizationId(); const schedule = await db.query.backupSchedulesTable.findFirst({ where: and(eq(backupSchedulesTable.id, scheduleId), eq(backupSchedulesTable.organizationId, organizationId)), with: { repository: true }, @@ -699,7 +713,8 @@ const getMirrorCompatibility = async (scheduleId: number, organizationId: string return compatibility; }; -const reorderSchedules = async (scheduleIds: number[], organizationId: string) => { +const reorderSchedules = async (scheduleIds: number[]) => { + const organizationId = getOrganizationId(); const uniqueIds = new Set(scheduleIds); if (uniqueIds.size !== scheduleIds.length) { throw new BadRequestError("Duplicate schedule IDs in reorder request"); diff --git a/app/server/modules/lifecycle/startup.ts b/app/server/modules/lifecycle/startup.ts index 95750e56..ae143cc9 100644 --- a/app/server/modules/lifecycle/startup.ts +++ b/app/server/modules/lifecycle/startup.ts @@ -15,32 +15,37 @@ import { VolumeAutoRemountJob } from "~/server/jobs/auto-remount"; import { cache } from "~/server/utils/cache"; import { initAuth } from "~/lib/auth"; import { toMessage } from "~/server/utils/errors"; +import { withContext } from "~/server/core/request-context"; const ensureLatestConfigurationSchema = async () => { const volumes = await db.query.volumesTable.findMany({}); for (const volume of volumes) { - await volumeService.updateVolume(volume.id, volume, volume.organizationId).catch((err) => { - logger.error(`Failed to update volume ${volume.name}: ${err}`); + await withContext({ organizationId: volume.organizationId }, async () => { + await volumeService.updateVolume(volume.id, volume).catch((err) => { + logger.error(`Failed to update volume ${volume.name}: ${err}`); + }); }); } const repositories = await db.query.repositoriesTable.findMany({}); for (const repo of repositories) { - await repositoriesService.updateRepository(repo.id, repo.organizationId, {}).catch((err) => { - logger.error(`Failed to update repository ${repo.name}: ${err}`); + await withContext({ organizationId: repo.organizationId }, async () => { + await repositoriesService.updateRepository(repo.id, {}).catch((err) => { + logger.error(`Failed to update repository ${repo.name}: ${err}`); + }); }); } const notifications = await db.query.notificationDestinationsTable.findMany({}); for (const notification of notifications) { - await notificationsService - .updateDestination(notification.id, notification.organizationId, notification) - .catch((err) => { + await withContext({ organizationId: notification.organizationId }, async () => { + await notificationsService.updateDestination(notification.id, notification).catch((err) => { logger.error(`Failed to update notification destination ${notification.id}: ${err}`); }); + }); } }; @@ -69,8 +74,10 @@ export const startup = async () => { }); for (const volume of volumes) { - await volumeService.mountVolume(volume.id, volume.organizationId).catch((err) => { - logger.error(`Error auto-remounting volume ${volume.name} on startup: ${err.message}`); + await withContext({ organizationId: volume.organizationId }, async () => { + await volumeService.mountVolume(volume.id).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 b1d23f83..42a4b0e7 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(c.get("organizationId")); + const destinations = await notificationsService.listDestinations(); 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, c.get("organizationId")); + const destination = await notificationsService.createDestination(body.name, body.config); 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, c.get("organizationId")); + const destination = await notificationsService.getDestination(id); 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, c.get("organizationId"), body); + const destination = await notificationsService.updateDestination(id, 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, c.get("organizationId")); + await notificationsService.deleteDestination(id); 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, c.get("organizationId")); + const result = await notificationsService.testDestination(id); return c.json(result, 200); }); diff --git a/app/server/modules/notifications/notifications.service.ts b/app/server/modules/notifications/notifications.service.ts index cb1201af..28188a6b 100644 --- a/app/server/modules/notifications/notifications.service.ts +++ b/app/server/modules/notifications/notifications.service.ts @@ -14,8 +14,10 @@ import { buildShoutrrrUrl } from "./builders"; import { notificationConfigSchema, type NotificationConfig, type NotificationEvent } from "~/schemas/notifications"; import { toMessage } from "../../utils/errors"; import { type } from "arktype"; +import { getOrganizationId } from "~/server/core/request-context"; -const listDestinations = async (organizationId: string) => { +const listDestinations = async () => { + const organizationId = getOrganizationId(); const destinations = await db.query.notificationDestinationsTable.findMany({ where: eq(notificationDestinationsTable.organizationId, organizationId), orderBy: (destinations, { asc }) => [asc(destinations.name)], @@ -23,7 +25,8 @@ const listDestinations = async (organizationId: string) => { return destinations; }; -const getDestination = async (id: number, organizationId: string) => { +const getDestination = async (id: number) => { + const organizationId = getOrganizationId(); const destination = await db.query.notificationDestinationsTable.findFirst({ where: and(eq(notificationDestinationsTable.id, id), eq(notificationDestinationsTable.organizationId, organizationId)), }); @@ -133,7 +136,8 @@ async function decryptSensitiveFields(config: NotificationConfig): Promise { +const createDestination = async (name: string, config: NotificationConfig) => { + const organizationId = getOrganizationId(); const slug = slugify(name, { lower: true, strict: true }); const existing = await db.query.notificationDestinationsTable.findFirst({ @@ -165,10 +169,10 @@ const createDestination = async (name: string, config: NotificationConfig, organ const updateDestination = async ( id: number, - organizationId: string, updates: { name?: string; enabled?: boolean; config?: NotificationConfig }, ) => { - const existing = await getDestination(id, organizationId); + const organizationId = getOrganizationId(); + const existing = await getDestination(id); if (!existing) { throw new NotFoundError("Notification destination not found"); @@ -217,15 +221,16 @@ const updateDestination = async ( return updated; }; -const deleteDestination = async (id: number, organizationId: string) => { - await getDestination(id, organizationId); +const deleteDestination = async (id: number) => { + const organizationId = getOrganizationId(); + await getDestination(id); await db .delete(notificationDestinationsTable) .where(and(eq(notificationDestinationsTable.id, id), eq(notificationDestinationsTable.organizationId, organizationId))); }; -const testDestination = async (id: number, organizationId: string) => { - const destination = await getDestination(id, organizationId); +const testDestination = async (id: number) => { + const destination = await getDestination(id); 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 83c0f249..5548eae5 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(c.get("organizationId")); + const repositories = await repositoriesService.listRepositories(); 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, c.get("organizationId"), body.compressionMode); + const res = await repositoriesService.createRepository(body.name, body.config, body.compressionMode); return c.json({ message: "Repository created", repository: res.repository }, 201); }) @@ -69,21 +69,20 @@ export const repositoriesController = new Hono() }) .get("/:id", getRepositoryDto, async (c) => { const { id } = c.req.param(); - const res = await repositoriesService.getRepository(id, c.get("organizationId")); + const res = await repositoriesService.getRepository(id); return c.json(res.repository, 200); }) .delete("/:id", deleteRepositoryDto, async (c) => { const { id } = c.req.param(); - await repositoriesService.deleteRepository(id, c.get("organizationId")); + await repositoriesService.deleteRepository(id); return c.json({ message: "Repository deleted" }, 200); }) .get("/:id/snapshots", listSnapshotsDto, validator("query", listSnapshotsFilters), async (c) => { const { id } = c.req.param(); const { backupId } = c.req.valid("query"); - - const res = await repositoriesService.listSnapshots(id, c.get("organizationId"), backupId); + const res = await repositoriesService.listSnapshots(id, backupId); const snapshots = res.map((snapshot) => { const { summary } = snapshot; @@ -108,7 +107,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, c.get("organizationId"), snapshotId); + const snapshot = await repositoriesService.getSnapshotDetails(id, snapshotId); let duration = 0; if (snapshot.summary) { @@ -137,7 +136,7 @@ export const repositoriesController = new Hono() const { path } = c.req.valid("query"); const decodedPath = path ? decodeURIComponent(path) : undefined; - const result = await repositoriesService.listSnapshotFiles(id, c.get("organizationId"), snapshotId, decodedPath); + const result = await repositoriesService.listSnapshotFiles(id, snapshotId, decodedPath); c.header("Cache-Control", "max-age=300, stale-while-revalidate=600"); @@ -147,46 +146,40 @@ export const repositoriesController = new Hono() .post("/:id/restore", restoreSnapshotDto, validator("json", restoreSnapshotBody), async (c) => { const { id } = c.req.param(); const { snapshotId, ...options } = c.req.valid("json"); - - const result = await repositoriesService.restoreSnapshot(id, c.get("organizationId"), snapshotId, options); + const result = await repositoriesService.restoreSnapshot(id, snapshotId, options); return c.json(result, 200); }) .post("/:id/doctor", doctorRepositoryDto, async (c) => { const { id } = c.req.param(); - - const result = await repositoriesService.doctorRepository(id, c.get("organizationId")); + const result = await repositoriesService.doctorRepository(id); return c.json(result, 200); }) .delete("/:id/snapshots/:snapshotId", deleteSnapshotDto, async (c) => { const { id, snapshotId } = c.req.param(); - - await repositoriesService.deleteSnapshot(id, c.get("organizationId"), snapshotId); + await repositoriesService.deleteSnapshot(id, snapshotId); return c.json({ message: "Snapshot deleted" }, 200); }) .delete("/:id/snapshots", deleteSnapshotsDto, validator("json", deleteSnapshotsBody), async (c) => { const { id } = c.req.param(); const { snapshotIds } = c.req.valid("json"); - - await repositoriesService.deleteSnapshots(id, c.get("organizationId"), snapshotIds); + await repositoriesService.deleteSnapshots(id, snapshotIds); return c.json({ message: "Snapshots deleted" }, 200); }) .post("/:id/snapshots/tag", tagSnapshotsDto, validator("json", tagSnapshotsBody), async (c) => { const { id } = c.req.param(); const { snapshotIds, ...tags } = c.req.valid("json"); - - await repositoriesService.tagSnapshots(id, c.get("organizationId"), snapshotIds, tags); + await repositoriesService.tagSnapshots(id, snapshotIds, tags); return c.json({ message: "Snapshots tagged" }, 200); }) .patch("/:id", updateRepositoryDto, validator("json", updateRepositoryBody), async (c) => { const { id } = c.req.param(); const body = c.req.valid("json"); - - const res = await repositoriesService.updateRepository(id, c.get("organizationId"), body); + const res = await repositoriesService.updateRepository(id, 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 3e653791..c1a704e2 100644 --- a/app/server/modules/repositories/repositories.service.ts +++ b/app/server/modules/repositories/repositories.service.ts @@ -16,8 +16,10 @@ import { type OverwriteMode, type RepositoryConfig, } from "~/schemas/restic"; +import { getOrganizationId } from "~/server/core/request-context"; -const findRepository = async (idOrShortId: string, organizationId: string) => { +const findRepository = async (idOrShortId: string) => { + const organizationId = getOrganizationId(); return await db.query.repositoriesTable.findFirst({ where: and( or(eq(repositoriesTable.id, idOrShortId), eq(repositoriesTable.shortId, idOrShortId)), @@ -26,7 +28,8 @@ const findRepository = async (idOrShortId: string, organizationId: string) => { }); }; -const listRepositories = async (organizationId: string) => { +const listRepositories = async () => { + const organizationId = getOrganizationId(); const repositories = await db.query.repositoriesTable.findMany({ where: eq(repositoriesTable.organizationId, organizationId), }); @@ -72,12 +75,8 @@ const encryptConfig = async (config: RepositoryConfig): Promise { +const createRepository = async (name: string, config: RepositoryConfig, compressionMode?: CompressionMode) => { + const organizationId = getOrganizationId(); const id = crypto.randomUUID(); const shortId = generateShortId(); @@ -135,8 +134,8 @@ const createRepository = async ( throw new InternalServerError(`Failed to initialize repository: ${errorMessage}`); }; -const getRepository = async (id: string, organizationId: string) => { - const repository = await findRepository(id, organizationId); +const getRepository = async (id: string) => { + const repository = await findRepository(id); if (!repository) { throw new NotFoundError("Repository not found"); @@ -145,8 +144,8 @@ const getRepository = async (id: string, organizationId: string) => { return { repository }; }; -const deleteRepository = async (id: string, organizationId: string) => { - const repository = await findRepository(id, organizationId); +const deleteRepository = async (id: string) => { + const repository = await findRepository(id); if (!repository) { throw new NotFoundError("Repository not found"); @@ -170,8 +169,9 @@ const deleteRepository = async (id: string, organizationId: string) => { * * @returns List of snapshots */ -const listSnapshots = async (id: string, organizationId: string, backupId?: string) => { - const repository = await findRepository(id, organizationId); +const listSnapshots = async (id: string, backupId?: string) => { + const organizationId = getOrganizationId(); + const repository = await findRepository(id); if (!repository) { throw new NotFoundError("Repository not found"); @@ -201,8 +201,9 @@ const listSnapshots = async (id: string, organizationId: string, backupId?: stri } }; -const listSnapshotFiles = async (id: string, organizationId: string, snapshotId: string, path?: string) => { - const repository = await findRepository(id, organizationId); +const listSnapshotFiles = async (id: string, snapshotId: string, path?: string) => { + const organizationId = getOrganizationId(); + const repository = await findRepository(id); if (!repository) { throw new NotFoundError("Repository not found"); @@ -246,7 +247,6 @@ const listSnapshotFiles = async (id: string, organizationId: string, snapshotId: const restoreSnapshot = async ( id: string, - organizationId: string, snapshotId: string, options?: { include?: string[]; @@ -257,7 +257,8 @@ const restoreSnapshot = async ( overwrite?: OverwriteMode; }, ) => { - const repository = await findRepository(id, organizationId); + const organizationId = getOrganizationId(); + const repository = await findRepository(id); if (!repository) { throw new NotFoundError("Repository not found"); @@ -280,8 +281,9 @@ const restoreSnapshot = async ( } }; -const getSnapshotDetails = async (id: string, organizationId: string, snapshotId: string) => { - const repository = await findRepository(id, organizationId); +const getSnapshotDetails = async (id: string, snapshotId: string) => { + const organizationId = getOrganizationId(); + const repository = await findRepository(id); if (!repository) { throw new NotFoundError("Repository not found"); @@ -309,8 +311,9 @@ const getSnapshotDetails = async (id: string, organizationId: string, snapshotId return snapshot; }; -const checkHealth = async (repositoryId: string, organizationId: string) => { - const repository = await findRepository(repositoryId, organizationId); +const checkHealth = async (repositoryId: string) => { + const organizationId = getOrganizationId(); + const repository = await findRepository(repositoryId); if (!repository) { throw new NotFoundError("Repository not found"); @@ -335,8 +338,9 @@ const checkHealth = async (repositoryId: string, organizationId: string) => { } }; -const doctorRepository = async (id: string, organizationId: string) => { - const repository = await findRepository(id, organizationId); +const doctorRepository = async (id: string) => { + const organizationId = getOrganizationId(); + const repository = await findRepository(id); if (!repository) { throw new NotFoundError("Repository not found"); @@ -424,8 +428,9 @@ const doctorRepository = async (id: string, organizationId: string) => { }; }; -const deleteSnapshot = async (id: string, organizationId: string, snapshotId: string) => { - const repository = await findRepository(id, organizationId); +const deleteSnapshot = async (id: string, snapshotId: string) => { + const organizationId = getOrganizationId(); + const repository = await findRepository(id); if (!repository) { throw new NotFoundError("Repository not found"); @@ -441,8 +446,9 @@ const deleteSnapshot = async (id: string, organizationId: string, snapshotId: st } }; -const deleteSnapshots = async (id: string, organizationId: string, snapshotIds: string[]) => { - const repository = await findRepository(id, organizationId); +const deleteSnapshots = async (id: string, snapshotIds: string[]) => { + const organizationId = getOrganizationId(); + const repository = await findRepository(id); if (!repository) { throw new NotFoundError("Repository not found"); @@ -460,13 +466,9 @@ const deleteSnapshots = async (id: string, organizationId: string, snapshotIds: } }; -const tagSnapshots = async ( - id: string, - organizationId: string, - snapshotIds: string[], - tags: { add?: string[]; remove?: string[]; set?: string[] }, -) => { - const repository = await findRepository(id, organizationId); +const tagSnapshots = async (id: string, snapshotIds: string[], tags: { add?: string[]; remove?: string[]; set?: string[] }) => { + const organizationId = getOrganizationId(); + const repository = await findRepository(id); if (!repository) { throw new NotFoundError("Repository not found"); @@ -484,12 +486,8 @@ const tagSnapshots = async ( } }; -const updateRepository = async ( - id: string, - organizationId: string, - updates: { name?: string; compressionMode?: CompressionMode }, -) => { - const existing = await findRepository(id, organizationId); +const updateRepository = async (id: string, updates: { name?: string; compressionMode?: CompressionMode }) => { + const existing = await findRepository(id); if (!existing) { throw new NotFoundError("Repository not found"); diff --git a/app/server/modules/system/system.controller.ts b/app/server/modules/system/system.controller.ts index e6123bdc..4f9b2e1a 100644 --- a/app/server/modules/system/system.controller.ts +++ b/app/server/modules/system/system.controller.ts @@ -20,6 +20,7 @@ import { eq } from "drizzle-orm"; import { verifyUserPassword } from "../auth/helpers"; import { cryptoUtils } from "../../utils/crypto"; import { createMiddleware } from "hono/factory"; +import { getOrganizationId } from "~/server/core/request-context"; const requireGlobalAdmin = createMiddleware(async (c, next) => { const user = c.get("user"); @@ -68,7 +69,7 @@ export const systemController = new Hono() validator("json", downloadResticPasswordBodySchema), async (c) => { const user = c.get("user"); - const organizationId = c.get("organizationId"); + const organizationId = getOrganizationId(); const body = c.req.valid("json"); const isPasswordValid = await verifyUserPassword({ password: body.password, userId: user.id }); diff --git a/app/server/modules/volumes/volume.controller.ts b/app/server/modules/volumes/volume.controller.ts index f572b4c9..d8fbf10b 100644 --- a/app/server/modules/volumes/volume.controller.ts +++ b/app/server/modules/volumes/volume.controller.ts @@ -29,13 +29,13 @@ import { requireAuth } from "../auth/auth.middleware"; export const volumeController = new Hono() .use(requireAuth) .get("/", listVolumesDto, async (c) => { - const volumes = await volumeService.listVolumes(c.get("organizationId")); + const volumes = await volumeService.listVolumes(); return c.json(volumes, 200); }) .post("/", createVolumeDto, validator("json", createVolumeBody), async (c) => { const body = c.req.valid("json"); - const res = await volumeService.createVolume(body.name, body.config, c.get("organizationId")); + const res = await volumeService.createVolume(body.name, body.config); const response = { ...res.volume, @@ -52,13 +52,13 @@ export const volumeController = new Hono() }) .delete("/:id", deleteVolumeDto, async (c) => { const { id } = c.req.param(); - await volumeService.deleteVolume(id, c.get("organizationId")); + await volumeService.deleteVolume(id); return c.json({ message: "Volume deleted" }, 200); }) .get("/:id", getVolumeDto, async (c) => { const { id } = c.req.param(); - const res = await volumeService.getVolume(id, c.get("organizationId")); + const res = await volumeService.getVolume(id); const response = { volume: { @@ -77,7 +77,7 @@ export const volumeController = new Hono() .put("/:id", updateVolumeDto, validator("json", updateVolumeBody), async (c) => { const { id } = c.req.param(); const body = c.req.valid("json"); - const res = await volumeService.updateVolume(id, body, c.get("organizationId")); + const res = await volumeService.updateVolume(id, body); const response = { ...res.volume, @@ -88,26 +88,26 @@ export const volumeController = new Hono() }) .post("/:id/mount", mountVolumeDto, async (c) => { const { id } = c.req.param(); - const { error, status } = await volumeService.mountVolume(id, c.get("organizationId")); + const { error, status } = await volumeService.mountVolume(id); return c.json({ error, status }, error ? 500 : 200); }) .post("/:id/unmount", unmountVolumeDto, async (c) => { const { id } = c.req.param(); - const { error, status } = await volumeService.unmountVolume(id, c.get("organizationId")); + const { error, status } = await volumeService.unmountVolume(id); return c.json({ error, status }, error ? 500 : 200); }) .post("/:id/health-check", healthCheckDto, async (c) => { const { id } = c.req.param(); - const { error, status } = await volumeService.checkHealth(id, c.get("organizationId")); + const { error, status } = await volumeService.checkHealth(id); return c.json({ error, status }, 200); }) .get("/:id/files", listFilesDto, async (c) => { const { id } = c.req.param(); const subPath = c.req.query("path"); - const result = await volumeService.listFiles(id, c.get("organizationId"), subPath); + const result = await volumeService.listFiles(id, 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 6d0ec3d2..70c0f0d9 100644 --- a/app/server/modules/volumes/volume.service.ts +++ b/app/server/modules/volumes/volume.service.ts @@ -18,6 +18,7 @@ import { logger } from "../../utils/logger"; import { serverEvents } from "../../core/events"; import { volumeConfigSchema, type BackendConfig } from "~/schemas/volumes"; import { type } from "arktype"; +import { getOrganizationId } from "~/server/core/request-context"; async function encryptSensitiveFields(config: BackendConfig): Promise { switch (config.backend) { @@ -42,7 +43,8 @@ async function encryptSensitiveFields(config: BackendConfig): Promise { +const listVolumes = async () => { + const organizationId = getOrganizationId(); const volumes = await db.query.volumesTable.findMany({ where: eq(volumesTable.organizationId, organizationId), }); @@ -50,7 +52,8 @@ const listVolumes = async (organizationId: string) => { return volumes; }; -const findVolume = async (idOrShortId: string | number, organizationId: string) => { +const findVolume = async (idOrShortId: string | number) => { + const organizationId = getOrganizationId(); const isNumeric = typeof idOrShortId === "number" || /^\d+$/.test(String(idOrShortId)); return await db.query.volumesTable.findFirst({ where: and( @@ -60,7 +63,8 @@ const findVolume = async (idOrShortId: string | number, organizationId: string) }); }; -const createVolume = async (name: string, backendConfig: BackendConfig, organizationId: string) => { +const createVolume = async (name: string, backendConfig: BackendConfig) => { + const organizationId = getOrganizationId(); const slug = slugify(name, { lower: true, strict: true }); const existing = await db.query.volumesTable.findFirst({ @@ -100,8 +104,9 @@ const createVolume = async (name: string, backendConfig: BackendConfig, organiza return { volume: created, status: 201 }; }; -const deleteVolume = async (idOrShortId: string | number, organizationId: string) => { - const volume = await findVolume(idOrShortId, organizationId); +const deleteVolume = async (idOrShortId: string | number) => { + const organizationId = getOrganizationId(); + const volume = await findVolume(idOrShortId); if (!volume) { throw new NotFoundError("Volume not found"); @@ -114,8 +119,9 @@ const deleteVolume = async (idOrShortId: string | number, organizationId: string .where(and(eq(volumesTable.id, volume.id), eq(volumesTable.organizationId, organizationId))); }; -const mountVolume = async (idOrShortId: string | number, organizationId: string) => { - const volume = await findVolume(idOrShortId, organizationId); +const mountVolume = async (idOrShortId: string | number) => { + const organizationId = getOrganizationId(); + const volume = await findVolume(idOrShortId); if (!volume) { throw new NotFoundError("Volume not found"); @@ -136,8 +142,9 @@ const mountVolume = async (idOrShortId: string | number, organizationId: string) return { error, status }; }; -const unmountVolume = async (idOrShortId: string | number, organizationId: string) => { - const volume = await findVolume(idOrShortId, organizationId); +const unmountVolume = async (idOrShortId: string | number) => { + const organizationId = getOrganizationId(); + const volume = await findVolume(idOrShortId); if (!volume) { throw new NotFoundError("Volume not found"); @@ -158,8 +165,8 @@ const unmountVolume = async (idOrShortId: string | number, organizationId: strin return { error, status }; }; -const getVolume = async (idOrShortId: string | number, organizationId: string) => { - const volume = await findVolume(idOrShortId, organizationId); +const getVolume = async (idOrShortId: string | number) => { + const volume = await findVolume(idOrShortId); if (!volume) { throw new NotFoundError("Volume not found"); @@ -176,8 +183,9 @@ const getVolume = async (idOrShortId: string | number, organizationId: string) = return { volume, statfs }; }; -const updateVolume = async (idOrShortId: string | number, volumeData: UpdateVolumeBody, organizationId: string) => { - const existing = await findVolume(idOrShortId, organizationId); +const updateVolume = async (idOrShortId: string | number, volumeData: UpdateVolumeBody) => { + const organizationId = getOrganizationId(); + const existing = await findVolume(idOrShortId); if (!existing) { throw new NotFoundError("Volume not found"); @@ -188,7 +196,11 @@ const updateVolume = async (idOrShortId: string | number, volumeData: UpdateVolu 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), eq(volumesTable.organizationId, organizationId)), + where: and( + eq(volumesTable.name, newSlug), + ne(volumesTable.id, existing.id), + eq(volumesTable.organizationId, organizationId), + ), }); if (conflict) { @@ -277,8 +289,9 @@ const testConnection = async (backendConfig: BackendConfig) => { }; }; -const checkHealth = async (idOrShortId: string | number, organizationId: string) => { - const volume = await findVolume(idOrShortId, organizationId); +const checkHealth = async (idOrShortId: string | number) => { + const organizationId = getOrganizationId(); + const volume = await findVolume(idOrShortId); if (!volume) { throw new NotFoundError("Volume not found"); @@ -299,8 +312,8 @@ const checkHealth = async (idOrShortId: string | number, organizationId: string) return { status, error }; }; -const listFiles = async (idOrShortId: string | number, organizationId: string, subPath?: string) => { - const volume = await findVolume(idOrShortId, organizationId); +const listFiles = async (idOrShortId: string | number, subPath?: string) => { + const volume = await findVolume(idOrShortId); if (!volume) { throw new NotFoundError("Volume not found");