@noatgnu/cupcake-core 1.2.8 → 1.2.10

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';
@@ -75,6 +75,114 @@ const InvitationStatusLabels = {
75
75
  */
76
76
  // Base types and enums
77
77
 
78
+ let isRefreshing = false;
79
+ let refreshFailedCount = 0;
80
+ let lastRefreshFailTime = 0;
81
+ const MAX_REFRESH_RETRIES = 3;
82
+ const REFRESH_RETRY_WINDOW_MS = 60000;
83
+ const refreshTokenSubject = new BehaviorSubject(null);
84
+ function resetRefreshState() {
85
+ refreshFailedCount = 0;
86
+ lastRefreshFailTime = 0;
87
+ isRefreshing = false;
88
+ }
89
+ const authInterceptor = (req, next) => {
90
+ const router = inject(Router);
91
+ const http = inject(HttpClient);
92
+ const config = inject(CUPCAKE_CORE_CONFIG);
93
+ if (req.url.includes('/auth/login/') ||
94
+ req.url.includes('/auth/token/') ||
95
+ req.url.includes('/auth/orcid/') ||
96
+ req.url.includes('/auth/register/') ||
97
+ req.url.includes('/site-config/public/')) {
98
+ return next(req);
99
+ }
100
+ const token = localStorage.getItem('ccvAccessToken');
101
+ let authReq = req;
102
+ if (token) {
103
+ authReq = addTokenToRequest(req, token);
104
+ }
105
+ return next(authReq).pipe(catchError((error) => {
106
+ if (error.status === 401) {
107
+ return handle401Error(authReq, next, http, router, config);
108
+ }
109
+ return throwError(() => error);
110
+ }));
111
+ };
112
+ function addTokenToRequest(request, token) {
113
+ return request.clone({
114
+ setHeaders: {
115
+ Authorization: `Bearer ${token}`
116
+ }
117
+ });
118
+ }
119
+ function handle401Error(request, next, http, router, config) {
120
+ const now = Date.now();
121
+ if (now - lastRefreshFailTime > REFRESH_RETRY_WINDOW_MS) {
122
+ refreshFailedCount = 0;
123
+ }
124
+ if (refreshFailedCount >= MAX_REFRESH_RETRIES) {
125
+ return throwError(() => new Error('Maximum token refresh attempts exceeded. Please log in again.'));
126
+ }
127
+ if (!isRefreshing) {
128
+ isRefreshing = true;
129
+ refreshTokenSubject.next(null);
130
+ const refreshToken = localStorage.getItem('ccvRefreshToken');
131
+ if (refreshToken) {
132
+ return http.post(`${config.apiUrl}/auth/token/refresh/`, {
133
+ refresh: refreshToken
134
+ }).pipe(switchMap((tokenResponse) => {
135
+ isRefreshing = false;
136
+ refreshFailedCount = 0;
137
+ lastRefreshFailTime = 0;
138
+ localStorage.setItem('ccvAccessToken', tokenResponse.access);
139
+ refreshTokenSubject.next(tokenResponse.access);
140
+ if (typeof window !== 'undefined') {
141
+ window.dispatchEvent(new CustomEvent('tokenRefreshed'));
142
+ }
143
+ return next(addTokenToRequest(request, tokenResponse.access));
144
+ }), catchError((refreshError) => {
145
+ isRefreshing = false;
146
+ refreshFailedCount++;
147
+ lastRefreshFailTime = Date.now();
148
+ localStorage.removeItem('ccvAccessToken');
149
+ localStorage.removeItem('ccvRefreshToken');
150
+ refreshTokenSubject.next(null);
151
+ if (typeof window !== 'undefined') {
152
+ window.dispatchEvent(new CustomEvent('authCleared'));
153
+ }
154
+ if (refreshFailedCount >= MAX_REFRESH_RETRIES) {
155
+ router.navigate(['/login'], {
156
+ queryParams: { returnUrl: router.url }
157
+ });
158
+ }
159
+ return throwError(() => refreshError);
160
+ }));
161
+ }
162
+ else {
163
+ isRefreshing = false;
164
+ refreshFailedCount++;
165
+ lastRefreshFailTime = Date.now();
166
+ localStorage.removeItem('ccvAccessToken');
167
+ localStorage.removeItem('ccvRefreshToken');
168
+ if (typeof window !== 'undefined') {
169
+ window.dispatchEvent(new CustomEvent('authCleared'));
170
+ }
171
+ if (refreshFailedCount >= MAX_REFRESH_RETRIES) {
172
+ router.navigate(['/login'], {
173
+ queryParams: { returnUrl: router.url }
174
+ });
175
+ }
176
+ return throwError(() => new Error('No refresh token available'));
177
+ }
178
+ }
179
+ else {
180
+ return refreshTokenSubject.pipe(filter((token) => token !== null), take(1), switchMap((token) => {
181
+ return next(addTokenToRequest(request, token));
182
+ }));
183
+ }
184
+ }
185
+
78
186
  const CUPCAKE_CORE_CONFIG = new InjectionToken('CUPCAKE_CORE_CONFIG');
79
187
  class AuthService {
80
188
  http = inject(HttpClient);
@@ -258,6 +366,7 @@ class AuthService {
258
366
  const convertedUser = this.convertUserFromSnakeToCamel(response.user);
259
367
  this.currentUserSubject.next(convertedUser);
260
368
  this.isAuthenticatedSubject.next(true);
369
+ resetRefreshState();
261
370
  }
262
371
  tryRefreshToken() {
263
372
  const refreshToken = this.getRefreshToken();
@@ -699,6 +808,17 @@ class ApiService {
699
808
  deleteAnnotationFolder(id) {
700
809
  return this.http.delete(`${this.apiUrl}/annotation-folders/${id}/`);
701
810
  }
811
+ /**
812
+ * WARNING: This method accesses the base annotation endpoint and should only be used
813
+ * for standalone annotations that are NOT attached to parent resources.
814
+ *
815
+ * For annotations attached to parent resources, use the specialized services instead:
816
+ * - Instrument annotations: Use InstrumentService from @noatgnu/cupcake-macaron
817
+ * - StoredReagent annotations: Use ReagentService from @noatgnu/cupcake-macaron
818
+ * - Session annotations: Use SessionService from @noatgnu/cupcake-red-velvet
819
+ *
820
+ * These specialized services ensure proper permission checking through parent resources.
821
+ */
702
822
  getAnnotations(params) {
703
823
  let httpParams = new HttpParams();
704
824
  if (params) {
@@ -714,9 +834,33 @@ class ApiService {
714
834
  results: response.results.map((annotation) => this.resourceService.transformLegacyResource(annotation))
715
835
  })));
716
836
  }
837
+ /**
838
+ * WARNING: This method accesses the base annotation endpoint and should only be used
839
+ * for standalone annotations that are NOT attached to parent resources.
840
+ *
841
+ * For annotations attached to parent resources, use the specialized services instead:
842
+ * - Instrument annotations: Use InstrumentService.getInstrumentAnnotation()
843
+ * - StoredReagent annotations: Use ReagentService.getStoredReagentAnnotation()
844
+ * - Session annotations: Use SessionService (session folder annotations)
845
+ *
846
+ * The backend enforces parent resource permissions, but using specialized services
847
+ * provides cleaner access control and better context.
848
+ */
717
849
  getAnnotation(id) {
718
850
  return this.http.get(`${this.apiUrl}/annotations/${id}/`).pipe(map$1(response => this.resourceService.transformLegacyResource(response)));
719
851
  }
852
+ /**
853
+ * WARNING: This method accesses the base annotation endpoint and should only be used
854
+ * for standalone annotations that are NOT attached to parent resources.
855
+ *
856
+ * For creating annotations attached to parent resources, use specialized upload methods:
857
+ * - Instrument annotations: Use InstrumentService.uploadAnnotation()
858
+ * - StoredReagent annotations: Use ReagentService.uploadAnnotation()
859
+ * - Session/Step annotations: Use the appropriate chunked upload service
860
+ *
861
+ * These specialized methods provide chunked upload support, progress tracking,
862
+ * and automatic binding to parent resources with proper permission enforcement.
863
+ */
720
864
  createAnnotation(annotationData) {
721
865
  const formData = new FormData();
722
866
  Object.keys(annotationData).forEach(key => {
@@ -732,10 +876,29 @@ class ApiService {
732
876
  });
733
877
  return this.http.post(`${this.apiUrl}/annotations/`, formData).pipe(map$1(response => this.resourceService.transformLegacyResource(response)));
734
878
  }
879
+ /**
880
+ * WARNING: This method accesses the base annotation endpoint and should only be used
881
+ * for standalone annotations that are NOT attached to parent resources.
882
+ *
883
+ * For annotations attached to parent resources, the backend enforces parent resource
884
+ * permissions. However, using specialized services provides better context and access control.
885
+ */
735
886
  updateAnnotation(id, annotationData) {
736
887
  const preparedData = this.resourceService.prepareForAPI(annotationData);
737
888
  return this.http.patch(`${this.apiUrl}/annotations/${id}/`, preparedData).pipe(map$1(response => this.resourceService.transformLegacyResource(response)));
738
889
  }
890
+ /**
891
+ * WARNING: This method accesses the base annotation endpoint and should only be used
892
+ * for standalone annotations that are NOT attached to parent resources.
893
+ *
894
+ * For deleting annotations attached to parent resources, use specialized services:
895
+ * - Instrument annotations: Use InstrumentService.deleteInstrumentAnnotation()
896
+ * - StoredReagent annotations: Use ReagentService.deleteStoredReagentAnnotation()
897
+ * - Session annotations: Use appropriate session/step annotation delete methods
898
+ *
899
+ * The backend enforces parent resource permissions, but using specialized services
900
+ * provides clearer intent and better access control context.
901
+ */
739
902
  deleteAnnotation(id) {
740
903
  return this.http.delete(`${this.apiUrl}/annotations/${id}/`);
741
904
  }
@@ -1475,7 +1638,7 @@ class WebSocketService {
1475
1638
  })).subscribe();
1476
1639
  }
1477
1640
  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));
1641
+ 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
1642
  }
1480
1643
  getNotifications() {
1481
1644
  return this.filterMessages('notification');
@@ -1856,89 +2019,6 @@ const adminGuard = (route, state) => {
1856
2019
  return true;
1857
2020
  };
1858
2021
 
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
2022
  class LoginComponent {
1943
2023
  authService = inject(AuthService);
1944
2024
  fb = inject(FormBuilder);
@@ -3328,5 +3408,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImpor
3328
3408
  * Generated bundle index. Do not edit.
3329
3409
  */
3330
3410
 
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 };
3411
+ 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, resetRefreshState };
3332
3412
  //# sourceMappingURL=noatgnu-cupcake-core.mjs.map