diff --git a/common/models/user.js b/common/models/user.js index b8d63e66db..b3291b773b 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -17,13 +17,14 @@ import _ from 'lodash'; import jwt from 'jsonwebtoken'; import generate from 'nanoid/generate'; +import { homeLocation } from '../../config/env'; + import { fixCompletedChallengeItem } from '../utils'; import { themes } from '../utils/themes'; import { saveUser, observeMethod } from '../../server/utils/rx.js'; import { blacklistedUsernames } from '../../server/utils/constants.js'; import { wrapHandledError } from '../../server/utils/create-handled-error.js'; import { - getServerFullURL, getEmailSender } from '../../server/utils/url-utils.js'; import { @@ -255,7 +256,7 @@ module.exports = function(User) { throw wrapHandledError( new Error('user already exists'), { - redirectTo: '/signin', + redirectTo: `${homeLocation}/signin`, message: dedent` The ${user.email} email address is already associated with an account. Try signing in with it here instead. @@ -595,7 +596,7 @@ module.exports = function(User) { } const { id: loginToken, created: emailAuthLinkTTL } = token; const loginEmail = this.getEncodedEmail(newEmail ? newEmail : null); - const host = getServerFullURL(); + const host = homeLocation; const mailOptions = { type: 'email', to: newEmail ? newEmail : this.email, diff --git a/config/env.js b/config/env.js new file mode 100644 index 0000000000..d0833ae121 --- /dev/null +++ b/config/env.js @@ -0,0 +1,3 @@ +module.exports = { + homeLocation: process.env.HOME_LOCATION +}; diff --git a/gatsby-config.js b/gatsby-config.js index 944db9d25c..4ee0192d26 100644 --- a/gatsby-config.js +++ b/gatsby-config.js @@ -3,7 +3,7 @@ module.exports = { title: 'Gatsby Default Starter' }, proxy: { - prefix: '/external', + prefix: '/internal', url: 'http://localhost:3000' }, plugins: [ diff --git a/sample.env b/sample.env index 037b84803f..63828aab11 100644 --- a/sample.env +++ b/sample.env @@ -18,3 +18,5 @@ PEER=stuff DEBUG=true IMAGE_BASE_URL='https://s3.amazonaws.com/freecodecamp/images/' + +HOME_LOCATION='http://localhost:8000' diff --git a/server/boot/a-services.js b/server/boot/a-services.js index 9286814d60..a14422a2af 100644 --- a/server/boot/a-services.js +++ b/server/boot/a-services.js @@ -16,4 +16,5 @@ export default function bootServices(app) { const middleware = Fetchr.middleware(); app.use('/services', middleware); app.use('/external/services', middleware); + app.use('/internal/services', middleware); } diff --git a/server/boot/authentication.js b/server/boot/authentication.js index 9a354adb4a..6363fe48e0 100644 --- a/server/boot/authentication.js +++ b/server/boot/authentication.js @@ -5,13 +5,14 @@ import dedent from 'dedent'; import { isEmail } from 'validator'; import { check } from 'express-validator/check'; +import { homeLocation } from '../../config/env'; + import { ifUserRedirectTo, ifNoUserRedirectTo, createValidatorErrorHandler } from '../utils/middleware'; import { wrapHandledError } from '../utils/create-handled-error.js'; -import { homeURL } from '../../common/utils/constantStrings.json'; const isSignUpDisabled = !!process.env.DISABLE_SIGNUP; // const debug = debugFactory('fcc:boot:auth'); @@ -24,43 +25,21 @@ module.exports = function enableAuthentication(app) { // loopback.io/doc/en/lb2/Authentication-authorization-and-permissions.html app.enableAuth(); const ifUserRedirect = ifUserRedirectTo(); - const ifNoUserRedirectHome = ifNoUserRedirectTo(homeURL); - const router = app.loopback.Router(); + const ifNoUserRedirectHome = ifNoUserRedirectTo(homeLocation); const api = app.loopback.Router(); const { AuthToken, User } = app.models; - router.get('/signup', (req, res) => res.redirect(301, '/signin')); - router.get('/email-signin', (req, res) => res.redirect(301, '/signin')); - router.get('/login', (req, res) => res.redirect(301, '/signin')); - router.get('/deprecated-signin', (req, res) => res.redirect(301, '/signin')); + api.get('/signin', ifUserRedirect, (req, res) => res.redirect('/auth/auth0')); - router.get('/logout', (req, res) => res.redirect(301, '/signout')); - - router.get('/signin', - ifUserRedirect, - (req, res) => res.redirect('/auth/auth0') - ); - - router.get( - '/update-email', - ifNoUserRedirectHome, - (req, res) => res.render('account/update-email', { - title: 'Update your email' - }) - ); - - router.get('/signout', (req, res) => { + api.get('/signout', (req, res) => { req.logout(); - req.session.destroy( (err) => { + req.session.destroy(err => { if (err) { - throw wrapHandledError( - new Error('could not destroy session'), - { - type: 'info', - message: 'Oops, something is not right.', - redirectTo: '/' - } - ); + throw wrapHandledError(new Error('could not destroy session'), { + type: 'info', + message: 'Oops, something is not right.', + redirectTo: homeLocation + }); } const config = { signed: !!req.signedCookies, @@ -70,8 +49,8 @@ module.exports = function enableAuthentication(app) { res.clearCookie('access_token', config); res.clearCookie('userId', config); res.clearCookie('_csrf', config); - res.redirect('/'); - }); + res.redirect(homeLocation); + }); }); const defaultErrorMsg = dedent` @@ -93,112 +72,105 @@ module.exports = function enableAuthentication(app) { function getPasswordlessAuth(req, res, next) { const { - query: { - email: encodedEmail, - token: authTokenId, - emailChange - } = {} + query: { email: encodedEmail, token: authTokenId, emailChange } = {} } = req; const email = User.decodeEmail(encodedEmail); if (!isEmail(email)) { - return next(wrapHandledError( - new TypeError('decoded email is invalid'), - { + return next( + wrapHandledError(new TypeError('decoded email is invalid'), { type: 'info', message: 'The email encoded in the link is incorrectly formatted', - redirectTo: '/signin' - } - )); + redirectTo: `${homeLocation}/signin` + }) + ); } // first find - return AuthToken.findOne$({ where: { id: authTokenId } }) - .flatMap(authToken => { - if (!authToken) { - throw wrapHandledError( - new Error(`no token found for id: ${authTokenId}`), - { - type: 'info', - message: defaultErrorMsg, - redirectTo: '/signin' - } - ); - } - // find user then validate and destroy email validation token - // finally retun user instance - return User.findOne$({ where: { id: authToken.userId } }) - .flatMap(user => { - if (!user) { - throw wrapHandledError( - new Error(`no user found for token: ${authTokenId}`), - { - type: 'info', - message: defaultErrorMsg, - redirectTo: '/signin' - } - ); - } - if (user.email !== email) { - if (!emailChange || (emailChange && user.newEmail !== email)) { + return ( + AuthToken.findOne$({ where: { id: authTokenId } }) + .flatMap(authToken => { + if (!authToken) { + throw wrapHandledError( + new Error(`no token found for id: ${authTokenId}`), + { + type: 'info', + message: defaultErrorMsg, + redirectTo: `${homeLocation}/signin` + } + ); + } + // find user then validate and destroy email validation token + // finally retun user instance + return User.findOne$({ where: { id: authToken.userId } }).flatMap( + user => { + if (!user) { throw wrapHandledError( - new Error('user email does not match'), + new Error(`no user found for token: ${authTokenId}`), { type: 'info', message: defaultErrorMsg, - redirectTo: '/signin' + redirectTo: `${homeLocation}/signin` } ); } - } - return authToken.validate$() - .map(isValid => { - if (!isValid) { + if (user.email !== email) { + if (!emailChange || (emailChange && user.newEmail !== email)) { throw wrapHandledError( - new Error('token is invalid'), + new Error('user email does not match'), { + type: 'info', + message: defaultErrorMsg, + redirectTo: `${homeLocation}/signin` + } + ); + } + } + return authToken + .validate$() + .map(isValid => { + if (!isValid) { + throw wrapHandledError(new Error('token is invalid'), { type: 'info', message: ` Looks like the link you clicked has expired, please request a fresh link, to sign in. `, - redirectTo: '/signin' - } - ); - } - return authToken.destroy$(); - }) - .map(() => user); - }); - }) - // at this point token has been validated and destroyed - // update user and log them in - .map(user => user.loginByRequest(req, res)) - .do(() => { - req.flash( - 'success', - 'Success! You have signed in to your account. Happy Coding!' - ); - return res.redirect('/'); - }) - .subscribe( - () => {}, - next - ); + redirectTo: `${homeLocation}/signin` + }); + } + return authToken.destroy$(); + }) + .map(() => user); + } + ); + }) + // at this point token has been validated and destroyed + // update user and log them in + .map(user => user.loginByRequest(req, res)) + .do(() => { + req.flash( + 'success', + 'Success! You have signed in to your account. Happy Coding!' + ); + return res.redirect(homeLocation); + }) + .subscribe(() => {}, next) + ); } - router.get( + api.get( '/passwordless-auth', ifUserRedirect, passwordlessGetValidators, - createValidatorErrorHandler('errors', '/signin'), + createValidatorErrorHandler('errors', `${homeLocation}/signin`), getPasswordlessAuth ); - router.get( - '/passwordless-change', - (req, res) => res.redirect(301, '/confirm-email') + api.get('/passwordless-change', (req, res) => + res.redirect(301, '/confirm-email') ); - router.get( + + api.get( '/confirm-email', ifNoUserRedirectHome, passwordlessGetValidators, @@ -214,21 +186,18 @@ module.exports = function enableAuthentication(app) { const { body: { email } = {} } = req; return User.findOne$({ where: { email } }) - .flatMap(_user => Observable.if( + .flatMap(_user => + Observable.if( // if no user found create new user and save to db _.constant(_user), Observable.of(_user), User.create$({ email }) - ) - .flatMap(user => user.requestAuthEmail(!_user)) + ).flatMap(user => user.requestAuthEmail(!_user)) ) .do(msg => { - let redirectTo = '/'; + let redirectTo = homeLocation; - if ( - req.session && - req.session.returnTo - ) { + if (req.session && req.session.returnTo) { redirectTo = req.session.returnTo; } @@ -242,10 +211,10 @@ module.exports = function enableAuthentication(app) { '/passwordless-auth', ifUserRedirect, passwordlessPostValidators, - createValidatorErrorHandler('errors', '/signin'), + createValidatorErrorHandler('errors', `${homeLocation}/signin`), postPasswordlessAuth ); - app.use(router); app.use(api); + app.use('/internal', api); }; diff --git a/server/boot/challenge.js b/server/boot/challenge.js index b6264febc8..83d754984f 100644 --- a/server/boot/challenge.js +++ b/server/boot/challenge.js @@ -169,6 +169,7 @@ export default function(app) { app.use(api); app.use('/external', api); + app.use('/internal', api); app.use(router); function modernChallengeCompleted(req, res, next) { diff --git a/server/boot/commit.js b/server/boot/commit.js index d5d523217c..4224a562c9 100644 --- a/server/boot/commit.js +++ b/server/boot/commit.js @@ -3,6 +3,8 @@ import { Observable } from 'rx'; import debugFactory from 'debug'; import dedent from 'dedent'; +import { homeLocation } from '../../config/env'; + import nonprofits from '../utils/commit.json'; import { commitGoals, @@ -23,7 +25,7 @@ import { } from '../utils/middleware'; const sendNonUserToSignIn = ifNoUserRedirectTo( - '/signin', + `${homeLocation}/signin`, 'You must be signed in to commit to a nonprofit.', 'info' ); diff --git a/server/boot/settings.js b/server/boot/settings.js index 64b695fab3..4de5360cbd 100644 --- a/server/boot/settings.js +++ b/server/boot/settings.js @@ -175,7 +175,7 @@ export default function settingsController(app) { refetchCompletedChallenges ); api.post('/update-flags', ifNoUser401, updateFlags); - api.post( + api.put( '/update-my-email', ifNoUser401, updateMyEmailValidators, @@ -190,7 +190,7 @@ export default function settingsController(app) { updateMyCurrentChallenge ); api.post( - '/external/update-my-current-challenge', + '/update-my-current-challenge', ifNoUser401, updateMyCurrentChallengeValidators, createValidatorErrorHandler(alertTypes.danger), @@ -209,5 +209,6 @@ export default function settingsController(app) { api.post('/update-my-username', ifNoUser401, updateMyUsername); app.use('/external', api); + app.use('/internal', api); app.use(api); } diff --git a/server/boot/user.js b/server/boot/user.js index 9c690f01c7..93cbaf29b8 100644 --- a/server/boot/user.js +++ b/server/boot/user.js @@ -42,9 +42,9 @@ module.exports = function bootUser(app) { getReportUserProfile ); - app.use(router); app.use('/external', api); + app.use('/internal', api); }; function readSessionUser(req, res, next) { diff --git a/server/middlewares/csurf.js b/server/middlewares/csurf.js index bf49d0fb66..081a44f1ce 100644 --- a/server/middlewares/csurf.js +++ b/server/middlewares/csurf.js @@ -11,7 +11,7 @@ export default function() { return function csrf(req, res, next) { const path = req.path.split('/')[1]; - if (/(api|external|^p$)/.test(path)) { + if (/(^api$|^external$|^internal$|^p$)/.test(path)) { return next(); } return protection(req, res, next); diff --git a/server/middlewares/error-handlers.js b/server/middlewares/error-handlers.js index 59519db5b8..99f6ecb39f 100644 --- a/server/middlewares/error-handlers.js +++ b/server/middlewares/error-handlers.js @@ -1,6 +1,9 @@ import { inspect } from 'util'; import _ from 'lodash/fp'; import accepts from 'accepts'; + +import { homeLocation } from '../../config/env'; + import { unwrapHandledError } from '../utils/create-handled-error.js'; const isDev = process.env.NODE_ENV !== 'production'; @@ -57,7 +60,7 @@ export default function prodErrorHandler() { const accept = accepts(req); const type = accept.type('html', 'json', 'text'); - const redirectTo = handled.redirectTo || '/'; + const redirectTo = handled.redirectTo || `${homeLocation}/`; const message = handled.message || 'Oops! Something went wrong. Please try again later'; diff --git a/server/middlewares/jwt-authorization.js b/server/middlewares/jwt-authorization.js index 7473f0df0c..058cb1e53b 100644 --- a/server/middlewares/jwt-authorization.js +++ b/server/middlewares/jwt-authorization.js @@ -2,11 +2,13 @@ import loopback from 'loopback'; import jwt from 'jsonwebtoken'; import { isBefore } from 'date-fns'; +import { homeLocation } from '../../config/env'; + import { wrapHandledError } from '../utils/create-handled-error'; export default () => function authorizeByJWT(req, res, next) { const path = req.path.split('/')[1]; - if (/external/.test(path)) { + if (/^external$|^internal$/.test(path)) { const cookie = req.signedCookies && req.signedCookies['jwt_access_token'] || req.cookie && req.cookie['jwt_access_token']; if (!cookie) { @@ -14,7 +16,7 @@ export default () => function authorizeByJWT(req, res, next) { new Error('Access token is required for this request'), { type: 'info', - redirect: '/signin', + redirect: `${homeLocation}/signin`, message: 'Access token is required for this request', status: 403 } @@ -28,7 +30,7 @@ export default () => function authorizeByJWT(req, res, next) { new Error(err.message), { type: 'info', - redirct: '/signin', + redirect: `${homeLocation}/signin`, message: 'Your access token is invalid', status: 403 } @@ -41,7 +43,7 @@ export default () => function authorizeByJWT(req, res, next) { new Error('Access token is no longer vaild'), { type: 'info', - redirect: '/signin', + redirect: `${homeLocation}/signin`, message: 'Access token is no longer vaild', status: 403 } diff --git a/server/views/emails/user-request-sign-in.ejs b/server/views/emails/user-request-sign-in.ejs index 91d052b6a8..ea9c39b320 100644 --- a/server/views/emails/user-request-sign-in.ejs +++ b/server/views/emails/user-request-sign-in.ejs @@ -1,6 +1,6 @@ -Here's your sign in link. It will instantly sign you into freeCodeCamp.org - no password necessary: +Here's your sign in link. It will instantly sign you into freeCodeCamp.org - no password necessary: -<%= host %>/passwordless-auth/?email=<%= loginEmail %>&token=<%= loginToken %> +<%= host %>/internal/passwordless-auth/?email=<%= loginEmail %>&token=<%= loginToken %> Note: this sign in link will expire after 15 minutes. If you need a new sign in link, go to https://www.freecodecamp.org/signin diff --git a/server/views/emails/user-request-sign-up.ejs b/server/views/emails/user-request-sign-up.ejs index 6cbc1ca584..bf591dc501 100644 --- a/server/views/emails/user-request-sign-up.ejs +++ b/server/views/emails/user-request-sign-up.ejs @@ -1,10 +1,10 @@ -Welcome to the freeCodeCamp community! +Welcome to the freeCodeCamp community! We have created a new account for you. Here's your sign in link. It will instantly sign you into freeCodeCamp.org - no password necessary: -<%= host %>/passwordless-auth/?email=<%= loginEmail %>&token=<%= loginToken %> +<%= host %>/internal/passwordless-auth/?email=<%= loginEmail %>&token=<%= loginToken %> Note: this sign in link will expire after 15 minutes. If you need a new sign in link, go to https://www.freecodecamp.org/signin diff --git a/server/views/emails/user-request-update-email.ejs b/server/views/emails/user-request-update-email.ejs index 3cbdfd99ea..50c5e6c99e 100644 --- a/server/views/emails/user-request-update-email.ejs +++ b/server/views/emails/user-request-update-email.ejs @@ -1,6 +1,6 @@ Please confirm this address for freeCodeCamp: -<%= host %>/confirm-email?email=<%= loginEmail %>&token=<%= loginToken %>&emailChange=<%= emailChange %> +<%= host %>/internal/confirm-email?email=<%= loginEmail %>&token=<%= loginToken %>&emailChange=<%= emailChange %> Happy coding! diff --git a/src/utils/ajax.js b/src/utils/ajax.js index fc4592ee12..554b124a7c 100644 --- a/src/utils/ajax.js +++ b/src/utils/ajax.js @@ -1,15 +1,17 @@ import axios from 'axios'; +const base = '/internal'; + function get(path) { - return axios.get(`/external${path}`); + return axios.get(`${base}${path}`); } function post(path, body) { - return axios.post(`/external${path}`, body); + return axios.post(`${base}${path}`, body); } function put(path, body) { - return axios.put(`/external${path}`, body); + return axios.put(`${base}${path}`, body); } function sniff(things) { @@ -22,5 +24,9 @@ export function getSessionUser() { } export function putUserAcceptsTerms(quincyEmails) { - return put('/update-privacy-terms', {quincyEmails}) + return put('/update-privacy-terms', { quincyEmails }); +} + +export function putUserUpdateEmail(email) { + return put('/update-my-email', { email }); }