├── .github └── assets │ └── logo.png ├── app ├── Models │ ├── Token.js │ ├── Project.js │ ├── Traits │ │ └── NoTimestamp.js │ ├── Invite.js │ ├── UserTeam.js │ ├── Hooks │ │ └── InviteHook.js │ ├── Team.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 │ │ ├── TeamController.js │ │ └── ProjectController.js ├── Middleware │ ├── ConvertEmptyStringsToNull.js │ └── Team.js └── Jobs │ └── InvitationEmail.js ├── resources └── views │ └── emails │ └── invitation.edge ├── .gitignore ├── .editorconfig ├── .env.example ├── .eslintrc.json ├── database ├── migrations │ ├── 1577019205377_create_roles_table.js │ ├── 1503250034279_user.js │ ├── 1577019205341_create_permissions_table.js │ ├── 1503250034280_token.js │ ├── 1576678403285_project_schema.js │ ├── 1576678356361_team_schema.js │ ├── 1577019205503_create_role_user_table.js │ ├── 1576678877737_user_team_schema.js │ ├── 1577019205409_create_permission_role_table.js │ ├── 1576678929068_invite_schema.js │ └── 1577019205456_create_permission_user_table.js ├── factory.js └── seeds │ └── DatabaseSeeder.js ├── start ├── redis.js ├── routes.js ├── kernel.js └── app.js ├── ace ├── server.js ├── LICENSE ├── package.json ├── config ├── hash.js ├── redis.js ├── database.js ├── auth.js ├── cors.js ├── mail.js ├── bodyParser.js └── app.js ├── README.md └── Insomnia.json /.github/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osvaldokalvaitir/nodejs-saas/HEAD/.github/assets/logo.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /resources/views/emails/invitation.edge: -------------------------------------------------------------------------------- 1 |

Você foi convidado para participar do time {{ team }} por {{ user }}.

2 | 3 |

Para criar sua conta e participar, basta clicar aqui 4 | -------------------------------------------------------------------------------- /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 | 3 | const Role = use('Adonis/Acl/Role') 4 | 5 | class RoleController { 6 | async index () { 7 | const roles = await Role.all() 8 | 9 | return roles 10 | } 11 | } 12 | 13 | module.exports = RoleController 14 | -------------------------------------------------------------------------------- /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/Controllers/Http/SessionController.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class SessionController { 4 | async store ({ request, auth }) { 5 | const { email, password } = request.all() 6 | 7 | const token = await auth.attempt(email, password) 8 | 9 | return token 10 | } 11 | } 12 | 13 | module.exports = SessionController 14 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | 8 | CACHE_VIEWS=false 9 | 10 | APP_KEY= 11 | 12 | DB_CONNECTION=sqlite 13 | DB_HOST=127.0.0.1 14 | DB_PORT=3306 15 | DB_USER=root 16 | DB_PASSWORD= 17 | DB_DATABASE=adonis 18 | 19 | SMTP_HOST= 20 | MAIL_USERNAME= 21 | MAIL_PASSWORD= 22 | 23 | HASH_DRIVER=bcrypt 24 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "standard" 9 | ], 10 | "globals": { 11 | "Atomics": "readonly", 12 | "SharedArrayBuffer": "readonly", 13 | "use": true, 14 | }, 15 | "parserOptions": { 16 | "ecmaVersion": 2018 17 | }, 18 | "rules": { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /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 | 10 | this.addHook('afterCreate', 'InviteHook.sendInvitationEmail') 11 | } 12 | 13 | user () { 14 | return this.belongsTo('App/Models/User') 15 | } 16 | 17 | team () { 18 | return this.belongsTo('App/Models/Team') 19 | } 20 | } 21 | 22 | module.exports = Invite 23 | -------------------------------------------------------------------------------- /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 | 12 | return { 13 | roles: await teamJoin.getRoles(), 14 | permissions: await teamJoin.getPermissions() 15 | } 16 | } 17 | } 18 | 19 | module.exports = PermissionController 20 | -------------------------------------------------------------------------------- /database/migrations/1577019205377_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/1577019205341_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/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 | -------------------------------------------------------------------------------- /app/Controllers/Http/MemberController.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const UserTeam = use('App/Models/UserTeam') 4 | 5 | class MemberController { 6 | async index ({ request }) { 7 | const members = await UserTeam.query() 8 | .where('team_id', request.team.id) 9 | .with('user') 10 | .with('roles') 11 | .fetch() 12 | 13 | return members 14 | } 15 | 16 | async update ({ request, params }) { 17 | const roles = request.input('roles') 18 | 19 | const teamJoin = await UserTeam.find(params.id) 20 | 21 | await teamJoin.roles().sync(roles) 22 | } 23 | 24 | } 25 | 26 | module.exports = MemberController 27 | -------------------------------------------------------------------------------- /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/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 | 7 | const InviteHook = exports = module.exports = {} 8 | 9 | InviteHook.sendInvitationEmail = async invite => { 10 | const { email } = invite 11 | const invited = await User.findBy('email', email) 12 | 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 | 19 | Kue.dispatch(Job.key, { user, team, email }, { attempts: 3 }) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 10 | this.addTrait('@provider:Lucid/Slugify', { 11 | fields: { 12 | slug: 'name' 13 | }, 14 | strategy: 'dbIncrement', 15 | disableUpdates: false 16 | }) 17 | } 18 | 19 | users () { 20 | return this.belongsToMany('App/Models/User').pivotModel( 21 | 'App/Models/UserTeam' 22 | ) 23 | } 24 | 25 | projects () { 26 | return this.hasMany('App/Models/Project') 27 | } 28 | } 29 | 30 | module.exports = Team 31 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/Jobs/InvitationEmail.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Mail = use('Mail') 4 | 5 | class InvitationEmail { 6 | static get concurrency () { 7 | return 1 8 | } 9 | 10 | static get key () { 11 | return 'InvitationEmail-job' 12 | } 13 | 14 | // This is where the work is done. 15 | async handle ({ user, team, email }) { 16 | await Mail.send( 17 | ['emails.invitation'], 18 | { team: team.name, user: user.name }, 19 | message => { 20 | message 21 | .to(email) 22 | .from('osvaldokalvaitir@outlook.com', 'Osvaldo | Omnistack') 23 | .subject(`Convite para o time ${team.name}`) 24 | } 25 | ) 26 | } 27 | } 28 | 29 | module.exports = InvitationEmail 30 | -------------------------------------------------------------------------------- /database/migrations/1576678403285_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 12 | .integer('team_id') 13 | .unsigned() 14 | .notNullable() 15 | .references('id') 16 | .inTable('teams') 17 | .onUpdate('CASCADE') 18 | .onDelete('CASCADE') 19 | table.timestamps() 20 | }) 21 | } 22 | 23 | down () { 24 | this.drop('projects') 25 | } 26 | } 27 | 28 | module.exports = ProjectSchema 29 | -------------------------------------------------------------------------------- /database/migrations/1576678356361_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 12 | .integer('user_id') 13 | .unsigned() 14 | .notNullable() 15 | .references('id') 16 | .inTable('users') 17 | .onUpdate('CASCADE') 18 | .onDelete('CASCADE') 19 | table 20 | .string('slug') 21 | .notNullable() 22 | .unique() 23 | table.timestamps() 24 | }) 25 | } 26 | 27 | down () { 28 | this.drop('teams') 29 | } 30 | } 31 | 32 | module.exports = TeamSchema 33 | -------------------------------------------------------------------------------- /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 | 17 | if (slug) { 18 | team = await auth.user.teams().where('slug', slug).first() 19 | } 20 | 21 | if (!team) { 22 | return response.status(401).send() 23 | } 24 | 25 | auth.user.currentTeam = team.id 26 | request.team = team 27 | 28 | await next() 29 | } 30 | } 31 | 32 | module.exports = Team 33 | -------------------------------------------------------------------------------- /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(['name', 'email', 'password']) 9 | 10 | const teamsQuery = Invite.query().where('email', data.email) 11 | const teams = await teamsQuery.pluck('team_id') 12 | 13 | if (teams.length === 0) { 14 | return response 15 | .status(401) 16 | .send({ message: "You're not invited to any team." }) 17 | } 18 | 19 | const user = await User.create(data) 20 | 21 | await user.teams().attach(teams) 22 | 23 | await teamsQuery.delete() 24 | 25 | const token = await auth.attempt(data.email, data.password) 26 | 27 | return token 28 | } 29 | } 30 | 31 | module.exports = UserController 32 | -------------------------------------------------------------------------------- /database/migrations/1577019205503_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 10 | .integer('role_id') 11 | .unsigned() 12 | .index() 13 | table 14 | .foreign('role_id') 15 | .references('id') 16 | .on('roles') 17 | .onDelete('cascade') 18 | table 19 | .integer('user_team_id') 20 | .unsigned() 21 | .index() 22 | table 23 | .foreign('user_team_id') 24 | .references('id') 25 | .on('user_teams') 26 | .onDelete('cascade') 27 | table.timestamps() 28 | }) 29 | } 30 | 31 | down () { 32 | this.drop('role_user_team') 33 | } 34 | } 35 | 36 | module.exports = RoleUserTableSchema 37 | -------------------------------------------------------------------------------- /database/migrations/1576678877737_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 11 | .integer('user_id') 12 | .unsigned() 13 | .notNullable() 14 | .references('id') 15 | .inTable('users') 16 | .onUpdate('CASCADE') 17 | .onDelete('CASCADE') 18 | table 19 | .integer('team_id') 20 | .unsigned() 21 | .notNullable() 22 | .references('id') 23 | .inTable('teams') 24 | .onUpdate('CASCADE') 25 | .onDelete('CASCADE') 26 | table.timestamps() 27 | }) 28 | } 29 | 30 | down () { 31 | this.drop('user_teams') 32 | } 33 | } 34 | 35 | module.exports = UserTeamSchema 36 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /database/migrations/1577019205409_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 10 | .integer('permission_id') 11 | .unsigned() 12 | .index() 13 | table 14 | .foreign('permission_id') 15 | .references('id') 16 | .on('permissions') 17 | .onDelete('cascade') 18 | table 19 | .integer('role_id') 20 | .unsigned() 21 | .index() 22 | table 23 | .foreign('role_id') 24 | .references('id') 25 | .on('roles') 26 | .onDelete('cascade') 27 | table.timestamps() 28 | }) 29 | } 30 | 31 | down () { 32 | this.drop('permission_role') 33 | } 34 | } 35 | 36 | module.exports = PermissionRoleTableSchema 37 | -------------------------------------------------------------------------------- /database/migrations/1576678929068_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 11 | .integer('user_id') 12 | .unsigned() 13 | .notNullable() 14 | .references('id') 15 | .inTable('users') 16 | .onUpdate('CASCADE') 17 | .onDelete('CASCADE') 18 | table 19 | .integer('team_id') 20 | .unsigned() 21 | .notNullable() 22 | .references('id') 23 | .inTable('teams') 24 | .onUpdate('CASCADE') 25 | .onDelete('CASCADE') 26 | table.string('email').notNullable() 27 | table.timestamps() 28 | }) 29 | } 30 | 31 | down () { 32 | this.drop('invites') 33 | } 34 | } 35 | 36 | module.exports = InviteSchema 37 | -------------------------------------------------------------------------------- /database/migrations/1577019205456_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 10 | .integer('permission_id') 11 | .unsigned() 12 | .index() 13 | table 14 | .foreign('permission_id') 15 | .references('id') 16 | .on('permissions') 17 | .onDelete('cascade') 18 | table 19 | .integer('user_team_id') 20 | .unsigned() 21 | .index() 22 | table 23 | .foreign('user_team_id') 24 | .references('id') 25 | .on('user_teams') 26 | .onDelete('cascade') 27 | table.timestamps() 28 | }) 29 | } 30 | 31 | down () { 32 | this.drop('permission_user_team') 33 | } 34 | } 35 | 36 | module.exports = PermissionUserTableSchema 37 | -------------------------------------------------------------------------------- /app/Controllers/Http/InviteController.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Invite = use('App/Models/Invite') 4 | 5 | /** @typedef {import('@adonisjs/framework/src/Request')} Request */ 6 | /** @typedef {import('@adonisjs/framework/src/Response')} Response */ 7 | /** @typedef {import('@adonisjs/framework/src/View')} View */ 8 | 9 | /** 10 | * Resourceful controller for interacting with invites 11 | */ 12 | class InviteController { 13 | /** 14 | * Create/save a new invite. 15 | * POST invites 16 | * 17 | * @param {object} ctx 18 | * @param {Request} ctx.request 19 | * @param {Response} ctx.response 20 | */ 21 | async store ({ request, auth }) { 22 | const invites = request.input('invites') 23 | 24 | const data = invites.map(email => ({ 25 | email, 26 | user_id: auth.user.id, 27 | team_id: request.team.id 28 | })) 29 | 30 | await Invite.createMany(data) 31 | } 32 | } 33 | 34 | module.exports = InviteController 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Osvaldo Kalvaitir Filho 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /start/routes.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** @type {typeof import('@adonisjs/framework/src/Route/Manager')} */ 4 | const Route = use('Route') 5 | 6 | Route.post('sessions', 'SessionController.store').validator('Session') 7 | Route.post('users', 'UserController.store').validator('User') 8 | 9 | Route.group(() => { 10 | Route.get('roles', 'RoleController.index') 11 | 12 | Route.resource('teams', 'TeamController') 13 | .apiOnly() 14 | .validator(new Map([[['teams.store', 'teams.update'], ['Team']]])) 15 | }).middleware('auth') 16 | 17 | Route.group(() => { 18 | Route.post('invites', 'InviteController.store').validator('Invite').middleware('can:invites_create') 19 | 20 | Route.resource('projects', 'ProjectController') 21 | .apiOnly() 22 | .validator(new Map([[['projects.store', 'projects.update'], ['Project']]])) 23 | .middleware( 24 | new Map([ 25 | [['projects.store', 'projects.update'], ['can:projects_create']] 26 | ]) 27 | ) 28 | 29 | Route.get('members', 'MemberController.index') 30 | Route.put('members/:id', 'MemberController.update').middleware( 31 | 'is:administrator' 32 | ) 33 | 34 | Route.get('permissions', 'PermissionController.show') 35 | }).middleware(['auth', 'team']) 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs-saas", 3 | "version": "4.1.0", 4 | "main": "index.js", 5 | "repository": "https://github.com/osvaldokalvaitir/nodejs-saas.git", 6 | "author": "osvaldokalvaitir ", 7 | "license": "MIT", 8 | "private": true, 9 | "adonis-version": "4.1.0", 10 | "scripts": { 11 | "start": "node server.js", 12 | "test": "node ace test" 13 | }, 14 | "keywords": [ 15 | "adonisjs", 16 | "adonis-app" 17 | ], 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 | "pg": "^7.15.0" 34 | }, 35 | "devDependencies": { 36 | "eslint": "^6.7.2", 37 | "eslint-config-standard": "^14.1.0", 38 | "eslint-plugin-import": "^2.19.1", 39 | "eslint-plugin-node": "^10.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 | -------------------------------------------------------------------------------- /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 boot () { 11 | super.boot() 12 | 13 | /** 14 | * A hook to hash the user password before saving 15 | * it to the database. 16 | */ 17 | this.addHook('beforeSave', async (userInstance) => { 18 | if (userInstance.dirty.password) { 19 | userInstance.password = await Hash.make(userInstance.password) 20 | } 21 | }) 22 | } 23 | 24 | teamJoins () { 25 | return this.hasMany('App/Models/UserTeam') 26 | } 27 | 28 | tokens () { 29 | return this.hasMany('App/Models/Token') 30 | } 31 | 32 | teams () { 33 | return this.belongsToMany('App/Models/Team').pivotModel( 34 | 'App/Models/UserTeam' 35 | ) 36 | } 37 | 38 | async is (expression) { 39 | const team = await this.teamJoins() 40 | .where('team_id', this.currentTeam) 41 | .first() 42 | 43 | return team.is(expression) 44 | } 45 | 46 | async can (expression) { 47 | const team = await this.teamJoins() 48 | .where('team_id', this.currentTeam) 49 | .first() 50 | 51 | return team.can(expression) 52 | } 53 | 54 | async scope (required) { 55 | const team = await this.teamJoins() 56 | .where('team_id', this.currentTeam) 57 | .first() 58 | 59 | return team.scope(required) 60 | } 61 | } 62 | 63 | module.exports = User 64 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | ] 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Named Middleware 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Named middleware is key/value object to conditionally add middleware on 26 | | specific routes or group of routes. 27 | | 28 | | // define 29 | | { 30 | | auth: 'Adonis/Middleware/Auth' 31 | | } 32 | | 33 | | // use 34 | | Route.get().middleware('auth') 35 | | 36 | */ 37 | const namedMiddleware = { 38 | auth: 'Adonis/Middleware/Auth', 39 | guest: 'Adonis/Middleware/AllowGuestOnly', 40 | team: 'App/Middleware/Team', 41 | is: 'Adonis/Acl/Is', 42 | can: 'Adonis/Acl/Can' 43 | } 44 | 45 | /* 46 | |-------------------------------------------------------------------------- 47 | | Server Middleware 48 | |-------------------------------------------------------------------------- 49 | | 50 | | Server level middleware are executed even when route for a given URL is 51 | | not registered. Features like `static assets` and `cors` needs better 52 | | control over request lifecycle. 53 | | 54 | */ 55 | const serverMiddleware = [ 56 | // 'Adonis/Middleware/Static', 57 | 'Adonis/Middleware/Cors' 58 | ] 59 | 60 | Server 61 | .registerGlobal(globalMiddleware) 62 | .registerNamed(namedMiddleware) 63 | .use(serverMiddleware) 64 | -------------------------------------------------------------------------------- /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 | const User = use('App/Models/User') 15 | 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: 'Osvaldo Kalvaitir Filho', 23 | email: 'osvaldokalvaitir@outlook.com', 24 | password: '123456' 25 | }) 26 | 27 | const createInvite = await Permission.create({ 28 | slug: 'invites_create', 29 | name: 'Convidar membros' 30 | }) 31 | 32 | const createProject = await Permission.create({ 33 | slug: 'projects_create', 34 | name: 'Criar projetos' 35 | }) 36 | 37 | const admin = await Role.create({ 38 | slug: 'administrator', 39 | name: 'Administrador' 40 | }) 41 | 42 | const moderator = await Role.create({ 43 | slug: 'moderator', 44 | name: 'Moderador' 45 | }) 46 | 47 | await Role.create({ 48 | slug: 'visitor', 49 | name: 'Visitante' 50 | }) 51 | 52 | await admin.permissions().attach([createInvite.id, createProject.id]) 53 | await moderator.permissions().attach([createProject.id]) 54 | 55 | const team = await user.teams().create({ 56 | name: 'Rocketseat', 57 | user_id: user.id 58 | }) 59 | 60 | const teamJoin = await user 61 | .teamJoins() 62 | .where('team_id', team.id) 63 | .first() 64 | 65 | await teamJoin.roles().attach([admin.id]); 66 | } 67 | } 68 | 69 | module.exports = DatabaseSeeder 70 | -------------------------------------------------------------------------------- /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 | 70 | const jobs = ['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 | const Role = use('Adonis/Acl/Role') 4 | 5 | /** 6 | * Resourceful controller for interacting with teams 7 | */ 8 | class TeamController { 9 | /** 10 | * Show a list of all teams. 11 | * GET teams 12 | * 13 | * @param {object} ctx 14 | * @param {Request} ctx.request 15 | * @param {Response} ctx.response 16 | * @param {View} ctx.view 17 | */ 18 | async index ({ auth }) { 19 | const teams = await auth.user.teams().fetch() 20 | 21 | return teams 22 | } 23 | 24 | /** 25 | * Create/save a new team. 26 | * POST teams 27 | * 28 | * @param {object} ctx 29 | * @param {Request} ctx.request 30 | * @param {Response} ctx.response 31 | */ 32 | async store ({ request, auth }) { 33 | const data = request.only(['name']) 34 | 35 | const team = await auth.user.teams().create({ 36 | ...data, 37 | user_id: auth.user.id 38 | }) 39 | 40 | const teamJoin = await auth.user 41 | .teamJoins() 42 | .where('team_id', team.id) 43 | .first() 44 | 45 | const admin = await Role.findBy('slug', 'administrator') 46 | 47 | await teamJoin.roles().attach([admin.id]) 48 | 49 | return team 50 | } 51 | 52 | /** 53 | * Display a single team. 54 | * GET teams/:id 55 | * 56 | * @param {object} ctx 57 | * @param {Request} ctx.request 58 | * @param {Response} ctx.response 59 | * @param {View} ctx.view 60 | */ 61 | async show ({ params, auth }) { 62 | const team = await auth.user.teams().where('teams.id', params.id).first() 63 | 64 | return team 65 | } 66 | 67 | /** 68 | * Update team details. 69 | * PUT or PATCH teams/:id 70 | * 71 | * @param {object} ctx 72 | * @param {Request} ctx.request 73 | * @param {Response} ctx.response 74 | */ 75 | async update ({ params, request, auth }) { 76 | const data = request.only(['name']) 77 | const team = await auth.user.teams().where('teams.id', params.id).first() 78 | 79 | team.merge(data) 80 | 81 | await team.save() 82 | 83 | return team 84 | } 85 | 86 | /** 87 | * Delete a team with id. 88 | * DELETE teams/:id 89 | * 90 | * @param {object} ctx 91 | * @param {Request} ctx.request 92 | * @param {Response} ctx.response 93 | */ 94 | async destroy ({ params, auth }) { 95 | const team = await auth.user.teams().where('teams.id', params.id).first() 96 | 97 | await team.delete() 98 | } 99 | } 100 | 101 | module.exports = TeamController 102 | -------------------------------------------------------------------------------- /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 = request.team.projects().fetch() 22 | 23 | return projects 24 | } 25 | 26 | /** 27 | * Create/save a new project. 28 | * POST projects 29 | * 30 | * @param {object} ctx 31 | * @param {Request} ctx.request 32 | * @param {Response} ctx.response 33 | */ 34 | async store ({ request }) { 35 | const data = request.only(['title']) 36 | const project = request.team.projects().create(data) 37 | 38 | return project 39 | } 40 | 41 | /** 42 | * Display a single project. 43 | * GET projects/:id 44 | * 45 | * @param {object} ctx 46 | * @param {Request} ctx.request 47 | * @param {Response} ctx.response 48 | * @param {View} ctx.view 49 | */ 50 | async show ({ params, request }) { 51 | const project = await request.team 52 | .projects() 53 | .where('id', params.id) 54 | .first() 55 | 56 | return project 57 | } 58 | 59 | /** 60 | * Update project details. 61 | * PUT or PATCH projects/:id 62 | * 63 | * @param {object} ctx 64 | * @param {Request} ctx.request 65 | * @param {Response} ctx.response 66 | */ 67 | async update ({ params, request }) { 68 | const data = request.only(['title']) 69 | const project = await request.team 70 | .projects() 71 | .where('id', params.id) 72 | .first() 73 | 74 | project.merge(data) 75 | 76 | await project.save() 77 | 78 | return project 79 | } 80 | 81 | /** 82 | * Delete a project with id. 83 | * DELETE projects/:id 84 | * 85 | * @param {object} ctx 86 | * @param {Request} ctx.request 87 | * @param {Response} ctx.response 88 | */ 89 | async destroy ({ params, request }) { 90 | const project = await request.team 91 | .projects() 92 | .where('id', params.id) 93 | .first() 94 | 95 | await project.delete() 96 | } 97 | } 98 | 99 | module.exports = ProjectController 100 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Logo 5 |

6 | 7 |

8 | Node.js - SaaS 9 |

10 | 11 |

12 | :cloud: Application using Node.js, AdonisJs, Adonis ACL, Adonis Kue Provider, Adonis Mail, Adonis Lucid Slugify, Adonis Validator, AdonisJs Redis, ESLint and pg 13 |

14 | 15 |

16 | GitHub language count 17 | 18 | GitHub language top 19 | 20 | 21 | Made by Kalvaitir 22 | 23 | 24 | License 25 |

26 | 27 |

28 | Install and run   |   License 29 |

30 | 31 | ## :wrench: Install and run 32 | 33 | Open terminal: 34 | 35 | ```sh 36 | # Clone this repo 37 | git clone https://github.com/osvaldokalvaitir/nodejs-saas 38 | 39 | # Entry in folder 40 | cd nodejs-saas 41 | 42 | # Install deps with npm or yarn 43 | npm install | yarn 44 | 45 | # Make a copy of the .env.example file, rename it to .env and change the variables according to your environment. 46 | 47 | # Launch the app with npm or yarn 48 | npm run start | yarn start 49 | 50 | # Run API 51 | adonis serve --dev 52 | ``` 53 | 54 | Click to learn more about the tools used: [Insomnia](https://github.com/osvaldokalvaitir/awesome/blob/main/src/api-clients/insomnia/insomnia.md), [Docker](https://github.com/osvaldokalvaitir/awesome/blob/main/src/containers/docker/docker.md), [PostgreSQL Docker Image postgres](https://github.com/osvaldokalvaitir/awesome/blob/main/src/containers/docker/images/postgres.md), [Redis Docker Image redis:alpine](https://github.com/osvaldokalvaitir/awesome/blob/main/src/containers/docker/images/redis-alpine.md), [Mailtrap](https://github.com/osvaldokalvaitir/awesome/blob/main/src/emails/mailtrap.md), [Postbird](https://github.com/osvaldokalvaitir/awesome/blob/main/src/sgdbs/postgresql/postbird.md). 55 | 56 | [![Run in Insomnia}](https://insomnia.rest/images/run.svg)](https://insomnia.rest/run/?label=SaaS&uri=https%3A%2F%2Fraw.githubusercontent.com%2Fosvaldokalvaitir%2Fnodejs-saas%2Fmain%2FInsomnia.json) 57 | 58 | ## :memo: License 59 | 60 | This project is under the MIT license. See [LICENSE](/LICENSE) for more information. 61 | 62 | --- 63 | 64 |

65 | Developed with 💚 by Osvaldo Kalvaitir Filho 66 |

67 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | |-------------------------------------------------------------------------- 88 | | Etag 89 | |-------------------------------------------------------------------------- 90 | | 91 | | Set etag on all HTTP response. In order to disable for selected routes, 92 | | you can call the `response.send` with an options object as follows. 93 | | 94 | | response.send('Hello', { ignoreEtag: true }) 95 | | 96 | */ 97 | etag: false 98 | }, 99 | 100 | views: { 101 | /* 102 | |-------------------------------------------------------------------------- 103 | | Cache Views 104 | |-------------------------------------------------------------------------- 105 | | 106 | | Define whether or not to cache the compiled view. Set it to true in 107 | | production to optimize view loading time. 108 | | 109 | */ 110 | cache: Env.get('CACHE_VIEWS', true) 111 | }, 112 | 113 | static: { 114 | /* 115 | |-------------------------------------------------------------------------- 116 | | Dot Files 117 | |-------------------------------------------------------------------------- 118 | | 119 | | Define how to treat dot files when trying to server static resources. 120 | | By default it is set to ignore, which will pretend that dotfiles 121 | | does not exists. 122 | | 123 | | Can be one of the following 124 | | ignore, deny, allow 125 | | 126 | */ 127 | dotfiles: 'ignore', 128 | 129 | /* 130 | |-------------------------------------------------------------------------- 131 | | ETag 132 | |-------------------------------------------------------------------------- 133 | | 134 | | Enable or disable etag generation 135 | | 136 | */ 137 | etag: true, 138 | 139 | /* 140 | |-------------------------------------------------------------------------- 141 | | Extensions 142 | |-------------------------------------------------------------------------- 143 | | 144 | | Set file extension fallbacks. When set, if a file is not found, the given 145 | | extensions will be added to the file name and search for. The first 146 | | that exists will be served. Example: ['html', 'htm']. 147 | | 148 | */ 149 | extensions: false 150 | }, 151 | 152 | locales: { 153 | /* 154 | |-------------------------------------------------------------------------- 155 | | Loader 156 | |-------------------------------------------------------------------------- 157 | | 158 | | The loader to be used for fetching and updating locales. Below is the 159 | | list of available options. 160 | | 161 | | file, database 162 | | 163 | */ 164 | loader: 'file', 165 | 166 | /* 167 | |-------------------------------------------------------------------------- 168 | | Default Locale 169 | |-------------------------------------------------------------------------- 170 | | 171 | | Default locale to be used by Antl provider. You can always switch drivers 172 | | in runtime or use the official Antl middleware to detect the driver 173 | | based on HTTP headers/query string. 174 | | 175 | */ 176 | locale: 'en' 177 | }, 178 | 179 | logger: { 180 | /* 181 | |-------------------------------------------------------------------------- 182 | | Transport 183 | |-------------------------------------------------------------------------- 184 | | 185 | | Transport to be used for logging messages. You can have multiple 186 | | transports using same driver. 187 | | 188 | | Available drivers are: `file` and `console`. 189 | | 190 | */ 191 | transport: 'console', 192 | 193 | /* 194 | |-------------------------------------------------------------------------- 195 | | Console Transport 196 | |-------------------------------------------------------------------------- 197 | | 198 | | Using `console` driver for logging. This driver writes to `stdout` 199 | | and `stderr` 200 | | 201 | */ 202 | console: { 203 | driver: 'console', 204 | name: 'adonis-app', 205 | level: 'info' 206 | }, 207 | 208 | /* 209 | |-------------------------------------------------------------------------- 210 | | File Transport 211 | |-------------------------------------------------------------------------- 212 | | 213 | | File transport uses file driver and writes log messages for a given 214 | | file inside `tmp` directory for your app. 215 | | 216 | | For a different directory, set an absolute path for the filename. 217 | | 218 | */ 219 | file: { 220 | driver: 'file', 221 | name: 'adonis-app', 222 | filename: 'adonis.log', 223 | level: 'info' 224 | } 225 | }, 226 | 227 | /* 228 | |-------------------------------------------------------------------------- 229 | | Generic Cookie Options 230 | |-------------------------------------------------------------------------- 231 | | 232 | | The following cookie options are generic settings used by AdonisJs to create 233 | | cookies. However, some parts of the application like `sessions` can have 234 | | separate settings for cookies inside `config/session.js`. 235 | | 236 | */ 237 | cookie: { 238 | httpOnly: true, 239 | sameSite: false, 240 | path: '/', 241 | maxAge: 7200 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /Insomnia.json: -------------------------------------------------------------------------------- 1 | {"_type":"export","__export_format":4,"__export_date":"2020-02-13T01:00:01.272Z","__export_source":"insomnia.desktop.app:v7.1.0","resources":[{"_id":"req_246d5448541f448c953b3e69d104cd26","authentication":{"token":"{{ token }}","type":"bearer"},"body":{},"created":1577404427486,"description":"","headers":[{"description":"","id":"pair_6b956ee0039a4b96b01db676602d62cf","name":"TEAM","value":"rocketseat"}],"isPrivate":false,"metaSortKey":-1577404427486,"method":"GET","modified":1577404474133,"name":"Show","parameters":[],"parentId":"fld_996e8ab642374a23a0a07a41439bc55f","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingFollowRedirects":"global","settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/permissions","_type":"request"},{"_id":"fld_996e8ab642374a23a0a07a41439bc55f","created":1577404413428,"description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1577404413428,"modified":1577404413428,"name":"Permissions","parentId":"wrk_b96a182036d74f2ab838a2a2530b094a","_type":"request_group"},{"_id":"wrk_b96a182036d74f2ab838a2a2530b094a","created":1576685800581,"description":"","modified":1581555556879,"name":"SaaS","parentId":null,"_type":"workspace"},{"_id":"req_6dec58db7ab64ccfb15b58ca10412600","authentication":{"token":"{{ token }}","type":"bearer"},"body":{},"created":1577403388216,"description":"","headers":[{"description":"","id":"pair_f1ffe8698c6c436ba34e2f8a2d7ce393","name":"TEAM","value":"rocketseat"}],"isPrivate":false,"metaSortKey":-1577403403857,"method":"GET","modified":1577403559596,"name":"Index","parameters":[],"parentId":"fld_1310f8f4819d49a183498803f978a3a9","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingFollowRedirects":"global","settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/members","_type":"request"},{"_id":"fld_1310f8f4819d49a183498803f978a3a9","created":1577403382174,"description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1577403382174,"modified":1577403382174,"name":"Members","parentId":"wrk_b96a182036d74f2ab838a2a2530b094a","_type":"request_group"},{"_id":"req_04a59477581c42d5af98e99c941db2e4","authentication":{"token":"{{ token }}","type":"bearer"},"body":{"mimeType":"application/json","text":"{\n\t\"roles\": [1]\n}"},"created":1577403589488,"description":"","headers":[{"description":"","id":"pair_f1ffe8698c6c436ba34e2f8a2d7ce393","name":"TEAM","value":"rocketseat"},{"id":"pair_afcb669dc7d84afba724d2a779a0b58b","name":"Content-Type","value":"application/json"}],"isPrivate":false,"metaSortKey":-1577402910847.5,"method":"PUT","modified":1577403770112,"name":"Update","parameters":[],"parentId":"fld_1310f8f4819d49a183498803f978a3a9","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingFollowRedirects":"global","settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/members/1","_type":"request"},{"_id":"req_dba87a89004e4b948bcca17a6f275194","authentication":{"token":"{{ token }}","type":"bearer"},"body":{},"created":1577402417838,"description":"","headers":[],"isPrivate":false,"metaSortKey":-1577402417838,"method":"GET","modified":1577402453645,"name":"Index","parameters":[],"parentId":"fld_8bacc001578e4dbaada8e0254f824089","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingFollowRedirects":"global","settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/roles","_type":"request"},{"_id":"fld_8bacc001578e4dbaada8e0254f824089","created":1577402409965,"description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1577402409966,"modified":1577402409965,"name":"Roles","parentId":"wrk_b96a182036d74f2ab838a2a2530b094a","_type":"request_group"},{"_id":"req_7930fa07f56445f0a30e7b8fd2ca1f7a","authentication":{"token":"{{ token }}","type":"bearer"},"body":{},"created":1577015511635,"description":"","headers":[{"description":"","id":"pair_0cd200b6adf44682bf9b74eac6dde3ad","name":"TEAM","value":"rocketseat"}],"isPrivate":false,"metaSortKey":-1577015511635,"method":"GET","modified":1577015549176,"name":"Index","parameters":[],"parentId":"fld_6d2d12d31ce148f5935b7d733cd661cd","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingFollowRedirects":"global","settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/projects","_type":"request"},{"_id":"fld_6d2d12d31ce148f5935b7d733cd661cd","created":1577015485176,"description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1577015485178,"modified":1577015485176,"name":"Projects","parentId":"wrk_b96a182036d74f2ab838a2a2530b094a","_type":"request_group"},{"_id":"req_2fde973397184fe9adf6016559875a27","authentication":{"token":"{{ token }}","type":"bearer"},"body":{"mimeType":"application/json","text":"{\n\t\"title\": \"Projeto em React Native\"\n}"},"created":1577015585560,"description":"","headers":[{"description":"","id":"pair_0cd200b6adf44682bf9b74eac6dde3ad","name":"TEAM","value":"rocketseat"},{"id":"pair_a6bba48b57814502a212fb72ceeec57b","name":"Content-Type","value":"application/json"}],"isPrivate":false,"metaSortKey":-1576979156359,"method":"POST","modified":1577400026365,"name":"Create","parameters":[],"parentId":"fld_6d2d12d31ce148f5935b7d733cd661cd","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingFollowRedirects":"global","settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/projects","_type":"request"},{"_id":"req_021faa86bfdd455daec783f74d2f06cb","authentication":{"token":"{{ token }}","type":"bearer"},"body":{},"created":1577015665173,"description":"","headers":[{"description":"","id":"pair_0cd200b6adf44682bf9b74eac6dde3ad","name":"TEAM","value":"rocketseat"}],"isPrivate":false,"metaSortKey":-1576979156309,"method":"GET","modified":1577015679282,"name":"Show","parameters":[],"parentId":"fld_6d2d12d31ce148f5935b7d733cd661cd","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingFollowRedirects":"global","settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/projects/1","_type":"request"},{"_id":"req_f8a55fde9bae47629d3b997a9bdbc099","authentication":{"token":"{{ token }}","type":"bearer"},"body":{"mimeType":"application/json","text":"{\n\t\"title\": \"Projeto em ReactJS\"\n}"},"created":1577015688323,"description":"","headers":[{"description":"","id":"pair_0cd200b6adf44682bf9b74eac6dde3ad","name":"TEAM","value":"rocketseat"},{"id":"pair_a6bba48b57814502a212fb72ceeec57b","name":"Content-Type","value":"application/json"}],"isPrivate":false,"metaSortKey":-1576979156259,"method":"PUT","modified":1577015723564,"name":"Update","parameters":[],"parentId":"fld_6d2d12d31ce148f5935b7d733cd661cd","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingFollowRedirects":"global","settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/projects/1","_type":"request"},{"_id":"req_9b37979d72da49fdba938f08ae6d1870","authentication":{"token":"{{ token }}","type":"bearer"},"body":{},"created":1577015730557,"description":"","headers":[{"description":"","id":"pair_0cd200b6adf44682bf9b74eac6dde3ad","name":"TEAM","value":"rocketseat"}],"isPrivate":false,"metaSortKey":-1576960978671,"method":"DELETE","modified":1577015744052,"name":"Delete","parameters":[],"parentId":"fld_6d2d12d31ce148f5935b7d733cd661cd","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingFollowRedirects":"global","settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/projects/1","_type":"request"},{"_id":"req_42c373c7e27047faa6f0715461c0cb63","authentication":{},"body":{"mimeType":"application/json","text":"{\n\t\"email\": \"cleiton@rocketseat.com.br\",\n\t\"name\": \"Cleiton\",\n\t\"password\": \"123123\"\n}"},"created":1576942801082,"description":"","headers":[{"id":"pair_aebbd0f174924d3cb1c81cd3b162c338","name":"Content-Type","value":"application/json"}],"isPrivate":false,"metaSortKey":-1576942801083,"method":"POST","modified":1576943049580,"name":"Create","parameters":[],"parentId":"fld_2652ec37f76f42b9ae01776955c8a296","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingFollowRedirects":"global","settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/users","_type":"request"},{"_id":"fld_2652ec37f76f42b9ae01776955c8a296","created":1576942776622,"description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1576942776624,"modified":1576942776622,"name":"Users","parentId":"wrk_b96a182036d74f2ab838a2a2530b094a","_type":"request_group"},{"_id":"req_5eaf2536c5c145629a1745112c573db5","authentication":{"token":"{{ token }}","type":"bearer"},"body":{"mimeType":"application/json","text":"{\n\t\"invites\": [\n\t\t\"cleiton@rocketseat.com.br\"\n\t]\n}"},"created":1576765841888,"description":"","headers":[{"id":"pair_f268a58f22304b12a3a947c4c950897b","name":"Content-Type","value":"application/json"},{"description":"","disabled":false,"id":"pair_92a9e7efe9be4ac19fda45123a5741b2","name":"TEAM","value":"rocketseat"},{"description":"","id":"pair_e3d54ea2fff440a29e334d670b6be3fa","name":"Accept","value":"application/json"}],"isPrivate":false,"metaSortKey":-1576765841888,"method":"POST","modified":1577390732114,"name":"Store","parameters":[],"parentId":"fld_129670680eb641c2a0bdbb7ffbb2a1e7","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingFollowRedirects":"global","settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/invites","_type":"request"},{"_id":"fld_129670680eb641c2a0bdbb7ffbb2a1e7","created":1576765832690,"description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1576765832691,"modified":1576765832690,"name":"Invites","parentId":"wrk_b96a182036d74f2ab838a2a2530b094a","_type":"request_group"},{"_id":"req_9eb4739780414123b1c0c566ae4e0569","authentication":{"token":"{{ token }}","type":"bearer"},"body":{},"created":1576695435730,"description":"","headers":[],"isPrivate":false,"metaSortKey":-1576695435730,"method":"GET","modified":1576695459069,"name":"Index","parameters":[],"parentId":"fld_4a49b6e17b4c4e7f84c8b7f3a5f0d8ed","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingFollowRedirects":"global","settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/teams","_type":"request"},{"_id":"fld_4a49b6e17b4c4e7f84c8b7f3a5f0d8ed","created":1576695405746,"description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1576695405746,"modified":1576695405746,"name":"Teams","parentId":"wrk_b96a182036d74f2ab838a2a2530b094a","_type":"request_group"},{"_id":"req_5c975ea14f1648d2a40d87fced308a35","authentication":{"token":"{{ token }}","type":"bearer"},"body":{"mimeType":"application/json","text":"{\n\t\"name\": \"Meu novo time\"\n}"},"created":1576698687405,"description":"","headers":[{"id":"pair_66d202fdcdbe4d31ae52dd97e2512020","name":"Content-Type","value":"application/json"}],"isPrivate":false,"metaSortKey":-1576693652889.0625,"method":"POST","modified":1576698753480,"name":"Create","parameters":[],"parentId":"fld_4a49b6e17b4c4e7f84c8b7f3a5f0d8ed","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingFollowRedirects":"global","settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/teams","_type":"request"},{"_id":"req_aa51e722a721414c96494300a3cc4716","authentication":{"token":"{{ token }}","type":"bearer"},"body":{},"created":1576698750366,"description":"","headers":[],"isPrivate":false,"metaSortKey":-1576693207178.8281,"method":"GET","modified":1576698833787,"name":"Show","parameters":[],"parentId":"fld_4a49b6e17b4c4e7f84c8b7f3a5f0d8ed","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingFollowRedirects":"global","settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/teams/2","_type":"request"},{"_id":"req_946893efbd3e46e7bf0002602992f3a4","authentication":{"token":"{{ token }}","type":"bearer"},"body":{"mimeType":"application/json","text":"{\n\t\"name\": \"Meu novo time atualizado\"\n}"},"created":1576698785103,"description":"","headers":[{"id":"pair_66d202fdcdbe4d31ae52dd97e2512020","name":"Content-Type","value":"application/json"}],"isPrivate":false,"metaSortKey":-1576692761468.5938,"method":"PUT","modified":1576698812600,"name":"Update","parameters":[],"parentId":"fld_4a49b6e17b4c4e7f84c8b7f3a5f0d8ed","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingFollowRedirects":"global","settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/teams/2","_type":"request"},{"_id":"req_c33efe9fe4fb4d57b5d8f32be37280cb","authentication":{"token":"{{ token }}","type":"bearer"},"body":{},"created":1576698841684,"description":"","headers":[],"isPrivate":false,"metaSortKey":-1576689344356.7969,"method":"DELETE","modified":1576698869163,"name":"Delete","parameters":[],"parentId":"fld_4a49b6e17b4c4e7f84c8b7f3a5f0d8ed","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingFollowRedirects":"global","settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/teams/2","_type":"request"},{"_id":"req_e96585285e2549c7955b3273169c9ba1","authentication":{},"body":{"mimeType":"application/json","text":"{\n\t\"email\": \"osvaldokalvaitir@outlook.com\",\n\t\"password\": \"123456\"\n}"},"created":1576685927244,"description":"","headers":[{"id":"pair_465323ce18db426dbfbe174b5a6afae0","name":"Content-Type","value":"application/json"}],"isPrivate":false,"metaSortKey":-1576685927245,"method":"POST","modified":1576692632893,"name":"Store","parameters":[],"parentId":"fld_82d370d975fb4ee4974df67f5ef9ea9a","settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingFollowRedirects":"global","settingRebuildPath":true,"settingSendCookies":true,"settingStoreCookies":true,"url":"{{ base_url }}/sessions","_type":"request"},{"_id":"fld_82d370d975fb4ee4974df67f5ef9ea9a","created":1576685850282,"description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1576685850282,"modified":1576685850282,"name":"Sessions","parentId":"wrk_b96a182036d74f2ab838a2a2530b094a","_type":"request_group"},{"_id":"env_e09c80a34b97dc62d0137741913279e06c2fb875","color":null,"created":1576685801041,"data":{"base_url":"http://localhost:3333","token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjEsImlhdCI6MTU3NjY5MzA2Mn0.2UXj8yXu6_CKa_vfSlhqcCG6gJSrCLIEQkkZ0YTC0oA"},"dataPropertyOrder":{"&":["base_url","token"]},"isPrivate":false,"metaSortKey":1576685801041,"modified":1576693073082,"name":"Base Environment","parentId":"wrk_b96a182036d74f2ab838a2a2530b094a","_type":"environment"},{"_id":"jar_e09c80a34b97dc62d0137741913279e06c2fb875","cookies":[],"created":1576685801101,"modified":1576685801101,"name":"Default Jar","parentId":"wrk_b96a182036d74f2ab838a2a2530b094a","_type":"cookie_jar"}]} --------------------------------------------------------------------------------