├── favicon.ico ├── database ├── scripts │ ├── execute.bat │ ├── 190321_01.sql │ ├── 180816_01.sql │ ├── 180603_01.sql │ ├── 180722_01.sql │ ├── 181003_01.sql │ ├── 180825_01.sql │ ├── 180709_01.sql │ ├── 180804_01.sql │ ├── 180420_01.sql │ ├── 190104_01.sql │ ├── 180716_02.sql │ ├── 180601_03.sql │ ├── 180601_01.sql │ ├── 180601_02.sql │ ├── 181228_01.sql │ ├── 180720_02.sql │ ├── 180523_01.sql │ ├── 180602_01.sql │ ├── 190313_01.sql │ ├── 190201_01.sql │ ├── 180708_01.sql │ ├── 180821_01.sql │ ├── 190110_01.sql │ ├── 180524_01.sql │ ├── 190109_01.sql │ ├── 190109_02.sql │ ├── 180517_01.sql │ ├── 180716_03.sql │ ├── 180720_01.sql │ ├── 180811_01.sql │ ├── 180805_01.sql │ ├── 180801_01.sql │ ├── 180804_02.sql │ ├── 180716_01.sql │ └── 190311_01.sql └── readme.md ├── distribution ├── images │ ├── avatar.png │ ├── loading.gif │ ├── logo_dark.png │ ├── logo_light.png │ ├── loading_spinner_small.gif │ ├── empty_image.svg │ ├── avatar.svg │ ├── logo_light.svg │ └── logo_dark.svg ├── index.html └── loader.min.js ├── documentation └── images │ ├── alert.png │ ├── vscode.png │ ├── database.png │ ├── features.png │ ├── userroles.png │ ├── failure_message.png │ ├── heroku-deploy-1.png │ ├── heroku-deploy-2.png │ ├── heroku-deploy-3.png │ ├── heroku-deploy-4.png │ ├── heroku-deploy-5.png │ ├── heroku-deploy-6.png │ ├── heroku-deploy-7.png │ ├── input_checkbox.png │ ├── success_message.png │ ├── input_inputField.png │ ├── redux-user-store.png │ ├── input_workspaceField.png │ └── success_notification.png ├── .prettierrc ├── client ├── common │ ├── styles │ │ ├── modules │ │ │ ├── _reset.scss │ │ │ ├── _alignment.scss │ │ │ ├── _borders.scss │ │ │ ├── _spacing.scss │ │ │ ├── _containers.scss │ │ │ ├── _images.scss │ │ │ └── _bootstrap_variations.scss │ │ └── entry.scss │ ├── components │ │ ├── Loading.js │ │ ├── User.js │ │ ├── AsyncComponent.js │ │ ├── ServerSuccess.js │ │ ├── SuccessNotification.js │ │ ├── AppOffline.js │ │ ├── inputs │ │ │ ├── Checkbox.js │ │ │ ├── WorkspaceURLField.js │ │ │ ├── InputField.js │ │ │ ├── TextArea.js │ │ │ └── ColorPicker.js │ │ ├── Alert.js │ │ ├── MissingPath.js │ │ ├── ServerError.js │ │ ├── GoogleAnalytics.js │ │ ├── Modal.js │ │ └── HideComponent.js │ ├── media │ │ └── icons │ │ │ ├── Profile.js │ │ │ ├── flags │ │ │ ├── Italy.js │ │ │ └── England.js │ │ │ ├── CreditCard.js │ │ │ ├── Help.js │ │ │ ├── BurgerMenu.js │ │ │ ├── Upload.js │ │ │ ├── Logout.js │ │ │ ├── Overview.js │ │ │ ├── Settings.js │ │ │ └── Logo.js │ ├── layouts │ │ ├── EmptyLayout.js │ │ └── DefaultLayout.js │ ├── store │ │ ├── store.js │ │ └── reducers │ │ │ └── language.js │ └── fetch.js ├── api │ ├── language.js │ ├── billing.js │ ├── profile.js │ ├── settings.js │ └── authentication.js ├── modules │ ├── header │ │ └── components │ │ │ ├── NavLogo.js │ │ │ ├── NavProfileMenuLogo.js │ │ │ ├── NavDropdownLink.js │ │ │ ├── NavMenuLink.js │ │ │ ├── ActiveTrial.js │ │ │ ├── HelpCaller.js │ │ │ ├── NavProfileMenu.js │ │ │ └── VerifyEmail.js │ ├── profile │ │ ├── components │ │ │ └── Avatar.js │ │ └── index.js │ ├── authentication │ │ ├── components │ │ │ ├── ClientStyling.js │ │ │ ├── WorkspaceURL.js │ │ │ └── LanguageSwitcher.js │ │ └── index.js │ ├── settings │ │ └── components │ │ │ ├── MenuLink.js │ │ │ └── Appearance │ │ │ └── components │ │ │ └── ImageField.js │ └── billing │ │ ├── components │ │ └── NewSubscription │ │ │ └── components │ │ │ └── InjectedCardForm.js │ │ └── index.js └── index.js ├── shared ├── utilities │ ├── date.js │ ├── queryStrings.js │ ├── securityToken.js │ ├── domains.js │ └── filters.js ├── translations │ ├── links │ │ ├── en.json │ │ └── it.json │ └── i18n.js ├── validation │ ├── validate.js │ ├── settings.js │ └── profile.js └── constants.js ├── server ├── services │ ├── stripe.js │ ├── sentry.js │ ├── router.js │ ├── devmiddleware.js │ ├── sequelize.js │ ├── papertrail.js │ ├── redis.js │ └── nodemailer.js ├── utilities │ ├── errors │ │ └── serverResponseError.js │ └── browserResponseLng.js ├── models │ ├── languages.js │ ├── billingIntervals.js │ ├── currencies.js │ ├── roles.js │ ├── features.js │ ├── emailTypes.js │ ├── subscriptionFeatures.js │ ├── subscriptions.js │ ├── userRoles.js │ ├── passwordReset.js │ ├── emailVerificationCode.js │ ├── changeEmailAddress.js │ ├── emailTemplates.js │ ├── clientStyling.js │ ├── sentEmails.js │ ├── failedEmails.js │ ├── plans.js │ ├── client.js │ └── user.js ├── controller │ └── billing.js └── orchestrator │ └── billing.js ├── ecosystem.config.js ├── .eslintrc.json ├── .babelrc ├── .gitignore ├── license.md ├── config.env └── changelog.md /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterjoseph/Reeve/HEAD/favicon.ico -------------------------------------------------------------------------------- /database/scripts/execute.bat: -------------------------------------------------------------------------------- 1 | for %%G in (*.sql) do sqlcmd /S servername /d reeve -E -i"%%G" 2 | pause -------------------------------------------------------------------------------- /distribution/images/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterjoseph/Reeve/HEAD/distribution/images/avatar.png -------------------------------------------------------------------------------- /documentation/images/alert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterjoseph/Reeve/HEAD/documentation/images/alert.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "printWidth": 200, 4 | "tabWidth": 4, 5 | "singleQuote": false 6 | } 7 | -------------------------------------------------------------------------------- /client/common/styles/modules/_reset.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | background-color: #fafafa; 5 | } 6 | -------------------------------------------------------------------------------- /distribution/images/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterjoseph/Reeve/HEAD/distribution/images/loading.gif -------------------------------------------------------------------------------- /documentation/images/vscode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterjoseph/Reeve/HEAD/documentation/images/vscode.png -------------------------------------------------------------------------------- /distribution/images/logo_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterjoseph/Reeve/HEAD/distribution/images/logo_dark.png -------------------------------------------------------------------------------- /distribution/images/logo_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterjoseph/Reeve/HEAD/distribution/images/logo_light.png -------------------------------------------------------------------------------- /documentation/images/database.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterjoseph/Reeve/HEAD/documentation/images/database.png -------------------------------------------------------------------------------- /documentation/images/features.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterjoseph/Reeve/HEAD/documentation/images/features.png -------------------------------------------------------------------------------- /documentation/images/userroles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterjoseph/Reeve/HEAD/documentation/images/userroles.png -------------------------------------------------------------------------------- /documentation/images/failure_message.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterjoseph/Reeve/HEAD/documentation/images/failure_message.png -------------------------------------------------------------------------------- /documentation/images/heroku-deploy-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterjoseph/Reeve/HEAD/documentation/images/heroku-deploy-1.png -------------------------------------------------------------------------------- /documentation/images/heroku-deploy-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterjoseph/Reeve/HEAD/documentation/images/heroku-deploy-2.png -------------------------------------------------------------------------------- /documentation/images/heroku-deploy-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterjoseph/Reeve/HEAD/documentation/images/heroku-deploy-3.png -------------------------------------------------------------------------------- /documentation/images/heroku-deploy-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterjoseph/Reeve/HEAD/documentation/images/heroku-deploy-4.png -------------------------------------------------------------------------------- /documentation/images/heroku-deploy-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterjoseph/Reeve/HEAD/documentation/images/heroku-deploy-5.png -------------------------------------------------------------------------------- /documentation/images/heroku-deploy-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterjoseph/Reeve/HEAD/documentation/images/heroku-deploy-6.png -------------------------------------------------------------------------------- /documentation/images/heroku-deploy-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterjoseph/Reeve/HEAD/documentation/images/heroku-deploy-7.png -------------------------------------------------------------------------------- /documentation/images/input_checkbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterjoseph/Reeve/HEAD/documentation/images/input_checkbox.png -------------------------------------------------------------------------------- /documentation/images/success_message.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterjoseph/Reeve/HEAD/documentation/images/success_message.png -------------------------------------------------------------------------------- /documentation/images/input_inputField.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterjoseph/Reeve/HEAD/documentation/images/input_inputField.png -------------------------------------------------------------------------------- /documentation/images/redux-user-store.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterjoseph/Reeve/HEAD/documentation/images/redux-user-store.png -------------------------------------------------------------------------------- /distribution/images/loading_spinner_small.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterjoseph/Reeve/HEAD/distribution/images/loading_spinner_small.gif -------------------------------------------------------------------------------- /documentation/images/input_workspaceField.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterjoseph/Reeve/HEAD/documentation/images/input_workspaceField.png -------------------------------------------------------------------------------- /documentation/images/success_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterjoseph/Reeve/HEAD/documentation/images/success_notification.png -------------------------------------------------------------------------------- /client/common/styles/modules/_alignment.scss: -------------------------------------------------------------------------------- 1 | .alignment { 2 | display: flex; 3 | min-height: 100%; 4 | min-height: 100vh; 5 | 6 | &.vertical { 7 | align-items: center; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /shared/utilities/date.js: -------------------------------------------------------------------------------- 1 | // Generate a date in MySQL dateTime format 2 | export function generateDate() { 3 | return new Date() 4 | .toISOString() 5 | .slice(0, 19) 6 | .replace("T", " "); 7 | } 8 | -------------------------------------------------------------------------------- /client/common/styles/modules/_borders.scss: -------------------------------------------------------------------------------- 1 | .border { 2 | &.light { 3 | border: 1px solid rgba(0, 0, 0, 0.125) !important; 4 | } 5 | 6 | &.dark { 7 | border: 1px solid rgba(0, 0, 0, 0.6) !important; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /shared/translations/links/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "supportArticles": { 3 | "overview": "#", 4 | "profile": "#", 5 | "billing": "#", 6 | "settings": "#" 7 | }, 8 | "termsAndConditions": "#", 9 | "privacyPolicy": "#" 10 | } 11 | -------------------------------------------------------------------------------- /shared/translations/links/it.json: -------------------------------------------------------------------------------- 1 | { 2 | "supportArticles": { 3 | "overview": "#", 4 | "profile": "#", 5 | "billing": "#", 6 | "settings": "#" 7 | }, 8 | "termsAndConditions": "#", 9 | "privacyPolicy": "#" 10 | } 11 | -------------------------------------------------------------------------------- /database/scripts/190321_01.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO `executedScripts` (`name`, `description`, `createdDate`, `executedDate`) 2 | VALUES 3 | ('190321_01', 'Added description on client table', NOW(), NOW()); 4 | 5 | ALTER TABLE `client` 6 | ADD COLUMN `description` varchar(255) NULL; -------------------------------------------------------------------------------- /database/scripts/180816_01.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO `executedScripts` (`name`, `description`, `createdDate`, `executedDate`) 2 | VALUES 3 | ('180816_01', 'Profile photo table column', NOW(), NOW()); 4 | 5 | ALTER TABLE `user` 6 | ADD COLUMN `profilePhoto` varchar(255) DEFAULT NULL; 7 | -------------------------------------------------------------------------------- /database/scripts/180603_01.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO `executedScripts` (`name`, `description`, `createdDate`, `executedDate`) 2 | VALUES 3 | ('180603_01', 'Set subscription id as required', NOW(), NOW()); 4 | 5 | ALTER TABLE `client` 6 | MODIFY COLUMN `subscriptionId` int(3) NOT NULL DEFAULT 1; -------------------------------------------------------------------------------- /database/scripts/180722_01.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO `executedScripts` (`name`, `description`, `createdDate`, `executedDate`) 2 | VALUES 3 | ('180722_01', 'Added emailVerified column to user table', NOW(), NOW()); 4 | 5 | ALTER TABLE `user` 6 | ADD COLUMN `emailVerified` tinyint(1) NOT NULL DEFAULT 0; -------------------------------------------------------------------------------- /server/services/stripe.js: -------------------------------------------------------------------------------- 1 | let config = require("../../config"); 2 | const stripe = require("stripe")(config.stripe.apiKey); 3 | 4 | function initialize(app) { 5 | if (!config.stripe.enabled) { 6 | return; 7 | } 8 | } 9 | 10 | module.exports = { 11 | initialize: initialize 12 | }; 13 | -------------------------------------------------------------------------------- /server/utilities/errors/serverResponseError.js: -------------------------------------------------------------------------------- 1 | export function ServerResponseError(status, message, reason) { 2 | this.name = "ServerResponseError"; 3 | this.status = status || 500; 4 | this.message = message || ""; 5 | this.reason = reason || ""; 6 | } 7 | 8 | ServerResponseError.prototype = Error.prototype; 9 | -------------------------------------------------------------------------------- /client/api/language.js: -------------------------------------------------------------------------------- 1 | import fetch from "common/fetch"; 2 | 3 | // Change User Language 4 | export function changeUserLanguage(body) { 5 | return fetch.perform("/api/v1.0/language/change-user-language", { 6 | method: "POST", 7 | body: JSON.stringify({ 8 | language: body.language 9 | }) 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /database/scripts/181003_01.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO `executedScripts` (`name`, `description`, `createdDate`, `executedDate`) 2 | VALUES 3 | ('181003_01', 'Add italian language to default language set', NOW(), NOW()); 4 | 5 | INSERT INTO `languages` (`id`, `language`, `createdAt`, `updatedAt`) 6 | VALUES 7 | (2, 'italian', NOW(), NOW()); 8 | -------------------------------------------------------------------------------- /database/scripts/180825_01.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO `executedScripts` (`name`, `description`, `createdDate`, `executedDate`) 2 | VALUES 3 | ('180825_01', 'Swap subscription start and end dates to dateTime', NOW(), NOW()); 4 | 5 | ALTER TABLE `client` 6 | MODIFY COLUMN `subscriptionStartDate` DATETIME, 7 | MODIFY COLUMN `subscriptionEndDate` DATETIME; -------------------------------------------------------------------------------- /ecosystem.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps: [ 3 | { 4 | name: "node.js-server (worker)", 5 | script: "server/server.js", 6 | env: { 7 | COMMON_VARIABLE: "true" 8 | }, 9 | env_production: { 10 | NODE_ENV: "production" 11 | }, 12 | instances: "max", 13 | exec_mode: "cluster" 14 | } 15 | ] 16 | }; 17 | -------------------------------------------------------------------------------- /client/common/components/Loading.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import Pace from "react-pace-progress"; 3 | 4 | class Loading extends Component { 5 | render() { 6 | return ( 7 |
8 | 9 |
10 | ); 11 | } 12 | } 13 | 14 | export default Loading; 15 | -------------------------------------------------------------------------------- /database/scripts/180709_01.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO `executedScripts` (`name`, `description`, `createdDate`, `executedDate`) 2 | VALUES 3 | ('180708_01', 'Added createdAt and updatedAt columns to subscriptionFeatures', NOW(), NOW()); 4 | 5 | ALTER TABLE `subscriptionFeatures` ADD COLUMN `createdAt` date NULL; 6 | ALTER TABLE `subscriptionFeatures` ADD COLUMN `updatedAt` date NULL; -------------------------------------------------------------------------------- /server/utilities/browserResponseLng.js: -------------------------------------------------------------------------------- 1 | import { LANGUAGE_CODES, RESTRICTED_LANGUAGES } from "shared/constants"; 2 | 3 | // Load accept-language from req header and validate for accuracy 4 | export default function(req) { 5 | if (RESTRICTED_LANGUAGES.includes(req.headers["accept-language"])) { 6 | return req.headers["accept-language"]; 7 | } 8 | return LANGUAGE_CODES[1]; 9 | } 10 | -------------------------------------------------------------------------------- /client/common/styles/entry.scss: -------------------------------------------------------------------------------- 1 | // Bootstrap 2 | @import "modules/bootstrap_variations"; 3 | @import "~bootstrap/scss/bootstrap"; 4 | 5 | // Custom component styling 6 | @import "modules/reset"; 7 | @import "modules/spacing"; 8 | @import "modules/containers"; 9 | @import "modules/borders"; 10 | @import "modules/alignment"; 11 | @import "modules/images"; 12 | @import "modules/custom_components"; 13 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["standard", "eslint:recommended", "plugin:react/recommended"], 3 | "parser": "babel-eslint", 4 | "rules": { 5 | "quotes": [2, "double"], 6 | "semi": [1, "always"], 7 | "space-before-function-paren": [1, "never"], 8 | "code": false, 9 | "indent": [2, "tab", { "SwitchCase": 1 }], 10 | "no-tabs": 0 11 | }, 12 | "globals": { 13 | "fetch": false 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /client/modules/header/components/NavLogo.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | class NavLogo extends Component { 5 | render() { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | } 12 | } 13 | 14 | export default NavLogo; 15 | -------------------------------------------------------------------------------- /database/scripts/180804_01.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO `executedScripts` (`name`, `description`, `createdDate`, `executedDate`) 2 | VALUES 3 | ('180804_01', 'Forgot Account Details Email', NOW(), NOW()); 4 | 5 | INSERT INTO `emailTypes` (`id`, `name`, `description`, `createdAt`, `updatedAt`) 6 | VALUES 7 | (3, 'Forgot Account Details', 'Email with workspace url and reset account links for all users associated with an email address', NOW(), NOW()); 8 | 9 | -------------------------------------------------------------------------------- /server/services/sentry.js: -------------------------------------------------------------------------------- 1 | let config = require("../../config"); 2 | 3 | function initialize(app) { 4 | if (!config.sentry.enabled && config.build.environment !== "production") { 5 | return; 6 | } 7 | 8 | let raven = require("raven"); 9 | raven.config(config.sentry.dns).install(); 10 | app.use(raven.requestHandler()); 11 | app.use(raven.errorHandler()); 12 | } 13 | 14 | module.exports = { 15 | initialize: initialize 16 | }; 17 | -------------------------------------------------------------------------------- /client/common/media/icons/Profile.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const ProfileIcon = props => { 4 | return ( 5 | 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default ProfileIcon; 13 | -------------------------------------------------------------------------------- /client/common/media/icons/flags/Italy.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const ItalyFlagIcon = props => { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | }; 14 | 15 | export default ItalyFlagIcon; 16 | -------------------------------------------------------------------------------- /database/scripts/180420_01.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `executedScripts` ( 2 | `name` varchar(255) NOT NULL DEFAULT '', 3 | `description` varchar(255) NOT NULL DEFAULT '', 4 | `createdDate` datetime NOT NULL, 5 | `executedDate` datetime NOT NULL 6 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 7 | 8 | INSERT INTO `executedScripts` (`name`, `description`, `createdDate`, `executedDate`) 9 | VALUES 10 | ('180420_01', 'Create and populate executed_scripts table', NOW(), NOW()); -------------------------------------------------------------------------------- /client/common/media/icons/CreditCard.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const CreditCardIcon = props => { 4 | return ( 5 | 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default CreditCardIcon; 13 | -------------------------------------------------------------------------------- /database/scripts/190104_01.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO `executedScripts` (`name`, `description`, `createdDate`, `executedDate`) 2 | VALUES 3 | ('190104_01', 'Insert 3 new columns in the plans table', NOW(), NOW()); 4 | 5 | ALTER TABLE `plans` 6 | ADD COLUMN `currency` CHAR(3) NOT NULL DEFAULT 'aud'; 7 | 8 | ALTER TABLE `plans` 9 | ADD COLUMN `monthlyPrice` decimal(6,2) NOT NULL DEFAULT '0.0'; 10 | 11 | ALTER TABLE `plans` 12 | ADD COLUMN `yearlyPrice` decimal(6,2) NOT NULL DEFAULT '0.0'; -------------------------------------------------------------------------------- /client/common/media/icons/Help.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Help = props => { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 | ); 11 | }; 12 | 13 | export default Help; 14 | -------------------------------------------------------------------------------- /client/common/media/icons/BurgerMenu.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const BurgerMenu = props => { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 | ); 11 | }; 12 | 13 | export default BurgerMenu; 14 | -------------------------------------------------------------------------------- /distribution/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Reeve 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /client/common/media/icons/Upload.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Upload = props => { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 | ); 11 | }; 12 | 13 | export default Upload; 14 | -------------------------------------------------------------------------------- /client/common/media/icons/Logout.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const LogoutIcon = props => { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 | ); 11 | }; 12 | 13 | export default LogoutIcon; 14 | -------------------------------------------------------------------------------- /shared/utilities/queryStrings.js: -------------------------------------------------------------------------------- 1 | import queryString from "query-string"; 2 | 3 | // Takes a url and object to generate a query string 4 | export function generateQueryStringURL(path, parameters) { 5 | // Check there is no forward slash at end of string 6 | if (path.slice(-1) === "/") { 7 | path = path.substring(0, path.length - 1); 8 | } 9 | 10 | // Generate query string from parameters 11 | const query = queryString.stringify(parameters); 12 | return `${path}${query ? "?" + query : "/"}`; 13 | } 14 | -------------------------------------------------------------------------------- /client/common/media/icons/Overview.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const OverviewIcon = props => { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 | ); 11 | }; 12 | 13 | export default OverviewIcon; 14 | -------------------------------------------------------------------------------- /distribution/images/empty_image.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /database/scripts/180716_02.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO `executedScripts` (`name`, `description`, `createdDate`, `executedDate`) 2 | VALUES 3 | ('180716_02', 'Added language column to client and user tables', NOW(), NOW()); 4 | 5 | ALTER TABLE `client` ADD COLUMN `defaultLanguage` int(11) NOT NULL DEFAULT 1; 6 | ALTER TABLE `user` ADD COLUMN `language` int(11) NOT NULL DEFAULT 1; 7 | 8 | ALTER TABLE `client` 9 | MODIFY COLUMN `defaultLanguage` int(11) UNSIGNED NOT NULL DEFAULT 1; 10 | 11 | ALTER TABLE `user` 12 | MODIFY COLUMN `language` int(11) UNSIGNED NOT NULL DEFAULT 1; -------------------------------------------------------------------------------- /database/scripts/180601_03.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO `executedScripts` (`name`, `description`, `createdDate`, `executedDate`) 2 | VALUES 3 | ('180601_03', 'Link features to subscriptionFeatures table', NOW(), NOW()); 4 | 5 | CREATE TABLE `subscriptionFeatures` ( 6 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 7 | `subscriptionId` int(3) unsigned NOT NULL, 8 | `featureId` int(11) unsigned NOT NULL, 9 | PRIMARY KEY (`id`) 10 | ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4; 11 | 12 | INSERT INTO `subscriptionFeatures` (`id`, `subscriptionId`, `featureId`) 13 | VALUES 14 | (1, 1, 1); -------------------------------------------------------------------------------- /client/api/billing.js: -------------------------------------------------------------------------------- 1 | import fetch from "common/fetch"; 2 | import { generateQueryStringURL } from "shared/utilities/queryStrings"; 3 | 4 | // Load Client Subscription Details 5 | export function clientSubscriptionDetails() { 6 | return fetch.perform("/api/v1.0/billing/client-subscription-details", { 7 | method: "GET" 8 | }); 9 | } 10 | 11 | // Load List of available subscriptions 12 | export function availableSubscriptions(parameters = {}) { 13 | return fetch.perform(generateQueryStringURL("/api/v1.0/billing/available-subscriptions", parameters), { 14 | method: "GET" 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /distribution/images/avatar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /database/readme.md: -------------------------------------------------------------------------------- 1 | ## MySQL Database Setup 2 | 3 | The folder ./scripts contains all the sql scripts required to build the MySQL database tables. 4 | 5 | All files should be run sequentially from the earliest to most recent date. 6 | 7 | ### First time Setup: 8 | 9 | **Windows:** 10 | 11 | Modify and execute the .bat file 'execute.bat' in the scripts folder. 12 | 13 | for %%G in (*.sql) do sqlcmd /S *servername* /d reeve -E -i"%%G" 14 | pause 15 | 16 | **Other:** 17 | 18 | Alternatively, the scripts/compiled folder contains a file called 'compiled.sql,' which is a single sql file of every database script. -------------------------------------------------------------------------------- /database/scripts/180601_01.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO `executedScripts` (`name`, `description`, `createdDate`, `executedDate`) 2 | VALUES 3 | ('180601_01', 'Create new client styling table', NOW(), NOW()); 4 | 5 | CREATE TABLE `clientStyling` ( 6 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 7 | `clientId` int(11) unsigned NOT NULL, 8 | `logoImage` varchar(255) DEFAULT NULL, 9 | `backgroundImage` varchar(255) DEFAULT NULL, 10 | `backgroundColor` varchar(32) DEFAULT NULL, 11 | `primaryColor` varchar(32) DEFAULT NULL, 12 | `secondaryColor` varchar(32) DEFAULT NULL, 13 | PRIMARY KEY (`id`) 14 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -------------------------------------------------------------------------------- /database/scripts/180601_02.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO `executedScripts` (`name`, `description`, `createdDate`, `executedDate`) 2 | VALUES 3 | ('180601_02', 'Create new features table', NOW(), NOW()); 4 | 5 | CREATE TABLE `features` ( 6 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 7 | `name` varchar(255) NOT NULL DEFAULT '', 8 | `description` varchar(255) NOT NULL DEFAULT '', 9 | PRIMARY KEY (`id`) 10 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 11 | 12 | INSERT INTO `features` (`id`, `name`, `description`) 13 | VALUES 14 | (1, 'Styling', 'Client can specify their own unique styling, logo image, background, primary and secondary colors'); -------------------------------------------------------------------------------- /client/common/components/User.js: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux"; 2 | import { AUTHENTICATION } from "../store/reducers/authentication.js"; 3 | import { LANGUAGE } from "../store/reducers/language.js"; 4 | 5 | const fetchUserProperties = connect(state => ({ 6 | user: state.getIn([AUTHENTICATION, "user", "payload"]), 7 | userStatus: state.getIn([AUTHENTICATION, "user", "status"]), 8 | language: state.getIn([LANGUAGE, "changeLanguage", "payload"]), 9 | languageStatus: state.getIn([LANGUAGE, "changeLanguage", "status"]) 10 | })); 11 | 12 | export default function User(WrappedComponent) { 13 | return fetchUserProperties(WrappedComponent); 14 | } 15 | -------------------------------------------------------------------------------- /database/scripts/181228_01.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO `executedScripts` (`name`, `description`, `createdDate`, `executedDate`) 2 | VALUES 3 | ('181228_01', 'New stripe plans table', NOW(), NOW()); 4 | 5 | CREATE TABLE `plans` ( 6 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 7 | `name` varchar(128) NOT NULL DEFAULT '', 8 | `description` varchar(255) DEFAULT NULL, 9 | `stripeProductId` varchar(128) DEFAULT NULL, 10 | `newSubscriptionsAllowed` tinyint(1) NOT NULL, 11 | `active` tinyint(1) NOT NULL DEFAULT '1', 12 | `createdAt` datetime DEFAULT NULL, 13 | `updatedAt` datetime DEFAULT NULL, 14 | PRIMARY KEY (`id`) 15 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -------------------------------------------------------------------------------- /database/scripts/180720_02.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO `executedScripts` (`name`, `description`, `createdDate`, `executedDate`) 2 | VALUES 3 | ('180720_02', 'Created email verification code table', NOW(), NOW()); 4 | 5 | CREATE TABLE `emailVerificationCode` ( 6 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 7 | `verificationCode` varchar(255) NOT NULL DEFAULT '', 8 | `activated` tinyint(1) NOT NULL, 9 | `userId` int(11) unsigned NOT NULL, 10 | `clientId` int(11) unsigned NOT NULL, 11 | `gracePeriod` int(2) unsigned NOT NULL, 12 | `createdAt` datetime NOT NULL, 13 | `updatedAt` datetime NOT NULL, 14 | PRIMARY KEY (`id`) 15 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -------------------------------------------------------------------------------- /server/services/router.js: -------------------------------------------------------------------------------- 1 | let express = require("express"); 2 | let path = require("path"); 3 | let fs = require("fs"); 4 | 5 | // Define our express router object 6 | let router = express.Router(); 7 | 8 | // Recursively retrieve endpoints from routes folder 9 | fs.readdirSync(path.join(__dirname, "../controller")).forEach(function(file) { 10 | if (file.toLowerCase().indexOf(".js")) { 11 | require(path.join(__dirname, "../controller", file))(router); 12 | } 13 | }); 14 | 15 | // Load index file for all other calls 16 | router.get("*", function(req, res) { 17 | res.sendFile(path.join(__dirname, "../../distribution/index.html")); 18 | }); 19 | 20 | module.exports = router; 21 | -------------------------------------------------------------------------------- /database/scripts/180523_01.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO `executedScripts` (`name`, `description`, `createdDate`, `executedDate`) 2 | VALUES 3 | ('180523_01', 'Create new user table', NOW(), NOW()); 4 | 5 | CREATE TABLE `user` ( 6 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 7 | `firstName` varchar(255) NOT NULL DEFAULT '', 8 | `lastName` varchar(255) NOT NULL DEFAULT '', 9 | `clientId` int(11) unsigned NOT NULL, 10 | `emailAddress` varchar(255) NOT NULL DEFAULT '', 11 | `password` varchar(255) NOT NULL DEFAULT '', 12 | `active` tinyint(1) NOT NULL DEFAULT '1', 13 | `createdDate` datetime NOT NULL, 14 | `ModifiedDate` datetime NOT NULL, 15 | PRIMARY KEY (`id`) 16 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 17 | -------------------------------------------------------------------------------- /database/scripts/180602_01.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO `executedScripts` (`name`, `description`, `createdDate`, `executedDate`) 2 | VALUES 3 | ('180602_01', 'Clean up client table and organize subscriptions', NOW(), NOW()); 4 | 5 | ALTER TABLE `client` 6 | DROP COLUMN `trial`, 7 | DROP COLUMN `trialExpiry`, 8 | DROP COLUMN `subscription`; 9 | 10 | ALTER TABLE `client` 11 | ADD COLUMN `subscriptionEndDate` date DEFAULT NULL; 12 | 13 | ALTER TABLE `client` 14 | MODIFY COLUMN `subscriptionId` int(3) DEFAULT 1; 15 | 16 | UPDATE `subscriptions` 17 | SET `name` = 'Trial', `description` = 'Default Trial account when a new client is created' 18 | WHERE id = 1; 19 | 20 | ALTER TABLE `user` 21 | ADD `lastLoginDate` datetime DEFAULT NULL; -------------------------------------------------------------------------------- /client/common/layouts/EmptyLayout.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { withRouter } from "react-router-dom"; 4 | import { Offline } from "react-detect-offline"; 5 | 6 | import AppOffline from "common/components/AppOffline"; 7 | 8 | class DefaultLayout extends Component { 9 | render() { 10 | const { children } = this.props; 11 | 12 | return ( 13 | 14 | 15 | 16 | 17 | {children} 18 | 19 | ); 20 | } 21 | } 22 | 23 | DefaultLayout.propTypes = { 24 | children: PropTypes.oneOfType([PropTypes.array, PropTypes.object]) 25 | }; 26 | 27 | export default withRouter(DefaultLayout); 28 | -------------------------------------------------------------------------------- /client/common/components/AsyncComponent.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import Loading from "common/components/Loading"; 3 | 4 | export default function asyncComponent(importComponent) { 5 | class AsyncComponent extends Component { 6 | constructor(props) { 7 | super(props); 8 | 9 | this.state = { 10 | component: null 11 | }; 12 | } 13 | 14 | async componentDidMount() { 15 | const { default: component } = await importComponent(); 16 | this.setState({ 17 | component: component 18 | }); 19 | } 20 | 21 | render() { 22 | const Component = this.state.component; 23 | return Component ? : ; 24 | } 25 | } 26 | 27 | return AsyncComponent; 28 | } 29 | -------------------------------------------------------------------------------- /distribution/loader.min.js: -------------------------------------------------------------------------------- 1 | let body=document.getElementsByTagName('BODY')[0];let script=document.createElement('script');script.type='text/javascript';const entryDiv=document.currentScript.getAttribute('entry');body.innerHTML+='
'+''+'
';let xhr=new XMLHttpRequest(),method="GET",url=document.currentScript.getAttribute('bundle');xhr.responseType='blob';xhr.open(method,url,!0);xhr.onload=function(){if(xhr.readyState===XMLHttpRequest.DONE&&xhr.status===200){script.src=URL.createObjectURL(xhr.response);body.innerHTML=(entryDiv?`
`:"");body.appendChild(script)}};xhr.send() -------------------------------------------------------------------------------- /server/models/languages.js: -------------------------------------------------------------------------------- 1 | module.exports = function(sequelize, DataTypes) { 2 | return sequelize.define( 3 | "languages", 4 | { 5 | id: { 6 | type: DataTypes.INTEGER(11).UNSIGNED, 7 | allowNull: false, 8 | primaryKey: true, 9 | autoIncrement: true, 10 | field: "id" 11 | }, 12 | language: { 13 | type: DataTypes.STRING(11), 14 | allowNull: false, 15 | defaultValue: "", 16 | field: "language" 17 | }, 18 | createdAt: { 19 | type: DataTypes.DATE, 20 | allowNull: true, 21 | field: "createdAt" 22 | }, 23 | updatedAt: { 24 | type: DataTypes.DATE, 25 | allowNull: true, 26 | field: "updatedAt" 27 | } 28 | }, 29 | { 30 | tableName: "languages" 31 | } 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | [ 4 | "module-resolver", 5 | { 6 | "alias": { 7 | "shared": "./shared", 8 | "client": "./client", 9 | "api": "./client/api", 10 | "common": "./client/common", 11 | "modules": "./client/modules", 12 | "server": "./server", 13 | "services": "./server/services", 14 | "controller": "./server/controller", 15 | "models": "./server/models", 16 | "utilities": "./server/utilities", 17 | "orchestrator": "./server/orchestrator", 18 | "distribution": "./distribution" 19 | } 20 | } 21 | ], 22 | ["@babel/plugin-transform-runtime"], 23 | ["@babel/plugin-syntax-dynamic-import"], 24 | ["@babel/plugin-proposal-class-properties"], 25 | ["emotion"] 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /shared/utilities/securityToken.js: -------------------------------------------------------------------------------- 1 | // Store security token in browser 2 | export function saveToken(securityToken, keepSignedIn) { 3 | if (keepSignedIn === true) { 4 | window.localStorage.setItem("securityToken", JSON.stringify(securityToken)); 5 | } else { 6 | window.sessionStorage.setItem("securityToken", JSON.stringify(securityToken)); 7 | } 8 | } 9 | 10 | // Clear security token from browser 11 | export function clearToken() { 12 | window.localStorage.removeItem("securityToken"); 13 | window.sessionStorage.removeItem("securityToken"); 14 | } 15 | 16 | // Retrieve security token from browser 17 | export function getToken() { 18 | const token = window.sessionStorage.getItem("securityToken") || window.localStorage.getItem("securityToken"); 19 | return JSON.parse(token); 20 | } 21 | -------------------------------------------------------------------------------- /database/scripts/190313_01.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO `executedScripts` (`name`, `description`, `createdDate`, `executedDate`) 2 | VALUES 3 | ('190313_01', 'New user profile columns', NOW(), NOW()); 4 | 5 | ALTER TABLE `user` 6 | ADD COLUMN `bio` varchar(255) NULL; 7 | 8 | ALTER TABLE `user` 9 | ADD COLUMN `location` varchar(255) NULL; 10 | 11 | ALTER TABLE `user` 12 | ADD COLUMN `website` varchar(255) NULL; 13 | 14 | INSERT INTO `subscriptionFeatures` (`id`, `subscriptionId`, `featureId`, `createdAt`, `updatedAt`) 15 | VALUES 16 | (4, 2, 1, '2019-03-13 11:25:33', '2019-03-13 11:25:33'), 17 | (5, 3, 1, '2019-03-13 11:25:33', '2019-03-13 11:25:33'), 18 | (6, 3, 2, '2019-03-13 11:25:33', '2019-03-13 11:25:33'), 19 | (7, 4, 1, '2019-03-13 11:25:33', '2019-03-13 11:25:33'), 20 | (8, 4, 2, '2019-03-13 11:25:33', '2019-03-13 11:25:33'); -------------------------------------------------------------------------------- /server/models/billingIntervals.js: -------------------------------------------------------------------------------- 1 | module.exports = function(sequelize, DataTypes) { 2 | return sequelize.define( 3 | "billingIntervals", 4 | { 5 | id: { 6 | type: DataTypes.INTEGER(11).UNSIGNED, 7 | allowNull: false, 8 | primaryKey: true, 9 | autoIncrement: true, 10 | field: "id" 11 | }, 12 | name: { 13 | type: DataTypes.STRING(11), 14 | allowNull: true, 15 | field: "name" 16 | }, 17 | description: { 18 | type: DataTypes.STRING(255), 19 | allowNull: true, 20 | field: "description" 21 | }, 22 | createdAt: { 23 | type: DataTypes.DATE, 24 | allowNull: true, 25 | field: "createdAt" 26 | }, 27 | updatedAt: { 28 | type: DataTypes.DATE, 29 | allowNull: true, 30 | field: "updatedAt" 31 | } 32 | }, 33 | { 34 | tableName: "billingIntervals" 35 | } 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /server/models/currencies.js: -------------------------------------------------------------------------------- 1 | module.exports = function(sequelize, DataTypes) { 2 | return sequelize.define( 3 | "currencies", 4 | { 5 | id: { 6 | type: DataTypes.INTEGER(11).UNSIGNED, 7 | allowNull: false, 8 | primaryKey: true, 9 | autoIncrement: true, 10 | field: "id" 11 | }, 12 | identifier: { 13 | type: DataTypes.CHAR(3), 14 | allowNull: false, 15 | defaultValue: "", 16 | field: "identifier" 17 | }, 18 | description: { 19 | type: DataTypes.STRING(255), 20 | allowNull: true, 21 | field: "description" 22 | }, 23 | createdAt: { 24 | type: DataTypes.DATE, 25 | allowNull: false, 26 | field: "createdAt" 27 | }, 28 | updatedAt: { 29 | type: DataTypes.DATE, 30 | allowNull: false, 31 | field: "updatedAt" 32 | } 33 | }, 34 | { 35 | tableName: "currencies" 36 | } 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /server/models/roles.js: -------------------------------------------------------------------------------- 1 | module.exports = function(sequelize, DataTypes) { 2 | return sequelize.define( 3 | "roles", 4 | { 5 | id: { 6 | type: DataTypes.INTEGER(11).UNSIGNED, 7 | allowNull: false, 8 | primaryKey: true, 9 | autoIncrement: true, 10 | field: "id" 11 | }, 12 | name: { 13 | type: DataTypes.STRING(255), 14 | allowNull: false, 15 | defaultValue: "", 16 | field: "name" 17 | }, 18 | description: { 19 | type: DataTypes.STRING(255), 20 | allowNull: false, 21 | defaultValue: "", 22 | field: "description" 23 | }, 24 | createdAt: { 25 | type: DataTypes.DATE, 26 | allowNull: true, 27 | field: "createdAt" 28 | }, 29 | updatedAt: { 30 | type: DataTypes.DATE, 31 | allowNull: true, 32 | field: "updatedAt" 33 | } 34 | }, 35 | { 36 | tableName: "roles" 37 | } 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /server/models/features.js: -------------------------------------------------------------------------------- 1 | module.exports = function(sequelize, DataTypes) { 2 | return sequelize.define( 3 | "features", 4 | { 5 | id: { 6 | type: DataTypes.INTEGER(11).UNSIGNED, 7 | allowNull: false, 8 | primaryKey: true, 9 | autoIncrement: true, 10 | field: "id" 11 | }, 12 | name: { 13 | type: DataTypes.STRING(255), 14 | allowNull: false, 15 | defaultValue: "", 16 | field: "name" 17 | }, 18 | description: { 19 | type: DataTypes.STRING(255), 20 | allowNull: false, 21 | defaultValue: "", 22 | field: "description" 23 | }, 24 | createdAt: { 25 | type: DataTypes.DATE, 26 | allowNull: true, 27 | field: "createdAt" 28 | }, 29 | updatedAt: { 30 | type: DataTypes.DATE, 31 | allowNull: true, 32 | field: "updatedAt" 33 | } 34 | }, 35 | { 36 | tableName: "features" 37 | } 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /server/models/emailTypes.js: -------------------------------------------------------------------------------- 1 | module.exports = function(sequelize, DataTypes) { 2 | return sequelize.define( 3 | "emailTypes", 4 | { 5 | id: { 6 | type: DataTypes.INTEGER(11).UNSIGNED, 7 | allowNull: false, 8 | primaryKey: true, 9 | autoIncrement: true, 10 | field: "id" 11 | }, 12 | name: { 13 | type: DataTypes.STRING(255), 14 | allowNull: false, 15 | defaultValue: "", 16 | field: "name" 17 | }, 18 | description: { 19 | type: DataTypes.STRING(255), 20 | allowNull: false, 21 | defaultValue: "", 22 | field: "description" 23 | }, 24 | createdAt: { 25 | type: DataTypes.DATE, 26 | allowNull: true, 27 | field: "createdAt" 28 | }, 29 | updatedAt: { 30 | type: DataTypes.DATE, 31 | allowNull: true, 32 | field: "updatedAt" 33 | } 34 | }, 35 | { 36 | tableName: "emailTypes" 37 | } 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /server/models/subscriptionFeatures.js: -------------------------------------------------------------------------------- 1 | module.exports = function(sequelize, DataTypes) { 2 | return sequelize.define( 3 | "subscriptionFeatures", 4 | { 5 | id: { 6 | type: DataTypes.INTEGER(11).UNSIGNED, 7 | allowNull: false, 8 | primaryKey: true, 9 | autoIncrement: true, 10 | field: "id" 11 | }, 12 | subscriptionId: { 13 | type: DataTypes.INTEGER(3).UNSIGNED, 14 | allowNull: false, 15 | field: "subscriptionId" 16 | }, 17 | featureId: { 18 | type: DataTypes.INTEGER(11).UNSIGNED, 19 | allowNull: false, 20 | field: "featureId" 21 | }, 22 | createdAt: { 23 | type: DataTypes.DATE, 24 | allowNull: true, 25 | field: "createdAt" 26 | }, 27 | updatedAt: { 28 | type: DataTypes.DATE, 29 | allowNull: true, 30 | field: "updatedAt" 31 | } 32 | }, 33 | { 34 | tableName: "subscriptionFeatures" 35 | } 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /server/services/devmiddleware.js: -------------------------------------------------------------------------------- 1 | let webpack = require("webpack"); 2 | let webpackDevMiddleware = require("webpack-dev-middleware"); 3 | let webpackHotMiddleware = require("webpack-hot-middleware"); 4 | let webpackConfig = require("../../webpack.config.js"); 5 | let webpackcompiler = webpack(webpackConfig); 6 | let config = require("../../config"); 7 | 8 | function initialize(app) { 9 | if (config.build.environment !== "development") { 10 | return; 11 | } 12 | 13 | app.use( 14 | webpackDevMiddleware(webpackcompiler, { 15 | publicPath: webpackConfig.output.publicPath, 16 | hot: true, 17 | headers: { 18 | "Access-Control-Allow-Origin": "*", 19 | "Access-Control-Allow-Headers": "*" 20 | }, 21 | stats: { 22 | modules: 0 23 | } 24 | }) 25 | ); 26 | app.use(webpackHotMiddleware(webpackcompiler)); 27 | 28 | return app; 29 | } 30 | 31 | module.exports = { 32 | initialize: initialize 33 | }; 34 | -------------------------------------------------------------------------------- /client/common/components/ServerSuccess.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { withRouter } from "react-router-dom"; 4 | import queryString from "query-string"; 5 | import equal from "deep-equal"; 6 | 7 | class ServerSuccess extends Component { 8 | render() { 9 | const { history, path, message } = this.props; 10 | 11 | const query = queryString.parse(history.location.search); 12 | 13 | // Show success message if query string matches path object 14 | if (message && equal(query, path) === true) { 15 | return
{message}
; 16 | } else { 17 | return null; 18 | } 19 | } 20 | } 21 | 22 | ServerSuccess.propTypes = { 23 | history: PropTypes.object, 24 | path: PropTypes.object, 25 | message: PropTypes.oneOfType([PropTypes.string, PropTypes.object]) 26 | }; 27 | 28 | export default withRouter(ServerSuccess); 29 | -------------------------------------------------------------------------------- /client/modules/header/components/NavProfileMenuLogo.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from "react"; 2 | import { Link } from "react-router-dom"; 3 | import PropTypes from "prop-types"; 4 | 5 | import User from "common/components/User"; 6 | 7 | class NavProfileMenuLogo extends Component { 8 | render() { 9 | const { user } = this.props; 10 | 11 | // Check if client has uploaded a logo 12 | const clientLogo = user.get("logoImage") != null && user.get("logoImage") != ""; 13 | 14 | return ( 15 | 16 |
17 | {clientLogo ? : {`${user.get("clientName")}`}} 18 |
19 |
20 | 21 | ); 22 | } 23 | } 24 | 25 | NavProfileMenuLogo.propTypes = { 26 | user: PropTypes.object 27 | }; 28 | 29 | export default User(NavProfileMenuLogo); 30 | -------------------------------------------------------------------------------- /client/common/components/SuccessNotification.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { withRouter } from "react-router-dom"; 4 | import { notify } from "react-notify-toast"; 5 | import queryString from "query-string"; 6 | import equal from "deep-equal"; 7 | 8 | class SuccessNotification extends Component { 9 | render() { 10 | const { history, path, message } = this.props; 11 | 12 | const query = queryString.parse(history.location.search); 13 | 14 | // Show success notification if query string matches path object 15 | if (message && equal(query, path) === true) { 16 | notify.show({message}, "success"); 17 | } 18 | 19 | return null; 20 | } 21 | } 22 | 23 | SuccessNotification.propTypes = { 24 | history: PropTypes.object, 25 | path: PropTypes.object, 26 | message: PropTypes.oneOfType([PropTypes.string, PropTypes.object]) 27 | }; 28 | 29 | export default withRouter(SuccessNotification); 30 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import i18next from "shared/translations/i18n"; 4 | import Raven from "raven-js"; 5 | import { Provider } from "react-redux"; 6 | import { I18nextProvider } from "react-i18next"; 7 | import { initializeGA } from "common/components/GoogleAnalytics"; 8 | 9 | import "./common/styles/entry.scss"; 10 | 11 | import App from "./App"; 12 | import store from "./common/store/store"; 13 | 14 | /* eslint-disable */ 15 | 16 | // Load Google Analytics Tracking 17 | initializeGA(); 18 | 19 | // Load Sentry error reporting 20 | if (SENTRY_ENABLED) { 21 | Raven.config(SENTRY_DSN, { 22 | release: BUILD_RELEASE, 23 | environment: BUILD_ENVIRONMENT 24 | }).install(); 25 | } 26 | 27 | /* eslint-enable */ 28 | 29 | ReactDOM.render( 30 | 31 | 32 | 33 | 34 | , 35 | document.getElementById("app") 36 | ); 37 | -------------------------------------------------------------------------------- /database/scripts/190201_01.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO `executedScripts` (`name`, `description`, `createdDate`, `executedDate`) 2 | VALUES 3 | ('190201_01', 'Add new subscriptionID column to plans table', NOW(), NOW()); 4 | 5 | ALTER TABLE `plans` 6 | ADD COLUMN `subscriptionId` INT(11) UNSIGNED; 7 | 8 | INSERT INTO `subscriptions` (`id`, `name`, `description`, `active`, `createdAt`, `updatedAt`) 9 | VALUES 10 | (2, 'Basic', 'Basic paying customer', 1, NOW(), NOW()), 11 | (3, 'Standard', 'Standard plan paying customer', 1, NOW(), NOW()), 12 | (4, 'Professional', 'Professional plan paying customer', 1, NOW(), NOW()); 13 | 14 | UPDATE `plans` SET `subscriptionId` = '2' WHERE `id` = 1; 15 | UPDATE `plans` SET `subscriptionId` = '3' WHERE `id` = 2; 16 | UPDATE `plans` SET `subscriptionId` = '4' WHERE `id` = 3; 17 | UPDATE `plans` SET `subscriptionId` = '2' WHERE `id` = 4; 18 | UPDATE `plans` SET `subscriptionId` = '3' WHERE `id` = 5; 19 | UPDATE `plans` SET `subscriptionId` = '4' WHERE `id` = 6; 20 | -------------------------------------------------------------------------------- /server/models/subscriptions.js: -------------------------------------------------------------------------------- 1 | module.exports = function(sequelize, DataTypes) { 2 | return sequelize.define( 3 | "subscriptions", 4 | { 5 | id: { 6 | type: DataTypes.INTEGER(3).UNSIGNED, 7 | allowNull: false, 8 | primaryKey: true, 9 | autoIncrement: true, 10 | field: "id" 11 | }, 12 | name: { 13 | type: DataTypes.STRING(255), 14 | allowNull: false, 15 | defaultValue: "", 16 | field: "name" 17 | }, 18 | description: { 19 | type: DataTypes.STRING(255), 20 | allowNull: true, 21 | field: "description" 22 | }, 23 | active: { 24 | type: DataTypes.INTEGER(1), 25 | allowNull: false, 26 | field: "active" 27 | }, 28 | createdAt: { 29 | type: DataTypes.DATE, 30 | allowNull: true, 31 | field: "createdAt" 32 | }, 33 | updatedAt: { 34 | type: DataTypes.DATE, 35 | allowNull: true, 36 | field: "updatedAt" 37 | } 38 | }, 39 | { 40 | tableName: "subscriptions" 41 | } 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /server/models/userRoles.js: -------------------------------------------------------------------------------- 1 | module.exports = function(sequelize, DataTypes) { 2 | return sequelize.define( 3 | "userRoles", 4 | { 5 | id: { 6 | type: DataTypes.INTEGER(21).UNSIGNED, 7 | allowNull: false, 8 | primaryKey: true, 9 | autoIncrement: true, 10 | field: "id" 11 | }, 12 | userId: { 13 | type: DataTypes.INTEGER(11).UNSIGNED, 14 | allowNull: false, 15 | field: "userId" 16 | }, 17 | roleId: { 18 | type: DataTypes.INTEGER(11).UNSIGNED, 19 | allowNull: false, 20 | field: "roleId" 21 | }, 22 | active: { 23 | type: DataTypes.INTEGER(1), 24 | allowNull: false, 25 | defaultValue: "1", 26 | field: "active" 27 | }, 28 | createdAt: { 29 | type: DataTypes.DATE, 30 | allowNull: true, 31 | field: "createdAt" 32 | }, 33 | updatedAt: { 34 | type: DataTypes.DATE, 35 | allowNull: true, 36 | field: "updatedAt" 37 | } 38 | }, 39 | { 40 | tableName: "userRoles" 41 | } 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /database/scripts/180708_01.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO `executedScripts` (`name`, `description`, `createdDate`, `executedDate`) 2 | VALUES 3 | ('180708_01', 'Add createdAt and updatedAt columns', NOW(), NOW()); 4 | 5 | ALTER TABLE `client` CHANGE `createdDate` `createdAt` date NOT NULL; 6 | ALTER TABLE `client` CHANGE `modifiedDate` `updatedAt` date NOT NULL; 7 | ALTER TABLE `clientStyling` ADD COLUMN `createdAt` date NULL; 8 | ALTER TABLE `clientStyling` ADD COLUMN `updatedAt` date NULL; 9 | ALTER TABLE `features` ADD COLUMN `createdAt` date NULL; 10 | ALTER TABLE `features` ADD COLUMN `updatedAt` date NULL; 11 | ALTER TABLE `roles` CHANGE `createdDate` `createdAt` date NOT NULL; 12 | ALTER TABLE `roles` CHANGE `modifiedDate` `updatedAt` date NOT NULL; 13 | ALTER TABLE `user` CHANGE `createdDate` `createdAt` date NOT NULL; 14 | ALTER TABLE `user` CHANGE `modifiedDate` `updatedAt` date NOT NULL; 15 | ALTER TABLE `userRoles` CHANGE `createdDate` `createdAt` date NOT NULL; 16 | ALTER TABLE `userRoles` CHANGE `modifiedDate` `updatedAt` date NOT NULL; -------------------------------------------------------------------------------- /database/scripts/180821_01.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO `executedScripts` (`name`, `description`, `createdDate`, `executedDate`) 2 | VALUES 3 | ('180821_01', 'New billing features and subscriptions', NOW(), NOW()); 4 | 5 | INSERT INTO `features` (`id`, `name`, `description`, `createdAt`, `updatedAt`) 6 | VALUES 7 | (2, 'Billing', 'Client can pay an ongoing fee and receive access to the platform', NOW(), NOW()); 8 | 9 | INSERT INTO `roles` (`id`, `name`, `description`, `createdAt`, `updatedAt`) 10 | VALUES 11 | (2, 'Administrator', 'Access to secondary management functions throughout the app', NOW(), NOW()), 12 | (3, 'Finance', 'Finance and payment specific parts of the app', NOW(), NOW()); 13 | 14 | INSERT INTO `subscriptions` (`id`, `name`, `description`, `active`, `createdAt`, `updatedAt`) 15 | VALUES 16 | (2, 'Basic', 'Basic paying customer', 1, NOW(), NOW()); 17 | 18 | INSERT INTO `subscriptionFeatures` (`id`, `subscriptionId`, `featureId`, `createdAt`, `updatedAt`) 19 | VALUES 20 | (2, 1, 2, NOW(), NOW()), 21 | (3, 2, 2, NOW(), NOW()); 22 | -------------------------------------------------------------------------------- /client/common/components/AppOffline.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Helmet } from "react-helmet"; 4 | import { t } from "shared/translations/i18n"; 5 | 6 | class AppOffline extends Component { 7 | render() { 8 | const { navMargin } = this.props; 9 | return ( 10 | 11 | 20 |
21 |
22 | {t("label.offline")}: {t("components.offline.disconnectedLead")} {t("components.offline.continueAsNormal")} 23 |
24 |
25 |
26 | ); 27 | } 28 | } 29 | 30 | AppOffline.propTypes = { 31 | navMargin: PropTypes.bool 32 | }; 33 | 34 | export default AppOffline; 35 | -------------------------------------------------------------------------------- /client/common/media/icons/flags/England.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const EnglandFlagIcon = props => { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 20 | 21 | 22 | 23 | ); 24 | }; 25 | 26 | export default EnglandFlagIcon; 27 | -------------------------------------------------------------------------------- /server/models/passwordReset.js: -------------------------------------------------------------------------------- 1 | module.exports = function(sequelize, DataTypes) { 2 | return sequelize.define( 3 | "passwordReset", 4 | { 5 | id: { 6 | type: DataTypes.INTEGER(11).UNSIGNED, 7 | allowNull: false, 8 | primaryKey: true, 9 | autoIncrement: true 10 | }, 11 | resetCode: { 12 | type: DataTypes.STRING(255), 13 | allowNull: false, 14 | defaultValue: "" 15 | }, 16 | activated: { 17 | type: DataTypes.INTEGER(1), 18 | allowNull: false 19 | }, 20 | userId: { 21 | type: DataTypes.INTEGER(11).UNSIGNED, 22 | allowNull: false 23 | }, 24 | clientId: { 25 | type: DataTypes.INTEGER(11).UNSIGNED, 26 | allowNull: false 27 | }, 28 | gracePeriod: { 29 | type: DataTypes.INTEGER(2).UNSIGNED, 30 | allowNull: false 31 | }, 32 | createdAt: { 33 | type: DataTypes.DATE, 34 | allowNull: false 35 | }, 36 | updatedAt: { 37 | type: DataTypes.DATE, 38 | allowNull: false 39 | } 40 | }, 41 | { 42 | tableName: "passwordReset" 43 | } 44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /client/common/components/inputs/Checkbox.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | class Checkbox extends React.Component { 5 | render() { 6 | const { name, value, onClick, label, id, smallText, disabled, error } = this.props; 7 | 8 | return ( 9 |
10 | 11 | {label && ( 12 | 15 | )} 16 | {error && error[name] &&
{error[name][0]}
} 17 |
18 | ); 19 | } 20 | } 21 | 22 | Checkbox.propTypes = { 23 | name: PropTypes.string, 24 | value: PropTypes.bool, 25 | onClick: PropTypes.func, 26 | label: PropTypes.string, 27 | id: PropTypes.string, 28 | smallText: PropTypes.bool, 29 | disabled: PropTypes.bool, 30 | error: PropTypes.object 31 | }; 32 | 33 | export default Checkbox; 34 | -------------------------------------------------------------------------------- /client/common/layouts/DefaultLayout.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { withRouter } from "react-router-dom"; 4 | import { Offline } from "react-detect-offline"; 5 | 6 | import Header from "client/modules/header"; 7 | import GlobalStyling from "common/components/GlobalStyling"; 8 | import AppOffline from "common/components/AppOffline"; 9 | 10 | class DefaultLayout extends Component { 11 | render() { 12 | const { history, children } = this.props; 13 | 14 | return ( 15 | 16 | 17 |
18 |
19 | 20 | 21 | 22 | {children} 23 |
24 | 25 | 26 | ); 27 | } 28 | } 29 | 30 | DefaultLayout.propTypes = { 31 | history: PropTypes.object, 32 | children: PropTypes.oneOfType([PropTypes.array, PropTypes.object]) 33 | }; 34 | 35 | export default withRouter(DefaultLayout); 36 | -------------------------------------------------------------------------------- /database/scripts/190110_01.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO `executedScripts` (`name`, `description`, `createdDate`, `executedDate`) 2 | VALUES 3 | ('190110_01', 'Created currencies table', NOW(), NOW()); 4 | 5 | CREATE TABLE `currencies` ( 6 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 7 | `identifier` char(3) NOT NULL DEFAULT '', 8 | `description` varchar(255) DEFAULT NULL, 9 | `createdAt` datetime NOT NULL, 10 | `updatedAt` datetime NOT NULL, 11 | PRIMARY KEY (`id`) 12 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 13 | 14 | INSERT INTO `currencies` (`id`, `identifier`, `description`, `createdAt`, `updatedAt`) 15 | VALUES 16 | (1, 'aud', 'Australian Dollar', NOW(), NOW()); 17 | 18 | UPDATE `plans` SET `currency` = '1' WHERE `id` = 1; 19 | UPDATE `plans` SET `currency` = '1' WHERE `id` = 2; 20 | UPDATE `plans` SET `currency` = '1' WHERE `id` = 3; 21 | UPDATE `plans` SET `currency` = '1' WHERE `id` = 4; 22 | UPDATE `plans` SET `currency` = '1' WHERE `id` = 5; 23 | UPDATE `plans` SET `currency` = '1' WHERE `id` = 6; 24 | 25 | ALTER TABLE `plans` MODIFY `currency` int(11) unsigned NOT NULL DEFAULT 1; -------------------------------------------------------------------------------- /database/scripts/180524_01.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO `executedScripts` (`name`, `description`, `createdDate`, `executedDate`) 2 | VALUES 3 | ('180524_01', 'Create new role tables and foreign keys', NOW(), NOW()); 4 | 5 | CREATE TABLE `roles` ( 6 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 7 | `name` varchar(255) NOT NULL DEFAULT '', 8 | `description` varchar(255) NOT NULL DEFAULT '', 9 | `createdDate` datetime NOT NULL, 10 | `modifiedDate` datetime NOT NULL, 11 | PRIMARY KEY (`id`) 12 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 13 | 14 | INSERT INTO `roles` (`id`, `name`, `description`, `createdDate`, `modifiedDate`) 15 | VALUES 16 | (1, 'Owner', 'Highest level role with access to all subscription features', NOW(), NOW()); 17 | 18 | CREATE TABLE `userRoles` ( 19 | `id` int(21) unsigned NOT NULL AUTO_INCREMENT, 20 | `userId` int(11) unsigned NOT NULL, 21 | `roleId` int(11) unsigned NOT NULL, 22 | `active` tinyint(1) NOT NULL DEFAULT '1', 23 | `createdDate` datetime NOT NULL, 24 | `modifiedDate` datetime NOT NULL, 25 | PRIMARY KEY (`id`) 26 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -------------------------------------------------------------------------------- /database/scripts/190109_01.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO `executedScripts` (`name`, `description`, `createdDate`, `executedDate`) 2 | VALUES 3 | ('190109_01', 'Removed redundant columns from database', NOW(), NOW()); 4 | 5 | 6 | ALTER TABLE `plans` DROP `monthlyPrice`; 7 | ALTER TABLE `plans` DROP `yearlyPrice`; 8 | 9 | ALTER TABLE `plans` 10 | ADD COLUMN `billingInterval` TINYINT(16) UNSIGNED; 11 | 12 | CREATE TABLE `billingIntervals` ( 13 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 14 | `name` varchar(11) DEFAULT NULL, 15 | `description` varchar(255) DEFAULT NULL, 16 | `createdAt` datetime DEFAULT NULL, 17 | `updatedAt` datetime DEFAULT NULL, 18 | PRIMARY KEY (`id`) 19 | ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4; 20 | 21 | INSERT INTO `billingIntervals` (`id`, `name`, `description`, `createdAt`, `updatedAt`) 22 | VALUES 23 | (1, 'month', 'Clients are billed on a monthly basis (on the same day of each month)', NOW(), NOW()), 24 | (2, 'year', 'Clients are billed on a yearly basis', NOW(), NOW()); 25 | 26 | ALTER TABLE `plans` 27 | ADD COLUMN `price` decimal(6,2) NOT NULL DEFAULT '0.0'; -------------------------------------------------------------------------------- /client/common/media/icons/Settings.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const SettingsIcon = props => { 4 | return ( 5 | 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default SettingsIcon; 13 | -------------------------------------------------------------------------------- /database/scripts/190109_02.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO `executedScripts` (`name`, `description`, `createdDate`, `executedDate`) 2 | VALUES 3 | ('190109_02', 'Added 6 plans to plans table', NOW(), NOW()); 4 | 5 | INSERT INTO `plans` (`id`, `name`, `description`, `stripeProductId`, `billingInterval`, `currency`, `price`, `newSubscriptionsAllowed`, `active`, `createdAt`, `updatedAt`) 6 | VALUES 7 | (1, 'Basic Plan Monthly', 'Description of stripe basic plan', 'xxxxxxxxx_stripe', 1, 'aud', 0.00, 1, 1, NOW(), NOW()), 8 | (2, 'Standard Plan Monthly', 'Description of stripe standard plan', 'xxxxxxxxx_stripe', 1, 'aud', 0.00, 1, 1, NOW(), NOW()), 9 | (3, 'Professional Plan Monthly', 'Description of stripe professional plan', 'xxxxxxxxx_stripe', 1, 'aud', 0.00, 1, 1, NOW(), NOW()), 10 | (4, 'Basic Plan Yearly', 'Description of stripe basic plan', 'xxxxxxxxx_stripe', 2, 'aud', 0.00, 1, 1, NOW(), NOW()), 11 | (5, 'Standard Plan Yearly', 'Description of stripe strandard plan', 'xxxxxxxxx_stripe', 2, 'aud', 0.00, 1, 1, NOW(), NOW()), 12 | (6, 'Professional Plan Yearly', 'Description of stripe professional plan', 'xxxxxxxxx_stripe', 2, 'aud', 0.00, 1, 1, NOW(), NOW()); 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Peter Joseph 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /database/scripts/180517_01.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO `executedScripts` (`name`, `description`, `createdDate`, `executedDate`) 2 | VALUES 3 | ('180517_01', 'Create new client and subscription tables', NOW(), NOW()); 4 | 5 | CREATE TABLE `client` ( 6 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 7 | `name` varchar(255) NOT NULL DEFAULT '', 8 | `workspaceURL` varchar(255) NOT NULL DEFAULT '', 9 | `trial` tinyint(1) NOT NULL DEFAULT '1', 10 | `trialExpiry` date DEFAULT NULL, 11 | `subscription` tinyint(1) NOT NULL DEFAULT '0', 12 | `subscriptionId` int(3) DEFAULT NULL, 13 | `subscriptionStartDate` date DEFAULT NULL, 14 | `billingCycle` int(2) DEFAULT NULL, 15 | `createdDate` datetime NOT NULL, 16 | `modifiedDate` datetime NOT NULL, 17 | `active` tinyint(1) NOT NULL DEFAULT '1', 18 | PRIMARY KEY (`ID`) 19 | ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4; 20 | 21 | CREATE TABLE `subscriptions` ( 22 | `id` int(3) unsigned NOT NULL AUTO_INCREMENT, 23 | `name` varchar(255) NOT NULL DEFAULT '', 24 | `description` varchar(255) DEFAULT NULL, 25 | `active` tinyint(1) NOT NULL, 26 | PRIMARY KEY (`ID`) 27 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -------------------------------------------------------------------------------- /shared/utilities/domains.js: -------------------------------------------------------------------------------- 1 | const safe = require("safe-regex"); 2 | 3 | // Validate a URL is valid 4 | export function validateURL(href) { 5 | let regex = /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(:[0-9]+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)/; 6 | 7 | // Validate regex expression and determine if domain is valid 8 | if (safe(href.match(regex))) { 9 | const validDomain = href.match(regex); 10 | if (validDomain && validDomain[1]) { 11 | return true; 12 | } 13 | } 14 | return false; 15 | } 16 | 17 | // Extract a complete subdomain from a href url 18 | export function extractSubdomain(href) { 19 | let regex = /(?:http[s]*\:\/\/)*(.*?)\.(?=[^\/]*\..{2,4})/; 20 | 21 | // Alternative regex for localhost development environments 22 | if (process.env.NODE_ENV === "development") { 23 | regex = /(?:http[s]*\:\/\/)*(.*?)\.(?=[^\/]*)/; 24 | } 25 | 26 | // Validate regex expression and determine if subdomain is valid 27 | if (safe(href.match(regex))) { 28 | const validDomain = href.match(regex); 29 | if (validDomain && validDomain[1]) { 30 | return validDomain[1]; 31 | } 32 | } 33 | return ""; 34 | } 35 | -------------------------------------------------------------------------------- /client/common/styles/modules/_spacing.scss: -------------------------------------------------------------------------------- 1 | @import "./node_modules/bootstrap/scss/_nav"; 2 | 3 | // Custom margins based on unique page elements 4 | .margin { 5 | // Margin sizing from height of navigation bar 6 | &.nav-height { 7 | $nav-bar-height: calc(#{$nav-link-height} + #{$nav-link-padding-y * 1.5}); 8 | 9 | &.top { 10 | margin-top: $nav-bar-height; 11 | } 12 | 13 | &.bottom { 14 | margin-bottom: $nav-bar-height; 15 | } 16 | 17 | &.left { 18 | margin-left: $nav-bar-height; 19 | } 20 | 21 | &.right { 22 | margin-right: $nav-bar-height; 23 | } 24 | } 25 | } 26 | 27 | // Custom padding based on unique page elements 28 | .padding { 29 | // Padding sizing from height of navigation bar 30 | &.nav-height { 31 | $nav-bar-height: calc(#{$nav-link-height} + #{$nav-link-padding-y * 1.5}); 32 | 33 | &.top { 34 | padding-top: $nav-bar-height; 35 | } 36 | 37 | &.bottom { 38 | padding-bottom: $nav-bar-height; 39 | } 40 | 41 | &.left { 42 | padding-left: $nav-bar-height; 43 | } 44 | 45 | &.right { 46 | padding-right: $nav-bar-height; 47 | } 48 | } 49 | } 50 | 51 | // Line heights 52 | .line-height { 53 | &.none { 54 | line-height: 0 !important; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /client/modules/profile/components/Avatar.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Link } from "react-router-dom"; 4 | import ReactTooltip from "react-tooltip"; 5 | 6 | import { t } from "shared/translations/i18n"; 7 | 8 | import Upload from "common/media/icons/Upload"; 9 | 10 | class Avatar extends Component { 11 | render() { 12 | return ( 13 |
14 |
15 | 16 |
17 | 18 |
19 |
20 | 21 |
22 |
23 |
24 | 25 | 26 |
27 |
28 | ); 29 | } 30 | } 31 | 32 | Avatar.propTypes = { 33 | photo: PropTypes.string 34 | }; 35 | 36 | export default Avatar; 37 | -------------------------------------------------------------------------------- /client/modules/authentication/components/ClientStyling.js: -------------------------------------------------------------------------------- 1 | import { css } from "emotion"; 2 | import { REDUX_STATE } from "shared/constants"; 3 | 4 | export function clientStyling(workspaceURLStatus, clientStyle) { 5 | const style = { button: "", links: "", background: "" }; 6 | if (workspaceURLStatus == REDUX_STATE.FULFILLED && clientStyle != null && clientStyle.size > 0) { 7 | style.button = css` 8 | &, 9 | &:hover, 10 | &:active, 11 | &:visited, 12 | &:focus { 13 | background-color: ${clientStyle.get("primaryColor")} !important; 14 | border-color: ${clientStyle.get("primaryColor")} !important; 15 | } 16 | &:hover:not([disabled]) { 17 | opacity: 0.9; 18 | } 19 | `; 20 | style.links = css` 21 | a, 22 | a:active, 23 | a.visited { 24 | color: ${clientStyle.get("primaryColor")}; 25 | } 26 | a:hover { 27 | color: ${clientStyle.get("primaryColor")}; 28 | } 29 | `; 30 | style.background = css( 31 | Object.assign( 32 | {}, 33 | clientStyle.get("backgroundColor") != null && { backgroundColor: `${clientStyle.get("backgroundColor")} !important` }, 34 | clientStyle.get("backgroundImage") && { backgroundImage: `url('${clientStyle.get("backgroundImage")}') !important` } 35 | ) 36 | ); 37 | } 38 | return style; 39 | } 40 | -------------------------------------------------------------------------------- /server/models/emailVerificationCode.js: -------------------------------------------------------------------------------- 1 | module.exports = function(sequelize, DataTypes) { 2 | return sequelize.define( 3 | "emailVerificationCode", 4 | { 5 | id: { 6 | type: DataTypes.INTEGER(11).UNSIGNED, 7 | allowNull: false, 8 | primaryKey: true, 9 | autoIncrement: true, 10 | field: "id" 11 | }, 12 | verificationCode: { 13 | type: DataTypes.STRING(255), 14 | allowNull: false, 15 | defaultValue: "", 16 | field: "verificationCode" 17 | }, 18 | activated: { 19 | type: DataTypes.INTEGER(1), 20 | allowNull: false, 21 | field: "activated" 22 | }, 23 | userId: { 24 | type: DataTypes.INTEGER(11).UNSIGNED, 25 | allowNull: false, 26 | field: "userId" 27 | }, 28 | clientId: { 29 | type: DataTypes.INTEGER(11).UNSIGNED, 30 | allowNull: false, 31 | field: "clientId" 32 | }, 33 | gracePeriod: { 34 | type: DataTypes.INTEGER(2).UNSIGNED, 35 | allowNull: false, 36 | field: "gracePeriod" 37 | }, 38 | createdAt: { 39 | type: DataTypes.DATE, 40 | allowNull: false, 41 | field: "createdAt" 42 | }, 43 | updatedAt: { 44 | type: DataTypes.DATE, 45 | allowNull: false, 46 | field: "updatedAt" 47 | } 48 | }, 49 | { 50 | tableName: "emailVerificationCode" 51 | } 52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /server/models/changeEmailAddress.js: -------------------------------------------------------------------------------- 1 | module.exports = function(sequelize, DataTypes) { 2 | return sequelize.define( 3 | "changeEmailAddress", 4 | { 5 | id: { 6 | type: DataTypes.INTEGER(11).UNSIGNED, 7 | allowNull: false, 8 | primaryKey: true, 9 | autoIncrement: true 10 | }, 11 | emailCode: { 12 | type: DataTypes.STRING(255), 13 | allowNull: false, 14 | defaultValue: "" 15 | }, 16 | activated: { 17 | type: DataTypes.INTEGER(1), 18 | allowNull: false 19 | }, 20 | userId: { 21 | type: DataTypes.INTEGER(11).UNSIGNED, 22 | allowNull: false, 23 | }, 24 | clientId: { 25 | type: DataTypes.INTEGER(11).UNSIGNED, 26 | allowNull: false, 27 | }, 28 | gracePeriod: { 29 | type: DataTypes.INTEGER(2).UNSIGNED, 30 | allowNull: false 31 | }, 32 | oldEmailAddress: { 33 | type: DataTypes.STRING(255), 34 | allowNull: false, 35 | defaultValue: "" 36 | }, 37 | newEmailAddress: { 38 | type: DataTypes.STRING(255), 39 | allowNull: false, 40 | defaultValue: "" 41 | }, 42 | createdAt: { 43 | type: DataTypes.DATE, 44 | allowNull: false 45 | }, 46 | updatedAt: { 47 | type: DataTypes.DATE, 48 | allowNull: false 49 | } 50 | }, 51 | { 52 | tableName: "changeEmailAddress" 53 | } 54 | ); 55 | }; 56 | -------------------------------------------------------------------------------- /database/scripts/180716_03.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO `executedScripts` (`name`, `description`, `createdDate`, `executedDate`) 2 | VALUES 3 | ('180716_03', 'Updated created at table columns', NOW(), NOW()); 4 | 5 | ALTER TABLE `subscriptions` ADD COLUMN `createdAt` DATETIME NULL; 6 | ALTER TABLE `subscriptions` ADD COLUMN `updatedAt` DATETIME NULL; 7 | 8 | ALTER TABLE `client` 9 | MODIFY COLUMN `createdAt` DATETIME; 10 | 11 | ALTER TABLE `client` 12 | MODIFY COLUMN `updatedAt` DATETIME; 13 | 14 | ALTER TABLE `clientStyling` 15 | MODIFY COLUMN `createdAt` DATETIME; 16 | 17 | ALTER TABLE `clientStyling` 18 | MODIFY COLUMN `updatedAt` DATETIME; 19 | 20 | ALTER TABLE `features` 21 | MODIFY COLUMN `createdAt` DATETIME; 22 | 23 | ALTER TABLE `features` 24 | MODIFY COLUMN `updatedAt` DATETIME; 25 | 26 | ALTER TABLE `roles` 27 | MODIFY COLUMN `createdAt` DATETIME; 28 | 29 | ALTER TABLE `roles` 30 | MODIFY COLUMN `updatedAt` DATETIME; 31 | 32 | ALTER TABLE `subscriptionFeatures` 33 | MODIFY COLUMN `createdAt` DATETIME; 34 | 35 | ALTER TABLE `subscriptionFeatures` 36 | MODIFY COLUMN `updatedAt` DATETIME; 37 | 38 | ALTER TABLE `user` 39 | MODIFY COLUMN `createdAt` DATETIME; 40 | 41 | ALTER TABLE `user` 42 | MODIFY COLUMN `updatedAt` DATETIME; 43 | 44 | ALTER TABLE `userRoles` 45 | MODIFY COLUMN `createdAt` DATETIME; 46 | 47 | ALTER TABLE `userRoles` 48 | MODIFY COLUMN `updatedAt` DATETIME; -------------------------------------------------------------------------------- /server/models/emailTemplates.js: -------------------------------------------------------------------------------- 1 | module.exports = function(sequelize, DataTypes) { 2 | return sequelize.define( 3 | "emailTemplates", 4 | { 5 | id: { 6 | type: DataTypes.INTEGER(11).UNSIGNED, 7 | allowNull: false, 8 | primaryKey: true, 9 | autoIncrement: true, 10 | field: "id" 11 | }, 12 | type: { 13 | type: DataTypes.INTEGER(11).UNSIGNED, 14 | allowNull: false, 15 | field: "type" 16 | }, 17 | language: { 18 | type: DataTypes.INTEGER(11).UNSIGNED, 19 | allowNull: false, 20 | field: "language" 21 | }, 22 | name: { 23 | type: DataTypes.STRING(255), 24 | allowNull: false, 25 | defaultValue: "", 26 | field: "name" 27 | }, 28 | description: { 29 | type: DataTypes.STRING(255), 30 | allowNull: true, 31 | field: "description" 32 | }, 33 | subject: { 34 | type: DataTypes.STRING(255), 35 | allowNull: false, 36 | defaultValue: "", 37 | field: "subject" 38 | }, 39 | html: { 40 | type: DataTypes.TEXT, 41 | allowNull: false, 42 | field: "html" 43 | }, 44 | createdAt: { 45 | type: DataTypes.DATE, 46 | allowNull: true, 47 | field: "createdAt" 48 | }, 49 | updatedAt: { 50 | type: DataTypes.DATE, 51 | allowNull: true, 52 | field: "updatedAt" 53 | } 54 | }, 55 | { 56 | tableName: "emailTemplates" 57 | } 58 | ); 59 | }; 60 | -------------------------------------------------------------------------------- /client/modules/settings/components/MenuLink.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { NavLink } from "react-router-dom"; 3 | import PropTypes from "prop-types"; 4 | import HideComponent from "common/components/HideComponent"; 5 | 6 | import User from "common/components/User"; 7 | 8 | class MenuLink extends Component { 9 | render() { 10 | const { title, route, isExact, user, feature, role, subscription, hasVerifiedEmail } = this.props; 11 | 12 | return ( 13 | 14 |
  • 15 | 16 | {title} 17 | 18 |
  • 19 |
    20 | ); 21 | } 22 | } 23 | 24 | MenuLink.defaultProps = { 25 | title: "", 26 | route: "/", 27 | isExact: false 28 | }; 29 | 30 | MenuLink.propTypes = { 31 | title: PropTypes.string, 32 | route: PropTypes.string, 33 | user: PropTypes.object, 34 | isExact: PropTypes.bool, 35 | feature: PropTypes.oneOfType([PropTypes.array, PropTypes.number]), 36 | role: PropTypes.oneOfType([PropTypes.array, PropTypes.number]), 37 | subscription: PropTypes.oneOfType([PropTypes.array, PropTypes.number]), 38 | hasVerifiedEmail: PropTypes.bool 39 | }; 40 | 41 | export default User(MenuLink); 42 | -------------------------------------------------------------------------------- /client/common/components/inputs/WorkspaceURLField.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { t } from "shared/translations/i18n"; 4 | 5 | class InputField extends React.Component { 6 | render() { 7 | const { label, value, onChange, disabled, error } = this.props; 8 | 9 | return ( 10 |
    11 | {label && } 12 |
    13 | 23 |
    24 |
    {`.${BUILD_DOMAINPATH /* Environmental variable defined by Webpack DefinePlugin */}`}
    25 |
    26 | {error && error["workspaceURL"] &&
    {error["workspaceURL"][0]}
    } 27 |
    28 |
    29 | ); 30 | } 31 | } 32 | 33 | InputField.propTypes = { 34 | label: PropTypes.string, 35 | value: PropTypes.string, 36 | onChange: PropTypes.func, 37 | disabled: PropTypes.bool, 38 | error: PropTypes.object 39 | }; 40 | 41 | export default InputField; 42 | -------------------------------------------------------------------------------- /server/services/sequelize.js: -------------------------------------------------------------------------------- 1 | import Sequelize from "sequelize"; 2 | import fs from "fs"; 3 | import path from "path"; 4 | import config from "../../config"; 5 | 6 | let connection = null; 7 | let sqModels = {}; 8 | 9 | export function connect(done) { 10 | // Connect to database through Sequelize 11 | connection = new Sequelize(config.database.schema, config.database.username, config.database.password, { 12 | host: config.database.host, 13 | dialect: "mysql", 14 | pool: { 15 | max: config.database.max, 16 | min: config.database.min, 17 | acquire: config.database.acquire, 18 | idle: config.database.idle 19 | }, 20 | logging: false 21 | }); 22 | 23 | // Import models to sequelize from the models directory 24 | fs.readdirSync(path.join(__dirname, "../models")).forEach(function(file) { 25 | if (file.toLowerCase().indexOf(".js")) { 26 | var model = connection.import(path.join(__dirname, "../models", file)); 27 | sqModels[model.name] = model; 28 | } 29 | }); 30 | 31 | // Store models in object for retrieval 32 | Object.keys(sqModels).forEach(modelName => { 33 | if (sqModels[modelName].associate) { 34 | sqModels[modelName].associate(sqModels); 35 | } 36 | }); 37 | 38 | // Notify once complete 39 | done(); 40 | } 41 | 42 | export { Sequelize }; 43 | 44 | export function database() { 45 | return connection; 46 | } 47 | 48 | export function models() { 49 | return sqModels; 50 | } 51 | -------------------------------------------------------------------------------- /server/models/clientStyling.js: -------------------------------------------------------------------------------- 1 | module.exports = function(sequelize, DataTypes) { 2 | return sequelize.define( 3 | "clientStyling", 4 | { 5 | id: { 6 | type: DataTypes.INTEGER(11).UNSIGNED, 7 | allowNull: false, 8 | primaryKey: true, 9 | autoIncrement: true, 10 | field: "id" 11 | }, 12 | clientId: { 13 | type: DataTypes.INTEGER(11).UNSIGNED, 14 | allowNull: false, 15 | references: { 16 | model: "client", 17 | key: "id" 18 | }, 19 | field: "clientId" 20 | }, 21 | logoImage: { 22 | type: DataTypes.STRING(255), 23 | allowNull: true, 24 | field: "logoImage" 25 | }, 26 | backgroundImage: { 27 | type: DataTypes.STRING(255), 28 | allowNull: true, 29 | field: "backgroundImage" 30 | }, 31 | backgroundColor: { 32 | type: DataTypes.STRING(32), 33 | allowNull: true, 34 | field: "backgroundColor" 35 | }, 36 | primaryColor: { 37 | type: DataTypes.STRING(32), 38 | allowNull: true, 39 | field: "primaryColor" 40 | }, 41 | secondaryColor: { 42 | type: DataTypes.STRING(32), 43 | allowNull: true, 44 | field: "secondaryColor" 45 | }, 46 | createdAt: { 47 | type: DataTypes.DATE, 48 | allowNull: true, 49 | field: "createdAt" 50 | }, 51 | updatedAt: { 52 | type: DataTypes.DATE, 53 | allowNull: true, 54 | field: "updatedAt" 55 | } 56 | }, 57 | { 58 | tableName: "clientStyling" 59 | } 60 | ); 61 | }; 62 | -------------------------------------------------------------------------------- /client/common/components/Alert.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | import ReactModal from "react-modal"; 4 | import { t } from "shared/translations/i18n"; 5 | 6 | class Alert extends Component { 7 | render() { 8 | const { title, children, closeModal } = this.props; 9 | 10 | return ( 11 |
    12 | 13 |
    14 |
    15 |
    {title}
    16 | 19 |
    20 |
    {children}
    21 |
    22 | {closeModal && ( 23 | 26 | )} 27 |
    28 |
    29 |
    30 |
    31 | ); 32 | } 33 | } 34 | 35 | Alert.propTypes = { 36 | title: PropTypes.string, 37 | children: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), 38 | closeModal: PropTypes.func 39 | }; 40 | 41 | export default Alert; 42 | -------------------------------------------------------------------------------- /client/modules/authentication/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Route } from "react-router"; 3 | import { Switch } from "react-router-dom"; 4 | 5 | import AsyncComponent from "common/components/AsyncComponent"; 6 | 7 | import SignIn from "./SignIn"; 8 | import Forgot from "./Forgot"; 9 | 10 | const Register = AsyncComponent(() => import("./Register")); 11 | const ResetPassword = AsyncComponent(() => import("./ResetPassword")); 12 | const VerifyEmail = AsyncComponent(() => import("common/components/verification/VerifyEmail")); 13 | const VerifyEmailChange = AsyncComponent(() => import("common/components/verification/VerifyEmailChange")); 14 | 15 | class Authentication extends Component { 16 | render() { 17 | return ( 18 |
    19 |
    20 |
    21 | 22 | } /> 23 | } /> 24 | } /> 25 | } /> 26 | } /> 27 | } /> 28 | } /> 29 | 30 |
    31 |
    32 |
    33 | ); 34 | } 35 | } 36 | 37 | export default Authentication; 38 | -------------------------------------------------------------------------------- /client/common/components/inputs/InputField.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | class InputField extends React.Component { 5 | render() { 6 | const { name, value, onChange, label, id, type, ariaLabel, smallText, placeholder, required, disabled, error } = this.props; 7 | 8 | return ( 9 |
    10 | {label && ( 11 | 14 | )} 15 |
    16 | 27 | {error && error[name] &&
    {error[name][0]}
    } 28 | {smallText && ( 29 |
    30 | {smallText} 31 |
    32 | )} 33 |
    34 |
    35 | ); 36 | } 37 | } 38 | 39 | InputField.propTypes = { 40 | name: PropTypes.string, 41 | value: PropTypes.string, 42 | onChange: PropTypes.func, 43 | label: PropTypes.string, 44 | id: PropTypes.string, 45 | type: PropTypes.string, 46 | ariaLabel: PropTypes.string, 47 | placeholder: PropTypes.string, 48 | smallText: PropTypes.string, 49 | required: PropTypes.bool, 50 | disabled: PropTypes.bool, 51 | error: PropTypes.object 52 | }; 53 | 54 | export default InputField; 55 | -------------------------------------------------------------------------------- /client/common/components/MissingPath.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from "react"; 2 | import { Link } from "react-router-dom"; 3 | import { Helmet } from "react-helmet"; 4 | import PropTypes from "prop-types"; 5 | import { t } from "shared/translations/i18n"; 6 | import { FEATURES } from "shared/constants"; 7 | import { arrayContains } from "shared/utilities/filters"; 8 | 9 | import User from "common/components/User"; 10 | 11 | class MissingPath extends React.Component { 12 | render() { 13 | const { user } = this.props; 14 | 15 | const subscriptionExpired = user && user.get("subscriptionActive") === false && arrayContains(FEATURES.BILLING, user.get("clientFeatures").toJS() || []); 16 | 17 | return ( 18 | 19 | 28 |
    29 |
    30 |

    {t("label.oops")}

    31 |

    {t("error.code.404")}

    32 | {subscriptionExpired &&

    {t("components.billing.subscriptionExpired")}

    } 33 | 34 | {t("label.returnToHomepage")} 35 | 36 |
    37 |
    38 |
    39 | ); 40 | } 41 | } 42 | 43 | MissingPath.propTypes = { 44 | user: PropTypes.object 45 | }; 46 | 47 | export default User(MissingPath); 48 | -------------------------------------------------------------------------------- /client/common/styles/modules/_containers.scss: -------------------------------------------------------------------------------- 1 | @import "./node_modules/bootstrap/scss/_nav"; 2 | 3 | // Container with 100% height that grows with the web browser 4 | .container-flexible-height { 5 | // Calculate a container height excluding bootstrap nav-bar 6 | $container-height: calc(100vh - #{$nav-link-height} - #{$nav-link-padding-y * 2}); 7 | $navigation-height: calc(#{$nav-link-height} + #{$nav-link-padding-y * 2}); 8 | 9 | display: flex !important; 10 | flex-direction: column !important; 11 | min-height: $container-height; 12 | 13 | .row { 14 | flex-grow: 1; 15 | } 16 | 17 | .sticky-sidebar { 18 | position: sticky; 19 | top: $navigation-height; 20 | } 21 | } 22 | 23 | // Fixed position overlay over the entire browser window 24 | .window-overlay { 25 | position: fixed; 26 | z-index: 1300; 27 | top: 0; 28 | bottom: 0; 29 | left: 0; 30 | right: 0; 31 | 32 | &.light { 33 | background: rgba(255, 255, 255, 0.3); 34 | } 35 | 36 | &.dark { 37 | background: rgba(0, 0, 0, 0.3); 38 | } 39 | } 40 | 41 | // Container element for positioning forms 42 | .form-container { 43 | background-color: #ffffff; 44 | 45 | .logo { 46 | img { 47 | max-width: 180px; 48 | height: auto; 49 | } 50 | } 51 | } 52 | 53 | // Container for positioning background images 54 | .background-container { 55 | background-color: #3c6fa5; 56 | background-repeat: no-repeat; 57 | background-position: center center; 58 | background-attachment: fixed; 59 | background-size: cover; 60 | -webkit-background-size: cover; 61 | -moz-background-size: cover; 62 | -o-background-size: cover; 63 | } 64 | -------------------------------------------------------------------------------- /server/models/sentEmails.js: -------------------------------------------------------------------------------- 1 | module.exports = function(sequelize, DataTypes) { 2 | return sequelize.define( 3 | "sentEmails", 4 | { 5 | id: { 6 | type: DataTypes.BIGINT, 7 | allowNull: false, 8 | primaryKey: true, 9 | autoIncrement: true, 10 | field: "id" 11 | }, 12 | clientId: { 13 | type: DataTypes.INTEGER(11).UNSIGNED, 14 | allowNull: true, 15 | field: "clientId" 16 | }, 17 | userId: { 18 | type: DataTypes.INTEGER(11).UNSIGNED, 19 | allowNull: true, 20 | field: "userId" 21 | }, 22 | emailType: { 23 | type: DataTypes.INTEGER(11).UNSIGNED, 24 | allowNull: false, 25 | field: "emailType" 26 | }, 27 | emailLanguage: { 28 | type: DataTypes.INTEGER(11).UNSIGNED, 29 | allowNull: false, 30 | field: "emailLanguage" 31 | }, 32 | to: { 33 | type: DataTypes.STRING(255), 34 | allowNull: false, 35 | defaultValue: "", 36 | field: "to" 37 | }, 38 | from: { 39 | type: DataTypes.STRING(255), 40 | allowNull: false, 41 | defaultValue: "", 42 | field: "from" 43 | }, 44 | subject: { 45 | type: DataTypes.STRING(255), 46 | allowNull: false, 47 | defaultValue: "", 48 | field: "subject" 49 | }, 50 | contents: { 51 | type: DataTypes.TEXT, 52 | allowNull: false, 53 | field: "contents" 54 | }, 55 | createdAt: { 56 | type: DataTypes.DATE, 57 | allowNull: false, 58 | field: "createdAt" 59 | }, 60 | updatedAt: { 61 | type: DataTypes.DATE, 62 | allowNull: false, 63 | field: "updatedAt" 64 | } 65 | }, 66 | { 67 | tableName: "sentEmails" 68 | } 69 | ); 70 | }; 71 | -------------------------------------------------------------------------------- /client/common/components/inputs/TextArea.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | class TextArea extends React.Component { 5 | render() { 6 | const { name, value, onChange, label, rows, id, ariaLabel, smallText, resize, placeholder, required, disabled, error } = this.props; 7 | 8 | return ( 9 |
    10 | {label && ( 11 | 14 | )} 15 |
    16 |