diff --git a/api/controllers/AuthController.js b/api/controllers/AuthController.js index 502afca..5460626 100644 --- a/api/controllers/AuthController.js +++ b/api/controllers/AuthController.js @@ -115,6 +115,8 @@ module.exports = { } passportHelper.callback(req, res, function (err, user, info, status) { + // console.log(err) + // console.log(user) if (err || !user) { sails.log.warn(user, err, info, status) if (!err && info) { @@ -126,6 +128,7 @@ module.exports = { req.login(user, function (err) { if (err) { sails.log.warn(err) + // console.log(err) return negotiateError(err) } @@ -135,7 +138,7 @@ module.exports = { if (req.query.next) { res.status(302).set('Location', req.query.next) } else if (req.query.code) { // if came from oauth callback - res.status(302).set('Location', '/app') + res.status(302).set('Location', '/targets') } sails.log.info('user', user, 'authenticated successfully') diff --git a/api/controllers/TargetController.js b/api/controllers/TargetController.js index 057027e..6497265 100644 --- a/api/controllers/TargetController.js +++ b/api/controllers/TargetController.js @@ -3,7 +3,7 @@ const HttpError = require('../errors/HttpError') module.exports = { show: function (req, res) { - res.view('pages/targets', { + res.view('pages/app', { email: req.user.email }) }, diff --git a/api/controllers/UserController.js b/api/controllers/UserController.js index 03f8604..768051f 100644 --- a/api/controllers/UserController.js +++ b/api/controllers/UserController.js @@ -17,12 +17,11 @@ module.exports = { error: err.toString() }) } - res.json(user) }) }, - update: async function (req, res, next) { + edit: async function (req, res, next) { const passportHelper = await sails.helpers.passport() passportHelper.protocols.local.update(req.body, function (err, user) { if (err) { @@ -30,7 +29,6 @@ module.exports = { error: err.toString() }) } - res.json(user) }) }, diff --git a/assets/js/actions/index.js b/assets/js/actions/index.js index ddaffdc..8f1f566 100644 --- a/assets/js/actions/index.js +++ b/assets/js/actions/index.js @@ -126,3 +126,31 @@ export const setUrl = (value) => async (dispatch, getState) => { dispatch(setWorking(false)) } } + +export const editUser = (user) => async (dispatch, getState) => { + dispatch(setWorking(true)) + + try { + if (!user.currentPassword) throw new Error('Please enter your current password.') + await Ajax.patch({ + url: '/api/me', + data: { + id: user.id, + email: user.email, + password: user.password, + currentPassword: user.currentPassword + } + }) + dispatch({ + type: ACTIONS.error, + data: null + }) + } catch (e) { + dispatch({ + type: ACTIONS.error, + data: e + }) + } finally { + dispatch(setWorking(false)) + } +} diff --git a/assets/js/actions/login.js b/assets/js/actions/login.js index 1976253..26347a5 100644 --- a/assets/js/actions/login.js +++ b/assets/js/actions/login.js @@ -47,7 +47,7 @@ export const clearError = () => ({ export const setLoggedIn = (data) => (dispatch, getState) => { window.localStorage.setItem('roe-token', JSON.stringify(data)) - window.location.href = '/app' + window.location.href = '/targets' } export const checkEmail = email => async (dispatch, getState) => { diff --git a/assets/js/index.js b/assets/js/index.js index 6afbcc9..37a484e 100644 --- a/assets/js/index.js +++ b/assets/js/index.js @@ -2,10 +2,12 @@ import React from 'react' import ReactDOM from 'react-dom' +import { BrowserRouter as Router, Route, NavLink, Switch, Redirect } from 'react-router-dom' import Progress from './components/Progress' import UriListItem from './containers/UriListItem' import reducer from './reducers' -import { fetchData, createNewUrl, setEditing } from './actions' +import { fetchData, createNewUrl, setEditing, editUser } from './actions' +import UnderlineInput from './components/UnderlineInput' import '../styles/index.scss' class App extends React.Component { @@ -14,8 +16,10 @@ class App extends React.Component { this.state = { error: '', user: { + id: '', email: '', - password: '' + password: '', + currentPassword: '' }, urls: [], editingUrl: null, @@ -24,6 +28,8 @@ class App extends React.Component { this.dispatch = this.dispatch.bind(this) this.getRegisteredUris = this.getRegisteredUris.bind(this) + this.setUserValue = this.setUserValue.bind(this) + this.saveUser = this.saveUser.bind(this) } dispatch (action) { if (!action) throw new Error('dispatch: missing action') @@ -49,33 +55,97 @@ class App extends React.Component { editing={this.state.editingUrl === item.id} />) }) } + setUserValue (which, e) { + this.setState({ + user: { + ...this.state.user, + [which]: e.target.value + } + }) + } + saveUser () { + this.dispatch(editUser(this.state.user)) + this.setState({ + user: { + ...this.state.user, + email: this.state.user.email, + password: '', + currentPassword: '' + } + }) + } render () { return ( -
this.dispatch(setEditing(null))}> - -
- - {this.state.error &&
{this.state.error}
} -
-
-

Push URIs

-

Newly published books will be sent to these addresses.

-
- -
-
    - {this.getRegisteredUris()} -
-
-
+ +
this.dispatch(setEditing(null))}> + +
+ + {this.state.error &&
{this.state.error}
} + + ( +
+
+
+

Push URIs

+

Newly published books will be sent to these addresses.

+
+ +
+
    + {this.getRegisteredUris()} +
+
+ )} /> + + ( +
+
+
+

My account

+

User account settings

+
+
+
+ this.setUserValue('email', e)} /> + this.setUserValue('password', e)} /> + this.setUserValue('currentPassword', e)} /> +
+ +
+
+
+ )} /> + + } /> +
+
+
+
) } } diff --git a/assets/js/login.js b/assets/js/login.js index 790ffdf..940e983 100644 --- a/assets/js/login.js +++ b/assets/js/login.js @@ -108,8 +108,8 @@ class App extends React.Component { error={this.state.passwordError} button='Sign in' onButtonClick={() => this.dispatch(checkPassword(this.state.user.email, this.state.user.password))} - smallButton='Forgot password?' - onSmallButtonClick={() => this.dispatch(setCarousel(3))} /> + comment={null/*smallButton='Forgot password?' + onSmallButtonClick={() => this.dispatch(setCarousel(3))}*/} /> { } case Actions.error: return { - error: data.message + error: (data || {}).message || '' } default: return {} } diff --git a/assets/styles/index.scss b/assets/styles/index.scss index e55ba57..42e3935 100644 --- a/assets/styles/index.scss +++ b/assets/styles/index.scss @@ -13,9 +13,10 @@ padding: 0 14px; margin: -14px 0 8px 0; } - & > header { - padding: 0 14px; - + & > div { + & > header { + padding: 0 14px; + } h1 { text-shadow: 1px 1px 2px $black-3; } @@ -35,6 +36,14 @@ list-style: none; // overflow: hidden; } + .inputs { + padding: 20px 14px; + + .buttons { + margin-top: 14px; + text-align: right; + } + } &.working { & > .progress { top: 0; diff --git a/assets/styles/shared/twopanels.scss b/assets/styles/shared/twopanels.scss index 8c72e21..b53c1a1 100644 --- a/assets/styles/shared/twopanels.scss +++ b/assets/styles/shared/twopanels.scss @@ -6,7 +6,6 @@ box-shadow: $shadow-1; header { - height: 50px; line-height: 50px; padding: 0 14px; diff --git a/config/protocols.js b/config/protocols.js index 0a6183e..aed2dbe 100644 --- a/config/protocols.js +++ b/config/protocols.js @@ -66,7 +66,48 @@ module.exports.protocols = { } }, update: async function (user, next) { - throw new Error('not implemented') + try { + const dbUser = await User.findOne({ + id: user.id + }) + if (!dbUser) throw new Error('an account with that id was not found') + + const passport = await Passport.findOne({ + protocol: 'local', + user: user.id + }) + if (!user.currentPassword && passport) throw new Error('Missing current password') + if (passport) { + const res = await Passport.validatePassword(user.currentPassword, passport) + if (!res) throw new Error('incorrect password') + + await User.update({ id: user.id }, { + email: user.email + }) + if (user.password && user.password.length) { + await Passport.update({ id: passport.id }, { + password: user.password + }) + } + } else { // no password yet, add one + await User.update({ id: user.id }, { + email: user.email + }) + if (user.password && user.password.length) { + const token = generateToken() + await Passport.create({ + protocol: 'local', + password: user.password, + user: dbUser.id, + accesstoken: token + }) + } + } + delete dbUser.password + next(null, dbUser) + } catch (e) { + return next(e) + } }, connect: async function (req, res, next) { try { diff --git a/config/routes.js b/config/routes.js index dfe64c9..e85fc02 100644 --- a/config/routes.js +++ b/config/routes.js @@ -31,7 +31,9 @@ module.exports.routes = { 'GET /register': { view: 'pages/login' }, - 'GET /app': 'TargetController.show', + // figure out why proper clientside routing breaks the backend session + 'GET /account': 'TargetController.show', + 'GET /targets': 'TargetController.show', /*************************************************************************** * * @@ -55,6 +57,8 @@ module.exports.routes = { 'POST /auth/email_available': 'AuthController.emailAvailable', // 'POST /auth/local': 'AuthController.callback', // 'POST /auth/local/:action': 'AuthController.callback', + 'GET /api/me': 'UserController.me', + 'PATCH /api/me': 'UserController.edit', 'POST /auth/:provider': 'AuthController.callback', 'POST /auth/:provider/:action': 'AuthController.callback', @@ -64,9 +68,7 @@ module.exports.routes = { 'GET /auth/:provider/:action': 'AuthController.callback', 'POST /api/publish': 'BooksController.publish', - 'GET /api/books': 'BooksController.list', - 'GET /api/me': 'UserController.me', 'POST /api/targets': 'TargetController.create', 'PATCH /api/targets/:id': 'TargetController.edit', diff --git a/package.json b/package.json index aba769e..5fff937 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "mocha": "^5.2.0", "node-sass": "^4.9.4", "npm-run-all": "^4.1.3", + "react-router-dom": "^4.3.1", "rimraf": "^2.6.2", "sass-loader": "^7.1.0", "standard": "^12.0.1", diff --git a/shrinkwrap.yaml b/shrinkwrap.yaml index 5dcf30d..282f6e6 100644 --- a/shrinkwrap.yaml +++ b/shrinkwrap.yaml @@ -1,5 +1,7 @@ dependencies: request: 2.88.0 +devDependencies: + react-router-dom: 4.3.1 packages: /ajv/6.8.1: dependencies: @@ -136,6 +138,20 @@ packages: node: '>=6' resolution: integrity: sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g== + /history/4.7.2: + dependencies: + invariant: 2.2.4 + loose-envify: 1.4.0 + resolve-pathname: 2.2.0 + value-equal: 0.4.0 + warning: 3.0.0 + dev: true + resolution: + integrity: sha512-1zkBRWW6XweO0NBcjiphtVJVsIQ+SXF29z9DVkceeaSLVMFXHool+fdCZD4spDCfZJCILPILc3bm7Bc+HRi0nA== + /hoist-non-react-statics/2.5.5: + dev: true + resolution: + integrity: sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw== /http-signature/1.2.0: dependencies: assert-plus: 1.0.0 @@ -147,14 +163,28 @@ packages: npm: '>=1.3.7' resolution: integrity: sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= + /invariant/2.2.4: + dependencies: + loose-envify: 1.4.0 + dev: true + resolution: + integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== /is-typedarray/1.0.0: dev: false resolution: integrity: sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= + /isarray/0.0.1: + dev: true + resolution: + integrity: sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= /isstream/0.1.2: dev: false resolution: integrity: sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= + /js-tokens/4.0.0: + dev: true + resolution: + integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== /jsbn/0.1.1: dev: false resolution: @@ -182,6 +212,13 @@ packages: '0': node >=0.6.0 resolution: integrity: sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= + /loose-envify/1.4.0: + dependencies: + js-tokens: 4.0.0 + dev: true + hasBin: true + resolution: + integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== /mime-db/1.37.0: dev: false engines: @@ -200,10 +237,29 @@ packages: dev: false resolution: integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== + /object-assign/4.1.1: + dev: true + engines: + node: '>=0.10.0' + resolution: + integrity: sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + /path-to-regexp/1.7.0: + dependencies: + isarray: 0.0.1 + dev: true + resolution: + integrity: sha1-Wf3g9DW62suhA6hOnTvGTpa5k30= /performance-now/2.1.0: dev: false resolution: integrity: sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= + /prop-types/15.6.2: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + dev: true + resolution: + integrity: sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ== /psl/1.1.31: dev: false resolution: @@ -224,6 +280,33 @@ packages: node: '>=0.6' resolution: integrity: sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + /react-router-dom/4.3.1: + dependencies: + history: 4.7.2 + invariant: 2.2.4 + loose-envify: 1.4.0 + prop-types: 15.6.2 + react-router: 4.3.1 + warning: 4.0.2 + dev: true + peerDependencies: + react: '>=15' + resolution: + integrity: sha512-c/MlywfxDdCp7EnB7YfPMOfMD3tOtIjrQlj/CKfNMBxdmpJP8xcz5P/UAFn3JbnQCNUxsHyVVqllF9LhgVyFCA== + /react-router/4.3.1: + dependencies: + history: 4.7.2 + hoist-non-react-statics: 2.5.5 + invariant: 2.2.4 + loose-envify: 1.4.0 + path-to-regexp: 1.7.0 + prop-types: 15.6.2 + warning: 4.0.2 + dev: true + peerDependencies: + react: '>=15' + resolution: + integrity: sha512-yrvL8AogDh2X42Dt9iknk4wF4V8bWREPirFfS9gLU1huk6qK41sg7Z/1S81jjTrGHxa3B8R3J6xIkDAA6CVarg== /request/2.88.0: dependencies: aws-sign2: 0.7.0 @@ -251,6 +334,10 @@ packages: node: '>= 4' resolution: integrity: sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== + /resolve-pathname/2.2.0: + dev: true + resolution: + integrity: sha512-bAFz9ld18RzJfddgrO2e/0S2O81710++chRMUxHjXOYKF6jTAMrUNZrEZ1PvV0zlhfjidm08iRPdTLPno1FuRg== /safe-buffer/5.1.2: dev: false resolution: @@ -306,6 +393,10 @@ packages: hasBin: true resolution: integrity: sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== + /value-equal/0.4.0: + dev: true + resolution: + integrity: sha512-x+cYdNnaA3CxvMaTX0INdTCN8m8aF2uY9BvEqmxuYp8bL09cs/kWVQPVGcA35fMktdOsP69IgU7wFj/61dJHEw== /verror/1.10.0: dependencies: assert-plus: 1.0.0 @@ -316,8 +407,21 @@ packages: '0': node >=0.6.0 resolution: integrity: sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= + /warning/3.0.0: + dependencies: + loose-envify: 1.4.0 + dev: true + resolution: + integrity: sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w= + /warning/4.0.2: + dependencies: + loose-envify: 1.4.0 + dev: true + resolution: + integrity: sha512-wbTp09q/9C+jJn4KKJfJfoS6VleK/Dti0yqWSm6KMvJ4MRCXFQNapHuJXutJIrWV0Cf4AhTdeIe4qdKHR1+Hug== registry: 'https://registry.npmjs.org/' shrinkwrapMinorVersion: 9 shrinkwrapVersion: 3 specifiers: + react-router-dom: ^4.3.1 request: ^2.88.0 diff --git a/views/pages/app.ejs b/views/pages/app.ejs new file mode 100644 index 0000000..11f0cbb --- /dev/null +++ b/views/pages/app.ejs @@ -0,0 +1 @@ +<%- partial('../../.tmp/public/index.html') %> diff --git a/views/pages/targets.ejs b/views/pages/targets.ejs deleted file mode 100644 index 9816e60..0000000 --- a/views/pages/targets.ejs +++ /dev/null @@ -1 +0,0 @@ -<%- partial('../../.tmp/public/targets.html') %>