├── resources └── views │ ├── emails │ ├── invitation-text.edge │ └── invitation.edge │ └── register.edge ├── app ├── Models │ ├── Token.js │ ├── Project.js │ ├── Traits │ │ └── NoTimestamp.js │ ├── Invite.js │ ├── UserTeam.js │ ├── Team.js │ ├── Hooks │ │ └── InviteHook.js │ └── User.js ├── Validators │ ├── Team.js │ ├── Project.js │ ├── Session.js │ ├── Invite.js │ └── User.js ├── Controllers │ └── Http │ │ ├── RoleController.js │ │ ├── SessionController.js │ │ ├── PermissionController.js │ │ ├── MemberController.js │ │ ├── UserController.js │ │ ├── InviteController.js │ │ ├── ProjectController.js │ │ └── TeamController.js ├── Middleware │ ├── ConvertEmptyStringsToNull.js │ └── Team.js └── Jobs │ └── InvitationEmail.js ├── .gitignore ├── .editorconfig ├── .eslintrc.json ├── .env.example ├── database ├── migrations │ ├── 1577975108755_create_roles_table.js │ ├── 1503250034279_user.js │ ├── 1577975108737_create_permissions_table.js │ ├── 1503250034280_token.js │ ├── 1577816439753_project_schema.js │ ├── 1577975108761_create_permission_role_table.js │ ├── 1577816421560_team_schema.js │ ├── 1577975108765_create_role_user_table.js │ ├── 1577975108763_create_permission_user_table.js │ ├── 1577816464630_user_team_schema.js │ └── 1577816508220_invite_schema.js ├── factory.js └── seeds │ └── DatabaseSeeder.js ├── start ├── redis.js ├── kernel.js ├── routes.js └── app.js ├── ace ├── server.js ├── package.json ├── config ├── hash.js ├── redis.js ├── database.js ├── auth.js ├── cors.js ├── mail.js ├── bodyParser.js └── app.js ├── README.md └── Insomnia.json /resources/views/emails/invitation-text.edge: -------------------------------------------------------------------------------- 1 | You were invited by the user {{ user }} to be a part of the team {{ team }} 2 | In order to create an account, click on the link below {{ link }} 3 | -------------------------------------------------------------------------------- /resources/views/emails/invitation.edge: -------------------------------------------------------------------------------- 1 |

You were invited by the user {{ user }} to be a part of the team {{ team }}.

2 |

In order to create an account, click here

3 | -------------------------------------------------------------------------------- /app/Models/Token.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */ 4 | const Model = use('Model') 5 | 6 | class Token extends Model { 7 | } 8 | 9 | module.exports = Token 10 | -------------------------------------------------------------------------------- /app/Models/Project.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */ 4 | const Model = use('Model') 5 | 6 | class Project extends Model { 7 | } 8 | 9 | module.exports = Project 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node modules 2 | node_modules 3 | 4 | # Adonis directory for storing tmp files 5 | tmp 6 | 7 | # Environment variables, never commit this file 8 | .env 9 | 10 | # The development sqlite file 11 | database/*.sqlite 12 | -------------------------------------------------------------------------------- /app/Validators/Team.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class Team { 4 | get validateAll () { 5 | return true 6 | } 7 | 8 | get rules () { 9 | return { 10 | name: 'required' 11 | } 12 | } 13 | } 14 | 15 | module.exports = Team 16 | -------------------------------------------------------------------------------- /app/Validators/Project.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class Project { 4 | get validateAll () { 5 | return true 6 | } 7 | 8 | get rules () { 9 | return { 10 | title: 'required' 11 | } 12 | } 13 | } 14 | 15 | module.exports = Project 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_size = 2 6 | indent_style = space 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /app/Controllers/Http/RoleController.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const Role = use('Adonis/Acl/Role') 3 | 4 | class RoleController { 5 | async index ({ request }) { 6 | const roles = await Role.all() 7 | return roles 8 | } 9 | } 10 | 11 | module.exports = RoleController 12 | -------------------------------------------------------------------------------- /app/Validators/Session.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class Session { 4 | get validateAll () { 5 | return true 6 | } 7 | 8 | get rules () { 9 | return { 10 | email: 'required|email', 11 | password: 'required' 12 | } 13 | } 14 | } 15 | 16 | module.exports = Session 17 | -------------------------------------------------------------------------------- /app/Validators/Invite.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class Invite { 4 | get validateAll () { 5 | return true 6 | } 7 | 8 | get rules () { 9 | return { 10 | invites: 'required|array', 11 | 'invites.*': 'required|email' 12 | } 13 | } 14 | } 15 | 16 | module.exports = Invite 17 | -------------------------------------------------------------------------------- /app/Validators/User.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class User { 4 | get validateAll () { 5 | return true 6 | } 7 | 8 | get rules () { 9 | return { 10 | name: 'required', 11 | email: 'required|email|unique:users', 12 | password: 'required' 13 | } 14 | } 15 | } 16 | 17 | module.exports = User 18 | -------------------------------------------------------------------------------- /app/Models/Traits/NoTimestamp.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class NoTimestamp { 4 | register (Model) { 5 | Object.defineProperties(Model, { 6 | createdAtColumn: { 7 | get: () => null, 8 | }, 9 | updatedAtColumn: { 10 | get: () => null, 11 | } 12 | }) 13 | } 14 | } 15 | 16 | module.exports = NoTimestamp 17 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "extends": [ 7 | "standard" 8 | ], 9 | "globals": { 10 | "Atomics": "readonly", 11 | "SharedArrayBuffer": "readonly", 12 | "use": true 13 | }, 14 | "parserOptions": { 15 | "ecmaVersion": 2018 16 | }, 17 | "rules": { 18 | "max-len": [2, 80, 4, {"ignoreUrls": true}] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | HOST=127.0.0.1 2 | PORT=3333 3 | NODE_ENV=development 4 | 5 | APP_NAME=AdonisJs 6 | APP_URL=http://${HOST}:${PORT} 7 | FRONT_URL=http://localhost:3000 8 | CACHE_VIEWS=false 9 | 10 | APP_KEY= 11 | 12 | DB_CONNECTION=mysql 13 | DB_HOST=127.0.0.1 14 | DB_PORT=3306 15 | DB_USER=root 16 | DB_PASSWORD= 17 | DB_DATABASE=adonis_saas 18 | 19 | HASH_DRIVER=bcrypt 20 | 21 | SMTP_PORT= 22 | SMTP_HOST= 23 | MAIL_USERNAME= 24 | MAIL_PASSWORD= 25 | MAIL_FROM='no-reply@omnistack.com.br' 26 | -------------------------------------------------------------------------------- /app/Middleware/ConvertEmptyStringsToNull.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class ConvertEmptyStringsToNull { 4 | async handle ({ request }, next) { 5 | if (Object.keys(request.body).length) { 6 | request.body = Object.assign( 7 | ...Object.keys(request.body).map(key => ({ 8 | [key]: request.body[key] !== '' ? request.body[key] : null 9 | })) 10 | ) 11 | } 12 | 13 | await next() 14 | } 15 | } 16 | 17 | module.exports = ConvertEmptyStringsToNull 18 | -------------------------------------------------------------------------------- /app/Models/Invite.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */ 4 | const Model = use('Model') 5 | 6 | class Invite extends Model { 7 | static boot () { 8 | super.boot() 9 | this.addHook('afterCreate', 'InviteHook.sendInvitationEmail') 10 | } 11 | 12 | user () { 13 | return this.belongsTo('App/Models/User') 14 | } 15 | 16 | team () { 17 | return this.belongsTo('App/Models/Team') 18 | } 19 | } 20 | 21 | module.exports = Invite 22 | -------------------------------------------------------------------------------- /app/Controllers/Http/SessionController.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const User = use('App/Models/User') 3 | class SessionController { 4 | async store ({ request, auth }) { 5 | const { email, password } = request.all() 6 | const token = await auth.attempt(email, password) 7 | if (token) { 8 | const user = await User.query() 9 | .where('email', email).first() 10 | return { token: token.token, user } 11 | } 12 | return token 13 | } 14 | } 15 | 16 | module.exports = SessionController 17 | -------------------------------------------------------------------------------- /app/Controllers/Http/PermissionController.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const UserTeam = use('App/Models/UserTeam') 4 | 5 | class PermissionController { 6 | async show ({ request, auth }) { 7 | const teamJoin = await UserTeam.query() 8 | .where('team_id', request.team.id) 9 | .where('user_id', auth.user.id) 10 | .first() 11 | return { 12 | roles: await teamJoin.getRoles(), 13 | permissions: await teamJoin.getPermissions() 14 | } 15 | } 16 | } 17 | 18 | module.exports = PermissionController 19 | -------------------------------------------------------------------------------- /database/migrations/1577975108755_create_roles_table.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Schema = use('Schema') 4 | 5 | class RolesTableSchema extends Schema { 6 | up () { 7 | this.create('roles', table => { 8 | table.increments() 9 | table.string('slug').notNullable().unique() 10 | table.string('name').notNullable().unique() 11 | table.text('description').nullable() 12 | table.timestamps() 13 | }) 14 | } 15 | 16 | down () { 17 | this.drop('roles') 18 | } 19 | } 20 | 21 | module.exports = RolesTableSchema 22 | -------------------------------------------------------------------------------- /database/migrations/1503250034279_user.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** @type {import('@adonisjs/lucid/src/Schema')} */ 4 | const Schema = use('Schema') 5 | 6 | class UserSchema extends Schema { 7 | up () { 8 | this.create('users', (table) => { 9 | table.increments() 10 | table.string('name').notNullable() 11 | table.string('email').notNullable().unique() 12 | table.string('password').notNullable() 13 | table.timestamps() 14 | }) 15 | } 16 | 17 | down () { 18 | this.drop('users') 19 | } 20 | } 21 | 22 | module.exports = UserSchema 23 | -------------------------------------------------------------------------------- /database/migrations/1577975108737_create_permissions_table.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Schema = use('Schema') 4 | 5 | class PermissionsTableSchema extends Schema { 6 | up () { 7 | this.create('permissions', table => { 8 | table.increments() 9 | table.string('slug').notNullable().unique() 10 | table.string('name').notNullable().unique() 11 | table.text('description').nullable() 12 | table.timestamps() 13 | }) 14 | } 15 | 16 | down () { 17 | this.drop('permissions') 18 | } 19 | } 20 | 21 | module.exports = PermissionsTableSchema 22 | -------------------------------------------------------------------------------- /app/Controllers/Http/MemberController.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const UserTeam = use('App/Models/UserTeam') 3 | 4 | class MemberController { 5 | async index ({ request }) { 6 | const members = await UserTeam.query() 7 | .where('team_id', request.team.id) 8 | .with('user') 9 | .with('roles') 10 | .fetch() 11 | return members 12 | } 13 | 14 | async update ({ request, params }) { 15 | const roles = request.input('roles') 16 | 17 | const teamJoin = await UserTeam.find(params.id) 18 | await teamJoin.roles().sync(roles) 19 | } 20 | } 21 | 22 | module.exports = MemberController 23 | -------------------------------------------------------------------------------- /app/Models/UserTeam.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */ 4 | const Model = use('Model') 5 | 6 | class UserTeam extends Model { 7 | static get traits () { 8 | return [ 9 | '@provider:Adonis/Acl/HasRole', 10 | '@provider:Adonis/Acl/HasPermission' 11 | ] 12 | } 13 | 14 | roles () { 15 | return this.belongsToMany('Adonis/Acl/Role') 16 | } 17 | 18 | permission () { 19 | return this.belongsToMany('Adonis/Acl/Permission') 20 | } 21 | 22 | user () { 23 | return this.belongsTo('App/Models/User') 24 | } 25 | } 26 | 27 | module.exports = UserTeam 28 | -------------------------------------------------------------------------------- /database/factory.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* 4 | |-------------------------------------------------------------------------- 5 | | Factory 6 | |-------------------------------------------------------------------------- 7 | | 8 | | Factories are used to define blueprints for database tables or Lucid 9 | | models. Later you can use these blueprints to seed your database 10 | | with dummy data. 11 | | 12 | */ 13 | 14 | /** @type {import('@adonisjs/lucid/src/Factory')} */ 15 | // const Factory = use('Factory') 16 | 17 | // Factory.blueprint('App/Models/User', (faker) => { 18 | // return { 19 | // username: faker.username() 20 | // } 21 | // }) 22 | -------------------------------------------------------------------------------- /app/Models/Team.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */ 4 | const Model = use('Model') 5 | 6 | class Team extends Model { 7 | static boot () { 8 | super.boot() 9 | this.addTrait('@provider:Lucid/Slugify', { 10 | fields: { 11 | slug: 'name' 12 | }, 13 | strategy: 'dbIncrement', 14 | disableUpdates: false 15 | }) 16 | } 17 | 18 | users () { 19 | return this.belongsToMany('App/Models/User') 20 | .pivotModel('App/Models/UserTeam') 21 | } 22 | 23 | projects () { 24 | return this.hasMany('App/Models/Project') 25 | } 26 | } 27 | 28 | module.exports = Team 29 | -------------------------------------------------------------------------------- /start/redis.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* 4 | |-------------------------------------------------------------------------- 5 | | Redis Subscribers 6 | |-------------------------------------------------------------------------- 7 | | 8 | | Here you can register the subscribers to redis channels. Adonis assumes 9 | | your listeners are stored inside `app/Listeners` directory. 10 | | 11 | */ 12 | 13 | // const Redis = use('Redis') 14 | 15 | /** 16 | * Inline subscriber 17 | */ 18 | // Redis.subscribe('news', async () => { 19 | // }) 20 | 21 | /** 22 | * Binding method from a module saved inside `app/Listeners/News` 23 | */ 24 | // Redis.subcribe('news', 'News.onMessage') 25 | -------------------------------------------------------------------------------- /ace: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* 4 | |-------------------------------------------------------------------------- 5 | | Ace Commands 6 | |-------------------------------------------------------------------------- 7 | | 8 | | The ace file is just a regular Javascript file but with no extension. You 9 | | can call `node ace` followed by the command name and it just works. 10 | | 11 | | Also you can use `adonis` followed by the command name, since the adonis 12 | | global proxies all the ace commands. 13 | | 14 | */ 15 | 16 | const { Ignitor } = require('@adonisjs/ignitor') 17 | 18 | new Ignitor(require('@adonisjs/fold')) 19 | .appRoot(__dirname) 20 | .fireAce() 21 | .catch(console.error) 22 | -------------------------------------------------------------------------------- /database/migrations/1503250034280_token.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** @type {import('@adonisjs/lucid/src/Schema')} */ 4 | const Schema = use('Schema') 5 | 6 | class TokensSchema extends Schema { 7 | up () { 8 | this.create('tokens', (table) => { 9 | table.increments() 10 | table.integer('user_id').unsigned().references('id').inTable('users') 11 | table.string('token', 255).notNullable().unique().index() 12 | table.string('type', 80).notNullable() 13 | table.boolean('is_revoked').defaultTo(false) 14 | table.timestamps() 15 | }) 16 | } 17 | 18 | down () { 19 | this.drop('tokens') 20 | } 21 | } 22 | 23 | module.exports = TokensSchema 24 | -------------------------------------------------------------------------------- /database/migrations/1577816439753_project_schema.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** @type {import('@adonisjs/lucid/src/Schema')} */ 4 | const Schema = use('Schema') 5 | 6 | class ProjectSchema extends Schema { 7 | up () { 8 | this.create('projects', (table) => { 9 | table.increments() 10 | table.string('title').notNullable() 11 | table.integer('team_id') 12 | .unsigned() 13 | .notNullable() 14 | .references('id') 15 | .inTable('teams') 16 | .onUpdate('CASCADE') 17 | .onDelete('CASCADE') 18 | table.timestamps() 19 | }) 20 | } 21 | 22 | down () { 23 | this.drop('projects') 24 | } 25 | } 26 | 27 | module.exports = ProjectSchema 28 | -------------------------------------------------------------------------------- /app/Jobs/InvitationEmail.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const Mail = use('Mail') 3 | const Env = use('Env') 4 | 5 | class InvitationEmail { 6 | static get concurrency () { 7 | return 1 8 | } 9 | 10 | static get key () { 11 | return 'InvitationEmail-job' 12 | } 13 | 14 | async handle ({ user, team, email, link }) { 15 | await Mail.send( 16 | ['emails.invitation', 'emails.invitation-text'], 17 | { team: team.name, user: user.name, link }, 18 | message => { 19 | message 20 | .to(email) 21 | .from(Env.get('MAIL_FROM'), 'Igor | Omnistack') 22 | .subject(`Invitation for the team ${team.name}`) 23 | } 24 | ) 25 | } 26 | } 27 | 28 | module.exports = InvitationEmail 29 | -------------------------------------------------------------------------------- /app/Models/Hooks/InviteHook.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const User = use('App/Models/User') 4 | const Kue = use('Kue') 5 | const Job = use('App/Jobs/InvitationEmail') 6 | const Env = use('Env') 7 | 8 | const InviteHook = exports = module.exports = {} 9 | 10 | InviteHook.sendInvitationEmail = async (invite) => { 11 | const { email } = invite 12 | const invited = await User.findBy('email', email) 13 | if (invited) { 14 | await invited.teams().attach(invite.team_id) 15 | } else { 16 | const user = await invite.user().fetch() 17 | const team = await invite.team().fetch() 18 | const link = `${Env.get('FRONT_URL')}/signup` 19 | 20 | Kue.dispatch(Job.key, { user, team, email, link }, { attemps: 3 }) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /database/migrations/1577975108761_create_permission_role_table.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Schema = use('Schema') 4 | 5 | class PermissionRoleTableSchema extends Schema { 6 | up () { 7 | this.create('permission_role', table => { 8 | table.increments() 9 | table.integer('permission_id').unsigned().index() 10 | table.foreign('permission_id').references('id').on('permissions').onDelete('cascade') 11 | table.integer('role_id').unsigned().index() 12 | table.foreign('role_id').references('id').on('roles').onDelete('cascade') 13 | table.timestamps() 14 | }) 15 | } 16 | 17 | down () { 18 | this.drop('permission_role') 19 | } 20 | } 21 | 22 | module.exports = PermissionRoleTableSchema 23 | -------------------------------------------------------------------------------- /database/migrations/1577816421560_team_schema.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** @type {import('@adonisjs/lucid/src/Schema')} */ 4 | const Schema = use('Schema') 5 | 6 | class TeamSchema extends Schema { 7 | up () { 8 | this.create('teams', (table) => { 9 | table.increments() 10 | table.string('name').notNullable() 11 | table.integer('user_id') 12 | .unsigned() 13 | .notNullable() 14 | .references('id') 15 | .inTable('users') 16 | .onUpdate('CASCADE') 17 | .onDelete('CASCADE') 18 | table.string('slug') 19 | .notNullable() 20 | .unique() 21 | table.timestamps() 22 | }) 23 | } 24 | 25 | down () { 26 | this.drop('teams') 27 | } 28 | } 29 | 30 | module.exports = TeamSchema 31 | -------------------------------------------------------------------------------- /database/migrations/1577975108765_create_role_user_table.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Schema = use('Schema') 4 | 5 | class RoleUserTableSchema extends Schema { 6 | up () { 7 | this.create('role_user_team', table => { 8 | table.increments() 9 | table.integer('role_id').unsigned().index() 10 | table.foreign('role_id') 11 | .references('id') 12 | .on('roles') 13 | .onDelete('cascade') 14 | table.integer('user_team_id').unsigned().index() 15 | table.foreign('user_team_id') 16 | .references('id') 17 | .on('user_teams') 18 | .onDelete('cascade') 19 | table.timestamps() 20 | }) 21 | } 22 | 23 | down () { 24 | this.drop('role_user_team') 25 | } 26 | } 27 | 28 | module.exports = RoleUserTableSchema 29 | -------------------------------------------------------------------------------- /database/migrations/1577975108763_create_permission_user_table.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Schema = use('Schema') 4 | 5 | class PermissionUserTableSchema extends Schema { 6 | up () { 7 | this.create('permission_user_team', table => { 8 | table.increments() 9 | table.integer('permission_id').unsigned().index() 10 | table.foreign('permission_id') 11 | .references('id') 12 | .on('permissions') 13 | .onDelete('cascade') 14 | table.integer('user_team_id').unsigned().index() 15 | table.foreign('user_team_id') 16 | .references('id') 17 | .on('user_teams') 18 | .onDelete('cascade') 19 | table.timestamps() 20 | }) 21 | } 22 | 23 | down () { 24 | this.drop('permission_user_team') 25 | } 26 | } 27 | 28 | module.exports = PermissionUserTableSchema 29 | -------------------------------------------------------------------------------- /database/migrations/1577816464630_user_team_schema.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** @type {import('@adonisjs/lucid/src/Schema')} */ 4 | const Schema = use('Schema') 5 | 6 | class UserTeamSchema extends Schema { 7 | up () { 8 | this.create('user_teams', (table) => { 9 | table.increments() 10 | table.integer('user_id') 11 | .unsigned() 12 | .notNullable() 13 | .references('id') 14 | .inTable('users') 15 | .onUpdate('CASCADE') 16 | .onDelete('CASCADE') 17 | table.integer('team_id') 18 | .unsigned() 19 | .notNullable() 20 | .references('id') 21 | .inTable('teams') 22 | .onUpdate('CASCADE') 23 | .onDelete('CASCADE') 24 | table.timestamps() 25 | }) 26 | } 27 | 28 | down () { 29 | this.drop('user_teams') 30 | } 31 | } 32 | 33 | module.exports = UserTeamSchema 34 | -------------------------------------------------------------------------------- /app/Middleware/Team.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /** @typedef {import('@adonisjs/framework/src/Request')} Request */ 3 | /** @typedef {import('@adonisjs/framework/src/Response')} Response */ 4 | /** @typedef {import('@adonisjs/framework/src/View')} View */ 5 | 6 | class Team { 7 | /** 8 | * @param {object} ctx 9 | * @param {Request} ctx.request 10 | * @param {Function} next 11 | */ 12 | async handle ({ request, response, auth }, next) { 13 | const slug = request.header('TEAM') 14 | 15 | let team = null 16 | if (slug) { 17 | team = await auth.user.teams().where('slug', slug).first() 18 | } 19 | 20 | if (!team) { 21 | return response.status(401) 22 | .send({ error: { message: 'You are not a member of this team' } }) 23 | } 24 | 25 | request.team = team 26 | auth.user.currentTeam = team.id 27 | await next() 28 | } 29 | } 30 | 31 | module.exports = Team 32 | -------------------------------------------------------------------------------- /database/migrations/1577816508220_invite_schema.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** @type {import('@adonisjs/lucid/src/Schema')} */ 4 | const Schema = use('Schema') 5 | 6 | class InviteSchema extends Schema { 7 | up () { 8 | this.create('invites', (table) => { 9 | table.increments() 10 | table.integer('user_id') 11 | .unsigned() 12 | .notNullable() 13 | .references('id') 14 | .inTable('users') 15 | .onUpdate('CASCADE') 16 | .onDelete('CASCADE') 17 | table.integer('team_id') 18 | .unsigned() 19 | .notNullable() 20 | .references('id') 21 | .inTable('teams') 22 | .onUpdate('CASCADE') 23 | .onDelete('CASCADE') 24 | table.string('email').notNullable() 25 | table.timestamps() 26 | }) 27 | } 28 | 29 | down () { 30 | this.drop('invites') 31 | } 32 | } 33 | 34 | module.exports = InviteSchema 35 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* 4 | |-------------------------------------------------------------------------- 5 | | Http server 6 | |-------------------------------------------------------------------------- 7 | | 8 | | This file bootstraps Adonisjs to start the HTTP server. You are free to 9 | | customize the process of booting the http server. 10 | | 11 | | """ Loading ace commands """ 12 | | At times you may want to load ace commands when starting the HTTP server. 13 | | Same can be done by chaining `loadCommands()` method after 14 | | 15 | | """ Preloading files """ 16 | | Also you can preload files by calling `preLoad('path/to/file')` method. 17 | | Make sure to pass a relative path from the project root. 18 | */ 19 | 20 | const { Ignitor } = require('@adonisjs/ignitor') 21 | 22 | new Ignitor(require('@adonisjs/fold')) 23 | .appRoot(__dirname) 24 | .fireHttpServer() 25 | .catch(console.error) 26 | -------------------------------------------------------------------------------- /app/Controllers/Http/UserController.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const User = use('App/Models/User') 4 | const Invite = use('App/Models/Invite') 5 | 6 | class UserController { 7 | async store ({ request, response, auth }) { 8 | const data = request.only(['email', 'name', 'password']) 9 | 10 | const teamsQuery = Invite.query().where('email', data.email) 11 | const teams = await teamsQuery.pluck('team_id') 12 | if (teams.length === 0) { 13 | return response.status(401).send({ 14 | error: { message: 'You were not invited to any team' } 15 | }) 16 | } 17 | const user = await User.create(data) 18 | await user.teams().attach(teams) 19 | await teamsQuery.delete() 20 | const token = await auth.attempt(data.email, data.password) 21 | return token 22 | } 23 | 24 | async create ({ view }) { 25 | return view.render('register') 26 | } 27 | } 28 | 29 | module.exports = UserController 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adonis-api-app", 3 | "version": "4.1.0", 4 | "adonis-version": "4.1.0", 5 | "description": "Adonisjs boilerplate for API server with pre-configured JWT", 6 | "main": "index.js", 7 | "scripts": { 8 | "start": "node server.js", 9 | "test": "node ace test" 10 | }, 11 | "keywords": [ 12 | "adonisjs", 13 | "adonis-app" 14 | ], 15 | "author": "", 16 | "license": "UNLICENSED", 17 | "private": true, 18 | "dependencies": { 19 | "@adonisjs/ace": "^5.0.8", 20 | "@adonisjs/auth": "^3.0.7", 21 | "@adonisjs/bodyparser": "^2.0.5", 22 | "@adonisjs/cors": "^1.0.7", 23 | "@adonisjs/fold": "^4.0.9", 24 | "@adonisjs/framework": "^5.0.9", 25 | "@adonisjs/ignitor": "^2.0.8", 26 | "@adonisjs/lucid": "^6.1.3", 27 | "@adonisjs/lucid-slugify": "^1.0.3", 28 | "@adonisjs/mail": "^3.0.10", 29 | "@adonisjs/redis": "^2.0.7", 30 | "@adonisjs/validator": "^5.0.6", 31 | "adonis-acl": "^1.1.1", 32 | "adonis-kue": "^5.0.1", 33 | "mysql": "^2.17.1" 34 | }, 35 | "devDependencies": { 36 | "eslint": "^6.8.0", 37 | "eslint-config-standard": "^14.1.0", 38 | "eslint-plugin-import": "^2.19.1", 39 | "eslint-plugin-node": "^11.0.0", 40 | "eslint-plugin-promise": "^4.2.1", 41 | "eslint-plugin-standard": "^4.0.1" 42 | }, 43 | "autoload": { 44 | "App": "./app" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /config/hash.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** @type {import('@adonisjs/framework/src/Env')} */ 4 | const Env = use('Env') 5 | 6 | module.exports = { 7 | /* 8 | |-------------------------------------------------------------------------- 9 | | Driver 10 | |-------------------------------------------------------------------------- 11 | | 12 | | Driver to be used for hashing values. The same driver is used by the 13 | | auth module too. 14 | | 15 | */ 16 | driver: Env.get('HASH_DRIVER', 'bcrypt'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Bcrypt 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Config related to bcrypt hashing. https://www.npmjs.com/package/bcrypt 24 | | package is used internally. 25 | | 26 | */ 27 | bcrypt: { 28 | rounds: 10 29 | }, 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | Argon 34 | |-------------------------------------------------------------------------- 35 | | 36 | | Config related to argon. https://www.npmjs.com/package/argon2 package is 37 | | used internally. 38 | | 39 | | Since argon is optional, you will have to install the dependency yourself 40 | | 41 | |============================================================================ 42 | | npm i argon2 43 | |============================================================================ 44 | | 45 | */ 46 | argon: { 47 | type: 1 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /config/redis.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* 4 | |-------------------------------------------------------------------------- 5 | | Redis Configuaration 6 | |-------------------------------------------------------------------------- 7 | | 8 | | Here we define the configuration for redis server. A single application 9 | | can make use of multiple redis connections using the redis provider. 10 | | 11 | */ 12 | 13 | const Env = use('Env') 14 | 15 | module.exports = { 16 | /* 17 | |-------------------------------------------------------------------------- 18 | | connection 19 | |-------------------------------------------------------------------------- 20 | | 21 | | Redis connection to be used by default. 22 | | 23 | */ 24 | connection: Env.get('REDIS_CONNECTION', 'local'), 25 | 26 | /* 27 | |-------------------------------------------------------------------------- 28 | | local connection config 29 | |-------------------------------------------------------------------------- 30 | | 31 | | Configuration for a named connection. 32 | | 33 | */ 34 | local: { 35 | host: '127.0.0.1', 36 | port: 6379, 37 | password: null, 38 | db: 0, 39 | keyPrefix: '' 40 | }, 41 | 42 | /* 43 | |-------------------------------------------------------------------------- 44 | | cluster config 45 | |-------------------------------------------------------------------------- 46 | | 47 | | Below is the configuration for the redis cluster. 48 | | 49 | */ 50 | cluster: { 51 | clusters: [{ 52 | host: '127.0.0.1', 53 | port: 6379, 54 | password: null, 55 | db: 0 56 | }, 57 | { 58 | host: '127.0.0.1', 59 | port: 6380, 60 | password: null, 61 | db: 0 62 | }] 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /database/seeds/DatabaseSeeder.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* 4 | |-------------------------------------------------------------------------- 5 | | DatabaseSeeder 6 | |-------------------------------------------------------------------------- 7 | | 8 | | Make use of the Factory instance to seed database with dummy data or 9 | | make use of Lucid models directly. 10 | | 11 | */ 12 | 13 | /** @type {import('@adonisjs/lucid/src/Factory')} */ 14 | 15 | const User = use('App/Models/User') 16 | const Role = use('Adonis/Acl/Role') 17 | const Permission = use('Adonis/Acl/Permission') 18 | 19 | class DatabaseSeeder { 20 | async run () { 21 | const user = await User.create({ 22 | name: 'Admin', 23 | email: 'admin@gmail.com', 24 | password: '123456' 25 | }) 26 | const createInvite = await Permission.create({ 27 | slug: 'invites_create', 28 | name: 'Invite members' 29 | }) 30 | const createProject = await Permission.create({ 31 | slug: 'projects_create', 32 | name: 'Create projects' 33 | }) 34 | const admin = await Role.create({ 35 | slug: 'administrator', 36 | name: 'Administrator' 37 | }) 38 | 39 | const moderator = await Role.create({ 40 | slug: 'moderator', 41 | name: 'Moderator' 42 | }) 43 | 44 | await Role.create({ 45 | slug: 'visitor', 46 | name: 'Visitor' 47 | }) 48 | 49 | await admin.permissions().attach([createInvite.id, createProject.id]) 50 | await moderator.permissions().attach([createProject.id]) 51 | 52 | const team = await user.teams().create({ 53 | name: 'Default', 54 | user_id: user.id 55 | }) 56 | 57 | const teamJoin = await user.teamJoins().where('team_id', team.id).first() 58 | await teamJoin.roles().attach([admin.id]) 59 | } 60 | } 61 | 62 | module.exports = DatabaseSeeder 63 | -------------------------------------------------------------------------------- /app/Models/User.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** @type {typeof import('@adonisjs/lucid/src/Lucid/Model')} */ 4 | const Model = use('Model') 5 | 6 | /** @type {import('@adonisjs/framework/src/Hash')} */ 7 | const Hash = use('Hash') 8 | 9 | class User extends Model { 10 | static get hidden () { 11 | return ['password'] 12 | } 13 | 14 | static boot () { 15 | super.boot() 16 | 17 | /** 18 | * A hook to hash the user password before saving 19 | * it to the database. 20 | */ 21 | this.addHook('beforeSave', async (userInstance) => { 22 | if (userInstance.dirty.password) { 23 | userInstance.password = await Hash.make(userInstance.password) 24 | } 25 | }) 26 | } 27 | 28 | /** 29 | * A relationship on tokens is required for auth to 30 | * work. Since features like `refreshTokens` or 31 | * `rememberToken` will be saved inside the 32 | * tokens table. 33 | * 34 | * @method tokens 35 | * 36 | * @return {Object} 37 | */ 38 | tokens () { 39 | return this.hasMany('App/Models/Token') 40 | } 41 | 42 | teamJoins () { 43 | return this.hasMany('App/Models/UserTeam') 44 | } 45 | 46 | teams () { 47 | return this.belongsToMany('App/Models/Team') 48 | .pivotModel('App/Models/UserTeam') 49 | } 50 | 51 | async is (expression) { 52 | const team = await this.teamJoins() 53 | .where('team_id', this.currentTeam) 54 | .first() 55 | return team.is(expression) 56 | } 57 | 58 | async can (expression) { 59 | const team = await this.teamJoins() 60 | .where('team_id', this.currentTeam) 61 | .first() 62 | return team.can(expression) 63 | } 64 | 65 | async scope (required) { 66 | const team = await this.teamJoins() 67 | .where('team_id', this.currentTeam) 68 | .first() 69 | return team.scope(required) 70 | } 71 | } 72 | 73 | module.exports = User 74 | -------------------------------------------------------------------------------- /start/kernel.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** @type {import('@adonisjs/framework/src/Server')} */ 4 | const Server = use('Server') 5 | 6 | /* 7 | |-------------------------------------------------------------------------- 8 | | Global Middleware 9 | |-------------------------------------------------------------------------- 10 | | 11 | | Global middleware are executed on each http request only when the routes 12 | | match. 13 | | 14 | */ 15 | const globalMiddleware = [ 16 | 'Adonis/Middleware/BodyParser', 17 | 'App/Middleware/ConvertEmptyStringsToNull', 18 | 'Adonis/Acl/Init' 19 | ] 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Named Middleware 24 | |-------------------------------------------------------------------------- 25 | | 26 | | Named middleware is key/value object to conditionally add middleware on 27 | | specific routes or group of routes. 28 | | 29 | | // define 30 | | { 31 | | auth: 'Adonis/Middleware/Auth' 32 | | } 33 | | 34 | | // use 35 | | Route.get().middleware('auth') 36 | | 37 | */ 38 | const namedMiddleware = { 39 | auth: 'Adonis/Middleware/Auth', 40 | guest: 'Adonis/Middleware/AllowGuestOnly', 41 | team: 'App/Middleware/Team', 42 | is: 'Adonis/Acl/Is', 43 | can: 'Adonis/Acl/Can' 44 | } 45 | 46 | /* 47 | |-------------------------------------------------------------------------- 48 | | Server Middleware 49 | |-------------------------------------------------------------------------- 50 | | 51 | | Server level middleware are executed even when route for a given URL is 52 | | not registered. Features like `static assets` and `cors` needs better 53 | | control over request lifecycle. 54 | | 55 | */ 56 | const serverMiddleware = [ 57 | // 'Adonis/Middleware/Static', 58 | 'Adonis/Middleware/Cors' 59 | ] 60 | 61 | Server 62 | .registerGlobal(globalMiddleware) 63 | .registerNamed(namedMiddleware) 64 | .use(serverMiddleware) 65 | -------------------------------------------------------------------------------- /resources/views/register.edge: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ style('https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css')}} 8 | Register 9 | 10 | 11 |
12 |
13 |
14 |
Welcome aboard!
15 |
If you have an invitation, you can sign up
16 |
17 |
18 | 19 | 25 |
26 |
27 | 28 | 34 |
35 |
36 | 37 | 43 |
44 | 45 |
46 |
47 | 48 |
49 | 50 |
51 | {{ script('https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js') }} 52 | 53 | 54 | -------------------------------------------------------------------------------- /start/routes.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* 4 | |-------------------------------------------------------------------------- 5 | | Routes 6 | |-------------------------------------------------------------------------- 7 | | 8 | | Http routes are entry points to your web application. You can create 9 | | routes for different URLs and bind Controller actions to them. 10 | | 11 | | A complete guide on routing is available here. 12 | | http://adonisjs.com/docs/4.1/routing 13 | | 14 | */ 15 | 16 | /** @type {typeof import('@adonisjs/framework/src/Route/Manager')} */ 17 | const Route = use('Route') 18 | 19 | Route.post('sessions', 'SessionController.store').validator('Session') 20 | Route.post('users', 'UserController.store').validator('User') 21 | Route.get('register', 'UserController.create') 22 | 23 | Route.group(() => { 24 | Route.resource('teams', 'TeamController') 25 | .apiOnly() 26 | .validator(new Map( 27 | [ 28 | [ 29 | ['teams.store', 'teams.update'], 30 | ['Team'] 31 | ] 32 | ] 33 | )) 34 | Route.get('roles', 'RoleController.index') 35 | }).middleware('auth') 36 | 37 | Route.group(() => { 38 | Route.post('invites', 'InviteController.store') 39 | .validator('Invite') 40 | .middleware('can:invites_create') 41 | Route.resource('projects', 'ProjectController') 42 | .apiOnly() 43 | .validator(new Map( 44 | [ 45 | [ 46 | ['projects.store', 'projects.update'], 47 | ['Project'] 48 | ] 49 | ] 50 | )) 51 | .middleware(new Map( 52 | [ 53 | [ 54 | ['projects.store', 'projects.update'], 55 | ['can:projects_create'] 56 | ] 57 | ] 58 | )) 59 | Route.get('members', 'MemberController.index') 60 | Route.put('members/:id', 'MemberController.update') 61 | .middleware('is:administrator') 62 | Route.get('permissions', 'PermissionController.show') 63 | }).middleware(['auth', 'team']) 64 | -------------------------------------------------------------------------------- /app/Controllers/Http/InviteController.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** @typedef {import('@adonisjs/framework/src/Request')} Request */ 4 | /** @typedef {import('@adonisjs/framework/src/Response')} Response */ 5 | /** @typedef {import('@adonisjs/framework/src/View')} View */ 6 | 7 | const Invite = use('App/Models/Invite') 8 | 9 | /** 10 | * Resourceful controller for interacting with invites 11 | */ 12 | class InviteController { 13 | /** 14 | * Show a list of all invites. 15 | * GET invites 16 | * 17 | * @param {object} ctx 18 | * @param {Request} ctx.request 19 | * @param {Response} ctx.response 20 | * @param {View} ctx.view 21 | */ 22 | async index ({ request, response, view }) { 23 | } 24 | 25 | /** 26 | * Create/save a new invite. 27 | * POST invites 28 | * 29 | * @param {object} ctx 30 | * @param {Request} ctx.request 31 | * @param {Response} ctx.response 32 | */ 33 | async store ({ request, auth }) { 34 | const invites = request.input('invites') 35 | const data = invites.map(email => ({ 36 | email, 37 | user_id: auth.user.id, 38 | team_id: request.team.id 39 | })) 40 | 41 | await Invite.createMany(data) 42 | } 43 | 44 | /** 45 | * Display a single invite. 46 | * GET invites/:id 47 | * 48 | * @param {object} ctx 49 | * @param {Request} ctx.request 50 | * @param {Response} ctx.response 51 | * @param {View} ctx.view 52 | */ 53 | async show ({ params, request, response, view }) { 54 | } 55 | 56 | /** 57 | * Update invite details. 58 | * PUT or PATCH invites/:id 59 | * 60 | * @param {object} ctx 61 | * @param {Request} ctx.request 62 | * @param {Response} ctx.response 63 | */ 64 | async update ({ params, request, response }) { 65 | } 66 | 67 | /** 68 | * Delete a invite with id. 69 | * DELETE invites/:id 70 | * 71 | * @param {object} ctx 72 | * @param {Request} ctx.request 73 | * @param {Response} ctx.response 74 | */ 75 | async destroy ({ params, request, response }) { 76 | } 77 | } 78 | 79 | module.exports = InviteController 80 | -------------------------------------------------------------------------------- /app/Controllers/Http/ProjectController.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** @typedef {import('@adonisjs/framework/src/Request')} Request */ 4 | /** @typedef {import('@adonisjs/framework/src/Response')} Response */ 5 | /** @typedef {import('@adonisjs/framework/src/View')} View */ 6 | 7 | /** 8 | * Resourceful controller for interacting with projects 9 | */ 10 | class ProjectController { 11 | /** 12 | * Show a list of all projects. 13 | * GET projects 14 | * 15 | * @param {object} ctx 16 | * @param {Request} ctx.request 17 | * @param {Response} ctx.response 18 | * @param {View} ctx.view 19 | */ 20 | async index ({ request }) { 21 | const projects = await request.team.projects().fetch() 22 | return projects 23 | } 24 | 25 | /** 26 | * Create/save a new project. 27 | * POST projects 28 | * 29 | * @param {object} ctx 30 | * @param {Request} ctx.request 31 | * @param {Response} ctx.response 32 | */ 33 | async store ({ request }) { 34 | const data = request.only(['title']) 35 | const project = await request.team.projects().create(data) 36 | return project 37 | } 38 | 39 | /** 40 | * Display a single project. 41 | * GET projects/:id 42 | * 43 | * @param {object} ctx 44 | * @param {Request} ctx.request 45 | * @param {Response} ctx.response 46 | * @param {View} ctx.view 47 | */ 48 | async show ({ params, request }) { 49 | const project = await request.team.projects().where('id', params.id).first() 50 | return project 51 | } 52 | 53 | /** 54 | * Update project details. 55 | * PUT or PATCH projects/:id 56 | * 57 | * @param {object} ctx 58 | * @param {Request} ctx.request 59 | * @param {Response} ctx.response 60 | */ 61 | async update ({ params, request }) { 62 | const project = await request.team.projects().where('id', params.id).first() 63 | const data = request.only(['title']) 64 | project.merge(data) 65 | await project.save() 66 | return project 67 | } 68 | 69 | /** 70 | * Delete a project with id. 71 | * DELETE projects/:id 72 | * 73 | * @param {object} ctx 74 | * @param {Request} ctx.request 75 | * @param {Response} ctx.response 76 | */ 77 | async destroy ({ params, request }) { 78 | const project = await request.team.projects().where('id', params.id).first() 79 | project.delete() 80 | } 81 | } 82 | 83 | module.exports = ProjectController 84 | -------------------------------------------------------------------------------- /start/app.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* 4 | |-------------------------------------------------------------------------- 5 | | Providers 6 | |-------------------------------------------------------------------------- 7 | | 8 | | Providers are building blocks for your Adonis app. Anytime you install 9 | | a new Adonis specific package, chances are you will register the 10 | | provider here. 11 | | 12 | */ 13 | const providers = [ 14 | '@adonisjs/framework/providers/AppProvider', 15 | '@adonisjs/auth/providers/AuthProvider', 16 | '@adonisjs/bodyparser/providers/BodyParserProvider', 17 | '@adonisjs/cors/providers/CorsProvider', 18 | '@adonisjs/lucid/providers/LucidProvider', 19 | '@adonisjs/lucid-slugify/providers/SlugifyProvider', 20 | 'adonis-kue/providers/KueProvider', 21 | '@adonisjs/redis/providers/RedisProvider', 22 | '@adonisjs/mail/providers/MailProvider', 23 | '@adonisjs/framework/providers/ViewProvider', 24 | '@adonisjs/validator/providers/ValidatorProvider', 25 | 'adonis-acl/providers/AclProvider' 26 | ] 27 | 28 | /* 29 | |-------------------------------------------------------------------------- 30 | | Ace Providers 31 | |-------------------------------------------------------------------------- 32 | | 33 | | Ace providers are required only when running ace commands. For example 34 | | Providers for migrations, tests etc. 35 | | 36 | */ 37 | const aceProviders = [ 38 | '@adonisjs/lucid/providers/MigrationsProvider', 39 | 'adonis-kue/providers/CommandsProvider', 40 | 'adonis-acl/providers/CommandsProvider' 41 | ] 42 | 43 | /* 44 | |-------------------------------------------------------------------------- 45 | | Aliases 46 | |-------------------------------------------------------------------------- 47 | | 48 | | Aliases are short unique names for IoC container bindings. You are free 49 | | to create your own aliases. 50 | | 51 | | For example: 52 | | { Route: 'Adonis/Src/Route' } 53 | | 54 | */ 55 | const aliases = { 56 | Role: 'Adonis/Acl/Role', 57 | Permission: 'Adonis/Acl/Permission' 58 | } 59 | 60 | /* 61 | |-------------------------------------------------------------------------- 62 | | Commands 63 | |-------------------------------------------------------------------------- 64 | | 65 | | Here you store ace commands for your package 66 | | 67 | */ 68 | const commands = [] 69 | const jobs = [ 70 | 'App/Jobs/InvitationEmail' 71 | ] 72 | module.exports = { providers, aceProviders, aliases, commands, jobs } 73 | -------------------------------------------------------------------------------- /config/database.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** @type {import('@adonisjs/framework/src/Env')} */ 4 | const Env = use('Env') 5 | 6 | /** @type {import('@adonisjs/ignitor/src/Helpers')} */ 7 | const Helpers = use('Helpers') 8 | 9 | module.exports = { 10 | /* 11 | |-------------------------------------------------------------------------- 12 | | Default Connection 13 | |-------------------------------------------------------------------------- 14 | | 15 | | Connection defines the default connection settings to be used while 16 | | interacting with SQL databases. 17 | | 18 | */ 19 | connection: Env.get('DB_CONNECTION', 'sqlite'), 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Sqlite 24 | |-------------------------------------------------------------------------- 25 | | 26 | | Sqlite is a flat file database and can be a good choice for a development 27 | | environment. 28 | | 29 | | npm i --save sqlite3 30 | | 31 | */ 32 | sqlite: { 33 | client: 'sqlite3', 34 | connection: { 35 | filename: Helpers.databasePath(`${Env.get('DB_DATABASE', 'development')}.sqlite`) 36 | }, 37 | useNullAsDefault: true, 38 | debug: Env.get('DB_DEBUG', false) 39 | }, 40 | 41 | /* 42 | |-------------------------------------------------------------------------- 43 | | MySQL 44 | |-------------------------------------------------------------------------- 45 | | 46 | | Here we define connection settings for MySQL database. 47 | | 48 | | npm i --save mysql 49 | | 50 | */ 51 | mysql: { 52 | client: 'mysql', 53 | connection: { 54 | host: Env.get('DB_HOST', 'localhost'), 55 | port: Env.get('DB_PORT', ''), 56 | user: Env.get('DB_USER', 'root'), 57 | password: Env.get('DB_PASSWORD', ''), 58 | database: Env.get('DB_DATABASE', 'adonis') 59 | }, 60 | debug: Env.get('DB_DEBUG', false) 61 | }, 62 | 63 | /* 64 | |-------------------------------------------------------------------------- 65 | | PostgreSQL 66 | |-------------------------------------------------------------------------- 67 | | 68 | | Here we define connection settings for PostgreSQL database. 69 | | 70 | | npm i --save pg 71 | | 72 | */ 73 | pg: { 74 | client: 'pg', 75 | connection: { 76 | host: Env.get('DB_HOST', 'localhost'), 77 | port: Env.get('DB_PORT', ''), 78 | user: Env.get('DB_USER', 'root'), 79 | password: Env.get('DB_PASSWORD', ''), 80 | database: Env.get('DB_DATABASE', 'adonis') 81 | }, 82 | debug: Env.get('DB_DEBUG', false) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /app/Controllers/Http/TeamController.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** @typedef {import('@adonisjs/framework/src/Request')} Request */ 4 | /** @typedef {import('@adonisjs/framework/src/Response')} Response */ 5 | /** @typedef {import('@adonisjs/framework/src/View')} View */ 6 | 7 | const Role = use('Adonis/Acl/Role') 8 | /** 9 | * Resourceful controller for interacting with teams 10 | */ 11 | 12 | class TeamController { 13 | /** 14 | * Show a list of all teams. 15 | * GET teams 16 | * 17 | * @param {object} ctx 18 | * @param {Request} ctx.request 19 | * @param {Response} ctx.response 20 | * @param {View} ctx.view 21 | */ 22 | async index ({ auth }) { 23 | const teams = await auth.user.teams().fetch() 24 | return teams 25 | } 26 | 27 | /** 28 | * Create/save a new team. 29 | * POST teams 30 | * 31 | * @param {object} ctx 32 | * @param {Request} ctx.request 33 | * @param {Response} ctx.response 34 | */ 35 | async store ({ request, auth }) { 36 | const data = request.only(['name']) 37 | const team = await auth.user.teams().create({ 38 | ...data, 39 | user_id: auth.user.id 40 | }) 41 | 42 | const teamJoin = await auth.user.teamJoins() 43 | .where('team_id', team.id) 44 | .first() 45 | 46 | const admin = await Role.findBy('slug', 'administrator') 47 | await teamJoin.roles().attach([admin.id]) 48 | return team 49 | } 50 | 51 | /** 52 | * Display a single team. 53 | * GET teams/:id 54 | * 55 | * @param {object} ctx 56 | * @param {Request} ctx.request 57 | * @param {Response} ctx.response 58 | * @param {View} ctx.view 59 | */ 60 | async show ({ params, auth }) { 61 | const team = await auth.user.teams().where('teams.id', params.id).first() 62 | return team 63 | } 64 | 65 | /** 66 | * Update team details. 67 | * PUT or PATCH teams/:id 68 | * 69 | * @param {object} ctx 70 | * @param {Request} ctx.request 71 | * @param {Response} ctx.response 72 | */ 73 | async update ({ params, request, auth }) { 74 | const team = await auth.user.teams().where('teams.id', params.id).first() 75 | const data = request.only(['name']) 76 | team.merge(data) 77 | await team.save() 78 | return team 79 | } 80 | 81 | /** 82 | * Delete a team with id. 83 | * DELETE teams/:id 84 | * 85 | * @param {object} ctx 86 | * @param {Request} ctx.request 87 | * @param {Response} ctx.response 88 | */ 89 | async destroy ({ params, auth }) { 90 | const team = await auth.user.teams().where('teams.id', params.id).first() 91 | await team.delete() 92 | } 93 | } 94 | 95 | module.exports = TeamController 96 | -------------------------------------------------------------------------------- /config/auth.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** @type {import('@adonisjs/framework/src/Env')} */ 4 | const Env = use('Env') 5 | 6 | module.exports = { 7 | /* 8 | |-------------------------------------------------------------------------- 9 | | Authenticator 10 | |-------------------------------------------------------------------------- 11 | | 12 | | Authentication is a combination of serializer and scheme with extra 13 | | config to define on how to authenticate a user. 14 | | 15 | | Available Schemes - basic, session, jwt, api 16 | | Available Serializers - lucid, database 17 | | 18 | */ 19 | authenticator: 'jwt', 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Session 24 | |-------------------------------------------------------------------------- 25 | | 26 | | Session authenticator makes use of sessions to authenticate a user. 27 | | Session authentication is always persistent. 28 | | 29 | */ 30 | session: { 31 | serializer: 'lucid', 32 | model: 'App/Models/User', 33 | scheme: 'session', 34 | uid: 'email', 35 | password: 'password' 36 | }, 37 | 38 | /* 39 | |-------------------------------------------------------------------------- 40 | | Basic Auth 41 | |-------------------------------------------------------------------------- 42 | | 43 | | The basic auth authenticator uses basic auth header to authenticate a 44 | | user. 45 | | 46 | | NOTE: 47 | | This scheme is not persistent and users are supposed to pass 48 | | login credentials on each request. 49 | | 50 | */ 51 | basic: { 52 | serializer: 'lucid', 53 | model: 'App/Models/User', 54 | scheme: 'basic', 55 | uid: 'email', 56 | password: 'password' 57 | }, 58 | 59 | /* 60 | |-------------------------------------------------------------------------- 61 | | Jwt 62 | |-------------------------------------------------------------------------- 63 | | 64 | | The jwt authenticator works by passing a jwt token on each HTTP request 65 | | via HTTP `Authorization` header. 66 | | 67 | */ 68 | jwt: { 69 | serializer: 'lucid', 70 | model: 'App/Models/User', 71 | scheme: 'jwt', 72 | uid: 'email', 73 | password: 'password', 74 | options: { 75 | secret: Env.get('APP_KEY') 76 | } 77 | }, 78 | 79 | /* 80 | |-------------------------------------------------------------------------- 81 | | Api 82 | |-------------------------------------------------------------------------- 83 | | 84 | | The Api scheme makes use of API personal tokens to authenticate a user. 85 | | 86 | */ 87 | api: { 88 | serializer: 'lucid', 89 | model: 'App/Models/User', 90 | scheme: 'api', 91 | uid: 'email', 92 | password: 'password' 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /config/cors.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | /* 5 | |-------------------------------------------------------------------------- 6 | | Origin 7 | |-------------------------------------------------------------------------- 8 | | 9 | | Set a list of origins to be allowed. The value can be one of the following 10 | | 11 | | Boolean: true - Allow current request origin 12 | | Boolean: false - Disallow all 13 | | String - Comma separated list of allowed origins 14 | | Array - An array of allowed origins 15 | | String: * - A wildcard to allow current request origin 16 | | Function - Receives the current origin and should return one of the above values. 17 | | 18 | */ 19 | origin: true, 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Methods 24 | |-------------------------------------------------------------------------- 25 | | 26 | | HTTP methods to be allowed. The value can be one of the following 27 | | 28 | | String - Comma separated list of allowed methods 29 | | Array - An array of allowed methods 30 | | 31 | */ 32 | methods: ['GET', 'PUT', 'PATCH', 'POST', 'DELETE'], 33 | 34 | /* 35 | |-------------------------------------------------------------------------- 36 | | Headers 37 | |-------------------------------------------------------------------------- 38 | | 39 | | List of headers to be allowed via Access-Control-Request-Headers header. 40 | | The value can be one of the following. 41 | | 42 | | Boolean: true - Allow current request headers 43 | | Boolean: false - Disallow all 44 | | String - Comma separated list of allowed headers 45 | | Array - An array of allowed headers 46 | | String: * - A wildcard to allow current request headers 47 | | Function - Receives the current header and should return one of the above values. 48 | | 49 | */ 50 | headers: true, 51 | 52 | /* 53 | |-------------------------------------------------------------------------- 54 | | Expose Headers 55 | |-------------------------------------------------------------------------- 56 | | 57 | | A list of headers to be exposed via `Access-Control-Expose-Headers` 58 | | header. The value can be one of the following. 59 | | 60 | | Boolean: false - Disallow all 61 | | String: Comma separated list of allowed headers 62 | | Array - An array of allowed headers 63 | | 64 | */ 65 | exposeHeaders: false, 66 | 67 | /* 68 | |-------------------------------------------------------------------------- 69 | | Credentials 70 | |-------------------------------------------------------------------------- 71 | | 72 | | Define Access-Control-Allow-Credentials header. It should always be a 73 | | boolean. 74 | | 75 | */ 76 | credentials: false, 77 | 78 | /* 79 | |-------------------------------------------------------------------------- 80 | | MaxAge 81 | |-------------------------------------------------------------------------- 82 | | 83 | | Define Access-Control-Allow-Max-Age 84 | | 85 | */ 86 | maxAge: 90 87 | } 88 | -------------------------------------------------------------------------------- /config/mail.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Env = use('Env') 4 | 5 | module.exports = { 6 | /* 7 | |-------------------------------------------------------------------------- 8 | | Connection 9 | |-------------------------------------------------------------------------- 10 | | 11 | | Connection to be used for sending emails. Each connection needs to 12 | | define a driver too. 13 | | 14 | */ 15 | connection: Env.get('MAIL_CONNECTION', 'smtp'), 16 | 17 | /* 18 | |-------------------------------------------------------------------------- 19 | | SMTP 20 | |-------------------------------------------------------------------------- 21 | | 22 | | Here we define configuration for sending emails via SMTP. 23 | | 24 | */ 25 | smtp: { 26 | driver: 'smtp', 27 | pool: true, 28 | port: Env.get('SMTP_PORT', 2525), 29 | host: Env.get('SMTP_HOST'), 30 | secure: false, 31 | auth: { 32 | user: Env.get('MAIL_USERNAME'), 33 | pass: Env.get('MAIL_PASSWORD') 34 | }, 35 | maxConnections: 5, 36 | maxMessages: 100, 37 | rateLimit: 10 38 | }, 39 | 40 | /* 41 | |-------------------------------------------------------------------------- 42 | | SparkPost 43 | |-------------------------------------------------------------------------- 44 | | 45 | | Here we define configuration for spark post. Extra options can be defined 46 | | inside the `extra` object. 47 | | 48 | | https://developer.sparkpost.com/api/transmissions.html#header-options-attributes 49 | | 50 | | extras: { 51 | | campaign_id: 'sparkpost campaign id', 52 | | options: { // sparkpost options } 53 | | } 54 | | 55 | */ 56 | sparkpost: { 57 | driver: 'sparkpost', 58 | apiKey: Env.get('SPARKPOST_API_KEY'), 59 | extras: {} 60 | }, 61 | 62 | /* 63 | |-------------------------------------------------------------------------- 64 | | Mailgun 65 | |-------------------------------------------------------------------------- 66 | | 67 | | Here we define configuration for mailgun. Extra options can be defined 68 | | inside the `extra` object. 69 | | 70 | | https://mailgun-documentation.readthedocs.io/en/latest/api-sending.html#sending 71 | | 72 | | extras: { 73 | | 'o:tag': '', 74 | | 'o:campaign': '',, 75 | | . . . 76 | | } 77 | | 78 | */ 79 | mailgun: { 80 | driver: 'mailgun', 81 | domain: Env.get('MAILGUN_DOMAIN'), 82 | region: Env.get('MAILGUN_API_REGION'), 83 | apiKey: Env.get('MAILGUN_API_KEY'), 84 | extras: {} 85 | }, 86 | 87 | /* 88 | |-------------------------------------------------------------------------- 89 | | Ethereal 90 | |-------------------------------------------------------------------------- 91 | | 92 | | Ethereal driver to quickly test emails in your browser. A disposable 93 | | account is created automatically for you. 94 | | 95 | | https://ethereal.email 96 | | 97 | */ 98 | ethereal: { 99 | driver: 'ethereal' 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Adonis SaaS Project Manager - Backend 2 | 3 | ## What is this project? 4 | 5 | This is a personal project made with AdonisJs with the only purpose of learning the Adonis framework and ACL's in a Multi-tenant system. This is only the API, the frontend can be found [here](https://github.com/igorsouza-dev/adonis-saas-frontend). 6 | This software is a project management tool that allows users to manage multiple teams, projects and its members. 7 | 8 | ## Features 9 | 10 | - ACL (adonis-acl) 11 | - Token Authentication 12 | - Queue for mail sending (adonis-kue and Redis) 13 | - Mysql database 14 | 15 | ## Setup 16 | 17 | 1. Run `yarn` to install the dependencies 18 | 2. Run `cp .env.example .env` 19 | 3. At the `.env` file, setup your SMTP configuration and your database config. Don't forget to create your database through your DBMS. 20 | 4. The variable `FRONT_URL` is used in the mail template, you should inform your front end base url here 21 | 5. Run `adonis migration:run` to create the database tables. Your tables should look like this: 22 | ``` 23 | +-----------------------+ 24 | | invites | 25 | | permission_role | 26 | | permission_user_team | 27 | | permissions | 28 | | projects | 29 | | role_user_team | 30 | | roles | 31 | | teams | 32 | | tokens | 33 | | user_teams | 34 | | users | 35 | +-----------------------+ 36 | ``` 37 | 6. Run `adonis seed` to create the admin user and the default Team. The following row will be added to your database. You can log in with user `admin@gmail.com` and password `123456`. 38 | 7. Install Redis and inform your access data in the `.env` file. If you use docker you can run: 39 | ``` 40 | docker run --name redis-name -p 6379:6379 -d -t redis:alpine 41 | ``` 42 | 43 | ## Starging server 44 | 45 | - Run `adonis serve` to start the api and `adonis kue:listen` to start the Redis Queue 46 | 47 | ## Routes 48 | 49 | - You can import the `Insomnia.json` into [Insomnia](https://insomnia.rest/download/) to test the API without a frontend 50 | 51 | | Route | Verb(s) | Handler | Middleware | 52 | |---------------|-----------|---------------------------|------------| 53 | | /sessions | POST | SessionController.store | av:Session | 54 | | /users | POST | UserController.store | av:User | 55 | | /register | HEAD,GET | UserController.create | | 56 | | /teams | HEAD,GET | TeamController.index | auth | 57 | | /teams | POST | TeamController.store | auth,av:Team| 58 | | /teams/:id | HEAD,GET | TeamController.show | auth | 59 | | /teams/:id | PUT,PATCH | TeamController.update | auth,av:Team| 60 | | /teams/:id | DELETE | TeamController.destroy | auth | 61 | | /roles | HEAD,GET | RoleController.index | auth | 62 | | /invites | POST | InviteController.store | auth,team,av:Invite,can:invites_create | 63 | | /projects | HEAD,GET | ProjectController.index | auth,team | 64 | | /projects | POST | ProjectController.store | auth,team,av:Project,can:projects_create | 65 | | /projects/:id | HEAD,GET | ProjectController.show | auth,team | 66 | | /projects/:id | PUT,PATCH | ProjectController.update | auth,team,av:Project,can:projects_create | 67 | | /projects/:id | DELETE | ProjectController.destroy | auth,team | 68 | | /members | HEAD,GET | MemberController.index | auth,team | 69 | | /members/:id | PUT | MemberController.update | auth,team,is:administrator| 70 | | /permissions | HEAD,GET | PermissionController.show | auth,team | 71 | 72 | ## Middlewares 73 | 74 | - Auth = Demands token in header. 75 | - Team = Demands a team slug informed in a TEAM header. This middleware injects a Team object in the request and in the Auth object as the current selected team. 76 | - Can = Middleware for permission checking. Example: `can:projects_create` 77 | - Is = Middleware for role checking. Example: `is:administrator` 78 | 79 | ## Business Model 80 | 81 | - A user can be in multiple teams and a team can have multiple members 82 | - Users can create teams 83 | - Users can create projects inside teams 84 | - Users can only sign up if they have an invitation to be a member of a team 85 | -------------------------------------------------------------------------------- /config/bodyParser.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | /* 5 | |-------------------------------------------------------------------------- 6 | | JSON Parser 7 | |-------------------------------------------------------------------------- 8 | | 9 | | Below settings are applied when the request body contains a JSON payload. 10 | | If you want body parser to ignore JSON payloads, then simply set `types` 11 | | to an empty array. 12 | */ 13 | json: { 14 | /* 15 | |-------------------------------------------------------------------------- 16 | | limit 17 | |-------------------------------------------------------------------------- 18 | | 19 | | Defines the limit of JSON that can be sent by the client. If payload 20 | | is over 1mb it will not be processed. 21 | | 22 | */ 23 | limit: '1mb', 24 | 25 | /* 26 | |-------------------------------------------------------------------------- 27 | | strict 28 | |-------------------------------------------------------------------------- 29 | | 30 | | When `strict` is set to true, body parser will only parse Arrays and 31 | | Object. Otherwise everything parseable by `JSON.parse` is parsed. 32 | | 33 | */ 34 | strict: true, 35 | 36 | /* 37 | |-------------------------------------------------------------------------- 38 | | types 39 | |-------------------------------------------------------------------------- 40 | | 41 | | Which content types are processed as JSON payloads. You are free to 42 | | add your own types here, but the request body should be parseable 43 | | by `JSON.parse` method. 44 | | 45 | */ 46 | types: [ 47 | 'application/json', 48 | 'application/json-patch+json', 49 | 'application/vnd.api+json', 50 | 'application/csp-report' 51 | ] 52 | }, 53 | 54 | /* 55 | |-------------------------------------------------------------------------- 56 | | Raw Parser 57 | |-------------------------------------------------------------------------- 58 | | 59 | | 60 | | 61 | */ 62 | raw: { 63 | types: [ 64 | 'text/*' 65 | ] 66 | }, 67 | 68 | /* 69 | |-------------------------------------------------------------------------- 70 | | Form Parser 71 | |-------------------------------------------------------------------------- 72 | | 73 | | 74 | | 75 | */ 76 | form: { 77 | types: [ 78 | 'application/x-www-form-urlencoded' 79 | ] 80 | }, 81 | 82 | /* 83 | |-------------------------------------------------------------------------- 84 | | Files Parser 85 | |-------------------------------------------------------------------------- 86 | | 87 | | 88 | | 89 | */ 90 | files: { 91 | types: [ 92 | 'multipart/form-data' 93 | ], 94 | 95 | /* 96 | |-------------------------------------------------------------------------- 97 | | Max Size 98 | |-------------------------------------------------------------------------- 99 | | 100 | | Below value is the max size of all the files uploaded to the server. It 101 | | is validated even before files have been processed and hard exception 102 | | is thrown. 103 | | 104 | | Consider setting a reasonable value here, otherwise people may upload GB's 105 | | of files which will keep your server busy. 106 | | 107 | | Also this value is considered when `autoProcess` is set to true. 108 | | 109 | */ 110 | maxSize: '20mb', 111 | 112 | /* 113 | |-------------------------------------------------------------------------- 114 | | Auto Process 115 | |-------------------------------------------------------------------------- 116 | | 117 | | Whether or not to auto-process files. Since HTTP servers handle files via 118 | | couple of specific endpoints. It is better to set this value off and 119 | | manually process the files when required. 120 | | 121 | | This value can contain a boolean or an array of route patterns 122 | | to be autoprocessed. 123 | */ 124 | autoProcess: true, 125 | 126 | /* 127 | |-------------------------------------------------------------------------- 128 | | Process Manually 129 | |-------------------------------------------------------------------------- 130 | | 131 | | The list of routes that should not process files and instead rely on 132 | | manual process. This list should only contain routes when autoProcess 133 | | is to true. Otherwise everything is processed manually. 134 | | 135 | */ 136 | processManually: [] 137 | 138 | /* 139 | |-------------------------------------------------------------------------- 140 | | Temporary file name 141 | |-------------------------------------------------------------------------- 142 | | 143 | | Define a function, which should return a string to be used as the 144 | | tmp file name. 145 | | 146 | | If not defined, Bodyparser will use `uuid` as the tmp file name. 147 | | 148 | | To be defined as. If you are defining the function, then do make sure 149 | | to return a value from it. 150 | | 151 | | tmpFileName () { 152 | | return 'some-unique-value' 153 | | } 154 | | 155 | */ 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /config/app.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** @type {import('@adonisjs/framework/src/Env')} */ 4 | const Env = use('Env') 5 | 6 | module.exports = { 7 | 8 | /* 9 | |-------------------------------------------------------------------------- 10 | | Application Name 11 | |-------------------------------------------------------------------------- 12 | | 13 | | This value is the name of your application and can used when you 14 | | need to place the application's name in a email, view or 15 | | other location. 16 | | 17 | */ 18 | 19 | name: Env.get('APP_NAME', 'AdonisJs'), 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | App Key 24 | |-------------------------------------------------------------------------- 25 | | 26 | | App key is a randomly generated 16 or 32 characters long string required 27 | | to encrypt cookies, sessions and other sensitive data. 28 | | 29 | */ 30 | appKey: Env.getOrFail('APP_KEY'), 31 | 32 | http: { 33 | /* 34 | |-------------------------------------------------------------------------- 35 | | Allow Method Spoofing 36 | |-------------------------------------------------------------------------- 37 | | 38 | | Method spoofing allows to make requests by spoofing the http verb. 39 | | Which means you can make a GET request but instruct the server to 40 | | treat as a POST or PUT request. If you want this feature, set the 41 | | below value to true. 42 | | 43 | */ 44 | allowMethodSpoofing: true, 45 | 46 | /* 47 | |-------------------------------------------------------------------------- 48 | | Trust Proxy 49 | |-------------------------------------------------------------------------- 50 | | 51 | | Trust proxy defines whether X-Forwarded-* headers should be trusted or not. 52 | | When your application is behind a proxy server like nginx, these values 53 | | are set automatically and should be trusted. Apart from setting it 54 | | to true or false Adonis supports handful or ways to allow proxy 55 | | values. Read documentation for that. 56 | | 57 | */ 58 | trustProxy: false, 59 | 60 | /* 61 | |-------------------------------------------------------------------------- 62 | | Subdomains 63 | |-------------------------------------------------------------------------- 64 | | 65 | | Offset to be used for returning subdomains for a given request.For 66 | | majority of applications it will be 2, until you have nested 67 | | sudomains. 68 | | cheatsheet.adonisjs.com - offset - 2 69 | | virk.cheatsheet.adonisjs.com - offset - 3 70 | | 71 | */ 72 | subdomainOffset: 2, 73 | 74 | /* 75 | |-------------------------------------------------------------------------- 76 | | JSONP Callback 77 | |-------------------------------------------------------------------------- 78 | | 79 | | Default jsonp callback to be used when callback query string is missing 80 | | in request url. 81 | | 82 | */ 83 | jsonpCallback: 'callback', 84 | 85 | /* 86 | |-------------------------------------------------------------------------- 87 | | Etag 88 | |-------------------------------------------------------------------------- 89 | | 90 | | Set etag on all HTTP response. In order to disable for selected routes, 91 | | you can call the `response.send` with an options object as follows. 92 | | 93 | | response.send('Hello', { ignoreEtag: true }) 94 | | 95 | */ 96 | etag: false 97 | }, 98 | 99 | views: { 100 | /* 101 | |-------------------------------------------------------------------------- 102 | | Cache Views 103 | |-------------------------------------------------------------------------- 104 | | 105 | | Define whether or not to cache the compiled view. Set it to true in 106 | | production to optimize view loading time. 107 | | 108 | */ 109 | cache: Env.get('CACHE_VIEWS', true) 110 | }, 111 | 112 | static: { 113 | /* 114 | |-------------------------------------------------------------------------- 115 | | Dot Files 116 | |-------------------------------------------------------------------------- 117 | | 118 | | Define how to treat dot files when trying to server static resources. 119 | | By default it is set to ignore, which will pretend that dotfiles 120 | | does not exists. 121 | | 122 | | Can be one of the following 123 | | ignore, deny, allow 124 | | 125 | */ 126 | dotfiles: 'ignore', 127 | 128 | /* 129 | |-------------------------------------------------------------------------- 130 | | ETag 131 | |-------------------------------------------------------------------------- 132 | | 133 | | Enable or disable etag generation 134 | | 135 | */ 136 | etag: true, 137 | 138 | /* 139 | |-------------------------------------------------------------------------- 140 | | Extensions 141 | |-------------------------------------------------------------------------- 142 | | 143 | | Set file extension fallbacks. When set, if a file is not found, the given 144 | | extensions will be added to the file name and search for. The first 145 | | that exists will be served. Example: ['html', 'htm']. 146 | | 147 | */ 148 | extensions: false 149 | }, 150 | 151 | locales: { 152 | /* 153 | |-------------------------------------------------------------------------- 154 | | Loader 155 | |-------------------------------------------------------------------------- 156 | | 157 | | The loader to be used for fetching and updating locales. Below is the 158 | | list of available options. 159 | | 160 | | file, database 161 | | 162 | */ 163 | loader: 'file', 164 | 165 | /* 166 | |-------------------------------------------------------------------------- 167 | | Default Locale 168 | |-------------------------------------------------------------------------- 169 | | 170 | | Default locale to be used by Antl provider. You can always switch drivers 171 | | in runtime or use the official Antl middleware to detect the driver 172 | | based on HTTP headers/query string. 173 | | 174 | */ 175 | locale: 'en' 176 | }, 177 | 178 | logger: { 179 | /* 180 | |-------------------------------------------------------------------------- 181 | | Transport 182 | |-------------------------------------------------------------------------- 183 | | 184 | | Transport to be used for logging messages. You can have multiple 185 | | transports using same driver. 186 | | 187 | | Available drivers are: `file` and `console`. 188 | | 189 | */ 190 | transport: 'console', 191 | 192 | /* 193 | |-------------------------------------------------------------------------- 194 | | Console Transport 195 | |-------------------------------------------------------------------------- 196 | | 197 | | Using `console` driver for logging. This driver writes to `stdout` 198 | | and `stderr` 199 | | 200 | */ 201 | console: { 202 | driver: 'console', 203 | name: 'adonis-app', 204 | level: 'info' 205 | }, 206 | 207 | /* 208 | |-------------------------------------------------------------------------- 209 | | File Transport 210 | |-------------------------------------------------------------------------- 211 | | 212 | | File transport uses file driver and writes log messages for a given 213 | | file inside `tmp` directory for your app. 214 | | 215 | | For a different directory, set an absolute path for the filename. 216 | | 217 | */ 218 | file: { 219 | driver: 'file', 220 | name: 'adonis-app', 221 | filename: 'adonis.log', 222 | level: 'info' 223 | } 224 | }, 225 | 226 | /* 227 | |-------------------------------------------------------------------------- 228 | | Generic Cookie Options 229 | |-------------------------------------------------------------------------- 230 | | 231 | | The following cookie options are generic settings used by AdonisJs to create 232 | | cookies. However, some parts of the application like `sessions` can have 233 | | separate settings for cookies inside `config/session.js`. 234 | | 235 | */ 236 | cookie: { 237 | httpOnly: true, 238 | sameSite: false, 239 | path: '/', 240 | maxAge: 7200 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /Insomnia.json: -------------------------------------------------------------------------------- 1 | {"_type":"export","__export_format":4,"__export_date":"2020-01-02T17:39:03.805Z","__export_source":"insomnia.desktop.app:v7.0.3","resources":[{"_id":"req_faa68f7d61c9438c951074db36807856","authentication":{"token":"{{ token }}","type":"bearer"},"body":{},"created":1577986552761,"description":"","headers":[{"id":"pair_a7ee775b439f44b2bb29c2baea55da29","name":"TEAM","value":"havok"}],"isPrivate":false,"metaSortKey":-1577986552761,"method":"GET","modified":1577986578906,"name":"Index","parameters":[],"parentId":"fld_29a3c60eef3f4421a8e153867ad16888","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/permissions","_type":"request"},{"_id":"fld_29a3c60eef3f4421a8e153867ad16888","created":1577986548588,"description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1577986548588,"modified":1577986548588,"name":"Permissions","parentId":"wrk_a7f6877053114bf7a3fc17ceca95405a","_type":"request_group"},{"_id":"wrk_a7f6877053114bf7a3fc17ceca95405a","created":1577821174044,"description":"","modified":1577821174044,"name":"Adonis SaaS","parentId":null,"_type":"workspace"},{"_id":"req_33d82a2cb8f444bcb23410c5af888f3e","authentication":{"token":"{{ token }}","type":"bearer"},"body":{},"created":1577985501712,"description":"","headers":[{"id":"pair_a20cfbb649b14f6d889683713c41a0dd","name":"TEAM","value":"havok"}],"isPrivate":false,"metaSortKey":-1577985501712,"method":"GET","modified":1577985804061,"name":"Index","parameters":[],"parentId":"fld_829df199920c4b8daa3a6d5d642c2d3f","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/members","_type":"request"},{"_id":"fld_829df199920c4b8daa3a6d5d642c2d3f","created":1577985486740,"description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1577985486740,"modified":1577985486740,"name":"Members","parentId":"wrk_a7f6877053114bf7a3fc17ceca95405a","_type":"request_group"},{"_id":"req_3cbdd6c1eb574bfdae61d28a650588b7","authentication":{"token":"{{ token }}","type":"bearer"},"body":{"mimeType":"application/json","text":"{\n\t\"roles\": [2]\n}"},"created":1577985535948,"description":"","headers":[{"id":"pair_a20cfbb649b14f6d889683713c41a0dd","name":"TEAM","value":"havok"},{"id":"pair_da08a70dee0b441b9d17fb20ba89987d","name":"Content-Type","value":"application/json"}],"isPrivate":false,"metaSortKey":-1577985096889.5,"method":"PUT","modified":1577985823208,"name":"Update","parameters":[],"parentId":"fld_829df199920c4b8daa3a6d5d642c2d3f","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/members/2","_type":"request"},{"_id":"req_e5820b64de3442c09a715692c0731bf2","authentication":{"token":"{{ token }}","type":"bearer"},"body":{},"created":1577984692067,"description":"","headers":[],"isPrivate":false,"metaSortKey":-1577984692067,"method":"GET","modified":1577984708514,"name":"Index","parameters":[],"parentId":"fld_6e2f79ffbcbd44a9b0e3c90488bf8821","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/roles","_type":"request"},{"_id":"fld_6e2f79ffbcbd44a9b0e3c90488bf8821","created":1577984686147,"description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1577984686147,"modified":1577984686147,"name":"Roles","parentId":"wrk_a7f6877053114bf7a3fc17ceca95405a","_type":"request_group"},{"_id":"req_cbb4c6f0947b4451afbfff6a523ea738","authentication":{"token":"{{ token }}","type":"bearer"},"body":{},"created":1577971399911,"description":"","headers":[{"id":"pair_368441fe897c4caa8363cc65ec73e302","name":"TEAM","value":"havok"},{"id":"pair_688101470ad9459a957ea9b8ba5288bb","name":"","value":""}],"isPrivate":false,"metaSortKey":-1577971399911,"method":"GET","modified":1577984895231,"name":"Index","parameters":[],"parentId":"fld_1ff3731011f644a9ab75474bc4df4140","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/projects","_type":"request"},{"_id":"fld_1ff3731011f644a9ab75474bc4df4140","created":1577971394667,"description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1577971394667,"modified":1577971394667,"name":"Projects","parentId":"wrk_a7f6877053114bf7a3fc17ceca95405a","_type":"request_group"},{"_id":"req_d4a9a12e99d641019034bce49215fe43","authentication":{"token":"{{ token }}","type":"bearer"},"body":{},"created":1577971468387,"description":"","headers":[{"id":"pair_368441fe897c4caa8363cc65ec73e302","name":"TEAM","value":"havok"}],"isPrivate":false,"metaSortKey":-1577971388134.75,"method":"GET","modified":1577984584511,"name":"Show","parameters":[],"parentId":"fld_1ff3731011f644a9ab75474bc4df4140","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/projects/1","_type":"request"},{"_id":"req_14e1bd57d9f6445ab024e99ed59a24ea","authentication":{"token":"{{ token }}","type":"bearer"},"body":{},"created":1577971480178,"description":"","headers":[{"id":"pair_368441fe897c4caa8363cc65ec73e302","name":"TEAM","value":"huga-buga"}],"isPrivate":false,"metaSortKey":-1577971382246.625,"method":"DELETE","modified":1577971485094,"name":"Delete","parameters":[],"parentId":"fld_1ff3731011f644a9ab75474bc4df4140","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/projects/1","_type":"request"},{"_id":"req_6099c2586bea41beaa05ce1534897042","authentication":{"token":"{{ token }}","type":"bearer"},"body":{"mimeType":"application/json","text":"{\n\t\"title\": \"oiiee\"\n}"},"created":1577971430907,"description":"","headers":[{"id":"pair_368441fe897c4caa8363cc65ec73e302","name":"TEAM","value":"havok"},{"id":"pair_c616013fc1e24fc19ed402c1a2f94b49","name":"Content-Type","value":"application/json"}],"isPrivate":false,"metaSortKey":-1577971376358.5,"method":"POST","modified":1577984512306,"name":"Create","parameters":[],"parentId":"fld_1ff3731011f644a9ab75474bc4df4140","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/projects","_type":"request"},{"_id":"req_e7053c8fe3b94819b6f914156319a510","authentication":{"token":"{{ token }}","type":"bearer"},"body":{"mimeType":"application/json","text":"{\n\t\"title\": \"EHEHEHE\"\n}"},"created":1577971492456,"description":"","headers":[{"id":"pair_368441fe897c4caa8363cc65ec73e302","name":"TEAM","value":"huga-buga"},{"id":"pair_c616013fc1e24fc19ed402c1a2f94b49","name":"Content-Type","value":"application/json"}],"isPrivate":false,"metaSortKey":-1577971364582.25,"method":"PUT","modified":1577971504642,"name":"Update","parameters":[],"parentId":"fld_1ff3731011f644a9ab75474bc4df4140","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/projects/1","_type":"request"},{"_id":"req_a19f1e4d0d0d40ad9bb3506da0653945","authentication":{},"body":{"mimeType":"application/json","text":"{\n\t\"name\": \"Igor\",\n\t\"email\": \"igor.souza@ifto.edu.br\",\n\t\"password\": \"123456\"\n}"},"created":1577971352806,"description":"","headers":[{"id":"pair_2b8621c5db1c40de9e5099b6be561a8d","name":"Content-Type","value":"application/json"}],"isPrivate":false,"metaSortKey":-1577971352806,"method":"POST","modified":1577971380628,"name":"Create","parameters":[],"parentId":"fld_386e55c8b82b481fbd5f1c3edee5ccf1","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/users","_type":"request"},{"_id":"fld_386e55c8b82b481fbd5f1c3edee5ccf1","created":1577971344550,"description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1577971344550,"modified":1577971344550,"name":"Users","parentId":"wrk_a7f6877053114bf7a3fc17ceca95405a","_type":"request_group"},{"_id":"req_07d8934fd894498a8304e36e141ad80f","authentication":{"token":"{{ token }}","type":"bearer"},"body":{"mimeType":"application/json","text":"{\n\t\"invites\": [\n\t\t\"igor.souza@ifto.edu.br\"\t\t\n\t]\n}"},"created":1577911932298,"description":"","headers":[{"id":"pair_4999661eece3418a9a2a002b9099ea77","name":"Content-Type","value":"application/json"},{"id":"pair_a1e00b8c747d41dfb4938d2707adde51","name":"TEAM","value":"havok"},{"id":"pair_59c02b1057594b348fd7ec4eb3d73dea","name":"Accept","value":"application/json"}],"isPrivate":false,"metaSortKey":-1577911932299,"method":"POST","modified":1577982992421,"name":"Create","parameters":[],"parentId":"fld_69a035b6aad740c3919433bc2fb012f3","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/invites","_type":"request"},{"_id":"fld_69a035b6aad740c3919433bc2fb012f3","created":1577911919529,"description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1577911919529,"modified":1577911919529,"name":"Invites","parentId":"wrk_a7f6877053114bf7a3fc17ceca95405a","_type":"request_group"},{"_id":"req_12651a407231475a86b6b3428574cc5b","authentication":{"token":"{{ token }}","type":"bearer"},"body":{},"created":1577822071406,"description":"","headers":[],"isPrivate":false,"metaSortKey":-1577822071406,"method":"GET","modified":1577822084477,"name":"Index","parameters":[],"parentId":"fld_5c7f81ed78f64124b0b635d16a05cf79","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/teams","_type":"request"},{"_id":"fld_5c7f81ed78f64124b0b635d16a05cf79","created":1577822066885,"description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1577822066885,"modified":1577822066885,"name":"Teams","parentId":"wrk_a7f6877053114bf7a3fc17ceca95405a","_type":"request_group"},{"_id":"req_d0dde72e0c1942c986a1f25193f5c6f3","authentication":{"token":"{{ token }}","type":"bearer"},"body":{},"created":1577822411512,"description":"","headers":[],"isPrivate":false,"metaSortKey":-1577821854245.75,"method":"GET","modified":1577822418984,"name":"Show","parameters":[],"parentId":"fld_5c7f81ed78f64124b0b635d16a05cf79","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/teams/1","_type":"request"},{"_id":"req_51fd75a7027b4cf08dd159baba3999bb","authentication":{"token":"{{ token }}","type":"bearer"},"body":{"mimeType":"application/json","text":"{\n\t\"name\": \"Huga buga\"\n}"},"created":1577822287158,"description":"","headers":[{"id":"pair_84a9e0e283fc4b27a1e3a18b8a34f662","name":"Content-Type","value":"application/json"}],"isPrivate":false,"metaSortKey":-1577821637085.5,"method":"POST","modified":1577822307068,"name":"Create","parameters":[],"parentId":"fld_5c7f81ed78f64124b0b635d16a05cf79","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/teams","_type":"request"},{"_id":"req_7bfc331df7fe45b0add7a976dade9382","authentication":{"token":"{{ token }}","type":"bearer"},"body":{"mimeType":"application/json","text":"{\n\t\"name\": \"Havoc\"\n}"},"created":1577822520541,"description":"","headers":[{"id":"pair_84a9e0e283fc4b27a1e3a18b8a34f662","name":"Content-Type","value":"application/json"}],"isPrivate":false,"metaSortKey":-1577821419925.25,"method":"PUT","modified":1577822541167,"name":"Update","parameters":[],"parentId":"fld_5c7f81ed78f64124b0b635d16a05cf79","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/teams/1","_type":"request"},{"_id":"req_c86fa8827772425083783781787dd717","authentication":{"token":"{{ token }}","type":"bearer"},"body":{},"created":1577822594084,"description":"","headers":[],"isPrivate":false,"metaSortKey":-1577821311345.125,"method":"DELETE","modified":1577822602180,"name":"Delete","parameters":[],"parentId":"fld_5c7f81ed78f64124b0b635d16a05cf79","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/teams/1","_type":"request"},{"_id":"req_a1e783620286410b81b5725847f2802f","authentication":{},"body":{"mimeType":"application/json","text":"{\n\t\"email\": \"igor.souza@ifto.edu.br\",\n\t\"password\": \"123456\"\n}"},"created":1577821202765,"description":"","headers":[{"id":"pair_913dfcc71dbf49fcb176d0295fd7bcf9","name":"Content-Type","value":"application/json"}],"isPrivate":false,"metaSortKey":-1577821202765,"method":"POST","modified":1577984036735,"name":"Create","parameters":[],"parentId":"fld_16dc595095114155b719cd9b63f9d7c1","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/sessions","_type":"request"},{"_id":"fld_16dc595095114155b719cd9b63f9d7c1","created":1577821193705,"description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1577821193705,"modified":1577821193705,"name":"Sessions","parentId":"wrk_a7f6877053114bf7a3fc17ceca95405a","_type":"request_group"},{"_id":"env_7325fbc4ca91eaee3cc11a35e77c44c2e4dc46c4","color":null,"created":1577821174223,"data":{"base_url":"http://localhost:3333"},"dataPropertyOrder":{"&":["base_url"]},"isPrivate":false,"metaSortKey":1577821174223,"modified":1577821188276,"name":"Base Environment","parentId":"wrk_a7f6877053114bf7a3fc17ceca95405a","_type":"environment"},{"_id":"jar_7325fbc4ca91eaee3cc11a35e77c44c2e4dc46c4","cookies":[],"created":1577821174228,"modified":1577821174228,"name":"Default Jar","parentId":"wrk_a7f6877053114bf7a3fc17ceca95405a","_type":"cookie_jar"},{"_id":"env_bd5236f9008c4e3f81d085b9dc5fef8f","color":"#4027a0","created":1577821276890,"data":{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjEsImlhdCI6MTU3Nzk4Mjg4OX0.pA7quxBnbsw4MtHezSWpnqTGpnhmDft31FU-MPrzP-c"},"dataPropertyOrder":{"&":["token"]},"isPrivate":false,"metaSortKey":1577821276890,"modified":1577982904835,"name":"Admin","parentId":"env_7325fbc4ca91eaee3cc11a35e77c44c2e4dc46c4","_type":"environment"},{"_id":"env_60d7b8b6112d463d801b53158f55ac0d","color":"#20cad2","created":1577982907761,"data":{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjIsImlhdCI6MTU3Nzk4NDAzOH0.5tZlJ2-3OK9nTJDmIuosgCvnaBCHhbgnKNemqP26nLI"},"dataPropertyOrder":{"&":["token"]},"isPrivate":false,"metaSortKey":1577982907761,"modified":1577984046748,"name":"Moderator","parentId":"env_7325fbc4ca91eaee3cc11a35e77c44c2e4dc46c4","_type":"environment"}]} --------------------------------------------------------------------------------