diff --git a/backend/src/admin/admin.service.ts b/backend/src/admin/admin.service.ts index e614d7819e02e07fe0798e3c9f66294888507a82..3ec4fcc242f63a3d9a187351bdce34fe240928c1 100644 --- a/backend/src/admin/admin.service.ts +++ b/backend/src/admin/admin.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, UnauthorizedException } from '@nestjs/common'; import { CreateAdminDto } from './dto/create-admin.dto'; import { Admin } from './entities/admin.entity'; import { Repository } from 'typeorm'; @@ -12,6 +12,10 @@ export class AdminService { } async findOne(email: string): Promise { - return await this.adminRepository.findOneByOrFail({ email }); + try { + return await this.adminRepository.findOneByOrFail({ email }); + } catch (error) { + throw new UnauthorizedException('Admin not found'); + } } } diff --git a/backend/src/admin/dto/create-admin.dto.ts b/backend/src/admin/dto/create-admin.dto.ts index 2b67b15ac081317cc58f41310d743527ac86735c..c5a5ca71447f40558381cb8b93996c8aef10460e 100644 --- a/backend/src/admin/dto/create-admin.dto.ts +++ b/backend/src/admin/dto/create-admin.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; -import { ADMIN_ROLE } from 'src/enum/admin-role.enum'; -import { UserStatus } from 'src/enum/user-status.enum'; +import { ADMIN_ROLE } from 'src/shared/enum/admin-role.enum'; +import { UserStatus } from 'src/shared/enum/user-status.enum'; export class CreateAdminDto { @ApiProperty() diff --git a/backend/src/admin/entities/admin.entity.ts b/backend/src/admin/entities/admin.entity.ts index 2edb68ebfd46273465990ff2109677ccf2cc3182..b7fc6cbd33be5bee1f6d3d85e037b4de9db1aa73 100644 --- a/backend/src/admin/entities/admin.entity.ts +++ b/backend/src/admin/entities/admin.entity.ts @@ -1,5 +1,5 @@ -import { ADMIN_ROLE } from 'src/enum/admin-role.enum'; -import { UserStatus } from 'src/enum/user-status.enum'; +import { ADMIN_ROLE } from 'src/shared/enum/admin-role.enum'; +import { UserStatus } from 'src/shared/enum/user-status.enum'; import { Entity, Column, PrimaryColumn } from 'typeorm'; @Entity() diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 5a809a0cd1ddd58c0565a2d9f2abdb8740af4c8e..befa2d439fcaae4961d1823e2a76e46cff0994df 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -17,6 +17,9 @@ import { join } from 'path'; import { Town } from './town/entities/town.entity'; import { TownModule } from './town/town.module'; import { TownTraduction } from './town/entities/town-traduction.entity'; +import { AuthModule } from './shared/service/auth.module'; +import { APP_GUARD } from '@nestjs/core'; +import { AuthGuard } from './auth/admin/auth.guard'; @Module({ imports: [ @@ -38,12 +41,13 @@ import { TownTraduction } from './town/entities/town-traduction.entity'; StateModule, DatabaseSeederModule, TownModule, + AuthModule, ServeStaticModule.forRoot({ rootPath: join(__dirname, '..', 'static'), }), ], controllers: [AppController], - providers: [AppService, DatabaseSeederModule], + providers: [AppService, DatabaseSeederModule, { provide: APP_GUARD, useClass: AuthGuard }], exports: [TypeOrmModule], }) export class AppModule {} diff --git a/backend/src/auth/admin/auth.guard.ts b/backend/src/auth/admin/auth.guard.ts new file mode 100644 index 0000000000000000000000000000000000000000..067114b29f2767c02e2ddc51c9f45ed21b0c5da0 --- /dev/null +++ b/backend/src/auth/admin/auth.guard.ts @@ -0,0 +1,23 @@ +import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { AuthService } from 'src/shared/service/auth.service'; +import { Roles } from '../role.decorator'; + +@Injectable() +export class AuthGuard implements CanActivate { + constructor( + private authService: AuthService, + private reflector: Reflector, + ) {} + async canActivate(context: ExecutionContext): Promise { + const requiredRole = this.reflector.get(Roles, context.getHandler()); + if (!requiredRole) return true; + const request = context.switchToHttp().getRequest(); + let { authorization }: any = request.headers; + if (!authorization) throw new UnauthorizedException('session expired! Please sign In'); + authorization = authorization.split(' ')[1]; + const role = await this.authService.validateToken(authorization); + if (!requiredRole.includes(role)) throw new UnauthorizedException('Unauthorized access'); + return true; + } +} diff --git a/backend/src/auth/admin/authAdmincontroller.ts b/backend/src/auth/admin/authAdmincontroller.ts index bc2e0906e8850c0e4bbebfaeb16549692d6610e0..ff6b3d098bc8366e04a5047ed45a1a9440144392 100644 --- a/backend/src/auth/admin/authAdmincontroller.ts +++ b/backend/src/auth/admin/authAdmincontroller.ts @@ -2,8 +2,10 @@ import { Body, Controller, Post } from '@nestjs/common'; import { AuthAdminService } from './authAdminservice'; import { CreateAdminDto } from 'src/admin/dto/create-admin.dto'; import { LoginAdminDto } from 'src/auth/admin/dto/login-admin.dto'; -import { ApiBody, ApiCreatedResponse, ApiTags, ApiUnauthorizedResponse } from '@nestjs/swagger'; +import { ApiBearerAuth, ApiBody, ApiCreatedResponse, ApiTags, ApiUnauthorizedResponse } from '@nestjs/swagger'; import { AdminSigninResDto } from './dto/admin-signin-res.dto'; +import { Roles } from '../role.decorator'; +import { ADMIN_ROLE, SUPERADMIN_ROLES } from 'src/shared/enum/admin-role.enum'; @Controller() @ApiTags('Create admin account and sign in as admin') @@ -20,10 +22,12 @@ export class AuthAdminController { }, }, }) + @Roles(SUPERADMIN_ROLES) @Post('admin/signup') + @ApiBearerAuth('jwt') async signUp(@Body() createAdminDto: CreateAdminDto) { - console.log(createAdminDto); try { + createAdminDto.role = ADMIN_ROLE.ADMIN; const accessToken = await this.authAdminService.signUp(createAdminDto); return { token: accessToken }; } catch (e) { diff --git a/backend/src/auth/admin/authAdminservice.ts b/backend/src/auth/admin/authAdminservice.ts index 3d780937e4cc0f05d322873e2cb3e538a18fe6e9..f94aa66b610f772fffcfd8722d5853fb25960ba9 100644 --- a/backend/src/auth/admin/authAdminservice.ts +++ b/backend/src/auth/admin/authAdminservice.ts @@ -7,7 +7,7 @@ import { LoginAdminDto } from 'src/auth/admin/dto/login-admin.dto'; import { JwtConstants } from 'src/constants/jwt.constants'; import { AdminSigninResDto } from './dto/admin-signin-res.dto'; import { Admin } from 'src/admin/entities/admin.entity'; -import { ADMIN_ROLE } from 'src/enum/admin-role.enum'; +import { ADMIN_ROLE } from 'src/shared/enum/admin-role.enum'; @Injectable() export class AuthAdminService { @@ -36,7 +36,10 @@ export class AuthAdminService { if (!validPwd) { throw new HttpException('Invalid credentials', HttpStatus.UNAUTHORIZED); } - const accessToken = await this.jwtService.sign({ email: admin.email }, { secret: JwtConstants.SECRET }); + const accessToken = await this.jwtService.sign( + { email: admin.email, role: admin.role }, + { secret: JwtConstants.SECRET }, + ); const adminSigninResDto: AdminSigninResDto = { email: admin.email, name: admin.name, diff --git a/backend/src/auth/admin/dto/admin-signin-res.dto.ts b/backend/src/auth/admin/dto/admin-signin-res.dto.ts index b19eefc741d48545485a0284f20305da503be3ea..c2aeac79cd522a45fc39f8c77cdd0963aa5596cc 100644 --- a/backend/src/auth/admin/dto/admin-signin-res.dto.ts +++ b/backend/src/auth/admin/dto/admin-signin-res.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { ADMIN_ROLE } from 'src/enum/admin-role.enum'; +import { ADMIN_ROLE } from 'src/shared/enum/admin-role.enum'; export class AdminSigninResDto { @ApiProperty() diff --git a/backend/src/auth/role.decorator.ts b/backend/src/auth/role.decorator.ts new file mode 100644 index 0000000000000000000000000000000000000000..b69b3734a4653c6c1022399676b74e26b9a173ab --- /dev/null +++ b/backend/src/auth/role.decorator.ts @@ -0,0 +1,3 @@ +import { Reflector } from '@nestjs/core'; + +export const Roles = Reflector.createDecorator(); diff --git a/backend/src/database-seeder/database-seeder.module.ts b/backend/src/database-seeder/database-seeder.module.ts index eb8e2f96fc6ee64c031311e8a1c863ec85df63d7..54d83e1b0e7260afb01dd267c3ec3b9664af15fc 100644 --- a/backend/src/database-seeder/database-seeder.module.ts +++ b/backend/src/database-seeder/database-seeder.module.ts @@ -4,9 +4,14 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { State } from 'src/state/entities/state.entity'; import { StateService } from 'src/state/state.service'; import { Town } from 'src/town/entities/town.entity'; +import { Admin } from 'src/admin/entities/admin.entity'; +import { AuthAdminService } from 'src/auth/admin/authAdminservice'; +import { AdminService } from 'src/admin/admin.service'; +import { JwtService } from '@nestjs/jwt'; +import { EncryptionService } from 'src/auth/encryption/encryption.service'; @Module({ - providers: [DatabaseSeederService, StateService], - imports: [TypeOrmModule.forFeature([State, Town])], + providers: [DatabaseSeederService, StateService, AuthAdminService, AdminService, JwtService, EncryptionService], + imports: [TypeOrmModule.forFeature([State, Town, Admin])], }) export class DatabaseSeederModule {} diff --git a/backend/src/database-seeder/database-seeder.service.ts b/backend/src/database-seeder/database-seeder.service.ts index d76e3660c8ef6ac5910d08bd8302c86597b35e67..9a3fd5c9af2f679dd3dea41a929778ff434bdf4c 100644 --- a/backend/src/database-seeder/database-seeder.service.ts +++ b/backend/src/database-seeder/database-seeder.service.ts @@ -4,12 +4,17 @@ import { State } from 'src/state/entities/state.entity'; import { StateService } from 'src/state/state.service'; import { Repository } from 'typeorm'; import * as data from './states.json'; +import { CreateAdminDto } from 'src/admin/dto/create-admin.dto'; +import { ADMIN_ROLE } from 'src/shared/enum/admin-role.enum'; +import { UserStatus } from 'src/shared/enum/user-status.enum'; +import { AuthAdminService } from 'src/auth/admin/authAdminservice'; @Injectable() export class DatabaseSeederService implements OnModuleInit { constructor( @InjectRepository(State) private stateRepo: Repository, private readonly stateService: StateService, + private readonly authAdminService: AuthAdminService, ) {} async insertStates() { @@ -19,7 +24,57 @@ export class DatabaseSeederService implements OnModuleInit { } } + async insertSuperAdmin() { + const createSuperAdmin: CreateAdminDto = { + email: 'superadmin@gmail.com', + password: '123', + name: 'Super Admin', + lastName: 'super', + role: ADMIN_ROLE.SUPERADMIN, + status: UserStatus.ACTIVE, + }; + const createAdmin: CreateAdminDto = { + email: 'admin@gmail.com', + password: '123', + name: 'Admin', + lastName: 'admin', + role: ADMIN_ROLE.ADMIN, + status: UserStatus.ACTIVE, + }; + let tokenSuper = '', + tokenAdmin = ''; + try { + tokenSuper = await this.authAdminService.signUp({ ...createSuperAdmin }); + } catch (error) { + tokenSuper = ( + await this.authAdminService.signIn({ + email: createSuperAdmin.email, + password: createSuperAdmin.password, + }) + ).token; + } + try { + tokenAdmin = await this.authAdminService.signUp({ ...createAdmin }); + } catch (error) { + tokenAdmin = ( + await this.authAdminService.signIn({ + email: createAdmin.email, + password: createSuperAdmin.password, + }) + ).token; + } + console.log( + `Super Admin created with email: ${createSuperAdmin.email} + , password: ${createSuperAdmin.password}, and token: ${tokenSuper}`, + ); + console.log( + `Admin created with email: ${createAdmin.email}, + password: ${createAdmin.password}, and token: ${tokenAdmin}`, + ); + } + async onModuleInit() { await this.insertStates(); + await this.insertSuperAdmin(); } } diff --git a/backend/src/enum/admin-role.enum.ts b/backend/src/enum/admin-role.enum.ts deleted file mode 100644 index 52e5c067cbb40ac715da04bf04787395749a40e0..0000000000000000000000000000000000000000 --- a/backend/src/enum/admin-role.enum.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum ADMIN_ROLE { - ADMIN = 'admin', - SUPERADMIN = 'superadmin', -} diff --git a/backend/src/main.ts b/backend/src/main.ts index a45b1c900f0fc73573ca0ff2d1c19fa595d125c3..733878dbb782afba76661ae84c079bf3365b12d7 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -16,7 +16,7 @@ async function bootstrap() { name: 'Authorization', in: 'header', }, - 'JWT', + 'jwt', ) .build(); const document = SwaggerModule.createDocument(app, config); diff --git a/backend/src/shared/dto/payload-jwt.dto.ts b/backend/src/shared/dto/payload-jwt.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..5726376d85337976d3f4d745831547b653e1d4e1 --- /dev/null +++ b/backend/src/shared/dto/payload-jwt.dto.ts @@ -0,0 +1,4 @@ +export class PayloadJwtDto { + email: string; + role: string; +} diff --git a/backend/src/shared/enum/admin-role.enum.ts b/backend/src/shared/enum/admin-role.enum.ts new file mode 100644 index 0000000000000000000000000000000000000000..e398a9352b7fc76153d7eb7a07dea95287a198ad --- /dev/null +++ b/backend/src/shared/enum/admin-role.enum.ts @@ -0,0 +1,7 @@ +export enum ADMIN_ROLE { + ADMIN = 'admin', + SUPERADMIN = 'superadmin', +} + +export const ADMIN_ROLES = [ADMIN_ROLE.ADMIN, ADMIN_ROLE.SUPERADMIN]; +export const SUPERADMIN_ROLES = [ADMIN_ROLE.SUPERADMIN]; diff --git a/backend/src/enum/languages.enum.ts b/backend/src/shared/enum/languages.enum.ts similarity index 100% rename from backend/src/enum/languages.enum.ts rename to backend/src/shared/enum/languages.enum.ts diff --git a/backend/src/enum/user-status.enum.ts b/backend/src/shared/enum/user-status.enum.ts similarity index 100% rename from backend/src/enum/user-status.enum.ts rename to backend/src/shared/enum/user-status.enum.ts diff --git a/backend/src/shared/service/auth.module.ts b/backend/src/shared/service/auth.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..02d7b024d67588a8a4bf3998bded0d9058a16e26 --- /dev/null +++ b/backend/src/shared/service/auth.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import { AuthService } from './auth.service'; + +@Module({ + controllers: [], + providers: [JwtService, AuthService], + imports: [], + exports: [AuthService], +}) +export class AuthModule {} diff --git a/backend/src/shared/service/auth.service.ts b/backend/src/shared/service/auth.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..72d992887109e64125762da90ba62030a080804a --- /dev/null +++ b/backend/src/shared/service/auth.service.ts @@ -0,0 +1,18 @@ +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import { JwtConstants } from 'src/constants/jwt.constants'; +import { PayloadJwtDto } from 'src/shared/dto/payload-jwt.dto'; + +@Injectable() +export class AuthService { + constructor(private jwtService: JwtService) {} + + async validateToken(token: string): Promise { + try { + const payload: PayloadJwtDto = await this.jwtService.verify(token, { secret: JwtConstants.SECRET }); + return payload.role; + } catch (error) { + throw new UnauthorizedException('Invalid token'); + } + } +} diff --git a/backend/src/town/dto/create-town-trad.dto.ts b/backend/src/town/dto/create-town-trad.dto.ts index bb0bba99c47c342a68ada7a246a0ff2b525a147c..eb9058fc6be2d18ea8f3199c3bc7a6ca894f4b37 100644 --- a/backend/src/town/dto/create-town-trad.dto.ts +++ b/backend/src/town/dto/create-town-trad.dto.ts @@ -1,4 +1,4 @@ -import { LANGUAGES } from 'src/enum/languages.enum'; +import { LANGUAGES } from 'src/shared/enum/languages.enum'; export class CreateTownTraductionDto { townId: number; diff --git a/backend/src/town/entities/town-traduction.entity.ts b/backend/src/town/entities/town-traduction.entity.ts index 77922ec5f9f65a200eb91eb90e48743c7303bff3..42a4d3f4b05f5555cdba68295213eec76fc671a0 100644 --- a/backend/src/town/entities/town-traduction.entity.ts +++ b/backend/src/town/entities/town-traduction.entity.ts @@ -1,6 +1,6 @@ import { Entity, Column, PrimaryColumn, ManyToOne } from 'typeorm'; import { Town } from './town.entity'; -import { LANGUAGES } from 'src/enum/languages.enum'; +import { LANGUAGES } from 'src/shared/enum/languages.enum'; @Entity() export class TownTraduction { @PrimaryColumn({ name: 'townId' }) diff --git a/backend/src/town/town.controller.ts b/backend/src/town/town.controller.ts index a21509d36dd6b5eda9327cafee16fb2528c0ea0a..a8767c9ae50df240d70d132ef33235dd9707ac65 100644 --- a/backend/src/town/town.controller.ts +++ b/backend/src/town/town.controller.ts @@ -1,17 +1,18 @@ import { Controller, Get, Post, Param, Delete, UseInterceptors, UploadedFile, Body, Query } from '@nestjs/common'; import { TownService } from './town.service'; -import { ApiBody, ApiConsumes, ApiParam, ApiQuery, ApiTags } from '@nestjs/swagger'; +import { ApiBearerAuth, ApiBody, ApiConsumes, ApiParam, ApiQuery, ApiTags } from '@nestjs/swagger'; import { FileValidationPipe } from 'src/shared/pipe/file-validation.pipe'; import { fileInterceptor } from 'src/shared/interceptors/file-save.interceptor'; import { CreateTownDto } from './dto/create-town.dto'; +import { Roles } from 'src/auth/role.decorator'; +import { SUPERADMIN_ROLES } from 'src/shared/enum/admin-role.enum'; @Controller() @ApiTags('Pueblos') export class TownController { - constructor( - private readonly townService: TownService, - // private readonly stateService: StateService, - ) {} + constructor(private readonly townService: TownService) {} + @Roles(SUPERADMIN_ROLES) + @ApiBearerAuth('jwt') @ApiBody({ type: CreateTownDto }) @ApiConsumes('multipart/form-data') @Post('town') diff --git a/backend/src/town/town.service.ts b/backend/src/town/town.service.ts index 6f04b28920c60e87e53be13650c5976aae8bf472..badd465d4d2a1efd248461b57c438832d0631b05 100644 --- a/backend/src/town/town.service.ts +++ b/backend/src/town/town.service.ts @@ -6,7 +6,7 @@ import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; import { StateService } from 'src/state/state.service'; import { TownTraduction } from './entities/town-traduction.entity'; import { CreateTownTraductionDto } from './dto/create-town-trad.dto'; -import { LANGUAGES } from 'src/enum/languages.enum'; +import { LANGUAGES } from 'src/shared/enum/languages.enum'; import { DataSource } from 'typeorm'; import { ServerConstants } from 'src/constants/server.contants'; import { TownResDto } from './dto/town-res.dto';