diff --git a/.env.example b/.env.example index f9556d6..6c43eaf 100644 --- a/.env.example +++ b/.env.example @@ -7,9 +7,9 @@ DB_HOST="pgsql" DB_PORT="5432" DB_USER="user_db_dev" DB_PASS="pass_db_dev" -DB_NAME="db-${PROJECT_NAME}" +DB_NAME="db-starter-kit" -MAIL_HOST=host.docker.internal +MAIL_HOST=smtp MAIL_PORT=1025 MAIL_USER= MAIL_PASS= @@ -28,7 +28,6 @@ GCS_BUCKET_NAME=app.appspot.com GOOGLE_APPLICATION_CREDENTIALS=/app/dist/service-account.json - # DOCKER ENV DB_EXPOSED_PORT=8610 APP_EXPOSED_PORT=8611 diff --git a/migrations/1697207588388-CreatePermissionsTable.ts b/migrations/1697207588388-CreatePermissionsTable.ts index bf699c3..5f23263 100644 --- a/migrations/1697207588388-CreatePermissionsTable.ts +++ b/migrations/1697207588388-CreatePermissionsTable.ts @@ -9,7 +9,9 @@ export class CreatePermissionsTable1697207588388 implements MigrationInterface { { name: "id", type: "int8", isPrimary: true, isGenerated: true, generationStrategy: 'increment' }, { name: "name", type: "varchar", isNullable: false }, { name: "path", type: "varchar", isNullable: true }, - { name: "description", type: "varchar", isNullable: true,}, + { name: "actions", type: "json", isNullable: true }, + { name: "created_at", type: "timestamp", isNullable: true }, + { name: "updated_at", type: "timestamp", isNullable: true }, ], indices: [ { columnNames: ['id'] }, diff --git a/migrations/1697208335927-CreateRoleToPermissionsTable.ts b/migrations/1697208335927-CreateRoleToPermissionsTable.ts index 490bfb4..98f463a 100644 --- a/migrations/1697208335927-CreateRoleToPermissionsTable.ts +++ b/migrations/1697208335927-CreateRoleToPermissionsTable.ts @@ -15,6 +15,20 @@ export class CreateRoleToPermissionsTable1697208335927 implements MigrationInter indices: [ { columnNames: ['role_id'] }, { columnNames: ['role_id', 'permission_id'] }, + ], + foreignKeys: [ + { + columnNames: ['role_id'], + referencedColumnNames: ['id'], + referencedTableName: 'roles', + onDelete: 'CASCADE', + }, + { + columnNames: ['permission_id'], + referencedColumnNames: ['id'], + referencedTableName: 'permissions', + onDelete: 'CASCADE', + } ] })) } diff --git a/migrations/1699245515021-AddColumnRoleIdToUsersTable.ts b/migrations/1699245515021-AddColumnRoleIdToUsersTable.ts new file mode 100644 index 0000000..db6ac5e --- /dev/null +++ b/migrations/1699245515021-AddColumnRoleIdToUsersTable.ts @@ -0,0 +1,28 @@ +import { MigrationInterface, QueryRunner, TableColumn, TableForeignKey } from "typeorm" + +export class AddColumnRoleIdToUsersTable1699245515021 implements MigrationInterface { + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.addColumn('users', new TableColumn({ + name: "role_id", + type: "int8", + isNullable: true, + foreignKeyConstraintName: "fk_users_role_id", + })); + + await queryRunner.createForeignKey('users', new TableForeignKey({ + columnNames: ['role_id'], + referencedColumnNames: ['id'], + referencedTableName: 'roles', + onDelete: 'SET NULL', + name: "fk_users_role_id", + })) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropColumn('users', 'role_id'); + + await queryRunner.dropForeignKey('users', 'fk_users_role_id'); + } + +} diff --git a/migrations/1699262051499-CreateInitialData.ts b/migrations/1699262051499-CreateInitialData.ts new file mode 100644 index 0000000..38f2477 --- /dev/null +++ b/migrations/1699262051499-CreateInitialData.ts @@ -0,0 +1,90 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class CreateInitialData1699262051499 implements MigrationInterface { + + public async up(queryRunner: QueryRunner): Promise { + /* Create Permission */ + await queryRunner.connect(); + + const query = queryRunner.manager.createQueryBuilder(); + + await query.insert().into('roles', ['name', 'key', 'description']).values([ + { + name: 'Admin', + key: 'admin', + description: 'Super User', + } + ]).execute() + + await query.insert().into('permissions', ['name', 'path', 'actions']).values([ + { + name: "User", + path: "users", + actions: `["list", "create"]`, + }, + { + name: "User {*}", + path: "users/*", + actions: `["update", "show", "delete", "edit"]`, + }, + { + name: "access_control", + path: "access_control", + actions: "[]", + }, + { + name: "Roles", + path: "roles", + actions: `["list", "create"]`, + }, + { + name: "Roles {*}", + path: "roles/*", + actions: `["update", "show", "delete", "edit"]`, + }, + { + name: "Permissions", + path: "permissions", + actions: `["list", "create"]`, + }, + { + name: "Permissions", + path: "permissions/*", + actions: `["update", "show", "delete", "edit"]`, + }, + ]).execute() + + /* get admin role ID */ + const adminRole = await query + .select('role.id') + .from("roles", 'role') + .where({ key: 'admin' }) + .getRawOne(); + + /* get all permissions ids */ + const permissionIds = await query + .select('permission.id') + .from("permissions", 'permission') + .getRawMany(); + + + /* assign all permissions to admin */ + await query + .insert() + .into('role_to_permissions') + .values(permissionIds.map((permission) => ({ role_id: adminRole.id, permission_id: permission.id }))) + .execute(); + + /* update superadmin role */ + await query + .update('users') + .set({ role_id: adminRole.id }) + .where({ email: 'admin@mail.com' }) + .execute() + + } + + public async down(queryRunner: QueryRunner): Promise { + } + +} diff --git a/nest-cli.json b/nest-cli.json index 60b0fc2..42df025 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -5,6 +5,7 @@ "compilerOptions": { "deleteOutDir": true, "watchAssets": true, + "plugins": ["@nestjs/swagger"], "assets": [ { "include": "service-account.json", diff --git a/package.json b/package.json index 2d85aa3..c543987 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "ejs": "^3.1.8", "ioredis": "^5.3.2", "jsonwebtoken": "^9.0.1", + "nest-access-control": "^3.1.0", "nestjs-i18n": "^10.3.6", "passport": "^0.6.0", "passport-jwt": "^4.0.1", diff --git a/src/admin/admin.module.ts b/src/admin/admin.module.ts new file mode 100644 index 0000000..7d1ee37 --- /dev/null +++ b/src/admin/admin.module.ts @@ -0,0 +1,17 @@ +import { Module } from "@nestjs/common"; +import { SwaggerModule } from "@nestjs/swagger"; +import { UserModule } from "src/apps/user/user.module"; +import { UsersModule } from "./users/users.module"; +import { RolesModule } from './roles/roles.module'; +import { PermissionsModule } from './permissions/permissions.module'; +import { MiscModule } from './misc/misc.module'; + +@Module({ + imports: [ + UsersModule, + RolesModule, + PermissionsModule, + MiscModule, + ] +}) +export class AdminModule {} \ No newline at end of file diff --git a/src/admin/admin.roles.ts b/src/admin/admin.roles.ts new file mode 100644 index 0000000..62594da --- /dev/null +++ b/src/admin/admin.roles.ts @@ -0,0 +1,19 @@ +import { RolesBuilder } from 'nest-access-control'; + +export enum AppRoles { + ADMIN = 'ADMIN', + EDITOR = 'EDITOR', +} + +export const roles: RolesBuilder = new RolesBuilder(); + +roles + .grant(AppRoles.EDITOR) + .create('jobs') + .update('jobs') + // admin + .grant(AppRoles.ADMIN) + .extend(AppRoles.EDITOR) + .create(['companies']) + .update(['companies']) + .delete(['companies', 'jobs']); \ No newline at end of file diff --git a/src/admin/misc/misc.controller.ts b/src/admin/misc/misc.controller.ts new file mode 100644 index 0000000..3eec11a --- /dev/null +++ b/src/admin/misc/misc.controller.ts @@ -0,0 +1,48 @@ +import { Controller, Get, Header, Req } from '@nestjs/common'; +import { Request } from 'express'; +import { UseJwtGuard } from 'src/common/guards/jwt.guard'; +import { MiscService } from './misc.service'; + +@Controller('misc') +export class MiscController { + + constructor( + private miscService: MiscService, + ) {} + + @Get('/permissions') + @UseJwtGuard() + // @Header('content-type', 'text/plain') + async getPermission( + @Req() req: Request + ) { + console.log(req.user); + + // await this.miscService.getUserPermission(req.user.id); + return await this.miscService.getUserPermission(req.user.id); + + // return ` + // p, admin, posts, (list)|(create) + // p, admin, posts/*, (edit)|(show)|(delete) + // p, admin, posts/*, field + + // p, admin, users, (list)|(create) + // p, admin, users/*, (edit)|(show)|(delete) + + // p, admin, roles, (list)|(create) + // p, admin, roles/*, (edit)|(show)|(delete) + + // p, admin, access_control + + // p, admin, permissions, (list)|(create) + // p, admin, permissions/*, (edit)|(show)|(delete) + + // p, editor, posts, (list)|(create) + // p, editor, posts/*, (edit)|(show) + // p, editor, posts/hit, field, deny + + // p, editor, categories, list + // `; + } + +} diff --git a/src/admin/misc/misc.module.ts b/src/admin/misc/misc.module.ts new file mode 100644 index 0000000..2d1fd04 --- /dev/null +++ b/src/admin/misc/misc.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { MiscController } from './misc.controller'; +import { MiscService } from './misc.service'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { User } from 'src/entities/user.entity'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([User]), + ], + controllers: [MiscController], + providers: [MiscService] +}) +export class MiscModule {} diff --git a/src/admin/misc/misc.service.ts b/src/admin/misc/misc.service.ts new file mode 100644 index 0000000..d958cc4 --- /dev/null +++ b/src/admin/misc/misc.service.ts @@ -0,0 +1,50 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { RoleToPermission } from 'src/entities/role-to-permission.entity'; +import { User } from 'src/entities/user.entity'; +import { Repository } from 'typeorm'; + +@Injectable() +export class MiscService { + + constructor( + @InjectRepository(User) + private userRepository: Repository, + ) {} + + + async getUserPermission(userId: number) { + const user = await this.userRepository.findOne({ + where: { + id: userId + }, + relations: { + role: { + permissions: { + permission: true + }, + } + } + }) + + const permissions = user.role.permissions; + let buildPermissionCasbin = ''; + permissions.forEach((item: RoleToPermission) => { + console.log(item.permission); + let actions = item.permission.actions.map((x: string) => `(${x})`).join('|'); + if(actions == '') { + buildPermissionCasbin += `p, ${user.role.key}, ${item.permission.path}${"\n"}` + } else { + buildPermissionCasbin += `p, ${user.role.key}, ${item.permission.path}, ${actions}${"\n"}` + } + }) + + console.log(buildPermissionCasbin.trim()); + return { + role: user.role.key, + permissions, + casbin_permission_adapter: buildPermissionCasbin.trim() + } + + } +} diff --git a/src/admin/permissions/dto/create-permission.dto.ts b/src/admin/permissions/dto/create-permission.dto.ts new file mode 100644 index 0000000..4965068 --- /dev/null +++ b/src/admin/permissions/dto/create-permission.dto.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsNotEmpty } from "class-validator"; + +export class CreatePermissionDto { + @ApiProperty() + name: string; + + @ApiProperty() + path: string; + + @ApiProperty() + actions: string[]; +} diff --git a/src/admin/permissions/dto/update-permission.dto.ts b/src/admin/permissions/dto/update-permission.dto.ts new file mode 100644 index 0000000..fbb7640 --- /dev/null +++ b/src/admin/permissions/dto/update-permission.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreatePermissionDto } from './create-permission.dto'; + +export class UpdatePermissionDto extends PartialType(CreatePermissionDto) {} diff --git a/src/admin/permissions/permissions.controller.ts b/src/admin/permissions/permissions.controller.ts new file mode 100644 index 0000000..e2efd6b --- /dev/null +++ b/src/admin/permissions/permissions.controller.ts @@ -0,0 +1,26 @@ +import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common'; +import { PermissionsService } from './permissions.service'; +import { CreatePermissionDto } from './dto/create-permission.dto'; +import { UpdatePermissionDto } from './dto/update-permission.dto'; +import { BaseCrudController } from 'src/shared/base-crud/base-crud.controller'; +import { ApiSecurity, ApiTags } from '@nestjs/swagger'; + +@Controller('admin/permissions') +@ApiTags("Admin > Permissions") +@ApiSecurity('api-key') +export class PermissionsController extends BaseCrudController { + + constructor(public readonly permissionsService: PermissionsService) { + super(permissionsService); + } + + // @Post() + // create(createDTO: CreatePermissionDto) { + // return this.create(createDTO); + // } + + // @Patch(':id') + // update(@Param('id') id: string, @Body() updateDTO: UpdatePermissionDto) { + // return this.service.update(+id, updateDTO); + // } +} diff --git a/src/admin/permissions/permissions.module.ts b/src/admin/permissions/permissions.module.ts new file mode 100644 index 0000000..ba72a0b --- /dev/null +++ b/src/admin/permissions/permissions.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { PermissionsService } from './permissions.service'; +import { PermissionsController } from './permissions.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Permission } from 'src/entities/permission.entity'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([Permission]), + ], + controllers: [PermissionsController], + providers: [PermissionsService], +}) +export class PermissionsModule {} diff --git a/src/admin/permissions/permissions.service.ts b/src/admin/permissions/permissions.service.ts new file mode 100644 index 0000000..3146692 --- /dev/null +++ b/src/admin/permissions/permissions.service.ts @@ -0,0 +1,18 @@ +import { Injectable } from '@nestjs/common'; +import { CreatePermissionDto } from './dto/create-permission.dto'; +import { UpdatePermissionDto } from './dto/update-permission.dto'; +import { BaseCrudService } from 'src/shared/base-crud/base-crud.service'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Permission } from 'src/entities/permission.entity'; +import { Repository } from 'typeorm'; +import { IBaseService } from 'src/shared/base-crud/base-service.interface'; + +@Injectable() +export class PermissionsService extends BaseCrudService { + constructor( + @InjectRepository(Permission) + repo: Repository + ) { + super(repo); + } +} diff --git a/src/admin/roles/dto/create-role.dto.ts b/src/admin/roles/dto/create-role.dto.ts new file mode 100644 index 0000000..37174da --- /dev/null +++ b/src/admin/roles/dto/create-role.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsString } from "class-validator"; + +export class CreateRoleDto { + @ApiProperty({example: 'Admin'}) + @IsString() + name: string; + + @ApiProperty({example: 'ADMIN'}) + key: string; + + @ApiProperty() + description: string; +} diff --git a/src/admin/roles/dto/update-role.dto.ts b/src/admin/roles/dto/update-role.dto.ts new file mode 100644 index 0000000..450134d --- /dev/null +++ b/src/admin/roles/dto/update-role.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateRoleDto } from './create-role.dto'; + +export class UpdateRoleDto extends PartialType(CreateRoleDto) {} diff --git a/src/admin/roles/roles.controller.ts b/src/admin/roles/roles.controller.ts new file mode 100644 index 0000000..bfe259e --- /dev/null +++ b/src/admin/roles/roles.controller.ts @@ -0,0 +1,41 @@ +import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common'; +import { RolesService } from './roles.service'; +import { CreateRoleDto } from './dto/create-role.dto'; +import { UpdateRoleDto } from './dto/update-role.dto'; +import { ApiSecurity, ApiTags } from '@nestjs/swagger'; +import { BaseCrudController } from 'src/shared/base-crud/base-crud.controller'; + +@Controller('admin/roles') +@ApiTags("Admin > Roles") +@ApiSecurity('api-key') +export class RolesController extends BaseCrudController { + constructor(public readonly service: RolesService) { + super(service); + } + + // @Post() + // create(createDTO: CreateRoleDto) { + // return this.create(createDTO); + // } + + // @Patch(':id') + // update(@Param('id') id: string, @Body() updateDTO: UpdateRoleDto) { + // return this.service.update(+id, updateDTO); + // } + + @Patch(':id/assign') + async assignPermisssion(@Body('ids') permissionIds: string[], @Param('id') id: string) { + + await this.service.assignPermission(+id, permissionIds); + + return { + status: true, + } + } + + @Get(':id/permissions') + async getPermisssion(@Param('id') id: string) { + + return await this.service.getPermission(+id); + } +} diff --git a/src/admin/roles/roles.module.ts b/src/admin/roles/roles.module.ts new file mode 100644 index 0000000..4b26547 --- /dev/null +++ b/src/admin/roles/roles.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { RolesService } from './roles.service'; +import { RolesController } from './roles.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Role } from 'src/entities/role.entity'; +import { RoleToPermission } from 'src/entities/role-to-permission.entity'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([Role, RoleToPermission]) + ], + controllers: [RolesController], + providers: [RolesService], +}) +export class RolesModule {} diff --git a/src/admin/roles/roles.service.ts b/src/admin/roles/roles.service.ts new file mode 100644 index 0000000..ae34b5d --- /dev/null +++ b/src/admin/roles/roles.service.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@nestjs/common'; +import { CreateRoleDto } from './dto/create-role.dto'; +import { UpdateRoleDto } from './dto/update-role.dto'; +import { Role } from 'src/entities/role.entity'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { IBaseService } from 'src/shared/base-crud/base-service.interface'; +import { BaseCrudService } from 'src/shared/base-crud/base-crud.service'; +import { RoleToPermission } from 'src/entities/role-to-permission.entity'; + +@Injectable() +export class RolesService extends BaseCrudService { + constructor( + @InjectRepository(Role) + public roleRepository: Repository, + + @InjectRepository(RoleToPermission) + public roleToPermission: Repository, + ) { + super(roleRepository) + } + + + async assignPermission(roleId: number, permissionIds: string[]) { + await this.roleToPermission.delete({role_id: roleId}); + console.log(permissionIds); + await this.roleToPermission.createQueryBuilder().insert().into('role_to_permissions').values(permissionIds.map(id => ({ role_id: roleId, permission_id: id }))).execute(); + } + + async getPermission(roleId: number) { + return await this.roleToPermission.find({ + where: { + role_id: roleId + }, + relations: { + permission: true, + role: true + } + }); + } +} diff --git a/src/admin/users/dto/create-user.dto.ts b/src/admin/users/dto/create-user.dto.ts new file mode 100644 index 0000000..e76434f --- /dev/null +++ b/src/admin/users/dto/create-user.dto.ts @@ -0,0 +1,22 @@ + +import { ApiProperty } from '@nestjs/swagger'; +import { IsString, IsEmail, MinLength, Matches, IsNotEmpty } from 'class-validator'; +import { Match } from "src/common/decorators/validation.decorator"; + +export class CreateUserDto { + @ApiProperty() + @IsString() + name: string; + + @ApiProperty() + @IsEmail() + email: string; + + @ApiProperty() + @MinLength(8) + password: string; + + @ApiProperty() + @Match('password', { message: 'Confirm password must match the password'}) + confirmPassword: string; +} diff --git a/src/admin/users/dto/update-user.dto.ts b/src/admin/users/dto/update-user.dto.ts new file mode 100644 index 0000000..0e35289 --- /dev/null +++ b/src/admin/users/dto/update-user.dto.ts @@ -0,0 +1,6 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateUserDto } from './create-user.dto'; + +export class UpdateUserDto extends PartialType(CreateUserDto) { + +} diff --git a/src/admin/users/users.controller.ts b/src/admin/users/users.controller.ts new file mode 100644 index 0000000..92e73d7 --- /dev/null +++ b/src/admin/users/users.controller.ts @@ -0,0 +1,37 @@ +import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common'; +import { UsersService } from './users.service'; +import { CreateUserDto } from './dto/create-user.dto'; +import { UpdateUserDto } from './dto/update-user.dto'; +import { ApiSecurity, ApiTags } from '@nestjs/swagger'; + +@Controller('admin/users') +@ApiSecurity('api-key') +@ApiTags("Admin > Users") +export class UsersController { + constructor(private readonly usersService: UsersService) {} + + @Post() + create(@Body() createUserDto: CreateUserDto) { + return this.usersService.create(createUserDto); + } + + @Get() + findAll() { + return this.usersService.findAll(); + } + + @Get(':id') + findOne(@Param('id') id: string) { + return this.usersService.findOne(+id); + } + + @Patch(':id') + update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) { + return this.usersService.update(+id, updateUserDto); + } + + @Delete(':id') + remove(@Param('id') id: string) { + return this.usersService.remove(+id); + } +} diff --git a/src/admin/users/users.module.ts b/src/admin/users/users.module.ts new file mode 100644 index 0000000..c0f0bc7 --- /dev/null +++ b/src/admin/users/users.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { UsersService } from './users.service'; +import { UsersController } from './users.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { User } from 'src/entities/user.entity'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([User]), + ], + controllers: [UsersController], + providers: [UsersService], +}) +export class UsersModule {} diff --git a/src/admin/users/users.service.ts b/src/admin/users/users.service.ts new file mode 100644 index 0000000..21a489b --- /dev/null +++ b/src/admin/users/users.service.ts @@ -0,0 +1,46 @@ +import { Injectable } from '@nestjs/common'; +import { CreateUserDto } from './dto/create-user.dto'; +import { UpdateUserDto } from './dto/update-user.dto'; +import { InjectRepository } from '@nestjs/typeorm'; +import { User } from 'src/entities/user.entity'; +import { Repository } from 'typeorm'; +import { hash } from 'bcrypt'; + +@Injectable() +export class UsersService { + + constructor( + @InjectRepository(User) + private userRepository: Repository + ) {} + + + get repo() { return this.userRepository; } + + async create(createUserDto: CreateUserDto) { + const { password, ...rest } = createUserDto; + const hashedPassword = await hash(password, 10); + const newUser = this.repo.create({ password: hashedPassword, ...rest }); + const createdUser = await this.repo.save(newUser); + return createdUser; + } + + findAll() { + + return this.repo.find(); + } + + findOne(id: number) { + return this.repo.findOne({where: {id}}); + } + + async update(id: number, updateUserDto: UpdateUserDto) { + + return await this.repo.update(id, updateUserDto); + } + + remove(id: number) { + // delete with typeorm + return this.repo.delete(id); + } +} diff --git a/src/apps/app.module.ts b/src/apps/app.module.ts index 3e278f6..758798d 100644 --- a/src/apps/app.module.ts +++ b/src/apps/app.module.ts @@ -10,6 +10,9 @@ import { FileModule } from './file/file.module'; import { I18nModule, QueryResolver, HeaderResolver, AcceptLanguageResolver } from 'nestjs-i18n' import { join } from 'path'; import { ConfigModule as AppConfigModule } from './config/config.module'; +import { UsersModule } from 'src/admin/users/users.module'; +import { AdminModule } from 'src/admin/admin.module'; +import { UserModule } from './user/user.module'; @Module({ imports: [ ConfigModule.forRoot({ @@ -38,7 +41,8 @@ import { ConfigModule as AppConfigModule } from './config/config.module'; }), AuthModule, FileModule, - AppConfigModule + AppConfigModule, + AdminModule, ], controllers: [AppController], providers: [AppService], diff --git a/src/apps/user/user.controller.ts b/src/apps/user/user.controller.ts index 277d6bf..fc6e0e2 100644 --- a/src/apps/user/user.controller.ts +++ b/src/apps/user/user.controller.ts @@ -4,6 +4,7 @@ import { UseJwtGuard } from 'src/common/guards/jwt.guard'; import { Response } from 'src/common/utils'; import { UpdateUserDTO } from './update-user.dto'; import { UserService } from './user.service'; +import { ApiNotAcceptableResponse, ApiTags } from '@nestjs/swagger'; @Controller('user') @UseJwtGuard() diff --git a/src/common/decorators/validation.decorator.ts b/src/common/decorators/validation.decorator.ts new file mode 100644 index 0000000..b6d3883 --- /dev/null +++ b/src/common/decorators/validation.decorator.ts @@ -0,0 +1,24 @@ +import {registerDecorator, ValidationArguments, ValidationOptions, ValidatorConstraint, ValidatorConstraintInterface} from 'class-validator'; + +export function Match(property: string, validationOptions?: ValidationOptions) { + return (object: any, propertyName: string) => { + registerDecorator({ + target: object.constructor, + propertyName, + options: validationOptions, + constraints: [property], + validator: MatchConstraint, + }); + }; +} + +@ValidatorConstraint({name: 'Match'}) +export class MatchConstraint implements ValidatorConstraintInterface { + + validate(value: any, args: ValidationArguments) { + const [relatedPropertyName] = args.constraints; + const relatedValue = (args.object as any)[relatedPropertyName]; + return value === relatedValue; + } + +} \ No newline at end of file diff --git a/src/common/guards/api-key.guard.ts b/src/common/guards/api-key.guard.ts index 48a49a5..04511ed 100644 --- a/src/common/guards/api-key.guard.ts +++ b/src/common/guards/api-key.guard.ts @@ -9,7 +9,8 @@ export class ApiKeyGuard implements CanActivate { private excluded = [ /^\/$/, - /^\/webhook(.*)$/ + /^\/webhook(.*)$/, + /^\/misc(.*)$/, ]; canActivate( @@ -25,6 +26,7 @@ export class ApiKeyGuard implements CanActivate { /* catch x-api-key from header and verify with the env */ const key = req.headers['x-api-key'] ?? req.query.api_key + console.log(key) if(key == undefined || key == '') { throw new HttpException('X-API-KEY is not provided.', HttpStatus.UNAUTHORIZED); } diff --git a/src/entities/permission.entity.ts b/src/entities/permission.entity.ts index c99baa4..226fec8 100644 --- a/src/entities/permission.entity.ts +++ b/src/entities/permission.entity.ts @@ -13,8 +13,8 @@ export class Permission { @Column({ type: "varchar", nullable: true }) path: string; - @Column({ type: "varchar", nullable: true }) - description: string; + @Column({ type: "json", nullable: true }) + actions: string[]; @OneToMany(type => RoleToPermission, roleToPermission => roleToPermission.permission) roles: RoleToPermission[]; diff --git a/src/entities/role.entity.ts b/src/entities/role.entity.ts index 5e4111e..f5575b6 100644 --- a/src/entities/role.entity.ts +++ b/src/entities/role.entity.ts @@ -26,4 +26,3 @@ export class Role { @OneToMany(type => RoleToPermission, roleToPermission => roleToPermission.role) permissions: RoleToPermission[]; } - diff --git a/src/entities/user.entity.ts b/src/entities/user.entity.ts index e0073b6..934fa09 100644 --- a/src/entities/user.entity.ts +++ b/src/entities/user.entity.ts @@ -1,6 +1,7 @@ import { strRandom } from 'src/common/utils'; -import { Entity, Column, PrimaryGeneratedColumn, BeforeInsert, UpdateDateColumn, CreateDateColumn } from 'typeorm'; +import { Entity, Column, PrimaryGeneratedColumn, BeforeInsert, UpdateDateColumn, CreateDateColumn, OneToOne, JoinColumn } from 'typeorm'; import { v4 } from 'uuid' +import { Role } from './role.entity'; @Entity({ name: 'users' }) @@ -26,6 +27,9 @@ export class User { @Column() name: string; + @Column() + role_id: string; + @Column() avatar: string; @@ -54,6 +58,10 @@ export class User { @CreateDateColumn() created_at: Date + @OneToOne(type => Role) + @JoinColumn({ name: "role_id" }) + role: Role; + @BeforeInsert() beforeInsert() { this.code = strRandom(6) diff --git a/src/main.ts b/src/main.ts index 1a3b12d..3d61dec 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,6 +5,7 @@ import { ApiKeyGuard } from './common/guards/api-key.guard'; import { AppModule } from './apps/app.module'; import { HttpExceptionFilter } from './common/filters/http-exception.filter'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; +import { ValidationPipe } from '@nestjs/common'; const APP_PORT = 3000; @@ -19,6 +20,9 @@ async function bootstrap() { app.useGlobalGuards(new ApiKeyGuard()); app.useGlobalFilters(new HttpExceptionFilter()) + app.useGlobalPipes(new ValidationPipe()); + + app.setViewEngine('ejs'); @@ -35,6 +39,9 @@ async function bootstrap() { .addBearerAuth({name: "AuthorizationToken", type: "http"}) .addBearerAuth({name: "RefreshToken", type: "http"}) .addTag('Auth', "All about authentication") + .addTag('Admin > Users', "User CRUD") + .addTag('Admin > Permissions', "Permissions CRUD") + .addTag('Admin > Roles', "Roles CRUD") .build(); const document = SwaggerModule.createDocument(app, config); diff --git a/src/shared/base-crud/base-crud.controller.ts b/src/shared/base-crud/base-crud.controller.ts new file mode 100644 index 0000000..a130e1e --- /dev/null +++ b/src/shared/base-crud/base-crud.controller.ts @@ -0,0 +1,38 @@ +import { Get, Post, Body, Patch, Param, Delete, Injectable } from '@nestjs/common'; +import { IBaseService } from './base-service.interface'; +import { ApiBody, ApiConsumes } from '@nestjs/swagger'; +import { CreateRoleDto } from 'src/admin/roles/dto/create-role.dto'; + +export abstract class BaseCrudController { + constructor(public readonly service: IBaseService) {} + + @Post() + create(@Body() createDTO: CreateDTO) { + return this.service.create(createDTO); + } + + @Get() + findAll() { + return this.service.findAll(); + } + + @Get(':id') + findOne(@Param('id') id: string) { + return this.service.findOne(+id); + } + + @Patch(':id') + update(@Param('id') id: string, @Body() updateDTO: UpdateDTO) { + return this.service.update(+id, updateDTO); + } + + @Delete(':id') + remove(@Param('id') id: string) { + return this.service.remove(+id); + } +} + + +class CreateDTO { + +} \ No newline at end of file diff --git a/src/shared/base-crud/base-crud.service.ts b/src/shared/base-crud/base-crud.service.ts new file mode 100644 index 0000000..a4b6e2a --- /dev/null +++ b/src/shared/base-crud/base-crud.service.ts @@ -0,0 +1,35 @@ +import { Repository } from "typeorm"; +import { IBaseService } from "./base-service.interface"; +import { InjectRepository } from "@nestjs/typeorm"; + +export abstract class BaseCrudService implements IBaseService { + constructor( + public roleRepository: Repository + ) {} + + get repo() { return this.roleRepository; } + + create(createRoleDto: A|any) { + const newRole = this.repo.create(createRoleDto); + return this.repo.save(newRole); + } + + findAll() { + return this.repo.find(); + } + + findOne(id: number) { + return this.repo.findOne({where: { + id, + }}); + } + + update(id: number, updateRoleDto: B | any ) { + return this.repo.update(id, updateRoleDto); + } + + remove(id: number) { + return this.repo.delete(id); + } + } + \ No newline at end of file diff --git a/src/shared/base-crud/base-service.interface.ts b/src/shared/base-crud/base-service.interface.ts new file mode 100644 index 0000000..5605b40 --- /dev/null +++ b/src/shared/base-crud/base-service.interface.ts @@ -0,0 +1,7 @@ +export interface IBaseService { + create(data: any): any; + findAll(): any; + findOne(id: number): any; + update(id: number, data:any ): any; + remove(id: number): any; +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 92c7cef..152abc9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1600,6 +1600,13 @@ accepts@~1.3.8: mime-types "~2.1.34" negotiator "0.6.3" +accesscontrol@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/accesscontrol/-/accesscontrol-2.2.1.tgz#c942c48e330841c619682309a8c3aeec6bec66eb" + integrity sha512-52EvFk/J9EF+w4mYQoKnOTkEMj01R1U5n2fc1dai6x1xkgOks3DGkx01qQL2cKFxGmE4Tn1krAU3jJA9L1NMkg== + dependencies: + notation "^1.3.6" + acorn-import-assertions@^1.9.0: version "1.9.0" resolved "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz" @@ -5530,6 +5537,14 @@ neo-async@^2.6.0, neo-async@^2.6.2: resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== +nest-access-control@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/nest-access-control/-/nest-access-control-3.1.0.tgz#0e35b2503f205ea80caf55bd5d0c53dc1df04a1b" + integrity sha512-rg8OWIcvA2gGiSjnl141RDg2B+R7YgyYgCl3D67NcK7/1TXoTJCdqSyFysYXzkiHiXZOvANKYkvke//p6Yzl8g== + dependencies: + accesscontrol "^2.2.1" + tslib "^2.6.2" + nestjs-i18n@^10.3.6: version "10.3.6" resolved "https://registry.yarnpkg.com/nestjs-i18n/-/nestjs-i18n-10.3.6.tgz#c666d0ce3ff89ed49e9944afbfa8200529843a7c" @@ -5627,6 +5642,11 @@ normalize-path@^3.0.0, normalize-path@~3.0.0: resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== +notation@^1.3.6: + version "1.3.6" + resolved "https://registry.yarnpkg.com/notation/-/notation-1.3.6.tgz#bc87b88d1f1159e2931e7f9317a3020313790321" + integrity sha512-DIuJmrP/Gg1DcXKaApsqcjsJD6jEccqKSfmU3BUx/f1GHsMiTJh70cERwYc64tOmTRTARCeMwkqNNzjh3AHhiw== + npm-run-path@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" @@ -7234,7 +7254,7 @@ tsconfig-paths@4.2.0, tsconfig-paths@^4.1.2, tsconfig-paths@^4.2.0: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@2.6.2: +tslib@2.6.2, tslib@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==