mirror of
https://github.com/wassname/talk.git
synced 2026-07-08 03:15:09 +08:00
Merge branch 'master' of https://github.com/coralproject/talk into notification-room
This commit is contained in:
+1
-1
@@ -1,7 +1,7 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = tab
|
||||
indent_style = space
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
@@ -5,6 +5,7 @@ RUN mkdir -p /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Setup the environment
|
||||
ENV NODE_ENV production
|
||||
ENV PATH /usr/src/app/bin:$PATH
|
||||
ENV TALK_PORT 5000
|
||||
EXPOSE 5000
|
||||
|
||||
@@ -14,6 +14,19 @@ Run it once to install the dependencies.
|
||||
`npm start`
|
||||
Runs Talk.
|
||||
|
||||
### Configuration
|
||||
|
||||
The Talk application requires specific configuration options to be available
|
||||
inside the environment in order to run, those variables are listed here:
|
||||
|
||||
- `TALK_SESSION_SECRET` (*required*) - a random string which will be used to
|
||||
secure cookies
|
||||
- `TALK_FACEBOOK_APP_ID` (*required*) - the Facebook app id for your Facebook
|
||||
Login enabled app.
|
||||
- `TALK_FACEBOOK_APP_SECRET` (*required*) - the Facebook app secret for your
|
||||
Facebook Login enabled app.
|
||||
- `TALK_ROOT_URL` (*required*) - Root url of the installed application externally available in the format: `<scheme>://<host>` without the path.
|
||||
|
||||
### Running with Docker
|
||||
Make sure you have Docker running first and then run `docker-compose up -d`
|
||||
|
||||
|
||||
@@ -2,6 +2,11 @@ const express = require('express');
|
||||
const bodyParser = require('body-parser');
|
||||
const morgan = require('morgan');
|
||||
const path = require('path');
|
||||
const helmet = require('helmet');
|
||||
const passport = require('./passport');
|
||||
const session = require('express-session');
|
||||
const RedisStore = require('connect-redis')(session);
|
||||
const redis = require('./redis');
|
||||
|
||||
const app = express();
|
||||
|
||||
@@ -12,12 +17,62 @@ if (app.get('env') !== 'test') {
|
||||
app.use(morgan('dev'));
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// APP MIDDLEWARE
|
||||
//==============================================================================
|
||||
|
||||
app.set('trust proxy', 'loopback');
|
||||
app.use(helmet());
|
||||
app.use(bodyParser.json());
|
||||
app.use('/client', express.static(path.join(__dirname, 'dist')));
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
app.set('view engine', 'ejs');
|
||||
|
||||
// Routes.
|
||||
app.use('/client', express.static(path.join(__dirname, 'dist')));
|
||||
//==============================================================================
|
||||
// SESSION MIDDLEWARE
|
||||
//==============================================================================
|
||||
|
||||
const session_opts = {
|
||||
secret: process.env.TALK_SESSION_SECRET,
|
||||
httpOnly: true,
|
||||
rolling: true,
|
||||
saveUninitialized: false,
|
||||
resave: false,
|
||||
name: 'talk.sid',
|
||||
cookie: {
|
||||
secure: false,
|
||||
maxAge: 18000000, // 30 minutes for expiry.
|
||||
},
|
||||
store: new RedisStore({
|
||||
ttl: 1800,
|
||||
client: redis,
|
||||
})
|
||||
};
|
||||
|
||||
if (app.get('env') === 'production') {
|
||||
|
||||
// Enable the secure cookie when we are in production mode.
|
||||
session_opts.cookie.secure = true;
|
||||
} else if (app.get('env') === 'test') {
|
||||
|
||||
// Add in the secret during tests.
|
||||
session_opts.secret = 'keyboard cat';
|
||||
}
|
||||
|
||||
app.use(session(session_opts));
|
||||
|
||||
//==============================================================================
|
||||
// PASSPORT MIDDLEWARE
|
||||
//==============================================================================
|
||||
|
||||
// Setup the PassportJS Middleware.
|
||||
app.use(passport.initialize());
|
||||
app.use(passport.session());
|
||||
|
||||
//==============================================================================
|
||||
// ROUTES
|
||||
//==============================================================================
|
||||
|
||||
app.use('/', require('./routes'));
|
||||
|
||||
//==============================================================================
|
||||
@@ -34,37 +89,27 @@ app.use((req, res, next) => {
|
||||
|
||||
// General error handler. Respond with the message and error if we have it while
|
||||
// returning a status code that makes sense.
|
||||
if (app.get('env') === 'development') {
|
||||
app.use('/api', (err, req, res, next) => {
|
||||
res.status(err.status || 500);
|
||||
res.json({
|
||||
message: err.message,
|
||||
error: err
|
||||
});
|
||||
});
|
||||
|
||||
app.use('/', (err, req, res, next) => {
|
||||
res.status(err.status || 500);
|
||||
res.render('error', {
|
||||
message: err.message,
|
||||
error: err
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
app.use('/api', (err, req, res, next) => {
|
||||
if (err !== ErrNotFound) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
res.status(err.status || 500);
|
||||
res.json({
|
||||
message: err.message,
|
||||
error: {}
|
||||
error: app.get('env') === 'development' ? err : null
|
||||
});
|
||||
});
|
||||
|
||||
app.use('/', (err, req, res, next) => {
|
||||
if (err !== ErrNotFound) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
res.status(err.status || 500);
|
||||
res.render('error', {
|
||||
message: err.message,
|
||||
error: {}
|
||||
error: app.get('env') === 'development' ? err : null
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+1
-1
@@ -206,7 +206,7 @@ function listUsers() {
|
||||
const mongoose = require('../mongoose');
|
||||
|
||||
User
|
||||
.find()
|
||||
.all()
|
||||
.then((users) => {
|
||||
let table = new Table({
|
||||
head: [
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
const redis = require('./redis');
|
||||
|
||||
const cache = module.exports = {};
|
||||
|
||||
/**
|
||||
* This collects a key that may either be an array or a string and creates a
|
||||
* unified key out of it.
|
||||
* @param {Mixed} key Either an array of items composing a key or a string
|
||||
* @return {String} A string that represents a key
|
||||
*/
|
||||
const keyfunc = (key) => {
|
||||
if (Array.isArray(key)) {
|
||||
return `cache[${key.join(':')}]`;
|
||||
}
|
||||
|
||||
return `cache[${key}]`;
|
||||
};
|
||||
|
||||
/**
|
||||
* This wraps a complicated function with a cache, in the event that the item is
|
||||
* not inside the cache, it will perform the work to get it and then set it
|
||||
* followed by returning the value.
|
||||
* @param {Mixed} key Either an array of items or string represening this
|
||||
* work
|
||||
* @param {Integer} expiry Time in seconds for the cache entry to live for
|
||||
* @param {Function} work A function that returns a promise that can be
|
||||
* resolved as the value to cache.
|
||||
* @return {Promise} Resolves to the value either retrieved from cache
|
||||
*/
|
||||
cache.wrap = (key, expiry, work) => {
|
||||
return cache
|
||||
.get(key)
|
||||
.then((value) => {
|
||||
if (value !== null) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return work()
|
||||
.then((value) => {
|
||||
return cache
|
||||
.set(key, value, expiry)
|
||||
.then(() => value);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* This returns a promise that returns a promise that resolves with the value
|
||||
* from the cache or null if it does not exist in the cache.
|
||||
* @param {Mixed} key Either an array of items composing a key or a string
|
||||
* @return {Promise}
|
||||
*/
|
||||
cache.get = (key) => new Promise((resolve, reject) => {
|
||||
redis.get(keyfunc(key), (err, reply) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
if (reply !== null) {
|
||||
let value;
|
||||
|
||||
try {
|
||||
|
||||
// Parse the stored cache value from JSON.
|
||||
value = JSON.parse(reply);
|
||||
} catch (e) {
|
||||
return reject(e);
|
||||
}
|
||||
|
||||
return resolve(value);
|
||||
}
|
||||
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* This sets a value on the key with the expiry and then resolves once it is
|
||||
* done.
|
||||
* @param {Mixed} key Either an array of items composing a key or a string
|
||||
* @param {Mixed} value Object to be serialized and set to the cache
|
||||
* @param {Integer} expiry Time in seconds for the cache entry to live for
|
||||
* @return {Promise}
|
||||
*/
|
||||
cache.set = (key, value, expiry) => new Promise((resolve, reject) => {
|
||||
|
||||
// Serialize the value as JSON.
|
||||
let reply = JSON.stringify(value);
|
||||
|
||||
redis.set(keyfunc(key), reply, 'EX', expiry, (err) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,7 @@ import React from 'react';
|
||||
import {Button, Icon} from 'react-mdl';
|
||||
import timeago from 'timeago.js';
|
||||
import styles from './CommentList.css';
|
||||
import I18n from 'coral-framework/i18n/i18n';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations.json';
|
||||
|
||||
// Render a single comment for the list
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import {Layout, Navigation, Drawer, Header} from 'react-mdl';
|
||||
import {Link} from 'react-router';
|
||||
import styles from './Header.css';
|
||||
import I18n from 'coral-framework/i18n/i18n';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations.json';
|
||||
|
||||
// App header. If we add a navbar it should be here
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import I18n from 'coral-framework/i18n/i18n';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations.json';
|
||||
import React from 'react';
|
||||
import Modal from 'components/Modal';
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import {Navigation, Drawer} from 'react-mdl';
|
||||
import {Link} from 'react-router';
|
||||
import styles from './Header.css';
|
||||
import I18n from 'coral-framework/i18n/i18n';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../translations.json';
|
||||
|
||||
export default () => (
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import {Navigation, Header} from 'react-mdl';
|
||||
import {Link} from 'react-router';
|
||||
import styles from './Header.css';
|
||||
import I18n from 'coral-framework/i18n/i18n';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../translations.json';
|
||||
|
||||
export default () => (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import I18n from 'coral-framework/i18n/i18n';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../translations.json';
|
||||
import {Grid, Cell} from 'react-mdl';
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {SelectField, Option} from 'react-mdl-selectfield';
|
||||
import styles from './Community.css';
|
||||
import I18n from 'coral-framework/i18n/i18n';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../../translations';
|
||||
import {setRole} from '../../actions/community';
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
Icon
|
||||
} from 'react-mdl';
|
||||
import styles from './Configure.css';
|
||||
import I18n from 'coral-framework/i18n/i18n';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations.json';
|
||||
|
||||
class Configure extends React.Component {
|
||||
|
||||
@@ -5,7 +5,7 @@ import CommentList from 'components/CommentList';
|
||||
import {updateStatus} from 'actions/comments';
|
||||
import styles from './ModerationQueue.css';
|
||||
import key from 'keymaster';
|
||||
import I18n from 'coral-framework/i18n/i18n';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations.json';
|
||||
|
||||
/*
|
||||
|
||||
@@ -17,10 +17,12 @@ import Pym from 'pym.js';
|
||||
import FlagButton from '../../coral-plugin-flags/FlagButton';
|
||||
import LikeButton from '../../coral-plugin-likes/LikeButton';
|
||||
import PermalinkButton from '../../coral-plugin-permalinks/PermalinkButton';
|
||||
import SignInContainer from '../../coral-sign-in/containers/SignInContainer';
|
||||
import UserBox from '../../coral-sign-in/components/UserBox';
|
||||
|
||||
const {addItem, updateItem, postItem, getStream, postAction, deleteAction, appendItemArray} = itemActions;
|
||||
const {addNotification, clearNotification} = notificationActions;
|
||||
const {setLoggedInUser} = authActions;
|
||||
const {logout} = authActions;
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
@@ -31,40 +33,21 @@ const mapStateToProps = (state) => {
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
addItem: (item, itemType) => {
|
||||
return dispatch(addItem(item, itemType));
|
||||
},
|
||||
updateItem: (id, property, value, itemType) => {
|
||||
return dispatch(updateItem(id, property, value, itemType));
|
||||
},
|
||||
postItem: (data, type, id) => {
|
||||
return dispatch(postItem(data, type, id));
|
||||
},
|
||||
getStream: (rootId) => {
|
||||
return dispatch(getStream(rootId));
|
||||
},
|
||||
addNotification: (type, text) => {
|
||||
return dispatch(addNotification(type, text));
|
||||
},
|
||||
clearNotification: () => {
|
||||
return dispatch(clearNotification());
|
||||
},
|
||||
setLoggedInUser: (user_id) => {
|
||||
return dispatch(setLoggedInUser(user_id));
|
||||
},
|
||||
postAction: (item, action, user, itemType) => {
|
||||
return dispatch(postAction(item, action, user, itemType));
|
||||
},
|
||||
deleteAction: (item, action, user, itemType) => {
|
||||
return dispatch(deleteAction(item, action, user, itemType));
|
||||
},
|
||||
appendItemArray: (item, property, value, addToFront, itemType) => {
|
||||
return dispatch(appendItemArray(item, property, value, addToFront, itemType));
|
||||
}
|
||||
};
|
||||
};
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
addItem: (item, itemType) => dispatch(addItem(item, itemType)),
|
||||
updateItem: (id, property, value, itemType) => dispatch(updateItem(id, property, value, itemType)),
|
||||
postItem: (data, type, id) => dispatch(postItem(data, type, id)),
|
||||
getStream: (rootId) => dispatch(getStream(rootId)),
|
||||
addNotification: (type, text) => dispatch(addNotification(type, text)),
|
||||
clearNotification: () => dispatch(clearNotification()),
|
||||
postAction: (item, action, user, itemType) => dispatch(postAction(item, action, user, itemType)),
|
||||
deleteAction: (item, action, user, itemType) => {
|
||||
return dispatch(deleteAction(item, action, user, itemType));
|
||||
},
|
||||
appendItemArray: (item, property, value, addToFront, itemType) =>
|
||||
dispatch(appendItemArray(item, property, value, addToFront, itemType)),
|
||||
logout: () => dispatch(logout())
|
||||
});
|
||||
|
||||
class CommentStream extends Component {
|
||||
|
||||
@@ -83,11 +66,25 @@ class CommentStream extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
if (Object.keys(this.props.items).length === 0) {
|
||||
// Loading mock asset
|
||||
this.props.postItem({
|
||||
comments: [],
|
||||
url: 'http://coralproject.net'
|
||||
}, 'asset', 'assetTest');
|
||||
|
||||
// Loading mock user
|
||||
//this.props.postItem({name: 'Ban Ki-Moon'}, 'user', 'user_8989')
|
||||
// .then((id) => {
|
||||
// this.props.setLoggedInUser(id);
|
||||
// });
|
||||
}
|
||||
|
||||
// TODO: Replace teststream id with id from params
|
||||
|
||||
const rootItemId = this.props.items.assets && Object.keys(this.props.items.assets)[0];
|
||||
const rootItem = this.props.items.assets && this.props.items.assets[rootItemId];
|
||||
const {loggedIn, user} = this.props.auth;
|
||||
return <div>
|
||||
{
|
||||
rootItem
|
||||
@@ -99,6 +96,7 @@ class CommentStream extends Component {
|
||||
<Count
|
||||
id={rootItemId}
|
||||
items={this.props.items}/>
|
||||
{loggedIn && <UserBox user={user} logout={this.props.logout} />}
|
||||
<CommentBox
|
||||
addNotification={this.props.addNotification}
|
||||
postItem={this.props.postItem}
|
||||
@@ -106,7 +104,10 @@ class CommentStream extends Component {
|
||||
updateItem={this.props.updateItem}
|
||||
id={rootItemId}
|
||||
premod={this.props.config.moderation}
|
||||
reply={false}/>
|
||||
reply={false}
|
||||
canPost={loggedIn}
|
||||
/>
|
||||
{!loggedIn && <SignInContainer />}
|
||||
</div>
|
||||
{
|
||||
rootItem.comments && rootItem.comments.map((commentId) => {
|
||||
|
||||
@@ -5,7 +5,6 @@ body {
|
||||
font-size: 12px;
|
||||
margin: 0px;
|
||||
padding: 0px 0px 50px 0px;
|
||||
|
||||
}
|
||||
|
||||
button {
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from './../translations';
|
||||
const lang = new I18n(translations);
|
||||
import * as actions from '../constants/auth';
|
||||
import {base, handleResp, getInit} from '../helpers/response';
|
||||
|
||||
// Dialog Actions
|
||||
export const showSignInDialog = () => ({type: actions.SHOW_SIGNIN_DIALOG});
|
||||
export const hideSignInDialog = () => ({type: actions.HIDE_SIGNIN_DIALOG});
|
||||
|
||||
export const changeView = view => dispatch =>
|
||||
dispatch({
|
||||
type: actions.CHANGE_VIEW,
|
||||
view
|
||||
});
|
||||
|
||||
export const cleanState = () => ({type: actions.CLEAN_STATE});
|
||||
|
||||
// Sign In Actions
|
||||
|
||||
const signInRequest = () => ({type: actions.FETCH_SIGNIN_REQUEST});
|
||||
const signInSuccess = (user) => ({type: actions.FETCH_SIGNIN_SUCCESS, user});
|
||||
const signInFailure = (error) => ({type: actions.FETCH_SIGNIN_FAILURE, error});
|
||||
|
||||
export const fetchSignIn = (formData) => dispatch => {
|
||||
dispatch(signInRequest());
|
||||
fetch(`${base}/auth/local`, getInit('POST', formData))
|
||||
.then(handleResp)
|
||||
.then(({user}) => {
|
||||
dispatch(hideSignInDialog());
|
||||
dispatch(signInSuccess(user));
|
||||
})
|
||||
.catch(() => dispatch(signInFailure(lang.t('error.emailPasswordError'))));
|
||||
};
|
||||
|
||||
// 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});
|
||||
|
||||
export const fetchSignInFacebook = () => dispatch => {
|
||||
dispatch(signInFacebookRequest());
|
||||
window.open(
|
||||
`${base}/auth/facebook`,
|
||||
'Continue with Facebook',
|
||||
'menubar=0,resizable=0,width=500,height=500,top=200,left=500'
|
||||
);
|
||||
};
|
||||
|
||||
export const facebookCallback = (err, data) => dispatch => {
|
||||
if (err) {
|
||||
signInFacebookFailure(err);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
dispatch(signInFacebookSuccess(JSON.parse(data)));
|
||||
dispatch(hideSignInDialog());
|
||||
} catch (err) {
|
||||
dispatch(signInFacebookFailure(err));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Sign Up Actions
|
||||
|
||||
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 => dispatch => {
|
||||
dispatch(signUpRequest());
|
||||
fetch(`${base}/user`, getInit('POST', formData))
|
||||
.then(handleResp)
|
||||
.then(({user}) => {
|
||||
dispatch(signUpSuccess(user));
|
||||
setTimeout(() =>{
|
||||
dispatch(changeView('SIGNIN'));
|
||||
}, 3000);
|
||||
})
|
||||
.catch(() => dispatch(signUpFailure(lang.t('error.emailInUse')))); // We need to inprove error handling. TODO (bc)
|
||||
};
|
||||
|
||||
// Forgot Password Actions
|
||||
|
||||
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 fetchForgotPassword = () => dispatch => {
|
||||
dispatch(forgotPassowordRequest());
|
||||
fetch(`${base}/user/request-password-reset`, getInit('POST'))
|
||||
.then(handleResp)
|
||||
.then(() => dispatch(forgotPassowordSuccess()))
|
||||
.catch(error => dispatch(forgotPassowordFailure(error)));
|
||||
};
|
||||
|
||||
// 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());
|
||||
fetch(`${base}/auth`, getInit('DELETE'))
|
||||
.then(handleResp)
|
||||
.then(() => dispatch(logOutSuccess()))
|
||||
.catch(error => dispatch(logOutFailure(error)));
|
||||
};
|
||||
|
||||
// LogOut Actions
|
||||
|
||||
export const validForm = () => ({type: actions.VALID_FORM});
|
||||
export const invalidForm = error => ({type: actions.INVALID_FORM, error});
|
||||
|
||||
-2
@@ -1,5 +1,3 @@
|
||||
/* @flow */
|
||||
|
||||
import {fromJS} from 'immutable';
|
||||
|
||||
/**
|
||||
-2
@@ -1,5 +1,3 @@
|
||||
/* Notification Actions */
|
||||
|
||||
export const ADD_NOTIFICATION = 'ADD_NOTIFICATION';
|
||||
export const CLEAR_NOTIFICATION = 'CLEAR_NOTIFICATION';
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
export const CHANGE_VIEW = 'CHANGE_VIEW';
|
||||
export const CLEAN_STATE = 'CLEAN_STATE';
|
||||
|
||||
export const SHOW_SIGNIN_DIALOG = 'SHOW_SIGNIN_DIALOG';
|
||||
export const HIDE_SIGNIN_DIALOG = 'HIDE_SIGNIN_DIALOG';
|
||||
|
||||
export const FETCH_SIGNUP_REQUEST = 'FETCH_SIGNUP_REQUEST';
|
||||
export const FETCH_SIGNUP_FAILURE = 'FETCH_SIGNUP_FAILURE';
|
||||
export const FETCH_SIGNUP_SUCCESS = 'FETCH_SIGNUP_SUCCESS';
|
||||
|
||||
export const FETCH_SIGNIN_REQUEST = 'FETCH_SIGNIN_REQUEST';
|
||||
export const FETCH_SIGNIN_FAILURE = 'FETCH_SIGNIN_FAILURE';
|
||||
export const FETCH_SIGNIN_SUCCESS = 'FETCH_SIGNIN_SUCCESS';
|
||||
|
||||
export const FETCH_SIGNIN_FACEBOOK_REQUEST = 'FETCH_SIGNIN_FACEBOOK_REQUEST';
|
||||
export const FETCH_SIGNIN_FACEBOOK_FAILURE = 'FETCH_SIGNIN_FACEBOOK_FAILURE';
|
||||
export const FETCH_SIGNIN_FACEBOOK_SUCCESS = 'FETCH_SIGNIN_FACEBOOK_SUCCESS';
|
||||
|
||||
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 INVALID_FORM = 'INVALID_FORM';
|
||||
export const VALID_FORM = 'VALID_FORM';
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from './../translations';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
export default {
|
||||
email: lang.t('error.email'),
|
||||
password: lang.t('error.password'),
|
||||
displayName: lang.t('error.displayName'),
|
||||
confirmPassword: lang.t('error.confirmPassword')
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
export const base = '/api/v1';
|
||||
|
||||
export const getInit = (method, body) => {
|
||||
let init = {
|
||||
method,
|
||||
headers: new Headers({
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}),
|
||||
credentials: 'same-origin'
|
||||
};
|
||||
|
||||
if (method.toLowerCase() !== 'get') {
|
||||
init.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
return init;
|
||||
};
|
||||
|
||||
export const handleResp = res => {
|
||||
if (res.status === 401) {
|
||||
throw new Error('Not Authorized to make this request');
|
||||
} else if (res.status > 399) {
|
||||
throw new Error('Error! Status ', res.status);
|
||||
} else if (res.status === 204) {
|
||||
return res.text();
|
||||
} else {
|
||||
return res.json();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
email: email => (/^([A-Za-z0-9_\-\.])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/.test(email)),
|
||||
password: pass => (/^(?=.{8,}).*$/.test(pass)),
|
||||
confirmPassword: () => true,
|
||||
displayName: displayName => (/^(?=.{3,}).*$/.test(displayName))
|
||||
};
|
||||
@@ -1,10 +1,10 @@
|
||||
import Notification from './notification/Notification';
|
||||
import store from './store/store';
|
||||
import {fetchConfig} from './store/actions/config';
|
||||
import * as itemActions from './store/actions/items';
|
||||
import I18n from './i18n/i18n';
|
||||
import * as notificationActions from './store/actions/notification';
|
||||
import * as authActions from './store/actions/auth';
|
||||
import Notification from './modules/notification/Notification';
|
||||
import store from './store';
|
||||
import {fetchConfig} from './actions/config';
|
||||
import * as itemActions from './actions/items';
|
||||
import I18n from './modules/i18n/i18n';
|
||||
import * as notificationActions from './actions/notification';
|
||||
import * as authActions from './actions/auth';
|
||||
|
||||
export {
|
||||
Notification,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import timeago from 'timeago.js';
|
||||
import esTA from 'timeago.js/locales/es';
|
||||
import esTA from '../../../../node_modules/timeago.js/locales/es';
|
||||
|
||||
/**
|
||||
* Default locales, this should be overriden by config file
|
||||
@@ -0,0 +1,76 @@
|
||||
import {Map} from 'immutable';
|
||||
import * as actions from '../constants/auth';
|
||||
|
||||
const initialState = Map({
|
||||
isLoading: false,
|
||||
loggedIn: false,
|
||||
user: null,
|
||||
showSignInDialog: false,
|
||||
view: 'SIGNIN',
|
||||
error: '',
|
||||
successSignUp: false
|
||||
});
|
||||
|
||||
export default function auth (state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.SHOW_SIGNIN_DIALOG :
|
||||
return state
|
||||
.set('showSignInDialog', true);
|
||||
case actions.HIDE_SIGNIN_DIALOG :
|
||||
return state.merge(Map({
|
||||
isLoading: false,
|
||||
showSignInDialog: false,
|
||||
view: 'SIGNIN',
|
||||
error: '',
|
||||
successSignUp: false
|
||||
}));
|
||||
case actions.CHANGE_VIEW :
|
||||
return state
|
||||
.set('error', '')
|
||||
.set('view', action.view);
|
||||
case actions.CLEAN_STATE:
|
||||
return initialState;
|
||||
case actions.FETCH_SIGNIN_REQUEST:
|
||||
return state
|
||||
.set('isLoading', true);
|
||||
case actions.FETCH_SIGNIN_SUCCESS:
|
||||
return state
|
||||
.set('loggedIn', true)
|
||||
.set('user', action.user);
|
||||
case actions.FETCH_SIGNIN_FAILURE:
|
||||
return state
|
||||
.set('isLoading', false)
|
||||
.set('error', action.error);
|
||||
case actions.FETCH_SIGNIN_FACEBOOK_SUCCESS:
|
||||
return state
|
||||
.set('user', action.user)
|
||||
.set('loggedIn', true);
|
||||
case actions.FETCH_SIGNIN_FACEBOOK_FAILURE:
|
||||
return state
|
||||
.set('error', action.error)
|
||||
.set('user', null);
|
||||
case actions.FETCH_SIGNUP_REQUEST:
|
||||
return state
|
||||
.set('isLoading', true);
|
||||
case actions.FETCH_SIGNUP_FAILURE:
|
||||
return state
|
||||
.set('error', action.error)
|
||||
.set('isLoading', false);
|
||||
case actions.FETCH_SIGNUP_SUCCESS:
|
||||
return state
|
||||
.set('isLoading', false)
|
||||
.set('successSignUp', true);
|
||||
case actions.LOGOUT_SUCCESS:
|
||||
return state
|
||||
.set('loggedIn', false)
|
||||
.set('user', null);
|
||||
case actions.INVALID_FORM:
|
||||
return state
|
||||
.set('error', action.error);
|
||||
case actions.VALID_FORM:
|
||||
return state
|
||||
.set('error', '');
|
||||
default :
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import {createStore, applyMiddleware} from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
import mainReducer from './reducers';
|
||||
@@ -1,17 +0,0 @@
|
||||
/* Auth Actions */
|
||||
|
||||
export const SET_LOGGED_IN_USER = 'SET_LOGGED_IN_USER';
|
||||
export const LOG_OUT_USER = 'LOG_OUT_USER';
|
||||
|
||||
export const setLoggedInUser = (user_id) => {
|
||||
return {
|
||||
type: SET_LOGGED_IN_USER,
|
||||
user_id
|
||||
};
|
||||
};
|
||||
|
||||
export const LogOutUser = () => {
|
||||
return {
|
||||
type: LOG_OUT_USER
|
||||
};
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
/* Auth */
|
||||
|
||||
import * as actions from '../actions/auth';
|
||||
import {fromJS} from 'immutable';
|
||||
|
||||
const initialState = fromJS({});
|
||||
|
||||
export default (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case actions.SET_LOGGED_IN_USER:
|
||||
return state.set('user', action.user_id);
|
||||
case actions.LOG_OUT_USER:
|
||||
return initialState;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"en": {
|
||||
"error": {
|
||||
"email": "Not a valid E-Mail",
|
||||
"password": "Password must be at least 8 characters",
|
||||
"displayName": "Display name is too short",
|
||||
"confirmPassword": "Passwords don`t match. Please, check again",
|
||||
"emailPasswordError": "Email and/or password combination incorrect.",
|
||||
"emailInUse": "Email address already in use"
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
"error": {
|
||||
"email": "No es un email válido",
|
||||
"password": "La contraseña debe tener por lo menos 8 caracteres",
|
||||
"displayName": "El nombre es muy corto",
|
||||
"confirmPassword": "Las contraseñas no coinciden",
|
||||
"emailPasswordError": "Email y/o contraseña incorrecta.",
|
||||
"emailInUse": "Email address already in use"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,8 @@ class CommentBox extends Component {
|
||||
updateItem: PropTypes.func,
|
||||
id: PropTypes.string,
|
||||
comments: PropTypes.array,
|
||||
reply: PropTypes.bool
|
||||
reply: PropTypes.bool,
|
||||
canPost: PropTypes.bool
|
||||
}
|
||||
|
||||
state = {
|
||||
@@ -51,18 +52,9 @@ class CommentBox extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const {styles, reply} = this.props;
|
||||
const {styles, reply, canPost} = this.props;
|
||||
// How to handle language in plugins? Should we have a dependency on our central translation file?
|
||||
return <div>
|
||||
<div className={`${name}-container`}>
|
||||
<input type='text'
|
||||
className={`${name}-username`}
|
||||
style={styles && styles.textarea}
|
||||
value={this.state.username}
|
||||
id={reply ? 'replyUser' : 'commentUser'}
|
||||
placeholder={lang.t('name')}
|
||||
onChange={(e) => this.setState({username: e.target.value})}/>
|
||||
</div>
|
||||
<div
|
||||
className={`${name}-container`}>
|
||||
<label
|
||||
@@ -81,12 +73,15 @@ class CommentBox extends Component {
|
||||
rows={3}/>
|
||||
</div>
|
||||
<div className={`${name}-button-container`}>
|
||||
<button
|
||||
className={`${name}-button`}
|
||||
style={styles && styles.button}
|
||||
onClick={this.postComment}>
|
||||
{lang.t('post')}
|
||||
</button>
|
||||
{ canPost && (
|
||||
<button
|
||||
className={`${name}-button`}
|
||||
style={styles && styles.button}
|
||||
onClick={this.postComment}>
|
||||
{lang.t('post')}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import I18n from 'coral-framework/i18n/i18n';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from './translations';
|
||||
import onClickOutside from 'react-onclickoutside';
|
||||
const name = 'coral-plugin-permalinks';
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import styles from './styles.css';
|
||||
|
||||
const Alert = ({cStyle = 'error', children, className, ...props}) => (
|
||||
<div
|
||||
className={`${styles.alert} ${styles[`alert--${cStyle}`]} ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Alert;
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import styles from './styles.css';
|
||||
import Button from 'coral-ui/components/Button';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
const ForgotContent = ({changeView, ...props}) => (
|
||||
<div>
|
||||
<div className={styles.header}>
|
||||
<h1>{lang.t('signIn.recoverPassword')}</h1>
|
||||
</div>
|
||||
<form onSubmit={(e) => {e.preventDefault(); props.fetchForgotPassword();}}>
|
||||
<div className={styles.formField}>
|
||||
<label htmlFor="email">{lang.t('signIn.email')}</label>
|
||||
<input type="text" id="email" />
|
||||
</div>
|
||||
<Button type="submit" cStyle="black" className={styles.signInButton}>
|
||||
{lang.t('signIn.recoverPassword')}
|
||||
</Button>
|
||||
</form>
|
||||
<div className={styles.footer}>
|
||||
<span>{lang.t('signIn.needAnAccount')} <a onClick={() => changeView('SIGNUP')}>{lang.t('signIn.register')}</a></span>
|
||||
<span>{lang.t('signIn.alreadyHaveAnAccount')} <a onClick={() => changeView('SIGNIN')}>{lang.t('signIn.signIn')}</a></span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default ForgotContent;
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import styles from './styles.css';
|
||||
|
||||
const FormField = ({className, showErrors = false, errorMsg, label, ...props}) => (
|
||||
<div className={`${styles.formField} ${className ? className : ''}`}>
|
||||
<label htmlFor={props.id}>
|
||||
{label}
|
||||
</label>
|
||||
<input
|
||||
className={showErrors && errorMsg ? styles.error : ''}
|
||||
name={props.id}
|
||||
{...props}
|
||||
/>
|
||||
{showErrors && errorMsg && <span className={styles.errorMsg}><span className={styles.attention}>!</span>{errorMsg}</span>}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default FormField;
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import {Dialog} from 'coral-ui';
|
||||
import styles from './styles.css';
|
||||
|
||||
import SignInContent from './SignInContent';
|
||||
import SingUpContent from './SignUpContent';
|
||||
import ForgotContent from './ForgotContent';
|
||||
|
||||
const SignDialog = ({open, view, handleClose, ...props}) => (
|
||||
<Dialog className={styles.dialog} open={open}>
|
||||
<span className={styles.close} onClick={handleClose}>×</span>
|
||||
{view === 'SIGNIN' && <SignInContent {...props} />}
|
||||
{view === 'SIGNUP' && <SingUpContent {...props} />}
|
||||
{view === 'FORGOT' && <ForgotContent {...props} />}
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
export default SignDialog;
|
||||
@@ -0,0 +1,67 @@
|
||||
import React from 'react';
|
||||
import Button from 'coral-ui/components/Button';
|
||||
import FormField from './FormField';
|
||||
import Alert from './Alert';
|
||||
import Spinner from 'coral-ui/components/Spinner';
|
||||
import styles from './styles.css';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
const SignInContent = ({handleChange, formData, ...props}) => (
|
||||
<div>
|
||||
<div className={styles.header}>
|
||||
<h1>
|
||||
{lang.t('signIn.signIn')}
|
||||
</h1>
|
||||
</div>
|
||||
<div className={styles.socialConnections}>
|
||||
<Button cStyle="facebook" onClick={props.fetchSignInFacebook}>
|
||||
{lang.t('signIn.facebookSignIn')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.separator}>
|
||||
<h1>
|
||||
{lang.t('signIn.or')}
|
||||
</h1>
|
||||
</div>
|
||||
{ props.auth.error && <Alert>{props.auth.error}</Alert> }
|
||||
<form onSubmit={props.handleSignIn}>
|
||||
<FormField
|
||||
id="email"
|
||||
type="email"
|
||||
label={lang.t('signIn.email')}
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<FormField
|
||||
id="password"
|
||||
type="password"
|
||||
label={lang.t('signIn.password')}
|
||||
value={formData.password}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<div className={styles.action}>
|
||||
{
|
||||
!props.auth.isLoading ?
|
||||
<Button type="submit" cStyle="black" className={styles.signInButton}>
|
||||
{lang.t('signIn.signIn')}
|
||||
</Button>
|
||||
:
|
||||
<Spinner />
|
||||
}
|
||||
</div>
|
||||
</form>
|
||||
<div className={styles.footer}>
|
||||
<span><a onClick={() => props.changeView('FORGOT')}>{lang.t('signIn.forgotYourPass')}</a></span>
|
||||
<span>
|
||||
{lang.t('signIn.needAnAccount')}
|
||||
<a onClick={() => props.changeView('SIGNUP')}>
|
||||
{lang.t('signIn.register')}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default SignInContent;
|
||||
@@ -0,0 +1,91 @@
|
||||
import React from 'react';
|
||||
import FormField from './FormField';
|
||||
import Alert from './Alert';
|
||||
import Button from 'coral-ui/components/Button';
|
||||
import Spinner from 'coral-ui/components/Spinner';
|
||||
import Success from 'coral-ui/components/Success';
|
||||
import styles from './styles.css';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
const SignUpContent = ({handleChange, formData, ...props}) => (
|
||||
<div>
|
||||
<div className={styles.header}>
|
||||
<h1>
|
||||
{lang.t('signIn.signUp')}
|
||||
</h1>
|
||||
</div>
|
||||
<div className={styles.socialConnections}>
|
||||
<Button cStyle="facebook" onClick={props.fetchSignInFacebook}>
|
||||
{lang.t('signIn.facebookSignUp')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className={styles.separator}>
|
||||
<h1>
|
||||
{lang.t('signIn.or')}
|
||||
</h1>
|
||||
</div>
|
||||
{ props.auth.error && <Alert>{props.auth.error}</Alert> }
|
||||
<form onSubmit={props.handleSignUp}>
|
||||
<FormField
|
||||
id="email"
|
||||
type="email"
|
||||
label={lang.t('signIn.email')}
|
||||
value={formData.email}
|
||||
showErrors={props.showErrors}
|
||||
errorMsg={props.errors.email}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<FormField
|
||||
id="displayName"
|
||||
type="text"
|
||||
label={lang.t('signIn.displayName')}
|
||||
value={formData.displayName}
|
||||
showErrors={props.showErrors}
|
||||
errorMsg={props.errors.displayName}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<FormField
|
||||
id="password"
|
||||
type="password"
|
||||
label={lang.t('signIn.password')}
|
||||
value={formData.password}
|
||||
showErrors={props.showErrors}
|
||||
errorMsg={props.errors.password}
|
||||
onChange={handleChange}
|
||||
minLength="8"
|
||||
/>
|
||||
{ !props.errors.password && <span className={styles.hint}> Password must be at least 8 characters. </span> }
|
||||
<FormField
|
||||
id="confirmPassword"
|
||||
type="password"
|
||||
label={lang.t('signIn.confirmPassword')}
|
||||
value={formData.confirmPassword}
|
||||
showErrors={props.showErrors}
|
||||
errorMsg={props.errors.confirmPassword}
|
||||
onChange={handleChange}
|
||||
minLength="8"
|
||||
/>
|
||||
<div className={styles.action}>
|
||||
{ !props.auth.isLoading && !props.auth.successSignUp && (
|
||||
<Button type="submit" cStyle="black" className={styles.signInButton}>
|
||||
{lang.t('signIn.signUp')}
|
||||
</Button>
|
||||
)}
|
||||
{ props.auth.isLoading && <Spinner /> }
|
||||
{ !props.auth.isLoading && props.auth.successSignUp && <Success /> }
|
||||
</div>
|
||||
</form>
|
||||
<div className={styles.footer}>
|
||||
<span>
|
||||
{lang.t('signIn.alreadyHaveAnAccount')}
|
||||
<a onClick={() => props.changeView('SIGNIN')}>
|
||||
{lang.t('signIn.signIn')}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default SignUpContent;
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import styles from './styles.css';
|
||||
|
||||
const UserBox = ({className, user, logout, ...props}) => (
|
||||
<div
|
||||
className={`${styles.userBox} ${className}`}
|
||||
{...props}
|
||||
>
|
||||
Signed in as <a>{user.displayName}</a>. Not you? <a onClick={logout}>Sign out</a>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default UserBox;
|
||||
@@ -0,0 +1,131 @@
|
||||
.dialog {
|
||||
border: none;
|
||||
box-shadow: 0 9px 46px 8px rgba(0, 0, 0, 0.14), 0 11px 15px -7px rgba(0, 0, 0, 0.12), 0 24px 38px 3px rgba(0, 0, 0, 0.2);
|
||||
width: 280px;
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header h1, .separator h1{
|
||||
text-align: center;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.formField {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.formField label {
|
||||
font-size: 1.08em;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.formField input {
|
||||
width: 100%;
|
||||
display: block;
|
||||
border: none;
|
||||
outline: none;
|
||||
border: 1px solid rgba(0,0,0,.12);
|
||||
padding: 10px 6px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 2px;
|
||||
margin: 5px auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin: 20px auto 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer span {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
color: #2c69b6;
|
||||
cursor: pointer;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.socialConnections {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.signInButton {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.close {
|
||||
font-size: 20px;
|
||||
line-height: 14px;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
position: absolute;
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
color: #363636;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.close:hover {
|
||||
color: #6b6b6b;
|
||||
}
|
||||
|
||||
input.error{
|
||||
border: solid 2px #f44336;
|
||||
}
|
||||
|
||||
.errorMsg, .hint {
|
||||
color: grey;
|
||||
font-weight: 600;
|
||||
padding: 3px 0 16px;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 10px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.alert--success {
|
||||
border: solid 1px #1ec00e;
|
||||
background: #cbf1b8;
|
||||
color: #006900;
|
||||
}
|
||||
|
||||
.alert--error {
|
||||
background: #FFEBEE;
|
||||
color: #B71C1C;
|
||||
}
|
||||
|
||||
.userBox a {
|
||||
color: #2c69b6;
|
||||
cursor: pointer;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.attention {
|
||||
display: inline-block;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
background: #B71C1C;
|
||||
color: #FFEBEE;
|
||||
font-weight: bolder;
|
||||
padding: 4px;
|
||||
vertical-align: middle;
|
||||
border-radius: 20px;
|
||||
box-sizing: border-box;
|
||||
font-size: 9px;
|
||||
line-height: 7px;
|
||||
text-align: center;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.action {
|
||||
margin-top: 15px;
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import React, {Component} from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import SignDialog from '../components/SignDialog';
|
||||
import Button from 'coral-ui/components/Button';
|
||||
import validate from 'coral-framework/helpers/validate';
|
||||
import errorMsj from 'coral-framework/helpers/error';
|
||||
import I18n from 'coral-framework/modules/i18n/i18n';
|
||||
import translations from '../translations';
|
||||
const lang = new I18n(translations);
|
||||
|
||||
import {
|
||||
changeView,
|
||||
fetchSignUp,
|
||||
fetchSignIn,
|
||||
showSignInDialog,
|
||||
hideSignInDialog,
|
||||
fetchSignInFacebook,
|
||||
fetchForgotPassword,
|
||||
facebookCallback,
|
||||
invalidForm,
|
||||
validForm
|
||||
} from '../../coral-framework/actions/auth';
|
||||
|
||||
class SignInContainer extends Component {
|
||||
initialState = {
|
||||
formData: {
|
||||
email: '',
|
||||
displayName: '',
|
||||
password: '',
|
||||
confirmPassword: ''
|
||||
},
|
||||
errors: {},
|
||||
showErrors: false
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = this.initialState;
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleSignUp = this.handleSignUp.bind(this);
|
||||
this.handleSignIn = this.handleSignIn.bind(this);
|
||||
this.handleClose = this.handleClose.bind(this);
|
||||
this.addError = this.addError.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.authCallback = this.props.facebookCallback;
|
||||
const {formData} = this.state;
|
||||
const errors = Object.keys(formData).reduce((map, prop) => {
|
||||
map[prop] = lang.t('signIn.requiredField');
|
||||
return map;
|
||||
}, {});
|
||||
this.setState({errors});
|
||||
}
|
||||
|
||||
handleChange(e) {
|
||||
const {name, value} = e.target;
|
||||
this.setState(state => ({
|
||||
...state,
|
||||
formData: {
|
||||
...state.formData,
|
||||
[name]: value
|
||||
}
|
||||
}), () => {
|
||||
this.validation(name, value);
|
||||
});
|
||||
}
|
||||
|
||||
addError(name, error) {
|
||||
return this.setState(state => ({
|
||||
errors: {
|
||||
...state.errors,
|
||||
[name]: error
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
validation(name, value) {
|
||||
const {addError} = this;
|
||||
const {formData} = this.state;
|
||||
|
||||
if (!value.length) {
|
||||
addError(name, lang.t('signIn.requiredField'));
|
||||
} else if (name === 'confirmPassword' && formData.confirmPassword !== formData.password) {
|
||||
addError('confirmPassword', lang.t('signIn.passwordsDontMatch'));
|
||||
} else if (!validate[name](value)) {
|
||||
addError(name, errorMsj[name]);
|
||||
} else {
|
||||
const { [name]: prop, ...errors } = this.state.errors; // eslint-disable-line
|
||||
// Removes Error
|
||||
this.setState(state => ({...state, errors}));
|
||||
}
|
||||
}
|
||||
|
||||
isCompleted() {
|
||||
const {formData} = this.state;
|
||||
return !Object.keys(formData).filter(prop => !formData[prop].length).length;
|
||||
}
|
||||
|
||||
displayErrors(show = true) {
|
||||
this.setState({showErrors: show});
|
||||
}
|
||||
|
||||
handleSignUp(e) {
|
||||
e.preventDefault();
|
||||
const {errors} = this.state;
|
||||
const {fetchSignUp, validForm, invalidForm} = this.props;
|
||||
this.displayErrors();
|
||||
if (this.isCompleted() && !Object.keys(errors).length) {
|
||||
fetchSignUp(this.state.formData);
|
||||
validForm();
|
||||
} else {
|
||||
invalidForm(lang.t('signIn.checkTheForm'));
|
||||
}
|
||||
}
|
||||
|
||||
handleSignIn(e) {
|
||||
e.preventDefault();
|
||||
this.props.fetchSignIn(this.state.formData);
|
||||
}
|
||||
|
||||
handleClose() {
|
||||
this.props.hideSignInDialog();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {auth, showSignInDialog} = this.props;
|
||||
return (
|
||||
<div>
|
||||
<Button onClick={showSignInDialog}>
|
||||
Sign in to comment
|
||||
</Button>
|
||||
<SignDialog
|
||||
open={auth.showSignInDialog}
|
||||
view={auth.view}
|
||||
{...this}
|
||||
{...this.state}
|
||||
{...this.props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
auth: state.auth.toJS()
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
facebookCallback: (err, data) => dispatch(facebookCallback(err, data)),
|
||||
fetchSignUp: formData => dispatch(fetchSignUp(formData)),
|
||||
fetchSignIn: formData => dispatch(fetchSignIn(formData)),
|
||||
fetchSignInFacebook: () => dispatch(fetchSignInFacebook()),
|
||||
fetchForgotPassword: formData => dispatch(fetchForgotPassword(formData)),
|
||||
showSignInDialog: () => dispatch(showSignInDialog()),
|
||||
changeView: view => dispatch(changeView(view)),
|
||||
handleClose: () => dispatch(hideSignInDialog()),
|
||||
invalidForm: error => dispatch(invalidForm(error)),
|
||||
validForm: () => dispatch(validForm())
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(SignInContainer);
|
||||
@@ -0,0 +1,48 @@
|
||||
export default {
|
||||
en: {
|
||||
'signIn': {
|
||||
facebookSignIn: 'Sign in with Facebook',
|
||||
facebookSignUp: 'Sign up with Facebook',
|
||||
logout: 'Logout',
|
||||
signIn: 'Sign In',
|
||||
or: 'Or',
|
||||
email: 'E-mail Address',
|
||||
password: 'Password',
|
||||
forgotYourPass: 'Forgot your password?',
|
||||
needAnAccount: 'Need an account?',
|
||||
register: 'Register',
|
||||
signUp: 'Sign Up',
|
||||
confirmPassword: 'Confirm Password',
|
||||
displayName: 'Display Name',
|
||||
alreadyHaveAnAccount: 'Already have an account?',
|
||||
recoverPassword: 'Recover password',
|
||||
emailInUse: 'Email address already in use',
|
||||
requiredField: 'This field is required',
|
||||
passwordsDontMatch: 'Passwords don\'t match.',
|
||||
checkTheForm: 'Invalid Form. Please, check the fields'
|
||||
}
|
||||
},
|
||||
es: {
|
||||
'signIn': {
|
||||
facebookSignIn: 'Entrar con Facebook',
|
||||
facebookSignUp: 'Regístrate con Facebook',
|
||||
logout: 'Salir',
|
||||
signIn: 'Entrar',
|
||||
or: 'o',
|
||||
email: 'E-mail',
|
||||
password: 'Contraseña',
|
||||
forgotYourPass: 'Has olvidado tu contraseña?',
|
||||
needAnAccount: 'Necesitas una cuenta?',
|
||||
register: 'Regístrate',
|
||||
signUp: 'Registro',
|
||||
confirmPassword: 'Confirmar Contraseña',
|
||||
displayName: 'Nombre',
|
||||
alreadyHaveAnAccount: 'Ya tienes una cuenta?',
|
||||
recoverPassword: 'Recuperar contraseña',
|
||||
emailInUse: 'Este email se encuentra en uso',
|
||||
requiredField: 'Este campo es requerido',
|
||||
passwordsDontMatch: 'Las contraseñas no coinciden',
|
||||
checkTheForm: 'Formulario Inválido. Por favor, completa los campos'
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
.button {
|
||||
background: 0 0;
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
color: #000;
|
||||
display: block;
|
||||
position: relative;
|
||||
height: 36px;
|
||||
min-width: 64px;
|
||||
padding: 0 8px;
|
||||
display: inline-block;
|
||||
font-family: 'Roboto','Helvetica','Arial',sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0;
|
||||
overflow: hidden;
|
||||
will-change: box-shadow,transform;
|
||||
-webkit-transition: box-shadow .2s cubic-bezier(.4,0,1,1),background-color .2s cubic-bezier(.4,0,.2,1),color .2s cubic-bezier(.4,0,.2,1);
|
||||
transition: box-shadow .2s cubic-bezier(.4,0,1,1),background-color .2s cubic-bezier(.4,0,.2,1),color .2s cubic-bezier(.4,0,.2,1);
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
line-height: 36px;
|
||||
vertical-align: middle;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.type--black {
|
||||
color: #E0E0E0;
|
||||
background: #212121;
|
||||
}
|
||||
|
||||
.type--local {
|
||||
background: #E0E0E0;
|
||||
color: #212121;
|
||||
}
|
||||
|
||||
.type--facebook {
|
||||
background-color: #4267b2;
|
||||
border-color: #4267b2;
|
||||
color: rgb(255, 255, 255);
|
||||
}
|
||||
|
||||
.type--facebook:hover {
|
||||
background-color: #365899;
|
||||
border-color: #365899;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import styles from './Button.css';
|
||||
|
||||
const Button = ({cStyle = 'local', children, className, ...props}) => (
|
||||
<button
|
||||
className={`${styles.button} ${styles[`type--${cStyle}`]} ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
|
||||
export default Button;
|
||||
@@ -0,0 +1,62 @@
|
||||
import React, {Component, PropTypes} from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import dialogPolyfill from 'dialog-polyfill';
|
||||
import 'dialog-polyfill/dialog-polyfill.css';
|
||||
|
||||
export default class Dialog extends Component {
|
||||
static propTypes = {
|
||||
className: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
onCancel: PropTypes.func,
|
||||
onClose: PropTypes.func,
|
||||
open: PropTypes.bool,
|
||||
style: PropTypes.object
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
onCancel: e => e.preventDefault(),
|
||||
onClose: e => e.preventDefault()
|
||||
};
|
||||
|
||||
componentDidMount(){
|
||||
const dialog = ReactDOM.findDOMNode(this.refs.dialog);
|
||||
dialogPolyfill.registerDialog(dialog);
|
||||
|
||||
if (this.props.open) {
|
||||
dialog.showModal();
|
||||
}
|
||||
|
||||
dialog.addEventListener('close', this.props.onClose);
|
||||
dialog.addEventListener('cancel', this.props.onCancel);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const dialog = ReactDOM.findDOMNode(this.refs.dialog);
|
||||
if (this.props.open !== prevProps.open) {
|
||||
if (this.props.open) {
|
||||
dialog.showModal();
|
||||
} else {
|
||||
dialog.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const dialog = ReactDOM.findDOMNode(this.refs.dialog);
|
||||
dialog.removeEventListener('cancel', this.props.onCancel);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {children, className = '', onClose, onCancel, open, ...rest} = this.props; // eslint-disable-line
|
||||
|
||||
return (
|
||||
<dialog
|
||||
ref="dialog"
|
||||
className={`mdl-dialog ${className}`}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
.container {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
-webkit-animation: rotator 1.4s linear infinite;
|
||||
animation: rotator 1.4s linear infinite;
|
||||
}
|
||||
|
||||
@-webkit-keyframes rotator {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(270deg);
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotator {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(270deg);
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
}
|
||||
.path {
|
||||
stroke-dasharray: 187;
|
||||
stroke-dashoffset: 0;
|
||||
-webkit-transform-origin: center;
|
||||
transform-origin: center;
|
||||
-webkit-animation: dash 1.4s ease-in-out infinite, colors 5.6s ease-in-out infinite;
|
||||
animation: dash 1.4s ease-in-out infinite, colors 5.6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@-webkit-keyframes colors {
|
||||
0% {
|
||||
stroke: #f67150;
|
||||
}
|
||||
100% {
|
||||
stroke: #f6a47e;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes colors {
|
||||
0% {
|
||||
stroke: #f67150;
|
||||
}
|
||||
100% {
|
||||
stroke: #f6a47e;
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes dash {
|
||||
0% {
|
||||
stroke-dashoffset: 187;
|
||||
}
|
||||
50% {
|
||||
stroke-dashoffset: 46.75;
|
||||
-webkit-transform: rotate(135deg);
|
||||
transform: rotate(135deg);
|
||||
}
|
||||
100% {
|
||||
stroke-dashoffset: 187;
|
||||
-webkit-transform: rotate(450deg);
|
||||
transform: rotate(450deg);
|
||||
}
|
||||
}
|
||||
@keyframes dash {
|
||||
0% {
|
||||
stroke-dashoffset: 187;
|
||||
}
|
||||
50% {
|
||||
stroke-dashoffset: 46.75;
|
||||
-webkit-transform: rotate(135deg);
|
||||
transform: rotate(135deg);
|
||||
}
|
||||
100% {
|
||||
stroke-dashoffset: 187;
|
||||
-webkit-transform: rotate(450deg);
|
||||
transform: rotate(450deg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import styles from './Spinner.css';
|
||||
|
||||
const Spinner = () => (
|
||||
<div className={styles.container}>
|
||||
<svg className={styles.spinner} width="40px" height="40px" viewBox="0 0 66 66" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle className={styles.path} fill="none" strokeWidth="6" strokeLinecap="round" cx="33" cy="33" r="30"></circle>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Spinner;
|
||||
@@ -0,0 +1,55 @@
|
||||
.container {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.circle {
|
||||
stroke-dasharray: 166;
|
||||
stroke-dashoffset: 166;
|
||||
stroke-width: 2;
|
||||
stroke-miterlimit: 10;
|
||||
stroke: #00897B;
|
||||
fill: none;
|
||||
animation: stroke .6s cubic-bezier(0.650, 0.000, 0.450, 1.000) forwards;
|
||||
}
|
||||
|
||||
.checkmark {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 50%;
|
||||
display: block;
|
||||
stroke-width: 2;
|
||||
stroke: #fff;
|
||||
stroke-miterlimit: 10;
|
||||
margin: 10% auto;
|
||||
box-shadow: inset 0px 0px 0px #00897B;
|
||||
animation: fill .4s ease-in-out .4s forwards, scale .3s ease-in-out .9s both;
|
||||
}
|
||||
|
||||
.check {
|
||||
transform-origin: 50% 50%;
|
||||
stroke-dasharray: 48;
|
||||
stroke-dashoffset: 48;
|
||||
animation: stroke .3s cubic-bezier(0.650, 0.000, 0.450, 1.000) .8s forwards;
|
||||
}
|
||||
|
||||
@keyframes stroke {
|
||||
100% {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scale {
|
||||
0%, 100% {
|
||||
transform: none;
|
||||
}
|
||||
50% {
|
||||
transform: scale3d(1.1, 1.1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fill {
|
||||
100% {
|
||||
box-shadow: inset 0px 0px 0px 30px #00897B;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import styles from './Success.css';
|
||||
|
||||
const Success = () => (
|
||||
<div className={styles.container}>
|
||||
<svg className={styles.checkmark} viewBox="0 0 52 52" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle className={styles.circle} cx="26" cy="26" r="25" fill="none"></circle>
|
||||
<path className={styles.check} fill="none" d="M14.1 27.2l7.1 7.2 16.7-16.8"/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Success;
|
||||
@@ -0,0 +1 @@
|
||||
export {default as Dialog} from './components/Dialog';
|
||||
@@ -10,8 +10,10 @@ services:
|
||||
environment:
|
||||
- "TALK_PORT=5000"
|
||||
- "TALK_MONGO_URL=mongodb://mongo"
|
||||
- "TALK_REDIS_URL=redis://redis"
|
||||
depends_on:
|
||||
- mongo
|
||||
- redis
|
||||
|
||||
mongo:
|
||||
image: mongo:3.2
|
||||
@@ -19,6 +21,14 @@ services:
|
||||
volumes:
|
||||
- mongo:/data/db
|
||||
|
||||
redis:
|
||||
image: redis:3.2-alpine
|
||||
restart: always
|
||||
volumes:
|
||||
- redis:/data
|
||||
|
||||
volumes:
|
||||
mongo:
|
||||
external: false
|
||||
redis:
|
||||
external: false
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* authorization contains the references to the authorization middleware.
|
||||
* @type {Object}
|
||||
*/
|
||||
const authorization = module.exports = {};
|
||||
|
||||
const debug = require('debug')('talk:middleware:authorization');
|
||||
|
||||
/**
|
||||
* ErrNotAuthorized is an error that is returned in the event an operation is
|
||||
* deemed not authorized.
|
||||
* @type {Error}
|
||||
*/
|
||||
const ErrNotAuthorized = new Error('not authorized');
|
||||
ErrNotAuthorized.status = 401;
|
||||
|
||||
// Add the ErrNotAuthorized error to the authorization object to be exported.
|
||||
authorization.ErrNotAuthorized = ErrNotAuthorized;
|
||||
|
||||
/**
|
||||
* has returns true if the user has all the roles specified, otherwise it will
|
||||
* return false.
|
||||
* @param {Object} user the user to check for roles
|
||||
* @param {Array} roles all the roles that a user must have
|
||||
* @return {Boolean} true if the user has all the roles required, false
|
||||
* otherwise
|
||||
*/
|
||||
authorization.has = (user, ...roles) => roles.every((role) => user.roles.indexOf(role) >= 0);
|
||||
|
||||
/**
|
||||
* needed is a connect middleware layer that ensures that all requests coming
|
||||
* here are both authenticated and match a set of roles required to continue.
|
||||
* @param {Array} roles all the roles that a user must have
|
||||
* @return {Callback} connect middleware
|
||||
*/
|
||||
authorization.needed = (...roles) => (req, res, next) => {
|
||||
// All routes that are wrapepd with this middleware actually require a role.
|
||||
if (!req.user) {
|
||||
debug(`No user on request, returning with ${ErrNotAuthorized}`);
|
||||
return next(ErrNotAuthorized);
|
||||
}
|
||||
|
||||
// Check to see if the current user has all the roles requested for the given
|
||||
// array of roles requested, if one is not on the user, then this will
|
||||
// evaluate to true.
|
||||
if (!authorization.has(req.user, ...roles)) {
|
||||
debug('User does not have all the required roles to access this page');
|
||||
return next(ErrNotAuthorized);
|
||||
}
|
||||
|
||||
// Looks like they're allowed!
|
||||
return next();
|
||||
};
|
||||
@@ -1,6 +1,13 @@
|
||||
const mongoose = require('../mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
/**
|
||||
* this Schema manages application settings that get used on front- and backend
|
||||
* NOTE: when you set a setting here, it will not automatically be exposed to
|
||||
* the front end. You must add it to the whitelist in the settings route
|
||||
* in /routes/api/settings/index.js
|
||||
* @type {Schema}
|
||||
*/
|
||||
const SettingSchema = new Schema({
|
||||
id: {type: String, default: '1'},
|
||||
moderation: {type: String, enum: ['pre', 'post'], default: 'pre'},
|
||||
|
||||
+87
-2
@@ -1,6 +1,7 @@
|
||||
const mongoose = require('../mongoose');
|
||||
const uuid = require('uuid');
|
||||
const bcrypt = require('bcrypt');
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
// SALT_ROUNDS is the number of rounds that the bcrypt algorithm will run
|
||||
// through during the salting process.
|
||||
@@ -12,6 +13,14 @@ const USER_ROLES = [
|
||||
'moderator'
|
||||
];
|
||||
|
||||
// In the event that the TALK_SESSION_SECRET is missing but we are testing, then
|
||||
// set the process.env.TALK_SESSION_SECRET.
|
||||
if (process.env.NODE_ENV === 'test' && !process.env.TALK_SESSION_SECRET) {
|
||||
process.env.TALK_SESSION_SECRET = 'keyboard cat';
|
||||
} else if (!process.env.TALK_SESSION_SECRET) {
|
||||
throw new Error('TALK_SESSION_SECRET must be defined to encode JSON Web Tokens and other auth functionality');
|
||||
}
|
||||
|
||||
// UserSchema is the mongoose schema defined as the representation of a User in
|
||||
// MongoDB.
|
||||
const UserSchema = new mongoose.Schema({
|
||||
@@ -130,10 +139,14 @@ const UserService = module.exports = {};
|
||||
* @param {Function} done [description]
|
||||
*/
|
||||
UserService.findLocalUser = (email, password) => {
|
||||
if (!email || typeof email !== 'string') {
|
||||
return Promise.reject('email is required for findLocalUser');
|
||||
}
|
||||
|
||||
return UserModel.findOne({
|
||||
profiles: {
|
||||
$elemMatch: {
|
||||
id: email,
|
||||
id: email.toLowerCase(),
|
||||
provider: 'local'
|
||||
}
|
||||
}
|
||||
@@ -237,6 +250,7 @@ UserService.changePassword = (id, password) => {
|
||||
})
|
||||
.then((hashedPassword) => {
|
||||
return UserModel.update({id}, {
|
||||
$inc: {__v: 1},
|
||||
$set: {
|
||||
password: hashedPassword
|
||||
}
|
||||
@@ -268,6 +282,8 @@ UserService.createLocalUser = (email, password, displayName) => {
|
||||
return Promise.reject('email is required');
|
||||
}
|
||||
|
||||
email = email.toLowerCase();
|
||||
|
||||
if (!password) {
|
||||
return Promise.reject('password is required');
|
||||
}
|
||||
@@ -296,9 +312,11 @@ UserService.createLocalUser = (email, password, displayName) => {
|
||||
|
||||
user.save((err) => {
|
||||
if (err) {
|
||||
if (err.code === 11000) {
|
||||
return reject('Email address already in use');
|
||||
}
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
return resolve(user);
|
||||
});
|
||||
});
|
||||
@@ -393,6 +411,57 @@ UserService.findByIdArray = (ids) => {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a JWT from a user email. Only works for local accounts.
|
||||
* @param {String} email of the local user
|
||||
*/
|
||||
UserService.createPasswordResetToken = function (email) {
|
||||
if (!email || typeof email !== 'string') {
|
||||
return Promise.reject('email is required when creating a JWT for resetting passord');
|
||||
}
|
||||
|
||||
email = email.toLowerCase();
|
||||
|
||||
return UserModel.findOne({profiles: {$elemMatch: {id: email}}})
|
||||
.then(user => {
|
||||
|
||||
if (user === null) {
|
||||
// since we don't want to reveal that the email does/doesn't exist
|
||||
// just go ahead and resolve the Promise with null and check in the endpoint
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
const payload = {email, jti: uuid.v4(), userId: user.id, version: user.__v};
|
||||
const token = jwt.sign(payload, process.env.TALK_SESSION_SECRET, {expiresIn: '1d'});
|
||||
|
||||
return token;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* verifies a jwt and returns the associated user
|
||||
* @param {String} token the JSON Web Token to verify
|
||||
*/
|
||||
UserService.verifyPasswordResetToken = token => {
|
||||
return new Promise((resolve, reject) => {
|
||||
jwt.verify(token, process.env.TALK_SESSION_SECRET, (error, decoded) => {
|
||||
if (error) {
|
||||
return reject(error);
|
||||
}
|
||||
|
||||
resolve(decoded);
|
||||
});
|
||||
})
|
||||
.then(decoded => {
|
||||
/**
|
||||
* TODO: check the jti from this decoded token in redis
|
||||
* and make an entry if it does not exist.
|
||||
* reject if entry already exists.
|
||||
*/
|
||||
return UserService.findById(decoded.userId);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds a user using a value which gets compared using a prefix match against
|
||||
* the user's email address and/or their display name.
|
||||
@@ -426,3 +495,19 @@ UserService.search = (value) => {
|
||||
]
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a count of the current users.
|
||||
* @return {Promise}
|
||||
*/
|
||||
UserService.count = () => {
|
||||
return UserModel.count();
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns all the users.
|
||||
* @return {Promise}
|
||||
*/
|
||||
UserService.all = () => {
|
||||
return UserModel.find();
|
||||
};
|
||||
|
||||
+6
-10
@@ -10,16 +10,12 @@ if (enabled('talk:db')) {
|
||||
mongoose.set('debug', true);
|
||||
}
|
||||
|
||||
try {
|
||||
mongoose.connect(url, (err) => {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
mongoose.connect(url, (err) => {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
debug('Connected to MongoDB!');
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Cannot stablish a connection with MongoDB', err);
|
||||
}
|
||||
debug('connection established');
|
||||
});
|
||||
|
||||
module.exports = mongoose;
|
||||
|
||||
+15
-1
@@ -9,7 +9,6 @@
|
||||
"build-watch": "NODE_ENV=development webpack --config webpack.config.dev.js --watch",
|
||||
"lint": "eslint bin/* .",
|
||||
"lint-fix": "eslint . --fix",
|
||||
"pretest": "npm install",
|
||||
"test": "mocha --compilers js:babel-core/register --recursive tests",
|
||||
"test-watch": "mocha --compilers js:babel-core/register --recursive -w tests",
|
||||
"embed-start": "NODE_ENV=development npm run build && ./bin/www"
|
||||
@@ -37,13 +36,27 @@
|
||||
"dependencies": {
|
||||
"bcrypt": "^0.8.7",
|
||||
"body-parser": "^1.15.2",
|
||||
"cli-table": "^0.3.1",
|
||||
"commander": "^2.9.0",
|
||||
"connect-redis": "^3.1.0",
|
||||
"debug": "^2.2.0",
|
||||
"ejs": "^2.5.2",
|
||||
"express": "^4.14.0",
|
||||
"express-session": "^1.14.2",
|
||||
"helmet": "^3.1.0",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"mongoose": "^4.6.5",
|
||||
"morgan": "^1.7.0",
|
||||
"passport": "^0.3.2",
|
||||
"passport-facebook": "^2.1.1",
|
||||
"passport-local": "^1.0.0",
|
||||
"jsonwebtoken": "^7.1.9",
|
||||
"lodash": "^4.16.6",
|
||||
"mongoose": "^4.6.5",
|
||||
"morgan": "^1.7.0",
|
||||
"nodemailer": "^2.6.4",
|
||||
"prompt": "^1.0.0",
|
||||
"redis": "^2.6.3",
|
||||
"uuid": "^2.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -66,6 +79,7 @@
|
||||
"chai-http": "^3.0.0",
|
||||
"copy-webpack-plugin": "^4.0.0",
|
||||
"css-loader": "^0.25.0",
|
||||
"dialog-polyfill": "^0.4.4",
|
||||
"eslint": "^3.9.1",
|
||||
"eslint-config-postcss": "^2.0.2",
|
||||
"eslint-config-standard": "^6.2.1",
|
||||
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
const passport = require('passport');
|
||||
const User = require('./models/user');
|
||||
const LocalStrategy = require('passport-local').Strategy;
|
||||
const FacebookStrategy = require('passport-facebook').Strategy;
|
||||
|
||||
//==============================================================================
|
||||
// SESSION SERIALIZATION
|
||||
//==============================================================================
|
||||
|
||||
passport.serializeUser((user, done) => {
|
||||
done(null, user.id);
|
||||
});
|
||||
|
||||
passport.deserializeUser((id, done) => {
|
||||
User
|
||||
.findById(id)
|
||||
.then((user) => {
|
||||
done(null, user);
|
||||
})
|
||||
.catch((err) => {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Validates that a user is allowed to login.
|
||||
* @param {User} user the user to be validated
|
||||
* @param {Function} done the callback for the validation
|
||||
*/
|
||||
function ValidateUserLogin(user, done) {
|
||||
if (!user) {
|
||||
return done(new Error('user not found'));
|
||||
}
|
||||
|
||||
if (user.disabled) {
|
||||
return done(null, false, {message: 'Account disabled'});
|
||||
}
|
||||
|
||||
return done(null, user);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// STRATEGIES
|
||||
//==============================================================================
|
||||
|
||||
passport.use(new LocalStrategy({
|
||||
usernameField: 'email',
|
||||
passwordField: 'password'
|
||||
}, (email, password, done) => {
|
||||
User
|
||||
.findLocalUser(email, password)
|
||||
.then((user) => {
|
||||
if (!user) {
|
||||
return done(null, false, {message: 'Incorrect email/password combination'});
|
||||
}
|
||||
|
||||
return ValidateUserLogin(user, done);
|
||||
})
|
||||
.catch((err) => {
|
||||
done(err);
|
||||
});
|
||||
}));
|
||||
|
||||
if (process.env.TALK_FACEBOOK_APP_ID && process.env.TALK_FACEBOOK_APP_SECRET && process.env.TALK_ROOT_URL) {
|
||||
passport.use(new FacebookStrategy({
|
||||
clientID: process.env.TALK_FACEBOOK_APP_ID,
|
||||
clientSecret: process.env.TALK_FACEBOOK_APP_SECRET,
|
||||
callbackURL: `${process.env.TALK_ROOT_URL}/api/v1/auth/facebook/callback`,
|
||||
profileFields: ['id', 'displayName', 'picture.type(large)']
|
||||
}, (accessToken, refreshToken, profile, done) => {
|
||||
User
|
||||
.findOrCreateExternalUser(profile)
|
||||
.then((user) =>
|
||||
ValidateUserLogin(user, done)
|
||||
)
|
||||
.catch((err) => {
|
||||
done(err);
|
||||
});
|
||||
}));
|
||||
} else {
|
||||
console.error('Facebook cannot be enabled, missing one of TALK_FACEBOOK_APP_ID, TALK_FACEBOOK_APP_SECRET, TALK_ROOT_URL');
|
||||
}
|
||||
|
||||
module.exports = passport;
|
||||
@@ -0,0 +1,39 @@
|
||||
const redis = require('redis');
|
||||
const debug = require('debug')('talk:redis');
|
||||
const url = process.env.TALK_REDIS_URL || 'redis://localhost';
|
||||
|
||||
const client = redis.createClient(url, {
|
||||
retry_strategy: function(options) {
|
||||
if (options.error && options.error.code === 'ECONNREFUSED') {
|
||||
|
||||
// End reconnecting on a specific error and flush all commands with a individual error
|
||||
return new Error('The server refused the connection');
|
||||
}
|
||||
if (options.total_retry_time > 1000 * 60 * 60) {
|
||||
|
||||
// End reconnecting after a specific timeout and flush all commands with a individual error
|
||||
return new Error('Retry time exhausted');
|
||||
}
|
||||
|
||||
if (options.times_connected > 10) {
|
||||
|
||||
// End reconnecting with built in error
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// reconnect after
|
||||
return Math.max(options.attempt * 100, 3000);
|
||||
}
|
||||
});
|
||||
|
||||
client.ping((err) => {
|
||||
if (err) {
|
||||
console.error('Can\'t ping the redis server!');
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
debug('connection established');
|
||||
});
|
||||
|
||||
module.exports = client;
|
||||
@@ -1,11 +1,15 @@
|
||||
const express = require('express');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/embed/stream/preview', (req, res) => {
|
||||
res.render('embed-stream', {basePath: '/client/embed/stream'});
|
||||
});
|
||||
|
||||
router.get('/password-reset/:token', (req, res, next) => {
|
||||
// render a page or something?
|
||||
res.send('ok');
|
||||
});
|
||||
|
||||
router.get('*', (req, res) => {
|
||||
res.render('admin', {basePath: '/client/coral-admin'});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
const express = require('express');
|
||||
const passport = require('../../../passport');
|
||||
const authorization = require('../../../middleware/authorization');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* This returns the user if they are logged in.
|
||||
*/
|
||||
router.get('/', authorization.needed(), (req, res) => {
|
||||
res.json(req.user);
|
||||
});
|
||||
|
||||
/**
|
||||
* This destroys the session of a user, if they have one.
|
||||
*/
|
||||
router.delete('/', (req, res) => {
|
||||
req.session.destroy(() => {
|
||||
res.status(204).end();
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* This sends back the user data as JSON.
|
||||
*/
|
||||
const HandleAuthCallback = (req, res, next) => (err, user) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return next(authorization.ErrNotAuthorized);
|
||||
}
|
||||
|
||||
// Perform the login of the user!
|
||||
req.logIn(user, (err) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
// We logged in the user! Let's send back the user data.
|
||||
res.json({user});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the response to the login attempt via a popup callback with some JS.
|
||||
*/
|
||||
const HandleAuthPopupCallback = (req, res, next) => (err, user) => {
|
||||
if (err) {
|
||||
return res.render('auth-callback', {err: JSON.stringify(err), data: null});
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return res.render('auth-callback', {err: JSON.stringify(authorization.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});
|
||||
}
|
||||
|
||||
// We logged in the user! Let's send back the user data.
|
||||
res.render('auth-callback', {err: null, data: JSON.stringify(user)});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Local auth endpoint, will recieve a email and password
|
||||
*/
|
||||
router.post('/local', (req, res, next) => {
|
||||
|
||||
// Perform the local authentication.
|
||||
passport.authenticate('local', HandleAuthCallback(req, res, next))(req, res, next);
|
||||
});
|
||||
|
||||
/**
|
||||
* Facebook auth endpoint, this will redirect the user immediatly to facebook
|
||||
* for authorization.
|
||||
*/
|
||||
router.get('/facebook', passport.authenticate('facebook', {display: 'popup', authType: 'rerequest', scope: ['public_profile']}));
|
||||
|
||||
/**
|
||||
* Facebook callback endpoint, this will send the user a html page designed to
|
||||
* send back the user credentials upon sucesfull login.
|
||||
*/
|
||||
router.get('/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);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -3,6 +3,7 @@ const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
router.use('/asset', require('./asset'));
|
||||
router.use('/auth', require('./auth'));
|
||||
router.use('/comments', require('./comments'));
|
||||
router.use('/queue', require('./queue'));
|
||||
router.use('/settings', require('./settings'));
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const _ = require('lodash');
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const Setting = require('../../../models/setting');
|
||||
@@ -5,7 +6,10 @@ const Setting = require('../../../models/setting');
|
||||
router.get('/', (req, res, next) => {
|
||||
Setting
|
||||
.getSettings()
|
||||
.then(settings => res.json(settings))
|
||||
.then(settings => {
|
||||
const whitelist = ['moderation'];
|
||||
res.json(_.pick(settings, whitelist));
|
||||
})
|
||||
.catch(next);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const User = require('../../../models/user');
|
||||
const mailer = require('../../../services/mailer');
|
||||
const ejs = require('ejs');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const resetEmailFile = fs.readFileSync(path.resolve(__dirname, '../../../views/password-reset-email.ejs'));
|
||||
const resetEmailTemplate = ejs.compile(resetEmailFile.toString());
|
||||
|
||||
router.get('/', (req, res, next) => {
|
||||
const {
|
||||
@@ -52,4 +58,80 @@ router.post('/:user_id/role', (req, res, next) => {
|
||||
.catch(next);
|
||||
});
|
||||
|
||||
router.post('/', (req, res, next) => {
|
||||
const {email, password, displayName} = req.body;
|
||||
|
||||
User
|
||||
.createLocalUser(email, password, displayName)
|
||||
.then(user => {
|
||||
res.status(201).send(user);
|
||||
})
|
||||
.catch(err => {
|
||||
next(err);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* expects 2 fields in the body of the request
|
||||
* 1) the token that was in the url of the email link {String}
|
||||
* 2) the new password {String}
|
||||
*/
|
||||
router.post('/update-password', (req, res, next) => {
|
||||
const {token, password} = req.body;
|
||||
|
||||
User.verifyPasswordResetToken(token)
|
||||
.then(user => {
|
||||
return User.changePassword(user.id, password);
|
||||
})
|
||||
.then(() => {
|
||||
res.status(204).end();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
res.status(401).send('Not Authorized');
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* this endpoint takes an email (username) and checks if it belongs to a User account
|
||||
* if it does, create a JWT and send an email
|
||||
*/
|
||||
router.post('/request-password-reset', (req, res, next) => {
|
||||
const {email} = req.body;
|
||||
|
||||
if (!email) {
|
||||
return next();
|
||||
}
|
||||
|
||||
User
|
||||
.createPasswordResetToken(email)
|
||||
.then(token => {
|
||||
if (token === null) {
|
||||
return Promise.resolve('the email was not found in the db.');
|
||||
}
|
||||
|
||||
const options = {
|
||||
subject: 'Password Reset Requested - Talk',
|
||||
from: 'noreply@coralproject.net',
|
||||
to: email,
|
||||
html: resetEmailTemplate({
|
||||
token,
|
||||
// probably more clear to explicitly pass this
|
||||
rootURL: process.env.TALK_ROOT_URL
|
||||
})
|
||||
};
|
||||
return mailer.sendSimple(options);
|
||||
})
|
||||
.then(() => {
|
||||
// we want to send a 204 regardless of the user being found in the db
|
||||
// if we fail on missing emails, it would reveal if people are registered or not.
|
||||
res.status(204).send('OK');
|
||||
})
|
||||
.catch(error => {
|
||||
const errorMsg = typeof error === 'string' ? error : error.message;
|
||||
|
||||
res.status(500).json({error: errorMsg});
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
const nodemailer = require('nodemailer');
|
||||
|
||||
if (!process.env.TALK_SMTP_CONNECTION_URL) {
|
||||
console.error('TALK_SMTP_CONNECTION_URL should be defined if you would like to send password reset emails from Talk');
|
||||
}
|
||||
|
||||
const defaultTransporter = nodemailer.createTransport(process.env.TALK_SMTP_CONNECTION_URL);
|
||||
|
||||
const mailer = {
|
||||
|
||||
/**
|
||||
* sendSimple
|
||||
*
|
||||
* @param {Object} {from, to, subject, text = '', html = ''}
|
||||
* @returns
|
||||
*/
|
||||
sendSimple({from, to, subject, text = '', html = '', transporter = defaultTransporter}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!from) {
|
||||
reject('sendSimple requires a from address');
|
||||
}
|
||||
if (!to) {
|
||||
reject('sendSimple requires a comma-separated list of "to" addresses');
|
||||
}
|
||||
if (!subject) {
|
||||
reject('sendSimple requires a subject for the email');
|
||||
}
|
||||
|
||||
return resolve(transporter.sendMail({from, to, subject, text, html}));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = mailer;
|
||||
+31
-1
@@ -162,6 +162,36 @@ paths:
|
||||
responses:
|
||||
204:
|
||||
description: OK
|
||||
|
||||
/user/request-password-reset:
|
||||
post:
|
||||
tags:
|
||||
- Users
|
||||
description: trigger a reset password email. sends a success code whether email was found or no.
|
||||
responses:
|
||||
204:
|
||||
description: OK
|
||||
|
||||
/user/update-password:
|
||||
post:
|
||||
tags:
|
||||
- Users
|
||||
description: Update existing user password
|
||||
parameters:
|
||||
- name: token
|
||||
type: string
|
||||
in: body
|
||||
description: JSON Web token taken taken from emailed link
|
||||
required: true
|
||||
- name: password
|
||||
type: string
|
||||
in: body
|
||||
description: new password to be settings
|
||||
required: true
|
||||
responses:
|
||||
204:
|
||||
description: OK
|
||||
|
||||
/asset:
|
||||
get:
|
||||
tags:
|
||||
@@ -276,7 +306,7 @@ definitions:
|
||||
description: A summary of the asset, inferred on initial load.
|
||||
section:
|
||||
type: string
|
||||
description: The section the asset is in.
|
||||
description: The section the asset is in.
|
||||
subsection:
|
||||
type: string
|
||||
description: The subsection that the asset is in.
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import {Map} from 'immutable';
|
||||
import {expect} from 'chai';
|
||||
import authReducer from '../../../../client/coral-framework/store/reducers/auth';
|
||||
import * as actions from '../../../../client/coral-framework/store/actions/auth';
|
||||
|
||||
describe ('authReducer', () => {
|
||||
describe('SET_LOGGED_IN_USER', () => {
|
||||
it('should set a logged in user', () => {
|
||||
const action = {
|
||||
type: actions.SET_LOGGED_IN_USER,
|
||||
user_id: '123'
|
||||
};
|
||||
const store = new Map({});
|
||||
const result = authReducer(store, action);
|
||||
expect(result.get('user')).to.equal(action.user_id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('LOG_OUT_USER', () => {
|
||||
it('should clear the user store', () => {
|
||||
const action = {
|
||||
type: actions.LOG_OUT_USER
|
||||
};
|
||||
const store = new Map({
|
||||
user: '123'
|
||||
});
|
||||
const result = authReducer(store, action);
|
||||
expect(result.get('user')).to.equal(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,7 @@ import 'react';
|
||||
import 'redux';
|
||||
import {expect} from 'chai';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import * as actions from '../../../../client/coral-framework/store/actions/items';
|
||||
import * as actions from '../../../../client/coral-framework/actions/items';
|
||||
import {Map} from 'immutable';
|
||||
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {Map, fromJS} from 'immutable';
|
||||
import {expect} from 'chai';
|
||||
import itemsReducer from '../../../../client/coral-framework/store/reducers/items';
|
||||
import itemsReducer from '../../../../client/coral-framework/reducers/items';
|
||||
|
||||
describe ('itemsReducer', () => {
|
||||
describe('ADD_ITEM', () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {Map} from 'immutable';
|
||||
import {expect} from 'chai';
|
||||
import notificationReducer from '../../../../client/coral-framework/store/reducers/notification';
|
||||
import * as actions from '../../../../client/coral-framework/store/actions/notification';
|
||||
import notificationReducer from '../../../../client/coral-framework/reducers/notification';
|
||||
import * as actions from '../../../../client/coral-framework/actions/notification';
|
||||
|
||||
describe ('notificationsReducer', () => {
|
||||
describe('ADD_NOTIFICATION', () => {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
require('../../../utils/mongoose');
|
||||
|
||||
const app = require('../../../../app');
|
||||
const chai = require('chai');
|
||||
const expect = chai.expect;
|
||||
|
||||
chai.use(require('chai-http'));
|
||||
|
||||
const User = require('../../../../models/user');
|
||||
|
||||
describe('POST /auth/local', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
return User.createLocalUser('maria@gmail.com', 'password!', 'Maria');
|
||||
});
|
||||
|
||||
it('should send back the user on a successful login', () => {
|
||||
return chai.request(app)
|
||||
.post('/api/v1/auth/local')
|
||||
.send({email: 'maria@gmail.com', password: 'password!'})
|
||||
.catch((res) => {
|
||||
expect(res).to.have.status(200);
|
||||
expect(res).to.be.json;
|
||||
expect(res.body).to.have.property('user');
|
||||
expect(res.body.user).to.have.property('displayName', 'Maria');
|
||||
});
|
||||
});
|
||||
|
||||
it('should not send back the user on a unsuccessful login', () => {
|
||||
return chai.request(app)
|
||||
.post('/api/v1/auth/local')
|
||||
.send({email: 'maria@gmail.com', password: 'password!3'})
|
||||
.catch((err) => {
|
||||
expect(err).to.not.be.null;
|
||||
expect(err.response).to.have.status(401);
|
||||
expect(err.response.body).to.have.property('message', 'not authorized');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,22 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="initial-scale=1, maximum-scale=1">
|
||||
<title>Talk - Coral Admin</title>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
|
||||
<link rel="stylesheet" href="https://code.getmdl.io/1.2.1/material.indigo-pink.min.css">
|
||||
<style media="screen">
|
||||
body, #root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
background: #fff;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="<%= basePath %>/bundle.js" charset="utf-8"></script>
|
||||
</body>
|
||||
</html>
|
||||
+27
-10
@@ -3,23 +3,40 @@
|
||||
<meta property="og:title" content="<%= title %>" />
|
||||
<meta property="og:author" content="A. J. Ournalist" />
|
||||
<meta property="og:description" content="A description of this article." />
|
||||
|
||||
<meta property="article:published" itemprop="datePublished" content="2016-11-16T11:46:06-05:00" />
|
||||
<meta property="article:modified" itemprop="dateModified" content="2016-11-16T12:09:44-05:00" />
|
||||
<meta property="article:section" itemprop="articleSection" content="The Section!" />
|
||||
|
||||
<style>
|
||||
main {
|
||||
margin-left:auto;
|
||||
margin-right:auto;
|
||||
width:500px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div style="margin-left:auto; margin-right:auto; width:500px">
|
||||
<main>
|
||||
<h1><%= title %></h1>
|
||||
<p>Lorem ipsum dolor sponge amet, consectetur adipiscing clam. Ut lobortis sollicitudin pillar a ornare. Curabitur dignissim vestibulum cay non rhoncus. Cras laoreet ante vel nunc hendrerit, shelf imperdiet neque egestas. Suspendisse aliquet iaculis fermentum. Talk volutpat, tellus posuere laoreet consequat, mi lacus laoreet massa, sed vehicula mauris velit non lectus. Integer non trust nec neque congue faucibus porttitor sit amet elkhorn.</p>
|
||||
<p>
|
||||
Lorem ipsum dolor sponge amet, consectetur adipiscing clam.
|
||||
Ut lobortis sollicitudin pillar a ornare. Curabitur dignissim
|
||||
vestibulum cay non rhoncus. Cras laoreet ante vel nunc hendrerit,
|
||||
shelf imperdiet neque egestas. Suspendisse aliquet iaculis fermentum.
|
||||
Talk volutpat, tellus posuere laoreet consequat, mi lacus laoreet massa,
|
||||
sed vehicula mauris velit non lectus. Integer non trust nec neque congue
|
||||
faucibus porttitor sit amet elkhorn.
|
||||
</p>
|
||||
<p><a href="/admin">Visit the moderation console</a></p>
|
||||
<div id='coralStreamEmbed'></div>
|
||||
</main>
|
||||
|
||||
<div id='coralStreamEmbed'></div>
|
||||
<script type='text/javascript' src='https://pym.nprapps.org/pym.v1.min.js'></script>
|
||||
<script>
|
||||
var pymParent = new pym.Parent('coralStreamEmbed', '/embed/stream', {title: 'Talk Comments'});
|
||||
pymParent.onMessage('height', function(height) {document.querySelector('#coralStreamEmbed iframe').height = height + 'px'})</script>
|
||||
</div>
|
||||
<!--- Script --->
|
||||
<script type='text/javascript' src='https://pym.nprapps.org/pym.v1.min.js'></script>
|
||||
<script>
|
||||
var pymParent = new pym.Parent('coralStreamEmbed', '/embed/stream', {title: 'Talk Comments'});
|
||||
pymParent.onMessage('height', function(height) {
|
||||
document.querySelector('#coralStreamEmbed iframe').height = height + 'px';
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<script type="text/javascript">
|
||||
window.opener.authCallback(<% if (err) { %>'<%- err %>'<% } else { %>null<% } %>, '<%- data %>');
|
||||
setTimeout(function() { window.close(); }, 50);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+10
-6
@@ -11,18 +11,22 @@
|
||||
body, #root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
background: #fff;
|
||||
min-height: 600px;
|
||||
margin: 0;
|
||||
background: #fff;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id='coralStreamEmbed'></div>
|
||||
|
||||
<!--- Script --->
|
||||
<script type='text/javascript' src='https://pym.nprapps.org/pym.v1.min.js'></script>
|
||||
<script>
|
||||
var pymParent = new pym.Parent('coralStreamEmbed', '/embed/stream', {title: 'Talk Comments'});
|
||||
pymParent.onMessage('height', function(height) {document.querySelector('#coralStreamEmbed iframe').height = height + 'px'})</script>
|
||||
</div>
|
||||
</body>
|
||||
var pymParent = new pym.Parent('coralStreamEmbed', '/embed/stream', {title: 'Talk Comments'});
|
||||
pymParent.onMessage('height', function(height) {
|
||||
document.querySelector('#coralStreamEmbed iframe').height = height + 'px';
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div id="coralStream"/>
|
||||
<div id="coralStream"></div>
|
||||
<script src="/client/embed/stream/bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<!-- extremely naive implementation of a password reset email -->
|
||||
<p>We received a request to reset your password. If you did not request this change, you can ignore this email.<br />
|
||||
If you did, <a href="<%= rootURL %>/admin/password-reset/<%= token %>">please click here to reset password</a>.</p>
|
||||
<% if (process.env.NODE_ENV !== 'production') { %>
|
||||
<p style="color: red"><%= token %></p>
|
||||
<% } %>
|
||||
Reference in New Issue
Block a user