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
@@ -0,0 +1,22 @@
import { IsInt, IsString, IsOptional, IsBoolean } from 'class-validator';
export class UpsertUserProductDto {
@IsInt()
productId: number;
@IsOptional()
@IsString()
note?: string;
@IsOptional()
@IsString()
preferredBrand?: string;
@IsOptional()
@IsString()
preferredStore?: string;
@IsOptional()
@IsBoolean()
isPrivate?: boolean;
}
@@ -0,0 +1,38 @@
import {
Controller,
Get,
Post,
Delete,
Body,
Param,
ParseIntPipe,
} from '@nestjs/common';
import { UserProductsService } from './user-products.service';
import { UpsertUserProductDto } from './dto/upsert-user-product.dto';
import { CurrentUser } from '../auth/decorators/current-user.decorator';
@Controller('user-products')
export class UserProductsController {
constructor(private readonly service: UserProductsService) {}
@Get()
findAll(@CurrentUser() user: { userId: number }) {
return this.service.findAll(user.userId);
}
@Post()
upsert(
@CurrentUser() user: { userId: number },
@Body() dto: UpsertUserProductDto,
) {
return this.service.upsert(user.userId, dto);
}
@Delete(':productId')
remove(
@CurrentUser() user: { userId: number },
@Param('productId', ParseIntPipe) productId: number,
) {
return this.service.remove(user.userId, productId);
}
}
@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { UserProductsController } from './user-products.controller';
import { UserProductsService } from './user-products.service';
import { PrismaModule } from '../prisma/prisma.module';
@Module({
imports: [PrismaModule],
controllers: [UserProductsController],
providers: [UserProductsService],
})
export class UserProductsModule {}
@@ -0,0 +1,48 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { UpsertUserProductDto } from './dto/upsert-user-product.dto';
const PRODUCT_INCLUDE = {
product: {
include: {
nutrition: true,
tags: { include: { tag: true } },
},
},
};
@Injectable()
export class UserProductsService {
constructor(private readonly prisma: PrismaService) {}
findAll(userId: number) {
return this.prisma.userProduct.findMany({
where: { userId },
include: PRODUCT_INCLUDE,
orderBy: { updatedAt: 'desc' },
});
}
findOne(userId: number, productId: number) {
return this.prisma.userProduct.findUnique({
where: { userId_productId: { userId, productId } },
include: PRODUCT_INCLUDE,
});
}
upsert(userId: number, dto: UpsertUserProductDto) {
const { productId, ...data } = dto;
return this.prisma.userProduct.upsert({
where: { userId_productId: { userId, productId } },
create: { userId, productId, ...data },
update: data,
include: PRODUCT_INCLUDE,
});
}
remove(userId: number, productId: number) {
return this.prisma.userProduct.delete({
where: { userId_productId: { userId, productId } },
});
}
}