feat(recipes): add recipe visibility and sharing features

- Implemented functionality to set recipe visibility (public/private) with appropriate checks for user permissions.
- Added ability to share recipes with other users, including validation for existing users and permissions.
- Introduced new DTOs for setting visibility and sharing recipes.
- Updated RecipesController and RecipesService to handle new endpoints for visibility and sharing.
- Enhanced inventory preview to consider user permissions and shared recipes.
- Updated front-end to support new sharing and visibility features, including UI changes for recipe detail and admin user management.
This commit is contained in:
Nils-Johan Gynther
2026-05-02 09:19:59 +02:00
parent f67bf8baef
commit 41ae7d4d06
17 changed files with 742 additions and 124 deletions
+108 -6
View File
@@ -1,4 +1,4 @@
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { ForbiddenException, Injectable, Logger, NotFoundException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
@@ -31,9 +31,16 @@ export class RecipesService {
constructor(private readonly prisma: PrismaService) {}
async getInventoryPreview(id: number) {
const recipe = await this.prisma.recipe.findUnique({
where: { id },
async getInventoryPreview(id: number, userId: number) {
const recipe = await this.prisma.recipe.findFirst({
where: {
id,
OR: [
{ isPublic: true },
{ ownerId: userId },
{ shares: { some: { userId } } },
],
},
include: {
ingredients: {
include: {
@@ -287,21 +294,116 @@ export class RecipesService {
}
}
async updateImage(id: number, sourceUrl: string) {
async updateImage(id: number, sourceUrl: string, userId: number) {
const existingRecipe = await this.prisma.recipe.findUnique({ where: { id } });
if (!existingRecipe) {
throw new NotFoundException(`Recipe with id ${id} not found`);
}
if (existingRecipe.ownerId !== userId) {
throw new NotFoundException(`Recipe with id ${id} not found`);
}
const imageUrl = await downloadAndOptimizeImage(sourceUrl, IMAGE_DEST_DIR);
return this.prisma.recipe.update({
where: { id },
data: { imageUrl },
include: { ingredients: { include: { product: { include: { nutrition: true } } } } },
include: {
ingredients: { include: { product: { include: { nutrition: true } } } },
owner: { select: { id: true, username: true } },
shares: { select: { userId: true } },
},
});
}
async setVisibility(id: number, userId: number, isPublic: boolean) {
const existingRecipe = await this.prisma.recipe.findUnique({ where: { id } });
if (!existingRecipe || existingRecipe.ownerId !== userId) {
throw new NotFoundException(`Recipe with id ${id} not found`);
}
if (isPublic) {
const owner = await this.prisma.user.findUnique({
where: { id: userId },
select: { canShareRecipes: true },
});
if (!owner?.canShareRecipes) {
throw new ForbiddenException('Du har inte behörighet att dela recept.');
}
}
return this.prisma.recipe.update({
where: { id },
data: { isPublic },
include: {
ingredients: { include: { product: { include: { nutrition: true } } } },
owner: { select: { id: true, username: true } },
shares: { select: { userId: true } },
},
});
}
async shareWithUser(id: number, ownerId: number, username: string) {
const recipe = await this.prisma.recipe.findUnique({
where: { id },
select: { id: true, ownerId: true },
});
if (!recipe || recipe.ownerId !== ownerId) {
throw new NotFoundException(`Recipe with id ${id} not found`);
}
const owner = await this.prisma.user.findUnique({
where: { id: ownerId },
select: { canShareRecipes: true },
});
if (!owner?.canShareRecipes) {
throw new ForbiddenException('Du har inte behörighet att dela recept.');
}
const targetUser = await this.prisma.user.findUnique({
where: { username },
select: { id: true },
});
if (!targetUser) {
throw new NotFoundException(`User ${username} not found`);
}
if (targetUser.id === ownerId) {
return this.findOne(id, ownerId);
}
await this.prisma.recipeShare.upsert({
where: { recipeId_userId: { recipeId: id, userId: targetUser.id } },
create: { recipeId: id, userId: targetUser.id },
update: {},
});
return this.findOne(id, ownerId);
}
async unshareWithUser(id: number, ownerId: number, username: string) {
const recipe = await this.prisma.recipe.findUnique({
where: { id },
select: { id: true, ownerId: true },
});
if (!recipe || recipe.ownerId !== ownerId) {
throw new NotFoundException(`Recipe with id ${id} not found`);
}
const targetUser = await this.prisma.user.findUnique({
where: { username },
select: { id: true },
});
if (!targetUser) {
throw new NotFoundException(`User ${username} not found`);
}
await this.prisma.recipeShare.deleteMany({
where: { recipeId: id, userId: targetUser.id },
});
return this.findOne(id, ownerId);
}
async create(createRecipeDto: CreateRecipeDto, userId: number) {
this.logger.log(
`[create] Incoming imageUrl from client: ${createRecipeDto.imageUrl ?? 'null'}`,