@noatgnu/cupcake-core 1.2.9 → 1.2.11

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.
@@ -1,10 +1,10 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, inject, Injectable, signal, computed, Component, effect, ChangeDetectorRef, NgModule } from '@angular/core';
2
+ import { inject, InjectionToken, Injectable, signal, computed, Component, effect, ChangeDetectorRef, NgModule } from '@angular/core';
3
3
  import * as i1 from '@angular/common/http';
4
4
  import { HttpClient, HttpParams, provideHttpClient, withInterceptors, HttpClientModule } from '@angular/common/http';
5
- import { BehaviorSubject, map, tap, throwError, catchError, Subject, timer, EMPTY, take, switchMap as switchMap$1, filter, debounceTime, distinctUntilChanged } from 'rxjs';
6
- import { map as map$1, takeUntil, tap as tap$1, switchMap } from 'rxjs/operators';
5
+ import { BehaviorSubject, catchError, throwError, switchMap, filter, take, map, tap, Subject, timer, EMPTY, debounceTime, distinctUntilChanged } from 'rxjs';
7
6
  import { Router, ActivatedRoute, RouterModule } from '@angular/router';
7
+ import { map as map$1, takeUntil, tap as tap$1, switchMap as switchMap$1 } from 'rxjs/operators';
8
8
  import * as i1$1 from '@angular/forms';
9
9
  import { FormBuilder, Validators, ReactiveFormsModule, FormsModule, NonNullableFormBuilder } from '@angular/forms';
10
10
  import * as i2 from '@angular/common';
@@ -69,12 +69,141 @@ const InvitationStatusLabels = {
69
69
  [InvitationStatus.CANCELLED]: 'Cancelled'
70
70
  };
71
71
 
72
+ var AnnotationType;
73
+ (function (AnnotationType) {
74
+ AnnotationType["Text"] = "text";
75
+ AnnotationType["File"] = "file";
76
+ AnnotationType["Image"] = "image";
77
+ AnnotationType["Video"] = "video";
78
+ AnnotationType["Audio"] = "audio";
79
+ AnnotationType["Sketch"] = "sketch";
80
+ AnnotationType["Other"] = "other";
81
+ AnnotationType["Checklist"] = "checklist";
82
+ AnnotationType["Counter"] = "counter";
83
+ AnnotationType["Table"] = "table";
84
+ AnnotationType["Alignment"] = "alignment";
85
+ AnnotationType["Calculator"] = "calculator";
86
+ AnnotationType["MolarityCalculator"] = "mcalculator";
87
+ AnnotationType["Randomization"] = "randomization";
88
+ AnnotationType["Instrument"] = "instrument";
89
+ AnnotationType["Metadata"] = "metadata";
90
+ AnnotationType["Booking"] = "booking";
91
+ })(AnnotationType || (AnnotationType = {}));
92
+
72
93
  /**
73
94
  * CUPCAKE Core (CCC) - Models barrel export
74
95
  * User management, lab groups, and core functionality interfaces
75
96
  */
76
97
  // Base types and enums
77
98
 
99
+ let isRefreshing = false;
100
+ let refreshFailedCount = 0;
101
+ let lastRefreshFailTime = 0;
102
+ const MAX_REFRESH_RETRIES = 3;
103
+ const REFRESH_RETRY_WINDOW_MS = 60000;
104
+ const refreshTokenSubject = new BehaviorSubject(null);
105
+ function resetRefreshState() {
106
+ refreshFailedCount = 0;
107
+ lastRefreshFailTime = 0;
108
+ isRefreshing = false;
109
+ }
110
+ const authInterceptor = (req, next) => {
111
+ const router = inject(Router);
112
+ const http = inject(HttpClient);
113
+ const config = inject(CUPCAKE_CORE_CONFIG);
114
+ if (req.url.includes('/auth/login/') ||
115
+ req.url.includes('/auth/token/') ||
116
+ req.url.includes('/auth/orcid/') ||
117
+ req.url.includes('/auth/register/') ||
118
+ req.url.includes('/site-config/public/')) {
119
+ return next(req);
120
+ }
121
+ const token = localStorage.getItem('ccvAccessToken');
122
+ let authReq = req;
123
+ if (token) {
124
+ authReq = addTokenToRequest(req, token);
125
+ }
126
+ return next(authReq).pipe(catchError((error) => {
127
+ if (error.status === 401) {
128
+ return handle401Error(authReq, next, http, router, config);
129
+ }
130
+ return throwError(() => error);
131
+ }));
132
+ };
133
+ function addTokenToRequest(request, token) {
134
+ return request.clone({
135
+ setHeaders: {
136
+ Authorization: `Bearer ${token}`
137
+ }
138
+ });
139
+ }
140
+ function handle401Error(request, next, http, router, config) {
141
+ const now = Date.now();
142
+ if (now - lastRefreshFailTime > REFRESH_RETRY_WINDOW_MS) {
143
+ refreshFailedCount = 0;
144
+ }
145
+ if (refreshFailedCount >= MAX_REFRESH_RETRIES) {
146
+ return throwError(() => new Error('Maximum token refresh attempts exceeded. Please log in again.'));
147
+ }
148
+ if (!isRefreshing) {
149
+ isRefreshing = true;
150
+ refreshTokenSubject.next(null);
151
+ const refreshToken = localStorage.getItem('ccvRefreshToken');
152
+ if (refreshToken) {
153
+ return http.post(`${config.apiUrl}/auth/token/refresh/`, {
154
+ refresh: refreshToken
155
+ }).pipe(switchMap((tokenResponse) => {
156
+ isRefreshing = false;
157
+ refreshFailedCount = 0;
158
+ lastRefreshFailTime = 0;
159
+ localStorage.setItem('ccvAccessToken', tokenResponse.access);
160
+ refreshTokenSubject.next(tokenResponse.access);
161
+ if (typeof window !== 'undefined') {
162
+ window.dispatchEvent(new CustomEvent('tokenRefreshed'));
163
+ }
164
+ return next(addTokenToRequest(request, tokenResponse.access));
165
+ }), catchError((refreshError) => {
166
+ isRefreshing = false;
167
+ refreshFailedCount++;
168
+ lastRefreshFailTime = Date.now();
169
+ localStorage.removeItem('ccvAccessToken');
170
+ localStorage.removeItem('ccvRefreshToken');
171
+ refreshTokenSubject.next(null);
172
+ if (typeof window !== 'undefined') {
173
+ window.dispatchEvent(new CustomEvent('authCleared'));
174
+ }
175
+ if (refreshFailedCount >= MAX_REFRESH_RETRIES) {
176
+ router.navigate(['/login'], {
177
+ queryParams: { returnUrl: router.url }
178
+ });
179
+ }
180
+ return throwError(() => refreshError);
181
+ }));
182
+ }
183
+ else {
184
+ isRefreshing = false;
185
+ refreshFailedCount++;
186
+ lastRefreshFailTime = Date.now();
187
+ localStorage.removeItem('ccvAccessToken');
188
+ localStorage.removeItem('ccvRefreshToken');
189
+ if (typeof window !== 'undefined') {
190
+ window.dispatchEvent(new CustomEvent('authCleared'));
191
+ }
192
+ if (refreshFailedCount >= MAX_REFRESH_RETRIES) {
193
+ router.navigate(['/login'], {
194
+ queryParams: { returnUrl: router.url }
195
+ });
196
+ }
197
+ return throwError(() => new Error('No refresh token available'));
198
+ }
199
+ }
200
+ else {
201
+ return refreshTokenSubject.pipe(filter((token) => token !== null), take(1), switchMap((token) => {
202
+ return next(addTokenToRequest(request, token));
203
+ }));
204
+ }
205
+ }
206
+
78
207
  const CUPCAKE_CORE_CONFIG = new InjectionToken('CUPCAKE_CORE_CONFIG');
79
208
  class AuthService {
80
209
  http = inject(HttpClient);
@@ -258,6 +387,7 @@ class AuthService {
258
387
  const convertedUser = this.convertUserFromSnakeToCamel(response.user);
259
388
  this.currentUserSubject.next(convertedUser);
260
389
  this.isAuthenticatedSubject.next(true);
390
+ resetRefreshState();
261
391
  }
262
392
  tryRefreshToken() {
263
393
  const refreshToken = this.getRefreshToken();
@@ -699,6 +829,17 @@ class ApiService {
699
829
  deleteAnnotationFolder(id) {
700
830
  return this.http.delete(`${this.apiUrl}/annotation-folders/${id}/`);
701
831
  }
832
+ /**
833
+ * WARNING: This method accesses the base annotation endpoint and should only be used
834
+ * for standalone annotations that are NOT attached to parent resources.
835
+ *
836
+ * For annotations attached to parent resources, use the specialized services instead:
837
+ * - Instrument annotations: Use InstrumentService from @noatgnu/cupcake-macaron
838
+ * - StoredReagent annotations: Use ReagentService from @noatgnu/cupcake-macaron
839
+ * - Session annotations: Use SessionService from @noatgnu/cupcake-red-velvet
840
+ *
841
+ * These specialized services ensure proper permission checking through parent resources.
842
+ */
702
843
  getAnnotations(params) {
703
844
  let httpParams = new HttpParams();
704
845
  if (params) {
@@ -714,9 +855,33 @@ class ApiService {
714
855
  results: response.results.map((annotation) => this.resourceService.transformLegacyResource(annotation))
715
856
  })));
716
857
  }
858
+ /**
859
+ * WARNING: This method accesses the base annotation endpoint and should only be used
860
+ * for standalone annotations that are NOT attached to parent resources.
861
+ *
862
+ * For annotations attached to parent resources, use the specialized services instead:
863
+ * - Instrument annotations: Use InstrumentService.getInstrumentAnnotation()
864
+ * - StoredReagent annotations: Use ReagentService.getStoredReagentAnnotation()
865
+ * - Session annotations: Use SessionService (session folder annotations)
866
+ *
867
+ * The backend enforces parent resource permissions, but using specialized services
868
+ * provides cleaner access control and better context.
869
+ */
717
870
  getAnnotation(id) {
718
871
  return this.http.get(`${this.apiUrl}/annotations/${id}/`).pipe(map$1(response => this.resourceService.transformLegacyResource(response)));
719
872
  }
873
+ /**
874
+ * WARNING: This method accesses the base annotation endpoint and should only be used
875
+ * for standalone annotations that are NOT attached to parent resources.
876
+ *
877
+ * For creating annotations attached to parent resources, use specialized upload methods:
878
+ * - Instrument annotations: Use InstrumentService.uploadAnnotation()
879
+ * - StoredReagent annotations: Use ReagentService.uploadAnnotation()
880
+ * - Session/Step annotations: Use the appropriate chunked upload service
881
+ *
882
+ * These specialized methods provide chunked upload support, progress tracking,
883
+ * and automatic binding to parent resources with proper permission enforcement.
884
+ */
720
885
  createAnnotation(annotationData) {
721
886
  const formData = new FormData();
722
887
  Object.keys(annotationData).forEach(key => {
@@ -732,10 +897,29 @@ class ApiService {
732
897
  });
733
898
  return this.http.post(`${this.apiUrl}/annotations/`, formData).pipe(map$1(response => this.resourceService.transformLegacyResource(response)));
734
899
  }
900
+ /**
901
+ * WARNING: This method accesses the base annotation endpoint and should only be used
902
+ * for standalone annotations that are NOT attached to parent resources.
903
+ *
904
+ * For annotations attached to parent resources, the backend enforces parent resource
905
+ * permissions. However, using specialized services provides better context and access control.
906
+ */
735
907
  updateAnnotation(id, annotationData) {
736
908
  const preparedData = this.resourceService.prepareForAPI(annotationData);
737
909
  return this.http.patch(`${this.apiUrl}/annotations/${id}/`, preparedData).pipe(map$1(response => this.resourceService.transformLegacyResource(response)));
738
910
  }
911
+ /**
912
+ * WARNING: This method accesses the base annotation endpoint and should only be used
913
+ * for standalone annotations that are NOT attached to parent resources.
914
+ *
915
+ * For deleting annotations attached to parent resources, use specialized services:
916
+ * - Instrument annotations: Use InstrumentService.deleteInstrumentAnnotation()
917
+ * - StoredReagent annotations: Use ReagentService.deleteStoredReagentAnnotation()
918
+ * - Session annotations: Use appropriate session/step annotation delete methods
919
+ *
920
+ * The backend enforces parent resource permissions, but using specialized services
921
+ * provides clearer intent and better access control context.
922
+ */
739
923
  deleteAnnotation(id) {
740
924
  return this.http.delete(`${this.apiUrl}/annotations/${id}/`);
741
925
  }
@@ -1475,7 +1659,7 @@ class WebSocketService {
1475
1659
  })).subscribe();
1476
1660
  }
1477
1661
  filterMessages(type) {
1478
- return this.messages$.pipe(tap$1(msg => console.log('Filtering message:', msg.type, 'looking for:', type)), switchMap(message => message.type === type ? [message] : EMPTY));
1662
+ return this.messages$.pipe(tap$1(msg => console.log('Filtering message:', msg.type, 'looking for:', type)), switchMap$1(message => message.type === type ? [message] : EMPTY));
1479
1663
  }
1480
1664
  getNotifications() {
1481
1665
  return this.filterMessages('notification');
@@ -1856,89 +2040,6 @@ const adminGuard = (route, state) => {
1856
2040
  return true;
1857
2041
  };
1858
2042
 
1859
- let isRefreshing = false;
1860
- const refreshTokenSubject = new BehaviorSubject(null);
1861
- const authInterceptor = (req, next) => {
1862
- const router = inject(Router);
1863
- const http = inject(HttpClient);
1864
- const config = inject(CUPCAKE_CORE_CONFIG);
1865
- if (req.url.includes('/auth/login/') ||
1866
- req.url.includes('/auth/token/') ||
1867
- req.url.includes('/auth/orcid/') ||
1868
- req.url.includes('/auth/register/') ||
1869
- req.url.includes('/site-config/public/')) {
1870
- return next(req);
1871
- }
1872
- const token = localStorage.getItem('ccvAccessToken');
1873
- let authReq = req;
1874
- if (token) {
1875
- authReq = addTokenToRequest(req, token);
1876
- }
1877
- return next(authReq).pipe(catchError((error) => {
1878
- if (error.status === 401) {
1879
- return handle401Error(authReq, next, http, router, config);
1880
- }
1881
- return throwError(() => error);
1882
- }));
1883
- };
1884
- function addTokenToRequest(request, token) {
1885
- return request.clone({
1886
- setHeaders: {
1887
- Authorization: `Bearer ${token}`
1888
- }
1889
- });
1890
- }
1891
- function handle401Error(request, next, http, router, config) {
1892
- if (!isRefreshing) {
1893
- isRefreshing = true;
1894
- refreshTokenSubject.next(null);
1895
- const refreshToken = localStorage.getItem('ccvRefreshToken');
1896
- if (refreshToken) {
1897
- return http.post(`${config.apiUrl}/auth/token/refresh/`, {
1898
- refresh: refreshToken
1899
- }).pipe(switchMap$1((tokenResponse) => {
1900
- isRefreshing = false;
1901
- localStorage.setItem('ccvAccessToken', tokenResponse.access);
1902
- refreshTokenSubject.next(tokenResponse.access);
1903
- if (typeof window !== 'undefined') {
1904
- window.dispatchEvent(new CustomEvent('tokenRefreshed'));
1905
- }
1906
- return next(addTokenToRequest(request, tokenResponse.access));
1907
- }), catchError((refreshError) => {
1908
- isRefreshing = false;
1909
- localStorage.removeItem('ccvAccessToken');
1910
- localStorage.removeItem('ccvRefreshToken');
1911
- refreshTokenSubject.next(null);
1912
- if (typeof window !== 'undefined') {
1913
- window.dispatchEvent(new CustomEvent('authCleared'));
1914
- }
1915
- router.navigate(['/login'], {
1916
- queryParams: { returnUrl: router.url }
1917
- });
1918
- return throwError(() => refreshError);
1919
- }));
1920
- }
1921
- else {
1922
- isRefreshing = false;
1923
- localStorage.removeItem('ccvAccessToken');
1924
- localStorage.removeItem('ccvRefreshToken');
1925
- // Notify AuthService that auth was cleared
1926
- if (typeof window !== 'undefined') {
1927
- window.dispatchEvent(new CustomEvent('authCleared'));
1928
- }
1929
- router.navigate(['/login'], {
1930
- queryParams: { returnUrl: router.url }
1931
- });
1932
- return throwError(() => new Error('No refresh token available'));
1933
- }
1934
- }
1935
- else {
1936
- return refreshTokenSubject.pipe(filter((token) => token !== null), take(1), switchMap$1((token) => {
1937
- return next(addTokenToRequest(request, token));
1938
- }));
1939
- }
1940
- }
1941
-
1942
2043
  class LoginComponent {
1943
2044
  authService = inject(AuthService);
1944
2045
  fb = inject(FormBuilder);
@@ -3328,5 +3429,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImpor
3328
3429
  * Generated bundle index. Do not edit.
3329
3430
  */
3330
3431
 
3331
- export { ApiService, AuthService, BaseApiService, CUPCAKE_CORE_CONFIG, CupcakeCoreModule, InvitationStatus, InvitationStatusLabels, LabGroupService, LabGroupsComponent, LoginComponent, NotificationService, PoweredByFooterComponent, RegisterComponent, ResourceRole, ResourceRoleLabels, ResourceService, ResourceType, ResourceTypeLabels, ResourceVisibility, ResourceVisibilityLabels, SiteConfigComponent, SiteConfigService, ThemeService, ToastContainerComponent, ToastService, UserManagementComponent, UserManagementService, UserProfileComponent, WEBSOCKET_ENDPOINT, WEBSOCKET_ENDPOINTS, WebSocketConfigService, WebSocketEndpoints, WebSocketService, adminGuard, authGuard, authInterceptor };
3432
+ export { AnnotationType, ApiService, AuthService, BaseApiService, CUPCAKE_CORE_CONFIG, CupcakeCoreModule, InvitationStatus, InvitationStatusLabels, LabGroupService, LabGroupsComponent, LoginComponent, NotificationService, PoweredByFooterComponent, RegisterComponent, ResourceRole, ResourceRoleLabels, ResourceService, ResourceType, ResourceTypeLabels, ResourceVisibility, ResourceVisibilityLabels, SiteConfigComponent, SiteConfigService, ThemeService, ToastContainerComponent, ToastService, UserManagementComponent, UserManagementService, UserProfileComponent, WEBSOCKET_ENDPOINT, WEBSOCKET_ENDPOINTS, WebSocketConfigService, WebSocketEndpoints, WebSocketService, adminGuard, authGuard, authInterceptor, resetRefreshState };
3332
3433
  //# sourceMappingURL=noatgnu-cupcake-core.mjs.map