@goralys/core 1.4.4 → 1.5.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.
Files changed (3) hide show
  1. package/dist/index.d.ts +62 -14
  2. package/dist/index.js +210 -251
  3. package/package.json +4 -2
package/dist/index.d.ts CHANGED
@@ -45,11 +45,26 @@ type CheckAuthImpl = () => boolean;
45
45
  declare function configureCheckAuth(impl: CheckAuthImpl): void;
46
46
  declare function isAuthenticated(): boolean;
47
47
 
48
+ type Toast = {
49
+ type: "error" | "warning" | "info" | "success";
50
+ title: string;
51
+ message: string;
52
+ expires?: number;
53
+ };
54
+ type ToastFn = (toast: Toast, duration?: number) => void;
55
+
56
+ interface ToastConfig {
57
+ getShowToast: () => ToastFn;
58
+ }
59
+ declare function configureToast(cfg: ToastConfig): void;
60
+ declare function getToastConfig(): ToastConfig;
61
+
48
62
  interface GoralysCoreConfig {
49
63
  client: GoralysClientConfig;
50
64
  storage: StorageAdapter;
51
65
  cookies: CookiesAdapter;
52
66
  auth: CheckAuthImpl;
67
+ toast: ToastConfig;
53
68
  }
54
69
  declare function configGoralysCore(cfg: GoralysCoreConfig): void;
55
70
 
@@ -76,14 +91,19 @@ type ConfirmOptions = {
76
91
  title: string;
77
92
  message: string;
78
93
  };
94
+ type AuthTokenContext = "admin-panel" | "profile";
95
+ type AuthToken = {
96
+ username: string;
97
+ name: string;
98
+ expires: Date;
99
+ created: Date;
100
+ };
79
101
 
80
- type Toast = {
81
- type: "error" | "warning" | "info" | "success";
82
- title: string;
83
- message: string;
84
- expires?: number;
102
+ declare function useAuthTokens(context: AuthTokenContext, target?: string | null): {
103
+ tokens: AuthToken[] | null;
104
+ refetch: () => Promise<undefined | void>;
105
+ syncKey: string;
85
106
  };
86
- type ToastFn = (toast: Toast, duration?: number) => void;
87
107
 
88
108
  type SubjectStatus = "not_submitted" | "submitted" | "rejected" | "approved";
89
109
  declare const SubjectStatusConfig: Record<SubjectStatus, {
@@ -121,7 +141,7 @@ type DraftModalResult = {
121
141
  declare const getStatusLabel: (status: SubjectStatus) => string;
122
142
  declare const getStatusHelper: (status: SubjectStatus, role: UserRole["role"]) => string;
123
143
 
124
- declare function useSubjects(role: UserRole["role"], showToast: ToastFn): {
144
+ declare function useSubjects(role: UserRole["role"]): {
125
145
  subjects: Subject[] | null;
126
146
  refetch: () => Promise<undefined | void>;
127
147
  syncKey: string;
@@ -151,21 +171,40 @@ type SupportTicket = {
151
171
  createdAt: PHPDateTime;
152
172
  };
153
173
 
154
- declare function useSupportTickets(showToast: ToastFn): {
174
+ declare function useSupportTickets(): {
155
175
  supportTickets: SupportTicket[] | null;
156
176
  refetch: () => Promise<undefined | void>;
157
177
  syncKey: string;
158
178
  };
159
179
 
180
+ interface UseSyncedResourceOptions<T> {
181
+ name: string;
182
+ cacheKey: string;
183
+ syncKey: string;
184
+ fetcher: () => Promise<Response | null | undefined>;
185
+ parse: (data: unknown) => T | null;
186
+ requireAuth?: boolean;
187
+ guard?: () => string | null | undefined;
188
+ }
189
+ /**
190
+ * Shared implementation behind useSubjects / useAuthTokens: cookies sync, localStorage cache, single-flight de-duplication, and a cross-tab
191
+ * `cookiesOnChange` listener that re-fetches once another tab invalidates the sync flag.
192
+ */
193
+ declare function useSyncedResource<T>({ name, cacheKey, syncKey, fetcher, parse, requireAuth, guard }: UseSyncedResourceOptions<T>): {
194
+ data: T | null;
195
+ refetch: () => Promise<undefined | void>;
196
+ syncKey: string;
197
+ };
198
+
160
199
  interface UserFetchResult {
161
200
  users: User[] | null;
162
201
  refetch: () => Promise<void | undefined>;
163
202
  syncKey: string;
164
203
  }
165
- declare function useUsers(showToast: ToastFn): UserFetchResult;
166
- declare function useVirtualUsers(showToast: ToastFn): UserFetchResult;
167
- declare function useAdmins(showToast: ToastFn): UserFetchResult;
168
- declare function useVirtualAdmins(showToast: ToastFn): UserFetchResult;
204
+ declare function useUsers(): UserFetchResult;
205
+ declare function useVirtualUsers(): UserFetchResult;
206
+ declare function useAdmins(): UserFetchResult;
207
+ declare function useVirtualAdmins(): UserFetchResult;
169
208
 
170
209
  declare const LOGO_PATH: {
171
210
  main: string;
@@ -183,6 +222,8 @@ declare const EMAIL_KEY = "email";
183
222
  declare const PUB_ID_KEY = "public-id";
184
223
  declare const SUBJECT_SYNC_RADIX = "subjects-synced-";
185
224
  declare const SUBJECT_CACHE_RADIX = "subjects-cache-";
225
+ declare const AUTH_TOKEN_SYNC_RADIX = "auth-tokens-sync-";
226
+ declare const AUTH_TOKEN_CACHE_RADIX = "auth-tokens-cache-";
186
227
  declare const USER_CACHE_RADIX = "users-cache-";
187
228
  declare const USER_SYNC_RADIX = "users-synced-";
188
229
  declare const ADMIN_CACHE_RADIX = "admins-cache-";
@@ -191,6 +232,8 @@ declare const SUPPORT_TICKET_CACHE = "support-tickets-cache";
191
232
  declare const SUPPORT_TICKET_SYNC = "support-tickets-synced";
192
233
  declare const SUBJECT_SYNCS: Record<UserRole["role"], string>;
193
234
  declare const SUBJECT_CACHES: Record<UserRole["role"], string>;
235
+ declare const AUTH_TOKEN_SYNCS: Record<AuthTokenContext, string>;
236
+ declare const AUTH_TOKEN_CACHES: Record<AuthTokenContext, string>;
194
237
  type UserType = {
195
238
  type: "users-virtual" | "users-real" | "admins-virtual" | "admins-real";
196
239
  };
@@ -221,11 +264,16 @@ declare const helpContent: Section[];
221
264
  declare function isFoolsDay(): boolean;
222
265
 
223
266
  declare function buildArray<T>(...items: (T | false | null | undefined)[]): T[];
267
+ declare const fromPhpDate: (phpDate: PHPDateTime) => Date;
224
268
  declare const parsePhpDateTime: (phpDate: PHPDateTime) => string;
225
269
 
226
270
  declare function emitAuthEvent(event: AuthEvent): void;
227
271
  declare function onAuthEvent(callback: (event: AuthEvent) => void): () => void;
228
272
 
273
+ declare function fetchTokensProfile(): Promise<Response | null>;
274
+ declare function fetchTokensAdminPanel(target: string): Promise<Response | null>;
275
+ declare function fetchAuthTokensForContext(ctx: AuthTokenContext, target?: string | null): Promise<Response | null>;
276
+
229
277
  declare function emitUserEvent(event: UserEvent): void;
230
278
  declare function onUserEvent(callback: (event: UserEvent) => void): () => void;
231
279
 
@@ -245,7 +293,7 @@ declare function isHTTPMethod(maybeMethod: string): maybeMethod is HttpMethod;
245
293
  declare function goralysFetchClient(method: HttpMethod, input: string | URL | Request, payload?: Record<string, string | number | boolean | null> | FormData, requestOptions?: RequestInit): Promise<Response>;
246
294
  declare function fetchCsrfClient(formId: string): Promise<string | null>;
247
295
  declare function buildApiUrl(endpoint: string, params: Record<string, string | null>, domain?: boolean): string;
248
- declare function handleToastRequest(r: Response, showToast: (payload: Toast, duration?: number) => void, redirect?: boolean, duration?: number): Promise<boolean>;
296
+ declare function handleToastRequest(r: Response, showToast: ToastFn, redirect?: boolean, duration?: number): Promise<boolean>;
249
297
 
250
298
  declare class GoralysActionHandler {
251
299
  handle: (r: Response) => Promise<void>;
@@ -291,4 +339,4 @@ declare function fetchVirtualUsersClient(): Promise<Response>;
291
339
  declare function fetchAdminsClient(): Promise<Response>;
292
340
  declare function fetchVirtualAdminsClient(): Promise<Response>;
293
341
 
294
- export { ADMIN_CACHE_RADIX, ADMIN_SYNC_RADIX, type AuthEvent, COOKIES_DISMISSED_KEY, type CheckAuthImpl, type ConfirmOptions, type CookieValue, type CookiesAdapter, type DraftModalResult, EMAIL_KEY, FLASH_TOAST_KEY, FULL_NAME_KEY, GoralysActionHandler, type GoralysClientConfig, HTTP_METHODS, type HttpMethod, LOGO_PATH, PERSISTANT_COOKIES, PERSISTANT_LOCALS, type PHPDateTime, PUB_ID_KEY, ROLE_KEY, SUBJECT_CACHES, SUBJECT_CACHE_RADIX, SUBJECT_SYNCS, SUBJECT_SYNC_RADIX, SUPPORT_TICKET_CACHE, SUPPORT_TICKET_SYNC, type StorageAdapter, type Subject, type SubjectStatus, SubjectStatusConfig, type SubjectsSearchField, type SupportReasons, type SupportTicket, type Toast, type ToastFn, USERNAME_KEY, USER_CACHES, USER_CACHE_RADIX, USER_ROLES, USER_SYNCS, USER_SYNC_RADIX, type User, type UserData, type UserEvent, type UserRole, type UserType, buildApiUrl, buildArray, cacheUserDataClient, configGoralysCore, configureCheckAuth, configureCookies, configureGoralysClient, configureStorage, cookiesGet, cookiesGetAll, cookiesOnChange, cookiesRemove, cookiesSet, emitAuthEvent, emitNavigationEvent, emitUserEvent, emptyUserCacheClient, fetchAdminsClient, fetchCsrfClient, fetchSubjectsForRoleClient, fetchUsersClient, fetchVirtualAdminsClient, fetchVirtualUsersClient, getGoralysClientConfig, getLongFromShort, getShortFromLong, getStatusHelper, getStatusLabel, goralysFetchClient, handleToastRequest, helpContent, isAuthenticated, isFoolsDay, isHTTPMethod, onAuthEvent, onNavigationEvent, onUserEvent, parsePhpDateTime, reasonsConfig, searchFields, storageGet, storageKeyAt, storageRemove, storageSet, storageSize, useAdmins, useSubjects, useSupportTickets, useUsers, useVirtualAdmins, useVirtualUsers };
342
+ export { ADMIN_CACHE_RADIX, ADMIN_SYNC_RADIX, AUTH_TOKEN_CACHES, AUTH_TOKEN_CACHE_RADIX, AUTH_TOKEN_SYNCS, AUTH_TOKEN_SYNC_RADIX, type AuthEvent, type AuthToken, type AuthTokenContext, COOKIES_DISMISSED_KEY, type CheckAuthImpl, type ConfirmOptions, type CookieValue, type CookiesAdapter, type DraftModalResult, EMAIL_KEY, FLASH_TOAST_KEY, FULL_NAME_KEY, GoralysActionHandler, type GoralysClientConfig, HTTP_METHODS, type HttpMethod, LOGO_PATH, PERSISTANT_COOKIES, PERSISTANT_LOCALS, type PHPDateTime, PUB_ID_KEY, ROLE_KEY, SUBJECT_CACHES, SUBJECT_CACHE_RADIX, SUBJECT_SYNCS, SUBJECT_SYNC_RADIX, SUPPORT_TICKET_CACHE, SUPPORT_TICKET_SYNC, type StorageAdapter, type Subject, type SubjectStatus, SubjectStatusConfig, type SubjectsSearchField, type SupportReasons, type SupportTicket, type Toast, type ToastConfig, type ToastFn, USERNAME_KEY, USER_CACHES, USER_CACHE_RADIX, USER_ROLES, USER_SYNCS, USER_SYNC_RADIX, type UseSyncedResourceOptions, type User, type UserData, type UserEvent, type UserRole, type UserType, buildApiUrl, buildArray, cacheUserDataClient, configGoralysCore, configureCheckAuth, configureCookies, configureGoralysClient, configureStorage, configureToast, cookiesGet, cookiesGetAll, cookiesOnChange, cookiesRemove, cookiesSet, emitAuthEvent, emitNavigationEvent, emitUserEvent, emptyUserCacheClient, fetchAdminsClient, fetchAuthTokensForContext, fetchCsrfClient, fetchSubjectsForRoleClient, fetchTokensAdminPanel, fetchTokensProfile, fetchUsersClient, fetchVirtualAdminsClient, fetchVirtualUsersClient, fromPhpDate, getGoralysClientConfig, getLongFromShort, getShortFromLong, getStatusHelper, getStatusLabel, getToastConfig, goralysFetchClient, handleToastRequest, helpContent, isAuthenticated, isFoolsDay, isHTTPMethod, onAuthEvent, onNavigationEvent, onUserEvent, parsePhpDateTime, reasonsConfig, searchFields, storageGet, storageKeyAt, storageRemove, storageSet, storageSize, useAdmins, useAuthTokens, useSubjects, useSupportTickets, useSyncedResource, useUsers, useVirtualAdmins, useVirtualUsers };
package/dist/index.js CHANGED
@@ -4,9 +4,8 @@ function configureGoralysClient(cfg) {
4
4
  config = cfg;
5
5
  }
6
6
  function getGoralysClientConfig() {
7
- if (!config) {
7
+ if (!config)
8
8
  throw new Error("Goralys client not configured. Call configureGoralysClient() first.");
9
- }
10
9
  return config;
11
10
  }
12
11
 
@@ -56,38 +55,28 @@ function isAuthenticated() {
56
55
  return checkAuthImpl();
57
56
  }
58
57
 
58
+ // src/lib/toast/config.ts
59
+ var config2 = null;
60
+ function configureToast(cfg) {
61
+ config2 = cfg;
62
+ }
63
+ function getToastConfig() {
64
+ if (!config2)
65
+ throw new Error("Toasts have not been configured yet. Call configureToast() first.");
66
+ return config2;
67
+ }
68
+
59
69
  // src/config.ts
60
70
  function configGoralysCore(cfg) {
61
71
  configureGoralysClient(cfg.client);
62
72
  configureCookies(cfg.cookies);
63
73
  configureStorage(cfg.storage);
64
74
  configureCheckAuth(cfg.auth);
75
+ configureToast(cfg.toast);
65
76
  }
66
77
 
67
- // src/hooks/useSubjects.ts
68
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
69
-
70
- // src/lib/auth/auth-event.ts
71
- var listeners = /* @__PURE__ */ new Set();
72
- function emitAuthEvent(event) {
73
- listeners.forEach((l) => l(event));
74
- }
75
- function onAuthEvent(callback) {
76
- listeners.add(callback);
77
- return () => {
78
- listeners.delete(callback);
79
- };
80
- }
81
-
82
- // src/lib/fetch/navigation-event.ts
83
- var listeners2 = /* @__PURE__ */ new Set();
84
- function emitNavigationEvent(event) {
85
- listeners2.forEach((l) => l(event));
86
- }
87
- function onNavigationEvent(listener) {
88
- listeners2.add(listener);
89
- return () => listeners2.delete(listener);
90
- }
78
+ // src/hooks/use-auth-tokens.ts
79
+ import { useCallback as useCallback2 } from "react";
91
80
 
92
81
  // src/lib/config.ts
93
82
  var COOKIES_DISMISSED_KEY = "cookie-banner-dismissed";
@@ -101,6 +90,8 @@ var EMAIL_KEY = "email";
101
90
  var PUB_ID_KEY = "public-id";
102
91
  var SUBJECT_SYNC_RADIX = "subjects-synced-";
103
92
  var SUBJECT_CACHE_RADIX = "subjects-cache-";
93
+ var AUTH_TOKEN_SYNC_RADIX = "auth-tokens-sync-";
94
+ var AUTH_TOKEN_CACHE_RADIX = "auth-tokens-cache-";
104
95
  var USER_CACHE_RADIX = "users-cache-";
105
96
  var USER_SYNC_RADIX = "users-synced-";
106
97
  var ADMIN_CACHE_RADIX = "admins-cache-";
@@ -119,6 +110,14 @@ var SUBJECT_CACHES = {
119
110
  admin: SUBJECT_CACHE_RADIX + "admin",
120
111
  none: SUBJECT_CACHE_RADIX + "unknown"
121
112
  };
113
+ var AUTH_TOKEN_SYNCS = {
114
+ "admin-panel": AUTH_TOKEN_SYNC_RADIX + "admin-panel",
115
+ profile: AUTH_TOKEN_SYNC_RADIX + "profile"
116
+ };
117
+ var AUTH_TOKEN_CACHES = {
118
+ "admin-panel": AUTH_TOKEN_CACHE_RADIX + "admin-panel",
119
+ profile: AUTH_TOKEN_CACHE_RADIX + "profile"
120
+ };
122
121
  var USER_SYNCS = {
123
122
  "users-real": USER_SYNC_RADIX + "users",
124
123
  "users-virtual": USER_SYNC_RADIX + "users-virtual",
@@ -332,6 +331,28 @@ Attention : les administrateurs ont acc\xE8s \xE0 toutes les fonctionnalit\xE9s
332
331
  }
333
332
  ];
334
333
 
334
+ // src/lib/auth/auth-event.ts
335
+ var listeners = /* @__PURE__ */ new Set();
336
+ function emitAuthEvent(event) {
337
+ listeners.forEach((l) => l(event));
338
+ }
339
+ function onAuthEvent(callback) {
340
+ listeners.add(callback);
341
+ return () => {
342
+ listeners.delete(callback);
343
+ };
344
+ }
345
+
346
+ // src/lib/fetch/navigation-event.ts
347
+ var listeners2 = /* @__PURE__ */ new Set();
348
+ function emitNavigationEvent(event) {
349
+ listeners2.forEach((l) => l(event));
350
+ }
351
+ function onNavigationEvent(listener) {
352
+ listeners2.add(listener);
353
+ return () => listeners2.delete(listener);
354
+ }
355
+
335
356
  // src/lib/user/user.client.ts
336
357
  async function cacheUserDataClient() {
337
358
  const res = await goralysFetchClient("GET", "user/profile");
@@ -495,56 +516,52 @@ async function handleToastRequest(r, showToast, redirect = true, duration) {
495
516
  return false;
496
517
  }
497
518
 
498
- // src/lib/subjects/subjects.client.ts
499
- async function fetchStudentSubjectsClient() {
500
- const csrfToken = await fetchCsrfClient("get-student-subjects");
501
- if (!csrfToken)
502
- return null;
503
- return await goralysFetchClient("GET", buildApiUrl("subjects/student", { "csrf-token": csrfToken }));
504
- }
505
- async function fetchTeacherSubjectsClient() {
506
- const csrfToken = await fetchCsrfClient("get-teacher-subjects");
507
- if (!csrfToken)
519
+ // src/lib/auth/fetch-auth-tokens.ts
520
+ async function fetchTokensProfile() {
521
+ const username = cookiesGet(USERNAME_KEY);
522
+ if (!username) {
523
+ console.error("[fetchTokensProfile] No username cookies, aborting");
508
524
  return null;
509
- return await goralysFetchClient("GET", buildApiUrl("subjects/teacher", { "csrf-token": csrfToken }));
525
+ }
526
+ return await goralysFetchClient("GET", "user/tokens");
510
527
  }
511
- async function fetchAdminSubjectsClient() {
512
- const csrfToken = await fetchCsrfClient("get-admin-subjects");
513
- if (!csrfToken)
528
+ async function fetchTokensAdminPanel(target) {
529
+ const username = cookiesGet(USERNAME_KEY);
530
+ if (!username) {
531
+ console.error("[fetchTokensProfile] No username cookies, aborting");
514
532
  return null;
515
- return await goralysFetchClient("GET", buildApiUrl("subjects/admin", { "csrf-token": csrfToken }));
516
- }
517
- async function fetchSubjectsForRoleClient(role) {
518
- switch (role.role) {
519
- case "student":
520
- return await fetchStudentSubjectsClient();
521
- case "teacher":
522
- return await fetchTeacherSubjectsClient();
523
- case "admin":
524
- return await fetchAdminSubjectsClient();
525
- default:
526
- return null;
533
+ }
534
+ return await goralysFetchClient("GET", "user/tokens/any", { username: target });
535
+ }
536
+ async function fetchAuthTokensForContext(ctx, target = null) {
537
+ switch (ctx) {
538
+ case "profile":
539
+ return await fetchTokensProfile();
540
+ case "admin-panel":
541
+ return await fetchTokensAdminPanel(target);
527
542
  }
528
543
  }
529
544
 
530
- // src/hooks/useSubjects.ts
531
- function useSubjects(role, showToast) {
532
- const [subjects, setSubjects] = useState(null);
545
+ // src/hooks/use-synced-ressource.ts
546
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
547
+ function useSyncedResource({ name, cacheKey, syncKey, fetcher, parse, requireAuth = true, guard }) {
548
+ const [data, setData] = useState(null);
549
+ const showToast = getToastConfig().getShowToast();
533
550
  const showToastRef = useRef(showToast);
534
551
  useEffect(() => {
535
552
  showToastRef.current = showToast;
536
553
  }, [showToast]);
537
554
  const inFlightRef = useRef(null);
538
- const fetchSubjects = useCallback(async () => {
539
- const cacheKey = SUBJECT_CACHES[role];
540
- const syncKey = SUBJECT_SYNCS[role];
541
- console.log("[useSubjects] fetchSubjects called", { role });
542
- if (!isAuthenticated()) {
543
- console.log("[useSubjects] not authenticated, aborting");
555
+ const fetchResource = useCallback(async () => {
556
+ const abortReason = guard?.();
557
+ if (abortReason) {
558
+ console.error(`[${name}] ${abortReason}`);
559
+ return;
560
+ }
561
+ if (requireAuth && !isAuthenticated()) {
544
562
  return;
545
563
  }
546
564
  if (inFlightRef.current) {
547
- console.log("[useSubjects] in-flight, waiting");
548
565
  return inFlightRef.current;
549
566
  }
550
567
  let resolve;
@@ -554,223 +571,152 @@ function useSubjects(role, showToast) {
554
571
  try {
555
572
  const syncValue = cookiesGet(syncKey);
556
573
  if (syncValue == "1") {
557
- const raw = storageGet(cacheKey);
558
- if (raw === null || raw === void 0) {
574
+ const raw2 = storageGet(cacheKey);
575
+ if (raw2 === null || raw2 === void 0) {
559
576
  cookiesSet(syncKey, "0");
560
577
  storageRemove(cacheKey);
561
- await fetchSubjects();
578
+ inFlightRef.current = null;
579
+ resolve();
580
+ await fetchResource();
562
581
  return;
563
582
  }
564
- const cached = JSON.parse(raw ?? "null");
565
- setSubjects((prev) => {
566
- if (JSON.stringify(prev) === JSON.stringify(cached))
567
- return prev;
568
- return cached;
569
- });
583
+ const cached = JSON.parse(raw2 ?? "null");
584
+ setData((prev) => JSON.stringify(prev) === JSON.stringify(cached) ? prev : cached);
570
585
  return;
571
586
  }
572
- const res = await fetchSubjectsForRoleClient({ role });
587
+ const res = await fetcher();
573
588
  if (res)
574
589
  await handleToastRequest(res, showToastRef.current, false);
575
- const data = await res?.json();
590
+ const raw = await res?.json();
576
591
  cookiesSet(syncKey, "1");
577
- storageSet(cacheKey, JSON.stringify(data));
578
- console.log("[useSubjects] set syncKey and cached to localStorage");
579
- const result = Array.isArray(data) ? data : null;
580
- setSubjects((prev) => {
581
- if (JSON.stringify(prev) === JSON.stringify(result))
582
- return prev;
583
- return result;
584
- });
592
+ storageSet(cacheKey, JSON.stringify(raw));
593
+ const result = parse(raw);
594
+ setData((prev) => JSON.stringify(prev) === JSON.stringify(result) ? prev : result);
585
595
  } finally {
586
596
  inFlightRef.current = null;
587
597
  resolve();
588
598
  }
589
- }, [role, showToastRef]);
599
+ }, [cacheKey, syncKey, fetcher, parse, requireAuth, guard, name]);
590
600
  useEffect(() => {
591
601
  const onChange = () => {
592
602
  if (inFlightRef.current)
593
603
  return;
594
- if (cookiesGet(SUBJECT_SYNCS[role]) != "1") {
595
- void fetchSubjects();
604
+ if (cookiesGet(syncKey) != "1") {
605
+ void fetchResource();
596
606
  }
597
607
  };
598
608
  return cookiesOnChange(onChange);
599
- }, [fetchSubjects, role]);
609
+ }, [fetchResource, syncKey]);
600
610
  useEffect(() => {
601
- void fetchSubjects();
602
- }, [fetchSubjects]);
603
- return useMemo(() => ({
604
- subjects,
605
- refetch: fetchSubjects,
606
- syncKey: SUBJECT_SYNCS[role]
607
- }), [subjects, fetchSubjects, role]);
611
+ void fetchResource();
612
+ }, [fetchResource]);
613
+ return useMemo(() => ({ data, refetch: fetchResource, syncKey }), [data, fetchResource, syncKey]);
608
614
  }
609
615
 
610
- // src/hooks/useSupportTickets.ts
611
- import { useCallback as useCallback2, useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2, useState as useState2 } from "react";
612
- function useSupportTickets(showToast) {
613
- const [supportTickets, setSupportTickets] = useState2(null);
614
- const showToastRef = useRef2(showToast);
615
- useEffect2(() => {
616
- showToastRef.current = showToast;
617
- }, [showToast]);
618
- const inFlightRef = useRef2(null);
619
- const fetchSupportTickets = useCallback2(async () => {
620
- const syncKey = SUPPORT_TICKET_SYNC;
621
- const cacheKey = SUPPORT_TICKET_CACHE;
622
- if (!isAuthenticated()) {
623
- console.log("[useSupportTickets] not authenticated, aborting");
624
- return;
625
- }
626
- if (inFlightRef.current) {
627
- console.log("[useSupportTickets] in-flight, waiting");
628
- return inFlightRef.current;
629
- }
630
- let resolve;
631
- inFlightRef.current = new Promise((r) => {
632
- resolve = r;
633
- });
634
- try {
635
- const syncValue = cookiesGet(syncKey);
636
- if (syncValue == "1") {
637
- const raw = storageGet(cacheKey);
638
- if (raw === null || raw === void 0) {
639
- cookiesSet(syncKey, "0");
640
- storageRemove(cacheKey);
641
- await fetchSupportTickets();
642
- return;
643
- }
644
- const cached = JSON.parse(raw ?? "null");
645
- setSupportTickets((prev) => {
646
- if (JSON.stringify(prev) === JSON.stringify(cached))
647
- return prev;
648
- return cached;
649
- });
650
- return;
651
- }
652
- const res = await goralysFetchClient("GET", buildApiUrl("support/tickets", { "csrf-token": await fetchCsrfClient("get-tickets") }));
653
- if (res)
654
- await handleToastRequest(res, showToastRef.current, false);
655
- const data = await res?.json();
656
- cookiesSet(syncKey, "1");
657
- storageSet(cacheKey, JSON.stringify(data));
658
- const result = Array.isArray(data) ? data : null;
659
- setSupportTickets((prev) => {
660
- if (JSON.stringify(prev) === JSON.stringify(result))
661
- return prev;
662
- return result;
663
- });
664
- } finally {
665
- inFlightRef.current = null;
666
- resolve();
667
- }
668
- }, [showToastRef]);
669
- useEffect2(() => {
670
- const onChange = () => {
671
- if (inFlightRef.current)
672
- return;
673
- if (cookiesGet(SUPPORT_TICKET_SYNC) != "1") {
674
- void fetchSupportTickets();
675
- }
676
- };
677
- return cookiesOnChange(onChange);
678
- }, [fetchSupportTickets]);
679
- useEffect2(() => {
680
- void fetchSupportTickets();
681
- }, [fetchSupportTickets]);
682
- return useMemo2(() => ({
683
- supportTickets,
684
- refetch: fetchSupportTickets,
685
- syncKey: SUPPORT_TICKET_SYNC
686
- }), [supportTickets, fetchSupportTickets]);
616
+ // src/hooks/use-auth-tokens.ts
617
+ function useAuthTokens(context, target = null) {
618
+ const guard = useCallback2(() => context === "admin-panel" && !target ? "Cannot use admin panel context without target" : null, [context, target]);
619
+ const fetcher = useCallback2(async () => context === "admin-panel" ? await fetchTokensAdminPanel(target) : await fetchTokensProfile(), [context, target]);
620
+ const parse = useCallback2((data) => Array.isArray(data) ? data : null, []);
621
+ const { data: tokens, refetch, syncKey } = useSyncedResource({
622
+ name: "useAuthTokens",
623
+ cacheKey: AUTH_TOKEN_CACHES[context],
624
+ syncKey: AUTH_TOKEN_SYNCS[context],
625
+ fetcher,
626
+ parse,
627
+ guard
628
+ });
629
+ return { tokens, refetch, syncKey };
687
630
  }
688
631
 
689
- // src/hooks/useUsers.ts
690
- import { useCallback as useCallback3, useEffect as useEffect3, useMemo as useMemo3, useRef as useRef3, useState as useState3 } from "react";
691
- function useUserCollection(fetchFn, type, showToast) {
692
- const syncKey = USER_SYNCS[type];
693
- const cacheKey = USER_CACHES[type];
694
- const [users, setUsers] = useState3(null);
695
- const showToastRef = useRef3(showToast);
696
- useEffect3(() => {
697
- showToastRef.current = showToast;
698
- }, [showToast]);
699
- const inFlightRef = useRef3(null);
700
- const fetchUsers = useCallback3(async () => {
701
- if (!isAuthenticated()) {
702
- console.log("[useUsers] not authenticated, aborting");
703
- return;
704
- }
705
- if (inFlightRef.current)
706
- return inFlightRef.current;
707
- let resolve;
708
- inFlightRef.current = new Promise((r) => {
709
- resolve = r;
710
- });
711
- try {
712
- const syncValue = cookiesGet(syncKey);
713
- if (syncValue == "1") {
714
- const raw = storageGet(cacheKey);
715
- if (raw === null || raw === void 0) {
716
- cookiesSet(syncKey, "0");
717
- storageRemove(cacheKey);
718
- await fetchUsers();
719
- return;
720
- }
721
- const cached = JSON.parse(raw ?? "null");
722
- setUsers((prev) => {
723
- if (JSON.stringify(prev) === JSON.stringify(cached))
724
- return prev;
725
- return cached;
726
- });
727
- return;
728
- }
729
- const res = await fetchFn();
730
- if (res)
731
- await handleToastRequest(res, showToastRef.current, false);
732
- const data = await res?.json();
733
- if (res?.ok) {
734
- cookiesSet(syncKey, "1");
735
- storageSet(cacheKey, JSON.stringify(data));
736
- }
737
- const result = Array.isArray(data) ? data : null;
738
- setUsers((prev) => {
739
- if (JSON.stringify(prev) === JSON.stringify(result))
740
- return prev;
741
- return result;
742
- });
743
- } finally {
744
- inFlightRef.current = null;
745
- resolve();
746
- }
747
- }, [fetchFn, cacheKey, syncKey]);
748
- useEffect3(() => {
749
- const onChange = () => {
750
- if (inFlightRef.current)
751
- return;
752
- if (cookiesGet(syncKey) != "1") {
753
- void fetchUsers();
754
- }
755
- };
756
- return cookiesOnChange(onChange);
757
- }, [fetchUsers, syncKey]);
758
- useEffect3(() => {
759
- void fetchUsers();
760
- }, [fetchUsers]);
761
- return useMemo3(() => ({ users, refetch: fetchUsers, syncKey }), [users, fetchUsers, syncKey]);
632
+ // src/hooks/use-subjects.ts
633
+ import { useCallback as useCallback3 } from "react";
634
+
635
+ // src/lib/subjects/subjects.client.ts
636
+ async function fetchStudentSubjectsClient() {
637
+ const csrfToken = await fetchCsrfClient("get-student-subjects");
638
+ if (!csrfToken)
639
+ return null;
640
+ return await goralysFetchClient("GET", buildApiUrl("subjects/student", { "csrf-token": csrfToken }));
641
+ }
642
+ async function fetchTeacherSubjectsClient() {
643
+ const csrfToken = await fetchCsrfClient("get-teacher-subjects");
644
+ if (!csrfToken)
645
+ return null;
646
+ return await goralysFetchClient("GET", buildApiUrl("subjects/teacher", { "csrf-token": csrfToken }));
647
+ }
648
+ async function fetchAdminSubjectsClient() {
649
+ const csrfToken = await fetchCsrfClient("get-admin-subjects");
650
+ if (!csrfToken)
651
+ return null;
652
+ return await goralysFetchClient("GET", buildApiUrl("subjects/admin", { "csrf-token": csrfToken }));
653
+ }
654
+ async function fetchSubjectsForRoleClient(role) {
655
+ switch (role.role) {
656
+ case "student":
657
+ return await fetchStudentSubjectsClient();
658
+ case "teacher":
659
+ return await fetchTeacherSubjectsClient();
660
+ case "admin":
661
+ return await fetchAdminSubjectsClient();
662
+ default:
663
+ return null;
664
+ }
665
+ }
666
+
667
+ // src/hooks/use-subjects.ts
668
+ function useSubjects(role) {
669
+ const fetcher = useCallback3(async () => fetchSubjectsForRoleClient({ role }), [role]);
670
+ const parse = useCallback3((data) => Array.isArray(data) ? data : null, []);
671
+ const { data: subjects, refetch, syncKey } = useSyncedResource({
672
+ name: "useSubjects",
673
+ cacheKey: SUBJECT_CACHES[role],
674
+ syncKey: SUBJECT_SYNCS[role],
675
+ fetcher,
676
+ parse
677
+ });
678
+ return { subjects, refetch, syncKey };
679
+ }
680
+
681
+ // src/hooks/use-support-tickets.ts
682
+ import { useCallback as useCallback4 } from "react";
683
+ function useSupportTickets() {
684
+ const fetcher = useCallback4(async () => await goralysFetchClient("GET", buildApiUrl("support/tickets", { "csrf-token": await fetchCsrfClient("get-tickets") })), []);
685
+ const parse = useCallback4((data) => Array.isArray(data) ? data : null, []);
686
+ const { data: supportTickets, refetch, syncKey } = useSyncedResource({
687
+ name: "useSupportTickets",
688
+ cacheKey: SUPPORT_TICKET_CACHE,
689
+ syncKey: SUPPORT_TICKET_SYNC,
690
+ fetcher,
691
+ parse
692
+ });
693
+ return { supportTickets, refetch, syncKey };
694
+ }
695
+
696
+ // src/hooks/use-users-collection.ts
697
+ import { useCallback as useCallback5 } from "react";
698
+ function useUsersCollection(fetcher, type) {
699
+ const parse = useCallback5((data) => Array.isArray(data) ? data : null, []);
700
+ const { data: users, refetch, syncKey } = useSyncedResource({
701
+ name: "useUsersCollection",
702
+ cacheKey: USER_CACHES[type],
703
+ syncKey: USER_SYNCS[type],
704
+ fetcher,
705
+ parse
706
+ });
707
+ return { users, refetch, syncKey };
762
708
  }
763
- function useUsers(showToast) {
764
- return useUserCollection(fetchUsersClient, "users-real", showToast);
709
+ function useUsers() {
710
+ return useUsersCollection(fetchUsersClient, "users-real");
765
711
  }
766
- function useVirtualUsers(showToast) {
767
- return useUserCollection(fetchVirtualUsersClient, "users-virtual", showToast);
712
+ function useVirtualUsers() {
713
+ return useUsersCollection(fetchVirtualUsersClient, "users-virtual");
768
714
  }
769
- function useAdmins(showToast) {
770
- return useUserCollection(fetchAdminsClient, "admins-real", showToast);
715
+ function useAdmins() {
716
+ return useUsersCollection(fetchAdminsClient, "admins-real");
771
717
  }
772
- function useVirtualAdmins(showToast) {
773
- return useUserCollection(fetchVirtualAdminsClient, "admins-virtual", showToast);
718
+ function useVirtualAdmins() {
719
+ return useUsersCollection(fetchVirtualAdminsClient, "admins-virtual");
774
720
  }
775
721
 
776
722
  // src/lib/asset.ts
@@ -793,8 +739,9 @@ function isFoolsDay() {
793
739
  function buildArray(...items) {
794
740
  return items.filter((item) => Boolean(item));
795
741
  }
742
+ var fromPhpDate = (phpDate) => new Date(phpDate.date);
796
743
  var parsePhpDateTime = (phpDate) => {
797
- return new Date(phpDate.date).toLocaleString("fr-FR", {
744
+ return fromPhpDate(phpDate).toLocaleString("fr-FR", {
798
745
  year: "numeric",
799
746
  month: "2-digit",
800
747
  day: "2-digit",
@@ -957,6 +904,10 @@ var USER_ROLES = ["admin", "teacher", "student", "none"];
957
904
  export {
958
905
  ADMIN_CACHE_RADIX,
959
906
  ADMIN_SYNC_RADIX,
907
+ AUTH_TOKEN_CACHES,
908
+ AUTH_TOKEN_CACHE_RADIX,
909
+ AUTH_TOKEN_SYNCS,
910
+ AUTH_TOKEN_SYNC_RADIX,
960
911
  COOKIES_DISMISSED_KEY,
961
912
  EMAIL_KEY,
962
913
  FLASH_TOAST_KEY,
@@ -988,6 +939,7 @@ export {
988
939
  configureCookies,
989
940
  configureGoralysClient,
990
941
  configureStorage,
942
+ configureToast,
991
943
  cookiesGet,
992
944
  cookiesGetAll,
993
945
  cookiesOnChange,
@@ -998,16 +950,21 @@ export {
998
950
  emitUserEvent,
999
951
  emptyUserCacheClient,
1000
952
  fetchAdminsClient,
953
+ fetchAuthTokensForContext,
1001
954
  fetchCsrfClient,
1002
955
  fetchSubjectsForRoleClient,
956
+ fetchTokensAdminPanel,
957
+ fetchTokensProfile,
1003
958
  fetchUsersClient,
1004
959
  fetchVirtualAdminsClient,
1005
960
  fetchVirtualUsersClient,
961
+ fromPhpDate,
1006
962
  getGoralysClientConfig,
1007
963
  getLongFromShort,
1008
964
  getShortFromLong,
1009
965
  getStatusHelper,
1010
966
  getStatusLabel,
967
+ getToastConfig,
1011
968
  goralysFetchClient,
1012
969
  handleToastRequest,
1013
970
  helpContent,
@@ -1025,8 +982,10 @@ export {
1025
982
  storageSet,
1026
983
  storageSize,
1027
984
  useAdmins,
985
+ useAuthTokens,
1028
986
  useSubjects,
1029
987
  useSupportTickets,
988
+ useSyncedResource,
1030
989
  useUsers,
1031
990
  useVirtualAdmins,
1032
991
  useVirtualUsers
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goralys/core",
3
- "version": "1.4.4",
3
+ "version": "1.5.0",
4
4
  "license": "LGPL-2.1-or-later",
5
5
  "description": "Shared business logic for the Goralys web application",
6
6
  "repository": {
@@ -27,9 +27,11 @@
27
27
  "access": "public"
28
28
  },
29
29
  "scripts": {
30
+ "prebuild": "pnpm barrel",
30
31
  "build": "tsup",
31
32
  "typeCheck": "tsgo --noEmit",
32
- "dev": "tsup --watch"
33
+ "dev": "tsup --watch",
34
+ "barrel": "rm ./src/index.ts && barrelsby -d ./src -l top"
33
35
  },
34
36
  "peerDependencies": {
35
37
  "react": ">=18"