diff --git a/server/package.json b/server/package.json
index cca066143..ebacb4880 100644
--- a/server/package.json
+++ b/server/package.json
@@ -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",
diff --git a/server/src/app.js b/server/src/app.js
index 27244fb56..29f749873 100644
--- a/server/src/app.js
+++ b/server/src/app.js
@@ -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);
diff --git a/server/src/database/migrations/20190822214242_create_users_table.js b/server/src/database/migrations/20190822214242_create_users_table.js
index dd4260bcc..5e2117e50 100644
--- a/server/src/database/migrations/20190822214242_create_users_table.js
+++ b/server/src/database/migrations/20190822214242_create_users_table.js
@@ -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');
diff --git a/server/src/database/migrations/20190822214303_create_items_table.js b/server/src/database/migrations/20190822214303_create_items_table.js
index c01186c82..9c0e6aa7b 100644
--- a/server/src/database/migrations/20190822214303_create_items_table.js
+++ b/server/src/database/migrations/20190822214303_create_items_table.js
@@ -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();
});
};
diff --git a/server/src/database/migrations/20200105195823_create_manual_journals_table.js b/server/src/database/migrations/20200105195823_create_manual_journals_table.js
index 93cc9cbdf..9fef2ee22 100644
--- a/server/src/database/migrations/20200105195823_create_manual_journals_table.js
+++ b/server/src/database/migrations/20200105195823_create_manual_journals_table.js
@@ -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();
});
diff --git a/server/src/http/controllers/Accounting.js b/server/src/http/controllers/Accounting.js
index 94e097ede..2a738c07d 100644
--- a/server/src/http/controllers/Accounting.js
+++ b/server/src/http/controllers/Accounting.js
@@ -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(),
diff --git a/server/src/http/controllers/Authentication.js b/server/src/http/controllers/Authentication.js
index 926ea2ea8..79af8d14c 100644
--- a/server/src/http/controllers/Authentication.js
+++ b/server/src/http/controllers/Authentication.js
@@ -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({});
},
diff --git a/server/src/http/controllers/InviteUsers.js b/server/src/http/controllers/InviteUsers.js
new file mode 100644
index 000000000..945fb4038
--- /dev/null
+++ b/server/src/http/controllers/InviteUsers.js
@@ -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();
+ }
+ }
+}
\ No newline at end of file
diff --git a/server/src/http/controllers/Items.js b/server/src/http/controllers/Items.js
index f2a5bfdb4..6fd0aab3e 100644
--- a/server/src/http/controllers/Items.js
+++ b/server/src/http/controllers/Items.js
@@ -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();
},
},
diff --git a/server/src/http/index.js b/server/src/http/index.js
index 9ea7cbc38..5a10ed6d8 100644
--- a/server/src/http/index.js
+++ b/server/src/http/index.js
@@ -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());
diff --git a/server/src/http/middleware/ConfiguredMiddleware.js b/server/src/http/middleware/ConfiguredMiddleware.js
new file mode 100644
index 000000000..899053d91
--- /dev/null
+++ b/server/src/http/middleware/ConfiguredMiddleware.js
@@ -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();
+};
diff --git a/server/src/lib/DynamicFilter/DynamicFilterRoleAbstructor.js b/server/src/lib/DynamicFilter/DynamicFilterRoleAbstructor.js
index e7ee46bd5..fcb553867 100644
--- a/server/src/lib/DynamicFilter/DynamicFilterRoleAbstructor.js
+++ b/server/src/lib/DynamicFilter/DynamicFilterRoleAbstructor.js
@@ -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() {}
}
\ No newline at end of file
diff --git a/server/src/lib/Metable/MetableCollection.js b/server/src/lib/Metable/MetableCollection.js
index f6e5a31fb..947cca6bd 100644
--- a/server/src/lib/Metable/MetableCollection.js
+++ b/server/src/lib/Metable/MetableCollection.js
@@ -249,7 +249,6 @@ export default class MetableCollection {
this.metadata.push(meta);
}
-
toArray() {
return this.metadata;
}
diff --git a/server/src/models/Option.js b/server/src/models/Option.js
index ff8c033ea..0f9386906 100644
--- a/server/src/models/Option.js
+++ b/server/src/models/Option.js
@@ -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.
*/
diff --git a/server/src/models/PasswordReset.js b/server/src/models/PasswordReset.js
deleted file mode 100644
index 58b45eeae..000000000
--- a/server/src/models/PasswordReset.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import TenantModel from '@/models/TenantModel';
-
-export default class PasswordResets extends TenantModel {
- /**
- * Table name
- */
- static get tableName() {
- return 'password_resets';
- }
-}
diff --git a/server/src/services/mail.js b/server/src/services/mail.js
index 3a4f6b3ac..6beded076 100644
--- a/server/src/services/mail.js
+++ b/server/src/services/mail.js
@@ -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,
},
});
diff --git a/server/src/database/migrations/20190104195900_create_password_resets_table.js b/server/src/system/migrations/20190104195900_create_password_resets_table.js
similarity index 89%
rename from server/src/database/migrations/20190104195900_create_password_resets_table.js
rename to server/src/system/migrations/20190104195900_create_password_resets_table.js
index 64b67e4cc..bd274950e 100644
--- a/server/src/database/migrations/20190104195900_create_password_resets_table.js
+++ b/server/src/system/migrations/20190104195900_create_password_resets_table.js
@@ -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');
});
diff --git a/server/src/system/migrations/20200422225247_create_user_invites_table.js b/server/src/system/migrations/20200422225247_create_user_invites_table.js
new file mode 100644
index 000000000..59caaa32b
--- /dev/null
+++ b/server/src/system/migrations/20200422225247_create_user_invites_table.js
@@ -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');
+};
diff --git a/server/src/system/models/Invite.js b/server/src/system/models/Invite.js
new file mode 100644
index 000000000..243e3e2f2
--- /dev/null
+++ b/server/src/system/models/Invite.js
@@ -0,0 +1,10 @@
+import SystemModel from '@/system/models/SystemModel';
+
+export default class UserInvite extends SystemModel {
+ /**
+ * Table name.
+ */
+ static get tableName() {
+ return 'user_invites';
+ }
+}
diff --git a/server/src/system/models/PasswordReset.js b/server/src/system/models/PasswordReset.js
new file mode 100644
index 000000000..d5a52ffa0
--- /dev/null
+++ b/server/src/system/models/PasswordReset.js
@@ -0,0 +1,10 @@
+import SystemModel from '@/system/models/SystemModel';
+
+export default class PasswordResets extends SystemModel {
+ /**
+ * Table name
+ */
+ static get tableName() {
+ return 'password_resets';
+ }
+}
diff --git a/server/src/system/models/SystemUser.js b/server/src/system/models/SystemUser.js
index dd1c820cd..4f470907b 100644
--- a/server/src/system/models/SystemUser.js
+++ b/server/src/system/models/SystemUser.js
@@ -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',
},
},
diff --git a/server/views/mail/ResetPassword.html b/server/views/mail/ResetPassword.html
new file mode 100644
index 000000000..ea7014c3c
--- /dev/null
+++ b/server/views/mail/ResetPassword.html
@@ -0,0 +1,286 @@
+
+
+ Moosher Reset Password
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Reset Your Password
+ |
+
+
+ |
+ Hi {{ first_name }} {{ last_name }},
+ Click On The link blow to reset your password.
+ Reset Password
+ |
+
+
+ |
+ If you did not make this request, please contact us or ignore this message.
+ 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 }}
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+
+