@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.
- package/fesm2022/noatgnu-cupcake-core.mjs +189 -88
- package/fesm2022/noatgnu-cupcake-core.mjs.map +1 -1
- package/index.d.ts +140 -3
- package/package.json +1 -1
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import {
|
|
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,
|
|
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
|