From b29cfe19d79387fc815bfe8507195a07c20c9f05 Mon Sep 17 00:00:00 2001 From: Kayne Ruse Date: Fri, 14 Jun 2019 18:03:27 +1000 Subject: [PATCH] Added privacy settings page --- public/content/task_list.md | 4 +- public/news/2019-06-14-01.md | 1 + server/accounts.js | 62 ++++++++++- server/index.js | 2 + src/components/app.jsx | 2 + src/components/pages/privacy_settings.jsx | 85 ++++++++++++++ src/components/panels/common_links.jsx | 4 +- src/components/panels/privacy_settings.jsx | 122 +++++++++++++++++++++ 8 files changed, 277 insertions(+), 5 deletions(-) create mode 100644 src/components/pages/privacy_settings.jsx create mode 100644 src/components/panels/privacy_settings.jsx diff --git a/public/content/task_list.md b/public/content/task_list.md index b8ec8a6..0e76afe 100644 --- a/public/content/task_list.md +++ b/public/content/task_list.md @@ -2,9 +2,9 @@ Priority List --- * ~~Privacy policy / no children under 13.~~ -* Unsubscribe from promotional emails. +* ~~Unsubscribe from promotional emails.~~ * Delete own account (right to be forgotten). -* Write unit tests / refactor for unit tests (ensure that the game doesn't break from an update). +* refactor for unit tests / Write unit tests (ensure that the game doesn't break from an update). * Implement admin panel (write posts without having to commit, interact with the database safely). Major Effort Changes diff --git a/public/news/2019-06-14-01.md b/public/news/2019-06-14-01.md index 6c57f91..d7aedcb 100644 --- a/public/news/2019-06-14-01.md +++ b/public/news/2019-06-14-01.md @@ -5,4 +5,5 @@ _14 June 2019_ Today's updates: * Added a privacy policy applicable under Australian law. [See it here](/privacypolicy), or in the website's footer. +* Added an option in [Privacy Settings](/privacysettings) to enable or disable occasional emails concerning ingame events. Default is off. diff --git a/server/accounts.js b/server/accounts.js index d02149a..b3a9cf7 100644 --- a/server/accounts.js +++ b/server/accounts.js @@ -18,7 +18,7 @@ const signupRequest = (connection) => (req, res) => { //parse form form.parse(req, (err, fields) => { if (err) throw err; -console.log(fields); + //prevent too many clicks if (isThrottled(fields.email)) { res.status(400).write(log('Signup throttled', fields.email)); @@ -452,6 +452,62 @@ const passwordResetRequest = (connection) => (req, res) => { }); }; +const privacySettingsRequest = (connection) => (req, res) => { + //validate token + query = 'SELECT COUNT(*) AS total FROM sessions WHERE sessions.accountId = ? AND sessions.token = ?;'; + connection.query(query, [req.body.id, req.body.token], (err, results) => { + if (err) throw err; + + if (results[0].total !== 1) { + res.status(400).write(log('Invalid privacy settings credentials', req.body.id, req.body.token)); + res.end(); + return; + } + + //fetch each privacy setting + let query = 'SELECT promotions FROM accounts WHERE id = ?;'; + connection.query(query, [req.body.id], (err, results) => { + if (err) throw err; + + res.status(200).json({ + promotions: results[0].promotions + }); + res.end(); + }); + }); +}; + +const privacySettingsUpdateRequest = (connection) => (req, res) => { + //formidable handles forms + let form = formidable.IncomingForm(); //TODO: get rid of formidable + + //parse form + form.parse(req, (err, fields) => { + if (err) throw err; + + //validate token + query = 'SELECT COUNT(*) AS total FROM sessions WHERE sessions.accountId = ? AND sessions.token = ?;'; + connection.query(query, [fields.id, fields.token], (err, results) => { + if (err) throw err; + + if (results[0].total !== 1) { + res.status(400).write(log('Invalid privacy settings update credentials', fields.id, fields.token)); + res.end(); + return; + } + + //update each privacy setting + query = 'UPDATE accounts SET promotions = ? WHERE id = ?;'; + connection.query(query, [fields.promotions ? true : false, fields.id], (err) => { + if (err) throw err; + + res.status(200).json({ msg: log('Privacy settings updated!', fields.id, fields.token) }); + res.end(); + }); + }); + }); +}; + module.exports = { signupRequest: signupRequest, verifyRequest: verifyRequest, @@ -459,5 +515,7 @@ module.exports = { logoutRequest: logoutRequest, passwordChangeRequest: passwordChangeRequest, passwordRecoverRequest: passwordRecoverRequest, - passwordResetRequest: passwordResetRequest + passwordResetRequest: passwordResetRequest, + privacySettingsRequest: privacySettingsRequest, + privacySettingsUpdateRequest: privacySettingsUpdateRequest }; \ No newline at end of file diff --git a/server/index.js b/server/index.js index 620a81c..4db12c5 100644 --- a/server/index.js +++ b/server/index.js @@ -42,6 +42,8 @@ app.post('/logoutrequest', accounts.logoutRequest(connection)); app.post('/passwordchangerequest', accounts.passwordChangeRequest(connection)); app.post('/passwordrecoverrequest', accounts.passwordRecoverRequest(connection)); app.post('/passwordresetrequest', accounts.passwordResetRequest(connection)); +app.post('/privacysettingsrequest', accounts.privacySettingsRequest(connection)); +app.post('/privacysettingsupdaterequest', accounts.privacySettingsUpdateRequest(connection)); //handle profiles let profiles = require('./profiles.js'); diff --git a/src/components/app.jsx b/src/components/app.jsx index 8fe6362..efe3ba6 100644 --- a/src/components/app.jsx +++ b/src/components/app.jsx @@ -80,7 +80,9 @@ export default class App extends React.Component { import('./pages/news_index.jsx')} /> import('./pages/rules.jsx')} /> import('./pages/statistics.jsx')} /> + import('./pages/privacy_policy.jsx')} /> + import('./pages/privacy_settings.jsx')} /> import('./pages/page_not_found.jsx')} /> diff --git a/src/components/pages/privacy_settings.jsx b/src/components/pages/privacy_settings.jsx new file mode 100644 index 0000000..bf76695 --- /dev/null +++ b/src/components/pages/privacy_settings.jsx @@ -0,0 +1,85 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; + +//panels +import CommonLinks from '../panels/common_links.jsx'; +import PrivacySettingsPanel from '../panels/privacy_settings.jsx'; + +class PrivacySettings extends React.Component { + constructor(props) { + super(props); + this.state = { + message: '', + warning: '' //TODO: unified warning? + }; + } + + componentDidMount() { + if (!this.props.loggedIn) { + this.props.history.replace('/login'); + } + } + + render() { + let warningStyle = { + display: this.state.warning.length > 0 ? 'flex' : 'none' + }; + + let Panel; + + if (this.state.message) { + Panel = () =>

{this.state.message}

+ } else { + Panel = () => this.setState({message: msg})} setWarning={this.setWarning.bind(this)} />; + } + + return ( +
+
+
+ +
+ +
+
+

{this.state.warning}

+
+ +

Privacy Settings

+ +
+
+
+ ); + } + + setWarning(s) { + this.setState({ warning: s }); + } +}; + +PrivacySettings.propTypes = { + loggedIn: PropTypes.bool.isRequired, + id: PropTypes.number.isRequired, + token: PropTypes.number.isRequired +}; + +const mapStoreToProps = (store) => { + return { + loggedIn: store.account.id !== 0, + id: store.account.id, + token: store.account.token + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + // + }; +}; + +PrivacySettings = connect(mapStoreToProps, mapDispatchToProps)(PrivacySettings); + + +export default PrivacySettings; \ No newline at end of file diff --git a/src/components/panels/common_links.jsx b/src/components/panels/common_links.jsx index 6ad87bf..34e15cd 100644 --- a/src/components/panels/common_links.jsx +++ b/src/components/panels/common_links.jsx @@ -39,6 +39,7 @@ class CommonLinks extends React.Component {

Patron List

Rules

Game Stats

+

Privacy Settings

@@ -80,7 +81,8 @@ CommonLinks.propTypes = { onClickTaskList: PropTypes.func, onClickPatronList: PropTypes.func, onClickRules: PropTypes.func, - onClickStatistics: PropTypes.func + onClickStatistics: PropTypes.func, + onClickPrivacySettings: PropTypes.func }; function mapStoreToProps(store) { diff --git a/src/components/panels/privacy_settings.jsx b/src/components/panels/privacy_settings.jsx new file mode 100644 index 0000000..f09a5ec --- /dev/null +++ b/src/components/panels/privacy_settings.jsx @@ -0,0 +1,122 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +class Signup extends React.Component { + constructor(props) { + super(props); + + this.state = { + promotions: false + }; + + this.sendRequest('/privacysettingsrequest'); + } + + render() { + return ( +
+
+
+
+ +
+ + +
+
+ +
+ +
+ +
+
+
+ +
+ ); + } + + //TODO: Fix this copy/pasted crap + //gameplay functions + sendRequest(url, args = {}) { //send a unified request, using my credentials + //build the XHR + let xhr = new XMLHttpRequest(); + xhr.open('POST', url, true); + + xhr.onreadystatechange = () => { + if (xhr.readyState === 4) { + if (xhr.status === 200) { + let json = JSON.parse(xhr.responseText); + + this.setState({ + promotions: json.promotions + }); + } + else if (xhr.status === 400) { + this.setWarning(xhr.responseText); + } + } + }; + + xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); + xhr.send(JSON.stringify({ + id: this.props.id, + token: this.props.token, + ...args + })); + } + + submit(e) { + e.preventDefault(); + + //build the XHR + let form = e.target; + let formData = new FormData(form); + + formData.append('id', this.props.id); + formData.append('token', this.props.token); + + let xhr = new XMLHttpRequest(); + + xhr.onreadystatechange = () => { + if (xhr.readyState === 4) { + if (xhr.status === 200) { + let json = JSON.parse(xhr.responseText); + + if (this.props.onSuccess) { + this.props.onSuccess(json.msg); + } + } + + else if (xhr.status === 400 && this.props.setWarning) { + this.props.setWarning(xhr.responseText); + } + } + }; + + //send the XHR + xhr.open('POST', form.action, true); + xhr.send(formData); + + this.clearInput(); + } + + clearInput() { + this.setState({ promotions: false }); + } + + updatePromotions(evt) { + this.setState({ promotions: !this.state.promotions }); + } +}; + +Signup.propTypes = { + id: PropTypes.number.isRequired, + token: PropTypes.number.isRequired, + + setWarning: PropTypes.func, + onSuccess: PropTypes.func +}; + +export default Signup; \ No newline at end of file