From e59a3eac2fd900be7a0bb17cfa78228fad913da2 Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Sat, 17 Jan 2026 22:47:43 +0100 Subject: [PATCH] refactor: download restic password from stored db password --- app/server/modules/auth/auth.middleware.ts | 22 +++++++++++++++++++ .../modules/system/system.controller.ts | 21 +++++++++++++----- app/server/modules/system/system.dto.ts | 4 ++-- 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/app/server/modules/auth/auth.middleware.ts b/app/server/modules/auth/auth.middleware.ts index 992bbbf5..6456f7c3 100644 --- a/app/server/modules/auth/auth.middleware.ts +++ b/app/server/modules/auth/auth.middleware.ts @@ -1,5 +1,8 @@ import { createMiddleware } from "hono/factory"; import { auth } from "~/lib/auth"; +import { db } from "~/server/db/db"; +import { member } from "~/server/db/schema"; +import { eq, and } from "drizzle-orm"; declare module "hono" { interface ContextVariableMap { @@ -33,3 +36,22 @@ export const requireAuth = createMiddleware(async (c, next) => { await next(); }); + +/** + * Middleware to require organization owner or admin role + * Verifies the user has the required role in the current organization + */ +export const requireOrgAdmin = createMiddleware(async (c, next) => { + const user = c.get("user"); + const organizationId = c.get("organizationId"); + + const membership = await db.query.member.findFirst({ + where: and(eq(member.userId, user.id), eq(member.organizationId, organizationId)), + }); + + if (!membership || (membership.role !== "owner" && membership.role !== "admin")) { + return c.json({ message: "Forbidden" }, 403); + } + + await next(); +}); diff --git a/app/server/modules/system/system.controller.ts b/app/server/modules/system/system.controller.ts index 326fe33f..cfd5d724 100644 --- a/app/server/modules/system/system.controller.ts +++ b/app/server/modules/system/system.controller.ts @@ -9,12 +9,12 @@ import { type UpdateInfoDto, } from "./system.dto"; import { systemService } from "./system.service"; -import { requireAuth } from "../auth/auth.middleware"; -import { RESTIC_PASS_FILE } from "../../core/constants"; +import { requireAuth, requireOrgAdmin } from "../auth/auth.middleware"; import { db } from "../../db/db"; -import { usersTable } from "../../db/schema"; +import { organization, usersTable } from "../../db/schema"; import { eq } from "drizzle-orm"; import { verifyUserPassword } from "../auth/helpers"; +import { cryptoUtils } from "../../utils/crypto"; export const systemController = new Hono() .use(requireAuth) @@ -30,10 +30,12 @@ export const systemController = new Hono() }) .post( "/restic-password", + requireOrgAdmin, downloadResticPasswordDto, validator("json", downloadResticPasswordBodySchema), async (c) => { const user = c.get("user"); + const organizationId = c.get("organizationId"); const body = c.req.valid("json"); const isPasswordValid = await verifyUserPassword({ password: body.password, userId: user.id }); @@ -42,8 +44,15 @@ export const systemController = new Hono() } try { - const file = Bun.file(RESTIC_PASS_FILE); - const content = await file.text(); + const org = await db.query.organization.findFirst({ + where: eq(organization.id, organizationId), + }); + + if (!org?.metadata?.resticPassword) { + return c.json({ message: "Organization Restic password not found" }, 404); + } + + const content = await cryptoUtils.resolveSecret(org.metadata.resticPassword); await db.update(usersTable).set({ hasDownloadedResticPassword: true }).where(eq(usersTable.id, user.id)); @@ -52,7 +61,7 @@ export const systemController = new Hono() return c.text(content); } catch (_error) { - return c.json({ message: "Failed to read Restic password file" }, 500); + return c.json({ message: "Failed to retrieve Restic password" }, 500); } }, ); diff --git a/app/server/modules/system/system.dto.ts b/app/server/modules/system/system.dto.ts index 3eccafd8..312e53b7 100644 --- a/app/server/modules/system/system.dto.ts +++ b/app/server/modules/system/system.dto.ts @@ -65,12 +65,12 @@ export const downloadResticPasswordBodySchema = type({ }); export const downloadResticPasswordDto = describeRoute({ - description: "Download the Restic password file for backup recovery. Requires password re-authentication.", + description: "Download the organization's Restic password for backup recovery. Requires organization owner or admin role and password re-authentication.", tags: ["System"], operationId: "downloadResticPassword", responses: { 200: { - description: "Restic password file content", + description: "Organization's Restic password", content: { "text/plain": { schema: { type: "string" },