├── .gitignore ├── LICENSE ├── README.md ├── instructions.js ├── instructions.md ├── package.json ├── providers └── AuditableProvider.js ├── src └── Traits │ └── Auditable.js └── templates ├── Audit.js └── AuditSchema.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea/ 3 | .vscode/ 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Simon Tong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # adonis-auditable 2 | Audit models in AdonisJS 3 | 4 | ## How to use 5 | 6 | Install npm module: 7 | 8 | ```bash 9 | $ adonis install adonis-auditable 10 | ``` 11 | 12 | ## Register provider 13 | 14 | Once you have installed adonis-auditable, make sure to register the provider inside `start/app.js` in order to make use of it. 15 | 16 | ```js 17 | const providers = [ 18 | 'adonis-auditable/providers/AuditableProvider' 19 | ] 20 | ``` 21 | 22 | ## Using the module: 23 | 24 | Add the following to your model's `boot` method: 25 | 26 | ```js 27 | class MyModel extends Model { 28 | boot () { 29 | super.boot() 30 | this.addTrait('@provider:Auditable') 31 | } 32 | } 33 | ``` 34 | 35 | This you can start using as follows: 36 | 37 | ```js 38 | // create 39 | const model = await MyModel.audit().create({name: 'John'}) 40 | 41 | // update 42 | const model = MyModel.find(1) 43 | await model.audit().update({name: 'Simon'}) 44 | 45 | // delete 46 | const model = MyModel.find(1) 47 | await model.audit().delete() 48 | ``` 49 | 50 | ## Built With 51 | 52 | * [AdonisJS](http://adonisjs.com) - The web framework used. 53 | 54 | ## Versioning 55 | 56 | [SemVer](http://semver.org/) is used for versioning. For the versions available, see the [tags on this repository](https://github.com/simontong/adonis-auditable/tags). 57 | 58 | ## Authors 59 | 60 | * **Simon Tong** - *Developer* - [simontong](https://github.com/simontong) 61 | 62 | ## License 63 | 64 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 65 | 66 | 67 | ## Changelog 68 | 69 | - v2.0.1 70 | - Removed need to pass in `ctx` parameters. 71 | - Update README and instructions.md files. 72 | 73 | - v2.0.0 74 | - Removed ctx injection on boot. `ctx` parameters need to be passed in manually now. 75 | - Updated README to reflect new changes. 76 | - Added this changelog. 77 | 78 | - v1.0.1 79 | - Initial release. -------------------------------------------------------------------------------- /instructions.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | 5 | async function copyAuditMigration (cli) { 6 | try { 7 | const migrationsFile = cli.helpers.migrationsPath(`${new Date().getTime()}_audit.js`) 8 | await cli.copy( 9 | path.join(__dirname, 'templates', 'AuditSchema.js'), 10 | path.join(migrationsFile) 11 | ) 12 | cli.command.completed('create', migrationsFile.replace(cli.helpers.appRoot(), '').replace(path.sep, '')) 13 | } 14 | catch (error) { 15 | console.log(error) 16 | } 17 | } 18 | 19 | async function copyAuditModel (cli) { 20 | try { 21 | await cli.copy( 22 | path.join(__dirname, 'templates', 'Audit.js'), 23 | path.join(cli.appDir, 'Models/Audit.js') 24 | ) 25 | cli.command.completed('create', 'Models/Audit.js') 26 | } 27 | catch (error) { 28 | console.log(error) 29 | } 30 | } 31 | 32 | module.exports = async (cli) => { 33 | await copyAuditModel(cli) 34 | await copyAuditMigration(cli) 35 | } 36 | -------------------------------------------------------------------------------- /instructions.md: -------------------------------------------------------------------------------- 1 | ## Register provider 2 | 3 | Start by registering the provider inside `start/app.js` file. 4 | 5 | ```js 6 | const providers = [ 7 | 'adonis-auditable/providers/AuditableProvider' 8 | ] 9 | ``` 10 | 11 | ## Making a model auditable 12 | 13 | Add the following to your model's `boot` method: 14 | 15 | ```js 16 | class MyModel extends Model { 17 | boot () { 18 | super.boot() 19 | this.addTrait('@provider:Auditable') 20 | } 21 | } 22 | ``` 23 | 24 | This you can start using as follows: 25 | 26 | ```js 27 | // create 28 | await MyModel.audit().create(/** model data **/) 29 | 30 | // update 31 | await MyModel.audit().update(/** model data **/) 32 | 33 | // delete 34 | await MyModel.audit().delete() 35 | ``` 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adonis-auditable", 3 | "version": "2.0.1", 4 | "description": "Audit AdonisJS models", 5 | "main": " ", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/simontong/adonis-auditable.git" 12 | }, 13 | "keywords": [ 14 | "adonis", 15 | "auditable", 16 | "adonis-auditable" 17 | ], 18 | "author": "Simon Tong ", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/simontong/adonis-auditable/issues" 22 | }, 23 | "homepage": "https://github.com/simontong/adonis-auditable#readme", 24 | "dependencies": { 25 | "lodash": "^4.17.4" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /providers/AuditableProvider.js: -------------------------------------------------------------------------------- 1 | const {ServiceProvider} = require('@adonisjs/fold') 2 | 3 | class AuditableProvider extends ServiceProvider { 4 | register () { 5 | this.app.singleton('Adonis/Traits/Auditable', () => { 6 | // const Config = this.app.use('Adonis/Src/Config') 7 | const Auditable = require('../src/Traits/Auditable') 8 | return new Auditable() 9 | }) 10 | this.app.alias('Adonis/Traits/Auditable', 'Auditable') 11 | } 12 | 13 | boot () { 14 | const Context = this.app.use('Adonis/Src/HttpContext') 15 | const Auditable = this.app.use('Auditable') 16 | 17 | // add ctx to datagrid 18 | Context.onReady(ctx => { 19 | Auditable.ctx = ctx 20 | }) 21 | } 22 | } 23 | 24 | module.exports = AuditableProvider 25 | -------------------------------------------------------------------------------- /src/Traits/Auditable.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const _ = require('lodash') 4 | const Audit = use('App/Models/Audit') 5 | 6 | class Auditable { 7 | register (Model) { 8 | // create methods 9 | const self = this 10 | Model.audit = function () { 11 | return { 12 | create: createWithAudit(self.ctx).bind(this) 13 | } 14 | } 15 | 16 | // update/delete methods 17 | Model.prototype.audit = function () { 18 | return { 19 | update: updateWithAudit(self.ctx).bind(this), 20 | delete: deleteWithAudit(self.ctx).bind(this) 21 | } 22 | } 23 | } 24 | } 25 | 26 | /** 27 | * Create with audit 28 | * 29 | * @param auth 30 | * @param request 31 | * @returns {*} 32 | */ 33 | function createWithAudit ({request, auth}) { 34 | return async function (data) { 35 | const model = await this.create(data) 36 | const newModel = (await this.find(model.primaryKeyValue)) 37 | const auditable = newModel.constructor.name 38 | const auditableId = newModel.id 39 | const newData = newModel.$attributes 40 | const event = Audit.events.CREATE 41 | 42 | // save audit 43 | await createAudit(event, {request, auth}, auditable, auditableId, null, newData) 44 | 45 | return model 46 | } 47 | } 48 | 49 | /** 50 | * Update with audit 51 | * 52 | * @param auth 53 | * @param request 54 | * @returns {*} 55 | */ 56 | function updateWithAudit ({request, auth}) { 57 | return async function (data, ignoreDiff = ['updated_at']) { 58 | const auditable = this.constructor.name 59 | const auditableId = this.id 60 | const oldData = this.$originalAttributes 61 | this.merge(data) 62 | const result = await this.save() 63 | const newModel = (await this.constructor.find(this.primaryKeyValue)) 64 | const newData = newModel.$attributes 65 | 66 | // if new and old are equal then don't bother updating 67 | const isEqual = _.isEqual( 68 | _.omit(newData, ignoreDiff), 69 | _.omit(oldData, ignoreDiff) 70 | ) 71 | if (isEqual) { 72 | return result 73 | } 74 | 75 | // update / patch are shared 76 | const event = Audit.events.UPDATE 77 | 78 | // save audit 79 | await createAudit(event, {request, auth}, auditable, auditableId, oldData, newData) 80 | 81 | return result 82 | } 83 | } 84 | 85 | /** 86 | * Delete with audit 87 | * 88 | * @param auth 89 | * @param request 90 | * @returns {*} 91 | */ 92 | function deleteWithAudit ({request, auth}) { 93 | return async function () { 94 | const auditable = this.constructor.name 95 | const auditableId = this.id 96 | const oldData = this.$originalAttributes 97 | const result = await this.delete() 98 | 99 | // save audit 100 | await createAudit(Audit.events.DELETE, {request, auth}, auditable, auditableId, oldData) 101 | 102 | return result 103 | } 104 | } 105 | 106 | /** 107 | * Run the audit 108 | * 109 | * @param event 110 | * @param oldData 111 | * @param auditable 112 | * @param auditableId 113 | * @param newData 114 | * @param auth 115 | * @param request 116 | * @returns {Promise} 117 | */ 118 | async function createAudit (event, {request, auth}, auditable, auditableId, oldData, newData) { 119 | // check request was passed 120 | if (!request) { 121 | throw new Error('Request param is empty') 122 | } 123 | 124 | // get user data to store 125 | const userId = _.get(auth, 'user.id', null) 126 | const url = request.absoluteUrl() 127 | const ip = request.ip() 128 | 129 | // save audit 130 | await Audit.create({ 131 | user_id: userId, 132 | auditable_id: auditableId, 133 | auditable, 134 | event, 135 | url, 136 | ip, 137 | old_data: oldData, 138 | new_data: newData, 139 | }) 140 | } 141 | 142 | module.exports = Auditable 143 | -------------------------------------------------------------------------------- /templates/Audit.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Model = use('Model') 4 | 5 | class Audit extends Model { 6 | /** 7 | * @returns {null} 8 | */ 9 | static get updatedAtColumn () { 10 | return null 11 | } 12 | 13 | /** 14 | * Auditable events 15 | * 16 | * @returns {Object} 17 | */ 18 | static get events () { 19 | return Object.freeze({ 20 | CREATE: 'create', 21 | UPDATE: 'update', 22 | DELETE: 'delete', 23 | }) 24 | } 25 | 26 | /** 27 | * @param value 28 | * @returns {any} 29 | */ 30 | getOldData (value) { 31 | if (value) { 32 | return JSON.parse(value) 33 | } 34 | } 35 | 36 | /** 37 | * @param value 38 | * @returns {any} 39 | */ 40 | setOldData (value) { 41 | if (value !== null) { 42 | return JSON.stringify(value) 43 | } 44 | } 45 | 46 | /** 47 | * @param value 48 | * @returns {any} 49 | */ 50 | getNewData (value) { 51 | if (value) { 52 | return JSON.parse(value) 53 | } 54 | } 55 | 56 | /** 57 | * @param value 58 | * @returns {any} 59 | */ 60 | setNewData (value) { 61 | if (value !== null) { 62 | return JSON.stringify(value) 63 | } 64 | } 65 | } 66 | 67 | module.exports = Audit 68 | -------------------------------------------------------------------------------- /templates/AuditSchema.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Schema = use('Schema') 4 | 5 | class AuditSchema extends Schema { 6 | up () { 7 | this.create('audits', (table) => { 8 | table.increments() 9 | table.integer('user_id').unsigned().references('id').inTable('users') 10 | table.integer('auditable_id').notNullable().index() 11 | table.string('auditable').notNullable() 12 | table.string('event').notNullable() 13 | table.string('ip', 45).notNullable() 14 | table.text('url').notNullable() 15 | table.text('old_data', 'longtext') 16 | table.text('new_data', 'longtext') 17 | table.dateTime('created_at') 18 | }) 19 | } 20 | 21 | down () { 22 | this.drop('audits') 23 | } 24 | } 25 | 26 | module.exports = AuditSchema 27 | --------------------------------------------------------------------------------