Compare commits

...

10 Commits

Author SHA1 Message Date
Sockenklaus
27ad31d745 updated dependencies 2021-12-09 22:20:48 +01:00
Sockenklaus
f6788bbba9 Tried to work on EmployeesController.update function.
Didn't work because of adonisjs/validator#114
2021-11-17 22:21:34 +01:00
Sockenklaus
d13ec611a5 fixed EmployeesController. Correct error response in store function 2021-11-17 10:08:17 +01:00
Sockenklaus
583a74bcdc contractHours notNullable, defaultTo(0) 2021-11-15 16:48:20 +01:00
Sockenklaus
665bf6b22f SettingsController: fixed wrong error handling when line not found 2021-11-14 16:23:46 +01:00
Sockenklaus
2fcdc59677 added a proper response to logout-function 2021-11-14 11:19:14 +01:00
Sockenklaus
8d09db5c15 fixed defect error reporting on SettingsController 2021-11-12 23:53:33 +01:00
Sockenklaus
6da4c5743a some shit 2021-11-12 23:37:33 +01:00
Sockenklaus
ed7120ad2e Added settings API 2021-11-12 01:15:40 +01:00
Sockenklaus
654a829c16 updated package-lock.json? 2021-11-08 23:02:08 +01:00
13 changed files with 1869 additions and 1689 deletions

View File

@@ -35,10 +35,10 @@ export default class AuthController {
} }
} }
public async logout({auth}: HttpContextContract) { public async logout({auth, response}: HttpContextContract) {
try { try {
await auth.use('api').revoke() await auth.use('api').revoke()
return return response.ok(true)
} }
catch(error) { catch(error) {
Logger.error(error.message) Logger.error(error.message)

View File

@@ -22,7 +22,7 @@ type ResultShow = {
export default class EmployeesController { export default class EmployeesController {
public async index ({bouncer, request}: HttpContextContract) { 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 limit: number = request.qs().limit ?? 10
const page: number = request.qs().page ?? 1 const page: number = request.qs().page ?? 1
@@ -46,27 +46,27 @@ export default class EmployeesController {
return employees.paginate(page, limit) return employees.paginate(page, limit)
} }
public async store ({request}: HttpContextContract) { public async store ({request, bouncer}: HttpContextContract) {
try { await bouncer.with('EmployeesPolicy').authorize('store')
const payload = await request.validate(CreateEmployeeValidator) const payload = await request.validate(CreateEmployeeValidator)
return await Employee.create({ const employee = await Employee.create(payload)
firstName: payload.firstName, await employee.refresh()
lastName: payload.lastName,
shorthand: payload.shorthand,
email: payload.email,
phone: payload.phone,
mobile: payload.mobile,
contractHours: payload.contractHours,
username: payload.username,
password: payload.password,
role: payload.role,
isActive: payload.isActive
})
} catch (error) {
return error
}
return {
id: employee.id,
firstName: employee.firstName,
lastName: employee.lastName,
shorthand: employee.shorthand,
phone: employee.phone,
mobile: employee.mobile,
email: employee.email,
contractHours: employee.contractHours,
role: employee.role,
username: employee.username,
isActive: employee.isActive
}
} }
public async show ({params, bouncer, auth}: HttpContextContract) : Promise<ResultShow> { public async show ({params, bouncer, auth}: HttpContextContract) : Promise<ResultShow> {
@@ -79,7 +79,7 @@ export default class EmployeesController {
emp = await Employee.findOrFail(params.id) emp = await Employee.findOrFail(params.id)
} }
await bouncer.authorize('employees.show', emp) await bouncer.with('EmployeesPolicy').authorize('show', emp)
return { return {
id: emp.id, id: emp.id,
@@ -101,7 +101,8 @@ export default class EmployeesController {
const employee : Employee = await Employee.findOrFail(params.id) const employee : Employee = await Employee.findOrFail(params.id)
const editContractHours : boolean = employee.contractHours !== request.input('contractHours') 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) const payload = await request.validate(UpdateEmployeeValidator)
@@ -122,16 +123,13 @@ export default class EmployeesController {
employee.password = payload.password employee.password = payload.password
} }
await employee.save() employee.save()
return response.ok({ return employee
status: 200,
message: "Employee updated successfully"
})
} }
public async destroy ({params, bouncer}: HttpContextContract) { 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() return await Database.from('employees').where('id', params.id).delete()
} }
@@ -186,7 +184,7 @@ export default class EmployeesController {
let arr = qs.split(',').filter(item => item !== 'password' && item !== '' && columns.hasOwnProperty(item)) 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 return arr
} }

View File

@@ -0,0 +1,49 @@
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import SetSettingsValidator from 'App/Validators/SetSettingsValidator'
import Logger from '@ioc:Adonis/Core/Logger'
type ResultSetting = {
key: string,
value: string
}
export default class SettingsController {
public async list({ auth }: HttpContextContract ): Promise<ResultSetting[]> {
const result = await auth.user.related('settings').query().select(['key', 'value'])
return result
}
public async get({params, auth}: HttpContextContract): Promise<ResultSetting | null> {
const key = params.key
const result = await auth.user.related('settings').query().select(['key', 'value']).where('key', key).firstOrFail()
return result
}
/**
* Expects:
* {
* settings: [
* {key: 'key1', value: 'value1'},
* ]
* }
*/
public async set({request, auth}: HttpContextContract): Promise<'ok'> {
const payload = await request.validate(SetSettingsValidator)
await auth.user.related('settings').updateOrCreateMany(payload.settings, 'key')
return "ok"
}
public async delete({ params, auth }: HttpContextContract): Promise<(0 | 1)[]> {
const key = params.key
return await auth.user.related('settings').query().where('key', key).delete()
}
}

View File

@@ -1,5 +1,6 @@
import { DateTime } from 'luxon' 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' import Hash from '@ioc:Adonis/Core/Hash'
export default class Employee extends BaseModel { export default class Employee extends BaseModel {
@@ -45,10 +46,17 @@ export default class Employee extends BaseModel {
@column({serializeAs: null}) @column({serializeAs: null})
public password: string public password: string
@hasMany(() => Setting)
public settings: HasMany<typeof Setting>
@beforeSave() @beforeSave()
public static async hashPassword(employee: Employee){ public static async hashPassword(employee: Employee){
if(employee.$dirty.password){ if(employee.$dirty.password){
employee.password = await Hash.make(employee.password) employee.password = await Hash.make(employee.password)
} }
} }
public isAdmin(): boolean {
return this.role === 'admin'
}
} }

26
app/Models/Setting.ts Normal file
View File

@@ -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<typeof Employee>
@column()
public key: string
@column()
public value: string
}

View File

@@ -0,0 +1,28 @@
import { BasePolicy } from '@ioc:Adonis/Addons/Bouncer'
import Employee from 'App/Models/Employee'
import Logger from '@ioc:Adonis/Core/Logger'
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) {
Logger.info("Is Admin? "+employee.isAdmin())
Logger.info("Same ids? "+(employee.id === query.id))
Logger.info("Edit contract Hours? "+editContractHours)
return employee.isAdmin() || (employee.id === query.id && !editContractHours)
}
public async destroy(employee: Employee) {
return employee.isAdmin()
}
}

View File

@@ -0,0 +1,62 @@
import { schema, rules, validator } 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 = {
'settings.required': 'Settings are required',
'settings.key.required': 'Key is required',
'settings.key.alpha': 'Key must be alphabetic',
'settings.value.required': 'Value is required',
'settings.value.alpha': 'Value must be alphabetic',
}
}

View File

@@ -27,6 +27,8 @@ export default class Employees extends BaseSchema {
.defaultTo(false) .defaultTo(false)
.notNullable() .notNullable()
table.decimal('contract_hours', 2, 2) table.decimal('contract_hours', 2, 2)
.notNullable()
.defaultTo(0)
}) })
} }

View File

@@ -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)
}
}

3217
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,31 +10,31 @@
"format": "prettier --write ." "format": "prettier --write ."
}, },
"devDependencies": { "devDependencies": {
"@adonisjs/assembler": "^5.3.7", "@adonisjs/assembler": "^5.3.8",
"@types/uuid": "^8.3.1", "@types/uuid": "^8.3.3",
"adonis-preset-ts": "^2.1.0", "adonis-preset-ts": "^2.1.0",
"eslint": "^7.32.0", "eslint": "^8.4.1",
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.3.0",
"eslint-plugin-adonis": "^1.3.3", "eslint-plugin-adonis": "^2.1.0",
"eslint-plugin-prettier": "^4.0.0", "eslint-plugin-prettier": "^4.0.0",
"pino-pretty": "^7.1.0", "pino-pretty": "^7.2.0",
"prettier": "^2.4.1", "prettier": "^2.5.1",
"typescript": "~4.2", "typescript": "~4.5",
"youch": "^2.2.2", "youch": "^2.2.2",
"youch-terminal": "^1.1.1" "youch-terminal": "^1.1.1"
}, },
"dependencies": { "dependencies": {
"@adonisjs/auth": "^8.0.10", "@adonisjs/auth": "^8.0.10",
"@adonisjs/bouncer": "^2.2.5", "@adonisjs/bouncer": "^2.2.5",
"@adonisjs/core": "^5.4.0", "@adonisjs/core": "^5.4.2",
"@adonisjs/lucid": "^16.2.1", "@adonisjs/lucid": "^16.3.2",
"@adonisjs/repl": "^3.1.6", "@adonisjs/repl": "^3.1.7",
"@adonisjs/session": "^6.1.2", "@adonisjs/session": "^6.1.2",
"luxon": "^2.0.2", "luxon": "^2.1.1",
"phc-argon2": "^1.1.2", "phc-argon2": "^1.1.2",
"proxy-addr": "^2.0.7", "proxy-addr": "^2.0.7",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"source-map-support": "^0.5.20", "source-map-support": "^0.5.21",
"sqlite3": "^5.0.2", "sqlite3": "^5.0.2",
"uuid": "^8.3.2" "uuid": "^8.3.2"
} }

View File

@@ -6,7 +6,6 @@
*/ */
import Bouncer from '@ioc:Adonis/Addons/Bouncer' 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 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 | Bouncer Policies
@@ -86,4 +54,6 @@ export const { actions } = Bouncer
| NOTE: Always export the "policies" const from this file | NOTE: Always export the "policies" const from this file
|**************************************************************** |****************************************************************
*/ */
export const { policies } = Bouncer.registerPolicies({}) export const { policies } = Bouncer.registerPolicies({
EmployeesPolicy: () => import('App/Policies/EmployeesPolicy'),
})

View File

@@ -30,6 +30,11 @@ Route.group(() => {
Route.post('logout', 'AuthController.logout').as('logout') Route.post('logout', 'AuthController.logout').as('logout')
Route.resource('employees', 'EmployeesController').apiOnly() Route.resource('employees', 'EmployeesController').apiOnly()
Route.get('settings', 'SettingsController.list').as('settings.list')
Route.get('settings/:key', 'SettingsController.get').as('settings.get')
Route.post('settings', 'SettingsController.set').as('settings.set')
Route.delete('settings:key', 'SettingsController.delete').as('settings.delete')
}) })
.prefix('api/v1') .prefix('api/v1')
.middleware('auth') .middleware('auth')