mirror of
https://github.com/wassname/talk.git
synced 2026-07-14 11:18:50 +08:00
Merge branch 'master' into ban-user
This commit is contained in:
+1
-1
@@ -1,7 +1,7 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = tab
|
||||
indent_style = space
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from './../translations';
|
||||
const lang = new I18n(translations);
|
||||
import * as actions from '../constants/auth';
|
||||
import {base, handleResp, getInit} from '../helpers/response';
|
||||
|
||||
@@ -27,7 +30,7 @@ export const fetchSignIn = (formData) => dispatch => {
|
||||
dispatch(hideSignInDialog());
|
||||
dispatch(signInSuccess(user));
|
||||
})
|
||||
.catch(() => dispatch(signInFailure('Email and/or password combination incorrect.')));
|
||||
.catch(() => dispatch(signInFailure(lang.t('error.emailPasswordError'))));
|
||||
};
|
||||
|
||||
// Sign In - Facebook
|
||||
@@ -75,7 +78,7 @@ export const fetchSignUp = formData => dispatch => {
|
||||
dispatch(changeView('SIGNIN'));
|
||||
}, 3000);
|
||||
})
|
||||
.catch((error) => dispatch(signUpFailure(error)));
|
||||
.catch(() => dispatch(signUpFailure(lang.t('error.emailInUse')))); // We need to inprove error handling. TODO (bc)
|
||||
};
|
||||
|
||||
// Forgot Password Actions
|
||||
@@ -106,27 +109,8 @@ export const logout = () => dispatch => {
|
||||
.catch(error => dispatch(logOutFailure(error)));
|
||||
};
|
||||
|
||||
// Availability Checks Actions
|
||||
// LogOut Actions
|
||||
|
||||
const availabilityRequest = () => ({type: actions.FETCH_AVAILABILITY_REQUEST});
|
||||
const availabilitySuccess = () => ({type: actions.FETCH_AVAILABILITY_SUCCESS});
|
||||
const availabilityFailure = () => ({type: actions.FETCH_AVAILABILITY_FAILURE});
|
||||
|
||||
const availableField = field => ({type: actions.AVAILABLE_FIELD, field});
|
||||
const unavailableField = field => ({type: actions.UNAVAILABLE_FIELD, field});
|
||||
|
||||
export const fetchCheckAvailability = formData => dispatch => {
|
||||
dispatch(availabilityRequest());
|
||||
fetch(`${base}/user/availability`, getInit('POST', formData))
|
||||
.then(handleResp)
|
||||
.then(({status}) => {
|
||||
const [field] = Object.keys(formData);
|
||||
dispatch(availabilitySuccess());
|
||||
if (status === 'available') {
|
||||
return dispatch(availableField(field));
|
||||
}
|
||||
return dispatch(unavailableField(field));
|
||||
})
|
||||
.catch((error) => availabilityFailure(error));
|
||||
};
|
||||
export const validForm = () => ({type: actions.VALID_FORM});
|
||||
export const invalidForm = error => ({type: actions.INVALID_FORM, error});
|
||||
|
||||
|
||||
@@ -24,9 +24,6 @@ export const LOGOUT_REQUEST = 'LOGOUT_REQUEST';
|
||||
export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS';
|
||||
export const LOGOUT_FAILURE = 'LOGOUT_FAILURE';
|
||||
|
||||
export const FETCH_AVAILABILITY_REQUEST = 'FETCH_AVAILABILITY_REQUEST';
|
||||
export const FETCH_AVAILABILITY_SUCCESS = 'FETCH_AVAILABILITY_SUCCESS';
|
||||
export const FETCH_AVAILABILITY_FAILURE = 'FETCH_AVAILABILITY_FAILURE';
|
||||
export const INVALID_FORM = 'INVALID_FORM';
|
||||
export const VALID_FORM = 'VALID_FORM';
|
||||
|
||||
export const AVAILABLE_FIELD = 'AVAILABLE_FIELD';
|
||||
export const UNAVAILABLE_FIELD = 'UNAVAILABLE_FIELD';
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from './../translations';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
export default {
|
||||
email: 'Not a valid E-Mail',
|
||||
password: 'Password must be at least 8 characters',
|
||||
displayName: 'Display name is too short',
|
||||
confirmPassword: 'Passwords don`t match. Please, check again'
|
||||
email: lang.t('error.email'),
|
||||
password: lang.t('error.password'),
|
||||
displayName: lang.t('error.displayName'),
|
||||
confirmPassword: lang.t('error.confirmPassword')
|
||||
};
|
||||
|
||||
@@ -7,8 +7,7 @@ const initialState = Map({
|
||||
user: null,
|
||||
showSignInDialog: false,
|
||||
view: 'SIGNIN',
|
||||
signUpError: '',
|
||||
emailAvailable: true,
|
||||
error: '',
|
||||
successSignUp: false
|
||||
});
|
||||
|
||||
@@ -22,14 +21,12 @@ export default function auth (state = initialState, action) {
|
||||
isLoading: false,
|
||||
showSignInDialog: false,
|
||||
view: 'SIGNIN',
|
||||
signInError: '',
|
||||
signUpError: '',
|
||||
emailAvailable: true,
|
||||
error: '',
|
||||
successSignUp: false
|
||||
}));
|
||||
case actions.CHANGE_VIEW :
|
||||
return state
|
||||
.set('signInError', '')
|
||||
.set('error', '')
|
||||
.set('view', action.view);
|
||||
case actions.CLEAN_STATE:
|
||||
return initialState;
|
||||
@@ -43,7 +40,7 @@ export default function auth (state = initialState, action) {
|
||||
case actions.FETCH_SIGNIN_FAILURE:
|
||||
return state
|
||||
.set('isLoading', false)
|
||||
.set('signInError', action.error);
|
||||
.set('error', action.error);
|
||||
case actions.FETCH_SIGNIN_FACEBOOK_SUCCESS:
|
||||
return state
|
||||
.set('user', action.user)
|
||||
@@ -57,6 +54,7 @@ export default function auth (state = initialState, action) {
|
||||
.set('isLoading', true);
|
||||
case actions.FETCH_SIGNUP_FAILURE:
|
||||
return state
|
||||
.set('error', action.error)
|
||||
.set('isLoading', false);
|
||||
case actions.FETCH_SIGNUP_SUCCESS:
|
||||
return state
|
||||
@@ -66,12 +64,12 @@ export default function auth (state = initialState, action) {
|
||||
return state
|
||||
.set('loggedIn', false)
|
||||
.set('user', null);
|
||||
case actions.AVAILABLE_FIELD:
|
||||
case actions.INVALID_FORM:
|
||||
return state
|
||||
.set(`${action.field}Available`, true);
|
||||
case actions.UNAVAILABLE_FIELD:
|
||||
.set('error', action.error);
|
||||
case actions.VALID_FORM:
|
||||
return state
|
||||
.set(`${action.field}Available`, false);
|
||||
.set('error', '');
|
||||
default :
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"en": {
|
||||
"error": {
|
||||
"email": "Not a valid E-Mail",
|
||||
"password": "Password must be at least 8 characters",
|
||||
"displayName": "Display name is too short",
|
||||
"confirmPassword": "Passwords don`t match. Please, check again",
|
||||
"emailPasswordError": "Email and/or password combination incorrect.",
|
||||
"emailInUse": "Email address already in use"
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
"error": {
|
||||
"email": "No es un email válido",
|
||||
"password": "La contraseña debe tener por lo menos 8 caracteres",
|
||||
"displayName": "El nombre es muy corto",
|
||||
"confirmPassword": "Las contraseñas no coinciden",
|
||||
"emailPasswordError": "Email y/o contraseña incorrecta.",
|
||||
"emailInUse": "Email address already in use"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ const SignInContent = ({handleChange, formData, ...props}) => (
|
||||
{lang.t('signIn.or')}
|
||||
</h1>
|
||||
</div>
|
||||
{ props.auth.signInError && <Alert>{props.auth.signInError}</Alert> }
|
||||
{ props.auth.error && <Alert>{props.auth.error}</Alert> }
|
||||
<form onSubmit={props.handleSignIn}>
|
||||
<FormField
|
||||
id="email"
|
||||
|
||||
@@ -26,17 +26,16 @@ const SignUpContent = ({handleChange, formData, ...props}) => (
|
||||
{lang.t('signIn.or')}
|
||||
</h1>
|
||||
</div>
|
||||
{ props.auth.signUpError && <Alert>{props.auth.signUpError}</Alert> }
|
||||
{ props.auth.error && <Alert>{props.auth.error}</Alert> }
|
||||
<form onSubmit={props.handleSignUp}>
|
||||
<FormField
|
||||
id="email"
|
||||
type="email"
|
||||
label={lang.t('signIn.email')}
|
||||
value={formData.email}
|
||||
showErrors={!props.auth.emailAvailable || props.showErrors}
|
||||
errorMsg={!props.auth.emailAvailable ? lang.t('signIn.emailInUse') : props.errors.email}
|
||||
showErrors={props.showErrors}
|
||||
errorMsg={props.errors.email}
|
||||
onChange={handleChange}
|
||||
autoFocus
|
||||
/>
|
||||
<FormField
|
||||
id="displayName"
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import debounce from 'lodash.debounce';
|
||||
|
||||
import SignDialog from '../components/SignDialog';
|
||||
import Button from 'coral-ui/components/Button';
|
||||
|
||||
import validate from 'coral-framework/helpers/validate';
|
||||
import errorMsj from 'coral-framework/helpers/error';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
import {
|
||||
changeView,
|
||||
@@ -17,7 +17,8 @@ import {
|
||||
fetchSignInFacebook,
|
||||
fetchForgotPassword,
|
||||
facebookCallback,
|
||||
fetchCheckAvailability
|
||||
invalidForm,
|
||||
validForm
|
||||
} from '../../coral-framework/actions/auth';
|
||||
|
||||
class SignInContainer extends Component {
|
||||
@@ -44,6 +45,12 @@ class SignInContainer extends Component {
|
||||
|
||||
componentDidMount() {
|
||||
window.authCallback = this.props.facebookCallback;
|
||||
const {formData} = this.state;
|
||||
const errors = Object.keys(formData).reduce((map, prop) => {
|
||||
map[prop] = lang.t('signIn.requiredField');
|
||||
return map;
|
||||
}, {});
|
||||
this.setState({errors});
|
||||
}
|
||||
|
||||
handleChange(e) {
|
||||
@@ -71,29 +78,23 @@ class SignInContainer extends Component {
|
||||
validation(name, value) {
|
||||
const {addError} = this;
|
||||
const {formData} = this.state;
|
||||
const {checkAvailability} = this.props;
|
||||
|
||||
if (!value.length) {
|
||||
addError(name, 'Please, fill this field');
|
||||
addError(name, lang.t('signIn.requiredField'));
|
||||
} else if (name === 'confirmPassword' && formData.confirmPassword !== formData.password) {
|
||||
addError('confirmPassword', 'Passwords don`t match. Please, check again');
|
||||
addError('confirmPassword', lang.t('signIn.passwordsDontMatch'));
|
||||
} else if (!validate[name](value)) {
|
||||
addError(name, errorMsj[name]);
|
||||
} else {
|
||||
const { [name]: prop, ...errors } = this.state.errors; // eslint-disable-line
|
||||
// Removes Error
|
||||
this.setState(state => ({...state, errors}));
|
||||
// Checks Email Availability
|
||||
if (name === 'email') {
|
||||
debounce(() => checkAvailability({[name]: value}), 200, {'maxWait': 1000})();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isCompleted() {
|
||||
const {formData} = this.state;
|
||||
const {emailAvailable} = this.props.auth;
|
||||
return !Object.keys(formData).filter(prop => !formData[prop].length).length && emailAvailable;
|
||||
return !Object.keys(formData).filter(prop => !formData[prop].length).length;
|
||||
}
|
||||
|
||||
displayErrors(show = true) {
|
||||
@@ -103,9 +104,13 @@ class SignInContainer extends Component {
|
||||
handleSignUp(e) {
|
||||
e.preventDefault();
|
||||
const {errors} = this.state;
|
||||
const {fetchSignUp, validForm, invalidForm} = this.props;
|
||||
this.displayErrors();
|
||||
if (this.isCompleted() && !Object.keys(errors).length) {
|
||||
this.props.fetchSignUp(this.state.formData);
|
||||
fetchSignUp(this.state.formData);
|
||||
validForm();
|
||||
} else {
|
||||
invalidForm(lang.t('signIn.checkTheForm'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,10 +123,6 @@ class SignInContainer extends Component {
|
||||
this.props.hideSignInDialog();
|
||||
}
|
||||
|
||||
changeView(view) {
|
||||
this.props.changeView(view);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {auth, showSignInDialog} = this.props;
|
||||
return (
|
||||
@@ -147,14 +148,15 @@ const mapStateToProps = state => ({
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
facebookCallback: (err, data) => dispatch(facebookCallback(err, data)),
|
||||
fetchSignUp: (formData) => dispatch(fetchSignUp(formData)),
|
||||
fetchSignIn: (formData) => dispatch(fetchSignIn(formData)),
|
||||
fetchSignUp: formData => dispatch(fetchSignUp(formData)),
|
||||
fetchSignIn: formData => dispatch(fetchSignIn(formData)),
|
||||
fetchSignInFacebook: () => dispatch(fetchSignInFacebook()),
|
||||
fetchForgotPassword: (formData) => dispatch(fetchForgotPassword(formData)),
|
||||
fetchForgotPassword: formData => dispatch(fetchForgotPassword(formData)),
|
||||
showSignInDialog: () => dispatch(showSignInDialog()),
|
||||
changeView: (view) => dispatch(changeView(view)),
|
||||
changeView: view => dispatch(changeView(view)),
|
||||
handleClose: () => dispatch(hideSignInDialog()),
|
||||
checkAvailability: (formData) => dispatch(fetchCheckAvailability(formData))
|
||||
invalidForm: error => dispatch(invalidForm(error)),
|
||||
validForm: () => dispatch(validForm())
|
||||
});
|
||||
|
||||
export default connect(
|
||||
|
||||
@@ -16,7 +16,10 @@ export default {
|
||||
displayName: 'Display Name',
|
||||
alreadyHaveAnAccount: 'Already have an account?',
|
||||
recoverPassword: 'Recover password',
|
||||
emailInUse: 'Email address already in use'
|
||||
emailInUse: 'Email address already in use',
|
||||
requiredField: 'This field is required',
|
||||
passwordsDontMatch: 'Passwords don\'t match.',
|
||||
checkTheForm: 'Invalid Form. Please, check the fields'
|
||||
}
|
||||
},
|
||||
es: {
|
||||
@@ -36,7 +39,10 @@ export default {
|
||||
displayName: 'Nombre',
|
||||
alreadyHaveAnAccount: 'Ya tienes una cuenta?',
|
||||
recoverPassword: 'Recuperar contraseña',
|
||||
emailInUse: 'Este email se encuentra en uso'
|
||||
emailInUse: 'Este email se encuentra en uso',
|
||||
requiredField: 'Este campo es requerido',
|
||||
passwordsDontMatch: 'Las contraseñas no coinciden',
|
||||
checkTheForm: 'Formulario Inválido. Por favor, completa los campos'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
const mongoose = require('../mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
/**
|
||||
* this Schema manages application settings that get used on front- and backend
|
||||
* NOTE: when you set a setting here, it will not automatically be exposed to
|
||||
* the front end. You must add it to the whitelist in the settings route
|
||||
* in /routes/api/settings/index.js
|
||||
* @type {Schema}
|
||||
*/
|
||||
const SettingSchema = new Schema({
|
||||
id: {type: String, default: '1'},
|
||||
moderation: {type: String, enum: ['pre', 'post'], default: 'pre'},
|
||||
|
||||
+71
-18
@@ -1,6 +1,7 @@
|
||||
const mongoose = require('../mongoose');
|
||||
const uuid = require('uuid');
|
||||
const bcrypt = require('bcrypt');
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
// SALT_ROUNDS is the number of rounds that the bcrypt algorithm will run
|
||||
// through during the salting process.
|
||||
@@ -18,6 +19,14 @@ const USER_STATUS = [
|
||||
'banned'
|
||||
];
|
||||
|
||||
// In the event that the TALK_SESSION_SECRET is missing but we are testing, then
|
||||
// set the process.env.TALK_SESSION_SECRET.
|
||||
if (process.env.NODE_ENV === 'test' && !process.env.TALK_SESSION_SECRET) {
|
||||
process.env.TALK_SESSION_SECRET = 'keyboard cat';
|
||||
} else if (!process.env.TALK_SESSION_SECRET) {
|
||||
throw new Error('TALK_SESSION_SECRET must be defined to encode JSON Web Tokens and other auth functionality');
|
||||
}
|
||||
|
||||
// UserSchema is the mongoose schema defined as the representation of a User in
|
||||
// MongoDB.
|
||||
const UserSchema = new mongoose.Schema({
|
||||
@@ -140,10 +149,14 @@ const UserService = module.exports = {};
|
||||
* @param {Function} done [description]
|
||||
*/
|
||||
UserService.findLocalUser = (email, password) => {
|
||||
if (!email || typeof email !== 'string') {
|
||||
return Promise.reject('email is required for findLocalUser');
|
||||
}
|
||||
|
||||
return UserModel.findOne({
|
||||
profiles: {
|
||||
$elemMatch: {
|
||||
id: email,
|
||||
id: email.toLowerCase(),
|
||||
provider: 'local'
|
||||
}
|
||||
}
|
||||
@@ -247,6 +260,7 @@ UserService.changePassword = (id, password) => {
|
||||
})
|
||||
.then((hashedPassword) => {
|
||||
return UserModel.update({id}, {
|
||||
$inc: {__v: 1},
|
||||
$set: {
|
||||
password: hashedPassword
|
||||
}
|
||||
@@ -278,6 +292,8 @@ UserService.createLocalUser = (email, password, displayName) => {
|
||||
return Promise.reject('email is required');
|
||||
}
|
||||
|
||||
email = email.toLowerCase();
|
||||
|
||||
if (!password) {
|
||||
return Promise.reject('password is required');
|
||||
}
|
||||
@@ -306,9 +322,11 @@ UserService.createLocalUser = (email, password, displayName) => {
|
||||
|
||||
user.save((err) => {
|
||||
if (err) {
|
||||
if (err.code === 11000) {
|
||||
return reject('Email address already in use');
|
||||
}
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
return resolve(user);
|
||||
});
|
||||
});
|
||||
@@ -430,6 +448,57 @@ UserService.findByIdArray = (ids) => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a JWT from a user email. Only works for local accounts.
|
||||
* @param {String} email of the local user
|
||||
*/
|
||||
UserService.createPasswordResetToken = function (email) {
|
||||
if (!email || typeof email !== 'string') {
|
||||
return Promise.reject('email is required when creating a JWT for resetting passord');
|
||||
}
|
||||
|
||||
email = email.toLowerCase();
|
||||
|
||||
return UserModel.findOne({profiles: {$elemMatch: {id: email}}})
|
||||
.then(user => {
|
||||
|
||||
if (user === null) {
|
||||
// since we don't want to reveal that the email does/doesn't exist
|
||||
// just go ahead and resolve the Promise with null and check in the endpoint
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
const payload = {email, jti: uuid.v4(), userId: user.id, version: user.__v};
|
||||
const token = jwt.sign(payload, process.env.TALK_SESSION_SECRET, {expiresIn: '1d'});
|
||||
|
||||
return token;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* verifies a jwt and returns the associated user
|
||||
* @param {String} token the JSON Web Token to verify
|
||||
*/
|
||||
UserService.verifyPasswordResetToken = token => {
|
||||
return new Promise((resolve, reject) => {
|
||||
jwt.verify(token, process.env.TALK_SESSION_SECRET, (error, decoded) => {
|
||||
if (error) {
|
||||
return reject(error);
|
||||
}
|
||||
|
||||
resolve(decoded);
|
||||
});
|
||||
})
|
||||
.then(decoded => {
|
||||
/**
|
||||
* TODO: check the jti from this decoded token in redis
|
||||
* and make an entry if it does not exist.
|
||||
* reject if entry already exists.
|
||||
*/
|
||||
return UserService.findById(decoded.userId);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds a user using a value which gets compared using a prefix match against
|
||||
* the user's email address and/or their display name.
|
||||
@@ -464,22 +533,6 @@ UserService.search = (value) => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds users by email and returns the count. The result should be 1 or 0 (bool) indicating email availability
|
||||
* @param {String} email to search by
|
||||
* @return {Promise}
|
||||
*/
|
||||
UserService.availabilityCheck = (email) => {
|
||||
return UserModel.count({
|
||||
profiles: {
|
||||
$elemMatch: {
|
||||
id: email,
|
||||
provider: 'local'
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a count of the current users.
|
||||
* @return {Promise}
|
||||
|
||||
+8
-13
@@ -16,13 +16,8 @@
|
||||
"config": {
|
||||
"pre-git": {
|
||||
"commit-msg": [],
|
||||
"pre-commit": [
|
||||
"npm run lint",
|
||||
"npm test"
|
||||
],
|
||||
"pre-push": [
|
||||
"npm test"
|
||||
],
|
||||
"pre-commit": ["npm run lint", "npm test"],
|
||||
"pre-push": ["npm test"],
|
||||
"post-commit": [],
|
||||
"post-merge": []
|
||||
}
|
||||
@@ -31,12 +26,7 @@
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/coralproject/talk.git"
|
||||
},
|
||||
"keywords": [
|
||||
"talk",
|
||||
"coral",
|
||||
"coralproject",
|
||||
"ask"
|
||||
],
|
||||
"keywords": ["talk", "coral", "coralproject", "ask"],
|
||||
"author": "",
|
||||
"license": "Apache-2.0",
|
||||
"bugs": {
|
||||
@@ -60,6 +50,11 @@
|
||||
"passport": "^0.3.2",
|
||||
"passport-facebook": "^2.1.1",
|
||||
"passport-local": "^1.0.0",
|
||||
"jsonwebtoken": "^7.1.9",
|
||||
"lodash": "^4.16.6",
|
||||
"mongoose": "^4.6.5",
|
||||
"morgan": "^1.7.0",
|
||||
"nodemailer": "^2.6.4",
|
||||
"prompt": "^1.0.0",
|
||||
"redis": "^2.6.3",
|
||||
"uuid": "^2.0.3"
|
||||
|
||||
@@ -4,7 +4,7 @@ const url = process.env.TALK_REDIS_URL || 'redis://localhost';
|
||||
|
||||
const client = redis.createClient(url, {
|
||||
retry_strategy: function(options) {
|
||||
if (options.error.code === 'ECONNREFUSED') {
|
||||
if (options.error && options.error.code === 'ECONNREFUSED') {
|
||||
|
||||
// End reconnecting on a specific error and flush all commands with a individual error
|
||||
return new Error('The server refused the connection');
|
||||
@@ -26,10 +26,6 @@ const client = redis.createClient(url, {
|
||||
}
|
||||
});
|
||||
|
||||
// client.on('error', (err) => {
|
||||
// throw err;
|
||||
// });
|
||||
|
||||
client.ping((err) => {
|
||||
if (err) {
|
||||
console.error('Can\'t ping the redis server!');
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
const express = require('express');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/embed/stream/preview', (req, res) => {
|
||||
res.render('embed-stream', {basePath: '/client/embed/stream'});
|
||||
});
|
||||
|
||||
router.get('/password-reset/:token', (req, res, next) => {
|
||||
// render a page or something?
|
||||
res.send('ok');
|
||||
});
|
||||
|
||||
router.get('*', (req, res) => {
|
||||
res.render('admin', {basePath: '/client/coral-admin'});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const _ = require('lodash');
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const Setting = require('../../../models/setting');
|
||||
@@ -5,7 +6,10 @@ const Setting = require('../../../models/setting');
|
||||
router.get('/', (req, res, next) => {
|
||||
Setting
|
||||
.getSettings()
|
||||
.then(settings => res.json(settings))
|
||||
.then(settings => {
|
||||
const whitelist = ['moderation'];
|
||||
res.json(_.pick(settings, whitelist));
|
||||
})
|
||||
.catch(next);
|
||||
});
|
||||
|
||||
|
||||
+64
-13
@@ -1,6 +1,12 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const User = require('../../../models/user');
|
||||
const mailer = require('../../../services/mailer');
|
||||
const ejs = require('ejs');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const resetEmailFile = fs.readFileSync(path.resolve(__dirname, '../../../views/password-reset-email.ejs'));
|
||||
const resetEmailTemplate = ejs.compile(resetEmailFile.toString());
|
||||
|
||||
router.get('/', (req, res, next) => {
|
||||
const {
|
||||
@@ -75,22 +81,67 @@ router.post('/', (req, res, next) => {
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/availability', (req, res, next) => {
|
||||
/**
|
||||
* expects 2 fields in the body of the request
|
||||
* 1) the token that was in the url of the email link {String}
|
||||
* 2) the new password {String}
|
||||
*/
|
||||
router.post('/update-password', (req, res, next) => {
|
||||
const {token, password} = req.body;
|
||||
|
||||
User.verifyPasswordResetToken(token)
|
||||
.then(user => {
|
||||
return User.changePassword(user.id, password);
|
||||
})
|
||||
.then(() => {
|
||||
res.status(204).end();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
res.status(401).send('Not Authorized');
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* this endpoint takes an email (username) and checks if it belongs to a User account
|
||||
* if it does, create a JWT and send an email
|
||||
*/
|
||||
router.post('/request-password-reset', (req, res, next) => {
|
||||
const {email} = req.body;
|
||||
|
||||
if (email) {
|
||||
return User.availabilityCheck(email)
|
||||
.then(count => {
|
||||
if (count) {
|
||||
return res.json({status: 'unavailable'});
|
||||
}
|
||||
return res.json({status: 'available'});
|
||||
})
|
||||
.catch(err => {
|
||||
next(err);
|
||||
});
|
||||
if (!email) {
|
||||
return next();
|
||||
}
|
||||
return res.status(404).send('Wrong parameters');
|
||||
|
||||
User
|
||||
.createPasswordResetToken(email)
|
||||
.then(token => {
|
||||
if (token === null) {
|
||||
return Promise.resolve('the email was not found in the db.');
|
||||
}
|
||||
|
||||
const options = {
|
||||
subject: 'Password Reset Requested - Talk',
|
||||
from: 'noreply@coralproject.net',
|
||||
to: email,
|
||||
html: resetEmailTemplate({
|
||||
token,
|
||||
// probably more clear to explicitly pass this
|
||||
rootURL: process.env.TALK_ROOT_URL
|
||||
})
|
||||
};
|
||||
return mailer.sendSimple(options);
|
||||
})
|
||||
.then(() => {
|
||||
// we want to send a 204 regardless of the user being found in the db
|
||||
// if we fail on missing emails, it would reveal if people are registered or not.
|
||||
res.status(204).send('OK');
|
||||
})
|
||||
.catch(error => {
|
||||
const errorMsg = typeof error === 'string' ? error : error.message;
|
||||
|
||||
res.status(500).json({error: errorMsg});
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
const nodemailer = require('nodemailer');
|
||||
|
||||
if (!process.env.TALK_SMTP_CONNECTION_URL) {
|
||||
console.error('TALK_SMTP_CONNECTION_URL should be defined if you would like to send password reset emails from Talk');
|
||||
}
|
||||
|
||||
const defaultTransporter = nodemailer.createTransport(process.env.TALK_SMTP_CONNECTION_URL);
|
||||
|
||||
const mailer = {
|
||||
|
||||
/**
|
||||
* sendSimple
|
||||
*
|
||||
* @param {Object} {from, to, subject, text = '', html = ''}
|
||||
* @returns
|
||||
*/
|
||||
sendSimple({from, to, subject, text = '', html = '', transporter = defaultTransporter}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!from) {
|
||||
reject('sendSimple requires a from address');
|
||||
}
|
||||
if (!to) {
|
||||
reject('sendSimple requires a comma-separated list of "to" addresses');
|
||||
}
|
||||
if (!subject) {
|
||||
reject('sendSimple requires a subject for the email');
|
||||
}
|
||||
|
||||
return resolve(transporter.sendMail({from, to, subject, text, html}));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = mailer;
|
||||
+31
-1
@@ -162,6 +162,36 @@ paths:
|
||||
responses:
|
||||
204:
|
||||
description: OK
|
||||
|
||||
/user/request-password-reset:
|
||||
post:
|
||||
tags:
|
||||
- Users
|
||||
description: trigger a reset password email. sends a success code whether email was found or no.
|
||||
responses:
|
||||
204:
|
||||
description: OK
|
||||
|
||||
/user/update-password:
|
||||
post:
|
||||
tags:
|
||||
- Users
|
||||
description: Update existing user password
|
||||
parameters:
|
||||
- name: token
|
||||
type: string
|
||||
in: body
|
||||
description: JSON Web token taken taken from emailed link
|
||||
required: true
|
||||
- name: password
|
||||
type: string
|
||||
in: body
|
||||
description: new password to be settings
|
||||
required: true
|
||||
responses:
|
||||
204:
|
||||
description: OK
|
||||
|
||||
/asset:
|
||||
get:
|
||||
tags:
|
||||
@@ -276,7 +306,7 @@ definitions:
|
||||
description: A summary of the asset, inferred on initial load.
|
||||
section:
|
||||
type: string
|
||||
description: The section the asset is in.
|
||||
description: The section the asset is in.
|
||||
subsection:
|
||||
type: string
|
||||
description: The subsection that the asset is in.
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<!-- extremely naive implementation of a password reset email -->
|
||||
<p>We received a request to reset your password. If you did not request this change, you can ignore this email.<br />
|
||||
If you did, <a href="<%= rootURL %>/admin/password-reset/<%= token %>">please click here to reset password</a>.</p>
|
||||
<% if (process.env.NODE_ENV !== 'production') { %>
|
||||
<p style="color: red"><%= token %></p>
|
||||
<% } %>
|
||||
Reference in New Issue
Block a user