├── 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 |
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 |
13 | {smallText ? {label} : label}
14 |
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 |
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 &&
{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 |
17 | ×
18 |
19 |
20 |
{children}
21 |
22 | {closeModal && (
23 |
24 | {t("action.close")}
25 |
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 |
12 | {label} {required && "*"}
13 |
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 |
12 | {label} {required && "*"}
13 |
14 | )}
15 |
16 |
28 | {error && error[name] &&
{error[name][0]}
}
29 | {smallText && (
30 |
31 | {smallText}
32 |
33 | )}
34 |
35 |
36 | );
37 | }
38 | }
39 |
40 | TextArea.propTypes = {
41 | name: PropTypes.string,
42 | value: PropTypes.string,
43 | onChange: PropTypes.func,
44 | label: PropTypes.string,
45 | id: PropTypes.string,
46 | ariaLabel: PropTypes.string,
47 | rows: PropTypes.number,
48 | resize: PropTypes.bool,
49 | placeholder: PropTypes.string,
50 | smallText: PropTypes.string,
51 | required: PropTypes.bool,
52 | disabled: PropTypes.bool,
53 | error: PropTypes.object
54 | };
55 |
56 | export default TextArea;
57 |
--------------------------------------------------------------------------------
/client/common/components/ServerError.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import PropTypes from "prop-types";
3 | import { withRouter } from "react-router-dom";
4 |
5 | import Alert from "common/components/Alert";
6 | import { t } from "shared/translations/i18n";
7 |
8 | class ServerError extends Component {
9 | render() {
10 | const { history, error, showMessage, showAlert } = this.props;
11 |
12 | if (showAlert && error && (error.status === 429 || error.status === 500)) {
13 | return (
14 | history.push("/")}>
15 | {error.message}
16 |
17 | );
18 | }
19 |
20 | if (showMessage && error && error.reason) {
21 | const reason = error.reason;
22 | if (typeof reason === "string" || reason instanceof String) {
23 | return {reason}
;
24 | }
25 |
26 | if (typeof reason === "object" || reason instanceof Object) {
27 | let reasons = [];
28 | for (const key of Object.keys(reason)) {
29 | reasons.push({reason[key]}
);
30 | }
31 | if (reasons.length > 0) {
32 | return {reasons}
;
33 | }
34 | }
35 | }
36 |
37 | return null;
38 | }
39 | }
40 |
41 | ServerError.defaultProps = {
42 | showMessage: true,
43 | showAlert: true,
44 | error: {}
45 | };
46 |
47 | ServerError.propTypes = {
48 | history: PropTypes.object,
49 | showMessage: PropTypes.bool,
50 | showAlert: PropTypes.bool,
51 | error: PropTypes.object
52 | };
53 |
54 | export default withRouter(ServerError);
55 |
--------------------------------------------------------------------------------
/client/modules/header/components/NavDropdownLink.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { Link } 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 NavDropdownLink extends Component {
9 | render() {
10 | const { title, route, user, icon, hasAnyRole, hasAllRoles, hasAnyFeature, hasAllFeatures, hasAnySubscription, hasVerifiedEmail } = this.props;
11 |
12 | return (
13 |
22 |
23 | {icon && {icon} } {title}
24 |
25 |
26 | );
27 | }
28 | }
29 |
30 | NavDropdownLink.defaultProps = {
31 | title: "",
32 | route: "/"
33 | };
34 |
35 | NavDropdownLink.propTypes = {
36 | title: PropTypes.string,
37 | route: PropTypes.string,
38 | user: PropTypes.object,
39 | icon: PropTypes.object,
40 | hasAnyRole: PropTypes.oneOfType([PropTypes.array, PropTypes.number]),
41 | hasAllRoles: PropTypes.oneOfType([PropTypes.array, PropTypes.number]),
42 | hasAnyFeature: PropTypes.oneOfType([PropTypes.array, PropTypes.number]),
43 | hasAllFeatures: PropTypes.oneOfType([PropTypes.array, PropTypes.number]),
44 | hasAnySubscription: PropTypes.oneOfType([PropTypes.array, PropTypes.number]),
45 | hasVerifiedEmail: PropTypes.bool
46 | };
47 |
48 | export default User(NavDropdownLink);
49 |
--------------------------------------------------------------------------------
/client/modules/settings/components/Appearance/components/ImageField.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import PropTypes from "prop-types";
3 |
4 | class ImageField extends Component {
5 | render() {
6 | const { id, name, activeImage, imagePreview, label, required, smallText, disabled, error } = this.props;
7 |
8 | const showPreview = imagePreview && imagePreview.files && imagePreview.files.length > 0 && imagePreview.files[0].src && imagePreview.files[0].src.base64;
9 |
10 | return (
11 |
12 | {label && (
13 |
14 | {label} {required && "*"}
15 |
16 | )}
17 |
18 |
!disabled && this.props.onClick(name)}>
19 | {showPreview ?
:
}
20 |
21 | {error && error[name] &&
{error[name][0]}
}
22 | {smallText && (
23 |
24 | {smallText}
25 |
26 | )}
27 |
28 |
29 | );
30 | }
31 | }
32 |
33 | ImageField.propTypes = {
34 | id: PropTypes.string,
35 | name: PropTypes.string,
36 | activeImage: PropTypes.string,
37 | imagePreview: PropTypes.object,
38 | label: PropTypes.string,
39 | onClick: PropTypes.func,
40 | required: PropTypes.bool,
41 | smallText: PropTypes.string,
42 | disabled: PropTypes.bool,
43 | error: PropTypes.object
44 | };
45 |
46 | export default ImageField;
47 |
--------------------------------------------------------------------------------
/database/scripts/180720_01.sql:
--------------------------------------------------------------------------------
1 | INSERT INTO `executedScripts` (`name`, `description`, `createdDate`, `executedDate`)
2 | VALUES
3 | ('180720_01', 'Created Client Welcome Email template', NOW(), NOW());
4 |
5 | INSERT INTO `emailTemplates` (`id`, `type`, `language`, `name`, `description`, `subject`, `html`, `createdAt`, `updatedAt`)
6 | VALUES
7 | (1, 1, 1, 'Client Welcome', 'Introductory email (with activation link) on client registration', 'Welcome to Reeve', '\n\n \n \n \n \n Welcome to Reeve \n \n\n \n \n
Dear <%= firstName %>,
\n
Congratulations! You\'ve just joined a phenominal SaaS company, and an amazing trial product to enjoy for the next 30 days.
\n
Please click the following link to verify your email address \"><%= validationLink %> .
\n
Once your email is verified, you can continue to access your account from the following link \"><%= workspaceURL %> .
\n
Please reply to this email if you have any suggestions/feedback.
\n
Best Regards, Reeve Customer Support
\n
\n \n \n \n', NOW(), NOW());
8 |
--------------------------------------------------------------------------------
/client/common/components/GoogleAnalytics.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { Route } from "react-router";
3 | import { withRouter } from "react-router-dom";
4 | import ReactGA from "react-ga";
5 |
6 | /* eslint-disable */
7 | const enabled = GOOGLE_ANALYTICS_ENABLED;
8 | const tracking = GOOGLE_ANALYTICS_TRACKING;
9 | /* eslint-enable */
10 |
11 | export function initializeGA() {
12 | if (enabled) {
13 | ReactGA.initialize(tracking);
14 | }
15 | }
16 |
17 | export function setGAUser(userId) {
18 | if (enabled) {
19 | ReactGA.set({ userId: userId });
20 | }
21 | }
22 |
23 | class GoogleAnalytics extends Component {
24 | componentDidMount() {
25 | this.setPage(this.props.location.pathname, this.props.location.search);
26 | }
27 |
28 | componentDidUpdate({ location: prevLocation }) {
29 | const {
30 | location: { pathname, search }
31 | } = this.props;
32 | const differentPathname = pathname !== prevLocation.pathname;
33 | const differentSearch = search !== prevLocation.search;
34 |
35 | if (differentPathname || differentSearch) {
36 | this.setPage(pathname, search);
37 | }
38 | }
39 |
40 | setPage(pathname, search = "") {
41 | const page = pathname + search;
42 | const { location } = window;
43 | ReactGA.set({
44 | page,
45 | location: `${location.origin}${page}`,
46 | ...this.props.options
47 | });
48 | ReactGA.pageview(page);
49 | }
50 |
51 | render() {
52 | return null;
53 | }
54 | }
55 |
56 | export function GoogleAnalyticsTracker() {
57 | if (enabled) {
58 | const Tracker = withRouter(GoogleAnalytics);
59 | return } />;
60 | } else {
61 | return null;
62 | }
63 | }
64 |
65 | export default GoogleAnalytics;
66 |
--------------------------------------------------------------------------------
/client/common/styles/modules/_images.scss:
--------------------------------------------------------------------------------
1 | @import "./bootstrap_variations";
2 |
3 | // Avatar Icon
4 | .avatar {
5 | &.small {
6 | $square: 1.5rem;
7 |
8 | width: $square;
9 | height: $square;
10 |
11 | img {
12 | min-width: $square;
13 | min-height: $square;
14 | max-width: $square;
15 | max-height: $square;
16 | object-fit: contain;
17 | }
18 | }
19 |
20 | &.large {
21 | $square: 10rem;
22 |
23 | width: $square;
24 | height: $square;
25 |
26 | img {
27 | min-width: $square;
28 | min-height: $square;
29 | max-width: $square;
30 | max-height: $square;
31 | object-fit: contain;
32 | }
33 |
34 | .image-overlay {
35 | position: absolute;
36 | top: 0;
37 | bottom: 0;
38 | left: 50%;
39 | right: 0;
40 | min-width: $square;
41 | min-height: $square;
42 | max-width: $square;
43 | max-height: $square;
44 | margin-left: -5rem;
45 | opacity: 0;
46 | transition: 0.3s ease;
47 | background-color: #3a3a3a;
48 |
49 | .icon {
50 | color: white;
51 | font-size: 1rem;
52 | text-align: center;
53 | position: absolute;
54 | top: 50%;
55 | left: 50%;
56 | -webkit-transform: translate(-50%, -50%);
57 | -ms-transform: translate(-50%, -50%);
58 | transform: translate(-50%, -50%);
59 | }
60 | }
61 |
62 | :hover .image-overlay {
63 | opacity: 0.5;
64 | cursor: pointer;
65 | }
66 | }
67 | }
68 |
69 | // Appearance & Branding Image Containers
70 | .image-container {
71 | $size: 6rem;
72 | background-color: $gray-200;
73 |
74 | img {
75 | min-width: calc(#{$size * 1.8});
76 | min-height: $size;
77 | max-width: calc(#{$size * 1.8});
78 | max-height: $size;
79 | object-fit: contain;
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/database/scripts/180811_01.sql:
--------------------------------------------------------------------------------
1 | INSERT INTO `executedScripts` (`name`, `description`, `createdDate`, `executedDate`)
2 | VALUES
3 | ('180811_01', 'Reset password success notification email', NOW(), NOW());
4 |
5 | INSERT INTO `emailTypes` (`id`, `name`, `description`, `createdAt`, `updatedAt`)
6 | VALUES
7 | (5, 'Reset Password Success', 'When a users password is reset, send an email notifying the user', NULL, NULL);
8 |
9 | INSERT INTO `emailTemplates` (`id`, `type`, `language`, `name`, `description`, `subject`, `html`, `createdAt`, `updatedAt`)
10 | VALUES
11 | (5, 5, 1, 'Password Reset Success', 'Success email when a user resets their password', 'Your account password has changed | Reeve', '\n\n \n \n \n \n Reset Password | Reeve \n \n\n \n \n
Dear <%= firstName %>,
\n
We are sending you this email to confirm that the password to your account on the workspace <%= workspaceName %> has recently been updated.
\n
If you did not make this change. Please contact support immediately by replying to this email.
\n
Best Regards, Reeve Customer Support
\n
\n \n \n \n', NOW(), NOW());
12 |
--------------------------------------------------------------------------------
/database/scripts/180805_01.sql:
--------------------------------------------------------------------------------
1 | INSERT INTO `executedScripts` (`name`, `description`, `createdDate`, `executedDate`)
2 | VALUES
3 | ('180805_01', 'Email with link to reset user password', NOW(), NOW());
4 |
5 | INSERT INTO `emailTypes` (`id`, `name`, `description`, `createdAt`, `updatedAt`)
6 | VALUES
7 | (4, 'Forgot Password', 'Email with link to reset account password', NOW(), NOW());
8 |
9 | INSERT INTO `emailTemplates` (`id`, `type`, `language`, `name`, `description`, `subject`, `html`, `createdAt`, `updatedAt`)
10 | VALUES
11 | (4, 4, 1, 'Reset Password', 'Email with link to reset account password', 'Reset Password | Reeve', '\n\n \n \n \n \n Reset Password | Reeve \n \n\n \n \n
Dear <%= firstName %>,
\n
You recently entered your email address into the forgot account details page of the workspace <%= clientName %> . We want to help you get back into your account.
\n
You can use the following link below to reset the password to your account:
\n
\n
Best Regards, Reeve Customer Support
\n
\n \n \n \n', NOW(), NOW());
12 |
--------------------------------------------------------------------------------
/server/models/failedEmails.js:
--------------------------------------------------------------------------------
1 | module.exports = function(sequelize, DataTypes) {
2 | return sequelize.define(
3 | "failedEmails",
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 | reason: {
56 | type: DataTypes.TEXT,
57 | allowNull: false,
58 | field: "reason"
59 | },
60 | createdAt: {
61 | type: DataTypes.DATE,
62 | allowNull: false,
63 | field: "createdAt"
64 | },
65 | updatedAt: {
66 | type: DataTypes.DATE,
67 | allowNull: false,
68 | field: "updatedAt"
69 | }
70 | },
71 | {
72 | tableName: "failedEmails"
73 | }
74 | );
75 | };
76 |
--------------------------------------------------------------------------------
/client/modules/header/components/NavMenuLink.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { NavLink } from "react-router-dom";
3 | import PropTypes from "prop-types";
4 |
5 | import User from "common/components/User";
6 | import HideComponent from "common/components/HideComponent";
7 |
8 | class NavMenuLink extends Component {
9 | render() {
10 | const { title, route, isExact, user, hasAnyRole, hasAllRoles, hasAnyFeature, hasAllFeatures, hasAnySubscription, hasVerifiedEmail } = this.props;
11 |
12 | return (
13 |
22 |
23 |
24 | {title}
25 |
26 |
27 |
28 | );
29 | }
30 | }
31 |
32 | NavMenuLink.defaultProps = {
33 | title: "",
34 | route: "/",
35 | isExact: false
36 | };
37 |
38 | NavMenuLink.propTypes = {
39 | title: PropTypes.string,
40 | route: PropTypes.string,
41 | user: PropTypes.object,
42 | isExact: PropTypes.bool,
43 | hasAnyRole: PropTypes.oneOfType([PropTypes.array, PropTypes.number]),
44 | hasAllRoles: PropTypes.oneOfType([PropTypes.array, PropTypes.number]),
45 | hasAnyFeature: PropTypes.oneOfType([PropTypes.array, PropTypes.number]),
46 | hasAllFeatures: PropTypes.oneOfType([PropTypes.array, PropTypes.number]),
47 | hasAnySubscription: PropTypes.oneOfType([PropTypes.array, PropTypes.number]),
48 | hasVerifiedEmail: PropTypes.bool
49 | };
50 |
51 | export default User(NavMenuLink);
52 |
--------------------------------------------------------------------------------
/server/services/papertrail.js:
--------------------------------------------------------------------------------
1 | let winston = require("winston");
2 | let expressWinston = require("express-winston");
3 | let config = require("../../config");
4 |
5 | let logger = null;
6 |
7 | function initialize(app) {
8 | if (!config.papertrail.enabled) {
9 | return;
10 | }
11 |
12 | let WinstonPapertrail = require("winston-papertrail").Papertrail;
13 | const PTtransport = new WinstonPapertrail({
14 | host: config.papertrail.host,
15 | port: config.papertrail.port,
16 | hostname: config.papertrail.hostname,
17 | level: config.papertrail.level,
18 | handleExceptions: true,
19 | logFormat: function(level, message) {
20 | return "[" + level + "] " + message;
21 | }
22 | });
23 |
24 | // Connect express to Papertrail Logging
25 | app.use(
26 | expressWinston.logger({
27 | transports: [PTtransport],
28 | meta: false,
29 | msg:
30 | "{{req.ip}} - {{res.statusCode}} - {{req.method}} - {{res.responseTime}}ms - URL: {{req.url}} - ORIGINAL URL: {{req.originalUrl}} - HOST: {{req.headers['host']}} - ORIGIN: {{req.headers['origin']}} - REFERER: {{req.headers['referer']}} - USER AGENT: {{req.headers['user-agent']}}",
31 | expressFormat: false,
32 | colorize: true,
33 | ignoreRoute: function(req, res) {
34 | return false;
35 | }
36 | })
37 | );
38 |
39 | logger = new winston.Logger({
40 | transports: [PTtransport]
41 | });
42 |
43 | PTtransport.on("error", function(err) {
44 | logger && logger.error(err);
45 | });
46 |
47 | PTtransport.on("connect", function(message) {
48 | logger && logger.info(message);
49 | });
50 | }
51 |
52 | function logging() {
53 | if (!config.papertrail.enabled) {
54 | return;
55 | }
56 |
57 | return logger;
58 | }
59 |
60 | module.exports = {
61 | initialize: initialize,
62 | logging: logging
63 | };
64 |
--------------------------------------------------------------------------------
/client/modules/header/components/ActiveTrial.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { Link } from "react-router-dom";
3 | import PropTypes from "prop-types";
4 | import moment from "moment";
5 |
6 | import { t } from "shared/translations/i18n";
7 | import { SUBSCRIPTION_TYPE, FEATURES } from "shared/constants";
8 | import { arrayContains } from "shared/utilities/filters";
9 | import User from "common/components/User";
10 |
11 | class ActiveTrial extends Component {
12 | render() {
13 | const { user } = this.props;
14 |
15 | // Hide component if following requirements are not met
16 | // Billing Feature, Trial Subscription
17 | if (user.get("subscriptionId") !== SUBSCRIPTION_TYPE.TRIAL || !arrayContains(FEATURES.BILLING, user.get("clientFeatures").toJS() || [])) {
18 | return null;
19 | }
20 |
21 | // Subscription Active?
22 | const subscriptionActive = user.get("subscriptionActive");
23 |
24 | // Calculate days left in trial
25 | const currentDate = moment(user.get("loginTime"));
26 | const endDate = moment(user.get("subscriptionEndDate"));
27 | const trialDaysLeft = endDate.diff(currentDate, "days");
28 |
29 | return (
30 |
31 |
32 |
33 | {subscriptionActive
34 | ? trialDaysLeft > 0
35 | ? t("components.billing.trialDaysLeft", { count: trialDaysLeft })
36 | : t("components.billing.trialDays<1")
37 | : t("components.billing.trialExpired")}
38 |
39 |
40 |
41 | );
42 | }
43 | }
44 |
45 | ActiveTrial.propTypes = {
46 | user: PropTypes.object
47 | };
48 |
49 | export default User(ActiveTrial);
50 |
--------------------------------------------------------------------------------
/database/scripts/180801_01.sql:
--------------------------------------------------------------------------------
1 | INSERT INTO `executedScripts` (`name`, `description`, `createdDate`, `executedDate`)
2 | VALUES
3 | ('180801_01', 'Verify email template', NOW(), NOW());
4 |
5 | INSERT INTO `emailTypes` (`id`, `name`, `description`, `createdAt`, `updatedAt`)
6 | VALUES
7 | (2, 'Verify Email', 'Contains validation link to verify user email (Can be sent multiple times)', NOW(), NOW());
8 |
9 | INSERT INTO `emailTemplates` (`id`, `type`, `language`, `name`, `description`, `subject`, `html`, `createdAt`, `updatedAt`)
10 | VALUES
11 | (2, 2, 1, 'Verify Email', 'User email verification link email', 'Please verify your email address | Reeve', '\n\n \n \n \n \n Please verify your email address | Reeve \n \n\n \n \n
Dear <%= firstName %>,
\n
Please click the following link to verify your email address with Reeve \"><%= validationLink %> .
\n
A verified email address allows you to receive account notifications and ensures you receive updates relating to our product.
\n
Please reply to this email if you have any suggestions/feedback.
\n
Best Regards, Reeve Customer Support
\n
\n \n \n \n', '2018-08-01 21:45:00', '2018-08-01 21:45:03');
12 |
13 |
--------------------------------------------------------------------------------
/server/models/plans.js:
--------------------------------------------------------------------------------
1 | module.exports = function(sequelize, DataTypes) {
2 | return sequelize.define(
3 | "plans",
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(128),
14 | allowNull: false,
15 | defaultValue: "",
16 | field: "name"
17 | },
18 | description: {
19 | type: DataTypes.STRING(255),
20 | allowNull: true,
21 | field: "description"
22 | },
23 | stripeProductId: {
24 | type: DataTypes.STRING(128),
25 | allowNull: true,
26 | field: "stripeProductId"
27 | },
28 | billingInterval: {
29 | type: DataTypes.INTEGER(16).UNSIGNED,
30 | allowNull: true,
31 | field: "billingInterval"
32 | },
33 | currency: {
34 | type: DataTypes.INTEGER(11).UNSIGNED,
35 | allowNull: false,
36 | defaultValue: "1",
37 | field: "currency"
38 | },
39 | price: {
40 | type: DataTypes.DECIMAL,
41 | allowNull: false,
42 | defaultValue: "0.00",
43 | field: "price"
44 | },
45 | subscriptionId: {
46 | type: DataTypes.INTEGER(11).UNSIGNED,
47 | allowNull: true,
48 | field: "subscriptionId"
49 | },
50 | newSubscriptionsAllowed: {
51 | type: DataTypes.INTEGER(1),
52 | allowNull: false,
53 | field: "newSubscriptionsAllowed"
54 | },
55 | active: {
56 | type: DataTypes.INTEGER(1),
57 | allowNull: false,
58 | defaultValue: "1",
59 | field: "active"
60 | },
61 | createdAt: {
62 | type: DataTypes.DATE,
63 | allowNull: true,
64 | field: "createdAt"
65 | },
66 | updatedAt: {
67 | type: DataTypes.DATE,
68 | allowNull: true,
69 | field: "updatedAt"
70 | }
71 | },
72 | {
73 | tableName: "plans"
74 | }
75 | );
76 | };
77 |
--------------------------------------------------------------------------------
/shared/validation/validate.js:
--------------------------------------------------------------------------------
1 | import validatejs from "validate.js";
2 | import { t } from "shared/translations/i18n";
3 |
4 | function validate(body, constraints, options = {}) {
5 | let validate = validatejs;
6 |
7 | // Display full messages
8 | validate.options = { fullMessages: false };
9 |
10 | // Custom validator function for situations where we don't need a key to exist, but if it does, it must have a value
11 | validate.validators.noEmptyValue = function(value) {
12 | if (value === "" || (typeof value === "string" && value.trim() === "") || value === [] || value === {}) {
13 | return t("validation.validators.cannotBeBlank");
14 | }
15 | };
16 |
17 | // Overwrite default validator.js translation strings to ensure internationalization
18 | validate.validators.date.options = { message: t("validation.validators.validDate") };
19 | validate.validators.datetime.options = { message: t("validation.validators.validDate") };
20 | validate.validators.email.options = { message: t("validation.validators.invalidEmail") };
21 | validate.validators.equality.options = { message: t("validation.validators.notEqual") };
22 | validate.validators.exclusion.options = { message: t("validation.validators.restricted") };
23 | validate.validators.format.options = { message: t("validation.validators.invalid") };
24 | validate.validators.inclusion.options = { message: t("validation.validators.notIncluded") };
25 | validate.validators.length.options = { message: t("validation.validators.invalid") };
26 | validate.validators.numericality.options = { message: t("validation.validators.invalid") };
27 | validate.validators.presence.options = { message: t("validation.validators.cannotBeBlank") };
28 | validate.validators.url.options = { message: t("validation.validators.invalidURL") };
29 |
30 | return validate(body, constraints, options);
31 | }
32 |
33 | export default validate;
34 |
--------------------------------------------------------------------------------
/server/models/client.js:
--------------------------------------------------------------------------------
1 | module.exports = function(sequelize, DataTypes) {
2 | return sequelize.define(
3 | "client",
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 | workspaceURL: {
19 | type: DataTypes.STRING(255),
20 | allowNull: false,
21 | defaultValue: "",
22 | field: "workspaceURL"
23 | },
24 | description: {
25 | type: DataTypes.STRING(255),
26 | allowNull: true,
27 | field: "description"
28 | },
29 | subscriptionId: {
30 | type: DataTypes.INTEGER(3),
31 | allowNull: false,
32 | defaultValue: "1",
33 | field: "subscriptionId"
34 | },
35 | subscriptionStartDate: {
36 | type: DataTypes.DATE,
37 | allowNull: true,
38 | field: "subscriptionStartDate"
39 | },
40 | subscriptionEndDate: {
41 | type: DataTypes.DATE,
42 | allowNull: true,
43 | field: "subscriptionEndDate"
44 | },
45 | billingCycle: {
46 | type: DataTypes.INTEGER(2),
47 | allowNull: true,
48 | field: "billingCycle"
49 | },
50 | defaultLanguage: {
51 | type: DataTypes.INTEGER(11).UNSIGNED,
52 | allowNull: false,
53 | defaultValue: "1",
54 | field: "defaultLanguage"
55 | },
56 | active: {
57 | type: DataTypes.INTEGER(1),
58 | allowNull: false,
59 | defaultValue: "1",
60 | field: "active"
61 | },
62 | createdAt: {
63 | type: DataTypes.DATE,
64 | allowNull: true,
65 | field: "createdAt"
66 | },
67 | updatedAt: {
68 | type: DataTypes.DATE,
69 | allowNull: true,
70 | field: "updatedAt"
71 | }
72 | },
73 | {
74 | tableName: "client"
75 | }
76 | );
77 | };
78 |
--------------------------------------------------------------------------------
/client/common/store/store.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, compose } from "redux";
2 | import { combineReducers } from "redux-immutable";
3 | import thunk from "redux-thunk";
4 | import Raven from "raven-js";
5 | import createRavenMiddleware from "raven-for-redux";
6 |
7 | import authentication from "./reducers/authentication";
8 | import language from "./reducers/language";
9 |
10 | // Determine if React development tools should be enabled or disabled
11 | if (BUILD_ENVIRONMENT === "production" && window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && Object.keys(window.__REACT_DEVTOOLS_GLOBAL_HOOK__._renderers).length) {
12 | window.__REACT_DEVTOOLS_GLOBAL_HOOK__._renderers = {};
13 | }
14 | // Determine if Redux development tools should be enabled or disabled
15 | const composeEnhancers = (BUILD_ENVIRONMENT !== "production" && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) || compose;
16 |
17 | // Define middleware to be applied
18 | // Environmental variable defined by Webpack DefinePlugin
19 | const middleware = [thunk];
20 | if (SENTRY_ENABLED) {
21 | Raven.config(SENTRY_DSN).install();
22 | middleware.push(
23 | createRavenMiddleware(Raven, {
24 | release: BUILD_RELEASE,
25 | environment: BUILD_ENVIRONMENT
26 | })
27 | );
28 | }
29 |
30 | // Create default reducer object with combined authentication and language reducers
31 | const createReducer = asyncReducers =>
32 | combineReducers({
33 | authentication,
34 | language,
35 | ...asyncReducers
36 | });
37 |
38 | // Inject async reducers
39 | export const injectReducer = (store, key, asyncReducer) => {
40 | store.asyncReducers[key] = asyncReducer;
41 | store.replaceReducer(createReducer(store.asyncReducers));
42 | };
43 |
44 | export default (() => {
45 | const store = createStore(createReducer(), composeEnhancers(applyMiddleware(...middleware)));
46 |
47 | store.asyncReducers = {};
48 | return store;
49 | })();
50 |
--------------------------------------------------------------------------------
/client/modules/authentication/components/WorkspaceURL.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { Link, withRouter } from "react-router-dom";
3 | import PropTypes from "prop-types";
4 | import { t } from "shared/translations/i18n";
5 |
6 | import ServerError from "common/components/ServerError";
7 | import WorkspaceURLField from "common/components/inputs/WorkspaceURLField";
8 |
9 | class WorkspaceURL extends Component {
10 | render() {
11 | const { workspaceURL, changeField, changeSubdomain, redirectPending, validationErrors, serverError } = this.props;
12 |
13 | return (
14 |
15 |
{t("label.welcome")}
16 |
17 |
18 | {t("components.authentication.getStarted")}
19 |
20 |
21 |
22 |
23 | {t("components.authentication.whatIsWorkspaceName")}
24 |
25 |
26 |
27 | {t("action.continue")}
28 |
29 |
30 | {t("components.authentication.noAccount")} {t("action.register")}
31 |
32 |
33 | );
34 | }
35 | }
36 |
37 | WorkspaceURL.propTypes = {
38 | workspaceURL: PropTypes.string,
39 | changeSubdomain: PropTypes.func,
40 | changeField: PropTypes.func,
41 | redirectPending: PropTypes.bool,
42 | validationErrors: PropTypes.object,
43 | serverError: PropTypes.object
44 | };
45 |
46 | export default withRouter(WorkspaceURL);
47 |
--------------------------------------------------------------------------------
/server/controller/billing.js:
--------------------------------------------------------------------------------
1 | import restrict from "utilities/restrictRoutes";
2 | import browserResponseLng from "utilities/browserResponseLng";
3 | import { ROLE_TYPE, FEATURES } from "shared/constants";
4 |
5 | import { loadClientSubscriptionDetails, loadAvailableSubscriptions } from "../orchestrator/billing";
6 |
7 | module.exports = function(router) {
8 | // Load client subscription details
9 | router.get(
10 | "/api/v1.0/billing/client-subscription-details",
11 | restrict({
12 | registered: true,
13 | unregistered: true,
14 | hasAnyRole: [ROLE_TYPE.OWNER, ROLE_TYPE.FINANCE],
15 | hasAllFeatures: [FEATURES.BILLING]
16 | }),
17 | function(req, res, next) {
18 | // Load browser language from header
19 | const browserLng = browserResponseLng(req);
20 |
21 | // Retrieve client subscription details and return response
22 | loadClientSubscriptionDetails(null, null, browserLng).then(
23 | result => {
24 | return res.status(200).send(result);
25 | },
26 | error => {
27 | return next(error);
28 | }
29 | );
30 | }
31 | );
32 |
33 | // Load List of available subscriptions
34 | router.get(
35 | "/api/v1.0/billing/available-subscriptions",
36 | restrict({
37 | registered: true,
38 | unregistered: true,
39 | hasAnyRole: [ROLE_TYPE.OWNER, ROLE_TYPE.FINANCE],
40 | hasAllFeatures: [FEATURES.BILLING]
41 | }),
42 | function(req, res, next) {
43 | // Load browser language from header
44 | const browserLng = browserResponseLng(req);
45 |
46 | // Check for Query strings
47 | const requestProperties = { currency: req.query.currency, interval: req.query.interval };
48 |
49 | // Retrieve client subscription details and return response
50 | loadAvailableSubscriptions(requestProperties, null, browserLng).then(
51 | result => {
52 | return res.status(200).send(result);
53 | },
54 | error => {
55 | return next(error);
56 | }
57 | );
58 | }
59 | );
60 | };
61 |
--------------------------------------------------------------------------------
/config.env:
--------------------------------------------------------------------------------
1 | # This is a generic config file for storing important environment settings
2 | # Don't commit any actual server settings in this file to Git, store the file locally
3 |
4 | # Build
5 | NODE_ENV=development
6 | RELEASE=1.1.0
7 | PROTOCOL=http
8 | KEY=./server.key
9 | CERTIFICATE=./server.cert
10 | SERVER=localhost
11 | PORT=3000
12 | PUBLICPATH=http://localhost:3000/
13 | DOMAINPATH=localhost:3000
14 |
15 | # Authentication
16 | JWT_SECRET=jwtsecretkey
17 | JWT_EXPIRY=14d
18 |
19 | # Development
20 | ANALYZE_BUNDLE=false
21 |
22 | # MySQL Database
23 | DB_HOST=localhost
24 | DB_USER=root
25 | DB_PASS=root
26 | DB_SCHEMA=reeve
27 | DB_POOL_MIN=0
28 | DB_POOL_MAX=10
29 | DB_POOL_ACQUIRE=30000
30 | DB_POOL_IDLE=10000
31 |
32 | # Redis Session Store
33 | R_ENABLED=true
34 | R_HOST=127.0.0.1
35 | R_PORT=6379
36 | R_PASS=
37 | R_KEY_EXPIRY=1209600
38 |
39 | # Email Hosting
40 | EMAIL_ENABLED=false
41 | EMAIL_SENDER_ADDRESS=sender@example.com
42 | EMAIL_HOST=
43 | EMAIL_PORT=
44 | EMAIL_SECURE=false
45 | EMAIL_AUTH_USERNAME=
46 | EMAIL_AUTH_PASSWORD=
47 |
48 | # Google Analytics
49 | GOOGLE_ANALYTICS_ENABLED=false
50 | GOOGLE_ANALYTICS_TRACKING=UA-XXXXXXXX
51 |
52 | # Stripe
53 | STRIPE_ENABLED=true
54 | STRIPE_API_KEY=test_xxxxxxxxxxxxxx
55 |
56 | # AWS S3
57 | S3_ENABLED=false
58 | S3_ACCESS_KEY_ID=
59 | S3_SECRET_ACCESS_KEY=
60 | S3_REGION=
61 | S3_ENDPOINT_URL=
62 | S3_BUCKET=
63 | S3_ACCELERATE_ENDPOINT=
64 | S3_FORCE_PATH_STYLE=
65 | S3_BUCKET_ENDPOINT=
66 | S3_FILE_DEST=
67 | S3_SIGNATURE_VERSION=
68 |
69 | # AWS Cloudfront
70 | CF_ENABLED=false
71 | CF_ACCESS_KEY_ID=
72 | CF_PRIVATE_ACCESS_KEY=
73 | CF_ENDPOINT_URL=https://xxxxxxxxxx.cloudfront.net/
74 |
75 | # Sentry Error Reporting
76 | SENTRY_ENABLED=false
77 | SENTRY_DSN=https://@sentry.io/
78 |
79 | # Papertrail Logging
80 | PAPERTRAIL_ENABLED=false
81 | PAPERTRAIL_HOST=logs.papertrailapp.com
82 | PAPERTRAIL_PORT=1111
83 | PAPERTRAIL_HOSTNAME=host
84 | PAPERTRAIL_LEVEL=debug
85 |
86 | # Delete Workspace
87 | GDPR_HARD_DELETE=false
--------------------------------------------------------------------------------
/client/modules/header/components/HelpCaller.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { withRouter } from "react-router-dom";
3 | import PropTypes from "prop-types";
4 | import ReactTooltip from "react-tooltip";
5 |
6 | import Help from "common/media/icons/Help";
7 | import { t, l } from "shared/translations/i18n";
8 |
9 | class HelpCaller extends Component {
10 | render() {
11 | const { history } = this.props;
12 |
13 | let articleLink = null;
14 | switch (history.location.pathname) {
15 | case "/":
16 | articleLink = l("supportArticles.overview");
17 | break;
18 | case "/profile":
19 | articleLink = l("supportArticles.profile");
20 | break;
21 | case "/profile/change-profile-photo":
22 | articleLink = l("supportArticles.profile");
23 | break;
24 | case "/billing":
25 | articleLink = l("supportArticles.billing");
26 | break;
27 | case "/settings/general":
28 | articleLink = l("supportArticles.settings");
29 | break;
30 | case "/settings/appearance":
31 | articleLink = l("supportArticles.settings");
32 | break;
33 | case "/settings/localization":
34 | articleLink = l("supportArticles.settings");
35 | break;
36 | case "/settings/general/delete-workspace":
37 | articleLink = l("supportArticles.settings");
38 | break;
39 | default:
40 | articleLink = null;
41 | }
42 |
43 | // If path parameters are met, display help icon
44 | if (articleLink !== null) {
45 | return (
46 |
47 |
48 |
49 |
50 |
51 |
52 | );
53 | } else {
54 | return null;
55 | }
56 | }
57 | }
58 |
59 | HelpCaller.propTypes = {
60 | history: PropTypes.object
61 | };
62 |
63 | export default withRouter(HelpCaller);
64 |
--------------------------------------------------------------------------------
/distribution/images/logo_light.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/client/api/profile.js:
--------------------------------------------------------------------------------
1 | import fetch from "common/fetch";
2 |
3 | // Load User Personal Profile Details
4 | export function loadPersonalProfile() {
5 | return fetch.perform("/api/v1.0/profile", {
6 | method: "GET"
7 | });
8 | }
9 |
10 | // Update User Personal Profile
11 | export function updatePersonalProfile(body) {
12 | return fetch.perform("/api/v1.0/profile", {
13 | method: "PATCH",
14 | body: JSON.stringify(body)
15 | });
16 | }
17 |
18 | // Verify User Email Change
19 | export function verifyEmailChange(user) {
20 | return fetch.perform("/api/v1.0/profile/verify-email-change", {
21 | method: "POST",
22 | body: JSON.stringify({
23 | code: user.code,
24 | userId: user.userId,
25 | workspaceURL: user.workspaceURL
26 | })
27 | });
28 | }
29 |
30 | // Change User Password
31 | export function changeUserPassword(body) {
32 | return fetch.perform("/api/v1.0/profile/change-password", {
33 | method: "POST",
34 | body: JSON.stringify({
35 | currentPassword: body.currentPassword,
36 | newPassword: body.newPassword,
37 | confirmPassword: body.confirmPassword
38 | })
39 | });
40 | }
41 |
42 | // Generate signed S3 url to upload photo
43 | export function generateSignedProfilePhotoURL(body) {
44 | return fetch.perform("/api/v1.0/profile/generate-signed-profile-photo-url", {
45 | method: "GET",
46 | headers: {
47 | contentType: body.contentType
48 | }
49 | });
50 | }
51 |
52 | // Send file to signed S3 url
53 | export function uploadProfilePhotoToSignedURL(body) {
54 | return fetch.simple(body.signedURL, {
55 | method: "PUT",
56 | headers: {
57 | "Content-Type": body.contentType
58 | },
59 | body: body.data
60 | });
61 | }
62 |
63 | // Upload photo to signed url and notify backend
64 | export function saveUserProfilePhoto(body) {
65 | return fetch.perform("/api/v1.0/profile/save-profile-photo", {
66 | method: "POST",
67 | body: JSON.stringify({
68 | key: body.key
69 | })
70 | });
71 | }
72 |
73 | // Delete existing user profile photo
74 | export function deleteUserProfilePhoto() {
75 | return fetch.perform("/api/v1.0/profile/delete-profile-photo", {
76 | method: "PATCH"
77 | });
78 | }
79 |
--------------------------------------------------------------------------------
/client/modules/header/components/NavProfileMenu.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import PropTypes from "prop-types";
3 | import { Link } from "react-router-dom";
4 |
5 | import User from "common/components/User";
6 |
7 | class NavProfileMenu extends Component {
8 | constructor() {
9 | super();
10 |
11 | this.state = {
12 | menuVisible: false
13 | };
14 |
15 | this.showMenu = this.showMenu.bind(this);
16 | this.closeMenu = this.closeMenu.bind(this);
17 | }
18 |
19 | componentDidMount() {
20 | this._isMounted = true;
21 | }
22 |
23 | componentWillUnmount() {
24 | this._isMounted = false;
25 | }
26 |
27 | showMenu(evt) {
28 | evt.preventDefault();
29 |
30 | if (this._isMounted) {
31 | this.setState({ menuVisible: true }, () => {
32 | document.addEventListener("click", this.closeMenu);
33 | });
34 | }
35 | }
36 |
37 | closeMenu() {
38 | if (this._isMounted) {
39 | this.setState({ menuVisible: false }, () => {
40 | document.removeEventListener("click", this.closeMenu);
41 | });
42 | }
43 | }
44 |
45 | render() {
46 | const { user, children } = this.props;
47 | const { menuVisible } = this.state;
48 |
49 | return (
50 |
51 |
52 | {`${user.get("firstName")} ${user.get("lastName")}`}
53 |
54 |
55 |
56 |
57 |
58 | {children}
59 |
60 |
61 | );
62 | }
63 | }
64 |
65 | NavProfileMenu.propTypes = {
66 | user: PropTypes.object,
67 | children: PropTypes.array
68 | };
69 |
70 | export default User(NavProfileMenu);
71 |
--------------------------------------------------------------------------------
/distribution/images/logo_dark.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/client/api/settings.js:
--------------------------------------------------------------------------------
1 | import fetch from "common/fetch";
2 |
3 | // Load Client Settings
4 | export function loadClient() {
5 | return fetch.perform("/api/v1.0/settings", {
6 | method: "GET"
7 | });
8 | }
9 |
10 | // Update Client Settings
11 | export function updateClient(body) {
12 | return fetch.perform("/api/v1.0/settings", {
13 | method: "PATCH",
14 | body: JSON.stringify(body)
15 | });
16 | }
17 |
18 | // Load Client Workspace Styling
19 | export function loadClientStyling() {
20 | return fetch.perform("/api/v1.0/settings/workspace-styling", {
21 | method: "GET"
22 | });
23 | }
24 |
25 | // Update Client Workspace Styling
26 | export function updateClientStyling(body) {
27 | return fetch.perform("/api/v1.0/settings/workspace-styling", {
28 | method: "PATCH",
29 | body: JSON.stringify(body)
30 | });
31 | }
32 |
33 | // Reset Client Workspace Styling
34 | export function resetClientStyling() {
35 | return fetch.perform("/api/v1.0/settings/reset-workspace-styling", {
36 | method: "POST"
37 | });
38 | }
39 |
40 | // Generate signed S3 url to client styling photo
41 | export function generateSignedPhotoURL(body) {
42 | return fetch.perform("/api/v1.0/settings/generate-signed-photo-url", {
43 | method: "GET",
44 | headers: {
45 | contentType: body.contentType
46 | }
47 | });
48 | }
49 |
50 | // Send file to signed S3 url
51 | export function uploadPhotoToSignedURL(body) {
52 | return fetch.simple(body.signedURL, {
53 | method: "PUT",
54 | headers: {
55 | "Content-Type": body.contentType
56 | },
57 | body: body.data
58 | });
59 | }
60 |
61 | // Load Localization Settings
62 | export function loadLocalization() {
63 | return fetch.perform("/api/v1.0/settings/localization", {
64 | method: "GET"
65 | });
66 | }
67 |
68 | // Update Localization Settings
69 | export function updateLocalization(body) {
70 | return fetch.perform("/api/v1.0/settings/localization", {
71 | method: "PATCH",
72 | body: JSON.stringify(body)
73 | });
74 | }
75 |
76 | // Delete Workspace
77 | export function deleteWorkspace(body) {
78 | return fetch.perform("/api/v1.0/settings/delete-workspace", {
79 | method: "POST",
80 | body: JSON.stringify({
81 | workspaceURL: body.workspaceURL,
82 | accountPassword: body.accountPassword
83 | })
84 | });
85 | }
86 |
--------------------------------------------------------------------------------
/shared/constants.js:
--------------------------------------------------------------------------------
1 | module.exports = Object.freeze({
2 | CLIENT_INFORMATION: {
3 | TYPE: "Desktop",
4 | VERSION: "1.2.0"
5 | },
6 | MINIMUM_BROWSER_VERSIONS: {
7 | msie: ">11",
8 | msedge: ">16",
9 | firefox: ">45",
10 | chrome: ">59",
11 | safari: ">10",
12 | opera: ">46",
13 | ios: ">11",
14 | android: ">5.0"
15 | },
16 | FEATURES: {
17 | STYLING: 1,
18 | BILLING: 2
19 | },
20 | ROLE_TYPE: {
21 | UNREGISTERED: 0,
22 | OWNER: 1,
23 | ADMINISTRATOR: 2,
24 | FINANCE: 3
25 | },
26 | SUBSCRIPTION_TYPE: {
27 | TRIAL: 1,
28 | BASIC: 2,
29 | STANDARD: 3,
30 | PROFESSIONAL: 4
31 | },
32 | BILLING_CYCLE: {
33 | TRIAL: 14,
34 | BASIC: 28
35 | },
36 | EMAIL_TYPE: {
37 | CLIENT_WELCOME: 1,
38 | RESEND_VERIFY_EMAIL: 2,
39 | FORGOT_ACCOUNT_DETAILS: 3,
40 | FORGOT_PASSWORD: 4,
41 | RESET_PASSWORD_SUCCESS: 5,
42 | CHANGE_PASSWORD_SUCCESS: 6,
43 | CHANGE_EMAIL_ADDRESS: 7,
44 | CHANGE_EMAIL_ADDRESS_SUCCESS: 8
45 | },
46 | LANGUAGE: {
47 | ENGLISH: 1,
48 | ITALIAN: 2
49 | },
50 | LANGUAGE_CODES: {
51 | 1: "en",
52 | 2: "it"
53 | },
54 | REDUX_STATE: {
55 | PENDING: "PENDING",
56 | FULFILLED: "FULFILLED",
57 | REJECTED: "REJECTED"
58 | },
59 | PAYMENT_CURRENCY: {
60 | AUD: 1
61 | },
62 | PAYMENT_CURRENCY_CODES: {
63 | 1: "aud"
64 | },
65 | PAYMENT_INTERVALS: {
66 | MONTH: 1,
67 | YEAR: 2
68 | },
69 | PAYMENT_INTERVALS_CODES: {
70 | 1: "month",
71 | 2: "year"
72 | },
73 | MAX_FILE_UPLOAD_SIZE: {
74 | CHANGE_AVATAR: "256kb",
75 | LOGO_IMAGE: "256kb",
76 | BACKGROUND_IMAGE: "512kb"
77 | },
78 | SIGNED_URL_EXPIRY_TIME: {
79 | CHANGE_AVATAR: 120,
80 | DISPLAY_AVATAR: 240,
81 | DISPLAY_CLIENT_STYLING_IMAGES: 60,
82 | CHANGE_CLIENT_STYLING_IMAGES: 240
83 | },
84 | ACL_POLICIES: {
85 | PUBLIC_READ: "public-read"
86 | },
87 | RESTRICTED_LANGUAGES: ["en", "it"],
88 | RESTRICTED_DOMAINS: [
89 | "domain",
90 | "account",
91 | "accounts",
92 | "admin",
93 | "registration",
94 | "signup",
95 | "configuration",
96 | "web",
97 | "mobile",
98 | "app",
99 | "software",
100 | "com",
101 | "net",
102 | "org",
103 | "login",
104 | "register",
105 | "account",
106 | "app",
107 | "reeve",
108 | "member",
109 | "membership"
110 | ]
111 | });
112 |
--------------------------------------------------------------------------------
/client/common/store/reducers/language.js:
--------------------------------------------------------------------------------
1 | import { fromJS } from "immutable";
2 | import { REDUX_STATE } from "shared/constants";
3 | import i18next, { saveLNGToken } from "shared/translations/i18n";
4 | import { changeUserLanguage } from "client/api/language.js";
5 |
6 | export const LANGUAGE = "language";
7 |
8 | export const CHANGE_LANGUAGE_PENDING = LANGUAGE + "/CHANGE_LANGUAGE_PENDING";
9 | export const CHANGE_LANGUAGE_FULFILLED = LANGUAGE + "/CHANGE_LANGUAGE_FULFILLED";
10 | export const CHANGE_LANGUAGE_REJECTED = LANGUAGE + "/CHANGE_LANGUAGE_REJECTED";
11 |
12 | const DEFAULT_STATE = fromJS({});
13 |
14 | export default function language(state = DEFAULT_STATE, action) {
15 | switch (action.type) {
16 | case CHANGE_LANGUAGE_PENDING:
17 | return state.setIn(["changeLanguage", "status"], REDUX_STATE.PENDING);
18 | case CHANGE_LANGUAGE_FULFILLED:
19 | return state.set(
20 | "changeLanguage",
21 | fromJS({
22 | status: REDUX_STATE.FULFILLED,
23 | payload: action.payload
24 | })
25 | );
26 | case CHANGE_LANGUAGE_REJECTED:
27 | return state.set(
28 | "changeLanguage",
29 | fromJS({
30 | status: REDUX_STATE.REJECTED,
31 | payload: {},
32 | error: action.payload
33 | })
34 | );
35 | default:
36 | return state;
37 | }
38 | }
39 |
40 | export function changeLanguage(language) {
41 | return dispatch => {
42 | dispatch({
43 | type: CHANGE_LANGUAGE_PENDING
44 | });
45 |
46 | return i18next.changeLanguage(language, (err, t) => {
47 | if (err) {
48 | return dispatch({
49 | type: CHANGE_LANGUAGE_REJECTED
50 | });
51 | }
52 |
53 | // Reload i18n resources
54 | i18next.reloadResources();
55 |
56 | // Store active language in browser
57 | saveLNGToken(i18next.language);
58 |
59 | return dispatch({
60 | type: CHANGE_LANGUAGE_FULFILLED,
61 | payload: { language: i18next.language }
62 | });
63 | });
64 | };
65 | }
66 |
67 | export function saveUserLanguage(body) {
68 | return dispatch => {
69 | dispatch({
70 | type: CHANGE_LANGUAGE_PENDING
71 | });
72 |
73 | return changeUserLanguage(body).then(
74 | result => {
75 | return dispatch({
76 | type: CHANGE_LANGUAGE_FULFILLED,
77 | payload: result
78 | });
79 | },
80 | error =>
81 | dispatch({
82 | type: CHANGE_LANGUAGE_REJECTED,
83 | payload: error
84 | })
85 | );
86 | };
87 | }
88 |
--------------------------------------------------------------------------------
/client/common/media/icons/Logo.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const LogoIcon = props => {
4 | return (
5 |
6 |
7 |
8 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | );
22 | };
23 |
24 | export default LogoIcon;
25 |
--------------------------------------------------------------------------------
/server/models/user.js:
--------------------------------------------------------------------------------
1 | module.exports = function(sequelize, DataTypes) {
2 | return sequelize.define(
3 | "user",
4 | {
5 | id: {
6 | type: DataTypes.INTEGER(11).UNSIGNED,
7 | allowNull: false,
8 | primaryKey: true,
9 | autoIncrement: true,
10 | field: "id"
11 | },
12 | firstName: {
13 | type: DataTypes.STRING(255),
14 | allowNull: false,
15 | defaultValue: "",
16 | field: "firstName"
17 | },
18 | lastName: {
19 | type: DataTypes.STRING(255),
20 | allowNull: false,
21 | defaultValue: "",
22 | field: "lastName"
23 | },
24 | profilePhoto: {
25 | type: DataTypes.STRING(255),
26 | allowNull: true,
27 | field: "profilePhoto"
28 | },
29 | clientId: {
30 | type: DataTypes.INTEGER(11).UNSIGNED,
31 | allowNull: false,
32 | field: "clientId"
33 | },
34 | emailAddress: {
35 | type: DataTypes.STRING(255),
36 | allowNull: false,
37 | defaultValue: "",
38 | field: "emailAddress"
39 | },
40 | emailVerified: {
41 | type: DataTypes.INTEGER(1),
42 | allowNull: false,
43 | defaultValue: "0",
44 | field: "emailVerified"
45 | },
46 | password: {
47 | type: DataTypes.STRING(255),
48 | allowNull: false,
49 | defaultValue: "",
50 | field: "password"
51 | },
52 | lastLoginDate: {
53 | type: DataTypes.DATE,
54 | allowNull: true,
55 | field: "lastLoginDate"
56 | },
57 | language: {
58 | type: DataTypes.INTEGER(11).UNSIGNED,
59 | allowNull: false,
60 | defaultValue: "1",
61 | field: "language"
62 | },
63 | bio: {
64 | type: DataTypes.STRING(255),
65 | allowNull: true,
66 | field: "bio"
67 | },
68 | location: {
69 | type: DataTypes.STRING(255),
70 | allowNull: true,
71 | field: "location"
72 | },
73 | website: {
74 | type: DataTypes.STRING(255),
75 | allowNull: true,
76 | field: "website"
77 | },
78 | active: {
79 | type: DataTypes.INTEGER(1),
80 | allowNull: false,
81 | defaultValue: "1",
82 | field: "active"
83 | },
84 | createdAt: {
85 | type: DataTypes.DATE,
86 | allowNull: true,
87 | field: "createdAt"
88 | },
89 | updatedAt: {
90 | type: DataTypes.DATE,
91 | allowNull: true,
92 | field: "updatedAt"
93 | }
94 | },
95 | {
96 | tableName: "user"
97 | }
98 | );
99 | };
100 |
--------------------------------------------------------------------------------
/server/orchestrator/billing.js:
--------------------------------------------------------------------------------
1 | import { database, models } from "services/sequelize";
2 | import { t } from "shared/translations/i18n";
3 | import { ServerResponseError } from "utilities/errors/serverResponseError";
4 | import { PAYMENT_CURRENCY, PAYMENT_INTERVALS } from "shared/constants";
5 | import { variableExists } from "shared/utilities/filters";
6 |
7 | // Load client subscription details
8 | export function loadClientSubscriptionDetails(requestProperties, authenticatedUser, browserLng) {
9 | return database().transaction(async function(transaction) {
10 | try {
11 | // Create a response object
12 | const response = { status: 200, message: t("label.success", { lng: browserLng }) };
13 |
14 | // Return the response object
15 | return response;
16 | } catch (error) {
17 | throw error;
18 | }
19 | });
20 | }
21 |
22 | // Load list of available subscriptions
23 | export function loadAvailableSubscriptions(requestProperties, authenticatedUser, browserLng) {
24 | return database().transaction(async function(transaction) {
25 | try {
26 | // Filter subscriptions based on parameters
27 | const filterAttributes = { newSubscriptionsAllowed: true, active: true };
28 |
29 | // Filter by currency
30 | if (variableExists(requestProperties.currency)) {
31 | filterAttributes.currency = PAYMENT_CURRENCY[requestProperties.currency.toUpperCase()] || null;
32 | }
33 |
34 | // Filter by payment interval
35 | if (variableExists(requestProperties.interval)) {
36 | filterAttributes.billingInterval = PAYMENT_INTERVALS[requestProperties.interval.toUpperCase()] || null;
37 | }
38 |
39 | // Load a list of available stripe plans
40 | const loadPlans = await models().plans.findAll(
41 | {
42 | where: filterAttributes,
43 | attributes: { exclude: ["name", "description", "stripeProductId", "createdAt", "updatedAt", "newSubscriptionsAllowed", "active"] }
44 | },
45 | { transaction: transaction }
46 | );
47 |
48 | // Throw an error if no stripe plans are available
49 | if (loadPlans === null) {
50 | throw new ServerResponseError(500, t("error.somethingWentWrong", { lng: browserLng }), null);
51 | }
52 |
53 | const plans = JSON.parse(JSON.stringify(loadPlans));
54 |
55 | // Create a response object
56 | const response = { status: 200, message: t("label.success", { lng: browserLng }), subscriptions: plans };
57 |
58 | // Return the response object
59 | return response;
60 | } catch (error) {
61 | throw error;
62 | }
63 | });
64 | }
65 |
--------------------------------------------------------------------------------
/client/common/fetch.js:
--------------------------------------------------------------------------------
1 | import { activeLanguage } from "shared/translations/i18n";
2 | import { CLIENT_INFORMATION } from "shared/constants";
3 |
4 | // Async function for handling front-end get, post and patch calls
5 | export default {
6 | async execute(path, options) {
7 | const route = `${BUILD_PROTOCOL}://${BUILD_DOMAINPATH}${path}`;
8 | // Set default headers
9 | options.headers = Object.assign(
10 | {
11 | Accept: "application/json",
12 | Pragma: "no-cache",
13 | "Content-Type": "application/json",
14 | "Accept-Language": activeLanguage() || "",
15 | "X-Client-Type": CLIENT_INFORMATION.TYPE,
16 | "X-Client-Version": CLIENT_INFORMATION.VERSION
17 | },
18 | options.headers || {}
19 | );
20 |
21 | // Add security token to header
22 | if (this.token != null) {
23 | options.headers = Object.assign(options.headers, {
24 | Authorization: `jwt ${this.token}`
25 | });
26 | }
27 |
28 | // Perform fetch on the endpoint
29 | const response = await fetch(route, options);
30 |
31 | // Handle server side redirects
32 | if (response.redirected) {
33 | return window.location.replace(response.url);
34 | }
35 |
36 | // Valid response if status 200 ~ 299
37 | let json = response.json();
38 | if (response.status >= 200 && response.status < 300) {
39 | return json;
40 | }
41 |
42 | // Throw error if any other response from server
43 | return json.then(error => {
44 | throw error;
45 | });
46 | },
47 |
48 | // Set Security Token Header
49 | setSecurityToken(token) {
50 | this.token = token;
51 | },
52 |
53 | getSecurityToken() {
54 | return this.token;
55 | },
56 |
57 | clearSecurityToken() {
58 | this.token = null;
59 | },
60 |
61 | // Set Accept Language Header
62 | setLanguageToken(language) {
63 | this.language = language;
64 | },
65 |
66 | getLanguageToken() {
67 | return this.language;
68 | },
69 |
70 | clearLanguageToken() {
71 | this.language = null;
72 | },
73 |
74 | // Complex fetch with headers
75 | perform(path, options = {}) {
76 | return this.execute(path, options);
77 | },
78 |
79 | // Simple fetch method without all the headers
80 | async simple(path, options = {}) {
81 | // Perform fetch on the endpoint
82 | const response = await fetch(path, options);
83 |
84 | // Valid response if status 200 ~ 299
85 | if (response.status >= 200 && response.status < 300) {
86 | return response;
87 | }
88 |
89 | // Throw error if any other response from server
90 | return response.then(error => {
91 | throw error;
92 | });
93 | }
94 | };
95 |
--------------------------------------------------------------------------------
/server/services/redis.js:
--------------------------------------------------------------------------------
1 | let util = require("util");
2 | let redisClient = require("redis");
3 | let RateLimit = require("express-rate-limit");
4 | let RateLimitRedisStore = require("rate-limit-redis");
5 | let config = require("../../config");
6 |
7 | // Redis interface variables
8 | let redisInterface = null;
9 | let redisRateLimitStore = null;
10 |
11 | let getAsync = null;
12 | let delAsync = null;
13 |
14 | // Initialize the redis connection
15 | function initialize(app) {
16 | if (!config.redis.enabled) {
17 | return;
18 | }
19 |
20 | // Load redis client
21 | redisInterface = redisClient.createClient({
22 | host: config.redis.host,
23 | port: config.redis.port,
24 | password: config.redis.pass,
25 | retry_strategy: function(options) {
26 | if (options.error && options.error.code === "ECONNREFUSED") {
27 | process.stdout.write("ECONNREFUSED: Unable to connect to Redis server\n");
28 | process.exit(1);
29 | }
30 | if (options.total_retry_time > 1000 * 60 * 60) {
31 | process.stdout.write("Redis: Retry time exhausted\n");
32 | process.exit(1);
33 | }
34 | if (options.attempt > 10) {
35 | process.stdout.write("Redis: Connection attempts exhausted\n");
36 | process.exit(1);
37 | }
38 | return Math.min(options.attempt * 100, 3000);
39 | }
40 | });
41 |
42 | // Add promises functionality to specific redis client functions
43 | getAsync = util.promisify(redisInterface.get).bind(redisInterface);
44 | delAsync = util.promisify(redisInterface.del).bind(redisInterface);
45 |
46 | // Create Rate Limit Store
47 | redisRateLimitStore = new RateLimitRedisStore({ client: redisInterface });
48 |
49 | // Add rate limiting to endpoints to prevent excessive use
50 | app.enable("trust proxy");
51 | var apiLimiter = new RateLimit({
52 | store: redisRateLimitStore,
53 | windowMs: 15 * 60 * 1000,
54 | max: 200,
55 | delayMs: 0
56 | });
57 | app.use(apiLimiter);
58 | }
59 |
60 | // Retrieve key and value from Redis store
61 | function getKey(key) {
62 | if (!config.redis.enabled) {
63 | return;
64 | }
65 |
66 | return getAsync(key);
67 | }
68 |
69 | // Add key and value to Redis store
70 | function setKey(key, value, expiry) {
71 | if (!config.redis.enabled) {
72 | return;
73 | }
74 |
75 | return redisInterface.set(key, value, "EX", expiry);
76 | }
77 |
78 | // Delete existing key from Redis store
79 | function deleteKey(key) {
80 | if (!config.redis.enabled) {
81 | return;
82 | }
83 |
84 | return delAsync(key);
85 | }
86 |
87 | module.exports = {
88 | initialize: initialize,
89 | get: getKey,
90 | set: setKey,
91 | delete: deleteKey
92 | };
93 |
--------------------------------------------------------------------------------
/client/common/components/Modal.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 Modal extends Component {
7 | render() {
8 | const {
9 | title,
10 | children,
11 | actionButtonLabel,
12 | actionButtonFunc,
13 | actionDangerous,
14 | actionDisabled,
15 | actionLinkLabel,
16 | actionLinkFunc,
17 | actionLinkHidden,
18 | actionLinkDisabled,
19 | closeModal,
20 | closeModalDisabled
21 | } = this.props;
22 |
23 | return (
24 |
25 |
26 |
27 |
28 |
{title}
29 |
30 | ×
31 |
32 |
33 |
{children}
34 |
35 | {actionLinkLabel && !actionLinkHidden && (
36 |
37 | {actionLinkLabel}
38 |
39 | )}
40 | {actionButtonLabel && (
41 |
42 | {actionButtonLabel}
43 |
44 | )}
45 | {closeModal && (
46 |
47 | {t("action.close")}
48 |
49 | )}
50 |
51 |
52 |
53 |
54 | );
55 | }
56 | }
57 |
58 | Modal.propTypes = {
59 | title: PropTypes.string,
60 | children: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
61 | actionButtonLabel: PropTypes.string,
62 | actionButtonFunc: PropTypes.func,
63 | actionDangerous: PropTypes.bool,
64 | actionDisabled: PropTypes.bool,
65 | actionLinkLabel: PropTypes.string,
66 | actionLinkFunc: PropTypes.func,
67 | actionLinkHidden: PropTypes.bool,
68 | actionLinkDisabled: PropTypes.bool,
69 | closeModal: PropTypes.func,
70 | closeModalDisabled: PropTypes.bool
71 | };
72 |
73 | export default Modal;
74 |
--------------------------------------------------------------------------------
/client/modules/billing/components/NewSubscription/components/InjectedCardForm.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { injectStripe } from "react-stripe-elements";
3 |
4 | import InputField from "common/components/inputs/InputField";
5 | import Checkbox from "common/components/inputs/Checkbox";
6 |
7 | class InjectedCardForm extends Component {
8 | render() {
9 | return (
10 |
11 |
23 |
35 |
36 |
37 |
49 |
50 |
51 |
63 |
64 |
65 |
77 |
78 |
79 |
80 |
Total billed today ------------ $49.95
81 |
82 | Subscribe
83 |
84 |
85 | );
86 | }
87 | }
88 | export default injectStripe(InjectedCardForm);
89 |
--------------------------------------------------------------------------------
/database/scripts/180804_02.sql:
--------------------------------------------------------------------------------
1 | INSERT INTO `executedScripts` (`name`, `description`, `createdDate`, `executedDate`)
2 | VALUES
3 | ('180804_02', 'Create password reset table and add null to clientId, UserId in sentEmail table', NOW(), NOW());
4 |
5 | ALTER TABLE `sentEmails`
6 | MODIFY COLUMN `userId` int(11) UNSIGNED NULL,
7 | MODIFY COLUMN `clientId` int(11) UNSIGNED NULL;
8 |
9 | ALTER TABLE `failedEmails`
10 | MODIFY COLUMN `userId` int(11) UNSIGNED NULL,
11 | MODIFY COLUMN `clientId` int(11) UNSIGNED NULL;
12 |
13 | CREATE TABLE `passwordReset` (
14 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
15 | `resetCode` varchar(255) NOT NULL DEFAULT '',
16 | `activated` tinyint(1) NOT NULL,
17 | `userId` int(11) unsigned NOT NULL,
18 | `clientId` int(11) unsigned NOT NULL,
19 | `gracePeriod` int(2) unsigned NOT NULL,
20 | `createdAt` datetime NOT NULL,
21 | `updatedAt` datetime NOT NULL,
22 | PRIMARY KEY (`id`)
23 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
24 |
25 | INSERT INTO `emailTemplates` (`id`, `type`, `language`, `name`, `description`, `subject`, `html`, `createdAt`, `updatedAt`)
26 | VALUES
27 | (3, 3, 1, 'Forgot Account Details', 'Email containing account details associated with an email address for all clients', 'Forgot Account Details | Reeve', '\n\n \n \n \n \n Forgot Account Details | Reeve \n \n\n \n \n
Hi there!
\n
You recently entered your email address into the forgot account details page on Reeve. We want to help you get back into your account.
\n
The accounts linked to this email address include:
\n
\n
Best Regards, Reeve Customer Support
\n
\n \n \n \n', NOW(), NOW());
28 |
--------------------------------------------------------------------------------
/shared/translations/i18n.js:
--------------------------------------------------------------------------------
1 | import i18next from "i18next";
2 | import languageDetector from "i18next-browser-languagedetector";
3 | import markdownJsx from "i18next-markdown-jsx-plugin";
4 | import moment from "moment";
5 |
6 | const languages = {
7 | en: {
8 | translation: require("./languages/en.json"),
9 | links: require("./links/en.json"),
10 | countries: require("./countries/en.json"),
11 | currencies: require("./currencies/en.json")
12 | },
13 | it: {
14 | translation: require("./languages/it.json"),
15 | links: require("./links/it.json"),
16 | countries: require("./countries/it.json"),
17 | currencies: require("./currencies/it.json")
18 | }
19 | };
20 |
21 | i18next
22 | .use(markdownJsx)
23 | .use(languageDetector)
24 | .init({
25 | resources: languages,
26 | whitelist: ["it", "en"],
27 | fallbackLng: "en",
28 | ns: ["translation", "links", "countries", "currencies"],
29 | interpolation: {
30 | escapeValue: false
31 | },
32 | react: {
33 | wait: true
34 | },
35 | detection: {
36 | lookupLocalStorage: "lng",
37 | order: ["localStorage", "navigator"],
38 | caches: []
39 | }
40 | });
41 |
42 | // Update moment js locale when language is changed
43 | i18next.on("languageChanged", lng => {
44 | moment.locale(lng);
45 | });
46 |
47 | export default i18next;
48 |
49 | // Change translation language
50 | export function changeLanguage(lng) {
51 | return i18next.changeLanguage(lng);
52 | }
53 |
54 | // Reload active language
55 | export function reloadLanguage() {
56 | return i18next.reloadResources();
57 | }
58 |
59 | // Get currently active language string
60 | export function activeLanguage() {
61 | return i18next.language;
62 | }
63 |
64 | // Retrieve browser language string
65 | export function getLNGToken() {
66 | const lng = window.sessionStorage.getItem("lng") || window.localStorage.getItem("lng");
67 | return lng;
68 | }
69 |
70 | // Store browser language string
71 | export function saveLNGToken(lng) {
72 | window.localStorage.setItem("lng", lng);
73 | }
74 |
75 | // Clear browser language string
76 | export function clearLNGToken() {
77 | window.localStorage.removeItem("lng");
78 | window.sessionStorage.removeItem("lng");
79 | }
80 |
81 | // Standard text string translations
82 | export function t(...args) {
83 | return i18next.t(...args);
84 | }
85 |
86 | // Links and URLS
87 | export function l(link) {
88 | return i18next.t(`links:${link}`);
89 | }
90 |
91 | // ISO2 to country name mappings
92 | export function co(ISO2) {
93 | return i18next.t(`countries:${ISO2}`);
94 | }
95 |
96 | // ISO2 to currencies mappings
97 | export function cu(ISO2) {
98 | return i18next.t(`currencies:${ISO2}`);
99 | }
100 |
--------------------------------------------------------------------------------
/client/modules/header/components/VerifyEmail.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import PropTypes from "prop-types";
3 | import { connect } from "react-redux";
4 | import { bindActionCreators } from "redux";
5 | import User from "common/components/User";
6 | import { t } from "shared/translations/i18n";
7 | import { variableExists } from "shared/utilities/filters";
8 | import { REDUX_STATE } from "shared/constants";
9 | import { AUTHENTICATION, resendVerifyEmail } from "common/store/reducers/authentication";
10 |
11 | class VerifyEmail extends Component {
12 | constructor(props) {
13 | super(props);
14 |
15 | this.state = {
16 | language: "en"
17 | };
18 |
19 | this.resendVerifyEmail = this.resendVerifyEmail.bind(this);
20 | }
21 |
22 | static getDerivedStateFromProps(nextProps, prevState) {
23 | if (nextProps.user.get("language") !== prevState.language) {
24 | return {
25 | language: nextProps.user.get("language")
26 | };
27 | }
28 | return null;
29 | }
30 |
31 | resendVerifyEmail(evt) {
32 | evt.preventDefault(); // Prevent page refresh
33 |
34 | // Call API to send verify email
35 | this.props.resendVerifyEmail();
36 | }
37 |
38 | render() {
39 | const { user, resendVerifyEmailStatus } = this.props;
40 |
41 | // Hide header if user is not logged in
42 | if (!this.props.user || !variableExists(user.get("userId"))) {
43 | return null;
44 | }
45 |
46 | // Hide header if email has been verified
47 | if (this.props.user.get("emailVerified")) {
48 | return null;
49 | }
50 |
51 | const emailSent = resendVerifyEmailStatus === REDUX_STATE.FULFILLED;
52 |
53 | return (
54 |
55 | {t("components.authentication.verifyEmailNotification.welcomeEmailSent", { firstName: user.get("firstName"), email: user.get("emailAddress") })}
56 |
57 |
58 | {emailSent ? t("action.emailSent") : t("action.resendEmail")}
59 |
60 |
61 |
62 | );
63 | }
64 | }
65 |
66 | VerifyEmail.propTypes = {
67 | user: PropTypes.object,
68 | resendVerifyEmail: PropTypes.func,
69 | resendVerifyEmailStatus: PropTypes.string
70 | };
71 |
72 | function mapStateToProps(state, props) {
73 | return {
74 | resendVerifyEmailStatus: state.getIn([AUTHENTICATION, "resendVerifyEmail", "status"])
75 | };
76 | }
77 |
78 | function mapDispatchToProps(dispatch) {
79 | return {
80 | resendVerifyEmail: bindActionCreators(resendVerifyEmail, dispatch)
81 | };
82 | }
83 |
84 | export default User(
85 | connect(
86 | mapStateToProps,
87 | mapDispatchToProps
88 | )(VerifyEmail)
89 | );
90 |
--------------------------------------------------------------------------------
/client/common/components/inputs/ColorPicker.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import PropTypes from "prop-types";
3 | import { ChromePicker } from "react-color";
4 | import { variableExists } from "shared/utilities/filters";
5 |
6 | class ColorPicker extends Component {
7 | state = {
8 | displayColorPicker: false
9 | };
10 |
11 | handleClick = () => {
12 | if (this.props.disabled) return;
13 | this.setState({ displayColorPicker: !this.state.displayColorPicker });
14 | };
15 |
16 | handleClose = () => {
17 | this.setState({ displayColorPicker: false });
18 | };
19 |
20 | handleChange = color => {
21 | if (this.props.disabled) return;
22 |
23 | // Separate onChange variables for when input field is used
24 | if (color.target && variableExists(color.target.value)) {
25 | this.props.onChange({ name: this.props.name, color: color.target.value || "" });
26 | return;
27 | }
28 |
29 | this.props.onChange({ name: this.props.name, color: color.hex || "" });
30 | };
31 |
32 | render() {
33 | const { id, name, value, label, required, smallText, disabled, error } = this.props;
34 |
35 | return (
36 |
37 | {label && (
38 |
39 | {label} {required && "*"}
40 |
41 | )}
42 |
43 |
44 |
52 | {this.state.displayColorPicker ? (
53 |
57 | ) : null}
58 |
59 | {error && error[name] &&
{error[name][0]}
}
60 | {smallText && (
61 |
62 | {smallText}
63 |
64 | )}
65 |
66 |
67 | );
68 | }
69 | }
70 |
71 | ColorPicker.propTypes = {
72 | id: PropTypes.string,
73 | name: PropTypes.string,
74 | value: PropTypes.string,
75 | label: PropTypes.string,
76 | onChange: PropTypes.func,
77 | required: PropTypes.bool,
78 | smallText: PropTypes.string,
79 | disabled: PropTypes.bool,
80 | error: PropTypes.object
81 | };
82 |
83 | export default ColorPicker;
84 |
--------------------------------------------------------------------------------
/client/common/styles/modules/_bootstrap_variations.scss:
--------------------------------------------------------------------------------
1 | // Colors
2 | $primary: #3c6fa5;
3 | $secondary: #919aa1;
4 |
5 | $gray-100: #f8f9fa !default;
6 | $gray-200: #f7f7f9 !default;
7 | $gray-300: #eceeef !default;
8 | $gray-400: #ced4da !default;
9 | $gray-500: #adb5bd !default;
10 | $gray-600: #919aa1 !default;
11 | $gray-700: #55595c !default;
12 | $gray-800: #343a40 !default;
13 | $gray-900: #1a1a1a !default;
14 |
15 | $white: #fff !default;
16 | $black: #000 !default;
17 | $blue: #3c6fa5 !default;
18 | $indigo: #6610f2 !default;
19 | $purple: #6f42c1 !default;
20 | $pink: #e83e8c !default;
21 | $red: #d9534f !default;
22 | $orange: #fd7e14 !default;
23 | $yellow: #f0ad4e !default;
24 | $green: #4bbf73 !default;
25 | $teal: #20c997 !default;
26 | $cyan: #1f9bcf !default;
27 |
28 | $yiq-contrasted-threshold: 185 !default;
29 | $body-color: $gray-800 !default;
30 |
31 | // Fonts Sizing
32 | $font-size-base: 0.875rem !default;
33 | $h1-font-size: 2rem !default;
34 | $h2-font-size: 1.75rem !default;
35 | $h3-font-size: 1.5rem !default;
36 | $h4-font-size: 1.25rem !default;
37 | $h5-font-size: 1rem !default;
38 | $h6-font-size: 0.75rem !default;
39 | $headings-font-weight: 600 !default;
40 | $headings-color: $gray-900 !default;
41 |
42 | // Tables
43 | $table-border-color: rgba(0, 0, 0, 0.05) !default;
44 |
45 | // Buttons
46 | $btn-line-height: 1.5rem !default;
47 | $input-btn-padding-y: 0.75rem !default;
48 | $input-btn-padding-x: 2rem !default;
49 | $input-btn-padding-y-sm: 0.5rem !default;
50 | $input-btn-padding-x-sm: 1rem !default;
51 | $input-btn-padding-y-lg: 2rem !default;
52 | $input-btn-padding-x-lg: 3rem !default;
53 | $btn-font-weight: 600 !default;
54 |
55 | // Forms
56 | $input-bg: $white !default;
57 | $input-disabled-bg: $gray-200 !default;
58 | $input-group-addon-bg: $gray-200 !default;
59 | $input-padding-x: 1rem;
60 | $input-padding-y: 0.75rem;
61 |
62 | // Navbar
63 | $navbar-padding-y: 1.5rem !default;
64 | $navbar-dark-hover-color: $white !default;
65 | $navbar-light-color: rgba($black, 0.3) !default;
66 | $navbar-light-hover-color: $gray-900 !default;
67 | $navbar-light-active-color: $gray-900 !default;
68 |
69 | // Pagination
70 | $pagination-border-color: transparent !default;
71 | $pagination-hover-border-color: $pagination-border-color !default;
72 | $pagination-disabled-border-color: $pagination-border-color !default;
73 |
74 | // Breadcrumbs
75 | $breadcrumb-bg: transparent !default;
76 |
77 | /*******************************
78 | CSS Variations
79 | *******************************/
80 |
81 | // Nav-Bar Navigation Links
82 | .navbar-dark {
83 | .navbar-nav {
84 | .nav-link {
85 | color: rgba(255, 255, 255, 0.9);
86 | }
87 |
88 | .active {
89 | box-shadow: inset 0px -0.2rem 0px 0px rgba(255, 255, 255, 0.4);
90 | }
91 | }
92 | }
93 |
94 | // Dropdown Menus
95 | .dropdown-menu {
96 | border-radius: 0px !important;
97 | }
98 |
--------------------------------------------------------------------------------
/database/scripts/180716_01.sql:
--------------------------------------------------------------------------------
1 | INSERT INTO `executedScripts` (`name`, `description`, `createdDate`, `executedDate`)
2 | VALUES
3 | ('180716_01', 'New Email and Languages tables', NOW(), NOW());
4 |
5 | CREATE TABLE `languages` (
6 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
7 | `language` varchar(11) NOT NULL DEFAULT '',
8 | `createdAt` datetime DEFAULT NULL,
9 | `updatedAt` datetime DEFAULT NULL,
10 | PRIMARY KEY (`id`)
11 | ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
12 |
13 | INSERT INTO `languages` (`id`, `language`)
14 | VALUES
15 | (1, 'english');
16 |
17 | CREATE TABLE `emailTypes` (
18 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
19 | `name` varchar(255) NOT NULL DEFAULT '',
20 | `description` varchar(255) NOT NULL DEFAULT '',
21 | `createdAt` datetime DEFAULT NULL,
22 | `updatedAt` datetime DEFAULT NULL,
23 | PRIMARY KEY (`id`)
24 | ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
25 |
26 | INSERT INTO `emailTypes` (`id`, `name`, `description`, `createdAt`, `updatedAt`)
27 | VALUES
28 | (1, 'Client Welcome Email', 'Introductory email when a new client account is created', '2018-07-16 22:05:10', '2018-07-16 22:05:15');
29 |
30 | CREATE TABLE `emailTemplates` (
31 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
32 | `type` int(11) unsigned NOT NULL,
33 | `language` int(11) unsigned NOT NULL,
34 | `name` varchar(255) NOT NULL DEFAULT '',
35 | `description` varchar(255) DEFAULT NULL,
36 | `subject` varchar(255) NOT NULL DEFAULT '',
37 | `html` text NOT NULL,
38 | `createdAt` datetime DEFAULT NULL,
39 | `updatedAt` datetime DEFAULT NULL,
40 | PRIMARY KEY (`id`)
41 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
42 |
43 | CREATE TABLE `sentEmails` (
44 | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
45 | `clientId` int(11) unsigned NOT NULL,
46 | `userId` int(11) unsigned NOT NULL,
47 | `emailType` int(11) unsigned NOT NULL,
48 | `emailLanguage` int(11) unsigned NOT NULL,
49 | `to` varchar(255) NOT NULL DEFAULT '',
50 | `from` varchar(255) NOT NULL DEFAULT '',
51 | `subject` varchar(255) NOT NULL DEFAULT '',
52 | `contents` text NOT NULL,
53 | `createdAt` datetime NOT NULL,
54 | `updatedAt` datetime NOT NULL,
55 | PRIMARY KEY (`id`)
56 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
57 |
58 | CREATE TABLE `failedEmails` (
59 | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
60 | `clientId` int(11) unsigned NOT NULL,
61 | `userId` int(11) unsigned NOT NULL,
62 | `emailType` int(11) unsigned NOT NULL,
63 | `emailLanguage` int(11) unsigned NOT NULL,
64 | `to` varchar(255) NOT NULL DEFAULT '',
65 | `from` varchar(255) NOT NULL DEFAULT '',
66 | `subject` varchar(255) NOT NULL DEFAULT '',
67 | `contents` text NOT NULL,
68 | `reason` text NOT NULL,
69 | `createdAt` datetime NOT NULL,
70 | `updatedAt` datetime NOT NULL,
71 | PRIMARY KEY (`id`)
72 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
--------------------------------------------------------------------------------
/client/common/components/HideComponent.js:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } from "react";
2 | import PropTypes from "prop-types";
3 | import { arrayContains, arrayHasAny, variableExists } from "shared/utilities/filters";
4 |
5 | class HideComponent extends Component {
6 | render() {
7 | const { user, children, hasAnyRole, hasAllRoles, hasAnyFeature, hasAllFeatures, hasAnySubscription, hasVerifiedEmail, disabled } = this.props;
8 |
9 | // Hide component if disabled prop set to true
10 | if (disabled) {
11 | return null;
12 | }
13 |
14 | // Validate if user is logged in
15 | const userLoggedIn = variableExists(user) && user.get("userId") !== null;
16 |
17 | // Hide if user does not have any of the following roles
18 | if (hasAnyRole && ((userLoggedIn && !user.get("userRoles")) || !arrayHasAny(hasAnyRole, (userLoggedIn && user.get("userRoles").toJS()) || []))) {
19 | return null;
20 | }
21 |
22 | // Hide if user does not have all of the following roles
23 | if (hasAllRoles && ((userLoggedIn && !user.get("userRoles")) || !arrayHasAny(hasAllRoles, (userLoggedIn && user.get("userRoles").toJS()) || []))) {
24 | return null;
25 | }
26 |
27 | // Hide if user does not have any of the following features
28 | if (hasAnyFeature && ((userLoggedIn && !user.get("clientFeatures")) || !arrayHasAny(hasAnyFeature, (userLoggedIn && user.get("clientFeatures").toJS()) || []))) {
29 | return null;
30 | }
31 |
32 | // Hide if user does not have all of the following features
33 | if (hasAllFeatures && ((userLoggedIn && !user.get("clientFeatures")) || !arrayContains(hasAllFeatures, (userLoggedIn && user.get("clientFeatures").toJS()) || []))) {
34 | return null;
35 | }
36 |
37 | // Hide if user does not have any of the following subscriptions
38 | if (hasAnySubscription && ((userLoggedIn && !user.get("subscriptionId")) || !arrayHasAny(hasAnySubscription, (userLoggedIn && user.get("subscriptionId")) || []))) {
39 | return null;
40 | }
41 |
42 | // Hide if user does not have a verified email
43 | if (hasVerifiedEmail && ((userLoggedIn && !user.get("emailVerified")) || (userLoggedIn && user.get("emailVerified")) !== true)) {
44 | return null;
45 | }
46 |
47 | return {children} ;
48 | }
49 | }
50 |
51 | HideComponent.propTypes = {
52 | disabled: PropTypes.bool,
53 | user: PropTypes.object,
54 | hasAnyRole: PropTypes.oneOfType([PropTypes.array, PropTypes.number]),
55 | hasAllRoles: PropTypes.oneOfType([PropTypes.array, PropTypes.number]),
56 | hasAnyFeature: PropTypes.oneOfType([PropTypes.array, PropTypes.number]),
57 | hasAllFeatures: PropTypes.oneOfType([PropTypes.array, PropTypes.number]),
58 | hasAnySubscription: PropTypes.oneOfType([PropTypes.array, PropTypes.number]),
59 | hasVerifiedEmail: PropTypes.bool,
60 | children: PropTypes.oneOfType([PropTypes.string, PropTypes.object])
61 | };
62 |
63 | export default HideComponent;
64 |
--------------------------------------------------------------------------------
/client/modules/profile/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } from "react";
2 | import PropTypes from "prop-types";
3 | import { Helmet } from "react-helmet";
4 | import { Route, withRouter } from "react-router-dom";
5 |
6 | import { t } from "shared/translations/i18n";
7 |
8 | // Import Redux Store
9 | import store, { injectReducer } from "common/store/store";
10 | import profile, { PROFILE } from "common/store/reducers/profile.js";
11 |
12 | import User from "common/components/User";
13 |
14 | import Avatar from "./components/Avatar";
15 | import ChangeAvatar from "./components/ChangeAvatar";
16 | import EditProfile from "./components/EditProfile";
17 | import LanguageSettings from "./components/LanguageSettings";
18 | import ChangePassword from "./components/ChangePassword";
19 |
20 | class Profile extends Component {
21 | render() {
22 | const { user, history } = this.props;
23 |
24 | return (
25 |
26 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | {user.get("firstName")} {user.get("lastName")}
43 |
44 |
45 |
46 |
{t("label.profile")}
47 |
52 |
53 |
54 |
{t("label.languageSettings")}
55 |
60 |
61 |
62 |
{t("components.profile.changePassword")}
63 |
68 |
69 |
70 |
71 |
} />
72 |
73 |
74 |
75 | );
76 | }
77 | }
78 |
79 | // Inject Profile Reducer
80 | injectReducer(store, PROFILE, profile);
81 |
82 | Profile.propTypes = {
83 | history: PropTypes.object,
84 | user: PropTypes.object
85 | };
86 |
87 | export default withRouter(User(Profile));
88 |
--------------------------------------------------------------------------------
/database/scripts/190311_01.sql:
--------------------------------------------------------------------------------
1 | INSERT INTO `executedScripts` (`name`, `description`, `createdDate`, `executedDate`)
2 | VALUES
3 | ('190311_01', 'Change password success email', NOW(), NOW());
4 |
5 | INSERT INTO `emailTypes` (`id`, `name`, `description`, `createdAt`, `updatedAt`)
6 | VALUES
7 | (6, 'Change Password Success', 'When a logged in user changes their password, send an email notifying of the change', NOW(), NOW());
8 |
9 | INSERT INTO `emailTemplates` (`id`, `type`, `language`, `name`, `description`, `subject`, `html`, `createdAt`, `updatedAt`)
10 | VALUES
11 | (11, 6, 1, 'Change Password Success', 'Email confirmation on password change', 'Your account password has changed | Reeve', '\n\n \n \n \n \n Change Password | Reeve \n \n\n \n \n
Dear <%= firstName %>,
\n
We are sending you this email to confirm that the password to your account on the workspace <%= workspaceName %> has successfully been changed.
\n
If you did not make this change. Please contact support immediately by replying to this email.
\n
Best Regards, Reeve Customer Support
\n
\n \n \n \n', NOW(), NOW()),
12 | (12, 6, 2, 'Cambia password successo', 'Conferma e-mail al cambio della password', 'La password del tuo account è cambiata | Reeve', '\n\n \n \n \n \n Cambia la password | Reeve \n \n\n \n \n
Caro <%= firstName %>,
\n
Ti stiamo inviando questa email per confermare che la password del tuo account nello spazio di lavoro <%= workspaceName %> è stata modificata correttamente.
\n
Se non hai fatto questo cambiamento. Contatta immediatamente l\'assistenza rispondendo a questa email.
\n
I migliori saluti, Reeve Servizio Clienti
\n
\n \n \n \n', NOW(), NOW());
13 |
--------------------------------------------------------------------------------
/client/modules/billing/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } from "react";
2 | import PropTypes from "prop-types";
3 | import { connect } from "react-redux";
4 | import { bindActionCreators } from "redux";
5 | import { withRouter } from "react-router-dom";
6 | import { Helmet } from "react-helmet";
7 | import { t } from "shared/translations/i18n";
8 | import { REDUX_STATE } from "shared/constants";
9 |
10 | // Import Redux Store
11 | import store, { injectReducer } from "common/store/store";
12 | import billing, { BILLING, LOAD_CLIENT_SUBSCRIPTION_DETAILS_REJECTED, loadSubscriptionDetails } from "common/store/reducers/billing.js";
13 |
14 | import ServerError from "common/components/ServerError";
15 | import User from "common/components/User";
16 |
17 | import NewSubscription from "./components/NewSubscription";
18 |
19 | class Billing extends Component {
20 | constructor(props) {
21 | super(props);
22 |
23 | this.state = {
24 | serverError: null
25 | };
26 | }
27 |
28 | componentDidMount() {
29 | this.props.loadSubscriptionDetails().then(result => {
30 | if (result.type === LOAD_CLIENT_SUBSCRIPTION_DETAILS_REJECTED) {
31 | this.setState({
32 | serverError: result.payload
33 | });
34 | } else {
35 | this.setState({
36 | serverError: null
37 | });
38 | }
39 | });
40 | }
41 |
42 | render() {
43 | const { loadSubscriptionDetailsStatus } = this.props;
44 | const { serverError } = this.state;
45 |
46 | // Display alert and redirect if there is a server error
47 | if (serverError !== null) {
48 | return ;
49 | }
50 |
51 | // Show loading panel when subscription state has not loaded
52 | const loading = loadSubscriptionDetailsStatus !== REDUX_STATE.FULFILLED;
53 |
54 | return (
55 |
56 |
65 | {loading ? null : }
66 |
67 | );
68 | }
69 | }
70 |
71 | Billing.propTypes = {
72 | history: PropTypes.object,
73 | user: PropTypes.object,
74 | loadSubscriptionDetails: PropTypes.func,
75 | loadSubscriptionDetailsStatus: PropTypes.string
76 | };
77 |
78 | function mapStateToProps(state) {
79 | return {
80 | loadSubscriptionDetailsStatus: state.getIn([BILLING, "subscriptionDetails", "status"])
81 | };
82 | }
83 |
84 | function mapDispatchToProps(dispatch) {
85 | return {
86 | loadSubscriptionDetails: bindActionCreators(loadSubscriptionDetails, dispatch)
87 | };
88 | }
89 |
90 | // Inject Billing Reducer
91 | injectReducer(store, BILLING, billing);
92 |
93 | export default withRouter(
94 | User(
95 | connect(
96 | mapStateToProps,
97 | mapDispatchToProps
98 | )(Billing)
99 | )
100 | );
101 |
--------------------------------------------------------------------------------
/server/services/nodemailer.js:
--------------------------------------------------------------------------------
1 | import { database, models } from "services/sequelize";
2 | import { ServerResponseError } from "utilities/errors/serverResponseError";
3 | import { t } from "shared/translations/i18n";
4 |
5 | const nodemailer = require("nodemailer");
6 | const ejs = require("ejs");
7 | const serializeError = require("serialize-error");
8 | const config = require("../../config");
9 |
10 | let transporter = null;
11 |
12 | function initialize(app) {
13 | if (!config.email.enabled) {
14 | return;
15 | }
16 |
17 | transporter = nodemailer.createTransport({
18 | host: config.email.host,
19 | port: config.email.port,
20 | secure: config.email.secure,
21 | auth: {
22 | user: config.email.auth.username,
23 | pass: config.email.auth.password
24 | }
25 | });
26 | }
27 |
28 | function sendEmail(emailType, language, to, messageParams, clientId, userId) {
29 | database().transaction(async function(transaction) {
30 | try {
31 | // Load email template
32 | const emailTemplate = await models().emailTemplates.findOne({ where: { type: emailType, language: language } }, { transaction: transaction });
33 |
34 | // Throw an error if the email template does not exist
35 | if (emailTemplate === null) {
36 | throw new ServerResponseError(500, t("validation.emailTemplateNotFound"), null);
37 | }
38 |
39 | // Render html email parts using ejs
40 | const subject = ejs.render(emailTemplate.get("subject"), messageParams);
41 | const body = ejs.render(emailTemplate.get("html"), messageParams);
42 |
43 | // Store email properties in object for transporter
44 | const mailOptions = {
45 | to,
46 | from: config.email.senderAddress,
47 | subject: subject,
48 | html: body
49 | };
50 |
51 | // Send email through transporter, gracefully store failure errors in db
52 | try {
53 | await transporter.sendMail(mailOptions);
54 | await models().sentEmails.create(
55 | {
56 | clientId: clientId,
57 | userId: userId,
58 | emailType: emailType,
59 | emailLanguage: language,
60 | to: to,
61 | from: config.email.senderAddress,
62 | subject: subject,
63 | contents: body
64 | },
65 | { transaction: transaction }
66 | );
67 | } catch (error) {
68 | // If there is an email send failure, output result to db table
69 | const reason = JSON.stringify(serializeError(error));
70 | await models().failedEmails.create(
71 | {
72 | clientId: clientId,
73 | userId: userId,
74 | emailType: emailType,
75 | emailLanguage: language,
76 | to: to,
77 | from: config.email.senderAddress,
78 | subject: subject,
79 | contents: body,
80 | reason: reason
81 | },
82 | { transaction: transaction }
83 | );
84 | }
85 | return;
86 | } catch (error) {
87 | throw error;
88 | }
89 | });
90 | }
91 |
92 | module.exports = {
93 | initialize: initialize,
94 | sendEmail: sendEmail
95 | };
96 |
--------------------------------------------------------------------------------
/changelog.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | This file is a record of notable changes made to Reeve.
4 |
5 | ## 1.2.0
6 |
7 | ### Added
8 |
9 | - Change Password Functionality
10 | - Set Default Language
11 | - User Profile Page
12 | - S3 Bucket Image Uploads
13 | - Signed Image Uploads
14 | - Cloudfront image loading
15 | - EmailVerified option to endpoint security
16 | - HideComponent component based on user restriction rules
17 | - Offline warning when connection to internet is lost
18 | - Application version to request headers
19 | - GDPR hard delete online account
20 | - Workspace branding settings
21 | - Settings Page
22 | - Async load Redux states
23 | - Updated NPM packages
24 |
25 | ### Fixed
26 |
27 | - Logo href links to homepage on authentication pages
28 | - Reload user on email validation
29 | - Firefox SVG image sizing issues
30 | - Organized scss files
31 | - Loading bar at top of screen
32 | - Loading bar not visible during async page loading
33 | - Improved react router implementation
34 | - Improved "Page could not be found" component
35 | - Select border radius
36 | - Notification text translates on language change
37 | - Hover link colors during client color change
38 | - Duplicate sessions on page refresh
39 |
40 | ## 1.1.0
41 |
42 | ### Added
43 |
44 | - Google Analytics Support
45 | - Gzip compression to bundle.js files
46 | - EMAIL_ENABLED environmental variable to enable/disable email sending
47 | - Reeve project icon returned to navigation header
48 | - Client logo in profile dropdown menu
49 | - Help bubble on navigation header
50 | - Gif image on bundle.js loading
51 | - Safe regex confirmation when validating subdomains
52 |
53 | ### Fixed
54 |
55 | - Replaced UglifyJS with Terser to provide ES6 support
56 | - Missing Bootstrap "Burger Menu Icon" replaced with SVG
57 | - Client styling not visible on user overview page
58 | - Deprecated updateAttributes sequelize function updated
59 |
60 | ## 1.0.1
61 |
62 | ### Fixed
63 |
64 | - Event-stream security vulnerability
65 | - Incorrect integration url pathways
66 | - Updated key packages to resolve event-stream issue
67 |
68 | ## 1.0.0
69 |
70 | ### Added
71 |
72 | - Server configuration & environment store
73 | - Express Server
74 | - Client interface using React front-end framework
75 | - SASS stylesheet handling
76 | - Bootstrap Framework
77 | - Webpack module bundler
78 | - i18n translations on front-end and back-end
79 | - Redux
80 | - React-Tooltips
81 | - Dropdown Alerts
82 | - AVA Test System
83 | - JS Validation
84 | - Redis
85 | - MySQL Server
86 | - Subdomains
87 | - Session Storage
88 | - React Router
89 | - User Authentication
90 | - Error Reporting
91 | - JSON Web Tokens
92 | - Polyfills & IE Support
93 | - Integrated NodeMailer for email handling
94 | - Sentry error reporting on front-end
95 | - Papertrail logging
96 | - API Rate Limiting
97 | - Developer Documentation
98 |
--------------------------------------------------------------------------------
/client/modules/authentication/components/LanguageSwitcher.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import PropTypes from "prop-types";
3 | import { activeLanguage, t } from "shared/translations/i18n";
4 | import { bindActionCreators } from "redux";
5 | import { withRouter } from "react-router-dom";
6 | import { connect } from "react-redux";
7 |
8 | import { changeLanguage } from "common/store/reducers/language.js";
9 |
10 | import EnglandFlagIcon from "common/media/icons/flags/England";
11 | import ItalyFlagIcon from "common/media/icons/flags/Italy";
12 |
13 | class LanguageSwitcher extends Component {
14 | constructor() {
15 | super();
16 |
17 | this.state = {
18 | menuVisible: false
19 | };
20 |
21 | this.languageChange = this.languageChange.bind(this);
22 | this.showMenu = this.showMenu.bind(this);
23 | this.closeMenu = this.closeMenu.bind(this);
24 | }
25 |
26 | showMenu(evt) {
27 | evt.preventDefault();
28 |
29 | this.setState({ menuVisible: true }, () => {
30 | document.addEventListener("click", this.closeMenu);
31 | });
32 | }
33 |
34 | closeMenu() {
35 | this.setState({ menuVisible: false }, () => {
36 | document.removeEventListener("click", this.closeMenu);
37 | });
38 | }
39 |
40 | languageChange(evt) {
41 | evt.preventDefault();
42 | this.props.changeLanguage(evt.target.name);
43 | }
44 |
45 | flagIcon(lng) {
46 | switch (lng) {
47 | case "en":
48 | return ;
49 | case "it":
50 | return ;
51 | default:
52 | return null;
53 | }
54 | }
55 |
56 | render() {
57 | const { menuVisible } = this.state;
58 |
59 | const language = activeLanguage();
60 |
61 | return (
62 |
63 |
64 |
65 |
66 | {this.flagIcon(language)} {language.toUpperCase()}
67 |
68 |
69 |
70 | {this.flagIcon("en")} {t("languages.en")}
71 |
72 |
73 | {this.flagIcon("it")} {t("languages.it")}
74 |
75 |
76 |
77 |
78 |
79 | );
80 | }
81 | }
82 |
83 | LanguageSwitcher.propTypes = {
84 | changeLanguage: PropTypes.func
85 | };
86 |
87 | function mapDispatchToProps(dispatch) {
88 | return {
89 | changeLanguage: bindActionCreators(changeLanguage, dispatch)
90 | };
91 | }
92 |
93 | export default withRouter(
94 | connect(
95 | null,
96 | mapDispatchToProps
97 | )(LanguageSwitcher)
98 | );
99 |
--------------------------------------------------------------------------------
/shared/utilities/filters.js:
--------------------------------------------------------------------------------
1 | const safe = require("safe-regex");
2 |
3 | function array(element) {
4 | if (element && !Array.isArray(element)) {
5 | element = [element];
6 | }
7 | return element;
8 | }
9 |
10 | // Check if arr1 contains all elements in arr2
11 | export function arrayContains(arr1, arr2) {
12 | arr2 = array(arr2);
13 | return array(arr1).every(arr1Element => {
14 | return arr2.indexOf(arr1Element) !== -1;
15 | });
16 | }
17 |
18 | // Check if any properties in arr1 exists in arr2
19 | export function arrayHasAny(arr1, arr2) {
20 | arr2 = array(arr2);
21 | return array(arr1).some(arr1Element => {
22 | return arr2.indexOf(arr1Element) !== -1;
23 | });
24 | }
25 |
26 | // Check if variable exists and is not null or undefined
27 | export function variableExists(value) {
28 | if (value === null || value === "" || value === undefined || typeof value === "undefined" || value === "undefined") {
29 | return false;
30 | }
31 | return true;
32 | }
33 |
34 | // Check if an input object exists, returns undefined if the object cannot be found
35 | export function parameterIsSafe(fn) {
36 | try {
37 | fn();
38 | return true;
39 | } catch (e) {
40 | return false;
41 | }
42 | }
43 |
44 | // Check if an object does not contain any properties
45 | export function isObjectEmpty(object) {
46 | if (Object.keys(object).length === 0) {
47 | return true;
48 | }
49 | return false;
50 | }
51 |
52 | // Check if an S3 bucket object is of the correct naming convention for saving
53 | export function keyNameCorrect(key) {
54 | const regex = /[0-9]*_[0-9]*_[0-9]*.[a-z]+$/;
55 | if (safe(key.match(regex))) {
56 | return true;
57 | } else {
58 | return false;
59 | }
60 | }
61 |
62 | // Remove any properties from object 1 (genericObject) that are the same in object 2 (immutableObject)
63 | export function removeSimilarProperties(genericObject = {}, immutableObject = {}) {
64 | const newObject = { ...genericObject };
65 |
66 | // Iterate over all properties in genericObject
67 | for (var key in genericObject) {
68 | if (genericObject.hasOwnProperty(key)) {
69 | // Delete any properties that are the same in object2
70 | if ((!variableExists(genericObject[key]) && !variableExists(immutableObject.get(key))) || genericObject[key] == immutableObject.get(key)) {
71 | // null and "" and undefined should all be considered the same
72 | delete newObject[key];
73 | }
74 | }
75 | }
76 | return newObject;
77 | }
78 |
79 | // Remove any properties in an object where the key does not exist in an array
80 | export function removeUniqueProperties(genericObject = {}, propertiesArray = []) {
81 | const newObject = { ...genericObject };
82 |
83 | // Iterate over all properties in genericObject
84 | for (var key in genericObject) {
85 | if (genericObject.hasOwnProperty(key)) {
86 | // Delete any properties in the object that do not exist in the propertiesArray
87 | if (!propertiesArray.includes(key)) {
88 | delete newObject[key];
89 | }
90 | }
91 | }
92 | return newObject;
93 | }
94 |
--------------------------------------------------------------------------------
/shared/validation/settings.js:
--------------------------------------------------------------------------------
1 | import { t } from "shared/translations/i18n";
2 | import { RESTRICTED_LANGUAGES, RESTRICTED_DOMAINS } from "shared/constants";
3 |
4 | export function updateClient(updateType = "post") {
5 | // Check if patching is enabled
6 | const required = updateType == "patch" ? false : true;
7 |
8 | return {
9 | name: {
10 | presence: required,
11 | noEmptyValue: true,
12 | length: {
13 | maximum: 255
14 | }
15 | },
16 | description: {
17 | presence: required,
18 | noEmptyValue: false,
19 | length: {
20 | maximum: 255
21 | }
22 | }
23 | };
24 | }
25 |
26 | export function updateLocalization(updateType = "post") {
27 | // Check if patching is enabled
28 | const required = updateType == "patch" ? false : true;
29 |
30 | return {
31 | defaultLanguage: {
32 | presence: required,
33 | noEmptyValue: true,
34 | inclusion: {
35 | within: RESTRICTED_LANGUAGES
36 | }
37 | }
38 | };
39 | }
40 |
41 | export function updateClientStyling(updateType = "post") {
42 | // Check if patching is enabled
43 | const required = updateType == "patch" ? false : true;
44 |
45 | return {
46 | logoImage: {
47 | presence: required,
48 | format: {
49 | pattern: "^$|[0-9]*_[0-9]*_[0-9]*.[a-z]+$",
50 | flags: "i",
51 | message: t("validation.validators.validKey")
52 | }
53 | },
54 | backgroundImage: {
55 | presence: required,
56 | format: {
57 | pattern: "^$|[0-9]*_[0-9]*_[0-9]*.[a-z]+$",
58 | flags: "i",
59 | message: t("validation.validators.validKey")
60 | }
61 | },
62 | backgroundColor: {
63 | presence: required,
64 | noEmptyValue: true,
65 | format: {
66 | pattern: "^#[0-9A-F]{6}$",
67 | flags: "i",
68 | message: t("validation.validators.validHex")
69 | }
70 | },
71 | primaryColor: {
72 | presence: required,
73 | noEmptyValue: true,
74 | format: {
75 | pattern: "^#[0-9A-F]{6}$",
76 | flags: "i",
77 | message: t("validation.validators.validHex")
78 | }
79 | },
80 | secondaryColor: {
81 | presence: required,
82 | noEmptyValue: true,
83 | format: {
84 | pattern: "^#[0-9A-F]{6}$",
85 | flags: "i",
86 | message: t("validation.validators.validHex")
87 | }
88 | }
89 | };
90 | }
91 |
92 | export function deleteWorkspace() {
93 | return {
94 | workspaceURL: {
95 | presence: {
96 | allowEmpty: false
97 | },
98 | format: {
99 | pattern: "[a-z0-9]+",
100 | flags: "i",
101 | message: t("validation.validators.validCharactersAZ09")
102 | },
103 | exclusion: {
104 | within: RESTRICTED_DOMAINS,
105 | message: t("validation.validators.validWorkspaceURL")
106 | },
107 | length: {
108 | maximum: 255
109 | }
110 | },
111 | accountPassword: {
112 | presence: {
113 | allowEmpty: false
114 | },
115 | length: {
116 | minimum: 6,
117 | maximum: 66,
118 | message: t("validation.validators.invalidPasswordLength")
119 | }
120 | }
121 | };
122 | }
123 |
--------------------------------------------------------------------------------
/client/api/authentication.js:
--------------------------------------------------------------------------------
1 | import fetch from "common/fetch";
2 |
3 | // Workspace URL Validation
4 | export function validateWorkspaceURL(workspaceURL) {
5 | return fetch.perform("/api/v1.0/authentication/validate-workspace-url", {
6 | method: "GET",
7 | headers: {
8 | workspaceURL: workspaceURL
9 | }
10 | });
11 | }
12 |
13 | // Account Registration
14 | export function clientRegistration(client) {
15 | return fetch.perform("/api/v1.0/authentication/register", {
16 | method: "POST",
17 | body: JSON.stringify({
18 | workspaceURL: client.workspaceURL,
19 | firstName: client.firstName,
20 | lastName: client.lastName,
21 | emailAddress: client.emailAddress,
22 | password: client.password,
23 | privacyConsent: client.privacyConsent,
24 | language: client.language
25 | })
26 | });
27 | }
28 |
29 | // User Login
30 | export function userLogin(user) {
31 | let body = {
32 | workspaceURL: user.workspaceURL,
33 | emailAddress: user.emailAddress,
34 | password: user.password,
35 | keepSignedIn: user.keepSignedIn
36 | };
37 | if (user.authToken) {
38 | body = { authToken: user.authToken };
39 | }
40 | return fetch.perform("/api/v1.0/authentication/login", {
41 | method: "POST",
42 | body: JSON.stringify(body)
43 | });
44 | }
45 |
46 | // User Logout
47 | export function userLogout() {
48 | return fetch.perform("/api/v1.0/authentication/logout", {
49 | method: "POST"
50 | });
51 | }
52 |
53 | // Load user properties
54 | export function userLoad() {
55 | return fetch.perform("/api/v1.0/authentication/load-user", {
56 | method: "GET"
57 | });
58 | }
59 |
60 | // Resend verify email address notification
61 | export function resendVerifyEmail() {
62 | return fetch.perform("/api/v1.0/authentication/resend-verify-email", {
63 | method: "POST"
64 | });
65 | }
66 |
67 | // Forgot Account Details
68 | export function forgotAccountDetails(user) {
69 | return fetch.perform("/api/v1.0/authentication/forgot-account-details", {
70 | method: "POST",
71 | body: JSON.stringify(user)
72 | });
73 | }
74 |
75 | // Reset Password Code Validation
76 | export function resetPasswordCodeValidation(reset) {
77 | return fetch.perform("/api/v1.0/authentication/validate-reset-password-code", {
78 | method: "GET",
79 | headers: {
80 | code: reset.code,
81 | workspaceURL: reset.workspaceURL
82 | }
83 | });
84 | }
85 |
86 | // Reset User Password
87 | export function resetPassword(reset) {
88 | return fetch.perform("/api/v1.0/authentication/reset-password", {
89 | method: "POST",
90 | body: JSON.stringify({
91 | password: reset.password,
92 | verifyPassword: reset.verifyPassword,
93 | code: reset.code,
94 | workspaceURL: reset.workspaceURL
95 | })
96 | });
97 | }
98 |
99 | // Verify User Email
100 | export function verifyEmail(user) {
101 | return fetch.perform("/api/v1.0/authentication/verify-email", {
102 | method: "POST",
103 | body: JSON.stringify({
104 | code: user.code,
105 | userId: user.userId,
106 | workspaceURL: user.workspaceURL
107 | })
108 | });
109 | }
110 |
--------------------------------------------------------------------------------
/shared/validation/profile.js:
--------------------------------------------------------------------------------
1 | import { t } from "shared/translations/i18n";
2 | import { RESTRICTED_LANGUAGES, RESTRICTED_DOMAINS } from "shared/constants";
3 |
4 | export function updateUserProfile(updateType = "post") {
5 | // Check if patching is enabled
6 | const required = updateType == "patch" ? false : true;
7 |
8 | return {
9 | firstName: {
10 | presence: required,
11 | noEmptyValue: true,
12 | length: {
13 | maximum: 255
14 | }
15 | },
16 | lastName: {
17 | presence: required,
18 | noEmptyValue: true,
19 | length: {
20 | maximum: 255
21 | }
22 | },
23 | emailAddress: {
24 | email: true,
25 | presence: required,
26 | noEmptyValue: true,
27 | length: {
28 | maximum: 255
29 | }
30 | },
31 | bio: {
32 | presence: required,
33 | noEmptyValue: false,
34 | length: {
35 | maximum: 255
36 | }
37 | },
38 | location: {
39 | presence: required,
40 | noEmptyValue: false,
41 | length: {
42 | maximum: 255
43 | }
44 | },
45 | website: {
46 | presence: required,
47 | noEmptyValue: false,
48 | length: {
49 | maximum: 255
50 | }
51 | }
52 | };
53 | }
54 |
55 | export function verifyEmail() {
56 | return {
57 | code: {
58 | presence: {
59 | allowEmpty: false
60 | },
61 | length: {
62 | maximum: 255
63 | }
64 | },
65 | workspaceURL: {
66 | presence: {
67 | allowEmpty: false
68 | },
69 | format: {
70 | pattern: "[a-z0-9]+",
71 | flags: "i",
72 | message: t("validation.validators.validCharactersAZ09")
73 | },
74 | exclusion: {
75 | within: RESTRICTED_DOMAINS,
76 | message: t("validation.validators.validWorkspaceURL")
77 | },
78 | length: {
79 | minimum: 4,
80 | maximum: 255
81 | }
82 | }
83 | };
84 | }
85 |
86 | export function changeUserPassword() {
87 | return {
88 | currentPassword: {
89 | presence: {
90 | allowEmpty: false
91 | },
92 | length: {
93 | minimum: 6,
94 | maximum: 66,
95 | message: t("validation.validators.invalidPasswordLength")
96 | }
97 | },
98 | newPassword: {
99 | presence: {
100 | allowEmpty: false
101 | },
102 | length: {
103 | minimum: 6,
104 | maximum: 66,
105 | message: t("validation.validators.invalidPasswordLength")
106 | }
107 | },
108 | confirmPassword: {
109 | presence: {
110 | allowEmpty: false
111 | },
112 | length: {
113 | minimum: 6,
114 | maximum: 66,
115 | message: t("validation.validators.invalidPasswordLength")
116 | },
117 | equality: {
118 | attribute: "newPassword",
119 | comparator: function(v1, v2) {
120 | return JSON.stringify(v1) === JSON.stringify(v2);
121 | },
122 | message: t("validation.validators.unmatchedPasswords")
123 | }
124 | }
125 | };
126 | }
127 |
128 | export function changeSavedLanguage() {
129 | return {
130 | language: {
131 | presence: {
132 | allowEmpty: false
133 | },
134 | inclusion: {
135 | within: RESTRICTED_LANGUAGES
136 | }
137 | }
138 | };
139 | }
140 |
--------------------------------------------------------------------------------