Это руководство проведет вас через создание полного стека приложения Adonis, его развертывание в Heroku, работу с базой данных, аутентификацию пользователя и проверку данных, шаг за шагом.
О чем будет рассказано в этой статье:
- 1. Создание нового проекта AdonisJs
- 2. Настройка приложения Heroku.
- 3. Добавление новых маршрутов
- 4. Добавление данных в базу данных
- 5. Миграции
- 6. Контроллеры
- 7. Ясные модели
- 8. CSRF
- 9. Безопасность
- 10. Проверка данных
- 11. Тестирование
1. Создание нового проекта AdonisJs
Установите adonis / CLI
npm i -g @adonisjs/cli
Создайте новый проект с любым именем, которое вам нравится
adonis new projectname
Это создаст adonis-fullstack-app
ДОКУМЕНТЫ:
…, Он поставляется с предварительно настроенными:
Bodyparser, Session, Authentication, промежуточное ПО веб-безопасности, CORS, Edge, механизм шаблонов, Lucid ORM, миграции и начальные числа.
Вы можете использовать флаг
api-only
для создания только сервера api. Но это уже другой рассказ.
// Scaffold project for api server adonis new projectname --api-only adonis new projectname --api
Чтобы запустить проект локально и убедиться, что все работает должным образом, вы можете использовать этот сценарий (приложение будет запускаться с использованием .env
производственного файла)
adonis serve --dev
Флаг
--dev
означает, что он будет перезапускаться каждый раз, если код был изменен, без этого флага вы должны перезапустить сервер вручную
Чтобы запустить adonis локально с другим env
файлом (например, .env.dev
), вы можете запустить такой скрипт
ENV_PATH=.env.dev adonis serve --dev
Перед развертыванием в Heroku мы внесем несколько изменений. В будущем вы поймете «почему»:
Добавьте пакеты mysql
и url-parse
npm в package.json
npm i mysql url-parse
Добавьте новые строки в /config/database.js
файл:
//insert those line after Helpres const Url = use('url-parse') const CLEARDB_DATABASE_URL = new Url(Env.get('CLEARDB_DATABASE_URL')) // and before module.exports {
Измените конфигурацию MySQL в /config/database.js
файле:
mysql: { client: 'mysql', connection: { host: Env.get('DB_HOST', CLEARDB_DATABASE_URL.host), port: Env.get('DB_PORT', ''), user: Env.get('DB_USER', CLEARDB_DATABASE_URL.username), password: Env.get('DB_PASSWORD', CLEARDB_DATABASE_URL.password), database: Env.get('DB_DATABASE', CLEARDB_DATABASE_URL.pathname.substr(1)) } },
Надеюсь, вы знаете, как развернуть свой проект в git, поэтому я пропущу эту часть и буду считать, что вы это уже сделали.
2. Настройка приложения Heroku.
Создать новое приложение
Я предпочитаю выбирать автоматическое развертывание из ветки git master
, но вы можете выбрать любой способ, который вам нравится.
После подключения вашего репозитория к приложению AdonisJs добавьте надстройку ClearDB MySQL в свое приложение Heroku.
На всякий случай - › дополнительная документация
После добавления надстройки ClearDB вы увидите новую добавленную переменную env CLEARDB_DATABASE_URL
в вашем приложении Heroku. Как вы помните, я добавил строки кода, используя эту переменную env.
Затем добавьте переменные среды DB_CONNECTION
и APP_KEY
в настройки приложения Heroku. Значение APP_KEY
вы можете найти в файле .env вашего созданного проекта AdonisJs.
Если вы не добавите APP_KEY, вы можете получить сообщение об ошибке:
RuntimeException: E_MISSING_ENV_KEY: Make sure to define environment variable APP_KEY. App key is a randomly generated 16 or 32 characters long string required to encrypted cookies, sessions and other sensitive data.
Рекомендуется добавить еще две базы данных ClearDB в ваши надстройки Heroku, чтобы у вас было три среды:
- Производство с конфигурацией в
.env
файле; - Staging с конфигурацией в
.env.dev
файле; - Тестирование с конфигурацией в
.env.testing
файле;
3. Добавление новых маршрутов
Добавить новые строки в /start/routes.js
файл.
Возврат простого текстового сообщения
Route.get('/simple-text', () => 'Hello Adonis')
В ответ на {heroku-host}/simple-text
url вы получите текстовое сообщение «Hello Adonis». Просто не правда ли?
Возврат html-страницы
Route.on('/html-page').render('custom')
custom
- это имя файла/resources/views/custom.edge
(который представляет собой простой HTML-код с динамическими включениями, как вwelcom.edge
файле. Подробнее о файлах * .edge вы можете прочитать здесь
Динамические маршруты
Обязательные параметры
Route.get('dynamic/:id', ({ params }) => {
return `Post ${params.id}`
})
Необязательные параметры
Route.get('dynamic/:drink?', ({ params }) => { // use Coffee as fallback when drink is not defined const drink = params.drink || 'Coffee'
return `One ${drink}, coming right up!` })
Узнать больше - › Маршрутная документация
4. Добавление данных в базу данных
Добавить новые строки кода в /routes.js
// Database const Database = use('Database') Route.get('users/:username', async ({ params }) => { const userId = await Database .table('users') .insert({ username: params.username }) return userId }).formats(['json'])
Если вы попытаетесь получить /users/custom-name
, вы получите сообщение об ошибке
Error: insert into `users` (`username`) values ('custom-name') - ER_NO_DEFAULT_FOR_FIELD: Field 'email' doesn't have a default value
Ты знаешь почему?
Потому что мы забыли внести изменения в файл схемы, который был создан после adonis new projectname
скрипта.
class UserSchema extends Schema { up () { this.create('users', table => { table.increments() table.string('username', 80).notNullable().unique() table.string('email', 254).notNullable().unique() table.string('password', 60).notNullable() table.timestamps() }) } down () { this.drop('users') } }
Потому что после того, как мы добрались до /users/some-name
, наша определенная схема UserSchema
была создана в базе данных Heroku.
Не сердись 😠. Это было сделано намеренно, чтобы мы могли найти подходы к решению таких проблем :) 😉
Итак, нам нужно как-то исправить нашу схему в базе данных (внести изменения в обязательные поля email
и password
, которые в нашем случае не требуются)
В этом случае мы будем использовать миграции.
Из официальной документации:
Миграции - это мутации вашей базы данных по мере развития вашего приложения. Думайте о них как о пошаговом снимке экрана вашей схемы базы данных, который вы можете откатить в любой момент времени.
Кроме того, миграции упрощают работу в команде, где изменения базы данных от одного разработчика легко обнаруживаются и используются другими разработчиками в команде.
5. Миграции
Создание схемы
Для создания миграций вы можете использовать этот скрипт:
adonis make:migration users > Create table // for creating new table Select table // for altering old table
adonis migration:run // executes up() function defined in Schema
«Создать таблицу» и «Выбрать таблицу» имеют одно небольшое различие, и не более того:
Вы можете добавить переменную
NODE_ENV
перед сценарием (илиENV_PATH=/user/.env
) для выполнения миграции в разных базах данных. Если вы установите для переменнойNODE_ENV
значениеtesting
, AdonisJs попытается загрузить.env.testing
файл из корня вашего приложения.
Например:
NODE_ENV=testing adonis migration:run
Прежде чем вносить изменения в «производственную» базу данных, было бы хорошо протестировать будущие миграции, чтобы они работали должным образом.
Я не рекомендую использовать
sqlite
для тестирования миграции, поскольку, как говорится в официальной документации KnexJS, о Alter () функция:
Это работает только в .alterTable () и не поддерживается SQlite или Amazon Redshift.
Мы создадим ту же версию нашей «производственной» базы данных ClearDB Heroku, но для «разработки».
Не забудьте скопировать переменную базы данных среды Heroku в .env.testing
после создания новой «разработки» базы данных. (Имя переменной Env может отличаться от моего)
Но сначала проверим статус тестирования миграции, на всякий случай:
NODE_ENV=testing adonis migration:status
Мы увидим что-то вроде этого:
Давайте создадим нашу тестовую базу данных:
$ NODE_ENV=testing adonis migration:run
Если все прошло успешно, вы увидите сообщение Database migrated successfully
Если вы хотите удалить свою базу данных, запустите
NODE_ENV=testing adonis migration:reset
илиNODE_ENV=testing adonis migration:rollback
, если это было только одно действие
У нас есть два варианта: удалить столбец «электронная почта» из схемы или сделать его not required
.
Первый вариант:
NODE_ENV=testing adonis make:migration user > Select table
… С обновлением создано /database/migrations/***_user_schema.js file
class UserSchema extends Schema { up () { this.table('users', table => { table.dropColumn('email') }) } }
или второй вариант:
NODE_ENV=testing adonis make:migration user > Select table
а также
class UserSchema extends Schema { up () { this.table('users', table => { table.string('email', 254).nullable().alter() }) } down () { this.table('users', table => { table.string('email', 254).notNullable().alter() }) } }
Если честно, мне нравится второй. Так что давай сделаем это.
NODE_ENV=testing adonis migration:run
И результат… База данных успешно перенесена
Рекомендация. При создании миграции всегда добавляйте логику для функции down (), чтобы ваш перенос работал и по-другому - ›когда вы выполняете
migration:run
иmigration:rollback
Производство
Попробуем на «производственной» базе данных
adonis migration:run --force
И результат… База данных успешно перенесена!
Если вы хотите видеть запросы до того, как они будут выполнены, вы можете запустить
adonis migration:reset --force --log
Результат будет:
Queries for ***_user_schema.js alter table `users` modify `email` varchar(254) not null Queries for ***_token.js drop table `tokens` Queries for ***_user.js drop table `users`
Прежде чем вносить изменения в Heroku, давайте внесем одно изменение в наши маршруты.
6. Контроллеры
Замечательно, что он может легко добавить функцию обратного вызова с логикой в наш routes.js
вот так:
Route.get('users/:user/:password/:email', async ({ params }) => { const userId = await Database .table('users') .insert({ username: params.username, password: params.password, email: params.email, }); const user = await User.find(userId) Database.close(['mysql']) return user }).formats(['json'])
! Напоминание: НЕ используйте методы get для передачи личной информации пользователя. Это всего лишь пример.
Но с ростом нашего приложения файл /start/routes.js
мог стать огромным.
Чтобы предотвратить эту ситуацию в будущем, AdodisJs предоставляет полезные функции, называемые Контроллеры привязки, поэтому наши маршруты могут быть максимально простыми, например:
Route.get('users', 'UserController.index')
index
- это просто имя функции вUserController
, которая должна выполняться, когда пользователь пытается достичь конечной точки (несколько примеров имен:store
,show
,edit
,update
,destroy
). Чтобы узнать о других функциях контроллера, вы можете прочитать о них в документации.
class UserController { index () { return 'Some custom response' } }
Имя функции может быть любым, предпочтительны только условные обозначения.
Давайте создадим контроллер для пользователей:
adonis make:controller Users
Select controller type
❯ For HTTP requests
For Websocket channel
Будет создан новый app/Controllers/Http/UserController.js
файл:
Итак, как будет выглядеть функция обратного вызова в UserController
?
class UserController { customName ({ request }) { return { host: request.header('host'), url: request.url(), originalUrl: request.originalUrl(), method: request.method(), intended: request.intended(), ip: request.ip(), subdomains: request.subdomains(), 'user-agent': request.header('user-agent'), accept: request.header('accept'), hello: request.header('hello'), isAjax: request.ajax(), hostname: request.hostname(), protocol: request.protocol() } } ...
и в нашем /start/routes.js
файле
Route.get('request', 'UserController.customName').formats(['json'])
Теперь посмотрим на результат в Insomnia (или Почтальоне, если хотите).
7. Ясные модели
После того, как мы выясним, как это работает, давайте добавим несколько функций CRUD в наш UserController
, используя Lucid Inserts and Updates.
Lucid - это реализация паттерна Active Record в Javascript. Если вы пришли из мира Laravel или Rails, возможно, вы знакомы с ним.
«Официальная документация»
Давайте обновим наш UserController
новыми строками кода:
const User = use('App/Models/User') class UserController { async index () { return await User.all() } async show ({ params }) { return await User.find(params.id) } async store () {} async update () {} async destroy () {} }
/routes.js
:
Route.get('users', 'UserController.index').formats(['json']) Route.get('users/:id', 'UserController.show').formats(['json'])
Результат получения Все пользователи и Пользователь по идентификатору:
Если получится пустой объект - ›не волнуйтесь. Это потому, что вы не можете добавлять пользователей в свою базу данных. Все работает, если вы просто получите статус ответа 200 «ОК».
Теперь давайте добавим модификацию операций с базой данных:
class UserController { ... async store ({ request }) { const { username, password, email } = request.post() const user = new User() user.username = username user.password = password user.email = email await user.save() } async update ({ request, params }) { const user = await User.find(params.id) const { username, password, email } = request.post() user.username = username user.password = password user.email = email await user.save() } async destroy ({ params }) { const user = await User.find(params.id) await user.delete() } }
и в routes.js
Route.post('users', 'UserController.store').formats(['json']) Route.patch('users/:id', 'UserController.update').formats(['json']) Route.delete('users/:id', 'UserController.destroy').formats(['json'])
8. CSRF
Если вы попытаетесь достичь одной из трех ранее добавленных конечных точек и попытаетесь изменить базу данных, вы получите сообщение об ошибке:
403:Forbidden “HttpException: EBADCSRFTOKEN: Invalid CSRF token”
Это из-за части CSRF (подделка межсайтовых запросов).
ИЗ DOC: позволяет злоумышленнику выполнять действия от имени другого пользователя без его ведома или разрешения.
Если вы просто хотите отключить эту функцию, вы можете перейти к /config/shield.js
файлу в своем проекте и изменить csrf.enable
на false
. (не рекомендуется, если вы используете не только конечные точки API)
Как настроить CSRF мы увидим в следующий раз. Теперь давайте посмотрим, как наши функции будут работать после отключения функции CSRF:
Отличная работа!
Но прежде чем мы поговорим о CSRF, давайте немного оптимизируем наши маршруты. Вы можете объединить все 5 пользовательских маршрутов CRUD в один с одинаковым результатом:
Route
.resource('users', 'UserController')
.apiOnly()
И даже сгруппируйте наши маршруты API с добавлением префикса:
Route.group(() => { Route.get('request', 'CustomController.someCustom') .formats(['json']) Route .resource('users', 'UserController') .apiOnly() }).prefix('api/v1/')
А теперь мы можем подробнее узнать о CSRF.
ИЗ DOC: AdonisJs защищает ваше приложение от атак CSRF, отклоняя неопознанные запросы. HTTP-запросы с методами POST, PUT и DELETE проверяются, чтобы убедиться, что эти запросы вызывают нужные люди из нужного места.
Промежуточное ПО Shield полагается на сеансы, поэтому убедитесь, что они настроены правильно.
Хорошая статья на эту тему: Следует ли использовать защиту CSRF на конечных точках Rest API?
Итак, как это «исправить»? Поскольку сейчас мы будем использовать наше приложение в качестве API, мы можем добавить filterUrls в конфигурацию /config/shield.js
.
csrf: { enable: true, methods: ['POST', 'PUT', 'DELETE'], filterUris: ['/api/(.*)'], cookieOptions: { httpOnly: false, sameSite: true, path: '/', maxAge: 7200 } }
Библиотека
@adonisjs/shield
с файлом/config/shield.js
будет НЕ включена, если вы создадите приложение с флагом—-api-only
. Поэтому, если вы не заинтересованы в использовании файлов * .edge в качестве ответов на url-ы, вы можете отключить csrf.
9. Безопасность
Я думаю, что наше приложение выросло достаточно, чтобы добавить часть безопасности для аутентификации пользователей.
Провайдер аутентификации AdonisJs поставляется с предустановленными шаблонами fullstack
и api
.
По умолчанию AdonisJs использует session
для авторизации, давайте изменим его на JWT в /config/auth.js
файле:
authenticator: 'jwt', jwt: { algorithm: 'HS256', // by default serializer: 'lucid', model: 'App/Models/User', scheme: 'jwt', uid: 'email', password: 'password', options: { secret: Env.get('APP_KEY'), expiresIn: "1m", // 1 minute }, },
Затем измените наш /start/routes.js
файл. Добавьте новый маршрут для /login
пути и новую функцию цепочки middleware(‘auth’)
к нашему старому /users
маршруту:
// We don't want our logged-in user to access this Route. post('login', 'AuthController.login'). middleware('guest'); Route. resource('users', 'UserController'). apiOnly(). middleware('auth');
Создать новый AuthController
:
adonis make:controller Auth
Как вы заметили, мы также должны добавить функцию входа в AuthController
:
async login ({ auth, request }) { const { email, password } = request.post(); return auth.withRefreshToken(). attempt(email, password); }
Давай проверим. Во-первых, давайте проверим, что теперь мы не сможем получить всех пользователей из нашей базы данных:
И наш login
:
Теперь нам нужно найти email
и password
некоторых пользователей, с которыми нужно войти. В моем случае это пользователь с адресом электронной почты [email protected]
и паролем 123
.
Как видно на скриншоте, password
ранее хранился в базе данных со значением hashed
. Это было сделано промежуточным программным обеспечением AdonisJs в файле /app/models/User.js
.
/** * A hook to hash the user password before saving * it to the database. */ this.addHook('beforeSave', async (userInstance) => { if (userInstance.dirty.password) { userInstance.password = await Hash.make(userInstance.password); } });
Поэтому, когда пользователь пытается аутентифицировать AdonisJs, хеши передают пароль и сравнивают его с хешированным паролем базы данных.
Итак, попробуем авторизоваться:
Я бы предпочел не копировать-вставить значения token
и refreshToken
в каждый запрос, где требуется авторизация каждый раз, когда я вхожу в систему, поэтому давайте воспользуемся некоторыми помощниками Insomnia:
И нет у нас двух вариантов установки значения токена для других запросов.
Во-первых, это установить token
переменную env непосредственно в заголовки:
Во-вторых, использовать пункт меню Insomnia Auth, который будет делать то же самое:
Если вы добавите token
переменную env в запрос Login
, вы получите сообщение об ошибке:
Как вы помните, мы добавили middleware([‘guest’])
в login
Route, что означает, что маршрут вернет ошибку, если пользователь уже вошел в систему.
Давайте протестируем нашу функцию авторизации с GET_ALL_USERS
конечной точкой:
И поскольку мы ранее установили этот токен JWT expiresIn: 1m
, если мы подождем одну минуту, а затем попытаемся получить всех пользователей, мы получим:
Давайте обновим функцию входа в AuthController
для обновления функций токена:
async login ({ auth, request }) { const { refreshToken, email, password } = request.post(); if (refreshToken) { return await auth. generateForRefreshToken(refreshToken); } return auth.withRefreshToken(). attempt(email, password); }
И добавьте refreshToken
в переменные среды Insomnia, как мы это делали ранее с token
. После этого проверим результаты нашей работы:
Отличная работа!
10. Проверка данных
Для начала нам нужно добавить в наш проект @adonisjs/validator
библиотеку:
adonis install @adonisjs/validator
Затем зарегистрируйте валидатор в файле /start/app.js
:
const providers = [
...
'@adonisjs/validator/providers/ValidatorProvider'
]
А после этого давайте воспользуемся некоторыми проверками в нашем store()
методе UserController
:
class UserController { async store({ request }) { const rules = { email: 'required|email|unique:users,email', password: 'required', }; const validation = await validate(request.post(), rules); if (validation.fails()) { return validation.messages(); } ... }
Давайте проверим это на создании нового User
с неправильными email
данными:
Проверка работает должным образом, но сообщения об ошибках… могли бы быть лучше. Мы можем корректировать сообщения об ошибках по типам проверки, в нашем случае по: required
, email
и unique:
const messages = { 'email.required': 'Email is not present in request', 'email.email': 'Enter a valid email address.', 'email.unique': 'Email is already present in db', };
И передайте messages
в качестве третьего аргумента функции validate()
:
const validation = await validate(request.post(), rules, messages);
Функция Validate () останавливается при первой ошибке и возвращает ее. Но если мы хотим проверить все поля, которые мы передали, validateAll () здесь для спасения:
const { validateAll } = use('Validator'); ... const validation = await validateAll(request.post(), rules, messages);
Выглядит отлично, но в большинстве случаев проверки повторяются. Чтобы предотвратить это, мы можем создать новый Validator
и использовать его как промежуточное ПО. Создадим один:
adonis make:validator StoreUser
Давайте заполним этот валидатор /app/Validators/StoreUser.js
несколькими функциями:
class StoreUser { // use validateAll function instead of validate get validateAll () { return true; } get rules () { return { email: 'required|email|unique:users,email', password: 'required', }; } get messages () { return { 'email.required': 'Email is not present in request', 'email.email': 'Enter a valid email address.', 'email.unique': 'Email is already present in db', }; } get sanitizationRules () { return { email: 'normalize_email', }; } async fails (errorMessages) { return this.ctx.response.send(errorMessages); } }
Затем обновите User
Route для использования StoreUser
валидатора только в store
функции в UserController
:
Route .resource('users', 'UserController') .middleware('auth') .validator(new Map([ [['users.store'], ['StoreUser']], ])) .apiOnly();
После этого вы можете удалить все ранее добавленные функции проверки в store
функции UserController
:
async store ({ request }) { const { username, password, email } = request.post(); const user = new User(); user.username = username; user.password = password; user.email = email; await user.save(); }
И результат будет таким же. Круто, не правда ли?
Несколько слов о функции sanitizationRules
в валидаторе StoreUser
. Если пользователь передаст email
в «странном» формате, таком как этот [email protected]
, он будет «дезинфицирован» на более «правильный» способ, например: [email protected].
По мере роста нашего приложения, когда наше приложение становится все более и более сложным, больше шансов что-то сломать. Для предотвращения таких событий было бы здорово добавить какой-нибудь тест)
11. Тестирование
Для тестирования нашего кода AdonisJs предоставляет @adonisjs/vow
библиотеку
Вначале нам нужно его установить (он НЕ входит в состав строительных лесов)
adonis install @adonisjs/vow
Если скрипт будет выполняться должным образом, vowfile.js
и /test
, в корне вашего приложения будет создана папка с одним тестом:
test('make sure 2 + 2 is 4', async ({ assert }) => { assert.equal(2 + 2, 4); });
После этого нам нужно прописать '@adonisjs/vow/providers/VowProvider'
в файле /start/app.js
:
const aceProviders = [
...
'@adonisjs/vow/providers/VowProvider'
]
Для запуска наших тестов мы будем использовать команду:
adonis test
Если все было настроено правильно, вы увидите сообщение PASSED
после запуска этого сценария.
Теперь давайте добавим несколько тестов для нашего UserController
.
adonis make:test UserController
Unit test
> Functional test
Шаг за шагом:
- Мы создаем пользователя в нашей тестовой базе данных:
const user = await User.create({ username: 'some', email: EMAIL, password: PASSWORD, });
2. Мы авторизуемся этим созданным пользователем:
const login = await client .post('api/v1/login') .send({ email: EMAIL, password: PASSWORD, }) .end(); // You must call "end" to execute HTTP client requests.
3. Мы создаем нового пользователя, используя токен данных ответа в заголовке.
const response = await client .post('api/v1/users') .header('accept', 'application/json') .header('Authorization', `Bearer ${loginResponseJson.token}`) .send({ username: 'someName', email: '[email protected]', password: '123', }) .end();
4. Данные нашей базы данных вернутся после тестов.
trait('DatabaseTransactions');
5. Мы тестируем вызов API, как и в Insomnia.
trait('Test/ApiClient');
Это все! Молодец!
Спасибо за внимание!
Если у вас есть вопросы → с удовольствием отвечу 😉
Связанные истории
Если вам понравился этот рассказ, вы можете также проверить Список всех моих рассказов. Удачного кодирования 🎉