first commit

This commit is contained in:
2025-11-06 16:40:32 +03:00
commit f0948a5b05
51 changed files with 13047 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
import { Module } from '@nestjs/common';
import { auth } from './auth/auth';
import { AuthModule } from '@thallesp/nestjs-better-auth';
import { UserModule } from './user/user.module';
import { PrismaModule } from './prisma/prisma.module';
import { CategoryModule } from './category/category.module';
import { RestaurantModule } from './restaurant/restaurant.module';
import { ItemModule } from './item/item.module';
@Module({
imports: [
AuthModule.forRoot({ auth }),
PrismaModule,
UserModule,
CategoryModule,
RestaurantModule,
ItemModule,
],
})
export class AppModule {}
+22
View File
@@ -0,0 +1,22 @@
import { betterAuth } from 'better-auth';
import { prismaAdapter } from 'better-auth/adapters/prisma';
// If your Prisma file is located elsewhere, you can change the path
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export const auth = betterAuth({
database: prismaAdapter(prisma, {
provider: 'postgresql',
}),
emailAndPassword: {
enabled: true,
// requireEmailVerification: true,
},
socialProviders: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
},
},
});
+55
View File
@@ -0,0 +1,55 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Put,
UseGuards,
} from '@nestjs/common';
import { CategoryService } from './category.service';
import { CreateCategoryDto } from './dto/create-category.dto';
import { UpdateCategoryDto } from './dto/update-category.dto';
import { AuthGuard, Session } from '@thallesp/nestjs-better-auth';
import type { UserSession } from '@thallesp/nestjs-better-auth';
@Controller('categories')
export class CategoryController {
constructor(private readonly service: CategoryService) {}
@UseGuards(AuthGuard)
@Post()
async create(
@Body() dto: CreateCategoryDto,
@Session() session: UserSession,
) {
return await this.service.create(dto, session.user.id);
}
@Get('restaurant/:restaurantId')
async findAllForRestaurant(@Param('restaurantId') restaurantId: string) {
return await this.service.findAllForRestaurant(restaurantId);
}
@Get(':id')
async findOne(@Param('id') id: string) {
return await this.service.findOne(id);
}
@UseGuards(AuthGuard)
@Put(':id')
async update(
@Param('id') id: string,
@Body() dto: UpdateCategoryDto,
@Session() session: UserSession,
) {
return await this.service.update(id, dto, session.user.id);
}
@UseGuards(AuthGuard)
@Delete(':id')
async remove(@Param('id') id: string, @Session() session: UserSession) {
await this.service.remove(id, session.user.id);
}
}
+13
View File
@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { CategoryController } from './category.controller';
import { CategoryService } from './category.service';
import { RestaurantModule } from 'src/restaurant/restaurant.module';
import { PrismaModule } from 'src/prisma/prisma.module';
@Module({
imports: [RestaurantModule, PrismaModule],
controllers: [CategoryController],
providers: [CategoryService],
exports: [CategoryService],
})
export class CategoryModule {}
+71
View File
@@ -0,0 +1,71 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreateCategoryDto } from './dto/create-category.dto';
import { UpdateCategoryDto } from './dto/update-category.dto';
import { RestaurantService } from 'src/restaurant/restaurant.service';
import { CategoryDto } from './dto/category.dto';
@Injectable()
export class CategoryService {
constructor(
private prisma: PrismaService,
private readonly restaurantService: RestaurantService,
) {}
async create(dto: CreateCategoryDto, userId: string) {
await this.restaurantService.verifyOwnership(userId, dto.restaurantId);
return this.prisma.category.create({
data: {
name: dto.name,
restaurantId: dto.restaurantId,
},
});
}
async findAllForRestaurant(restaurantId: string) {
const categories = await this.prisma.category.findMany({
where: { restaurantId },
include: { items: true },
});
return categories.map(CategoryDto.fromEntity);
}
async findOne(id: string) {
const category = await this.prisma.category.findUnique({
where: { id },
include: { items: true },
});
if (!category) throw new NotFoundException('Category not found');
return CategoryDto.fromEntity(category);
}
async update(id: string, dto: UpdateCategoryDto, userId: string) {
const category = await this.findOne(id);
if (!category) {
throw new NotFoundException('Category not found');
}
await this.restaurantService.verifyOwnership(userId, category.restaurantId);
const savedCategory = await this.prisma.category.update({
where: { id },
data: {
name: dto.name,
restaurantId: dto.restaurantId,
},
});
return CategoryDto.fromEntity(savedCategory);
}
async remove(id: string, userId: string) {
const category = await this.findOne(id);
if (!category) {
throw new NotFoundException('Category not found');
}
await this.restaurantService.verifyOwnership(userId, category.restaurantId);
this.prisma.category.delete({ where: { id } });
}
}
+17
View File
@@ -0,0 +1,17 @@
import { MenuItemDto } from "src/item/dto/menu-item.dto";
export class CategoryDto {
id: string;
name: string;
restaurantId: string;
items?: MenuItemDto[];
static fromEntity(entity: any): CategoryDto {
return {
id: entity.id,
name: entity.name,
restaurantId: entity.restaurantId,
items: entity.items?.map(MenuItemDto.fromEntity) ?? [],
};
}
}
+11
View File
@@ -0,0 +1,11 @@
import { IsNotEmpty, IsString, IsUUID } from 'class-validator';
export class CreateCategoryDto {
@IsNotEmpty()
@IsString()
name: string;
@IsNotEmpty()
@IsUUID()
restaurantId: string;
}
+4
View File
@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateCategoryDto } from './create-category.dto';
export class UpdateCategoryDto extends PartialType(CreateCategoryDto) {}
+33
View File
@@ -0,0 +1,33 @@
import {
IsNotEmpty,
IsNumber,
IsOptional,
IsString,
IsUUID,
} from 'class-validator';
export class CreateItemDto {
@IsNotEmpty()
@IsString()
name: string;
@IsOptional()
@IsString()
description?: string;
@IsNotEmpty()
@IsNumber()
price: number;
@IsOptional()
@IsString()
image?: string;
@IsOptional()
@IsUUID()
categoryId?: string;
@IsNotEmpty()
@IsUUID()
restaurantId: string;
}
+23
View File
@@ -0,0 +1,23 @@
export class MenuItemDto {
id: string;
name: string;
description?: string;
price: number;
image?: string;
categoryId?: string;
restaurantId: string;
createdAt?: Date;
static fromEntity(entity: any): MenuItemDto {
return {
id: entity.id,
name: entity.name,
description: entity.description ?? undefined,
price: entity.price,
image: entity.image ?? undefined,
categoryId: entity.categoryId ?? undefined,
restaurantId: entity.restaurantId,
createdAt: entity.createdAt ? new Date(entity.createdAt) : undefined,
};
}
}
+4
View File
@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateItemDto } from './create-item.dto';
export class UpdateItemDto extends PartialType(CreateItemDto) {}
+58
View File
@@ -0,0 +1,58 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Put,
UseGuards,
} from '@nestjs/common';
import { ItemService } from './item.service';
import { CreateItemDto } from './dto/create-item.dto';
import { UpdateItemDto } from './dto/update-item.dto';
import { MenuItemDto } from './dto/menu-item.dto';
import { AuthGuard, Session } from '@thallesp/nestjs-better-auth';
import type { UserSession } from '@thallesp/nestjs-better-auth';
@Controller('items')
export class ItemController {
constructor(private readonly service: ItemService) {}
@UseGuards(AuthGuard)
@Post()
async create(@Body() dto: CreateItemDto, @Session() session: UserSession) {
const entity = await this.service.create(dto, session.user.id);
return MenuItemDto.fromEntity(entity);
}
@Get('restaurant/:restaurantId')
async findAllForRestaurant(@Param('restaurantId') restaurantId: string) {
const items = await this.service.findAllForRestaurant(restaurantId);
return items.map(MenuItemDto.fromEntity);
}
@Get(':id')
async findOne(@Param('id') id: string) {
const entity = await this.service.findOne(id);
return MenuItemDto.fromEntity(entity);
}
@UseGuards(AuthGuard)
@Put(':id')
async update(
@Param('id') id: string,
@Body() dto: UpdateItemDto,
@Session() session: UserSession,
) {
const entity = await this.service.update(id, dto, session.user.id);
return MenuItemDto.fromEntity(entity);
}
@UseGuards(AuthGuard)
@Delete(':id')
async remove(@Param('id') id: string, @Session() session: UserSession) {
await this.service.remove(id, session.user.id);
return { id };
}
}
+12
View File
@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { ItemController } from './item.controller';
import { ItemService } from './item.service';
import { RestaurantModule } from 'src/restaurant/restaurant.module';
import { PrismaModule } from 'src/prisma/prisma.module';
@Module({
imports: [RestaurantModule, PrismaModule],
controllers: [ItemController],
providers: [ItemService],
})
export class ItemModule {}
+80
View File
@@ -0,0 +1,80 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreateItemDto } from './dto/create-item.dto';
import { UpdateItemDto } from './dto/update-item.dto';
import { RestaurantService } from 'src/restaurant/restaurant.service';
import { MenuItemDto } from './dto/menu-item.dto';
@Injectable()
export class ItemService {
constructor(
private prisma: PrismaService,
private readonly restaurantService: RestaurantService,
) {}
async create(dto: CreateItemDto, userId: string) {
await this.restaurantService.verifyOwnership(userId, dto.restaurantId);
const item = await this.prisma.menuItem.create({
data: {
name: dto.name,
description: dto.description,
price: dto.price,
image: dto.image,
categoryId: dto.categoryId ?? null,
restaurantId: dto.restaurantId,
},
});
return MenuItemDto.fromEntity(item);
}
async findAllForRestaurant(restaurantId: string) {
const items = await this.prisma.menuItem.findMany({
where: { restaurantId },
orderBy: { createdAt: 'desc' },
});
return items.map(MenuItemDto.fromEntity);
}
async findOne(id: string) {
const item = await this.prisma.menuItem.findUnique({ where: { id } });
if (!item) throw new NotFoundException('Menu item not found');
return MenuItemDto.fromEntity(item);
}
async update(id: string, dto: UpdateItemDto, userId: string) {
const item = await this.findOne(id);
if (!item) {
throw new NotFoundException('Menu item not found');
}
await this.restaurantService.verifyOwnership(userId, item.restaurantId);
const updatedItem = await this.prisma.menuItem.update({
where: { id },
data: {
name: dto.name,
description: dto.description,
price: dto.price,
image: dto.image,
categoryId: dto.categoryId ?? undefined,
restaurantId: dto.restaurantId ?? undefined,
},
});
return MenuItemDto.fromEntity(updatedItem);
}
async remove(id: string, userId: string) {
const item = await this.findOne(id);
if (!item) {
throw new NotFoundException('Menu item not found');
}
await this.restaurantService.verifyOwnership(userId, item.restaurantId);
return this.prisma.menuItem.delete({ where: { id } });
}
}
+10
View File
@@ -0,0 +1,10 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
bodyParser: false,
});
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
+8
View File
@@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}
+16
View File
@@ -0,0 +1,16 @@
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService
extends PrismaClient
implements OnModuleInit, OnModuleDestroy
{
async onModuleInit() {
await this.$connect();
}
async onModuleDestroy() {
await this.$disconnect();
}
}
@@ -0,0 +1,23 @@
import { IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator';
export class CreateRestaurantDto {
@IsNotEmpty()
@IsString()
name: string;
@IsNotEmpty()
@IsString()
slug: string;
@IsOptional()
@IsString()
description?: string;
@IsOptional()
@IsString()
logo?: string;
@IsOptional()
@IsUUID()
template?: string;
}
+23
View File
@@ -0,0 +1,23 @@
import { CategoryDto } from "src/category/dto/category.dto";
export class RestaurantDto {
id: string;
name: string;
slug: string;
description?: string;
logo?: string;
categories?: CategoryDto[];
createdAt?: Date;
static fromEntity(entity: any): RestaurantDto {
return {
id: entity.id,
name: entity.name,
slug: entity.slug,
description: entity.description ?? undefined,
logo: entity.logo ?? undefined,
categories: entity.categories?.map(CategoryDto.fromEntity) ?? [],
createdAt: entity.createdAt ? new Date(entity.createdAt) : undefined,
};
}
}
@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateRestaurantDto } from './create-restaurant.dto';
export class UpdateRestaurantDto extends PartialType(CreateRestaurantDto) {}
+69
View File
@@ -0,0 +1,69 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Put,
Request,
UseGuards,
} from '@nestjs/common';
import { RestaurantService } from './restaurant.service';
import { CreateRestaurantDto } from './dto/create-restaurant.dto';
import { UpdateRestaurantDto } from './dto/update-restaurant.dto';
import { AuthGuard, Session } from '@thallesp/nestjs-better-auth';
import type { UserSession } from '@thallesp/nestjs-better-auth';
@Controller('restaurants')
export class RestaurantController {
constructor(private readonly service: RestaurantService) {}
@UseGuards(AuthGuard)
@Post()
async create(
@Body() dto: CreateRestaurantDto,
@Session() session: UserSession,
) {
const ownerId = session.user.id;
return await this.service.create(ownerId, dto);
}
@UseGuards(AuthGuard)
@Get('my')
async myRestaurants(@Session() session) {
return await this.service.findByOwner(session.user.id);
}
@Get()
async findAll() {
return await this.service.findAll();
}
@Get(':id')
async findOne(@Param('id') id: string) {
return await this.service.findOne(id);
}
@UseGuards(AuthGuard)
@Put(':id')
async update(
@Param('id') id: string,
@Body() dto: UpdateRestaurantDto,
@Session() session: UserSession,
) {
return await this.service.update(id, dto, session.user.id);
}
@UseGuards(AuthGuard)
@Delete(':id')
async remove(@Param('id') id: string, @Session() session: UserSession) {
await this.service.remove(id, session.user.id);
}
// public menu by slug
@Get('/public/:slug')
async publicBySlug(@Param('slug') slug: string) {
return await this.service.findBySlug(slug);
}
}
+12
View File
@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { RestaurantService } from './restaurant.service';
import { RestaurantController } from './restaurant.controller';
import { PrismaModule } from 'src/prisma/prisma.module';
@Module({
imports: [PrismaModule],
providers: [RestaurantService],
controllers: [RestaurantController],
exports: [RestaurantService],
})
export class RestaurantModule {}
+106
View File
@@ -0,0 +1,106 @@
import {
ForbiddenException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { CreateRestaurantDto } from './dto/create-restaurant.dto';
import { UpdateRestaurantDto } from './dto/update-restaurant.dto';
import { PrismaService } from 'src/prisma/prisma.service';
import { RestaurantDto } from './dto/restaurant.dto';
@Injectable()
export class RestaurantService {
constructor(private prisma: PrismaService) {}
async verifyOwnership(userId: string, restaurantId: string) {
const restaurant = await this.prisma.restaurant.findFirst({
where: { id: restaurantId, ownerId: userId },
});
if (!restaurant) {
throw new ForbiddenException('You do not own this restaurant');
}
}
async create(ownerId: string, dto: CreateRestaurantDto) {
const restaurant = await this.prisma.restaurant.create({
data: {
name: dto.name,
slug: dto.slug,
description: dto.description,
logo: dto.logo,
ownerId,
template: dto.template ?? null,
},
});
return RestaurantDto.fromEntity(restaurant);
}
async findByOwner(ownerId: string) {
const restaurants = await this.prisma.restaurant.findMany({
where: { ownerId },
});
return restaurants.map(RestaurantDto.fromEntity);
}
async findAll() {
const restaurants = await this.prisma.restaurant.findMany();
return restaurants.map(RestaurantDto.fromEntity);
}
async findOne(id: string) {
const restaurant = await this.prisma.restaurant.findUnique({
where: { id },
});
if (!restaurant) throw new NotFoundException('Restaurant not found');
return RestaurantDto.fromEntity(restaurant);
}
// find by slug with nested categories + items + template
async findBySlug(slug: string) {
const restaurant = await this.prisma.restaurant.findUnique({
where: { slug },
include: {
categories: {
include: { items: true },
orderBy: { name: 'asc' },
},
},
});
if (!restaurant) throw new NotFoundException('Restaurant not found');
return RestaurantDto.fromEntity(restaurant);
}
async update(id: string, dto: UpdateRestaurantDto, userId: string) {
const restaurant = await this.findOne(id);
if (!restaurant) {
throw new NotFoundException('Restaurant not found');
}
await this.verifyOwnership(userId, id);
return this.prisma.restaurant.update({
where: { id },
data: {
name: dto.name,
slug: dto.slug,
description: dto.description,
logo: dto.logo,
template: dto.template ?? undefined,
},
});
}
async remove(id: string, userId: string) {
const restaurant = await this.findOne(id);
if (!restaurant) throw new NotFoundException('Restaurant not found');
await this.verifyOwnership(userId, id);
return this.prisma.restaurant.delete({ where: { id } });
}
}
+48
View File
@@ -0,0 +1,48 @@
import { Controller, Get, Param, Delete, UseGuards } from '@nestjs/common';
import { AuthGuard, Session } from '@thallesp/nestjs-better-auth';
import type { UserSession } from '@thallesp/nestjs-better-auth';
import { UserService } from './user.service';
@Controller('users')
@UseGuards(AuthGuard)
export class UserController {
constructor(private readonly userService: UserService) {}
@Get()
async getAllUsers() {
return this.userService.getAllUsers();
}
@Get(':id')
async getUserById(@Param('id') id: string) {
return this.userService.getUserById(id);
}
@Delete(':id')
async deleteUser(@Param('id') id: string) {
return this.userService.deleteUser(id);
}
@Get(':id/sessions')
async getSessionsByUser(@Param('id') id: string) {
return this.userService.getSessionsByUserId(id);
}
@Delete(':id/sessions/:sessionId')
async removeSession(
@Param('id') userId: string,
@Param('sessionId') sessionId: string,
) {
return this.userService.removeSession(userId, sessionId);
}
@Delete(':id/sessions')
async removeAllSessions(@Param('id') id: string) {
return this.userService.removeAllSessions(id);
}
@Get('session')
async getSession(@Session() session: UserSession) {
return session;
}
}
+11
View File
@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { UserController } from './user.controller';
import { UserService } from './user.service';
import { PrismaModule } from 'src/prisma/prisma.module';
@Module({
imports: [PrismaModule],
controllers: [UserController],
providers: [UserService],
})
export class UserModule {}
+52
View File
@@ -0,0 +1,52 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from 'src/prisma/prisma.service';
@Injectable()
export class UserService {
constructor(private readonly prisma: PrismaService) {}
async getAllUsers() {
return this.prisma.user.findMany({
include: { accounts: true, sessions: true, restaurants: true },
});
}
async getUserById(id: string) {
const user = await this.prisma.user.findUnique({
where: { id },
include: { accounts: true, sessions: true, restaurants: true },
});
if (!user) throw new NotFoundException('User not found');
return user;
}
async deleteUser(id: string) {
// Delete related sessions, accounts, and restaurants first (cascade-like behavior)
await this.prisma.session.deleteMany({ where: { userId: id } });
await this.prisma.account.deleteMany({ where: { userId: id } });
await this.prisma.restaurant.deleteMany({ where: { ownerId: id } });
return this.prisma.user.delete({ where: { id } });
}
async getSessionsByUserId(id: string) {
const sessions = await this.prisma.session.findMany({
where: { userId: id },
});
if (sessions.length === 0)
throw new NotFoundException('No sessions found for this user');
return sessions;
}
async removeSession(userId: string, sessionId: string) {
const session = await this.prisma.session.findFirst({
where: { id: sessionId, userId },
});
if (!session) throw new NotFoundException('Session not found');
return this.prisma.session.delete({ where: { id: sessionId } });
}
async removeAllSessions(userId: string) {
await this.prisma.session.deleteMany({ where: { userId } });
return { message: 'All user sessions removed' };
}
}