mirror of
https://github.com/sergeyyarkov/educt-server.git
synced 2026-10-11 18:29:15 +03:00
feat: added adonisjs bouncer
This commit is contained in:
parent
00c2db0697
commit
077f1f91a6
@ -5,7 +5,8 @@
|
|||||||
"@adonisjs/core/commands",
|
"@adonisjs/core/commands",
|
||||||
"@adonisjs/repl/build/commands",
|
"@adonisjs/repl/build/commands",
|
||||||
"@adonisjs/lucid/build/commands",
|
"@adonisjs/lucid/build/commands",
|
||||||
"@adonisjs/mail/build/commands"
|
"@adonisjs/mail/build/commands",
|
||||||
|
"@adonisjs/bouncer/build/commands"
|
||||||
],
|
],
|
||||||
"exceptionHandlerNamespace": "App/Exceptions/Handler",
|
"exceptionHandlerNamespace": "App/Exceptions/Handler",
|
||||||
"namespaces": {
|
"namespaces": {
|
||||||
@ -26,7 +27,8 @@
|
|||||||
"preloads": [
|
"preloads": [
|
||||||
"./routes/index",
|
"./routes/index",
|
||||||
"./start/kernel",
|
"./start/kernel",
|
||||||
"./start/event"
|
"./start/event",
|
||||||
|
"./start/bouncer"
|
||||||
],
|
],
|
||||||
"providers": [
|
"providers": [
|
||||||
"./providers/AppProvider",
|
"./providers/AppProvider",
|
||||||
@ -37,7 +39,8 @@
|
|||||||
"@adonisjs/redis",
|
"@adonisjs/redis",
|
||||||
"@adonisjs/mail",
|
"@adonisjs/mail",
|
||||||
"@adonisjs/view",
|
"@adonisjs/view",
|
||||||
"@adonisjs/attachment-lite"
|
"@adonisjs/attachment-lite",
|
||||||
|
"@adonisjs/bouncer"
|
||||||
],
|
],
|
||||||
"aceProviders": [
|
"aceProviders": [
|
||||||
"@adonisjs/repl"
|
"@adonisjs/repl"
|
||||||
|
|||||||
@ -278,6 +278,42 @@
|
|||||||
],
|
],
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"flags": []
|
"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": {}
|
"aliases": {}
|
||||||
|
|||||||
@ -78,7 +78,7 @@ export default class UsersController extends BaseController {
|
|||||||
|
|
||||||
public async update(ctx: HttpContextContract) {
|
public async update(ctx: HttpContextContract) {
|
||||||
const payload = await ctx.request.validate(UpdateUserValidator);
|
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) {
|
if (!result.success && result.error) {
|
||||||
throw new Exception(result.message, result.status, result.error.code);
|
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> {
|
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 user = await this.User.query().preload('contacts').preload('roles').where('id', id).first();
|
||||||
const { role: roleSlug, ...updatedFields } = data;
|
const { role, ...updatedFields } = data;
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
/**
|
await user.merge(updatedFields).save();
|
||||||
* 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();
|
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
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
|
* Delete user
|
||||||
*
|
*
|
||||||
|
|||||||
@ -27,8 +27,6 @@ import UserRepository from 'App/Repositories/UserRepository';
|
|||||||
*/
|
*/
|
||||||
import CreateUserValidator from 'App/Validators/User/CreateUserValidator';
|
import CreateUserValidator from 'App/Validators/User/CreateUserValidator';
|
||||||
import UpdateUserValidator from 'App/Validators/User/UpdateUserValidator';
|
import UpdateUserValidator from 'App/Validators/User/UpdateUserValidator';
|
||||||
import RoleEnum from 'App/Datatypes/Enums/RoleEnum';
|
|
||||||
import RoleHelper from 'App/Helpers/RoleHelper';
|
|
||||||
|
|
||||||
@inject()
|
@inject()
|
||||||
export default class UserService {
|
export default class UserService {
|
||||||
@ -97,19 +95,12 @@ export default class UserService {
|
|||||||
* @returns Response
|
* @returns Response
|
||||||
*/
|
*/
|
||||||
public async createUser(data: CreateUserValidator['schema']['props'], ctx: HttpContextContract): Promise<IResponse> {
|
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);
|
const role = await this.roleRepository.getBySlug(data.role);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cannot find role
|
* Cannot find role
|
||||||
*/
|
*/
|
||||||
if (role === null) {
|
if (!role) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
status: HttpStatusEnum.NOT_FOUND,
|
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 (
|
if (await ctx.bouncer.denies('manageUserRole', role)) {
|
||||||
(role.slug === RoleEnum.ADMIN || role.slug === RoleEnum.TEACHER) &&
|
|
||||||
!RoleHelper.userContainRoles(user.roles, [RoleEnum.ADMIN])
|
|
||||||
) {
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
status: HttpStatusEnum.FORBIDDEN,
|
status: HttpStatusEnum.FORBIDDEN,
|
||||||
@ -145,19 +130,19 @@ export default class UserService {
|
|||||||
/**
|
/**
|
||||||
* Create new user
|
* Create new user
|
||||||
*/
|
*/
|
||||||
const createdUser = await this.userRepository.create(data);
|
const user = await this.userRepository.create(data);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attach role
|
* Attach role
|
||||||
*/
|
*/
|
||||||
await createdUser.related('roles').attach([role.id]);
|
await user.related('roles').attach([role.id]);
|
||||||
await createdUser.load('roles');
|
await user.load('roles');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
status: HttpStatusEnum.CREATED,
|
status: HttpStatusEnum.CREATED,
|
||||||
message: 'User created.',
|
message: 'User created.',
|
||||||
data: createdUser,
|
data: user,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,7 +153,11 @@ export default class UserService {
|
|||||||
* @param data Data to update
|
* @param data Data to update
|
||||||
* @returns Response
|
* @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);
|
const user = await this.userRepository.update(id, data);
|
||||||
|
|
||||||
if (!user) {
|
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 {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
status: HttpStatusEnum.OK,
|
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": {
|
"@adonisjs/config": {
|
||||||
"version": "3.0.5",
|
"version": "3.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@adonisjs/config/-/config-3.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@adonisjs/config/-/config-3.0.5.tgz",
|
||||||
|
|||||||
@ -34,6 +34,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@adonisjs/attachment-lite": "^1.0.1",
|
"@adonisjs/attachment-lite": "^1.0.1",
|
||||||
"@adonisjs/auth": "^8.0.10",
|
"@adonisjs/auth": "^8.0.10",
|
||||||
|
"@adonisjs/bouncer": "^2.2.5",
|
||||||
"@adonisjs/core": "^5.4.0",
|
"@adonisjs/core": "^5.4.0",
|
||||||
"@adonisjs/drive": "^2.0.7",
|
"@adonisjs/drive": "^2.0.7",
|
||||||
"@adonisjs/lucid": "^16.2.1",
|
"@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/redis",
|
||||||
"@adonisjs/mail",
|
"@adonisjs/mail",
|
||||||
"@adonisjs/view",
|
"@adonisjs/view",
|
||||||
"@adonisjs/attachment-lite"
|
"@adonisjs/attachment-lite",
|
||||||
|
"@adonisjs/bouncer"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user