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,55 @@
-- CreateTable: User
CREATE TABLE `User` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`username` VARCHAR(191) NOT NULL,
`email` VARCHAR(191) NOT NULL,
`passwordHash` VARCHAR(191) NOT NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE INDEX `User_username_key` (`username`),
UNIQUE INDEX `User_email_key` (`email`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable: UserProduct
CREATE TABLE `UserProduct` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`userId` INTEGER NOT NULL,
`productId` INTEGER NOT NULL,
`note` TEXT NULL,
`preferredBrand` VARCHAR(191) NULL,
`preferredStore` VARCHAR(191) NULL,
`isPrivate` BOOLEAN NOT NULL DEFAULT false,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE INDEX `UserProduct_userId_productId_key` (`userId`, `productId`),
INDEX `UserProduct_userId_idx` (`userId`),
INDEX `UserProduct_productId_idx` (`productId`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable: RecipeShare
CREATE TABLE `RecipeShare` (
`recipeId` INTEGER NOT NULL,
`userId` INTEGER NOT NULL,
PRIMARY KEY (`recipeId`, `userId`),
INDEX `RecipeShare_userId_idx` (`userId`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- AlterTable: Product add ownerId
ALTER TABLE `Product` ADD COLUMN `ownerId` INTEGER NULL;
-- AlterTable: Recipe add ownerId and isPublic
ALTER TABLE `Recipe` ADD COLUMN `ownerId` INTEGER NULL,
ADD COLUMN `isPublic` BOOLEAN NOT NULL DEFAULT false;
-- Make all existing recipes (without owner) public
UPDATE `Recipe` SET `isPublic` = true WHERE `ownerId` IS NULL;
-- AddForeignKey
ALTER TABLE `UserProduct` ADD CONSTRAINT `UserProduct_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `UserProduct` ADD CONSTRAINT `UserProduct_productId_fkey` FOREIGN KEY (`productId`) REFERENCES `Product`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `RecipeShare` ADD CONSTRAINT `RecipeShare_recipeId_fkey` FOREIGN KEY (`recipeId`) REFERENCES `Recipe`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `RecipeShare` ADD CONSTRAINT `RecipeShare_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE `Product` ADD CONSTRAINT `Product_ownerId_fkey` FOREIGN KEY (`ownerId`) REFERENCES `User`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE `Recipe` ADD CONSTRAINT `Recipe_ownerId_fkey` FOREIGN KEY (`ownerId`) REFERENCES `User`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
+50 -1
View File
@@ -7,6 +7,19 @@ datasource db {
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
username String @unique
email String @unique
passwordHash String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
userProducts UserProduct[]
ownedRecipes Recipe[] @relation("RecipeOwner")
sharedRecipes RecipeShare[]
}
model Product {
id Int @id @default(autoincrement())
name String
@@ -26,6 +39,28 @@ model Product {
receiptAliases ReceiptAlias[]
tags ProductTag[]
nutrition Nutrition?
ownerId Int?
owner User? @relation(fields: [ownerId], references: [id], onDelete: SetNull)
userProducts UserProduct[]
}
model UserProduct {
id Int @id @default(autoincrement())
userId Int
productId Int
note String? @db.Text
preferredBrand String?
preferredStore String?
isPrivate Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
@@unique([userId, productId])
@@index([userId])
@@index([productId])
}
model InventoryItem {
@@ -73,11 +108,25 @@ model Recipe {
instructions String? @db.Text
imageUrl String?
servings Int?
isPublic Boolean @default(false)
ownerId Int?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
ingredients RecipeIngredient[]
owner User? @relation("RecipeOwner", fields: [ownerId], references: [id], onDelete: SetNull)
ingredients RecipeIngredient[]
mealPlanEntries MealPlanEntry[]
shares RecipeShare[]
}
model RecipeShare {
recipeId Int
userId Int
recipe Recipe @relation(fields: [recipeId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@id([recipeId, userId])
@@index([userId])
}
model RecipeIngredient {