diff --git a/imports/api/Documents/server/publications.js b/imports/api/Documents/server/publications.js index 721a522..670f13e 100644 --- a/imports/api/Documents/server/publications.js +++ b/imports/api/Documents/server/publications.js @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; import Documents from '../Documents'; -Meteor.publish('documents', function documentsView() { +Meteor.publish('documents', function documents() { return Documents.find({ owner: this.userId }); }); diff --git a/imports/api/Users/server/methods.js b/imports/api/Users/server/methods.js new file mode 100644 index 0000000..85d4bdd --- /dev/null +++ b/imports/api/Users/server/methods.js @@ -0,0 +1,33 @@ +import { Meteor } from 'meteor/meteor'; +import { check, Match } from 'meteor/check'; +import editProfile from '../../../modules/server/edit-profile'; +import rateLimit from '../../../modules/rate-limit'; + +Meteor.methods({ + 'users.editProfile': function usersEditProfile(profile) { + check(profile, { + emailAddress: String, + password: Match.Optional(Object), + profile: { + name: { + first: String, + last: String, + }, + }, + }); + + return editProfile({ userId: this.userId, profile }) + .then(response => response) + .catch((exception) => { + throw new Meteor.Error('500', exception); + }); + }, +}); + +rateLimit({ + methods: [ + 'users.editProfile', + ], + limit: 5, + timeRange: 1000, +}); diff --git a/imports/api/Users/server/publications.js b/imports/api/Users/server/publications.js new file mode 100644 index 0000000..1b0acec --- /dev/null +++ b/imports/api/Users/server/publications.js @@ -0,0 +1,10 @@ +import { Meteor } from 'meteor/meteor'; + +Meteor.publish('users.editProfile', function usersProfile() { + return Meteor.users.find(this.userId, { + fields: { + emails: 1, + profile: 1, + }, + }); +}); diff --git a/imports/modules/server/edit-profile.js b/imports/modules/server/edit-profile.js new file mode 100644 index 0000000..b065fc9 --- /dev/null +++ b/imports/modules/server/edit-profile.js @@ -0,0 +1,43 @@ +/* eslint-disable consistent-return */ +import { Meteor } from 'meteor/meteor'; +import { Accounts } from 'meteor/accounts-base'; + +let action; + +const updatePassword = (userId, newPassword) => { + try { + Accounts.setPassword(userId, newPassword, { logout: false }); + } catch (exception) { + action.reject(`[editProfile.updatePassword] ${exception}`); + } +}; + +const updateUser = (userId, { emailAddress, profile }) => { + try { + Meteor.users.update(userId, { + $set: { + 'emails.0.address': emailAddress, + profile, + }, + }); + } catch (exception) { + action.reject(`[editProfile.updateUser] ${exception}`); + } +}; + +const editProfile = ({ userId, profile }, promise) => { + try { + action = promise; + + updateUser(userId, profile); + if (profile.password) updatePassword(userId, profile.password); + + action.resolve(); + } catch (exception) { + action.reject(`[editProfile.handler] ${exception}`); + } +}; + +export default options => +new Promise((resolve, reject) => +editProfile(options, { resolve, reject })); diff --git a/imports/startup/server/api.js b/imports/startup/server/api.js index 2311511..c39fc72 100644 --- a/imports/startup/server/api.js +++ b/imports/startup/server/api.js @@ -1,2 +1,5 @@ import '../../api/Documents/methods'; import '../../api/Documents/server/publications'; + +import '../../api/Users/server/methods'; +import '../../api/Users/server/publications'; diff --git a/imports/ui/layouts/App/App.js b/imports/ui/layouts/App/App.js index acc05d3..014d9b4 100644 --- a/imports/ui/layouts/App/App.js +++ b/imports/ui/layouts/App/App.js @@ -17,6 +17,7 @@ import Signup from '../../pages/Signup/Signup'; import Login from '../../pages/Login/Login'; import RecoverPassword from '../../pages/RecoverPassword/RecoverPassword'; import ResetPassword from '../../pages/ResetPassword/ResetPassword'; +import Profile from '../../pages/Profile/Profile'; import NotFound from '../../pages/NotFound/NotFound'; const App = props => ( @@ -30,6 +31,7 @@ const App = props => ( + diff --git a/imports/ui/pages/Profile/Profile.js b/imports/ui/pages/Profile/Profile.js index e69de29..6247a04 100644 --- a/imports/ui/pages/Profile/Profile.js +++ b/imports/ui/pages/Profile/Profile.js @@ -0,0 +1,176 @@ +/* eslint-disable no-underscore-dangle */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import { Row, Col, FormGroup, ControlLabel, Button } from 'react-bootstrap'; +import { Meteor } from 'meteor/meteor'; +import { Accounts } from 'meteor/accounts-base'; +import { Bert } from 'meteor/themeteorchef:bert'; +import { createContainer } from 'meteor/react-meteor-data'; +import InputHint from '../../components/InputHint/InputHint'; +import validate from '../../../modules/validate'; + +class Profile extends React.Component { + constructor(props) { + super(props); + this.handleSubmit = this.handleSubmit.bind(this); + } + + componentDidMount() { + const component = this; + + validate(component.form, { + rules: { + firstName: { + required: true, + }, + lastName: { + required: true, + }, + emailAddress: { + required: true, + email: true, + }, + currentPassword: { + required() { + // Only required if newPassword field has a value. + return component.newPassword.value.length > 0; + }, + }, + newPassword: { + required() { + // Only required if currentPassword field has a value. + return component.currentPassword.value.length > 0; + }, + }, + }, + messages: { + firstName: { + required: 'What\'s your first name?', + }, + lastName: { + required: 'What\'s your last name?', + }, + emailAddress: { + required: 'Need an email address here.', + email: 'Is this email address correct?', + }, + currentPassword: { + required: 'Need your current password if changing.', + }, + newPassword: { + required: 'Need your new password if changing.', + }, + }, + submitHandler() { component.handleSubmit(); }, + }); + } + + handleSubmit() { + const profile = { + emailAddress: this.emailAddress.value, + profile: { + name: { + first: this.firstName.value, + last: this.lastName.value, + }, + }, + }; + + if (this.newPassword.value) profile.password = Accounts._hashPassword(this.newPassword.value); + + Meteor.call('users.editProfile', profile, (error) => { + if (error) { + Bert.alert(error.reason, 'danger'); + } else { + Bert.alert('Profile updated!', 'success'); + } + }); + } + + render() { + const { loading, user } = this.props; + return (
+ + +

Edit Profile

+
(this.form = form)} onSubmit={event => event.preventDefault()}> + {!loading ? (
+ + + + First Name + (this.firstName = firstName)} + className="form-control" + /> + + + + + Last Name + (this.lastName = lastName)} + className="form-control" + /> + + + + + Email Address + (this.emailAddress = emailAddress)} + className="form-control" + /> + +
Change Password
+ + Current Password + (this.currentPassword = currentPassword)} + className="form-control" + /> + + + New Password + (this.newPassword = newPassword)} + className="form-control" + /> + Use at least six characters. + + +
) :
} + + + +
); + } +} + +Profile.propTypes = { + loading: PropTypes.bool.isRequired, + user: PropTypes.object.isRequired, +}; + +export default createContainer(() => { + const subscription = Meteor.subscribe('users.editProfile'); + + return { + loading: !subscription.ready(), + user: Meteor.user(), + }; +}, Profile);