mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 21:30:31 +00:00
feat: User invitation system.
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@hapi/boom": "^7.4.3",
|
||||
"app-root-path": "^3.0.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"bookshelf": "^0.15.1",
|
||||
"bookshelf-cascade-delete": "^2.0.1",
|
||||
@@ -26,6 +27,7 @@
|
||||
"errorhandler": "^1.5.1",
|
||||
"express": "^4.17.1",
|
||||
"express-boom": "^3.0.0",
|
||||
"express-fileupload": "^1.1.7-alpha.3",
|
||||
"express-oauth-server": "^2.0.0",
|
||||
"express-validator": "^6.2.0",
|
||||
"helmet": "^3.21.0",
|
||||
|
||||
@@ -2,10 +2,13 @@ import express from 'express';
|
||||
import helmet from 'helmet';
|
||||
import boom from 'express-boom';
|
||||
import i18n from 'i18n';
|
||||
import rootPath from 'app-root-path';
|
||||
import fileUpload from 'express-fileupload';
|
||||
import '../config';
|
||||
import '@/database/objection';
|
||||
import routes from '@/http';
|
||||
|
||||
global.rootPath = rootPath.path;
|
||||
|
||||
const app = express();
|
||||
|
||||
@@ -29,6 +32,10 @@ app.set('port', process.env.PORT || 3000);
|
||||
app.use(helmet());
|
||||
app.use(boom());
|
||||
app.use(express.json());
|
||||
app.use(fileUpload({
|
||||
createParentPath: true,
|
||||
// safeFileNames: true,
|
||||
}));
|
||||
|
||||
routes(app);
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ exports.up = function (knex) {
|
||||
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');
|
||||
|
||||
@@ -14,6 +14,7 @@ exports.up = function (knex) {
|
||||
table.text('note').nullable();
|
||||
table.integer('category_id').unsigned();
|
||||
table.integer('user_id').unsigned();
|
||||
table.string('attachment_file');
|
||||
table.timestamps();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ exports.up = function(knex) {
|
||||
table.date('date');
|
||||
table.boolean('status').defaultTo(false);
|
||||
table.string('description');
|
||||
table.string('attachment_file');
|
||||
table.integer('user_id').unsigned();
|
||||
table.timestamps();
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import JWTAuth from '@/http/middleware/jwtAuth';
|
||||
import JournalPoster from '@/services/Accounting/JournalPoster';
|
||||
import JournalEntry from '@/services/Accounting/JournalEntry';
|
||||
import TenancyMiddleware from '@/http/middleware/TenancyMiddleware';
|
||||
import {
|
||||
mapViewRolesToConditionals,
|
||||
mapFilterRolesToDynamicFilter,
|
||||
@@ -25,6 +26,7 @@ export default {
|
||||
router() {
|
||||
const router = express.Router();
|
||||
router.use(JWTAuth);
|
||||
router.use(TenancyMiddleware);
|
||||
|
||||
router.get('/manual-journals/:id',
|
||||
this.getManualJournal.validation,
|
||||
@@ -185,6 +187,7 @@ export default {
|
||||
check('entries.*.debit').optional({ nullable: true }).isNumeric().toInt(),
|
||||
check('entries.*.account_id').isNumeric().toInt(),
|
||||
check('entries.*.note').optional(),
|
||||
check('attachment').optional(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
@@ -243,9 +246,28 @@ export default {
|
||||
if (journalNumber.length > 0) {
|
||||
errorReasons.push({ type: 'JOURNAL.NUMBER.ALREADY.EXISTS', code: 300 });
|
||||
}
|
||||
|
||||
const { attachment } = req.files;
|
||||
const supportedMimes = ['image/png', 'image/jpeg'];
|
||||
|
||||
if (attachment && supportedMimes.indexOf(attachment.mimeType) === -1) {
|
||||
errorReasons.push({ type: 'ATTACHMENT.MINETYPE.NOT.SUPPORTED', code: 400 });
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
|
||||
if (attachment) {
|
||||
const publicPath = 'storage/app/public/';
|
||||
try {
|
||||
await attachment.mv(`${publicPath}${req.organizationId}/${attachment.md5}.png`);
|
||||
} catch (error) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'ATTACHMENT.UPLOAD.FAILED', code: 600 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Save manual journal transaction.
|
||||
const manualJournal = await ManualJournal.query().insert({
|
||||
reference: form.reference,
|
||||
@@ -256,6 +278,7 @@ export default {
|
||||
description: form.description,
|
||||
status: form.status,
|
||||
user_id: user.id,
|
||||
attachment_file: (attachment) ? `${attachment.md5}.png` : null,
|
||||
});
|
||||
const journalPoster = new JournalPoster();
|
||||
|
||||
@@ -279,7 +302,6 @@ export default {
|
||||
journalPoster.credit(jouranlEntry);
|
||||
}
|
||||
});
|
||||
|
||||
// Saves the journal entries and accounts balance changes.
|
||||
await Promise.all([
|
||||
journalPoster.saveEntries(),
|
||||
|
||||
@@ -17,6 +17,7 @@ import Tenant from '@/system/models/Tenant';
|
||||
import TenantUser from '@/models/TenantUser';
|
||||
import TenantsManager from '@/system/TenantsManager';
|
||||
import TenantModel from '@/models/TenantModel';
|
||||
import PasswordReset from '@/system/models/PasswordReset';
|
||||
|
||||
export default {
|
||||
/**
|
||||
@@ -187,44 +188,47 @@ export default {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
const { email } = req.body;
|
||||
const user = await User.where('email', email).fetch();
|
||||
const form = { ...req.body };
|
||||
Logger.log('info', 'User trying to send reset password.', { form });
|
||||
|
||||
const user = await SystemUser.query().where('email', form.email).first();
|
||||
|
||||
if (!user) {
|
||||
return res.status(422).send();
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'EMAIL.NOT.REGISTERED', code: 200 }],
|
||||
});
|
||||
}
|
||||
// Delete all stored tokens of reset password that associate to the give email.
|
||||
await PasswordReset.where({ email }).destroy({ require: false });
|
||||
await PasswordReset.query()
|
||||
.where('email', form.email)
|
||||
.delete();
|
||||
|
||||
const passwordReset = PasswordReset.forge({
|
||||
email,
|
||||
token: '123123',
|
||||
});
|
||||
await passwordReset.save();
|
||||
const token = uniqid();
|
||||
const passwordReset = await PasswordReset.query()
|
||||
.insert({ email: form.email, token });
|
||||
|
||||
const filePath = path.join(__dirname, '../../views/mail/ResetPassword.html');
|
||||
const filePath = path.join(global.rootPath, '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,
|
||||
url: `${req.protocol}://${req.hostname}/reset/${passwordReset.token}`,
|
||||
first_name: user.firstName,
|
||||
last_name: user.lastName,
|
||||
// contact_us_email: config.contactUsMail,
|
||||
});
|
||||
|
||||
const mailOptions = {
|
||||
to: user.attributes.email,
|
||||
to: user.email,
|
||||
from: `${process.env.MAIL_FROM_NAME} ${process.env.MAIL_FROM_ADDRESS}`,
|
||||
subject: 'Ratteb Password Reset',
|
||||
subject: 'Bigcapital - Password Reset',
|
||||
html: rendered,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
mail.sendMail(mailOptions, (error) => {
|
||||
if (error) {
|
||||
return res.status(400).send();
|
||||
Logger.log('error', 'Failed send reset password mail', { error, form });
|
||||
}
|
||||
res.status(200).send({ data: { email: passwordReset.attributes.email } });
|
||||
Logger.log('info', 'User has been sent reset password email successfuly.', { form });
|
||||
});
|
||||
res.status(200).send({ email: passwordReset.email });
|
||||
},
|
||||
},
|
||||
|
||||
@@ -246,7 +250,7 @@ export default {
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'VALIDATION_ERROR', ...validationErrors,
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
const { token } = req.params;
|
||||
@@ -262,9 +266,9 @@ export default {
|
||||
errors: [{ type: 'TOKEN_INVALID', code: 100 }],
|
||||
});
|
||||
}
|
||||
const user = await User.where({
|
||||
email: tokenModel.email,
|
||||
});
|
||||
const user = await SystemUser.query()
|
||||
.where('email', tokenModel.email).first();
|
||||
|
||||
if (!user) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'USER_NOT_FOUND', code: 120 }],
|
||||
@@ -272,10 +276,14 @@ export default {
|
||||
}
|
||||
const hashedPassword = await hashPassword(password);
|
||||
|
||||
user.password = hashedPassword;
|
||||
await user.save();
|
||||
await SystemUser.query()
|
||||
.where('email', tokenModel.email)
|
||||
.update({
|
||||
password: hashedPassword,
|
||||
});
|
||||
|
||||
await PasswordReset.where('email', user.get('email')).destroy({ require: false });
|
||||
// Delete the reset password token.
|
||||
await PasswordReset.query().where('token', token).delete();
|
||||
|
||||
return res.status(200).send({});
|
||||
},
|
||||
|
||||
186
server/src/http/controllers/InviteUsers.js
Normal file
186
server/src/http/controllers/InviteUsers.js
Normal file
@@ -0,0 +1,186 @@
|
||||
import express from 'express';
|
||||
import uniqid from 'uniqid';
|
||||
import {
|
||||
check,
|
||||
body,
|
||||
query,
|
||||
validationResult,
|
||||
} from 'express-validator';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import Mustache from 'mustache';
|
||||
import mail from '@/services/mail';
|
||||
import { hashPassword } from '@/utils';
|
||||
import SystemUser from '@/system/models/SystemUser';
|
||||
import Invite from '@/system/models/Invite';
|
||||
import TenantUser from '@/models/TenantUser';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import Tenant from '@/system/models/Tenant';
|
||||
import TenantsManager from '@/system/TenantsManager';
|
||||
import jwtAuth from '@/http/middleware/jwtAuth';
|
||||
import TenancyMiddleware from '@/http/middleware/TenancyMiddleware';
|
||||
import TenantModel from '@/models/TenantModel';
|
||||
import Logger from '@/services/Logger';
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
router.use('/send', jwtAuth);
|
||||
router.use('/send', TenancyMiddleware);
|
||||
|
||||
router.post('/send',
|
||||
this.invite.validation,
|
||||
asyncMiddleware(this.invite.handler));
|
||||
|
||||
router.post('/accept',
|
||||
this.accept.validation,
|
||||
asyncMiddleware(this.accept.handler));
|
||||
|
||||
return router;
|
||||
},
|
||||
|
||||
/**
|
||||
* Invite a user to the authorized user organization.
|
||||
*/
|
||||
invite: {
|
||||
validation: [
|
||||
body('email').exists().trim().escape(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
const form = { ...req.body };
|
||||
const foundUser = await SystemUser.query()
|
||||
.where('email', form.email).first();
|
||||
|
||||
const { user } = req;
|
||||
|
||||
if (foundUser) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'USER.EMAIL.ALREADY.REGISTERED', code: 100 }],
|
||||
});
|
||||
}
|
||||
const token = uniqid();
|
||||
const invite = await Invite.query().insert({
|
||||
email: form.email,
|
||||
tenant_id: user.tenantId,
|
||||
token,
|
||||
});
|
||||
const { Option } = req.models;
|
||||
const organizationOptions = await Option.query()
|
||||
.where('key', 'organization_name');
|
||||
|
||||
const filePath = path.join(global.rootPath, 'views/mail/UserInvite.html');
|
||||
const template = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
const rendered = Mustache.render(template, {
|
||||
url: `${req.protocol}://${req.hostname}/invite/accept/${invite.token}`,
|
||||
fullName: `${user.firstName} ${user.lastName}`,
|
||||
email: user.email,
|
||||
organizationName: organizationOptions.getMeta('organization_meta'),
|
||||
});
|
||||
const mailOptions = {
|
||||
to: user.email,
|
||||
from: `${process.env.MAIL_FROM_NAME} ${process.env.MAIL_FROM_ADDRESS}`,
|
||||
subject: `${user.firstName} ${user.lastName} has invited you to join a Bigcapital`,
|
||||
html: rendered,
|
||||
};
|
||||
mail.sendMail(mailOptions, (error) => {
|
||||
if (error) {
|
||||
Logger.log('error', 'Failed send user invite mail', { error, form });
|
||||
}
|
||||
Logger.log('info', 'User has been sent invite user email successfuly.', { form });
|
||||
});
|
||||
return res.status(200).send();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Acceprt the inviation.
|
||||
*/
|
||||
accept: {
|
||||
validation: [
|
||||
check('first_name').exists().trim().escape(),
|
||||
check('last_name').exists().trim().escape(),
|
||||
check('phone_number').exists().trim().escape(),
|
||||
check('language').exists().isIn(['ar', 'en']),
|
||||
check('password').exists().trim().escape(),
|
||||
|
||||
query('token').exists().trim().escape(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const inviteToken = await Invite.query()
|
||||
.where('token', req.query.token).first();
|
||||
|
||||
if (!inviteToken) {
|
||||
return res.status(404).send({
|
||||
errors: [{ type: 'INVITE.TOKEN.NOT.FOUND', code: 300 }],
|
||||
});
|
||||
}
|
||||
const form = { ...req.body };
|
||||
|
||||
const systemUser = await SystemUser.query()
|
||||
.where('phone_number', form.phone_number)
|
||||
.first();
|
||||
|
||||
const errorReasons = [];
|
||||
|
||||
// Validate there is already registered phone number.
|
||||
if (systemUser && systemUser.phoneNumber === form.phone_number) {
|
||||
errorReasons.push({
|
||||
type: 'PHONE_MUMNER.ALREADY.EXISTS', code: 400,
|
||||
});
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
// Find the tenant that associated to the given token.
|
||||
const tenant = await Tenant.query()
|
||||
.where('id', inviteToken.tenantId).first();
|
||||
|
||||
const tenantDb = TenantsManager.knexInstance(tenant.organizationId);
|
||||
const hashedPassword = await hashPassword(form.password);
|
||||
|
||||
const userForm = {
|
||||
first_name: form.first_name,
|
||||
last_name: form.last_name,
|
||||
email: form.email,
|
||||
phone_number: form.phone_number,
|
||||
language: form.language,
|
||||
active: 1,
|
||||
password: hashedPassword,
|
||||
};
|
||||
TenantModel.knexBinded = tenantDb;
|
||||
|
||||
const foundTenantUser = await TenantUser.query()
|
||||
.where('phone_number', form.phone_number).first();
|
||||
|
||||
if (foundTenantUser) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'PHONE_NUMBER.ALREADY.EXISTS', code: 400 }],
|
||||
});
|
||||
}
|
||||
const insertUserOper = TenantUser.bindKnex(tenantDb)
|
||||
.query().insert({ ...userForm });
|
||||
const insertSysUserOper = SystemUser.query().insert({ ...userForm });
|
||||
|
||||
await Promise.all([
|
||||
insertUserOper,
|
||||
insertSysUserOper,
|
||||
]);
|
||||
await Invite.query()
|
||||
.where('token', req.query.token).delete();
|
||||
|
||||
return res.status(200).send();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import express from 'express';
|
||||
import { check, query, validationResult } from 'express-validator';
|
||||
import { difference } from 'lodash';
|
||||
import fs from 'fs';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import jwtAuth from '@/http/middleware/jwtAuth';
|
||||
import TenancyMiddleware from '@/http/middleware/TenancyMiddleware';
|
||||
import {
|
||||
mapViewRolesToConditionals,
|
||||
mapFilterRolesToDynamicFilter,
|
||||
@@ -13,7 +15,10 @@ import {
|
||||
DynamicFilterViews,
|
||||
DynamicFilterFilterRoles,
|
||||
} from '@/lib/DynamicFilter';
|
||||
import Logger from '@/services/Logger';
|
||||
import ConfiguredMiddleware from '@/http/middleware/ConfiguredMiddleware';
|
||||
|
||||
const fsPromises = fs.promises;
|
||||
|
||||
export default {
|
||||
/**
|
||||
@@ -23,6 +28,8 @@ export default {
|
||||
const router = express.Router();
|
||||
|
||||
router.use(jwtAuth);
|
||||
router.use(TenancyMiddleware);
|
||||
router.use(ConfiguredMiddleware);
|
||||
|
||||
router.post('/:id',
|
||||
this.editItem.validation,
|
||||
@@ -68,6 +75,7 @@ export default {
|
||||
check('custom_fields.*.value').exists(),
|
||||
|
||||
check('note').optional(),
|
||||
check('attachment').optional(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
@@ -118,6 +126,13 @@ export default {
|
||||
errorReasons.push({ type: 'FIELD_KEY_NOT_FOUND', code: 150, fields: notFoundFields });
|
||||
}
|
||||
}
|
||||
const { attachment } = req.files;
|
||||
const attachmentsMimes = ['image/png', 'image/jpeg'];
|
||||
|
||||
// Validate the attachment.
|
||||
if (attachment && attachmentsMimes.indexOf(attachment.mimetype) === -1) {
|
||||
errorReasons.push({ type: 'ATTACHMENT.MINETYPE.NOT.SUPPORTED', code: 160 });
|
||||
}
|
||||
const [
|
||||
costAccount,
|
||||
sellAccount,
|
||||
@@ -142,6 +157,11 @@ export default {
|
||||
if (errorReasons.length > 0) {
|
||||
return res.boom.badRequest(null, { errors: errorReasons });
|
||||
}
|
||||
if (attachment) {
|
||||
const publicPath = 'storage/app/public/';
|
||||
await attachment.mv(`${publicPath}${req.organizationId}/${attachment.md5}.png`);
|
||||
}
|
||||
|
||||
const item = await Item.query().insertAndFetch({
|
||||
name: form.name,
|
||||
type: form.type,
|
||||
@@ -151,6 +171,7 @@ export default {
|
||||
cost_account_id: form.cost_account_id,
|
||||
currency_code: form.currency_code,
|
||||
note: form.note,
|
||||
attachment_file: (attachment) ? `${attachment.md5}.png` : null,
|
||||
});
|
||||
return res.status(200).send({ id: item.id });
|
||||
},
|
||||
@@ -173,6 +194,8 @@ export default {
|
||||
check('sell_account_id').exists().isInt(),
|
||||
check('category_id').optional().isInt(),
|
||||
check('note').optional(),
|
||||
check('attachment').optional(),
|
||||
check('')
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
@@ -215,9 +238,34 @@ export default {
|
||||
if (!itemCategory && form.category_id) {
|
||||
errorReasons.push({ type: 'ITEM_CATEGORY_NOT_FOUND', code: 140 });
|
||||
}
|
||||
|
||||
const { attachment } = req.files;
|
||||
const attachmentsMimes = ['image/png', 'image/jpeg'];
|
||||
|
||||
// Validate the attachment.
|
||||
if (attachment && attachmentsMimes.indexOf(attachment.mimetype) === -1) {
|
||||
errorReasons.push({ type: 'ATTACHMENT.MINETYPE.NOT.SUPPORTED', code: 160 });
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.boom.badRequest(null, { errors: errorReasons });
|
||||
}
|
||||
if (attachment) {
|
||||
const publicPath = 'storage/app/public/';
|
||||
const tenantPath = `${publicPath}${req.organizationId}`;
|
||||
try {
|
||||
await fsPromises.unlink(`${tenantPath}/${item.attachmentFile}`);
|
||||
} catch (error) {
|
||||
Logger.log('error', 'Delete item attachment file delete failed.', { error });
|
||||
}
|
||||
|
||||
try {
|
||||
await attachment.mv(`${tenantPath}/${attachment.md5}.png`);
|
||||
} catch (error) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'ATTACHMENT.UPLOAD.FAILED', code: 600 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const updatedItem = await Item.query().findById(id).patch({
|
||||
name: form.name,
|
||||
@@ -229,6 +277,7 @@ export default {
|
||||
cost_account_id: form.cost_account_id,
|
||||
category_id: form.category_id,
|
||||
note: form.note,
|
||||
attachment_file: (attachment) ? item.attachmentFile : null,
|
||||
});
|
||||
return res.status(200).send({ id: updatedItem.id });
|
||||
},
|
||||
@@ -252,6 +301,16 @@ export default {
|
||||
// Delete the fucking the given item id.
|
||||
await Item.query().findById(item.id).delete();
|
||||
|
||||
if (item.attachmentFile) {
|
||||
const publicPath = 'storage/app/public/';
|
||||
const tenantPath = `${publicPath}${req.organizationId}`;
|
||||
|
||||
try {
|
||||
await fsPromises.unlink(`${tenantPath}/${item.attachmentFile}`);
|
||||
} catch (error) {
|
||||
Logger.log('error', 'Delete item attachment file delete failed.', { error });
|
||||
}
|
||||
}
|
||||
return res.status(200).send();
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// import OAuth2 from '@/http/controllers/OAuth2';
|
||||
import Authentication from '@/http/controllers/Authentication';
|
||||
import InviteUsers from '@/http/controllers/InviteUsers';
|
||||
// import Users from '@/http/controllers/Users';
|
||||
// import Roles from '@/http/controllers/Roles';
|
||||
import Items from '@/http/controllers/Items';
|
||||
@@ -28,6 +29,7 @@ import ExchangeRates from '@/http/controllers/ExchangeRates';
|
||||
export default (app) => {
|
||||
// app.use('/api/oauth2', OAuth2.router());
|
||||
app.use('/api/auth', Authentication.router());
|
||||
app.use('/api/invite', InviteUsers.router());
|
||||
app.use('/api/currencies', Currencies.router());
|
||||
// app.use('/api/users', Users.router());
|
||||
// app.use('/api/roles', Roles.router());
|
||||
|
||||
13
server/src/http/middleware/ConfiguredMiddleware.js
Normal file
13
server/src/http/middleware/ConfiguredMiddleware.js
Normal file
@@ -0,0 +1,13 @@
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
export default async (req, res, next) => {
|
||||
const { Option } = req.models;
|
||||
const option = await Option.query().where('key', 'app_configured');
|
||||
|
||||
if (option.getMeta('app_configured', false)) {
|
||||
return res.res(400).send({
|
||||
errors: [{ type: 'TENANT.NOT.CONFIGURED', code: 700 }],
|
||||
});
|
||||
}
|
||||
next();
|
||||
};
|
||||
@@ -1,5 +1,4 @@
|
||||
|
||||
|
||||
export default class DynamicFilterAbstructor {
|
||||
constructor() {
|
||||
this.filterRoles = [];
|
||||
@@ -10,9 +9,21 @@ export default class DynamicFilterAbstructor {
|
||||
this.tableName = tableName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @interface
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
buildLogicExpression() {}
|
||||
|
||||
/**
|
||||
* @interface
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
validateFilterRoles() {}
|
||||
|
||||
/**
|
||||
* @interface
|
||||
*/
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
buildQuery() {}
|
||||
}
|
||||
@@ -249,7 +249,6 @@ export default class MetableCollection {
|
||||
this.metadata.push(meta);
|
||||
}
|
||||
|
||||
|
||||
toArray() {
|
||||
return this.metadata;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { mixin } from 'objection';
|
||||
import BaseModel from '@/models/Model';
|
||||
import TenantModel from '@/models/TenantModel';
|
||||
import MetableCollection from '@/lib/Metable/MetableCollection';
|
||||
import definedOptions from '@/data/options';
|
||||
|
||||
export default class Option extends mixin(BaseModel, [mixin]) {
|
||||
export default class Option extends mixin(TenantModel, [mixin]) {
|
||||
/**
|
||||
* Table name.
|
||||
*/
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import TenantModel from '@/models/TenantModel';
|
||||
|
||||
export default class PasswordResets extends TenantModel {
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'password_resets';
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,24 @@
|
||||
import nodemailer from 'nodemailer';
|
||||
import config from '@/../config/config';
|
||||
|
||||
// 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
|
||||
host: config.mail.host,
|
||||
port: config.mail.port,
|
||||
secure: config.mail.secure, // true for 465, false for other ports
|
||||
auth: {
|
||||
user: process.env.MAIL_USERNAME,
|
||||
pass: process.env.MAIL_PASSWORD,
|
||||
user: config.mail.username,
|
||||
pass: config.mail.password,
|
||||
},
|
||||
});
|
||||
|
||||
console.log({
|
||||
host: config.mail.host,
|
||||
port: config.mail.port,
|
||||
secure: config.mail.secure, // true for 465, false for other ports
|
||||
auth: {
|
||||
user: config.mail.username,
|
||||
pass: config.mail.password,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
exports.up = (knex) => knex.schema.createTable('password_resets', (table) => {
|
||||
table.increments();
|
||||
table.string('user_id');
|
||||
table.string('email');
|
||||
table.string('token');
|
||||
table.timestamp('created_at');
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.createTable('user_invites', (table) => {
|
||||
table.increments();
|
||||
table.string('email');
|
||||
table.string('token').unique();
|
||||
table.integer('tenant_id').unsigned();
|
||||
table.timestamps();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.dropTableIfExists('user_invites');
|
||||
};
|
||||
10
server/src/system/models/Invite.js
Normal file
10
server/src/system/models/Invite.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import SystemModel from '@/system/models/SystemModel';
|
||||
|
||||
export default class UserInvite extends SystemModel {
|
||||
/**
|
||||
* Table name.
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'user_invites';
|
||||
}
|
||||
}
|
||||
10
server/src/system/models/PasswordReset.js
Normal file
10
server/src/system/models/PasswordReset.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import SystemModel from '@/system/models/SystemModel';
|
||||
|
||||
export default class PasswordResets extends SystemModel {
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'password_resets';
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ export default class SystemUser extends SystemModel {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: Tenant.default,
|
||||
join: {
|
||||
from: 'users.tenant_id',
|
||||
from: 'users.tenantId',
|
||||
to: 'tenants.id',
|
||||
},
|
||||
},
|
||||
|
||||
286
server/views/mail/ResetPassword.html
Normal file
286
server/views/mail/ResetPassword.html
Normal file
@@ -0,0 +1,286 @@
|
||||
<!DOCTYPE html>
|
||||
<head>
|
||||
<title>Moosher Reset Password</title>
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
font-family: "Roboto",sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
font-weight: 400;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
position: relative;
|
||||
background: #e0e8f3;
|
||||
min-width: 320px;
|
||||
color: #495463;
|
||||
}
|
||||
/*! Email Template Preview Purpose */
|
||||
.email-wraper {
|
||||
background: #e0e8f3;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
font-weight: 400;
|
||||
color: #5c6e82;
|
||||
width: 100%
|
||||
}
|
||||
|
||||
.email-wraper a {
|
||||
color: #253992;
|
||||
word-break: break-all
|
||||
}
|
||||
|
||||
.email-wraper .link-block {
|
||||
display: block
|
||||
}
|
||||
|
||||
.email-ul {
|
||||
margin: 5px 0;
|
||||
padding: 0
|
||||
}
|
||||
|
||||
.email-ul:not(:last-child) {
|
||||
margin-bottom: 10px
|
||||
}
|
||||
|
||||
.email-ul li {
|
||||
list-style: disc;
|
||||
list-style-position: inside
|
||||
}
|
||||
|
||||
.email-ul-col2 {
|
||||
display: flex;
|
||||
flex-wrap: wrap
|
||||
}
|
||||
|
||||
.email-ul-col2 li {
|
||||
width: 50%;
|
||||
padding-right: 10px
|
||||
}
|
||||
|
||||
.email-body {
|
||||
width: 96%;
|
||||
max-width: 620px;
|
||||
margin: 0 auto;
|
||||
background: #ffffff;
|
||||
border: 1px solid #e6effb;
|
||||
border-bottom: 4px solid #007bff;
|
||||
}
|
||||
|
||||
.email-success {
|
||||
border-bottom-color: #00d285
|
||||
}
|
||||
|
||||
.email-warning {
|
||||
border-bottom-color: #ffc100
|
||||
}
|
||||
|
||||
.email-btn {
|
||||
background: #007bff;
|
||||
border-radius: 4px;
|
||||
color: #ffffff !important;
|
||||
display: inline-block;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 44px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
padding: 0 30px
|
||||
}
|
||||
|
||||
.email-btn-sm {
|
||||
line-height: 38px
|
||||
}
|
||||
|
||||
.email-header,.email-footer {
|
||||
width: 100%;
|
||||
max-width: 620px;
|
||||
margin: 0 auto
|
||||
}
|
||||
|
||||
.email-logo {
|
||||
height: 40px
|
||||
}
|
||||
|
||||
.email-title {
|
||||
font-size: 13px;
|
||||
color: #253992;
|
||||
padding-top: 12px
|
||||
}
|
||||
|
||||
.email-heading {
|
||||
font-size: 18px;
|
||||
color: #253992;
|
||||
font-weight: 600;
|
||||
margin: 0
|
||||
}
|
||||
|
||||
.email-heading-sm {
|
||||
font-size: 16px
|
||||
}
|
||||
|
||||
.email-heading-s2 {
|
||||
font-size: 15px;
|
||||
color: #000000;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 10px
|
||||
}
|
||||
|
||||
.email-heading-s3 {
|
||||
font-size: 18px;
|
||||
color: #1babfe;
|
||||
font-weight: 400;
|
||||
margin-bottom: 8px
|
||||
}
|
||||
|
||||
.email-heading-success {
|
||||
color: #00d285
|
||||
}
|
||||
|
||||
.email-heading-warning {
|
||||
color: #ffc100
|
||||
}
|
||||
|
||||
.email-note {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
line-height: 22px;
|
||||
color: #6e81a9
|
||||
}
|
||||
|
||||
.email-copyright-text {
|
||||
font-size: 13px
|
||||
}
|
||||
|
||||
.email-social li {
|
||||
display: inline-block;
|
||||
padding: 4px
|
||||
}
|
||||
|
||||
.email-social li a {
|
||||
display: inline-block;
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
border-radius: 50%;
|
||||
background: #ffffff
|
||||
}
|
||||
|
||||
.email-social li a img {
|
||||
width: 30px
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.email-preview-page .card {
|
||||
border-radius:0;
|
||||
margin-left: -20px;
|
||||
margin-right: -20px
|
||||
}
|
||||
|
||||
.email-ul-col2 li {
|
||||
width: 100%
|
||||
}
|
||||
}
|
||||
|
||||
.pdb-4x {
|
||||
padding-bottom: 40px
|
||||
}
|
||||
.pdt-4x {
|
||||
padding-top: 40px
|
||||
}
|
||||
.pdb-2-5x {
|
||||
padding-bottom: 25px
|
||||
}
|
||||
.pd-3x {
|
||||
padding: 30px
|
||||
}
|
||||
.pdb-1-5x {
|
||||
padding-bottom: 15px
|
||||
}
|
||||
.pdb-2x {
|
||||
padding-bottom: 20px
|
||||
}
|
||||
.pdl-2x {
|
||||
padding-left: 20px
|
||||
}
|
||||
.pdt-2-5x {
|
||||
padding-top: 25px
|
||||
}
|
||||
.text-center{
|
||||
text-align: center;
|
||||
}
|
||||
.pdt-0{
|
||||
padding-top: 0;
|
||||
}
|
||||
.pt-0{
|
||||
padding-top: 0;
|
||||
}
|
||||
.mgb-2-5x {
|
||||
margin-bottom: 25px
|
||||
}
|
||||
|
||||
.mt-0{
|
||||
margin-top: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<table class="email-wraper">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="pdt-4x pdb-4x">
|
||||
<table class="email-header">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-center pdb-2-5x">
|
||||
<a href="#">
|
||||
<img src="https://www.moosher.ly/logo-email.png" alt="Moosher" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="email-body">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-center pd-3x pdb-1-5x">
|
||||
<h2 class="email-heading">Reset Your Password</h2>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-center pd-3x pt-0 pdb-2x">
|
||||
<p class="mgb-1x">Hi {{ first_name }} {{ last_name }},</p>
|
||||
<p class="mgb-2-5x">Click On The link blow to reset your password.</p>
|
||||
<a href="{{ url }}" class="email-btn" target="_blank">Reset Password</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-center pd-3x pdt-0 pdb-4x">
|
||||
<p>If you did not make this request, please contact us or ignore this message.</p>
|
||||
<p class="email-note">This is an automatically generated email please do not reply to
|
||||
this email. If you face any issues, please contact us at {{ contact_us_email }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="email-footer">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-center pdt-2-5x pdl-2x pdr-2x">
|
||||
<p class="email-copyright-text mt-0">Copyright © 2019 Moosher.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
289
server/views/mail/UserInvite.html
Normal file
289
server/views/mail/UserInvite.html
Normal file
@@ -0,0 +1,289 @@
|
||||
<!DOCTYPE html>
|
||||
<head>
|
||||
<title>Moosher Reset Password</title>
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
font-family: "Roboto",sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
font-weight: 400;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
position: relative;
|
||||
background: #e0e8f3;
|
||||
min-width: 320px;
|
||||
color: #495463;
|
||||
}
|
||||
/*! Email Template Preview Purpose */
|
||||
.email-wraper {
|
||||
background: #e0e8f3;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
font-weight: 400;
|
||||
color: #5c6e82;
|
||||
width: 100%
|
||||
}
|
||||
|
||||
.email-wraper a {
|
||||
color: #253992;
|
||||
word-break: break-all
|
||||
}
|
||||
|
||||
.email-wraper .link-block {
|
||||
display: block
|
||||
}
|
||||
|
||||
.email-ul {
|
||||
margin: 5px 0;
|
||||
padding: 0
|
||||
}
|
||||
|
||||
.email-ul:not(:last-child) {
|
||||
margin-bottom: 10px
|
||||
}
|
||||
|
||||
.email-ul li {
|
||||
list-style: disc;
|
||||
list-style-position: inside
|
||||
}
|
||||
|
||||
.email-ul-col2 {
|
||||
display: flex;
|
||||
flex-wrap: wrap
|
||||
}
|
||||
|
||||
.email-ul-col2 li {
|
||||
width: 50%;
|
||||
padding-right: 10px
|
||||
}
|
||||
|
||||
.email-body {
|
||||
width: 96%;
|
||||
max-width: 620px;
|
||||
margin: 0 auto;
|
||||
background: #ffffff;
|
||||
border: 1px solid #e6effb;
|
||||
border-bottom: 4px solid #007bff;
|
||||
}
|
||||
|
||||
.email-success {
|
||||
border-bottom-color: #00d285
|
||||
}
|
||||
|
||||
.email-warning {
|
||||
border-bottom-color: #ffc100
|
||||
}
|
||||
|
||||
.email-btn {
|
||||
background: #007bff;
|
||||
border-radius: 4px;
|
||||
color: #ffffff !important;
|
||||
display: inline-block;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 44px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
padding: 0 30px
|
||||
}
|
||||
|
||||
.email-btn-sm {
|
||||
line-height: 38px
|
||||
}
|
||||
|
||||
.email-header,.email-footer {
|
||||
width: 100%;
|
||||
max-width: 620px;
|
||||
margin: 0 auto
|
||||
}
|
||||
|
||||
.email-logo {
|
||||
height: 40px
|
||||
}
|
||||
|
||||
.email-title {
|
||||
font-size: 13px;
|
||||
color: #253992;
|
||||
padding-top: 12px
|
||||
}
|
||||
|
||||
.email-heading {
|
||||
font-size: 18px;
|
||||
color: #253992;
|
||||
font-weight: 600;
|
||||
margin: 0
|
||||
}
|
||||
|
||||
.email-heading-sm {
|
||||
font-size: 16px
|
||||
}
|
||||
|
||||
.email-heading-s2 {
|
||||
font-size: 15px;
|
||||
color: #000000;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 10px
|
||||
}
|
||||
|
||||
.email-heading-s3 {
|
||||
font-size: 18px;
|
||||
color: #1babfe;
|
||||
font-weight: 400;
|
||||
margin-bottom: 8px
|
||||
}
|
||||
|
||||
.email-heading-success {
|
||||
color: #00d285
|
||||
}
|
||||
|
||||
.email-heading-warning {
|
||||
color: #ffc100
|
||||
}
|
||||
|
||||
.email-note {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
line-height: 22px;
|
||||
color: #6e81a9
|
||||
}
|
||||
|
||||
.email-copyright-text {
|
||||
font-size: 13px
|
||||
}
|
||||
|
||||
.email-social li {
|
||||
display: inline-block;
|
||||
padding: 4px
|
||||
}
|
||||
|
||||
.email-social li a {
|
||||
display: inline-block;
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
border-radius: 50%;
|
||||
background: #ffffff
|
||||
}
|
||||
|
||||
.email-social li a img {
|
||||
width: 30px
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.email-preview-page .card {
|
||||
border-radius:0;
|
||||
margin-left: -20px;
|
||||
margin-right: -20px
|
||||
}
|
||||
|
||||
.email-ul-col2 li {
|
||||
width: 100%
|
||||
}
|
||||
}
|
||||
|
||||
.pdb-4x {
|
||||
padding-bottom: 40px
|
||||
}
|
||||
.pdt-4x {
|
||||
padding-top: 40px
|
||||
}
|
||||
.pdb-2-5x {
|
||||
padding-bottom: 25px
|
||||
}
|
||||
.pd-3x {
|
||||
padding: 30px
|
||||
}
|
||||
.pdb-1-5x {
|
||||
padding-bottom: 15px
|
||||
}
|
||||
.pdb-2x {
|
||||
padding-bottom: 20px
|
||||
}
|
||||
.pdl-2x {
|
||||
padding-left: 20px
|
||||
}
|
||||
.pdt-2-5x {
|
||||
padding-top: 25px
|
||||
}
|
||||
.text-center{
|
||||
text-align: center;
|
||||
}
|
||||
.pdt-0{
|
||||
padding-top: 0;
|
||||
}
|
||||
.pt-0{
|
||||
padding-top: 0;
|
||||
}
|
||||
.mgb-2-5x {
|
||||
margin-bottom: 25px
|
||||
}
|
||||
|
||||
.mt-0{
|
||||
margin-top: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<table class="email-wraper">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="pdt-4x pdb-4x">
|
||||
<table class="email-header">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-center pdb-2-5x">
|
||||
<a href="#">
|
||||
<img src="https://www.moosher.ly/logo-email.png" alt="Moosher" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="email-body">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-center pd-3x pdb-1-5x">
|
||||
<h2 class="email-heading">Join {{ organizationName }} on Bigcapital</h2>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-center pd-3x pt-0 pdb-2x">
|
||||
|
||||
<p class="mgb-2-5x">
|
||||
{{ fullName }} ({{ email }}) has invited you to join the Bigcapital {{ organizationName }}.
|
||||
Join now to start collaborating!
|
||||
</p>
|
||||
<a href="{{ url }}" class="email-btn" target="_blank">Reset Password</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-center pd-3x pdt-0 pdb-4x">
|
||||
<p>If you did not make this request, please contact us or ignore this message.</p>
|
||||
<p class="email-note">This is an automatically generated email please do not reply to
|
||||
this email. If you face any issues, please contact us at {{ contact_us_email }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="email-footer">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="text-center pdt-2-5x pdl-2x pdr-2x">
|
||||
<p class="email-copyright-text mt-0">Copyright © 2019 Moosher.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user