Skip to content

Commit

Permalink
add permissions mixin
Browse files Browse the repository at this point in the history
  • Loading branch information
holoyan committed Feb 26, 2024
1 parent f50c140 commit 5d35c41
Show file tree
Hide file tree
Showing 13 changed files with 522 additions and 9 deletions.
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"@japa/assert": "^2.1.0",
"@japa/runner": "^3.1.1",
"@swc/core": "^1.3.102",
"@types/luxon": "^3.4.2",
"@types/node": "^20.10.7",
"c8": "^9.0.0",
"copyfiles": "^2.4.1",
Expand All @@ -58,7 +59,8 @@
},
"peerDependencies": {
"@adonisjs/core": "^6.2.0",
"@adonisjs/lucid": "^20.1.0"
"@adonisjs/lucid": "^20.1.0",
"luxon": "^3.4.4"
},
"publishConfig": {
"access": "public",
Expand All @@ -82,7 +84,5 @@
"eslintConfig": {
"extends": "@adonisjs/eslint-config/package"
},
"prettier": "@adonisjs/prettier-config",
"dependencies": {
}
"prettier": "@adonisjs/prettier-config"
}
5 changes: 0 additions & 5 deletions providers/README.md

This file was deleted.

Empty file added providers/role_provider
Empty file.
36 changes: 36 additions & 0 deletions src/mixins/has_permissions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { NormalizeConstructor } from '@adonisjs/core/types/helpers'
import PermissionsService from '../services/permissions_service.js'
import { HasPermissions as HasPermissionsContract } from '../../types/has_roles.js'
import Permission from '../models/permission.js'

const HasPermissions = <
Model extends NormalizeConstructor<import('@adonisjs/lucid/types/model').LucidModel>,
>(
superclass: Model
) => {
class HasPermissionsMixin extends superclass implements HasPermissionsContract {
getMorphMapName(): string {
throw new Error(
'method getMorphMapName must be implemented in target model, which will return string alias for model class'
)
}

getModelId(): number {
throw new Error(
'method getModelId must be implemented in target model, which will return key for current object'
)
}

/**
* return all permissions including global, direct
*/
permissions(): Promise<Permission[] | null> {
const service = new PermissionsService()
return service.all(this.getMorphMapName(), this.getModelId())
}
}

return HasPermissionsMixin
}

export default HasPermissions
58 changes: 58 additions & 0 deletions src/mixins/has_roles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import Role from '../models/role.js'
import type { NormalizeConstructor } from '@adonisjs/core/types/helpers'
import RolesService from '../services/roles_service.js'
import { HasRoles as HasRolesContract } from '../../types/has_roles.js'

const HasRoles = <
Model extends NormalizeConstructor<import('@adonisjs/lucid/types/model').LucidModel>,
>(
superclass: Model
) => {
class HasRolesMixin extends superclass implements HasRolesContract {
getMorphMapName(): string {
throw new Error(
'method getMorphMapName must be implemented in target model, which will return string alias for model class'
)
}

getModelId(): number | null {
throw new Error(
'method getModelId must be implemented in target model, which will return key for current object'
)
}

roles(): Promise<Role[] | null> {
const service = new RolesService()
return service.all(this.getMorphMapName(), this.getModelId())
}

hasRole(role: string | Role) {
const service = new RolesService()
return service.has(this.getMorphMapName(), this.getModelId(), role)
}

hasAllRoles(role: (string | Role)[]) {
const service = new RolesService()
return service.hasAll(this.getMorphMapName(), this.getModelId(), role)
}

hasAnyRole(role: (string | Role)[]) {
const service = new RolesService()
return service.hasAny(this.getMorphMapName(), this.getModelId(), role)
}

assigneRole(role: string | Role) {
const service = new RolesService()
return service.assigne(role, this)
}

revokeRole(role: string | Role) {
const service = new RolesService()
return service.revoke(role, this)
}
}

return HasRolesMixin
}

export default HasRoles
27 changes: 27 additions & 0 deletions src/models/model_permission.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { DateTime } from 'luxon'
import { BaseModel, column, scope } from '@adonisjs/lucid/orm'

export default class ModelPermission extends BaseModel {
@column({ isPrimary: true })
declare id: number

@column()
declare permissionId: number

@column()
declare modelType: string

@column()
declare modelId: number

@column.dateTime({ autoCreate: true })
declare createdAt: DateTime

@column.dateTime({ autoCreate: true, autoUpdate: true })
declare updatedAt: DateTime

static forModel = scope((query, modelType: string, modelId: number | null) => {
query.where('model_type', modelType)
modelId === null ? query.whereNull('model_id') : query.where('model_id', modelId)
})
}
22 changes: 22 additions & 0 deletions src/models/model_role.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { DateTime } from 'luxon'
import { BaseModel, column } from '@adonisjs/lucid/orm'

export default class ModelRole extends BaseModel {
@column({ isPrimary: true })
declare id: number

@column()
declare roleId: number

@column()
declare modelType: string

@column()
declare modelId: number | null

@column.dateTime({ autoCreate: true })
declare createdAt: DateTime

@column.dateTime({ autoCreate: true, autoUpdate: true })
declare updatedAt: DateTime
}
31 changes: 31 additions & 0 deletions src/models/permission.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { DateTime } from 'luxon'
import { BaseModel, column } from '@adonisjs/lucid/orm'

export default class Permission extends BaseModel {
@column({ isPrimary: true })
declare id: number

@column()
declare slug: string

@column()
declare title: string

@column()
declare entityType: string

@column()
declare entityId: number | null

@column()
declare allowed: boolean

@column()
declare scope: number

@column.dateTime({ autoCreate: true })
declare createdAt: DateTime

@column.dateTime({ autoCreate: true, autoUpdate: true })
declare updatedAt: DateTime
}
31 changes: 31 additions & 0 deletions src/models/role.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { DateTime } from 'luxon'
import { BaseModel, column } from '@adonisjs/lucid/orm'

export default class Role extends BaseModel {
@column({ isPrimary: true })
declare id: number

@column()
declare slug: string

@column()
declare title: string

@column()
declare entityType: string

@column()
declare entityId: number | null

@column()
declare scope: number

@column()
declare allowed: boolean

@column.dateTime({ autoCreate: true })
declare createdAt: DateTime

@column.dateTime({ autoCreate: true, autoUpdate: true })
declare updatedAt: DateTime
}
129 changes: 129 additions & 0 deletions src/services/permissions_service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import Permission from '../models/permission.js'

export default class PermissionsService {
/**
* return all permissions
*/
async all(modelType: string, modelId: number) {
const p = await this.permissionQuery(modelType, modelId)
.groupBy('permissions.id')
.select('permissions.*')

return p
}

/**
* return only global assigned permissions, through role or direct
*/
async global(modelType: string, modelId: number) {
const p = await this.permissionQuery(modelType, modelId)
.where('permissions.entity_type', '*')
.whereNull('permissions.entity_id')
.groupBy('permissions.id')
.select('permissions.*')

return p
}

/**
* get all permissions which is assigned to concrete resource
*/
async onResource(modelType: string, modelId: number) {
const p = await this.permissionQuery(modelType, modelId)
.where('permissions.entity_type', '*')
.whereNotNull('permissions.entity_id')
.groupBy('permissions.id')
.select('permissions.*')

return p
}

/**
* all direct permissions
*/
async direct(modelType: string, modelId: number) {
const p = await this.directPermissionQuery(modelType, modelId)
.groupBy('permissions.id')
.select('permissions.*')

return p
}

/**
* return direct and resource assigned permissions
*/
directResource() {}

/**
* check if it has permission
*/
has() {}

/**
* has all permissions
*/
hasAll() {}

/**
* has any of permissions
*/
hasAny() {}

/**
* give permission to model
*/
give() {}

/**
* give permissions to model
*/
giveAll() {}

/**
* sync permissions, remove everything outside of the list
*/
sync() {}

/**
* forbid permission on model
*/
forbid() {}

/**
* to remove forbidden permission on model
*/
unforbid() {}

/**
* check if permission is forbidden
*/
forbidden() {}

private permissionQuery(modelType: string, modelId: number) {
const q = Permission.query()
.join('model_permissions as mp', 'mp.permission_id', '=', 'permissions.id')
.join('model_roles as mr', (joinQuery) => {
joinQuery.onVal('mr.model_type', modelType).onVal('mr.model_id', modelId)
})
.where((subQuery) => {
subQuery
.where((query) => {
query.where('mp.model_type', modelType)
modelId === null ? query.whereNull('mp.model_id') : query.where('mp.model_id', modelId)
})
.orWhere((query) => {
query.whereRaw('mr.role_id=mp.model_id').where('mp.model_type', 'roles')
})
})

return q
}
private directPermissionQuery(modelType: string, modelId: number) {
const q = Permission.query()
.join('model_permissions as mp', 'mp.permission_id', '=', 'permissions.id')
.where('mp.model_type', modelType)
.where('mp.model_id', modelId)

return q
}
}
Loading

0 comments on commit 5d35c41

Please sign in to comment.