@shane_donnelly/dsi-internal-react-utils 0.0.1 → 0.1.0
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/README.md +159 -2
- package/dist/ProtectedRoute-Bl0cCdQ2.js +55 -0
- package/dist/assets/ProtectedRoute.css +1 -0
- package/dist/keycloak/core/client.d.ts +22 -0
- package/dist/keycloak/core/client.js +25 -0
- package/dist/keycloak/core/types.d.ts +67 -0
- package/dist/keycloak/core/types.js +0 -0
- package/dist/keycloak/index.d.ts +6 -0
- package/dist/keycloak/index.js +4 -0
- package/dist/keycloak/react/KeycloakProvider/index.d.ts +37 -0
- package/dist/keycloak/react/KeycloakProvider/index.js +70 -0
- package/dist/keycloak/react/ProtectedRoute/index.d.ts +69 -0
- package/dist/keycloak/react/ProtectedRoute/index.js +2 -0
- package/dist/keycloak/react/hooks/useAuth.d.ts +39 -0
- package/dist/keycloak/react/hooks/useAuth.js +10 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +5 -2
- package/package.json +5 -3
- package/dist/Example-CYi9vFcY.js +0 -12
- package/dist/assets/Example.css +0 -1
- package/dist/components/Example/index.js +0 -2
package/README.md
CHANGED
|
@@ -1,4 +1,161 @@
|
|
|
1
|
-
|
|
1
|
+
# @shane_donnelly/dsi-internal-react-utils
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Librairie de composants et utilitaires React pour les projets front-end de la DSI.
|
|
4
4
|
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @shane_donnelly/dsi-internal-react-utils keycloak-js
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
### Prérequis
|
|
12
|
+
|
|
13
|
+
- React >= 19
|
|
14
|
+
- keycloak-js >= 25
|
|
15
|
+
- Un serveur Keycloak configuré avec un realm, un client, et au moins un Identity Provider
|
|
16
|
+
|
|
17
|
+
> **Recommandé** : utiliser [react-router](https://reactrouter.com/) pour éviter de perdre l'état de la page lors des redirections d'authentification.
|
|
18
|
+
|
|
19
|
+
## Modules
|
|
20
|
+
|
|
21
|
+
### Keycloak
|
|
22
|
+
|
|
23
|
+
Wrapper de simplification pour `keycloak-js`. Gère l'authentification automatique via Identity Provider, le refresh de token, et la protection de routes/zones.
|
|
24
|
+
|
|
25
|
+
[Documentation complète du module Keycloak](docs/keycloak.md)
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Exemple complet avec React Router
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
// main.tsx
|
|
33
|
+
import React from 'react';
|
|
34
|
+
import ReactDOM from 'react-dom/client';
|
|
35
|
+
import { BrowserRouter } from 'react-router-dom';
|
|
36
|
+
import App from './App';
|
|
37
|
+
|
|
38
|
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
39
|
+
<React.StrictMode>
|
|
40
|
+
<BrowserRouter>
|
|
41
|
+
<App />
|
|
42
|
+
</BrowserRouter>
|
|
43
|
+
</React.StrictMode>,
|
|
44
|
+
);
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
// App.tsx
|
|
49
|
+
import { Routes, Route } from 'react-router-dom';
|
|
50
|
+
import { KeycloakProvider, ProtectedRoute } from '@shane_donnelly/dsi-internal-react-utils';
|
|
51
|
+
import PublicPage from './pages/PublicPage';
|
|
52
|
+
import Dashboard from './pages/Dashboard';
|
|
53
|
+
import Profile from './pages/Profile';
|
|
54
|
+
|
|
55
|
+
const keycloakConfig = {
|
|
56
|
+
url: 'https://keycloak.example.com',
|
|
57
|
+
realm: 'my-realm',
|
|
58
|
+
clientId: 'my-frontend',
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export default function App() {
|
|
62
|
+
return (
|
|
63
|
+
<KeycloakProvider config={keycloakConfig} idpHint="oidc" refreshInterval={300}>
|
|
64
|
+
<Routes>
|
|
65
|
+
{/* Route publique — accessible sans authentification */}
|
|
66
|
+
<Route path="/" element={<PublicPage />} />
|
|
67
|
+
|
|
68
|
+
{/* Routes protégées — redirection automatique vers l'IDP */}
|
|
69
|
+
<Route
|
|
70
|
+
path="/dashboard"
|
|
71
|
+
element={
|
|
72
|
+
<ProtectedRoute>
|
|
73
|
+
<Dashboard />
|
|
74
|
+
</ProtectedRoute>
|
|
75
|
+
}
|
|
76
|
+
/>
|
|
77
|
+
<Route
|
|
78
|
+
path="/profile"
|
|
79
|
+
element={
|
|
80
|
+
<ProtectedRoute fallback={<p>Chargement du profil...</p>}>
|
|
81
|
+
<Profile />
|
|
82
|
+
</ProtectedRoute>
|
|
83
|
+
}
|
|
84
|
+
/>
|
|
85
|
+
</Routes>
|
|
86
|
+
</KeycloakProvider>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
// pages/Dashboard.tsx
|
|
93
|
+
import { useAuth } from '@shane_donnelly/dsi-internal-react-utils';
|
|
94
|
+
|
|
95
|
+
export default function Dashboard() {
|
|
96
|
+
const { user, token, logout } = useAuth();
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<div>
|
|
100
|
+
<h1>Bienvenue {user?.name}</h1>
|
|
101
|
+
<p>Email : {user?.email}</p>
|
|
102
|
+
<button onClick={() => logout()}>Se déconnecter</button>
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Mode plug & play (sans Provider)
|
|
109
|
+
|
|
110
|
+
Pour un usage rapide sans `KeycloakProvider`, passez `config` directement à `ProtectedRoute` :
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
import { ProtectedRoute } from '@shane_donnelly/dsi-internal-react-utils';
|
|
114
|
+
|
|
115
|
+
function App() {
|
|
116
|
+
return (
|
|
117
|
+
<ProtectedRoute
|
|
118
|
+
config={{
|
|
119
|
+
url: 'https://keycloak.example.com',
|
|
120
|
+
realm: 'my-realm',
|
|
121
|
+
clientId: 'my-frontend',
|
|
122
|
+
}}
|
|
123
|
+
idpHint="oidc"
|
|
124
|
+
>
|
|
125
|
+
<Dashboard />
|
|
126
|
+
</ProtectedRoute>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Utiliser le token pour les appels API
|
|
132
|
+
|
|
133
|
+
```tsx
|
|
134
|
+
import { useAuth } from '@shane_donnelly/dsi-internal-react-utils';
|
|
135
|
+
|
|
136
|
+
function useAuthFetch() {
|
|
137
|
+
const { token } = useAuth();
|
|
138
|
+
|
|
139
|
+
return (url: string, options?: RequestInit) =>
|
|
140
|
+
fetch(url, {
|
|
141
|
+
...options,
|
|
142
|
+
headers: {
|
|
143
|
+
...options?.headers,
|
|
144
|
+
Authorization: `Bearer ${token}`,
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Accéder à l'instance keycloak-js (usage avancé)
|
|
151
|
+
|
|
152
|
+
```tsx
|
|
153
|
+
import { useAuth } from '@shane_donnelly/dsi-internal-react-utils';
|
|
154
|
+
|
|
155
|
+
function AdvancedComponent() {
|
|
156
|
+
const { keycloak } = useAuth();
|
|
157
|
+
|
|
158
|
+
// Accès direct à l'instance keycloak-js pour des cas spécifiques
|
|
159
|
+
console.log(keycloak?.tokenParsed);
|
|
160
|
+
}
|
|
161
|
+
```
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { KeycloakProvider as e } from "./keycloak/react/KeycloakProvider/index.js";
|
|
2
|
+
import { useAuth as t } from "./keycloak/react/hooks/useAuth.js";
|
|
3
|
+
import { useEffect as n, useRef as r } from "react";
|
|
4
|
+
import { Fragment as i, jsx as a, jsxs as o } from "react/jsx-runtime";
|
|
5
|
+
import './assets/ProtectedRoute.css';var s = {
|
|
6
|
+
loadingContainer: "_loadingContainer_1th3z_1",
|
|
7
|
+
spinner: "_spinner_1th3z_11",
|
|
8
|
+
spin: "_spin_1th3z_11",
|
|
9
|
+
errorContainer: "_errorContainer_1th3z_26",
|
|
10
|
+
errorMessage: "_errorMessage_1th3z_36"
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region lib/keycloak/react/ProtectedRoute/index.tsx
|
|
14
|
+
function c() {
|
|
15
|
+
return /* @__PURE__ */ o("div", {
|
|
16
|
+
className: s.loadingContainer,
|
|
17
|
+
children: [/* @__PURE__ */ a("div", { className: s.spinner }), /* @__PURE__ */ a("p", { children: "Authentication in progress..." })]
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function l() {
|
|
21
|
+
return /* @__PURE__ */ a("div", {
|
|
22
|
+
className: s.errorContainer,
|
|
23
|
+
children: /* @__PURE__ */ a("p", {
|
|
24
|
+
className: s.errorMessage,
|
|
25
|
+
children: "Authentication error. Please try again later."
|
|
26
|
+
})
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
function u({ children: e, fallback: o, errorFallback: s }) {
|
|
30
|
+
let { status: u, login: d } = t(), f = r(!1);
|
|
31
|
+
return n(() => {
|
|
32
|
+
u === "unauthenticated" && !f.current && (f.current = !0, d().catch((e) => {
|
|
33
|
+
console.error("[dsi-keycloak] Login redirect failed:", e);
|
|
34
|
+
}));
|
|
35
|
+
}, [u, d]), u === "loading" || u === "unauthenticated" ? /* @__PURE__ */ a(i, { children: o ?? /* @__PURE__ */ a(c, {}) }) : u === "error" ? /* @__PURE__ */ a(i, { children: s ?? /* @__PURE__ */ a(l, {}) }) : /* @__PURE__ */ a(i, { children: e });
|
|
36
|
+
}
|
|
37
|
+
function d(e) {
|
|
38
|
+
return "config" in e && e.config != null;
|
|
39
|
+
}
|
|
40
|
+
function f(t) {
|
|
41
|
+
if (d(t)) {
|
|
42
|
+
let { config: n, idpHint: r, refreshInterval: i, minTokenValidity: o, onAuthError: s, ...c } = t;
|
|
43
|
+
return /* @__PURE__ */ a(e, {
|
|
44
|
+
config: n,
|
|
45
|
+
idpHint: r,
|
|
46
|
+
refreshInterval: i,
|
|
47
|
+
minTokenValidity: o,
|
|
48
|
+
onAuthError: s,
|
|
49
|
+
children: /* @__PURE__ */ a(u, { ...c })
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return /* @__PURE__ */ a(u, { ...t });
|
|
53
|
+
}
|
|
54
|
+
//#endregion
|
|
55
|
+
export { f as t };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
._loadingContainer_1th3z_1{flex-direction:column;justify-content:center;align-items:center;gap:1rem;min-height:100vh;font-family:system-ui,-apple-system,sans-serif;display:flex}._spinner_1th3z_11{border:3px solid #e5e7eb;border-top-color:#3b82f6;border-radius:50%;width:40px;height:40px;animation:1s linear infinite _spin_1th3z_11}@keyframes _spin_1th3z_11{to{transform:rotate(360deg)}}._errorContainer_1th3z_26{flex-direction:column;justify-content:center;align-items:center;gap:1rem;min-height:100vh;font-family:system-ui,-apple-system,sans-serif;display:flex}._errorMessage_1th3z_36{color:#dc2626;text-align:center;background:#fef2f2;border-radius:12px;max-width:400px;padding:1.5rem}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { default as Keycloak } from 'keycloak-js';
|
|
2
|
+
import { KeycloakConfig, AuthUser } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Crée une nouvelle instance keycloak-js à partir de la configuration.
|
|
5
|
+
* @param config - Configuration du serveur Keycloak
|
|
6
|
+
* @returns Instance Keycloak
|
|
7
|
+
*/
|
|
8
|
+
export declare function createKeycloakInstance(config: KeycloakConfig): Keycloak;
|
|
9
|
+
/**
|
|
10
|
+
* Initialise une instance Keycloak avec `check-sso` et PKCE S256.
|
|
11
|
+
* Détecte automatiquement une session SSO existante sans forcer le login.
|
|
12
|
+
*
|
|
13
|
+
* @param keycloak - Instance Keycloak à initialiser
|
|
14
|
+
* @returns `true` si l'utilisateur est déjà authentifié
|
|
15
|
+
*/
|
|
16
|
+
export declare function initKeycloak(keycloak: Keycloak): Promise<boolean>;
|
|
17
|
+
/**
|
|
18
|
+
* Extrait les informations utilisateur du token parsé Keycloak.
|
|
19
|
+
* @param keycloak - Instance Keycloak
|
|
20
|
+
* @returns Informations utilisateur ou `null`
|
|
21
|
+
*/
|
|
22
|
+
export declare function parseUser(keycloak: Keycloak): AuthUser | null;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import e from "keycloak-js";
|
|
2
|
+
//#region lib/keycloak/core/client.ts
|
|
3
|
+
function t(t) {
|
|
4
|
+
return new e({
|
|
5
|
+
url: t.url,
|
|
6
|
+
realm: t.realm,
|
|
7
|
+
clientId: t.clientId
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
async function n(e) {
|
|
11
|
+
try {
|
|
12
|
+
return await e.init({
|
|
13
|
+
onLoad: "check-sso",
|
|
14
|
+
pkceMethod: "S256",
|
|
15
|
+
checkLoginIframe: !1
|
|
16
|
+
});
|
|
17
|
+
} catch (e) {
|
|
18
|
+
return console.error("[dsi-keycloak] Init failed:", e), !1;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function r(e) {
|
|
22
|
+
return e.tokenParsed ? e.tokenParsed : null;
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
export { t as createKeycloakInstance, n as initKeycloak, r as parseUser };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { default as Keycloak } from 'keycloak-js';
|
|
2
|
+
/**
|
|
3
|
+
* Configuration du serveur Keycloak.
|
|
4
|
+
*/
|
|
5
|
+
export type KeycloakConfig = {
|
|
6
|
+
/** URL du serveur Keycloak (ex: "https://keycloak.example.com") */
|
|
7
|
+
url: string;
|
|
8
|
+
/** Nom du realm Keycloak */
|
|
9
|
+
realm: string;
|
|
10
|
+
/** Client ID de l'application */
|
|
11
|
+
clientId: string;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Options d'authentification pour le module Keycloak.
|
|
15
|
+
*/
|
|
16
|
+
export type KeycloakAuthOptions = {
|
|
17
|
+
/** Hint d'identity provider pour redirection directe (ex: "oidc", "microsoft", "google") */
|
|
18
|
+
idpHint?: string;
|
|
19
|
+
/** Intervalle de rafraîchissement du token en secondes (défaut: 300 = 5min) */
|
|
20
|
+
refreshInterval?: number;
|
|
21
|
+
/** Validité minimale du token en secondes avant rafraîchissement (défaut: 30) */
|
|
22
|
+
minTokenValidity?: number;
|
|
23
|
+
/** Comportement en cas d'erreur d'auth: 'login' pour rediriger, 'logout' pour déconnecter, ou un handler custom */
|
|
24
|
+
onAuthError?: 'login' | 'logout' | ((error: unknown) => void);
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* État de l'authentification.
|
|
28
|
+
* - `loading` : vérification en cours
|
|
29
|
+
* - `authenticated` : utilisateur connecté
|
|
30
|
+
* - `unauthenticated` : utilisateur non connecté
|
|
31
|
+
* - `error` : erreur lors de l'initialisation
|
|
32
|
+
*/
|
|
33
|
+
export type AuthStatus = 'loading' | 'authenticated' | 'unauthenticated' | 'error';
|
|
34
|
+
/**
|
|
35
|
+
* Informations utilisateur extraites du token Keycloak.
|
|
36
|
+
* Contient les claims standard OpenID Connect + toute claim custom.
|
|
37
|
+
*/
|
|
38
|
+
export type AuthUser = {
|
|
39
|
+
sub?: string;
|
|
40
|
+
name?: string;
|
|
41
|
+
email?: string;
|
|
42
|
+
preferred_username?: string;
|
|
43
|
+
given_name?: string;
|
|
44
|
+
family_name?: string;
|
|
45
|
+
[key: string]: unknown;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Valeur du contexte d'authentification exposée par `KeycloakProvider` et `useAuth`.
|
|
49
|
+
*/
|
|
50
|
+
export type AuthContextValue = {
|
|
51
|
+
/** État actuel de l'authentification */
|
|
52
|
+
status: AuthStatus;
|
|
53
|
+
/** Token d'accès brut (null si non authentifié) */
|
|
54
|
+
token: string | null;
|
|
55
|
+
/** Informations utilisateur extraites du token */
|
|
56
|
+
user: AuthUser | null;
|
|
57
|
+
/** Raccourci pour `status === 'authenticated'` */
|
|
58
|
+
isAuthenticated: boolean;
|
|
59
|
+
/** Instance keycloak-js sous-jacente (pour usage avancé) */
|
|
60
|
+
keycloak: Keycloak | null;
|
|
61
|
+
/** Déclencher une redirection de login. Accepte un idpHint optionnel pour override celui du Provider. */
|
|
62
|
+
login: (idpHint?: string) => Promise<void>;
|
|
63
|
+
/** Déclencher un logout. Accepte un redirectUri optionnel (défaut: origin). */
|
|
64
|
+
logout: (redirectUri?: string) => Promise<void>;
|
|
65
|
+
/** Rafraîchir manuellement le token. Retourne true si réussi. */
|
|
66
|
+
refreshToken: () => Promise<boolean>;
|
|
67
|
+
};
|
|
File without changes
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { KeycloakProvider } from './react/KeycloakProvider';
|
|
2
|
+
export type { KeycloakProviderProps } from './react/KeycloakProvider';
|
|
3
|
+
export { ProtectedRoute } from './react/ProtectedRoute';
|
|
4
|
+
export type { ProtectedRouteProps } from './react/ProtectedRoute';
|
|
5
|
+
export { useAuth } from './react/hooks/useAuth';
|
|
6
|
+
export type { KeycloakConfig, KeycloakAuthOptions, AuthStatus, AuthUser, AuthContextValue, } from './core/types';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { AuthContextValue, KeycloakConfig, KeycloakAuthOptions } from '../../core/types';
|
|
3
|
+
/**
|
|
4
|
+
* Contexte React pour l'authentification Keycloak.
|
|
5
|
+
* Utilisé en interne par `useAuth` et `ProtectedRoute`.
|
|
6
|
+
*/
|
|
7
|
+
export declare const KeycloakAuthContext: import('react').Context<AuthContextValue | null>;
|
|
8
|
+
/**
|
|
9
|
+
* Props du composant `KeycloakProvider`.
|
|
10
|
+
*/
|
|
11
|
+
export interface KeycloakProviderProps extends KeycloakAuthOptions {
|
|
12
|
+
/** Configuration du serveur Keycloak */
|
|
13
|
+
config: KeycloakConfig;
|
|
14
|
+
/** Contenu de l'application */
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Provider d'authentification Keycloak.
|
|
19
|
+
*
|
|
20
|
+
* Initialise la connexion Keycloak, gère le cycle de vie des tokens
|
|
21
|
+
* (rafraîchissement automatique par intervalle et au retour de l'onglet),
|
|
22
|
+
* et expose l'état d'authentification via le contexte React.
|
|
23
|
+
*
|
|
24
|
+
* Basé sur `keycloak-js`. Utilise `check-sso` avec PKCE S256.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```tsx
|
|
28
|
+
* <KeycloakProvider
|
|
29
|
+
* config={{ url: 'https://keycloak.example.com', realm: 'my-realm', clientId: 'my-client' }}
|
|
30
|
+
* idpHint="oidc"
|
|
31
|
+
* refreshInterval={300}
|
|
32
|
+
* >
|
|
33
|
+
* <App />
|
|
34
|
+
* </KeycloakProvider>
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export declare function KeycloakProvider({ config, children, idpHint, refreshInterval, minTokenValidity, onAuthError, }: KeycloakProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { createKeycloakInstance as e, initKeycloak as t, parseUser as n } from "../../core/client.js";
|
|
2
|
+
import { createContext as r, useCallback as i, useEffect as a, useRef as o, useState as s } from "react";
|
|
3
|
+
import { jsx as c } from "react/jsx-runtime";
|
|
4
|
+
//#region lib/keycloak/react/KeycloakProvider/index.tsx
|
|
5
|
+
var l = r(null);
|
|
6
|
+
function u({ config: r, children: u, idpHint: d, refreshInterval: f = 300, minTokenValidity: p = 30, onAuthError: m }) {
|
|
7
|
+
let [h, g] = s("loading"), [_, v] = s(null), [y, b] = s(null), x = o(null), S = o(!1), C = i((e) => {
|
|
8
|
+
e.authenticated && e.token ? (v(e.token), b(n(e)), g("authenticated")) : (v(null), b(null), g("unauthenticated"));
|
|
9
|
+
}, []), w = i(async (e) => {
|
|
10
|
+
m === "login" ? await x.current?.login({ idpHint: d }) : m === "logout" ? await x.current?.logout({ redirectUri: window.location.origin }) : typeof m == "function" && m(e);
|
|
11
|
+
}, [m, d]), T = i(async () => {
|
|
12
|
+
let e = x.current;
|
|
13
|
+
if (!e) return !1;
|
|
14
|
+
try {
|
|
15
|
+
return await e.updateToken(p) && C(e), !0;
|
|
16
|
+
} catch (e) {
|
|
17
|
+
return console.warn("[dsi-keycloak] Token refresh failed:", e), await w(e), !1;
|
|
18
|
+
}
|
|
19
|
+
}, [
|
|
20
|
+
p,
|
|
21
|
+
C,
|
|
22
|
+
w
|
|
23
|
+
]);
|
|
24
|
+
a(() => {
|
|
25
|
+
if (S.current) return;
|
|
26
|
+
S.current = !0;
|
|
27
|
+
let n = e(r);
|
|
28
|
+
x.current = n, t(n).then((e) => {
|
|
29
|
+
C(n), e || g("unauthenticated");
|
|
30
|
+
}).catch((e) => {
|
|
31
|
+
console.error("[dsi-keycloak] Init error:", e), g("error");
|
|
32
|
+
});
|
|
33
|
+
}, []), a(() => {
|
|
34
|
+
if (h !== "authenticated") return;
|
|
35
|
+
let e = window.setInterval(() => {
|
|
36
|
+
T();
|
|
37
|
+
}, f * 1e3), t = () => {
|
|
38
|
+
document.visibilityState === "visible" && T();
|
|
39
|
+
};
|
|
40
|
+
return document.addEventListener("visibilitychange", t), () => {
|
|
41
|
+
clearInterval(e), document.removeEventListener("visibilitychange", t);
|
|
42
|
+
};
|
|
43
|
+
}, [
|
|
44
|
+
h,
|
|
45
|
+
f,
|
|
46
|
+
T
|
|
47
|
+
]);
|
|
48
|
+
let E = i(async (e) => {
|
|
49
|
+
let t = x.current;
|
|
50
|
+
t && await t.login({ idpHint: e ?? d });
|
|
51
|
+
}, [d]), D = i(async (e) => {
|
|
52
|
+
let t = x.current;
|
|
53
|
+
t && await t.logout({ redirectUri: e ?? window.location.origin });
|
|
54
|
+
}, []), O = {
|
|
55
|
+
status: h,
|
|
56
|
+
token: _,
|
|
57
|
+
user: y,
|
|
58
|
+
isAuthenticated: h === "authenticated",
|
|
59
|
+
keycloak: x.current,
|
|
60
|
+
login: E,
|
|
61
|
+
logout: D,
|
|
62
|
+
refreshToken: T
|
|
63
|
+
};
|
|
64
|
+
return /* @__PURE__ */ c(l.Provider, {
|
|
65
|
+
value: O,
|
|
66
|
+
children: u
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
export { l as KeycloakAuthContext, u as KeycloakProvider };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { KeycloakConfig, KeycloakAuthOptions } from '../../core/types';
|
|
3
|
+
/**
|
|
4
|
+
* Props de base du composant `ProtectedRoute`.
|
|
5
|
+
*/
|
|
6
|
+
interface BaseProtectedRouteProps {
|
|
7
|
+
/** Contenu protégé, affiché une fois authentifié */
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
/** Composant affiché pendant le chargement / la redirection (défaut: spinner) */
|
|
10
|
+
fallback?: ReactNode;
|
|
11
|
+
/** Composant affiché en cas d'erreur d'authentification (défaut: message d'erreur) */
|
|
12
|
+
errorFallback?: ReactNode;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Props pour le mode standalone (sans `KeycloakProvider` parent).
|
|
16
|
+
* Inclut la configuration Keycloak et les options d'authentification.
|
|
17
|
+
*/
|
|
18
|
+
interface StandaloneProtectedRouteProps extends BaseProtectedRouteProps, KeycloakAuthOptions {
|
|
19
|
+
/** Configuration du serveur Keycloak */
|
|
20
|
+
config: KeycloakConfig;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Props pour le mode contexte (à l'intérieur d'un `KeycloakProvider`).
|
|
24
|
+
*/
|
|
25
|
+
type ContextProtectedRouteProps = BaseProtectedRouteProps;
|
|
26
|
+
/**
|
|
27
|
+
* Union des props acceptées par `ProtectedRoute`.
|
|
28
|
+
*/
|
|
29
|
+
export type ProtectedRouteProps = StandaloneProtectedRouteProps | ContextProtectedRouteProps;
|
|
30
|
+
/**
|
|
31
|
+
* Composant de protection de route / zone authentifiée.
|
|
32
|
+
*
|
|
33
|
+
* Fonctionne en deux modes :
|
|
34
|
+
*
|
|
35
|
+
* **Mode standalone** (plug & play) : fournir `config` et les options directement.
|
|
36
|
+
* Crée automatiquement un `KeycloakProvider` interne.
|
|
37
|
+
*
|
|
38
|
+
* **Mode contexte** : utiliser à l'intérieur d'un `KeycloakProvider` parent.
|
|
39
|
+
* Ne nécessite que `children` et optionnellement `fallback` / `errorFallback`.
|
|
40
|
+
*
|
|
41
|
+
* Dans les deux cas, le composant :
|
|
42
|
+
* - Vérifie l'état d'authentification via `check-sso`
|
|
43
|
+
* - Redirige vers le login Keycloak (via l'IDP configuré) si non authentifié
|
|
44
|
+
* - Affiche un fallback pendant le chargement
|
|
45
|
+
* - Rend les enfants une fois authentifié
|
|
46
|
+
*
|
|
47
|
+
* Basé sur `keycloak-js`.
|
|
48
|
+
*
|
|
49
|
+
* @example Mode standalone (plug & play)
|
|
50
|
+
* ```tsx
|
|
51
|
+
* <ProtectedRoute
|
|
52
|
+
* config={{ url: 'https://keycloak.example.com', realm: 'my-realm', clientId: 'my-client' }}
|
|
53
|
+
* idpHint="oidc"
|
|
54
|
+
* >
|
|
55
|
+
* <Dashboard />
|
|
56
|
+
* </ProtectedRoute>
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* @example Mode contexte (avec KeycloakProvider parent)
|
|
60
|
+
* ```tsx
|
|
61
|
+
* <KeycloakProvider config={...} idpHint="oidc">
|
|
62
|
+
* <ProtectedRoute>
|
|
63
|
+
* <Dashboard />
|
|
64
|
+
* </ProtectedRoute>
|
|
65
|
+
* </KeycloakProvider>
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export declare function ProtectedRoute(props: ProtectedRouteProps): import("react/jsx-runtime").JSX.Element;
|
|
69
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { AuthContextValue } from '../../core/types';
|
|
2
|
+
/**
|
|
3
|
+
* Hook d'authentification Keycloak.
|
|
4
|
+
*
|
|
5
|
+
* Donne accès à l'état d'authentification, au token, aux infos utilisateur,
|
|
6
|
+
* et aux actions (login, logout, refreshToken).
|
|
7
|
+
*
|
|
8
|
+
* Doit être utilisé à l'intérieur d'un `KeycloakProvider` ou d'un `ProtectedRoute` avec `config`.
|
|
9
|
+
*
|
|
10
|
+
* @returns Objet d'authentification complet
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* function UserProfile() {
|
|
15
|
+
* const { user, token, isAuthenticated, logout } = useAuth();
|
|
16
|
+
*
|
|
17
|
+
* if (!isAuthenticated) return null;
|
|
18
|
+
*
|
|
19
|
+
* return (
|
|
20
|
+
* <div>
|
|
21
|
+
* <p>Bonjour {user?.name}</p>
|
|
22
|
+
* <button onClick={() => logout()}>Déconnexion</button>
|
|
23
|
+
* </div>
|
|
24
|
+
* );
|
|
25
|
+
* }
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* @example Utiliser le token pour des appels API
|
|
29
|
+
* ```tsx
|
|
30
|
+
* function useFetchWithAuth(url: string) {
|
|
31
|
+
* const { token } = useAuth();
|
|
32
|
+
*
|
|
33
|
+
* return fetch(url, {
|
|
34
|
+
* headers: { Authorization: `Bearer ${token}` },
|
|
35
|
+
* });
|
|
36
|
+
* }
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export declare function useAuth(): AuthContextValue;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { KeycloakAuthContext as e } from "../KeycloakProvider/index.js";
|
|
2
|
+
import { useContext as t } from "react";
|
|
3
|
+
//#region lib/keycloak/react/hooks/useAuth.ts
|
|
4
|
+
function n() {
|
|
5
|
+
let n = t(e);
|
|
6
|
+
if (!n) throw Error("[dsi-keycloak] useAuth() must be used within a <KeycloakProvider> or a <ProtectedRoute config={...}>.");
|
|
7
|
+
return n;
|
|
8
|
+
}
|
|
9
|
+
//#endregion
|
|
10
|
+
export { n as useAuth };
|
package/dist/main.d.ts
ADDED
package/dist/main.js
CHANGED
|
@@ -1,2 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { KeycloakProvider as e } from "./keycloak/react/KeycloakProvider/index.js";
|
|
2
|
+
import { useAuth as t } from "./keycloak/react/hooks/useAuth.js";
|
|
3
|
+
import { t as n } from "./ProtectedRoute-Bl0cCdQ2.js";
|
|
4
|
+
import "./keycloak/index.js";
|
|
5
|
+
export { e as KeycloakProvider, n as ProtectedRoute, t as useAuth };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shane_donnelly/dsi-internal-react-utils",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.0
|
|
4
|
+
"version": "0.1.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "vite",
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
"vite-plugin-dts": "^4.5.4",
|
|
29
29
|
"vite-plugin-lib-inject-css": "^2.2.2",
|
|
30
30
|
"react": "^19.2.4",
|
|
31
|
-
"react-dom": "^19.2.4"
|
|
31
|
+
"react-dom": "^19.2.4",
|
|
32
|
+
"keycloak-js": "^26.0.0"
|
|
32
33
|
},
|
|
33
34
|
"main": "dist/main.js",
|
|
34
35
|
"types": "dist/main.d.ts",
|
|
@@ -40,6 +41,7 @@
|
|
|
40
41
|
],
|
|
41
42
|
"peerDependencies": {
|
|
42
43
|
"react": "^19.2.4",
|
|
43
|
-
"react-dom": "^19.2.4"
|
|
44
|
+
"react-dom": "^19.2.4",
|
|
45
|
+
"keycloak-js": ">=25.0.0"
|
|
44
46
|
}
|
|
45
47
|
}
|
package/dist/Example-CYi9vFcY.js
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { jsx as e } from "react/jsx-runtime";
|
|
2
|
-
import './assets/Example.css';var t = { text: "_text_acpj5_1" };
|
|
3
|
-
//#endregion
|
|
4
|
-
//#region lib/components/Example/index.tsx
|
|
5
|
-
function n() {
|
|
6
|
-
return /* @__PURE__ */ e("p", {
|
|
7
|
-
className: t.text,
|
|
8
|
-
children: "Example"
|
|
9
|
-
});
|
|
10
|
-
}
|
|
11
|
-
//#endregion
|
|
12
|
-
export { n as t };
|
package/dist/assets/Example.css
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
._text_acpj5_1{color:red}
|