├── .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 |
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 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
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 | [](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"}]}
--------------------------------------------------------------------------------