add user settings page

pull/37/head
unknown 2019-02-04 16:08:45 -05:00
parent 0fcafdc711
commit 8a0c56644c
16 changed files with 300 additions and 45 deletions

View File

@ -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')

View File

@ -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
})
},

View File

@ -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)
})
},

View File

@ -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))
}
}

View File

@ -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) => {

View File

@ -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 (
<div className='root-container flex-container' onClick={() => this.dispatch(setEditing(null))}>
<aside className='nav nav-left'>
<header>
<h1>River of Ebooks</h1>
<h2 className='flex-container'>
<span className='flex'>{this.state.user.email}</span>
<a href='/logout'>Log out</a>
</h2>
</header>
</aside>
<section className={'content flex' + (this.state.working ? ' working' : '')}>
<Progress bound />
{this.state.error && <div className='error-box'>{this.state.error}</div>}
<header className='flex-container'>
<div className='flex'>
<h1>Push URIs</h1>
<h2>Newly published books will be sent to these addresses.</h2>
</div>
<button className='btn' onClick={() => this.dispatch(createNewUrl())}>New address</button>
</header>
<ul className='list'>
{this.getRegisteredUris()}
</ul>
</section>
</div>
<Router>
<div className='root-container flex-container' onClick={() => this.dispatch(setEditing(null))}>
<aside className='nav nav-left'>
<header>
<h1>River of Ebooks</h1>
<h2 className='flex-container'>
<span className='flex'>{this.state.user.email}</span>
<a href='/logout'>Log out</a>
</h2>
</header>
<ul>
<li><NavLink to='/targets'>Push URIs</NavLink></li>
<li><NavLink to='/account'>My account</NavLink></li>
</ul>
</aside>
<section className={'content flex' + (this.state.working ? ' working' : '')}>
<Progress bound />
{this.state.error && <div className='error-box'>{this.state.error}</div>}
<Switch>
<Route path='/targets' exact children={props => (
<div>
<header className='flex-container'>
<div className='flex'>
<h1>Push URIs</h1>
<h2>Newly published books will be sent to these addresses.</h2>
</div>
<button className='btn' onClick={() => this.dispatch(createNewUrl())}>New address</button>
</header>
<ul className='list'>
{this.getRegisteredUris()}
</ul>
</div>
)} />
<Route path='/account' exact children={props => (
<div>
<header className='flex-container'>
<div className='flex'>
<h1>My account</h1>
<h2>User account settings</h2>
</div>
</header>
<section className='inputs'>
<UnderlineInput
placeholder='Email address'
value={this.state.user.email}
pattern={/^([a-zA-Z0-9_\-.]+)@([a-zA-Z0-9_\-.]+)\.([a-zA-Z]{2,5})$/}
onChange={(e) => this.setUserValue('email', e)} />
<UnderlineInput
placeholder='Password'
type='password'
value={this.state.user.password}
onChange={(e) => this.setUserValue('password', e)} />
<UnderlineInput
placeholder='Current password'
type='password'
value={this.state.user.currentPassword}
onChange={(e) => this.setUserValue('currentPassword', e)} />
<div className='buttons'>
<button className='btn' onClick={this.saveUser}>Save</button>
</div>
</section>
</div>
)} />
<Route path='/' render={() => <Redirect to='/targets' />} />
</Switch>
</section>
</div>
</Router>
)
}
}

View File

@ -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))}*/} />
<CarouselItem
header='Password recovery'

View File

@ -43,7 +43,7 @@ const reducer = (state = {}, action) => {
}
case Actions.error:
return {
error: data.message
error: (data || {}).message || ''
}
default: return {}
}

View File

@ -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;

View File

@ -6,7 +6,6 @@
box-shadow: $shadow-1;
header {
height: 50px;
line-height: 50px;
padding: 0 14px;

View File

@ -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 {

View File

@ -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',

View File

@ -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",

View File

@ -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

1
views/pages/app.ejs Normal file
View File

@ -0,0 +1 @@
<%- partial('../../.tmp/public/index.html') %>

View File

@ -1 +0,0 @@
<%- partial('../../.tmp/public/targets.html') %>