diff --git a/server/.env.test b/server/.env.test index 5d0493d3c..ffb261951 100644 --- a/server/.env.test +++ b/server/.env.test @@ -11,4 +11,6 @@ DB_CLIENT=mysql DB_HOST=127.0.0.1 DB_USER=root DB_PASSWORD=root -DB_NAME=moosher \ No newline at end of file +DB_NAME=moosher + +JWT_SECRET_KEY=ahmedmohamked \ No newline at end of file diff --git a/server/package.json b/server/package.json index c07c7fe52..d6dcd1c73 100644 --- a/server/package.json +++ b/server/package.json @@ -21,12 +21,14 @@ "bookshelf-json-columns": "^2.1.1", "bookshelf-modelbase": "^2.10.4", "bookshelf-paranoia": "^0.13.1", + "csurf": "^1.10.0", "dotenv": "^8.1.0", "errorhandler": "^1.5.1", "express": "^4.17.1", "express-boom": "^3.0.0", "express-oauth-server": "^2.0.0", "express-validator": "^6.2.0", + "helmet": "^3.21.0", "jsonwebtoken": "^8.5.1", "knex": "^0.19.2", "lodash": "^4.17.15", @@ -34,6 +36,7 @@ "moment": "^2.24.0", "mustache": "^3.0.3", "mysql2": "^1.6.5", + "node-cache": "^4.2.1", "nodemailer": "^6.3.0", "nodemon": "^1.19.1" }, diff --git a/server/src/app.js b/server/src/app.js index 9cbc1396d..017f657fa 100644 --- a/server/src/app.js +++ b/server/src/app.js @@ -1,4 +1,5 @@ import express from 'express'; +import helmet from 'helmet'; import boom from 'express-boom'; import '../config'; import routes from '@/http'; @@ -8,6 +9,7 @@ const app = express(); // Express configuration app.set('port', process.env.PORT || 3000); +app.use(helmet()); app.use(boom()); app.use(express.json()); diff --git a/server/src/http/controllers/AccountOpeningBalance.js b/server/src/http/controllers/AccountOpeningBalance.js index cfc65db53..2ec025553 100644 --- a/server/src/http/controllers/AccountOpeningBalance.js +++ b/server/src/http/controllers/AccountOpeningBalance.js @@ -1,9 +1,10 @@ import express from 'express'; import { check, validationResult, oneOf } from 'express-validator'; import { difference } from 'lodash'; +import knex from 'knex'; import asyncMiddleware from '../middleware/asyncMiddleware'; import Account from '@/models/Account'; -// import AccountBalance from '@/models/AccountBalance'; +import '@/models/AccountBalance'; export default { /** @@ -35,6 +36,7 @@ export default { ], async handler(req, res) { const validationErrors = validationResult(req); + // const defaultCurrency = 'USD'; if (!validationErrors.isEmpty()) { return res.boom.badData(null, { @@ -48,11 +50,12 @@ export default { const accountsCollection = await Account.query((query) => { query.select(['id']); query.whereIn('id', accountsIds); - }).fetchAll(); + }).fetchAll({ + withRelated: ['balances'], + }); const accountsStoredIds = accountsCollection.map((account) => account.attributes.id); const notFoundAccountsIds = difference(accountsIds, accountsStoredIds); - const errorReasons = []; if (notFoundAccountsIds.length > 0) { @@ -64,6 +67,29 @@ export default { return res.boom.badData(null, { errors: errorReasons }); } + const storedAccountsBalances = accountsCollection.related('balances'); + + const submitBalancesMap = new Map(accounts.map((account) => [account, account.id])); + const storedBalancesMap = new Map(storedAccountsBalances.map((balance) => [ + balance.attributes, balance.attributes.id, + ])); + + // const updatedStoredBalanced = []; + const notStoredBalances = []; + + accountsIds.forEach((id) => { + if (!storedBalancesMap.get(id)) { + notStoredBalances.push(id); + } + }); + + await knex('accounts_balances').insert([ + ...notStoredBalances.map((id) => { + const account = submitBalancesMap.get(id); + return { ...account }; + }), + ]); + return res.status(200).send(); }, }, diff --git a/server/src/http/controllers/Accounts.js b/server/src/http/controllers/Accounts.js index 45b938e0d..0ce32032a 100644 --- a/server/src/http/controllers/Accounts.js +++ b/server/src/http/controllers/Accounts.js @@ -152,7 +152,6 @@ export default { if (!account) { return res.boom.notFound(); } - return res.status(200).send({ item: { ...account.attributes } }); }, }, diff --git a/server/src/http/controllers/Authentication.js b/server/src/http/controllers/Authentication.js index 8577c4317..3d3c0a86b 100644 --- a/server/src/http/controllers/Authentication.js +++ b/server/src/http/controllers/Authentication.js @@ -4,6 +4,7 @@ import { check, validationResult } from 'express-validator'; import path from 'path'; import fs from 'fs'; import Mustache from 'mustache'; +import jwt from 'jsonwebtoken'; import User from '@/models/User'; import asyncMiddleware from '../middleware/asyncMiddleware'; import PasswordReset from '@/models/PasswordReset'; @@ -49,6 +50,7 @@ export default { }); } const { crediential, password } = req.body; + const { JWT_SECRET_KEY } = process.env; const user = await User.query({ where: { email: crediential }, @@ -70,8 +72,15 @@ export default { errors: [{ type: 'USER_INACTIVE', code: 120 }], }); } - user.save({ alst_login_at: new Date() }); - return res.status(200).send({}); + user.save({ last_login_at: new Date() }); + + const token = jwt.sign({ + email: user.attributes.email, + _id: user.id, + }, JWT_SECRET_KEY, { + expiresIn: '1d', + }); + return res.status(200).send({ token }); }, }, diff --git a/server/src/http/controllers/ItemCategories.js b/server/src/http/controllers/ItemCategories.js index c62a55bec..2eb1919e5 100644 --- a/server/src/http/controllers/ItemCategories.js +++ b/server/src/http/controllers/ItemCategories.js @@ -2,7 +2,8 @@ import express from 'express'; import { check, param, validationResult } from 'express-validator'; import asyncMiddleware from '../middleware/asyncMiddleware'; import ItemCategory from '@/models/ItemCategory'; -// import JWTAuth from '@/http/middleware/jwtAuth'; +import Authorization from '@/http/middleware/authorization'; +import JWTAuth from '@/http/middleware/jwtAuth'; export default { /** @@ -10,24 +11,32 @@ export default { */ router() { const router = express.Router(); + const permit = Authorization('items_categories'); + + router.use(JWTAuth); router.post('/:id', + permit('create', 'edit'), this.editCategory.validation, asyncMiddleware(this.editCategory.handler)); router.post('/', + permit('create'), this.newCategory.validation, asyncMiddleware(this.newCategory.handler)); router.delete('/:id', + permit('create', 'edit', 'delete'), this.deleteItem.validation, asyncMiddleware(this.deleteItem.handler)); - // router.get('/:id', - // this.getCategory.validation, - // asyncMiddleware(this.getCategory.handler)); + router.get('/:id', + permit('view'), + this.getCategory.validation, + asyncMiddleware(this.getCategory.handler)); router.get('/', + permit('view'), this.getList.validation, asyncMiddleware(this.getList.validation)); @@ -152,6 +161,9 @@ export default { }, }, + /** + * Retrieve details of the given category. + */ getCategory: { validation: [ param('category_id').toInt(), diff --git a/server/src/http/controllers/Items.js b/server/src/http/controllers/Items.js index a634ff4ed..5a201be14 100644 --- a/server/src/http/controllers/Items.js +++ b/server/src/http/controllers/Items.js @@ -1,21 +1,30 @@ import express from 'express'; import { check, validationResult } from 'express-validator'; import moment from 'moment'; -import asyncMiddleware from '../middleware/asyncMiddleware'; +import { difference } from 'lodash'; +import asyncMiddleware from '@/http/middleware/asyncMiddleware'; +import jwtAuth from '@/http/middleware/jwtAuth'; import Item from '@/models/Item'; import Account from '@/models/Account'; import ItemCategory from '@/models/ItemCategory'; +import Resource from '@/models/Resource'; +import ResourceField from '@/models/ResourceField'; +import Authorization from '@/http/middleware/authorization'; export default { router() { const router = express.Router(); + const permit = Authorization('items'); + + router.use(jwtAuth); // router.post('/:id', // this.editItem.validation, // asyncMiddleware(this.editCategory.handler)); router.post('/', + permit('create'), this.newItem.validation, asyncMiddleware(this.newItem.handler)); @@ -46,6 +55,11 @@ export default { check('cost_account_id').exists().isInt(), check('sell_account_id').exists().isInt(), check('category_id').optional().isInt(), + + check('custom_fields').isArray({ min: 1 }), + check('custom_fields.*.key').exists().isNumeric().toInt(), + check('custom_fields.*.value').exists(), + check('note').optional(), ], async handler(req, res) { @@ -58,19 +72,38 @@ export default { } const { sell_account_id: sellAccountId, cost_account_id: costAccountId } = req.body; - const { category_id: categoryId } = req.body; + const { category_id: categoryId, custom_fields: customFields } = req.body; + const errorReasons = []; const costAccountPromise = Account.where('id', costAccountId).fetch(); const sellAccountPromise = Account.where('id', sellAccountId).fetch(); const itemCategoryPromise = (categoryId) ? ItemCategory.where('id', categoryId).fetch() : null; + // Validate the custom fields key and value type. + if (customFields.length > 0) { + const customFieldsKeys = customFields.map((field) => field.key); + + // Get resource id than get all resource fields. + const resource = await Resource.where('name', 'items').fetch(); + const fields = await ResourceField.query((query) => { + query.where('resource_id', resource.id); + query.whereIn('key', customFieldsKeys); + }).fetchAll(); + + const storedFieldsKey = fields.map((f) => f.attributes.key); + + // Get all not defined resource fields. + const notFoundFields = difference(customFieldsKeys, storedFieldsKey); + + if (notFoundFields.length > 0) { + errorReasons.push({ type: 'FIELD_KEY_NOT_FOUND', code: 150, fields: notFoundFields }); + } + } + const [costAccount, sellAccount, itemCategory] = await Promise.all([ costAccountPromise, sellAccountPromise, itemCategoryPromise, ]); - - const errorReasons = []; - if (!costAccount) { errorReasons.push({ type: 'COST_ACCOUNT_NOT_FOUND', code: 100 }); } @@ -92,7 +125,6 @@ export default { currency_code: req.body.currency_code, note: req.body.note, }); - await item.save(); return res.status(200).send(); diff --git a/server/src/http/controllers/Users.js b/server/src/http/controllers/Users.js index 011ad79fc..7a843ccd3 100644 --- a/server/src/http/controllers/Users.js +++ b/server/src/http/controllers/Users.js @@ -2,6 +2,8 @@ import express from 'express'; import { check, validationResult } from 'express-validator'; import User from '@/models/User'; import asyncMiddleware from '@/http/middleware/asyncMiddleware'; +import jwtAuth from '@/http/middleware/jwtAuth'; +import Authorization from '@/http/middleware/authorization'; export default { @@ -10,24 +12,32 @@ export default { */ router() { const router = express.Router(); + const permit = Authorization('users'); + + router.use(jwtAuth); router.post('/', + permit('create'), this.newUser.validation, asyncMiddleware(this.newUser.handler)); router.post('/:id', + permit('create', 'edit'), this.editUser.validation, asyncMiddleware(this.editUser.handler)); - // router.get('/', - // this.listUsers.validation, - // asyncMiddleware(this.listUsers.handler)); + router.get('/', + permit('view'), + this.listUsers.validation, + asyncMiddleware(this.listUsers.handler)); - // router.get('/:id', - // this.getUser.validation, - // asyncMiddleware(this.getUser.handler)); + router.get('/:id', + permit('view'), + this.getUser.validation, + asyncMiddleware(this.getUser.handler)); router.delete('/:id', + permit('create', 'edit', 'delete'), this.deleteUser.validation, asyncMiddleware(this.deleteUser.handler)); diff --git a/server/src/http/middleware/authorization.js b/server/src/http/middleware/authorization.js index 3caf12897..6d1acb7bc 100644 --- a/server/src/http/middleware/authorization.js +++ b/server/src/http/middleware/authorization.js @@ -1,4 +1,16 @@ - -const authorization = (req, res, next) => { +/* eslint-disable consistent-return */ +const authorization = (resourceName) => (...permissions) => (req, res, next) => { const { user } = req; -}; \ No newline at end of file + const onError = () => { + res.boom.unauthorized(); + }; + user.hasPermissions(resourceName, permissions) + .then((authorized) => { + if (!authorized) { + return onError(); + } + next(); + }).catch(onError); +}; + +export default authorization; diff --git a/server/src/http/middleware/jwtAuth.js b/server/src/http/middleware/jwtAuth.js index c837b1804..9437b3c67 100644 --- a/server/src/http/middleware/jwtAuth.js +++ b/server/src/http/middleware/jwtAuth.js @@ -1,23 +1,23 @@ /* eslint-disable consistent-return */ import jwt from 'jsonwebtoken'; import User from '@/models/User'; -import Auth from '@/models/Auth'; +// import Auth from '@/models/Auth'; const authMiddleware = (req, res, next) => { + const { JWT_SECRET_KEY } = process.env; const token = req.headers['x-access-token'] || req.query.token; const onError = () => { - Auth.loggedOut(); + // Auth.loggedOut(); res.status(401).send({ success: false, message: 'unauthorized', }); - } + }; if (!token) { return onError(); } - const { JWT_SECRET_KEY } = process.env; const verify = new Promise((resolve, reject) => { jwt.verify(token, JWT_SECRET_KEY, async (error, decoded) => { @@ -26,7 +26,7 @@ const authMiddleware = (req, res, next) => { } else { // eslint-disable-next-line no-underscore-dangle req.user = await User.where('id', decoded._id).fetch(); - Auth.setAuthenticatedUser(req.user); + // Auth.setAuthenticatedUser(req.user); if (!req.user) { return onError(); diff --git a/server/src/models/Account.js b/server/src/models/Account.js index 418e8deec..2dd3c66a8 100644 --- a/server/src/models/Account.js +++ b/server/src/models/Account.js @@ -18,8 +18,11 @@ const Account = bookshelf.Model.extend({ return this.belongsTo('AccountType', 'account_type_id'); }, + /** + * Account model may has many balances accounts. + */ balances() { - return this.hasMany('AccountBalance', 'accounnt_id'); + return this.hasMany('AccountBalance', 'account_id'); }, }, { /** diff --git a/server/src/models/Item.js b/server/src/models/Item.js index a3918a08b..b7b6765d6 100644 --- a/server/src/models/Item.js +++ b/server/src/models/Item.js @@ -1,7 +1,6 @@ import bookshelf from './bookshelf'; const Item = bookshelf.Model.extend({ - /** * Table name */ @@ -25,6 +24,11 @@ const Item = bookshelf.Model.extend({ category() { return this.belongsTo('ItemCategory', 'category_id'); }, +}, { + /** + * Cascade delete dependents. + */ + dependents: ['ItemMetadata'], }); export default bookshelf.model('Item', Item); diff --git a/server/src/models/Resource.js b/server/src/models/Resource.js index cc4a48284..9b0dd9090 100644 --- a/server/src/models/Resource.js +++ b/server/src/models/Resource.js @@ -24,6 +24,10 @@ const Resource = bookshelf.Model.extend({ fields() { return this.hasMany('ResourceField', 'resource_id'); }, + + permissions() { + return this.belongsToMany('Permission', 'role_has_permissions', 'resource_id', 'permission_id'); + }, }); export default bookshelf.model('Resource', Resource); diff --git a/server/src/models/User.js b/server/src/models/User.js index 575bda159..0a9cbe996 100644 --- a/server/src/models/User.js +++ b/server/src/models/User.js @@ -1,7 +1,9 @@ import bcrypt from 'bcryptjs'; import bookshelf from './bookshelf'; +import PermissionsService from '@/services/PermissionsService'; const User = bookshelf.Model.extend({ + ...PermissionsService, /** * Table name @@ -13,6 +15,10 @@ const User = bookshelf.Model.extend({ */ hasTimestamps: ['created_at', 'updated_at'], + initialize() { + this.initializeCache(); + }, + /** * Verify the password of the user. * @param {String} password - The given password. diff --git a/server/src/services/PermissionsService.js b/server/src/services/PermissionsService.js new file mode 100644 index 000000000..7a46500cc --- /dev/null +++ b/server/src/services/PermissionsService.js @@ -0,0 +1,75 @@ +import cache from 'memory-cache'; +import { difference } from 'lodash'; +import Role from '@/models/Role'; + +export default { + + cacheKey: 'ratteb.cache,', + cacheExpirationTime: null, + permissions: [], + cache: null, + + initializeCache() { + if (!this.cache) { + this.cache = new cache.Cache(); + } + }, + + /** + * Purge all cached permissions. + */ + forgetCachePermissions() { + this.cache.del(this.cacheKey); + this.permissions = []; + }, + + /** + * Get all stored permissions. + */ + async getPermissions() { + if (this.permissions.length <= 0) { + const cachedPerms = this.cache.get(this.cacheKey); + + if (!cachedPerms) { + this.permissions = await this.getPermissionsFromStorage(); + this.cache.put(this.cacheKey, this.permissions); + } else { + this.permissions = cachedPerms; + } + } + return this.permissions; + }, + + /** + * Fetches all roles and permissions from the storage. + */ + async getPermissionsFromStorage() { + const roles = await Role.fetchAll({ + withRelated: ['resources.permissions'], + }); + return roles.toJSON(); + }, + + /** + * Detarmine the given resource has the permissions. + * @param {String} resource - + * @param {Array} permissions - + */ + async hasPermissions(resource, permissions) { + await this.getPermissions(); + + const userRoles = this.permissions.filter((role) => role.id === this.id); + const perms = []; + + userRoles.forEach((role) => { + const roleResources = role.resources || []; + const foundResource = roleResources.find((r) => r.name === resource); + + if (foundResource && foundResource.permissions) { + foundResource.permissions.forEach((p) => perms.push(p.name)); + } + }); + const notAllowedPerms = difference(permissions, perms); + return (notAllowedPerms.length <= 0); + }, +}; diff --git a/server/tests/models/User.test.js b/server/tests/models/User.test.js index cb80e09e5..e8593defa 100644 --- a/server/tests/models/User.test.js +++ b/server/tests/models/User.test.js @@ -3,13 +3,44 @@ import User from '@/models/User'; import '@/models/Role'; describe('Model: User', () => { - it('User model may has many associated roles.', async () => { - const userHasRole = await create('user_has_role'); - await create('user_has_role', { user_id: userHasRole.user_id }); + describe('relations', () => { + it('User model may has many associated roles.', async () => { + const userHasRole = await create('user_has_role'); + await create('user_has_role', { user_id: userHasRole.user_id }); - const userModel = await User.where('id', userHasRole.user_id).fetch(); - const userRoles = await userModel.roles().fetch(); + const userModel = await User.where('id', userHasRole.user_id).fetch(); + const userRoles = await userModel.roles().fetch(); - expect(userRoles).to.have.lengthOf(2); + expect(userRoles).to.have.lengthOf(2); + }); + }); + + describe('hasPermissions', () => { + it('Should return true in case user has the given permissions.', async () => { + const resource = await create('resource'); + const permission = await create('permission'); + const roleHasPerms = await create('role_has_permission', { + resource_id: resource.id, + permission_id: permission.id, + }); + const userHasRole = await create('user_has_role', { role_id: roleHasPerms.role_id }); + await create('user_has_role', { user_id: userHasRole.user_id }); + + const userModel = await User.where('id', userHasRole.user_id).fetch(); + const hasPermission = await userModel.hasPermissions(resource.name, [permission.name]); + + expect(hasPermission).to.equals(true); + }); + + it('Should return false in case user has no the given permissions.', async () => { + const roleHasPerms = await create('role_has_permission'); + const userHasRole = await create('user_has_role', { role_id: roleHasPerms.role_id }); + await create('user_has_role', { user_id: userHasRole.user_id }); + + const userModel = await User.where('id', userHasRole.user_id).fetch(); + const hasPermission = await userModel.hasPermissions('resource', ['permission']); + + expect(hasPermission).to.equals(false); + }); }); }); diff --git a/server/tests/routes/accountOpeningBalance.test.js b/server/tests/routes/accountOpeningBalance.test.js index 13eb004ab..2dddea0d2 100644 --- a/server/tests/routes/accountOpeningBalance.test.js +++ b/server/tests/routes/accountOpeningBalance.test.js @@ -1,4 +1,4 @@ -import { request, expect } from '~/testInit'; +import { request, expect, create } from '~/testInit'; describe('routes: `/accountOpeningBalance`', () => { describe('POST `/accountOpeningBalance`', () => { @@ -40,5 +40,16 @@ describe('routes: `/accountOpeningBalance`', () => { type: 'NOT_FOUND_ACCOUNT', code: 100, ids: [100], }); }); + + it('Should store the given credit and debit to the account balance in the storage.', async () => { + const account = await create('account'); + const res = await request().post('/api/accountOpeningBalance').send({ + accounts: [ + { id: account.id, credit: 100, debit: 2 }, + ], + }); + + console.log(res.status); + }); }); }); diff --git a/server/tests/routes/auth.test.js b/server/tests/routes/auth.test.js index 734a9492f..ff2488dd2 100644 --- a/server/tests/routes/auth.test.js +++ b/server/tests/routes/auth.test.js @@ -85,14 +85,14 @@ describe('routes: /auth/', () => { }); it('Should autheticate success with correct phone number and password.', async () => { - const password = hashPassword('admin'); + const password = await hashPassword('admin'); const user = await create('user', { phone_number: '0920000000', password, }); const res = await request().post('/api/auth/login').send({ - crediential: user.phone_number, - password, + crediential: user.email, + password: 'admin', }); expect(res.status).equals(200); diff --git a/server/tests/routes/authorization.test.js b/server/tests/routes/authorization.test.js new file mode 100644 index 000000000..3f1eee0ed --- /dev/null +++ b/server/tests/routes/authorization.test.js @@ -0,0 +1,10 @@ + +describe('Authorization', () => { + it('Should response unauthorized in case use has no role has permissions to the given resource.', () => { + + }); + + it('Should response authorized in case user has role has all permissions.', () => { + + }); +}); diff --git a/server/tests/routes/items.test.js b/server/tests/routes/items.test.js index abfd66318..463a662a5 100644 --- a/server/tests/routes/items.test.js +++ b/server/tests/routes/items.test.js @@ -1,10 +1,26 @@ -import { request, expect, create } from '~/testInit'; +import { + request, + expect, + create, + login, +} from '~/testInit'; import knex from '@/database/knex'; -describe('routes: `/items`', () => { - describe('POST: `/items`', () => { +describe.only('routes: `/items`', () => { + describe.only('POST: `/items`', () => { it('Should not create a new item if the user was not authorized.', async () => { + const res = await request().post('/api/items').send(); + expect(res.status).equals(401); + expect(res.body.message).equals('unauthorized'); + }); + + it('Should user have create permission to create a new item.', async () => { + const loginRes = await login(); + const res = await request().post('/api/items') + .set('x-access-token', loginRes.body.token).send(); + + expect(res.status).equals(401); }); it('Should `name` be required.', async () => { diff --git a/server/tests/routes/views.test.js b/server/tests/routes/views.test.js index b58e8ca9b..ffd0a7166 100644 --- a/server/tests/routes/views.test.js +++ b/server/tests/routes/views.test.js @@ -154,7 +154,7 @@ describe('routes: `/views`', () => { }); }); - describe.only('POST: `/views/:view_id`', () => { + describe('POST: `/views/:view_id`', () => { it('Should `label` be required.', async () => { const view = await create('view'); const res = await request().post(`/api/views/${view.id}`); @@ -251,7 +251,7 @@ describe('routes: `/views`', () => { expect(res.status).equals(404); }); - it.only('Should response the roles fields not exist in case role field was not exist.', async () => { + it('Should response the roles fields not exist in case role field was not exist.', async () => { const view = await create('view'); await create('resource_field', { resource_id: view.resource_id, diff --git a/server/tests/testInit.js b/server/tests/testInit.js index 88dfa3212..f1c83b032 100644 --- a/server/tests/testInit.js +++ b/server/tests/testInit.js @@ -4,7 +4,7 @@ import chaiThings from 'chai-things'; import app from '@/app'; import knex from '@/database/knex'; import factory from '@/database/factories'; -import { hashPassword } from '@/utils'; +// import { hashPassword } from '@/utils'; const request = () => chai.request(app); const { expect } = chai; @@ -22,13 +22,13 @@ chai.use(chaiHttp); chai.use(chaiThings); const login = async (givenUser) => { - const user = givenUser === null ? await factory.create('user') : givenUser; + const user = !givenUser ? await factory.create('user') : givenUser; const response = await request() .post('/api/auth/login') .send({ crediential: user.email, - password: hashPassword('secret'), + password: 'admin', }); return response;