feat(auth): implement user authentication with JWT and NextAuth

- Added user registration and login functionality with JWT authentication.
- Created auth controller, service, and module in the backend.
- Implemented user model and user products management.
- Integrated NextAuth for session management on the frontend.
- Added middleware for protecting routes and handling public access.
- Updated frontend API routes to include authorization headers.
- Enhanced recipe and user product models to support ownership and visibility.
- Created registration and login pages in the frontend.
- Added necessary types for NextAuth session management.
This commit is contained in:
Nils-Johan Gynther
2026-04-17 19:57:08 +02:00
parent 4c0411a7f2
commit ce0cc6fbf0
55 changed files with 1006 additions and 137 deletions
+38 -8
View File
@@ -290,27 +290,45 @@ export class RecipesService {
};
}
async findAll() {
async findAll(userId: number) {
return this.prisma.recipe.findMany({
where: {
OR: [
{ isPublic: true },
{ ownerId: userId },
{ shares: { some: { userId } } },
],
},
include: {
ingredients: {
include: {
product: { include: { nutrition: true } },
},
},
owner: { select: { id: true, username: true } },
shares: { select: { userId: true } },
},
});
}
async findOne(id: number) {
const recipe = await this.prisma.recipe.findUnique({
where: { id },
async findOne(id: number, userId: number) {
const recipe = await this.prisma.recipe.findFirst({
where: {
id,
OR: [
{ isPublic: true },
{ ownerId: userId },
{ shares: { some: { userId } } },
],
},
include: {
ingredients: {
include: {
product: { include: { nutrition: true } },
},
},
owner: { select: { id: true, username: true } },
shares: { select: { userId: true } },
},
});
@@ -321,8 +339,8 @@ export class RecipesService {
return recipe;
}
async update(id: number, updateRecipeDto: CreateRecipeDto) {
// Verifiera att receptet finns
async update(id: number, updateRecipeDto: CreateRecipeDto, userId: number) {
// Verifiera att receptet finns och att användaren äger det
const existingRecipe = await this.prisma.recipe.findUnique({
where: { id },
});
@@ -331,6 +349,11 @@ export class RecipesService {
throw new NotFoundException(`Recipe with id ${id} not found`);
}
// Tillåt uppdatering om användaren är ägare ELLER om receptet är publikt utan ägare
if (existingRecipe.ownerId !== null && existingRecipe.ownerId !== userId) {
throw new NotFoundException(`Recipe with id ${id} not found`);
}
// Ta bort gamla ingredienser
await this.prisma.recipeIngredient.deleteMany({
where: { recipeId: id },
@@ -344,6 +367,7 @@ export class RecipesService {
description: updateRecipeDto.description || null,
instructions: updateRecipeDto.instructions || null,
servings: updateRecipeDto.servings ?? null,
...(updateRecipeDto.isPublic !== undefined && { isPublic: updateRecipeDto.isPublic }),
...(updateRecipeDto.imageUrl !== undefined && { imageUrl: updateRecipeDto.imageUrl || null }),
ingredients: {
create: updateRecipeDto.ingredients.map((ingredient) => ({
@@ -366,7 +390,7 @@ export class RecipesService {
return recipe;
}
async remove(id: number) {
async remove(id: number, userId: number) {
const existingRecipe = await this.prisma.recipe.findUnique({
where: { id },
});
@@ -375,6 +399,10 @@ export class RecipesService {
throw new NotFoundException(`Recipe with id ${id} not found`);
}
if (existingRecipe.ownerId !== null && existingRecipe.ownerId !== userId) {
throw new NotFoundException(`Recipe with id ${id} not found`);
}
await this.prisma.recipeIngredient.deleteMany({ where: { recipeId: id } });
await this.prisma.recipe.delete({ where: { id } });
}
@@ -394,7 +422,7 @@ export class RecipesService {
});
}
async create(createRecipeDto: CreateRecipeDto) {
async create(createRecipeDto: CreateRecipeDto, userId: number) {
// Om imageUrl är en extern URL — ladda ner och optimera
let imageUrl: string | null = createRecipeDto.imageUrl || null;
if (imageUrl && imageUrl.startsWith('http')) {
@@ -413,6 +441,8 @@ export class RecipesService {
instructions: createRecipeDto.instructions || null,
imageUrl,
servings: createRecipeDto.servings ?? null,
ownerId: userId,
isPublic: false,
ingredients: {
create: createRecipeDto.ingredients.map((ingredient) => ({
productId: ingredient.productId,