@goralys/core 1.4.3 → 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 +216 -253
  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");
@@ -414,17 +435,21 @@ var GoralysActionHandler = class {
414
435
  // src/lib/fetch/fetch.client.ts
415
436
  var actionHandler = new GoralysActionHandler();
416
437
  async function goralysFetchClient(method, input, payload, requestOptions) {
438
+ if (requestOptions?.body)
439
+ console.error("[Fetch] Found body inside request options. This is not allowed, consider using the payload parameter instead");
417
440
  const { apiDomain, getSchoolToken, isNative } = getGoralysClientConfig();
441
+ const { headers: extraHeaders, ...restOptions } = requestOptions ?? {};
418
442
  const res = await fetch(`${apiDomain}/${input}`, {
419
443
  credentials: "include",
420
444
  method: method === "BREW" ? "POST" : method,
421
445
  headers: {
422
446
  ...method === "BREW" ? { "X-HTTP-Method-Override": method } : {},
423
447
  "X-High-School-Token": getSchoolToken(),
424
- "X-Goralys-Client": isNative ? "mobile" : "desktop"
448
+ "X-Goralys-Client": isNative ? "mobile" : "desktop",
449
+ ...extraHeaders
425
450
  },
426
451
  body: payload ? payload instanceof FormData ? payload : JSON.stringify(payload) : void 0,
427
- ...requestOptions
452
+ ...restOptions
428
453
  });
429
454
  const clone = res.clone();
430
455
  const contentType = clone.headers.get("Content-Type");
@@ -491,56 +516,52 @@ async function handleToastRequest(r, showToast, redirect = true, duration) {
491
516
  return false;
492
517
  }
493
518
 
494
- // src/lib/subjects/subjects.client.ts
495
- async function fetchStudentSubjectsClient() {
496
- const csrfToken = await fetchCsrfClient("get-student-subjects");
497
- if (!csrfToken)
498
- return null;
499
- return await goralysFetchClient("GET", buildApiUrl("subjects/student", { "csrf-token": csrfToken }));
500
- }
501
- async function fetchTeacherSubjectsClient() {
502
- const csrfToken = await fetchCsrfClient("get-teacher-subjects");
503
- 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");
504
524
  return null;
505
- return await goralysFetchClient("GET", buildApiUrl("subjects/teacher", { "csrf-token": csrfToken }));
525
+ }
526
+ return await goralysFetchClient("GET", "user/tokens");
506
527
  }
507
- async function fetchAdminSubjectsClient() {
508
- const csrfToken = await fetchCsrfClient("get-admin-subjects");
509
- if (!csrfToken)
528
+ async function fetchTokensAdminPanel(target) {
529
+ const username = cookiesGet(USERNAME_KEY);
530
+ if (!username) {
531
+ console.error("[fetchTokensProfile] No username cookies, aborting");
510
532
  return null;
511
- return await goralysFetchClient("GET", buildApiUrl("subjects/admin", { "csrf-token": csrfToken }));
512
- }
513
- async function fetchSubjectsForRoleClient(role) {
514
- switch (role.role) {
515
- case "student":
516
- return await fetchStudentSubjectsClient();
517
- case "teacher":
518
- return await fetchTeacherSubjectsClient();
519
- case "admin":
520
- return await fetchAdminSubjectsClient();
521
- default:
522
- 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);
523
542
  }
524
543
  }
525
544
 
526
- // src/hooks/useSubjects.ts
527
- function useSubjects(role, showToast) {
528
- 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();
529
550
  const showToastRef = useRef(showToast);
530
551
  useEffect(() => {
531
552
  showToastRef.current = showToast;
532
553
  }, [showToast]);
533
554
  const inFlightRef = useRef(null);
534
- const fetchSubjects = useCallback(async () => {
535
- const cacheKey = SUBJECT_CACHES[role];
536
- const syncKey = SUBJECT_SYNCS[role];
537
- console.log("[useSubjects] fetchSubjects called", { role });
538
- if (!isAuthenticated()) {
539
- 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()) {
540
562
  return;
541
563
  }
542
564
  if (inFlightRef.current) {
543
- console.log("[useSubjects] in-flight, waiting");
544
565
  return inFlightRef.current;
545
566
  }
546
567
  let resolve;
@@ -550,223 +571,152 @@ function useSubjects(role, showToast) {
550
571
  try {
551
572
  const syncValue = cookiesGet(syncKey);
552
573
  if (syncValue == "1") {
553
- const raw = storageGet(cacheKey);
554
- if (raw === null || raw === void 0) {
574
+ const raw2 = storageGet(cacheKey);
575
+ if (raw2 === null || raw2 === void 0) {
555
576
  cookiesSet(syncKey, "0");
556
577
  storageRemove(cacheKey);
557
- await fetchSubjects();
578
+ inFlightRef.current = null;
579
+ resolve();
580
+ await fetchResource();
558
581
  return;
559
582
  }
560
- const cached = JSON.parse(raw ?? "null");
561
- setSubjects((prev) => {
562
- if (JSON.stringify(prev) === JSON.stringify(cached))
563
- return prev;
564
- return cached;
565
- });
583
+ const cached = JSON.parse(raw2 ?? "null");
584
+ setData((prev) => JSON.stringify(prev) === JSON.stringify(cached) ? prev : cached);
566
585
  return;
567
586
  }
568
- const res = await fetchSubjectsForRoleClient({ role });
587
+ const res = await fetcher();
569
588
  if (res)
570
589
  await handleToastRequest(res, showToastRef.current, false);
571
- const data = await res?.json();
590
+ const raw = await res?.json();
572
591
  cookiesSet(syncKey, "1");
573
- storageSet(cacheKey, JSON.stringify(data));
574
- console.log("[useSubjects] set syncKey and cached to localStorage");
575
- const result = Array.isArray(data) ? data : null;
576
- setSubjects((prev) => {
577
- if (JSON.stringify(prev) === JSON.stringify(result))
578
- return prev;
579
- return result;
580
- });
592
+ storageSet(cacheKey, JSON.stringify(raw));
593
+ const result = parse(raw);
594
+ setData((prev) => JSON.stringify(prev) === JSON.stringify(result) ? prev : result);
581
595
  } finally {
582
596
  inFlightRef.current = null;
583
597
  resolve();
584
598
  }
585
- }, [role, showToastRef]);
599
+ }, [cacheKey, syncKey, fetcher, parse, requireAuth, guard, name]);
586
600
  useEffect(() => {
587
601
  const onChange = () => {
588
602
  if (inFlightRef.current)
589
603
  return;
590
- if (cookiesGet(SUBJECT_SYNCS[role]) != "1") {
591
- void fetchSubjects();
604
+ if (cookiesGet(syncKey) != "1") {
605
+ void fetchResource();
592
606
  }
593
607
  };
594
608
  return cookiesOnChange(onChange);
595
- }, [fetchSubjects, role]);
609
+ }, [fetchResource, syncKey]);
596
610
  useEffect(() => {
597
- void fetchSubjects();
598
- }, [fetchSubjects]);
599
- return useMemo(() => ({
600
- subjects,
601
- refetch: fetchSubjects,
602
- syncKey: SUBJECT_SYNCS[role]
603
- }), [subjects, fetchSubjects, role]);
611
+ void fetchResource();
612
+ }, [fetchResource]);
613
+ return useMemo(() => ({ data, refetch: fetchResource, syncKey }), [data, fetchResource, syncKey]);
604
614
  }
605
615
 
606
- // src/hooks/useSupportTickets.ts
607
- import { useCallback as useCallback2, useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2, useState as useState2 } from "react";
608
- function useSupportTickets(showToast) {
609
- const [supportTickets, setSupportTickets] = useState2(null);
610
- const showToastRef = useRef2(showToast);
611
- useEffect2(() => {
612
- showToastRef.current = showToast;
613
- }, [showToast]);
614
- const inFlightRef = useRef2(null);
615
- const fetchSupportTickets = useCallback2(async () => {
616
- const syncKey = SUPPORT_TICKET_SYNC;
617
- const cacheKey = SUPPORT_TICKET_CACHE;
618
- if (!isAuthenticated()) {
619
- console.log("[useSupportTickets] not authenticated, aborting");
620
- return;
621
- }
622
- if (inFlightRef.current) {
623
- console.log("[useSupportTickets] in-flight, waiting");
624
- return inFlightRef.current;
625
- }
626
- let resolve;
627
- inFlightRef.current = new Promise((r) => {
628
- resolve = r;
629
- });
630
- try {
631
- const syncValue = cookiesGet(syncKey);
632
- if (syncValue == "1") {
633
- const raw = storageGet(cacheKey);
634
- if (raw === null || raw === void 0) {
635
- cookiesSet(syncKey, "0");
636
- storageRemove(cacheKey);
637
- await fetchSupportTickets();
638
- return;
639
- }
640
- const cached = JSON.parse(raw ?? "null");
641
- setSupportTickets((prev) => {
642
- if (JSON.stringify(prev) === JSON.stringify(cached))
643
- return prev;
644
- return cached;
645
- });
646
- return;
647
- }
648
- const res = await goralysFetchClient("GET", buildApiUrl("support/tickets", { "csrf-token": await fetchCsrfClient("get-tickets") }));
649
- if (res)
650
- await handleToastRequest(res, showToastRef.current, false);
651
- const data = await res?.json();
652
- cookiesSet(syncKey, "1");
653
- storageSet(cacheKey, JSON.stringify(data));
654
- const result = Array.isArray(data) ? data : null;
655
- setSupportTickets((prev) => {
656
- if (JSON.stringify(prev) === JSON.stringify(result))
657
- return prev;
658
- return result;
659
- });
660
- } finally {
661
- inFlightRef.current = null;
662
- resolve();
663
- }
664
- }, [showToastRef]);
665
- useEffect2(() => {
666
- const onChange = () => {
667
- if (inFlightRef.current)
668
- return;
669
- if (cookiesGet(SUPPORT_TICKET_SYNC) != "1") {
670
- void fetchSupportTickets();
671
- }
672
- };
673
- return cookiesOnChange(onChange);
674
- }, [fetchSupportTickets]);
675
- useEffect2(() => {
676
- void fetchSupportTickets();
677
- }, [fetchSupportTickets]);
678
- return useMemo2(() => ({
679
- supportTickets,
680
- refetch: fetchSupportTickets,
681
- syncKey: SUPPORT_TICKET_SYNC
682
- }), [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 };
683
630
  }
684
631
 
685
- // src/hooks/useUsers.ts
686
- import { useCallback as useCallback3, useEffect as useEffect3, useMemo as useMemo3, useRef as useRef3, useState as useState3 } from "react";
687
- function useUserCollection(fetchFn, type, showToast) {
688
- const syncKey = USER_SYNCS[type];
689
- const cacheKey = USER_CACHES[type];
690
- const [users, setUsers] = useState3(null);
691
- const showToastRef = useRef3(showToast);
692
- useEffect3(() => {
693
- showToastRef.current = showToast;
694
- }, [showToast]);
695
- const inFlightRef = useRef3(null);
696
- const fetchUsers = useCallback3(async () => {
697
- if (!isAuthenticated()) {
698
- console.log("[useUsers] not authenticated, aborting");
699
- return;
700
- }
701
- if (inFlightRef.current)
702
- return inFlightRef.current;
703
- let resolve;
704
- inFlightRef.current = new Promise((r) => {
705
- resolve = r;
706
- });
707
- try {
708
- const syncValue = cookiesGet(syncKey);
709
- if (syncValue == "1") {
710
- const raw = storageGet(cacheKey);
711
- if (raw === null || raw === void 0) {
712
- cookiesSet(syncKey, "0");
713
- storageRemove(cacheKey);
714
- await fetchUsers();
715
- return;
716
- }
717
- const cached = JSON.parse(raw ?? "null");
718
- setUsers((prev) => {
719
- if (JSON.stringify(prev) === JSON.stringify(cached))
720
- return prev;
721
- return cached;
722
- });
723
- return;
724
- }
725
- const res = await fetchFn();
726
- if (res)
727
- await handleToastRequest(res, showToastRef.current, false);
728
- const data = await res?.json();
729
- if (res?.ok) {
730
- cookiesSet(syncKey, "1");
731
- storageSet(cacheKey, JSON.stringify(data));
732
- }
733
- const result = Array.isArray(data) ? data : null;
734
- setUsers((prev) => {
735
- if (JSON.stringify(prev) === JSON.stringify(result))
736
- return prev;
737
- return result;
738
- });
739
- } finally {
740
- inFlightRef.current = null;
741
- resolve();
742
- }
743
- }, [fetchFn, cacheKey, syncKey]);
744
- useEffect3(() => {
745
- const onChange = () => {
746
- if (inFlightRef.current)
747
- return;
748
- if (cookiesGet(syncKey) != "1") {
749
- void fetchUsers();
750
- }
751
- };
752
- return cookiesOnChange(onChange);
753
- }, [fetchUsers, syncKey]);
754
- useEffect3(() => {
755
- void fetchUsers();
756
- }, [fetchUsers]);
757
- 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 };
758
708
  }
759
- function useUsers(showToast) {
760
- return useUserCollection(fetchUsersClient, "users-real", showToast);
709
+ function useUsers() {
710
+ return useUsersCollection(fetchUsersClient, "users-real");
761
711
  }
762
- function useVirtualUsers(showToast) {
763
- return useUserCollection(fetchVirtualUsersClient, "users-virtual", showToast);
712
+ function useVirtualUsers() {
713
+ return useUsersCollection(fetchVirtualUsersClient, "users-virtual");
764
714
  }
765
- function useAdmins(showToast) {
766
- return useUserCollection(fetchAdminsClient, "admins-real", showToast);
715
+ function useAdmins() {
716
+ return useUsersCollection(fetchAdminsClient, "admins-real");
767
717
  }
768
- function useVirtualAdmins(showToast) {
769
- return useUserCollection(fetchVirtualAdminsClient, "admins-virtual", showToast);
718
+ function useVirtualAdmins() {
719
+ return useUsersCollection(fetchVirtualAdminsClient, "admins-virtual");
770
720
  }
771
721
 
772
722
  // src/lib/asset.ts
@@ -789,8 +739,9 @@ function isFoolsDay() {
789
739
  function buildArray(...items) {
790
740
  return items.filter((item) => Boolean(item));
791
741
  }
742
+ var fromPhpDate = (phpDate) => new Date(phpDate.date);
792
743
  var parsePhpDateTime = (phpDate) => {
793
- return new Date(phpDate.date).toLocaleString("fr-FR", {
744
+ return fromPhpDate(phpDate).toLocaleString("fr-FR", {
794
745
  year: "numeric",
795
746
  month: "2-digit",
796
747
  day: "2-digit",
@@ -953,6 +904,10 @@ var USER_ROLES = ["admin", "teacher", "student", "none"];
953
904
  export {
954
905
  ADMIN_CACHE_RADIX,
955
906
  ADMIN_SYNC_RADIX,
907
+ AUTH_TOKEN_CACHES,
908
+ AUTH_TOKEN_CACHE_RADIX,
909
+ AUTH_TOKEN_SYNCS,
910
+ AUTH_TOKEN_SYNC_RADIX,
956
911
  COOKIES_DISMISSED_KEY,
957
912
  EMAIL_KEY,
958
913
  FLASH_TOAST_KEY,
@@ -984,6 +939,7 @@ export {
984
939
  configureCookies,
985
940
  configureGoralysClient,
986
941
  configureStorage,
942
+ configureToast,
987
943
  cookiesGet,
988
944
  cookiesGetAll,
989
945
  cookiesOnChange,
@@ -994,16 +950,21 @@ export {
994
950
  emitUserEvent,
995
951
  emptyUserCacheClient,
996
952
  fetchAdminsClient,
953
+ fetchAuthTokensForContext,
997
954
  fetchCsrfClient,
998
955
  fetchSubjectsForRoleClient,
956
+ fetchTokensAdminPanel,
957
+ fetchTokensProfile,
999
958
  fetchUsersClient,
1000
959
  fetchVirtualAdminsClient,
1001
960
  fetchVirtualUsersClient,
961
+ fromPhpDate,
1002
962
  getGoralysClientConfig,
1003
963
  getLongFromShort,
1004
964
  getShortFromLong,
1005
965
  getStatusHelper,
1006
966
  getStatusLabel,
967
+ getToastConfig,
1007
968
  goralysFetchClient,
1008
969
  handleToastRequest,
1009
970
  helpContent,
@@ -1021,8 +982,10 @@ export {
1021
982
  storageSet,
1022
983
  storageSize,
1023
984
  useAdmins,
985
+ useAuthTokens,
1024
986
  useSubjects,
1025
987
  useSupportTickets,
988
+ useSyncedResource,
1026
989
  useUsers,
1027
990
  useVirtualAdmins,
1028
991
  useVirtualUsers
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goralys/core",
3
- "version": "1.4.3",
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"