Skip to content

Commit a6ecb9c

Browse files
committed
Inital - base Adonis with Lucid and inital tests
0 parents  commit a6ecb9c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+7106
-0
lines changed

.adonisrc.json

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"typescript": true,
3+
"commands": [
4+
"./commands",
5+
"@adonisjs/core/build/commands",
6+
"@adonisjs/repl/build/commands",
7+
"@adonisjs/lucid/build/commands"
8+
],
9+
"exceptionHandlerNamespace": "App/Exceptions/Handler",
10+
"aliases": {
11+
"App": "app",
12+
"Config": "config",
13+
"Database": "database",
14+
"Contracts": "contracts"
15+
},
16+
"preloads": [
17+
"./start/routes",
18+
"./start/kernel"
19+
],
20+
"providers": [
21+
"./providers/AppProvider",
22+
"@adonisjs/core",
23+
"@adonisjs/lucid",
24+
"@adonisjs/auth"
25+
],
26+
"aceProviders": [
27+
"@adonisjs/repl"
28+
]
29+
}

.editorconfig

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
2+
[*]
3+
indent_style = space
4+
indent_size = 2
5+
end_of_line = lf
6+
charset = utf-8
7+
trim_trailing_whitespace = true
8+
insert_final_newline = true
9+
10+
[*.json]
11+
insert_final_newline = ignore
12+
13+
[*.md]
14+
trim_trailing_whitespace = false

.env.example

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
PORT=
2+
HOST=
3+
NODE_ENV=
4+
APP_KEY=
5+
DB_CONNECTION=mysql
6+
MYSQL_HOST=localhost
7+
MYSQL_PORT=3307
8+
MYSQL_USER=adonisjs
9+
MYSQL_PASSWORD=secret
10+
MYSQL_DB_NAME=adonisjs
11+
MYSQL_ALLOW_EMPTY_PASSWORD=true

.env.testing

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
PORT=3333
2+
HOST=0.0.0.0
3+
NODE_ENV=testing
4+
APP_KEY=UDMP4faE9vPtVd718Dg8nFAY_Pv_QU3n
5+
DB_CONNECTION=sqlite
6+

.eslintignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
build

.eslintrc.json

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"extends": ["plugin:adonis/typescriptApp", "prettier", "prettier/@typescript-eslint"],
3+
"plugins": ["prettier"],
4+
"rules": {
5+
"prettier/prettier": ["error"]
6+
}
7+
}

.gitignore

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
node_modules
2+
build
3+
coverage
4+
.vscode
5+
.DS_STORE
6+
.env
7+
tmp
8+
.idea

.prettierignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
build

.prettierrc

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"trailingComma": "es5",
3+
"semi": false,
4+
"singleQuote": true,
5+
"useTabs": false,
6+
"quoteProps": "consistent",
7+
"bracketSpacing": true,
8+
"arrowParens": "always",
9+
"printWidth": 100
10+
}

README.md

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# NodeJS Scaffold API with AdonisJS 5
2+
3+
## Requirements
4+
5+
- Node.js >= 12.0.0
6+
- npm >= 6.0.0
7+
8+
And if you don't have a database installed, you can user a Docker container.
9+
10+
11+
## Set It Up
12+
13+
#### 1. Install node dependencies
14+
15+
```
16+
npm install
17+
```
18+
19+
#### 2. Create the .env file
20+
```
21+
cp .env.example .env
22+
```
23+
24+
#### 4. Set up docker
25+
```
26+
docker-compose build && docker-compose up -d
27+
```
28+
29+
#### 5. Run the migrations
30+
31+
```
32+
node ace migrations:run
33+
```
34+
35+
#### 6. Start and watch the serve
36+
```
37+
node ace serve --watch
38+
```

__tests__/functional/Login.spec.ts

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { UserFactory } from 'Database/factories'
2+
import test from 'japa'
3+
import supertest from 'supertest'
4+
5+
const BASE_URL = `http://${process.env.HOST}:${process.env.PORT}`
6+
7+
test.group('AuthController - Login', () => {
8+
test('should login with right credentials ', async (assert) => {
9+
const sessionPayload = {
10+
password: 'secret',
11+
}
12+
const user = await UserFactory.merge({ ...sessionPayload }).create()
13+
14+
const result = await supertest(BASE_URL)
15+
.post('/login')
16+
.send({
17+
email: user.email,
18+
...sessionPayload,
19+
})
20+
.set('Accept', 'application/json')
21+
.expect(200)
22+
assert.exists(result.body)
23+
assert.exists(result.body.token)
24+
})
25+
26+
test('should not login with wrong credentials ', async (assert) => {
27+
const sessionPayload = {
28+
password: 'secret',
29+
}
30+
31+
const user = await UserFactory.create()
32+
33+
const result = await supertest(BASE_URL)
34+
.post('/login')
35+
.send({
36+
email: user.email,
37+
...sessionPayload,
38+
})
39+
.set('Accept', 'application/json')
40+
.expect(400)
41+
42+
assert.exists(result.body)
43+
assert.exists(result.body.errors)
44+
assert.equal(result.body.errors[0].message, 'Invalid user credentials')
45+
})
46+
})

__tests__/functional/Register.spec.ts

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import test from 'japa'
2+
import supertest from 'supertest'
3+
4+
const BASE_URL = `http://${process.env.HOST}:${process.env.PORT}`
5+
6+
test.group('AuthController - Register', () => {
7+
test('should create a new user with the right data', async (assert) => {
8+
const result = await supertest(BASE_URL)
9+
.post('/register')
10+
.send({
11+
email: 'vini@nodejs.com',
12+
password: 'secret',
13+
password_confirmation: 'secret',
14+
})
15+
.set('Accept', 'application/json')
16+
.expect(201)
17+
18+
assert.exists(result.body)
19+
assert.equal(result.body.email, 'vini@nodejs.com')
20+
})
21+
22+
test('should not create a new user with an exist email ', async (assert) => {
23+
const result = await supertest(BASE_URL)
24+
.post('/register')
25+
.send({
26+
email: 'vini@nodejs.com',
27+
password: 'secret',
28+
password_confirmation: 'secret',
29+
})
30+
.set('Accept', 'application/json')
31+
.expect('Content-Type', /json/)
32+
.expect(422)
33+
34+
assert.exists(result.body)
35+
assert.equal(result.body.errors[0].rule, 'unique')
36+
assert.equal(result.body.errors[0].field, 'email')
37+
assert.equal(result.body.errors[0].message, 'unique validation failure')
38+
})
39+
40+
test('should not create a new user without a password ', async (assert) => {
41+
const result = await supertest(BASE_URL)
42+
.post('/register')
43+
.send({
44+
email: 'vini-2@nodejs.com',
45+
})
46+
.set('Accept', 'application/json')
47+
.expect(422)
48+
49+
assert.exists(result.body)
50+
assert.equal(result.body.errors[0].rule, 'required')
51+
assert.equal(result.body.errors[0].field, 'password')
52+
assert.equal(result.body.errors[0].message, 'required validation failed')
53+
})
54+
55+
test('should not create a new user without a confirmation password ', async (assert) => {
56+
const result = await supertest(BASE_URL)
57+
.post('/register')
58+
.send({
59+
email: 'vini-2@nodejs.com',
60+
password: 'secret',
61+
})
62+
.set('Accept', 'application/json')
63+
.expect(422)
64+
65+
assert.exists(result.body)
66+
assert.equal(result.body.errors[0].rule, 'confirmed')
67+
assert.equal(result.body.errors[0].field, 'password_confirmation')
68+
assert.equal(result.body.errors[0].message, 'confirmed validation failed')
69+
})
70+
})

ace

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/*
2+
|--------------------------------------------------------------------------
3+
| Ace Commands
4+
|--------------------------------------------------------------------------
5+
|
6+
| This file is the entry point for running ace commands.
7+
|
8+
*/
9+
10+
require('reflect-metadata')
11+
require('source-map-support').install({ handleUncaughtExceptions: false })
12+
13+
const { Ignitor } = require('@adonisjs/core/build/standalone')
14+
new Ignitor(__dirname)
15+
.ace()
16+
.handle(process.argv.slice(2))

ace-manifest.json

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"dump:rcfile":{"settings":{},"commandPath":"@adonisjs/core/build/commands/DumpRc","commandName":"dump:rcfile","description":"Dump contents of .adonisrc.json file along with defaults","args":[],"flags":[]},"list:routes":{"settings":{"loadApp":true},"commandPath":"@adonisjs/core/build/commands/ListRoutes","commandName":"list:routes","description":"List application routes","args":[],"flags":[{"name":"json","propertyName":"json","type":"boolean","description":"Output as JSON"}]},"generate:key":{"settings":{},"commandPath":"@adonisjs/core/build/commands/GenerateKey","commandName":"generate:key","description":"Generate a new APP_KEY secret","args":[],"flags":[]},"repl":{"settings":{"loadApp":true,"environment":"repl","stayAlive":true},"commandPath":"@adonisjs/repl/build/commands/AdonisRepl","commandName":"repl","description":"Start a new REPL session","args":[],"flags":[]},"db:seed":{"settings":{"loadApp":true},"commandPath":"@adonisjs/lucid/build/commands/DbSeed","commandName":"db:seed","description":"Execute database seeder files","args":[],"flags":[{"name":"connection","propertyName":"connection","type":"string","description":"Define a custom database connection for the seeders","alias":"c"},{"name":"interactive","propertyName":"interactive","type":"boolean","description":"Run seeders in interactive mode","alias":"i"},{"name":"files","propertyName":"files","type":"array","description":"Define a custom set of seeders files names to run","alias":"f"}]},"make:model":{"settings":{},"commandPath":"@adonisjs/lucid/build/commands/MakeModel","commandName":"make:model","description":"Make a new Lucid model","args":[{"type":"string","propertyName":"name","name":"name","required":true,"description":"Name of the model class"}],"flags":[{"name":"migration","propertyName":"migration","type":"boolean","alias":"m","description":"Generate the migration for the model"},{"name":"controller","propertyName":"controller","type":"boolean","alias":"c","description":"Generate the controller for the model"}]},"make:migration":{"settings":{"loadApp":true},"commandPath":"@adonisjs/lucid/build/commands/MakeMigration","commandName":"make:migration","description":"Make a new migration file","args":[{"type":"string","propertyName":"name","name":"name","required":true,"description":"Name of the migration file"}],"flags":[{"name":"connection","propertyName":"connection","type":"string","description":"Define a custom database connection for the migration"},{"name":"folder","propertyName":"folder","type":"string","description":"Pre-select a migration directory"},{"name":"create","propertyName":"create","type":"string","description":"Define the table name for creating a new table"},{"name":"table","propertyName":"table","type":"string","description":"Define the table name for altering an existing table"}]},"make:seeder":{"settings":{},"commandPath":"@adonisjs/lucid/build/commands/MakeSeeder","commandName":"make:seeder","description":"Make a new Seeder file","args":[{"type":"string","propertyName":"name","name":"name","required":true,"description":"Name of the seeder class"}],"flags":[]},"migration:run":{"settings":{"loadApp":true},"commandPath":"@adonisjs/lucid/build/commands/Migration/Run","commandName":"migration:run","description":"Run pending migrations","args":[],"flags":[{"name":"connection","propertyName":"connection","type":"string","description":"Define a custom database connection","alias":"c"},{"name":"force","propertyName":"force","type":"boolean","description":"Explicitly force to run migrations in production"},{"name":"dry-run","propertyName":"dryRun","type":"boolean","description":"Print SQL queries, instead of running the migrations"}]},"migration:rollback":{"settings":{"loadApp":true},"commandPath":"@adonisjs/lucid/build/commands/Migration/Rollback","commandName":"migration:rollback","description":"Rollback migrations to a given batch number","args":[],"flags":[{"name":"connection","propertyName":"connection","type":"string","description":"Define a custom database connection","alias":"c"},{"name":"force","propertyName":"force","type":"boolean","description":"Explictly force to run migrations in production"},{"name":"dry-run","propertyName":"dryRun","type":"boolean","description":"Print SQL queries, instead of running the migrations"},{"name":"batch","propertyName":"batch","type":"number","description":"Define custom batch number for rollback. Use 0 to rollback to initial state"}]},"migration:status":{"settings":{"loadApp":true},"commandPath":"@adonisjs/lucid/build/commands/Migration/Status","commandName":"migration:status","description":"Check migrations current status.","args":[],"flags":[{"name":"connection","propertyName":"connection","type":"string","description":"Define a custom database connection","alias":"c"}]}}
+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import User from 'App/Models/User'
2+
import RegisterValidator from 'App/Validators/User/RegisterValidator'
3+
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
4+
5+
export default class AuthController {
6+
public async register({ request, response }: HttpContextContract) {
7+
const { email, password } = await request.validate(RegisterValidator)
8+
9+
const user = await User.create({ email, password})
10+
11+
return response.created(user)
12+
}
13+
public async login({ request, auth }: HttpContextContract) {
14+
const email = request.input('email')
15+
const password = request.input('password')
16+
17+
const token = await auth.use('api').attempt(email, password)
18+
return token.toJSON()
19+
}
20+
}

app/Exceptions/Handler.ts

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
|--------------------------------------------------------------------------
3+
| Http Exception Handler
4+
|--------------------------------------------------------------------------
5+
|
6+
| AdonisJs will forward all exceptions occurred during an HTTP request to
7+
| the following class. You can learn more about exception handling by
8+
| reading docs.
9+
|
10+
| The exception handler extends a base `HttpExceptionHandler` which is not
11+
| mandatory, however it can do lot of heavy lifting to handle the errors
12+
| properly.
13+
|
14+
*/
15+
16+
import Logger from '@ioc:Adonis/Core/Logger'
17+
import HttpExceptionHandler from '@ioc:Adonis/Core/HttpExceptionHandler'
18+
19+
export default class ExceptionHandler extends HttpExceptionHandler {
20+
constructor() {
21+
super(Logger)
22+
}
23+
}

app/Middleware/Auth.ts

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
2+
import { AuthenticationException } from '@adonisjs/auth/build/standalone'
3+
4+
/**
5+
* Auth middleware is meant to restrict un-authenticated access to a given route
6+
* or a group of routes.
7+
*
8+
* You must register this middleware inside `start/kernel.ts` file under the list
9+
* of named middleware.
10+
*/
11+
export default class AuthMiddleware {
12+
/**
13+
* The URL to redirect to when request is Unauthorized
14+
*/
15+
protected redirectTo = '/login'
16+
17+
/**
18+
* Authenticates the current HTTP request against a custom set of defined
19+
* guards.
20+
*
21+
* The authentication loop stops as soon as the user is authenticated using any
22+
* of the mentioned guards and that guard will be used by the rest of the code
23+
* during the current request.
24+
*/
25+
protected async authenticate (auth: HttpContextContract['auth'], guards: any[]) {
26+
/**
27+
* Hold reference to the guard last attempted within the for loop. We pass
28+
* the reference of the guard to the "AuthenticationException", so that
29+
* it can decide the correct response behavior based upon the guard
30+
* driver
31+
*/
32+
let guardLastAttempted: string | undefined
33+
34+
for (let guard of guards) {
35+
guardLastAttempted = guard
36+
37+
if (await auth.use(guard).check()) {
38+
/**
39+
* Instruct auth to use the given guard as the default guard for
40+
* the rest of the request, since the user authenticated
41+
* succeeded here
42+
*/
43+
auth.defaultGuard = guard
44+
return true
45+
}
46+
}
47+
48+
/**
49+
* Unable to authenticate using any guard
50+
*/
51+
throw new AuthenticationException(
52+
'Unauthorized access',
53+
'E_UNAUTHORIZED_ACCESS',
54+
guardLastAttempted,
55+
this.redirectTo,
56+
)
57+
}
58+
59+
/**
60+
* Handle request
61+
*/
62+
public async handle ({ auth }: HttpContextContract, next: () => Promise<void>, customGuards: string[]) {
63+
/**
64+
* Uses the user defined guards or the default guard mentioned in
65+
* the config file
66+
*/
67+
const guards = customGuards.length ? customGuards : [auth.name]
68+
await this.authenticate(auth, guards)
69+
await next()
70+
}
71+
}

0 commit comments

Comments
 (0)