diff --git a/.babelrc b/.babelrc index 5a01d43..4fae863 100644 --- a/.babelrc +++ b/.babelrc @@ -1,3 +1,3 @@ { - presets: ["es2015", "react", "stage-0"] + presets: ["es2015", "stage-0"] } diff --git a/.eslintrc b/.eslintrc index e759f2c..3ca6479 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,10 +1,6 @@ { "parser": "babel-eslint", - "plugins": [ - "react" - ], - "env": { "browser": true, "node": true, @@ -202,49 +198,6 @@ "markers": ["=", "!"] }], - // React - "react/jsx-boolean-value": 2, - "react/jsx-closing-bracket-location": 2, - "react/jsx-curly-spacing": [2, "never"], - "react/jsx-handler-names": 2, - "react/jsx-indent-props": [2, 2], - "react/jsx-indent": [2, 2], - "react/jsx-key": 2, - "react/jsx-max-props-per-line": [2, {maximum: 3}], - "react/jsx-no-bind": [2, { - "ignoreRefs": true, - "allowBind": true, - "allowArrowFunctions": true - }], - "react/jsx-no-duplicate-props": 2, - "react/jsx-no-undef": 2, - "react/jsx-pascal-case": 2, - "react/jsx-uses-react": 2, - "react/jsx-uses-vars": 2, -// "react/no-danger": 2, - "react/no-deprecated": 2, - "react/no-did-mount-set-state": 0, - "react/no-did-update-set-state": 2, - "react/no-direct-mutation-state": 2, - "react/no-is-mounted": 2, - "react/no-multi-comp": 2, - "react/no-string-refs": 2, - "react/no-unknown-property": 2, - "react/prefer-es6-class": 2, - "react/prop-types": 2, - "react/react-in-jsx-scope": 2, - "react/self-closing-comp": 2, - "react/sort-comp": [2, { - "order": [ - "lifecycle", - "/^handle.+$/", - "/^(get|set)(?!(InitialState$|DefaultProps$|ChildContext$)).+$/", - "everything-else", - "/^render.+$/", - "render" - ] - }], - "react/wrap-multilines": 2, // Legacy "max-depth": [0, 4], diff --git a/.gitignore b/.gitignore index 007aa77..a113566 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,4 @@ jspm_packages # Optional REPL history .node_repl_history .idea +dist diff --git a/LICENSE b/LICENSE index d2f2089..ec4c51c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2016 Yury Dymov +Copyright (c) 2016 Yuri Dymov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 6214bb0..fc65b39 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,164 @@ # redux-oauth +Bearer token-based authentication library with omniauth support for redux applications -Based on [Redux-Auth](https://github.com/lynndylanhurley/redux-auth) +# Notes on migration from 1.x version +First version is based and fully compatible with [Redux-Auth](https://github.com/lynndylanhurley/redux-auth). Support is discontinued. -* Dropped e-mail support, only omniauth logic left -* Dropped multiple endpoint support -* Dropped TokenBridge logic as we are using redux initial state anyway -* Dropped all unused code -* Dropped all shortcuts found in the code -* Dropped default and material-ui themes, dropped all modals +Second version is more simple to configure and more stable. All React Components were also extracted to separate packages, therefore React is removed from dependencies. -* Implemented isomorphic fetch -* extend -> lodash.assign -* Code styling, es6 -* Extracted ButtonLoader to separate package \ No newline at end of file +# Features +* Implements Bearer token-based authentication for your application to talk to 3d party APIs +* Provides universal fetch method for any HTTP/HTTPS calls both client and server side +* Supports OAuth2 +* Supports server-side rendering to make users and search engines happy. This means that page, which require several API requests, can be fully or partly rendered on the server first + +# Configuration +### Universal / Isomorphic use-case +Configuration is required only on server-side. Client will fetch configuration and latest authentication data from redux initial state. + +1. Add authStateReducer to your reducer as 'auth' +2. Dispatch initialize method with your configuration, cookies and current location before rendering step +3. Update cookies. In case javascript fails to load / initialize user session will be still valid + +### Client-only use-case +1. Add authStateReducer to your reducer as 'auth' +2. Dispatch initialize method with your configuration and current location before rendering step + +# Usage +Dispatch fetch action with any Url and [fetch API](https://github.com/github/fetch/) options. If it is an API request, than authentication headers are applied +prior the request and token value in redux store and browser cookies is updated right after. Otherwise, regular request is performed. + +# Workflow +### Universal / Isomorphic use-case +1. Browser sends request to the web-application. Initial credentials are provided within cookies +2. Server performs validateToken API request to check the provided credentials and load user information +3. Server performs desired API requests and each times update authentication information in redux store if required +4. Server sends content to the client. Most recent authentication information is sent within native http 'setCookie' method to ensure session persistence. +5. Client loads and initialize javascript. Redux initial state is loaded from markup including redux-oauth configuration, latest authentication information and user data + +### Client-only use-case +1. Client dispatch initialize action to setup configuration and initial authentication data +2. Client performs validateToken API request to check credentials and load user data + +# Configuration Options +### cookies +Provide request cookies for initial authentication information. Optional + +### currentLocation +Provide current url. MANDATORY to process OAuth callbacks properly. + +### backend +apiUrl - MANDATORY, base url to your API + +tokenValidationPath - path to validate token, default: `/auth/validate_token` + +signOutPath - path to sign out from backend, default: `/auth/sign_out` + +authProviderPaths - configuration for OAuth2 providers, default: {} + +### cookieOptions +key - cookie key for storing authentication headers for persistence, default: 'authHeaders' + +path - cookie path, default: '/' + +expire - cookie expiration in days, default: 14 + +### tokenFormat +authentication data structure, which is going back and forth between backend and client + +Access-Token - no default value + +Token-Type - default value: 'Bearer', + +Client - no default value + +Expiry - no default value + +Uid - no default value + +Authorization - default value: '{{ Token-Type } { Access-Token }}'. This expression means that default value is computed using current 'Token-Type' and 'Access-Token' values. + +Unlike 'backend' and 'cookieOptions' objects if you would like to override tokenFormat, you have to provide whole structure. + +## Example +#### server.js with Express +``` +import { initialize, authStateReducer } from "redux-auth"; + +// ... + +const rootReducer = combineReducers({ + auth: authStateReducer, + // ... add your own reducers here +}); + +app.use((req, res) => { + const store = createStore(rootReducer, {}, applyMiddleware(thunk)); + + store.dispatch(initialize({ + backend: { + apiUrl: 'https://my-super-api.zone', + authProviderPaths: { + facebook: '/auth/facebook', + github: '/auth/github' + } + }, + currentLocation: request.url, + cookies: request.cookies + }).then(() => { + // ... do your regular things like routing and rendering + + // We need to update browser headers. User will still have valid session in case javascript fails + // 'authHeaders' is default cookieOptions.key value bere. If you redefined it, use your value instead + res.cookie('authHeaders', JSON.stringify(getHeaders(store.getState())), { maxAge: ... }); + }) +} +``` + +# Full example +### Universal / Isomorphic use-case +[Live demo](https://yury-dymov.github.io/redux-oauth-client-demo) +[Source code](https://github.com/yury-dymov/redux-oauth-demo) + +### Client-side only +[Live demo](https://yury-dymov.github.io/redux-oauth-client-demo) +[Source code](https://github.com/yury-dymov/redux-oauth-client-demo) + +### Backend +[Backend source code](https://github.com/yury-dymov/redux-oauth-backend-demo) + +# Email-password authentication and other use-cases +I wanted to make library as light-weight as possible. Also many folks have very different use-cases so it is hard to satisfy everyone. Therefore it is considered that everyone can easily implement methods they need themselves. + +#### Email-password authentication method example + +``` +import { fetch, authenticateStart, authenticateComplete, authenticateError, parseResponse } from 'redux-oauth'; + +function signIn(email, password) { + return dispatch => { + dispatch(authenticateStart()); + + return dispatch(fetch(yourCustomAuthMethod(email, password))) + .then(parseResponse) + .then(user => { + dispatch(authenticateComplete(user)); + + // it is recommended to return resolve or reject for server side rendering case. + // It helps to know then all requests are finished and rendering can be performed + return Promise.resolve(user); + } + .catch(error => { + if (error.errors) { + dispatch(authenticateError(error.errors)); + } + + return Promise.reject(error.errors || error); + }; + }; +} + +``` + +# License +MIT (c) Yuri Dymov diff --git a/dist/bundle.js b/dist/bundle.js deleted file mode 100644 index 161d23d..0000000 --- a/dist/bundle.js +++ /dev/null @@ -1 +0,0 @@ -module.exports=function(e){function t(n){if(r[n])return r[n].exports;var u=r[n]={exports:{},id:n,loaded:!1};return e[n].call(u.exports,u,u.exports,t),u.loaded=!0,u.exports}var r={};return t.m=e,t.c=r,t.p="",t(0)}([function(e,t,r){e.exports=r(22)},function(e,t,r){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function u(e){E.authState.currentSettings=e}function o(){return E.authState.currentSettings}function i(){var e=[S.SAVED_CREDS_KEY];(0,O["default"])(e).forEach(function(t){var r=e[t];E.localStorage&&E.localStorage.removeItem(r),T["default"].remove(r,{path:E.authState.currentSettings.cookiePath||"/"})})}function a(e){return e&&e.replace(/("|')/g,"")}function c(){return""+l()+E.authState.currentSettings.signOutPath}function s(){return""+l()+E.authState.currentSettings.tokenValidationPath}function f(e){var t=e.provider,r=e.params,n=""+l()+E.authState.currentSettings.authProviderPaths[t]+"?auth_origin_url="+encodeURIComponent(E.location.href);if(r)for(var u in r)n+="&"+encodeURIComponent(u)+"="+encodeURIComponent(r[u]);return n}function l(){return E.authState.currentSettings.apiUrl}function d(){return E.authState.currentSettings.tokenFormat}function p(e,t){var r=JSON.stringify(t);switch(E.authState.currentSettings.storage){case"localStorage":E.localStorage.setItem(e,r);break;default:T["default"].set(e,r,{expires:E.authState.currentSettings.cookieExpiry,path:E.authState.currentSettings.cookiePath})}}function _(e){var t=null;switch(E.authState.currentSettings.storage){case"localStorage":t=E.localStorage&&E.localStorage.getItem(e);break;default:t=T["default"].get(e)}try{return JSON.parse(t)}catch(r){return a(t)}}Object.defineProperty(t,"__esModule",{value:!0}),t.setCurrentSettings=u,t.getCurrentSettings=o,t.destroySession=i,t.getSignOutUrl=c,t.getTokenValidationPath=s,t.getOAuthUrl=f,t.getApiUrl=l,t.getTokenFormat=d,t.persistData=p,t.retrieveData=_;var h=r(34),T=n(h),g=r(12),S=r(10),v=r(11),O=n(v),E=Function("return this")()||(0,eval)("this");E.authState={currentSettings:g.defaultSettings}},function(e,t){e.exports=require("redux-immutablejs")},function(e,t){e.exports=require("immutable")},function(e,t){"use strict";function r(e){var t=e.user,r=e.headers;return{type:u,user:t,headers:r}}function n(e){var t=e.headers;return{type:o,headers:t}}Object.defineProperty(t,"__esModule",{value:!0}),t.ssAuthTokenUpdate=r,t.ssAuthTokenReplace=n;var u=t.SS_AUTH_TOKEN_UPDATE="SS_AUTH_TOKEN_UPDATE",o=t.SS_AUTH_TOKEN_REPLACE="SS_AUTH_TOKEN_REPLACE"},function(e,t,r){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function u(e,t){return(0,l["default"])({},t,{Authorization:"Bearer "+e})}function o(e){if(v(e)){var t=function(){var e={};e=(0,h.getCurrentSettings)().isServer?(0,h.getCurrentSettings)().headers:(0,h.retrieveData)(_.SAVED_CREDS_KEY)||e;var t={};return t["If-Modified-Since"]="Mon, 26 Jul 1997 05:00:00 GMT","undefined"==typeof e?{v:t}:((0,p["default"])((0,h.getTokenFormat)()).forEach(function(r){var n=e[r];"undefined"!=typeof n&&(t[r]=e[r])}),(0,T.areHeadersBlank)(e)?void 0:{v:u((0,g.getAccessToken)(e),t)})}();if("object"===("undefined"==typeof t?"undefined":a(t)))return t.v}return{}}function i(e){if(v(e.url)){var t=e.headers;if(!(0,T.areHeadersBlank)(t)){var r=(0,T.parseHeaders)(t);(0,h.getCurrentSettings)().isServer?((0,h.getCurrentSettings)().headers=r,(0,h.getCurrentSettings)().dispatch((0,S.ssAuthTokenReplace)({headers:r}))):(0,h.persistData)(_.SAVED_CREDS_KEY,r)}}return e}Object.defineProperty(t,"__esModule",{value:!0});var a="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol?"symbol":typeof e};t.addAuthorizationHeader=u,t["default"]=function(e){var t=arguments.length<=1||void 0===arguments[1]?{}:arguments[1];return t.headers||(t.headers={}),(0,l["default"])(t.headers,o(e)),(0,s["default"])(e,t).then(function(e){return i(e)})};var c=r(18),s=n(c),f=r(6),l=n(f),d=r(11),p=n(d),_=r(10),h=r(1),T=r(13),g=r(12),S=r(4),v=function(e){return e.match((0,h.getApiUrl)())}},function(e,t){e.exports=require("lodash/assign")},function(e,t){"use strict";function r(){return{type:o}}function n(e){return{type:i,user:e}}function u(e){return{type:a,errors:e}}Object.defineProperty(t,"__esModule",{value:!0}),t.authenticateStart=r,t.authenticateComplete=n,t.authenticateError=u;var o=t.AUTHENTICATE_START="AUTHENTICATE_START",i=t.AUTHENTICATE_COMPLETE="AUTHENTICATE_COMPLETE",a=t.AUTHENTICATE_ERROR="AUTHENTICATE_ERROR"},function(e,t,r){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function u(e,t,r,n){if(!r)return new Promise(function(r,n){u(e,t,r,n)});var o=null;try{o=(0,l.getAllParams)(e.location)}catch(i){}o&&o.uid?(e.close(),(0,d.persistData)(f.SAVED_CREDS_KEY,o),(0,h["default"])((0,d.getTokenValidationPath)()).then(p.parseResponse).then(function(e){var t=e.data;return r(t)})["catch"](function(e){var t=e.errors;return n({errors:t})})):e.closed?n({errors:"Authentication was cancelled."}):setTimeout(function(){return u(e,t,r,n)},0)}function o(e){var t=e.provider,r=e.url,n=e.tab,o=void 0!==n&&n,i=o?"_blank":t,a=(0,g["default"])(t,r,i);return u(a,t)}function i(e){return{type:S,provider:e}}function a(e){return{type:v,user:e}}function c(e,t){return{type:O,provider:e,errors:t}}function s(e){var t=e.provider,r=e.params;return function(e){e(i(t));var n=(0,d.getOAuthUrl)({provider:t,params:r});return o({provider:t,url:n}).then(function(t){return e(a(t))})["catch"](function(r){var n=r.errors;return e(c(t,n))})}}Object.defineProperty(t,"__esModule",{value:!0}),t.OAUTH_SIGN_IN_ERROR=t.OAUTH_SIGN_IN_COMPLETE=t.OAUTH_SIGN_IN_START=void 0,t.oAuthSignInStart=i,t.oAuthSignInComplete=a,t.oAuthSignInError=c,t.oAuthSignIn=s;var f=r(10),l=r(14),d=r(1),p=r(16),_=r(5),h=n(_),T=r(29),g=n(T),S=t.OAUTH_SIGN_IN_START="OAUTH_SIGN_IN_START",v=t.OAUTH_SIGN_IN_COMPLETE="OAUTH_SIGN_IN_COMPLETE",O=t.OAUTH_SIGN_IN_ERROR="OAUTH_SIGN_IN_ERROR"},function(e,t,r){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function u(){return{type:d}}function o(e){return{type:p,user:e}}function i(e){return{type:_,errors:e}}function a(){return function(e){return e(u()),(0,l["default"])((0,c.getSignOutUrl)(),{method:"delete"}).then(s.parseResponse).then(function(t){return e(o(t)),(0,c.destroySession)()})["catch"](function(t){var r=t.errors;return e(i(r)),(0,c.destroySession)()})}}Object.defineProperty(t,"__esModule",{value:!0}),t.SIGN_OUT_ERROR=t.SIGN_OUT_COMPLETE=t.SIGN_OUT_START=void 0,t.signOutStart=u,t.signOutComplete=o,t.signOutError=i,t.signOut=a;var c=r(1),s=r(16),f=r(5),l=n(f),d=t.SIGN_OUT_START="SIGN_OUT_START",p=t.SIGN_OUT_COMPLETE="SIGN_OUT_COMPLETE",_=t.SIGN_OUT_ERROR="SIGN_OUT_ERROR"},function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r="authHeaders";t.SAVED_CREDS_KEY=r},function(e,t){e.exports=require("lodash/keys")},function(e,t,r){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function u(e){var t=(0,s["default"])({},p,e);return(0,d.setCurrentSettings)(t),t}function o(e){var t=e.settings,r=void 0===t?{}:t;u(r);var n=(0,d.retrieveData)(a.SAVED_CREDS_KEY);if((0,d.getCurrentSettings)().initialCredentials){var o=(0,d.getCurrentSettings)().initialCredentials,i=o.user,c=o.headers;return(0,d.persistData)(a.SAVED_CREDS_KEY,c),Promise.resolve(i)}return n?(0,l["default"])((0,d.getApiUrl)()):Promise.reject({reason:"No credentials."})}function i(e){return e.get&&"function"==typeof e.get?e.get("access-token"):e["access-token"]}Object.defineProperty(t,"__esModule",{value:!0}),t.defaultSettings=void 0,t.initSettings=u,t.applyConfig=o,t.getAccessToken=i;var a=r(10),c=r(6),s=n(c),f=r(5),l=n(f),d=r(1),p=t.defaultSettings={storage:"cookies",cookieExpiry:14,cookiePath:"/",initialCredentials:null,reduxInitialState:"__INITIAL_STATE__",tokenFormat:{"access-token":"{{ access-token }}","token-type":"Bearer",client:"{{ client }}",expiry:"{{ expiry }}",uid:"{{ uid }}"}}},function(e,t,r){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function u(e){if(e){var t={},r=(0,i.getTokenFormat)(),n=!0,u="Headers"===e.constructor.name;return(0,c["default"])(r).forEach(function(r){t[r]=u?e.get(r):e[r],t[r]&&((0,f["default"])(t[r])&&(t[r]=t[r][0]),n=!1)}),n?void 0:t}}function o(e){if(!e)return!0;for(var t=(0,i.getTokenFormat)(),r=(0,c["default"])(t),n="Headers"===e.constructor.name,u=0;u1?r[1]:null},_=function(e){var t=e.search||"",r=t.replace("?","");return r?a["default"].parse(r):{}},h=function(e){var t=p(e);return t?a["default"].parse(t):{}},T=function(e,t){for(var r in t)delete e[t[r]];return e},g=function(e,t){var r=u(e),n={};return(0,l["default"])(t).forEach(function(e){return n[e]=r[e]}),n},S=function(e,t){var r=a["default"].stringify(T(_(e),t)),n=a["default"].stringify(T(h(e),t)),u=(e.hash||"").split("?")[0];return r&&(r="?"+r),n&&(u+="?"+n),u&&!u.match(/^#/)&&(u="#/"+u),e.pathname+r+u}},function(e,t,r){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function u(){var e=arguments.length<=0||void 0===arguments[0]?{}:arguments[0];return function(t){if(e.currentLocation&&e.currentLocation.match(/blank=true/))return Promise.resolve({blank:!0});t((0,a.authenticateStart)());var r=void 0;if(e.isServer)(0,s.initSettings)((0,i["default"])({},e,{dispatch:t})),r=(0,d["default"])(e).then(function(e){var r=e.user,n=e.headers;return t((0,c.ssAuthTokenUpdate)({headers:n,user:r})),r})["catch"](function(e){var r=e.reason;return t((0,c.ssAuthTokenUpdate)()),Promise.reject({reason:r})});else{e=(0,s.initSettings)(e);var n=window[e.reduxInitialState],u=n.auth.server.headers,o=n.auth.user.attributes;o&&u&&(t((0,a.authenticateComplete)(o)),e.initialCredentials={user:o,headers:u},t((0,c.ssAuthTokenUpdate)({user:o,headers:u})));var l=(0,_["default"])(window.location),p=l.authRedirectPath,g=l.authRedirectHeaders;p&&t((0,T.push)({pathname:p})),(0,h.areHeadersBlank)(g)||(e.initialCredentials=(0,i["default"])({},e.initialCredentials,g));var S="undefined"==typeof e.serverSideRendering||e.serverSideRendering;(S&&!e.initialCredentials||e.cleanSession)&&(0,f.destroySession)(),r=Promise.resolve((0,s.applyConfig)({settings:e}))}return r.then(function(e){return t((0,a.authenticateComplete)(e)),e})["catch"](function(){var e=arguments.length<=0||void 0===arguments[0]?{}:arguments[0],r=e.reason;return t((0,a.authenticateError)([r])),Promise.resolve({reason:r})})}}Object.defineProperty(t,"__esModule",{value:!0}),t.configure=u;var o=r(6),i=n(o),a=r(7),c=r(4),s=r(12),f=r(1),l=r(17),d=n(l),p=r(14),_=n(p),h=r(13),T=r(37)},function(e,t){"use strict";function r(e){var t=e.json();return e.status>=200&&e.status<300?t:t.then(function(e){return Promise.reject(e)})}Object.defineProperty(t,"__esModule",{value:!0}),t.parseResponse=r},function(e,t,r){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function u(e){var t=e.cookies,r=e.currentLocation,n=(0,h["default"])(l["default"].parse(r)),u=n.authRedirectHeaders;return new Promise(function(e,r){if(!t&&!u)return r({reason:"No creds"});var n=function(){var n=s["default"].parse(t||"{}"),o=JSON.parse(n.authHeaders||"false"),i=void 0;if((0,g.areHeadersBlank)(u)?n&&o&&(i=o):i=(0,g.parseHeaders)(u),!i)return{v:r({reason:"No creds"})};var c=(0,S.getCurrentSettings)();c.isServer&&(0,S.setCurrentSettings)((0,p["default"])({},c,{headers:i}));var f=(0,S.getTokenValidationPath)()+"?unbatch=true",l=void 0;return{v:(0,a["default"])(f,{headers:(0,T.addAuthorizationHeader)(i["access-token"],i)}).then(function(e){return l=(0,g.parseHeaders)(e.headers),c.isServer&&(0,S.setCurrentSettings)((0,p["default"])({},c,{headers:l})),e.json()}).then(function(t){return t.success?e({headers:l,user:t.data}):r({reason:t.errors})})["catch"](function(e){return r({reason:e})})}}();return"object"===("undefined"==typeof n?"undefined":o(n))?n.v:void 0})}Object.defineProperty(t,"__esModule",{value:!0});var o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol?"symbol":typeof e};t.fetchToken=u;var i=r(18),a=n(i),c=r(33),s=n(c),f=r(38),l=n(f),d=r(6),p=n(d),_=r(14),h=n(_),T=r(5),g=r(13),S=r(1),v=function(e){var t=e.isServer,r=e.cookies,n=e.currentLocation;return new Promise(function(e,o){if(t)return u({cookies:r,currentLocation:n}).then(function(t){return e(t)})["catch"](function(e){return o(e)})})};t["default"]=v},function(e,t){e.exports=require("isomorphic-fetch")},function(e,t){e.exports=require("react")},function(e,t){e.exports=require("react-bootstrap-button-loader")},function(e,t){e.exports=require("react-redux")},function(e,t,r){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}Object.defineProperty(t,"__esModule",{value:!0}),t.SIGN_OUT_ERROR=t.SIGN_OUT_COMPLETE=t.SIGN_OUT_START=t.SS_AUTH_TOKEN_REPLACE=t.SS_AUTH_TOKEN_UPDATE=t.OAUTH_SIGN_IN_ERROR=t.OAUTH_SIGN_IN_COMPLETE=t.OAUTH_SIGN_IN_START=t.AUTHENTICATE_ERROR=t.AUTHENTICATE_COMPLETE=t.AUTHENTICATE_START=t.fetch=t.authStateReducer=t.verifyAuth=t.getApiUrl=t.oAuthSignIn=t.signOut=t.configure=t.OAuthSignInButton=t.SignOutButton=void 0;var u=r(32);Object.defineProperty(t,"SignOutButton",{enumerable:!0,get:function(){return u.SignOutButton}}),Object.defineProperty(t,"OAuthSignInButton",{enumerable:!0,get:function(){return u.OAuthSignInButton}});var o=r(15);Object.defineProperty(t,"configure",{enumerable:!0,get:function(){return o.configure}});var i=r(9);Object.defineProperty(t,"signOut",{enumerable:!0,get:function(){return i.signOut}});var a=r(8);Object.defineProperty(t,"oAuthSignIn",{enumerable:!0,get:function(){return a.oAuthSignIn}});var c=r(1);Object.defineProperty(t,"getApiUrl",{enumerable:!0,get:function(){return c.getApiUrl}});var s=r(5);Object.defineProperty(t,"fetch",{enumerable:!0,get:function(){return n(s)["default"]}});var f=r(7);Object.defineProperty(t,"AUTHENTICATE_START",{enumerable:!0,get:function(){return f.AUTHENTICATE_START}}),Object.defineProperty(t,"AUTHENTICATE_COMPLETE",{enumerable:!0,get:function(){return f.AUTHENTICATE_COMPLETE}}),Object.defineProperty(t,"AUTHENTICATE_ERROR",{enumerable:!0,get:function(){return f.AUTHENTICATE_ERROR}}),Object.defineProperty(t,"OAUTH_SIGN_IN_START",{enumerable:!0,get:function(){return a.OAUTH_SIGN_IN_START}}),Object.defineProperty(t,"OAUTH_SIGN_IN_COMPLETE",{enumerable:!0,get:function(){return a.OAUTH_SIGN_IN_COMPLETE}}),Object.defineProperty(t,"OAUTH_SIGN_IN_ERROR",{enumerable:!0,get:function(){return a.OAUTH_SIGN_IN_ERROR}});var l=r(4);Object.defineProperty(t,"SS_AUTH_TOKEN_UPDATE",{enumerable:!0,get:function(){return l.SS_AUTH_TOKEN_UPDATE}}),Object.defineProperty(t,"SS_AUTH_TOKEN_REPLACE",{enumerable:!0,get:function(){return l.SS_AUTH_TOKEN_REPLACE}}),Object.defineProperty(t,"SIGN_OUT_START",{enumerable:!0,get:function(){return i.SIGN_OUT_START}}),Object.defineProperty(t,"SIGN_OUT_COMPLETE",{enumerable:!0,get:function(){return i.SIGN_OUT_COMPLETE}}),Object.defineProperty(t,"SIGN_OUT_ERROR",{enumerable:!0,get:function(){return i.SIGN_OUT_ERROR}});var d=r(23),p=n(d),_=r(24),h=n(_),T=r(28),g=n(T),S=r(25),v=n(S),O=r(26),E=n(O),b=r(27),y=n(b),A=r(2),P=r(17),I=n(P),m=(0,A.combineReducers)({configure:h["default"],signOut:y["default"],authentication:p["default"],oAuthSignIn:v["default"],server:E["default"],user:g["default"]});t.verifyAuth=I["default"],t.authStateReducer=m},function(e,t,r){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r]);return t["default"]=e,t}function u(e){return e&&e.__esModule?e:{"default":e}}function o(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}Object.defineProperty(t,"__esModule",{value:!0});var i,a=r(3),c=u(a),s=r(2),f=r(7),l=n(f),d=c["default"].fromJS({loading:!1,valid:!1,errors:null});t["default"]=(0,s.createReducer)(d,(i={},o(i,l.AUTHENTICATE_START,function(e){return e.set("loading",!0)}),o(i,l.AUTHENTICATE_COMPLETE,function(e){return e.merge({loading:!1,errors:null,valid:!0})}),o(i,l.AUTHENTICATE_ERROR,function(e){return e.merge({loading:!1,errors:"Invalid token",valid:!1})}),i))},function(e,t,r){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r]);return t["default"]=e,t}function u(e){return e&&e.__esModule?e:{"default":e}}function o(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}Object.defineProperty(t,"__esModule",{value:!0});var i=r(3),a=u(i),c=r(2),s=r(15),f=n(s),l=a["default"].fromJS({loading:!0,errors:null});t["default"]=(0,c.createReducer)(l,o({},f.CONFIGURE_START,function(e){return e.set("loading",!0)}))},function(e,t,r){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r]);return t["default"]=e,t}function u(e){return e&&e.__esModule?e:{"default":e}}function o(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}Object.defineProperty(t,"__esModule",{value:!0});var i,a=r(3),c=u(a),s=r(2),f=r(8),l=n(f),d=c["default"].fromJS({loading:!1,errors:null});t["default"]=(0,s.createReducer)(d,(i={},o(i,l.OAUTH_SIGN_IN_START,function(e,t){var r=t.provider;return e.setIn([r,"loading"],!0)}),o(i,l.OAUTH_SIGN_IN_COMPLETE,function(e,t){var r=t.provider;return e.mergeDeep(o({},r,{loading:!1,errors:null}))}),o(i,l.OAUTH_SIGN_IN_ERROR,function(e,t){var r=t.provider,n=t.errors;return e.mergeDeep(o({},r,{loading:!1,errors:n}))}),i))},function(e,t,r){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function u(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}Object.defineProperty(t,"__esModule",{value:!0});var o,i=r(3),a=n(i),c=r(2),s=r(4),f=a["default"].fromJS({headers:null});t["default"]=(0,c.createReducer)(f,(o={},u(o,s.SS_AUTH_TOKEN_UPDATE,function(e,t){var r=t.headers;return e.merge({headers:r})}),u(o,s.SS_AUTH_TOKEN_REPLACE,function(e,t){var r=t.headers;return e.merge({headers:r})}),o))},function(e,t,r){"use strict";function n(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&(t[r]=e[r]);return t["default"]=e,t}function u(e){return e&&e.__esModule?e:{"default":e}}function o(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}Object.defineProperty(t,"__esModule",{value:!0});var i,a=r(3),c=u(a),s=r(2),f=r(9),l=n(f),d=c["default"].fromJS({loading:!1,errors:null});t["default"]=(0,s.createReducer)(d,(i={},o(i,l.SIGN_OUT_START,function(e){return e.setIn(["loading"],!0)}),o(i,l.SIGN_OUT_COMPLETE,function(e){return e.mergeDeep({loading:!1,errors:null})}),o(i,l.SIGN_OUT_ERROR,function(e,t){var r=t.errors;return e.mergeDeep({loading:!1,errors:r})}),i))},function(e,t,r){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function u(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}Object.defineProperty(t,"__esModule",{value:!0});var o,i=r(3),a=n(i),c=r(2),s=r(9),f=r(8),l=r(7),d=r(4),p=a["default"].fromJS({attributes:null,isSignedIn:!1});t["default"]=(0,c.createReducer)(p,(o={},u(o,l.AUTHENTICATE_COMPLETE,function(e,t){var r=t.user;return e.merge({attributes:r,isSignedIn:!0})}),u(o,f.OAUTH_SIGN_IN_COMPLETE,function(e,t){var r=t.user;return e.merge({attributes:r,isSignedIn:!0})}),u(o,d.SS_AUTH_TOKEN_UPDATE,function(e,t){var r=t.user;return e.merge({isSignedIn:!!r,attributes:r})}),u(o,l.AUTHENTICATE_FAILURE,function(e){return e.merge(p)}),u(o,s.SIGN_OUT_COMPLETE,function(e){return e.merge(p)}),u(o,s.SIGN_OUT_ERROR,function(e){return e.merge(p)}),o))},function(e,t){"use strict";function r(e){var t=e.width,r=e.height,n=window.screenLeft?window.screenLeft:window.screenX,u=window.screenTop?window.screenTop:window.screenY,o=n+window.innerWidth/2-t/2,i=u+window.innerHeight/2-r/2;return{top:i,left:o}}function n(e){switch(e){case"facebook":return{width:580,height:400};case"google":return{width:452,height:633};case"github":return{width:1020,height:618};case"linkedin":return{width:527,height:582};case"twitter":return{width:495,height:645};case"live":return{width:500,height:560};case"yahoo":return{width:559,height:519};default:return{width:1020,height:618}}}function u(e){var t=n(e),u=t.width,o=t.height,i=r({width:u,height:o}),a=i.top,c=i.left;return"width="+u+",height="+o+",top="+a+",left="+c}Object.defineProperty(t,"__esModule",{value:!0});var o="scrollbars=no,toolbar=no,location=no,titlebar=no,directories=no,status=no,menubar=no",i=function(e,t,r){return window.open(t,r,o+","+u(e))};t["default"]=i},function(e,t,r){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function u(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function i(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{value:!0});var a=Object.assign||function(e){for(var t=1;t { - if (settings.currentLocation && settings.currentLocation.match(/blank=true/)) { - return Promise.resolve({ blank: true }); - } - - dispatch(authenticateStart()); - - let promise; - - if (settings.isServer) { - initSettings(assign({}, settings, { dispatch })); - - promise = verifyAuth(settings) - .then(({ user, headers }) => { - dispatch(ssAuthTokenUpdate({ headers, user })); - - return user; - }) - .catch(({ reason }) => { - dispatch(ssAuthTokenUpdate()); - - return Promise.reject({ reason }); - }); - } else { - settings = initSettings(settings); - - const initialState = window[settings.reduxInitialState]; - const { headers } = initialState.auth.server; - const user = initialState.auth.user.attributes; - - if (user && headers) { - dispatch(authenticateComplete(user)); - - settings.initialCredentials = { user, headers }; - - dispatch(ssAuthTokenUpdate({ user, headers })); - } - - const { authRedirectPath, authRedirectHeaders } = getRedirectInfo(window.location); - - if (authRedirectPath) { - dispatch(push({ pathname: authRedirectPath })); - } - - if (!areHeadersBlank(authRedirectHeaders)) { - settings.initialCredentials = assign({}, settings.initialCredentials, authRedirectHeaders); - } - - const serverSideRendering = typeof settings.serverSideRendering === 'undefined' || settings.serverSideRendering; - - if (serverSideRendering && !settings.initialCredentials || settings.cleanSession) { - destroySession(); - } - - promise = Promise.resolve(applyConfig({ settings })); - } - - return promise - .then(user => { - dispatch(authenticateComplete(user)); - return user; - }) - .catch(({ reason } = {}) => { - dispatch(authenticateError([reason])); - - return Promise.resolve({ reason }); - }); - }; -} diff --git a/src/actions/headers.js b/src/actions/headers.js new file mode 100644 index 0000000..7084a30 --- /dev/null +++ b/src/actions/headers.js @@ -0,0 +1,17 @@ +import Cookies from 'js-cookie'; +import { getSettings } from 'models/settings'; + +export const UPDATE_HEADERS = 'UPDATE_HEADERS'; + +export function updateHeaders(headers = {}) { + return (dispatch, getState) => { + const { cookieOptions } = getSettings(getState()); + + Cookies.set(cookieOptions.key, headers, { + expires: cookieOptions.expires, + path: cookieOptions.path + }); + + return dispatch({ type: UPDATE_HEADERS, headers }); + }; +} diff --git a/src/actions/initialize.js b/src/actions/initialize.js new file mode 100644 index 0000000..8e892ff --- /dev/null +++ b/src/actions/initialize.js @@ -0,0 +1,60 @@ +import verifyAuth from './verifyAuth'; +import { authenticateComplete, authenticateError } from './authenticate'; +import { updateHeaders } from './headers'; +import backend from 'defaults/backend'; +import tokenFormat from 'defaults/tokenFormat'; +import cookieOptions from 'defaults/cookieOptions'; + +import assign from 'lodash/assign'; + +export const AUTH_INIT_SETTINGS = 'AUTH_INIT_SETTINGS'; + +function initSettings(config) { + return { type: AUTH_INIT_SETTINGS, config }; +} + +function mergeSettings(settings) { + return { + backend: assign({}, backend, settings.backend), + tokenFormat: settings.tokenFormat || tokenFormat, + cookieOptions: assign({}, cookieOptions, settings.cookieOptions), + cookies: settings.cookies + }; +} + +export function initialize(settings = {}) { + return (dispatch, getState) => { + if (getState().auth.getIn(['user', 'isSignedIn'])) { + return Promise.resolve(); + } + + const mergedSettings = mergeSettings(settings); + + dispatch(initSettings(mergedSettings)); + + if (mergedSettings.cookies && mergedSettings.cookies[mergedSettings.cookieOptions.key]) { + try { + dispatch(updateHeaders(JSON.parse(mergedSettings.cookies[mergedSettings.cookieOptions.key]))); + } catch (ex) { + dispatch(updateHeaders()); + } + } + + if (settings.currentLocation && settings.currentLocation.match(/blank=true/)) { + return Promise.resolve({ blank: true }); + } + + return dispatch(verifyAuth(settings.currentLocation)) + .then(({ user }) => { + dispatch(authenticateComplete(user)); + + return Promise.resolve(); + }) + .catch(({ errors }) => { + dispatch(updateHeaders()); + dispatch(authenticateError(errors)); + + return Promise.resolve(); + }); + }; +} diff --git a/src/actions/oauth-sign-in.js b/src/actions/oauth-sign-in.js deleted file mode 100644 index 50c31e0..0000000 --- a/src/actions/oauth-sign-in.js +++ /dev/null @@ -1,68 +0,0 @@ -import { SAVED_CREDS_KEY } from 'utils/constants'; -import { getAllParams } from 'utils/parse-url'; -import { getOAuthUrl } from 'utils/session-storage'; -import { getTokenValidationPath, persistData } from 'utils/session-storage'; -import { parseResponse } from 'utils/handle-fetch-response'; -import fetch from 'utils/fetch'; -import openPopup from 'utils/popup'; - -export const OAUTH_SIGN_IN_START = 'OAUTH_SIGN_IN_START'; -export const OAUTH_SIGN_IN_COMPLETE = 'OAUTH_SIGN_IN_COMPLETE'; -export const OAUTH_SIGN_IN_ERROR = 'OAUTH_SIGN_IN_ERROR'; - -function listenForCredentials(popup, provider, resolve, reject) { - if (!resolve) { - return new Promise((resolve, reject) => { - listenForCredentials(popup, provider, resolve, reject); - }); - } - - let creds = null; - - try { - creds = getAllParams(popup.location); - } catch (err) {} - - if (creds && creds.uid) { - popup.close(); - persistData(SAVED_CREDS_KEY, creds); - fetch(getTokenValidationPath()) - .then(parseResponse) - .then(({data}) => resolve(data)) - .catch(({errors}) => reject({errors})); - } else if (popup.closed) { - reject({ errors: 'Authentication was cancelled.' }) - } else { - setTimeout(() => listenForCredentials(popup, provider, resolve, reject), 0); - } -} - - -function authenticate({ provider, url, tab = false}) { - const name = (tab) ? '_blank' : provider; - const popup = openPopup(provider, url, name); - - return listenForCredentials(popup, provider); -} - - -export function oAuthSignInStart(provider) { - return { type: OAUTH_SIGN_IN_START, provider }; -} -export function oAuthSignInComplete(user) { - return { type: OAUTH_SIGN_IN_COMPLETE, user }; -} -export function oAuthSignInError(provider, errors) { - return { type: OAUTH_SIGN_IN_ERROR, provider, errors }; -} -export function oAuthSignIn({ provider, params }) { - return dispatch => { - dispatch(oAuthSignInStart(provider)); - - const url = getOAuthUrl( {provider, params }); - - return authenticate({ provider, url}) - .then(user => dispatch(oAuthSignInComplete(user))) - .catch(({ errors }) => dispatch(oAuthSignInError(provider, errors))); - }; -} diff --git a/src/actions/oauthSignIn.js b/src/actions/oauthSignIn.js new file mode 100644 index 0000000..e413cc2 --- /dev/null +++ b/src/actions/oauthSignIn.js @@ -0,0 +1,96 @@ +import { getSettings } from 'models/settings'; +import parseResponse from 'utils/parseResponse'; +import { parseHeaders, areHeadersBlank } from 'utils/headers'; +import { updateHeaders } from './headers'; +import fetch from 'utils/fetch'; +import openPopup from 'utils/popup'; +import { getAllParams } from 'utils/getRedirectInfo'; + +import keys from 'lodash/keys'; + +export const OAUTH_SIGN_IN_START = 'OAUTH_SIGN_IN_START'; +export const OAUTH_SIGN_IN_COMPLETE = 'OAUTH_SIGN_IN_COMPLETE'; +export const OAUTH_SIGN_IN_ERROR = 'OAUTH_SIGN_IN_ERROR'; + +export function oAuthSignInStart(provider) { + return { type: OAUTH_SIGN_IN_START, provider }; +} + +export function oAuthSignInComplete(user) { + return { type: OAUTH_SIGN_IN_COMPLETE, user }; +} + +export function oAuthSignInError(provider, errors) { + return { type: OAUTH_SIGN_IN_ERROR, provider, errors }; +} + +export function oAuthSignIn({ provider, params }) { + return (dispatch, getState) => { + dispatch(oAuthSignInStart(provider)); + + const state = getState(); + const url = getOAuthUrl({ provider, params, state }); + + return dispatch(authenticate({ provider, url, state })) + .then(user => dispatch(oAuthSignInComplete(user))) + .catch(({ errors }) => dispatch(oAuthSignInError(provider, errors))); + }; +} + +function getOAuthUrl({ provider, params, state }) { + const { authProviderPaths } = getSettings(state).backend; + const providerPath = authProviderPaths[provider]; + + if (!providerPath) { + throw `authProviderPath is not set for ${provider}`; + } + + let oAuthUrl = `${providerPath}?auth_origin_url=${encodeURIComponent(window.location.href)}`; + + if (params) { + keys(params).forEach(key => oAuthUrl += `&${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`); + } + + return oAuthUrl; +} + +function authenticate({ provider, url, state, tab = false }) { + const name = (tab) ? '_blank' : provider; + const popup = openPopup(provider, url, name); + + return (dispatch, getState) => { + const { tokenFormat } = getSettings(getState()); + + return new Promise((resolve, reject) => + dispatch(listenForCredentials({ popup, provider, state, resolve, reject, tokenFormat }))); + }; +} + +function listenForCredentials({ popup, provider, state, resolve, reject, tokenFormat }) { + return dispatch => { + let creds = null; + + try { + creds = getAllParams(popup.location); + } catch (err) { + console.log(err); + } + + if (!areHeadersBlank(creds, tokenFormat)) { + const { tokenValidationPath } = getSettings(state).backend; + + popup.close(); + + dispatch(updateHeaders(parseHeaders(creds, tokenFormat))); + + return dispatch(fetch(tokenValidationPath)) + .then(parseResponse) + .then(({ data }) => resolve(data)) + .catch(({ errors }) => reject({ errors })); + } else if (popup.closed) { + return reject({ errors: 'Authentication was cancelled.' }); + } + + return setTimeout(() => dispatch(listenForCredentials({ popup, provider, state, resolve, reject, tokenFormat })), 20); + }; +} diff --git a/src/actions/server.js b/src/actions/server.js deleted file mode 100644 index f71ffa8..0000000 --- a/src/actions/server.js +++ /dev/null @@ -1,10 +0,0 @@ -export const SS_AUTH_TOKEN_UPDATE = 'SS_AUTH_TOKEN_UPDATE'; -export const SS_AUTH_TOKEN_REPLACE = 'SS_AUTH_TOKEN_REPLACE'; - -export function ssAuthTokenUpdate({user, headers }) { - return { type: SS_AUTH_TOKEN_UPDATE, user, headers }; -} - -export function ssAuthTokenReplace({ headers }) { - return { type: SS_AUTH_TOKEN_REPLACE, headers }; -} diff --git a/src/actions/sign-out.js b/src/actions/sign-out.js deleted file mode 100644 index 11514b7..0000000 --- a/src/actions/sign-out.js +++ /dev/null @@ -1,33 +0,0 @@ -import { getSignOutUrl, destroySession } from 'utils/session-storage'; -import { parseResponse } from 'utils/handle-fetch-response'; -import fetch from 'utils/fetch'; - -export const SIGN_OUT_START = 'SIGN_OUT_START'; -export const SIGN_OUT_COMPLETE = 'SIGN_OUT_COMPLETE'; -export const SIGN_OUT_ERROR = 'SIGN_OUT_ERROR'; - -export function signOutStart() { - return { type: SIGN_OUT_START }; -} -export function signOutComplete(user) { - return { type: SIGN_OUT_COMPLETE, user }; -} -export function signOutError(errors) { - return { type: SIGN_OUT_ERROR, errors }; -} -export function signOut() { - return dispatch => { - dispatch(signOutStart()); - - return fetch(getSignOutUrl(), { method: 'delete' }) - .then(parseResponse) - .then((user) => { - dispatch(signOutComplete(user)); - return destroySession(); - }) - .catch(({errors}) => { - dispatch(signOutError(errors)); - return destroySession(); - }); - }; -} diff --git a/src/actions/signOut.js b/src/actions/signOut.js new file mode 100644 index 0000000..062a1b4 --- /dev/null +++ b/src/actions/signOut.js @@ -0,0 +1,43 @@ +import fetch from 'utils/fetch'; + +import { getSettings } from 'models/settings'; +import parseResponse from 'utils/parseResponse'; +import { updateHeaders } from 'actions/headers'; + +export const SIGN_OUT = 'SIGN_OUT'; +export const SIGN_OUT_COMPLETE = 'SIGN_OUT_COMPLETE'; +export const SIGN_OUT_ERROR = 'SIGN_OUT_ERROR'; + +function signOutStart() { + return { type: SIGN_OUT }; +} + +function signOutComplete() { + return { type: SIGN_OUT_COMPLETE }; +} + +function signOutError(errors) { + return { type: SIGN_OUT_ERROR, errors }; +} + +export function signOut() { + return (dispatch, getState) => { + const { backend } = getSettings(getState()); + + dispatch(signOutStart()); + dispatch(updateHeaders({})); + + return dispatch(fetch(backend.signOutPath, { method: 'delete' })) + .then(parseResponse) + .then(() => { + dispatch(signOutComplete()); + + return Promise.resolve(); + }) + .catch((er) => { + dispatch(signOutError(er)); + + return Promise.reject(er); + }); + }; +} diff --git a/src/actions/verifyAuth.js b/src/actions/verifyAuth.js new file mode 100644 index 0000000..140996d --- /dev/null +++ b/src/actions/verifyAuth.js @@ -0,0 +1,41 @@ +import Url from 'url'; + +import { parseHeaders, areHeadersBlank, getHeaders } from 'utils/headers'; +import { getSettings } from 'models/settings'; +import fetch from 'utils/fetch'; +import getRedirectInfo from 'utils/getRedirectInfo'; +import { authenticateStart } from 'actions/authenticate'; + +import keys from 'lodash/keys'; + +export default function (currentLocation) { + return (dispatch, getState) => { + const state = getState(); + let headers = getHeaders(state); + + const { backend, tokenFormat } = getSettings(state); + const { authRedirectHeaders } = getRedirectInfo(Url.parse(currentLocation), tokenFormat); + + if (!areHeadersBlank(authRedirectHeaders, tokenFormat)) { + headers = parseHeaders(authRedirectHeaders, tokenFormat); + } + + if (keys(headers).length === 0) { + return Promise.reject({ reason: 'No creds' }); + } + + const url = `${backend.tokenValidationPath}?unbatch=true`; + + dispatch(authenticateStart()); + + return dispatch(fetch(url)) + .then(resp => resp.json()) + .then(json => { + if (json.success) { + return Promise.resolve({ user: json.data }); + } + + return Promise.reject({ errors: json.errors }); + }); + }; +} diff --git a/src/defaults/backend.js b/src/defaults/backend.js new file mode 100644 index 0000000..9e8d9fd --- /dev/null +++ b/src/defaults/backend.js @@ -0,0 +1,5 @@ +export default { + tokenValidationPath: '/auth/validate_token', + signOutPath: '/auth/sign_out', + authProviderPaths: {} +}; diff --git a/src/defaults/cookieOptions.js b/src/defaults/cookieOptions.js new file mode 100644 index 0000000..e1c3586 --- /dev/null +++ b/src/defaults/cookieOptions.js @@ -0,0 +1,5 @@ +export default { + key: 'authHeaders', + path: '/', + expire: 14 +}; diff --git a/src/defaults/tokenFormat.js b/src/defaults/tokenFormat.js new file mode 100644 index 0000000..6a52f8b --- /dev/null +++ b/src/defaults/tokenFormat.js @@ -0,0 +1,8 @@ +export default { + 'Access-Token': null, + 'Token-Type': 'Bearer', + Client: null, + Expiry: null, + Uid: null, + Authorization: '{{ Token-Type } { Access-Token }}' +}; diff --git a/src/index.js b/src/index.js index 540085d..4bde6da 100644 --- a/src/index.js +++ b/src/index.js @@ -1,51 +1,15 @@ -import authentication from './reducers/authenticate'; -import configure from './reducers/configure'; -import user from './reducers/user'; -import oAuthSignIn from './reducers/oauth-sign-in'; -import server from './reducers/server'; -import signOut from './reducers/sign-out'; -import { combineReducers } from 'redux-immutablejs'; -import verifyAuth from './utils/verify-auth'; - -export { SignOutButton, OAuthSignInButton } from './views/bootstrap'; - -const authStateReducer = combineReducers({ - configure, - signOut, - authentication, - oAuthSignIn, - server, - user -}); - -export { configure } from './actions/configure'; -export { signOut } from './actions/sign-out'; -export { oAuthSignIn } from './actions/oauth-sign-in'; -export { getApiUrl } from './utils/session-storage'; - -export { verifyAuth, authStateReducer }; - -export { default as fetch } from './utils/fetch'; - -export { - AUTHENTICATE_START, - AUTHENTICATE_COMPLETE, - AUTHENTICATE_ERROR -} from './actions/authenticate'; - -export { - OAUTH_SIGN_IN_START, - OAUTH_SIGN_IN_COMPLETE, - OAUTH_SIGN_IN_ERROR -} from './actions/oauth-sign-in'; - -export { - SS_AUTH_TOKEN_UPDATE, - SS_AUTH_TOKEN_REPLACE -} from './actions/server'; - -export { - SIGN_OUT_START, - SIGN_OUT_COMPLETE, - SIGN_OUT_ERROR -} from './actions/sign-out'; +export authStateReducer from 'reducers'; +export fetch from 'utils/fetch'; +export { getHeaders } from 'utils/headers'; +export parseResponse from 'utils/parseResponse'; + +export { initialize } from 'actions/initialize'; +export { signOut, SIGN_OUT, SIGN_OUT_COMPLETE, SIGN_OUT_ERROR } from 'actions/signOut'; +export { authenticateStart, authenticateComplete, authenticateError } from 'actions/authenticate'; +export { oAuthSignIn } from 'actions/oauthSignIn'; +export { updateHeaders, UPDATE_HEADERS } from 'actions/headers'; +export verifyAuth from 'actions/verifyAuth'; + +export { AUTHENTICATE_START, AUTHENTICATE_COMPLETE, AUTHENTICATE_ERROR } from 'actions/authenticate'; + +export { OAUTH_SIGN_IN_START, OAUTH_SIGN_IN_COMPLETE, OAUTH_SIGN_IN_ERROR } from 'actions/oauthSignIn'; diff --git a/src/models/headers.js b/src/models/headers.js new file mode 100644 index 0000000..a28be33 --- /dev/null +++ b/src/models/headers.js @@ -0,0 +1,17 @@ +import { getSettings } from './settings'; +import keys from 'lodash/keys'; + +export function getHeaders(state) { + const { tokenFormat } = getSettings(state); + const ret = {}; + + keys(tokenFormat).forEach(key => { + const value = state.auth.getIn(['headers', key]); + + if (value) { + ret[key] = value; + } + }); + + return ret; +} diff --git a/src/models/settings.js b/src/models/settings.js new file mode 100644 index 0000000..a9f35cd --- /dev/null +++ b/src/models/settings.js @@ -0,0 +1,38 @@ +export function getSettings(state) { + return { + backend: getBackend(state), + cookieOptions: cookieOptions(state), + tokenFormat: getTokenFormat(state) + }; +} + +function getBackend(state) { + const apiUrl = `${state.auth.getIn(['config', 'backend', 'apiUrl'])}`; + const authProviderPaths = {}; + + state.auth.getIn(['config', 'backend', 'authProviderPaths']) + .forEach((path, key) => authProviderPaths[key] = `${apiUrl}${path}`); + + return { + tokenValidationPath: `${apiUrl}${state.auth.getIn(['config', 'backend', 'tokenValidationPath'])}`, + signOutPath: `${apiUrl}${state.auth.getIn(['config', 'backend', 'signOutPath'])}`, + authProviderPaths, + apiUrl + }; +} + +function cookieOptions(state) { + return { + key: state.auth.getIn(['config', 'cookieOptions', 'key']), + expire: state.auth.getIn(['config', 'cookieOptions', 'expire']), + path: state.auth.getIn(['config', 'cookieOptions', 'path']) + }; +} + +function getTokenFormat(state) { + const ret = {}; + + state.auth.getIn(['config', 'tokenFormat']).forEach((value, key) => ret[key] = value); + + return ret; +} diff --git a/src/reducers/authenticate.js b/src/reducers/authenticate.js index 97fbc51..a587814 100644 --- a/src/reducers/authenticate.js +++ b/src/reducers/authenticate.js @@ -1,27 +1,26 @@ -import Immutable from 'immutable'; -import { createReducer } from 'redux-immutablejs'; -import * as A from 'actions/authenticate'; - -const initialState = Immutable.fromJS({ - loading: false, - valid: false, - errors: null -}); - -export default createReducer(initialState, { - [A.AUTHENTICATE_START]: state => state.set('loading', true), - - [A.AUTHENTICATE_COMPLETE]: (state) => { - return state.merge({ - loading: false, - errors: null, - valid: true - }); - }, - - [A.AUTHENTICATE_ERROR]: state => state.merge({ - loading: false, - errors: 'Invalid token', - valid: false - }) -}); +import Immutable from 'immutable'; +import { createReducer } from 'redux-immutablejs'; +import { AUTHENTICATE_COMPLETE, AUTHENTICATE_ERROR } from 'actions/authenticate'; +import { SIGN_OUT } from 'actions/signOut'; + +const initialState = Immutable.fromJS({ + loading: false, + valid: false, + errors: null +}); + +export default createReducer(initialState, { + [AUTHENTICATE_COMPLETE]: state => state.merge({ + loading: false, + errors: null, + valid: true + }), + + [AUTHENTICATE_ERROR]: state => state.merge({ + loading: false, + errors: 'Invalid token', + valid: false + }), + + [SIGN_OUT]: () => initialState +}); diff --git a/src/reducers/config.js b/src/reducers/config.js new file mode 100644 index 0000000..3b2aea5 --- /dev/null +++ b/src/reducers/config.js @@ -0,0 +1,9 @@ +import Immutable from 'immutable'; +import { createReducer } from 'redux-immutablejs'; +import { AUTH_INIT_SETTINGS } from 'actions/initialize'; + +const initialState = Immutable.fromJS({}); + +export default createReducer(initialState, { + [AUTH_INIT_SETTINGS]: (state, { config }) => state.mergeDeep({ ...config }) +}); diff --git a/src/reducers/configure.js b/src/reducers/configure.js deleted file mode 100644 index 37282bd..0000000 --- a/src/reducers/configure.js +++ /dev/null @@ -1,12 +0,0 @@ -import Immutable from 'immutable'; -import { createReducer } from 'redux-immutablejs'; -import * as A from 'actions/configure'; - -const initialState = Immutable.fromJS({ - loading: true, - errors: null -}); - -export default createReducer(initialState, { - [A.CONFIGURE_START]: state => state.set('loading', true) -}); diff --git a/src/reducers/headers.js b/src/reducers/headers.js new file mode 100644 index 0000000..3bbc733 --- /dev/null +++ b/src/reducers/headers.js @@ -0,0 +1,14 @@ +import Immutable from 'immutable'; +import { createReducer } from 'redux-immutablejs'; + +import { UPDATE_HEADERS } from 'actions/headers'; + +import { SIGN_OUT } from 'actions/signOut'; + +const initialState = Immutable.fromJS({}); + +export default createReducer(initialState, { + [UPDATE_HEADERS]: (state, { headers }) => state.merge(headers), + + [SIGN_OUT]: () => initialState +}); diff --git a/src/reducers/index.js b/src/reducers/index.js new file mode 100644 index 0000000..18d8918 --- /dev/null +++ b/src/reducers/index.js @@ -0,0 +1,17 @@ +import { combineReducers } from 'redux-immutablejs'; + +import authentication from './authenticate'; +import user from './user'; +import oAuthSignIn from './oauthSignIn'; +import headers from './headers'; +import signOut from './signOut'; +import config from './config'; + +export default combineReducers({ + signOut, + authentication, + oAuthSignIn, + headers, + user, + config +}); diff --git a/src/reducers/oauth-sign-in.js b/src/reducers/oauth-sign-in.js deleted file mode 100644 index 8c21135..0000000 --- a/src/reducers/oauth-sign-in.js +++ /dev/null @@ -1,26 +0,0 @@ -import Immutable from 'immutable'; -import { createReducer } from 'redux-immutablejs'; -import * as A from 'actions/oauth-sign-in'; - -const initialState = Immutable.fromJS({ - loading: false, - errors: null -}); - -export default createReducer(initialState, { - [A.OAUTH_SIGN_IN_START]: (state, { provider }) => state.setIn([provider, 'loading'], true), - - [A.OAUTH_SIGN_IN_COMPLETE]: (state, { provider }) => state.mergeDeep({ - [provider]: { - loading: false, - errors: null - } - }), - - [A.OAUTH_SIGN_IN_ERROR]: (state, { provider, errors }) => state.mergeDeep({ - [provider]: { - loading: false, - errors - } - }) -}); diff --git a/src/reducers/oauthSignIn.js b/src/reducers/oauthSignIn.js new file mode 100644 index 0000000..77de1d8 --- /dev/null +++ b/src/reducers/oauthSignIn.js @@ -0,0 +1,29 @@ +import Immutable from 'immutable'; +import { createReducer } from 'redux-immutablejs'; +import { OAUTH_SIGN_IN_START, OAUTH_SIGN_IN_COMPLETE, OAUTH_SIGN_IN_ERROR } from 'actions/oauthSignIn'; +import { SIGN_OUT } from 'actions/signOut'; + +const initialState = Immutable.fromJS({ + loading: false, + errors: null +}); + +export default createReducer(initialState, { + [OAUTH_SIGN_IN_START]: (state, { provider }) => state.setIn([provider, 'loading'], true), + + [OAUTH_SIGN_IN_COMPLETE]: (state, { provider }) => state.mergeDeep({ + [provider]: { + loading: false, + errors: null + } + }), + + [OAUTH_SIGN_IN_ERROR]: (state, { provider, errors }) => state.mergeDeep({ + [provider]: { + loading: false, + errors + } + }), + + [SIGN_OUT]: () => initialState +}); diff --git a/src/reducers/server.js b/src/reducers/server.js deleted file mode 100644 index 93c158c..0000000 --- a/src/reducers/server.js +++ /dev/null @@ -1,12 +0,0 @@ -import Immutable from 'immutable'; -import { createReducer } from 'redux-immutablejs'; - -import { SS_AUTH_TOKEN_UPDATE, SS_AUTH_TOKEN_REPLACE } from 'actions/server'; - -const initialState = Immutable.fromJS({ headers: null }); - -export default createReducer(initialState, { - [SS_AUTH_TOKEN_UPDATE]: (state, { headers }) => state.merge({ headers }), - - [SS_AUTH_TOKEN_REPLACE]: (state, { headers }) => state.merge({ headers }) -}); diff --git a/src/reducers/sign-out.js b/src/reducers/sign-out.js deleted file mode 100644 index d239cac..0000000 --- a/src/reducers/sign-out.js +++ /dev/null @@ -1,16 +0,0 @@ -import Immutable from 'immutable'; -import { createReducer } from 'redux-immutablejs'; -import * as A from 'actions/sign-out'; - -const initialState = Immutable.fromJS({ - loading: false, - errors: null -}); - -export default createReducer(initialState, { - [A.SIGN_OUT_START]: (state) => state.setIn(['loading'], true), - - [A.SIGN_OUT_COMPLETE]: (state) => state.mergeDeep({ loading: false, errors: null }), - - [A.SIGN_OUT_ERROR]: (state, { errors }) => state.mergeDeep({ loading: false, errors }) -}); diff --git a/src/reducers/signOut.js b/src/reducers/signOut.js new file mode 100644 index 0000000..83e1961 --- /dev/null +++ b/src/reducers/signOut.js @@ -0,0 +1,16 @@ +import Immutable from 'immutable'; +import { createReducer } from 'redux-immutablejs'; +import { SIGN_OUT, SIGN_OUT_COMPLETE, SIGN_OUT_ERROR } from 'actions/signOut'; + +const initialState = Immutable.fromJS({ + loading: false, + errors: null +}); + +export default createReducer(initialState, { + [SIGN_OUT]: (state) => state.setIn([ 'loading' ], true), + + [SIGN_OUT_COMPLETE]: (state) => state.mergeDeep({ loading: false, errors: null }), + + [SIGN_OUT_ERROR]: (state, { errors }) => state.mergeDeep({ loading: false, errors }) +}); diff --git a/src/reducers/user.js b/src/reducers/user.js index 48ce504..84e2357 100644 --- a/src/reducers/user.js +++ b/src/reducers/user.js @@ -1,36 +1,28 @@ -import Immutable from 'immutable'; -import { createReducer } from 'redux-immutablejs'; - -import { SIGN_OUT_COMPLETE, SIGN_OUT_ERROR } from 'actions/sign-out'; -import { OAUTH_SIGN_IN_COMPLETE } from 'actions/oauth-sign-in'; -import { AUTHENTICATE_COMPLETE, AUTHENTICATE_FAILURE } from 'actions/authenticate'; -import { SS_AUTH_TOKEN_UPDATE} from 'actions/server'; - -const initialState = Immutable.fromJS({ - attributes: null, - isSignedIn: false -}); - -export default createReducer(initialState, { - [AUTHENTICATE_COMPLETE]: (state, { user }) => state.merge({ - attributes: user, - isSignedIn: true - }), - - [OAUTH_SIGN_IN_COMPLETE]: (state, { user }) => state.merge({ - attributes: user, - isSignedIn: true - }), - - [SS_AUTH_TOKEN_UPDATE]: (state, { user }) => { - return state.merge({ - isSignedIn: !!user, - attributes: user - }); - }, - - [AUTHENTICATE_FAILURE]: state => state.merge(initialState), - - [SIGN_OUT_COMPLETE]: state => state.merge(initialState), - [SIGN_OUT_ERROR]: state => state.merge(initialState) -}); +import Immutable from 'immutable'; +import { createReducer } from 'redux-immutablejs'; + +import { AUTHENTICATE_COMPLETE, AUTHENTICATE_ERROR } from 'actions/authenticate'; +import { OAUTH_SIGN_IN_COMPLETE } from 'actions/oauthSignIn'; + +import { SIGN_OUT } from 'actions/signOut'; + +const initialState = Immutable.fromJS({ + attributes: null, + isSignedIn: false +}); + +export default createReducer(initialState, { + [AUTHENTICATE_COMPLETE]: (state, { user }) => state.merge({ + attributes: user, + isSignedIn: true + }), + + [OAUTH_SIGN_IN_COMPLETE]: (state, { user }) => state.merge({ + attributes: user, + isSignedIn: true + }), + + [AUTHENTICATE_ERROR]: state => state.merge(initialState), + + [SIGN_OUT]: () => initialState +}); diff --git a/src/utils/client-settings.js b/src/utils/client-settings.js deleted file mode 100644 index 3ce8e76..0000000 --- a/src/utils/client-settings.js +++ /dev/null @@ -1,60 +0,0 @@ -import { SAVED_CREDS_KEY } from './constants'; -import assign from 'lodash/assign'; -import fetch from './fetch'; -import { - getApiUrl, - getCurrentSettings, - setCurrentSettings, - retrieveData, - persistData -} from './session-storage'; - -export const defaultSettings = { - storage: 'cookies', - cookieExpiry: 14, - cookiePath: '/', - initialCredentials: null, - reduxInitialState: '__INITIAL_STATE__', - - tokenFormat: { - 'access-token': '{{ access-token }}', - 'token-type': 'Bearer', - client: '{{ client }}', - expiry: '{{ expiry }}', - uid: '{{ uid }}' - } -}; - -export function initSettings(settings) { - const mergedSettings = assign({}, defaultSettings, settings); - - setCurrentSettings(mergedSettings); - - return mergedSettings; -} - -export function applyConfig({ settings = {} }) { - initSettings(settings); - - const savedCreds = retrieveData(SAVED_CREDS_KEY); - - if (getCurrentSettings().initialCredentials) { - const { user, headers } = getCurrentSettings().initialCredentials; - - persistData(SAVED_CREDS_KEY, headers); - - return Promise.resolve(user); - } else if (savedCreds) { - return fetch(getApiUrl()); - } - - return Promise.reject({ reason: 'No credentials.' }) -} - -export function getAccessToken(headers) { - if (headers.get && typeof headers.get === 'function') { - return headers.get('access-token'); - } - - return headers['access-token']; -} diff --git a/src/utils/constants.js b/src/utils/constants.js deleted file mode 100644 index 3839a0a..0000000 --- a/src/utils/constants.js +++ /dev/null @@ -1,3 +0,0 @@ -const SAVED_CREDS_KEY = "authHeaders"; - -export { SAVED_CREDS_KEY }; diff --git a/src/utils/fetch.js b/src/utils/fetch.js index 9dac0f0..7b620cf 100644 --- a/src/utils/fetch.js +++ b/src/utils/fetch.js @@ -1,87 +1,28 @@ -import originalFetch from 'isomorphic-fetch'; -import assign from 'lodash/assign'; -import keys from 'lodash/keys'; - -import { SAVED_CREDS_KEY } from './constants'; -import { - getApiUrl, - retrieveData, - persistData, - getCurrentSettings, - getTokenFormat -} from './session-storage'; -import { parseHeaders,areHeadersBlank } from './headers'; -import { getAccessToken } from './client-settings'; - -import { ssAuthTokenReplace } from 'actions/server'; - - -const isApiRequest = (url) => url.match(getApiUrl()); - -export function addAuthorizationHeader(accessToken, headers) { - return assign({}, headers, { Authorization: `Bearer ${accessToken}` }); -} - -function getAuthHeaders(url) { - if (isApiRequest(url)) { - let currentHeaders = {}; - - if (getCurrentSettings().isServer) { - currentHeaders = getCurrentSettings().headers; - } else { - currentHeaders = retrieveData(SAVED_CREDS_KEY) || currentHeaders; - } - - const nextHeaders = {}; - - nextHeaders['If-Modified-Since'] = 'Mon, 26 Jul 1997 05:00:00 GMT'; - - if (typeof currentHeaders === 'undefined') { - return nextHeaders; - } - - keys(getTokenFormat()).forEach((key) => { - const value = currentHeaders[key]; - - if (typeof value !== 'undefined') { - nextHeaders[key] = currentHeaders[key]; - } - }); - - if(!areHeadersBlank(currentHeaders)) { - return addAuthorizationHeader(getAccessToken(currentHeaders), nextHeaders); - } - } - - return {}; -} - -function updateAuthCredentials(resp) { - if (isApiRequest(resp.url)) { - const oldHeaders = resp.headers; - - if (!areHeadersBlank(oldHeaders)) { - const newHeaders = parseHeaders(oldHeaders); - if (getCurrentSettings().isServer) { - getCurrentSettings().headers = newHeaders; - - getCurrentSettings().dispatch(ssAuthTokenReplace({ headers: newHeaders })); - } else { - persistData(SAVED_CREDS_KEY, newHeaders); - } - } - } - - return resp; -} - -export default function(url, options={}) { - if (!options.headers) { - options.headers = {} - } - - assign(options.headers, getAuthHeaders(url)); - - return originalFetch(url, options) - .then(resp => updateAuthCredentials(resp)); -} +import originalFetch from 'isomorphic-fetch'; +import assign from 'lodash/assign'; +import { parseHeaders, getHeaders, prepareHeadersForFetch } from 'utils/headers'; +import { updateHeaders } from 'actions/headers'; +import { getSettings } from 'models/settings'; + +export default function (url, options = {}) { + return (dispatch, getState) => { + const state = getState(); + const { tokenFormat, backend } = getSettings(state); + + if (!url.match(backend.apiUrl)) { + return originalFetch(url, options) + .then(resp => Promise.resolve(resp)) + .catch(err => Promise.reject(err)); + } + + return originalFetch(url, assign({}, options, { headers: prepareHeadersForFetch(getHeaders(state), tokenFormat) })) + .then((resp) => { + const headers = parseHeaders(resp.headers, tokenFormat); + + dispatch(updateHeaders(headers)); + + return Promise.resolve(resp); + }) + .catch(err => Promise.reject(err)); + }; +} diff --git a/src/utils/getRedirectInfo.js b/src/utils/getRedirectInfo.js new file mode 100644 index 0000000..ed9ffd4 --- /dev/null +++ b/src/utils/getRedirectInfo.js @@ -0,0 +1,84 @@ +import querystring from 'querystring'; +import assign from 'lodash/assign'; +import keys from 'lodash/keys'; +import omit from 'lodash/omit'; + +function getAnchorSearch(location) { + const rawAnchor = location.anchor || ''; + const arr = rawAnchor.split('?'); + + return arr.length > 1 ? arr[1] : null; +} + + +function getSearchQs(location) { + const rawQs = location.search || ''; + const qs = rawQs.replace('?', ''); + + return qs ? querystring.parse(qs) : {}; +} + +function getAnchorQs(location) { + const anchorQs = getAnchorSearch(location); + + return (anchorQs) ? querystring.parse(anchorQs) : {}; +} + +export function getAllParams(location) { + return assign({}, getAnchorQs(location), getSearchQs(location)); +} + + +function buildCredentials(location, providedKeys) { + const params = getAllParams(location); + const authHeaders = {}; + + keys(providedKeys).forEach((key) => authHeaders[key] = params[key]); + + return authHeaders; +} + + +// this method is tricky. we want to reconstruct the current URL with the +// following conditions: +// 1. search contains none of the supplied keys +// 2. anchor search (i.e. `#/?key=val`) contains none of the supplied keys +// 3. all of the keys NOT supplied are presevered in their original form +// 4. url protocol, host, and path are preserved +function getLocationWithoutParams(currentLocation, keyz) { + let newSearch = querystring.stringify(omit(getSearchQs(currentLocation), keyz)); + const newAnchorQs = querystring.stringify(omit(getAnchorQs(currentLocation), keyz)); + let newAnchor = (currentLocation.hash || '').split('?')[0]; + + if (newSearch) { + newSearch = `?${newSearch}`; + } + + if (newAnchorQs) { + newAnchor += `?${newAnchorQs}`; + } + + if (newAnchor && !newAnchor.match(/^#/)) { + newAnchor = `#/${newAnchor}`; + } + + return currentLocation.pathname + newSearch + newAnchor; +} + + +export default function (currentLocation, tokenFormat) { + if (!currentLocation) { + return {}; + } + + const authKeys = keys(tokenFormat); + + const authRedirectHeaders = buildCredentials(currentLocation, authKeys); + const authRedirectPath = getLocationWithoutParams(currentLocation, authKeys); + + if (authRedirectPath !== currentLocation) { + return { authRedirectHeaders, authRedirectPath }; + } + + return {}; +} diff --git a/src/utils/headers.js b/src/utils/headers.js index 811524c..c41d5ae 100644 --- a/src/utils/headers.js +++ b/src/utils/headers.js @@ -1,19 +1,95 @@ -import { getTokenFormat } from './session-storage'; -import keys from 'lodash/keys'; -import isArray from 'lodash/isArray'; +import assign from 'lodash/assign'; +import keys from 'lodash/keys'; +import isArray from 'lodash/isArray'; -export function parseHeaders(headers) { +import Cookies from 'js-cookie'; + +import { getHeaders as _getHeaders } from 'models/headers'; +import { getSettings } from 'models/settings'; + +function evalHeader(expression, headers) { + try { + const preprocessed = expression.trim(); + + if (preprocessed.length > 1 && preprocessed[0] === '{' && preprocessed[preprocessed.length - 1] === '}') { + return preprocessed.substr(1, preprocessed.length - 2).replace(/\{(.*?)}/g, (...m) => { + const header = headers[m[1].trim()]; + + if (!header) { + throw 'required values missing'; + } + + return header; + }); + } + + return expression; + } catch (ex) { + return null; + } +} + +export function prepareHeadersForFetch(headers, tokenFormat) { + const fetchHeaders = assign({}, headers, { 'If-Modified-Since': 'Mon, 26 Jul 1997 05:00:00 GMT' }); + + keys(tokenFormat).forEach(key => { + const defaultValue = tokenFormat[key]; + + if (defaultValue && !fetchHeaders[key]) { + const evaluatedHeader = evalHeader(defaultValue, headers); + + if (evaluatedHeader) { + fetchHeaders[key] = evaluatedHeader; + } + } + }); + + return fetchHeaders; +/* + if (header['access-token']) { + return assign({}, header, { Authorization: `Bearer ${header['access-token']}` }); + } + + return header; + */ +} + +export function getHeaders(state) { + if (!state || state === undefined) { + return {}; + } + + const { cookieOptions, tokenFormat } = getSettings(state); + const ret = _getHeaders(state); + + if (!areHeadersBlank(ret, tokenFormat)) { + return ret; + } + + try { + return JSON.parse(Cookies.get(cookieOptions.key) || '{}'); + } catch (ex) { + return {}; + } +} + +export function parseHeaders(headers, tokenFormat) { if (!headers) { - return; + return {}; } const newHeaders = {}; - const tokenFormat = getTokenFormat(); + let blankHeaders = true; - const isHeaders = headers.constructor.name === 'Headers'; keys(tokenFormat).forEach((key) => { - newHeaders[key] = isHeaders ? headers.get(key) : headers[key]; + if (headers[key] === undefined) { + if (headers.get && headers.get(key)) { + newHeaders[key] = headers.get(key); + } + } else { + newHeaders[key] = headers[key]; + } if (newHeaders[key]) { if (isArray(newHeaders[key])) { @@ -27,19 +103,20 @@ export function parseHeaders(headers) { if (!blankHeaders) { return newHeaders; } + + return {}; } -export function areHeadersBlank(headers) { +export function areHeadersBlank(headers, tokenFormat) { if (!headers) { return true; } - - const tokenFormat = getTokenFormat(); - const allKeys = keys(tokenFormat); - const isHeaders = headers.constructor.name === 'Headers'; + + const allKeys = keys(tokenFormat); for (let i = 0; i < allKeys.length; ++i) { - const value = isHeaders ? headers.has(allKeys[i]) : typeof headers[allKeys[i]] !== 'undefined'; + const key = allKeys[i]; + const value = headers[key] === undefined ? (headers.has && headers.has(key)) : true; if (value) { return false; diff --git a/src/utils/parse-url.js b/src/utils/parse-url.js deleted file mode 100644 index 26d8f56..0000000 --- a/src/utils/parse-url.js +++ /dev/null @@ -1,95 +0,0 @@ -import querystring from 'querystring'; -import assign from 'lodash/assign'; -import keys from 'lodash/keys'; - -import { getTokenFormat } from './session-storage'; - -const getAnchorSearch = (location) => { - const rawAnchor = location.anchor || ''; - const arr = rawAnchor.split('?'); - - return arr.length > 1 ? arr[1] : null; -}; - - -const getSearchQs = (location) => { - const rawQs = location.search || ''; - const qs = rawQs.replace('?', ''); - - return qs ? querystring.parse(qs) : {}; -}; - - -const getAnchorQs = function(location) { - const anchorQs = getAnchorSearch(location); - - return (anchorQs) ? querystring.parse(anchorQs) : {}; -}; - -const stripKeys = (obj, keys) => { - for (const q in keys) { - delete obj[keys[q]]; - } - - return obj; -}; - -export function getAllParams (location) { - return assign({}, getAnchorQs(location), getSearchQs(location)); -} - - -const buildCredentials = (location, providedKeys) => { - const params = getAllParams(location); - const authHeaders = {}; - - keys(providedKeys).forEach((key) => authHeaders[key] = params[key]); - - return authHeaders; -}; - - -// this method is tricky. we want to reconstruct the current URL with the -// following conditions: -// 1. search contains none of the supplied keys -// 2. anchor search (i.e. `#/?key=val`) contains none of the supplied keys -// 3. all of the keys NOT supplied are presevered in their original form -// 4. url protocol, host, and path are preserved -const getLocationWithoutParams = (currentLocation, keys) => { - let newSearch = querystring.stringify(stripKeys(getSearchQs(currentLocation), keys)); - let newAnchorQs = querystring.stringify(stripKeys(getAnchorQs(currentLocation), keys)); - let newAnchor = (currentLocation.hash || '').split('?')[0]; - - if (newSearch) { - newSearch = '?' + newSearch; - } - - if (newAnchorQs) { - newAnchor += '?' + newAnchorQs; - } - - if (newAnchor && !newAnchor.match(/^#/)) { - newAnchor = '#/' + newAnchor; - } - - // reconstruct location with stripped auth keys - return currentLocation.pathname + newSearch + newAnchor; -}; - - -export default function getRedirectInfo(currentLocation) { - if (!currentLocation) { - return {}; - } else { - const authKeys = keys(getTokenFormat()); - - const authRedirectHeaders = buildCredentials(currentLocation, authKeys); - const authRedirectPath = getLocationWithoutParams(currentLocation, authKeys); - - if (authRedirectPath !== currentLocation) { - return {authRedirectHeaders, authRedirectPath}; - } else { - return {}; - } - } -} diff --git a/src/utils/handle-fetch-response.js b/src/utils/parseResponse.js similarity index 74% rename from src/utils/handle-fetch-response.js rename to src/utils/parseResponse.js index 80918a8..0269448 100644 --- a/src/utils/handle-fetch-response.js +++ b/src/utils/parseResponse.js @@ -1,9 +1,9 @@ -export function parseResponse(response) { - const json = response.json(); - - if (response.status >= 200 && response.status < 300) { - return json; - } - - return json.then(err => Promise.reject(err)); -} +export default function (response) { + const json = response.json(); + + if (response.status >= 200 && response.status < 300) { + return json; + } + + return json.then(err => Promise.reject(err)); +} diff --git a/src/utils/popup.js b/src/utils/popup.js index 27537bf..964b9ac 100644 --- a/src/utils/popup.js +++ b/src/utils/popup.js @@ -1,50 +1,49 @@ -const settings = 'scrollbars=no,toolbar=no,location=no,titlebar=no,directories=no,status=no,menubar=no'; - -function getPopupOffset({ width, height }) { - const wLeft = window.screenLeft ? window.screenLeft : window.screenX; - const wTop = window.screenTop ? window.screenTop : window.screenY; - - const left = wLeft + (window.innerWidth / 2) - (width / 2); - const top = wTop + (window.innerHeight / 2) - (height / 2); - - return { top, left }; -} - -function getPopupSize(provider) { - switch (provider) { - case 'facebook': - return {width: 580, height: 400}; - - case 'google': - return {width: 452, height: 633}; - - case 'github': - return {width: 1020, height: 618}; - - case 'linkedin': - return {width: 527, height: 582}; - - case 'twitter': - return {width: 495, height: 645}; - - case 'live': - return {width: 500, height: 560}; - - case 'yahoo': - return {width: 559, height: 519}; - - default: - return {width: 1020, height: 618}; - } -} - -function getPopupDimensions(provider) { - const { width, height} = getPopupSize(provider); - const { top, left} = getPopupOffset({ width, height }); - - return `width=${width},height=${height},top=${top},left=${left}`; -} - -const openPopup = (provider, url, name) => window.open(url, name, `${settings},${getPopupDimensions(provider)}`); - -export default openPopup; +const settings = 'scrollbars=no,toolbar=no,location=no,titlebar=no,directories=no,status=no,menubar=no'; + +function getPopupOffset({ width, height }) { + const wLeft = window.screenLeft ? window.screenLeft : window.screenX; + const wTop = window.screenTop ? window.screenTop : window.screenY; + + const left = wLeft + (window.innerWidth / 2) - (width / 2); + const top = wTop + (window.innerHeight / 2) - (height / 2); + + return { top, left }; +} + +function getPopupSize(provider) { + switch (provider) { + case 'facebook': + return { width: 580, height: 400 }; + + case 'google': + return { width: 452, height: 633 }; + + case 'github': + return { width: 1020, height: 618 }; + + case 'linkedin': + return { width: 527, height: 582 }; + + case 'twitter': + return { width: 495, height: 645 }; + + case 'live': + return { width: 500, height: 560 }; + + case 'yahoo': + return { width: 559, height: 519 }; + + default: + return { width: 1020, height: 618 }; + } +} + +function getPopupDimensions(provider) { + const { width, height } = getPopupSize(provider); + const { top, left } = getPopupOffset({ width, height }); + + return `width=${width},height=${height},top=${top},left=${left}`; +} + +export default (provider, url, name) => window.open(url, name, `${settings},${getPopupDimensions(provider)}`); + diff --git a/src/utils/session-storage.js b/src/utils/session-storage.js deleted file mode 100644 index b0a95d9..0000000 --- a/src/utils/session-storage.js +++ /dev/null @@ -1,103 +0,0 @@ -import Cookies from 'js-cookie'; -import { defaultSettings } from './client-settings'; -import { SAVED_CREDS_KEY} from './constants'; - -import keys from 'lodash/keys'; - -// even though this code shouldn't be used server-side, node will throw -// errors if "window" is used -const root = Function("return this")() || (42, eval)("this"); - -// stateful variables that persist throughout session -root.authState = { currentSettings: defaultSettings }; - -export function setCurrentSettings (s) { - root.authState.currentSettings = s; -} - -export function getCurrentSettings () { - return root.authState.currentSettings; -} - -export function destroySession () { - const sessionKeys = [SAVED_CREDS_KEY]; - - keys(sessionKeys).forEach((key) => { - const value = sessionKeys[key]; - - if (root.localStorage) { - root.localStorage.removeItem(value); - } - - Cookies.remove(value, { path: root.authState.currentSettings.cookiePath || '/' }); - }); -} - -function unescapeQuotes (val) { - return val && val.replace(/("|')/g, ''); -} - -export function getSignOutUrl() { - return `${getApiUrl()}${root.authState.currentSettings.signOutPath}` -} - -export function getTokenValidationPath() { - return `${getApiUrl()}${root.authState.currentSettings.tokenValidationPath}` -} - -export function getOAuthUrl ({ provider, params }) { - let oAuthUrl = `${getApiUrl()}${root.authState.currentSettings.authProviderPaths[provider]}?auth_origin_url=${encodeURIComponent(root.location.href)}`; - - if (params) { - for(const key in params) { - oAuthUrl += `&${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`; - } - } - - return oAuthUrl; -} - -export function getApiUrl() { - return root.authState.currentSettings.apiUrl; -} - -export function getTokenFormat() { - return root.authState.currentSettings.tokenFormat; -} - -export function persistData(key, val) { - const valInJson = JSON.stringify(val); - - switch (root.authState.currentSettings.storage) { - case 'localStorage': - root.localStorage.setItem(key, valInJson); - break; - - default: - Cookies.set(key, valInJson, { - expires: root.authState.currentSettings.cookieExpiry, - path: root.authState.currentSettings.cookiePath - }); - break; - } -} - -export function retrieveData(key) { - let val = null; - - switch (root.authState.currentSettings.storage) { - case 'localStorage': - val = root.localStorage && root.localStorage.getItem(key); - break; - - default: - val = Cookies.get(key); - break; - } - - try { - return JSON.parse(val); - } catch (err) { - return unescapeQuotes(val); - } -} diff --git a/src/utils/verify-auth.js b/src/utils/verify-auth.js deleted file mode 100644 index 24b8172..0000000 --- a/src/utils/verify-auth.js +++ /dev/null @@ -1,75 +0,0 @@ -import fetch from 'isomorphic-fetch'; -import cookie from 'cookie'; -import url from 'url'; -import assign from 'lodash/assign'; - -import getRedirectInfo from './parse-url'; -import { addAuthorizationHeader } from './fetch'; -import { parseHeaders,areHeadersBlank } from './headers' -import { getTokenValidationPath, getCurrentSettings, setCurrentSettings } from './session-storage'; - -export function fetchToken({ cookies, currentLocation } ) { - const { authRedirectHeaders } = getRedirectInfo(url.parse(currentLocation)); - - return new Promise((resolve, reject) => { - if (cookies || authRedirectHeaders) { - const rawCookies = cookie.parse(cookies || '{}'); - const parsedCookies = JSON.parse(rawCookies.authHeaders || 'false'); - - let headers; - - if (!areHeadersBlank(authRedirectHeaders)) { - headers = parseHeaders(authRedirectHeaders); - } else if (rawCookies && parsedCookies) { - headers = parsedCookies; - } - - if (!headers) { - return reject({ reason: 'No creds' }); - } - - const settings = getCurrentSettings(); - - if (settings.isServer) { - setCurrentSettings(assign({}, settings, { headers })); - } - - const validationUrl = `${getTokenValidationPath()}?unbatch=true`; - - let newHeaders; - - return fetch(validationUrl, { - headers: addAuthorizationHeader(headers['access-token'], headers) - }).then((resp) => { - newHeaders = parseHeaders(resp.headers); - - if (settings.isServer) { - setCurrentSettings(assign({}, settings, { headers: newHeaders })); - } - - return resp.json(); - }).then((json) => { - if (json.success) { - return resolve({ headers: newHeaders, user: json.data }); - } else { - return reject({ reason: json.errors }); - } - }).catch(reason => reject({ reason })); - } else { - return reject({ reason: 'No creds' }); - } - }); -} - -const verifyAuth = ({ isServer, cookies, currentLocation }) => new Promise((resolve, reject) => { - if (isServer) { - return fetchToken({ cookies, currentLocation }) - .then(res => resolve(res)) - .catch(res => reject(res)); - } - // TODO: deal with localStorage - //Auth.validateToken(getCurrentEndpointKey()) - //.then((user) => resolve(user.data), (err) => reject({reason: err})); -}); - -export default verifyAuth; diff --git a/src/views/bootstrap/OAuthSignInButton.js b/src/views/bootstrap/OAuthSignInButton.js deleted file mode 100644 index 9b7f2a5..0000000 --- a/src/views/bootstrap/OAuthSignInButton.js +++ /dev/null @@ -1,46 +0,0 @@ -import React, { PropTypes, Component } from 'react'; -import { connect } from 'react-redux'; - -import ButtonLoader from 'react-bootstrap-button-loader'; - -import { oAuthSignIn } from 'actions/oauth-sign-in'; - -class OAuthSignInButton extends Component { - static propTypes = { - provider: PropTypes.string.isRequired, - label: PropTypes.string, - children: PropTypes.node, - icon: PropTypes.node, - dispatch: PropTypes.func - }; - - static defaultProps = { - children: OAuth Sign In, - icon: null - }; - - handleClick = () => { - const { provider, dispatch } = this.props; - - dispatch(oAuthSignIn({ provider })); - }; - - render() { - const auth = this.props.auth; - const disabled = auth.getIn(['user', 'isSignedIn']); - const loading = auth.getIn(['oAuthSignIn', this.props.provider, 'loading']); - - return ( - - ); - } -} - -export default connect(({ auth }) => ({ auth }))(OAuthSignInButton); diff --git a/src/views/bootstrap/SignOutButton.js b/src/views/bootstrap/SignOutButton.js deleted file mode 100644 index 5957a4b..0000000 --- a/src/views/bootstrap/SignOutButton.js +++ /dev/null @@ -1,42 +0,0 @@ -import React, { PropTypes, Component } from 'react'; - -import ButtonLoader from 'react-bootstrap-button-loader'; - -import { connect } from 'react-redux'; -import { signOut } from 'actions/sign-out'; - -class SignOutButton extends Component { - static propTypes = { - children: PropTypes.node, - icon: PropTypes.node, - dispatch: PropTypes.func - }; - - static defaultProps = { - children: Sign Out, - icon: null - }; - - handleClick = () => { - this.props.dispatch(signOut()); - }; - - render() { - const auth = this.props.auth; - const disabled = !auth.getIn(['user', 'isSignedIn']); - const loading = auth.getIn(['signOut', 'loading']); - - return ( - - ); - } -} - -export default connect(({ auth }) => ({ auth }))(SignOutButton); diff --git a/src/views/bootstrap/index.js b/src/views/bootstrap/index.js deleted file mode 100644 index 2775ace..0000000 --- a/src/views/bootstrap/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export SignOutButton from './SignOutButton'; -export OAuthSignInButton from './OAuthSignInButton'; diff --git a/webpack.release.js b/webpack.config.js similarity index 87% rename from webpack.release.js rename to webpack.config.js index 66ff6bc..278caa5 100644 --- a/webpack.release.js +++ b/webpack.config.js @@ -9,7 +9,7 @@ module.exports = { output: { path: path.join(__dirname, 'dist'), filename: 'bundle.js', - libraryTarget: 'commonjs2' + libraryTarget: 'umd' }, externals: [nodeExternals()], plugins: [ @@ -23,7 +23,6 @@ module.exports = { ], module: { loaders: [ - { include: /\.json$/, loader: 'json' }, { include: /\.js$/, loader: 'babel', exclude: /node_modules/ } ] },