diff --git a/app/Controllers/Http/Api/v1/CoursesController.ts b/app/Controllers/Http/Api/v1/CoursesController.ts index 0aaa60f..bea9c1e 100644 --- a/app/Controllers/Http/Api/v1/CoursesController.ts +++ b/app/Controllers/Http/Api/v1/CoursesController.ts @@ -4,7 +4,7 @@ import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; /** * Datatype */ -import Role from 'App/Datatype/Role'; +import Role from 'App/Datatypes/Enums/RoleEnum'; /** * Models diff --git a/app/Controllers/Http/Api/v1/UsersController.ts b/app/Controllers/Http/Api/v1/UsersController.ts index d5e1efc..45e862c 100644 --- a/app/Controllers/Http/Api/v1/UsersController.ts +++ b/app/Controllers/Http/Api/v1/UsersController.ts @@ -1,11 +1,10 @@ -import { Exception } from '@adonisjs/core/build/standalone'; +import { Exception, inject, Ioc } from '@adonisjs/core/build/standalone'; import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; /** - * Models + * Services */ -import Role from 'App/Models/Role'; -import User from 'App/Models/User'; +import UserService from 'App/Services/UserService'; /** * Validators @@ -17,15 +16,13 @@ import UpdateUserValidator from 'App/Validators/User/UpdateUserValidator'; import BaseController from '../../BaseController'; +@inject() export default class UsersController extends BaseController { - private readonly User: typeof User; + private userService: UserService; - private readonly Role: typeof Role; - - constructor() { + constructor(userService: UserService) { super(); - this.User = User; - this.Role = Role; + this.userService = userService; } /** @@ -33,13 +30,14 @@ export default class UsersController extends BaseController { * GET /users */ - public async list({ response }: HttpContextContract) { - const users = await this.User.query().preload('contacts').preload('roles'); + public async list(ctx: HttpContextContract) { + const result = await this.userService.fetchUsers(ctx.request.qs()); - return response.ok({ - message: 'Fetched all users.', - data: users, - }); + if (!result.success && result.error) { + throw new Exception(result.message, result.status, result.error.code); + } + + return this.sendResponse(ctx, result.data, result.message, result.status); } /** @@ -47,16 +45,14 @@ export default class UsersController extends BaseController { * GET /users/:id */ - public async show({ response, params }: HttpContextContract) { - const user = await this.User.findOrFail(params.id); + public async show(ctx: HttpContextContract) { + const result = await this.userService.fetchUser(ctx.params.id); - await user.load('contacts'); - await user.load('roles'); + if (!result.success && result.error) { + throw new Exception(result.message, result.status, result.error.code); + } - return response.ok({ - message: `Fetched user with id: "${user.id}"`, - data: user, - }); + return this.sendResponse(ctx, result.data, result.message, result.status); } /** @@ -64,20 +60,15 @@ export default class UsersController extends BaseController { * POST /users */ - public async create({ response, request }: HttpContextContract) { - await request.validate(CreateUserValidator); + public async create(ctx: HttpContextContract) { + const payload = await ctx.request.validate(CreateUserValidator); + const result = await this.userService.createUser(payload); - const user = await this.User.create({ - first_name: request.input('first_name'), - last_name: request.input('last_name'), - login: request.input('login'), - password: request.input('password'), - }); + if (!result.success && result.error) { + throw new Exception(result.message, result.status, result.error.code); + } - await user.related('contacts').create({ email: request.input('email') }); - await user.load('contacts'); - - return response.created({ message: 'Successfully created.', data: user }); + return this.sendResponse(ctx, result.data, result.message, result.status); } /** @@ -85,34 +76,15 @@ export default class UsersController extends BaseController { * PATCH /users/:id */ - public async update({ request, response, params }: HttpContextContract) { - await request.validate(UpdateUserValidator); + public async update(ctx: HttpContextContract) { + const payload = await ctx.request.validate(UpdateUserValidator); + const result = await this.userService.updateUser(ctx.params.id, payload); - const user = await this.User.findOrFail(params.id); - const input: Pick = { - first_name: request.input('first_name'), - last_name: request.input('last_name'), - login: request.input('login'), - password: request.input('password'), - }; + if (!result.success && result.error) { + throw new Exception(result.message, result.status, result.error.code); + } - /** - * Update roles - */ - Object.keys(input).forEach(k => { - if (input[k] !== undefined) { - user[k] = input[k]; - } - }); - - await user.save(); - await user.load('roles'); - await user.load('contacts'); - - return response.ok({ - message: `User with id: "${user.id}" was successfully updated.`, - data: user, - }); + return this.sendResponse(ctx, result.data, result.message, result.status); } /** @@ -120,74 +92,45 @@ export default class UsersController extends BaseController { * DELETE /users/:id */ - public async delete({ response, params }: HttpContextContract) { - const user = await this.User.query().preload('contacts').preload('roles').where('id', params.id).firstOrFail(); + public async delete(ctx: HttpContextContract) { + const result = await this.userService.deleteUser(ctx.params.id); - await user.delete(); + if (!result.success && result.error) { + throw new Exception(result.message, result.status, result.error.code); + } - return response.ok({ - message: `Used with id: "${user.id}" was successfully deleted.`, - data: user, - }); + return this.sendResponse(ctx, result.data, result.message, result.status); } /** * Attach array of roles to user * POST /users/:id/attach-roles */ - public async attachRoles({ request, response, params }: HttpContextContract) { - await request.validate(AddRoleToUserValidator); + public async attachRoles(ctx: HttpContextContract) { + const payload = await ctx.request.validate(AddRoleToUserValidator); + const result = await this.userService.attachRolesToUser(ctx.params.id, payload.roles); - 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); + if (!result.success && result.error) { + throw new Exception(result.message, result.status, result.error.code); + } - await this.User.attachRoles(user, roles); - - return response.ok({ - message: 'Roles attached', - data: user.roles, - }); + return this.sendResponse(ctx, result.data, result.message, result.status); } /** * Detach array of roles from user * DELETE /users/:id/detach-roles */ - public async detachRoles({ request, response, params }: HttpContextContract) { - await request.validate(DelRoleFromUserValidator); + public async detachRoles(ctx: HttpContextContract) { + const payload = await ctx.request.validate(DelRoleFromUserValidator); + const result = await this.userService.detachRolesFromUser(ctx.params.id, payload.roles); - 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, - }); - } - - /** - * 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'); + if (!result.success && result.error) { + throw new Exception(result.message, result.status, result.error.code); } - 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; + return this.sendResponse(ctx, result.data, result.message, result.status); } } + +new Ioc().make(UsersController); diff --git a/app/Datatype/Role.ts b/app/Datatypes/Enums/RoleEnum.ts similarity index 62% rename from app/Datatype/Role.ts rename to app/Datatypes/Enums/RoleEnum.ts index 4e37fd7..e465772 100644 --- a/app/Datatype/Role.ts +++ b/app/Datatypes/Enums/RoleEnum.ts @@ -1,7 +1,7 @@ -enum Role { +enum RoleEnum { ADMIN = 'admin', TEACHER = 'teacher', STUDENT = 'student', } -export default Role; +export default RoleEnum; diff --git a/app/Datatypes/Interfaces/IResponse.ts b/app/Datatypes/Interfaces/IResponse.ts new file mode 100644 index 0000000..cfa186a --- /dev/null +++ b/app/Datatypes/Interfaces/IResponse.ts @@ -0,0 +1,9 @@ +export default interface IResponse { + success: boolean; + status: number; + message: string; + data: object; + error?: { + code: string; + }; +} diff --git a/app/Models/Contact.ts b/app/Models/Contact.ts index b465aaa..dc0da12 100644 --- a/app/Models/Contact.ts +++ b/app/Models/Contact.ts @@ -1,5 +1,6 @@ import { DateTime } from 'luxon'; -import { BaseModel, column } from '@ioc:Adonis/Lucid/Orm'; +import { BaseModel, BelongsTo, belongsTo, column } from '@ioc:Adonis/Lucid/Orm'; +import User from './User'; export default class Contact extends BaseModel { @column({ isPrimary: true }) @@ -23,6 +24,11 @@ export default class Contact extends BaseModel { @column() public telegram_id: string | null; + @belongsTo(() => User, { + foreignKey: 'user_id', + }) + public user: BelongsTo; + @column.dateTime({ autoCreate: true, serializeAs: null }) public createdAt: DateTime; diff --git a/app/Models/User.ts b/app/Models/User.ts index dd9cf3d..59d5298 100644 --- a/app/Models/User.ts +++ b/app/Models/User.ts @@ -1,7 +1,6 @@ /* eslint-disable import/no-cycle */ /* eslint-disable no-param-reassign */ import { DateTime } from 'luxon'; -import { Exception } from '@adonisjs/core/build/standalone'; import { BaseModel, beforeCreate, @@ -55,45 +54,6 @@ 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'); - } - - /** - * 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 diff --git a/app/Repositories/ContactRepository.ts b/app/Repositories/ContactRepository.ts new file mode 100644 index 0000000..0ac3b3e --- /dev/null +++ b/app/Repositories/ContactRepository.ts @@ -0,0 +1,56 @@ +import Contact from 'App/Models/Contact'; +import User from 'App/Models/User'; +import CreateUserValidator from 'App/Validators/User/CreateUserValidator'; +import UpdateUserValidator from 'App/Validators/User/UpdateUserValidator'; + +export default class ContactRepository { + private Contact: typeof Contact; + + constructor() { + this.Contact = Contact; + } + + /** + * Creates user contacts in database + * + * @param user User for associate the contacts + * @param data Data contacts + * @returns Created contacts + */ + public async create(user: User, data: Pick) { + const contacts = await this.Contact.create(data); + + await contacts.related('user').associate(user); + await user.load('contacts'); + await user.load('roles'); + + return contacts; + } + + public async update(user: User, data: Pick) { + const contacts = await this.Contact.query().where('user_id', user.id).first(); + + /** + * Update user contacts + */ + if (contacts) { + contacts.email = data.email; + + await contacts.save(); + await user.load('contacts'); + + return contacts; + } + + /** + * Create new user contacts if contacts doesn't exist + */ + await user.related('contacts').create({ + email: data.email, + }); + + await user.load('contacts'); + + return user.contacts; + } +} diff --git a/app/Repositories/RoleRepository.ts b/app/Repositories/RoleRepository.ts new file mode 100644 index 0000000..c957b01 --- /dev/null +++ b/app/Repositories/RoleRepository.ts @@ -0,0 +1,26 @@ +import Role from 'App/Models/Role'; + +export default class RoleRepository { + private Role: typeof Role; + + constructor() { + this.Role = Role; + } + + public async getAll(params?: any) { + const { roles }: any = params || {}; + const query = this.Role.query(); + + if (roles) { + query.whereIn('slug', roles); + } + + const data = await query; + return data; + } + + public async getBySlug(slug: string) { + const role = await this.Role.query().where('slug', slug); + return role; + } +} diff --git a/app/Repositories/UserRepository.ts b/app/Repositories/UserRepository.ts new file mode 100644 index 0000000..0d54780 --- /dev/null +++ b/app/Repositories/UserRepository.ts @@ -0,0 +1,106 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import User from 'App/Models/User'; +import CreateUserValidator from 'App/Validators/User/CreateUserValidator'; +import UpdateUserValidator from 'App/Validators/User/UpdateUserValidator'; + +export default class UserRepository { + private User: typeof User; + + constructor() { + this.User = User; + } + + public async getById(id: string | number) { + const data = await this.User.query().preload('contacts').preload('roles').where('id', id).first(); + return data; + } + + public async getAllByRole(role: string) { + const data = await this.User.query() + .preload('contacts') + .preload('roles') + .whereHas('roles', q => q.where('slug', role)); + return data; + } + + public async getAll(params?: any) { + const { email, login, first_name, last_name, role }: any = params || {}; + const query = this.User.query(); + + if (email) { + query.preload('contacts').whereHas('contacts', q => q.where('email', 'like', `%${email}%`)); + } + + if (login) { + query.where('login', 'like', `%${login}%`); + } + + if (first_name) { + query.where('first_name', 'like', `%${first_name}%`); + } + + if (last_name) { + query.where('last_name', 'like', `%${last_name}%`); + } + + if (role) { + query.preload('roles').whereHas('roles', q => q.where('slug', role)); + } + + const data = await query.preload('contacts').preload('roles'); + return data; + } + + public async create(data: CreateUserValidator['schema']['props']) { + const createdUser = await this.User.create({ + first_name: data.first_name, + last_name: data.last_name, + login: data.login, + password: data.password, + }); + + return createdUser; + } + + /** + * Update user + * + * @param id User id + * @param data Data to update + * @returns User or null if user is not found + */ + public async update(id: number | string, data: UpdateUserValidator['schema']['props']) { + const user = await this.User.query().preload('contacts').preload('roles').where('id', id).first(); + + if (user) { + await user + .merge({ + first_name: data.first_name, + last_name: data.last_name, + login: data.login, + password: data.password, + }) + .save(); + return user; + } + + return null; + } + + /** + * Delete user + * + * @param id User id + * @returns Deleted user or null if user is not found + */ + public async delete(id: number | string) { + const user = await this.User.query().preload('contacts').preload('roles').where('id', id).first(); + + if (user) { + await user.delete(); + return user; + } + + return null; + } +} diff --git a/app/Services/UserService.ts b/app/Services/UserService.ts new file mode 100644 index 0000000..a88a7f4 --- /dev/null +++ b/app/Services/UserService.ts @@ -0,0 +1,261 @@ +import { inject, Ioc } from '@adonisjs/core/build/standalone'; +import IResponse from 'App/Datatypes/Interfaces/IResponse'; +import Role from 'App/Models/Role'; +import ContactRepository from 'App/Repositories/ContactRepository'; +import RoleRepository from 'App/Repositories/RoleRepository'; +import UserRepository from 'App/Repositories/UserRepository'; +import CreateUserValidator from 'App/Validators/User/CreateUserValidator'; +import UpdateUserValidator from 'App/Validators/User/UpdateUserValidator'; + +@inject() +export default class UserService { + private userRepository: UserRepository; + + private contactRepository: ContactRepository; + + private roleRepository: RoleRepository; + + constructor(userRepository: UserRepository, contactRepository: ContactRepository, roleRepository: RoleRepository) { + this.userRepository = userRepository; + this.contactRepository = contactRepository; + this.roleRepository = roleRepository; + } + + /** + * Fetch one user by id + * + * @param id Id of user + * @returns Response data + */ + public async fetchUser(id: string | number): Promise { + const user = await this.userRepository.getById(id); + + if (!user) { + return { + success: false, + status: 404, + message: 'User not found.', + data: {}, + error: { + code: 'USER_NOT_FOUND', + }, + }; + } + + return { + success: true, + status: 200, + message: 'Fetched user.', + data: user, + }; + } + + /** + * Fetch list of users + * + * @param params Params for finding user + * @returns Response data + */ + public async fetchUsers(params?: any): Promise { + const users = await this.userRepository.getAll(params); + + return { + success: true, + status: 200, + message: 'Fetched all users.', + data: users, + }; + } + + /** + * Create new user + * + * @param data Input for create new user + * @returns Response data + */ + public async createUser(data: CreateUserValidator['schema']['props']): Promise { + const user = await this.userRepository.create(data); + + /* Associate user contacts */ + await this.contactRepository.create(user, { email: data.email }); + + return { + success: true, + status: 201, + message: 'User created.', + data: user, + }; + } + + public async updateUser(id: string | number, data: UpdateUserValidator['schema']['props']): Promise { + const user = await this.userRepository.update(id, data); + + if (!user) { + return { + success: false, + status: 404, + message: 'User not found.', + data: {}, + error: { + code: 'E_USER_NOT_FOUND', + }, + }; + } + + /** + * Update user contacts + */ + await this.contactRepository.update(user, data); + + return { + success: true, + status: 200, + message: 'User updated.', + data: user, + }; + } + + public async deleteUser(id: string | number): Promise { + const user = await this.userRepository.delete(id); + + if (!user) { + return { + success: false, + status: 404, + message: 'User not found.', + data: {}, + error: { + code: 'E_USER_NOT_FOUND', + }, + }; + } + + return { + success: true, + status: 200, + message: 'User deleted.', + data: user, + }; + } + + /** + * Attach array of roles to user + * + * @param id User id + * @param input Roles + * @returns Result + */ + public async attachRolesToUser(id: string | number, input: string[]): Promise { + const user = await this.userRepository.getById(id); + + if (!user) { + return { + success: false, + status: 404, + message: 'User not found.', + data: {}, + error: { + code: 'E_USER_NOT_FOUND', + }, + }; + } + + const roles = await this.roleRepository.getAll({ roles: input }); + let attachedRole: Role | undefined; + + /** + * Set attachedRole variable if some role already + * attached to user. + */ + roles.some(inputRole => { + const existed = user.roles.find(role => role.slug === inputRole.slug); + if (existed) { + attachedRole = inputRole; + return true; + } + return false; + }); + + if (attachedRole) { + return { + success: false, + status: 404, + message: `Role "${attachedRole.name}" already attached to that user.`, + data: {}, + error: { + code: 'E_ROLE_ATTACHED', + }, + }; + } + + await user.related('roles').attach([...roles.map(r => r.id)]); + await user.load('roles'); + + return { + success: true, + status: 200, + message: 'Roles attached.', + data: user.roles, + }; + } + + /** + * Detach array of role from user + * + * @param id User id + * @param input Roles + * @returns Result + */ + public async detachRolesFromUser(id: string | number, input: string[]): Promise { + const user = await this.userRepository.getById(id); + + if (!user) { + return { + success: false, + status: 404, + message: 'User not found.', + data: {}, + error: { + code: 'E_USER_NOT_FOUND', + }, + }; + } + + const roles = await this.roleRepository.getAll({ roles: input }); + let notAttachedRole: Role | undefined; + + /** + * Set notAttachedRole variable if some role + * not attached to user. + */ + roles.forEach(role => { + if (!user.roles.map(r => r.id).includes(role.id)) { + notAttachedRole = role; + } + }); + + if (notAttachedRole) { + return { + success: false, + status: 404, + message: `Role "${notAttachedRole.name}" not attached to that user.`, + data: {}, + error: { + code: 'E_ROLE_NOT_ATTACHED', + }, + }; + } + + await user.related('roles').detach([...roles.map(r => r.id)]); + await user.load('roles'); + + return { + success: true, + status: 200, + message: 'Roles detached.', + data: user.roles, + }; + } +} + +new Ioc().make(UserService); diff --git a/app/Validators/User/AddRoleToUserValidator.ts b/app/Validators/User/AddRoleToUserValidator.ts index 1986975..04a8cc8 100644 --- a/app/Validators/User/AddRoleToUserValidator.ts +++ b/app/Validators/User/AddRoleToUserValidator.ts @@ -1,11 +1,12 @@ import { schema } from '@ioc:Adonis/Core/Validator'; import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; +import RoleEnum from 'App/Datatypes/Enums/RoleEnum'; export default class AddRoleToUserValidator { constructor(protected ctx: HttpContextContract) {} public schema = schema.create({ - roles: schema.array().members(schema.string()), + roles: schema.array().members(schema.enum(Object.values(RoleEnum))), }); public messages = {}; diff --git a/app/Validators/User/DelRoleFromUserValidator.ts b/app/Validators/User/DelRoleFromUserValidator.ts index 34a16c1..ec3d69c 100644 --- a/app/Validators/User/DelRoleFromUserValidator.ts +++ b/app/Validators/User/DelRoleFromUserValidator.ts @@ -1,11 +1,12 @@ import { schema } from '@ioc:Adonis/Core/Validator'; import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; +import RoleEnum from 'App/Datatypes/Enums/RoleEnum'; export default class DelRoleFromUserValidator { constructor(protected ctx: HttpContextContract) {} public schema = schema.create({ - roles: schema.array().members(schema.string()), + roles: schema.array().members(schema.enum(Object.values(RoleEnum))), }); public messages = {}; diff --git a/app/Validators/User/UpdateUserValidator.ts b/app/Validators/User/UpdateUserValidator.ts index da58035..e481753 100644 --- a/app/Validators/User/UpdateUserValidator.ts +++ b/app/Validators/User/UpdateUserValidator.ts @@ -5,11 +5,11 @@ export default class UpdateUserValidator { constructor(protected ctx: HttpContextContract) {} public schema = schema.create({ - 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({}, [rules.unique({ table: 'users', column: 'password' })]), + first_name: schema.string(), + last_name: schema.string(), + login: schema.string({}, [rules.unique({ table: 'users', column: 'login' })]), + email: schema.string({}, [rules.email(), rules.unique({ table: 'contacts', column: 'email' })]), + password: schema.string(), }); public messages = {}; diff --git a/commands/index.ts b/commands/index.ts index 47bad75..1268128 100644 --- a/commands/index.ts +++ b/commands/index.ts @@ -17,6 +17,4 @@ import Application from '@ioc:Adonis/Core/Application'; | */ -export default listDirectoryFiles(__dirname, Application.appRoot, [ - './commands/index', -]); +export default listDirectoryFiles(__dirname, Application.appRoot, ['./commands/index']); diff --git a/config/bodyparser.ts b/config/bodyparser.ts index c5893d1..5bd9a83 100644 --- a/config/bodyparser.ts +++ b/config/bodyparser.ts @@ -32,12 +32,7 @@ const bodyParserConfig: BodyParserConfig = { encoding: 'utf-8', limit: '1mb', strict: true, - types: [ - 'application/json', - 'application/json-patch+json', - 'application/vnd.api+json', - 'application/csp-report', - ], + types: ['application/json', 'application/json-patch+json', 'application/vnd.api+json', 'application/csp-report'], }, /* diff --git a/config/redis.ts b/config/redis.ts index a3998ee..80e0bad 100644 --- a/config/redis.ts +++ b/config/redis.ts @@ -41,6 +41,7 @@ const redisConfig: RedisConfig = { password: Env.get('REDIS_PASSWORD', ''), db: 0, keyPrefix: '', + healthCheck: true, }, }, }; diff --git a/contracts/auth.ts b/contracts/auth.ts index 90916e2..9317be3 100644 --- a/contracts/auth.ts +++ b/contracts/auth.ts @@ -5,7 +5,7 @@ * file. */ -import User from 'App/Models/User' +import User from 'App/Models/User'; declare module '@ioc:Adonis/Addons/Auth' { /* @@ -34,9 +34,9 @@ declare module '@ioc:Adonis/Addons/Auth' { | */ user: { - implementation: LucidProviderContract, - config: LucidProviderConfig, - }, + implementation: LucidProviderContract; + config: LucidProviderConfig; + }; } /* @@ -65,8 +65,8 @@ declare module '@ioc:Adonis/Addons/Auth' { | */ api: { - implementation: OATGuardContract<'user', 'api'>, - config: OATGuardConfig<'user'>, - }, + implementation: OATGuardContract<'user', 'api'>; + config: OATGuardConfig<'user'>; + }; } } diff --git a/package-lock.json b/package-lock.json index 509ea40..7de7337 100644 --- a/package-lock.json +++ b/package-lock.json @@ -145,11 +145,41 @@ } }, "@adonisjs/fold": { - "version": "8.1.4", - "resolved": "https://registry.npmjs.org/@adonisjs/fold/-/fold-8.1.4.tgz", - "integrity": "sha512-+B56Lxb19TZ2DkK2deZH1kTfqyXVUKwrfNirVZ3JfGeO6RbXjW3IYeUTJ7MWzcbiSTqmHXw2HC+TTIpE/EEDpg==", + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@adonisjs/fold/-/fold-8.1.5.tgz", + "integrity": "sha512-eqbq0zQd0TV7AoN/RP4vUUg442BP4GSc0U1CsNYGKJ3YBhxfgThVLDlgn1K4/nadC1lv4/Gf3ZSnLj1Rx5OzFA==", "requires": { - "@poppinss/utils": "^3.1.3" + "@poppinss/utils": "^3.1.4" + }, + "dependencies": { + "@poppinss/utils": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@poppinss/utils/-/utils-3.2.0.tgz", + "integrity": "sha512-gnqlrxE4zMvqW+LpviyCWCg4+RShCYCFyBdVTyg9ZZEEgPBV9bEY+YB5r/2klFrKx30k9nfClHVI2n8ZKmXgsw==", + "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", + "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.0", + "truncatise": "0.0.8" + } + }, + "slugify": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.0.tgz", + "integrity": "sha512-FkMq+MQc5hzYgM86nLuHI98Acwi3p4wX+a5BO9Hhw4JdK4L7WueIiZ4tXEobImPqBz2sVcV0+Mu3GRB30IGang==" + } } }, "@adonisjs/hash": { diff --git a/providers/AppProvider.ts b/providers/AppProvider.ts index 8d64412..96558de 100644 --- a/providers/AppProvider.ts +++ b/providers/AppProvider.ts @@ -3,19 +3,11 @@ import { ApplicationContract } from '@ioc:Adonis/Core/Application'; export default class AppProvider { constructor(protected app: ApplicationContract) {} - public register() { - // Register your own bindings - } + public register() {} - public async boot() { - // IoC container is ready - } + public async boot() {} - public async ready() { - // App is ready - } + public async ready() {} - public async shutdown() { - // Cleanup, since app is going down - } + public async shutdown() {} } diff --git a/routes/apis/v1/health.ts b/routes/apis/v1/health.ts new file mode 100644 index 0000000..b3ba089 --- /dev/null +++ b/routes/apis/v1/health.ts @@ -0,0 +1,11 @@ +import Route from '@ioc:Adonis/Core/Route'; +import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; +import HealthCheck from '@ioc:Adonis/Core/HealthCheck'; + +Route.group(() => { + Route.get('/', async (ctx: HttpContextContract) => { + const report = await HealthCheck.getReport(); + + return report.healthy ? ctx.response.ok(report) : ctx.response.badRequest(report); + }).as('health'); +}).prefix('health'); diff --git a/routes/apis/v1/index.ts b/routes/apis/v1/index.ts index fac334f..57346de 100644 --- a/routes/apis/v1/index.ts +++ b/routes/apis/v1/index.ts @@ -12,6 +12,13 @@ Route.group(() => { require('./auth'); Route.group(() => { + /** + * + * Health check + * + */ + require('./health'); + /** * * Users module diff --git a/routes/apis/v1/users.ts b/routes/apis/v1/users.ts index 13181e2..e468147 100644 --- a/routes/apis/v1/users.ts +++ b/routes/apis/v1/users.ts @@ -2,14 +2,14 @@ import Route from '@ioc:Adonis/Core/Route'; Route.group(() => { Route.get('/', 'Api/v1/UsersController.list').middleware('role:admin,teacher,student').as('users.list'); - Route.post('/', 'Api/v1/UsersController.create').middleware('role:admin').as('users.create'); + Route.post('/', 'Api/v1/UsersController.create').middleware('role:admin,student').as('users.create'); Route.get('/:id', 'Api/v1/UsersController.show').middleware('role:admin,teacher,student').as('users.show'); - Route.patch('/:id', 'Api/v1/UsersController.update').middleware('role:admin').as('users.update'); - Route.delete('/:id', 'Api/v1/UsersController.delete').middleware('role:admin').as('users.delete'); + Route.put('/:id', 'Api/v1/UsersController.update').middleware('role:admin,student,teacher').as('users.update'); + Route.delete('/:id', 'Api/v1/UsersController.delete').middleware('role:admin,student,teacher').as('users.delete'); Route.post('/:id/attach-roles', 'Api/v1/UsersController.attachRoles') - .middleware('role:admin') + .middleware('role:admin,student,teacher') .as('users.attach-role'); Route.delete('/:id/detach-roles', 'Api/v1/UsersController.detachRoles') - .middleware('role:admin') + .middleware('role:admin,student,teacher') .as('users.detach-role'); }).prefix('users'); diff --git a/tsconfig.json b/tsconfig.json index 78177af..9fa1ec1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,36 +1,17 @@ { "extends": "./node_modules/adonis-preset-ts/tsconfig", - "include": [ - "**/*" - ], - "exclude": [ - "node_modules", - "build" - ], + "include": ["**/*"], + "exclude": ["node_modules", "build"], "compilerOptions": { "outDir": "build", "rootDir": "./", "sourceMap": true, "paths": { - "App/*": [ - "./app/*" - ], - "Config/*": [ - "./config/*" - ], - "Contracts/*": [ - "./contracts/*" - ], - "Database/*": [ - "./database/*" - ] + "App/*": ["./app/*"], + "Config/*": ["./config/*"], + "Contracts/*": ["./contracts/*"], + "Database/*": ["./database/*"] }, - "types": [ - "@adonisjs/core", - "@adonisjs/repl", - "@adonisjs/lucid", - "@adonisjs/auth", - "@adonisjs/redis" - ] + "types": ["@adonisjs/core", "@adonisjs/repl", "@adonisjs/lucid", "@adonisjs/auth", "@adonisjs/redis"] } }