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:
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsInt,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
@@ -47,6 +48,10 @@ export class CreateRecipeDto {
|
||||
@Min(1)
|
||||
servings?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isPublic?: boolean;
|
||||
|
||||
@IsArray()
|
||||
@ArrayMinSize(1)
|
||||
@ValidateNested({ each: true })
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Body, Controller, Delete, Get, HttpCode, Param, ParseIntPipe, Post, Patch } from '@nestjs/common';
|
||||
import { Body, Controller, Delete, Get, HttpCode, Param, ParseIntPipe, Post, Patch, Request } from '@nestjs/common';
|
||||
import { IsString } from 'class-validator';
|
||||
import { RecipesService } from './recipes.service';
|
||||
import { CreateRecipeDto } from './dto/create-recipe.dto';
|
||||
import { ParseMarkdownDto } from './dto/parse-markdown.dto';
|
||||
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
||||
|
||||
class UpdateImageDto {
|
||||
@IsString()
|
||||
@@ -19,8 +20,8 @@ export class RecipesController {
|
||||
}
|
||||
|
||||
@Get()
|
||||
findAll() {
|
||||
return this.recipesService.findAll();
|
||||
findAll(@CurrentUser() user: { userId: number }) {
|
||||
return this.recipesService.findAll(user.userId);
|
||||
}
|
||||
|
||||
@Get(':id/inventory-preview')
|
||||
@@ -29,27 +30,37 @@ export class RecipesController {
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
findOne(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.recipesService.findOne(id);
|
||||
findOne(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@CurrentUser() user: { userId: number },
|
||||
) {
|
||||
return this.recipesService.findOne(id, user.userId);
|
||||
}
|
||||
|
||||
@Post()
|
||||
async create(@Body() createRecipeDto: CreateRecipeDto) {
|
||||
return this.recipesService.create(createRecipeDto);
|
||||
async create(
|
||||
@Body() createRecipeDto: CreateRecipeDto,
|
||||
@CurrentUser() user: { userId: number },
|
||||
) {
|
||||
return this.recipesService.create(createRecipeDto, user.userId);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
async update(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Body() createRecipeDto: CreateRecipeDto,
|
||||
@CurrentUser() user: { userId: number },
|
||||
) {
|
||||
return this.recipesService.update(id, createRecipeDto);
|
||||
return this.recipesService.update(id, createRecipeDto, user.userId);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@HttpCode(204)
|
||||
async remove(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.recipesService.remove(id);
|
||||
async remove(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@CurrentUser() user: { userId: number },
|
||||
) {
|
||||
return this.recipesService.remove(id, user.userId);
|
||||
}
|
||||
|
||||
@Post(':id/image')
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user