- {logo.url ? (
+ {logo && logo.url ? (
rendering", () => {
},
};
props = createTestProps(links);
- const component = renderer.create(
).toJSON();
+ const component = renderer
+ .create(
+
+
+ ,
+ )
+ .toJSON();
expect(component).toMatchSnapshot();
});
it("should render with links", () => {
- const component = renderer.create(
).toJSON();
+ const component = renderer
+ .create(
+
+
+ ,
+ )
+ .toJSON();
expect(component).toMatchSnapshot();
});
it("should render 2 links", () => {
@@ -53,19 +66,19 @@ describe("
rendering", () => {
0,
);
});
- it("should not render logo", () => {
- expect(wrapper.find(".owisp-header-logo-image")).toHaveLength(0);
- });
it("should render logo", () => {
+ expect(wrapper.find(".owisp-header-logo-image")).toHaveLength(1);
+ });
+ it("should not render logo", () => {
const logo = {
header: {
...props.header,
- logo: {alternate_text: "test_alternate_text", url: "/test-logo.jpg"},
+ logo: null,
},
};
props = createTestProps(logo);
wrapper = shallow(
);
- expect(wrapper.find(".owisp-header-logo-image")).toHaveLength(1);
+ expect(wrapper.find(".owisp-header-logo-image")).toHaveLength(0);
});
});
diff --git a/client/components/login/index.js b/client/components/login/index.js
index 0094357b5..af5768bc6 100644
--- a/client/components/login/index.js
+++ b/client/components/login/index.js
@@ -1,5 +1,6 @@
import {connect} from "react-redux";
+import {SET_AUTHENTICATION_STATUS} from "../../constants/action-types";
import Component from "./login";
const mapStateToProps = state => {
@@ -12,7 +13,14 @@ const mapStateToProps = state => {
};
};
+const mapDispatchToProps = dispatch => {
+ return {
+ authenticate: status => {
+ dispatch({type: SET_AUTHENTICATION_STATUS, payload: status});
+ },
+ };
+};
export default connect(
mapStateToProps,
- null,
+ mapDispatchToProps,
)(Component);
diff --git a/client/components/login/login.js b/client/components/login/login.js
index 1e87f2a25..fdb30c99c 100644
--- a/client/components/login/login.js
+++ b/client/components/login/login.js
@@ -30,7 +30,7 @@ export default class Login extends React.Component {
handleSubmit(event) {
event.preventDefault();
- const {orgSlug} = this.props;
+ const {orgSlug, authenticate} = this.props;
const {username, password, errors} = this.state;
const url = loginApiUrl.replace("{orgSlug}", orgSlug);
this.setState({
@@ -48,11 +48,7 @@ export default class Login extends React.Component {
}),
})
.then(() => {
- this.setState({
- errors: {},
- username: "",
- password: "",
- });
+ authenticate(true);
})
.catch(error => {
const {data} = error.response;
@@ -337,4 +333,5 @@ Login.propTypes = {
title: PropTypes.object,
content: PropTypes.object,
}).isRequired,
+ authenticate: PropTypes.func.isRequired,
};
diff --git a/client/components/login/login.test.js b/client/components/login/login.test.js
index 02f42fd50..26b6d6c00 100644
--- a/client/components/login/login.test.js
+++ b/client/components/login/login.test.js
@@ -18,6 +18,7 @@ const createTestProps = props => {
loginForm: defaultConfig.components.login_form,
privacyPolicy: defaultConfig.privacy_policy,
termsAndConditions: defaultConfig.terms_and_conditions,
+ authenticate: jest.fn(),
...props,
};
};
@@ -120,6 +121,9 @@ describe("
interactions", () => {
.handleSubmit(event)
.then(() => {
expect(wrapper.instance().state.errors).toEqual({});
+ expect(
+ wrapper.instance().props.authenticate.mock.calls.length,
+ ).toBe(1);
});
});
});
diff --git a/client/components/organization-wrapper/index.js b/client/components/organization-wrapper/index.js
index 93f78e3fc..7eb1c5bff 100644
--- a/client/components/organization-wrapper/index.js
+++ b/client/components/organization-wrapper/index.js
@@ -13,8 +13,8 @@ const mapStateToProps = (state, ownProps) => {
const mapDispatchToProps = dispatch => {
return {
- setOrganization: slug => {
- dispatch(setOrganization(slug));
+ setOrganization: (slug, cookies) => {
+ dispatch(setOrganization(slug, cookies));
},
};
};
diff --git a/client/components/organization-wrapper/organization-wrapper.js b/client/components/organization-wrapper/organization-wrapper.js
index ed87ad2c5..ef34824ab 100644
--- a/client/components/organization-wrapper/organization-wrapper.js
+++ b/client/components/organization-wrapper/organization-wrapper.js
@@ -2,8 +2,9 @@ import "./index.css";
import PropTypes from "prop-types";
import React from "react";
+import {Cookies} from "react-cookie";
import {Helmet} from "react-helmet";
-import {Route, Switch} from "react-router-dom";
+import {Redirect, Route, Switch} from "react-router-dom";
import getAssetPath from "../../utils/get-asset-path";
import DoesNotExist from "../404";
@@ -13,25 +14,27 @@ import Login from "../login";
import PasswordConfirm from "../password-confirm";
import PasswordReset from "../password-reset";
import Registration from "../registration";
+import Status from "../status";
export default class OrganizationWrapper extends React.Component {
constructor(props) {
super(props);
- const {match, setOrganization} = props;
+ const {match, setOrganization, cookies} = props;
const organizationSlug = match.params.organization;
- if (organizationSlug) setOrganization(organizationSlug);
+ if (organizationSlug) setOrganization(organizationSlug, cookies);
}
componentDidUpdate(prevProps) {
- const {setOrganization, match} = this.props;
+ const {setOrganization, match, cookies} = this.props;
if (prevProps.match.params.organization !== match.params.organization) {
- if (match.params.organization) setOrganization(match.params.organization);
+ if (match.params.organization)
+ setOrganization(match.params.organization, cookies);
}
}
render() {
- const {organization, match} = this.props;
- const {title, favicon} = organization.configuration;
+ const {organization, match, cookies} = this.props;
+ const {title, favicon, isAuthenticated} = organization.configuration;
const orgSlug = organization.configuration.slug;
const cssPath = organization.configuration.css_path;
if (organization.exists === true) {
@@ -40,20 +43,53 @@ export default class OrganizationWrapper extends React.Component {
} />
+ {
+ return ;
+ }}
+ />
}
+ render={() => {
+ if (isAuthenticated)
+ return ;
+ return ;
+ }}
/>
}
+ render={props => {
+ if (isAuthenticated)
+ return ;
+ return ;
+ }}
/>
}
+ render={() => {
+ if (isAuthenticated)
+ return ;
+ return ;
+ }}
+ />
+ {
+ if (isAuthenticated)
+ return ;
+ return ;
+ }}
+ />
+ {
+ if (isAuthenticated) return ;
+ return ;
+ }}
/>
- } />
} />
@@ -113,7 +149,9 @@ OrganizationWrapper.propTypes = {
css_path: PropTypes.string,
slug: PropTypes.string,
favicon: PropTypes.string,
+ isAuthenticated: PropTypes.bool,
}),
exists: PropTypes.bool,
}).isRequired,
+ cookies: PropTypes.instanceOf(Cookies).isRequired,
};
diff --git a/client/components/organization-wrapper/organization-wrapper.test.js b/client/components/organization-wrapper/organization-wrapper.test.js
index d7f259962..d63ad7e18 100644
--- a/client/components/organization-wrapper/organization-wrapper.test.js
+++ b/client/components/organization-wrapper/organization-wrapper.test.js
@@ -1,6 +1,7 @@
/* eslint-disable camelcase */
import {shallow} from "enzyme";
import React from "react";
+import {Cookies} from "react-cookie";
import OrganizationWrapper from "./organization-wrapper";
@@ -17,6 +18,7 @@ const createTestProps = props => {
exists: true,
},
setOrganization: jest.fn(),
+ cookies: new Cookies(),
...props,
};
};
diff --git a/client/components/registration/index.js b/client/components/registration/index.js
index 3029cefad..6f1c16e72 100644
--- a/client/components/registration/index.js
+++ b/client/components/registration/index.js
@@ -1,5 +1,6 @@
import {connect} from "react-redux";
+import {SET_AUTHENTICATION_STATUS} from "../../constants/action-types";
import Component from "./registration";
const mapStateToProps = state => {
@@ -11,8 +12,15 @@ const mapStateToProps = state => {
orgSlug: state.organization.configuration.slug,
};
};
+const mapDispatchToProps = dispatch => {
+ return {
+ authenticate: status => {
+ dispatch({type: SET_AUTHENTICATION_STATUS, payload: status});
+ },
+ };
+};
export default connect(
mapStateToProps,
- null,
+ mapDispatchToProps,
)(Component);
diff --git a/client/components/registration/registration.js b/client/components/registration/registration.js
index 877234405..3e4125cfc 100644
--- a/client/components/registration/registration.js
+++ b/client/components/registration/registration.js
@@ -32,7 +32,7 @@ export default class Registration extends React.Component {
handleSubmit(event) {
event.preventDefault();
- const {registration, orgSlug} = this.props;
+ const {registration, orgSlug, authenticate} = this.props;
const {input_fields} = registration;
const {username, email, password1, password2, errors} = this.state;
if (input_fields.password_confirm) {
@@ -70,6 +70,7 @@ export default class Registration extends React.Component {
password2: "",
success: true,
});
+ authenticate(true);
})
.catch(error => {
const {data} = error.response;
@@ -393,4 +394,5 @@ Registration.propTypes = {
title: PropTypes.object,
content: PropTypes.object,
}).isRequired,
+ authenticate: PropTypes.func.isRequired,
};
diff --git a/client/components/registration/registration.test.js b/client/components/registration/registration.test.js
index 9cc00d3aa..a603cd4b6 100644
--- a/client/components/registration/registration.test.js
+++ b/client/components/registration/registration.test.js
@@ -19,6 +19,7 @@ const createTestProps = props => {
registration: defaultConfig.components.registration_form,
privacyPolicy: defaultConfig.privacy_policy,
termsAndConditions: defaultConfig.terms_and_conditions,
+ authenticate: jest.fn(),
...props,
};
};
@@ -119,6 +120,9 @@ describe("
interactions", () => {
expect(
wrapper.find(".owisp-registration-form.success"),
).toHaveLength(1);
+ expect(
+ wrapper.instance().props.authenticate.mock.calls.length,
+ ).toBe(1);
});
});
});
diff --git a/client/components/status/__snapshots__/status.test.js.snap b/client/components/status/__snapshots__/status.test.js.snap
new file mode 100644
index 000000000..6adcfcff4
--- /dev/null
+++ b/client/components/status/__snapshots__/status.test.js.snap
@@ -0,0 +1,47 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`
rendering should render correctly 1`] = `
+
+
+
+
+
+ WiFi Login Successful!
+
+
+ You can now use the internet.
+
+
+ You may leave this page open in case you want to log out.
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/client/components/status/index.css b/client/components/status/index.css
new file mode 100644
index 000000000..f9250a138
--- /dev/null
+++ b/client/components/status/index.css
@@ -0,0 +1,81 @@
+.owisp-status-container {
+ flex-grow: 1;
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: flex-start;
+ width: 100%;
+ box-sizing: border-box;
+ padding-bottom: 27px;
+}
+.owisp-status-inner {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: flex-start;
+ flex-wrap: wrap;
+ width: 80%;
+}
+.owisp-status-content-div {
+ background: rgb(74, 132, 86);
+ padding: 30px;
+ color: #ffffff;
+ border-radius: 2px;
+ margin-top: 27px;
+ box-sizing: border-box;
+ width: 60%;
+}
+.owisp-status-contact-div {
+ margin-top: 27px;
+ width: 37%;
+}
+.owisp-status-content-line {
+ line-height: 1.9;
+}
+.owisp-status-logout-btn {
+ background-color: rgba(0, 0, 0, 0.3);
+ width: 100%;
+ line-height: 1.6;
+ color: #ffffff;
+ font-size: 1.6;
+ font-weight: 700;
+ margin-top: 10px;
+ padding: 8px 15px;
+ border-radius: 5px;
+ text-decoration: none;
+ cursor: pointer;
+ border: none;
+}
+.owisp-status-logout-btn:hover {
+ background-color: rgba(0, 0, 0, 0.4);
+}
+.owisp-status-logout-btn:focus {
+ outline: dotted 1px #ffffff;
+ outline-offset: 0.5px;
+}
+@media screen and (min-width: 767px) and (max-width: 1000px) {
+ .owisp-status-inner {
+ width: 98%;
+ justify-content: space-between;
+ }
+ .owisp-status-content-div {
+ width: 60%;
+ margin-right: 1%;
+ }
+ .owisp-status-contact-div {
+ width: 39%;
+ }
+}
+@media screen and (min-width: 0px) and (max-width: 767px) {
+ .owisp-status-inner {
+ width: 98%;
+ justify-content: space-between;
+ }
+ .owisp-status-content-div {
+ width: 100%;
+ margin-right: 0;
+ }
+ .owisp-status-contact-div {
+ width: 100%;
+ }
+}
diff --git a/client/components/status/index.js b/client/components/status/index.js
new file mode 100644
index 000000000..2e1fb12c8
--- /dev/null
+++ b/client/components/status/index.js
@@ -0,0 +1,25 @@
+import {connect} from "react-redux";
+
+import logout from "../../actions/logout";
+import Component from "./status";
+
+const mapStateToProps = (state, ownProps) => {
+ return {
+ statusPage: state.organization.configuration.components.status_page,
+ language: state.language,
+ orgSlug: state.organization.configuration.slug,
+ cookies: ownProps.cookies,
+ };
+};
+
+const mapDispatchToProps = dispatch => {
+ return {
+ logout: (cookies, slug) => {
+ dispatch(logout(cookies, slug));
+ },
+ };
+};
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps,
+)(Component);
diff --git a/client/components/status/status.js b/client/components/status/status.js
new file mode 100644
index 000000000..67c5b717e
--- /dev/null
+++ b/client/components/status/status.js
@@ -0,0 +1,101 @@
+import "./index.css";
+
+import axios from "axios";
+import PropTypes from "prop-types";
+import qs from "qs";
+import React from "react";
+import {Cookies} from "react-cookie";
+
+import {validateApiUrl} from "../../constants";
+import getText from "../../utils/get-text";
+import Contact from "../contact-box";
+
+export default class Status extends React.Component {
+ componentDidMount() {
+ const {cookies, orgSlug, logout} = this.props;
+ const token = cookies.get(`${orgSlug}_auth_token`);
+ const url = validateApiUrl.replace("{orgSlug}", orgSlug);
+ axios({
+ method: "post",
+ headers: {
+ "content-type": "application/x-www-form-urlencoded",
+ },
+ url,
+ data: qs.stringify({
+ token,
+ }),
+ })
+ .then(response => {
+ if (response.data["control:Auth-Type"] !== "Accept") {
+ logout(cookies, orgSlug);
+ }
+ })
+ .catch(() => {
+ logout(cookies, orgSlug);
+ });
+ }
+
+ render() {
+ const {statusPage, language, orgSlug, logout, cookies} = this.props;
+ const {content, buttons} = statusPage;
+ const contentArr = getText(content, language).split("\n");
+ return (
+
+
+
+
+ {contentArr.map(text => {
+ if (text !== "")
+ return (
+
+ {text}
+
+ );
+ return null;
+ })}
+ {buttons.logout ? (
+ <>
+ {buttons.logout.label ? (
+ <>
+
+ >
+ ) : null}
+
logout(cookies, orgSlug)}
+ />
+ >
+ ) : null}
+
+
+
+
+
+
+
+ );
+ }
+}
+
+Status.propTypes = {
+ statusPage: PropTypes.shape({
+ buttons: PropTypes.shape({
+ logout: PropTypes.object,
+ }),
+ content: PropTypes.object,
+ }).isRequired,
+ language: PropTypes.string.isRequired,
+ orgSlug: PropTypes.string.isRequired,
+ cookies: PropTypes.instanceOf(Cookies).isRequired,
+ logout: PropTypes.func.isRequired,
+};
diff --git a/client/components/status/status.test.js b/client/components/status/status.test.js
new file mode 100644
index 000000000..2cafc1e59
--- /dev/null
+++ b/client/components/status/status.test.js
@@ -0,0 +1,47 @@
+import axios from "axios";
+/* eslint-disable camelcase */
+import {shallow} from "enzyme";
+import React from "react";
+import {Cookies} from "react-cookie";
+import ShallowRenderer from "react-test-renderer/shallow";
+
+import getConfig from "../../utils/get-config";
+import Status from "./status";
+
+jest.mock("axios");
+
+const defaultConfig = getConfig("default");
+const createTestProps = props => {
+ return {
+ language: "en",
+ orgSlug: "default",
+ statusPage: defaultConfig.components.status_page,
+ cookies: new Cookies(),
+ logout: jest.fn(),
+ ...props,
+ };
+};
+
+describe("
rendering", () => {
+ let props;
+ it("should render correctly", () => {
+ props = createTestProps();
+ const renderer = new ShallowRenderer();
+ const component = renderer.render(
);
+ expect(component).toMatchSnapshot();
+ });
+});
+
+describe("
interactions", () => {
+ let props;
+ let wrapper;
+ it("should call logout function when logout button is clicked", () => {
+ axios.mockImplementationOnce(() => {
+ return Promise.resolve({});
+ });
+ props = createTestProps();
+ wrapper = shallow(
);
+ wrapper.find("#owisp-status-logout-btn").simulate("click", {});
+ expect(wrapper.instance().props.logout.mock.calls.length).toBe(1);
+ });
+});
diff --git a/client/constants/action-types.js b/client/constants/action-types.js
index f3b138498..261d0de93 100644
--- a/client/constants/action-types.js
+++ b/client/constants/action-types.js
@@ -1,4 +1,5 @@
export const PARSE_ORGANIZATIONS = "PARSE_ORGANIZATIONS";
+export const SET_AUTHENTICATION_STATUS = "SET_AUTHENTICATION_STATUS";
+export const SET_LANGUAGE = "SET_LANGUAGE";
export const SET_ORGANIZATION_CONFIG = "SET_ORGANIZATION_CONFIG";
export const SET_ORGANIZATION_STATUS = "SET_ORGANIZATION_STATUS";
-export const SET_LANGUAGE = "SET_LANGUAGE";
diff --git a/client/constants/index.js b/client/constants/index.js
index f65bcfa78..373d02e18 100644
--- a/client/constants/index.js
+++ b/client/constants/index.js
@@ -1,5 +1,6 @@
+export const confirmApiUrl = "/api/v1/{orgSlug}/account/password/reset/confirm";
+export const loginApiUrl = "/api/v1/{orgSlug}/account/token";
export const passwordConfirmError = "The two password fields didn't match.";
export const registerApiUrl = "/api/v1/{orgSlug}/account/";
export const resetApiUrl = "/api/v1/{orgSlug}/account/password/reset/";
-export const confirmApiUrl = "/api/v1/{orgSlug}/account/password/reset/confirm";
-export const loginApiUrl = "/api/v1/{orgSlug}/account/token";
+export const validateApiUrl = "/api/v1/{orgSlug}/account/token/validate";
diff --git a/client/index.css b/client/index.css
index e36f67e2c..2a6da5e1c 100644
--- a/client/index.css
+++ b/client/index.css
@@ -1,6 +1,8 @@
body {
margin: 0;
padding: 0;
+ font-family: "Montserrat", sans-serif;
+ font-size: 16px;
}
:focus {
outline: none;
diff --git a/client/reducers/organization.js b/client/reducers/organization.js
index 4bc7abf35..e984f40d5 100644
--- a/client/reducers/organization.js
+++ b/client/reducers/organization.js
@@ -1,5 +1,6 @@
import {
PARSE_ORGANIZATIONS,
+ SET_AUTHENTICATION_STATUS,
SET_ORGANIZATION_CONFIG,
SET_ORGANIZATION_STATUS,
} from "../constants/action-types";
@@ -22,6 +23,14 @@ export const organization = (
return {...state, configuration: action.payload};
case SET_ORGANIZATION_STATUS:
return {...state, exists: action.payload};
+ case SET_AUTHENTICATION_STATUS:
+ return {
+ ...state,
+ configuration: {
+ ...state.configuration,
+ isAuthenticated: action.payload,
+ },
+ };
default:
return state;
}
diff --git a/client/utils/authenticate.js b/client/utils/authenticate.js
new file mode 100644
index 000000000..f8c6ad9d3
--- /dev/null
+++ b/client/utils/authenticate.js
@@ -0,0 +1,6 @@
+const authenticate = (cookies, orgSlug) => {
+ const token = cookies.get(`${orgSlug}_auth_token`);
+ if (token) return true;
+ return false;
+};
+export default authenticate;
diff --git a/client/utils/utils.test.js b/client/utils/utils.test.js
index 2d2f35882..a70ac5d70 100644
--- a/client/utils/utils.test.js
+++ b/client/utils/utils.test.js
@@ -1,3 +1,4 @@
+import authenticate from "./authenticate";
import customMerge from "./custom-merge";
import renderAdditionalInfo from "./render-additional-info";
@@ -70,3 +71,16 @@ describe("customMerge tests", () => {
expect(customMerge(arr1, arr2)).toEqual(arr2);
});
});
+describe("authenticate tests", () => {
+ const cookies = {
+ get: jest
+ .fn()
+ .mockImplementationOnce(() => true)
+ .mockImplementationOnce(() => false),
+ };
+ const orgSlug = "test-org";
+ it("should perform authentication", () => {
+ expect(authenticate(cookies, orgSlug)).toEqual(true);
+ expect(authenticate(cookies, orgSlug)).toEqual(false);
+ });
+});
diff --git a/org-configurations/default-configuration.yml b/org-configurations/default-configuration.yml
index 7bc890f9a..116e0e14b 100644
--- a/org-configurations/default-configuration.yml
+++ b/org-configurations/default-configuration.yml
@@ -11,6 +11,9 @@ server:
password_reset_confirm: "/api/v1/{org_slug}/account/password/reset/confirm"
registration: "/api/v1/{org_slug}/account"
user_auth_token: "/api/v1/{org_slug}/account/token"
+ validate_auth_token: "/api/v1/{org_slug}/account/token/validate"
+ authorize: "/api/v1/authorize"
+ uuid: organization_uuid
secret_key: organization_secret_key
timeout: 2 #request timeout period in seconds
@@ -24,7 +27,7 @@ client:
auto_login: True
# path of favicon
- favicon: null
+ favicon: "favicon.png"
# path of the custom css file relative to organization's folder in
# assets directory.
@@ -36,8 +39,8 @@ client:
components:
header:
logo:
- url: null # logo url
- alternate_text: null
+ url: "openwisp.svg" # logo url
+ alternate_text: "openwisp"
links:
- text:
en: "link-1" #link text in english
@@ -160,6 +163,43 @@ client:
submit:
en: 'change password'
+ contact_page:
+ email:
+ label:
+ en: "E-mail"
+ value:
+ en: "support@openwisp.co"
+ helpdesk:
+ label:
+ en: "Helpdesk"
+ value:
+ en: "+789 948 564"
+ social_links:
+ - alt:
+ en: "twitter"
+ icon: "twitter.svg"
+ url: "https://twitter.com/openwisp"
+ - alt:
+ en: "facebook"
+ icon: "facebook.svg"
+ url: "https://facebook.com/openwisp"
+ - alt:
+ en: "linkedIn"
+ icon: "linkedin.svg"
+ url: "https://www.linkedin.com/groups/4777261"
+
+ status_page:
+ content:
+ en: |
+ WiFi Login Successful!
+ You can now use the internet.
+ You may leave this page open in case you want to log out.
+ buttons:
+ logout:
+ label: null
+ text:
+ en: "Logout"
+
login_form:
header:
en: " sign in"
diff --git a/package-lock.json b/package-lock.json
index f26b16f2e..9d2f83721 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11106,6 +11106,14 @@
"object-assign": "^4.1.1"
}
},
+ "universal-cookie-express": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/universal-cookie-express/-/universal-cookie-express-4.0.1.tgz",
+ "integrity": "sha512-XbyGQiZLU7TcFs5s4yI17wq2g4djnAmcJbrd1FJQjIcwoXPzRRSzRO+mcCccocE9Xl2wyAX393truwFElmk+Qg==",
+ "requires": {
+ "universal-cookie": "^4.0.0"
+ }
+ },
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
diff --git a/package.json b/package.json
index 93d5537ec..629095133 100644
--- a/package.json
+++ b/package.json
@@ -23,7 +23,8 @@
"react-redux": "^7.1.0",
"react-router-dom": "^5.0.1",
"redux": "^4.0.4",
- "redux-thunk": "^2.3.0"
+ "redux-thunk": "^2.3.0",
+ "universal-cookie-express": "^4.0.1"
},
"devDependencies": {
"@babel/cli": "^7.5.5",
diff --git a/readme.md b/readme.md
index ccb4c35d3..6d6bdff2c 100644
--- a/readme.md
+++ b/readme.md
@@ -1,6 +1,7 @@
# openwisp-wifi-login-pages
+
[![Build Status](https://travis-ci.org/openwisp/openwisp-wifi-login-pages.svg?branch=master)](https://travis-ci.org/openwisp/openwisp-wifi-login-pages)
[![Coverage Status](https://coveralls.io/repos/github/openwisp/openwisp-wifi-login-pages/badge.svg)](https://coveralls.io/github/openwisp/openwisp-wifi-login-pages)
@@ -42,6 +43,20 @@ or
yarn
```
+### Setup
+
+Write configuration of the organization in a yml file in `org-configuration` directory.
+List of variables required in organization configuration:
+
+- name
+- slug
+- uuid: uuid of the organization
+- secret_key: token of the organization
+
+Copy all the assets to `client/assets/{slug}` directory
+Run `$ npm run setup`
+Start servers using `$ npm run start`
+
### Usage
List of NPM Commands:
@@ -52,6 +67,7 @@ $ npm run setup # Discover Organization configs and generate config.json and a
$ npm run build # Build the app
$ npm run server # Run server
$ npm run client # Run client
+$ npm run coveralls # Run coveralls
$ npm run lint # Run ESLint
$ npm run lint:fix # Run ESLint with automatically fix problems option
$ npm test # Run tests
diff --git a/server/controllers/obtain-token-controller.js b/server/controllers/obtain-token-controller.js
index 8786de54e..363e8c75a 100644
--- a/server/controllers/obtain-token-controller.js
+++ b/server/controllers/obtain-token-controller.js
@@ -30,25 +30,22 @@ const obtainToken = (req, res) => {
})
.then(response => {
// save token in signed cookie
- const radTokenCookie = cookie.sign(
- response.data.radius_user_token,
- conf.secret_key,
- );
const authTokenCookie = cookie.sign(
response.data.key,
conf.secret_key,
);
+ const usernameCookie = cookie.sign(username, conf.secret_key);
// forward response
res
.status(response.status)
.type("application/json")
- .cookie(`${conf.slug}_radius_user_token`, radTokenCookie, {
+ .cookie(`${conf.slug}_auth_token`, authTokenCookie, {
maxAge: 1000 * 60 * 60 * 24,
})
- .cookie(`${conf.slug}_auth_token`, authTokenCookie, {
+ .cookie(`${conf.slug}_username`, usernameCookie, {
maxAge: 1000 * 60 * 60 * 24,
})
- .send(response.data);
+ .send();
})
.catch(error => {
// forward error
diff --git a/server/controllers/registration-controller.js b/server/controllers/registration-controller.js
index 3115c21a5..6f9a69b2e 100644
--- a/server/controllers/registration-controller.js
+++ b/server/controllers/registration-controller.js
@@ -1,4 +1,5 @@
import axios from "axios";
+import cookie from "cookie-signature";
import merge from "deepmerge";
import qs from "qs";
@@ -34,11 +35,22 @@ const registration = (req, res) => {
}),
})
.then(response => {
+ const authTokenCookie = cookie.sign(
+ response.data.key,
+ conf.secret_key,
+ );
+ const usernameCookie = cookie.sign(username, conf.secret_key);
// forward response
res
.status(response.status)
.type("application/json")
- .send(response.data);
+ .cookie(`${conf.slug}_auth_token`, authTokenCookie, {
+ maxAge: 1000 * 60 * 60 * 24,
+ })
+ .cookie(`${conf.slug}_username`, usernameCookie, {
+ maxAge: 1000 * 60 * 60 * 24,
+ })
+ .send();
})
.catch(error => {
// forward error
diff --git a/server/controllers/validate-token-controller.js b/server/controllers/validate-token-controller.js
new file mode 100644
index 000000000..69a47caba
--- /dev/null
+++ b/server/controllers/validate-token-controller.js
@@ -0,0 +1,97 @@
+import axios from "axios";
+import cookie from "cookie-signature";
+import merge from "deepmerge";
+import qs from "qs";
+
+import config from "../config.json";
+import defaultConfig from "../utils/default-config";
+
+const validateToken = (req, res) => {
+ const reqOrg = req.params.organization;
+ const validSlug = config.some(org => {
+ if (org.slug === reqOrg) {
+ // merge default config and custom config
+ const conf = merge(defaultConfig, org);
+ const {host} = conf;
+ let validateTokenUrl = conf.proxy_urls.validate_auth_token;
+ // replacing org_slug param with the slug
+ validateTokenUrl = validateTokenUrl.replace("{org_slug}", org.slug);
+ const timeout = conf.timeout * 1000;
+ let {token} = req.body;
+ token = cookie.unsign(token, conf.secret_key);
+ // make AJAX request
+ axios({
+ method: "post",
+ headers: {
+ "content-type": "application/x-www-form-urlencoded",
+ },
+ url: `${host}${validateTokenUrl}/`,
+ timeout,
+ data: qs.stringify({token}),
+ })
+ .then(response => {
+ const authorizeUrl = conf.proxy_urls.authorize;
+ const username = cookie.unsign(
+ req.universalCookies.get(`${org.slug}_username`),
+ conf.secret_key,
+ );
+ axios({
+ method: "post",
+ headers: {
+ "content-type": "application/x-www-form-urlencoded",
+ },
+ url: `${host}${authorizeUrl}/`,
+ timeout,
+ params: {
+ uuid: conf.uuid,
+ token: conf.secret_key,
+ },
+ data: qs.stringify({
+ username,
+ password: response.data.radius_user_token,
+ }),
+ })
+ .then(responseAuth => {
+ res
+ .status(responseAuth.status)
+ .type("application/json")
+ .send(responseAuth.data);
+ })
+ .catch(errorAuth => {
+ res
+ .status(errorAuth.response.status)
+ .type("application/json")
+ .send(errorAuth.response.data);
+ });
+ })
+ .catch(error => {
+ // forward error
+ try {
+ res
+ .status(error.response.status)
+ .type("application/json")
+ .send(error.response.data);
+ } catch (err) {
+ res
+ .status(500)
+ .type("application/json")
+ .send({
+ response_code: "INTERNAL_SERVER_ERROR",
+ });
+ }
+ });
+ }
+ return org.slug === reqOrg;
+ });
+ // return 404 for invalid organization slug or org not listed in config
+ if (!validSlug) {
+ res
+ .status(404)
+ .type("application/json")
+ .send({
+ response_code: "INTERNAL_SERVER_ERROR",
+ });
+ }
+};
+
+export default validateToken;
diff --git a/server/index.js b/server/index.js
index 60055ddae..ab076444d 100644
--- a/server/index.js
+++ b/server/index.js
@@ -4,8 +4,10 @@ import express from "express";
import routes from "./routes";
const app = express();
+const cookiesMiddleware = require("universal-cookie-express");
app.use(cookieParser());
+app.use(cookiesMiddleware());
app.use(express.json());
app.use(express.urlencoded({extended: false}));
app.use("/api/v1/:organization/account", routes.account);
diff --git a/server/routes/account.js b/server/routes/account.js
index 0e6aef22d..a94117698 100644
--- a/server/routes/account.js
+++ b/server/routes/account.js
@@ -5,10 +5,12 @@ import passwordChange from "../controllers/password-change-controller";
import passwordResetConfirm from "../controllers/password-reset-confirm-controller";
import passwordReset from "../controllers/password-reset-controller";
import registration from "../controllers/registration-controller";
+import validateToken from "../controllers/validate-token-controller";
const router = Router({mergeParams: true});
router.post("/token", obtainToken);
+router.post("/token/validate", validateToken);
router.post("/password/change", passwordChange);
router.post("/password/reset/confirm/", passwordResetConfirm);
router.post("/password/reset", passwordReset);