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 + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + \ No newline at end of file diff --git a/server/views/mail/UserInvite.html b/server/views/mail/UserInvite.html new file mode 100644 index 000000000..64d73ebf7 --- /dev/null +++ b/server/views/mail/UserInvite.html @@ -0,0 +1,289 @@ + + + Moosher Reset Password + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + \ No newline at end of file