├── 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 |