Validación de token existente en sessionStorage

This commit is contained in:
Germán Enríquez 2024-07-28 13:17:54 -05:00
parent 9afc3b2bd0
commit 1d6b93c51c
18 changed files with 2044 additions and 1306 deletions

View file

@ -1,6 +1,6 @@
/* eslint-env node */
module.exports = {
module.exports={
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',

2971
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"name": "@chimera-pe/react-saas",
"version": "0.2.1",
"version": "0.3.0",
"type": "module",
"scripts": {
"dev": "vite",
@ -8,42 +8,38 @@
"lint": "eslint src --ext js,jsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.15.11",
"@mui/lab": "^5.0.0-alpha.166",
"@mui/material": "^5.15.11",
"@mui/x-date-pickers": "^6.19.5",
"@reduxjs/toolkit": "^2.2.1",
"axios": "^1.6.7",
"peerDependencies": {
"@emotion/react": "^11.13.0",
"@emotion/styled": "^11.13.0",
"@mui/icons-material": "^5.16.5",
"@mui/lab": "^5.0.0-alpha.171",
"@mui/material": "^5.16.5",
"@mui/x-date-pickers": "^7.11.1",
"@reduxjs/toolkit": "^2.2.7",
"axios": "^1.7.2",
"date-fns": "^2.30.0",
"final-form": "^4.20.10",
"jwt-decode": "^4.0.0",
"lodash": "^4.17.21",
"mui-rff": "^7.3.0",
"mui-rff": "^7.4.1",
"navigator-languages": "^2.0.2",
"node-polyglot": "^2.5.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"node-polyglot": "^2.6.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-final-form": "^6.5.9",
"react-polyglot": "^0.7.2",
"react-redux": "^9.1.0",
"react-router-dom": "^6.22.2"
},
"peerDependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-redux": "^9.1.2",
"react-router-dom": "^6.25.1"
},
"devDependencies": {
"@types/react": "^18.2.56",
"@types/react-dom": "^18.2.19",
"@vitejs/plugin-react": "^4.2.1",
"eslint": "^8.56.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"vite": "^5.1.4"
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
"eslint": "^8.57.0",
"eslint-plugin-react": "^7.35.0",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-refresh": "^0.4.9",
"vite": "^5.3.5"
},
"description": "Componente integrador con SaaS",
"main": "./dist/react-saas.umd.cjs",

View file

@ -1,2 +1,6 @@
export const saasURL="https://saas.chimera.com.pe/backend";
export const authURL="https://saas.chimera.com.pe/oauth";
export const TOKEN="saas-token";
export const TOKEN_EXPIRATION="saas-token-expiration";
export const REFRESH_TOKEN="saas-refresh-token";

View file

@ -1,7 +1,7 @@
import axios from "axios";
import {saasURL} from "../Constantes";
export const identidadApi = (url = saasURL,aplicacion) => axios({
export const identidadApi=(url=saasURL,aplicacion) => axios({
url: `${url}/identidad/`,
params: {
codigoAplicacion: aplicacion

View file

@ -1,7 +1,7 @@
import axios from "axios";
import {authURL} from "../Constantes";
export const loginApi = (url = authURL) => ({
export const loginApi=(url=authURL) => ({
login: (clientCredentials,data) => axios({
url: `${url}/oauth/token`,
headers: {

View file

@ -2,8 +2,8 @@ import {Box,Alert,AlertTitle} from "@mui/material";
import {useTranslate} from "react-polyglot";
import PropTypes from "prop-types";
const ErrorAlert = ({titulo,texto,align = "center",severity = "error"}) => {
const translate = useTranslate();
const ErrorAlert=({titulo,texto,align="center",severity="error"}) => {
const translate=useTranslate();
return (
<Box sx={{

View file

@ -9,10 +9,10 @@ import navigatorLanguages from "navigator-languages";
import {cambiarIdioma} from "../redux";
import saasMessages from "../i18n";
const supportedLocales = {en: enGB,es};
const supportedLocales={en: enGB,es};
const IdiomaInner = ({messages,children}) => {
const idioma = useSelector(store => store.ui.idioma);
const IdiomaInner=({messages,children}) => {
const idioma=useSelector(store => store.ui.idioma);
return (
<I18n locale={idioma} messages={messages[idioma]}>
@ -23,41 +23,41 @@ const IdiomaInner = ({messages,children}) => {
);
};
IdiomaInner.propTypes = {
IdiomaInner.propTypes={
messages: PropTypes.object,
children: PropTypes.element.isRequired
};
const getIdiomaNavegador = () => {
const idiomas = navigatorLanguages();
const getIdiomaNavegador=() => {
const idiomas=navigatorLanguages();
if(!idiomas?.length) {
return undefined;
}
const idioma = idiomas[0];
const idioma=idiomas[0];
if(idioma.indexOf("-")) {
return idioma.substring(0,idioma.indexOf("-"));
}
return idioma;
};
const Idioma = ({messages,idiomaDefecto,children}) => {
const dispatch = useDispatch();
const idiomaNavegador = getIdiomaNavegador();
const Idioma=({messages,idiomaDefecto,children}) => {
const dispatch=useDispatch();
const idiomaNavegador=getIdiomaNavegador();
useEffect(() => {
let idioma = Reflect.ownKeys(messages)[0];
let idioma=Reflect.ownKeys(messages)[0];
if(idiomaNavegador && Object.hasOwn(messages,idiomaNavegador)) {
idioma = idiomaNavegador;
idioma=idiomaNavegador;
}
else if(idiomaDefecto && Object.hasOwn(messages,idiomaDefecto)) {
idioma = idiomaDefecto;
idioma=idiomaDefecto;
}
dispatch(cambiarIdioma(idioma));
},[dispatch,messages,idiomaDefecto,idiomaNavegador]);
const m = {};
const m={};
Object.keys(messages).forEach(key => {
m[key] = {
m[key]={
...messages[key],
saas: saasMessages[key]
};
@ -70,7 +70,7 @@ const Idioma = ({messages,idiomaDefecto,children}) => {
);
};
Idioma.propTypes = {
Idioma.propTypes={
messages: PropTypes.object,
idiomaDefecto: PropTypes.string,
children: PropTypes.element.isRequired

View file

@ -10,8 +10,8 @@ import Idioma from "./Idioma";
import Tema from "./Tema";
import MainRouter from "./MainRouter";
const InicializarInner = ({devURL,basename,children}) => {
const aplicacion = useSelector(store => store.aplicacion);
const InicializarInner=({devURL,basename,children}) => {
const aplicacion=useSelector(store => store.aplicacion);
return (
<Box sx={{
@ -47,7 +47,7 @@ InicializarInner.propTypes={
children: PropTypes.element.isRequired
};
const Inicializar = ({
const Inicializar=({
aplicacion,
devSaasURL,
devAuthURL,
@ -56,7 +56,7 @@ const Inicializar = ({
basename,
children
}) => {
const dispatch = useDispatch();
const dispatch=useDispatch();
useEffect(() => {
dispatch(inicializar({devURL: devSaasURL,aplicacion: aplicacion}));

View file

@ -24,14 +24,14 @@ import {useTranslate} from "react-polyglot";
import {requestToken} from "../redux";
import {useCheckLogin,useNotificar} from "../hooks";
const FormularioLogin = ({devURL}) => {
const dispatch = useDispatch();
const translate = useTranslate();
const notificar = useNotificar();
const {cargando,error} = useSelector(store => store.login);
const instancia = useSelector(store => store.aplicacion.instancia);
const FormularioLogin=({devURL}) => {
const dispatch=useDispatch();
const translate=useTranslate();
const notificar=useNotificar();
const {cargando,error}=useSelector(store => store.login);
const instancia=useSelector(store => store.aplicacion.instancia);
const submit = values => {
const submit=values => {
dispatch(requestToken({
devURL: devURL,
clientCredentials: instancia.clientCredentials,
@ -42,14 +42,14 @@ const FormularioLogin = ({devURL}) => {
}));
};
const validate = values => {
const errors = {correo: undefined,password: undefined};
const validate=values => {
const errors={correo: undefined,password: undefined};
if(!values.correo) {
errors.correo = translate("saas.login.validacion.correo");
errors.correo=translate("saas.login.validacion.correo");
}
if(!values.password) {
errors.password = translate("saas.login.validacion.password");
errors.password=translate("saas.login.validacion.password");
}
return errors;
};
@ -121,16 +121,16 @@ const FormularioLogin = ({devURL}) => {
);
};
FormularioLogin.propTypes = {
FormularioLogin.propTypes={
devURL: PropTypes.string
};
const Login = ({devURL}) => {
const instancia = useSelector(store => store.aplicacion.instancia);
const translate = useTranslate();
const location = useLocation();
const autenticado = useCheckLogin(devURL);
const {from} = location.state || {from: {pathname: "/"}};
const Login=({devURL}) => {
const instancia=useSelector(store => store.aplicacion.instancia);
const translate=useTranslate();
const location=useLocation();
const autenticado=useCheckLogin(devURL);
const {from}=location.state || {from: {pathname: "/"}};
if(!instancia.requiereLogin || autenticado) {
return <Navigate to={from} />;
@ -204,7 +204,7 @@ const Login = ({devURL}) => {
);
};
Login.propTypes = {
Login.propTypes={
devURL: PropTypes.string
};

View file

@ -9,9 +9,9 @@ import {
import {useCheckLogin} from "../hooks";
import Login from "./Login";
const RequiereAuth = ({devURL,redirectTo,children}) => {
const location = useLocation();
const autenticado = useCheckLogin(devURL);
const RequiereAuth=({devURL,redirectTo,children}) => {
const location=useLocation();
const autenticado=useCheckLogin(devURL);
return autenticado ? children : <Navigate to={redirectTo} state={{from: location}} replace />;
};
@ -22,7 +22,7 @@ RequiereAuth.propTypes={
children: PropTypes.element.isRequired
};
const MainRouter = ({
const MainRouter=({
devURL,
requiereLogin,
basename,

View file

@ -3,12 +3,12 @@ import {Provider} from "react-redux";
import {store} from "../redux";
import Inicializar from "./Inicializar";
const SaasApp = ({
const SaasApp=({
customReducers,
aplicacion,
devSaasURL,
devAuthURL,
dev = false,
dev=false,
idiomaDefecto,
messages,
basename,

View file

@ -1,24 +1,45 @@
import {useEffect} from "react";
import {useDispatch,useSelector} from "react-redux";
import {logout,refreshToken} from "../redux";
import {TOKEN,TOKEN_EXPIRATION,REFRESH_TOKEN} from "../Constantes";
import {login,logout,refreshToken} from "../redux";
const useCheckLogin = (devURL) => {
const dispatch = useDispatch();
const login = useSelector(store => store.login);
const instancia = useSelector(store => store.aplicacion.instancia);
const useCheckLogin=(devURL) => {
const dispatch=useDispatch();
const loginStatus=useSelector(store => store.loginStatus);
const instancia=useSelector(store => store.aplicacion.instancia);
useEffect(() => {
if(login.autenticado && !!login.expiracion && new Date(login.expiracion) < new Date()){
if(login.refreshToken){
dispatch(refreshToken(devURL,instancia.clientCredentials,login.refreshToken));
}
else{
dispatch(logout());
if(loginStatus.autenticado){
if(!!loginStatus.expiracion && new Date(loginStatus.expiracion) < new Date()){
if(loginStatus.refreshToken){
dispatch(refreshToken(devURL,instancia.clientCredentials,loginStatus.refreshToken));
}
else{
dispatch(logout());
}
}
}
},[devURL,instancia.clientCredentials,login,dispatch]);
else{
const token=sessionStorage.getItem(TOKEN);
const expiracion=sessionStorage.getItem(TOKEN_EXPIRATION);
const refreshToken=sessionStorage.getItem(REFRESH_TOKEN);
if(token && expiracion){
if(new Date(expiracion) < new Date()){
if(refreshToken){
dispatch(refreshToken(devURL,instancia.clientCredentials,refreshToken));
}
else{
dispatch(logout());
}
}
else{
dispatch(login({token,expiracion,refreshToken}));
}
}
}
},[devURL,instancia.clientCredentials,loginStatus,dispatch]);
return login.autenticado;
return loginStatus.autenticado;
};
export default useCheckLogin;

View file

@ -2,9 +2,9 @@ import {useCallback} from "react";
import {useDispatch} from "react-redux";
import {mostrarNotificacion} from "../redux";
const useNotificar = () => {
const dispatch = useDispatch();
return useCallback((mensaje,tipo = "default") => {
const useNotificar=() => {
const dispatch=useDispatch();
return useCallback((mensaje,tipo="default") => {
dispatch(mostrarNotificacion({mensaje,tipo}));
},[dispatch]);
};

View file

@ -3,6 +3,7 @@ import {inicializar,getInstancia} from "./inicializarSlice";
import {
refreshToken,
requestToken,
login,
logout,
getPerfiles,
getToken,
@ -16,6 +17,7 @@ export {
inicializar,
refreshToken,
requestToken,
login,
logout,
mostrarNotificacion,
ocultarNotificacion,

View file

@ -1,7 +1,7 @@
import {createSlice,createAsyncThunk} from "@reduxjs/toolkit";
import {identidadApi} from "../api/inicializarApi";
const colorDefault = {
const colorDefault={
primary: "#1C6CCC",
secondary: "#17A7FF",
error: "#f44336",
@ -10,7 +10,7 @@ const colorDefault = {
success: "#4caf50"
};
const inicializarSlice = createSlice({
const inicializarSlice=createSlice({
name: "inicializar",
initialState: {
inicializando: true,
@ -23,12 +23,12 @@ const inicializarSlice = createSlice({
extraReducers(builder) {
builder
.addCase(inicializar.pending,state => {
state.inicializando = true;
state.inicializando=true;
})
.addCase(inicializar.fulfilled,(state,action) => {
state.inicializando = false;
state.inicializado = true;
state.instancia = {
state.inicializando=false;
state.inicializado=true;
state.instancia={
...action.payload,
abreviatura: action.payload.nombre.match(/\b([A-Z])/g).join(""),
color: {
@ -36,21 +36,21 @@ const inicializarSlice = createSlice({
...action.payload.color
}
};
state.error = null;
state.error=null;
})
.addCase(inicializar.rejected,(state,action) => {
state.inicializando = false;
state.inicializado = false;
state.instancia = {
state.inicializando=false;
state.inicializado=false;
state.instancia={
color: colorDefault
};
state.error = action.payload;
state.error=action.payload;
});
}
});
export const inicializar = createAsyncThunk("inicializar",async (payload) => {
const response = await identidadApi(payload.devURL,payload.aplicacion);
export const inicializar=createAsyncThunk("inicializar",async (payload) => {
const response=await identidadApi(payload.devURL,payload.aplicacion);
return response.data;
});

View file

@ -1,8 +1,9 @@
import {createSlice,createAsyncThunk} from "@reduxjs/toolkit";
import {loginApi} from "../api/loginApi";
import {TOKEN,TOKEN_EXPIRATION,REFRESH_TOKEN} from "../Constantes";
import {jwtDecode} from "jwt-decode";
const loginSlice = createSlice({
const loginSlice=createSlice({
name: "login",
initialState: {
cargando: false,
@ -15,96 +16,109 @@ const loginSlice = createSlice({
error: null
},
reducers: {
login: (state,action) => {
const token=action.payload.token;
const jwtToken=jwtDecode(token);
const expiracion=new Date();
expiracion.setSeconds(expiracion.getSeconds() + action.payload.expiracion);
state.cargando=false;
state.autenticado=true;
state.token=token;
state.refreshToken=action.payload.refreshToken;
state.expiracion=expiracion.getTime();
state.usuario=jwtToken.name;
state.perfiles=jwtToken.authorities;
},
logout: state => {
state.cargando = false;
state.autenticado = false;
state.token = null;
state.refreshToken = null;
state.expiracion = null;
state.usuario = null;
state.perfiles = [];
state.error = null;
sessionStorage.removeItem("saas-token");
sessionStorage.removeItem("saas-refresh-token");
sessionStorage.removeItem("saas-expiration");
state.cargando=false;
state.autenticado=false;
state.token=null;
state.refreshToken=null;
state.expiracion=null;
state.usuario=null;
state.perfiles=[];
state.error=null;
sessionStorage.removeItem(TOKEN);
sessionStorage.removeItem(REFRESH_TOKEN);
sessionStorage.removeItem(TOKEN_EXPIRATION);
}
},
extraReducers(builder) {
builder
.addCase(requestToken.pending,state => {
state.cargando = true;
state.error = null;
state.cargando=true;
state.error=null;
})
.addCase(requestToken.fulfilled,(state,action) => {
const token = action.payload.access_token;
const jwtToken = jwtDecode(token);
const expiracion = new Date();
const token=action.payload.access_token;
const jwtToken=jwtDecode(token);
const expiracion=new Date();
expiracion.setSeconds(expiracion.getSeconds() + action.payload.expires_in);
state.cargando = false;
state.autenticado = true;
state.token = token;
state.refreshToken = action.payload.refresh_token;
state.expiracion = expiracion.getTime();
state.usuario = jwtToken.name;
state.perfiles = jwtToken.authorities;
sessionStorage.setItem("saas-token",token);
sessionStorage.setItem("saas-refresh-token",action.payload.refresh_token);
sessionStorage.setItem("saas-expiration",expiracion.getTime());
state.cargando=false;
state.autenticado=true;
state.token=token;
state.refreshToken=action.payload.refresh_token;
state.expiracion=expiracion.getTime();
state.usuario=jwtToken.name;
state.perfiles=jwtToken.authorities;
sessionStorage.setItem(TOKEN,token);
sessionStorage.setItem(REFRESH_TOKEN,action.payload.refresh_token);
sessionStorage.setItem(TOKEN_EXPIRATION,expiracion.getTime());
})
.addCase(requestToken.rejected,(state,action) => {
console.log(action);
state.cargando = false;
state.autenticado = false;
state.token = null;
state.refreshToken = null;
state.expiracion = null;
state.usuario = null;
state.perfiles = [];
state.error = action.error?.message;
sessionStorage.removeItem("saas-token");
sessionStorage.removeItem("saas-refresh-token");
sessionStorage.removeItem("saas-expiration");
state.cargando=false;
state.autenticado=false;
state.token=null;
state.refreshToken=null;
state.expiracion=null;
state.usuario=null;
state.perfiles=[];
state.error=action.error?.message;
sessionStorage.removeItem(TOKEN);
sessionStorage.removeItem(REFRESH_TOKEN);
sessionStorage.removeItem(TOKEN_EXPIRATION);
})
.addCase(refreshToken.pending,state => {
state.cargando = true;
state.cargando=true;
})
.addCase(refreshToken.fulfilled,(state,action) => {
const expiracion = new Date();
const expiracion=new Date();
expiracion.setSeconds(expiracion.getSeconds() + action.payload.expires_in);
state.token = action.payload.access_token;
state.refreshToken = action.payload.refresh_token;
state.expiracion = expiracion.getTime();
sessionStorage.setItem("saas-token",action.payload.access_token);
sessionStorage.setItem("saas-refresh-token",action.payload.refresh_token);
sessionStorage.setItem("saas-expiration",expiracion.getTime());
state.token=action.payload.access_token;
state.refreshToken=action.payload.refresh_token;
state.expiracion=expiracion.getTime();
sessionStorage.setItem(TOKEN,action.payload.access_token);
sessionStorage.setItem(REFRESH_TOKEN,action.payload.refresh_token);
sessionStorage.setItem(TOKEN_EXPIRATION,expiracion.getTime());
})
.addCase(refreshToken.rejected,(state,action) => {
state.cargando = false;
state.autenticado = false;
state.token = null;
state.refreshToken = null;
state.expiracion = null;
state.usuario = null;
state.perfiles = [];
state.error = action.error?.message;
sessionStorage.removeItem("saas-token");
sessionStorage.removeItem("saas-refresh-token");
sessionStorage.removeItem("saas-expiration");
state.cargando=false;
state.autenticado=false;
state.token=null;
state.refreshToken=null;
state.expiracion=null;
state.usuario=null;
state.perfiles=[];
state.error=action.error?.message;
sessionStorage.removeItem(TOKEN);
sessionStorage.removeItem(REFRESH_TOKEN);
sessionStorage.removeItem(TOKEN_EXPIRATION);
});
}
});
export const requestToken = createAsyncThunk("login/requestToken",async (payload) => {
const response = await loginApi(payload.devURL).login(payload.clientCredentials,payload.data);
export const requestToken=createAsyncThunk("login/requestToken",async (payload) => {
const response=await loginApi(payload.devURL).login(payload.clientCredentials,payload.data);
return response.data;
});
export const refreshToken = createAsyncThunk("login/refreshToken",async (devURL,clientCredentials,refreshToken) => {
const response = await loginApi(devURL).refreshToken(clientCredentials,refreshToken);
export const refreshToken=createAsyncThunk("login/refreshToken",async (devURL,clientCredentials,refreshToken) => {
const response=await loginApi(devURL).refreshToken(clientCredentials,refreshToken);
return response.data;
});
export const {logout} = loginSlice.actions;
export const {login,logout}=loginSlice.actions;
export const getToken=state => state.login.token;

View file

@ -19,7 +19,7 @@ const getMessage=(message,messageArgs,value,values) => (
const memoize=(fn) => lodashMemoize(fn,(...args) => JSON.stringify(args));
export const composeValidators=(...validators) => (value,values,meta) => {
const allValidators = Array.isArray(validators[0]) ? validators[0] : validators;
const allValidators=Array.isArray(validators[0]) ? validators[0] : validators;
return allValidators.reduce(
(error,validator) =>
error ||