mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-20 06:40:31 +00:00
WIP Items module.
This commit is contained in:
6
server/.babelrc
Normal file
6
server/.babelrc
Normal file
@@ -0,0 +1,6 @@
|
||||
|
||||
{
|
||||
"presets": ["@babel/preset-env"],
|
||||
"retainLines": true,
|
||||
"plugins": ["@babel/plugin-transform-runtime"]
|
||||
}
|
||||
14
server/.env
Normal file
14
server/.env
Normal file
@@ -0,0 +1,14 @@
|
||||
MAIL_HOST=
|
||||
MAIL_USERNAME=
|
||||
MAIL_PASSWORD=
|
||||
MAIL_PORT=
|
||||
MAIL_SECURE=false
|
||||
|
||||
MAIL_FROM_ADDRESS=
|
||||
MAIL_FROM_NAME=
|
||||
|
||||
DB_CLIENT=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_USER=root
|
||||
DB_PASSWORD=root
|
||||
DB_NAME=moosher
|
||||
14
server/.env.test
Normal file
14
server/.env.test
Normal file
@@ -0,0 +1,14 @@
|
||||
MAIL_HOST=
|
||||
MAIL_USERNAME=
|
||||
MAIL_PASSWORD=
|
||||
MAIL_PORT=
|
||||
MAIL_SECURE=false
|
||||
|
||||
MAIL_FROM_ADDRESS=
|
||||
MAIL_FROM_NAME=
|
||||
|
||||
DB_CLIENT=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_USER=root
|
||||
DB_PASSWORD=root
|
||||
DB_NAME=moosher
|
||||
41
server/.eslintrc.js
Normal file
41
server/.eslintrc.js
Normal file
@@ -0,0 +1,41 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
browser: false,
|
||||
es6: true,
|
||||
node: true,
|
||||
mocha: true,
|
||||
},
|
||||
extends: [
|
||||
'airbnb-base',
|
||||
],
|
||||
plugins: [
|
||||
'import',
|
||||
],
|
||||
globals: {
|
||||
Atomics: 'readonly',
|
||||
SharedArrayBuffer: 'readonly',
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: 2018,
|
||||
sourceType: 'module',
|
||||
},
|
||||
rules: {
|
||||
"no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
"varsIgnorePattern": "describe|afterEach|beforeEach"
|
||||
}
|
||||
],
|
||||
"import/no-extraneous-dependencies": [
|
||||
"error", {"devDependencies": true}
|
||||
],
|
||||
},
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
node: {},
|
||||
webpack: {
|
||||
config: 'webpack.config.js'
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
6
server/config/index.js
Normal file
6
server/config/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import path from 'path';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config({
|
||||
path: path.resolve(process.cwd(), '.env.test'),
|
||||
});
|
||||
351
server/dist/bundle.js
vendored
Normal file
351
server/dist/bundle.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
server/dist/bundle.js.map
vendored
Normal file
1
server/dist/bundle.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
51
server/knexfile.js
Normal file
51
server/knexfile.js
Normal file
@@ -0,0 +1,51 @@
|
||||
|
||||
const MIGRATIONS_DIR = `./${__dirname}/src/database/migrations`;
|
||||
const SEEDS_DIR = `./${__dirname}/src/database/seeds`;
|
||||
|
||||
module.exports = {
|
||||
test: {
|
||||
client: process.env.DB_CLIENT,
|
||||
migrations: {
|
||||
directory: MIGRATIONS_DIR,
|
||||
},
|
||||
connection: {
|
||||
host: '172.17.0.2',
|
||||
user: 'root',
|
||||
password: 'root',
|
||||
database: 'moosher',
|
||||
charset: 'utf8',
|
||||
},
|
||||
},
|
||||
development: {
|
||||
client: process.env.DB_CLIENT,
|
||||
connection: {
|
||||
host: process.env.DB_HOST,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_NAME,
|
||||
charset: 'utf8',
|
||||
},
|
||||
migrations: {
|
||||
directory: MIGRATIONS_DIR,
|
||||
},
|
||||
seeds: {
|
||||
directory: SEEDS_DIR,
|
||||
},
|
||||
},
|
||||
production: {
|
||||
client: process.env.DB_CLIENT,
|
||||
connection: {
|
||||
host: process.env.DB_HOST,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_NAME,
|
||||
charset: 'utf8',
|
||||
},
|
||||
migrations: {
|
||||
directory: MIGRATIONS_DIR,
|
||||
},
|
||||
seeds: {
|
||||
directory: SEEDS_DIR,
|
||||
},
|
||||
},
|
||||
};
|
||||
7108
server/package-lock.json
generated
7108
server/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,19 +4,61 @@
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"build": "webpack",
|
||||
"start": "npm-run-all --parallel watch:server watch:build",
|
||||
"watch:build": "webpack --watch",
|
||||
"watch:server": "nodemon --inspect=\"9229\" \"./dist/bundle.js\" --watch \"./dist\" ",
|
||||
"test": "cross-env NODE_ENV=test mocha-webpack --webpack-config webpack.config.js \"tests/**/*.test.js\"",
|
||||
"test:watch": "cross-env NODE_ENV=test mocha-webpack --watch --webpack-config webpack.config.js --timeout=30000 tests/**/*.test.js"
|
||||
},
|
||||
"author": "",
|
||||
"author": "Ahmed Bouhuolia, <a.bouhuolia@gmail.com>",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@hapi/boom": "^7.4.3",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"bookshelf": "^0.15.1",
|
||||
"bookshelf-json-columns": "^2.1.1",
|
||||
"bookshelf-modelbase": "^2.10.4",
|
||||
"bookshelf-paranoia": "^0.13.1",
|
||||
"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.1.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"knex": "^0.19.2",
|
||||
"mysql2": "^1.6.5"
|
||||
"moment": "^2.24.0",
|
||||
"mustache": "^3.0.3",
|
||||
"mysql2": "^1.6.5",
|
||||
"nodemailer": "^6.3.0",
|
||||
"nodemon": "^1.19.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.5.5",
|
||||
"@babel/plugin-transform-runtime": "^7.5.5",
|
||||
"@babel/polyfill": "^7.4.4",
|
||||
"@babel/preset-env": "^7.5.5",
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"babel-loader": "^8.0.6",
|
||||
"chai": "^4.2.0",
|
||||
"chai-http": "^4.3.0",
|
||||
"chai-things": "^0.2.0",
|
||||
"cross-env": "^5.2.0",
|
||||
"eslint": "^6.2.1",
|
||||
"eslint-config-airbnb-base": "^14.0.0",
|
||||
"eslint-friendly-formatter": "^4.0.1",
|
||||
"eslint-import-resolver-webpack": "^0.11.1",
|
||||
"eslint-loader": "^2.2.1",
|
||||
"eslint-plugin-import": "^2.18.2",
|
||||
"faker": "^4.1.0",
|
||||
"knex-factory": "0.0.6",
|
||||
"mocha": "^5.2.0",
|
||||
"mocha-webpack": "^2.0.0-beta.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"nyc": "^14.1.1",
|
||||
"webpack": "^4.0.0",
|
||||
"webpack-cli": "^3.3.7",
|
||||
"webpack-node-externals": "^1.7.2"
|
||||
}
|
||||
}
|
||||
|
||||
31
server/scripts/run_test_db.sh
Normal file
31
server/scripts/run_test_db.sh
Normal file
@@ -0,0 +1,31 @@
|
||||
MYSQL_USER="moosher"
|
||||
MYSQL_DATABASE="moosher"
|
||||
MYSQL_CONTAINER_NAME="moosher_test"
|
||||
|
||||
MYSQL_ROOT_PASSWORD="root"
|
||||
MYSQL_PASSWORD="root"
|
||||
|
||||
echo "Start the testing MySql database..."
|
||||
|
||||
docker \
|
||||
run \
|
||||
--detach \
|
||||
--env MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} \
|
||||
--env MYSQL_USER=${MYSQL_USER} \
|
||||
--env MYSQL_PASSWORD=${MYSQL_PASSWORD} \
|
||||
--env MYSQL_DATABASE=${MYSQL_DATABASE} \
|
||||
--name ${MYSQL_CONTAINER_NAME} \
|
||||
--publish 3306:3306 \
|
||||
--tmpfs /var/lib/mysql:rw \
|
||||
mysql:5.7;
|
||||
|
||||
echo "Sleeping for 10 seconds to allow time for the DB to be provisioned:"
|
||||
for i in `seq 1 10`;
|
||||
do
|
||||
echo "."
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "Database '${MYSQL_DATABASE}' running."
|
||||
echo " Username: ${MYSQL_USER}"
|
||||
echo " Password: ${MYSQL_PASSWORD}"
|
||||
@@ -1,21 +0,0 @@
|
||||
import path from 'path';
|
||||
import dotenv from 'dotenv';
|
||||
import errorHandler from 'errorhandler';
|
||||
import app from './http/app';
|
||||
|
||||
dotenv.config({
|
||||
path: path.join(__dirname, '.env')
|
||||
});
|
||||
|
||||
app.use(errorHandler);
|
||||
|
||||
const server = app.listen(app.get('port'), () => {
|
||||
console.log(
|
||||
" App is running at http://localhost:%d in %s mode",
|
||||
app.get("port"),
|
||||
app.get("env")
|
||||
);
|
||||
console.log(" Press CTRL-C to stop\n");
|
||||
});
|
||||
|
||||
export default server;
|
||||
16
server/src/app.js
Normal file
16
server/src/app.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import express from 'express';
|
||||
import boom from 'express-boom';
|
||||
import '../config';
|
||||
import routes from '@/http';
|
||||
|
||||
const app = express();
|
||||
|
||||
// Express configuration
|
||||
app.set('port', process.env.PORT || 3000);
|
||||
|
||||
app.use(boom());
|
||||
app.use(express.json());
|
||||
|
||||
routes(app);
|
||||
|
||||
export default app;
|
||||
58
server/src/database/factories/index.js
Normal file
58
server/src/database/factories/index.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import knexFactory from 'knex-factory';
|
||||
import faker from 'faker';
|
||||
import knex from '@/database/knex';
|
||||
import { hashPassword } from '@/utils';
|
||||
|
||||
const factory = knexFactory(knex);
|
||||
|
||||
factory.define('user', 'users', async () => {
|
||||
const hashedPassword = await hashPassword('admin');
|
||||
|
||||
return {
|
||||
first_name: faker.name.firstName(),
|
||||
last_name: faker.name.lastName(),
|
||||
email: faker.internet.email(),
|
||||
phone_number: faker.phone.phoneNumber(),
|
||||
active: 1,
|
||||
password: hashedPassword,
|
||||
};
|
||||
});
|
||||
|
||||
factory.define('account', 'accounts', async () => ({
|
||||
name: faker.lorem.word(),
|
||||
type: faker.lorem.word(),
|
||||
description: faker.lorem.paragraph(),
|
||||
}));
|
||||
|
||||
factory.define('item_category', 'items_categories', () => ({
|
||||
label: faker.name.firstName(),
|
||||
description: faker.lorem.text(),
|
||||
parent_category_id: null,
|
||||
}));
|
||||
|
||||
factory.define('item_metadata', 'items_metadata', async () => {
|
||||
const item = await factory.create('item');
|
||||
|
||||
return {
|
||||
key: faker.lorem.slug(),
|
||||
value: faker.lorem.word(),
|
||||
item_id: item.id,
|
||||
};
|
||||
});
|
||||
|
||||
factory.define('item', 'items', async () => {
|
||||
const category = await factory.create('item_category');
|
||||
const account = await factory.create('account');
|
||||
|
||||
return {
|
||||
name: faker.lorem.word(),
|
||||
note: faker.lorem.paragraph(),
|
||||
cost_price: faker.random.number(),
|
||||
sell_price: faker.random.number(),
|
||||
cost_account_id: account.id,
|
||||
sell_account_id: account.id,
|
||||
category_id: category.id,
|
||||
};
|
||||
});
|
||||
|
||||
export default factory;
|
||||
7
server/src/database/knex.js
Normal file
7
server/src/database/knex.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import Knex from 'knex';
|
||||
import knexfile from '@/../knexfile';
|
||||
|
||||
const config = knexfile[process.env.NODE_ENV];
|
||||
const knex = Knex(config);
|
||||
|
||||
export default knex;
|
||||
@@ -0,0 +1,10 @@
|
||||
|
||||
exports.up = (knex) => knex.schema.createTable('roles', (table) => {
|
||||
table.increments();
|
||||
table.string('name');
|
||||
table.string('description');
|
||||
table.boolean('predefined').default(false);
|
||||
table.timestamps();
|
||||
});
|
||||
|
||||
exports.down = (knex) => knex.schema.dropTable('roles');
|
||||
@@ -0,0 +1,20 @@
|
||||
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.createTable('users', (table) => {
|
||||
table.increments();
|
||||
table.string('first_name');
|
||||
table.string('last_name');
|
||||
table.string('email').unique();
|
||||
table.string('phone_number').unique();
|
||||
table.string('password');
|
||||
table.boolean('active');
|
||||
table.integer('role_id').unique();
|
||||
table.string('language');
|
||||
table.date('last_login_at');
|
||||
table.timestamps();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.dropTableIfExists('users');
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.createTable('oauth_tokens', table => {
|
||||
table.increments();
|
||||
table.string('access_token');
|
||||
table.date('access_token_expires_on');
|
||||
table.integer('client_id').unsigned();
|
||||
table.string('refresh_token');
|
||||
table.date('refresh_token_expires_on');
|
||||
table.integer('user_id').unsigned().references('id').inTable('users');
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.dropTableIfExists('oauth_tokens');
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.createTable('oauth_clients', table => {
|
||||
table.increments();
|
||||
table.integer('client_id').unsigned();
|
||||
table.string('client_secret');
|
||||
table.string('redirect_uri');
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = (knex) => knex.schema.dropTableIfExists('oauth_clients');
|
||||
@@ -0,0 +1,11 @@
|
||||
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.createTable('settings', table => {
|
||||
table.increments();
|
||||
table.integer('user_id').unsigned().references('id').inTable('users');
|
||||
table.string('key');
|
||||
table.string('value');
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = (knex) => knex.schema.dropTableIfExists('settings');
|
||||
@@ -0,0 +1,20 @@
|
||||
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.createTable('items', (table) => {
|
||||
table.increments();
|
||||
table.string('name');
|
||||
table.integer('type_id').unsigned();
|
||||
table.decimal('cost_price').unsigned();
|
||||
table.decimal('sell_price').unsigned();
|
||||
table.string('currency_code', 3);
|
||||
table.string('picture_uri');
|
||||
table.integer('cost_account_id').unsigned();
|
||||
table.integer('sell_account_id').unsigned();
|
||||
table.text('note').nullable();
|
||||
table.integer('category_id').unsigned();
|
||||
table.integer('user_id').unsigned();
|
||||
table.timestamps();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = (knex) => knex.schema.dropTableIfExists('items');
|
||||
@@ -0,0 +1,14 @@
|
||||
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.createTable('accounts', (table) => {
|
||||
table.increments();
|
||||
table.string('name');
|
||||
table.string('type');
|
||||
table.integer('parent_account_id');
|
||||
table.string('code', 10);
|
||||
table.text('description');
|
||||
table.timestamps();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = (knex) => knex.schema.dropTableIfExists('accounts');
|
||||
@@ -0,0 +1,11 @@
|
||||
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.createTable('account_balance', (table) => {
|
||||
table.increments();
|
||||
table.integer('account_id');
|
||||
table.decimal('amount');
|
||||
table.string('currency_code', 3);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = (knex) => knex.schema.dropTableIfExists('account_balance');
|
||||
@@ -0,0 +1,13 @@
|
||||
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.createTable('items_categories', (table) => {
|
||||
table.increments();
|
||||
table.string('label');
|
||||
table.integer('parent_category_id').unsigned();
|
||||
table.text('description');
|
||||
table.integer('user_id').unsigned();
|
||||
table.timestamps();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = (knex) => knex.schema.dropTableIfExists('items_categories');
|
||||
@@ -0,0 +1,11 @@
|
||||
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.createTable('items_metadata', (table) => {
|
||||
table.increments();
|
||||
table.string('key');
|
||||
table.string('value');
|
||||
table.integer('item_id').unsigned();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = (knex) => knex.schema.dropTableIfExists('items_metadata');
|
||||
115
server/src/http/controllers/Accounts.js
Normal file
115
server/src/http/controllers/Accounts.js
Normal file
@@ -0,0 +1,115 @@
|
||||
import express from 'express';
|
||||
import { check, validationResult } from 'express-validator';
|
||||
import asyncMiddleware from '../middleware/asyncMiddleware';
|
||||
import Account from '@/models/Account';
|
||||
import AccountBalance from '@/models/AccountBalance';
|
||||
import AccountType from '@/models/AccountType';
|
||||
import JWTAuth from '@/http/middleware/jwtAuth';
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Router constructor method.
|
||||
*/
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
router.use(JWTAuth);
|
||||
router.post('/',
|
||||
this.newAccount.validation,
|
||||
asyncMiddleware(this.newAccount.handler));
|
||||
|
||||
router.get('/:id',
|
||||
this.getAccount.validation,
|
||||
asyncMiddleware(this.getAccount.handler));
|
||||
|
||||
router.delete('/:id',
|
||||
this.deleteAccount.validation,
|
||||
asyncMiddleware(this.deleteAccount.handler));
|
||||
|
||||
return router;
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a new account.
|
||||
*/
|
||||
newAccount: {
|
||||
validation: [
|
||||
check('name').isLength({ min: 3 }).trim().escape(),
|
||||
check('code').isLength({ max: 10 }).trim().escape(),
|
||||
check('type_id').isNumeric().toInt(),
|
||||
check('description').trim().escape(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const errors = validationResult(req);
|
||||
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(422).json({ errors: errors.array() });
|
||||
}
|
||||
|
||||
const { name, code, description } = req.body;
|
||||
const { type_id: typeId } = req.body;
|
||||
|
||||
const foundAccountCodePromise = Account.where('code', code).fetch();
|
||||
const foundAccountTypePromise = AccountType.where('id', typeId).fetch();
|
||||
|
||||
const [foundAccountCode, foundAccountType] = await Promise.all([
|
||||
foundAccountCodePromise,
|
||||
foundAccountTypePromise,
|
||||
]);
|
||||
|
||||
if (!foundAccountCode) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'NOT_UNIQUE_CODE', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (!foundAccountType) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'NOT_EXIST_ACCOUNT_TYPE', code: 110 }],
|
||||
});
|
||||
}
|
||||
const account = Account.forge({
|
||||
name, code, type_id: typeId, description,
|
||||
});
|
||||
|
||||
await account.save();
|
||||
return res.boom.success({ item: { ...account.attributes } });
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Get details of the given account.
|
||||
*/
|
||||
getAccount: {
|
||||
valiation: [],
|
||||
async handler(req, res) {
|
||||
const { id } = req.params;
|
||||
const account = await Account.where('id', id).fetch();
|
||||
|
||||
if (!account) {
|
||||
return res.boom.notFound();
|
||||
}
|
||||
|
||||
return res.status(200).send({ item: { ...account.attributes } });
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete the given account.
|
||||
*/
|
||||
deleteAccount: {
|
||||
validation: [],
|
||||
async handler(req, res) {
|
||||
const { id } = req.params;
|
||||
const account = await Account.where('id', id).fetch();
|
||||
|
||||
if (!account) {
|
||||
return res.boom.notFound();
|
||||
}
|
||||
|
||||
await account.destroy();
|
||||
await AccountBalance.where('account_id', id).destroy({ require: false });
|
||||
|
||||
return res.status(200).send({ id: account.previous('id') });
|
||||
},
|
||||
},
|
||||
};
|
||||
184
server/src/http/controllers/Authentication.js
Normal file
184
server/src/http/controllers/Authentication.js
Normal file
@@ -0,0 +1,184 @@
|
||||
|
||||
import express from 'express';
|
||||
import { check, validationResult } from 'express-validator';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import Mustache from 'mustache';
|
||||
import User from '@/models/User';
|
||||
import asyncMiddleware from '../middleware/asyncMiddleware';
|
||||
import PasswordReset from '@/models/PasswordReset';
|
||||
import mail from '@/services/mail';
|
||||
import { hashPassword } from '@/utils';
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/login',
|
||||
this.login.validation,
|
||||
asyncMiddleware(this.login.handler));
|
||||
|
||||
router.post('/send_reset_password',
|
||||
this.sendResetPassword.validation,
|
||||
asyncMiddleware(this.sendResetPassword.handler));
|
||||
|
||||
router.post('/reset/:token',
|
||||
this.resetPassword.validation,
|
||||
asyncMiddleware(this.resetPassword.handler));
|
||||
|
||||
return router;
|
||||
},
|
||||
|
||||
/**
|
||||
* User login authentication request.
|
||||
*/
|
||||
login: {
|
||||
validation: [
|
||||
check('crediential').isEmail(),
|
||||
check('password').isLength({ min: 5 }),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
const { crediential, password } = req.body;
|
||||
|
||||
const user = await User.query({
|
||||
where: { email: crediential },
|
||||
orWhere: { phone_number: crediential },
|
||||
}).fetch();
|
||||
|
||||
if (!user) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'INVALID_DETAILS', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (!user.verifyPassword(password)) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'INCORRECT_PASSWORD', code: 110 }],
|
||||
});
|
||||
}
|
||||
if (!user.attributes.active) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'USER_INACTIVE', code: 120 }],
|
||||
});
|
||||
}
|
||||
user.save({ alst_login_at: new Date() });
|
||||
return res.status(200).send({});
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Send reset password link via email or SMS.
|
||||
*/
|
||||
sendResetPassword: {
|
||||
validation: [
|
||||
check('email').isEmail(),
|
||||
],
|
||||
// eslint-disable-next-line consistent-return
|
||||
async handler(req, res) {
|
||||
const errors = validationResult(req);
|
||||
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(422).json({ errors: errors.array() });
|
||||
}
|
||||
const { email } = req.body;
|
||||
const user = User.where('email').fetch();
|
||||
|
||||
if (!user) {
|
||||
return res.status(422).send();
|
||||
}
|
||||
// Delete all stored tokens of reset password that associate to the give email.
|
||||
await PasswordReset.where({ email }).destroy({ require: false });
|
||||
|
||||
const passwordReset = PasswordReset.forge({
|
||||
email,
|
||||
token: '123123',
|
||||
});
|
||||
|
||||
await passwordReset.save();
|
||||
|
||||
const filePath = path.join(__dirname, '../../views/mail/ResetPassword.html');
|
||||
const template = fs.readFileSync(filePath, 'utf8');
|
||||
const rendered = Mustache.render(template, {
|
||||
url: `${req.protocol}://${req.hostname}/reset/${passwordReset.attributes.token}`,
|
||||
first_name: user.attributes.first_name,
|
||||
last_name: user.attributes.last_name,
|
||||
contact_us_email: process.env.CONTACT_US_EMAIL,
|
||||
});
|
||||
|
||||
const mailOptions = {
|
||||
to: user.attributes.email,
|
||||
from: `${process.env.MAIL_FROM_NAME} ${process.env.MAIL_FROM_ADDRESS}`,
|
||||
subject: 'Ratteb Password Reset',
|
||||
html: rendered,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
mail.sendMail(mailOptions, (error) => {
|
||||
if (error) {
|
||||
return res.status(400).send();
|
||||
}
|
||||
res.status(200).send({ data: { email: passwordReset.attributes.email } });
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset password.
|
||||
*/
|
||||
resetPassword: {
|
||||
validation: [
|
||||
check('password').isLength({ min: 5 }),
|
||||
check('reset_password'),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const errors = validationResult(req);
|
||||
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(422).json({ errors: errors.array() });
|
||||
}
|
||||
const { token } = req.params;
|
||||
const { password } = req.body;
|
||||
|
||||
const tokenModel = await PasswordReset.query((query) => {
|
||||
query.where({ token });
|
||||
query.where('created_at', '>=', Date.now() - 3600000);
|
||||
}).fetch();
|
||||
|
||||
if (!tokenModel) {
|
||||
return res.status(400).send({
|
||||
error: {
|
||||
type: 'token.invalid',
|
||||
message: 'Password reset token is invalid or has expired',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const user = await User.where({
|
||||
email: tokenModel.attributes.email,
|
||||
});
|
||||
if (!user) {
|
||||
return res.status(400).send({
|
||||
error: { message: 'An unexpected error occurred.' },
|
||||
});
|
||||
}
|
||||
const hashedPassword = await hashPassword(password);
|
||||
|
||||
user.set('password', hashedPassword);
|
||||
await user.save();
|
||||
|
||||
await PasswordReset.where('email', user.get('email')).destroy({ require: false });
|
||||
|
||||
return res.status(200).send({});
|
||||
},
|
||||
},
|
||||
};
|
||||
154
server/src/http/controllers/ItemCategories.js
Normal file
154
server/src/http/controllers/ItemCategories.js
Normal file
@@ -0,0 +1,154 @@
|
||||
import express from 'express';
|
||||
import { check, validationResult } from 'express-validator';
|
||||
import asyncMiddleware from '../middleware/asyncMiddleware';
|
||||
import ItemCategory from '@/models/ItemCategory';
|
||||
// import JWTAuth from '@/http/middleware/jwtAuth';
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Router constructor method.
|
||||
*/
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/:id',
|
||||
this.editCategory.validation,
|
||||
asyncMiddleware(this.editCategory.handler));
|
||||
|
||||
router.post('/',
|
||||
this.newCategory.validation,
|
||||
asyncMiddleware(this.newCategory.handler));
|
||||
|
||||
router.delete('/:id',
|
||||
this.deleteItem.validation,
|
||||
asyncMiddleware(this.deleteItem.handler));
|
||||
|
||||
// router.get('/:id',
|
||||
// this.getCategory.validation,
|
||||
// asyncMiddleware(this.getCategory.handler));
|
||||
|
||||
router.get('/',
|
||||
this.getList.validation,
|
||||
asyncMiddleware(this.getList.validation));
|
||||
|
||||
return router;
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a new item category.
|
||||
*/
|
||||
newCategory: {
|
||||
validation: [
|
||||
check('name').exists({ checkFalsy: true }).trim().escape(),
|
||||
check('parent_category_id').optional().isNumeric().toInt(),
|
||||
check('description').optional().trim().escape(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
|
||||
const { name, parent_category_id: parentCategoryId, description } = req.body;
|
||||
|
||||
if (parentCategoryId) {
|
||||
const foundParentCategory = await ItemCategory.where('id', parentCategoryId).fetch();
|
||||
|
||||
if (!foundParentCategory) {
|
||||
return res.boom.notFound('The parent category ID is not found.', {
|
||||
errors: [{ type: 'PARENT_CATEGORY_NOT_FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
const category = await ItemCategory.forge({
|
||||
label: name,
|
||||
parent_category_id: parentCategoryId,
|
||||
description,
|
||||
});
|
||||
|
||||
await category.save();
|
||||
return res.status(200).send({ id: category.get('id') });
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Edit details of the given category item.
|
||||
*/
|
||||
editCategory: {
|
||||
validation: [
|
||||
check('name').exists({ checkFalsy: true }).trim().escape(),
|
||||
check('parent_category_id').optional().isNumeric().toInt(),
|
||||
check('description').optional().trim().escape(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const { id } = req.params;
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
const { name, parent_category_id: parentCategoryId, description } = req.body;
|
||||
|
||||
const itemCategory = await ItemCategory.where('id', id).fetch();
|
||||
|
||||
if (!itemCategory) {
|
||||
return res.boom.notFound();
|
||||
}
|
||||
|
||||
if (parentCategoryId && parentCategoryId !== itemCategory.attributes.parent_category_id) {
|
||||
const foundParentCategory = await ItemCategory.where('id', parentCategoryId).fetch();
|
||||
|
||||
if (!foundParentCategory) {
|
||||
return res.boom.notFound('The parent category ID is not found.', {
|
||||
errors: [{ type: 'PARENT_CATEGORY_NOT_FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await itemCategory.save({
|
||||
label: name,
|
||||
description,
|
||||
parent_category_id: parentCategoryId,
|
||||
});
|
||||
|
||||
return res.status(200).send({ id: itemCategory.id });
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete the give item category.
|
||||
*/
|
||||
deleteItem: {
|
||||
validation: [],
|
||||
async handler(req, res) {
|
||||
const { id } = req.params;
|
||||
const itemCategory = await ItemCategory.where('id', id).fetch();
|
||||
|
||||
if (!itemCategory) {
|
||||
return res.boom.notFound();
|
||||
}
|
||||
await itemCategory.destroy();
|
||||
return res.status(200).send();
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve the list of items.
|
||||
*/
|
||||
getList: {
|
||||
validation: [],
|
||||
async handler(req, res) {
|
||||
const items = await ItemCategory.fetch();
|
||||
|
||||
if (!items) {
|
||||
return res.boom.notFound();
|
||||
}
|
||||
return res.status(200).send({ items: items.toJSON() });
|
||||
},
|
||||
},
|
||||
};
|
||||
190
server/src/http/controllers/Items.js
Normal file
190
server/src/http/controllers/Items.js
Normal file
@@ -0,0 +1,190 @@
|
||||
import express from 'express';
|
||||
import { check, validationResult } from 'express-validator';
|
||||
import moment from 'moment';
|
||||
import asyncMiddleware from '../middleware/asyncMiddleware';
|
||||
import Item from '@/models/Item';
|
||||
import Account from '@/models/Account';
|
||||
import ItemCategory from '@/models/ItemCategory';
|
||||
|
||||
export default {
|
||||
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
// router.post('/:id',
|
||||
// this.editItem.validation,
|
||||
// asyncMiddleware(this.editCategory.handler));
|
||||
|
||||
router.post('/',
|
||||
this.newItem.validation,
|
||||
asyncMiddleware(this.newItem.handler));
|
||||
|
||||
// router.delete('/:id',
|
||||
// this.deleteItem.validation,
|
||||
// asyncMiddleware(this.deleteItem.handler));
|
||||
|
||||
// router.get('/:id',
|
||||
// this.getCategory.validation,
|
||||
// asyncMiddleware(this.getCategory.handler));
|
||||
|
||||
// router.get('/',
|
||||
// this.categoriesList.validation,
|
||||
// asyncMiddleware(this.categoriesList.validation));
|
||||
|
||||
return router;
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a new item.
|
||||
*/
|
||||
newItem: {
|
||||
validation: [
|
||||
check('name').exists(),
|
||||
check('type_id').exists().isInt(),
|
||||
check('buy_price').exists().isNumeric(),
|
||||
check('cost_price').exists().isNumeric(),
|
||||
check('cost_account_id').exists().isInt(),
|
||||
check('sell_account_id').exists().isInt(),
|
||||
check('category_id').optional().isInt(),
|
||||
check('note').optional(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
|
||||
const { sell_account_id: sellAccountId, cost_account_id: costAccountId } = req.body;
|
||||
const { category_id: categoryId } = req.body;
|
||||
|
||||
const costAccountPromise = Account.where('id', costAccountId).fetch();
|
||||
const sellAccountPromise = Account.where('id', sellAccountId).fetch();
|
||||
const itemCategoryPromise = (categoryId)
|
||||
? ItemCategory.where('id', categoryId).fetch() : null;
|
||||
|
||||
const [costAccount, sellAccount, itemCategory] = await Promise.all([
|
||||
costAccountPromise, sellAccountPromise, itemCategoryPromise,
|
||||
]);
|
||||
|
||||
const errorReasons = [];
|
||||
|
||||
if (!costAccount) {
|
||||
errorReasons.push({ type: 'COST_ACCOUNT_NOT_FOUND', code: 100 });
|
||||
}
|
||||
if (!sellAccount) {
|
||||
errorReasons.push({ type: 'SELL_ACCOUNT_NOT_FOUND', code: 120 });
|
||||
}
|
||||
if (!itemCategory && categoryId) {
|
||||
errorReasons.push({ type: 'ITEM_CATEGORY_NOT_FOUND', code: 140 });
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.boom.badRequest(null, { errors: errorReasons });
|
||||
}
|
||||
|
||||
const item = Item.forge({
|
||||
name: req.body.name,
|
||||
type_id: 1,
|
||||
buy_price: req.body.buy_price,
|
||||
sell_price: req.body.sell_price,
|
||||
currency_code: req.body.currency_code,
|
||||
note: req.body.note,
|
||||
});
|
||||
|
||||
await item.save();
|
||||
|
||||
return res.status(200).send();
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Edit the given item.
|
||||
*/
|
||||
editItem: {
|
||||
validation: [],
|
||||
async handler(req, res) {
|
||||
const { id } = req.params;
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
|
||||
const item = await Item.where('id', id).fetch();
|
||||
|
||||
if (!item) {
|
||||
return res.boom.notFound();
|
||||
}
|
||||
return res.status(200).send();
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete the given item from the storage.
|
||||
*/
|
||||
deleteItem: {
|
||||
validation: [],
|
||||
async handler(req, res) {
|
||||
const { id } = req.params;
|
||||
const item = await Item.where('id', id).fetch();
|
||||
|
||||
if (!item) {
|
||||
return res.boom.notFound(null, {
|
||||
errors: [{ type: 'ITEM_NOT_FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
|
||||
await item.destroy();
|
||||
return res.status(200).send();
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrive the list items with pagination meta.
|
||||
*/
|
||||
listItems: {
|
||||
validation: [],
|
||||
async handler(req, res) {
|
||||
const filter = {
|
||||
name: '',
|
||||
description: '',
|
||||
SKU: '',
|
||||
account_id: null,
|
||||
page_size: 10,
|
||||
page: 1,
|
||||
start_date: null,
|
||||
end_date: null,
|
||||
...req.query,
|
||||
};
|
||||
|
||||
const items = await Item.query((query) => {
|
||||
if (filter.description) {
|
||||
query.where('description', 'like', `%${filter.description}%`);
|
||||
}
|
||||
if (filter.description) {
|
||||
query.where('SKU', filter.SKY);
|
||||
}
|
||||
if (filter.name) {
|
||||
query.where('name', filter.name);
|
||||
}
|
||||
if (filter.start_date) {
|
||||
const startDateFormatted = moment(filter.start_date).format('YYYY-MM-DD HH:mm:SS');
|
||||
query.where('created_at', '>=', startDateFormatted);
|
||||
}
|
||||
if (filter.end_date) {
|
||||
const endDateFormatted = moment(filter.end_date).format('YYYY-MM-DD HH:mm:SS');
|
||||
query.where('created_at', '<=', endDateFormatted);
|
||||
}
|
||||
}).fetchPage({
|
||||
page_size: filter.page_size,
|
||||
page: filter.page,
|
||||
});
|
||||
|
||||
return res.status(200).send({ ...items.toJSON() });
|
||||
},
|
||||
},
|
||||
};
|
||||
23
server/src/http/controllers/OAuth2.js
Normal file
23
server/src/http/controllers/OAuth2.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import express from 'express';
|
||||
import OAuthServer from 'express-oauth-server';
|
||||
import OAuthServerModel from '@/models/OAuthServerModel';
|
||||
|
||||
export default {
|
||||
|
||||
/**
|
||||
* Router constructor method.
|
||||
*/
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
router.oauth = new OAuthServer({
|
||||
model: OAuthServerModel,
|
||||
});
|
||||
|
||||
router.post('/token', router.oauth.token());
|
||||
// router.get('authorize', this.getAuthorize);
|
||||
// router.post('authorize', this.postAuthorize);
|
||||
|
||||
return router;
|
||||
},
|
||||
};
|
||||
0
server/src/http/controllers/Users.js
Normal file
0
server/src/http/controllers/Users.js
Normal file
15
server/src/http/index.js
Normal file
15
server/src/http/index.js
Normal file
@@ -0,0 +1,15 @@
|
||||
// import OAuth2 from '@/http/controllers/OAuth2';
|
||||
import Authentication from '@/http/controllers/Authentication';
|
||||
import Users from '@/http/controllers/Users';
|
||||
import Items from '@/http/controllers/Items';
|
||||
import ItemCategories from '@/http/controllers/ItemCategories';
|
||||
import Accounts from '@/http/controllers/Accounts';
|
||||
|
||||
export default (app) => {
|
||||
// app.use('/api/oauth2', OAuth2.router());
|
||||
app.use('/api/auth', Authentication.router());
|
||||
app.use('/api/users', Users.router());
|
||||
app.use('/api/accounts', Accounts.router());
|
||||
app.use('/api/items', Items.router());
|
||||
app.use('/api/item_categories', ItemCategories.router());
|
||||
};
|
||||
9
server/src/http/middleware/asyncMiddleware.js
Normal file
9
server/src/http/middleware/asyncMiddleware.js
Normal file
@@ -0,0 +1,9 @@
|
||||
const asyncMiddleware = (fn) => (req, res, next) => {
|
||||
Promise.resolve(fn(req, res, next))
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
next(error);
|
||||
});
|
||||
};
|
||||
|
||||
export default asyncMiddleware;
|
||||
34
server/src/http/middleware/jwtAuth.js
Normal file
34
server/src/http/middleware/jwtAuth.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import jwt from 'jsonwebtoken';
|
||||
import User from '@/models/User';
|
||||
|
||||
const authMiddleware = (req, res, next) => {
|
||||
const token = req.headers['x-access-token'] || req.query.token;
|
||||
|
||||
const onError = () => 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) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
req.user = await User.where('id', decoded._id).fetch();
|
||||
|
||||
if (!req.user) {
|
||||
return onError();
|
||||
}
|
||||
resolve(decoded);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
verify.then(() => { next(); }).catch(onError);
|
||||
};
|
||||
export default authMiddleware;
|
||||
16
server/src/models/Account.js
Normal file
16
server/src/models/Account.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import bookshelf from './bookshelf';
|
||||
|
||||
const Account = bookshelf.Model.extend({
|
||||
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
tableName: 'accounts',
|
||||
|
||||
/**
|
||||
* Timestamp columns.
|
||||
*/
|
||||
hasTimestamps: ['created_at', 'updated_at'],
|
||||
});
|
||||
|
||||
export default bookshelf.model('Account', Account);
|
||||
0
server/src/models/AccountBalance.js
Normal file
0
server/src/models/AccountBalance.js
Normal file
0
server/src/models/AccountType.js
Normal file
0
server/src/models/AccountType.js
Normal file
30
server/src/models/Item.js
Normal file
30
server/src/models/Item.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import bookshelf from './bookshelf';
|
||||
|
||||
const Item = bookshelf.Model.extend({
|
||||
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
tableName: 'items',
|
||||
|
||||
/**
|
||||
* Timestamp columns.
|
||||
*/
|
||||
hasTimestamps: false,
|
||||
|
||||
/**
|
||||
* Item may has many meta data.
|
||||
*/
|
||||
metadata() {
|
||||
return this.hasMany('ItemMetadata', 'item_id');
|
||||
},
|
||||
|
||||
/**
|
||||
* Item may belongs to the item category.
|
||||
*/
|
||||
category() {
|
||||
return this.belongsTo('ItemCategory', 'category_id');
|
||||
},
|
||||
});
|
||||
|
||||
export default bookshelf.model('Item', Item);
|
||||
23
server/src/models/ItemCategory.js
Normal file
23
server/src/models/ItemCategory.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import bookshelf from './bookshelf';
|
||||
|
||||
const ItemCategory = bookshelf.Model.extend({
|
||||
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
tableName: 'items_categories',
|
||||
|
||||
/**
|
||||
* Timestamp columns.
|
||||
*/
|
||||
hasTimestamps: ['created_at', 'updated_at'],
|
||||
|
||||
/**
|
||||
* Item category may has many items.
|
||||
*/
|
||||
items() {
|
||||
return this.hasMany('Item', 'category_id');
|
||||
},
|
||||
});
|
||||
|
||||
export default bookshelf.model('ItemCategory', ItemCategory);
|
||||
23
server/src/models/ItemMetadata.js
Normal file
23
server/src/models/ItemMetadata.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import bookshelf from './bookshelf';
|
||||
|
||||
const ItemMetadata = bookshelf.Model.extend({
|
||||
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
tableName: 'items_metadata',
|
||||
|
||||
/**
|
||||
* Timestamp columns.
|
||||
*/
|
||||
hasTimestamps: ['created_at', 'updated_at'],
|
||||
|
||||
/**
|
||||
* Item category may has many items.
|
||||
*/
|
||||
items() {
|
||||
return this.belongsTo('Item', 'item_id');
|
||||
},
|
||||
});
|
||||
|
||||
export default bookshelf.model('ItemMetadata', ItemMetadata);
|
||||
16
server/src/models/OAuthClient.js
Normal file
16
server/src/models/OAuthClient.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import bookshelf from './bookshelf';
|
||||
|
||||
const OAuthClient = bookshelf.Model.extend({
|
||||
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
tableName: 'oauth_clients',
|
||||
|
||||
/**
|
||||
* Timestamp columns.
|
||||
*/
|
||||
hasTimestamps: false,
|
||||
});
|
||||
|
||||
export default bookshelf.model('OAuthClient', OAuthClient);
|
||||
81
server/src/models/OAuthServerModel.js
Normal file
81
server/src/models/OAuthServerModel.js
Normal file
@@ -0,0 +1,81 @@
|
||||
import OAuthClient from '@/models/OAuthClient';
|
||||
import OAuthToken from '@/models/OAuthToken';
|
||||
import User from '@/models/User';
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Retrieve the access token.
|
||||
* @param {String} bearerToken -
|
||||
*/
|
||||
async getAccessToken(bearerToken) {
|
||||
const token = await OAuthClient.where({
|
||||
access_token: bearerToken,
|
||||
}).fetch();
|
||||
|
||||
return {
|
||||
accessToken: token.attributes.access_token,
|
||||
client: {
|
||||
id: token.attributes.client_id,
|
||||
},
|
||||
expires: token.attributes.access_token_expires_on,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve the client from client id and secret.
|
||||
* @param {Number} clientId -
|
||||
* @param {String} clientSecret -
|
||||
*/
|
||||
async getClient(clientId, clientSecret) {
|
||||
const token = await OAuthClient.where({
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
});
|
||||
|
||||
if (!token) { return {}; }
|
||||
|
||||
return {
|
||||
clientId: token.attributes.client_id,
|
||||
clientSecret: token.attributes.client_secret,
|
||||
grants: ['password'],
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Get specific user with given username and password.
|
||||
*/
|
||||
async getUser(username, password) {
|
||||
const user = await User.query((query) => {
|
||||
query.where('username', username);
|
||||
query.where('password', password);
|
||||
}).fetch();
|
||||
|
||||
return {
|
||||
...user.attributes,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Saves the access token.
|
||||
* @param {Object} token -
|
||||
* @param {Object} client -
|
||||
* @param {Object} user -
|
||||
*/
|
||||
async saveAccessToken(token, client, user) {
|
||||
const oauthToken = OAuthToken.forge({
|
||||
access_token: token.accessToken,
|
||||
access_token_expires_on: token.accessTokenExpiresOn,
|
||||
client_id: client.id,
|
||||
refresh_token: token.refreshToken,
|
||||
refresh_token_expires_on: token.refreshTokenExpiresOn,
|
||||
user_id: user.id,
|
||||
});
|
||||
|
||||
await oauthToken.save();
|
||||
|
||||
return {
|
||||
client: { id: client.id },
|
||||
user: { id: user.id },
|
||||
};
|
||||
},
|
||||
};
|
||||
16
server/src/models/OAuthToken.js
Normal file
16
server/src/models/OAuthToken.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import bookshelf from './bookshelf';
|
||||
|
||||
const OAuthToken = bookshelf.Model.extend({
|
||||
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
tableName: 'oauth_tokens',
|
||||
|
||||
/**
|
||||
* Timestamp columns.
|
||||
*/
|
||||
hasTimestamps: false,
|
||||
});
|
||||
|
||||
export default bookshelf.model('OAuthToken', OAuthToken);
|
||||
16
server/src/models/PasswordReset.js
Normal file
16
server/src/models/PasswordReset.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import bookshelf from './bookshelf';
|
||||
|
||||
const PasswordResets = bookshelf.Model.extend({
|
||||
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
tableName: 'password_resets',
|
||||
|
||||
/**
|
||||
* Timestamp columns.
|
||||
*/
|
||||
hasTimestamps: false,
|
||||
});
|
||||
|
||||
export default bookshelf.model('PasswordResets', PasswordResets);
|
||||
31
server/src/models/Role.js
Normal file
31
server/src/models/Role.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import bookshelf from './bookshelf';
|
||||
|
||||
const Role = bookshelf.Model.extend({
|
||||
|
||||
/**
|
||||
* Table name of Role model.
|
||||
* @type {String}
|
||||
*/
|
||||
tableName: 'roles',
|
||||
|
||||
/**
|
||||
* Timestamp columns.
|
||||
*/
|
||||
hasTimestamps: false,
|
||||
|
||||
/**
|
||||
* Role may has many permissions.
|
||||
*/
|
||||
permissions() {
|
||||
return this.belongsToMany('Permission', 'role_has_permissions', 'role_id', 'permission_id');
|
||||
},
|
||||
|
||||
/**
|
||||
* Role model may has many users.
|
||||
*/
|
||||
users() {
|
||||
return this.belongsTo('User');
|
||||
},
|
||||
});
|
||||
|
||||
export default bookshelf.model('Role', Role);
|
||||
16
server/src/models/Setting.js
Normal file
16
server/src/models/Setting.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import bookshelf from './bookshelf';
|
||||
|
||||
const Setting = bookshelf.Model.extend({
|
||||
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
tableName: 'settings',
|
||||
|
||||
/**
|
||||
* Timestamp columns.
|
||||
*/
|
||||
hasTimestamps: false,
|
||||
});
|
||||
|
||||
export default bookshelf.model('Setting', Setting);
|
||||
26
server/src/models/User.js
Normal file
26
server/src/models/User.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import bcrypt from 'bcryptjs';
|
||||
import bookshelf from './bookshelf';
|
||||
|
||||
const User = bookshelf.Model.extend({
|
||||
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
tableName: 'users',
|
||||
|
||||
/**
|
||||
* Timestamp columns.
|
||||
*/
|
||||
hasTimestamps: ['created_at', 'updated_at'],
|
||||
|
||||
/**
|
||||
* Verify the password of the user.
|
||||
* @param {String} password - The given password.
|
||||
* @return {Boolean}
|
||||
*/
|
||||
verifyPassword(password) {
|
||||
return bcrypt.compareSync(password, this.get('password'));
|
||||
},
|
||||
});
|
||||
|
||||
export default bookshelf.model('User', User);
|
||||
17
server/src/models/bookshelf.js
Normal file
17
server/src/models/bookshelf.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import Bookshelf from 'bookshelf';
|
||||
import jsonColumns from 'bookshelf-json-columns';
|
||||
import bookshelfParanoia from 'bookshelf-paranoia';
|
||||
import bookshelfModelBase from 'bookshelf-modelbase';
|
||||
import knex from '../database/knex';
|
||||
|
||||
const bookshelf = Bookshelf(knex);
|
||||
|
||||
bookshelf.plugin('pagination');
|
||||
bookshelf.plugin('visibility');
|
||||
bookshelf.plugin('registry');
|
||||
bookshelf.plugin('virtuals');
|
||||
bookshelf.plugin(jsonColumns);
|
||||
bookshelf.plugin(bookshelfParanoia);
|
||||
bookshelf.plugin(bookshelfModelBase.pluggable);
|
||||
|
||||
export default bookshelf;
|
||||
15
server/src/server.js
Normal file
15
server/src/server.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import errorHandler from 'errorhandler';
|
||||
import app from '@/app';
|
||||
|
||||
app.use(errorHandler);
|
||||
|
||||
const server = app.listen(app.get('port'), () => {
|
||||
console.log(
|
||||
' App is running at http://localhost:%d in %s mode',
|
||||
app.get('port'),
|
||||
app.get('env'),
|
||||
);
|
||||
console.log(' Press CTRL-C to stop');
|
||||
});
|
||||
|
||||
export default server;
|
||||
14
server/src/services/mail.js
Normal file
14
server/src/services/mail.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import nodemailer from 'nodemailer';
|
||||
|
||||
// create reusable transporter object using the default SMTP transport
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: process.env.MAIL_HOST,
|
||||
port: Number(process.env.MAIL_PORT),
|
||||
secure: process.env.MAIL_SECURE === 'true', // true for 465, false for other ports
|
||||
auth: {
|
||||
user: process.env.MAIL_USERNAME,
|
||||
pass: process.env.MAIL_PASSWORD,
|
||||
},
|
||||
});
|
||||
|
||||
export default transporter;
|
||||
16
server/src/utils.js
Normal file
16
server/src/utils.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
const hashPassword = (password) => new Promise((resolve) => {
|
||||
bcrypt.genSalt(10, (error, salt) => {
|
||||
bcrypt.hash(password, salt, (err, hash) => { resolve(hash); });
|
||||
});
|
||||
});
|
||||
|
||||
const origin = (request) => {
|
||||
return `${request.protocol}://${request.hostname}`;
|
||||
};
|
||||
|
||||
export {
|
||||
hashPassword,
|
||||
origin,
|
||||
};
|
||||
11
server/tests/docker-compose.yml
Normal file
11
server/tests/docker-compose.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
on: '2'
|
||||
services:
|
||||
mysql:
|
||||
image: mysql/mysql-server:5.7
|
||||
ports:
|
||||
- "3306:3306"
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=root
|
||||
- MYSQL_DATABASE=moosher_test
|
||||
- MYSQL_USER=moosher
|
||||
- MYSQL_PASSWORD=moosher
|
||||
28
server/tests/models/Item.test.js
Normal file
28
server/tests/models/Item.test.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { create, expect } from '~/testInit';
|
||||
import Item from '@/models/Item';
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import itemCategory from '@/models/ItemCategory';
|
||||
import '@/models/ItemMetadata';
|
||||
|
||||
describe('Model: Item', () => {
|
||||
it('Should item model belongs to the associated category model.', async () => {
|
||||
const category = await create('item_category');
|
||||
const item = await create('item', { category_id: category.id });
|
||||
|
||||
const itemModel = await Item.where('id', item.id).fetch();
|
||||
const itemCategoryModel = await itemModel.category().fetch();
|
||||
|
||||
expect(itemCategoryModel.attributes.id).equals(category.id);
|
||||
});
|
||||
|
||||
it('Should item model has many metadata that assciated to the item model.', async () => {
|
||||
const item = await create('item');
|
||||
await create('item_metadata', { item_id: item.id });
|
||||
await create('item_metadata', { item_id: item.id });
|
||||
|
||||
const itemModel = await Item.where('id', item.id).fetch();
|
||||
const itemMetadataCollection = await itemModel.metadata().fetch();
|
||||
|
||||
expect(itemMetadataCollection.length).equals(2);
|
||||
});
|
||||
});
|
||||
16
server/tests/models/ItemCategories.test.js
Normal file
16
server/tests/models/ItemCategories.test.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { create, expect } from '~/testInit';
|
||||
import '@/models/Item';
|
||||
import ItemCategory from '@/models/ItemCategory';
|
||||
|
||||
describe('Model: ItemCategories', () => {
|
||||
it('Shoud item category model has many associated items.', async () => {
|
||||
const category = await create('item_category');
|
||||
await create('item', { category_id: category.id });
|
||||
await create('item', { category_id: category.id });
|
||||
|
||||
const categoryModel = await ItemCategory.where('id', category.id).fetch();
|
||||
const categoryItems = await categoryModel.items().fetch();
|
||||
|
||||
expect(categoryItems.length).equals(2);
|
||||
});
|
||||
});
|
||||
170
server/tests/routes/auth.test.js
Normal file
170
server/tests/routes/auth.test.js
Normal file
@@ -0,0 +1,170 @@
|
||||
import { request, expect, create } from '~/testInit';
|
||||
import { hashPassword } from '@/utils';
|
||||
|
||||
describe('routes: /auth/', () => {
|
||||
describe('POST `/api/auth/login`', () => {
|
||||
it('Should `crediential` be required.', async () => {
|
||||
const res = await request().post('/api/auth/login').send({});
|
||||
expect(res.status).equals(422);
|
||||
expect(res.body.code).equals('validation_error');
|
||||
|
||||
const paramsErrors = res.body.errors.map((error) => error.param);
|
||||
expect(paramsErrors).to.include('crediential');
|
||||
});
|
||||
|
||||
it('Should `password` be required.', async () => {
|
||||
const res = await request().post('/api/auth/login').send();
|
||||
expect(res.status).equals(422);
|
||||
expect(res.body.code).equals('validation_error');
|
||||
|
||||
const paramsErrors = res.body.errors.map((error) => error.param);
|
||||
expect(paramsErrors).to.include('password');
|
||||
});
|
||||
|
||||
it('Should the min length of the `password` be 5 ch.', async () => {
|
||||
const res = await request().post('/api/auth/login').send({
|
||||
crediential: 'admin@admin.com',
|
||||
password: 'test',
|
||||
});
|
||||
|
||||
expect(res.status).equals(422);
|
||||
expect(res.body.code).equals('validation_error');
|
||||
|
||||
const paramsErrors = res.body.errors.map((error) => error.param);
|
||||
expect(paramsErrors).to.include('password');
|
||||
});
|
||||
|
||||
it('Should be a valid email format in crediential attribute.', async () => {
|
||||
const res = await request().post('/api/auth/login').send({
|
||||
crediential: 'admin',
|
||||
password: 'test',
|
||||
});
|
||||
|
||||
expect(res.status).equals(422);
|
||||
expect(res.body.code).equals('validation_error');
|
||||
|
||||
const paramsErrors = res.body.errors.map((error) => error.param);
|
||||
expect(paramsErrors).to.include('password');
|
||||
});
|
||||
|
||||
it('Should not authenticate with wrong user email and password.', async () => {
|
||||
const res = await request().post('/api/auth/login').send({
|
||||
crediential: 'admin@admin.com',
|
||||
password: 'admin',
|
||||
});
|
||||
expect(res.body.errors).include.something.that.deep.equals({
|
||||
type: 'INVALID_DETAILS', code: 100,
|
||||
});
|
||||
});
|
||||
|
||||
it('Should not authenticate in case user was not active.', async () => {
|
||||
const user = await create('user', {
|
||||
active: false,
|
||||
});
|
||||
const res = await request().post('/api/auth/login').send({
|
||||
crediential: user.email,
|
||||
password: 'admin',
|
||||
});
|
||||
|
||||
expect(res.status).equals(400);
|
||||
expect(res.body.errors).include.something.that.deep.equals({
|
||||
type: 'INCORRECT_PASSWORD', code: 120,
|
||||
});
|
||||
});
|
||||
|
||||
it('Should authenticate with correct email and password and active user.', async () => {
|
||||
const user = await create('user', {
|
||||
password: hashPassword('admin'),
|
||||
});
|
||||
const res = await request().post('/api/auth/login').send({
|
||||
crediential: user.email,
|
||||
});
|
||||
|
||||
expect(res.status).equals(200);
|
||||
});
|
||||
|
||||
it('Should autheticate success with correct phone number and password.', async () => {
|
||||
const password = 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,
|
||||
});
|
||||
|
||||
expect(res.status).equals(200);
|
||||
});
|
||||
|
||||
it('Should last login date be saved after success login.', async () => {
|
||||
const user = await create('user', {
|
||||
password: hashPassword('admin'),
|
||||
});
|
||||
const res = await request().post('/api/auth/login').send({
|
||||
crediential: user.email,
|
||||
password: 'admin',
|
||||
});
|
||||
|
||||
expect(res.status).equals(200);
|
||||
});
|
||||
});
|
||||
|
||||
// describe('POST: `auth/send_reset_password`', () => {
|
||||
|
||||
// it('Should `email` be required.', () => {
|
||||
|
||||
// });
|
||||
|
||||
// it('Should response unproccessable if the email address was invalid.', () => {
|
||||
|
||||
// });
|
||||
|
||||
// it('Should response unproccessable if the email address was not exist.', () => {
|
||||
|
||||
// });
|
||||
|
||||
// it('Should delete all already tokens that associate to the given email.', () => {
|
||||
|
||||
// });
|
||||
|
||||
// it('Should store new token associate with the given email.', () => {
|
||||
|
||||
// });
|
||||
|
||||
// it('Should response success if the email was exist.', () => {
|
||||
|
||||
// });
|
||||
|
||||
// it('Should token be stored to the table after success request.', () => {
|
||||
|
||||
// });
|
||||
// });
|
||||
|
||||
// describe('POST: `/auth/reset/:token`', () => {
|
||||
|
||||
// it('Should response forbidden if the token was invalid.', () => {
|
||||
|
||||
// });
|
||||
|
||||
// it('Should response forbidden if the token was expired.', () => {
|
||||
|
||||
// });
|
||||
|
||||
// it('Should password be required.', () => {
|
||||
|
||||
// });
|
||||
|
||||
// it('Should password and confirm_password be equal.', () => {
|
||||
|
||||
// });
|
||||
|
||||
// it('Should token be deleted after success response.', () => {
|
||||
|
||||
// });
|
||||
|
||||
// it('Should password be updated after success response.', () => {
|
||||
|
||||
// })
|
||||
// });
|
||||
});
|
||||
176
server/tests/routes/items.test.js
Normal file
176
server/tests/routes/items.test.js
Normal file
@@ -0,0 +1,176 @@
|
||||
import { request, expect, create } from '~/testInit';
|
||||
import knex from '@/database/knex';
|
||||
|
||||
describe('routes: `/items`', () => {
|
||||
describe('POST: `/items`', () => {
|
||||
it('Should not create a new item if the user was not authorized.', async () => {
|
||||
|
||||
});
|
||||
|
||||
it('Should `name` be required.', async () => {
|
||||
const res = await request().post('/api/items').send();
|
||||
|
||||
expect(res.status).equals(422);
|
||||
expect(res.body.code).equals('validation_error');
|
||||
|
||||
const foundNameParam = res.body.errors.find((error) => error.param === 'name');
|
||||
expect(!!foundNameParam).equals(true);
|
||||
});
|
||||
|
||||
it('Should `type_id` be required.', async () => {
|
||||
const res = await request().post('/api/items').send();
|
||||
|
||||
expect(res.status).equals(422);
|
||||
expect(res.body.code).equals('validation_error');
|
||||
|
||||
const foundTypeParam = res.body.errors.find((error) => error.param === 'type_id');
|
||||
expect(!!foundTypeParam).equals(true);
|
||||
});
|
||||
|
||||
it('Should `buy_price` be numeric.', async () => {
|
||||
const res = await request().post('/api/items').send({
|
||||
buy_price: 'not_numeric',
|
||||
});
|
||||
|
||||
expect(res.status).equals(422);
|
||||
expect(res.body.code).equals('validation_error');
|
||||
|
||||
const foundBuyPrice = res.body.errors.find((error) => error.param === 'buy_price');
|
||||
expect(!!foundBuyPrice).equals(true);
|
||||
});
|
||||
|
||||
it('Should `cost_price` be numeric.', async () => {
|
||||
const res = await request().post('/api/items').send({
|
||||
cost_price: 'not_numeric',
|
||||
});
|
||||
|
||||
expect(res.status).equals(422);
|
||||
expect(res.body.code).equals('validation_error');
|
||||
|
||||
const foundCostParam = res.body.errors.find((error) => error.param === 'cost_price');
|
||||
expect(!!foundCostParam).equals(true);
|
||||
});
|
||||
|
||||
it('Should `buy_account_id` be integer.', async () => {
|
||||
const res = await request().post('/api/items').send({
|
||||
buy_account_id: 'not_numeric',
|
||||
});
|
||||
|
||||
expect(res.status).equals(422);
|
||||
expect(res.body.code).equals('validation_error');
|
||||
|
||||
const foundAccount = res.body.errors.find((error) => error.param === 'buy_account_id');
|
||||
expect(!!foundAccount).equals(true);
|
||||
});
|
||||
|
||||
it('Should `cost_account_id` be integer.', async () => {
|
||||
const res = await request().post('/api/items').send({
|
||||
cost_account_id: 'not_numeric',
|
||||
});
|
||||
|
||||
expect(res.status).equals(422);
|
||||
expect(res.body.code).equals('validation_error');
|
||||
|
||||
const foundAccount = res.body.errors.find((error) => error.param === 'cost_account_id');
|
||||
expect(!!foundAccount).equals(true);
|
||||
});
|
||||
|
||||
it('Should `cost_account_id` be required if `cost_price` was presented.', async () => {
|
||||
|
||||
});
|
||||
|
||||
it('Should `buy_account_id` be required if `buy_price` was presented.', async () => {
|
||||
|
||||
});
|
||||
|
||||
it('Should response bad request in case cost account was not exist.', async () => {
|
||||
const res = await request().post('/api/items').send({
|
||||
name: 'Item Name',
|
||||
type_id: 1,
|
||||
buy_price: 10.2,
|
||||
cost_price: 20.2,
|
||||
sell_account_id: 10,
|
||||
cost_account_id: 20,
|
||||
});
|
||||
|
||||
expect(res.status).equals(400);
|
||||
expect(res.body.errors).include.something.that.deep.equals({
|
||||
type: 'COST_ACCOUNT_NOT_FOUND', code: 100,
|
||||
});
|
||||
});
|
||||
|
||||
it('Should response bad request in case sell account was not exist.', async () => {
|
||||
const res = await request().post('/api/items').send({
|
||||
name: 'Item Name',
|
||||
type_id: 1,
|
||||
buy_price: 10.2,
|
||||
cost_price: 20.2,
|
||||
sell_account_id: 10,
|
||||
cost_account_id: 20,
|
||||
});
|
||||
|
||||
expect(res.status).equals(400);
|
||||
expect(res.body.errors).include.something.that.deep.equals({
|
||||
type: 'SELL_ACCOUNT_NOT_FOUND', code: 120,
|
||||
});
|
||||
});
|
||||
|
||||
it('Should response not category found in case item category was not exist.', async () => {
|
||||
const res = await request().post('/api/items').send({
|
||||
name: 'Item Name',
|
||||
type_id: 1,
|
||||
buy_price: 10.2,
|
||||
cost_price: 20.2,
|
||||
sell_account_id: 10,
|
||||
cost_account_id: 20,
|
||||
category_id: 20,
|
||||
});
|
||||
|
||||
expect(res.status).equals(400);
|
||||
expect(res.body.errors).include.something.that.deep.equals({
|
||||
type: 'ITEM_CATEGORY_NOT_FOUND', code: 140,
|
||||
});
|
||||
});
|
||||
|
||||
it('Should response success with correct data format.', async () => {
|
||||
const account = await create('account');
|
||||
const anotherAccount = await create('account');
|
||||
const itemCategory = await create('item_category');
|
||||
|
||||
const res = await request().post('/api/items').send({
|
||||
name: 'Item Name',
|
||||
type_id: 1,
|
||||
buy_price: 10.2,
|
||||
cost_price: 20.2,
|
||||
sell_account_id: account.id,
|
||||
cost_account_id: anotherAccount.id,
|
||||
category_id: itemCategory.id,
|
||||
});
|
||||
|
||||
expect(res.status).equals(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE: `items/:id`', () => {
|
||||
it('Should response not found in case the item was not exist.', async () => {
|
||||
const res = await request().delete('/api/items/10').send();
|
||||
|
||||
expect(res.status).equals(404);
|
||||
});
|
||||
|
||||
it('Should response success in case was exist.', async () => {
|
||||
const item = await create('item');
|
||||
const res = await request().delete(`/api/items/${item.id}`);
|
||||
|
||||
expect(res.status).equals(200);
|
||||
});
|
||||
|
||||
it('Should delete the given item from the storage.', async () => {
|
||||
const item = await create('item');
|
||||
await request().delete(`/api/items/${item.id}`);
|
||||
|
||||
const storedItem = await knex('items').where('id', item.id);
|
||||
expect(storedItem).to.have.lengthOf(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
160
server/tests/routes/itemsCategories.test.js
Normal file
160
server/tests/routes/itemsCategories.test.js
Normal file
@@ -0,0 +1,160 @@
|
||||
import { request, expect, create } from '~/testInit';
|
||||
import knex from '@/database/knex';
|
||||
|
||||
describe('routes: /item_categories/', () => {
|
||||
describe('POST `/items_categories``', async () => {
|
||||
it('Should not create a item category if the user was not authorized.', () => {
|
||||
|
||||
});
|
||||
|
||||
it('Should `name` be required.', async () => {
|
||||
const res = await request().post('/api/item_categories').send();
|
||||
|
||||
expect(res.status).equals(422);
|
||||
expect(res.body.code).equals('validation_error');
|
||||
});
|
||||
|
||||
it('Should `parent_category_id` be exist in the storage.', async () => {
|
||||
const res = await request().posjt('/api/item_categories').send({
|
||||
name: 'Clothes',
|
||||
parent_category_id: 10,
|
||||
});
|
||||
|
||||
expect(res.status).equals(404);
|
||||
expect(res.body.errors).include.something.that.deep.equals({
|
||||
type: 'PARENT_CATEGORY_NOT_FOUND', code: 100,
|
||||
});
|
||||
});
|
||||
|
||||
it('Should response success with correct form data.', async () => {
|
||||
const res = await request().post('/api/item_categories').send({
|
||||
name: 'Clothes',
|
||||
description: 'Here is description',
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
expect(res.body.id).to.exist;
|
||||
expect(res.status).equals(200);
|
||||
});
|
||||
|
||||
it('Should item category data be saved to the storage.', async () => {
|
||||
const category = await create('item_category');
|
||||
const res = await request().post('/api/item_categories').send({
|
||||
name: 'Clothes',
|
||||
description: 'Here is description',
|
||||
parent_category_id: category.id,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
expect(res.body.id).to.exist;
|
||||
|
||||
const storedCategory = await knex('items_categories').where('id', res.body.id).first();
|
||||
|
||||
expect(storedCategory.label).equals('Clothes');
|
||||
expect(storedCategory.description).equals('Here is description');
|
||||
expect(storedCategory.parent_category_id).equals(category.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST `/items_category/{id}`', () => {
|
||||
it('Should not update a item category if the user was not authorized.', () => {
|
||||
|
||||
});
|
||||
|
||||
it('Should `name` be required.', async () => {
|
||||
const category = await create('item_category');
|
||||
const res = await request().post(`/api/item_categories/${category.id}`).send({
|
||||
name: '',
|
||||
});
|
||||
expect(res.status).equals(422);
|
||||
expect(res.body.code).equals('validation_error');
|
||||
});
|
||||
|
||||
it('Should `parent_category_id` be exist in the storage.', async () => {
|
||||
const category = await create('item_category');
|
||||
const res = await request().post(`/api/item_categories/${category.id}`).send({
|
||||
name: 'Name',
|
||||
parent_category_id: 10,
|
||||
});
|
||||
|
||||
expect(res.status).equals(404);
|
||||
expect(res.body.errors).include.something.that.deep.equals({
|
||||
type: 'PARENT_CATEGORY_NOT_FOUND', code: 100,
|
||||
});
|
||||
});
|
||||
|
||||
it('Should response success with correct data format.', async () => {
|
||||
const category = await create('item_category');
|
||||
const anotherCategory = await create('item_category');
|
||||
|
||||
const res = await request().post(`/api/item_categories/${category.id}`).send({
|
||||
name: 'Name',
|
||||
parent_category_id: anotherCategory.id,
|
||||
description: 'updated description',
|
||||
});
|
||||
|
||||
expect(res.status).equals(200);
|
||||
});
|
||||
|
||||
it('Should item category data be update in the storage.', async () => {
|
||||
const category = await create('item_category');
|
||||
const anotherCategory = await create('item_category');
|
||||
|
||||
const res = await request().post(`/api/item_categories/${category.id}`).send({
|
||||
name: 'Name',
|
||||
parent_category_id: anotherCategory.id,
|
||||
description: 'updated description',
|
||||
});
|
||||
|
||||
const storedCategory = await knex('items_categories').where('id', res.body.id).first();
|
||||
|
||||
expect(storedCategory.label).equals('Name');
|
||||
expect(storedCategory.description).equals('updated description');
|
||||
expect(storedCategory.parent_category_id).equals(anotherCategory.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE: `/items_categories`', async () => {
|
||||
it('Should not delete the give item category if the user was not authorized.', () => {
|
||||
|
||||
});
|
||||
|
||||
it('Should not delete if the item category was not found.', async () => {
|
||||
const res = await request().delete('/api/item_categories/10');
|
||||
|
||||
expect(res.status).equals(404);
|
||||
});
|
||||
|
||||
it('Should response success after delete the given item category.', async () => {
|
||||
const category = await create('item_category');
|
||||
|
||||
const res = await request().delete(`/api/item_categories/${category.id}`);
|
||||
|
||||
expect(res.status).equals(200);
|
||||
});
|
||||
|
||||
it('Should delete the give item category from the storage.', async () => {
|
||||
const category = await create('item_category');
|
||||
await request().delete(`/api/item_categories/${category.id}`);
|
||||
|
||||
const categories = await knex('items_categories').where('id', category.id);
|
||||
|
||||
expect(categories).to.have.lengthOf(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('GET `/items_category/{id}', () => {
|
||||
it('Should response not found with incorrect item category ID.', () => {
|
||||
|
||||
});
|
||||
|
||||
it('Should response success with exist item category.', () => {
|
||||
|
||||
});
|
||||
|
||||
it('Should response data of item category.', () => {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
11
server/tests/routes/oauth.test.js
Normal file
11
server/tests/routes/oauth.test.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { request, expect } from '~/testInit';
|
||||
|
||||
describe('routes: /oauth2/', () => {
|
||||
describe('POST `/api/oauth/token`', () => {
|
||||
it('Should `crediential` be required.', async () => {
|
||||
const res = await request().post('/api/oauth2/token').send({});
|
||||
console.log(res.body);
|
||||
expect(res.status).equals(200);
|
||||
});
|
||||
});
|
||||
});
|
||||
0
server/tests/routes/roles.test.js
Normal file
0
server/tests/routes/roles.test.js
Normal file
42
server/tests/routes/users.test.js
Normal file
42
server/tests/routes/users.test.js
Normal file
@@ -0,0 +1,42 @@
|
||||
|
||||
describe('routes: `/routes`', () => {
|
||||
describe('POST: `/routes`', () => {
|
||||
it('Should create a new user if the user was not authorized.', () => {
|
||||
|
||||
});
|
||||
|
||||
it('Should `email` be required.', () => {
|
||||
|
||||
});
|
||||
|
||||
it('Should `password` be required.', () => {
|
||||
|
||||
});
|
||||
|
||||
it('Should `status` be boolean', () => {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE: `/users/:id`', () => {
|
||||
it('Should not success if the user was not authorized.', () => {
|
||||
|
||||
});
|
||||
|
||||
it('Should not success if the user was pre-defined user.', () => {
|
||||
|
||||
});
|
||||
|
||||
it('Should response not found if the user was not exist.', () => {
|
||||
|
||||
});
|
||||
|
||||
it('Should response success if the user was exist.', () => {
|
||||
|
||||
});
|
||||
|
||||
it('Should delete the give user after success response.', () => {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
46
server/tests/testInit.js
Normal file
46
server/tests/testInit.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import chai from 'chai';
|
||||
import chaiHttp from 'chai-http';
|
||||
import chaiThings from 'chai-things';
|
||||
import app from '@/app';
|
||||
import knex from '@/database/knex';
|
||||
import factory from '@/database/factories';
|
||||
import { hashPassword } from '@/utils';
|
||||
|
||||
const request = () => chai.request(app);
|
||||
const { expect } = chai;
|
||||
|
||||
beforeEach(async () => {
|
||||
await knex.migrate.rollback();
|
||||
await knex.migrate.latest();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await knex.migrate.rollback();
|
||||
});
|
||||
|
||||
chai.use(chaiHttp);
|
||||
chai.use(chaiThings);
|
||||
|
||||
const login = async (givenUser) => {
|
||||
const user = givenUser === null ? await factory.create('user') : givenUser;
|
||||
|
||||
const response = await request()
|
||||
.post('/api/auth/login')
|
||||
.send({
|
||||
crediential: user.email,
|
||||
password: hashPassword('secret'),
|
||||
});
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
const create = async (name, data) => factory.create(name, data);
|
||||
const make = async (name, data) => factory.build(name, data);
|
||||
|
||||
export {
|
||||
login,
|
||||
create,
|
||||
make,
|
||||
expect,
|
||||
request,
|
||||
};
|
||||
52
server/webpack.config.js
Normal file
52
server/webpack.config.js
Normal file
@@ -0,0 +1,52 @@
|
||||
const path = require('path');
|
||||
const nodeExternals = require('webpack-node-externals');
|
||||
|
||||
function resolve(dir) {
|
||||
return path.join(__dirname, '.', dir);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
mode: 'development',
|
||||
entry: [
|
||||
'@babel/plugin-transform-runtime',
|
||||
'@/server.js',
|
||||
],
|
||||
target: 'node',
|
||||
devtool: 'inline-cheap-module-source-map',
|
||||
externals: [nodeExternals()],
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
filename: 'bundle.js',
|
||||
publicPath: 'dist/',
|
||||
|
||||
// use absolute paths in sourcemaps (important for debugging via IDE)
|
||||
devtoolModuleFilenameTemplate: '[absolute-resource-path]',
|
||||
devtoolFallbackModuleFilenameTemplate: '[absolute-resource-path]?[hash]',
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, 'src'),
|
||||
'~': path.resolve(__dirname, 'tests'),
|
||||
},
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(js)$/,
|
||||
loader: 'eslint-loader',
|
||||
enforce: 'pre',
|
||||
include: [resolve('src'), resolve('test')],
|
||||
options: {
|
||||
// eslint-disable-next-line global-require
|
||||
formatter: require('eslint-friendly-formatter'),
|
||||
// emitWarning: !config.dev.showEslintErrorsInOverlay
|
||||
},
|
||||
},
|
||||
{
|
||||
use: 'babel-loader',
|
||||
exclude: /(node_modules)/,
|
||||
test: /\.js$/,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user