diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index e3dad7f..7350b75 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -1,8 +1,15 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ConfigModule, ConfigService } from '@nestjs/config'; +import { ThrottlerModule } from '@nestjs/throttler'; import { AppController } from './app.controller'; import { AppService } from './app.service'; +import { AuthModule } from './modules/auth/auth.module'; +import { AdminModule } from './modules/admin/admin.module'; +import { AnalyticsModule } from './modules/analytics/analytics.module'; +import { ContactModule } from './modules/contact/contact.module'; +import { InstagramModule } from './modules/instagram/instagram.module'; +import { CloudinaryModule } from './modules/cloudinary/cloudinary.module'; @Module({ imports: [ @@ -12,6 +19,14 @@ import { AppService } from './app.service'; envFilePath: '.env', }), + // Rate Limiting + ThrottlerModule.forRoot([ + { + ttl: +(process.env.THROTTLE_TTL || 60) * 1000, + limit: +(process.env.THROTTLE_LIMIT || 100), + }, + ]), + // Database TypeOrmModule.forRootAsync({ imports: [ConfigModule], @@ -33,6 +48,14 @@ import { AppService } from './app.service'; }; }, }), + + // Feature Modules + AuthModule, + AdminModule, + AnalyticsModule, + ContactModule, + InstagramModule, + CloudinaryModule, ], controllers: [AppController], providers: [AppService], diff --git a/backend/src/harouns-ux/admin-user.entity.ts b/backend/src/harouns-ux/admin-user.entity.ts deleted file mode 100644 index 097914c..0000000 --- a/backend/src/harouns-ux/admin-user.entity.ts +++ /dev/null @@ -1,70 +0,0 @@ -// #81 – Backend — Admin User Entity -import { - Entity, - PrimaryGeneratedColumn, - Column, - CreateDateColumn, - UpdateDateColumn, - BeforeInsert, - BeforeUpdate, - ManyToOne, - JoinColumn -} from 'typeorm'; -import * as bcrypt from 'bcrypt'; -import { Restaurant } from '../modules/restaurant/restaurant.entity'; - -export enum AdminRole { - SUPER_ADMIN = 'super_admin', - ADMIN = 'admin', - MANAGER = 'manager', - STAFF = 'staff' -} - -@Entity('admin_users') -export class AdminUser { - @PrimaryGeneratedColumn('uuid') - id: string; - - @Column({ type: 'varchar', length: 100, unique: true }) - username: string; - - @Column({ type: 'varchar', length: 255 }) - passwordHash: string; - - @Column({ type: 'varchar', length: 255, unique: true }) - email: string; - - @Column({ type: 'enum', enum: AdminRole, default: AdminRole.MANAGER }) - role: AdminRole; - - @Column({ type: 'boolean', default: true }) - isActive: boolean; - - @Column({ type: 'timestamp', nullable: true }) - lastLoginAt: Date; - - @Column({ type: 'uuid' }) - restaurantId: string; - - @ManyToOne(() => Restaurant, (restaurant) => restaurant.users, { onDelete: 'CASCADE' }) - @JoinColumn({ name: 'restaurantId' }) - restaurant: Restaurant; - - @CreateDateColumn() - createdAt: Date; - - @UpdateDateColumn() - updatedAt: Date; - - @BeforeInsert() - @BeforeUpdate() - async hashPassword() { - if (this.passwordHash && !this.passwordHash.startsWith('$2')) { - this.passwordHash = await bcrypt.hash(this.passwordHash, 10); - } - } - - async validatePassword(password: string): Promise { - return bcrypt.compare(password, this.passwordHash); - } -} diff --git a/backend/src/harouns-ux/admin-users.module.ts b/backend/src/harouns-ux/admin-users.module.ts deleted file mode 100644 index 8a4387b..0000000 --- a/backend/src/harouns-ux/admin-users.module.ts +++ /dev/null @@ -1,26 +0,0 @@ -// #83 – Backend — Admin Users Module -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { AdminController } from '../modules/admin/admin.controller'; -import { AdminService } from '../modules/admin/admin.service'; -import { AuditLog } from '../modules/admin/audit-log.entity'; -import { AdminUser } from './admin-user.entity'; -import { MenuItem } from '../modules/menu/menu-item.entity'; -import { ContactSubmission } from '../modules/contact/contact-submission.entity'; -import { AnalyticsEvent } from '../modules/analytics/analytics-event.entity'; - -@Module({ - imports: [ - TypeOrmModule.forFeature([ - AdminUser, - AuditLog, - MenuItem, - ContactSubmission, - AnalyticsEvent - ]) - ], - controllers: [AdminController], - providers: [AdminService], - exports: [AdminService] -}) -export class AdminUsersModule {} diff --git a/backend/src/harouns-ux/auth.module.ts b/backend/src/harouns-ux/auth.module.ts deleted file mode 100644 index d159672..0000000 --- a/backend/src/harouns-ux/auth.module.ts +++ /dev/null @@ -1,32 +0,0 @@ -// #82 – Backend — Auth Module -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { JwtModule } from '@nestjs/jwt'; -import { PassportModule } from '@nestjs/passport'; -import { ConfigModule, ConfigService } from '@nestjs/config'; -import { AuthController } from '../modules/auth/auth.controller'; -import { AuthService } from '../modules/auth/auth.service'; -import { JwtStrategy } from '../modules/auth/jwt.strategy'; -import { AdminUser } from './admin-user.entity'; -import { Restaurant } from '../modules/restaurant/restaurant.entity'; - -@Module({ - imports: [ - TypeOrmModule.forFeature([AdminUser, Restaurant]), - PassportModule, - JwtModule.registerAsync({ - imports: [ConfigModule], - inject: [ConfigService], - useFactory: (configService: ConfigService) => ({ - secret: configService.get('JWT_SECRET') || 'your-secret-key', - signOptions: { - expiresIn: configService.get('JWT_EXPIRES_IN') || '1d' - } - }) - }) - ], - controllers: [AuthController], - providers: [AuthService, JwtStrategy], - exports: [AuthService] -}) -export class AuthModule {} diff --git a/backend/src/modules/admin/admin.service.ts b/backend/src/modules/admin/admin.service.ts index 50b5181..ef79810 100644 --- a/backend/src/modules/admin/admin.service.ts +++ b/backend/src/modules/admin/admin.service.ts @@ -48,7 +48,7 @@ export class AdminService { // Prevent creating another SUPER_ADMIN if (createUserDto.role === AdminRole.SUPER_ADMIN) { - throw new BadRequestException('Cannot create another super admin'); + throw new ForbiddenException('Cannot create another super admin'); } // Check if username already exists in this restaurant @@ -86,14 +86,6 @@ export class AdminService { await this.adminUserRepository.save(user); - // const savedUser = await this.adminUserRepository.save(user); - - // // Reload user with restaurant relation to get restaurant details - // const userWithRestaurant = await this.adminUserRepository.findOne({ - // where: { id: savedUser.id }, - // relations: ['restaurant'], - // }); - return { message: 'User created successfully', user: { @@ -153,12 +145,12 @@ export class AdminService { // Prevent toggling own status if (admin.id === requesterId) { - throw new BadRequestException('Cannot toggle your own status'); + throw new ForbiddenException('Cannot toggle your own status'); } // Prevent toggling SUPER_ADMIN status if (admin.role === AdminRole.SUPER_ADMIN) { - throw new BadRequestException('Cannot toggle super admin status'); + throw new ForbiddenException('Cannot toggle super admin status'); } await this.adminUserRepository.update(adminId, { isActive }); @@ -185,12 +177,12 @@ export class AdminService { // Prevent assigning SUPER_ADMIN role if (role === AdminRole.SUPER_ADMIN) { - throw new BadRequestException('Cannot assign super admin role'); + throw new ForbiddenException('Cannot assign super admin role'); } // Find admin in this restaurant const admin = await this.adminUserRepository.findOne({ - where: { id: adminId, restaurantId }, // ADD restaurantId filter + where: { id: adminId, restaurantId }, }); if (!admin) { @@ -199,12 +191,12 @@ export class AdminService { // Prevent changing SUPER_ADMIN role if (admin.role === AdminRole.SUPER_ADMIN) { - throw new BadRequestException('Cannot change Super Admin role'); + throw new ForbiddenException('Cannot change Super Admin role'); } // Prevent changing own role if (admin.id === requesterId) { - throw new BadRequestException('Cannot change your own role'); + throw new ForbiddenException('Cannot change your own role'); } await this.adminUserRepository.update(adminId, { role: role as AdminRole }); diff --git a/backend/src/modules/auth/auth.controller.ts b/backend/src/modules/auth/auth.controller.ts index a31f398..55781a0 100644 --- a/backend/src/modules/auth/auth.controller.ts +++ b/backend/src/modules/auth/auth.controller.ts @@ -10,6 +10,7 @@ import { HttpStatus, Patch, } from '@nestjs/common'; +import { Throttle } from '@nestjs/throttler'; import { AuthService } from './auth.service'; import { LoginDto, RegisterAdminDto, ChangePasswordDto } from './auth.dto'; import { JwtAuthGuard } from './jwt-auth.guard'; @@ -21,12 +22,14 @@ export class AuthController { @Post('login') @HttpCode(HttpStatus.OK) + @Throttle({ default: { limit: 5, ttl: 60000 } }) async login(@Body() loginDto: LoginDto) { return await this.authService.login(loginDto); } @Post('register') @HttpCode(HttpStatus.CREATED) + @Throttle({ default: { limit: 3, ttl: 60000 } }) async register(@Body() registerAdminDto: RegisterAdminDto) { return await this.authService.register(registerAdminDto); } diff --git a/backend/src/modules/auth/auth.dto.ts b/backend/src/modules/auth/auth.dto.ts index 7025b41..5b60c66 100644 --- a/backend/src/modules/auth/auth.dto.ts +++ b/backend/src/modules/auth/auth.dto.ts @@ -1,12 +1,21 @@ // backend/src/modules/auth/auth.dto.ts -import { IsString, IsEmail, MinLength, MaxLength } from 'class-validator'; +import { + IsString, + IsEmail, + MinLength, + MaxLength, + Matches, +} from 'class-validator'; +import { Transform } from 'class-transformer'; export class LoginDto { @IsString() + @Transform(({ value }) => value?.trim()) username: string; @IsString() - @MinLength(6) + @MinLength(8) + @MaxLength(128) password: string; } @@ -14,34 +23,48 @@ export class RegisterAdminDto { @IsString() @MinLength(3) @MaxLength(100) + @Matches(/^[a-zA-Z0-9_-]+$/, { + message: 'Username can only contain letters, numbers, hyphens, and underscores', + }) + @Transform(({ value }) => value?.trim()) username: string; @IsEmail() + @Transform(({ value }) => value?.trim().toLowerCase()) email: string; @IsString() @MinLength(8) + @MaxLength(128) password: string; @IsString() @MinLength(3) @MaxLength(255) + @Transform(({ value }) => value?.trim()) restaurantName: string; @IsString() @MinLength(10) @MaxLength(20) + @Matches(/^[0-9+\-() ]+$/, { + message: 'Phone number can only contain digits, +, -, (, ) and spaces', + }) restaurantPhone: string; @IsEmail() + @Transform(({ value }) => value?.trim().toLowerCase()) restaurantEmail: string; } export class ChangePasswordDto { @IsString() + @MinLength(8) + @MaxLength(128) currentPassword: string; @IsString() @MinLength(8) + @MaxLength(128) newPassword: string; } diff --git a/backend/src/modules/auth/auth.module.ts b/backend/src/modules/auth/auth.module.ts index f89bdc2..4026df5 100644 --- a/backend/src/modules/auth/auth.module.ts +++ b/backend/src/modules/auth/auth.module.ts @@ -18,7 +18,7 @@ import { Restaurant } from '../restaurant/restaurant.entity'; imports: [ConfigModule], inject: [ConfigService], useFactory: (configService: ConfigService) => ({ - secret: configService.get('JWT_SECRET') || 'your-secret-key', + secret: configService.getOrThrow('JWT_SECRET'), signOptions: { expiresIn: configService.get('JWT_EXPIRES_IN') || '1d', }, diff --git a/backend/src/modules/auth/auth.service.ts b/backend/src/modules/auth/auth.service.ts index eefe05d..549ae92 100644 --- a/backend/src/modules/auth/auth.service.ts +++ b/backend/src/modules/auth/auth.service.ts @@ -7,7 +7,7 @@ import { } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { DataSource, Repository } from 'typeorm'; import { AdminUser, AdminRole } from './admin-user.entity'; import { LoginDto, RegisterAdminDto, ChangePasswordDto } from './auth.dto'; import { UpdateRegisterAdminDto } from './update-auth.dto'; @@ -22,6 +22,7 @@ export class AuthService { private readonly jwtService: JwtService, @InjectRepository(Restaurant) private readonly restaurantRepository: Repository, + private readonly dataSource: DataSource, ) {} async login(loginDto: LoginDto) { @@ -38,8 +39,8 @@ export class AuthService { throw new UnauthorizedException('Account is deactivated'); } - // Check if restaurant is active - if (!admin.restaurant.isActive) { + // Check if restaurant exists and is active + if (!admin.restaurant || !admin.restaurant.isActive) { throw new UnauthorizedException('Restaurant account is deactivated'); } @@ -107,50 +108,64 @@ export class AuthService { ); } - // Create restaurant first - const restaurant = this.restaurantRepository.create({ - name: registerAdminDto.restaurantName, - slug, - phone: registerAdminDto.restaurantPhone, - email: registerAdminDto.restaurantEmail, - whatsappNumber: registerAdminDto.restaurantPhone, - }); + // Use transaction to ensure restaurant + admin are created atomically + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); - await this.restaurantRepository.save(restaurant); + try { + // Create restaurant first + const restaurant = this.restaurantRepository.create({ + name: registerAdminDto.restaurantName, + slug, + phone: registerAdminDto.restaurantPhone, + email: registerAdminDto.restaurantEmail, + whatsappNumber: registerAdminDto.restaurantPhone, + }); - // Create admin user with SUPER_ADMIN role and link to restaurant - const admin = this.adminUserRepository.create({ - username: registerAdminDto.username, - email: registerAdminDto.email, - passwordHash: registerAdminDto.password, // Will be hashed by @BeforeInsert - role: AdminRole.SUPER_ADMIN, - restaurantId: restaurant.id, - }); + await queryRunner.manager.save(restaurant); - await this.adminUserRepository.save(admin); + // Create admin user with SUPER_ADMIN role and link to restaurant + const admin = this.adminUserRepository.create({ + username: registerAdminDto.username, + email: registerAdminDto.email, + passwordHash: registerAdminDto.password, // Will be hashed by @BeforeInsert + role: AdminRole.SUPER_ADMIN, + restaurantId: restaurant.id, + }); - const payload = { - sub: admin.id, - username: admin.username, - role: admin.role, - restaurantId: restaurant.id, - }; + await queryRunner.manager.save(admin); - return { - accessToken: this.jwtService.sign(payload), - user: { - id: admin.id, + await queryRunner.commitTransaction(); + + const payload = { + sub: admin.id, username: admin.username, - email: admin.email, role: admin.role, - lastLoginAt: admin.lastLoginAt, - createdAt: admin.createdAt, - isActive: admin.isActive, restaurantId: restaurant.id, - restaurantName: restaurant.name, - restaurantSlug: restaurant.slug, - }, - }; + }; + + return { + accessToken: this.jwtService.sign(payload), + user: { + id: admin.id, + username: admin.username, + email: admin.email, + role: admin.role, + lastLoginAt: admin.lastLoginAt, + createdAt: admin.createdAt, + isActive: admin.isActive, + restaurantId: restaurant.id, + restaurantName: restaurant.name, + restaurantSlug: restaurant.slug, + }, + }; + } catch (error) { + await queryRunner.rollbackTransaction(); + throw error; + } finally { + await queryRunner.release(); + } } async changePassword( @@ -188,31 +203,12 @@ export class AuthService { }); } - // async createDefaultAdmin() { - // const existingAdmin = await this.adminUserRepository.findOne({ - // where: { username: process.env.ADMIN_DEFAULT_USERNAME || 'admin' }, - // }); - - // if (!existingAdmin) { - // const admin = this.adminUserRepository.create({ - // username: process.env.ADMIN_DEFAULT_USERNAME || 'admin', - // email: process.env.ADMIN_DEFAULT_EMAIL || 'admin@restaurant.com', - // passwordHash: process.env.ADMIN_DEFAULT_PASSWORD || 'changeme123', - // role: AdminRole.ADMIN, - // }); - - // await this.adminUserRepository.save(admin); - // console.log('Default admin user created'); - // } - // } - async updateUserProfile( id: string, updateUserDto: UpdateRegisterAdminDto, restaurantId: string, ) { try { - // check if user exists const existingUser = await this.adminUserRepository.findOne({ where: { id, restaurantId }, }); @@ -221,7 +217,6 @@ export class AuthService { throw new NotFoundException('User not found'); } - // Use preload to properly merge the updates with the existing entity const userToUpdate = await this.adminUserRepository.preload({ id: id, ...updateUserDto, @@ -231,19 +226,13 @@ export class AuthService { throw new NotFoundException('User not found'); } - // save the updated user const updatedUser = await this.adminUserRepository.save(userToUpdate); - // Explicitly fetch the updated user with relations to ensure we get the correct data - const finalUser = await this.adminUserRepository.findOne({ - where: { id: updatedUser.id }, - }); - return { user: { - id: finalUser.id, - username: finalUser.username, - email: finalUser.email, + id: updatedUser.id, + username: updatedUser.username, + email: updatedUser.email, }, }; } catch (error) { diff --git a/backend/src/modules/auth/jwt.strategy.ts b/backend/src/modules/auth/jwt.strategy.ts index 8f0d5f8..262d9b1 100644 --- a/backend/src/modules/auth/jwt.strategy.ts +++ b/backend/src/modules/auth/jwt.strategy.ts @@ -1,5 +1,6 @@ // backend/src/modules/auth/jwt.strategy.ts import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { PassportStrategy } from '@nestjs/passport'; import { ExtractJwt, Strategy } from 'passport-jwt'; import { InjectRepository } from '@nestjs/typeorm'; @@ -11,11 +12,12 @@ export class JwtStrategy extends PassportStrategy(Strategy) { constructor( @InjectRepository(AdminUser) private readonly adminUserRepository: Repository, + configService: ConfigService, ) { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), ignoreExpiration: false, - secretOrKey: process.env.JWT_SECRET || 'your-secret-key', + secretOrKey: configService.getOrThrow('JWT_SECRET'), }); } @@ -29,7 +31,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) { throw new UnauthorizedException(); } - if (!admin.restaurant.isActive) { + if (!admin.restaurant || !admin.restaurant.isActive) { throw new UnauthorizedException('Restaurant account is deactivated'); } diff --git a/backend/src/scrap-menu/about-management.controller.ts b/backend/src/scrap-menu/about-management.controller.ts deleted file mode 100644 index 82043ca..0000000 --- a/backend/src/scrap-menu/about-management.controller.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Body, Controller, Delete, Get, Param, Patch, Post, UseGuards } from '@nestjs/common'; -import { AboutManagementService } from './about-management.service'; -import { CreateAboutManagementDto } from './dto/create-about-management.dto'; -import { UpdateAboutManagementDto } from './dto/update-about-management.dto'; -import { JwtAuthGuard } from '../common/guards/jwt-auth.guard'; -import { RolesGuard } from '../common/guards/roles.guard'; -import { Role } from '../common/enums/role.enum'; -import { Roles } from '../common/decorators/roles.decorator'; - -@Controller('about') -export class AboutManagementController { - constructor(private readonly service: AboutManagementService) {} - - @Get() - findAll() { - return this.service.findAll(); - } - - @Get(':id') - findOne(@Param('id') id: string) { - return this.service.findOne(id); - } - - @Post() - @UseGuards(JwtAuthGuard, RolesGuard) - @Roles(Role.ADMIN, Role.MODERATOR, Role.TUTOR) - create(@Body() payload: CreateAboutManagementDto) { - return this.service.create(payload); - } - - @Patch(':id') - @UseGuards(JwtAuthGuard, RolesGuard) - @Roles(Role.ADMIN, Role.MODERATOR, Role.TUTOR) - update(@Param('id') id: string, @Body() payload: UpdateAboutManagementDto) { - return this.service.update(id, payload); - } - - @Delete(':id') - @UseGuards(JwtAuthGuard, RolesGuard) - @Roles(Role.ADMIN, Role.MODERATOR) - remove(@Param('id') id: string) { - return this.service.remove(id); - } -} diff --git a/backend/src/scrap-menu/about-management.module.ts b/backend/src/scrap-menu/about-management.module.ts deleted file mode 100644 index ac64e24..0000000 --- a/backend/src/scrap-menu/about-management.module.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Module } from '@nestjs/common'; -import { AboutManagementController } from './about-management.controller'; -import { AboutManagementService } from './about-management.service'; - -@Module({ - controllers: [AboutManagementController], - providers: [AboutManagementService], -}) -export class AboutManagementModule {} diff --git a/backend/src/scrap-menu/about-management.service.ts b/backend/src/scrap-menu/about-management.service.ts deleted file mode 100644 index 3e72a4f..0000000 --- a/backend/src/scrap-menu/about-management.service.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; -import { CreateAboutManagementDto } from './dto/create-about-management.dto'; -import { UpdateAboutManagementDto } from './dto/update-about-management.dto'; - -@Injectable() -export class AboutManagementService { - private readonly items: Array<{ id: string } & CreateAboutManagementDto> = []; - - findAll() { - return this.items; - } - - findOne(id: string) { - const item = this.items.find((entry) => entry.id === id); - if (!item) { - throw new NotFoundException('AboutManagement item not found'); - } - return item; - } - - create(payload: CreateAboutManagementDto) { - const created = { id: crypto.randomUUID(), ...payload }; - this.items.push(created); - return created; - } - - update(id: string, payload: UpdateAboutManagementDto) { - const item = this.findOne(id); - Object.assign(item, payload); - return item; - } - - remove(id: string) { - const index = this.items.findIndex((entry) => entry.id === id); - if (index === -1) { - throw new NotFoundException('AboutManagement item not found'); - } - this.items.splice(index, 1); - return { id, deleted: true }; - } -} diff --git a/backend/src/scrap-menu/admin-financial-aid-management.controller.ts b/backend/src/scrap-menu/admin-financial-aid-management.controller.ts deleted file mode 100644 index 01d60ca..0000000 --- a/backend/src/scrap-menu/admin-financial-aid-management.controller.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Body, Controller, Delete, Get, Param, Patch, Post, UseGuards } from '@nestjs/common'; -import { AdminFinancialAidManagementService } from './admin-financial-aid-management.service'; -import { CreateAdminFinancialAidManagementDto } from './dto/create-admin-financial-aid-management.dto'; -import { UpdateAdminFinancialAidManagementDto } from './dto/update-admin-financial-aid-management.dto'; -import { JwtAuthGuard } from '../common/guards/jwt-auth.guard'; -import { RolesGuard } from '../common/guards/roles.guard'; -import { Role } from '../common/enums/role.enum'; -import { Roles } from '../common/decorators/roles.decorator'; - -@Controller('admin/financial-aid') -export class AdminFinancialAidManagementController { - constructor(private readonly service: AdminFinancialAidManagementService) {} - - @Get() - findAll() { - return this.service.findAll(); - } - - @Get(':id') - findOne(@Param('id') id: string) { - return this.service.findOne(id); - } - - @Post() - @UseGuards(JwtAuthGuard, RolesGuard) - @Roles(Role.ADMIN, Role.MODERATOR, Role.TUTOR) - create(@Body() payload: CreateAdminFinancialAidManagementDto) { - return this.service.create(payload); - } - - @Patch(':id') - @UseGuards(JwtAuthGuard, RolesGuard) - @Roles(Role.ADMIN, Role.MODERATOR, Role.TUTOR) - update(@Param('id') id: string, @Body() payload: UpdateAdminFinancialAidManagementDto) { - return this.service.update(id, payload); - } - - @Delete(':id') - @UseGuards(JwtAuthGuard, RolesGuard) - @Roles(Role.ADMIN, Role.MODERATOR) - remove(@Param('id') id: string) { - return this.service.remove(id); - } -} diff --git a/backend/src/scrap-menu/admin-financial-aid-management.module.ts b/backend/src/scrap-menu/admin-financial-aid-management.module.ts deleted file mode 100644 index 91ef312..0000000 --- a/backend/src/scrap-menu/admin-financial-aid-management.module.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Module } from '@nestjs/common'; -import { AdminFinancialAidManagementController } from './admin-financial-aid-management.controller'; -import { AdminFinancialAidManagementService } from './admin-financial-aid-management.service'; - -@Module({ - controllers: [AdminFinancialAidManagementController], - providers: [AdminFinancialAidManagementService], -}) -export class AdminFinancialAidManagementModule {} diff --git a/backend/src/scrap-menu/admin-financial-aid-management.service.ts b/backend/src/scrap-menu/admin-financial-aid-management.service.ts deleted file mode 100644 index 121a95c..0000000 --- a/backend/src/scrap-menu/admin-financial-aid-management.service.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; -import { CreateAdminFinancialAidManagementDto } from './dto/create-admin-financial-aid-management.dto'; -import { UpdateAdminFinancialAidManagementDto } from './dto/update-admin-financial-aid-management.dto'; - -@Injectable() -export class AdminFinancialAidManagementService { - private readonly items: Array<{ id: string } & CreateAdminFinancialAidManagementDto> = []; - - findAll() { - return this.items; - } - - findOne(id: string) { - const item = this.items.find((entry) => entry.id === id); - if (!item) { - throw new NotFoundException('AdminFinancialAidManagement item not found'); - } - return item; - } - - create(payload: CreateAdminFinancialAidManagementDto) { - const created = { id: crypto.randomUUID(), ...payload }; - this.items.push(created); - return created; - } - - update(id: string, payload: UpdateAdminFinancialAidManagementDto) { - const item = this.findOne(id); - Object.assign(item, payload); - return item; - } - - remove(id: string) { - const index = this.items.findIndex((entry) => entry.id === id); - if (index === -1) { - throw new NotFoundException('AdminFinancialAidManagement item not found'); - } - this.items.splice(index, 1); - return { id, deleted: true }; - } -} diff --git a/backend/src/scrap-menu/admin-moderator-account-settings.controller.ts b/backend/src/scrap-menu/admin-moderator-account-settings.controller.ts deleted file mode 100644 index 9add5ff..0000000 --- a/backend/src/scrap-menu/admin-moderator-account-settings.controller.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Body, Controller, Delete, Get, Param, Patch, Post, UseGuards } from '@nestjs/common'; -import { AdminModeratorAccountSettingsService } from './admin-moderator-account-settings.service'; -import { CreateAdminModeratorAccountSettingsDto } from './dto/create-admin-moderator-account-settings.dto'; -import { UpdateAdminModeratorAccountSettingsDto } from './dto/update-admin-moderator-account-settings.dto'; -import { JwtAuthGuard } from '../common/guards/jwt-auth.guard'; -import { RolesGuard } from '../common/guards/roles.guard'; -import { Role } from '../common/enums/role.enum'; -import { Roles } from '../common/decorators/roles.decorator'; - -@Controller('admin-moderator/account-settings') -export class AdminModeratorAccountSettingsController { - constructor(private readonly service: AdminModeratorAccountSettingsService) {} - - @Get() - findAll() { - return this.service.findAll(); - } - - @Get(':id') - findOne(@Param('id') id: string) { - return this.service.findOne(id); - } - - @Post() - @UseGuards(JwtAuthGuard, RolesGuard) - @Roles(Role.ADMIN, Role.MODERATOR, Role.TUTOR) - create(@Body() payload: CreateAdminModeratorAccountSettingsDto) { - return this.service.create(payload); - } - - @Patch(':id') - @UseGuards(JwtAuthGuard, RolesGuard) - @Roles(Role.ADMIN, Role.MODERATOR, Role.TUTOR) - update(@Param('id') id: string, @Body() payload: UpdateAdminModeratorAccountSettingsDto) { - return this.service.update(id, payload); - } - - @Delete(':id') - @UseGuards(JwtAuthGuard, RolesGuard) - @Roles(Role.ADMIN, Role.MODERATOR) - remove(@Param('id') id: string) { - return this.service.remove(id); - } -} diff --git a/backend/src/scrap-menu/admin-moderator-account-settings.module.ts b/backend/src/scrap-menu/admin-moderator-account-settings.module.ts deleted file mode 100644 index 4ddcbc0..0000000 --- a/backend/src/scrap-menu/admin-moderator-account-settings.module.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Module } from '@nestjs/common'; -import { AdminModeratorAccountSettingsController } from './admin-moderator-account-settings.controller'; -import { AdminModeratorAccountSettingsService } from './admin-moderator-account-settings.service'; - -@Module({ - controllers: [AdminModeratorAccountSettingsController], - providers: [AdminModeratorAccountSettingsService], -}) -export class AdminModeratorAccountSettingsModule {} diff --git a/backend/src/scrap-menu/admin-moderator-account-settings.service.ts b/backend/src/scrap-menu/admin-moderator-account-settings.service.ts deleted file mode 100644 index 4baf912..0000000 --- a/backend/src/scrap-menu/admin-moderator-account-settings.service.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; -import { CreateAdminModeratorAccountSettingsDto } from './dto/create-admin-moderator-account-settings.dto'; -import { UpdateAdminModeratorAccountSettingsDto } from './dto/update-admin-moderator-account-settings.dto'; - -@Injectable() -export class AdminModeratorAccountSettingsService { - private readonly items: Array<{ id: string } & CreateAdminModeratorAccountSettingsDto> = []; - - findAll() { - return this.items; - } - - findOne(id: string) { - const item = this.items.find((entry) => entry.id === id); - if (!item) { - throw new NotFoundException('AdminModeratorAccountSettings item not found'); - } - return item; - } - - create(payload: CreateAdminModeratorAccountSettingsDto) { - const created = { id: crypto.randomUUID(), ...payload }; - this.items.push(created); - return created; - } - - update(id: string, payload: UpdateAdminModeratorAccountSettingsDto) { - const item = this.findOne(id); - Object.assign(item, payload); - return item; - } - - remove(id: string) { - const index = this.items.findIndex((entry) => entry.id === id); - if (index === -1) { - throw new NotFoundException('AdminModeratorAccountSettings item not found'); - } - this.items.splice(index, 1); - return { id, deleted: true }; - } -} diff --git a/backend/src/scrap-menu/badges-nft.controller.ts b/backend/src/scrap-menu/badges-nft.controller.ts deleted file mode 100644 index 586664e..0000000 --- a/backend/src/scrap-menu/badges-nft.controller.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Body, Controller, Delete, Get, Param, Patch, Post, UseGuards } from '@nestjs/common'; -import { BadgesNftService } from './badges-nft.service'; -import { CreateBadgesNftDto } from './dto/create-badges-nft.dto'; -import { UpdateBadgesNftDto } from './dto/update-badges-nft.dto'; -import { JwtAuthGuard } from '../common/guards/jwt-auth.guard'; -import { RolesGuard } from '../common/guards/roles.guard'; -import { Role } from '../common/enums/role.enum'; -import { Roles } from '../common/decorators/roles.decorator'; - -@Controller('badges-nft') -export class BadgesNftController { - constructor(private readonly service: BadgesNftService) {} - - @Get() - findAll() { - return this.service.findAll(); - } - - @Get(':id') - findOne(@Param('id') id: string) { - return this.service.findOne(id); - } - - @Post() - @UseGuards(JwtAuthGuard, RolesGuard) - @Roles(Role.ADMIN, Role.MODERATOR, Role.TUTOR) - create(@Body() payload: CreateBadgesNftDto) { - return this.service.create(payload); - } - - @Patch(':id') - @UseGuards(JwtAuthGuard, RolesGuard) - @Roles(Role.ADMIN, Role.MODERATOR, Role.TUTOR) - update(@Param('id') id: string, @Body() payload: UpdateBadgesNftDto) { - return this.service.update(id, payload); - } - - @Delete(':id') - @UseGuards(JwtAuthGuard, RolesGuard) - @Roles(Role.ADMIN, Role.MODERATOR) - remove(@Param('id') id: string) { - return this.service.remove(id); - } -} diff --git a/backend/src/scrap-menu/badges-nft.module.ts b/backend/src/scrap-menu/badges-nft.module.ts deleted file mode 100644 index f4e229d..0000000 --- a/backend/src/scrap-menu/badges-nft.module.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Module } from '@nestjs/common'; -import { BadgesNftController } from './badges-nft.controller'; -import { BadgesNftService } from './badges-nft.service'; - -@Module({ - controllers: [BadgesNftController], - providers: [BadgesNftService], -}) -export class BadgesNftModule {} diff --git a/backend/src/scrap-menu/badges-nft.service.ts b/backend/src/scrap-menu/badges-nft.service.ts deleted file mode 100644 index 786d5c1..0000000 --- a/backend/src/scrap-menu/badges-nft.service.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; -import { CreateBadgesNftDto } from './dto/create-badges-nft.dto'; -import { UpdateBadgesNftDto } from './dto/update-badges-nft.dto'; - -@Injectable() -export class BadgesNftService { - private readonly items: Array<{ id: string } & CreateBadgesNftDto> = []; - - findAll() { - return this.items; - } - - findOne(id: string) { - const item = this.items.find((entry) => entry.id === id); - if (!item) { - throw new NotFoundException('BadgesNft item not found'); - } - return item; - } - - create(payload: CreateBadgesNftDto) { - const created = { id: crypto.randomUUID(), ...payload }; - this.items.push(created); - return created; - } - - update(id: string, payload: UpdateBadgesNftDto) { - const item = this.findOne(id); - Object.assign(item, payload); - return item; - } - - remove(id: string) { - const index = this.items.findIndex((entry) => entry.id === id); - if (index === -1) { - throw new NotFoundException('BadgesNft item not found'); - } - this.items.splice(index, 1); - return { id, deleted: true }; - } -} diff --git a/frontend/app/(auth)/login/LoginClient.tsx b/frontend/app/(auth)/login/LoginClient.tsx index d068215..9003085 100644 --- a/frontend/app/(auth)/login/LoginClient.tsx +++ b/frontend/app/(auth)/login/LoginClient.tsx @@ -6,7 +6,7 @@ import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { Eye, EyeOff, Loader2 } from "lucide-react"; -import { adminApi } from "@/lib/api/api"; +import { adminApi } from "@/lib/api/admin"; import { useAuthStore } from "@/lib/store/authStore"; import Link from "next/link"; @@ -44,7 +44,7 @@ export default function LoginClient() { try { const response = await adminApi.login(data); - setAuth(response.user, response.token); + setAuth(response.user, response.accessToken); router.push("/admin/dashboard"); } catch (err) { setError(err instanceof Error ? err.message : "Login failed"); @@ -147,7 +147,7 @@ export default function LoginClient() {

Don't have an account?{" "} Register diff --git a/frontend/app/(public)/admin-registration/page.tsx b/frontend/app/(public)/admin-registration/page.tsx new file mode 100644 index 0000000..ad3ff3c --- /dev/null +++ b/frontend/app/(public)/admin-registration/page.tsx @@ -0,0 +1,243 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { Eye, EyeOff, Loader2 } from "lucide-react"; +import { adminApi } from "@/lib/api/admin"; +import { useAuthStore } from "@/lib/store/authStore"; +import Link from "next/link"; + +const registerSchema = z.object({ + username: z + .string() + .min(3, "Username must be at least 3 characters") + .max(100) + .regex( + /^[a-zA-Z0-9_-]+$/, + "Username can only contain letters, numbers, hyphens, and underscores" + ), + email: z.string().email("Valid email required"), + password: z.string().min(8, "Password must be at least 8 characters"), + restaurantName: z + .string() + .min(3, "Restaurant name must be at least 3 characters") + .max(255), + restaurantPhone: z + .string() + .min(10, "Phone number must be at least 10 characters") + .max(20) + .regex( + /^[0-9+\-() ]+$/, + "Phone number can only contain digits, +, -, (, ) and spaces" + ), + restaurantEmail: z.string().email("Valid restaurant email required"), +}); + +type RegisterFormData = z.infer; + +export default function AdminRegistrationPage() { + const router = useRouter(); + const { setAuth } = useAuthStore(); + const [showPassword, setShowPassword] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: zodResolver(registerSchema), + }); + + const onSubmit = async (data: RegisterFormData) => { + setIsLoading(true); + setError(null); + + try { + const response = await adminApi.register(data); + setAuth(response.user, response.accessToken); + router.push("/admin/dashboard"); + } catch (err: any) { + setError(err?.message ?? "Registration failed"); + } finally { + setIsLoading(false); + } + }; + + return ( +

+
+
+
+
+
+

+ Register Your Restaurant +

+

+ Create an admin account for your restaurant +

+
+ +
+
+

+ Admin Account +

+ +
+ + + {errors.username && ( +

{errors.username.message}

+ )} +
+ +
+ + + {errors.email && ( +

{errors.email.message}

+ )} +
+ +
+ +
+ + +
+ {errors.password && ( +

{errors.password.message}

+ )} +
+
+ +
+

+ Restaurant Details +

+ +
+ + + {errors.restaurantName && ( +

{errors.restaurantName.message}

+ )} +
+ +
+ + + {errors.restaurantPhone && ( +

{errors.restaurantPhone.message}

+ )} +
+ +
+ + + {errors.restaurantEmail && ( +

{errors.restaurantEmail.message}

+ )} +
+
+ + {error && ( +
+

{error}

+
+ )} + +
+ +
+ +
+

+ Already have an account?{" "} + + Sign in + +

+
+
+
+
+ ); +} diff --git a/frontend/app/(public)/admin-regitration/page.tsx b/frontend/app/(public)/admin-regitration/page.tsx deleted file mode 100644 index 1b3871d..0000000 --- a/frontend/app/(public)/admin-regitration/page.tsx +++ /dev/null @@ -1,139 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { useRouter } from "next/navigation"; -import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { z } from "zod"; -import { useMutation } from "@tanstack/react-query"; -import { Eye, EyeOff } from "lucide-react"; -import { adminApi } from "@/lib/api/admin"; -import { AdminRole } from "@/lib/types/user"; - -const schema = z.object({ - username: z.string().min(1, "Username is required"), - email: z.string().email("Valid email required"), - password: z.string().min(8, "Password must be at least 8 characters"), - role: z.enum([AdminRole.STAFF, AdminRole.MANAGER, AdminRole.ADMIN], { - required_error: "Role is required", - }), -}); - -type FormData = z.infer; - -const rolePermissions = [ - { role: "Staff", desc: "View orders, manage table status, limited menu access." }, - { role: "Manager", desc: "All Staff permissions + manage menu items and view reports." }, - { role: "Admin", desc: "All Manager permissions + manage users and system settings." }, -]; - -export default function CreateUserPage() { - const router = useRouter(); - const [showPassword, setShowPassword] = useState(false); - - const { - register, - handleSubmit, - formState: { errors }, - } = useForm({ resolver: zodResolver(schema) }); - - const mutation = useMutation({ - mutationFn: (data: FormData) => adminApi.createUser(data), - onSuccess: () => { - alert("User created successfully!"); - router.push("/admin/users"); - }, - onError: (err: any) => { - alert(err?.message ?? "Failed to create user."); - }, - }); - - return ( -
-

Create Admin User

- -
mutation.mutate(d))} className="space-y-4"> -
- - - {errors.username &&

{errors.username.message}

} -
- -
- - - {errors.email &&

{errors.email.message}

} -
- -
- -
- - -
- {errors.password &&

{errors.password.message}

} -
- -
- - - {errors.role &&

{errors.role.message}

} -
- -
- - -
-
- -
-

Role Permissions

-
    - {rolePermissions.map(({ role, desc }) => ( -
  • - {role}: - {desc} -
  • - ))} -
-
-
- ); -} diff --git a/frontend/lib/api/api.ts b/frontend/lib/api/api.ts index 986b4f6..5ff8784 100644 --- a/frontend/lib/api/api.ts +++ b/frontend/lib/api/api.ts @@ -6,9 +6,8 @@ import type { InstagramPost, } from "@/lib/types"; import { GalleryImage } from "./admin"; -import { AdminUser } from "@/lib/types/user"; -const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:9001/api"; +const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:9002/api"; const RESTAURANT_ID = process.env.NEXT_PUBLIC_RESTAURANT_ID; if (!RESTAURANT_ID) { @@ -187,29 +186,8 @@ export const qrApi = { }), }; -// Admin API -export const adminApi = { - login: async (credentials: { username: string; password: string }) => { - const response = await fetch(`${API_URL}/admin/login`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(credentials), - }); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error(errorData.message || "Login failed"); - } - - const data = await response.json(); - return { - token: data.token, - user: data.user as AdminUser, - }; - }, -}; +// Admin API - use adminApi from @/lib/api/admin for authenticated admin operations +export { adminApi } from "./admin"; const apiClient = { menu: menuApi, diff --git a/frontend/lib/api/client.ts b/frontend/lib/api/client.ts index 8a1ad9b..bc099ed 100644 --- a/frontend/lib/api/client.ts +++ b/frontend/lib/api/client.ts @@ -1,7 +1,7 @@ // frontend/src/lib/api/client.ts import { useAuthStore } from "@/lib/store/authStore"; -const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:9001/api"; +const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:9002/api"; export class ApiError extends Error { constructor(message: string, public status: number, public data?: any) { @@ -31,12 +31,12 @@ async function fetchWithAuth( }); // Handle 401 Unauthorized - clear auth and redirect - // if (response.status === 401) { - // useAuthStore.getState().clearAuth(); - // if (typeof window !== "undefined") { - // window.location.href = "/admin/login"; - // } - // } + if (response.status === 401) { + useAuthStore.getState().clearAuth(); + if (typeof window !== "undefined") { + window.location.href = "/login"; + } + } return response; }