1.0.1 • Published 8 months ago

reactive-auth-store v1.0.1

Weekly downloads
-
License
MIT
Repository
github
Last release
8 months ago

Reactive Auth Store

Firebase like reactive authentication store for user state and authentication tokens.

Features

  • Store user data and tokens.
  • State is synchronized across tabs.
  • State can be used in react.
  • State can be used outside react, in places like axios interceptors, etc.
  • Your own types for user and tokens
  • Uses local storage, and is extendable to use other storage options.

Installation

yarn add reactive-auth-store
npm install --save reactive-auth-store

General Usage

Step 1: Create the getStore Function

store.ts

import {
    AuthStore,
    LocalStoragePersistance
} from 'reactive-auth-store';

// Define type for user
export interface AuthUser {
    id: string
    email: string
}

// Define type for tokens
export interface AuthTokens {
    access_token: string
    refresh_token: string
    access_token_expiration: number
    refresh_token_expiration: number
}


let store: AuthStore<AuthUser, AuthTokens> | null = null;

const AUTH_STORAGE_KEY = '__auth__'

// function to lazily create store when required
export default function getStore() {
    if (store === null) {
        store = new AuthStore(
            // create persistor to save state
            new LocalStoragePersistance(AUTH_STORAGE_KEY),
            // a function to compares user equality
            (userA, userB) => (userA?.id || null) === (userB?.id || null)
        )
    }
    return store;
}

Step 2: Use the Auth Store

Login and Logout Users

import getStore from './store';
import type { AuthUser, AuthTokens } from './store';

// Login User
async function login(user : AuthUser, tokens: AuthTokens) {
    const store = getStore();
    await store.setUser(user);
    await store.setTokens(tokens);
}

// Logout
async function logout() {
    const store = getStore();
    await store.setUser(null);
    await store.setTokens(null);
}

Get Current State

const user = getStore().getUser();
const tokens = getStore().getTokens();

Subscribe to Changes

import getStore from './store';

const store = getStore();

store.on('initialized', function() {
    console.log('storeInitialized');
});

store.on('userChanged', function(user : AuthUser) {
    console.log('userChanged', user);
});

Usecase Examples

Creating a useAuthUser Hook and a Protected Component

Create a hook to get the current user from the store:

use-auth-user.ts

import getStore from './store';
import type { AuthUser, AuthTokens } from './store';
import { useEffect, useState } from "react"

export const useAuthUser = () => {

    const [initialized, setInitialized] = useState(false);
    const [user, setUser] = useState<AuthUser | null>(null);

    useEffect(() => {

        const store = getStore();

        const onInitialized = () => {
            setInitialized(true);
        }

        const onUserChanged = (user: AuthUser | null) => {
            setUser(user);
        }

        // initial values
        setInitialized(store.getInitialized());
        setUser(store.getUser());

        // listen to changes
        store.on('initialized', onInitialized);
        store.on('userChanged', onUserChanged);

        return () => {
            // cleanup
            store.off('initialized', onInitialized);
            store.off('userChanged', onUserChanged);
        }
    }, []);

    return { initialized, user }
}

Create a component that allows only logged-in users:

protected.tsx

import { useAuthUser } from './user-auth-user';
import { Navigate } from "react-router-dom";
import * as React from "react";

export function Protected ({ children }: { children: React.ReactElement }) {
    const { initialized, user } = useAuthUser();
    if (!initialized) {
        return null;
    }
    if (!user) {
        return <Navigate to='login' replace={true} />
    }
    return children;
}

Axios Interceptor to Attach the Access Token

attach-token-interceptor.ts

import { InternalAxiosRequestConfig } from "axios";
import getStore from "./store";

export default function attachTokenInterceptor(
    config : InternalAxiosRequestConfig<any>
) {
    // attach access token if present
    const accessToken = getStore().getTokens()?.accessToken;
    if (accessToken) {
        config.headers.set('Authorization', `Bearer ${accessToken}`);
    }

    return config;
}

Axios Interceptor to Refresh the Access Token

refresh-token-interceptor.ts

import { InternalAxiosRequestConfig } from 'axios';
import getStore from './store';

export default async function refreshTokenInterceptor(
    config : InternalAxiosRequestConfig<any>
) {

    // user is not logged
    const user = getStore().getUser();
    if (!user) {
        return config;
    }

    // refresh access token if expired
    await refreshTokenIfExpired();

    return config;
}

// refresh access token if it has expired
async function refreshTokenIfExpired() {

    // Get current tokens
    const store = getStore();
    const tokens = store.getTokens();

    // Destructure the token properties with default values
    const {
        access_token='',
        refresh_token='',
        access_token_expiration=0,
        refresh_token_expiration=0,
    } = tokens || {};

    // check if access token is expired
    const now = new Date();
    const expiration = new Date(access_token_expiration * 1000);
    const tokenExpired = expiration.getTime() <= now.getTime();

    // token is still valid 
    if (!tokenExpired) {
        return;
    }

    // api call to refresh access token
    const {
        token,
        expiration
    } = await refreshAccessTokenApi();

    // update tokens in store
    await store.setTokens({
        access_token: token,
        access_token_expiration: expiration,
        refresh_token: refresh_token,
        refresh_token_expiration: refresh_token_expiration,
    })
}

Feel free to use and extend the Reactive Auth Store to simplify user authentication and state management in your react application.

1.0.1

8 months ago

1.0.0

10 months ago