@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.
- package/fesm2022/noatgnu-cupcake-core.mjs +168 -88
- package/fesm2022/noatgnu-cupcake-core.mjs.map +1 -1
- package/index.d.ts +63 -2
- 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';
|
|
@@ -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
|