@inwink-mcp/backoffice-extension-helpers 1.0.5 → 1.0.7
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/dist/cli.js +8 -7
- package/package.json +1 -1
- package/templates/test-app/src/components/EventApiTester.tsx +54 -0
- package/templates/test-app/src/pages/HomePage.tsx.tpl +4 -28
- package/templates/vite-extension/src/auth/AuthProvider.tsx +41 -7
- package/templates/vite-extension/src/hooks/useInwinkApi.ts +6 -1
package/dist/cli.js
CHANGED
|
@@ -486,7 +486,7 @@ function zipDist(projectDir, name, version) {
|
|
|
486
486
|
const zip = new AdmZip2();
|
|
487
487
|
zip.addLocalFolder(distDir);
|
|
488
488
|
const sourceZipBuffer = createSourceZip(projectDir);
|
|
489
|
-
zip.addFile(
|
|
489
|
+
zip.addFile("source.zip", sourceZipBuffer);
|
|
490
490
|
zip.writeZip(zipFilePath);
|
|
491
491
|
return zipFilePath;
|
|
492
492
|
}
|
|
@@ -508,22 +508,23 @@ function pack(opts) {
|
|
|
508
508
|
error("Le build a \xE9chou\xE9 \u2014 corrigez les erreurs ci-dessus.");
|
|
509
509
|
return buildCode;
|
|
510
510
|
}
|
|
511
|
-
|
|
511
|
+
let zipPath;
|
|
512
512
|
try {
|
|
513
|
-
|
|
514
|
-
success(`Nouvelle version : ${version}`);
|
|
513
|
+
zipPath = zipDist(projectDir, name, version);
|
|
515
514
|
} catch (err) {
|
|
516
515
|
error(err.message);
|
|
517
516
|
return 1;
|
|
518
517
|
}
|
|
519
|
-
|
|
518
|
+
success(`Zip cr\xE9\xE9 : ${path7.basename(zipPath)}`);
|
|
519
|
+
info(`Version : ${version}`);
|
|
520
|
+
info("Mise \xE0 jour de la version avec la date du jour\u2026");
|
|
520
521
|
try {
|
|
521
|
-
|
|
522
|
+
const newVersion = updateVersionToDateFormat(projectDir);
|
|
523
|
+
success(`Nouvelle version : ${newVersion}`);
|
|
522
524
|
} catch (err) {
|
|
523
525
|
error(err.message);
|
|
524
526
|
return 1;
|
|
525
527
|
}
|
|
526
|
-
success(`Zip cr\xE9\xE9 : ${path7.basename(zipPath)}`);
|
|
527
528
|
info("V\xE9rification de conformit\xE9\u2026");
|
|
528
529
|
const result = verifyExtension({ projectDir, zipPath, requireDist: true });
|
|
529
530
|
const ok = printVerifyResult(result);
|
package/package.json
CHANGED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { FiRefreshCw } from 'react-icons/fi';
|
|
3
|
+
import { useInwinkEventApi } from '../hooks/useInwinkEventApi';
|
|
4
|
+
|
|
5
|
+
// Composant de test (overlay "test-app") : exerce useInwinkEventApi en lançant
|
|
6
|
+
// l'appel automatiquement au montage. N'est monté que lorsque l'utilisateur est
|
|
7
|
+
// authentifié (cf. HomePage), mais l'appel passe de toute façon par le gate
|
|
8
|
+
// `whenReady` du hook, qui attend la résolution d'auth avant de lire le token.
|
|
9
|
+
export default function EventApiTester() {
|
|
10
|
+
const eventApi = useInwinkEventApi();
|
|
11
|
+
const [result, setResult] = useState<string>('');
|
|
12
|
+
const [error, setError] = useState<string>('');
|
|
13
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
14
|
+
|
|
15
|
+
async function callEventApi(signal?: { aborted: boolean }) {
|
|
16
|
+
setError('');
|
|
17
|
+
setResult('');
|
|
18
|
+
setIsLoading(true);
|
|
19
|
+
try {
|
|
20
|
+
const data = await eventApi.post('/dae274d9-e86e-f011-8dca-0022488a3cad/session/query', {
|
|
21
|
+
"selects": { "id": true, "title": true, "status": true, "moderators": { "person": { "id": true, "firstname": true, "lastname": true, "company": true, "mail": true } }, "startDate": true, "endDate": true, "room": { "name": true, "id": true }, "speakers": { "id": true, "firstname": true, "lastname": true, "company": true, "mail": true }, "exhibitors": { "id": true, "name": true }, "infos": { "registered": true, "attended": true, "attendedOnsite": true, "attendedOnlineLive": true, "attendedOnlineReplay": true } },
|
|
22
|
+
"orders": [{ "desc": true, "value": { "validFrom": {} } }],
|
|
23
|
+
"page": { "index": 0, "size": 200 }
|
|
24
|
+
});
|
|
25
|
+
if (!signal?.aborted) setResult(JSON.stringify(data, null, 2));
|
|
26
|
+
} catch (err) {
|
|
27
|
+
if (!signal?.aborted) setError(err instanceof Error ? err.message : String(err));
|
|
28
|
+
} finally {
|
|
29
|
+
if (!signal?.aborted) setIsLoading(false);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Appel automatique au montage ; on ignore le résultat si le composant est
|
|
34
|
+
// démonté entre-temps (évite un setState sur composant démonté).
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
const signal = { aborted: false };
|
|
37
|
+
void callEventApi(signal);
|
|
38
|
+
return () => {
|
|
39
|
+
signal.aborted = true;
|
|
40
|
+
};
|
|
41
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
42
|
+
}, []);
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<section>
|
|
46
|
+
<button disabled={isLoading} onClick={() => void callEventApi()}>
|
|
47
|
+
<FiRefreshCw aria-hidden />
|
|
48
|
+
{isLoading ? 'Appel en cours…' : "Rappeler l'API Event"}
|
|
49
|
+
</button>
|
|
50
|
+
{result && <pre>{result}</pre>}
|
|
51
|
+
{error && <p role="alert">Erreur : {error}</p>}
|
|
52
|
+
</section>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -1,31 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { FiLogIn, FiLogOut, FiRefreshCw } from 'react-icons/fi';
|
|
1
|
+
import { FiLogIn, FiLogOut } from 'react-icons/fi';
|
|
3
2
|
import { useAuth } from '../hooks/useAuth';
|
|
4
|
-
import
|
|
3
|
+
import EventApiTester from '../components/EventApiTester';
|
|
5
4
|
|
|
6
5
|
// Version de HomePage générée par le template overlay "test-app".
|
|
7
6
|
// Sert à vérifier que le squelette bootstrappé compile et que les hooks API
|
|
8
|
-
// (ici useInwinkEventApi) sont utilisables.
|
|
7
|
+
// (ici useInwinkEventApi, via EventApiTester) sont utilisables.
|
|
9
8
|
export default function HomePage() {
|
|
10
9
|
const { isAuthenticated, isLoading, login, logout, user } = useAuth();
|
|
11
|
-
const eventApi = useInwinkEventApi();
|
|
12
|
-
const [result, setResult] = useState<string>('');
|
|
13
|
-
const [error, setError] = useState<string>('');
|
|
14
|
-
|
|
15
|
-
async function callEventApi() {
|
|
16
|
-
setError('');
|
|
17
|
-
setResult('');
|
|
18
|
-
try {
|
|
19
|
-
const data = await eventApi.post('/dae274d9-e86e-f011-8dca-0022488a3cad/session/query', {
|
|
20
|
-
"selects": { "id": true, "title": true, "status": true, "moderators": { "person": { "id": true, "firstname": true, "lastname": true, "company": true, "mail": true } }, "startDate": true, "endDate": true, "room": { "name": true, "id": true }, "speakers": { "id": true, "firstname": true, "lastname": true, "company": true, "mail": true }, "exhibitors": { "id": true, "name": true }, "infos": { "registered": true, "attended": true, "attendedOnsite": true, "attendedOnlineLive": true, "attendedOnlineReplay": true } },
|
|
21
|
-
"orders": [{ "desc": true, "value": { "validFrom": {} } }],
|
|
22
|
-
"page": { "index": 0, "size": 200 }
|
|
23
|
-
});
|
|
24
|
-
setResult(JSON.stringify(data, null, 2));
|
|
25
|
-
} catch (err) {
|
|
26
|
-
setError(err instanceof Error ? err.message : String(err));
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
10
|
|
|
30
11
|
return (
|
|
31
12
|
<main>
|
|
@@ -38,16 +19,11 @@ export default function HomePage() {
|
|
|
38
19
|
) : (
|
|
39
20
|
<>
|
|
40
21
|
<p>Connecté en tant que : {user?.profile.email ?? 'Inconnu'}</p>
|
|
41
|
-
<button onClick={() => void callEventApi()}>
|
|
42
|
-
<FiRefreshCw aria-hidden />
|
|
43
|
-
Appeler l'API Event
|
|
44
|
-
</button>
|
|
45
22
|
<button onClick={() => void logout()}>
|
|
46
23
|
<FiLogOut aria-hidden />
|
|
47
24
|
Se déconnecter
|
|
48
25
|
</button>
|
|
49
|
-
|
|
50
|
-
{error && <p role="alert">Erreur : {error}</p>}
|
|
26
|
+
<EventApiTester />
|
|
51
27
|
</>
|
|
52
28
|
)}
|
|
53
29
|
</main>
|
|
@@ -12,6 +12,13 @@ type AuthContextValue = {
|
|
|
12
12
|
getAccessToken: () => string | null;
|
|
13
13
|
/** Renouvelle le token silencieusement. Retourne le nouvel access_token, ou null si échec. */
|
|
14
14
|
renewToken: () => Promise<string | null>;
|
|
15
|
+
/**
|
|
16
|
+
* Résout une fois la résolution d'auth initiale terminée (callback OIDC ou
|
|
17
|
+
* `getUser()`). À attendre avant de lire le token dans un appel API : évite la
|
|
18
|
+
* course où un effet enfant part avant que la session soit chargée et
|
|
19
|
+
* déclenche un login interactif intempestif.
|
|
20
|
+
*/
|
|
21
|
+
whenReady: () => Promise<void>;
|
|
15
22
|
};
|
|
16
23
|
|
|
17
24
|
export const AuthContext = createContext<AuthContextValue | null>(null);
|
|
@@ -29,8 +36,18 @@ export function AuthProvider({ children }: PropsWithChildren) {
|
|
|
29
36
|
const [isLoading, setIsLoading] = useState(true);
|
|
30
37
|
const userRef = useRef(user);
|
|
31
38
|
const loginInFlightRef = useRef(false);
|
|
39
|
+
const renewInFlightRef = useRef<Promise<string | null> | null>(null);
|
|
32
40
|
useEffect(() => { userRef.current = user; }, [user]);
|
|
33
41
|
|
|
42
|
+
// Deferred résolu à la fin de la résolution d'auth initiale (cf. `whenReady`).
|
|
43
|
+
// Créé en lazy-init pour exister dès le premier rendu (avant tout effet enfant).
|
|
44
|
+
const readyRef = useRef<{ promise: Promise<void>; resolve: () => void } | null>(null);
|
|
45
|
+
if (!readyRef.current) {
|
|
46
|
+
let resolve!: () => void;
|
|
47
|
+
const promise = new Promise<void>((r) => { resolve = r; });
|
|
48
|
+
readyRef.current = { promise, resolve };
|
|
49
|
+
}
|
|
50
|
+
|
|
34
51
|
useEffect(() => {
|
|
35
52
|
let isMounted = true;
|
|
36
53
|
|
|
@@ -49,7 +66,10 @@ export function AuthProvider({ children }: PropsWithChildren) {
|
|
|
49
66
|
}
|
|
50
67
|
}
|
|
51
68
|
|
|
52
|
-
|
|
69
|
+
// Le gate `ready` est résolu quelle que soit l'issue de init() (succès,
|
|
70
|
+
// absence de session, ou erreur) : les appels en attente reprennent alors,
|
|
71
|
+
// token présent ou non — le cas « réellement non authentifié » mène à login().
|
|
72
|
+
void init().finally(() => readyRef.current?.resolve());
|
|
53
73
|
|
|
54
74
|
const handleUserLoaded = (u: User) => { setUser(u); setIsLoading(false); };
|
|
55
75
|
const handleUserUnloaded = () => { setUser(null); setIsLoading(false); };
|
|
@@ -92,14 +112,27 @@ export function AuthProvider({ children }: PropsWithChildren) {
|
|
|
92
112
|
|
|
93
113
|
const getAccessToken = useCallback(() => userRef.current?.access_token ?? null, []);
|
|
94
114
|
|
|
115
|
+
const whenReady = useCallback(() => readyRef.current!.promise, []);
|
|
116
|
+
|
|
95
117
|
const renewToken = useCallback(async () => {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
return
|
|
100
|
-
} catch {
|
|
101
|
-
return null;
|
|
118
|
+
// Déduplication : des appels concurrents recevant un 401 partagent un seul
|
|
119
|
+
// signinSilent() en vol plutôt que d'en lancer un chacun.
|
|
120
|
+
if (renewInFlightRef.current) {
|
|
121
|
+
return renewInFlightRef.current;
|
|
102
122
|
}
|
|
123
|
+
const renewal = (async () => {
|
|
124
|
+
try {
|
|
125
|
+
const renewed = await userManager.signinSilent();
|
|
126
|
+
setUser(renewed);
|
|
127
|
+
return renewed?.access_token ?? null;
|
|
128
|
+
} catch {
|
|
129
|
+
return null;
|
|
130
|
+
} finally {
|
|
131
|
+
renewInFlightRef.current = null;
|
|
132
|
+
}
|
|
133
|
+
})();
|
|
134
|
+
renewInFlightRef.current = renewal;
|
|
135
|
+
return renewal;
|
|
103
136
|
}, []);
|
|
104
137
|
|
|
105
138
|
const value = useMemo(() => ({
|
|
@@ -110,6 +143,7 @@ export function AuthProvider({ children }: PropsWithChildren) {
|
|
|
110
143
|
user,
|
|
111
144
|
getAccessToken,
|
|
112
145
|
renewToken,
|
|
146
|
+
whenReady,
|
|
113
147
|
}), [user, isLoading]);
|
|
114
148
|
|
|
115
149
|
return (
|
|
@@ -59,7 +59,12 @@ export function useInwinkApi(baseUrl: string): InwinkApi {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
async function request<T = unknown>(path: string, init?: RequestInit): Promise<T> {
|
|
62
|
-
const { getAccessToken, renewToken, login } = authRef.current;
|
|
62
|
+
const { getAccessToken, renewToken, login, whenReady } = authRef.current;
|
|
63
|
+
// Attend la fin de la résolution d'auth initiale avant de lire le token :
|
|
64
|
+
// sans ce gate, un appel parti depuis l'effet de montage d'un composant
|
|
65
|
+
// (hors ProtectedRoute) verrait token=null et déclencherait un login()
|
|
66
|
+
// intempestif alors que la session est en cours de chargement.
|
|
67
|
+
await whenReady();
|
|
63
68
|
let token = getAccessToken();
|
|
64
69
|
if (!token) {
|
|
65
70
|
await login();
|