feat(SPG-124) Authorization

pull/2/head
ashar 2024-06-03 16:13:11 +07:00
parent 34ea6a501d
commit f5c1008ece
16 changed files with 364 additions and 27 deletions

View File

@ -1,6 +1,5 @@
import { Module, Scope } from '@nestjs/common'; import { Module, Scope } from '@nestjs/common';
import { RefreshTokenInterceptor, SessionModule } from './core/sessions'; import { RefreshTokenInterceptor, SessionModule } from './core/sessions';
import { AuthModule } from './auth/auth.module';
import { JWTGuard } from './core/guards'; import { JWTGuard } from './core/guards';
import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core'; import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core';
import { HttpExceptionFilter, TransformInterceptor } from './core/response'; import { HttpExceptionFilter, TransformInterceptor } from './core/response';
@ -9,9 +8,14 @@ import { CONNECTION_NAME } from './core/strings/constants/base.constants';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config'; import { ConfigModule } from '@nestjs/config';
import { UserPrivilegeModule } from './modules/user-related/user-privilege/user-privilege.module'; import { UserPrivilegeModule } from './modules/user-related/user-privilege/user-privilege.module';
import { UserPrivilegeModel } from './modules/user-related/user-privilege/data/model/user-privilege.model';
import { CqrsModule } from '@nestjs/cqrs'; import { CqrsModule } from '@nestjs/cqrs';
import { CouchModule } from './modules/configuration/couch/couch.module'; import { CouchModule } from './modules/configuration/couch/couch.module';
import { UserPrivilegeModels } from './modules/user-related/user-privilege/constants';
import { RolesGuard } from './core/guards/domain/roles.guard';
import { PrivilegeService } from './core/guards/domain/services/privilege.service';
import { UserModel } from './modules/user-related/user/data/models/user.model';
import { AuthModule } from './modules/configuration/auth/auth.module';
import { UserModule } from './modules/user-related/user/user.module';
@Module({ @Module({
imports: [ imports: [
@ -27,18 +31,20 @@ import { CouchModule } from './modules/configuration/couch/couch.module';
username: process.env.DEFAULT_DB_USER, username: process.env.DEFAULT_DB_USER,
password: process.env.DEFAULT_DB_PASS, password: process.env.DEFAULT_DB_PASS,
database: process.env.DEFAULT_DB_NAME, database: process.env.DEFAULT_DB_NAME,
entities: [UserPrivilegeModel], entities: [...UserPrivilegeModels, UserModel],
synchronize: true, synchronize: false,
}), }),
CqrsModule, CqrsModule,
SessionModule, SessionModule,
AuthModule, AuthModule,
CouchModule, CouchModule,
UserModule,
UserPrivilegeModule, UserPrivilegeModule,
], ],
controllers: [], controllers: [],
providers: [ providers: [
PrivilegeService,
/** /**
* By default all request from client will protect by JWT * By default all request from client will protect by JWT
* if there is some endpoint/function that does'nt require authentication * if there is some endpoint/function that does'nt require authentication
@ -47,7 +53,7 @@ import { CouchModule } from './modules/configuration/couch/couch.module';
{ {
provide: APP_GUARD, provide: APP_GUARD,
scope: Scope.REQUEST, scope: Scope.REQUEST,
useClass: JWTGuard, useClass: RolesGuard,
}, },
{ {
provide: APP_INTERCEPTOR, provide: APP_INTERCEPTOR,

View File

@ -1 +1,3 @@
export const UNPROTECTED_URL = 'unprotected_url'; export const UNPROTECTED_URL = 'unprotected_url';
export const PRIVILEGE_KEY = 'privilege_key';
export const MAIN_MENU = 'main_menu';

View File

@ -1,7 +1,9 @@
import { SetMetadata } from '@nestjs/common'; import { SetMetadata } from '@nestjs/common';
import { UNPROTECTED_URL } from '../../constants'; import { MAIN_MENU, UNPROTECTED_URL } from '../../constants';
/** /**
* @deprecated
* Use Public instead
* This decorator will exclude the request from token check * This decorator will exclude the request from token check
* *
* NOTE: * NOTE:
@ -11,3 +13,9 @@ import { UNPROTECTED_URL } from '../../constants';
*/ */
export const Unprotected = (isUnprotected = true) => export const Unprotected = (isUnprotected = true) =>
SetMetadata(UNPROTECTED_URL, isUnprotected); SetMetadata(UNPROTECTED_URL, isUnprotected);
export const Public = (isUnprotected = true) =>
SetMetadata(UNPROTECTED_URL, isUnprotected);
export const MainMenu = () => SetMetadata(MAIN_MENU, true);
export const ExcludePrivilege = () => SetMetadata(MAIN_MENU, true);

View File

@ -2,22 +2,25 @@ import {
Injectable, Injectable,
CanActivate, CanActivate,
ExecutionContext, ExecutionContext,
UnauthorizedException,
Scope, Scope,
Logger, Logger,
UnauthorizedException,
} from '@nestjs/common'; } from '@nestjs/common';
import { Reflector } from '@nestjs/core'; import { Reflector } from '@nestjs/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { SessionService, UsersSession } from 'src/core/sessions'; import { SessionService, UsersSession } from 'src/core/sessions';
import { UNPROTECTED_URL } from '../constants'; import { UNPROTECTED_URL } from '../constants';
import { PrivilegeService } from './services/privilege.service';
@Injectable({ scope: Scope.REQUEST }) @Injectable({ scope: Scope.REQUEST })
export class JWTGuard implements CanActivate { export class JWTGuard implements CanActivate {
constructor( constructor(
protected readonly session: SessionService, protected readonly session: SessionService,
protected readonly reflector: Reflector, protected readonly reflector: Reflector,
protected readonly privilege: PrivilegeService,
) {} ) {}
protected isPublic = false;
protected userSession: UsersSession; protected userSession: UsersSession;
canActivate( canActivate(
@ -31,6 +34,8 @@ export class JWTGuard implements CanActivate {
UNPROTECTED_URL, UNPROTECTED_URL,
[context.getHandler(), context.getClass()], [context.getHandler(), context.getClass()],
); );
this.isPublic = isUnprotected;
this.session.setPublic(isUnprotected);
if (isUnprotected) return true; if (isUnprotected) return true;
/** /**

View File

@ -1,23 +1,36 @@
import { Injectable, ExecutionContext } from '@nestjs/common'; import {
import { Observable } from 'rxjs'; Injectable,
ExecutionContext,
ForbiddenException,
} from '@nestjs/common';
import { JWTGuard } from './jwt.guard'; import { JWTGuard } from './jwt.guard';
import { MAIN_MENU } from '../constants';
@Injectable() @Injectable()
export class RolesGuard extends JWTGuard { export class RolesGuard extends JWTGuard {
canActivate( async canActivate(context: ExecutionContext): Promise<boolean> {
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
super.canActivate(context); super.canActivate(context);
/** // jika endpoint tersebut bukan public, maka lakukan check lanjutan
* Create function to check if `this.userSession` have access if (!this.isPublic) {
* to Read / Create / Update / and Other Action // Check apakah endpoint ada decorator untuk exlude privilege (@ExcludePrivilege())
*/ const excludePrivilege = this.reflector.getAllAndOverride<boolean>(
MAIN_MENU,
[context.getHandler(), context.getClass()],
);
if (excludePrivilege) return true;
// check apakah dapat akses module
const isNotAllow = await this.privilege.isNotAllowed();
if (isNotAllow) {
throw new ForbiddenException({
statusCode: 10003,
message: `Forbidden Access, you don't have access to this module!`,
error: 'ACCESS_FORBIDDEN',
});
}
}
/**
* Assign rules to session, So Query can take the rules and give
* the data base on user request
*/
return true; return true;
} }
} }

View File

@ -0,0 +1,74 @@
import { ForbiddenException, Inject, Injectable, Scope } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { Request } from 'express';
import { InjectDataSource } from '@nestjs/typeorm';
import { getAction } from 'src/core/helpers/path/get-action-from-path.helper';
import { UserProvider } from 'src/core/sessions';
import { CONNECTION_NAME } from 'src/core/strings/constants/base.constants';
import { UserPrivilegeConfigurationModel } from 'src/modules/user-related/user-privilege/data/models/user-privilege-configuration.model';
import { DataSource, IsNull } from 'typeorm';
import { UserRole } from 'src/modules/user-related/user/constants';
import { UserPrivilegeConfigurationEntity } from 'src/modules/user-related/user-privilege/domain/entities/user-privilege-configuration.entity';
@Injectable({ scope: Scope.REQUEST })
export class PrivilegeService {
constructor(
@InjectDataSource(CONNECTION_NAME.DEFAULT)
protected readonly dataSource: DataSource,
@Inject(REQUEST) private readonly request: Request,
protected readonly session: UserProvider,
) {}
get repository() {
return this.dataSource.getRepository(UserPrivilegeConfigurationModel);
}
get user() {
return this.session.user;
}
get action() {
const headerAction = this.request.headers['ex-model-action'] as string;
return headerAction ?? getAction(this.request.method, this.request.path);
}
async isAllowed() {
// jika rolenya adalah superadmin, abaikan dan return true
if (this.user.role == UserRole.SUPERADMIN) return true;
// check privilege dan sesuaikan dengan akse
const configurations = await this.privilegeConfiguration();
return configurations[this.action];
}
async isNotAllowed() {
return !(await this.isAllowed());
}
private moduleKey() {
const headerKey = 'ex-model-key';
const moduleKey = this.request.headers[headerKey] as string;
if (!moduleKey) {
throw new ForbiddenException({
statusCode: 10005,
message: `Forbidden Access, access Module is Require!`,
error: 'MODULE_KEY_NOT_FOUND',
});
}
const [module, menu, sub_menu, section] = moduleKey.split('.');
return { module, menu, sub_menu, section };
}
async privilegeConfiguration(): Promise<UserPrivilegeConfigurationEntity> {
const { module, menu, sub_menu, section } = this.moduleKey();
return await this.repository.findOne({
select: ['id', 'view', 'create', 'edit', 'delete', 'cancel', 'confirm'],
where: {
user_privilege_id: this.user.user_privilege_id,
module: module,
menu: menu ?? IsNull(),
},
});
}
}

View File

@ -0,0 +1,28 @@
import { PrivilegeAction } from 'src/core/strings/constants/privilege.constants';
function containsUuid(str) {
const parts = str.split('/'); // Split the string by "/"
for (const part of parts) {
if (
/^[0-9a-f]{8}\b-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
part,
)
) {
return true;
}
}
return false;
}
export function getAction(method: string, path: string): string {
if (method === 'GET') return PrivilegeAction.VIEW;
else if (method === 'POST') return PrivilegeAction.CREATE;
else if (method === 'DELETE') return PrivilegeAction.DELETE;
else if (method === 'PATCH' || method === 'PUT') {
if (['confirm', 'active', 'inactive'].includes(path))
return PrivilegeAction.CONFIRM;
else if (path.includes('cancel')) return PrivilegeAction.CANCEL;
else return PrivilegeAction.EDIT;
}
return 'forbidden';
}

View File

@ -1,4 +1,8 @@
import { UserRole } from 'src/modules/user-related/user/constants';
export interface UsersSession { export interface UsersSession {
id: number; id: number;
name: string; name: string;
role: UserRole;
user_privilege_id: string;
} }

View File

@ -6,7 +6,23 @@ import { isTokenNearExpired } from '../utils/jwt.helpers';
@Injectable({ scope: Scope.REQUEST }) @Injectable({ scope: Scope.REQUEST })
export class SessionService { export class SessionService {
private public = false;
public ignorePrivilegeCondition = false;
constructor(private readonly jwt: JwtService) {} constructor(private readonly jwt: JwtService) {}
setPublic(value: boolean) {
this.public = value;
}
get isPublic() {
return this.public;
}
get isPrivate() {
return !this.isPublic;
}
createAccessToken(session: UsersSession): string { createAccessToken(session: UsersSession): string {
return this.jwt.sign(session); return this.jwt.sign(session);
} }

View File

@ -1,8 +1,8 @@
import { Global, Module } from '@nestjs/common'; import { Global, Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt'; import { JwtModule } from '@nestjs/jwt';
import { JWT_EXPIRED, JWT_SECRET } from '../../auth/constants';
import { UserProvider } from './domain/providers/user'; import { UserProvider } from './domain/providers/user';
import { SessionService } from './domain/services/session.service'; import { SessionService } from './domain/services/session.service';
import { JWT_EXPIRED, JWT_SECRET } from './constants';
@Global() @Global()
@Module({ @Module({

View File

@ -42,4 +42,6 @@ export enum OPERATION {
export const BLANK_USER = { export const BLANK_USER = {
id: null, id: null,
name: null, name: null,
role: null,
user_privilege_id: null,
}; };

View File

@ -0,0 +1,178 @@
export enum PrivilegeAction {
VIEW = 'view',
CREATE = 'create',
EDIT = 'edit',
DELETE = 'delete',
CANCEL = 'cancel',
CONFIRM = 'confirm',
}
export const PrivilegeAdminConstant = [
{
menu: 'DASHBOARD',
menu_label: 'Dashboard',
actions: [PrivilegeAction.VIEW],
index: 1,
},
{
menu: 'CALENDAR',
menu_label: 'Kalender',
actions: [PrivilegeAction.VIEW],
index: 2,
},
{
menu: 'BOOKING',
menu_label: 'Pemesanan',
actions: [
PrivilegeAction.VIEW,
PrivilegeAction.CREATE,
PrivilegeAction.CONFIRM,
PrivilegeAction.DELETE,
PrivilegeAction.CANCEL,
PrivilegeAction.EDIT,
],
index: 3,
},
{
menu: 'REFUND',
menu_label: 'Pengembalian',
actions: [
PrivilegeAction.VIEW,
PrivilegeAction.CREATE,
PrivilegeAction.CONFIRM,
PrivilegeAction.DELETE,
PrivilegeAction.CANCEL,
PrivilegeAction.EDIT,
],
index: 4,
},
{
menu: 'REKONSILIASI',
menu_label: 'Rekonsiliasi',
actions: [
PrivilegeAction.VIEW,
PrivilegeAction.CREATE,
PrivilegeAction.CONFIRM,
PrivilegeAction.DELETE,
PrivilegeAction.CANCEL,
PrivilegeAction.EDIT,
],
index: 5,
},
{
menu: 'ITEM',
menu_label: 'Item',
actions: [
PrivilegeAction.VIEW,
PrivilegeAction.CREATE,
PrivilegeAction.EDIT,
PrivilegeAction.DELETE,
],
index: 6,
},
{
menu: 'RATE_TYPE',
menu_label: 'Tipe Rate',
actions: [
PrivilegeAction.VIEW,
PrivilegeAction.CREATE,
PrivilegeAction.EDIT,
PrivilegeAction.DELETE,
],
index: 7,
},
{
menu: 'USER',
menu_label: 'Pengguna',
actions: [
PrivilegeAction.VIEW,
PrivilegeAction.CREATE,
PrivilegeAction.EDIT,
PrivilegeAction.DELETE,
],
index: 8,
},
{
menu: 'TENANT',
menu_label: 'Tenant',
actions: [
PrivilegeAction.VIEW,
PrivilegeAction.CREATE,
PrivilegeAction.EDIT,
PrivilegeAction.DELETE,
],
index: 9,
},
{
menu: 'WEB_INFORMATION',
menu_label: 'Informasi Web',
actions: [
PrivilegeAction.VIEW,
PrivilegeAction.CREATE,
PrivilegeAction.EDIT,
PrivilegeAction.DELETE,
],
index: 10,
},
{
menu: 'SETTING',
menu_label: 'Setting',
actions: [
PrivilegeAction.VIEW,
PrivilegeAction.CREATE,
PrivilegeAction.EDIT,
PrivilegeAction.DELETE,
],
index: 11,
},
{
menu: 'LAPORAN',
menu_label: 'Laporan',
actions: [PrivilegeAction.VIEW],
index: 12,
},
{
menu: 'DISKON_CODE',
menu_label: 'Generate Diskon Kode',
actions: [PrivilegeAction.CREATE],
index: 13,
},
];
export const PrivilegePOSConstant = [
{
menu: 'SALES',
menu_label: 'Penjualan',
actions: [
PrivilegeAction.VIEW,
PrivilegeAction.CREATE,
PrivilegeAction.DELETE,
PrivilegeAction.EDIT,
],
index: 14,
},
{
menu: 'QR_PRINT',
menu_label: 'Print QR',
actions: [PrivilegeAction.VIEW, PrivilegeAction.CREATE],
index: 15,
},
{
menu: 'BOOKING',
menu_label: 'Pemesanan',
actions: [PrivilegeAction.VIEW, PrivilegeAction.CREATE],
index: 16,
},
{
menu: 'WITHDRAW',
menu_label: 'Penarikan Kas',
actions: [PrivilegeAction.VIEW, PrivilegeAction.CREATE],
index: 17,
},
{
menu: 'POS_DISKON_CODE',
menu_label: 'Generate Diskon Kode',
actions: [PrivilegeAction.CREATE],
index: 18,
},
];

View File

@ -9,7 +9,7 @@ export class AuthController {
constructor(private orchestrator: AuthOrchestrator) {} constructor(private orchestrator: AuthOrchestrator) {}
@Post() @Post()
@Public(false) @Public(true)
async login(@Body() body: LoginDto) { async login(@Body() body: LoginDto) {
return await this.orchestrator.login(body); return await this.orchestrator.login(body);
} }

View File

@ -1,11 +1,10 @@
import { Body, Controller, Get, Param, Put, Query } from '@nestjs/common'; import { Body, Controller, Get, Param, Put, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger'; import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { Public } from 'src/core/guards'; import { Public } from 'src/core/guards';
import { MODULE_NAME } from 'src/core/strings/constants/module.constants'; import { MODULE_NAME } from 'src/core/strings/constants/module.constants';
import { UserPrivilegeConfigurationDataOrchestrator } from '../domain/usecases/user-privilege-configuration/user-privilege-configuration-data.orchestrator'; import { UserPrivilegeConfigurationDataOrchestrator } from '../domain/usecases/user-privilege-configuration/user-privilege-configuration-data.orchestrator';
import { UserPrivilegeConfigurationDto } from './dto/user-privilege-configuration.dto'; import { UserPrivilegeConfigurationDto } from './dto/user-privilege-configuration.dto';
import { UserPrivilegeConfigurationEntity } from '../domain/entities/user-privilege-configuration.entity'; import { UserPrivilegeConfigurationEntity } from '../domain/entities/user-privilege-configuration.entity';
import { Pagination } from 'src/core/response';
import { FilterUserPrivilegeConfigurationDto } from './dto/filter-user-privilege-configuration.dto'; import { FilterUserPrivilegeConfigurationDto } from './dto/filter-user-privilege-configuration.dto';
import { PaginationResponse } from 'src/core/response/domain/ok-response.interface'; import { PaginationResponse } from 'src/core/response/domain/ok-response.interface';
@ -14,6 +13,7 @@ import { PaginationResponse } from 'src/core/response/domain/ok-response.interfa
) )
@Controller(MODULE_NAME.USER_PRIVILEGE_CONFIGURATION) @Controller(MODULE_NAME.USER_PRIVILEGE_CONFIGURATION)
@Public(false) @Public(false)
@ApiBearerAuth('JWT')
export class UserPrivilegeConfigurationController { export class UserPrivilegeConfigurationController {
constructor( constructor(
private orchestrator: UserPrivilegeConfigurationDataOrchestrator, private orchestrator: UserPrivilegeConfigurationDataOrchestrator,

View File

@ -10,7 +10,7 @@ import {
import { UserPrivilegeDataOrchestrator } from '../domain/usecases/user-privilege/user-privilege-data.orchestrator'; import { UserPrivilegeDataOrchestrator } from '../domain/usecases/user-privilege/user-privilege-data.orchestrator';
import { CreateUserPrivilegeDto } from './dto/create-user-privilege.dto'; import { CreateUserPrivilegeDto } from './dto/create-user-privilege.dto';
import { MODULE_NAME } from 'src/core/strings/constants/module.constants'; import { MODULE_NAME } from 'src/core/strings/constants/module.constants';
import { ApiTags } from '@nestjs/swagger'; import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { UserPrivilegeEntity } from '../domain/entities/user-privilege.entity'; import { UserPrivilegeEntity } from '../domain/entities/user-privilege.entity';
import { BatchResult } from 'src/core/response/domain/ok-response.interface'; import { BatchResult } from 'src/core/response/domain/ok-response.interface';
import { BatchIdsDto } from 'src/core/modules/infrastructure/dto/base-batch.dto'; import { BatchIdsDto } from 'src/core/modules/infrastructure/dto/base-batch.dto';
@ -19,6 +19,7 @@ import { Public } from 'src/core/guards';
@ApiTags(`${MODULE_NAME.USER_PRIVILEGE.split('-').join(' ')} - data`) @ApiTags(`${MODULE_NAME.USER_PRIVILEGE.split('-').join(' ')} - data`)
@Controller(MODULE_NAME.USER_PRIVILEGE) @Controller(MODULE_NAME.USER_PRIVILEGE)
@Public(false) @Public(false)
@ApiBearerAuth('JWT')
export class UserPrivilegeDataController { export class UserPrivilegeDataController {
constructor(private orchestrator: UserPrivilegeDataOrchestrator) {} constructor(private orchestrator: UserPrivilegeDataOrchestrator) {}

View File

@ -4,19 +4,19 @@ import { Pagination } from 'src/core/response';
import { PaginationResponse } from 'src/core/response/domain/ok-response.interface'; import { PaginationResponse } from 'src/core/response/domain/ok-response.interface';
import { UserPrivilegeEntity } from '../domain/entities/user-privilege.entity'; import { UserPrivilegeEntity } from '../domain/entities/user-privilege.entity';
import { UserPrivilegeReadOrchestrator } from '../domain/usecases/user-privilege/user-privilege-read.orchestrator'; import { UserPrivilegeReadOrchestrator } from '../domain/usecases/user-privilege/user-privilege-read.orchestrator';
import { ApiTags } from '@nestjs/swagger'; import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { MODULE_NAME } from 'src/core/strings/constants/module.constants'; import { MODULE_NAME } from 'src/core/strings/constants/module.constants';
import { ExcludePrivilege, Public } from 'src/core/guards'; import { ExcludePrivilege, Public } from 'src/core/guards';
@ApiTags(`${MODULE_NAME.USER_PRIVILEGE.split('-').join(' ')} - read`) @ApiTags(`${MODULE_NAME.USER_PRIVILEGE.split('-').join(' ')} - read`)
@Controller(MODULE_NAME.USER_PRIVILEGE) @Controller(MODULE_NAME.USER_PRIVILEGE)
@Public(false) @Public(false)
@ApiBearerAuth('JWT')
export class UserPrivilegeReadController { export class UserPrivilegeReadController {
constructor(private orchestrator: UserPrivilegeReadOrchestrator) {} constructor(private orchestrator: UserPrivilegeReadOrchestrator) {}
@Get() @Get()
@Pagination() @Pagination()
@ExcludePrivilege()
async index( async index(
@Query() params: FilterUserPrivilegeDto, @Query() params: FilterUserPrivilegeDto,
): Promise<PaginationResponse<UserPrivilegeEntity>> { ): Promise<PaginationResponse<UserPrivilegeEntity>> {