@chimera-pe/react-saas 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.cjs +20 -0
- package/package.json +59 -0
- package/src/Constantes.js +2 -0
- package/src/api/inicializarApi.jsx +9 -0
- package/src/api/loginApi.jsx +30 -0
- package/src/components/Cargando.jsx +15 -0
- package/src/components/Error.jsx +31 -0
- package/src/components/Idioma.jsx +48 -0
- package/src/components/Inicializar.jsx +64 -0
- package/src/components/Login.jsx +202 -0
- package/src/components/MainRouter.jsx +35 -0
- package/src/components/Notificacion.jsx +36 -0
- package/src/components/SaasApp.jsx +30 -0
- package/src/components/Tema.jsx +59 -0
- package/src/components/index.jsx +9 -0
- package/src/hooks/index.jsx +7 -0
- package/src/hooks/useCheckLogin.jsx +24 -0
- package/src/hooks/useNotificar.jsx +12 -0
- package/src/i18n/es.jsx +22 -0
- package/src/i18n/index.jsx +7 -0
- package/src/index.js +12 -0
- package/src/redux/index.jsx +17 -0
- package/src/redux/inicializarSlice.jsx +57 -0
- package/src/redux/loginSlice.jsx +94 -0
- package/src/redux/notificacionSlice.jsx +20 -0
- package/src/redux/store.jsx +15 -0
- package/src/redux/uiSlice.jsx +24 -0
- package/vite.config.js +27 -0
package/.eslintrc.cjs
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/* eslint-env node */
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
env: { browser: true, es2020: true },
|
|
5
|
+
extends: [
|
|
6
|
+
'eslint:recommended',
|
|
7
|
+
'plugin:react/recommended',
|
|
8
|
+
'plugin:react/jsx-runtime',
|
|
9
|
+
'plugin:react-hooks/recommended',
|
|
10
|
+
],
|
|
11
|
+
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
|
|
12
|
+
settings: { react: { version: '18.2' } },
|
|
13
|
+
plugins: ['react-refresh'],
|
|
14
|
+
rules: {
|
|
15
|
+
'react-refresh/only-export-components': [
|
|
16
|
+
'warn',
|
|
17
|
+
{ allowConstantExport: true },
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@chimera-pe/react-saas",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"lint": "eslint src --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
|
10
|
+
"preview": "vite preview"
|
|
11
|
+
},
|
|
12
|
+
"peerDependencies": {
|
|
13
|
+
"@reduxjs/toolkit": ">=1.9",
|
|
14
|
+
"react": ">=18"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@emotion/react": "^11.11.1",
|
|
18
|
+
"@emotion/styled": "^11.11.0",
|
|
19
|
+
"@mui/icons-material": "^5.14.1",
|
|
20
|
+
"@mui/lab": "^5.0.0-alpha.137",
|
|
21
|
+
"@mui/material": "^5.14.1",
|
|
22
|
+
"@reduxjs/toolkit": "^1.9.5",
|
|
23
|
+
"@types/react": "^18.2.14",
|
|
24
|
+
"@types/react-dom": "^18.2.6",
|
|
25
|
+
"@vitejs/plugin-react": "^4.0.1",
|
|
26
|
+
"axios": "^1.4.0",
|
|
27
|
+
"date-fns": "^2.30.0",
|
|
28
|
+
"eslint": "^8.44.0",
|
|
29
|
+
"eslint-plugin-react": "^7.32.2",
|
|
30
|
+
"eslint-plugin-react-hooks": "^4.6.0",
|
|
31
|
+
"eslint-plugin-react-refresh": "^0.4.1",
|
|
32
|
+
"jwt-decode": "^3.1.2",
|
|
33
|
+
"mui-rff": "^6.1.4",
|
|
34
|
+
"react": "^18.2.0",
|
|
35
|
+
"react-dom": "^18.2.0",
|
|
36
|
+
"react-polyglot": "^0.7.2",
|
|
37
|
+
"react-redux": "^8.1.1",
|
|
38
|
+
"react-router-dom": "^6.14.2",
|
|
39
|
+
"vite": "^4.4.0"
|
|
40
|
+
},
|
|
41
|
+
"description": "Componente integrador con SaaS",
|
|
42
|
+
"main": "./dist/react-saas.umd.js",
|
|
43
|
+
"module": "./dist/react-saas.es.js",
|
|
44
|
+
"exports": {
|
|
45
|
+
".": {
|
|
46
|
+
"import": "./dist/react-saas.es.js",
|
|
47
|
+
"export": "./dist/react-saas.umd.js"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"repository": {
|
|
51
|
+
"type": "git",
|
|
52
|
+
"url": "https://git.chimera.com.pe/chimera/react-saas.git"
|
|
53
|
+
},
|
|
54
|
+
"keywords": [
|
|
55
|
+
"saas"
|
|
56
|
+
],
|
|
57
|
+
"author": "Germán Enríquez",
|
|
58
|
+
"license": "ISC"
|
|
59
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import {authURL} from "../Constantes";
|
|
3
|
+
|
|
4
|
+
export const loginApi = (url = authURL) => ({
|
|
5
|
+
login: (clientCredentials,data) => axios({
|
|
6
|
+
url: `${url}/oauth/token`,
|
|
7
|
+
headers: {
|
|
8
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
9
|
+
Authorization: `Basic ${clientCredentials}`
|
|
10
|
+
},
|
|
11
|
+
method: "post",
|
|
12
|
+
data: {
|
|
13
|
+
username: data.correo,
|
|
14
|
+
password: data.password,
|
|
15
|
+
"grant_type": "password"
|
|
16
|
+
}
|
|
17
|
+
}),
|
|
18
|
+
refreshToken: (clientCredentials,refreshToken) => axios({
|
|
19
|
+
url: `${url}/oauth/token`,
|
|
20
|
+
headers: {
|
|
21
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
22
|
+
Authorization: `Basic ${clientCredentials}`
|
|
23
|
+
},
|
|
24
|
+
method: "post",
|
|
25
|
+
data: {
|
|
26
|
+
"refresh_token": refreshToken,
|
|
27
|
+
"grant_type": "refresh_token"
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {Box,CircularProgress} from "@mui/material";
|
|
2
|
+
|
|
3
|
+
const Cargando=() => (
|
|
4
|
+
<Box sx={{
|
|
5
|
+
display: "flex",
|
|
6
|
+
flexDirection: "column",
|
|
7
|
+
flexGrow: 1,
|
|
8
|
+
justifyContent: "center",
|
|
9
|
+
alignItems: "center"
|
|
10
|
+
}}>
|
|
11
|
+
<CircularProgress />
|
|
12
|
+
</Box>
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
export default Cargando;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import {Box,Alert,AlertTitle} from "@mui/material";
|
|
2
|
+
import {useTranslate} from "react-polyglot";
|
|
3
|
+
import PropTypes from "prop-types";
|
|
4
|
+
|
|
5
|
+
const Error = ({titulo,texto,align = "center",severity = "error"}) => {
|
|
6
|
+
const translate = useTranslate();
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<Box sx={{
|
|
10
|
+
display: "flex",
|
|
11
|
+
flexDirection: "column",
|
|
12
|
+
flexGrow: 1,
|
|
13
|
+
justifyContent: "center",
|
|
14
|
+
alignItems: align
|
|
15
|
+
}}>
|
|
16
|
+
<Alert severity={severity}>
|
|
17
|
+
<AlertTitle>{translate(titulo)}</AlertTitle>
|
|
18
|
+
{texto && translate(texto)}
|
|
19
|
+
</Alert>
|
|
20
|
+
</Box>
|
|
21
|
+
);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
Error.propTypes={
|
|
25
|
+
titulo: PropTypes.string.isRequired,
|
|
26
|
+
texto: PropTypes.string,
|
|
27
|
+
align: PropTypes.string,
|
|
28
|
+
severity: PropTypes.string
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export default Error;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {useEffect} from "react";
|
|
2
|
+
import {useDispatch,useSelector} from "react-redux";
|
|
3
|
+
import {I18n} from "react-polyglot";
|
|
4
|
+
import {enGB,es} from "date-fns/locale"
|
|
5
|
+
import {LocalizationProvider} from "@mui/x-date-pickers";
|
|
6
|
+
import {AdapterDateFns} from "@mui/x-date-pickers/AdapterDateFns";
|
|
7
|
+
import {cambiarIdioma} from "../redux";
|
|
8
|
+
import saasMessages from "../i18n";
|
|
9
|
+
|
|
10
|
+
const supportedLocales={en: enGB,es};
|
|
11
|
+
|
|
12
|
+
const IdiomaInner=({messages,children}) => {
|
|
13
|
+
const idioma=useSelector(store => store.ui.idioma);
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<I18n locale={idioma} messages={messages[idioma]}>
|
|
17
|
+
<LocalizationProvider dateAdapter={AdapterDateFns} adapterLocale={supportedLocales[idioma]}>
|
|
18
|
+
{children}
|
|
19
|
+
</LocalizationProvider>
|
|
20
|
+
</I18n>
|
|
21
|
+
);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const Idioma=({messages,idioma,children}) => {
|
|
25
|
+
const dispatch=useDispatch();
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if(idioma){
|
|
29
|
+
dispatch(cambiarIdioma(idioma));
|
|
30
|
+
}
|
|
31
|
+
},[dispatch,idioma]);
|
|
32
|
+
|
|
33
|
+
const m={};
|
|
34
|
+
Object.keys(messages).forEach(key => {
|
|
35
|
+
m[key]={
|
|
36
|
+
...messages[key],
|
|
37
|
+
saas: saasMessages[key]
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<IdiomaInner messages={m}>
|
|
43
|
+
{children}
|
|
44
|
+
</IdiomaInner>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export default Idioma;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import {useEffect} from "react";
|
|
2
|
+
import {Box} from "@mui/material";
|
|
3
|
+
import {useDispatch,useSelector} from "react-redux";
|
|
4
|
+
import {inicializar} from "../redux";
|
|
5
|
+
import Cargando from "./Cargando";
|
|
6
|
+
import Error from "./Error";
|
|
7
|
+
import Notificacion from "./Notificacion";
|
|
8
|
+
import Idioma from "./Idioma";
|
|
9
|
+
import Tema from "./Tema";
|
|
10
|
+
import MainRouter from "./MainRouter";
|
|
11
|
+
|
|
12
|
+
const InicializarInner = ({devURL,children}) => {
|
|
13
|
+
const aplicacion = useSelector(store => store.aplicacion);
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<Box sx={{
|
|
17
|
+
display: "flex",
|
|
18
|
+
flexDirection: "column",
|
|
19
|
+
minHeight: "100vh",
|
|
20
|
+
justifyContent: "flex-start",
|
|
21
|
+
backgroundColor: "background.default"
|
|
22
|
+
}}>
|
|
23
|
+
{aplicacion.inicializando ?
|
|
24
|
+
<Cargando />
|
|
25
|
+
: (aplicacion.error || !aplicacion.inicializado) ?
|
|
26
|
+
<Error titulo={"saas.inicializar.error.titulo"} texto={"saas.inicializar.error.mensaje"} />
|
|
27
|
+
:
|
|
28
|
+
<>
|
|
29
|
+
<MainRouter devURL={devURL} requiereLogin={aplicacion.instancia.requiereLogin}>
|
|
30
|
+
{children}
|
|
31
|
+
</MainRouter>
|
|
32
|
+
<Notificacion />
|
|
33
|
+
</>
|
|
34
|
+
}
|
|
35
|
+
</Box>
|
|
36
|
+
);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const Inicializar = ({
|
|
40
|
+
aplicacion,
|
|
41
|
+
devSaasURL,
|
|
42
|
+
devAuthURL,
|
|
43
|
+
messages,
|
|
44
|
+
idioma,
|
|
45
|
+
children
|
|
46
|
+
}) => {
|
|
47
|
+
const dispatch = useDispatch();
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
dispatch(inicializar({devURL: devSaasURL,aplicacion: aplicacion}));
|
|
51
|
+
},[dispatch,aplicacion,devSaasURL]);
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Idioma messages={messages} idioma={idioma}>
|
|
55
|
+
<Tema>
|
|
56
|
+
<InicializarInner devURL={devAuthURL}>
|
|
57
|
+
{children}
|
|
58
|
+
</InicializarInner>
|
|
59
|
+
</Tema>
|
|
60
|
+
</Idioma>
|
|
61
|
+
);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export default Inicializar;
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import {useEffect} from "react";
|
|
2
|
+
import {useSelector,useDispatch} from "react-redux";
|
|
3
|
+
import {Navigate,useLocation} from "react-router-dom";
|
|
4
|
+
import {
|
|
5
|
+
Box,
|
|
6
|
+
Container,
|
|
7
|
+
Grid,
|
|
8
|
+
Typography,
|
|
9
|
+
Card,
|
|
10
|
+
CardHeader,
|
|
11
|
+
CardContent,
|
|
12
|
+
InputAdornment,
|
|
13
|
+
Button,
|
|
14
|
+
CircularProgress,
|
|
15
|
+
AppBar,
|
|
16
|
+
Toolbar,
|
|
17
|
+
Link
|
|
18
|
+
} from "@mui/material";
|
|
19
|
+
import {Email,Lock} from "@mui/icons-material";
|
|
20
|
+
import {Form} from "react-final-form";
|
|
21
|
+
import {TextField} from "mui-rff";
|
|
22
|
+
import {useTranslate} from "react-polyglot";
|
|
23
|
+
import {requestToken} from "../redux";
|
|
24
|
+
import {useCheckLogin,useNotificar} from "../hooks";
|
|
25
|
+
|
|
26
|
+
const FormularioLogin = ({devURL}) => {
|
|
27
|
+
const dispatch = useDispatch();
|
|
28
|
+
const translate = useTranslate();
|
|
29
|
+
const notificar = useNotificar();
|
|
30
|
+
const {cargando,error} = useSelector(store => store.login);
|
|
31
|
+
const instancia = useSelector(store => store.aplicacion.instancia);
|
|
32
|
+
|
|
33
|
+
const submit = values => {
|
|
34
|
+
dispatch(requestToken({
|
|
35
|
+
devURL: devURL,
|
|
36
|
+
clientCredentials: instancia.clientCredentials,
|
|
37
|
+
data: {
|
|
38
|
+
correo: values.correo,
|
|
39
|
+
password: values.password
|
|
40
|
+
}
|
|
41
|
+
}));
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const validate = values => {
|
|
45
|
+
const errors = {correo: undefined,password: undefined};
|
|
46
|
+
|
|
47
|
+
if(!values.correo) {
|
|
48
|
+
errors.correo = translate("saas.login.validacion.correo");
|
|
49
|
+
}
|
|
50
|
+
if(!values.password) {
|
|
51
|
+
errors.password = translate("saas.login.validacion.password");
|
|
52
|
+
}
|
|
53
|
+
return errors;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if(error) {
|
|
58
|
+
notificar("saas.login.error","error");
|
|
59
|
+
}
|
|
60
|
+
},[notificar,error]);
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<Form
|
|
64
|
+
onSubmit={submit}
|
|
65
|
+
validate={validate}
|
|
66
|
+
render={({handleSubmit}) => (
|
|
67
|
+
<form onSubmit={handleSubmit}>
|
|
68
|
+
<TextField
|
|
69
|
+
id="correo"
|
|
70
|
+
name="correo"
|
|
71
|
+
label={translate("saas.login.correo")}
|
|
72
|
+
variant="outlined"
|
|
73
|
+
autoComplete="off"
|
|
74
|
+
disabled={cargando}
|
|
75
|
+
autoFocus
|
|
76
|
+
InputProps={{
|
|
77
|
+
startAdornment: (
|
|
78
|
+
<InputAdornment position="start">
|
|
79
|
+
<Email color="primary" />
|
|
80
|
+
</InputAdornment>
|
|
81
|
+
)
|
|
82
|
+
}}
|
|
83
|
+
/>
|
|
84
|
+
<TextField
|
|
85
|
+
id="password"
|
|
86
|
+
name="password"
|
|
87
|
+
type="password"
|
|
88
|
+
label={translate("saas.login.password")}
|
|
89
|
+
variant="outlined"
|
|
90
|
+
autoComplete="current-password"
|
|
91
|
+
disabled={cargando}
|
|
92
|
+
InputProps={{
|
|
93
|
+
startAdornment: (
|
|
94
|
+
<InputAdornment position="start">
|
|
95
|
+
<Lock color="primary" />
|
|
96
|
+
</InputAdornment>
|
|
97
|
+
)
|
|
98
|
+
}}
|
|
99
|
+
/>
|
|
100
|
+
<Grid container>
|
|
101
|
+
<Grid item xs={6}></Grid>
|
|
102
|
+
<Grid item xs={6} align="right">
|
|
103
|
+
<Button
|
|
104
|
+
variant="contained"
|
|
105
|
+
color="primary"
|
|
106
|
+
type="submit"
|
|
107
|
+
disabled={cargando}
|
|
108
|
+
>
|
|
109
|
+
{cargando ?
|
|
110
|
+
<CircularProgress size={24} thickness={4} />
|
|
111
|
+
:
|
|
112
|
+
translate("saas.login.ingresar")
|
|
113
|
+
}
|
|
114
|
+
</Button>
|
|
115
|
+
</Grid>
|
|
116
|
+
</Grid>
|
|
117
|
+
</form>
|
|
118
|
+
)}
|
|
119
|
+
/>
|
|
120
|
+
);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const Login = ({devURL}) => {
|
|
124
|
+
const instancia = useSelector(store => store.aplicacion.instancia);
|
|
125
|
+
const translate = useTranslate();
|
|
126
|
+
const location = useLocation();
|
|
127
|
+
const autenticado = useCheckLogin(devURL);
|
|
128
|
+
const {from} = location.state || {from: {pathname: "/"}};
|
|
129
|
+
|
|
130
|
+
if(!instancia.requiereLogin || autenticado) {
|
|
131
|
+
return <Navigate to={from} />;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<Box sx={{
|
|
136
|
+
position: "relative",
|
|
137
|
+
display: "flex",
|
|
138
|
+
flexDirection: "column",
|
|
139
|
+
flexGrow: 1,
|
|
140
|
+
minHeight: "100vh",
|
|
141
|
+
backgroundColor: "primary.main",
|
|
142
|
+
"&::before": {
|
|
143
|
+
content: '""',
|
|
144
|
+
backgroundImage: `url(${instancia.logo})`,
|
|
145
|
+
backgroundSize: "contain",
|
|
146
|
+
backgroundRepeat: "no-repeat",
|
|
147
|
+
backgroundPosition: "right",
|
|
148
|
+
position: "absolute",
|
|
149
|
+
top: "20%",
|
|
150
|
+
bottom: 0,
|
|
151
|
+
right: 0,
|
|
152
|
+
width: "50%",
|
|
153
|
+
opacity: 0.02,
|
|
154
|
+
filter: "grayscale(100%)"
|
|
155
|
+
}
|
|
156
|
+
}}>
|
|
157
|
+
<Box sx={{
|
|
158
|
+
display: "flex",
|
|
159
|
+
justifyContent: "center",
|
|
160
|
+
alignItems: "center",
|
|
161
|
+
flexGrow: 1
|
|
162
|
+
}}>
|
|
163
|
+
<Container maxWidth="md">
|
|
164
|
+
<Grid container spacing={3}>
|
|
165
|
+
<Grid item xs={12} lg={5} align="center">
|
|
166
|
+
<img src={instancia.logo} alt={instancia.nombre} style={{maxWidth: "100%"}} />
|
|
167
|
+
<Typography variant="h3" align="center">{translate("aplicacion.nombre",{smart_count: 1})}</Typography>
|
|
168
|
+
<Typography variant="h5" align="center">{instancia.nombre}</Typography>
|
|
169
|
+
</Grid>
|
|
170
|
+
<Grid item xs={12} lg={7} sx={{
|
|
171
|
+
display: "flex",
|
|
172
|
+
alignItems: "center",
|
|
173
|
+
justifyContent: "center",
|
|
174
|
+
zIndex: 5
|
|
175
|
+
}}>
|
|
176
|
+
<Card elevation={5}>
|
|
177
|
+
<CardHeader title={translate("saas.login.titulo")} titleTypographyProps={{align: "center"}} />
|
|
178
|
+
<CardContent sx={{
|
|
179
|
+
"& .MuiTextField-root": {
|
|
180
|
+
mb: 2
|
|
181
|
+
},
|
|
182
|
+
}}>
|
|
183
|
+
<FormularioLogin devURL={devURL} />
|
|
184
|
+
</CardContent>
|
|
185
|
+
</Card>
|
|
186
|
+
</Grid>
|
|
187
|
+
</Grid>
|
|
188
|
+
</Container>
|
|
189
|
+
</Box>
|
|
190
|
+
<AppBar position="static" color="primary">
|
|
191
|
+
<Toolbar sx={{justifyContent: "center"}}>
|
|
192
|
+
<Typography variant="caption">
|
|
193
|
+
{translate("saas.copy")}
|
|
194
|
+
<Link href="//chimera.com.pe" color="inherit" target="_blank" rel="noreferrer">Chimera Software</Link>
|
|
195
|
+
</Typography>
|
|
196
|
+
</Toolbar>
|
|
197
|
+
</AppBar>
|
|
198
|
+
</Box>
|
|
199
|
+
);
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
export default Login;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BrowserRouter,
|
|
3
|
+
Routes,
|
|
4
|
+
Route,
|
|
5
|
+
Navigate,
|
|
6
|
+
useLocation
|
|
7
|
+
} from "react-router-dom";
|
|
8
|
+
import {useCheckLogin} from "../hooks";
|
|
9
|
+
import Login from "./Login";
|
|
10
|
+
|
|
11
|
+
const RequiereAuth = ({devURL,redirectTo,children}) => {
|
|
12
|
+
const location = useLocation();
|
|
13
|
+
const autenticado = useCheckLogin(devURL);
|
|
14
|
+
|
|
15
|
+
return autenticado ? children : <Navigate to={redirectTo} state={{from: location}} replace />;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const MainRouter = ({devURL,requiereLogin,children}) => (
|
|
19
|
+
<BrowserRouter>
|
|
20
|
+
<Routes>
|
|
21
|
+
<Route path="/login" element={<Login devURL={devURL} />} />
|
|
22
|
+
<Route
|
|
23
|
+
path="/*"
|
|
24
|
+
element={requiereLogin ? (
|
|
25
|
+
<RequiereAuth devURL={devURL} redirectTo="/login">
|
|
26
|
+
{children}
|
|
27
|
+
</RequiereAuth>
|
|
28
|
+
) : children
|
|
29
|
+
}
|
|
30
|
+
/>
|
|
31
|
+
</Routes>
|
|
32
|
+
</BrowserRouter>
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
export default MainRouter;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {useEffect,useState} from "react";
|
|
2
|
+
import {useSelector,useDispatch} from "react-redux";
|
|
3
|
+
import {Snackbar,Alert} from "@mui/material";
|
|
4
|
+
import {useTranslate} from "react-polyglot";
|
|
5
|
+
import {ocultarNotificacion} from "../redux";
|
|
6
|
+
|
|
7
|
+
const Notificacion=() => {
|
|
8
|
+
const [open,setOpen]=useState(false);
|
|
9
|
+
const dispatch=useDispatch();
|
|
10
|
+
const translate=useTranslate();
|
|
11
|
+
const notificacion=useSelector(store => store.notificaciones[0]);
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
setOpen(!!notificacion);
|
|
15
|
+
},[notificacion]);
|
|
16
|
+
|
|
17
|
+
const handleClose=() => {
|
|
18
|
+
setOpen(false);
|
|
19
|
+
dispatch(ocultarNotificacion());
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<Snackbar
|
|
24
|
+
open={open}
|
|
25
|
+
message={notificacion && notificacion.mensaje && notificacion.tipo === "default" && translate(notificacion.mensaje)}
|
|
26
|
+
autoHideDuration={5000}
|
|
27
|
+
onClose={handleClose}
|
|
28
|
+
>
|
|
29
|
+
{notificacion && notificacion.mensaje && notificacion.tipo !== "default" &&
|
|
30
|
+
<Alert severity={notificacion.tipo}>{translate(notificacion.mensaje)}</Alert>
|
|
31
|
+
}
|
|
32
|
+
</Snackbar>
|
|
33
|
+
);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export default Notificacion;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import {Provider} from "react-redux";
|
|
2
|
+
import {store} from "../redux";
|
|
3
|
+
import Inicializar from "./Inicializar";
|
|
4
|
+
|
|
5
|
+
const SaasApp = ({
|
|
6
|
+
customReducers,
|
|
7
|
+
aplicacion,
|
|
8
|
+
devSaasURL,
|
|
9
|
+
devAuthURL,
|
|
10
|
+
dev = false,
|
|
11
|
+
idioma,
|
|
12
|
+
messages,
|
|
13
|
+
children
|
|
14
|
+
}) => {
|
|
15
|
+
return (
|
|
16
|
+
<Provider store={store(customReducers)}>
|
|
17
|
+
<Inicializar
|
|
18
|
+
aplicacion={aplicacion}
|
|
19
|
+
devSaasURL={dev && devSaasURL}
|
|
20
|
+
devAuthURL={dev && devAuthURL}
|
|
21
|
+
idioma={idioma}
|
|
22
|
+
messages={messages}
|
|
23
|
+
>
|
|
24
|
+
{children}
|
|
25
|
+
</Inicializar>
|
|
26
|
+
</Provider>
|
|
27
|
+
);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export default SaasApp;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {useMemo,useEffect} from "react";
|
|
2
|
+
import {useDispatch,useSelector} from "react-redux";
|
|
3
|
+
import {CssBaseline,useMediaQuery} from "@mui/material";
|
|
4
|
+
import {ThemeProvider,createTheme} from "@mui/material/styles";
|
|
5
|
+
import {grey} from "@mui/material/colors";
|
|
6
|
+
import {cambiarTema} from "../redux";
|
|
7
|
+
|
|
8
|
+
const Tema=({children}) => {
|
|
9
|
+
const dispatch=useDispatch();
|
|
10
|
+
const {instancia}=useSelector(store => store.aplicacion);
|
|
11
|
+
const tema=useSelector(store => store.ui.tema);
|
|
12
|
+
const prefersDarkMode=useMediaQuery("(prefers-color-scheme: dark)");
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
dispatch(cambiarTema(prefersDarkMode ? "dark" : "light"));
|
|
16
|
+
},[dispatch,prefersDarkMode]);
|
|
17
|
+
|
|
18
|
+
const theme=useMemo(() => createTheme({
|
|
19
|
+
palette: {
|
|
20
|
+
mode: tema,
|
|
21
|
+
primary: {
|
|
22
|
+
main: instancia.color.primary
|
|
23
|
+
},
|
|
24
|
+
secondary: {
|
|
25
|
+
main: instancia.color.secondary
|
|
26
|
+
},
|
|
27
|
+
error: {
|
|
28
|
+
main: instancia.color.error
|
|
29
|
+
},
|
|
30
|
+
warning: {
|
|
31
|
+
main: instancia.color.warning
|
|
32
|
+
},
|
|
33
|
+
info: {
|
|
34
|
+
main: instancia.color.info
|
|
35
|
+
},
|
|
36
|
+
success: {
|
|
37
|
+
main: instancia.color.success
|
|
38
|
+
},
|
|
39
|
+
...(tema === "light" && {
|
|
40
|
+
background: {
|
|
41
|
+
default: grey["A200"],
|
|
42
|
+
paper: grey[100]
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
},
|
|
46
|
+
shape: {
|
|
47
|
+
borderRadius: 6
|
|
48
|
+
}
|
|
49
|
+
}),[tema,instancia]);
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<ThemeProvider theme={theme}>
|
|
53
|
+
<CssBaseline />
|
|
54
|
+
{children}
|
|
55
|
+
</ThemeProvider>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export default Tema;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {useEffect} from "react";
|
|
2
|
+
import {useDispatch,useSelector} from "react-redux";
|
|
3
|
+
import {logout, refreshToken} from "../redux";
|
|
4
|
+
|
|
5
|
+
const useCheckLogin = (devURL) => {
|
|
6
|
+
const dispatch = useDispatch();
|
|
7
|
+
const login = useSelector(store => store.login);
|
|
8
|
+
const instancia = useSelector(store => store.aplicacion.instancia);
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if(login.autenticado && !!login.expiracion && new Date(login.expiracion) < new Date()){
|
|
12
|
+
if(login.refreshToken){
|
|
13
|
+
dispatch(refreshToken(devURL,instancia.clientCredentials,login.refreshToken));
|
|
14
|
+
}
|
|
15
|
+
else{
|
|
16
|
+
dispatch(logout());
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
},[devURL,instancia.clientCredentials,login,dispatch]);
|
|
20
|
+
|
|
21
|
+
return login.autenticado;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default useCheckLogin;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import {useCallback} from "react";
|
|
2
|
+
import {useDispatch} from "react-redux";
|
|
3
|
+
import {mostrarNotificacion} from "../redux";
|
|
4
|
+
|
|
5
|
+
const useNotificar = () => {
|
|
6
|
+
const dispatch = useDispatch();
|
|
7
|
+
return useCallback((mensaje,tipo = "default") => {
|
|
8
|
+
dispatch(mostrarNotificacion({mensaje,tipo}));
|
|
9
|
+
},[dispatch]);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default useNotificar;
|
package/src/i18n/es.jsx
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const es={
|
|
2
|
+
inicializar: {
|
|
3
|
+
error: {
|
|
4
|
+
titulo: "Error iniciando aplicación",
|
|
5
|
+
mensaje: "No fue posible conectarnos con el servicio"
|
|
6
|
+
}
|
|
7
|
+
},
|
|
8
|
+
login: {
|
|
9
|
+
titulo: "Acceder al sistema",
|
|
10
|
+
correo: "Correo electrónico",
|
|
11
|
+
password: "Contraseña",
|
|
12
|
+
ingresar: "Acceder",
|
|
13
|
+
validacion: {
|
|
14
|
+
correo: "Debe ingresar una dirección de correo",
|
|
15
|
+
password: "Debe ingresar una contraseña"
|
|
16
|
+
},
|
|
17
|
+
error: "Usuario o contraseña incorrectos"
|
|
18
|
+
},
|
|
19
|
+
copy: "Todos los derechos reservados "
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default es;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {store} from "./store";
|
|
2
|
+
import {inicializar} from "./inicializarSlice";
|
|
3
|
+
import {refreshToken,requestToken,logout} from "./loginSlice";
|
|
4
|
+
import {mostrarNotificacion,ocultarNotificacion} from "./notificacionSlice";
|
|
5
|
+
import {cambiarIdioma,cambiarTema} from "./uiSlice";
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
store,
|
|
9
|
+
inicializar,
|
|
10
|
+
refreshToken,
|
|
11
|
+
requestToken,
|
|
12
|
+
logout,
|
|
13
|
+
mostrarNotificacion,
|
|
14
|
+
ocultarNotificacion,
|
|
15
|
+
cambiarIdioma,
|
|
16
|
+
cambiarTema
|
|
17
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import {createSlice,createAsyncThunk} from "@reduxjs/toolkit";
|
|
2
|
+
import {identidadApi} from "../api/inicializarApi";
|
|
3
|
+
|
|
4
|
+
const colorDefault = {
|
|
5
|
+
primary: "#1C6CCC",
|
|
6
|
+
secondary: "#17A7FF",
|
|
7
|
+
error: "#f44336",
|
|
8
|
+
warning: "#ff9800",
|
|
9
|
+
info: "#2196f3",
|
|
10
|
+
success: "#4caf50"
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const inicializarSlice = createSlice({
|
|
14
|
+
name: "inicializar",
|
|
15
|
+
initialState: {
|
|
16
|
+
inicializando: true,
|
|
17
|
+
inicializado: false,
|
|
18
|
+
instancia: {
|
|
19
|
+
color: colorDefault
|
|
20
|
+
},
|
|
21
|
+
error: null
|
|
22
|
+
},
|
|
23
|
+
extraReducers(builder) {
|
|
24
|
+
builder
|
|
25
|
+
.addCase(inicializar.pending,state => {
|
|
26
|
+
state.inicializando = true;
|
|
27
|
+
})
|
|
28
|
+
.addCase(inicializar.fulfilled,(state,action) => {
|
|
29
|
+
state.inicializando = false;
|
|
30
|
+
state.inicializado = true;
|
|
31
|
+
state.instancia = {
|
|
32
|
+
...action.payload,
|
|
33
|
+
abreviatura: action.payload.nombre.match(/\b([A-Z])/g).join(""),
|
|
34
|
+
color: {
|
|
35
|
+
...colorDefault,
|
|
36
|
+
...action.payload.color
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
state.error = null;
|
|
40
|
+
})
|
|
41
|
+
.addCase(inicializar.rejected,(state,action) => {
|
|
42
|
+
state.inicializando = false;
|
|
43
|
+
state.inicializado = false;
|
|
44
|
+
state.instancia = {
|
|
45
|
+
color: colorDefault
|
|
46
|
+
};
|
|
47
|
+
state.error = action.payload;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export const inicializar = createAsyncThunk("inicializar",async (payload) => {
|
|
53
|
+
const response = await identidadApi(payload.devURL,payload.aplicacion);
|
|
54
|
+
return response.data;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export default inicializarSlice.reducer;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import {createSlice,createAsyncThunk} from "@reduxjs/toolkit";
|
|
2
|
+
import {loginApi} from "../api/loginApi";
|
|
3
|
+
import jwtDecode from "jwt-decode";
|
|
4
|
+
|
|
5
|
+
const loginSlice = createSlice({
|
|
6
|
+
name: "login",
|
|
7
|
+
initialState: {
|
|
8
|
+
cargando: false,
|
|
9
|
+
autenticado: false,
|
|
10
|
+
token: null,
|
|
11
|
+
refreshToken: null,
|
|
12
|
+
expiracion: null,
|
|
13
|
+
usuario: null,
|
|
14
|
+
perfiles: [],
|
|
15
|
+
error: null
|
|
16
|
+
},
|
|
17
|
+
reducers: {
|
|
18
|
+
logout: state => {
|
|
19
|
+
state.cargando = false;
|
|
20
|
+
state.autenticado = false;
|
|
21
|
+
state.token = null;
|
|
22
|
+
state.refreshToken = null;
|
|
23
|
+
state.expiracion = null;
|
|
24
|
+
state.usuario = null;
|
|
25
|
+
state.perfiles = [];
|
|
26
|
+
state.error = null;
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
extraReducers(builder) {
|
|
30
|
+
builder
|
|
31
|
+
.addCase(requestToken.pending,state => {
|
|
32
|
+
state.cargando = true;
|
|
33
|
+
state.error = null;
|
|
34
|
+
})
|
|
35
|
+
.addCase(requestToken.fulfilled,(state,action) => {
|
|
36
|
+
const token = action.payload.access_token;
|
|
37
|
+
const jwtToken = jwtDecode(token);
|
|
38
|
+
const expiracion = new Date();
|
|
39
|
+
expiracion.setSeconds(expiracion.getSeconds() + action.payload.expires_in);
|
|
40
|
+
state.cargando = false;
|
|
41
|
+
state.autenticado = true;
|
|
42
|
+
state.token = token;
|
|
43
|
+
state.refreshToken = action.payload.refresh_token;
|
|
44
|
+
state.expiracion = expiracion.getTime();
|
|
45
|
+
state.usuario = jwtToken.name;
|
|
46
|
+
state.perfiles = jwtToken.authorities;
|
|
47
|
+
})
|
|
48
|
+
.addCase(requestToken.rejected,(state,action) => {
|
|
49
|
+
console.log(action);
|
|
50
|
+
state.cargando = false;
|
|
51
|
+
state.autenticado = false;
|
|
52
|
+
state.token = null;
|
|
53
|
+
state.refreshToken = null;
|
|
54
|
+
state.expiracion = null;
|
|
55
|
+
state.usuario = null;
|
|
56
|
+
state.perfiles = [];
|
|
57
|
+
state.error = action.error?.message;
|
|
58
|
+
})
|
|
59
|
+
.addCase(refreshToken.pending,state => {
|
|
60
|
+
state.cargando = true;
|
|
61
|
+
})
|
|
62
|
+
.addCase(refreshToken.fulfilled,(state,action) => {
|
|
63
|
+
const expiracion = new Date();
|
|
64
|
+
expiracion.setSeconds(expiracion.getSeconds() + action.payload.expires_in);
|
|
65
|
+
state.token = action.payload.access_token;
|
|
66
|
+
state.refreshToken = action.payload.refresh_token;
|
|
67
|
+
state.expiracion = expiracion.getTime();
|
|
68
|
+
})
|
|
69
|
+
.addCase(refreshToken.rejected,(state,action) => {
|
|
70
|
+
state.cargando = false;
|
|
71
|
+
state.autenticado = false;
|
|
72
|
+
state.token = null;
|
|
73
|
+
state.refreshToken = null;
|
|
74
|
+
state.expiracion = null;
|
|
75
|
+
state.usuario = null;
|
|
76
|
+
state.perfiles = [];
|
|
77
|
+
state.error = action.error?.message;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
export const requestToken = createAsyncThunk("login/requestToken",async (payload) => {
|
|
83
|
+
const response = await loginApi(payload.devURL).login(payload.clientCredentials,payload.data);
|
|
84
|
+
return response.data;
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
export const refreshToken = createAsyncThunk("login/refreshToken",async (devURL,clientCredentials,refreshToken) => {
|
|
88
|
+
const response = await loginApi(devURL).refreshToken(clientCredentials,refreshToken);
|
|
89
|
+
return response.data;
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
export const {logout} = loginSlice.actions;
|
|
93
|
+
|
|
94
|
+
export default loginSlice.reducer;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {createSlice} from "@reduxjs/toolkit";
|
|
2
|
+
|
|
3
|
+
const notificacionSlice=createSlice({
|
|
4
|
+
name: "notificacion",
|
|
5
|
+
initialState: [],
|
|
6
|
+
reducers: {
|
|
7
|
+
mostrarNotificacion: (state,action) => {
|
|
8
|
+
state.push(action.payload);
|
|
9
|
+
},
|
|
10
|
+
ocultarNotificacion: state => {
|
|
11
|
+
state.pop();
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const getNotificacion=store => Array.isArray(store.notificacion) && store.notificacion.length ? store.notificacion[0] : null;
|
|
17
|
+
|
|
18
|
+
export const {mostrarNotificacion,ocultarNotificacion}=notificacionSlice.actions;
|
|
19
|
+
|
|
20
|
+
export default notificacionSlice.reducer;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {configureStore} from "@reduxjs/toolkit";
|
|
2
|
+
import uiSlice from "./uiSlice";
|
|
3
|
+
import loginSlice from "./loginSlice";
|
|
4
|
+
import notificacionSlice from "./notificacionSlice";
|
|
5
|
+
import inicializarSlice from "./inicializarSlice";
|
|
6
|
+
|
|
7
|
+
export const store=(customReducers) => configureStore({
|
|
8
|
+
reducer: {
|
|
9
|
+
ui: uiSlice,
|
|
10
|
+
aplicacion: inicializarSlice,
|
|
11
|
+
login: loginSlice,
|
|
12
|
+
notificaciones: notificacionSlice,
|
|
13
|
+
...customReducers
|
|
14
|
+
}
|
|
15
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {createSlice} from "@reduxjs/toolkit";
|
|
2
|
+
|
|
3
|
+
const uiSlice=createSlice({
|
|
4
|
+
name: "ui",
|
|
5
|
+
initialState: {
|
|
6
|
+
tema: "light",
|
|
7
|
+
temaSeleccionado: false,
|
|
8
|
+
idioma: "es",
|
|
9
|
+
},
|
|
10
|
+
reducers: {
|
|
11
|
+
cambiarTema: (state,action) => {
|
|
12
|
+
state.tema=action.payload;
|
|
13
|
+
state.temaSeleccionado=true;
|
|
14
|
+
localStorage.setItem("tema",action.payload);
|
|
15
|
+
},
|
|
16
|
+
cambiarIdioma: (state,action) => {
|
|
17
|
+
state.idioma=action.payload;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export const {cambiarTema,cambiarIdioma}=uiSlice.actions;
|
|
23
|
+
|
|
24
|
+
export default uiSlice.reducer;
|
package/vite.config.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {defineConfig} from "vite";
|
|
2
|
+
import {resolve} from "node:path";
|
|
3
|
+
import react from "@vitejs/plugin-react";
|
|
4
|
+
|
|
5
|
+
// https://vitejs.dev/config/
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
plugins: [react()],
|
|
8
|
+
build: {
|
|
9
|
+
lib: {
|
|
10
|
+
entry: resolve("src","index.js"),
|
|
11
|
+
name: "react-saas",
|
|
12
|
+
fileName: format => `react-saas.${format}.js`
|
|
13
|
+
},
|
|
14
|
+
rollupOptions: {
|
|
15
|
+
external: [
|
|
16
|
+
"react",
|
|
17
|
+
"react-dom"
|
|
18
|
+
],
|
|
19
|
+
output: {
|
|
20
|
+
globals: {
|
|
21
|
+
react: "React",
|
|
22
|
+
"react-dom": "ReactDOM"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
});
|