refactor(api): share owned resource read access

This commit is contained in:
2026-04-01 07:35:34 +02:00
parent a0c98cf24d
commit 41916a4e46
7 changed files with 336 additions and 118 deletions
@@ -1,48 +1,20 @@
import { PreviewResolvedHolidaysSchema } from "@capakraken/shared";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { findUniqueOrThrow } from "../db/helpers.js";
import { asHolidayResolverDb, getResolvedCalendarHolidays } from "../lib/holiday-availability.js";
import { protectedProcedure } from "../trpc.js";
import { type HolidayReadContext } from "./holiday-calendar-shared.js";
function canManageHolidayResourceReads(ctx: HolidayReadContext): boolean {
const role = ctx.dbUser?.systemRole;
return role === "ADMIN" || role === "MANAGER";
}
async function findOwnedHolidayResourceId(ctx: HolidayReadContext): Promise<string | null> {
if (!ctx.dbUser?.id) {
return null;
}
if (!ctx.db.resource || typeof ctx.db.resource.findFirst !== "function") {
return null;
}
const resource = await ctx.db.resource.findFirst({
where: { userId: ctx.dbUser.id },
select: { id: true },
});
return resource?.id ?? null;
}
import { assertCanReadOwnedResource } from "./resource-owned-read-access.js";
async function assertCanReadHolidayResource(
ctx: HolidayReadContext,
resourceId: string,
): Promise<void> {
if (canManageHolidayResourceReads(ctx)) {
return;
}
const ownedResourceId = await findOwnedHolidayResourceId(ctx);
if (!ownedResourceId || ownedResourceId !== resourceId) {
throw new TRPCError({
code: "FORBIDDEN",
message: "You can only view holiday data for your own resource",
});
}
await assertCanReadOwnedResource(
ctx,
resourceId,
"You can only view holiday data for your own resource",
);
}
function formatResolvedHolidayDetail(holiday: {
@@ -0,0 +1,66 @@
import { TRPCError } from "@trpc/server";
import type { TRPCContext } from "../trpc.js";
export type OwnedResourceReadContext = Pick<TRPCContext, "db" | "dbUser">;
export function canManageOwnedResourceReads(ctx: { dbUser: { systemRole: string } | null }): boolean {
const role = ctx.dbUser?.systemRole;
return role === "ADMIN" || role === "MANAGER";
}
export async function findOwnedReadResourceId(
ctx: OwnedResourceReadContext,
): Promise<string | null> {
if (!ctx.dbUser?.id) {
return null;
}
if (!ctx.db.resource || typeof ctx.db.resource.findFirst !== "function") {
return null;
}
const resource = await ctx.db.resource.findFirst({
where: { userId: ctx.dbUser.id },
select: { id: true },
});
return resource?.id ?? null;
}
export async function assertCanReadOwnedResource(
ctx: OwnedResourceReadContext,
resourceId: string,
forbiddenMessage: string,
): Promise<void> {
if (canManageOwnedResourceReads(ctx)) {
return;
}
const ownedResourceId = await findOwnedReadResourceId(ctx);
if (!ownedResourceId || ownedResourceId !== resourceId) {
throw new TRPCError({
code: "FORBIDDEN",
message: forbiddenMessage,
});
}
}
export async function resolveOwnedResourceReadFilter(
ctx: OwnedResourceReadContext,
requestedResourceId: string | undefined,
forbiddenMessage: string,
): Promise<string | null | undefined> {
if (canManageOwnedResourceReads(ctx)) {
return requestedResourceId;
}
const ownedResourceId = await findOwnedReadResourceId(ctx);
if (requestedResourceId && requestedResourceId !== ownedResourceId) {
throw new TRPCError({
code: "FORBIDDEN",
message: forbiddenMessage,
});
}
return ownedResourceId;
}
+18 -45
View File
@@ -11,48 +11,27 @@ import {
findVacationResourceChapter,
listChapterVacationOverlaps,
} from "./vacation-read-support.js";
import {
assertCanReadOwnedResource,
canManageOwnedResourceReads,
resolveOwnedResourceReadFilter,
} from "./resource-owned-read-access.js";
type VacationReadContext = Pick<TRPCContext, "db" | "dbUser">;
export function canManageVacationReads(ctx: { dbUser: { systemRole: string } | null }): boolean {
const role = ctx.dbUser?.systemRole;
return role === "ADMIN" || role === "MANAGER";
}
export async function findOwnedResourceId(
ctx: VacationReadContext,
): Promise<string | null> {
if (!ctx.dbUser?.id) {
return null;
}
if (!ctx.db.resource || typeof ctx.db.resource.findFirst !== "function") {
return null;
}
const resource = await ctx.db.resource.findFirst({
where: { userId: ctx.dbUser.id },
select: { id: true },
});
return resource?.id ?? null;
return canManageOwnedResourceReads(ctx);
}
export async function assertCanReadVacationResource(
ctx: VacationReadContext,
resourceId: string,
): Promise<void> {
if (canManageVacationReads(ctx)) {
return;
}
const ownedResourceId = await findOwnedResourceId(ctx);
if (!ownedResourceId || ownedResourceId !== resourceId) {
throw new TRPCError({
code: "FORBIDDEN",
message: "You can only view vacation data for your own resource",
});
}
await assertCanReadOwnedResource(
ctx,
resourceId,
"You can only view vacation data for your own resource",
);
}
export function isSameUtcDay(left: Date, right: Date): boolean {
@@ -159,20 +138,14 @@ export const vacationReadProcedures = {
}),
)
.query(async ({ ctx, input }) => {
let resourceIdFilter = input.resourceId;
const resourceIdFilter = await resolveOwnedResourceReadFilter(
ctx,
input.resourceId,
"You can only view vacation data for your own resource",
);
if (!canManageVacationReads(ctx)) {
const ownedResourceId = await findOwnedResourceId(ctx);
if (input.resourceId && input.resourceId !== ownedResourceId) {
throw new TRPCError({
code: "FORBIDDEN",
message: "You can only view vacation data for your own resource",
});
}
if (!ownedResourceId) {
return [];
}
resourceIdFilter = ownedResourceId;
if (!canManageVacationReads(ctx) && !resourceIdFilter) {
return [];
}
const vacations = await ctx.db.vacation.findMany({