From 6867760099eca0f8b6366c1457fcafa957b735b7 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 10 Nov 2016 17:14:33 -0700 Subject: [PATCH 01/50] Initial pass at adding middleware for users --- Dockerfile | 1 + README.md | 10 ++++ app.js | 54 +++++++++++++++++++++- docker-compose.yml | 10 ++++ middleware/authorization.js | 53 ++++++++++++++++++++++ mongoose.js | 16 +++---- package.json | 24 ++++------ passport.js | 83 ++++++++++++++++++++++++++++++++++ redis.js | 43 ++++++++++++++++++ routes/api/auth/index.js | 43 ++++++++++++++++++ routes/api/index.js | 1 + routes/index.js | 2 +- tests/routes/api/auth/index.js | 39 ++++++++++++++++ views/admin.ejs~HEAD | 22 --------- 14 files changed, 352 insertions(+), 49 deletions(-) create mode 100644 middleware/authorization.js create mode 100644 passport.js create mode 100644 redis.js create mode 100644 routes/api/auth/index.js create mode 100644 tests/routes/api/auth/index.js delete mode 100644 views/admin.ejs~HEAD diff --git a/Dockerfile b/Dockerfile index 3be5916a8..6776e2f04 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/README.md b/README.md index f4d8b31de..ffc1f3077 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,16 @@ 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*) - +- `TALK_FACEBOOK_APP_ID` (*required*) - +- `TALK_FACEBOOK_APP_SECRET` (*required*) - +- `TALK_ROOT_URL` (*required*) - Root url of the installed application externally available in the format: `://` without the path. + ### Running with Docker Make sure you have Docker running first and then run `docker-compose up -d` diff --git a/app.js b/app.js index 22f343221..ed928622c 100644 --- a/app.js +++ b/app.js @@ -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,57 @@ 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, + 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; +} + +app.use(session(session_opts)); + +//============================================================================== +// PASSPORT MIDDLEWARE +//============================================================================== + +// Setup the PassportJS Middleware. +app.use(passport.initialize()); +app.use(passport.session()); + +//============================================================================== +// ROUTES +//============================================================================== + app.use('/', require('./routes')); //============================================================================== diff --git a/docker-compose.yml b/docker-compose.yml index ecfac00bc..d7a180277 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/middleware/authorization.js b/middleware/authorization.js new file mode 100644 index 000000000..238219a06 --- /dev/null +++ b/middleware/authorization.js @@ -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(); +}; diff --git a/mongoose.js b/mongoose.js index 9062816e6..712b2fcb0 100644 --- a/mongoose.js +++ b/mongoose.js @@ -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; diff --git a/package.json b/package.json index 207724d61..0de0b8607 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,6 @@ "build-watch": "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": "npm run build && ./bin/www" @@ -17,13 +16,8 @@ "config": { "pre-git": { "commit-msg": [], - "pre-commit": [ - "npm run lint", - "npm test" - ], - "pre-push": [ - "npm test" - ], + "pre-commit": ["npm run lint", "npm test"], + "pre-push": ["npm test"], "post-commit": [], "post-merge": [] } @@ -32,12 +26,7 @@ "type": "git", "url": "git+https://github.com/coralproject/talk.git" }, - "keywords": [ - "talk", - "coral", - "coralproject", - "ask" - ], + "keywords": ["talk", "coral", "coralproject", "ask"], "author": "", "license": "Apache-2.0", "bugs": { @@ -48,12 +37,19 @@ "bcrypt": "^0.8.7", "body-parser": "^1.15.2", "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", "mongoose": "^4.6.5", "morgan": "^1.7.0", + "passport": "^0.3.2", + "passport-facebook": "^2.1.1", + "passport-local": "^1.0.0", "prompt": "^1.0.0", + "redis": "^2.6.3", "uuid": "^2.0.3" }, "devDependencies": { diff --git a/passport.js b/passport.js new file mode 100644 index 000000000..ae4776894 --- /dev/null +++ b/passport.js @@ -0,0 +1,83 @@ +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}/connect/facebook/callback` + }, (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; diff --git a/redis.js b/redis.js new file mode 100644 index 000000000..0f2f211f9 --- /dev/null +++ b/redis.js @@ -0,0 +1,43 @@ +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.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.on('error', (err) => { +// throw err; +// }); + +client.ping((err) => { + if (err) { + console.error('Can\'t ping the redis server!'); + + throw err; + } + + debug('connection established'); +}); + +module.exports = client; diff --git a/routes/api/auth/index.js b/routes/api/auth/index.js new file mode 100644 index 000000000..57d126449 --- /dev/null +++ b/routes/api/auth/index.js @@ -0,0 +1,43 @@ +const express = require('express'); +const passport = require('../../../passport'); +const authorization = require('../../../middleware/authorization'); + +const router = express.Router(); + +router.get('/', authorization.needed(), (req, res) => { + res.json(req.user); +}); + +router.delete('/', (req, res) => { + req.logout(); + res.status(204).end(); +}); + +/** + * Local auth endpoint, will recieve a email and password + */ +router.post('/local', (req, res, next) => { + + // Perform the local authentication. + passport.authenticate('local', (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}); + }); + })(req, res, next); +}); + +module.exports = router; diff --git a/routes/api/index.js b/routes/api/index.js index d161cf622..46570f588 100644 --- a/routes/api/index.js +++ b/routes/api/index.js @@ -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('/settings', require('./settings')); router.use('/stream', require('./stream')); diff --git a/routes/index.js b/routes/index.js index cde5b07c7..6fbc54ca9 100644 --- a/routes/index.js +++ b/routes/index.js @@ -6,7 +6,7 @@ router.use('/admin', require('./admin')); router.use('/embed', require('./embed')); router.get('/', (req, res) => { - return res.render('home', {}); + res.render('home', {}); }); module.exports = router; diff --git a/tests/routes/api/auth/index.js b/tests/routes/api/auth/index.js new file mode 100644 index 000000000..dd49a1472 --- /dev/null +++ b/tests/routes/api/auth/index.js @@ -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'); + }); + }); +}); diff --git a/views/admin.ejs~HEAD b/views/admin.ejs~HEAD deleted file mode 100644 index 038fb58d7..000000000 --- a/views/admin.ejs~HEAD +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - Talk - Coral Admin - - - - - -
- - - From 6c13e1c1c99fff0ef8744d3558df76776f0b927a Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 10 Nov 2016 17:22:14 -0700 Subject: [PATCH 02/50] Added fallback for secret during testing --- app.js | 4 ++++ tests/routes/api/comments/index.js | 13 +++---------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/app.js b/app.js index ed928622c..d945d6254 100644 --- a/app.js +++ b/app.js @@ -52,6 +52,10 @@ 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)); diff --git a/tests/routes/api/comments/index.js b/tests/routes/api/comments/index.js index e635e988c..900cdac37 100644 --- a/tests/routes/api/comments/index.js +++ b/tests/routes/api/comments/index.js @@ -168,15 +168,13 @@ describe('Get moderation queues rejected, pending, flags', () => { }); }); - it('should return all the flagged comments', function(done){ - chai.request(app) + it('should return all the flagged comments', () => { + return chai.request(app) .get('/api/v1/comments/action/flag') - .end(function(err, res){ + .then((res) => { expect(res).to.have.status(200); - expect(err).to.be.null; expect(res.body.length).to.equal(1); expect(res.body[0]).to.have.property('id', 'abc'); - done(); }); }); }); @@ -392,11 +390,6 @@ describe('Remove /:comment_id', () => { }); }); -process.on('unhandledRejection', (reason) => { - console.error('Reason: '); - console.error(reason); -}); - describe('Post /:comment_id/status', () => { const comments = [{ From f6184fddb09a1787fed259b046a66c919269a12f Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 11 Nov 2016 12:53:51 -0700 Subject: [PATCH 03/50] Added facebook login support + redis cache --- README.md | 9 ++-- app.js | 1 + cache.js | 97 ++++++++++++++++++++++++++++++++++++ passport.js | 2 +- routes/api/auth/index.js | 89 ++++++++++++++++++++++++++------- routes/api/comments/index.js | 2 +- views/auth-callback.ejs | 9 ++++ 7 files changed, 185 insertions(+), 24 deletions(-) create mode 100644 cache.js create mode 100644 views/auth-callback.ejs diff --git a/README.md b/README.md index ffc1f3077..0f217d89c 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,12 @@ Runs Talk. 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*) - -- `TALK_FACEBOOK_APP_ID` (*required*) - -- `TALK_FACEBOOK_APP_SECRET` (*required*) - +- `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: `://` without the path. ### Running with Docker diff --git a/app.js b/app.js index d945d6254..f5b39a074 100644 --- a/app.js +++ b/app.js @@ -38,6 +38,7 @@ const session_opts = { rolling: true, saveUninitialized: false, resave: false, + name: 'talk.sid', cookie: { secure: false, maxAge: 18000000, // 30 minutes for expiry. diff --git a/cache.js b/cache.js new file mode 100644 index 000000000..efe689f9c --- /dev/null +++ b/cache.js @@ -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(); + }); +}); diff --git a/passport.js b/passport.js index ae4776894..38a92075a 100644 --- a/passport.js +++ b/passport.js @@ -65,7 +65,7 @@ if (process.env.TALK_FACEBOOK_APP_ID && process.env.TALK_FACEBOOK_APP_SECRET && passport.use(new FacebookStrategy({ clientID: process.env.TALK_FACEBOOK_APP_ID, clientSecret: process.env.TALK_FACEBOOK_APP_SECRET, - callbackURL: `${process.env.TALK_ROOT_URL}/connect/facebook/callback` + callbackURL: `${process.env.TALK_ROOT_URL}/api/v1/auth/facebook/callback` }, (accessToken, refreshToken, profile, done) => { User .findOrCreateExternalUser(profile) diff --git a/routes/api/auth/index.js b/routes/api/auth/index.js index 57d126449..2c6f91676 100644 --- a/routes/api/auth/index.js +++ b/routes/api/auth/index.js @@ -4,40 +4,91 @@ 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); }); -router.delete('/', (req, res) => { - req.logout(); - res.status(204).end(); +/** + * This destroys the session of a user, if they have one. + */ +router.delete('/', authorization.needed(), (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', (err, user) => { - if (err) { - return next(err); - } + passport.authenticate('local', HandleAuthCallback(req, res, next))(req, res, next); +}); - if (!user) { - return next(authorization.ErrNotAuthorized); - } +/** + * Facebook auth endpoint, this will redirect the user immediatly to facebook + * for authorization. + */ +router.get('/facebook', passport.authenticate('facebook')); - // Perform the login of the user! - req.logIn(user, (err) => { - if (err) { - return next(err); - } +/** + * 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) => { - // We logged in the user! Let's send back the user data. - res.json({user}); - }); - })(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; diff --git a/routes/api/comments/index.js b/routes/api/comments/index.js index a2e77b417..bafc7dbcc 100644 --- a/routes/api/comments/index.js +++ b/routes/api/comments/index.js @@ -94,7 +94,7 @@ router.post('/:comment_id', (req, res, next) => { }); router.post('/:comment_id/status', (req, res, next) => { - + Comment .changeStatus(req.params.comment_id, req.body.status) .then(comment => res.status(200).send(comment)) diff --git a/views/auth-callback.ejs b/views/auth-callback.ejs new file mode 100644 index 000000000..d38d759ee --- /dev/null +++ b/views/auth-callback.ejs @@ -0,0 +1,9 @@ + + + + + + From 68433ec973d349d6500169eb593b0093b43d412c Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 11 Nov 2016 14:27:48 -0700 Subject: [PATCH 04/50] Added "display: popup" to the facebook dialog --- routes/api/auth/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/api/auth/index.js b/routes/api/auth/index.js index 2c6f91676..37b41df6b 100644 --- a/routes/api/auth/index.js +++ b/routes/api/auth/index.js @@ -79,7 +79,7 @@ router.post('/local', (req, res, next) => { * Facebook auth endpoint, this will redirect the user immediatly to facebook * for authorization. */ -router.get('/facebook', passport.authenticate('facebook')); +router.get('/facebook', passport.authenticate('facebook', {display: 'popup'})); /** * Facebook callback endpoint, this will send the user a html page designed to From 80e12d2e303b14e7d4ed1e51e6688912fec69276 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 11 Nov 2016 14:59:27 -0700 Subject: [PATCH 05/50] Added photo support --- models/user.js | 2 ++ passport.js | 3 ++- routes/api/auth/index.js | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/models/user.js b/models/user.js index 6800b59ef..2cfb03fac 100644 --- a/models/user.js +++ b/models/user.js @@ -12,6 +12,7 @@ const UserSchema = new mongoose.Schema({ required: true }, displayName: String, + photo: String, disabled: Boolean, password: String, profiles: [{ @@ -139,6 +140,7 @@ UserSchema.statics.findOrCreateExternalUser = function(profile) { user = new User({ displayName: profile.displayName, roles: [], + photo: Array.isArray(profile.photos) && profile.photos.length > 0 ? profile.photos[0].value : null, profiles: [ { id: profile.id, diff --git a/passport.js b/passport.js index 38a92075a..2e084eafd 100644 --- a/passport.js +++ b/passport.js @@ -65,7 +65,8 @@ if (process.env.TALK_FACEBOOK_APP_ID && process.env.TALK_FACEBOOK_APP_SECRET && 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` + callbackURL: `${process.env.TALK_ROOT_URL}/api/v1/auth/facebook/callback`, + profileFields: ['id', 'displayName', 'picture.type(large)'] }, (accessToken, refreshToken, profile, done) => { User .findOrCreateExternalUser(profile) diff --git a/routes/api/auth/index.js b/routes/api/auth/index.js index 37b41df6b..87ffb62c8 100644 --- a/routes/api/auth/index.js +++ b/routes/api/auth/index.js @@ -14,7 +14,7 @@ router.get('/', authorization.needed(), (req, res) => { /** * This destroys the session of a user, if they have one. */ -router.delete('/', authorization.needed(), (req, res) => { +router.delete('/', (req, res) => { req.session.destroy(() => { res.status(204).end(); }); @@ -79,7 +79,7 @@ router.post('/local', (req, res, next) => { * Facebook auth endpoint, this will redirect the user immediatly to facebook * for authorization. */ -router.get('/facebook', passport.authenticate('facebook', {display: 'popup'})); +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 From 103acd4bce2aab37d962a08ff4d8ab0e63ff8f4f Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Fri, 11 Nov 2016 20:18:22 -0300 Subject: [PATCH 06/50] Continue with Facebook Funtionalit --- client/coral-admin/src/components/Comment.js | 2 +- .../src/components/ModerationKeysModal.js | 2 +- .../src/containers/Community/Community.js | 2 +- .../coral-admin/src/containers/Configure.js | 2 +- .../src/containers/ModerationQueue.js | 2 +- .../coral-embed-stream/src/CommentStream.js | 2 + client/coral-framework/actions/auth.js | 61 +++++++++++++++++++ .../{store => }/actions/config.js | 2 - .../{store => }/actions/items.js | 0 .../{store => }/actions/notification.js | 2 - client/coral-framework/constants/auth.js | 9 +++ client/coral-framework/helpers/index.js | 27 ++++++++ client/coral-framework/index.js | 14 ++--- .../{ => modules}/i18n/i18n.js | 2 +- .../notification/Notification.js | 0 client/coral-framework/reducers/auth.js | 22 +++++++ .../{store => }/reducers/config.js | 0 .../{store => }/reducers/index.js | 0 .../{store => }/reducers/items.js | 0 .../{store => }/reducers/notification.js | 0 client/coral-framework/{store => }/store.js | 1 - client/coral-framework/store/actions/auth.js | 17 ------ client/coral-framework/store/reducers/auth.js | 17 ------ .../PermalinkButton.js | 2 +- client/coral-sign-in/components/Button.js | 13 ++++ client/coral-sign-in/components/SignIn.js | 15 +++++ client/coral-sign-in/components/styles.css | 43 +++++++++++++ .../containers/SignInContainer.js | 37 +++++++++++ .../coral-framework/store/authReducer.js | 31 ---------- .../coral-framework/store/itemActions.spec.js | 2 +- .../coral-framework/store/itemReducer.spec.js | 2 +- .../store/notificationReducer.spec.js | 4 +- 32 files changed, 247 insertions(+), 88 deletions(-) create mode 100644 client/coral-framework/actions/auth.js rename client/coral-framework/{store => }/actions/config.js (98%) rename client/coral-framework/{store => }/actions/items.js (100%) rename client/coral-framework/{store => }/actions/notification.js (92%) create mode 100644 client/coral-framework/constants/auth.js create mode 100644 client/coral-framework/helpers/index.js rename client/coral-framework/{ => modules}/i18n/i18n.js (96%) rename client/coral-framework/{ => modules}/notification/Notification.js (100%) create mode 100644 client/coral-framework/reducers/auth.js rename client/coral-framework/{store => }/reducers/config.js (100%) rename client/coral-framework/{store => }/reducers/index.js (100%) rename client/coral-framework/{store => }/reducers/items.js (100%) rename client/coral-framework/{store => }/reducers/notification.js (100%) rename client/coral-framework/{store => }/store.js (99%) delete mode 100644 client/coral-framework/store/actions/auth.js delete mode 100644 client/coral-framework/store/reducers/auth.js create mode 100644 client/coral-sign-in/components/Button.js create mode 100644 client/coral-sign-in/components/SignIn.js create mode 100644 client/coral-sign-in/components/styles.css create mode 100644 client/coral-sign-in/containers/SignInContainer.js delete mode 100644 tests/client/coral-framework/store/authReducer.js diff --git a/client/coral-admin/src/components/Comment.js b/client/coral-admin/src/components/Comment.js index 2b0b7561b..e5a753f80 100644 --- a/client/coral-admin/src/components/Comment.js +++ b/client/coral-admin/src/components/Comment.js @@ -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'; // Render a single comment for the list diff --git a/client/coral-admin/src/components/ModerationKeysModal.js b/client/coral-admin/src/components/ModerationKeysModal.js index 5cf9fe17e..b1282e5d6 100644 --- a/client/coral-admin/src/components/ModerationKeysModal.js +++ b/client/coral-admin/src/components/ModerationKeysModal.js @@ -1,4 +1,4 @@ -import I18n from 'coral-framework/i18n/i18n'; +import I18n from 'coral-framework/modules/i18n/i18n'; import translations from '../translations'; import React from 'react'; import Modal from 'components/Modal'; diff --git a/client/coral-admin/src/containers/Community/Community.js b/client/coral-admin/src/containers/Community/Community.js index 8e0b955a4..5c1bb2e19 100644 --- a/client/coral-admin/src/containers/Community/Community.js +++ b/client/coral-admin/src/containers/Community/Community.js @@ -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'; import {Grid, Cell} from 'react-mdl'; diff --git a/client/coral-admin/src/containers/Configure.js b/client/coral-admin/src/containers/Configure.js index 229c4e948..952e2acd1 100644 --- a/client/coral-admin/src/containers/Configure.js +++ b/client/coral-admin/src/containers/Configure.js @@ -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'; class Configure extends React.Component { diff --git a/client/coral-admin/src/containers/ModerationQueue.js b/client/coral-admin/src/containers/ModerationQueue.js index 9178bda57..a92080c39 100644 --- a/client/coral-admin/src/containers/ModerationQueue.js +++ b/client/coral-admin/src/containers/ModerationQueue.js @@ -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'; /* diff --git a/client/coral-embed-stream/src/CommentStream.js b/client/coral-embed-stream/src/CommentStream.js index bfccf7cfc..31457787a 100644 --- a/client/coral-embed-stream/src/CommentStream.js +++ b/client/coral-embed-stream/src/CommentStream.js @@ -15,6 +15,7 @@ import {ReplyBox, ReplyButton} from '../../coral-plugin-replies'; import Pym from 'pym.js'; import FlagButton from '../../coral-plugin-flags/FlagButton'; import PermalinkButton from '../../coral-plugin-permalinks/PermalinkButton'; +import SignInContainer from '../../coral-sign-in/containers/SignInContainer'; const {addItem, updateItem, postItem, getStream, postAction, appendItemArray} = itemActions; const {addNotification, clearNotification} = notificationActions; @@ -111,6 +112,7 @@ class CommentStream extends Component { id={rootItemId} premod={this.props.config.moderation} reply={false}/> + { rootItem.comments.map((commentId) => { diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js new file mode 100644 index 000000000..9b1053161 --- /dev/null +++ b/client/coral-framework/actions/auth.js @@ -0,0 +1,61 @@ +import * as actions from '../constants/auth'; +import {base, handleResp} from '../helpers'; + +export const loginFacebook = () => dispatch => { + dispatch(loginFacebookRequest()); + window.open( + `${base}/auth/facebook`, + 'Continue with Facebook', + 'menubar=0,resizable=0,width=500,height=500,top=200,left=500' + ); +}; + +export const loginFacebookCallback = (err, data) => { + let user; + + if (err) { + console.error(err); + return; + } + + try { + user = JSON.parse(data); + } catch (err) { + console.error('Can\'t parse the user json', err); + return; + } + + console.log('User was loaded!', user); +}; + +export const logout = () => dispatch => { + dispatch(logoutRequest()); + fetch(`${base}/api/v1/auth`, { + method: 'DELETE', + credentials: 'same-origin' + }) + .then(handleResp) + .then(() => dispatch(dispatch(logoutSuccess()))) + .catch(error => dispatch(logoutFailure(error))); +}; + +const logoutRequest = () => ({ + type: actions.USER_LOGOUT_SUCCESS +}); + +const logoutSuccess = () => ({ + type: actions.USER_LOGOUT_SUCCESS +}); + +const logoutFailure = (error) => ({ + type: actions.USER_LOGOUT_SUCCESS, + error +}); + +export const loginFacebookRequest = () => ({ + type: actions.USER_FACEBOOK_LOGIN_REQUEST +}); + +export const loginRequest = () => ({ + type: actions.USER_SIGNIN_REQUEST +}); diff --git a/client/coral-framework/store/actions/config.js b/client/coral-framework/actions/config.js similarity index 98% rename from client/coral-framework/store/actions/config.js rename to client/coral-framework/actions/config.js index f131b8c82..d8fd886be 100644 --- a/client/coral-framework/store/actions/config.js +++ b/client/coral-framework/actions/config.js @@ -1,5 +1,3 @@ -/* @flow */ - import {fromJS} from 'immutable'; /** diff --git a/client/coral-framework/store/actions/items.js b/client/coral-framework/actions/items.js similarity index 100% rename from client/coral-framework/store/actions/items.js rename to client/coral-framework/actions/items.js diff --git a/client/coral-framework/store/actions/notification.js b/client/coral-framework/actions/notification.js similarity index 92% rename from client/coral-framework/store/actions/notification.js rename to client/coral-framework/actions/notification.js index c34adc414..4edc17601 100644 --- a/client/coral-framework/store/actions/notification.js +++ b/client/coral-framework/actions/notification.js @@ -1,5 +1,3 @@ -/* Notification Actions */ - export const ADD_NOTIFICATION = 'ADD_NOTIFICATION'; export const CLEAR_NOTIFICATION = 'CLEAR_NOTIFICATION'; diff --git a/client/coral-framework/constants/auth.js b/client/coral-framework/constants/auth.js new file mode 100644 index 000000000..7f93a51fe --- /dev/null +++ b/client/coral-framework/constants/auth.js @@ -0,0 +1,9 @@ +export const USER_LOGIN_REQUEST = 'USER_LOGIN_REQUEST'; +export const USER_LOGIN_SUCCESS = 'USER_LOGIN_SUCCESS'; +export const USER_LOGIN_FAILURE = 'USER_LOGIN_FAILURE'; +export const USER_FACEBOOK_LOGIN_REQUEST = 'USER_FACEBOOK_LOGIN_REQUEST'; +export const USER_FACEBOOK_SUCCESS = 'USER_FACEBOOK_SUCCESS'; +export const USER_FACEBOOK_FAILURE = 'USER_FACEBOOK_FAILURE'; +export const USER_LOGOUT_REQUEST = 'USER_LOGOUT_REQUEST'; +export const USER_LOGOUT_SUCCESS = 'USER_LOGOUT_SUCCESS'; +export const USER_LOGOUT_FAILURE = 'USER_LOGOUT_FAILURE'; diff --git a/client/coral-framework/helpers/index.js b/client/coral-framework/helpers/index.js new file mode 100644 index 000000000..439ba5a8a --- /dev/null +++ b/client/coral-framework/helpers/index.js @@ -0,0 +1,27 @@ +export const base = '/api/v1'; + +export const getInit = (method, body) => { + const headers = new Headers({ + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }); + + const init = {method, headers}; + 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(); + } +}; diff --git a/client/coral-framework/index.js b/client/coral-framework/index.js index 118935c50..4c6ae741f 100644 --- a/client/coral-framework/index.js +++ b/client/coral-framework/index.js @@ -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, diff --git a/client/coral-framework/i18n/i18n.js b/client/coral-framework/modules/i18n/i18n.js similarity index 96% rename from client/coral-framework/i18n/i18n.js rename to client/coral-framework/modules/i18n/i18n.js index 43144f3a6..afe8606d1 100644 --- a/client/coral-framework/i18n/i18n.js +++ b/client/coral-framework/modules/i18n/i18n.js @@ -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 diff --git a/client/coral-framework/notification/Notification.js b/client/coral-framework/modules/notification/Notification.js similarity index 100% rename from client/coral-framework/notification/Notification.js rename to client/coral-framework/modules/notification/Notification.js diff --git a/client/coral-framework/reducers/auth.js b/client/coral-framework/reducers/auth.js new file mode 100644 index 000000000..c585ea237 --- /dev/null +++ b/client/coral-framework/reducers/auth.js @@ -0,0 +1,22 @@ +import {Map} from 'immutable'; +// +//import { +// FETCH_COMMENTERS_REQUEST, +// FETCH_COMMENTERS_FAILURE, +// FETCH_COMMENTERS_SUCCESS, +// SORT_UPDATE +//} from '../constants/community'; +// +const initialState = Map({ + auth: Map(), + loggedIn: false, + error: '', + user: {} +}); + +export default function community (state = initialState, action) { + switch (action.type) { + default : + return state; + } +} diff --git a/client/coral-framework/store/reducers/config.js b/client/coral-framework/reducers/config.js similarity index 100% rename from client/coral-framework/store/reducers/config.js rename to client/coral-framework/reducers/config.js diff --git a/client/coral-framework/store/reducers/index.js b/client/coral-framework/reducers/index.js similarity index 100% rename from client/coral-framework/store/reducers/index.js rename to client/coral-framework/reducers/index.js diff --git a/client/coral-framework/store/reducers/items.js b/client/coral-framework/reducers/items.js similarity index 100% rename from client/coral-framework/store/reducers/items.js rename to client/coral-framework/reducers/items.js diff --git a/client/coral-framework/store/reducers/notification.js b/client/coral-framework/reducers/notification.js similarity index 100% rename from client/coral-framework/store/reducers/notification.js rename to client/coral-framework/reducers/notification.js diff --git a/client/coral-framework/store/store.js b/client/coral-framework/store.js similarity index 99% rename from client/coral-framework/store/store.js rename to client/coral-framework/store.js index 33cc9efb5..8c3f9edc1 100644 --- a/client/coral-framework/store/store.js +++ b/client/coral-framework/store.js @@ -1,4 +1,3 @@ - import {createStore, applyMiddleware} from 'redux'; import thunk from 'redux-thunk'; import mainReducer from './reducers'; diff --git a/client/coral-framework/store/actions/auth.js b/client/coral-framework/store/actions/auth.js deleted file mode 100644 index af1e3cb34..000000000 --- a/client/coral-framework/store/actions/auth.js +++ /dev/null @@ -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 - }; -}; diff --git a/client/coral-framework/store/reducers/auth.js b/client/coral-framework/store/reducers/auth.js deleted file mode 100644 index b79444cc9..000000000 --- a/client/coral-framework/store/reducers/auth.js +++ /dev/null @@ -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; - } -}; diff --git a/client/coral-plugin-permalinks/PermalinkButton.js b/client/coral-plugin-permalinks/PermalinkButton.js index 978a6dd03..2372e2035 100644 --- a/client/coral-plugin-permalinks/PermalinkButton.js +++ b/client/coral-plugin-permalinks/PermalinkButton.js @@ -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'; diff --git a/client/coral-sign-in/components/Button.js b/client/coral-sign-in/components/Button.js new file mode 100644 index 000000000..fd8cc85c7 --- /dev/null +++ b/client/coral-sign-in/components/Button.js @@ -0,0 +1,13 @@ +import React from 'react'; +import styles from './styles.css'; + +const Button = ({type = 'local', children, ...props}) => ( + +); + +export default Button; diff --git a/client/coral-sign-in/components/SignIn.js b/client/coral-sign-in/components/SignIn.js new file mode 100644 index 000000000..61f1e387e --- /dev/null +++ b/client/coral-sign-in/components/SignIn.js @@ -0,0 +1,15 @@ +import React from 'react'; +import Button from './Button'; + +const SignIn = ({openFacebookWindow, logout}) => ( +
+ + + Logout + +
+); + +export default SignIn; diff --git a/client/coral-sign-in/components/styles.css b/client/coral-sign-in/components/styles.css new file mode 100644 index 000000000..6dbbd5b2f --- /dev/null +++ b/client/coral-sign-in/components/styles.css @@ -0,0 +1,43 @@ +.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; + text-transform: uppercase; + 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; +} + +.local { + background: #E0E0E0; + color: #212121; +} + +.facebook { + background-color: #4267b2; + border-color: #4267b2; + color: rgb(255, 255, 255); +} + +.facebook:hover { + background-color: #365899; + border-color: #365899; +} \ No newline at end of file diff --git a/client/coral-sign-in/containers/SignInContainer.js b/client/coral-sign-in/containers/SignInContainer.js new file mode 100644 index 000000000..72b46536e --- /dev/null +++ b/client/coral-sign-in/containers/SignInContainer.js @@ -0,0 +1,37 @@ +import React, {Component} from 'react'; +import {connect} from 'react-redux'; + +import SignIn from '../components/SignIn'; + +import { + loginFacebookCallback, + loginFacebook, + logout +} from '../../coral-framework/actions/auth'; + +class SignInContainer extends Component { + constructor(props) { + super(props); + + this.openFacebookWindow = this.openFacebookWindow.bind(this); + this.logout = this.logout.bind(this); + } + + componentDidMount() { + window.authCallback = loginFacebookCallback; + } + + openFacebookWindow() { + this.props.dispatch(loginFacebook()); + } + + logout() { + this.props.dispatch(logout()); + } + + render() { + return ; + } +} + +export default connect(({auth}) => ({auth}))(SignInContainer); diff --git a/tests/client/coral-framework/store/authReducer.js b/tests/client/coral-framework/store/authReducer.js deleted file mode 100644 index aaebe1897..000000000 --- a/tests/client/coral-framework/store/authReducer.js +++ /dev/null @@ -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); - }); - }); -}); diff --git a/tests/client/coral-framework/store/itemActions.spec.js b/tests/client/coral-framework/store/itemActions.spec.js index d45212794..e50b82344 100644 --- a/tests/client/coral-framework/store/itemActions.spec.js +++ b/tests/client/coral-framework/store/itemActions.spec.js @@ -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'; diff --git a/tests/client/coral-framework/store/itemReducer.spec.js b/tests/client/coral-framework/store/itemReducer.spec.js index 9aea845f3..16377327b 100644 --- a/tests/client/coral-framework/store/itemReducer.spec.js +++ b/tests/client/coral-framework/store/itemReducer.spec.js @@ -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', () => { diff --git a/tests/client/coral-framework/store/notificationReducer.spec.js b/tests/client/coral-framework/store/notificationReducer.spec.js index 60b944a33..960034119 100644 --- a/tests/client/coral-framework/store/notificationReducer.spec.js +++ b/tests/client/coral-framework/store/notificationReducer.spec.js @@ -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', () => { From 984b2acac49d882405d90517f561ad28744f15f4 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Fri, 11 Nov 2016 22:38:15 -0300 Subject: [PATCH 07/50] =?UTF-8?q?=C3=81dding=20Traslations=20and=20Same=20?= =?UTF-8?q?Origin=20Requests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/coral-framework/actions/auth.js | 9 +++------ client/coral-framework/helpers/index.js | 13 ++++++++----- client/coral-sign-in/components/SignIn.js | 8 ++++++-- client/coral-sign-in/translations.js | 14 ++++++++++++++ 4 files changed, 31 insertions(+), 13 deletions(-) create mode 100644 client/coral-sign-in/translations.js diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js index 9b1053161..fb5e8d3d7 100644 --- a/client/coral-framework/actions/auth.js +++ b/client/coral-framework/actions/auth.js @@ -1,5 +1,5 @@ import * as actions from '../constants/auth'; -import {base, handleResp} from '../helpers'; +import {base, handleResp, getInit} from '../helpers'; export const loginFacebook = () => dispatch => { dispatch(loginFacebookRequest()); @@ -30,10 +30,7 @@ export const loginFacebookCallback = (err, data) => { export const logout = () => dispatch => { dispatch(logoutRequest()); - fetch(`${base}/api/v1/auth`, { - method: 'DELETE', - credentials: 'same-origin' - }) + fetch(`${base}/auth`, getInit('DELETE')) .then(handleResp) .then(() => dispatch(dispatch(logoutSuccess()))) .catch(error => dispatch(logoutFailure(error))); @@ -44,7 +41,7 @@ const logoutRequest = () => ({ }); const logoutSuccess = () => ({ - type: actions.USER_LOGOUT_SUCCESS + type: actions.USER_LOGOUT_SUCCESS, }); const logoutFailure = (error) => ({ diff --git a/client/coral-framework/helpers/index.js b/client/coral-framework/helpers/index.js index 439ba5a8a..bccfc5a04 100644 --- a/client/coral-framework/helpers/index.js +++ b/client/coral-framework/helpers/index.js @@ -1,12 +1,15 @@ export const base = '/api/v1'; export const getInit = (method, body) => { - const headers = new Headers({ - 'Content-Type': 'application/json', - 'Accept': 'application/json' - }); + let init = { + method, + headers: new Headers({ + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }), + credentials: 'same-origin' + }; - const init = {method, headers}; if (method.toLowerCase() !== 'get') { init.body = JSON.stringify(body); } diff --git a/client/coral-sign-in/components/SignIn.js b/client/coral-sign-in/components/SignIn.js index 61f1e387e..52b6ba19d 100644 --- a/client/coral-sign-in/components/SignIn.js +++ b/client/coral-sign-in/components/SignIn.js @@ -1,13 +1,17 @@ import React from 'react'; import Button from './Button'; +import I18n from 'coral-framework/modules/i18n/i18n'; +import translations from '../translations'; + +const lang = new I18n(translations); const SignIn = ({openFacebookWindow, logout}) => (
- Logout + {lang.t('signIn.logout')}
); diff --git a/client/coral-sign-in/translations.js b/client/coral-sign-in/translations.js new file mode 100644 index 000000000..16e3a0425 --- /dev/null +++ b/client/coral-sign-in/translations.js @@ -0,0 +1,14 @@ +export default { + en: { + 'signIn': { + facebookLogin: 'Continue with Facebook', + logout: 'Logout' + } + }, + es: { + 'signIn': { + facebookLogin: 'Continuar con Facebook', + logout: 'Salir' + } + } +}; From f14353d464b184f931cdf3d061ad0198eee41660 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Sat, 12 Nov 2016 00:12:54 -0300 Subject: [PATCH 08/50] SignIn Dialog --- client/coral-framework/actions/auth.js | 8 +++ client/coral-framework/constants/auth.js | 2 + client/coral-framework/reducers/auth.js | 23 ++++--- client/coral-sign-in/components/SignIn.js | 15 ++--- .../coral-sign-in/components/SignInDialog.js | 37 +++++++++++ client/coral-sign-in/components/styles.css | 5 ++ .../containers/SignInContainer.js | 23 ++++++- client/coral-ui/components/Dialog.js | 62 +++++++++++++++++++ client/coral-ui/index.js | 1 + package.json | 17 ++++- 10 files changed, 168 insertions(+), 25 deletions(-) create mode 100644 client/coral-sign-in/components/SignInDialog.js create mode 100644 client/coral-ui/components/Dialog.js create mode 100644 client/coral-ui/index.js diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js index fb5e8d3d7..805f9f412 100644 --- a/client/coral-framework/actions/auth.js +++ b/client/coral-framework/actions/auth.js @@ -1,6 +1,14 @@ import * as actions from '../constants/auth'; import {base, handleResp, getInit} from '../helpers'; +export const showSignInDialog = () => ({ + type: actions.SHOW_SIGNIN_DIALOG +}); + +export const hideSignInDialog = () => ({ + type: actions.HIDE_SIGNIN_DIALOG +}); + export const loginFacebook = () => dispatch => { dispatch(loginFacebookRequest()); window.open( diff --git a/client/coral-framework/constants/auth.js b/client/coral-framework/constants/auth.js index 7f93a51fe..c69bad344 100644 --- a/client/coral-framework/constants/auth.js +++ b/client/coral-framework/constants/auth.js @@ -7,3 +7,5 @@ export const USER_FACEBOOK_FAILURE = 'USER_FACEBOOK_FAILURE'; export const USER_LOGOUT_REQUEST = 'USER_LOGOUT_REQUEST'; export const USER_LOGOUT_SUCCESS = 'USER_LOGOUT_SUCCESS'; export const USER_LOGOUT_FAILURE = 'USER_LOGOUT_FAILURE'; +export const SHOW_SIGNIN_DIALOG = 'SHOW_SIGNIN_DIALOG'; +export const HIDE_SIGNIN_DIALOG = 'HIDE_SIGNIN_DIALOG'; \ No newline at end of file diff --git a/client/coral-framework/reducers/auth.js b/client/coral-framework/reducers/auth.js index c585ea237..a6c4369f9 100644 --- a/client/coral-framework/reducers/auth.js +++ b/client/coral-framework/reducers/auth.js @@ -1,21 +1,26 @@ import {Map} from 'immutable'; -// -//import { -// FETCH_COMMENTERS_REQUEST, -// FETCH_COMMENTERS_FAILURE, -// FETCH_COMMENTERS_SUCCESS, -// SORT_UPDATE -//} from '../constants/community'; -// + +import { + SHOW_SIGNIN_DIALOG, + HIDE_SIGNIN_DIALOG +} from '../constants/auth'; + const initialState = Map({ auth: Map(), loggedIn: false, error: '', - user: {} + user: {}, + showSignInDialog: false }); export default function community (state = initialState, action) { switch (action.type) { + case SHOW_SIGNIN_DIALOG : + return state + .set('showSignInDialog', true) + case HIDE_SIGNIN_DIALOG : + return state + .set('showSignInDialog', false) default : return state; } diff --git a/client/coral-sign-in/components/SignIn.js b/client/coral-sign-in/components/SignIn.js index 52b6ba19d..ad0fb5ca3 100644 --- a/client/coral-sign-in/components/SignIn.js +++ b/client/coral-sign-in/components/SignIn.js @@ -1,18 +1,11 @@ import React from 'react'; -import Button from './Button'; -import I18n from 'coral-framework/modules/i18n/i18n'; -import translations from '../translations'; -const lang = new I18n(translations); - -const SignIn = ({openFacebookWindow, logout}) => ( +const SignIn = ({openSignInDialog, children}) => (
- - - {lang.t('signIn.logout')} - + {children}
); diff --git a/client/coral-sign-in/components/SignInDialog.js b/client/coral-sign-in/components/SignInDialog.js new file mode 100644 index 000000000..f4f089a19 --- /dev/null +++ b/client/coral-sign-in/components/SignInDialog.js @@ -0,0 +1,37 @@ +import React, {Component} from 'react'; +import { Dialog } from 'coral-ui'; +import styles from './styles.css'; +import Button from './Button'; +import I18n from 'coral-framework/modules/i18n/i18n'; +import translations from '../translations'; +const lang = new I18n(translations); + +const SignInDialog = ({open, openFacebookWindow}) => ( + +

Sign In

+ +
+
+ + + + Forgot Password + Need an Account? Register +
+
+); + +export default SignInDialog; diff --git a/client/coral-sign-in/components/styles.css b/client/coral-sign-in/components/styles.css index 6dbbd5b2f..594b2e701 100644 --- a/client/coral-sign-in/components/styles.css +++ b/client/coral-sign-in/components/styles.css @@ -40,4 +40,9 @@ .facebook:hover { background-color: #365899; border-color: #365899; +} + +.dialog { + width: 400px; + border: none; } \ No newline at end of file diff --git a/client/coral-sign-in/containers/SignInContainer.js b/client/coral-sign-in/containers/SignInContainer.js index 72b46536e..4df96c49a 100644 --- a/client/coral-sign-in/containers/SignInContainer.js +++ b/client/coral-sign-in/containers/SignInContainer.js @@ -2,11 +2,14 @@ import React, {Component} from 'react'; import {connect} from 'react-redux'; import SignIn from '../components/SignIn'; +import SignInDialog from '../components/SignInDialog'; +import Button from '../components/Button'; import { loginFacebookCallback, loginFacebook, - logout + logout, + showSignInDialog } from '../../coral-framework/actions/auth'; class SignInContainer extends Component { @@ -14,6 +17,7 @@ class SignInContainer extends Component { super(props); this.openFacebookWindow = this.openFacebookWindow.bind(this); + this.openSignInDialog = this.openSignInDialog.bind(this); this.logout = this.logout.bind(this); } @@ -29,8 +33,23 @@ class SignInContainer extends Component { this.props.dispatch(logout()); } + openSignInDialog() { + this.props.dispatch(showSignInDialog()); + } + render() { - return ; + const {auth} = this.props; + return ( +
+ + +
+ ); } } diff --git a/client/coral-ui/components/Dialog.js b/client/coral-ui/components/Dialog.js new file mode 100644 index 000000000..54f0723e6 --- /dev/null +++ b/client/coral-ui/components/Dialog.js @@ -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; + + return ( + + {children} + + ) + } +} \ No newline at end of file diff --git a/client/coral-ui/index.js b/client/coral-ui/index.js new file mode 100644 index 000000000..3d76a5912 --- /dev/null +++ b/client/coral-ui/index.js @@ -0,0 +1 @@ +export { default as Dialog } from './components/Dialog'; \ No newline at end of file diff --git a/package.json b/package.json index 6312e51ff..4341483af 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,13 @@ "config": { "pre-git": { "commit-msg": [], - "pre-commit": ["npm run lint", "npm test"], - "pre-push": ["npm test"], + "pre-commit": [ + "npm run lint", + "npm test" + ], + "pre-push": [ + "npm test" + ], "post-commit": [], "post-merge": [] } @@ -26,7 +31,12 @@ "type": "git", "url": "git+https://github.com/coralproject/talk.git" }, - "keywords": ["talk", "coral", "coralproject", "ask"], + "keywords": [ + "talk", + "coral", + "coralproject", + "ask" + ], "author": "", "license": "Apache-2.0", "bugs": { @@ -72,6 +82,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", From d6dc3a8ba1163d8c96adbdc172a0e5519c7c0e29 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Sat, 12 Nov 2016 12:21:33 -0300 Subject: [PATCH 09/50] Sign up steps and Login --- client/coral-framework/actions/auth.js | 25 +++++++ client/coral-framework/constants/auth.js | 6 +- client/coral-framework/reducers/auth.js | 13 ++-- client/coral-sign-in/components/Button.js | 4 +- client/coral-sign-in/components/SignDialog.js | 15 +++++ client/coral-sign-in/components/SignIn.js | 12 ---- .../coral-sign-in/components/SignInContent.js | 41 ++++++++++++ .../coral-sign-in/components/SignInDialog.js | 37 ---------- .../coral-sign-in/components/SignUpContent.js | 48 +++++++++++++ client/coral-sign-in/components/styles.css | 67 +++++++++++++++++-- .../containers/SignInContainer.js | 15 +++-- client/coral-sign-in/translations.js | 32 +++++++-- client/coral-ui/components/Dialog.js | 6 +- client/coral-ui/index.js | 2 +- 14 files changed, 251 insertions(+), 72 deletions(-) create mode 100644 client/coral-sign-in/components/SignDialog.js delete mode 100644 client/coral-sign-in/components/SignIn.js create mode 100644 client/coral-sign-in/components/SignInContent.js delete mode 100644 client/coral-sign-in/components/SignInDialog.js create mode 100644 client/coral-sign-in/components/SignUpContent.js diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js index 805f9f412..b77b00003 100644 --- a/client/coral-framework/actions/auth.js +++ b/client/coral-framework/actions/auth.js @@ -64,3 +64,28 @@ export const loginFacebookRequest = () => ({ export const loginRequest = () => ({ type: actions.USER_SIGNIN_REQUEST }); + +const showSignInForm = () => ({ + type: actions.SHOW_SIGNIN_FORM +}); + +const showSignUpForm = () => ({ + type: actions.SHOW_SIGNUP_FORM +}); + +const newStep = (step) => ({ + type: actions.NEW_STEP, + step +}); + +export const goTo = (step) => dispatch => { + dispatch(newStep(step)); + switch (step) { + case 1 : + return dispatch(showSignInForm()); + case 2 : + return dispatch(showSignUpForm()); + default : + return dispatch(showSignInForm()); + } +}; diff --git a/client/coral-framework/constants/auth.js b/client/coral-framework/constants/auth.js index c69bad344..476e4f225 100644 --- a/client/coral-framework/constants/auth.js +++ b/client/coral-framework/constants/auth.js @@ -8,4 +8,8 @@ export const USER_LOGOUT_REQUEST = 'USER_LOGOUT_REQUEST'; export const USER_LOGOUT_SUCCESS = 'USER_LOGOUT_SUCCESS'; export const USER_LOGOUT_FAILURE = 'USER_LOGOUT_FAILURE'; export const SHOW_SIGNIN_DIALOG = 'SHOW_SIGNIN_DIALOG'; -export const HIDE_SIGNIN_DIALOG = 'HIDE_SIGNIN_DIALOG'; \ No newline at end of file +export const HIDE_SIGNIN_DIALOG = 'HIDE_SIGNIN_DIALOG'; + +export const NEW_STEP = 'NEW_STEP'; +export const SHOW_SIGNIN_FORM = 'SHOW_SIGNIN_FORM'; +export const SHOW_SIGNUP_FORM = 'SHOW_SIGNUP_FORM'; diff --git a/client/coral-framework/reducers/auth.js b/client/coral-framework/reducers/auth.js index a6c4369f9..babbe20d1 100644 --- a/client/coral-framework/reducers/auth.js +++ b/client/coral-framework/reducers/auth.js @@ -2,7 +2,8 @@ import {Map} from 'immutable'; import { SHOW_SIGNIN_DIALOG, - HIDE_SIGNIN_DIALOG + HIDE_SIGNIN_DIALOG, + NEW_STEP } from '../constants/auth'; const initialState = Map({ @@ -10,17 +11,21 @@ const initialState = Map({ loggedIn: false, error: '', user: {}, - showSignInDialog: false + showSignInDialog: false, + step: 1 }); export default function community (state = initialState, action) { switch (action.type) { case SHOW_SIGNIN_DIALOG : return state - .set('showSignInDialog', true) + .set('showSignInDialog', true); case HIDE_SIGNIN_DIALOG : return state - .set('showSignInDialog', false) + .set('showSignInDialog', false); + case NEW_STEP : + return state + .set('step', action.step); default : return state; } diff --git a/client/coral-sign-in/components/Button.js b/client/coral-sign-in/components/Button.js index fd8cc85c7..3f736ac3d 100644 --- a/client/coral-sign-in/components/Button.js +++ b/client/coral-sign-in/components/Button.js @@ -1,9 +1,9 @@ import React from 'react'; import styles from './styles.css'; -const Button = ({type = 'local', children, ...props}) => ( +const Button = ({type = 'local', children, className, ...props}) => ( - {children} - -); - -export default SignIn; diff --git a/client/coral-sign-in/components/SignInContent.js b/client/coral-sign-in/components/SignInContent.js new file mode 100644 index 000000000..f8893fb25 --- /dev/null +++ b/client/coral-sign-in/components/SignInContent.js @@ -0,0 +1,41 @@ +import React from 'react'; +import styles from './styles.css'; +import Button from './Button'; +import I18n from 'coral-framework/modules/i18n/i18n'; +import translations from '../translations'; +const lang = new I18n(translations); + +const SignInContent = ({openFacebookWindow, goTo}) => ( +
+
+

{lang.t('signIn.signIn')}

+
+
+ +
+
+

{lang.t('signIn.or')}

+
+
+
+ + +
+
+ + +
+ +
+ +
+); + +export default SignInContent; diff --git a/client/coral-sign-in/components/SignInDialog.js b/client/coral-sign-in/components/SignInDialog.js deleted file mode 100644 index f4f089a19..000000000 --- a/client/coral-sign-in/components/SignInDialog.js +++ /dev/null @@ -1,37 +0,0 @@ -import React, {Component} from 'react'; -import { Dialog } from 'coral-ui'; -import styles from './styles.css'; -import Button from './Button'; -import I18n from 'coral-framework/modules/i18n/i18n'; -import translations from '../translations'; -const lang = new I18n(translations); - -const SignInDialog = ({open, openFacebookWindow}) => ( - -

Sign In

- -
-
- - - - Forgot Password - Need an Account? Register -
-
-); - -export default SignInDialog; diff --git a/client/coral-sign-in/components/SignUpContent.js b/client/coral-sign-in/components/SignUpContent.js new file mode 100644 index 000000000..2568797a7 --- /dev/null +++ b/client/coral-sign-in/components/SignUpContent.js @@ -0,0 +1,48 @@ +import React from 'react'; +import styles from './styles.css'; +import Button from './Button'; +import I18n from 'coral-framework/modules/i18n/i18n'; +import translations from '../translations'; +const lang = new I18n(translations); + +const SignUpContent = ({goTo, openFacebookWindow}) => ( +
+
+

{lang.t('signIn.signUp')}

+
+
+ +
+
+

{lang.t('signIn.or')}

+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ {lang.t('signIn.alreadyHaveAnAccount')} goTo(1)}>{lang.t('signIn.signIn')} +
+
+); + +export default SignUpContent; diff --git a/client/coral-sign-in/components/styles.css b/client/coral-sign-in/components/styles.css index 594b2e701..b289ac8fb 100644 --- a/client/coral-sign-in/components/styles.css +++ b/client/coral-sign-in/components/styles.css @@ -12,7 +12,6 @@ font-family: 'Roboto','Helvetica','Arial',sans-serif; font-size: 14px; font-weight: 500; - text-transform: uppercase; letter-spacing: 0; overflow: hidden; will-change: box-shadow,transform; @@ -24,20 +23,27 @@ text-align: center; line-height: 36px; vertical-align: middle; + width: 100%; + margin: 0; } -.local { +.type--black { + color: #E0E0E0; + background: #212121; +} + +.type--local { background: #E0E0E0; color: #212121; } -.facebook { +.type--facebook { background-color: #4267b2; border-color: #4267b2; color: rgb(255, 255, 255); } -.facebook:hover { +.type--facebook:hover { background-color: #365899; border-color: #365899; } @@ -45,4 +51,57 @@ .dialog { width: 400px; border: none; + 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; +} + +.header { + margin-bottom: 20px; +} + +.header h1, .separator h1{ + text-align: center; + font-size: 1.2em; +} + +.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; +} + +.socialConnections { + margin-bottom: 20px; +} + +.signInButton { + margin-top: 10px; } \ No newline at end of file diff --git a/client/coral-sign-in/containers/SignInContainer.js b/client/coral-sign-in/containers/SignInContainer.js index 4df96c49a..7c7bd7775 100644 --- a/client/coral-sign-in/containers/SignInContainer.js +++ b/client/coral-sign-in/containers/SignInContainer.js @@ -1,15 +1,15 @@ import React, {Component} from 'react'; import {connect} from 'react-redux'; -import SignIn from '../components/SignIn'; -import SignInDialog from '../components/SignInDialog'; +import SignDialog from '../components/SignDialog'; import Button from '../components/Button'; import { loginFacebookCallback, loginFacebook, logout, - showSignInDialog + showSignInDialog, + goTo, } from '../../coral-framework/actions/auth'; class SignInContainer extends Component { @@ -19,6 +19,7 @@ class SignInContainer extends Component { this.openFacebookWindow = this.openFacebookWindow.bind(this); this.openSignInDialog = this.openSignInDialog.bind(this); this.logout = this.logout.bind(this); + this.goTo = this.goTo.bind(this); } componentDidMount() { @@ -37,6 +38,10 @@ class SignInContainer extends Component { this.props.dispatch(showSignInDialog()); } + goTo(step = 1) { + this.props.dispatch(goTo(step)); + } + render() { const {auth} = this.props; return ( @@ -44,9 +49,11 @@ class SignInContainer extends Component { - ); diff --git a/client/coral-sign-in/translations.js b/client/coral-sign-in/translations.js index 16e3a0425..1f11935bd 100644 --- a/client/coral-sign-in/translations.js +++ b/client/coral-sign-in/translations.js @@ -1,14 +1,38 @@ export default { en: { 'signIn': { - facebookLogin: 'Continue with Facebook', - logout: 'Logout' + 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', + username: 'Username', + alreadyHaveAnAccount: 'Already have an account?' } }, es: { 'signIn': { - facebookLogin: 'Continuar con Facebook', - logout: 'Salir' + 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', + username: 'Usuario', + alreadyHaveAnAccount: 'Ya tienes una cuenta?' } } }; diff --git a/client/coral-ui/components/Dialog.js b/client/coral-ui/components/Dialog.js index 54f0723e6..3f6585297 100644 --- a/client/coral-ui/components/Dialog.js +++ b/client/coral-ui/components/Dialog.js @@ -47,7 +47,7 @@ export default class Dialog extends Component { } render() { - const { children, className = '', onClose, onCancel, open, ...rest } = this.props; + const {children, className = '', onClose, onCancel, open, ...rest} = this.props; // eslint-disable-line return ( {children} - ) + ); } -} \ No newline at end of file +} diff --git a/client/coral-ui/index.js b/client/coral-ui/index.js index 3d76a5912..5116a16dd 100644 --- a/client/coral-ui/index.js +++ b/client/coral-ui/index.js @@ -1 +1 @@ -export { default as Dialog } from './components/Dialog'; \ No newline at end of file +export {default as Dialog} from './components/Dialog'; From eb752e6f734fdd6a12e63dc7e55a9f05b4d82d9b Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Sat, 12 Nov 2016 12:38:02 -0300 Subject: [PATCH 10/50] Handling Views --- client/coral-framework/actions/auth.js | 30 ++++--------------- client/coral-framework/constants/auth.js | 5 ++-- client/coral-framework/reducers/auth.js | 8 ++--- client/coral-sign-in/components/SignDialog.js | 6 ++-- .../coral-sign-in/components/SignInContent.js | 4 +-- .../coral-sign-in/components/SignUpContent.js | 4 +-- .../containers/SignInContainer.js | 12 ++++---- 7 files changed, 25 insertions(+), 44 deletions(-) diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js index b77b00003..0a70da7ba 100644 --- a/client/coral-framework/actions/auth.js +++ b/client/coral-framework/actions/auth.js @@ -52,7 +52,7 @@ const logoutSuccess = () => ({ type: actions.USER_LOGOUT_SUCCESS, }); -const logoutFailure = (error) => ({ +const logoutFailure = error => ({ type: actions.USER_LOGOUT_SUCCESS, error }); @@ -65,27 +65,9 @@ export const loginRequest = () => ({ type: actions.USER_SIGNIN_REQUEST }); -const showSignInForm = () => ({ - type: actions.SHOW_SIGNIN_FORM -}); +export const changeView = view => dispatch => + dispatch({ + type: actions.CHANGE_VIEW, + view + }); -const showSignUpForm = () => ({ - type: actions.SHOW_SIGNUP_FORM -}); - -const newStep = (step) => ({ - type: actions.NEW_STEP, - step -}); - -export const goTo = (step) => dispatch => { - dispatch(newStep(step)); - switch (step) { - case 1 : - return dispatch(showSignInForm()); - case 2 : - return dispatch(showSignUpForm()); - default : - return dispatch(showSignInForm()); - } -}; diff --git a/client/coral-framework/constants/auth.js b/client/coral-framework/constants/auth.js index 476e4f225..f590fce12 100644 --- a/client/coral-framework/constants/auth.js +++ b/client/coral-framework/constants/auth.js @@ -7,9 +7,8 @@ export const USER_FACEBOOK_FAILURE = 'USER_FACEBOOK_FAILURE'; export const USER_LOGOUT_REQUEST = 'USER_LOGOUT_REQUEST'; export const USER_LOGOUT_SUCCESS = 'USER_LOGOUT_SUCCESS'; export const USER_LOGOUT_FAILURE = 'USER_LOGOUT_FAILURE'; + export const SHOW_SIGNIN_DIALOG = 'SHOW_SIGNIN_DIALOG'; export const HIDE_SIGNIN_DIALOG = 'HIDE_SIGNIN_DIALOG'; -export const NEW_STEP = 'NEW_STEP'; -export const SHOW_SIGNIN_FORM = 'SHOW_SIGNIN_FORM'; -export const SHOW_SIGNUP_FORM = 'SHOW_SIGNUP_FORM'; +export const CHANGE_VIEW = 'CHANGE_VIEW'; diff --git a/client/coral-framework/reducers/auth.js b/client/coral-framework/reducers/auth.js index babbe20d1..1cc67d192 100644 --- a/client/coral-framework/reducers/auth.js +++ b/client/coral-framework/reducers/auth.js @@ -3,7 +3,7 @@ import {Map} from 'immutable'; import { SHOW_SIGNIN_DIALOG, HIDE_SIGNIN_DIALOG, - NEW_STEP + CHANGE_VIEW } from '../constants/auth'; const initialState = Map({ @@ -12,7 +12,7 @@ const initialState = Map({ error: '', user: {}, showSignInDialog: false, - step: 1 + view: 'SIGNIN' }); export default function community (state = initialState, action) { @@ -23,9 +23,9 @@ export default function community (state = initialState, action) { case HIDE_SIGNIN_DIALOG : return state .set('showSignInDialog', false); - case NEW_STEP : + case CHANGE_VIEW : return state - .set('step', action.step); + .set('view', action.view); default : return state; } diff --git a/client/coral-sign-in/components/SignDialog.js b/client/coral-sign-in/components/SignDialog.js index 713c9789b..6aadc1744 100644 --- a/client/coral-sign-in/components/SignDialog.js +++ b/client/coral-sign-in/components/SignDialog.js @@ -5,10 +5,10 @@ import styles from './styles.css'; import SignInContent from './SignInContent'; import SingUpContent from './SignUpContent'; -const SignDialog = ({open, step, ...props}) => ( +const SignDialog = ({open, view, ...props}) => ( - {step === 1 && } - {step === 2 && } + {view === 'SIGNIN' && } + {view === 'SIGNUP' && } ); diff --git a/client/coral-sign-in/components/SignInContent.js b/client/coral-sign-in/components/SignInContent.js index f8893fb25..54428dbe9 100644 --- a/client/coral-sign-in/components/SignInContent.js +++ b/client/coral-sign-in/components/SignInContent.js @@ -5,7 +5,7 @@ import I18n from 'coral-framework/modules/i18n/i18n'; import translations from '../translations'; const lang = new I18n(translations); -const SignInContent = ({openFacebookWindow, goTo}) => ( +const SignInContent = ({openFacebookWindow, changeView}) => (

{lang.t('signIn.signIn')}

@@ -33,7 +33,7 @@ const SignInContent = ({openFacebookWindow, goTo}) => (
); diff --git a/client/coral-sign-in/components/SignUpContent.js b/client/coral-sign-in/components/SignUpContent.js index 2568797a7..70fb30d06 100644 --- a/client/coral-sign-in/components/SignUpContent.js +++ b/client/coral-sign-in/components/SignUpContent.js @@ -5,7 +5,7 @@ import I18n from 'coral-framework/modules/i18n/i18n'; import translations from '../translations'; const lang = new I18n(translations); -const SignUpContent = ({goTo, openFacebookWindow}) => ( +const SignUpContent = ({changeView, openFacebookWindow}) => (

{lang.t('signIn.signUp')}

@@ -40,7 +40,7 @@ const SignUpContent = ({goTo, openFacebookWindow}) => (
- {lang.t('signIn.alreadyHaveAnAccount')} goTo(1)}>{lang.t('signIn.signIn')} + {lang.t('signIn.alreadyHaveAnAccount')} changeView('SIGNIN')}>{lang.t('signIn.signIn')}
); diff --git a/client/coral-sign-in/containers/SignInContainer.js b/client/coral-sign-in/containers/SignInContainer.js index 7c7bd7775..42a32799c 100644 --- a/client/coral-sign-in/containers/SignInContainer.js +++ b/client/coral-sign-in/containers/SignInContainer.js @@ -9,7 +9,7 @@ import { loginFacebook, logout, showSignInDialog, - goTo, + changeView, } from '../../coral-framework/actions/auth'; class SignInContainer extends Component { @@ -19,7 +19,7 @@ class SignInContainer extends Component { this.openFacebookWindow = this.openFacebookWindow.bind(this); this.openSignInDialog = this.openSignInDialog.bind(this); this.logout = this.logout.bind(this); - this.goTo = this.goTo.bind(this); + this.changeView = this.changeView.bind(this); } componentDidMount() { @@ -38,8 +38,8 @@ class SignInContainer extends Component { this.props.dispatch(showSignInDialog()); } - goTo(step = 1) { - this.props.dispatch(goTo(step)); + changeView(view) { + this.props.dispatch(changeView(view)); } render() { @@ -52,8 +52,8 @@ class SignInContainer extends Component {
); From dab93547b552269fc60e6ccecf6a28f9f7aba446 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Sat, 12 Nov 2016 12:54:27 -0300 Subject: [PATCH 11/50] Handling Views --- .../containers/SignInContainer.js | 47 ++++++++----------- 1 file changed, 20 insertions(+), 27 deletions(-) diff --git a/client/coral-sign-in/containers/SignInContainer.js b/client/coral-sign-in/containers/SignInContainer.js index 42a32799c..4a1364ca6 100644 --- a/client/coral-sign-in/containers/SignInContainer.js +++ b/client/coral-sign-in/containers/SignInContainer.js @@ -15,49 +15,42 @@ import { class SignInContainer extends Component { constructor(props) { super(props); - - this.openFacebookWindow = this.openFacebookWindow.bind(this); - this.openSignInDialog = this.openSignInDialog.bind(this); - this.logout = this.logout.bind(this); - this.changeView = this.changeView.bind(this); } componentDidMount() { - window.authCallback = loginFacebookCallback; - } - - openFacebookWindow() { - this.props.dispatch(loginFacebook()); - } - - logout() { - this.props.dispatch(logout()); - } - - openSignInDialog() { - this.props.dispatch(showSignInDialog()); - } - - changeView(view) { - this.props.dispatch(changeView(view)); + window.authCallback = this.props.loginFacebookCallback; } render() { - const {auth} = this.props; + const {auth, showSignInDialog} = this.props; return (
-
); } } -export default connect(({auth}) => ({auth}))(SignInContainer); +const mapStateToProps = state => ({ + auth: state.auth +}); + +const mapDispatchToProps = dispatch => ({ + loginFacebookCallback: () => dispatch(loginFacebookCallback()), + loginFacebook: () => dispatch(loginFacebook()), + logout: () => dispatch(logout()), + showSignInDialog: () => dispatch(showSignInDialog()), + changeView: (view) => dispatch(changeView(view)) +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(SignInContainer); From e7d114d5dfbd6f41925ec8389fe28823e0754b2e Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Sat, 12 Nov 2016 13:27:04 -0300 Subject: [PATCH 12/50] Close and Cancel actions --- .../coral-sign-in/components/ForgotContent.js | 29 +++++++++++++++++++ client/coral-sign-in/components/SignDialog.js | 5 +++- .../coral-sign-in/components/SignInContent.js | 2 +- client/coral-sign-in/components/styles.css | 16 ++++++++++ .../containers/SignInContainer.js | 8 ++--- client/coral-sign-in/translations.js | 6 ++-- 6 files changed, 58 insertions(+), 8 deletions(-) create mode 100644 client/coral-sign-in/components/ForgotContent.js diff --git a/client/coral-sign-in/components/ForgotContent.js b/client/coral-sign-in/components/ForgotContent.js new file mode 100644 index 000000000..9f32f6933 --- /dev/null +++ b/client/coral-sign-in/components/ForgotContent.js @@ -0,0 +1,29 @@ +import React from 'react'; +import styles from './styles.css'; +import Button from './Button'; +import I18n from 'coral-framework/modules/i18n/i18n'; +import translations from '../translations'; +const lang = new I18n(translations); + +const ForgotContent = ({changeView}) => ( +
+
+

{lang.t('signIn.recoverPassword')}

+
+
+
+ + +
+ +
+
+ {lang.t('signIn.needAnAccount')} changeView('SIGNUP')}>{lang.t('signIn.register')} + {lang.t('signIn.alreadyHaveAnAccount')} changeView('SIGNIN')}>{lang.t('signIn.signIn')} +
+
+); + +export default ForgotContent; diff --git a/client/coral-sign-in/components/SignDialog.js b/client/coral-sign-in/components/SignDialog.js index 6aadc1744..e5c6f0ea4 100644 --- a/client/coral-sign-in/components/SignDialog.js +++ b/client/coral-sign-in/components/SignDialog.js @@ -4,11 +4,14 @@ import styles from './styles.css'; import SignInContent from './SignInContent'; import SingUpContent from './SignUpContent'; +import ForgotContent from './ForgotContent'; -const SignDialog = ({open, view, ...props}) => ( +const SignDialog = ({open, view, onClose, ...props}) => ( + × {view === 'SIGNIN' && } {view === 'SIGNUP' && } + {view === 'FORGOT' && } ); diff --git a/client/coral-sign-in/components/SignInContent.js b/client/coral-sign-in/components/SignInContent.js index 54428dbe9..4764f1cb2 100644 --- a/client/coral-sign-in/components/SignInContent.js +++ b/client/coral-sign-in/components/SignInContent.js @@ -32,7 +32,7 @@ const SignInContent = ({openFacebookWindow, changeView}) => (
diff --git a/client/coral-sign-in/components/styles.css b/client/coral-sign-in/components/styles.css index b289ac8fb..e7d77b85c 100644 --- a/client/coral-sign-in/components/styles.css +++ b/client/coral-sign-in/components/styles.css @@ -104,4 +104,20 @@ .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; } \ No newline at end of file diff --git a/client/coral-sign-in/containers/SignInContainer.js b/client/coral-sign-in/containers/SignInContainer.js index 4a1364ca6..e9e32d474 100644 --- a/client/coral-sign-in/containers/SignInContainer.js +++ b/client/coral-sign-in/containers/SignInContainer.js @@ -10,6 +10,7 @@ import { logout, showSignInDialog, changeView, + hideSignInDialog } from '../../coral-framework/actions/auth'; class SignInContainer extends Component { @@ -38,16 +39,15 @@ class SignInContainer extends Component { } } -const mapStateToProps = state => ({ - auth: state.auth -}); +const mapStateToProps = ({auth}) => ({auth}); const mapDispatchToProps = dispatch => ({ loginFacebookCallback: () => dispatch(loginFacebookCallback()), loginFacebook: () => dispatch(loginFacebook()), logout: () => dispatch(logout()), showSignInDialog: () => dispatch(showSignInDialog()), - changeView: (view) => dispatch(changeView(view)) + changeView: (view) => dispatch(changeView(view)), + onClose: () => dispatch(hideSignInDialog()) }); export default connect( diff --git a/client/coral-sign-in/translations.js b/client/coral-sign-in/translations.js index 1f11935bd..a0fd7015e 100644 --- a/client/coral-sign-in/translations.js +++ b/client/coral-sign-in/translations.js @@ -14,7 +14,8 @@ export default { signUp: 'Sign Up', confirmPassword: 'Confirm Password', username: 'Username', - alreadyHaveAnAccount: 'Already have an account?' + alreadyHaveAnAccount: 'Already have an account?', + recoverPassword: 'Recover password' } }, es: { @@ -32,7 +33,8 @@ export default { signUp: 'Registro', confirmPassword: 'Confirmar Contraseña', username: 'Usuario', - alreadyHaveAnAccount: 'Ya tienes una cuenta?' + alreadyHaveAnAccount: 'Ya tienes una cuenta?', + recoverPassword: 'Recuperar contraseña' } } }; From 9341caa4d5707db26c1912e85da059962dc3137a Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Mon, 14 Nov 2016 13:04:44 -0300 Subject: [PATCH 13/50] Auth Actions --- client/coral-framework/actions/auth.js | 137 ++++++++++-------- client/coral-framework/helpers/error.js | 4 + .../helpers/{index.js => response.js} | 0 client/coral-framework/helpers/validate.js | 0 client/coral-sign-in/components/Alert.js | 0 client/coral-sign-in/components/Button.js | 13 -- client/coral-sign-in/components/FormField.js | 0 client/coral-ui/components/Button.css | 0 client/coral-ui/components/Button.js | 13 ++ client/coral-ui/components/Spinner.css | 87 +++++++++++ client/coral-ui/components/Spinner.js | 12 ++ 11 files changed, 189 insertions(+), 77 deletions(-) create mode 100644 client/coral-framework/helpers/error.js rename client/coral-framework/helpers/{index.js => response.js} (100%) create mode 100644 client/coral-framework/helpers/validate.js create mode 100644 client/coral-sign-in/components/Alert.js delete mode 100644 client/coral-sign-in/components/Button.js create mode 100644 client/coral-sign-in/components/FormField.js create mode 100644 client/coral-ui/components/Button.css create mode 100644 client/coral-ui/components/Button.js create mode 100644 client/coral-ui/components/Spinner.css create mode 100644 client/coral-ui/components/Spinner.js diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js index 0a70da7ba..eb1a63f22 100644 --- a/client/coral-framework/actions/auth.js +++ b/client/coral-framework/actions/auth.js @@ -1,69 +1,9 @@ import * as actions from '../constants/auth'; -import {base, handleResp, getInit} from '../helpers'; +import {base, handleResp, getInit} from '../helpers/response'; -export const showSignInDialog = () => ({ - type: actions.SHOW_SIGNIN_DIALOG -}); - -export const hideSignInDialog = () => ({ - type: actions.HIDE_SIGNIN_DIALOG -}); - -export const loginFacebook = () => dispatch => { - dispatch(loginFacebookRequest()); - window.open( - `${base}/auth/facebook`, - 'Continue with Facebook', - 'menubar=0,resizable=0,width=500,height=500,top=200,left=500' - ); -}; - -export const loginFacebookCallback = (err, data) => { - let user; - - if (err) { - console.error(err); - return; - } - - try { - user = JSON.parse(data); - } catch (err) { - console.error('Can\'t parse the user json', err); - return; - } - - console.log('User was loaded!', user); -}; - -export const logout = () => dispatch => { - dispatch(logoutRequest()); - fetch(`${base}/auth`, getInit('DELETE')) - .then(handleResp) - .then(() => dispatch(dispatch(logoutSuccess()))) - .catch(error => dispatch(logoutFailure(error))); -}; - -const logoutRequest = () => ({ - type: actions.USER_LOGOUT_SUCCESS -}); - -const logoutSuccess = () => ({ - type: actions.USER_LOGOUT_SUCCESS, -}); - -const logoutFailure = error => ({ - type: actions.USER_LOGOUT_SUCCESS, - error -}); - -export const loginFacebookRequest = () => ({ - type: actions.USER_FACEBOOK_LOGIN_REQUEST -}); - -export const loginRequest = () => ({ - type: actions.USER_SIGNIN_REQUEST -}); +// Dialog Actions +export const showSignInDialog = () => ({type: actions.SHOW_SIGNIN_DIALOG}); +export const hideSignInDialog = () => ({type: actions.HIDE_SIGNIN_DIALOG}); export const changeView = view => dispatch => dispatch({ @@ -71,3 +11,72 @@ export const changeView = view => dispatch => view }); +export const cleanState = () => ({type: actions.CLEAN_STATE}); + +// Sign In Actions + +const signInRequest = () => ({type: actions.FETCH_SIGNIN_REQUEST}); +const signInSuccess = () => ({type: actions.FETCH_SIGNIN_SUCCESS}); +const signInFailure = () => ({type: actions.FETCH_SIGNIN_FAILURE}); + +export const fetchSignIn = () => dispatch => { + dispatch(signInRequest()); + fetch(`${base}/auth`, getInit('POST')) + .then(handleResp) + .then(() => dispatch(signInSuccess())) + .catch(error => dispatch(signInFailure(error))); +}; + +// Sign In - Facebook + +const signInFacebookRequest = () => ({type: actions.FETCH_SIGNIN_FACEBOOK_REQUEST}); +//const signInFacebookSuccess = () => ({type: actions.FETCH_SIGNIN_FACEBOOK_SUCCESS}); +//const signInFacebookFailure = () => ({type: actions.FETCH_SIGNIN_FACEBOOK_FAILURE}); + +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' + ); +}; + +// Sign Up Actions + +const signUpRequest = () => ({type: actions.FETCH_SIGNUP_REQUEST}); +const signUpSuccess = () => ({type: actions.FETCH_SIGNUP_SUCCESS}); +const signUpFailure = () => ({type: actions.FETCH_SIGNUP_FAILURE}); + +export const fetchSignUp = () => dispatch => { + dispatch(signUpRequest()); + fetch(`${base}/auth`, getInit('POST')) + .then(handleResp) + .then(() => dispatch(signUpSuccess())) + .catch(error => dispatch(signUpFailure(error))); +}; + +// 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}/forgot`, getInit('POST')) + .then(handleResp) + .then(() => dispatch(forgotPassowordSuccess())) + .catch(error => dispatch(forgotPassowordFailure(error))); +}; + +// LogOut + +export const logout = () => dispatch => { + dispatch(signInRequest()); + fetch(`${base}/auth`, getInit('DELETE')) + .then(handleResp) + .then(() => dispatch(signInSuccess())) + .catch(error => dispatch(signInFailure(error))); +}; + diff --git a/client/coral-framework/helpers/error.js b/client/coral-framework/helpers/error.js new file mode 100644 index 000000000..6ae79a996 --- /dev/null +++ b/client/coral-framework/helpers/error.js @@ -0,0 +1,4 @@ +export default { + email: 'Not a valid E-Mail', + password: 'Password must be at least 8 characters' +}; diff --git a/client/coral-framework/helpers/index.js b/client/coral-framework/helpers/response.js similarity index 100% rename from client/coral-framework/helpers/index.js rename to client/coral-framework/helpers/response.js diff --git a/client/coral-framework/helpers/validate.js b/client/coral-framework/helpers/validate.js new file mode 100644 index 000000000..e69de29bb diff --git a/client/coral-sign-in/components/Alert.js b/client/coral-sign-in/components/Alert.js new file mode 100644 index 000000000..e69de29bb diff --git a/client/coral-sign-in/components/Button.js b/client/coral-sign-in/components/Button.js deleted file mode 100644 index 3f736ac3d..000000000 --- a/client/coral-sign-in/components/Button.js +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react'; -import styles from './styles.css'; - -const Button = ({type = 'local', children, className, ...props}) => ( - -); - -export default Button; diff --git a/client/coral-sign-in/components/FormField.js b/client/coral-sign-in/components/FormField.js new file mode 100644 index 000000000..e69de29bb diff --git a/client/coral-ui/components/Button.css b/client/coral-ui/components/Button.css new file mode 100644 index 000000000..e69de29bb diff --git a/client/coral-ui/components/Button.js b/client/coral-ui/components/Button.js new file mode 100644 index 000000000..9c0d239b7 --- /dev/null +++ b/client/coral-ui/components/Button.js @@ -0,0 +1,13 @@ +import React from 'react'; +import styles from './styles.css'; + +const Button = ({cStyle = 'local', children, className, ...props}) => ( + +); + +export default Button; diff --git a/client/coral-ui/components/Spinner.css b/client/coral-ui/components/Spinner.css new file mode 100644 index 000000000..00b7f93a5 --- /dev/null +++ b/client/coral-ui/components/Spinner.css @@ -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); + } +} diff --git a/client/coral-ui/components/Spinner.js b/client/coral-ui/components/Spinner.js new file mode 100644 index 000000000..058f1af7c --- /dev/null +++ b/client/coral-ui/components/Spinner.js @@ -0,0 +1,12 @@ +import React from 'react'; +import styles from './Spinner.css'; + +const Spinner = () => ( +
+ + + +
+); + +export default Spinner; From ef2d67a940d713e8c2c257c1a3d613d4cc66a1bf Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Mon, 14 Nov 2016 13:06:48 -0300 Subject: [PATCH 14/50] Coral-UI, Coral Auth with Reducers, Helpers and Validation --- client/coral-framework/constants/auth.js | 27 ++-- client/coral-framework/helpers/error.js | 4 +- client/coral-framework/helpers/validate.js | 6 + client/coral-framework/reducers/auth.js | 25 +++- client/coral-sign-in/components/Alert.js | 13 ++ .../coral-sign-in/components/ForgotContent.js | 8 +- client/coral-sign-in/components/FormField.js | 18 +++ client/coral-sign-in/components/SignDialog.js | 4 +- .../coral-sign-in/components/SignInContent.js | 64 ++++++--- .../coral-sign-in/components/SignUpContent.js | 89 +++++++++---- client/coral-sign-in/components/styles.css | 79 +++++------- .../containers/SignInContainer.js | 122 ++++++++++++++++-- client/coral-ui/components/Button.css | 49 +++++++ client/coral-ui/components/Button.js | 2 +- 14 files changed, 381 insertions(+), 129 deletions(-) diff --git a/client/coral-framework/constants/auth.js b/client/coral-framework/constants/auth.js index f590fce12..c48894cc5 100644 --- a/client/coral-framework/constants/auth.js +++ b/client/coral-framework/constants/auth.js @@ -1,14 +1,21 @@ -export const USER_LOGIN_REQUEST = 'USER_LOGIN_REQUEST'; -export const USER_LOGIN_SUCCESS = 'USER_LOGIN_SUCCESS'; -export const USER_LOGIN_FAILURE = 'USER_LOGIN_FAILURE'; -export const USER_FACEBOOK_LOGIN_REQUEST = 'USER_FACEBOOK_LOGIN_REQUEST'; -export const USER_FACEBOOK_SUCCESS = 'USER_FACEBOOK_SUCCESS'; -export const USER_FACEBOOK_FAILURE = 'USER_FACEBOOK_FAILURE'; -export const USER_LOGOUT_REQUEST = 'USER_LOGOUT_REQUEST'; -export const USER_LOGOUT_SUCCESS = 'USER_LOGOUT_SUCCESS'; -export const USER_LOGOUT_FAILURE = 'USER_LOGOUT_FAILURE'; +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 CHANGE_VIEW = 'CHANGE_VIEW'; +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'; diff --git a/client/coral-framework/helpers/error.js b/client/coral-framework/helpers/error.js index 6ae79a996..2674bc55b 100644 --- a/client/coral-framework/helpers/error.js +++ b/client/coral-framework/helpers/error.js @@ -1,4 +1,6 @@ export default { email: 'Not a valid E-Mail', - password: 'Password must be at least 8 characters' + password: 'Password must be at least 8 characters', + username: 'Username is too short', + confirmPassword: 'Passwords do not match' }; diff --git a/client/coral-framework/helpers/validate.js b/client/coral-framework/helpers/validate.js index e69de29bb..9fda4b1c2 100644 --- a/client/coral-framework/helpers/validate.js +++ b/client/coral-framework/helpers/validate.js @@ -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, + username: username => (/^(?=.{3,}).*$/.test(username)) +}; diff --git a/client/coral-framework/reducers/auth.js b/client/coral-framework/reducers/auth.js index 1cc67d192..5e54d70b6 100644 --- a/client/coral-framework/reducers/auth.js +++ b/client/coral-framework/reducers/auth.js @@ -3,11 +3,16 @@ import {Map} from 'immutable'; import { SHOW_SIGNIN_DIALOG, HIDE_SIGNIN_DIALOG, - CHANGE_VIEW + CHANGE_VIEW, + CLEAN_STATE, + FETCH_SIGNIN_REQUEST, + FETCH_SIGNIN_FAILURE, + FETCH_SIGNIN_SUCCESS } from '../constants/auth'; const initialState = Map({ auth: Map(), + isLoading: false, loggedIn: false, error: '', user: {}, @@ -15,17 +20,29 @@ const initialState = Map({ view: 'SIGNIN' }); -export default function community (state = initialState, action) { +export const getEmail = state => state.auth.formData.email; + +export default function auth (state = initialState, action) { switch (action.type) { case SHOW_SIGNIN_DIALOG : return state .set('showSignInDialog', true); case HIDE_SIGNIN_DIALOG : - return state - .set('showSignInDialog', false); + return initialState; case CHANGE_VIEW : return state .set('view', action.view); + case CLEAN_STATE: + return initialState; + case FETCH_SIGNIN_REQUEST: + return state + .set('isLoading', true); + case FETCH_SIGNIN_FAILURE: + return state + .set('isLoading', false); + case FETCH_SIGNIN_SUCCESS: + return state + .set('isLoading', false); default : return state; } diff --git a/client/coral-sign-in/components/Alert.js b/client/coral-sign-in/components/Alert.js index e69de29bb..019c3456a 100644 --- a/client/coral-sign-in/components/Alert.js +++ b/client/coral-sign-in/components/Alert.js @@ -0,0 +1,13 @@ +import React from 'react'; +import styles from './styles.css'; + +const Alert = ({cStyle = 'error', children, className, ...props}) => ( +
+ {children} +
+); + +export default Alert; diff --git a/client/coral-sign-in/components/ForgotContent.js b/client/coral-sign-in/components/ForgotContent.js index 9f32f6933..7214d2bb6 100644 --- a/client/coral-sign-in/components/ForgotContent.js +++ b/client/coral-sign-in/components/ForgotContent.js @@ -1,21 +1,21 @@ import React from 'react'; import styles from './styles.css'; -import Button from './Button'; +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}) => ( +const ForgotContent = ({changeView, ...props}) => (

{lang.t('signIn.recoverPassword')}

-
+ {e.preventDefault(); props.fetchForgotPassword();}}>
-
diff --git a/client/coral-sign-in/components/FormField.js b/client/coral-sign-in/components/FormField.js index e69de29bb..62e50e637 100644 --- a/client/coral-sign-in/components/FormField.js +++ b/client/coral-sign-in/components/FormField.js @@ -0,0 +1,18 @@ +import React from 'react'; +import styles from './styles.css'; + +const FormField = ({className, showErrors = false, errorMsg, label, ...props}) => ( +
+ + + {showErrors && errorMsg && {errorMsg}} +
+); + +export default FormField; diff --git a/client/coral-sign-in/components/SignDialog.js b/client/coral-sign-in/components/SignDialog.js index e5c6f0ea4..242b7443a 100644 --- a/client/coral-sign-in/components/SignDialog.js +++ b/client/coral-sign-in/components/SignDialog.js @@ -6,9 +6,9 @@ import SignInContent from './SignInContent'; import SingUpContent from './SignUpContent'; import ForgotContent from './ForgotContent'; -const SignDialog = ({open, view, onClose, ...props}) => ( +const SignDialog = ({open, view, handleClose, ...props}) => ( - × + × {view === 'SIGNIN' && } {view === 'SIGNUP' && } {view === 'FORGOT' && } diff --git a/client/coral-sign-in/components/SignInContent.js b/client/coral-sign-in/components/SignInContent.js index 4764f1cb2..6a1e726c5 100644 --- a/client/coral-sign-in/components/SignInContent.js +++ b/client/coral-sign-in/components/SignInContent.js @@ -1,39 +1,65 @@ import React from 'react'; import styles from './styles.css'; -import Button from './Button'; + +import Button from 'coral-ui/components/Button'; +import FormField from './FormField'; +import Alert from './Alert'; +import Spinner from 'coral-ui/components/Spinner'; + import I18n from 'coral-framework/modules/i18n/i18n'; import translations from '../translations'; const lang = new I18n(translations); -const SignInContent = ({openFacebookWindow, changeView}) => ( +const SignInContent = ({handleChange, formData, ...props}) => (
-

{lang.t('signIn.signIn')}

+

+ {lang.t('signIn.signIn')} +

-
-

{lang.t('signIn.or')}

+

+ {lang.t('signIn.or')} +

-
-
- - -
-
- - -
- + { props.message && {props.message} } + + + + { + !props.isLoading ? + + : + + }
); diff --git a/client/coral-sign-in/components/SignUpContent.js b/client/coral-sign-in/components/SignUpContent.js index 70fb30d06..db005368f 100644 --- a/client/coral-sign-in/components/SignUpContent.js +++ b/client/coral-sign-in/components/SignUpContent.js @@ -1,46 +1,83 @@ import React from 'react'; import styles from './styles.css'; -import Button from './Button'; +import FormField from './FormField'; +import Button from 'coral-ui/components/Button'; +import Spinner from 'coral-ui/components/Spinner'; import I18n from 'coral-framework/modules/i18n/i18n'; import translations from '../translations'; const lang = new I18n(translations); -const SignUpContent = ({changeView, openFacebookWindow}) => ( +const SignUpContent = ({handleChange, formData, ...props}) => (
-

{lang.t('signIn.signUp')}

+

+ {lang.t('signIn.signUp')} +

-
-

{lang.t('signIn.or')}

+

+ {lang.t('signIn.or')} +

-
-
- - -
-
- - -
-
- - -
-
- - -
- + + + + + + + { + !props.isLoading ? + + : + + }
- {lang.t('signIn.alreadyHaveAnAccount')} changeView('SIGNIN')}>{lang.t('signIn.signIn')} + + {lang.t('signIn.alreadyHaveAnAccount')} + props.changeView('SIGNIN')}> + {lang.t('signIn.signIn')} + +
); diff --git a/client/coral-sign-in/components/styles.css b/client/coral-sign-in/components/styles.css index e7d77b85c..3800f53e1 100644 --- a/client/coral-sign-in/components/styles.css +++ b/client/coral-sign-in/components/styles.css @@ -1,53 +1,3 @@ -.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; -} - .dialog { width: 400px; border: none; @@ -96,6 +46,7 @@ .footer a { color: #2c69b6; cursor: pointer; + margin: 0 5px; } .socialConnections { @@ -120,4 +71,32 @@ .close:hover { color: #6b6b6b; +} + +input.error{ + border: solid 1px #f44336; +} + +.errorMsg { + color: grey; + font-weight: 600; + padding: 3px 0 16px; +} + +.alert--success { + border: solid 1px #c0c4da; + background: #afb5c2; + color: #131314; +} + +.alert--success { + border: solid 1px #1ec00e; + background: #cbf1b8; + color: #006900; +} + +.alert--error { + border: solid 1px #f44336; + background: #F1A097; + color: #741818; } \ No newline at end of file diff --git a/client/coral-sign-in/containers/SignInContainer.js b/client/coral-sign-in/containers/SignInContainer.js index e9e32d474..b4d8a14e2 100644 --- a/client/coral-sign-in/containers/SignInContainer.js +++ b/client/coral-sign-in/containers/SignInContainer.js @@ -2,28 +2,119 @@ import React, {Component} from 'react'; import {connect} from 'react-redux'; import SignDialog from '../components/SignDialog'; -import Button from '../components/Button'; +import Button from 'coral-ui/components/Button'; + +import validate from 'coral-framework/helpers/validate'; +import error from 'coral-framework/helpers/error'; import { - loginFacebookCallback, - loginFacebook, - logout, - showSignInDialog, changeView, - hideSignInDialog + fetchSignUp, + fetchSignIn, + showSignInDialog, + hideSignInDialog, + fetchSignInFacebook, + fetchForgotPassword } from '../../coral-framework/actions/auth'; class SignInContainer extends Component { + initialState = { + formData: { + email: '', + username: '', + 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.cleanState = this.cleanState.bind(this); + this.validation = this.validation.bind(this); } - componentDidMount() { - window.authCallback = this.props.loginFacebookCallback; + cleanState () { + this.setState(this.initialState); + } + + handleChange(e) { + const {name, value} = e.target; + this.validation(name, value, this.state.formData); + + this.setState(state => ({ + ...state, + formData: { + ...state.formData, + [name]: value + } + })); + } + + validation(name, value) { + if (!validate[name](value)) { + this.setState(state => ({ + errors: { + ...state.errors, + [name]: error[name] + } + })); + } else { + const { [name]: prop, ...errors } = this.state.errors; // eslint-disable-line + 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(); + this.displayErrors(); + if (this.isCompleted()) { + console.log('Is Completed!!'); + } else { + console.log('Is not Completed!!'); + } + this.props.fetchSignUp(this.state.formData); + } + + handleSignIn(e) { + e.preventDefault(); + this.displayErrors(); + this.props.fetchSignIn(this.state.formData); + } + + handleClose() { + this.cleanState(); + this.props.hideSignInDialog(); + } + + changeView(view) { + this.cleanState(); + this.props.changeView(view); } render() { const {auth, showSignInDialog} = this.props; + const {errors, showErrors, formData} = this.state; return (
@@ -42,12 +139,13 @@ class SignInContainer extends Component { const mapStateToProps = ({auth}) => ({auth}); const mapDispatchToProps = dispatch => ({ - loginFacebookCallback: () => dispatch(loginFacebookCallback()), - loginFacebook: () => dispatch(loginFacebook()), - logout: () => dispatch(logout()), + 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)), - onClose: () => dispatch(hideSignInDialog()) + handleClose: () => dispatch(hideSignInDialog()) }); export default connect( diff --git a/client/coral-ui/components/Button.css b/client/coral-ui/components/Button.css index e69de29bb..150f1df7b 100644 --- a/client/coral-ui/components/Button.css +++ b/client/coral-ui/components/Button.css @@ -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; +} \ No newline at end of file diff --git a/client/coral-ui/components/Button.js b/client/coral-ui/components/Button.js index 9c0d239b7..13ea46349 100644 --- a/client/coral-ui/components/Button.js +++ b/client/coral-ui/components/Button.js @@ -1,5 +1,5 @@ import React from 'react'; -import styles from './styles.css'; +import styles from './Button.css'; const Button = ({cStyle = 'local', children, className, ...props}) => ( + { canPost && ( + + ) + }
; } diff --git a/client/coral-sign-in/components/UserBox.js b/client/coral-sign-in/components/UserBox.js new file mode 100644 index 000000000..17c8453fb --- /dev/null +++ b/client/coral-sign-in/components/UserBox.js @@ -0,0 +1,13 @@ +import React from 'react'; +import styles from './styles.css'; + +const UserBox = ({className, user, logout, ...props}) => ( +
+ Signed in as {user.displayName}. Not you? Sign out +
+); + +export default UserBox; diff --git a/client/coral-sign-in/components/styles.css b/client/coral-sign-in/components/styles.css index 3800f53e1..3a83d0985 100644 --- a/client/coral-sign-in/components/styles.css +++ b/client/coral-sign-in/components/styles.css @@ -99,4 +99,10 @@ input.error{ border: solid 1px #f44336; background: #F1A097; color: #741818; +} + +.userBox a { + color: #2c69b6; + cursor: pointer; + margin: 0 5px; } \ No newline at end of file From 69ea37a520a220f9b185c2d55287bbfcf2a8470d Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Mon, 14 Nov 2016 18:48:08 -0300 Subject: [PATCH 19/50] removing mock --- client/coral-embed-stream/src/CommentStream.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/client/coral-embed-stream/src/CommentStream.js b/client/coral-embed-stream/src/CommentStream.js index 3fa989dcc..9728d07c8 100644 --- a/client/coral-embed-stream/src/CommentStream.js +++ b/client/coral-embed-stream/src/CommentStream.js @@ -38,7 +38,6 @@ const mapDispatchToProps = (dispatch) => ({ getStream: (rootId) => dispatch(getStream(rootId)), addNotification: (type, text) => dispatch(addNotification(type, text)), clearNotification: () => dispatch(clearNotification()), - setLoggedInUser: (user_id) => dispatch(setLoggedInUser(user_id)), postAction: (item, action, user, itemType) => dispatch(postAction(item, action, user, itemType)), appendItemArray: (item, property, value, addToFront, itemType) => dispatch(appendItemArray(item, property, value, addToFront, itemType)), @@ -68,11 +67,11 @@ class CommentStream extends Component { 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); - }); + // 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 From 954f6d9ee37d09840dd6b16295406bd3a5cfa832 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Tue, 15 Nov 2016 17:41:41 -0300 Subject: [PATCH 20/50] Sign in with local account --- client/coral-framework/actions/auth.js | 12 ++++++------ client/coral-framework/reducers/auth.js | 13 +++++++++---- client/coral-sign-in/components/SignInContent.js | 2 +- client/coral-sign-in/components/styles.css | 12 +++++------- client/coral-sign-in/containers/SignInContainer.js | 1 + 5 files changed, 22 insertions(+), 18 deletions(-) diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js index 3981d8021..896fb9aab 100644 --- a/client/coral-framework/actions/auth.js +++ b/client/coral-framework/actions/auth.js @@ -16,15 +16,15 @@ export const cleanState = () => ({type: actions.CLEAN_STATE}); // Sign In Actions const signInRequest = () => ({type: actions.FETCH_SIGNIN_REQUEST}); -const signInSuccess = () => ({type: actions.FETCH_SIGNIN_SUCCESS}); -const signInFailure = () => ({type: actions.FETCH_SIGNIN_FAILURE}); +const signInSuccess = (user) => ({type: actions.FETCH_SIGNIN_SUCCESS, user}); +const signInFailure = (error) => ({type: actions.FETCH_SIGNIN_FAILURE, error}); -export const fetchSignIn = () => dispatch => { +export const fetchSignIn = (formData) => dispatch => { dispatch(signInRequest()); - fetch(`${base}/auth`, getInit('POST')) + fetch(`${base}/auth/local`, getInit('POST', formData)) .then(handleResp) - .then(() => dispatch(signInSuccess())) - .catch(error => dispatch(signInFailure(error))); + .then(({user}) => dispatch(signInSuccess(user))) + .catch(() => dispatch(signInFailure('Email and/or password combination incorrect.'))); }; // Sign In - Facebook diff --git a/client/coral-framework/reducers/auth.js b/client/coral-framework/reducers/auth.js index 89e7c8012..001c98346 100644 --- a/client/coral-framework/reducers/auth.js +++ b/client/coral-framework/reducers/auth.js @@ -17,10 +17,10 @@ const initialState = Map({ auth: Map(), isLoading: false, loggedIn: false, - error: '', user: null, showSignInDialog: false, - view: 'SIGNIN' + view: 'SIGNIN', + signInError: '' }); export const getEmail = state => state.auth.formData.email; @@ -42,10 +42,15 @@ export default function auth (state = initialState, action) { .set('isLoading', true); case FETCH_SIGNIN_FAILURE: return state - .set('isLoading', false); + .set('isLoading', false) + .set('signInError', action.error); case FETCH_SIGNIN_SUCCESS: return state - .set('isLoading', false); + .set('signInError', '') + .set('isLoading', false) + .set('loggedIn', true) + .set('user', action.user) + .set('showSignInDialog', false); case FETCH_SIGNIN_FACEBOOK_SUCCESS: return state .set('user', action.user) diff --git a/client/coral-sign-in/components/SignInContent.js b/client/coral-sign-in/components/SignInContent.js index 6a1e726c5..aa3b712fa 100644 --- a/client/coral-sign-in/components/SignInContent.js +++ b/client/coral-sign-in/components/SignInContent.js @@ -27,7 +27,7 @@ const SignInContent = ({handleChange, formData, ...props}) => ( {lang.t('signIn.or')} - { props.message && {props.message} } + { props.signInError && {props.signInError} }
From c86e1e8d47452a03470b0c35640b1355f1780756 Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Tue, 15 Nov 2016 16:21:25 -0700 Subject: [PATCH 21/50] create and decode jwt --- models/user.js | 24 +++++++++++++++++++++++- package.json | 3 ++- routes/admin/index.js | 14 +++++++++++++- routes/api/user/index.js | 34 ++++++++++++++++++++++++++++++++++ services/mailer.js | 17 ----------------- 5 files changed, 72 insertions(+), 20 deletions(-) diff --git a/models/user.js b/models/user.js index c6197a220..f4edc8f63 100644 --- a/models/user.js +++ b/models/user.js @@ -1,6 +1,7 @@ const mongoose = require('../mongoose'); const uuid = require('uuid'); const bcrypt = require('bcrypt'); +const jwt = require('jsonwebtoken'); const SALT_ROUNDS = 10; @@ -83,10 +84,15 @@ UserSchema.options.toObject.transform = (doc, ret, options) => { * @param {Function} done [description] */ UserSchema.statics.findLocalUser = function(email, password) { + + if (!email || typeof email !== 'string') { + return Promise.reject('email is required for findLocalUser'); + } + return User.findOne({ profiles: { $elemMatch: { - id: email, + id: email.toLowerCase(), provider: 'local' } } @@ -221,6 +227,8 @@ UserSchema.statics.createLocalUser = function(email, password, displayName) { return Promise.reject('email is required'); } + email = email.toLowerCase(); + if (!password) { return Promise.reject('password is required'); } @@ -338,6 +346,20 @@ UserSchema.statics.findByIdArray = function(ids) { }); }; +UserSchema.statics.createJWT = function (email) { + if (!email || typeof email !== 'string') { + return Promise.reject('email is required when creating a JWT for resetting passord'); + } + + email = email.toLowerCase(); + + const payload = {email, jti: uuid.v4()}; + + const token = jwt.sign(payload, process.env.TALK_SESSION_SECRET, {expiresIn: '1d'}); + + return Promise.resolve(token); +}; + const User = mongoose.model('User', UserSchema); module.exports = User; diff --git a/package.json b/package.json index ad0e2dbb0..76173b7aa 100644 --- a/package.json +++ b/package.json @@ -51,13 +51,13 @@ "debug": "^2.2.0", "ejs": "^2.5.2", "express": "^4.14.0", + "jsonwebtoken": "^7.1.9", "lodash": "^4.16.6", "mongoose": "^4.6.5", "morgan": "^1.7.0", "nodemailer": "^2.6.4", "nodemailer-sendgrid-transport": "^0.2.0", "prompt": "^1.0.0", - "react-mdl-selectfield": "^0.2.0", "uuid": "^2.0.3" }, "devDependencies": { @@ -107,6 +107,7 @@ "react": "15.3.2", "react-dom": "15.3.2", "react-mdl": "^1.7.2", + "react-mdl-selectfield": "^0.2.0", "react-onclickoutside": "^5.7.1", "react-redux": "^4.4.5", "react-router": "^3.0.0", diff --git a/routes/admin/index.js b/routes/admin/index.js index 52957f54a..84c130ba8 100644 --- a/routes/admin/index.js +++ b/routes/admin/index.js @@ -1,11 +1,23 @@ const express = require('express'); - +const jwt = require('jsonwebtoken'); 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) => { + jwt.verify(req.params.token, process.env.TALK_SESSION_SECRET, (error, decoded) => { + if (error) { + return res.status(400).json({error}); + } + + console.log(decoded); + + res.json(decoded); + }); +}); + router.get('*', (req, res) => { res.render('admin', {basePath: '/client/coral-admin'}); }); diff --git a/routes/api/user/index.js b/routes/api/user/index.js index 4665f4e52..c0bc43c6a 100644 --- a/routes/api/user/index.js +++ b/routes/api/user/index.js @@ -1,6 +1,7 @@ const express = require('express'); const router = express.Router(); const User = require('../../../models/user'); +const mailer = require('../../../services/mailer'); router.get('/', (req, res, next) => { const { @@ -70,4 +71,37 @@ router.post('/:user_id/role', (req, res, next) => { .catch(next); }); +/** + * 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; + + console.log('/request-password-reset', req.body); + + if (!email) { + return next(); + } + + User + .createJWT(email) + .then(token => { + const options = { + subject: 'password reset requested', + from: 'coralcore@mozillafoundation.org', + to: 'riley.davis@gmail.com', + html: `reset password` + }; + return mailer.sendSimple(options); + }) + .then(success => { + console.log(success); + res.json({success: true}); + }) + .catch(error => { + res.status(500).json({error}); + }); +}); + module.exports = router; diff --git a/services/mailer.js b/services/mailer.js index ec1dd5f5e..eb900d782 100644 --- a/services/mailer.js +++ b/services/mailer.js @@ -8,23 +8,6 @@ const options = { const transporter = nodemailer.createTransport(sgTransport(options)); -transporter.sendMail({ - from: 'support@mrdavis.com', - to: 'riley.davis@gmail.com', - subject: 'this is only a test', - text: 'this is the body of the email maybe?', - html: ` - - - - - - - - -
foobar
rileydavis
` -}); - const mailer = { /** * sendSimple From 7df7279323c2fa017b7907421aef9a55dd6aa34a Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Wed, 16 Nov 2016 11:49:58 -0700 Subject: [PATCH 22/50] endpoints for updating password --- models/user.js | 42 +++++++++++++++++++++++++++++++++++++--- routes/admin/index.js | 12 ++---------- routes/api/user/index.js | 25 +++++++++++++++++++++++- 3 files changed, 65 insertions(+), 14 deletions(-) diff --git a/models/user.js b/models/user.js index f4edc8f63..c3c81010a 100644 --- a/models/user.js +++ b/models/user.js @@ -196,6 +196,7 @@ UserSchema.statics.changePassword = function(id, password) { }) .then((hashedPassword) => { return User.update({id}, { + $inc: {__v: 1}, $set: { password: hashedPassword } @@ -346,6 +347,10 @@ UserSchema.statics.findByIdArray = function(ids) { }); }; +/** + * Creates a JWT from a user email. Only works for local accounts. + * @param {String} email of the local user + */ UserSchema.statics.createJWT = function (email) { if (!email || typeof email !== 'string') { return Promise.reject('email is required when creating a JWT for resetting passord'); @@ -353,11 +358,42 @@ UserSchema.statics.createJWT = function (email) { email = email.toLowerCase(); - const payload = {email, jti: uuid.v4()}; + return this.findOne({profiles: {$elemMatch: {id: email}}}) + .then(user => { - const token = jwt.sign(payload, process.env.TALK_SESSION_SECRET, {expiresIn: '1d'}); + if (user === null) { + return Promise.reject(`Uh oh! We've never heard of ${email}. Maybe there's a typo in there?`); + } - return Promise.resolve(token); + 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 + */ +UserSchema.statics.verifyToken = function (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 this.findOne({id: decoded.userId}); + }); }; const User = mongoose.model('User', UserSchema); diff --git a/routes/admin/index.js b/routes/admin/index.js index 84c130ba8..2b967708a 100644 --- a/routes/admin/index.js +++ b/routes/admin/index.js @@ -1,5 +1,4 @@ const express = require('express'); -const jwt = require('jsonwebtoken'); const router = express.Router(); router.get('/embed/stream/preview', (req, res) => { @@ -7,15 +6,8 @@ router.get('/embed/stream/preview', (req, res) => { }); router.get('/password-reset/:token', (req, res, next) => { - jwt.verify(req.params.token, process.env.TALK_SESSION_SECRET, (error, decoded) => { - if (error) { - return res.status(400).json({error}); - } - - console.log(decoded); - - res.json(decoded); - }); + // render a page or something? + res.send('ok'); }); router.get('*', (req, res) => { diff --git a/routes/api/user/index.js b/routes/api/user/index.js index c0bc43c6a..8cbebfd1f 100644 --- a/routes/api/user/index.js +++ b/routes/api/user/index.js @@ -71,6 +71,27 @@ router.post('/:user_id/role', (req, res, next) => { .catch(next); }); +/** + * expects 2 fields in the body of the request + * 1) the token that was in the url of the email link {String} + * 2) the new password {String} + */ +router.post('/update-password', (req, res, next) => { + const {token, password} = req.body; + + User.verifyToken(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 @@ -91,7 +112,9 @@ router.post('/request-password-reset', (req, res, next) => { subject: 'password reset requested', from: 'coralcore@mozillafoundation.org', to: 'riley.davis@gmail.com', - html: `reset password` + html: `

We received a request to reset your password. If you did not request this change, you can ignore this email. + If you did, please click here to reset password.

+ ${process.env.NODE_ENV === 'production' ? '' : `

${token}

`}` }; return mailer.sendSimple(options); }) From 1795fc8863c290da8ccac19bec97f8b8180c4397 Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Wed, 16 Nov 2016 12:00:44 -0700 Subject: [PATCH 23/50] remove log --- routes/api/user/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/routes/api/user/index.js b/routes/api/user/index.js index 8cbebfd1f..67bc9197d 100644 --- a/routes/api/user/index.js +++ b/routes/api/user/index.js @@ -119,7 +119,6 @@ router.post('/request-password-reset', (req, res, next) => { return mailer.sendSimple(options); }) .then(success => { - console.log(success); res.json({success: true}); }) .catch(error => { From 9942d6eee40a80aa9305ffdac46a8aee3ffcbb91 Mon Sep 17 00:00:00 2001 From: riley Date: Wed, 16 Nov 2016 12:09:47 -0700 Subject: [PATCH 24/50] remove reference --- routes/api/user/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/api/user/index.js b/routes/api/user/index.js index 67bc9197d..808ad6940 100644 --- a/routes/api/user/index.js +++ b/routes/api/user/index.js @@ -118,7 +118,7 @@ router.post('/request-password-reset', (req, res, next) => { }; return mailer.sendSimple(options); }) - .then(success => { + .then(() => { res.json({success: true}); }) .catch(error => { From bd47495b633495d69f3427f2b96cbc20101cd1a5 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Wed, 16 Nov 2016 16:55:47 -0300 Subject: [PATCH 25/50] User Email Availability check --- client/coral-framework/actions/auth.js | 40 +++++++++++++---- client/coral-framework/constants/auth.js | 7 +++ client/coral-framework/helpers/response.js | 2 + client/coral-framework/reducers/auth.js | 23 +++++++--- .../coral-sign-in/components/SignInContent.js | 8 ++-- .../coral-sign-in/components/SignUpContent.js | 9 ++-- .../containers/SignInContainer.js | 24 ++++++---- routes/api/user/index.js | 44 +++++++++++++++++++ 8 files changed, 128 insertions(+), 29 deletions(-) diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js index 896fb9aab..e25eb4174 100644 --- a/client/coral-framework/actions/auth.js +++ b/client/coral-framework/actions/auth.js @@ -30,8 +30,8 @@ export const fetchSignIn = (formData) => dispatch => { // 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 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()); @@ -58,15 +58,15 @@ export const facebookCallback = (err, data) => dispatch => { // Sign Up Actions const signUpRequest = () => ({type: actions.FETCH_SIGNUP_REQUEST}); -const signUpSuccess = () => ({type: actions.FETCH_SIGNUP_SUCCESS}); -const signUpFailure = () => ({type: actions.FETCH_SIGNUP_FAILURE}); +const signUpSuccess = user => ({type: actions.FETCH_SIGNUP_SUCCESS, user}); +const signUpFailure = error => ({type: actions.FETCH_SIGNUP_FAILURE, error}); -export const fetchSignUp = () => dispatch => { +export const fetchSignUp = formData => dispatch => { dispatch(signUpRequest()); - fetch(`${base}/auth`, getInit('POST')) + fetch(`${base}/user`, getInit('POST', formData)) .then(handleResp) - .then(() => dispatch(signUpSuccess())) - .catch(error => dispatch(signUpFailure(error))); + .then(({user}) => signUpSuccess(user)) + .catch((error) => signUpFailure(error)); }; // Forgot Password Actions @@ -97,3 +97,27 @@ export const logout = () => dispatch => { .catch(error => dispatch(logOutFailure(error))); }; +// Availability Checks Actions + +const availabilityRequest = () => ({type: actions.FETCH_AVAILABILITY_REQUEST}); +const availabilitySuccess = () => ({type: actions.FETCH_AVAILABILITY_SUCCESS}); +const availabilityFailure = () => ({type: actions.FETCH_AVAILABILITY_FAILURE}); + +const availableField = field => ({type: actions.AVAILABLE_FIELD, field}); +const unavailableField = field => ({type: actions.UNAVAILABLE_FIELD, field}); + +export const fetchCheckAvailability = formData => dispatch => { + dispatch(availabilityRequest()); + fetch(`${base}/user/availability`, getInit('POST', formData)) + .then(handleResp) + .then(({status}) => { + const [field] = Object.keys(formData); + dispatch(availabilitySuccess()); + if (status === 'available') { + return dispatch(availableField(field)); + } + return dispatch(unavailableField(field)); + }) + .catch((error) => availabilityFailure(error)); +}; + diff --git a/client/coral-framework/constants/auth.js b/client/coral-framework/constants/auth.js index 06558d513..5c0135f58 100644 --- a/client/coral-framework/constants/auth.js +++ b/client/coral-framework/constants/auth.js @@ -23,3 +23,10 @@ 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 FETCH_AVAILABILITY_REQUEST = 'FETCH_AVAILABILITY_REQUEST'; +export const FETCH_AVAILABILITY_SUCCESS = 'FETCH_AVAILABILITY_SUCCESS'; +export const FETCH_AVAILABILITY_FAILURE = 'FETCH_AVAILABILITY_FAILURE'; + +export const AVAILABLE_FIELD = 'AVAILABLE_FIELD'; +export const UNAVAILABLE_FIELD = 'UNAVAILABLE_FIELD'; diff --git a/client/coral-framework/helpers/response.js b/client/coral-framework/helpers/response.js index bccfc5a04..a18bd155f 100644 --- a/client/coral-framework/helpers/response.js +++ b/client/coral-framework/helpers/response.js @@ -20,6 +20,8 @@ export const getInit = (method, body) => { export const handleResp = res => { if (res.status === 401) { throw new Error('Not Authorized to make this request'); + } else if (res.status === 500) { + throw new Error(res.json()); } else if (res.status > 399) { throw new Error('Error! Status ', res.status); } else if (res.status === 204) { diff --git a/client/coral-framework/reducers/auth.js b/client/coral-framework/reducers/auth.js index 001c98346..10ac121e7 100644 --- a/client/coral-framework/reducers/auth.js +++ b/client/coral-framework/reducers/auth.js @@ -10,21 +10,24 @@ import { FETCH_SIGNIN_SUCCESS, FETCH_SIGNIN_FACEBOOK_SUCCESS, FETCH_SIGNIN_FACEBOOK_FAILURE, - LOGOUT_SUCCESS + LOGOUT_SUCCESS, + FETCH_SIGNUP_FAILURE, + AVAILABLE_FIELD, + UNAVAILABLE_FIELD } from '../constants/auth'; const initialState = Map({ - auth: Map(), isLoading: false, loggedIn: false, user: null, showSignInDialog: false, view: 'SIGNIN', - signInError: '' + signInError: '', + signUpError: '', + emailAvailable: true, + displayNameAvailable: true, }); -export const getEmail = state => state.auth.formData.email; - export default function auth (state = initialState, action) { switch (action.type) { case SHOW_SIGNIN_DIALOG : @@ -60,10 +63,20 @@ export default function auth (state = initialState, action) { return state .set('error', action.error) .set('user', null); + case FETCH_SIGNUP_FAILURE: + console.log(action); + return state + .set('signUpError', action.error); case LOGOUT_SUCCESS: return state .set('loggedIn', false) .set('user', null); + case AVAILABLE_FIELD: + return state + .set(`${action.field}Available`, true); + case UNAVAILABLE_FIELD: + return state + .set(`${action.field}Available`, false); default : return state; } diff --git a/client/coral-sign-in/components/SignInContent.js b/client/coral-sign-in/components/SignInContent.js index aa3b712fa..2e22c0696 100644 --- a/client/coral-sign-in/components/SignInContent.js +++ b/client/coral-sign-in/components/SignInContent.js @@ -1,11 +1,9 @@ import React from 'react'; -import styles from './styles.css'; - 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); @@ -27,7 +25,7 @@ const SignInContent = ({handleChange, formData, ...props}) => ( {lang.t('signIn.or')} - { props.signInError && {props.signInError} } + { props.auth.signInError && {props.auth.signInError} } ( onChange={handleChange} /> { - !props.isLoading ? + !props.auth.isLoading ? diff --git a/client/coral-sign-in/components/SignUpContent.js b/client/coral-sign-in/components/SignUpContent.js index db005368f..2ae6267dc 100644 --- a/client/coral-sign-in/components/SignUpContent.js +++ b/client/coral-sign-in/components/SignUpContent.js @@ -1,8 +1,9 @@ import React from 'react'; -import styles from './styles.css'; import FormField from './FormField'; +import Alert from './Alert'; import Button from 'coral-ui/components/Button'; 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); @@ -24,6 +25,7 @@ const SignUpContent = ({handleChange, formData, ...props}) => ( {lang.t('signIn.or')} + { props.auth.signUpError && {props.auth.signUpError} } ( errorMsg={props.errors.email} onChange={handleChange} /> + { !props.auth.emailAvailable && This email is not available. } ( errorMsg={props.errors.username} onChange={handleChange} /> + { !props.auth.displayNameAvailable && This username is not available. } ( errorMsg={props.errors.confirmPassword} onChange={handleChange} /> - { - !props.isLoading ? + !props.auth.isLoading ? diff --git a/client/coral-sign-in/containers/SignInContainer.js b/client/coral-sign-in/containers/SignInContainer.js index 1fa3ff053..471c0c166 100644 --- a/client/coral-sign-in/containers/SignInContainer.js +++ b/client/coral-sign-in/containers/SignInContainer.js @@ -15,7 +15,8 @@ import { hideSignInDialog, fetchSignInFacebook, fetchForgotPassword, - facebookCallback + facebookCallback, + fetchCheckAvailability } from '../../coral-framework/actions/auth'; class SignInContainer extends Component { @@ -76,6 +77,13 @@ class SignInContainer extends Component { ...state, errors })); + + // Check Availability + + if (name === 'email' || name === 'displayName') { + this.props.checkAvailability({[name]: value}); + } + } } @@ -126,14 +134,11 @@ class SignInContainer extends Component { Sign in to comment @@ -142,7 +147,9 @@ class SignInContainer extends Component { } } -const mapStateToProps = ({auth}) => ({auth}); +const mapStateToProps = (state) => ({ + auth: state.auth.toJS() +}); const mapDispatchToProps = dispatch => ({ facebookCallback: (err, data) => dispatch(facebookCallback(err, data)), @@ -152,7 +159,8 @@ const mapDispatchToProps = dispatch => ({ fetchForgotPassword: (formData) => dispatch(fetchForgotPassword(formData)), showSignInDialog: () => dispatch(showSignInDialog()), changeView: (view) => dispatch(changeView(view)), - handleClose: () => dispatch(hideSignInDialog()) + handleClose: () => dispatch(hideSignInDialog()), + checkAvailability: (formData) => dispatch(fetchCheckAvailability(formData)) }); export default connect( diff --git a/routes/api/user/index.js b/routes/api/user/index.js index 18872eab7..4b428be33 100644 --- a/routes/api/user/index.js +++ b/routes/api/user/index.js @@ -60,4 +60,48 @@ router.get('/', (req, res, next) => { .catch(next); }); +router.post('/', (req, res, next) => { + const {email, password, displayName} = req.query; + + User + .createLocalUser(email, password, displayName) + .then(user => { + res.status(201).send(user); + }) + .catch(err => { + next(err); + }); +}); + +router.post('/availability', (req, res, next) => { + const {email} = req.body; + + if (email) { + return User.count({ + profiles: { + $elemMatch: { + id: email, + provider: 'local' + } + } + }) + .then(count => { + if (count) { + res.json({ + status: 'unavailable' + }); + } else { + res.json({ + status: 'available' + }); + } + }) + .catch(err => { + next(err); + }); + } + + return res.status(404).send('Wrong parameters'); +}); + module.exports = router; From 81299e8add5f4e15dcc194581196d956e26fa32c Mon Sep 17 00:00:00 2001 From: riley Date: Wed, 16 Nov 2016 13:20:07 -0700 Subject: [PATCH 26/50] better error reporting. log when env variable is unset --- models/user.js | 8 ++++++++ routes/api/user/index.js | 12 ++++++------ services/mailer.js | 8 ++++++++ 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/models/user.js b/models/user.js index c3c81010a..7d083f70d 100644 --- a/models/user.js +++ b/models/user.js @@ -2,9 +2,17 @@ const mongoose = require('../mongoose'); const uuid = require('uuid'); const bcrypt = require('bcrypt'); const jwt = require('jsonwebtoken'); +const debug = require('debug')('talk:user_model'); const SALT_ROUNDS = 10; +if (!process.env.TALK_SESSION_SECRET) { + debug('\n////////////////////////////////////////////////////////////\n' + + '/// TALK_SESSION_SECRET must be defined to encode ///\n' + + '/// JSON Web Tokens and other auth functionality ///\n' + + '////////////////////////////////////////////////////////////'); +} + const UserSchema = new mongoose.Schema({ id: { type: String, diff --git a/routes/api/user/index.js b/routes/api/user/index.js index 808ad6940..c9b497659 100644 --- a/routes/api/user/index.js +++ b/routes/api/user/index.js @@ -99,8 +99,6 @@ router.post('/update-password', (req, res, next) => { router.post('/request-password-reset', (req, res, next) => { const {email} = req.body; - console.log('/request-password-reset', req.body); - if (!email) { return next(); } @@ -109,12 +107,12 @@ router.post('/request-password-reset', (req, res, next) => { .createJWT(email) .then(token => { const options = { - subject: 'password reset requested', + subject: 'Password Reset Requested - Talk', from: 'coralcore@mozillafoundation.org', - to: 'riley.davis@gmail.com', + to: email, html: `

We received a request to reset your password. If you did not request this change, you can ignore this email. If you did, please click here to reset password.

- ${process.env.NODE_ENV === 'production' ? '' : `

${token}

`}` + ${process.env.NODE_ENV === 'production' ? '' : `

${token}

`}` }; return mailer.sendSimple(options); }) @@ -122,7 +120,9 @@ router.post('/request-password-reset', (req, res, next) => { res.json({success: true}); }) .catch(error => { - res.status(500).json({error}); + const errorMsg = typeof error === 'string' ? error : error.message; + + res.status(500).json({error: errorMsg}); }); }); diff --git a/services/mailer.js b/services/mailer.js index eb900d782..89115d619 100644 --- a/services/mailer.js +++ b/services/mailer.js @@ -1,11 +1,19 @@ const nodemailer = require('nodemailer'); const sgTransport = require('nodemailer-sendgrid-transport'); +const debug = require('debug')('talk:mail'); const options = { auth: { api_key: process.env.TALK_SENDGRID_APIKEY } }; +if (!process.env.TALK_SENDGRID_APIKEY) { + debug('\n///////////////////////////////////////////////////////////////\n' + + '/// TALK_SENDGRID_APIKEY should be defined if you would ///\n' + + '/// like to send password reset emails from Talk ///\n' + + '///////////////////////////////////////////////////////////////'); +} + const transporter = nodemailer.createTransport(sgTransport(options)); const mailer = { From e027a162d6d46599e0cb30b04498ffc1831b7343 Mon Sep 17 00:00:00 2001 From: riley Date: Wed, 16 Nov 2016 14:00:33 -0700 Subject: [PATCH 27/50] remove sendgrid shim and use connection string --- package.json | 1 - services/mailer.js | 8 +------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/package.json b/package.json index 17d5bdc9f..b279050f6 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,6 @@ "mongoose": "^4.6.5", "morgan": "^1.7.0", "nodemailer": "^2.6.4", - "nodemailer-sendgrid-transport": "^0.2.0", "prompt": "^1.0.0", "uuid": "^2.0.3" }, diff --git a/services/mailer.js b/services/mailer.js index 89115d619..cf7a4d26f 100644 --- a/services/mailer.js +++ b/services/mailer.js @@ -1,11 +1,5 @@ const nodemailer = require('nodemailer'); -const sgTransport = require('nodemailer-sendgrid-transport'); const debug = require('debug')('talk:mail'); -const options = { - auth: { - api_key: process.env.TALK_SENDGRID_APIKEY - } -}; if (!process.env.TALK_SENDGRID_APIKEY) { debug('\n///////////////////////////////////////////////////////////////\n' + @@ -14,7 +8,7 @@ if (!process.env.TALK_SENDGRID_APIKEY) { '///////////////////////////////////////////////////////////////'); } -const transporter = nodemailer.createTransport(sgTransport(options)); +const transporter = nodemailer.createTransport(`smtps://apikey:${process.env.TALK_SENDGRID_APIKEY}@smtp.sendgrid.net`); const mailer = { /** From 967472ea74443d14dd69a4216c1c86b7de37f7b8 Mon Sep 17 00:00:00 2001 From: riley Date: Wed, 16 Nov 2016 14:38:29 -0700 Subject: [PATCH 28/50] udpate smtp module to take a full smtp connection string --- services/mailer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/mailer.js b/services/mailer.js index cf7a4d26f..d90d04071 100644 --- a/services/mailer.js +++ b/services/mailer.js @@ -1,14 +1,14 @@ const nodemailer = require('nodemailer'); const debug = require('debug')('talk:mail'); -if (!process.env.TALK_SENDGRID_APIKEY) { +if (!process.env.TALK_SMTP_STRING) { debug('\n///////////////////////////////////////////////////////////////\n' + '/// TALK_SENDGRID_APIKEY should be defined if you would ///\n' + '/// like to send password reset emails from Talk ///\n' + '///////////////////////////////////////////////////////////////'); } -const transporter = nodemailer.createTransport(`smtps://apikey:${process.env.TALK_SENDGRID_APIKEY}@smtp.sendgrid.net`); +const transporter = nodemailer.createTransport(process.env.TALK_SMTP_STRING); const mailer = { /** From 6e932b840ef11689240289a35312b98441884b20 Mon Sep 17 00:00:00 2001 From: Riley Davis Date: Wed, 16 Nov 2016 15:32:44 -0700 Subject: [PATCH 29/50] merge master. load reset password template from a view --- .../coral-plugin-commentbox/translations.json | 2 +- models/user.js | 159 +++++++++++++----- routes/api/user/index.js | 47 ++---- views/password-reset-email.ejs | 6 + 4 files changed, 146 insertions(+), 68 deletions(-) create mode 100644 views/password-reset-email.ejs diff --git a/client/coral-plugin-commentbox/translations.json b/client/coral-plugin-commentbox/translations.json index e8eb8ad52..29f3212b9 100644 --- a/client/coral-plugin-commentbox/translations.json +++ b/client/coral-plugin-commentbox/translations.json @@ -5,7 +5,7 @@ "comment": "Comment", "name": "Name", "comment-post-notif": "Your comment has been posted.", - "comment-post-notif-premod": "Thank you for reporting this comment. Our moderation team has been notified and will review it shortly." + "comment-post-notif-premod": "Thank you for posting. Our moderation team will review your comment shortly." }, "es": { "post": "Publicar", diff --git a/models/user.js b/models/user.js index 7d083f70d..bbf8f21e6 100644 --- a/models/user.js +++ b/models/user.js @@ -4,8 +4,16 @@ const bcrypt = require('bcrypt'); const jwt = require('jsonwebtoken'); const debug = require('debug')('talk:user_model'); +// SALT_ROUNDS is the number of rounds that the bcrypt algorithm will run +// through during the salting process. const SALT_ROUNDS = 10; +// USER_ROLES is the array of roles that is permissible as a user role. +const USER_ROLES = [ + 'admin', + 'moderator' +]; + if (!process.env.TALK_SESSION_SECRET) { debug('\n////////////////////////////////////////////////////////////\n' + '/// TALK_SESSION_SECRET must be defined to encode ///\n' + @@ -13,30 +21,62 @@ if (!process.env.TALK_SESSION_SECRET) { '////////////////////////////////////////////////////////////'); } +// UserSchema is the mongoose schema defined as the representation of a User in +// MongoDB. const UserSchema = new mongoose.Schema({ + + // This ID represents the most unique identifier for a user, it is generated + // when the user is created as a random uuid. id: { type: String, default: uuid.v4, unique: true, required: true }, + + // This is sourced from the social provider or set manually during user setup + // and simply provides a name to display for the given user. displayName: String, + + // This is true when the user account is disabled, no action should be + // acknowledged when they are disabled. Logins are also prevented. disabled: Boolean, + + // This provides a source of identity proof for users who login using the + // local provider. A local provider will be assumed for users who do not + // have any social profiles. password: String, - profiles: [{ + + // Profiles describes the array of identities for a given user. Any one user + // can have multiple profiles associated with them, including multiple email + // addresses. + profiles: [new mongoose.Schema({ + + // ID provides the identifier for the user profile, in the case of a local + // provider, the id would be an email, in the case of a social provider, + // the id would be the foreign providers identifier. id: { type: String, required: true }, + + // Provider is simply the name attached to the authentication mode. In the + // case of a locally provided profile, this will simply be `local`, or a + // social provider which for Facebook would just be `facebook`. provider: { type: String, required: true } - }], - roles: { - type: [{type: String, enum: ['admin', 'moderator']}] - } + }, { + _id: false + })], + + // Roles provides an array of roles (as strings) that is associated with a + // user. + roles: [String] }, { + + // This will ensure that we have proper timestamps available on this model. timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' @@ -84,6 +124,13 @@ UserSchema.options.toObject.transform = (doc, ret, options) => { return ret; }; +// Create the User model. +const UserModel = mongoose.model('User', UserSchema); + +// UserService is the interface for the application to interact with the +// UserModel through. +const UserService = module.exports = {}; + /** * Finds a user given their email address that we have for them in the system * and ensures that the retuned user matches the password passed in as well. @@ -91,13 +138,12 @@ UserSchema.options.toObject.transform = (doc, ret, options) => { * @param {string} password - password to match against the found user * @param {Function} done [description] */ -UserSchema.statics.findLocalUser = function(email, password) { - +UserService.findLocalUser = (email, password) => { if (!email || typeof email !== 'string') { return Promise.reject('email is required for findLocalUser'); } - return User.findOne({ + return UserModel.findOne({ profiles: { $elemMatch: { id: email.toLowerCase(), @@ -134,13 +180,13 @@ UserSchema.statics.findLocalUser = function(email, password) { * @param {String} srcUserID id of the user to which is the source of the merge * @return {Promise} resolves when the users are merged */ -UserSchema.statics.mergeUsers = function(dstUserID, srcUserID) { +UserService.mergeUsers = (dstUserID, srcUserID) => { let srcUser, dstUser; return Promise .all([ - User.findOne({id: dstUserID}).exec(), - User.findOne({id: srcUserID}).exec() + UserModel.findOne({id: dstUserID}).exec(), + UserModel.findOne({id: srcUserID}).exec() ]) .then((users) => { dstUser = users[0]; @@ -161,8 +207,8 @@ UserSchema.statics.mergeUsers = function(dstUserID, srcUserID) { * @param {Object} profile - User social/external profile * @param {Function} done [description] */ -UserSchema.statics.findOrCreateExternalUser = function(profile) { - return User +UserService.findOrCreateExternalUser = (profile) => { + return UserModel .findOne({ profiles: { $elemMatch: { @@ -177,7 +223,7 @@ UserSchema.statics.findOrCreateExternalUser = function(profile) { } // The user was not found, lets create them! - user = new User({ + user = new UserModel({ displayName: profile.displayName, roles: [], profiles: [ @@ -192,7 +238,7 @@ UserSchema.statics.findOrCreateExternalUser = function(profile) { }); }; -UserSchema.statics.changePassword = function(id, password) { +UserService.changePassword = (id, password) => { return new Promise((resolve, reject) => { bcrypt.hash(password, SALT_ROUNDS, (err, hashedPassword) => { if (err) { @@ -203,7 +249,7 @@ UserSchema.statics.changePassword = function(id, password) { }); }) .then((hashedPassword) => { - return User.update({id}, { + return UserModel.update({id}, { $inc: {__v: 1}, $set: { password: hashedPassword @@ -217,9 +263,9 @@ UserSchema.statics.changePassword = function(id, password) { * @param {Array} users Users to create * @return {Promise} Resolves with the users that were created */ -UserSchema.statics.createLocalUsers = function(users) { +UserService.createLocalUsers = (users) => { return Promise.all(users.map((user) => { - return User + return UserService .createLocalUser(user.email, user.password, user.displayName); })); }; @@ -231,7 +277,7 @@ UserSchema.statics.createLocalUsers = function(users) { * @param {String} displayName name of the display user * @param {Function} done callback */ -UserSchema.statics.createLocalUser = function(email, password, displayName) { +UserService.createLocalUser = (email, password, displayName) => { if (!email) { return Promise.reject('email is required'); } @@ -252,7 +298,7 @@ UserSchema.statics.createLocalUser = function(email, password, displayName) { return reject(err); } - let user = new User({ + let user = new UserModel({ displayName: displayName, password: hashedPassword, roles: [], @@ -280,8 +326,8 @@ UserSchema.statics.createLocalUser = function(email, password, displayName) { * @param {String} id id of a user * @param {Function} done callback after the operation is complete */ -UserSchema.statics.disableUser = function(id) { - return User.update({ +UserService.disableUser = (id) => { + return UserModel.update({ id: id }, { $set: { @@ -295,8 +341,8 @@ UserSchema.statics.disableUser = function(id) { * @param {String} id id of a user * @param {Function} done callback after the operation is complete */ -UserSchema.statics.enableUser = function(id) { - return User.update({ +UserService.enableUser = (id) => { + return UserModel.update({ id: id }, { $set: { @@ -311,8 +357,16 @@ UserSchema.statics.enableUser = function(id) { * @param {String} role role to add * @param {Function} done callback after the operation is complete */ -UserSchema.statics.addRoleToUser = function(id, role) { - return User.update({ +UserService.addRoleToUser = (id, role) => { + + // Check to see if the user role is in the allowable set of roles. + if (USER_ROLES.indexOf(role) === -1) { + + // User role is not supported! Error out here. + return Promise.reject(new Error(`role ${role} is not supported`)); + } + + return UserModel.update({ id: id }, { $addToSet: { @@ -327,8 +381,8 @@ UserSchema.statics.addRoleToUser = function(id, role) { * @param {String} role role to remove * @param {Function} done callback after the operation is complete */ -UserSchema.statics.removeRoleFromUser = function(id, role) { - return User.update({ +UserService.removeRoleFromUser = (id, role) => { + return UserModel.update({ id: id }, { $pull: { @@ -341,16 +395,16 @@ UserSchema.statics.removeRoleFromUser = function(id, role) { * Finds a user with the id. * @param {String} id user id (uuid) */ -UserSchema.statics.findById = function(id) { - return User.findOne({id}); +UserService.findById = (id) => { + return UserModel.findOne({id}); }; /** * Finds users in an array of idd. * @param {Array} ids array of user identifiers (uuid) */ -UserSchema.statics.findByIdArray = function(ids) { - return User.find({ +UserService.findByIdArray = (ids) => { + return UserModel.find({ 'id': {$in: ids} }); }; @@ -359,14 +413,14 @@ UserSchema.statics.findByIdArray = function(ids) { * Creates a JWT from a user email. Only works for local accounts. * @param {String} email of the local user */ -UserSchema.statics.createJWT = function (email) { +UserService.createJWT = function (email) { if (!email || typeof email !== 'string') { return Promise.reject('email is required when creating a JWT for resetting passord'); } email = email.toLowerCase(); - return this.findOne({profiles: {$elemMatch: {id: email}}}) + return UserModel.findOne({profiles: {$elemMatch: {id: email}}}) .then(user => { if (user === null) { @@ -384,7 +438,7 @@ UserSchema.statics.createJWT = function (email) { * verifies a jwt and returns the associated user * @param {String} token the JSON Web Token to verify */ -UserSchema.statics.verifyToken = function (token) { +UserService.verifyToken = function (token) { return new Promise((resolve, reject) => { jwt.verify(token, process.env.TALK_SESSION_SECRET, (error, decoded) => { if (error) { @@ -404,7 +458,36 @@ UserSchema.statics.verifyToken = function (token) { }); }; -const User = mongoose.model('User', UserSchema); +/** + * Finds a user using a value which gets compared using a prefix match against + * the user's email address and/or their display name. + * @param {String} value value to search by + * @return {Promise} + */ +UserService.search = (value) => { + return UserModel.find({ + $or: [ -module.exports = User; -module.exports.Schema = UserSchema; + // Search by a prefix match on the displayName. + { + 'displayName': { + $regex: new RegExp(`^${value}`), + $options: 'i' + } + }, + + // Search by a prefix match on the email address. + { + 'profiles': { + $elemMatch: { + id: { + $regex: new RegExp(`^${value}`), + $options: 'i' + }, + provider: 'local' + } + } + } + ] + }); +}; diff --git a/routes/api/user/index.js b/routes/api/user/index.js index c9b497659..4f9110b8f 100644 --- a/routes/api/user/index.js +++ b/routes/api/user/index.js @@ -2,6 +2,11 @@ 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 { @@ -12,28 +17,9 @@ router.get('/', (req, res, next) => { limit = 50 // Total Per Page } = req.query; - let q = { - $or: [ - { - 'displayName': { - $regex: new RegExp(`^${value}`), - $options: 'i' - }, - 'profiles': { - $elemMatch: { - id: { - $regex: new RegExp(`^${value}`), - $options: 'i' - }, - provider: 'local' - } - } - } - ] - }; - Promise.all([ - User.find(q) + User + .search(value) .sort({[field]: (asc === 'true') ? 1 : -1}) .skip((page - 1) * limit) .limit(limit), @@ -64,11 +50,12 @@ router.get('/', (req, res, next) => { }); router.post('/:user_id/role', (req, res, next) => { - User.addRoleToUser(req.params.user_id, req.body.role) - .then(role => { - res.send(role); - }) - .catch(next); + User + .addRoleToUser(req.params.user_id, req.body.role) + .then(role => { + res.send(role); + }) + .catch(next); }); /** @@ -110,9 +97,11 @@ router.post('/request-password-reset', (req, res, next) => { subject: 'Password Reset Requested - Talk', from: 'coralcore@mozillafoundation.org', to: email, - html: `

We received a request to reset your password. If you did not request this change, you can ignore this email. - If you did, please click here to reset password.

- ${process.env.NODE_ENV === 'production' ? '' : `

${token}

`}` + html: resetEmailTemplate({ + token, + // probably more clear to explicitly pass this + rootURL: process.env.TALK_ROOT_URL + }) }; return mailer.sendSimple(options); }) diff --git a/views/password-reset-email.ejs b/views/password-reset-email.ejs new file mode 100644 index 000000000..0637b6bb2 --- /dev/null +++ b/views/password-reset-email.ejs @@ -0,0 +1,6 @@ + +

We received a request to reset your password. If you did not request this change, you can ignore this email.
+If you did, please click here to reset password.

+<% if (process.env.NODE_ENV !== 'production') { %> +

<%= token %>

+<% } %> From 5b35d3b61dd454fa73c593a33aae257416cee4d9 Mon Sep 17 00:00:00 2001 From: riley Date: Wed, 16 Nov 2016 15:38:42 -0700 Subject: [PATCH 30/50] update to TALK_SMTP_CONNECTION_URL --- services/mailer.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/mailer.js b/services/mailer.js index d90d04071..347e7a63d 100644 --- a/services/mailer.js +++ b/services/mailer.js @@ -1,14 +1,14 @@ const nodemailer = require('nodemailer'); const debug = require('debug')('talk:mail'); -if (!process.env.TALK_SMTP_STRING) { +if (!process.env.TALK_SMTP_CONNECTION_URL) { debug('\n///////////////////////////////////////////////////////////////\n' + - '/// TALK_SENDGRID_APIKEY should be defined if you would ///\n' + + '/// TALK_SMTP_CONNECTION_URL should be defined if you would ///\n' + '/// like to send password reset emails from Talk ///\n' + '///////////////////////////////////////////////////////////////'); } -const transporter = nodemailer.createTransport(process.env.TALK_SMTP_STRING); +const transporter = nodemailer.createTransport(process.env.TALK_SMTP_CONNECTION_URL); const mailer = { /** From 3c835c1ce5a3ef7eb7e4ff7bf37d409615faa1a0 Mon Sep 17 00:00:00 2001 From: riley Date: Wed, 16 Nov 2016 15:59:44 -0700 Subject: [PATCH 31/50] update from address --- routes/api/user/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/api/user/index.js b/routes/api/user/index.js index 4f9110b8f..b9c6efa3b 100644 --- a/routes/api/user/index.js +++ b/routes/api/user/index.js @@ -95,7 +95,7 @@ router.post('/request-password-reset', (req, res, next) => { .then(token => { const options = { subject: 'Password Reset Requested - Talk', - from: 'coralcore@mozillafoundation.org', + from: 'noreply@coralproject.net', to: email, html: resetEmailTemplate({ token, From 60c17fc973fd38594434c253c95b36d670336733 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Wed, 16 Nov 2016 20:56:05 -0300 Subject: [PATCH 32/50] =?UTF-8?q?=C3=81dding=20Debouncing=20for=20CheckAva?= =?UTF-8?q?ilability,=20Valid=20Check=20form=20and=20Styling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/coral-framework/actions/auth.js | 13 ++- client/coral-framework/helpers/error.js | 4 +- client/coral-framework/helpers/response.js | 2 - client/coral-framework/helpers/validate.js | 2 +- client/coral-framework/reducers/auth.js | 24 +++--- client/coral-sign-in/components/FormField.js | 2 +- .../coral-sign-in/components/SignInContent.js | 18 +++-- .../coral-sign-in/components/SignUpContent.js | 36 +++++---- client/coral-sign-in/components/styles.css | 30 ++++++- .../containers/SignInContainer.js | 81 ++++++++++--------- client/coral-sign-in/translations.js | 10 ++- package.json | 1 + routes/api/user/index.js | 11 +-- 13 files changed, 137 insertions(+), 97 deletions(-) diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js index e25eb4174..19fa2c499 100644 --- a/client/coral-framework/actions/auth.js +++ b/client/coral-framework/actions/auth.js @@ -23,7 +23,10 @@ export const fetchSignIn = (formData) => dispatch => { dispatch(signInRequest()); fetch(`${base}/auth/local`, getInit('POST', formData)) .then(handleResp) - .then(({user}) => dispatch(signInSuccess(user))) + .then(({user}) => { + dispatch(hideSignInDialog()); + dispatch(signInSuccess(user)); + }) .catch(() => dispatch(signInFailure('Email and/or password combination incorrect.'))); }; @@ -49,6 +52,7 @@ export const facebookCallback = (err, data) => dispatch => { } try { dispatch(signInFacebookSuccess(JSON.parse(data))); + dispatch(hideSignInDialog()); } catch (err) { dispatch(signInFacebookFailure(err)); return; @@ -65,8 +69,11 @@ export const fetchSignUp = formData => dispatch => { dispatch(signUpRequest()); fetch(`${base}/user`, getInit('POST', formData)) .then(handleResp) - .then(({user}) => signUpSuccess(user)) - .catch((error) => signUpFailure(error)); + .then(({user}) => { + dispatch(signUpSuccess(user)); + dispatch(hideSignInDialog()); + }) + .catch((error) => dispatch(signUpFailure(error))); }; // Forgot Password Actions diff --git a/client/coral-framework/helpers/error.js b/client/coral-framework/helpers/error.js index 2674bc55b..735ad7a5d 100644 --- a/client/coral-framework/helpers/error.js +++ b/client/coral-framework/helpers/error.js @@ -1,6 +1,6 @@ export default { email: 'Not a valid E-Mail', password: 'Password must be at least 8 characters', - username: 'Username is too short', - confirmPassword: 'Passwords do not match' + displayName: 'Display name is too short', + confirmPassword: 'Passwords don`t match. Please, check again' }; diff --git a/client/coral-framework/helpers/response.js b/client/coral-framework/helpers/response.js index a18bd155f..bccfc5a04 100644 --- a/client/coral-framework/helpers/response.js +++ b/client/coral-framework/helpers/response.js @@ -20,8 +20,6 @@ export const getInit = (method, body) => { export const handleResp = res => { if (res.status === 401) { throw new Error('Not Authorized to make this request'); - } else if (res.status === 500) { - throw new Error(res.json()); } else if (res.status > 399) { throw new Error('Error! Status ', res.status); } else if (res.status === 204) { diff --git a/client/coral-framework/helpers/validate.js b/client/coral-framework/helpers/validate.js index 9fda4b1c2..8c5ebd36f 100644 --- a/client/coral-framework/helpers/validate.js +++ b/client/coral-framework/helpers/validate.js @@ -2,5 +2,5 @@ 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, - username: username => (/^(?=.{3,}).*$/.test(username)) + displayName: displayName => (/^(?=.{3,}).*$/.test(displayName)) }; diff --git a/client/coral-framework/reducers/auth.js b/client/coral-framework/reducers/auth.js index 10ac121e7..c2bdc20db 100644 --- a/client/coral-framework/reducers/auth.js +++ b/client/coral-framework/reducers/auth.js @@ -25,7 +25,6 @@ const initialState = Map({ signInError: '', signUpError: '', emailAvailable: true, - displayNameAvailable: true, }); export default function auth (state = initialState, action) { @@ -34,7 +33,14 @@ export default function auth (state = initialState, action) { return state .set('showSignInDialog', true); case HIDE_SIGNIN_DIALOG : - return initialState; + return state.merge(Map({ + isLoading: false, + showSignInDialog: false, + view: 'SIGNIN', + signInError: '', + signUpError: '', + emailAvailable: true, + })); case CHANGE_VIEW : return state .set('view', action.view); @@ -43,22 +49,18 @@ export default function auth (state = initialState, action) { case FETCH_SIGNIN_REQUEST: return state .set('isLoading', true); + case FETCH_SIGNIN_SUCCESS: + return state + .set('loggedIn', true) + .set('user', action.user); case FETCH_SIGNIN_FAILURE: return state .set('isLoading', false) .set('signInError', action.error); - case FETCH_SIGNIN_SUCCESS: - return state - .set('signInError', '') - .set('isLoading', false) - .set('loggedIn', true) - .set('user', action.user) - .set('showSignInDialog', false); case FETCH_SIGNIN_FACEBOOK_SUCCESS: return state .set('user', action.user) - .set('loggedIn', true) - .set('showSignInDialog', false); + .set('loggedIn', true); case FETCH_SIGNIN_FACEBOOK_FAILURE: return state .set('error', action.error) diff --git a/client/coral-sign-in/components/FormField.js b/client/coral-sign-in/components/FormField.js index 62e50e637..ceb2d939e 100644 --- a/client/coral-sign-in/components/FormField.js +++ b/client/coral-sign-in/components/FormField.js @@ -11,7 +11,7 @@ const FormField = ({className, showErrors = false, errorMsg, label, ...props}) = name={props.id} {...props} /> - {showErrors && errorMsg && {errorMsg}} + {showErrors && errorMsg && !{errorMsg}} ); diff --git a/client/coral-sign-in/components/SignInContent.js b/client/coral-sign-in/components/SignInContent.js index 2e22c0696..8aa688133 100644 --- a/client/coral-sign-in/components/SignInContent.js +++ b/client/coral-sign-in/components/SignInContent.js @@ -41,14 +41,16 @@ const SignInContent = ({handleChange, formData, ...props}) => ( value={formData.password} onChange={handleChange} /> - { - !props.auth.isLoading ? - - : - - } +
+ { + !props.auth.isLoading ? + + : + + } +
props.changeView('FORGOT')}>{lang.t('signIn.forgotYourPass')} diff --git a/client/coral-sign-in/components/SignUpContent.js b/client/coral-sign-in/components/SignUpContent.js index 2ae6267dc..adc377dbf 100644 --- a/client/coral-sign-in/components/SignUpContent.js +++ b/client/coral-sign-in/components/SignUpContent.js @@ -32,21 +32,20 @@ const SignUpContent = ({handleChange, formData, ...props}) => ( type="email" label={lang.t('signIn.email')} value={formData.email} - showErrors={props.showErrors} - errorMsg={props.errors.email} + showErrors={!props.auth.emailAvailable || props.showErrors} + errorMsg={!props.auth.emailAvailable ? lang.t('signIn.emailInUse') : props.errors.email} onChange={handleChange} + autoFocus /> - { !props.auth.emailAvailable && This email is not available. } - { !props.auth.displayNameAvailable && This username is not available. } ( showErrors={props.showErrors} errorMsg={props.errors.password} onChange={handleChange} + minLength="8" /> + { !props.errors.password && Password must be at least 8 characters. } ( showErrors={props.showErrors} errorMsg={props.errors.confirmPassword} onChange={handleChange} + minLength="8" /> - { - !props.auth.isLoading ? - - : - - } +
+ { + !props.auth.isLoading ? + + : + + } +
diff --git a/client/coral-sign-in/components/styles.css b/client/coral-sign-in/components/styles.css index a7c14d891..9cd7bd326 100644 --- a/client/coral-sign-in/components/styles.css +++ b/client/coral-sign-in/components/styles.css @@ -15,6 +15,10 @@ font-size: 1.2em; } +.formField { + margin-top: 15px; +} + .formField label { font-size: 1.08em; font-weight: bold; @@ -74,10 +78,10 @@ } input.error{ - border: solid 1px #f44336; + border: solid 2px #f44336; } -.errorMsg { +.errorMsg, .hint { color: grey; font-weight: 600; padding: 3px 0 16px; @@ -86,6 +90,7 @@ input.error{ .alert { padding: 10px; margin-bottom: 20px; + border-radius: 2px; } .alert--success { @@ -103,4 +108,25 @@ input.error{ 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; } \ No newline at end of file diff --git a/client/coral-sign-in/containers/SignInContainer.js b/client/coral-sign-in/containers/SignInContainer.js index 471c0c166..7a5ae51ac 100644 --- a/client/coral-sign-in/containers/SignInContainer.js +++ b/client/coral-sign-in/containers/SignInContainer.js @@ -1,11 +1,12 @@ import React, {Component} from 'react'; import {connect} from 'react-redux'; +import debounce from 'lodash.debounce'; import SignDialog from '../components/SignDialog'; import Button from 'coral-ui/components/Button'; import validate from 'coral-framework/helpers/validate'; -import error from 'coral-framework/helpers/error'; +import errorMsj from 'coral-framework/helpers/error'; import { changeView, @@ -23,7 +24,7 @@ class SignInContainer extends Component { initialState = { formData: { email: '', - username: '', + displayName: '', password: '', confirmPassword: '' }, @@ -39,7 +40,7 @@ class SignInContainer extends Component { this.handleSignIn = this.handleSignIn.bind(this); this.handleClose = this.handleClose.bind(this); this.cleanState = this.cleanState.bind(this); - this.validation = this.validation.bind(this); + this.addError = this.addError.bind(this); } componentDidMount() { @@ -52,72 +53,77 @@ class SignInContainer extends Component { handleChange(e) { const {name, value} = e.target; - this.validation(name, value, this.state.formData); - 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) { - if (!validate[name](value)) { - this.setState(state => ({ - errors: { - ...state.errors, - [name]: error[name] - } - })); + const {addError} = this; + const {formData} = this.state; + const {checkAvailability} = this.props; + + if (!value.length) { + addError(name, 'Please, fill this field'); + } else if (name === 'confirmPassword' && formData.confirmPassword !== formData.password) { + addError(name, 'Passwords don`t match. Please, check again.'); + } else if (!validate[name](value)) { + addError(name, errorMsj[name]); } else { const { [name]: prop, ...errors } = this.state.errors; // eslint-disable-line - this.setState(state => ({ - ...state, - errors - })); - - // Check Availability - - if (name === 'email' || name === 'displayName') { - this.props.checkAvailability({[name]: value}); + // Removes Error + this.setState(state => ({...state, errors})); + // Checks Email Availability + if (name === 'email') { + debounce(checkAvailability({[name]: value}), 250); } - } } isCompleted() { const {formData} = this.state; - return !Object.keys(formData).filter(prop => !formData[prop].length).length; + const {emailAvailable} = this.props.auth; + return !Object.keys(formData).filter(prop => !formData[prop].length).length && emailAvailable; } displayErrors(show = true) { - this.setState({ - showErrors: show - }); + this.setState({showErrors: show}); } handleSignUp(e) { e.preventDefault(); + const {errors} = this.state; this.displayErrors(); - if (this.isCompleted()) { - console.log('Is Completed!!'); - } else { - console.log('Is not Completed!!'); + if (this.isCompleted() && !Object.keys(errors).length) { + this.props.fetchSignUp(this.state.formData); + this.cleanState(); } - this.props.fetchSignUp(this.state.formData); } handleSignIn(e) { e.preventDefault(); - this.displayErrors(); this.props.fetchSignIn(this.state.formData); + this.cleanState(); } handleClose() { - this.cleanState(); this.props.hideSignInDialog(); + this.cleanState(); } changeView(view) { @@ -127,19 +133,16 @@ class SignInContainer extends Component { render() { const {auth, showSignInDialog} = this.props; - const {errors, showErrors, formData} = this.state; return (
@@ -147,7 +150,7 @@ class SignInContainer extends Component { } } -const mapStateToProps = (state) => ({ +const mapStateToProps = state => ({ auth: state.auth.toJS() }); diff --git a/client/coral-sign-in/translations.js b/client/coral-sign-in/translations.js index a0fd7015e..fdf7de0a0 100644 --- a/client/coral-sign-in/translations.js +++ b/client/coral-sign-in/translations.js @@ -13,9 +13,10 @@ export default { register: 'Register', signUp: 'Sign Up', confirmPassword: 'Confirm Password', - username: 'Username', + displayName: 'Display Name', alreadyHaveAnAccount: 'Already have an account?', - recoverPassword: 'Recover password' + recoverPassword: 'Recover password', + emailInUse: 'Email address already in use' } }, es: { @@ -32,9 +33,10 @@ export default { register: 'Regístrate', signUp: 'Registro', confirmPassword: 'Confirmar Contraseña', - username: 'Usuario', + displayName: 'Nombre', alreadyHaveAnAccount: 'Ya tienes una cuenta?', - recoverPassword: 'Recuperar contraseña' + recoverPassword: 'Recuperar contraseña', + emailInUse: 'Este email se encuentra en uso' } } }; diff --git a/package.json b/package.json index 4341483af..b913aefce 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "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", diff --git a/routes/api/user/index.js b/routes/api/user/index.js index 4b428be33..e8bc90ac2 100644 --- a/routes/api/user/index.js +++ b/routes/api/user/index.js @@ -61,7 +61,7 @@ router.get('/', (req, res, next) => { }); router.post('/', (req, res, next) => { - const {email, password, displayName} = req.query; + const {email, password, displayName} = req.body; User .createLocalUser(email, password, displayName) @@ -87,14 +87,9 @@ router.post('/availability', (req, res, next) => { }) .then(count => { if (count) { - res.json({ - status: 'unavailable' - }); - } else { - res.json({ - status: 'available' - }); + return res.json({status: 'unavailable'}); } + return res.json({status: 'available'}); }) .catch(err => { next(err); From b116bef0027705f1efd29c834ccc4dd43ffdc4d1 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Wed, 16 Nov 2016 20:57:42 -0300 Subject: [PATCH 33/50] =?UTF-8?q?=C3=81dding=20Debouncing=20for=20CheckAva?= =?UTF-8?q?ilability,=20Valid=20Check=20form=20and=20Styling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/coral-sign-in/containers/SignInContainer.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/client/coral-sign-in/containers/SignInContainer.js b/client/coral-sign-in/containers/SignInContainer.js index 7a5ae51ac..9c3e7903f 100644 --- a/client/coral-sign-in/containers/SignInContainer.js +++ b/client/coral-sign-in/containers/SignInContainer.js @@ -111,23 +111,19 @@ class SignInContainer extends Component { this.displayErrors(); if (this.isCompleted() && !Object.keys(errors).length) { this.props.fetchSignUp(this.state.formData); - this.cleanState(); } } handleSignIn(e) { e.preventDefault(); this.props.fetchSignIn(this.state.formData); - this.cleanState(); } handleClose() { this.props.hideSignInDialog(); - this.cleanState(); } changeView(view) { - this.cleanState(); this.props.changeView(view); } From 9f13357d258869d5129cdffc980db6785210c071 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Wed, 16 Nov 2016 21:15:34 -0300 Subject: [PATCH 34/50] =?UTF-8?q?=C3=81dding=20Debouncing=20for=20CheckAva?= =?UTF-8?q?ilability,=20Valid=20Check=20form=20and=20Styling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/coral-sign-in/containers/SignInContainer.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/client/coral-sign-in/containers/SignInContainer.js b/client/coral-sign-in/containers/SignInContainer.js index 9c3e7903f..120e97120 100644 --- a/client/coral-sign-in/containers/SignInContainer.js +++ b/client/coral-sign-in/containers/SignInContainer.js @@ -39,7 +39,6 @@ class SignInContainer extends Component { this.handleSignUp = this.handleSignUp.bind(this); this.handleSignIn = this.handleSignIn.bind(this); this.handleClose = this.handleClose.bind(this); - this.cleanState = this.cleanState.bind(this); this.addError = this.addError.bind(this); } @@ -47,10 +46,6 @@ class SignInContainer extends Component { window.authCallback = this.props.facebookCallback; } - cleanState () { - this.setState(this.initialState); - } - handleChange(e) { const {name, value} = e.target; this.setState(state => ({ @@ -90,7 +85,7 @@ class SignInContainer extends Component { this.setState(state => ({...state, errors})); // Checks Email Availability if (name === 'email') { - debounce(checkAvailability({[name]: value}), 250); + debounce(() => checkAvailability({[name]: value}), 250); } } } @@ -135,7 +130,7 @@ class SignInContainer extends Component { Sign in to comment Date: Thu, 17 Nov 2016 10:39:51 -0300 Subject: [PATCH 35/50] Success Button SVG and Redirect to signIn --- client/coral-framework/actions/auth.js | 4 +- client/coral-framework/reducers/auth.js | 56 ++++++++----------- .../coral-sign-in/components/SignUpContent.js | 16 +++--- .../containers/SignInContainer.js | 4 +- client/coral-ui/components/Success.css | 55 ++++++++++++++++++ client/coral-ui/components/Success.js | 13 +++++ 6 files changed, 105 insertions(+), 43 deletions(-) create mode 100644 client/coral-ui/components/Success.css create mode 100644 client/coral-ui/components/Success.js diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js index 19fa2c499..fd749cd50 100644 --- a/client/coral-framework/actions/auth.js +++ b/client/coral-framework/actions/auth.js @@ -71,7 +71,9 @@ export const fetchSignUp = formData => dispatch => { .then(handleResp) .then(({user}) => { dispatch(signUpSuccess(user)); - dispatch(hideSignInDialog()); + setTimeout(() =>{ + dispatch(changeView('SIGNIN')); + }, 3000); }) .catch((error) => dispatch(signUpFailure(error))); }; diff --git a/client/coral-framework/reducers/auth.js b/client/coral-framework/reducers/auth.js index c2bdc20db..1662a9635 100644 --- a/client/coral-framework/reducers/auth.js +++ b/client/coral-framework/reducers/auth.js @@ -1,20 +1,5 @@ import {Map} from 'immutable'; - -import { - SHOW_SIGNIN_DIALOG, - HIDE_SIGNIN_DIALOG, - CHANGE_VIEW, - CLEAN_STATE, - FETCH_SIGNIN_REQUEST, - FETCH_SIGNIN_FAILURE, - FETCH_SIGNIN_SUCCESS, - FETCH_SIGNIN_FACEBOOK_SUCCESS, - FETCH_SIGNIN_FACEBOOK_FAILURE, - LOGOUT_SUCCESS, - FETCH_SIGNUP_FAILURE, - AVAILABLE_FIELD, - UNAVAILABLE_FIELD -} from '../constants/auth'; +import * as actions from '../constants/auth'; const initialState = Map({ isLoading: false, @@ -22,17 +7,17 @@ const initialState = Map({ user: null, showSignInDialog: false, view: 'SIGNIN', - signInError: '', signUpError: '', emailAvailable: true, + successSignUp: false }); export default function auth (state = initialState, action) { switch (action.type) { - case SHOW_SIGNIN_DIALOG : + case actions.SHOW_SIGNIN_DIALOG : return state .set('showSignInDialog', true); - case HIDE_SIGNIN_DIALOG : + case actions.HIDE_SIGNIN_DIALOG : return state.merge(Map({ isLoading: false, showSignInDialog: false, @@ -40,43 +25,50 @@ export default function auth (state = initialState, action) { signInError: '', signUpError: '', emailAvailable: true, + successSignUp: false })); - case CHANGE_VIEW : + case actions.CHANGE_VIEW : return state .set('view', action.view); - case CLEAN_STATE: + case actions.CLEAN_STATE: return initialState; - case FETCH_SIGNIN_REQUEST: + case actions.FETCH_SIGNIN_REQUEST: return state .set('isLoading', true); - case FETCH_SIGNIN_SUCCESS: + case actions.FETCH_SIGNIN_SUCCESS: return state .set('loggedIn', true) .set('user', action.user); - case FETCH_SIGNIN_FAILURE: + case actions.FETCH_SIGNIN_FAILURE: return state .set('isLoading', false) .set('signInError', action.error); - case FETCH_SIGNIN_FACEBOOK_SUCCESS: + case actions.FETCH_SIGNIN_FACEBOOK_SUCCESS: return state .set('user', action.user) .set('loggedIn', true); - case FETCH_SIGNIN_FACEBOOK_FAILURE: + case actions.FETCH_SIGNIN_FACEBOOK_FAILURE: return state .set('error', action.error) .set('user', null); - case FETCH_SIGNUP_FAILURE: - console.log(action); + case actions.FETCH_SIGNUP_REQUEST: return state - .set('signUpError', action.error); - case LOGOUT_SUCCESS: + .set('isLoading', true); + case actions.FETCH_SIGNUP_FAILURE: + return state + .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 AVAILABLE_FIELD: + case actions.AVAILABLE_FIELD: return state .set(`${action.field}Available`, true); - case UNAVAILABLE_FIELD: + case actions.UNAVAILABLE_FIELD: return state .set(`${action.field}Available`, false); default : diff --git a/client/coral-sign-in/components/SignUpContent.js b/client/coral-sign-in/components/SignUpContent.js index adc377dbf..f90e0db5f 100644 --- a/client/coral-sign-in/components/SignUpContent.js +++ b/client/coral-sign-in/components/SignUpContent.js @@ -3,6 +3,7 @@ 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'; @@ -68,14 +69,13 @@ const SignUpContent = ({handleChange, formData, ...props}) => ( minLength="8" />
- { - !props.auth.isLoading ? - - : - - } + { !props.auth.isLoading && !props.auth.successSignUp && ( + + )} + { props.auth.isLoading && } + { !props.auth.isLoading && props.auth.successSignUp && }
diff --git a/client/coral-sign-in/containers/SignInContainer.js b/client/coral-sign-in/containers/SignInContainer.js index 120e97120..0d138c3b0 100644 --- a/client/coral-sign-in/containers/SignInContainer.js +++ b/client/coral-sign-in/containers/SignInContainer.js @@ -76,7 +76,7 @@ class SignInContainer extends Component { if (!value.length) { addError(name, 'Please, fill this field'); } else if (name === 'confirmPassword' && formData.confirmPassword !== formData.password) { - addError(name, 'Passwords don`t match. Please, check again.'); + addError('confirmPassword', 'Passwords don`t match. Please, check again'); } else if (!validate[name](value)) { addError(name, errorMsj[name]); } else { @@ -85,7 +85,7 @@ class SignInContainer extends Component { this.setState(state => ({...state, errors})); // Checks Email Availability if (name === 'email') { - debounce(() => checkAvailability({[name]: value}), 250); + debounce(() => checkAvailability({[name]: value}), 200, { 'maxWait': 1000 })(); } } } diff --git a/client/coral-ui/components/Success.css b/client/coral-ui/components/Success.css new file mode 100644 index 000000000..00b5ba260 --- /dev/null +++ b/client/coral-ui/components/Success.css @@ -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; + } +} \ No newline at end of file diff --git a/client/coral-ui/components/Success.js b/client/coral-ui/components/Success.js new file mode 100644 index 000000000..7429b9a6e --- /dev/null +++ b/client/coral-ui/components/Success.js @@ -0,0 +1,13 @@ +import React from 'react'; +import styles from './Success.css'; + +const Success = () => ( +
+ + + + +
+); + +export default Success; \ No newline at end of file From a8f28f64b71a34148213401e9f9441895882262e Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Thu, 17 Nov 2016 11:00:35 -0300 Subject: [PATCH 36/50] Merge Issues --- client/coral-admin/src/components/Comment.js | 2 +- client/coral-admin/src/components/Header.js | 2 +- client/coral-admin/src/components/ModerationKeysModal.js | 2 +- client/coral-admin/src/components/ui/Drawer.js | 2 +- client/coral-admin/src/components/ui/Header.js | 2 +- client/coral-admin/src/containers/Community/Community.js | 2 +- client/coral-admin/src/containers/Community/Table.js | 2 +- client/coral-admin/src/containers/Configure.js | 2 +- client/coral-admin/src/containers/ModerationQueue.js | 2 +- client/coral-framework/reducers/auth.js | 1 + 10 files changed, 10 insertions(+), 9 deletions(-) diff --git a/client/coral-admin/src/components/Comment.js b/client/coral-admin/src/components/Comment.js index c115bcf61..fa94f74a4 100644 --- a/client/coral-admin/src/components/Comment.js +++ b/client/coral-admin/src/components/Comment.js @@ -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 diff --git a/client/coral-admin/src/components/Header.js b/client/coral-admin/src/components/Header.js index b2bee0a44..aa3ba6708 100644 --- a/client/coral-admin/src/components/Header.js +++ b/client/coral-admin/src/components/Header.js @@ -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 diff --git a/client/coral-admin/src/components/ModerationKeysModal.js b/client/coral-admin/src/components/ModerationKeysModal.js index d350c74e0..5a951e588 100644 --- a/client/coral-admin/src/components/ModerationKeysModal.js +++ b/client/coral-admin/src/components/ModerationKeysModal.js @@ -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'; diff --git a/client/coral-admin/src/components/ui/Drawer.js b/client/coral-admin/src/components/ui/Drawer.js index 12bf7c1d2..2003ed34d 100644 --- a/client/coral-admin/src/components/ui/Drawer.js +++ b/client/coral-admin/src/components/ui/Drawer.js @@ -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 () => ( diff --git a/client/coral-admin/src/components/ui/Header.js b/client/coral-admin/src/components/ui/Header.js index d85a91744..2e0f77b6e 100644 --- a/client/coral-admin/src/components/ui/Header.js +++ b/client/coral-admin/src/components/ui/Header.js @@ -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 () => ( diff --git a/client/coral-admin/src/containers/Community/Community.js b/client/coral-admin/src/containers/Community/Community.js index fdb542811..f8c24fd3a 100644 --- a/client/coral-admin/src/containers/Community/Community.js +++ b/client/coral-admin/src/containers/Community/Community.js @@ -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'; diff --git a/client/coral-admin/src/containers/Community/Table.js b/client/coral-admin/src/containers/Community/Table.js index 97848bc9a..89737e33e 100644 --- a/client/coral-admin/src/containers/Community/Table.js +++ b/client/coral-admin/src/containers/Community/Table.js @@ -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'; diff --git a/client/coral-admin/src/containers/Configure.js b/client/coral-admin/src/containers/Configure.js index 7d057eacc..d5b4ce516 100644 --- a/client/coral-admin/src/containers/Configure.js +++ b/client/coral-admin/src/containers/Configure.js @@ -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 { diff --git a/client/coral-admin/src/containers/ModerationQueue.js b/client/coral-admin/src/containers/ModerationQueue.js index cfc48ae6b..7a30b8487 100644 --- a/client/coral-admin/src/containers/ModerationQueue.js +++ b/client/coral-admin/src/containers/ModerationQueue.js @@ -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'; /* diff --git a/client/coral-framework/reducers/auth.js b/client/coral-framework/reducers/auth.js index 1662a9635..5c2335f17 100644 --- a/client/coral-framework/reducers/auth.js +++ b/client/coral-framework/reducers/auth.js @@ -29,6 +29,7 @@ export default function auth (state = initialState, action) { })); case actions.CHANGE_VIEW : return state + .set('signUpError', '') .set('view', action.view); case actions.CLEAN_STATE: return initialState; From da1539422b6368bbeaec34485ba43889f9b8bc36 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Thu, 17 Nov 2016 11:18:17 -0300 Subject: [PATCH 37/50] Using User Interface --- models/user.js | 17 +++++++++++++++++ routes/api/user/index.js | 26 +++++++++----------------- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/models/user.js b/models/user.js index d841480d8..9446a949b 100644 --- a/models/user.js +++ b/models/user.js @@ -426,3 +426,20 @@ UserService.search = (value) => { ] }); }; + +/** + * Finds users by email and returns the count. The result should be 1 or 0 (bool) indicating email availability + * @param {String} email to search by + * @return {Promise} + */ +UserService.availabilityCheck = (email) => { + return UserModel.count({ + profiles: { + $elemMatch: { + id: email, + provider: 'local' + } + } + }); +}; + diff --git a/routes/api/user/index.js b/routes/api/user/index.js index ce594168c..a702adb41 100644 --- a/routes/api/user/index.js +++ b/routes/api/user/index.js @@ -69,25 +69,17 @@ router.post('/availability', (req, res, next) => { const {email} = req.body; if (email) { - return User.count({ - profiles: { - $elemMatch: { - id: email, - provider: 'local' + return User.availabilityCheck(email) + .then(count => { + if (count) { + return res.json({status: 'unavailable'}); } - } - }) - .then(count => { - if (count) { - return res.json({status: 'unavailable'}); - } - return res.json({status: 'available'}); - }) - .catch(err => { - next(err); - }); + return res.json({status: 'available'}); + }) + .catch(err => { + next(err); + }); } - return res.status(404).send('Wrong parameters'); }); From 032de329a6c82c6e7e46f3d96e8b89ebbb9205dd Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Thu, 17 Nov 2016 11:19:56 -0300 Subject: [PATCH 38/50] Linting --- client/coral-sign-in/containers/SignInContainer.js | 2 +- client/coral-ui/components/Success.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/coral-sign-in/containers/SignInContainer.js b/client/coral-sign-in/containers/SignInContainer.js index 0d138c3b0..d5972e9bf 100644 --- a/client/coral-sign-in/containers/SignInContainer.js +++ b/client/coral-sign-in/containers/SignInContainer.js @@ -85,7 +85,7 @@ class SignInContainer extends Component { this.setState(state => ({...state, errors})); // Checks Email Availability if (name === 'email') { - debounce(() => checkAvailability({[name]: value}), 200, { 'maxWait': 1000 })(); + debounce(() => checkAvailability({[name]: value}), 200, {'maxWait': 1000})(); } } } diff --git a/client/coral-ui/components/Success.js b/client/coral-ui/components/Success.js index 7429b9a6e..4db697794 100644 --- a/client/coral-ui/components/Success.js +++ b/client/coral-ui/components/Success.js @@ -10,4 +10,4 @@ const Success = () => (
); -export default Success; \ No newline at end of file +export default Success; From c967587b95043a72108bc0ed4d55be259b607341 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Thu, 17 Nov 2016 11:40:11 -0300 Subject: [PATCH 39/50] Pym and Iframe support embed --- client/coral-embed-stream/style/default.css | 1 + client/coral-framework/reducers/auth.js | 2 +- client/coral-sign-in/components/styles.css | 3 +- views/article.ejs | 37 +++++++++++++++------ views/embed-stream.ejs | 16 +++++---- views/embed/stream.ejs | 2 +- 6 files changed, 41 insertions(+), 20 deletions(-) diff --git a/client/coral-embed-stream/style/default.css b/client/coral-embed-stream/style/default.css index 2f7394d4d..285dd3cb9 100644 --- a/client/coral-embed-stream/style/default.css +++ b/client/coral-embed-stream/style/default.css @@ -4,6 +4,7 @@ body { width: 100%; font-size: 12px; margin: 0; + min-height: 700px; } button { diff --git a/client/coral-framework/reducers/auth.js b/client/coral-framework/reducers/auth.js index 5c2335f17..bf7fef368 100644 --- a/client/coral-framework/reducers/auth.js +++ b/client/coral-framework/reducers/auth.js @@ -29,7 +29,7 @@ export default function auth (state = initialState, action) { })); case actions.CHANGE_VIEW : return state - .set('signUpError', '') + .set('signInError', '') .set('view', action.view); case actions.CLEAN_STATE: return initialState; diff --git a/client/coral-sign-in/components/styles.css b/client/coral-sign-in/components/styles.css index 9cd7bd326..81864eb2f 100644 --- a/client/coral-sign-in/components/styles.css +++ b/client/coral-sign-in/components/styles.css @@ -1,9 +1,8 @@ .dialog { - width: 400px; - border: none; 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 { diff --git a/views/article.ejs b/views/article.ejs index 95c0d7904..7d168f960 100644 --- a/views/article.ejs +++ b/views/article.ejs @@ -3,23 +3,40 @@ - - + -
+

<%= title %>

-

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.

+

+ 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. +

Visit the moderation console

+
+
-
- - -
+ + + diff --git a/views/embed-stream.ejs b/views/embed-stream.ejs index 36f7ce43b..e1ddb17e7 100644 --- a/views/embed-stream.ejs +++ b/views/embed-stream.ejs @@ -11,18 +11,22 @@ body, #root { width: 100%; height: 100%; - margin: 0; - background: #fff; + min-height: 600px; + margin: 0; + background: #fff; }
+ + -
- + var pymParent = new pym.Parent('coralStreamEmbed', '/embed/stream', {title: 'Talk Comments'}); + pymParent.onMessage('height', function(height) { + document.querySelector('#coralStreamEmbed iframe').height = height + 'px'; + }); + diff --git a/views/embed/stream.ejs b/views/embed/stream.ejs index dd7142c3b..5cdef0811 100644 --- a/views/embed/stream.ejs +++ b/views/embed/stream.ejs @@ -7,7 +7,7 @@ rel="stylesheet"> -
+
From a2dd08fc9878ce03c45d16a59cf581b51c788d5f Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Thu, 17 Nov 2016 11:43:10 -0300 Subject: [PATCH 40/50] Forgot Password endpoint --- client/coral-framework/actions/auth.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js index fd749cd50..e3bf0babd 100644 --- a/client/coral-framework/actions/auth.js +++ b/client/coral-framework/actions/auth.js @@ -86,7 +86,7 @@ const forgotPassowordFailure = () => ({type: actions.FETCH_FORGOT_PASSWORD_FAILU export const fetchForgotPassword = () => dispatch => { dispatch(forgotPassowordRequest()); - fetch(`${base}/forgot`, getInit('POST')) + fetch(`${base}/user/request-password-reset`, getInit('POST')) .then(handleResp) .then(() => dispatch(forgotPassowordSuccess())) .catch(error => dispatch(forgotPassowordFailure(error))); From 21f83c6f9abfe2da9b0f07978cfb49e3b6da3bd4 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Thu, 17 Nov 2016 11:54:15 -0300 Subject: [PATCH 41/50] Removing add Name --- client/coral-plugin-commentbox/CommentBox.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/client/coral-plugin-commentbox/CommentBox.js b/client/coral-plugin-commentbox/CommentBox.js index 9f1ea39c6..e712d8e24 100644 --- a/client/coral-plugin-commentbox/CommentBox.js +++ b/client/coral-plugin-commentbox/CommentBox.js @@ -55,15 +55,6 @@ class CommentBox extends Component { const {styles, reply, canPost} = this.props; // How to handle language in plugins? Should we have a dependency on our central translation file? return
-
- this.setState({username: e.target.value})}/> -