WIP Items module.

This commit is contained in:
Ahmed Bouhuolia
2019-09-03 02:07:28 +02:00
parent cb8c294d74
commit 70809cb05c
142 changed files with 12674 additions and 64 deletions

16
server/src/app.js Normal file
View 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;

View 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;

View 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;

View File

@@ -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');

View File

@@ -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');
};

View File

@@ -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');
};

View File

@@ -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');

View File

@@ -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');

View File

@@ -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');

View File

@@ -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');

View File

@@ -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');

View File

@@ -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');

View File

@@ -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');

View 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') });
},
},
};

View 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({});
},
},
};

View 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() });
},
},
};

View 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() });
},
},
};

View 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;
},
};

View File

15
server/src/http/index.js Normal file
View 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());
};

View 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;

View 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;

View 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);

View File

View File

30
server/src/models/Item.js Normal file
View 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);

View 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);

View 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);

View 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);

View 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 },
};
},
};

View 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);

View 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
View 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);

View 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
View 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);

View 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
View 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;

View 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
View 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,
};