mirror of
https://github.com/sergeyyarkov/educt-server.git
synced 2026-10-11 10:19:20 +03:00
feat: added adonisjs bouncer
This commit is contained in:
parent
00c2db0697
commit
077f1f91a6
@ -5,7 +5,8 @@
|
||||
"@adonisjs/core/commands",
|
||||
"@adonisjs/repl/build/commands",
|
||||
"@adonisjs/lucid/build/commands",
|
||||
"@adonisjs/mail/build/commands"
|
||||
"@adonisjs/mail/build/commands",
|
||||
"@adonisjs/bouncer/build/commands"
|
||||
],
|
||||
"exceptionHandlerNamespace": "App/Exceptions/Handler",
|
||||
"namespaces": {
|
||||
@ -26,7 +27,8 @@
|
||||
"preloads": [
|
||||
"./routes/index",
|
||||
"./start/kernel",
|
||||
"./start/event"
|
||||
"./start/event",
|
||||
"./start/bouncer"
|
||||
],
|
||||
"providers": [
|
||||
"./providers/AppProvider",
|
||||
@ -37,7 +39,8 @@
|
||||
"@adonisjs/redis",
|
||||
"@adonisjs/mail",
|
||||
"@adonisjs/view",
|
||||
"@adonisjs/attachment-lite"
|
||||
"@adonisjs/attachment-lite",
|
||||
"@adonisjs/bouncer"
|
||||
],
|
||||
"aceProviders": [
|
||||
"@adonisjs/repl"
|
||||
|
||||
@ -278,6 +278,42 @@
|
||||
],
|
||||
"aliases": [],
|
||||
"flags": []
|
||||
},
|
||||
"make:policy": {
|
||||
"settings": {},
|
||||
"commandPath": "@adonisjs/bouncer/build/commands/MakePolicy",
|
||||
"commandName": "make:policy",
|
||||
"description": "Make a new bouncer policy",
|
||||
"args": [
|
||||
{
|
||||
"type": "string",
|
||||
"propertyName": "name",
|
||||
"name": "name",
|
||||
"required": true,
|
||||
"description": "Name of the policy to create"
|
||||
}
|
||||
],
|
||||
"aliases": [],
|
||||
"flags": [
|
||||
{
|
||||
"name": "resource-model",
|
||||
"propertyName": "resourceModel",
|
||||
"type": "string",
|
||||
"description": "Name of the resource model to authorize"
|
||||
},
|
||||
{
|
||||
"name": "user-model",
|
||||
"propertyName": "userModel",
|
||||
"type": "string",
|
||||
"description": "Name of the user model to be authorized"
|
||||
},
|
||||
{
|
||||
"name": "actions",
|
||||
"propertyName": "actions",
|
||||
"type": "array",
|
||||
"description": "Actions to implement"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"aliases": {}
|
||||
|
||||
@ -78,7 +78,7 @@ export default class UsersController extends BaseController {
|
||||
|
||||
public async update(ctx: HttpContextContract) {
|
||||
const payload = await ctx.request.validate(UpdateUserValidator);
|
||||
const result = await this.userService.updateUser(ctx.params.id, payload);
|
||||
const result = await this.userService.updateUser(ctx.params.id, payload, ctx);
|
||||
|
||||
if (!result.success && result.error) {
|
||||
throw new Exception(result.message, result.status, result.error.code);
|
||||
|
||||
@ -134,38 +134,32 @@ export default class UserRepository {
|
||||
*/
|
||||
public async update(id: number | string, data: UpdateUserValidator['schema']['props']): Promise<User | null> {
|
||||
const user = await this.User.query().preload('contacts').preload('roles').where('id', id).first();
|
||||
const { role: roleSlug, ...updatedFields } = data;
|
||||
const { role, ...updatedFields } = data;
|
||||
|
||||
if (user) {
|
||||
/**
|
||||
* Update fields
|
||||
*/
|
||||
user.merge(updatedFields);
|
||||
|
||||
/**
|
||||
* Attach new single role
|
||||
*/
|
||||
if (roleSlug) {
|
||||
const role = await this.Role.query().where('slug', roleSlug).first();
|
||||
|
||||
if (role) {
|
||||
await user.related('roles').detach();
|
||||
await user.related('roles').attach([role.id]);
|
||||
await user.load('roles');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save updated user in database
|
||||
*/
|
||||
await user.save();
|
||||
|
||||
await user.merge(updatedFields).save();
|
||||
return user;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrite all user roles
|
||||
*
|
||||
* @param user User
|
||||
* @param roles Updated roles
|
||||
* @returns Updated user roles
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
public async updateRoles(user: User, roles: Role[]): Promise<Role[]> {
|
||||
await user.related('roles').detach();
|
||||
await user.related('roles').attach(roles.map(role => role.id));
|
||||
await user.load('roles');
|
||||
|
||||
return user.roles.map(role => role);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete user
|
||||
*
|
||||
|
||||
@ -27,8 +27,6 @@ import UserRepository from 'App/Repositories/UserRepository';
|
||||
*/
|
||||
import CreateUserValidator from 'App/Validators/User/CreateUserValidator';
|
||||
import UpdateUserValidator from 'App/Validators/User/UpdateUserValidator';
|
||||
import RoleEnum from 'App/Datatypes/Enums/RoleEnum';
|
||||
import RoleHelper from 'App/Helpers/RoleHelper';
|
||||
|
||||
@inject()
|
||||
export default class UserService {
|
||||
@ -97,19 +95,12 @@ export default class UserService {
|
||||
* @returns Response
|
||||
*/
|
||||
public async createUser(data: CreateUserValidator['schema']['props'], ctx: HttpContextContract): Promise<IResponse> {
|
||||
const user = await ctx.auth.use('api').authenticate();
|
||||
|
||||
/**
|
||||
* Load current user roles
|
||||
*/
|
||||
await user.load('roles');
|
||||
|
||||
const role = await this.roleRepository.getBySlug(data.role);
|
||||
|
||||
/**
|
||||
* Cannot find role
|
||||
*/
|
||||
if (role === null) {
|
||||
if (!role) {
|
||||
return {
|
||||
success: false,
|
||||
status: HttpStatusEnum.NOT_FOUND,
|
||||
@ -121,16 +112,10 @@ export default class UserService {
|
||||
};
|
||||
}
|
||||
|
||||
/* TODO need to check the role of the authenticated user before creating a new user,
|
||||
so we can may be move this into a separate middleware
|
||||
*/
|
||||
/**
|
||||
* Check for authenticaded user roles
|
||||
* Check user permissions
|
||||
*/
|
||||
if (
|
||||
(role.slug === RoleEnum.ADMIN || role.slug === RoleEnum.TEACHER) &&
|
||||
!RoleHelper.userContainRoles(user.roles, [RoleEnum.ADMIN])
|
||||
) {
|
||||
if (await ctx.bouncer.denies('manageUserRole', role)) {
|
||||
return {
|
||||
success: false,
|
||||
status: HttpStatusEnum.FORBIDDEN,
|
||||
@ -145,19 +130,19 @@ export default class UserService {
|
||||
/**
|
||||
* Create new user
|
||||
*/
|
||||
const createdUser = await this.userRepository.create(data);
|
||||
const user = await this.userRepository.create(data);
|
||||
|
||||
/**
|
||||
* Attach role
|
||||
*/
|
||||
await createdUser.related('roles').attach([role.id]);
|
||||
await createdUser.load('roles');
|
||||
await user.related('roles').attach([role.id]);
|
||||
await user.load('roles');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
status: HttpStatusEnum.CREATED,
|
||||
message: 'User created.',
|
||||
data: createdUser,
|
||||
data: user,
|
||||
};
|
||||
}
|
||||
|
||||
@ -168,7 +153,11 @@ export default class UserService {
|
||||
* @param data Data to update
|
||||
* @returns Response
|
||||
*/
|
||||
public async updateUser(id: string | number, data: UpdateUserValidator['schema']['props']): Promise<IResponse> {
|
||||
public async updateUser(
|
||||
id: string | number,
|
||||
data: UpdateUserValidator['schema']['props'],
|
||||
ctx: HttpContextContract
|
||||
): Promise<IResponse> {
|
||||
const user = await this.userRepository.update(id, data);
|
||||
|
||||
if (!user) {
|
||||
@ -183,6 +172,29 @@ export default class UserService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user role
|
||||
*/
|
||||
if (data.role) {
|
||||
const role = await this.roleRepository.getBySlug(data.role);
|
||||
|
||||
if (role) {
|
||||
if (await ctx.bouncer.denies('manageUserRole', role)) {
|
||||
return {
|
||||
success: false,
|
||||
status: HttpStatusEnum.FORBIDDEN,
|
||||
message: 'You dont have permissions to permorm that action',
|
||||
data: {},
|
||||
error: {
|
||||
code: 'E_FORBIDDEN',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
await this.userRepository.updateRoles(user, [role]);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
status: HttpStatusEnum.OK,
|
||||
|
||||
16
contracts/bouncer.ts
Normal file
16
contracts/bouncer.ts
Normal file
@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Contract source: https://git.io/Jte3v
|
||||
*
|
||||
* Feel free to let us know via PR, if you find something broken in this config
|
||||
* file.
|
||||
*/
|
||||
|
||||
import { actions, policies } from '../start/bouncer';
|
||||
|
||||
declare module '@ioc:Adonis/Addons/Bouncer' {
|
||||
type ApplicationActions = ExtractActionsTypes<typeof actions>;
|
||||
type ApplicationPolicies = ExtractPoliciesTypes<typeof policies>;
|
||||
|
||||
interface ActionsList extends ApplicationActions {}
|
||||
interface PoliciesList extends ApplicationPolicies {}
|
||||
}
|
||||
39
package-lock.json
generated
39
package-lock.json
generated
@ -285,6 +285,45 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@adonisjs/bouncer": {
|
||||
"version": "2.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@adonisjs/bouncer/-/bouncer-2.2.5.tgz",
|
||||
"integrity": "sha512-f060bMQwjJBAQ2pTtUrCbJ3n3/J3nQvzdrpWpWSEzdn8bdZALGh2R1nIhH0cVh8lMQuXzowo9ojxjWGFI0LfmQ==",
|
||||
"requires": {
|
||||
"@poppinss/utils": "^3.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@poppinss/utils": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@poppinss/utils/-/utils-3.3.1.tgz",
|
||||
"integrity": "sha512-k4MFt+4JhOWJZ9D2xpEcG/mpZyXVXYT+dSOg83vHK1xhXl+7r0IYBXRKWX2+To7/90KJaWlwpcdCAalXE8Debg==",
|
||||
"requires": {
|
||||
"@types/bytes": "^3.1.1",
|
||||
"@types/he": "^1.1.2",
|
||||
"buffer-alloc": "^1.2.0",
|
||||
"bytes": "^3.1.0",
|
||||
"change-case": "^4.1.2",
|
||||
"cuid": "^2.1.8",
|
||||
"flattie": "^1.1.0",
|
||||
"fs-readdir-recursive": "^1.1.0",
|
||||
"he": "^1.2.0",
|
||||
"kind-of": "^6.0.3",
|
||||
"lodash": "^4.17.21",
|
||||
"ms": "^2.1.3",
|
||||
"pluralize": "^8.0.0",
|
||||
"require-all": "^3.0.0",
|
||||
"resolve-from": "^5.0.0",
|
||||
"slugify": "^1.6.1",
|
||||
"truncatise": "0.0.8"
|
||||
}
|
||||
},
|
||||
"slugify": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.1.tgz",
|
||||
"integrity": "sha512-5ofqMTbetNhxlzjYYLBaZFQd6oiTuSkQlyfPEFIMwgUABlZQ0hbk5xIV9Ydd5jghWeRoO7GkiJliUvTpLOjNRA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@adonisjs/config": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@adonisjs/config/-/config-3.0.5.tgz",
|
||||
|
||||
@ -34,6 +34,7 @@
|
||||
"dependencies": {
|
||||
"@adonisjs/attachment-lite": "^1.0.1",
|
||||
"@adonisjs/auth": "^8.0.10",
|
||||
"@adonisjs/bouncer": "^2.2.5",
|
||||
"@adonisjs/core": "^5.4.0",
|
||||
"@adonisjs/drive": "^2.0.7",
|
||||
"@adonisjs/lucid": "^16.2.1",
|
||||
|
||||
67
start/bouncer.ts
Normal file
67
start/bouncer.ts
Normal file
@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Contract source: https://git.io/Jte3T
|
||||
*
|
||||
* Feel free to let us know via PR, if you find something broken in this config
|
||||
* file.
|
||||
*/
|
||||
|
||||
import Bouncer from '@ioc:Adonis/Addons/Bouncer';
|
||||
import RoleEnum from 'App/Datatypes/Enums/RoleEnum';
|
||||
import RoleHelper from 'App/Helpers/RoleHelper';
|
||||
import Role from 'App/Models/Role';
|
||||
import User from 'App/Models/User';
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Bouncer Actions
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Actions allows you to separate your application business logic from the
|
||||
| authorization logic. Feel free to make use of policies when you find
|
||||
| yourself creating too many actions
|
||||
|
|
||||
| You can define an action using the `.define` method on the Bouncer object
|
||||
| as shown in the following example
|
||||
|
|
||||
| ```
|
||||
| Bouncer.define('deletePost', (user: User, post: Post) => {
|
||||
| return post.user_id === user.id
|
||||
| })
|
||||
| ```
|
||||
|
|
||||
|****************************************************************
|
||||
| NOTE: Always export the "actions" const from this file
|
||||
|****************************************************************
|
||||
*/
|
||||
export const { actions } = Bouncer.define('manageUserRole', async (user: User, role: Role) => {
|
||||
await user.load('roles');
|
||||
return !(
|
||||
(role.slug === RoleEnum.ADMIN || role.slug === RoleEnum.TEACHER) &&
|
||||
!RoleHelper.userContainRoles(user.roles, [RoleEnum.ADMIN])
|
||||
);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Bouncer Policies
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Policies are self contained actions for a given resource. For example: You
|
||||
| can create a policy for a "User" resource, one policy for a "Post" resource
|
||||
| and so on.
|
||||
|
|
||||
| The "registerPolicies" accepts a unique policy name and a function to lazy
|
||||
| import the policy
|
||||
|
|
||||
| ```
|
||||
| Bouncer.registerPolicies({
|
||||
| UserPolicy: () => import('App/Policies/User'),
|
||||
| PostPolicy: () => import('App/Policies/Post')
|
||||
| })
|
||||
| ```
|
||||
|
|
||||
|****************************************************************
|
||||
| NOTE: Always export the "policies" const from this file
|
||||
|****************************************************************
|
||||
*/
|
||||
export const { policies } = Bouncer.registerPolicies({});
|
||||
@ -34,7 +34,8 @@
|
||||
"@adonisjs/redis",
|
||||
"@adonisjs/mail",
|
||||
"@adonisjs/view",
|
||||
"@adonisjs/attachment-lite"
|
||||
"@adonisjs/attachment-lite",
|
||||
"@adonisjs/bouncer"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user