mirror of
https://github.com/wassname/talk.git
synced 2026-06-28 23:10:43 +08:00
@@ -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.
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
/**
|
||||
|
||||
@@ -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.
|
||||
//==============================================================================
|
||||
|
||||
@@ -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)));
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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());
|
||||
})
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
// });
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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') {
|
||||
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user