From e50700c2ab3803d69cfa535b70d3fa8e940ebb90 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Sun, 25 Jul 2021 05:11:40 +0300 Subject: [PATCH] some refactoring --- app/Controllers/Http/AuthController.ts | 10 +- app/Controllers/Http/ContactsController.ts | 4 - app/Controllers/Http/RolesController.ts | 99 ----------- app/Controllers/Http/UsersController.ts | 195 +++++++++++++++------ app/Models/User.ts | 52 ++++++ app/Validators/UpdateUserValidator.ts | 8 +- start/routes.ts | 14 +- 7 files changed, 205 insertions(+), 177 deletions(-) delete mode 100644 app/Controllers/Http/ContactsController.ts delete mode 100644 app/Controllers/Http/RolesController.ts diff --git a/app/Controllers/Http/AuthController.ts b/app/Controllers/Http/AuthController.ts index dbd62fe..3e3f09c 100644 --- a/app/Controllers/Http/AuthController.ts +++ b/app/Controllers/Http/AuthController.ts @@ -16,9 +16,6 @@ export default class AuthController { /** * Creates a new token for user and returns it. * POST /login - * - * @param {HttpContextContract} context - * @returns {object} - Token data */ public async login({ auth, request }: HttpContextContract) { @@ -43,13 +40,10 @@ export default class AuthController { /** * Revoke a token if user logged in. * POST /logout - * - * @param {HttpContextContract} context - * @returns {object} - Revoked message */ - public async logout({ auth }: HttpContextContract) { + public async logout({ response, auth }: HttpContextContract) { await auth.use(this.guard).revoke(); - return { message: 'REVOKED' }; + return response.noContent(); } } diff --git a/app/Controllers/Http/ContactsController.ts b/app/Controllers/Http/ContactsController.ts deleted file mode 100644 index 6f63214..0000000 --- a/app/Controllers/Http/ContactsController.ts +++ /dev/null @@ -1,4 +0,0 @@ -// import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' - -export default class ContactsController { -} diff --git a/app/Controllers/Http/RolesController.ts b/app/Controllers/Http/RolesController.ts deleted file mode 100644 index c6a37c0..0000000 --- a/app/Controllers/Http/RolesController.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { Exception } from '@adonisjs/core/build/standalone'; -import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; - -import Role from 'App/Models/Role'; -import User from 'App/Models/User'; -import AddRoleToUserValidator from 'App/Validators/AddRoleToUserValidator'; -import DelRoleFromUserValidator from 'App/Validators/DelRoleFromUserValidator'; - -export default class RolesController { - private readonly user: typeof User; - - private readonly role: typeof Role; - - constructor() { - this.user = User; - this.role = Role; - } - - private async findRolesBySlug(input: any) { - const roles = await this.role.query().whereIn('slug', input); - - if (roles.length !== input.length) { - throw new Exception( - 'Unable to find any role using input value "roles"', - 404, - 'E_ROLE_NOT_FOUND' - ); - } - - return roles; - } - - /** - * Add a role to a user by "id" - * POST /user/:id/roles - * - * @param {HttpContextContract} context - * @returns {Promise} - Updated user. - */ - public async store({ request, params }: HttpContextContract): Promise { - try { - await request.validate(AddRoleToUserValidator); - - const input = request.input('roles'); - const user = await this.user.findOrFail(params.id); - const roles = await this.findRolesBySlug(input); - - await user.related('roles').attach(roles.map(r => r.id)); - await user.load('roles'); - - return user.roles; - } catch (error) { - if (error.code === '23505') { - throw new Exception('Some role already attached to that user.', 400, 'E_ROLE_ATTACHED'); - } - throw error; - } - } - - /** - * Detach role from user by "id" - * DELETE /users/:id/roles - * - */ - public async destroy({ request, params }: HttpContextContract): Promise { - await request.validate(DelRoleFromUserValidator); - - const input = request.input('roles'); - const user = await this.user.query().preload('roles').where('id', params.id).firstOrFail(); - - /** - * Detach all when "roles" input not provided - */ - if (input === undefined) { - user.related('roles').detach([]); - return []; - } - - const roles = await this.findRolesBySlug(input); - - /** - * Check if role is attached to user before detach - */ - roles.forEach(role => { - if (!user.roles.map(r => r.id).includes(role.id)) { - throw new Exception( - `Role "${role.slug}" does not attached to that user.`, - 400, - 'E_ROLE_NOT_ATTACHED' - ); - } - }); - - user.related('roles').detach(roles.map(r => r.id)); - await user.load('roles'); - - return user.roles; - } -} diff --git a/app/Controllers/Http/UsersController.ts b/app/Controllers/Http/UsersController.ts index bf6c555..d58a1e8 100644 --- a/app/Controllers/Http/UsersController.ts +++ b/app/Controllers/Http/UsersController.ts @@ -1,8 +1,11 @@ +import { Exception } from '@adonisjs/core/build/standalone'; import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; +import Contact from 'App/Models/Contact'; import Role from 'App/Models/Role'; import User from 'App/Models/User'; -import Contact from 'App/Models/Contact'; +import AddRoleToUserValidator from 'App/Validators/AddRoleToUserValidator'; import CreateUserValidator from 'App/Validators/CreateUserValidator'; +import DelRoleFromUserValidator from 'App/Validators/DelRoleFromUserValidator'; import UpdateUserValidator from 'App/Validators/UpdateUserValidator'; export default class UsersController { @@ -19,27 +22,51 @@ export default class UsersController { } /** - * Show a list of all users. - * GET /users - * - * @returns {Promise} + * Attach array of roles to user + * POST /users/:id/attach_roles */ + public async attach_roles({ request, response, params }: HttpContextContract) { + await request.validate(AddRoleToUserValidator); - public async index(): Promise { - const users = await this.user.query().preload('contacts').preload('roles'); - return users; + const input: string[] | undefined = request.input('roles'); + const user = await this.user.query().preload('roles').where('id', params.id).firstOrFail(); + const roles = await this.findRolesBySlug(input); + + await this.user.attachRoles(user, roles); + + return response.ok({ + message: 'Roles attached', + data: user.roles, + }); } /** - * Show a current user by "id" parameter. - * GET /users/:id - * - * @param {HttpContextContract} context - * @returns {Promise} + * Detach array of roles from user + * DELETE /users/:id/detach_roles */ + public async detach_roles({ request, response, params }: HttpContextContract) { + await request.validate(DelRoleFromUserValidator); - public async show({ params }: HttpContextContract): Promise { - const user = await this.user.findOrFail(params.id); + const input: string[] | undefined = request.input('roles'); + const user = await this.user.query().preload('roles').where('id', params.id).firstOrFail(); + const roles = await this.findRolesBySlug(input); + + await this.user.detachRoles(user, roles); + + return response.ok({ + message: 'Roles detached', + data: user.roles, + }); + } + + /** + * Shows user info of the access token resource owner. + * GET /users/me + */ + public async me({ auth: { user } }: HttpContextContract): Promise { + if (!user) { + throw new AuthenticationException('Unauthorized access', 'E_UNAUTHORIZED_ACCESS'); + } await user.load('contacts'); await user.load('roles'); @@ -48,17 +75,44 @@ export default class UsersController { } /** - * Creates a new user in a system. - * POST /users - * - * @param {HttpContextContract} context - * @returns {Promise} - Created user. + * Show a list of all users. + * GET /users */ - public async store({ request }: HttpContextContract): Promise { + public async index({ response }: HttpContextContract) { + const users = await this.user.query().preload('contacts').preload('roles'); + + return response.ok({ + message: 'Fetched all users.', + data: users, + }); + } + + /** + * Show a current user by "id" parameter. + * GET /users/:id + */ + + public async show({ response, params }: HttpContextContract) { + const user = await this.user.findOrFail(params.id); + + await user.load('contacts'); + await user.load('roles'); + + return response.ok({ + message: `Fetched user with id: "${user.id}"`, + data: user, + }); + } + + /** + * Creates a new user in a system. + * POST /users + */ + + public async store({ response, request }: HttpContextContract) { await request.validate(CreateUserValidator); - const roles = await this.role.query().whereIn('slug', request.input('roles')); const user = await this.user.create({ first_name: request.input('first_name'), last_name: request.input('last_name'), @@ -67,69 +121,94 @@ export default class UsersController { }); await user.related('contacts').create({ email: request.input('email') }); - await user.related('roles').attach(roles.map(r => r.id)); - - await user.load('roles'); await user.load('contacts'); - return user; + return response.created({ message: 'Successfully created.', data: user }); } /** * Update a user in a system by "id". * PATCH /users/:id - * - * @param {HttpContextContract} context - * @returns {Promise} */ - public async update({ request, params }: HttpContextContract): Promise { + public async update({ request, response, params }: HttpContextContract) { await request.validate(UpdateUserValidator); - /** - * User update - */ const user = await this.user.findOrFail(params.id); - user.first_name = request.input('first_name'); - user.last_name = request.input('last_name'); - user.login = request.input('login'); - user.password = request.input('password'); - - // await user.load('roles'); + const dataInput: Pick = { + first_name: request.input('first_name'), + last_name: request.input('last_name'), + login: request.input('login'), + password: request.input('password'), + }; /** - * Contacts update + * Update only provided input roles value. */ - const contacts = await this.contact.findByOrFail('user_id', params.id); - contacts.email = request.input('email'); + Object.keys(dataInput).forEach(k => { + if (dataInput[k] !== undefined) { + user[k] = dataInput[k]; + } + }); - /** - * Save the updated data - */ await user.save(); - await contacts.save(); - - /** - * Load the updated data to return - */ await user.load('roles'); await user.load('contacts'); - return user; + return response.ok({ + message: `User with id: "${user.id}" was successfully updated.`, + data: user, + }); } /** * Delete a user by "id". - * DELETE /users - * - * @param {HttpContextContract} context - * @returns {Promise} + * DELETE /users/:id */ - public async destroy({ params }: HttpContextContract): Promise { - const user = await this.user.findOrFail(params.id); + public async destroy({ response, params }: HttpContextContract) { + const user = await this.user + .query() + .preload('contacts') + .preload('roles') + .where('id', params.id) + .firstOrFail(); + await user.delete(); - return user; + return response.ok({ + message: `Used with id: "${user.id}" was successfully deleted.`, + data: user, + }); + } + + /** + * Finds roles by input value and throw error if role does not exist. + * + * @param input Array of roles to find them + * @returns Finded roles + */ + private async findRolesBySlug(input: string[] | undefined): Promise { + if (!input) { + throw new Exception( + 'Unable to find "roles" field in input.', + 400, + 'E_ROLES_FIELD_NOT_PROVIDED' + ); + } + + const roles = await this.role.query().whereIn('slug', input); + + input.forEach(val => { + if (!roles.map(r => r.slug).includes(val)) { + throw new Exception( + `Unable to find role: "${val}" using input value "roles"`, + 404, + 'E_ROLE_NOT_FOUND' + ); + } + }); + + return roles; } } diff --git a/app/Models/User.ts b/app/Models/User.ts index 7052067..6087e7f 100644 --- a/app/Models/User.ts +++ b/app/Models/User.ts @@ -1,6 +1,7 @@ /* eslint-disable import/no-cycle */ /* eslint-disable no-param-reassign */ import { DateTime } from 'luxon'; +import { Exception } from '@adonisjs/core/build/standalone'; import { BaseModel, beforeCreate, @@ -48,11 +49,62 @@ export default class User extends BaseModel { @column.dateTime({ autoCreate: true, autoUpdate: true, serializeAs: null }) public updatedAt: DateTime; + /** + * Checks if the input role is already attached, and if so, throws an error, + * otherwise it attaches the role to the user. + * + * @param user User + * @param inputRoles Array of roles to attach + */ + public static async attachRoles(user: User, inputRoles: Role[]): Promise { + const currentRolesSlugs: string[] = user.roles.map(r => r.slug); + const inputRolesSlugs: string[] = inputRoles.map(r => r.slug); + + currentRolesSlugs.forEach(s => { + if (inputRolesSlugs.includes(s)) { + throw new Exception(`Role "${s}" already attached to that user.`, 400, 'E_ROLE_ATTACHED'); + } + }); + + await user.related('roles').attach(inputRoles.map(r => r.id)); + await user.load('roles'); // load updated roles + } + + /** + * Before detach, checks if input role is attached to that user and + * if not, throws an error. + * + * @param user User + * @param inputRoles Array of roles to detach + */ + public static async detachRoles(user: User, inputRoles: Role[]): Promise { + inputRoles.forEach(role => { + if (!user.roles.map(r => r.id).includes(role.id)) { + throw new Exception( + `Role "${role.slug}" not attached to that user.`, + 400, + 'E_ROLE_NOT_ATTACHED' + ); + } + }); + + user.related('roles').detach(inputRoles.map(r => r.id)); + await user.load('roles'); + } + + /** + * Assign id to user by nanoid. + * @param user User + */ @beforeCreate() public static assignUui(user: User) { user.id = nanoid(); } + /** + * Hash password before save + * @param user User + */ @beforeSave() public static async hashPassword(user: User) { if (user.$dirty.password) { diff --git a/app/Validators/UpdateUserValidator.ts b/app/Validators/UpdateUserValidator.ts index bfafb9b..acce2ae 100644 --- a/app/Validators/UpdateUserValidator.ts +++ b/app/Validators/UpdateUserValidator.ts @@ -5,14 +5,16 @@ export default class UpdateUserValidator { constructor(protected ctx: HttpContextContract) {} public schema = schema.create({ - first_name: schema.string.optional(), - last_name: schema.string.optional(), + first_name: schema.string.optional({}, [ + rules.unique({ table: 'users', column: 'first_name' }), + ]), + last_name: schema.string.optional({}, [rules.unique({ table: 'users', column: 'last_name' })]), login: schema.string.optional({}, [rules.unique({ table: 'users', column: 'login' })]), email: schema.string.optional({}, [ rules.email(), rules.unique({ table: 'contacts', column: 'email' }), ]), - password: schema.string.optional(), + password: schema.string.optional({}, [rules.unique({ table: 'users', column: 'password' })]), }); public messages = {}; diff --git a/start/routes.ts b/start/routes.ts index 483ce18..97e1eb0 100644 --- a/start/routes.ts +++ b/start/routes.ts @@ -20,21 +20,25 @@ import Route from '@ioc:Adonis/Core/Route'; -Route.get('/', () => `API REST Server.`); +Route.get('/', () => `Educt Backend API.`); Route.group(() => { - /* Auth */ + /* Auth controller */ Route.post('login', 'AuthController.login'); Route.post('logout', 'AuthController.logout').middleware('auth'); /* API */ Route.group(() => { + /** + * Users controller + */ + Route.get('users/me', 'UsersController.me').middleware('role:admin,teacher,student'); Route.get('users', 'UsersController.index').middleware('role:admin,teacher,student'); Route.get('users/:id', 'UsersController.show').middleware('role:admin,teacher,student'); Route.post('users', 'UsersController.store').middleware('role:admin'); Route.patch('users/:id', 'UsersController.update').middleware('role:admin'); - Route.delete('users', 'UsersController.destroy').middleware('role:admin'); - Route.post('users/:id/roles', 'RolesController.store').middleware('role:admin'); - Route.delete('users/:id/roles', 'RolesController.destroy').middleware('role:admin'); + Route.delete('users/:id', 'UsersController.destroy').middleware('role:admin'); + Route.post('users/:id/attach_roles', 'UsersController.attach_roles').middleware('role:admin'); + Route.delete('users/:id/detach_roles', 'UsersController.detach_roles').middleware('role:admin'); }).middleware('auth'); }).prefix('api');