instance.login(data) } logout={ () => instance.logout() } />;
27 | }
28 | }
29 | }, {
30 | acceptParameters: false
31 | })
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2018 Pierre Cabrière
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/src/hoc/guard.tsx:
--------------------------------------------------------------------------------
1 | import HOCManager from "hoc-manager";
2 | import React from "react";
3 |
4 | export default instance => HOCManager.create((Component, parameters) => {
5 | return class WithGuard extends React.Component {
6 | props: any;
7 | unsubscribe;
8 | state = {};
9 |
10 | componentDidMount() {
11 | this.unsubscribe = instance.store.subscribe(() => {
12 | const { user } = instance.store.getState();
13 | if (JSON.stringify(this.state) != JSON.stringify(user)) {
14 | // @ts-ignore
15 | this.setState(user);
16 | }
17 | });
18 | }
19 |
20 | componentWillUnmount() {
21 | this.unsubscribe && this.unsubscribe();
22 | }
23 |
24 | render() {
25 | const { user } = instance.store.getState();
26 | const next: Function = (nextProps = {}) => ;
27 |
28 | let guard = parameters[0];
29 | if ('string' === typeof guard) {
30 | guard = instance.getGuard(guard);
31 | }
32 |
33 | const render = guard(user, this.props, next);
34 |
35 | return React.isValidElement(render) ? render : "Guard must return a valid react component";
36 | }
37 | }
38 | }, {
39 | acceptParameters: true
40 | })
41 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import * as authActions from "./actions/authActions";
2 | import * as userActions from "./actions/userActions";
3 | import AuthHOC from "./hoc/auth";
4 | import GuardHOC from "./hoc/guard";
5 | import UserHOC from "./hoc/user";
6 | import store from "./store";
7 |
8 | interface IReactAuthConfig {
9 | fetchToken?: Function,
10 | fetchUser?: Function,
11 | isUserLogged?: Function
12 | }
13 |
14 | class ReactAuth {
15 | private static jwtName = 'REACT-AUTH-JWT';
16 | private static defaultConfig: IReactAuthConfig = {
17 | fetchToken: () => null,
18 | fetchUser: () => null,
19 | isUserLogged: data => !!data && Object.keys(data).length > 0
20 | };
21 |
22 | private _guards = {};
23 |
24 | name: string;
25 | config: IReactAuthConfig;
26 | store: any;
27 |
28 | // hoc
29 | withAuth: Function;
30 | withUser: Function;
31 | withGuard: Function;
32 |
33 | constructor(name?: string, config?: IReactAuthConfig) {
34 | const { constructor: { defaultConfig } } = Object.getPrototypeOf(this);
35 | this.config = Object.assign({}, defaultConfig, config);
36 | this.name = name;
37 | this.store = store();
38 | this.withAuth = AuthHOC(this);
39 | this.withUser = UserHOC(this);
40 | this.withGuard = GuardHOC(this);
41 |
42 | return this;
43 | }
44 |
45 | create(name: string, config: IReactAuthConfig) {
46 | return new ReactAuth(name, config);
47 | }
48 |
49 | get jwtName() {
50 | const { constructor: { jwtName } } = Object.getPrototypeOf(this);
51 | return this.name ? `${ jwtName }_${ this.name }` : jwtName;
52 | }
53 |
54 | getToken() {
55 | return localStorage.getItem(this.jwtName);
56 | }
57 |
58 | setToken(token) {
59 | localStorage.setItem(this.jwtName, token);
60 | return this;
61 | }
62 |
63 | deleteToken() {
64 | localStorage.removeItem(this.jwtName);
65 | return this;
66 | }
67 |
68 | addGuard(name, fn) {
69 | Object.assign(this._guards, { [name]: fn })
70 | }
71 |
72 | getGuard(name) {
73 | return this._guards[name];
74 | }
75 |
76 | // helpers
77 |
78 | async login(data) {
79 | this.store.dispatch(authActions.loginStart());
80 | try {
81 | const token = await this.config.fetchToken(data);
82 | this.setToken(token);
83 | this.store.dispatch(authActions.loginEnd());
84 | return await this.getUser();
85 | } catch (e) {
86 | this.store.dispatch(authActions.loginEnd());
87 | throw e;
88 | }
89 | }
90 |
91 | logout() {
92 | this.store.dispatch(authActions.logoutStart());
93 | this.store.dispatch(userActions.reset());
94 | this.deleteToken();
95 | this.store.dispatch(authActions.logoutEnd());
96 | return true;
97 | }
98 |
99 | async getUser() {
100 | this.store.dispatch(userActions.fetchStart());
101 | let user;
102 | try {
103 | user = await this.config.fetchUser();
104 | const logged = await this.config.isUserLogged(user);
105 | this.store.dispatch(userActions.fetchSuccess({ user, logged }));
106 | this.store.dispatch(userActions.fetchEnd());
107 | } catch (e) {
108 | this.store.dispatch(userActions.fetchError());
109 | this.store.dispatch(userActions.fetchEnd());
110 | throw e;
111 | }
112 | return user;
113 | }
114 | }
115 |
116 | export default new ReactAuth();
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React authentication manager 🔑
2 |
3 | [](https://www.npmjs.com/package/react-authmanager)
4 |
5 | **react-authmanager is a highly-configurable manager for react. It manages users authentication with JWT in your app and provides guards HOCs to secure components in a flexible and simple way.**
6 |
7 | ---
8 |
9 | - [Getting started](#1---getting-started)
10 | - [Authenticate users](#2---authenticate-users)
11 | - [Access user informations](#3---access-user-informations)
12 | - [Secure components](#4---secure-components)
13 | - [Authmanager](#5---authmanager)
14 | - [Authmanager.config](#51---authmanagerconfig)
15 | - [Authmanager.utils](#52---authmanager-utils)
16 |
17 | ## 1 - Getting started
18 | `npm install --save react-authmanager`, then you have to configure the package.
19 | To manage configuration, you need to import the default Authmanager.
20 | You need to configure the manager before starting to use, so your highest component file is the best place (by example, it's the `App.js` file with a [create-react-app](https://github.com/facebook/create-react-app) instance !)
21 |
22 | You can also create a new manager instance.
23 | ```js
24 | import Authmanager from 'react-authmanager';
25 |
26 | const customManager = Authmanager.create("customName", { ...config });
27 | ```
28 |
29 | **react-authmanager** needs:
30 | - to know how to login the user from the server and get a token back ([`fetchToken`](#configfetchtokencredentials-async))
31 | - to know how to get the current logged user informations from the server ([`fetchUser`](#configfetchuser-async))
32 |
33 | **you** will need:
34 | - to include the current token in your requests headers authorization ([`getToken`](#gettoken))
35 |
36 | ### Minimal configuration for the manager
37 | ```js
38 | import Authmanager from 'react-authmanager';
39 |
40 | // how to login the user from the server and get a token back
41 | Authmanager.config.fetchToken = async credentials => {
42 | ... // login user with an ajax call to the server and return the given token
43 | return token;
44 | }
45 |
46 | // how to get the current logged user informations from the server
47 | Authmanager.config.fetchUser = async () => {
48 | ... // get current logged user informations from the server with an ajax call and return any data you will need
49 | return user;
50 | }
51 | ```
52 |
53 | ### Authorize your requests
54 | ```js
55 | // include the current token in your requests headers authorization
56 | fetch(..., {
57 | headers: new Headers({
58 | 'Authorization': 'Bearer ' + Authmanager.getToken() // returns null if no token is stored
59 | }),
60 | ...
61 | });
62 | ```
63 |
64 | *For more configurations, please read the [Authmanager](#5---authmanager) section below.*
65 |
66 | ## 2 - Authenticate users
67 | **withAuth** HOC injects in your component helpers to manage authentication: **login**, **logout** and **auth**.
68 | **login** and **logout** are functions to log users. **auth** is an object that contains a state of operations.
69 |
70 | | prop | default | description |
71 | |:-----------|:--------|:--------------------------------------------------------------|
72 | | login | | `function` send credentials to the server to get a token back |
73 | | logout | | `function` remove the stored token |
74 | | auth: | | `object` informations about the current state of operations |
75 | | -- loading | false | `bool` is authentication (login or logout) currently loading |
76 |
77 | ```js
78 | import Authmanager from 'react-authmanager';
79 |
80 | class LoginComponent extends React.Component {
81 | handleSubmit() {
82 | const credentials = {
83 | email: 'hello@example.com',
84 | password: 'ThisIsASecret'
85 | }; // or whatever data you want to send to the server (see the getToken configuration in the Minimal configuration section above)
86 |
87 | this.props.login(credentials)
88 | .then(function() { alert('Hello !') })
89 | }
90 |
91 | render() {
92 | if (this.props.auth.loading)
93 | return ();
94 |
95 | return ( ... );
96 | }
97 |
98 | ...
99 | }
100 |
101 | export default Authmanager.withAuth(LoginComponent); // or customManager.withAuth(LoginComponent);
102 |
103 | ...
104 |
105 | class LogoutComponent extends React.Component {
106 | handleClick() {
107 | this.props.logout()
108 | .then(function() { alert('Good bye !') })
109 | }
110 |
111 | ...
112 | }
113 |
114 | export default Authmanager.withAuth(LogoutComponent); // or customManager.withAuth(LogoutComponent);
115 | ```
116 |
117 | You can also call the login and logout methods anywhere on the manager with `Authmanager.login()` and `Authmanager.logout()`
118 |
119 | ## 3 - Access user informations
120 | **withUser** HOC will automatically injects an user object in your component props.
121 | This object contains informations about the current user:
122 |
123 | | prop | default | description |
124 | |:-----------|:--------|:--------------------------------------------------------------------------------------------------------|
125 | | user: | | `object` containing current user informations |
126 | | -- loading | false | `bool` is user currently loaded from the server |
127 | | -- logged | false | `bool` is the current user logged in (setted by [`isUserLogged`](#configisuserloggeduser-async)) |
128 | | -- ... | null | `any` informations about the user sent by the server (setted by [`getUser`](#configgetuser-async)) |
129 |
130 | ```js
131 | import Authmanager from 'react-authmanager';
132 |
133 | class MyComponent extends React.Component {
134 | handleClick() {
135 | if (this.props.user.logged)
136 | alert('Hello ' + this.props.user.name);
137 | else
138 | alert('Hello John, please login !');
139 | }
140 |
141 | ...
142 | }
143 |
144 | export default Authmanager.withUser(MyComponent);
145 | ```
146 |
147 | ## 4 - Secure components
148 | **withGuard** HOC helps you protect your components in a flexible way. By example, you can render a login form instead of a component if no user is logged in.
149 | It needs a guard as parameter. A guard is just a function that returns a component, so you can easily create your own guards.
150 | A guard function has parameters:
151 |
152 | | prop | description |
153 | |:------|:---------------------------------------------------------|
154 | | user | `object` the current user object |
155 | | next | `function` a function that returns the current Component |
156 | | props | `object` the current component props |
157 |
158 | ```js
159 | import Authmanager from 'react-authmanager';
160 |
161 | const loggedGuard = function(user, props, next) {
162 | if (user.loading)
163 | return (); // render a loading component if user is currently fetched from the server
164 |
165 | if (user.logged)
166 | return next(); // render the component if user is not loading and is logged
167 |
168 | return (); // render a login component by default (if user is fetched from the server but not logged)
169 | }
170 |
171 | class MyComponent extends React.Component {
172 | render() {
173 | return (
174 | This message is visible only for logged users !
175 | )
176 | }
177 |
178 | ...
179 | }
180 |
181 | export default Authmanager.withGuard(loggedGuard)(MyComponent);
182 | ```
183 |
184 | You can inject data in your rendered component props through the next function
185 | ```js
186 | const guardThatInjects = function(user, props, next) {
187 | return next({ myNewProp: true });
188 | }
189 |
190 | class MyComponent extends React.Component {
191 | render() {
192 | console.log(this.props.myNewProp); // true
193 | return (
194 | This message is visible only for logged users !
195 | )
196 | }
197 |
198 | ...
199 | }
200 |
201 | export default Authmanager.withGuard(loggedGuard)(MyComponent);
202 | ```
203 |
204 | You can also configure you guards from outside the component file with the [`addGuard`](#addguard) Authmanager function :
205 |
206 | ```js
207 | Authmanager.addGuard('loggedGuard', function(user, props, next) {
208 | if (user.loading)
209 | return (); // render a loading component if user is currently fetched from the server
210 |
211 | if (user.logged)
212 | return next(); // render the component if user is not loading and is logged
213 |
214 | return (); // render a login component by default (if user is fetched from the server but not logged)
215 | });
216 |
217 | ...
218 |
219 | class MyComponent extends React.Component {
220 | render() {
221 | return (
222 | This message is visible only for logged users !
223 | )
224 | }
225 |
226 | ...
227 | }
228 |
229 | export default Authmanager.withGuard('loggedGuard')(MyComponent);
230 | ```
231 |
232 | ## 5 - Authmanager
233 |
234 | ### 5.1 - `Authmanager.config`
235 | To edit the configuration of **react-authmanager**your manager, you have to override the config object:
236 | ```js
237 | import Authmanager from 'react-authmanager';
238 |
239 | // will change the way how the manager will login the user and get a token back, see below
240 | Authmanager.fetchToken = function(credentials) {}
241 | ```
242 |
243 | ```typescript
244 | interface IReactAuthConfig {
245 | fetchToken?: Function,
246 | fetchUser?: Function,
247 | isUserLogged?: Function
248 | }
249 | ```
250 |
251 | #### `fetchToken([credentials]) [async]`
252 | Get an authentication token when an user tries to login. `fetchToken` is called when the auth login function is executed to store the token in *localStorage*.
253 |
254 | **Parameters**
255 | - [`credentials`] *(`Object`)* Argument given by the login function. (when you call `Authmanager.login(credentials)`)
256 |
257 | **Return *(`String`)***
258 | ```
259 | Need to return a token that will be stored
260 | ```
261 |
262 | **default**
263 | ```js
264 | fetchToken = null;
265 | ```
266 |
267 | **example with axios**
268 | ```js
269 | Authmanager.config.fetchToken = async credentials => {
270 | const { data } = await axios.post('https://example.com/login', credentials);
271 | return data.token;
272 | }
273 | ```
274 |
275 | #### `fetchUser() [async]`
276 | Get the current authenticated user. `fetchUser` is called when the manager initialize its store and after an user login.
277 |
278 | **Return *(`Object`)***
279 | ```
280 | Need to return informations about the current logged user
281 | ```
282 |
283 | **default**
284 | ```js
285 | fetchUser = null;
286 | ```
287 |
288 | **example with axios**
289 | ```js
290 | Authmanager.config.fetchUser = async () => {
291 | const { data } = await axios.get('https://example.com/current-user');
292 | return data;
293 | }
294 | ```
295 |
296 | #### `isUserLogged([user]) [async]`
297 | Define if the current user (returned by `getUser`) is logged. `isUserLogged` is called after each user state change. The result is set in `user.logged`.
298 |
299 | **Parameters**
300 | - [`user`] *(`Object`)* Object returned by the `getUser` function.
301 |
302 | **Return *(`Boolean`)***
303 | ```
304 | Need to return a boolean that tell if the current user (returned by `getUser`) is logged.
305 | ```
306 |
307 | **default**
308 | ```js
309 | isUserLogged = userData => !!userData && Object.keys(userData).length > 0;
310 | ```
311 | *By default, `isUserLogged` returns true if `fetchUser` returns a non-empty object*
312 |
313 | ### 5.2 - `Authmanager` utils
314 | **react-authmanager** also provides some utilities to manage the manager from your app:
315 | ```js
316 | import Authmanager from 'react-authmanager';
317 |
318 | // will return the current stored token, or null, see below
319 | const token = Authmanager.getToken()
320 | ```
321 |
322 | #### `getToken()`
323 | Returns the current stored token (in *localStorage*). You should use `getToken` to authorize your requests to the server
324 |
325 | **Return *(`String`)***
326 | ```
327 | Returns the token stored after the `fetchToken` call
328 | ```
329 |
330 | **example with axios**
331 | ```js
332 | axios.defaults.transformRequest.push((data, headers) => {
333 | const token = Authmanager.getToken();
334 | if (token) headers.common['Authorization'] = 'Bearer ' + token;
335 |
336 | return data;
337 | });
338 | ```
339 |
340 | #### `setToken([token])`
341 | Manually set a token. You can call the `setToken` if you want to implements your own login function.
342 |
343 | **Parameters**
344 | - [`token`] *(`String`)* String token that will be returned by the `fetchToken` function.
345 |
346 | **Return *utils (`Object`)***
347 | ```
348 | Need to return a boolean that tell if the current user (returned by `getUser`) is logged.
349 | ```
350 |
351 | **example**
352 | ```js
353 | Authmanager.setToken('aValidToken');
354 | Authmanager.getUser();
355 | ```
356 |
357 | #### `addGuard([guard])`
358 | Create a guard at the manager level, you will be able to reuse the guard just by giving its name
359 |
360 | **Parameters**
361 | - [`guard`] *(`Function`)* A function that returns a component or call the next function to render the default component.
362 |
363 | **Return *Component | next()***
364 | ```
365 | Need to return a valid React component or call the next function given in parameters.
366 | ```
367 |
368 | **example**
369 | ```js
370 | Authmanager.addGuard('loggedGuard', (user, next) => {
371 | if (user.loading)
372 | return loading
;
373 |
374 | if (user.logged)
375 | return next();
376 |
377 | return login
;
378 | });
379 | ```
380 |
381 | #### `getUser()`
382 | Call the `fetchUser` function and update the redux store. You can use this function to refresh the current logged user from the server
383 |
384 | **Return *utils (`Object`)***
385 | ```
386 | Returns a promise that resolves the new user data
387 | ```
388 |
389 | ---
390 |
391 | 🚀
392 |
--------------------------------------------------------------------------------