Merge pull request #578 from coralproject/auth-tokens

JWT Auth
This commit is contained in:
Belén Curcio
2017-05-15 16:51:10 -03:00
committed by GitHub
44 changed files with 1206 additions and 817 deletions
+5 -2
View File
@@ -24,10 +24,13 @@ The Talk application looks for the following configuration values either as envi
- `TALK_MONGO_URL` (*required*) - the database connection string for the MongoDB database.
- `TALK_REDIS_URL` (*required*) - the database connection string for the Redis database.
- `TALK_SESSION_SECRET` (*required*) - a random string which will be used to
secure cookies.
- `TALK_ROOT_URL` (*required*) - root url of the installed application externally
available in the format: `<scheme>://<host>` without the path.
- `TALK_JWT_SECRET` (*required*) - a long and cryptographical secure random string which will be used to
sign and verify tokens via a `HS256` algorithm.
- `TALK_JWT_EXPIRY` (_optional_) - the expiry duration (`exp`) for the tokens issued for logged in sessions (Default `1 day`)
- `TALK_JWT_ISSUER` (_optional_) - the issuer (`iss`) claim for login JWT tokens (Default `process.env.TALK_ROOT_URL`)
- `TALK_JWT_AUDIENCE` (_optional_) - the audience (`aud`) claim for login JWT tokens (Default `talk`)
- `TALK_SMTP_EMAIL` (*required for email*) - the address to send emails from using the
SMTP provider.
- `TALK_SMTP_USERNAME` (*required for email*) - username of the SMTP provider you are using.
+5 -32
View File
@@ -3,12 +3,11 @@ const bodyParser = require('body-parser');
const morgan = require('morgan');
const path = require('path');
const helmet = require('helmet');
const authentication = require('./middleware/authentication');
const {passport} = require('./services/passport');
const plugins = require('./services/plugins');
const enabled = require('debug').enabled;
const csrf = require('csurf');
const errors = require('./errors');
const session = require('./services/session');
const {createGraphOptions} = require('./graph');
const apollo = require('graphql-server-express');
@@ -37,12 +36,6 @@ app.use('/public', express.static(path.join(__dirname, 'public')));
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
//==============================================================================
// SESSION MIDDLEWARE
//==============================================================================
app.use(session);
//==============================================================================
// PASSPORT MIDDLEWARE
//==============================================================================
@@ -60,7 +53,10 @@ plugins.get('server', 'passport').forEach((plugin) => {
// Setup the PassportJS Middleware.
app.use(passport.initialize());
app.use(passport.session());
// Attach the authentication middleware, this will be responsible for decoding
// (if present) the JWT on the request.
app.use('/api', authentication);
//==============================================================================
// GraphQL Router
@@ -84,29 +80,6 @@ if (app.get('env') !== 'production') {
}
//==============================================================================
// CSRF MIDDLEWARE
//==============================================================================
if (process.env.TEST_MODE === 'unit') {
// Add this fake test token in the event we are in unit test mode, and don't
// include the CSRF protection.
app.locals.csrfToken = 'UNIT_TESTS';
} else {
// Setup route middlewares for CSRF protection.
// Default ignore methods are GET, HEAD, OPTIONS
app.use(csrf({}));
app.use((req, res, next) => {
res.locals.csrfToken = req.csrfToken();
next();
});
}
//==============================================================================
// ROUTES
//==============================================================================
+4 -2
View File
@@ -9,13 +9,15 @@ const kue = require('../services/kue');
const mongoose = require('../services/mongoose');
const util = require('./util');
const {createSubscriptionManager} = require('../graph/subscriptions');
const {
PORT
} = require('../config');
/**
* Get port from environment and store in Express.
*/
const port = normalizePort(process.env.TALK_PORT || '3000');
const port = normalizePort(PORT);
app.set('port', port);
/**
-5
View File
@@ -3,11 +3,6 @@ const dotenv = require('dotenv');
const fs = require('fs');
const program = require('commander');
// Perform rewrites to the runtime environment variables based on the contents
// of the process.env.REWRITE_ENV if it exists. This is done here as it is the
// entrypoint for the entire application.
require('env-rewrite').rewrite();
//==============================================================================
// Setting up the program command line arguments.
//==============================================================================
+46 -25
View File
@@ -1,7 +1,12 @@
import * as actions from '../constants/auth';
import * as Storage from 'coral-framework/helpers/storage';
import coralApi from 'coral-framework/helpers/response';
import {handleAuthToken} from 'coral-framework/actions/auth';
//==============================================================================
// SIGN IN
//==============================================================================
// Log In.
export const handleLogin = (email, password, recaptchaResponse) => dispatch => {
dispatch({type: actions.LOGIN_REQUEST});
const params = {method: 'POST', body: {email, password}};
@@ -9,27 +14,42 @@ export const handleLogin = (email, password, recaptchaResponse) => dispatch => {
params.headers = {'X-Recaptcha-Response': recaptchaResponse};
}
return coralApi('/auth/local', params)
.then(({user}) => {
.then(({user, token}) => {
if (!user) {
Storage.removeItem('token');
return dispatch(checkLoginFailure('not logged in'));
}
dispatch(handleAuthToken(token));
const isAdmin = !!user.roles.filter(i => i === 'ADMIN').length;
dispatch(checkLoginSuccess(user, isAdmin));
})
.catch(error => {
if (error.translation_key === 'LOGIN_MAXIMUM_EXCEEDED') {
dispatch({type: actions.LOGIN_MAXIMUM_EXCEEDED, message: error.translation_key});
dispatch({
type: actions.LOGIN_MAXIMUM_EXCEEDED,
message: error.translation_key
});
} else {
dispatch({type: actions.LOGIN_FAILURE, message: error.translation_key});
}
});
};
const forgotPassowordRequest = () => ({type: actions.FETCH_FORGOT_PASSWORD_REQUEST});
const forgotPassowordSuccess = () => ({type: actions.FETCH_FORGOT_PASSWORD_SUCCESS});
const forgotPassowordFailure = () => ({type: actions.FETCH_FORGOT_PASSWORD_FAILURE});
//==============================================================================
// FORGOT PASSWORD
//==============================================================================
const forgotPassowordRequest = () => ({
type: actions.FETCH_FORGOT_PASSWORD_REQUEST
});
const forgotPassowordSuccess = () => ({
type: actions.FETCH_FORGOT_PASSWORD_SUCCESS
});
const forgotPassowordFailure = () => ({
type: actions.FETCH_FORGOT_PASSWORD_FAILURE
});
export const requestPasswordReset = email => dispatch => {
dispatch(forgotPassowordRequest(email));
@@ -38,17 +58,31 @@ export const requestPasswordReset = email => dispatch => {
.catch(error => dispatch(forgotPassowordFailure(error)));
};
// Check Login
//==============================================================================
// CHECK LOGIN
//==============================================================================
const checkLoginRequest = () => ({type: actions.CHECK_LOGIN_REQUEST});
const checkLoginSuccess = (user, isAdmin) => ({type: actions.CHECK_LOGIN_SUCCESS, user, isAdmin});
const checkLoginFailure = error => ({type: actions.CHECK_LOGIN_FAILURE, error});
const checkLoginRequest = () => ({
type: actions.CHECK_LOGIN_REQUEST
});
const checkLoginSuccess = (user, isAdmin) => ({
type: actions.CHECK_LOGIN_SUCCESS,
user,
isAdmin
});
const checkLoginFailure = error => ({
type: actions.CHECK_LOGIN_FAILURE,
error
});
export const checkLogin = () => dispatch => {
dispatch(checkLoginRequest());
return coralApi('/auth')
.then(({user}) => {
if (!user) {
Storage.removeItem('token');
return dispatch(checkLoginFailure('not logged in'));
}
@@ -60,16 +94,3 @@ export const checkLogin = () => dispatch => {
dispatch(checkLoginFailure(`${error.translation_key}`));
});
};
// LogOut Actions
const logOutRequest = () => ({type: actions.LOGOUT_REQUEST});
const logOutSuccess = () => ({type: actions.LOGOUT_SUCCESS});
const logOutFailure = () => ({type: actions.LOGOUT_FAILURE});
export const logout = () => dispatch => {
dispatch(logOutRequest());
return coralApi('/auth', {method: 'DELETE'})
.then(() => dispatch(logOutSuccess()))
.catch(error => dispatch(logOutFailure(error)));
};
+1 -5
View File
@@ -2,11 +2,7 @@ export const CHECK_LOGIN_REQUEST = 'CHECK_LOGIN_REQUEST';
export const CHECK_LOGIN_SUCCESS = 'CHECK_LOGIN_SUCCESS';
export const CHECK_LOGIN_FAILURE = 'CHECK_LOGIN_FAILURE';
export const CHECK_CSRF_TOKEN = 'CHECK_CSRF_TOKEN';
export const LOGOUT_REQUEST = 'LOGOUT_REQUEST';
export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS';
export const LOGOUT_FAILURE = 'LOGOUT_FAILURE';
export const LOGOUT = 'LOGOUT';
export const LOGIN_REQUEST = 'LOGIN_REQUEST';
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
@@ -1,20 +1,21 @@
import React, {Component} from 'react';
import {connect} from 'react-redux';
import Layout from '../components/ui/Layout';
import {checkLogin, handleLogin, logout, requestPasswordReset} from '../actions/auth';
import {toggleModal as toggleShortcutModal} from '../actions/moderation';
import {fetchConfig} from '../actions/config';
import {logout} from 'coral-framework/actions/auth';
import {FullLoading} from '../components/FullLoading';
import AdminLogin from '../components/AdminLogin';
import {toggleModal as toggleShortcutModal} from '../actions/moderation';
import {checkLogin, handleLogin, requestPasswordReset} from '../actions/auth';
class LayoutContainer extends Component {
componentWillMount () {
componentWillMount() {
const {checkLogin, fetchConfig} = this.props;
checkLogin();
fetchConfig();
}
render () {
render() {
const {
isAdmin,
loggedIn,
@@ -24,19 +25,34 @@ class LayoutContainer extends Component {
passwordRequestSuccess
} = this.props.auth;
const {handleLogout, toggleShortcutModal, TALK_RECAPTCHA_PUBLIC} = this.props;
if (loadingUser) { return <FullLoading />; }
const {
handleLogout,
toggleShortcutModal,
TALK_RECAPTCHA_PUBLIC
} = this.props;
if (loadingUser) {
return <FullLoading />;
}
if (!isAdmin) {
return <AdminLogin
loginMaxExceeded={loginMaxExceeded}
handleLogin={this.props.handleLogin}
requestPasswordReset={this.props.requestPasswordReset}
passwordRequestSuccess={passwordRequestSuccess}
recaptchaPublic={TALK_RECAPTCHA_PUBLIC}
errorMessage={loginError} />;
return (
<AdminLogin
loginMaxExceeded={loginMaxExceeded}
handleLogin={this.props.handleLogin}
requestPasswordReset={this.props.requestPasswordReset}
passwordRequestSuccess={passwordRequestSuccess}
recaptchaPublic={TALK_RECAPTCHA_PUBLIC}
errorMessage={loginError}
/>
);
}
if (isAdmin && loggedIn) {
return <Layout handleLogout={handleLogout} toggleShortcutModal={toggleShortcutModal} {...this.props} />;
return (
<Layout
handleLogout={handleLogout}
toggleShortcutModal={toggleShortcutModal}
{...this.props}
/>
);
}
return <FullLoading />;
}
@@ -44,19 +60,19 @@ class LayoutContainer extends Component {
const mapStateToProps = state => ({
auth: state.auth.toJS(),
TALK_RECAPTCHA_PUBLIC: state.config.get('data').get('TALK_RECAPTCHA_PUBLIC', null)
TALK_RECAPTCHA_PUBLIC: state.config
.get('data')
.get('TALK_RECAPTCHA_PUBLIC', null)
});
const mapDispatchToProps = dispatch => ({
checkLogin: () => dispatch(checkLogin()),
fetchConfig: () => dispatch(fetchConfig()),
handleLogin: (username, password, recaptchaResponse) => dispatch(handleLogin(username, password, recaptchaResponse)),
handleLogin: (username, password, recaptchaResponse) =>
dispatch(handleLogin(username, password, recaptchaResponse)),
requestPasswordReset: email => dispatch(requestPasswordReset(email)),
toggleShortcutModal: toggle => dispatch(toggleShortcutModal(toggle)),
handleLogout: () => dispatch(logout())
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(LayoutContainer);
export default connect(mapStateToProps, mapDispatchToProps)(LayoutContainer);
+1 -3
View File
@@ -26,10 +26,8 @@ export default function auth (state = initialState, action) {
.set('loadingUser', false)
.set('isAdmin', action.isAdmin)
.set('user', action.user);
case actions.LOGOUT_SUCCESS:
case actions.LOGOUT:
return initialState;
case actions.LOGIN_REQUEST:
return state.set('loginError', null);
case actions.LOGIN_SUCCESS:
return state.set('loginMaxExceeded', false).set('loginError', null);
case actions.LOGIN_FAILURE:
@@ -3,7 +3,7 @@ import Pym from '../../node_modules/pym.js';
const pym = new Pym.Child({polling: 100});
export default pym;
export const link = (url) => (e) => {
export const link = url => e => {
e.preventDefault();
pym.sendMessage('navigate', url);
};
+2 -2
View File
@@ -1,5 +1,5 @@
import ApolloClient, {addTypename} from 'apollo-client';
import getNetworkInterface from './transport';
import {networkInterface} from 'coral-framework/services/transport';
import fragmentMatcher from './fragmentMatcher';
export const client = new ApolloClient({
@@ -12,5 +12,5 @@ export const client = new ApolloClient({
}
return null;
},
networkInterface: getNetworkInterface()
networkInterface
});
@@ -39,7 +39,7 @@ const fm = new IntrospectionFragmentMatcher({
{name: 'DefaultAction'},
{name: 'FlagAction'},
{name: 'DontAgreeAction'}
],
]
},
{
kind: 'INTERFACE',
@@ -48,18 +48,18 @@ const fm = new IntrospectionFragmentMatcher({
{name: 'DefaultActionSummary'},
{name: 'FlagActionSummary'},
{name: 'DontAgreeActionSummary'}
],
]
},
{
kind: 'INTERFACE',
name: 'AssetActionSummary',
possibleTypes: [
{name: 'DefaultAssetActionSummary'},
{name: 'FlagAssetActionSummary'},
{name: 'FlagAssetActionSummary'}
]
}
],
},
]
}
}
});
@@ -1,11 +0,0 @@
import {createNetworkInterface} from 'apollo-client';
export default function getNetworkInterface(apiUrl = '/api/v1/graph/ql', headers = {}) {
return new createNetworkInterface({
uri: apiUrl,
opts: {
credentials: 'same-origin',
headers,
},
});
}
+155 -61
View File
@@ -1,11 +1,14 @@
import {gql} from 'react-apollo';
import client from 'coral-framework/services/client';
import I18n from '../../coral-framework/modules/i18n/i18n';
import translations from './../translations';
const lang = new I18n(translations);
import {pym} from 'coral-framework';
import * as Storage from '../helpers/storage';
import * as actions from '../constants/auth';
import coralApi, {base} from '../helpers/response';
import {pym} from 'coral-framework';
import client from 'coral-framework/services/client';
import jwtDecode from 'jwt-decode';
const lang = new I18n(translations);
import translations from './../translations';
import I18n from '../../coral-framework/modules/i18n/i18n';
const ME_QUERY = gql`
query Me {
@@ -28,7 +31,8 @@ const ME_QUERY = gql`
function fetchMe() {
return client.query({
fetchPolicy: 'network-only',
query: ME_QUERY});
query: ME_QUERY
});
}
// Dialog Actions
@@ -61,14 +65,29 @@ export const hideSignInDialog = () => dispatch => {
window.close();
};
export const createUsernameRequest = () => ({type: actions.CREATE_USERNAME_REQUEST});
export const showCreateUsernameDialog = () => ({type: actions.SHOW_CREATEUSERNAME_DIALOG});
export const hideCreateUsernameDialog = () => ({type: actions.HIDE_CREATEUSERNAME_DIALOG});
export const createUsernameRequest = () => ({
type: actions.CREATE_USERNAME_REQUEST
});
export const showCreateUsernameDialog = () => ({
type: actions.SHOW_CREATEUSERNAME_DIALOG
});
export const hideCreateUsernameDialog = () => ({
type: actions.HIDE_CREATEUSERNAME_DIALOG
});
const createUsernameSuccess = () => ({type: actions.CREATE_USERNAME_SUCCESS});
const createUsernameFailure = error => ({type: actions.CREATE_USERNAME_FAILURE, error});
const createUsernameSuccess = () => ({
type: actions.CREATE_USERNAME_SUCCESS
});
export const updateUsername = ({username}) => ({type: actions.UPDATE_USERNAME, username});
const createUsernameFailure = error => ({
type: actions.CREATE_USERNAME_FAILURE,
error
});
export const updateUsername = ({username}) => ({
type: actions.UPDATE_USERNAME,
username
});
export const createUsername = (userId, formData) => dispatch => {
dispatch(createUsernameRequest());
@@ -89,7 +108,7 @@ export const changeView = view => dispatch => {
view
});
switch(view) {
switch (view) {
case 'SIGNUP':
window.resizeTo(500, 800);
break;
@@ -101,26 +120,49 @@ export const changeView = view => dispatch => {
}
};
export const cleanState = () => ({type: actions.CLEAN_STATE});
export const cleanState = () => ({
type: actions.CLEAN_STATE
});
// Sign In Actions
const signInRequest = () => ({type: actions.FETCH_SIGNIN_REQUEST});
const signInRequest = () => ({
type: actions.FETCH_SIGNIN_REQUEST
});
// TODO: revisit login redux flow.
// const signInSuccess = (user, isAdmin) => ({type: actions.FETCH_SIGNIN_SUCCESS, user, isAdmin});
//
const signInFailure = error => ({type: actions.FETCH_SIGNIN_FAILURE, error});
const signInFailure = error => ({
type: actions.FETCH_SIGNIN_FAILURE,
error
});
export const fetchSignIn = (formData) => (dispatch) => {
//==============================================================================
// AUTH TOKEN
//==============================================================================
export const handleAuthToken = token => dispatch => {
Storage.setItem('exp', jwtDecode(token).exp);
Storage.setItem('token', token);
dispatch({type: 'HANDLE_AUTH_TOKEN'});
};
//==============================================================================
// SIGN IN
//==============================================================================
export const fetchSignIn = formData => dispatch => {
dispatch(signInRequest());
return coralApi('/auth/local', {method: 'POST', body: formData})
.then(() => dispatch(hideSignInDialog()))
.then(({token}) => {
dispatch(handleAuthToken(token));
dispatch(hideSignInDialog());
})
.catch(error => {
if (error.metadata) {
// the user might not have a valid email. prompt the user user re-request the confirmation email
dispatch(signInFailure(lang.t('error.emailNotVerified', error.metadata)));
dispatch(
signInFailure(lang.t('error.emailNotVerified', error.metadata))
);
} else {
// invalid credentials
@@ -129,11 +171,21 @@ export const fetchSignIn = (formData) => (dispatch) => {
});
};
// Sign In - Facebook
//==============================================================================
// SIGN IN - FACEBOOK
//==============================================================================
const signInFacebookRequest = () => ({type: actions.FETCH_SIGNIN_FACEBOOK_REQUEST});
const signInFacebookSuccess = user => ({type: actions.FETCH_SIGNIN_FACEBOOK_SUCCESS, user});
const signInFacebookFailure = error => ({type: actions.FETCH_SIGNIN_FACEBOOK_FAILURE, error});
const signInFacebookRequest = () => ({
type: actions.FETCH_SIGNIN_FACEBOOK_REQUEST
});
const signInFacebookSuccess = user => ({
type: actions.FETCH_SIGNIN_FACEBOOK_SUCCESS,
user
});
const signInFacebookFailure = error => ({
type: actions.FETCH_SIGNIN_FACEBOOK_FAILURE,
error
});
export const fetchSignInFacebook = () => dispatch => {
dispatch(signInFacebookRequest());
@@ -144,9 +196,13 @@ export const fetchSignInFacebook = () => dispatch => {
);
};
// Sign Up Facebook
//==============================================================================
// SIGN UP - FACEBOOK
//==============================================================================
const signUpFacebookRequest = () => ({type: actions.FETCH_SIGNUP_FACEBOOK_REQUEST});
const signUpFacebookRequest = () => ({
type: actions.FETCH_SIGNUP_FACEBOOK_REQUEST
});
export const fetchSignUpFacebook = () => dispatch => {
dispatch(signUpFacebookRequest());
@@ -174,16 +230,22 @@ export const facebookCallback = (err, data) => dispatch => {
}
};
// Sign Up Actions
//==============================================================================
// SIGN UP
//==============================================================================
const signUpRequest = () => ({type: actions.FETCH_SIGNUP_REQUEST});
const signUpSuccess = user => ({type: actions.FETCH_SIGNUP_SUCCESS, user});
const signUpFailure = error => ({type: actions.FETCH_SIGNUP_FAILURE, error});
export const fetchSignUp = (formData, redirectUri) => (dispatch) => {
export const fetchSignUp = (formData, redirectUri) => dispatch => {
dispatch(signUpRequest());
coralApi('/users', {method: 'POST', body: formData, headers: {'X-Pym-Url': redirectUri}})
coralApi('/users', {
method: 'POST',
body: formData,
headers: {'X-Pym-Url': redirectUri}
})
.then(({user}) => {
dispatch(signUpSuccess(user));
})
@@ -198,52 +260,65 @@ export const fetchSignUp = (formData, redirectUri) => (dispatch) => {
});
};
// Forgot Password Actions
//==============================================================================
// FORGOT PASSWORD
//==============================================================================
const forgotPassowordRequest = () => ({type: actions.FETCH_FORGOT_PASSWORD_REQUEST});
const forgotPassowordSuccess = () => ({type: actions.FETCH_FORGOT_PASSWORD_SUCCESS});
const forgotPassowordFailure = () => ({type: actions.FETCH_FORGOT_PASSWORD_FAILURE});
const forgotPasswordRequest = () => ({
type: actions.FETCH_FORGOT_PASSWORD_REQUEST
});
export const fetchForgotPassword = email => (dispatch) => {
dispatch(forgotPassowordRequest(email));
const forgotPasswordSuccess = () => ({
type: actions.FETCH_FORGOT_PASSWORD_SUCCESS
});
const forgotPasswordFailure = () => ({
type: actions.FETCH_FORGOT_PASSWORD_FAILURE
});
export const fetchForgotPassword = email => dispatch => {
dispatch(forgotPasswordRequest(email));
const redirectUri = pym.parentUrl || location.href;
coralApi('/account/password/reset', {method: 'POST', body: {email, loc: redirectUri}})
.then(() => dispatch(forgotPassowordSuccess()))
.catch(error => dispatch(forgotPassowordFailure(error)));
coralApi('/account/password/reset', {
method: 'POST',
body: {email, loc: redirectUri}
})
.then(() => dispatch(forgotPasswordSuccess()))
.catch(error => dispatch(forgotPasswordFailure(error)));
};
// LogOut Actions
const logOutRequest = () => ({type: actions.LOGOUT_REQUEST});
const logOutSuccess = () => ({type: actions.LOGOUT_SUCCESS});
const logOutFailure = () => ({type: actions.LOGOUT_FAILURE});
//==============================================================================
// LOGOUT
//==============================================================================
export const logout = () => dispatch => {
dispatch(logOutRequest());
return coralApi('/auth', {method: 'DELETE'})
.then(() => {
dispatch(logOutSuccess());
dispatch({type: actions.LOGOUT});
Storage.removeItem('token');
fetchMe();
})
.catch(error => dispatch(logOutFailure(error)));
});
};
// LogOut Actions
export const validForm = () => ({type: actions.VALID_FORM});
export const invalidForm = error => ({type: actions.INVALID_FORM, error});
// Check Login
//==============================================================================
// CHECK LOGIN
//==============================================================================
const checkLoginRequest = () => ({type: actions.CHECK_LOGIN_REQUEST});
const checkLoginSuccess = (user, isAdmin) => ({type: actions.CHECK_LOGIN_SUCCESS, user, isAdmin});
const checkLoginFailure = error => ({type: actions.CHECK_LOGIN_FAILURE, error});
const checkLoginSuccess = (user, isAdmin) => ({
type: actions.CHECK_LOGIN_SUCCESS,
user,
isAdmin
});
export const checkLogin = () => dispatch => {
dispatch(checkLoginRequest());
coralApi('/auth')
.then((result) => {
.then(result => {
if (!result.user) {
Storage.removeItem('token');
throw new Error('Not logged in');
}
@@ -256,13 +331,32 @@ export const checkLogin = () => dispatch => {
});
};
const verifyEmailRequest = () => ({type: actions.VERIFY_EMAIL_REQUEST});
const verifyEmailSuccess = () => ({type: actions.VERIFY_EMAIL_SUCCESS});
const verifyEmailFailure = () => ({type: actions.VERIFY_EMAIL_FAILURE});
export const validForm = () => ({type: actions.VALID_FORM});
export const invalidForm = error => ({type: actions.INVALID_FORM, error});
//==============================================================================
// VERIFY EMAIL
//==============================================================================
const verifyEmailRequest = () => ({
type: actions.VERIFY_EMAIL_REQUEST
});
const verifyEmailSuccess = () => ({
type: actions.VERIFY_EMAIL_SUCCESS
});
const verifyEmailFailure = () => ({
type: actions.VERIFY_EMAIL_FAILURE
});
export const requestConfirmEmail = (email, redirectUri) => dispatch => {
dispatch(verifyEmailRequest());
return coralApi('/users/resend-verify', {method: 'POST', body: {email}, headers: {'X-Pym-Url': redirectUri}})
return coralApi('/users/resend-verify', {
method: 'POST',
body: {email},
headers: {'X-Pym-Url': redirectUri}
})
.then(() => {
dispatch(verifyEmailSuccess());
})
+1 -5
View File
@@ -33,9 +33,7 @@ export const FETCH_FORGOT_PASSWORD_REQUEST = 'FETCH_FORGOT_PASSWORD_REQUEST';
export const FETCH_FORGOT_PASSWORD_SUCCESS = 'FETCH_FORGOT_PASSWORD_SUCCESS';
export const FETCH_FORGOT_PASSWORD_FAILURE = 'FETCH_FORGOT_PASSWORD_FAILURE';
export const LOGOUT_REQUEST = 'LOGOUT_REQUEST';
export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS';
export const LOGOUT_FAILURE = 'LOGOUT_FAILURE';
export const LOGOUT = 'LOGOUT';
export const INVALID_FORM = 'INVALID_FORM';
export const VALID_FORM = 'VALID_FORM';
@@ -44,8 +42,6 @@ export const CHECK_LOGIN_REQUEST = 'CHECK_LOGIN_REQUEST';
export const CHECK_LOGIN_SUCCESS = 'CHECK_LOGIN_SUCCESS';
export const CHECK_LOGIN_FAILURE = 'CHECK_LOGIN_FAILURE';
export const CHECK_CSRF_TOKEN = 'CHECK_CSRF_TOKEN';
export const VERIFY_EMAIL_REQUEST = 'VERIFY_EMAIL_REQUEST';
export const VERIFY_EMAIL_SUCCESS = 'VERIFY_EMAIL_SUCCESS';
export const VERIFY_EMAIL_FAILURE = 'VERIFY_EMAIL_FAILURE';
+12 -19
View File
@@ -1,31 +1,22 @@
export const base = '/api/v1';
import * as Storage from './storage';
const buildOptions = (inputOptions = {}) => {
const csurfDOM = document.head.querySelector('[property=csrf]');
const defaultOptions = {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
Accept: 'application/json',
Authorization: `Bearer ${Storage.getItem('token')}`,
'Content-Type': 'application/json'
},
credentials: 'same-origin',
_csrf: csurfDOM ? csurfDOM.content : false
credentials: 'same-origin'
};
let options = Object.assign({}, defaultOptions, inputOptions);
options.headers = Object.assign({}, defaultOptions.headers, inputOptions.headers);
if (options._csrf) {
switch (options.method.toLowerCase()) {
case 'post':
case 'put':
case 'delete':
options.headers['x-csrf-token'] = options._csrf;
break;
}
}
options.headers = Object.assign(
{},
defaultOptions.headers,
inputOptions.headers
);
if (options.method.toLowerCase() !== 'get') {
options.body = JSON.stringify(options.body);
@@ -58,6 +49,8 @@ const handleResp = res => {
}
};
export const base = '/api/v1';
export default (url, options) => {
return fetch(`${base}${url}`, buildOptions(options)).then(handleResp);
};
+93
View File
@@ -0,0 +1,93 @@
let available, error;
function storageAvailable(type) {
let storage = window[type], x = '__storage_test__';
try {
storage.setItem(x, x);
storage.removeItem(x);
return true;
} catch (e) {
error = e;
return (
e instanceof DOMException &&
// everything except Firefox
(e.code === 22 ||
// Firefox
e.code === 1014 ||
// test name field too, because code might not be present
// everything except Firefox
e.name === 'QuotaExceededError' ||
// Firefox
e.name === 'NS_ERROR_DOM_QUOTA_REACHED') &&
// acknowledge QuotaExceededError only if there's something already stored
storage.length !== 0
);
}
}
function lazyCheckStorage() {
if (typeof available === 'undefined') {
available = storageAvailable('localStorage');
}
}
export function getItem(item = '') {
lazyCheckStorage();
if (available) {
return localStorage.getItem(item);
} else {
console.error(
`Cannot get from localStorage. localStorage is not available. ${error}`
);
}
}
export function setItem(item = '', value) {
lazyCheckStorage();
if (available) {
return localStorage.setItem(item, value);
} else {
console.error(
`Cannot set localStorage. localStorage is not available. ${error}`
);
}
}
export function removeItem(item = '') {
lazyCheckStorage();
if (available) {
return localStorage.removeItem(item);
} else {
console.error(
`Cannot remove item from localStorage. localStorage is not available. ${error}`
);
}
}
export function clear() {
lazyCheckStorage();
if (available) {
return localStorage.clear();
} else {
console.error(
`Cannot clear localStorage. localStorage is not available. ${error}`
);
}
}
// Enable this to debug WEB Storage events
// window.addEventListener('storage', function(e) {
// const msg = `${e.key} " was changed in page ${e.url} from ${e.oldValue} to ${e.newValue}`;
// console.log(msg);
// });
+1 -4
View File
@@ -64,9 +64,6 @@ export default function auth (state = initialState, action) {
.set('view', action.view);
case actions.CLEAN_STATE:
return initialState;
case actions.CHECK_CSRF_TOKEN:
return state
.set('_csrf', action._csrf);
case actions.FETCH_SIGNIN_REQUEST:
return state
.set('isLoading', true);
@@ -116,7 +113,7 @@ export default function auth (state = initialState, action) {
return state
.set('isLoading', false)
.set('successSignUp', true);
case actions.LOGOUT_SUCCESS:
case actions.LOGOUT:
return state
.set('user', null)
.set('isLoading', false)
+1 -3
View File
@@ -1,5 +1,5 @@
import ApolloClient, {addTypename} from 'apollo-client';
import getNetworkInterface from './transport';
import {networkInterface} from './transport';
// import {SubscriptionClient, addGraphQLSubscriptions} from 'subscriptions-transport-ws';
@@ -11,8 +11,6 @@ import getNetworkInterface from './transport';
// getNetworkInterface(),
// wsClient,
// );
const networkInterface = getNetworkInterface();
export const client = new ApolloClient({
connectToDevTools: true,
queryTransformer: addTypename,
+29 -9
View File
@@ -1,11 +1,31 @@
import {createNetworkInterface} from 'apollo-client';
import * as Storage from '../helpers/storage';
export default function getNetworkInterface(apiUrl = '/api/v1/graph/ql', headers = {}) {
return new createNetworkInterface({
uri: apiUrl,
opts: {
credentials: 'same-origin',
headers,
},
});
}
//==============================================================================
// NETWORK INTERFACE
//==============================================================================
const networkInterface = createNetworkInterface({
uri: '/api/v1/graph/ql',
opts: {
credentials: 'same-origin'
}
});
//==============================================================================
// MIDDLEWARES
//==============================================================================
networkInterface.use([{
applyMiddleware(req, next) {
if (!req.options.headers) {
req.options.headers = {}; // Create the header object if needed.
}
req.options.headers['authorization'] = `Bearer ${Storage.getItem('token')}`;
next();
}
}]);
export {
networkInterface
};
@@ -17,71 +17,68 @@ import translations from '../translations';
const lang = new I18n(translations);
class ProfileContainer extends Component {
constructor (props) {
super(props);
this.state = {
activeTab: 0,
};
constructor() {
super();
this.handleTabChange = this.handleTabChange.bind(this);
this.state = {
activeTab: 0
};
}
handleTabChange(tab) {
handleTabChange = tab => {
this.setState({
activeTab: tab
});
}
};
render() {
const {asset, data, showSignInDialog, myIgnoredUsersData, stopIgnoringUser} = this.props;
const {me} = this.props.data;
const {
auth,
data,
asset,
showSignInDialog,
stopIgnoringUser,
myIgnoredUsersData
} = this.props;
if (data.loading) {
return <Spinner/>;
}
const {me} = data;
if (!me) {
if (!auth.loggedIn) {
return <NotLoggedIn showSignInDialog={showSignInDialog} />;
}
const localProfile = this.props.user.profiles.find(p => p.provider === 'local');
if (data.loading) {
return <Spinner />;
}
const localProfile = this.props.user.profiles.find(
p => p.provider === 'local'
);
const emailAddress = localProfile && localProfile.id;
return (
<div>
<h2>{this.props.user.username}</h2>
{ emailAddress
? <p>{ emailAddress }</p>
: null
}
{emailAddress ? <p>{emailAddress}</p> : null}
{
myIgnoredUsersData.myIgnoredUsers && myIgnoredUsersData.myIgnoredUsers.length
? (
<div>
{myIgnoredUsersData.myIgnoredUsers &&
myIgnoredUsersData.myIgnoredUsers.length
? <div>
<h3>Ignored users</h3>
<IgnoredUsers
users={myIgnoredUsersData.myIgnoredUsers}
stopIgnoring={stopIgnoringUser}
/>
</div>
)
: null
}
: null}
<hr />
<h3>My comments</h3>
{
me.comments.length ?
<CommentHistory
comments={me.comments}
asset={asset}
link={link}
/>
:
<p>{lang.t('userNoComment')}</p>
}
{me.comments.length
? <CommentHistory comments={me.comments} asset={asset} link={link} />
: <p>{lang.t('userNoComment')}</p>}
</div>
);
}
@@ -89,21 +86,25 @@ class ProfileContainer extends Component {
// TODO: These currently relies on refetching (see ignoreUser and stopIgnoringUser mutations).
//
const withMyIgnoredUsersQuery = graphql(gql`
const withMyIgnoredUsersQuery = graphql(
gql`
query myIgnoredUsers {
myIgnoredUsers {
id,
username,
}
}`, {
}`,
{
props: ({data}) => {
return ({
return {
myIgnoredUsersData: data
});
};
}
});
}
);
const withMyCommentHistoryQuery = graphql(gql`
const withMyCommentHistoryQuery = graphql(
gql`
query myCommentHistory {
me {
comments {
@@ -117,7 +118,8 @@ const withMyCommentHistoryQuery = graphql(gql`
created_at
}
}
}`);
}`
);
const mapStateToProps = state => ({
user: state.user.toJS(),
@@ -132,5 +134,5 @@ export default compose(
connect(mapStateToProps, mapDispatchToProps),
withMyCommentHistoryQuery,
withMyIgnoredUsersQuery,
withStopIgnoringUser,
withStopIgnoringUser
)(ProfileContainer);
+141
View File
@@ -0,0 +1,141 @@
// This file serves as the entrypoint to all configuration loaded by the
// application. All defaults are assumed here, validation should also be
// completed here.
// Perform rewrites to the runtime environment variables based on the contents
// of the process.env.REWRITE_ENV if it exists. This is done here as it is the
// entrypoint for the entire applications configuration.
require('env-rewrite').rewrite();
//==============================================================================
// CONFIG INITIALIZATION
//==============================================================================
const CONFIG = {
//------------------------------------------------------------------------------
// JWT based configuration
//------------------------------------------------------------------------------
// JWT_SECRET is the secret used to sign and verify tokens issued by this
// application.
JWT_SECRET: process.env.TALK_JWT_SECRET || null,
// JWT_AUDIENCE is the value for the audience claim for the tokens that will be
// verified when decoding. If `JWT_AUDIENCE` is not in the environment, then it
// will default to `talk`.
JWT_AUDIENCE: process.env.TALK_JWT_AUDIENCE || 'talk',
// JWT_ISSUER is the value for the issuer for the tokens that will be verified
// when decoding. If `JWT_ISSUER` is not in the environment, then it will try
// `TALK_ROOT_URL`, otherwise, it will be undefined.
JWT_ISSUER: process.env.TALK_JWT_ISSUER || process.env.TALK_ROOT_URL || undefined,
// JWT_EXPIRY is the time for which a given token is valid for.
JWT_EXPIRY: process.env.TALK_JWT_EXPIRY || '1 day',
//------------------------------------------------------------------------------
// Installation locks
//------------------------------------------------------------------------------
INSTALL_LOCK: process.env.TALK_INSTALL_LOCK === 'TRUE',
//------------------------------------------------------------------------------
// External database url's
//------------------------------------------------------------------------------
MONGO_URL: process.env.TALK_MONGO_URL,
REDIS_URL: process.env.TALK_REDIS_URL,
//------------------------------------------------------------------------------
// Server Config
//------------------------------------------------------------------------------
// Port to bind to.
PORT: process.env.TALK_PORT || '3000',
// The URL for this Talk Instance as viewable from the outside.
ROOT_URL: process.env.TALK_ROOT_URL,
//------------------------------------------------------------------------------
// Recaptcha configuration
//------------------------------------------------------------------------------
RECAPTCHA_ENABLED: false, // updated below
RECAPTCHA_PUBLIC: process.env.TALK_RECAPTCHA_PUBLIC,
RECAPTCHA_SECRET: process.env.TALK_RECAPTCHA_SECRET,
//------------------------------------------------------------------------------
// SMTP Server configuration
//------------------------------------------------------------------------------
SMTP_FROM_ADDRESS: process.env.TALK_SMTP_FROM_ADDRESS,
SMTP_HOST: process.env.TALK_SMTP_HOST,
SMTP_PASSWORD: process.env.TALK_SMTP_PASSWORD,
SMTP_PORT: process.env.TALK_SMTP_PORT,
SMTP_USERNAME: process.env.TALK_SMTP_USERNAME
};
//==============================================================================
// CONFIG VALIDATION
//==============================================================================
//------------------------------------------------------------------------------
// JWT based configuration
//------------------------------------------------------------------------------
if (process.env.NODE_ENV === 'test' && !CONFIG.JWT_SECRET) {
CONFIG.JWT_SECRET = 'keyboard cat';
} else if (!CONFIG.JWT_SECRET) {
throw new Error('TALK_JWT_SECRET must be provided in the environment to sign/verify tokens');
}
//------------------------------------------------------------------------------
// External database url's
//------------------------------------------------------------------------------
// Reset the mongo url in the event it hasn't been overrided and we are in a
// testing environment. Every new mongo instance comes with a test database by
// default, this is consistent with common testing and use case practices.
if (process.env.NODE_ENV === 'test' && !CONFIG.MONGO_URL) {
CONFIG.MONGO_URL = 'mongodb://localhost/test';
}
// Reset the redis url in the event it hasn't been overrided and we are in a
// testing environment.
if (process.env.NODE_ENV === 'test' && !CONFIG.REDIS_URL) {
CONFIG.REDIS_URL = 'redis://localhost';
}
//------------------------------------------------------------------------------
// Recaptcha configuration
//------------------------------------------------------------------------------
/**
* This is true when the recaptcha secret is provided and the Recaptcha feature
* is to be enabled.
*/
CONFIG.RECAPTCHA_ENABLED = CONFIG.RECAPTCHA_SECRET && CONFIG.RECAPTCHA_SECRET.length > 0 &&
CONFIG.RECAPTCHA_PUBLIC && CONFIG.RECAPTCHA_PUBLIC.length > 0;
if (!CONFIG.RECAPTCHA_ENABLED) {
console.warn('Recaptcha is not enabled for login/signup abuse prevention, set TALK_RECAPTCHA_SECRET and TALK_RECAPTCHA_PUBLIC to enable Recaptcha.');
}
//------------------------------------------------------------------------------
// SMTP Server configuration
//------------------------------------------------------------------------------
{
const requiredProps = [
'SMTP_FROM_ADDRESS',
'SMTP_USERNAME',
'SMTP_PASSWORD',
'SMTP_HOST'
];
if (requiredProps.some((prop) => !CONFIG[prop])) {
console.warn(`${requiredProps.map((v) => `TALK_${v}`).join(', ')} should be defined in the environment if you would like to send password reset emails from Talk`);
}
}
module.exports = CONFIG;
+19
View File
@@ -0,0 +1,19 @@
const {passport} = require('../services/passport');
const authentication = (req, res, next) => passport.authenticate('jwt', {
session: false
}, (err, user) => {
if (err) {
return next(err);
}
if (user) {
// Attach the user to the request object, now that we know it exists.
req.user = user;
}
next();
})(req, res, next);
module.exports = authentication;
+3 -3
View File
@@ -79,6 +79,7 @@
"inquirer": "^3.0.6",
"joi": "^10.4.1",
"jsonwebtoken": "^7.3.0",
"jwt-decode": "^2.2.0",
"kue": "^0.11.5",
"linkify-it": "^2.0.3",
"lodash": "^4.16.6",
@@ -93,18 +94,17 @@
"nodemailer": "^2.6.4",
"parse-duration": "^0.1.1",
"passport": "^0.3.2",
"passport-jwt": "^2.2.1",
"passport-local": "^1.0.0",
"prop-types": "^15.5.8",
"react-apollo": "^1.1.0",
"react-recaptcha": "^2.2.6",
"recompose": "^0.23.1",
"redis": "^2.7.1",
"uuid": "^3.0.1",
"simplemde": "^1.11.2",
"subscriptions-transport-ws": "^0.5.5-alpha.0",
"resolve": "^1.3.2",
"semver": "^5.3.0",
"simplemde": "^1.11.2",
"subscriptions-transport-ws": "^0.5.5-alpha.0",
"timekeeper": "^1.0.0",
"uuid": "^3.0.1"
},
+3 -1
View File
@@ -5,6 +5,8 @@ const debug = require('debug')('talk:plugins');
const Joi = require('joi');
const amp = require('app-module-path');
const PLUGINS_JSON = process.env.TALK_PLUGINS_JSON;
// Add the current path to the module root.
amp.addPath(__dirname);
@@ -18,7 +20,7 @@ try {
let customPlugins = path.join(__dirname, 'plugins.json');
let defaultPlugins = path.join(__dirname, 'plugins.default.json');
if (process.env.TALK_PLUGINS_JSON && process.env.TALK_PLUGINS_JSON.length > 0) {
if (PLUGINS_JSON && PLUGINS_JSON.length > 0) {
debug('Now using TALK_PLUGINS_JSON environment variable for plugins');
plugins = require(envPlugins);
} else if (fs.existsSync(customPlugins)) {
@@ -15,6 +15,6 @@ module.exports = (router) => {
router.get('/api/v1/auth/facebook/callback', (req, res, next) => {
// Perform the facebook login flow and pass the data back through the opener.
passport.authenticate('facebook', HandleAuthPopupCallback(req, res, next))(req, res, next);
passport.authenticate('facebook', {session: false}, HandleAuthPopupCallback(req, res, next))(req, res, next);
});
};
+4 -1
View File
@@ -1,5 +1,8 @@
const express = require('express');
const router = express.Router();
const {
RECAPTCHA_PUBLIC
} = require('../../config');
// Get /email-confirmation expects a signed JWT in the hash
router.get('/confirm-email', (req, res) => {
@@ -17,7 +20,7 @@ router.get('/password-reset', (req, res) => {
router.get('*', (req, res) => {
const data = {
TALK_RECAPTCHA_PUBLIC: process.env.TALK_RECAPTCHA_PUBLIC
TALK_RECAPTCHA_PUBLIC: RECAPTCHA_PUBLIC
};
res.render('admin', {basePath: '/client/coral-admin', data});
+4 -1
View File
@@ -4,6 +4,9 @@ const UsersService = require('../../../services/users');
const mailer = require('../../../services/mailer');
const authorization = require('../../../middleware/authorization');
const errors = require('../../../errors');
const {
ROOT_URL
} = require('../../../config');
//==============================================================================
// ROUTES
@@ -62,7 +65,7 @@ router.post('/password/reset', (req, res, next) => {
template: 'password-reset', // needed to know which template to render!
locals: { // specifies the template locals.
token,
rootURL: process.env.TALK_ROOT_URL
rootURL: ROOT_URL
},
subject: 'Password Reset',
to: email
+4 -9
View File
@@ -1,6 +1,5 @@
const express = require('express');
const {passport, HandleAuthCallback} = require('../../../services/passport');
const authorization = require('../../../middleware/authorization');
const {passport, HandleGenerateCredentials, HandleLogout} = require('../../../services/passport');
const router = express.Router();
@@ -21,13 +20,9 @@ router.get('/', (req, res, next) => {
});
/**
* This destroys the session of a user, if they have one.
* This blacklists the token used to authenticate.
*/
router.delete('/', authorization.needed(), (req, res) => {
delete req.session.passport;
res.status(204).end();
});
router.delete('/', HandleLogout);
//==============================================================================
// PASSPORT ROUTES
@@ -39,7 +34,7 @@ router.delete('/', authorization.needed(), (req, res) => {
router.post('/local', (req, res, next) => {
// Perform the local authentication.
passport.authenticate('local', HandleAuthCallback(req, res, next))(req, res, next);
passport.authenticate('local', {session: false}, HandleGenerateCredentials(req, res, next))(req, res, next);
});
module.exports = router;
+4 -1
View File
@@ -5,6 +5,9 @@ const CommentsService = require('../../../services/comments');
const mailer = require('../../../services/mailer');
const errors = require('../../../errors');
const authorization = require('../../../middleware/authorization');
const {
ROOT_URL
} = require('../../../config');
// get a list of users.
router.get('/', authorization.needed('ADMIN'), (req, res, next) => {
@@ -108,7 +111,7 @@ const SendEmailConfirmation = (app, userID, email, referer) => UsersService
template: 'email-confirm', // needed to know which template to render!
locals: { // specifies the template locals.
token,
rootURL: process.env.TALK_ROOT_URL,
rootURL: ROOT_URL,
email
},
subject: 'Email Confirmation',
+4 -1
View File
@@ -1,6 +1,9 @@
const express = require('express');
const router = express.Router();
const SettingsService = require('../../services/settings');
const {
RECAPTCHA_PUBLIC
} = require('../../config');
router.use('/:embed', (req, res, next) => {
switch (req.params.embed) {
@@ -8,7 +11,7 @@ router.use('/:embed', (req, res, next) => {
return SettingsService.retrieve()
.then(({customCssUrl}) => {
const data = {
TALK_RECAPTCHA_PUBLIC: process.env.TALK_RECAPTCHA_PUBLIC
TALK_RECAPTCHA_PUBLIC: RECAPTCHA_PUBLIC
};
return res.render('embed/stream', {customCssUrl, data});
+13 -16
View File
@@ -5,16 +5,13 @@ const path = require('path');
const fs = require('fs');
const _ = require('lodash');
const smtpRequiredProps = [
'TALK_SMTP_FROM_ADDRESS',
'TALK_SMTP_USERNAME',
'TALK_SMTP_PASSWORD',
'TALK_SMTP_HOST'
];
if (smtpRequiredProps.some(prop => !process.env[prop])) {
console.error(`${smtpRequiredProps.join(', ')} should be defined in the environment if you would like to send password reset emails from Talk`);
}
const {
SMTP_HOST,
SMTP_USERNAME,
SMTP_PORT,
SMTP_PASSWORD,
SMTP_FROM_ADDRESS
} = require('../config');
// load all the templates as strings
const templates = {
@@ -56,15 +53,15 @@ templates.render = (name, format = 'txt', context) => new Promise((resolve, reje
});
const options = {
host: process.env.TALK_SMTP_HOST,
host: SMTP_HOST,
auth: {
user: process.env.TALK_SMTP_USERNAME,
pass: process.env.TALK_SMTP_PASSWORD
user: SMTP_USERNAME,
pass: SMTP_PASSWORD
}
};
if (process.env.TALK_SMTP_PORT) {
options.port = process.env.TALK_SMTP_PORT;
if (SMTP_PORT) {
options.port = SMTP_PORT;
} else {
options.port = 25;
}
@@ -126,7 +123,7 @@ const mailer = module.exports = {
debug(`Starting to send mail for Job[${id}]`);
// Set the `from` field.
data.message.from = process.env.TALK_SMTP_FROM_ADDRESS;
data.message.from = SMTP_FROM_ADDRESS;
// Actually send the email.
defaultTransporter.sendMail(data.message, (err) => {
+6 -13
View File
@@ -1,7 +1,12 @@
const mongoose = require('mongoose');
const debug = require('debug')('talk:db');
const enabled = require('debug').enabled;
const queryDebuger = require('debug')('talk:db:query');
const {
MONGO_URL
} = require('../config');
// Loading the formatter from Mongoose:
//
// https://github.com/Automattic/mongoose/blob/1a93d1f4d12e441e17ddf451e96fbc5f6e8f54b8/lib/drivers/node-mongodb-native/collection.js#L182
@@ -24,18 +29,6 @@ function debugQuery(name, i, ...args) {
queryDebuger(functionCall + params);
}
const enabled = require('debug').enabled;
// Pull the mongo url out of the environment.
let url = process.env.TALK_MONGO_URL;
// Reset the mongo url in the event it hasn't been overrided and we are in a
// testing environment. Every new mongo instance comes with a test database by
// default, this is consistent with common testing and use case practices.
if (process.env.NODE_ENV === 'test' && !url) {
url = 'mongodb://localhost/test';
}
// Use native promises
mongoose.Promise = global.Promise;
@@ -48,7 +41,7 @@ if (enabled('talk:db')) {
}
// Connect to the Mongo instance.
mongoose.connect(url, (err) => {
mongoose.connect(MONGO_URL, (err) => {
if (err) {
throw err;
}
+125 -56
View File
@@ -3,33 +3,39 @@ const UsersService = require('./users');
const SettingsService = require('./settings');
const fetch = require('node-fetch');
const FormData = require('form-data');
const JWT = require('jsonwebtoken');
const LocalStrategy = require('passport-local').Strategy;
const errors = require('../errors');
const uuid = require('uuid');
const debug = require('debug')('talk:passport');
const {createClient} = require('./redis');
//==============================================================================
// SESSION SERIALIZATION
//==============================================================================
// Create a redis client to use for authentication.
const client = createClient();
passport.serializeUser((user, done) => {
done(null, user.id);
const {
JWT_SECRET,
JWT_ISSUER,
JWT_EXPIRY,
JWT_AUDIENCE,
RECAPTCHA_SECRET,
RECAPTCHA_ENABLED
} = require('../config');
// GenerateToken will sign a token to include all the authorization information
// needed for the front end.
const GenerateToken = (user) => JWT.sign({}, JWT_SECRET, {
jwtid: uuid.v4(),
expiresIn: JWT_EXPIRY,
issuer: JWT_ISSUER,
subject: user.id,
audience: JWT_AUDIENCE
});
passport.deserializeUser((id, done) => {
UsersService
.findById(id)
.then((user) => {
done(null, user);
})
.catch((err) => {
done(err);
});
});
/**
* This sends back the user data as JSON.
*/
const HandleAuthCallback = (req, res, next) => (err, user) => {
// HandleGenerateCredentials validates that an authentication scheme did indeed
// return a user, if it did, then sign and return the user and token to be used
// by the frontend to display and update the UI.
const HandleGenerateCredentials = (req, res, next) => (err, user) => {
if (err) {
return next(err);
}
@@ -38,15 +44,11 @@ const HandleAuthCallback = (req, res, next) => (err, user) => {
return next(errors.ErrNotAuthorized);
}
// Perform the login of the user!
req.logIn(user, (err) => {
if (err) {
return next(err);
}
// Generate the token to re-issue to the frontend.
const token = GenerateToken(user);
// We logged in the user! Let's send back the user data and the CSRF token.
res.json({user});
});
// Send back the details!
res.json({user, token});
};
/**
@@ -54,22 +56,18 @@ const HandleAuthCallback = (req, res, next) => (err, user) => {
*/
const HandleAuthPopupCallback = (req, res, next) => (err, user) => {
if (err) {
return res.render('auth-callback', {err: JSON.stringify(err), data: null});
return res.render('auth-callback', {auth: JSON.stringify({err, data: null})});
}
if (!user) {
return res.render('auth-callback', {err: JSON.stringify(errors.ErrNotAuthorized), data: null});
return res.render('auth-callback', {auth: JSON.stringify({err: errors.ErrNotAuthorized, data: null})});
}
// Perform the login of the user!
req.logIn(user, (err) => {
if (err) {
return res.render('auth-callback', {err: JSON.stringify(err), data: null});
}
// Generate the token to re-issue to the frontend.
const token = GenerateToken(user);
// We logged in the user! Let's send back the user data.
res.render('auth-callback', {err: null, data: JSON.stringify(user)});
});
// We logged in the user! Let's send back the user data.
res.render('auth-callback', {auth: JSON.stringify({err: null, data: {user, token}})});
};
/**
@@ -119,7 +117,91 @@ function ValidateUserLogin(loginProfile, user, done) {
}
//==============================================================================
// STRATEGIES
// JWT STRATEGY
//==============================================================================
/**
* Revoke the token on the request.
*/
const HandleLogout = (req, res, next) => {
const {jwt} = req;
const now = new Date();
const expiry = (jwt.exp - now.getTime() / 1000).toFixed(0);
client.set(`jtir[${jwt.jti}]`, now.toISOString(), 'EX', expiry, (err) => {
if (err) {
return next(err);
}
res.status(204).end();
});
};
/**
* Check if the given token is already blacklisted, throw an error if it is.
*/
const CheckBlacklisted = (jwt) => new Promise((resolve, reject) => {
client.get(`jtir[${jwt.jti}]`, (err, expiry) => {
if (err) {
return reject(err);
}
if (expiry != null) {
return reject(new errors.ErrAuthentication('token was revoked'));
}
return resolve();
});
});
const JwtStrategy = require('passport-jwt').Strategy;
const ExtractJwt = require('passport-jwt').ExtractJwt;
// Extract the JWT from the 'Authorization' header with the 'Bearer' scheme.
passport.use(new JwtStrategy({
// Prepare the extractor from the header.
jwtFromRequest: ExtractJwt.fromAuthHeaderWithScheme('Bearer'),
// Use the secret passed in which is loaded from the environment. This can be
// a certificate (loaded) or a HMAC key.
secretOrKey: JWT_SECRET,
// Verify the issuer.
issuer: JWT_ISSUER,
// Verify the audience.
audience: JWT_AUDIENCE,
// Enable only the HS256 algorithm.
algorithms: ['HS256'],
// Pass the request objecto back to the callback so we can attach the JWT to
// it.
passReqToCallback: true
}, async (req, jwt, done) => {
// Load the user from the environment, because we just got a user from the
// header.
try {
// Check to see if the token has been revoked
await CheckBlacklisted(jwt);
let user = await UsersService.findById(jwt.sub);
// Attach the JWT to the request.
req.jwt = jwt;
return done(null, user);
} catch(e) {
return done(e);
}
}));
//==============================================================================
// LOCAL STRATEGY
//==============================================================================
/**
@@ -157,21 +239,6 @@ const CheckIfNeedsRecaptcha = (user, email) => {
return false;
};
/**
* This stores the Recaptcha secret.
*/
const RECAPTCHA_SECRET = process.env.TALK_RECAPTCHA_SECRET;
const RECAPTCHA_PUBLIC = process.env.TALK_RECAPTCHA_PUBLIC;
/**
* This is true when the recaptcha secret is provided and the Recaptcha feature
* is to be enabled.
*/
const RECAPTCHA_ENABLED = RECAPTCHA_SECRET && RECAPTCHA_SECRET.length > 0 && RECAPTCHA_PUBLIC && RECAPTCHA_PUBLIC.length > 0;
if (!RECAPTCHA_ENABLED) {
console.log('Recaptcha is not enabled for login/signup abuse prevention, set TALK_RECAPTCHA_SECRET and TALK_RECAPTCHA_PUBLIC to enable Recaptcha.');
}
/**
* This sends the request details down Google to check to see if the response is
* genuine or not.
@@ -356,6 +423,8 @@ module.exports = {
passport,
ValidateUserLogin,
HandleFailedAttempt,
HandleAuthCallback,
HandleAuthPopupCallback
HandleAuthPopupCallback,
HandleGenerateCredentials,
HandleLogout,
CheckBlacklisted
};
+4 -2
View File
@@ -1,9 +1,11 @@
const redis = require('redis');
const debug = require('debug')('talk:redis');
const url = process.env.TALK_REDIS_URL || 'redis://localhost';
const {
REDIS_URL
} = require('../config');
const connectionOptions = {
url,
url: REDIS_URL,
retry_strategy: function(options) {
if (options.error && options.error.code === 'ECONNREFUSED') {
-36
View File
@@ -1,36 +0,0 @@
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
const redis = require('./redis');
//==============================================================================
// SESSION MIDDLEWARE
//==============================================================================
const session_opts = {
secret: process.env.TALK_SESSION_SECRET,
httpOnly: true,
rolling: true,
saveUninitialized: true,
resave: true,
unset: 'destroy',
name: 'talk.sid',
cookie: {
secure: false,
maxAge: 8.64e+7, // 24 hours for session token expiry
},
store: new RedisStore({
client: redis.createClient(),
})
};
if (process.env.NODE_ENV === 'production') {
// Enable the secure cookie when we are in production mode.
session_opts.cookie.secure = true;
} else if (process.env.NODE_ENV === 'test') {
// Add in the secret during tests.
session_opts.secret = 'keyboard cat';
}
module.exports = session(session_opts);
+4 -1
View File
@@ -2,6 +2,9 @@ const UsersService = require('./users');
const SettingsService = require('./settings');
const SettingsModel = require('../models/setting');
const errors = require('../errors');
const {
INSTALL_LOCK
} = require('../config');
/**
* This service is used when we want to setup the application. It is consumed by
@@ -15,7 +18,7 @@ module.exports = class SetupService {
static isAvailable() {
// Check if we have an install lock present.
if (process.env.TALK_INSTALL_LOCK === 'TRUE') {
if (INSTALL_LOCK) {
return Promise.reject(errors.ErrInstallLock);
}
+8 -2
View File
@@ -1,12 +1,18 @@
const session = require('./session');
const passport = require('./passport');
const authentication = require('../middleware/authentication');
// Session data does not automatically attach to websocket req objects.
// This middleware code looks for a user in the session and, if it exists,
// attaches it to the graph req.
const deserializeUser = (req) => {
return new Promise((resolve, reject) => {
session(req, {}, () => {
// This uses the authentication connect middleware to establish the session
// user details from the headers.
authentication(req, null, (err) => {
if (err) {
return reject(err);
}
if ('session' in req && 'passport' in req.session && 'user' in req.session.passport) {
passport.deserializeUser(req.session.passport.user, (err, user) => {
+9 -15
View File
@@ -1,12 +1,14 @@
const assert = require('assert');
const uuid = require('uuid');
const bcrypt = require('bcrypt');
const url = require('url');
const jwt = require('jsonwebtoken');
const Wordlist = require('./wordlist');
const errors = require('../errors');
const uuid = require('uuid');
const {
JWT_SECRET,
ROOT_URL
} = require('../config');
const redis = require('./redis');
const redisClient = redis.createClient();
@@ -22,14 +24,6 @@ const SettingsService = require('./settings');
const ActionsService = require('./actions');
const MailerService = require('./mailer');
// 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');
}
const EMAIL_CONFIRM_JWT_SUBJECT = 'email_confirm';
const PASSWORD_RESET_JWT_SUBJECT = 'password_reset';
@@ -564,7 +558,7 @@ module.exports = class UsersService {
version: user.__v
};
return jwt.sign(payload, process.env.TALK_SESSION_SECRET, {
return jwt.sign(payload, JWT_SECRET, {
algorithm: 'HS256',
expiresIn: '1d',
subject: PASSWORD_RESET_JWT_SUBJECT
@@ -583,7 +577,7 @@ module.exports = class UsersService {
// Set the allowed algorithms.
options.algorithms = ['HS256'];
jwt.verify(token, process.env.TALK_SESSION_SECRET, options, (err, decoded) => {
jwt.verify(token, JWT_SECRET, options, (err, decoded) => {
if (err) {
return reject(err);
}
@@ -697,7 +691,7 @@ module.exports = class UsersService {
* @param {String} email The email that we are needing to get confirmed.
* @return {Promise}
*/
static createEmailConfirmToken(userID = null, email, referer = process.env.TALK_ROOT_URL) {
static createEmailConfirmToken(userID = null, email, referer = ROOT_URL) {
if (!email || typeof email !== 'string') {
return Promise.reject('email is required when creating a JWT for resetting passord');
}
@@ -740,7 +734,7 @@ module.exports = class UsersService {
email,
referer,
userID: user.id
}, process.env.TALK_SESSION_SECRET, tokenOptions);
}, JWT_SECRET, tokenOptions);
});
}
-1
View File
@@ -3,7 +3,6 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1, maximum-scale=1">
<meta property="csrf" content="<%= csrfToken %>">
<title>Talk - Coral Admin</title>
<link rel="apple-touch-icon" sizes="57x57" href="/public/img/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="/public/img/apple-icon-60x60.png">
-3
View File
@@ -74,9 +74,6 @@
url: '/api/v1/account/email/verify',
contentType: 'application/json',
method: 'POST',
headers: {
'X-CSRF-Token': '<%= csrfToken %>'
},
data: JSON.stringify({token: location.hash.replace('#', '')})
}).then(function (success) {
location.href = success.redirectUri;
-4
View File
@@ -82,7 +82,6 @@
<body>
<div id="root">
<form id="reset-password-form">
<input type="hidden" name="_csrf" value={{csrfToken}}/>
<legend class="legend">Set new password</legend>
<label for="password">
New password
@@ -121,9 +120,6 @@
url: '/api/v1/account/password/reset',
contentType: 'application/json',
method: 'PUT',
headers: {
'X-CSRF-Token': '<%= csrfToken %>'
},
data: JSON.stringify({password: password, token: location.hash.replace('#', '')})
}).then(function (success) {
location.href = success.redirect;
+4 -1
View File
@@ -2,7 +2,10 @@
<html>
<body>
<script type="text/javascript">
window.opener.authCallback(<% if (err) { %>'<%- err %>'<% } else { %>null<% } %>, '<%- data %>');
<%/* set the auth data in localStorage, this will ensure that only
javascript on the same domain can access the data, they can listen
for updates by attaching to localStorage event changes */%>
localStorage.setItem('auth', <%- auth %>);
setTimeout(function() { window.close(); }, 50);
</script>
</body>
-1
View File
@@ -3,7 +3,6 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no">
<meta property="csrf" content="<%= csrfToken %>">
<link rel="stylesheet" type="text/css" href="/client/embed/stream/default.css">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
+402 -390
View File
File diff suppressed because it is too large Load Diff