├── .eslintignore ├── .babelrc ├── .npmignore ├── src ├── services │ └── auth.js ├── lib │ ├── helpers.js │ ├── redirect.js │ ├── request.js │ ├── session.js │ └── auth.js └── index.js ├── .gitignore ├── .eslintrc ├── package.json └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | /* 2 | !/src/ 3 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["next", "@babel/preset-env"] 3 | } 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # .npmignore 2 | src 3 | examples 4 | .babelrc 5 | .gitignore 6 | webpack.config.js -------------------------------------------------------------------------------- /src/services/auth.js: -------------------------------------------------------------------------------- 1 | import { get } from '../lib/request' 2 | import { to } from '../lib/helpers' 3 | 4 | export const profile = async token => { 5 | const [err, data] = await to( 6 | get(process.env.GET_SESSION_URL || '/user/session', token) 7 | ) 8 | return (err || !data.success) ? null : data.data 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/helpers.js: -------------------------------------------------------------------------------- 1 | export const to = promise => promise 2 | .then(res => [null, { ...res.data, statusCode: res.status }, res]) 3 | .catch(err => { 4 | try { 5 | return [ 6 | { ...err.response.data, statusCode: err.response.status } || 'Unknown error' 7 | ] 8 | } catch (e) { return ['Unknown error'] } 9 | }) 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore all files in root dir 2 | /* 3 | 4 | # Excepts source files 5 | !/src/ 6 | !/__mocks__/ 7 | !/__tests__/ 8 | 9 | # Excepts documentation 10 | !/doc/ 11 | 12 | # Excepts config files 13 | !.babelrc 14 | !.eslintignore 15 | !.eslintrc 16 | !jest.config.js 17 | !jest.setup.js 18 | !.gitignore 19 | !.gitlab-ci.yml 20 | !.npmignore 21 | !package.json 22 | !README.md 23 | -------------------------------------------------------------------------------- /src/lib/redirect.js: -------------------------------------------------------------------------------- 1 | import Router from 'next/router' 2 | 3 | export default (target, ctx = {}) => { 4 | if (ctx.res) { 5 | // server 303: "See other" 6 | ctx.res.writeHead(303, { Location: target }) 7 | ctx.res.end() 8 | } else { 9 | // In the browser, we just pretend like this never even happened ;) 10 | Router.push(target) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/lib/request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | export const getUrl = endpoint => process.env.API_HOST + endpoint 4 | 5 | const buildHeader = jwt => ({ 6 | 'Content-Type': 'application/json', 7 | ...(jwt ? { Authorization: `Bearer ${jwt}` } : {}) 8 | }) 9 | 10 | export const get = async (endpoint, jwt, params = {}) => { 11 | const headers = buildHeader(jwt) 12 | 13 | return axios.get(endpoint.startsWith('https') ? endpoint : getUrl(endpoint), { 14 | headers, 15 | params 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /src/lib/session.js: -------------------------------------------------------------------------------- 1 | import cookie from 'js-cookie' 2 | 3 | export const setCookie = (key, value, sessionCookie = false) => { 4 | if (process.browser) { 5 | const options = { 6 | expires: sessionCookie ? (1 / 288) : 1, 7 | domain: process.env.COOKIE_DOMAIN 8 | } 9 | if (!process.env.COOKIE_DOMAIN) delete options.domain 10 | cookie.set(key, value, options) 11 | } 12 | } 13 | 14 | export const removeCookie = (key, opt) => { 15 | if (process.browser) 16 | cookie.remove(key, opt) 17 | } 18 | 19 | export const getCookie = (key, req) => (process.browser 20 | ? getCookieFromBrowser(key) 21 | : getCookieFromServer(key, req)) 22 | 23 | const getCookieFromBrowser = key => cookie.get(key) 24 | 25 | const getCookieFromServer = (key, req) => { 26 | if (!req.headers.cookie) 27 | return undefined 28 | 29 | const rawCookie = req.headers.cookie 30 | .split(';') 31 | .find(c => c.trim().startsWith(`${key}=`)) 32 | if (!rawCookie) 33 | return undefined 34 | 35 | return rawCookie.split('=')[1] 36 | } 37 | -------------------------------------------------------------------------------- /src/lib/auth.js: -------------------------------------------------------------------------------- 1 | import intersection from 'lodash/intersection' 2 | import redirect from './redirect' 3 | import { getCookie } from './session' 4 | 5 | export const getJwt = req => getCookie(process.env.JWT_COOKIE_NAME, req) 6 | 7 | const check_acl = (ACL, roles) => ( 8 | !ACL 9 | ? true 10 | : intersection(ACL, roles).length > 0 11 | ) 12 | 13 | export const redirectIfAuthenticated = async (ctx, user) => ( 14 | user && redirect(process.env.REDIRECT_IF_AUTHENTICATED, ctx) 15 | ) 16 | 17 | export const redirectIfNotAuthenticated = async (ctx, { user, ACL, pathname }) => { 18 | // if not logged in 19 | if (!user) { 20 | const url = process.env.REDIRECT_IF_NOT_AUTHENTICATED + ( 21 | process.env.REDIRECT_IF_NOT_AUTHENTICATED && process.env.REDIRECT_IF_NOT_AUTHENTICATED.startsWith('http') 22 | ? `?ref=${process.env.REFERER}${pathname || ''}` 23 | : '' 24 | ) 25 | return redirect(url) 26 | } 27 | 28 | // if do not have access 29 | if (user && !check_acl(ACL, user.roles)) 30 | return redirect(process.env.REDIRECT_IF_NO_ACCESS, ctx) 31 | } 32 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "plugins": ["react"], 4 | "parserOptions": { 5 | "ecmaFeatures": { 6 | "jsx": true, 7 | "modules": true 8 | } 9 | }, 10 | "env": { 11 | "browser": true 12 | }, 13 | "settings": { 14 | "react": { 15 | "version": "detect" 16 | } 17 | }, 18 | "extends" : [ 19 | "eslint:recommended", 20 | "plugin:react/recommended", 21 | "airbnb-base" 22 | ], 23 | "rules" : { 24 | "camelcase": "off", 25 | "no-return-assign": ["error", "except-parens"], 26 | "implicit-arrow-linebreak": "off", 27 | "no-use-before-define": "off", 28 | "consistent-return": "off", 29 | "semi": ["error", "never"], 30 | "indent": ["error", 4, { "SwitchCase": 1 }], 31 | "no-underscore-dangle": "off", 32 | "curly": ["error", "multi", "consistent"], 33 | "nonblock-statement-body-position": ["error", "any"], 34 | "one-var": "off", 35 | "arrow-parens": ["error", "as-needed"], 36 | "no-class-assign": "off", 37 | "comma-dangle": ["error", "never"], 38 | "padded-blocks": ["error", "never"], 39 | "class-methods-use-this": "off", 40 | "import/prefer-default-export": "off", 41 | "react/jsx-indent": ["error", 4], 42 | "react/jsx-indent-props": ["error", 4], 43 | "react/display-name": "off" 44 | } 45 | } 46 | 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-auth-hoc", 3 | "version": "1.0.5", 4 | "description": "A Higher Order Component for restricting page access.", 5 | "homepage": "https://github.com/hackerart/nextjs-auth-hoc#readme", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/hackerart/nextjs-auth-hoc" 9 | }, 10 | "keywords": [ 11 | "next", 12 | "nextjs", 13 | "react", 14 | "auth", 15 | "nextjs-auth", 16 | "next-auth" 17 | ], 18 | "main": "index.js", 19 | "scripts": { 20 | "test": "jest", 21 | "start": "webpack-dev-server --mode development", 22 | "transpile": "babel src -d . --copy-files", 23 | "prepublishOnly": "npm run transpile", 24 | "lint": "eslint ." 25 | }, 26 | "author": "hackerart", 27 | "license": "ISC", 28 | "peerDependencies": { 29 | "prop-types": "^15.7.2", 30 | "react": "^16.3.0", 31 | "react-dom": "^16.3.0" 32 | }, 33 | "devDependencies": { 34 | "@babel/cli": "^7.0.0-beta.44", 35 | "@babel/core": "^7.2.2", 36 | "@babel/plugin-transform-classes": "^7.4.4", 37 | "@babel/preset-env": "^7.4.4", 38 | "@babel/preset-react": "^7.0.0", 39 | "@babel/register": "^7.4.4", 40 | "@babel/runtime": "^7.4.4", 41 | "babel-eslint": "^10.0.1", 42 | "babel-loader": "^8.0.0-beta.0", 43 | "eslint": "^6.0.1", 44 | "eslint-config-airbnb-base": "^13.1.0", 45 | "eslint-import-resolver-babel-module": "^5.1.0", 46 | "eslint-plugin-import": "^2.18.0", 47 | "eslint-plugin-react": "^7.11.1", 48 | "html-webpack-plugin": "^3.2.0", 49 | "react": "^16.8.6", 50 | "react-dom": "^16.8.6", 51 | "webpack": "^4.30.0", 52 | "webpack-cli": "^3.3.1" 53 | }, 54 | "dependencies": { 55 | "axios": "^0.19.0", 56 | "babel-core": "^6.26.3", 57 | "babel-plugin-transform-class-properties": "^6.24.1", 58 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 59 | "babel-preset-next": "^1.2.0", 60 | "js-cookie": "^2.2.0", 61 | "lodash": "^4.17.15", 62 | "next": "^9.0.3", 63 | "prop-types": "^15.7.2" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { getJwt, redirectIfAuthenticated, redirectIfNotAuthenticated } from './lib/auth' 4 | import { profile } from './services/auth' 5 | 6 | const UserContext = React.createContext() 7 | 8 | export const Auth = options => 9 | Component => { 10 | class AuthHOC extends React.Component { 11 | static async getInitialProps(ctx) { 12 | const { ACL, action } = options 13 | const token = getJwt(ctx.req) 14 | const user = await profile(token) 15 | switch (action) { 16 | case 'RINA': 17 | await redirectIfNotAuthenticated(ctx, { user, ACL, pathname: ctx.pathname }) 18 | break 19 | case 'RIA': 20 | await redirectIfAuthenticated(ctx, user) 21 | break 22 | default: 23 | break 24 | } 25 | const props = ( 26 | Component.getInitialProps 27 | ? await Component.getInitialProps(ctx) 28 | : null 29 | ) || {} 30 | return { ...props, token, user } 31 | } 32 | 33 | render() { 34 | const { user, token, ...rest } = this.props 35 | return ( 36 | 37 | 38 | 39 | ) 40 | } 41 | } 42 | 43 | AuthHOC.propTypes = { 44 | user: PropTypes.object, 45 | token: PropTypes.string 46 | } 47 | 48 | return AuthHOC 49 | } 50 | 51 | export const withUser = Component => 52 | props => ( 53 | 54 | {user => 55 | 1) ? user : null} 58 | />} 59 | 60 | ) 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nextjs-auth-hoc 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | [![npm download][download-image]][download-url] 5 | 6 | [npm-image]: http://img.shields.io/npm/v/nextjs-auth-hoc.svg?style=flat-square 7 | [npm-url]: http://npmjs.org/package/nextjs-auth-hoc 8 | [download-image]: https://img.shields.io/npm/dm/nextjs-auth-hoc.svg?style=flat-square 9 | [download-url]: https://npmjs.org/package/nextjs-auth-hoc 10 | 11 | A Higher Order Component for restricting page access. 12 | 13 | ## Installation 14 | [![nextjs-auth-hoc](https://nodei.co/npm/nextjs-auth-hoc.png)](https://npmjs.org/package/nextjs-auth-hoc) 15 | 16 | // with npm 17 | npm install nextjs-auth-hoc 18 | 19 | // with yarn 20 | yarn add nextjs-auth-hoc 21 | 22 | ## Configuration 23 | Before using you have to specify some variables in .env of your project: 24 | 25 | # Default page to redirect users if user is not authenticated 26 | REDIRECT_IF_NOT_AUTHENTICATED=/auth/signin 27 | 28 | # Default page to redirect if user is authenticated 29 | REDIRECT_IF_AUTHENTICATED=/dashboard 30 | 31 | # Default page to redirect if action is not authorized 32 | REDIRECT_IF_NO_ACCESS=/dashboard 33 | 34 | # Name of cookie key for JWT token storage 35 | JWT_COOKIE_NAME=jwt 36 | 37 | # Host of site, only need if your authentication happens on other domain 38 | # It needed to pass "ref" query 39 | REFERER=http://example.com 40 | 41 | # Base URL of root endpoint 42 | API_HOST=http://api.example.com/v1 43 | 44 | # URL to get user session 45 | # if not specified default will be "/auth/session" 46 | GET_SESSION_URL=/user/profile 47 | 48 | ## Usage 49 | Here is a quick example to get you started, **it's all you need**: 50 | 51 | import React from 'react'; 52 | import { Auth } from 'nextjs-auth-hoc'; 53 | 54 | class Posts extends React.Component { 55 | static async getInitialProps() { 56 | return {}; 57 | } 58 | 59 | render() { 60 | const { user: { token } } = this.props // You also can access user object 61 | return ( 62 |
List of posts
63 | ); 64 | } 65 | } 66 | export default Auth({ action: 'RINA' })(Posts) 67 | 68 | There is also a special HOC withUser to access user object, **it's all you need**: 69 | 70 | import React from 'react'; 71 | import { withUser } from 'nextjs-auth-hoc'; 72 | 73 | const Header = (props) => { 74 | return ( 75 |
{props.user.name}
76 | ) 77 | } 78 | export default withUser(Header) 79 | 80 | You can restrict accessing page by passing ACL option, **it's all you need**: 81 | 82 | import React from 'react'; 83 | import { Auth } from 'nextjs-auth-hoc'; 84 | 85 | class Dashboard extends React.Component { 86 | static async getInitialProps() { 87 | return {}; 88 | } 89 | 90 | render() { 91 | const { user: { token } } = this.props // You also can access user object 92 | return ( 93 |
List of posts
94 | ); 95 | } 96 | } 97 | export default Auth({ action: 'RINA', ACL: ['admin'] })(Dashboard) 98 | 99 | ## API 100 | 101 | ### action (**optional**) 102 | type: **"string"** 103 | 104 | | value | description | 105 | |----------|---------------| 106 | | RINA | Redirect if not authenticated 107 | | RIA | Redirect if authenticated 108 | 109 | ### ACL (**optional**) 110 | type: **"array"** 111 | 112 | --------------------------------------------------------------------------------