Files
homarr/packages/api/src/router/medias/media-router.ts
Meier Lukas 5c99622fa8 fix(deps): upgrade zod to v4 and fix breaking changes (#3461)
* fix(deps): update dependency drizzle-zod to ^0.8.2

* chore: update zod to v4 import

* fix: path is no longer available in transform context

* fix: AnyZodObject does no longer exist

* fix: auth env.ts using wrong createEnv and remove unused file env-validation.ts

* fix: required_error no longer exists on z.string

* fix: zod error map is deprecated and replaced with config

* fix: default requires callback now

* fix: migrate zod resolver for mantine

* fix: remove unused form translation file

* fix: wrong enum type

* fix: record now requires two arguments

* fix: add-confirm-password-refinement type issues

* fix: add missing first record argument for entityStateSchema

* fix: migrate superrefine to check

* fix(deps): upgrade zod-form-data to v3

* fix: migrate superRefine to check for mediaUploadSchema

* fix: authProvidersSchema default is array

* fix: use stringbool instead of custom implementation

* fix: record requires first argument

* fix: migrate superRefine to check for certificate router

* fix: confirm pasword refinement is overwriting types

* fix: email optional not working

* fix: migrate intersection to object converter

* fix: safe parse return value rename

* fix: easier access for min and max number value

* fix: migrate superRefine to check for oldmarr import file

* fix: inference of enum shape for old-import board-size wrong

* fix: errors renamed to issues

* chore: address pull request feedback

* fix: zod form requires object

* fix: inference for use-zod-form not working

* fix: remove unnecessary convertion

* fix(deps): upgrade trpc-to-openapi to v3

* fix: build error

* fix: migrate missing zod imports to v4

* fix: migrate zod records to v4

* fix: missing core package dependency in api module

* fix: unable to convert custom zod schema to openapi schema

* fix(deps): upgrade zod to v4

* chore(renovate): enable zod dependency updates

* test: add simple unit test for convertIntersectionToZodObject

---------

Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-15 18:15:58 +00:00

115 lines
3.7 KiB
TypeScript

import { TRPCError } from "@trpc/server";
import { z } from "zod/v4";
import { createId } from "@homarr/common";
import type { InferInsertModel } from "@homarr/db";
import { and, desc, eq, like } from "@homarr/db";
import { iconRepositories, icons, medias } from "@homarr/db/schema";
import { createLocalImageUrl, LOCAL_ICON_REPOSITORY_SLUG, mapMediaToIcon } from "@homarr/icons/local";
import { byIdSchema, paginatedSchema } from "@homarr/validation/common";
import { mediaUploadSchema } from "@homarr/validation/media";
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure } from "../../trpc";
export const mediaRouter = createTRPCRouter({
getPaginated: protectedProcedure
.input(
paginatedSchema.and(
z.object({ includeFromAllUsers: z.boolean().default(false), search: z.string().trim().default("") }),
),
)
.query(async ({ ctx, input }) => {
const includeFromAllUsers = ctx.session.user.permissions.includes("media-view-all") && input.includeFromAllUsers;
const where = and(
input.search.length >= 1 ? like(medias.name, `%${input.search}%`) : undefined,
includeFromAllUsers ? undefined : eq(medias.creatorId, ctx.session.user.id),
);
const dbMedias = await ctx.db.query.medias.findMany({
where,
orderBy: desc(medias.createdAt),
limit: input.pageSize,
offset: (input.page - 1) * input.pageSize,
columns: {
content: false,
},
with: {
creator: {
columns: {
id: true,
name: true,
image: true,
},
},
},
});
const totalCount = await ctx.db.$count(medias, where);
return {
items: dbMedias,
totalCount,
};
}),
uploadMedia: permissionRequiredProcedure
.requiresPermission("media-upload")
.input(mediaUploadSchema)
.mutation(async ({ ctx, input }) => {
const content = Buffer.from(await input.file.arrayBuffer());
const id = createId();
const media = {
id,
creatorId: ctx.session.user.id,
content,
size: input.file.size,
contentType: input.file.type,
name: input.file.name,
} satisfies InferInsertModel<typeof medias>;
await ctx.db.insert(medias).values(media);
const localIconRepository = await ctx.db.query.iconRepositories.findFirst({
where: eq(iconRepositories.slug, LOCAL_ICON_REPOSITORY_SLUG),
});
if (!localIconRepository) return id;
const icon = mapMediaToIcon(media);
await ctx.db.insert(icons).values({
id: createId(),
checksum: icon.checksum,
name: icon.fileNameWithExtension,
url: icon.imageUrl,
iconRepositoryId: localIconRepository.id,
});
return id;
}),
deleteMedia: protectedProcedure.input(byIdSchema).mutation(async ({ ctx, input }) => {
const dbMedia = await ctx.db.query.medias.findFirst({
where: eq(medias.id, input.id),
columns: {
id: true,
creatorId: true,
},
});
if (!dbMedia) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Media not found",
});
}
// Only allow users with media-full-all permission and the creator of the media to delete it
if (!ctx.session.user.permissions.includes("media-full-all") && ctx.session.user.id !== dbMedia.creatorId) {
throw new TRPCError({
code: "FORBIDDEN",
message: "You don't have permission to delete this media",
});
}
await ctx.db.delete(medias).where(eq(medias.id, input.id));
await ctx.db.delete(icons).where(eq(icons.url, createLocalImageUrl(input.id)));
}),
});