diff --git a/app/Controllers/Http/EmployeesController.ts b/app/Controllers/Http/EmployeesController.ts index e6bd2fc..57d0644 100644 --- a/app/Controllers/Http/EmployeesController.ts +++ b/app/Controllers/Http/EmployeesController.ts @@ -22,7 +22,7 @@ type ResultShow = { export default class EmployeesController { public async index ({bouncer, request}: HttpContextContract) { - await bouncer.authorize('employees.index') + await bouncer.with('EmployeesPolicy').authorize('index') const limit: number = request.qs().limit ?? 10 const page: number = request.qs().page ?? 1 @@ -46,7 +46,9 @@ export default class EmployeesController { return employees.paginate(page, limit) } - public async store ({request}: HttpContextContract) { + public async store ({request, bouncer}: HttpContextContract) { + await bouncer.with('EmployeesPolicy').authorize('store') + try { const payload = await request.validate(CreateEmployeeValidator) @@ -79,7 +81,7 @@ export default class EmployeesController { emp = await Employee.findOrFail(params.id) } - await bouncer.authorize('employees.show', emp) + await bouncer.with('EmployeesPolicy').authorize('show', emp) return { id: emp.id, @@ -101,7 +103,7 @@ export default class EmployeesController { const employee : Employee = await Employee.findOrFail(params.id) const editContractHours : boolean = employee.contractHours !== request.input('contractHours') - await bouncer.authorize('employees.update', editContractHours, employee) + await bouncer.with('EmployeesPolicy').authorize('update', editContractHours, employee) const payload = await request.validate(UpdateEmployeeValidator) @@ -131,7 +133,7 @@ export default class EmployeesController { } public async destroy ({params, bouncer}: HttpContextContract) { - await bouncer.authorize('employees.destroy') + await bouncer.with('EmployeesPolicy').authorize('destroy') return await Database.from('employees').where('id', params.id).delete() } @@ -186,7 +188,7 @@ export default class EmployeesController { let arr = qs.split(',').filter(item => item !== 'password' && item !== '' && columns.hasOwnProperty(item)) - if(arr.length === 0) arr = ['id', 'last_name', 'first_name', 'email', 'mobile', 'phone', 'role'] + if(arr.length === 0) arr = ['id', 'last_name', 'first_name', 'shorthand', 'email', 'mobile', 'phone', 'role'] return arr } diff --git a/app/Controllers/Http/SettingsController.ts b/app/Controllers/Http/SettingsController.ts new file mode 100644 index 0000000..efee5bd --- /dev/null +++ b/app/Controllers/Http/SettingsController.ts @@ -0,0 +1,82 @@ +import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' +import Employee from 'App/Models/Employee' +import SetSettingsValidator from 'App/Validators/SetSettingsValidator' + +type ResultSetting = { + key: string, + value: string +} + +export default class SettingsController { + + public async list({ params, bouncer }: HttpContextContract ): Promise { + const userId = params.userId + + try { + const user = await Employee.findOrFail(userId) + + await bouncer.with('SettingsPolicy').authorize('do', user) + + const result = await user.related('settings').query().select(['key', 'value']) + + return result + } + catch(error) { + return error.message + } + + } + + public async get({params, bouncer}: HttpContextContract): Promise { + const userId = params.userId + const key = params.key + + try { + const user = await Employee.findOrFail(userId) + + await bouncer.with('SettingsPolicy').authorize('do', user) + + const result = user.related('settings').query().select(['key', 'value']).where('key', key).first() + + return result + } + catch(error) { + return error.message + } + + } + + public async set({params, request, bouncer}: HttpContextContract): Promise<'ok'> { + const userId = params.userId + + try { + const payload = await request.validate(SetSettingsValidator) + const user = await Employee.findOrFail(userId) + + await bouncer.with('SettingsPolicy').authorize('do', user) + + await user.related('settings').updateOrCreateMany(payload.settings, 'key') + + return "ok" + } + catch(error){ + return error.message + } + + } + + public async delete({ params, bouncer }: HttpContextContract): Promise<(0 | 1)[]> { + const userId = params.userId + const key = params.key + + try { + const user = await Employee.findOrFail(userId) + await bouncer.with('SettingsPolicy').authorize('do', user) + + return await user.related('settings').query().where('key', key).delete() + } + catch(error){ + return error.message + } + } +} diff --git a/app/Models/Employee.ts b/app/Models/Employee.ts index 65a510a..ef077dd 100644 --- a/app/Models/Employee.ts +++ b/app/Models/Employee.ts @@ -1,5 +1,6 @@ import { DateTime } from 'luxon' -import { BaseModel, beforeSave, column } from '@ioc:Adonis/Lucid/Orm' +import { BaseModel, beforeSave, column, hasMany, HasMany } from '@ioc:Adonis/Lucid/Orm' +import Setting from 'App/Models/Setting' import Hash from '@ioc:Adonis/Core/Hash' export default class Employee extends BaseModel { @@ -45,10 +46,17 @@ export default class Employee extends BaseModel { @column({serializeAs: null}) public password: string + @hasMany(() => Setting) + public settings: HasMany + @beforeSave() public static async hashPassword(employee: Employee){ if(employee.$dirty.password){ employee.password = await Hash.make(employee.password) } } + + public isAdmin(): boolean { + return this.role === 'admin' + } } diff --git a/app/Models/Setting.ts b/app/Models/Setting.ts new file mode 100644 index 0000000..0403f46 --- /dev/null +++ b/app/Models/Setting.ts @@ -0,0 +1,26 @@ +import { DateTime } from 'luxon' +import { BaseModel, belongsTo, BelongsTo, column } from '@ioc:Adonis/Lucid/Orm' +import Employee from 'App/Models/Employee' + +export default class Setting extends BaseModel { + @column({ isPrimary: true }) + public id: number + + @column.dateTime({ autoCreate: true }) + public createdAt: DateTime + + @column.dateTime({ autoCreate: true, autoUpdate: true }) + public updatedAt: DateTime + + @column() + public employeeId: number + + @belongsTo(() => Employee) + public employee: BelongsTo + + @column() + public key: string + + @column() + public value: string +} diff --git a/app/Policies/EmployeesPolicy.ts b/app/Policies/EmployeesPolicy.ts new file mode 100644 index 0000000..109eb0a --- /dev/null +++ b/app/Policies/EmployeesPolicy.ts @@ -0,0 +1,24 @@ +import { BasePolicy } from '@ioc:Adonis/Addons/Bouncer' +import Employee from 'App/Models/Employee' + +export default class EmployeesPolicy extends BasePolicy { + public async index(employee: Employee) { + return employee.isAdmin() + } + + public async show(employee: Employee, query: Employee) { + return employee.isAdmin() || employee.id === query.id + } + + public async store(employee: Employee) { + return employee.isAdmin() + } + + public async update(employee: Employee, editContractHours: boolean, query: Employee) { + return employee.isAdmin() || (employee.id === query.id && !editContractHours) + } + + public async destroy(employee: Employee) { + return employee.isAdmin() + } +} diff --git a/app/Policies/SettingsPolicy.ts b/app/Policies/SettingsPolicy.ts new file mode 100644 index 0000000..59f5aa2 --- /dev/null +++ b/app/Policies/SettingsPolicy.ts @@ -0,0 +1,8 @@ +import { BasePolicy } from '@ioc:Adonis/Addons/Bouncer' +import Employee from 'App/Models/Employee' + +export default class SettingsPolicy extends BasePolicy { + public async do(user: Employee, query: Employee){ + return user.isAdmin() || user.id === query.id + } +} diff --git a/app/Validators/SetSettingsValidator.ts b/app/Validators/SetSettingsValidator.ts new file mode 100644 index 0000000..6bca205 --- /dev/null +++ b/app/Validators/SetSettingsValidator.ts @@ -0,0 +1,56 @@ +import { schema, rules } from '@ioc:Adonis/Core/Validator' +import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' + +export default class SetSettingsValidator { + constructor(protected ctx: HttpContextContract) {} + + /* + * Define schema to validate the "shape", "type", "formatting" and "integrity" of data. + * + * For example: + * 1. The username must be of data type string. But then also, it should + * not contain special characters or numbers. + * ``` + * schema.string({}, [ rules.alpha() ]) + * ``` + * + * 2. The email must be of data type string, formatted as a valid + * email. But also, not used by any other user. + * ``` + * schema.string({}, [ + * rules.email(), + * rules.unique({ table: 'users', column: 'email' }), + * ]) + * ``` + */ + public schema = schema.create({ + settings: schema.array().members( + schema.object().members({ + key: schema.string({ + trim: true, + }, [ + rules.alpha({ + allow: ['dash', 'underscore'] + }) + ]), + + value: schema.string({ + trim: true + }), + }) + ), + }) + + /** + * Custom messages for validation failures. You can make use of dot notation `(.)` + * for targeting nested fields and array expressions `(*)` for targeting all + * children of an array. For example: + * + * { + * 'profile.username.required': 'Username is required', + * 'scores.*.number': 'Define scores as valid numbers' + * } + * + */ + public messages = {} +} diff --git a/database/migrations/1636667124834_settings.ts b/database/migrations/1636667124834_settings.ts new file mode 100644 index 0000000..e3eef1f --- /dev/null +++ b/database/migrations/1636667124834_settings.ts @@ -0,0 +1,29 @@ +import BaseSchema from '@ioc:Adonis/Lucid/Schema' + +export default class Settings extends BaseSchema { + protected tableName = 'settings' + + public async up () { + this.schema.createTable(this.tableName, (table) => { + table.increments('id') + + /** + * Uses timestamptz for PostgreSQL and DATETIME2 for MSSQL + */ + table.timestamp('created_at', { useTz: true }) + table.timestamp('updated_at', { useTz: true }) + + table.integer('employee_id') + .unsigned() + .references('employees.id') + .onDelete('CASCADE') + + table.string('key') + table.string('value') + }) + } + + public async down () { + this.schema.dropTable(this.tableName) + } +} diff --git a/start/bouncer.ts b/start/bouncer.ts index 1af8fff..dceb027 100644 --- a/start/bouncer.ts +++ b/start/bouncer.ts @@ -6,7 +6,6 @@ */ import Bouncer from '@ioc:Adonis/Addons/Bouncer' -import Employee from 'App/Models/Employee' /* |-------------------------------------------------------------------------- @@ -32,37 +31,6 @@ import Employee from 'App/Models/Employee' */ export const { actions } = Bouncer - .define('employees.index', (user: Employee) => { - if(user.role !== 'admin') return Bouncer.deny('You are not allowed to view all employees') - return true - }) - - .define('employees.show', (user: Employee, query: Employee) => { - if(user.role !== 'admin' && user.id !== query.id){ - return Bouncer.deny('You are not allowd to view employees other than yourself') - } - return true - }) - - .define('employees.store', (user: Employee) => { - if(user.role !== 'admin') return Bouncer.deny('You are not allowd to create any employees') - return true - }) - - .define('employees.destroy', (user: Employee) => { - if(user.role !== 'admin') return Bouncer.deny('You are not allowed to delete any employees') - return true - }) - - .define('employees.update', (user: Employee, editContractHours : boolean, query: Employee) => { - if(user.id !== query.id && user.role !== 'admin'){ - return Bouncer.deny('You are not allowed to edit employees other than yourself.') - } else if (editContractHours && user.role !== 'admin'){ - return Bouncer.deny('You are not allowed to edit your contract hours.') - } - return true - }) - /* |-------------------------------------------------------------------------- | Bouncer Policies @@ -86,4 +54,7 @@ export const { actions } = Bouncer | NOTE: Always export the "policies" const from this file |**************************************************************** */ -export const { policies } = Bouncer.registerPolicies({}) +export const { policies } = Bouncer.registerPolicies({ + EmployeesPolicy: () => import('App/Policies/EmployeesPolicy'), + SettingsPolicy: () => import('App/Policies/SettingsPolicy'), +}) diff --git a/start/routes.ts b/start/routes.ts index e4d08a8..a0e5ada 100644 --- a/start/routes.ts +++ b/start/routes.ts @@ -30,6 +30,11 @@ Route.group(() => { Route.post('logout', 'AuthController.logout').as('logout') Route.resource('employees', 'EmployeesController').apiOnly() + + Route.get('settings/:userId', 'SettingsController.list').as('settings.list') + Route.get('settings/:userId/:key', 'SettingsController.get').as('settings.get') + Route.post('settings/:userId', 'SettingsController.set').as('settings.set') + Route.delete('settings/:userId/:key', 'SettingsController.delete').as('settings.delete') }) .prefix('api/v1') .middleware('auth')