@arsedizioni/ars-utils 22.5.12 → 22.5.13
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/clipper.common/README.md +9 -3
- package/fesm2022/arsedizioni-ars-utils-clipper.common.mjs +927 -712
- package/fesm2022/arsedizioni-ars-utils-clipper.common.mjs.map +1 -1
- package/fesm2022/arsedizioni-ars-utils-clipper.ui.mjs +8 -8
- package/fesm2022/arsedizioni-ars-utils-clipper.ui.mjs.map +1 -1
- package/package.json +1 -1
- package/types/arsedizioni-ars-utils-clipper.common.d.ts +201 -55
- package/types/arsedizioni-ars-utils-clipper.ui.d.ts +6 -6
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
2
|
import { signal, computed, inject, Service, DestroyRef, Injector } from '@angular/core';
|
|
3
|
-
import {
|
|
4
|
-
import { BroadcastService,
|
|
5
|
-
import {
|
|
3
|
+
import { HttpResponse, HttpClient } from '@angular/common/http';
|
|
4
|
+
import { BroadcastService, SplashService, SystemUtils } from '@arsedizioni/ars-utils/core';
|
|
5
|
+
import { retry, timer, throwError, tap, catchError, EMPTY, finalize, map, of } from 'rxjs';
|
|
6
6
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
7
|
-
import { catchError as catchError$1
|
|
7
|
+
import { catchError as catchError$1 } from 'rxjs/operators';
|
|
8
8
|
|
|
9
9
|
const ClipperMessages = {
|
|
10
10
|
/**
|
|
@@ -12,6 +12,17 @@ const ClipperMessages = {
|
|
|
12
12
|
*/
|
|
13
13
|
// Error
|
|
14
14
|
ERROR: '§clipper-error',
|
|
15
|
+
/**
|
|
16
|
+
* The session is gone: a 401 on a request that was not a login attempt.
|
|
17
|
+
* Emitted once per dead session by `clipperAuthInterceptor`, which re-arms on the first
|
|
18
|
+
* good response.
|
|
19
|
+
*/
|
|
20
|
+
ERROR_401: '§clipper-error-401',
|
|
21
|
+
/**
|
|
22
|
+
* The API is not answering: the interceptor has already sent the request three times over
|
|
23
|
+
* ten seconds. Emitted once, and re-armed on the first good response.
|
|
24
|
+
*/
|
|
25
|
+
ERROR_UNAVAILABLE: '§clipper-error-unavailable',
|
|
15
26
|
SUCCESS: '§clipper-success',
|
|
16
27
|
SUCCESS_TOAST: '§clipper-success-toast',
|
|
17
28
|
// Login
|
|
@@ -2098,14 +2109,147 @@ class ClipperSearchUtils {
|
|
|
2098
2109
|
}
|
|
2099
2110
|
}
|
|
2100
2111
|
|
|
2101
|
-
/**
|
|
2112
|
+
/**
|
|
2113
|
+
* HTML text helpers with no dependencies of their own.
|
|
2114
|
+
*
|
|
2115
|
+
* Not exported from `public_api`: it is an internal detail of this entry point. `escapeHtml` lives
|
|
2116
|
+
* in a file of its own, and not next to the service that needs it, because `ClipperCoreService`
|
|
2117
|
+
* has to escape and everything else in its neighbourhood imports it back -- a helper that turns
|
|
2118
|
+
* text into text has no business being in that cycle.
|
|
2119
|
+
*/
|
|
2120
|
+
function escapeHtml(value) {
|
|
2121
|
+
if (value === undefined || value === null)
|
|
2122
|
+
return undefined;
|
|
2123
|
+
return String(value)
|
|
2124
|
+
.replaceAll('&', '&')
|
|
2125
|
+
.replaceAll('<', '<')
|
|
2126
|
+
.replaceAll('>', '>')
|
|
2127
|
+
.replaceAll('"', '"')
|
|
2128
|
+
.replaceAll("'", "'");
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
/** How many more times a request is sent when the API is not there. */
|
|
2132
|
+
const UNAVAILABLE_RETRIES = 2;
|
|
2133
|
+
/** How long to wait before each of those retries, in milliseconds. */
|
|
2134
|
+
const UNAVAILABLE_RETRY_DELAY = 5000;
|
|
2135
|
+
/** The statuses a server that is being replaced answers with, plus the one it cannot answer at all. */
|
|
2136
|
+
const UNAVAILABLE_STATUSES = [0, 500, 502, 503, 504];
|
|
2137
|
+
/** Minimum milliseconds between consecutive generic error broadcasts. */
|
|
2102
2138
|
const ERROR_DEBOUNCE_MS = 5000;
|
|
2139
|
+
/**
|
|
2140
|
+
* Returns whether the failure means the API is not there, rather than the request being wrong.
|
|
2141
|
+
*
|
|
2142
|
+
* The discriminator is the body, not the status. An error raised INSIDE the application arrives
|
|
2143
|
+
* through `ApiExceptionHandler` wrapped in the usual `ApiResult` envelope, so it has been seen,
|
|
2144
|
+
* parsed and acted upon: replaying it would run whatever it already ran a second time. A server
|
|
2145
|
+
* that is being replaced answers with IIS's own HTML, or with nothing at all — no envelope — and
|
|
2146
|
+
* that request never reached any handler. Only the second kind is retried, which is what makes it
|
|
2147
|
+
* safe to retry a POST.
|
|
2148
|
+
* @param error - The raw HTTP error object.
|
|
2149
|
+
* @returns True when the request is worth sending again.
|
|
2150
|
+
*/
|
|
2151
|
+
function isServiceUnavailable(error) {
|
|
2152
|
+
if (!UNAVAILABLE_STATUSES.includes(parseInt(error?.status ?? '0')))
|
|
2153
|
+
return false;
|
|
2154
|
+
const body = error?.error;
|
|
2155
|
+
const isApiEnvelope = !!body
|
|
2156
|
+
&& typeof body === 'object'
|
|
2157
|
+
&& !(body instanceof Blob)
|
|
2158
|
+
&& ('success' in body || 'problem' in body);
|
|
2159
|
+
return !isApiEnvelope;
|
|
2160
|
+
}
|
|
2161
|
+
/**
|
|
2162
|
+
* Returns whether the URL is an authentication endpoint where a 401 is a normal outcome — wrong
|
|
2163
|
+
* credentials, a pending code challenge, a rejected assertion — rather than an expired session.
|
|
2164
|
+
*
|
|
2165
|
+
* The list covers the whole Clipper 7.5 sign-in surface, including the ways in that this library
|
|
2166
|
+
* does not itself offer: a host application registers one interceptor for every call it makes to
|
|
2167
|
+
* the API, its own passkey and recovery flows included.
|
|
2168
|
+
* @param url - The request URL.
|
|
2169
|
+
* @returns True when the URL is a login/confirm/otp/oauth/passkey endpoint.
|
|
2170
|
+
*/
|
|
2171
|
+
function isAuthEndpoint(url) {
|
|
2172
|
+
return url.includes('/session/login')
|
|
2173
|
+
|| url.includes('/session/confirm')
|
|
2174
|
+
|| url.includes('/session/otp')
|
|
2175
|
+
|| url.includes('/session/oauth')
|
|
2176
|
+
|| url.includes('/passkeys/login')
|
|
2177
|
+
|| url.includes('/passkeys/request-options');
|
|
2178
|
+
}
|
|
2179
|
+
/**
|
|
2180
|
+
* Returns whether the failure means the session is gone rather than the request being wrong.
|
|
2181
|
+
* @param errorStatus - The numeric HTTP status code.
|
|
2182
|
+
* @param url - The request URL.
|
|
2183
|
+
* @returns True when the client must be sent back to the sign-in page.
|
|
2184
|
+
*/
|
|
2185
|
+
function isSessionExpired(errorStatus, url) {
|
|
2186
|
+
return errorStatus === 401 && !isAuthEndpoint(url);
|
|
2187
|
+
}
|
|
2188
|
+
/**
|
|
2189
|
+
* Resolves a user-facing error message based on the HTTP status code.
|
|
2190
|
+
*
|
|
2191
|
+
* There is no branch for status 0 any more, nor for the 5xx family: a request that got no answer,
|
|
2192
|
+
* or an answer with no `ApiResult` envelope, never reaches here — it is retried and then reported
|
|
2193
|
+
* as `ERROR_UNAVAILABLE`. What is left is the failures the API itself described.
|
|
2194
|
+
*
|
|
2195
|
+
* Whatever the API sends is escaped before anything else happens to it. The dialog that shows this
|
|
2196
|
+
* string binds it with `[innerHTML]` through a pipe that calls `bypassSecurityTrustHtml`, so an
|
|
2197
|
+
* error message echoing back something a user had typed would be rendered as markup rather than
|
|
2198
|
+
* read as text. The paragraph breaks in the default branch are applied AFTER the escape, which is
|
|
2199
|
+
* exactly why they survive it and nothing else does.
|
|
2200
|
+
* @param error - The raw HTTP error object.
|
|
2201
|
+
* @param errorStatus - The numeric HTTP status code.
|
|
2202
|
+
* @returns A localised error message, safe to render as HTML.
|
|
2203
|
+
*/
|
|
2204
|
+
function resolveErrorMessage(error, errorStatus) {
|
|
2205
|
+
const message = escapeHtml(error?.error?.message ?? error?.message);
|
|
2206
|
+
switch (errorStatus) {
|
|
2207
|
+
case 401:
|
|
2208
|
+
// A 401 that reaches here comes from a login attempt: wrong credentials, a rejected
|
|
2209
|
+
// assertion, or an account whose access was revoked.
|
|
2210
|
+
return message ?? 'Credenziali non valide o accesso non abilitato.';
|
|
2211
|
+
case 403:
|
|
2212
|
+
return 'Non hai i permessi necessari per eseguire l\'operazione richiesta.';
|
|
2213
|
+
case 429:
|
|
2214
|
+
return message ?? 'Troppe richieste consecutive. Aspetta qualche secondo.';
|
|
2215
|
+
default:
|
|
2216
|
+
return (message ?? 'Impossibile eseguire l\'operazione richiesta.')
|
|
2217
|
+
.replaceAll('\r\n', '</p><p>');
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2103
2220
|
/**
|
|
2104
2221
|
* Builds the Clipper auth HTTP interceptor.
|
|
2105
2222
|
*
|
|
2106
|
-
*
|
|
2107
|
-
*
|
|
2108
|
-
*
|
|
2223
|
+
* Attaches the credentials and the service-worker bypass header to every request aimed at the
|
|
2224
|
+
* Clipper API, and turns HTTP failures into broadcast messages — no UI dependency here.
|
|
2225
|
+
*
|
|
2226
|
+
* Takes the Clipper base URI and the service flags directly as arguments, captured in the closure,
|
|
2227
|
+
* so it never injects `ClipperService`: registering it does not pull the whole Clipper service into
|
|
2228
|
+
* the eager bundle. The debounce and the two notification latches live in that same closure, so two
|
|
2229
|
+
* applications registering two interceptors never share them.
|
|
2230
|
+
*
|
|
2231
|
+
* ## What the statuses mean
|
|
2232
|
+
*
|
|
2233
|
+
* Aligned with the Clipper 7.5 API on 2026-08-25. The meaning of the status codes changed with the
|
|
2234
|
+
* API: failures used to arrive as `200 OK` with `success: false`, and this interceptor had to guess.
|
|
2235
|
+
* Now the status is the truth — a 401 on an already-authenticated request means the session is
|
|
2236
|
+
* gone, a 403 means the permission is missing, and a 5xx means the server broke, which is not the
|
|
2237
|
+
* same thing at all and must not sign the user out.
|
|
2238
|
+
*
|
|
2239
|
+
* A dead session is broadcast as `ClipperMessages.ERROR_401`, for the host application to handle
|
|
2240
|
+
* centrally; every other failure stays a `ClipperMessages.ERROR` carrying a message. The two are
|
|
2241
|
+
* separate because they call for opposite things: one returns to the sign-in page, the other shows
|
|
2242
|
+
* a dialog and leaves the user where they are.
|
|
2243
|
+
*
|
|
2244
|
+
* Between them sits the third case, which is neither: the API is being replaced. A deployment
|
|
2245
|
+
* answers with a 5xx that has no `ApiResult` envelope, or does not answer at all, for the few
|
|
2246
|
+
* seconds the process takes to come back. That is not worth a dialog and not worth a redirect
|
|
2247
|
+
* either, so the request is simply sent again — twice, five seconds apart — and only if the third
|
|
2248
|
+
* attempt fails too does it become `ClipperMessages.ERROR_UNAVAILABLE`.
|
|
2249
|
+
*
|
|
2250
|
+
* `ClipperServiceFlags.NotifySystemErrors` still governs one narrow case, and only that one: a 5xx
|
|
2251
|
+
* that DOES carry an envelope, i.e. a server error the API itself described. Without the flag it is
|
|
2252
|
+
* swallowed, as it always was in this library.
|
|
2109
2253
|
*
|
|
2110
2254
|
* Register it with `withInterceptors`:
|
|
2111
2255
|
* @example
|
|
@@ -2118,48 +2262,35 @@ const ERROR_DEBOUNCE_MS = 5000;
|
|
|
2118
2262
|
* @returns An `HttpInterceptorFn` ready to register.
|
|
2119
2263
|
*/
|
|
2120
2264
|
function clipperAuthInterceptor(serviceUri, flags = ClipperServiceFlags.None) {
|
|
2121
|
-
|
|
2265
|
+
/** Timestamp of the last generic error shown, used to debounce repeated error messages. */
|
|
2122
2266
|
let lastErrorTime = -1;
|
|
2123
2267
|
/**
|
|
2124
|
-
*
|
|
2268
|
+
* Whether the unavailable-API notification has already been sent. A deployment fails every
|
|
2269
|
+
* request in flight, and one notification is enough. Re-armed on the next successful response.
|
|
2270
|
+
*/
|
|
2271
|
+
let unavailableNotified = false;
|
|
2272
|
+
/**
|
|
2273
|
+
* Whether the session-expiry notification has already been sent. Prevents a storm of dialogs when
|
|
2274
|
+
* several in-flight requests fail at once; re-armed on the next successful response.
|
|
2275
|
+
*/
|
|
2276
|
+
let sessionExpiredNotified = false;
|
|
2277
|
+
/**
|
|
2278
|
+
* Broadcasts a generic, user-facing error for a Clipper HTTP failure, debounced.
|
|
2125
2279
|
* @param error - The raw error value thrown by the HTTP layer.
|
|
2280
|
+
* @param errorStatus - The numeric HTTP status code.
|
|
2126
2281
|
* @param broadcastService - The broadcast service for the current request.
|
|
2127
2282
|
* @returns void
|
|
2128
2283
|
*/
|
|
2129
|
-
function
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
// Reject only errors that explicitly belong to a different service; a missing
|
|
2133
|
-
// url (network failure on a Clipper request) is allowed through.
|
|
2134
|
-
if (error.url && !error.url.startsWith(serviceUri))
|
|
2135
|
-
return;
|
|
2136
|
-
const errorStatus = error.status;
|
|
2137
|
-
const shouldNotify = errorStatus === 0 ||
|
|
2138
|
-
(errorStatus > 0 && errorStatus < 500) ||
|
|
2139
|
-
(flags & ClipperServiceFlags.NotifySystemErrors) > 0;
|
|
2140
|
-
if (!shouldNotify)
|
|
2284
|
+
function notifyError(error, errorStatus, broadcastService) {
|
|
2285
|
+
// A 5xx the API described itself is the one case the flag still governs.
|
|
2286
|
+
if (errorStatus >= 500 && (flags & ClipperServiceFlags.NotifySystemErrors) === 0)
|
|
2141
2287
|
return;
|
|
2142
2288
|
const now = Date.now();
|
|
2143
2289
|
if (now - lastErrorTime <= ERROR_DEBOUNCE_MS)
|
|
2144
2290
|
return;
|
|
2145
2291
|
lastErrorTime = now;
|
|
2146
|
-
let message;
|
|
2147
|
-
switch (errorStatus) {
|
|
2148
|
-
case 0:
|
|
2149
|
-
message = "In questo momento Clipper non è disponibile. Riprova tra qualche minuto.";
|
|
2150
|
-
break;
|
|
2151
|
-
case 403:
|
|
2152
|
-
message = "Non hai i permessi necessari per eseguire l'operazione richiesta.";
|
|
2153
|
-
break;
|
|
2154
|
-
default:
|
|
2155
|
-
message = (error.error?.['message'] ??
|
|
2156
|
-
error.message ??
|
|
2157
|
-
"Impossibile eseguire l'operazione richiesta.").replaceAll('\r\n', '</p><p>');
|
|
2158
|
-
break;
|
|
2159
|
-
}
|
|
2160
2292
|
broadcastService.sendMessage(ClipperMessages.ERROR, {
|
|
2161
|
-
|
|
2162
|
-
message,
|
|
2293
|
+
message: resolveErrorMessage(error, errorStatus),
|
|
2163
2294
|
title: "Errore in Clipper",
|
|
2164
2295
|
errorStatus,
|
|
2165
2296
|
service: serviceUri
|
|
@@ -2178,848 +2309,925 @@ function clipperAuthInterceptor(serviceUri, flags = ClipperServiceFlags.None) {
|
|
|
2178
2309
|
'X-Client-Id': sessionStorage.getItem('clipper_client_id') ?? ''
|
|
2179
2310
|
}
|
|
2180
2311
|
});
|
|
2181
|
-
return next(authenticatedRequest).pipe(
|
|
2182
|
-
|
|
2183
|
-
|
|
2312
|
+
return next(authenticatedRequest).pipe(
|
|
2313
|
+
// An API that is being replaced is not an error to show, it is a wait: two more attempts,
|
|
2314
|
+
// five seconds apart. Anything else is rethrown on the spot, so a request that genuinely
|
|
2315
|
+
// failed still fails immediately.
|
|
2316
|
+
retry({
|
|
2317
|
+
count: UNAVAILABLE_RETRIES,
|
|
2318
|
+
delay: error => isServiceUnavailable(error)
|
|
2319
|
+
? timer(UNAVAILABLE_RETRY_DELAY)
|
|
2320
|
+
: throwError(() => error)
|
|
2321
|
+
}), tap(event => {
|
|
2322
|
+
// A successful response means the session is valid again, and the API is back: re-arm both
|
|
2323
|
+
// notifiers.
|
|
2324
|
+
if (event instanceof HttpResponse) {
|
|
2325
|
+
sessionExpiredNotified = false;
|
|
2326
|
+
unavailableNotified = false;
|
|
2327
|
+
}
|
|
2328
|
+
}), catchError((error) => {
|
|
2329
|
+
// Which request this was is read from the request, not from `error.url`: a failure with no
|
|
2330
|
+
// response — the browser refusing the connection while the API restarts — carries no URL
|
|
2331
|
+
// at all. Everything that gets here is a Clipper request anyway: the others returned
|
|
2332
|
+
// before the pipe was built.
|
|
2333
|
+
const errorStatus = parseInt(error?.status ?? '0');
|
|
2334
|
+
if (isServiceUnavailable(error)) {
|
|
2335
|
+
// Three attempts over ten seconds and the API is still not answering. There is nothing
|
|
2336
|
+
// left to retry, so the host application is told once, however many requests were in
|
|
2337
|
+
// flight.
|
|
2338
|
+
if (!unavailableNotified) {
|
|
2339
|
+
unavailableNotified = true;
|
|
2340
|
+
broadcastService.sendMessage(ClipperMessages.ERROR_UNAVAILABLE);
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
else if (isSessionExpired(errorStatus, authenticatedRequest.url)) {
|
|
2344
|
+
// Expired or invalidated session on an authenticated request. Notified once: several
|
|
2345
|
+
// in-flight requests failing together is how a single expiry becomes a wall of dialogs.
|
|
2346
|
+
if (!sessionExpiredNotified) {
|
|
2347
|
+
sessionExpiredNotified = true;
|
|
2348
|
+
broadcastService.sendMessage(ClipperMessages.ERROR_401);
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
else {
|
|
2352
|
+
// Any other failure, including a 401 from a login attempt.
|
|
2353
|
+
notifyError(error, errorStatus, broadcastService);
|
|
2354
|
+
}
|
|
2355
|
+
// Consumed, not rethrown — as in Clipper 7.5. The failure has been reported on the bus, and
|
|
2356
|
+
// rethrowing it a second time only gave every subscriber its own chance to report it again.
|
|
2357
|
+
// A caller that must know how a request ended reads `ApiResult.success`; one that subscribes
|
|
2358
|
+
// to a failing request now simply completes without emitting.
|
|
2359
|
+
return EMPTY;
|
|
2184
2360
|
}));
|
|
2185
2361
|
};
|
|
2186
2362
|
}
|
|
2187
2363
|
|
|
2188
2364
|
/**
|
|
2189
|
-
*
|
|
2190
|
-
*
|
|
2365
|
+
* Authentication: password login, e-mailed code confirmation, logout and session restore.
|
|
2366
|
+
* The login context, the channels and the logged-in flag live in `ClipperCoreService`.
|
|
2367
|
+
*
|
|
2368
|
+
* Aligned with the Clipper 7.5 API on 2026-08-25. Everything here now goes through `/session/*`
|
|
2369
|
+
* and an HttpOnly cookie: there is no bearer token, no `remember` credential the API can replay,
|
|
2370
|
+
* and no OAuth token acquired by the caller — the external-provider flow is a full-page redirect
|
|
2371
|
+
* handled server-side, and all that reaches this service is the `/session/me` call on the way back.
|
|
2372
|
+
*
|
|
2373
|
+
* This is a deliberately reduced subset of what the Clipper application itself does. Passkeys and
|
|
2374
|
+
* the passwordless recovery code (`/session/otp`) are not here: a library whose job is showing
|
|
2375
|
+
* documents signs in with a password, and an application that offers the other ways in owns them.
|
|
2191
2376
|
*/
|
|
2192
|
-
class
|
|
2377
|
+
class ClipperLoginService {
|
|
2193
2378
|
constructor() {
|
|
2194
2379
|
this.httpClient = inject(HttpClient);
|
|
2195
2380
|
this.broadcastService = inject(BroadcastService);
|
|
2381
|
+
this.splashService = inject(SplashService);
|
|
2196
2382
|
this.core = inject(ClipperCoreService);
|
|
2383
|
+
/** Whether {@link initialize} has already run. */
|
|
2384
|
+
this.initialized = false;
|
|
2197
2385
|
}
|
|
2198
|
-
|
|
2199
|
-
//
|
|
2200
|
-
|
|
2386
|
+
////
|
|
2387
|
+
// BOOTSTRAP
|
|
2388
|
+
////
|
|
2201
2389
|
/**
|
|
2202
|
-
*
|
|
2203
|
-
*
|
|
2204
|
-
*
|
|
2390
|
+
* Validates a context that survived a page refresh before trusting it.
|
|
2391
|
+
*
|
|
2392
|
+
* Called by `ClipperService.initialize`, right after `ClipperCoreService.initialize`, so a
|
|
2393
|
+
* consumer gets the restore for free and never has to know this method exists. The auth cookie is
|
|
2394
|
+
* HttpOnly and may have expired while the stored context lived on, so a stored context only ever
|
|
2395
|
+
* means "there WAS a session" — this is what turns it into "there is one".
|
|
2396
|
+
*
|
|
2397
|
+
* A host application that drives its own return from an external provider, or that starts on its
|
|
2398
|
+
* own sign-in page, should call `core.setLoggedIn(false)` before bootstrapping rather than let
|
|
2399
|
+
* this race its own restore.
|
|
2400
|
+
* @returns void
|
|
2205
2401
|
*/
|
|
2206
|
-
|
|
2207
|
-
|
|
2402
|
+
initialize() {
|
|
2403
|
+
if (this.initialized)
|
|
2404
|
+
return;
|
|
2405
|
+
this.initialized = true;
|
|
2406
|
+
if (this.core.loggedIn()) {
|
|
2407
|
+
this.core.loggingIn.set(true);
|
|
2408
|
+
this.restoreSession();
|
|
2409
|
+
}
|
|
2208
2410
|
}
|
|
2209
2411
|
/**
|
|
2210
|
-
*
|
|
2211
|
-
*
|
|
2212
|
-
*
|
|
2412
|
+
* Asks `/session/me` whether the restored session is still alive. On success the login is
|
|
2413
|
+
* finalised from the current claims; on a dead session the interceptor's 401 handling takes over.
|
|
2414
|
+
*
|
|
2415
|
+
* Nothing is cleared on failure, and that is deliberate: a failure here is not proof of a dead
|
|
2416
|
+
* session — the API may be unreachable, the proxy may have answered 502, the device may be
|
|
2417
|
+
* offline. Clearing on any of those would sign the user out with a perfectly valid cookie.
|
|
2418
|
+
* @returns void
|
|
2213
2419
|
*/
|
|
2214
|
-
|
|
2215
|
-
|
|
2420
|
+
restoreSession() {
|
|
2421
|
+
this.me()
|
|
2422
|
+
.pipe(finalize(() => this.core.loggingIn.set(false)))
|
|
2423
|
+
.subscribe(r => {
|
|
2424
|
+
if (r?.success) {
|
|
2425
|
+
this.broadcastService.sendMessage(ClipperMessages.LOGIN_COMPLETED);
|
|
2426
|
+
}
|
|
2427
|
+
});
|
|
2216
2428
|
}
|
|
2429
|
+
////
|
|
2430
|
+
// SESSION
|
|
2431
|
+
////
|
|
2217
2432
|
/**
|
|
2218
|
-
*
|
|
2219
|
-
*
|
|
2220
|
-
*
|
|
2433
|
+
* Authenticates the user with email and password.
|
|
2434
|
+
*
|
|
2435
|
+
* Nothing is replayed from a stored credential any more: the former "remember me" token, which
|
|
2436
|
+
* let the API sign a caller in from something kept for sixty days, is gone. A login always starts
|
|
2437
|
+
* from what the user just typed.
|
|
2438
|
+
* @param email - The user's email address.
|
|
2439
|
+
* @param password - The user's password.
|
|
2440
|
+
* @returns An observable emitting the API result wrapping the login result.
|
|
2221
2441
|
*/
|
|
2222
|
-
|
|
2223
|
-
|
|
2442
|
+
login(email, password) {
|
|
2443
|
+
this.splashService.setMessage('Accesso in corso...');
|
|
2444
|
+
return this.httpClient
|
|
2445
|
+
.post(this.core.serviceUri + '/session/login', { user: email ?? '', password: password ?? '' })
|
|
2446
|
+
.pipe(catchError(err => throwError(() => err)), map(r => {
|
|
2447
|
+
if (r.success) {
|
|
2448
|
+
if (r.value.requiresMfa) {
|
|
2449
|
+
// Notify the login is pending an e-mailed code.
|
|
2450
|
+
this.broadcastService.sendMessage(ClipperMessages.LOGIN_PENDING, {});
|
|
2451
|
+
}
|
|
2452
|
+
else {
|
|
2453
|
+
this.completeLogin(r.value);
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
return r;
|
|
2457
|
+
}));
|
|
2224
2458
|
}
|
|
2225
2459
|
/**
|
|
2226
|
-
*
|
|
2227
|
-
*
|
|
2228
|
-
*
|
|
2460
|
+
* Confirms a pending e-mailed code challenge, which establishes the session.
|
|
2461
|
+
*
|
|
2462
|
+
* The address travels with the code because the API stores each code under the account it was
|
|
2463
|
+
* issued to: a code alone is not redeemable, which is what stops a guessed code from opening
|
|
2464
|
+
* whatever session happens to be pending. It also keeps the code out of the URL, where it used to
|
|
2465
|
+
* sit — `/login/confirm/{code}` put it in the path, and therefore in the server logs and in the
|
|
2466
|
+
* browser history.
|
|
2467
|
+
* @param code - The one-time confirmation code provided to the user.
|
|
2468
|
+
* @param email - The address the code was sent to.
|
|
2469
|
+
* @returns An observable emitting the API result wrapping the login result.
|
|
2229
2470
|
*/
|
|
2230
|
-
|
|
2231
|
-
return this.httpClient
|
|
2471
|
+
confirm(code, email) {
|
|
2472
|
+
return this.httpClient
|
|
2473
|
+
.post(this.core.serviceUri + '/session/confirm', { email: email, code: code })
|
|
2474
|
+
.pipe(catchError(err => throwError(() => err)), map(r => {
|
|
2475
|
+
if (r.success) {
|
|
2476
|
+
this.completeLogin(r.value);
|
|
2477
|
+
}
|
|
2478
|
+
return r;
|
|
2479
|
+
}));
|
|
2232
2480
|
}
|
|
2233
2481
|
/**
|
|
2234
|
-
*
|
|
2235
|
-
* @
|
|
2236
|
-
* @returns An observable emitting the exported content as a blob.
|
|
2482
|
+
* Logs the user out on the server and clears the local state.
|
|
2483
|
+
* @returns An observable that emits once the logout has been processed.
|
|
2237
2484
|
*/
|
|
2238
|
-
|
|
2239
|
-
return this.httpClient
|
|
2485
|
+
logout() {
|
|
2486
|
+
return this.httpClient
|
|
2487
|
+
.post(this.core.serviceUri + '/session/logout', {})
|
|
2488
|
+
.pipe(finalize(() => this.core.clear(true)), catchError(() => of({ success: false })));
|
|
2240
2489
|
}
|
|
2241
2490
|
/**
|
|
2242
|
-
*
|
|
2243
|
-
*
|
|
2244
|
-
*
|
|
2491
|
+
* Reads the current session from the server, which resolves it from the auth cookie claims
|
|
2492
|
+
* without touching the database. Used to restore the state after a page refresh instead of
|
|
2493
|
+
* trusting what local storage holds, and to close an external-provider round-trip.
|
|
2494
|
+
* @returns An observable emitting the current session.
|
|
2245
2495
|
*/
|
|
2246
|
-
|
|
2247
|
-
return this.httpClient
|
|
2496
|
+
me() {
|
|
2497
|
+
return this.httpClient
|
|
2498
|
+
.get(this.core.serviceUri + '/session/me', { withCredentials: true })
|
|
2499
|
+
.pipe(map(r => {
|
|
2500
|
+
if (r.success) {
|
|
2501
|
+
this.completeLogin(r.value, false);
|
|
2502
|
+
}
|
|
2503
|
+
return r;
|
|
2504
|
+
}));
|
|
2248
2505
|
}
|
|
2249
2506
|
/**
|
|
2250
|
-
*
|
|
2251
|
-
*
|
|
2252
|
-
*
|
|
2507
|
+
* Finalises the login flow after an authentication, a code confirmation or a session restore:
|
|
2508
|
+
* writes the context into the shared state, raises the logged-in signal, rebuilds the channel
|
|
2509
|
+
* list and announces `LOGIN_COMPLETED`.
|
|
2510
|
+
*
|
|
2511
|
+
* `loggingIn` is deliberately left alone. Whoever raised it lowers it — {@link restoreSession}
|
|
2512
|
+
* for a refresh, the host application for an external-login round-trip — because lowering it
|
|
2513
|
+
* here would drop the splash the instant the response arrived, before anything was on screen.
|
|
2514
|
+
* @param result - The login result returned by the API.
|
|
2515
|
+
* @param notify - Whether to broadcast `LOGIN_COMPLETED`. Defaults to `true`.
|
|
2516
|
+
* @returns void
|
|
2253
2517
|
*/
|
|
2254
|
-
|
|
2255
|
-
|
|
2518
|
+
completeLogin(result, notify = true) {
|
|
2519
|
+
this.core.updateContext(result);
|
|
2520
|
+
this.core.setLoggedIn(true);
|
|
2521
|
+
this.core.initializeChannels();
|
|
2522
|
+
if (notify) {
|
|
2523
|
+
this.broadcastService.sendMessage(ClipperMessages.LOGIN_COMPLETED);
|
|
2524
|
+
}
|
|
2256
2525
|
}
|
|
2257
2526
|
/**
|
|
2258
|
-
*
|
|
2259
|
-
* @param id - The ID of the
|
|
2260
|
-
* @returns An observable emitting the API result wrapping the
|
|
2527
|
+
* Requests a new one-time password used to authorise a binary download.
|
|
2528
|
+
* @param id - The repository ID of the binary the OTP is generated for.
|
|
2529
|
+
* @returns An observable emitting the API result wrapping the generated OTP info.
|
|
2261
2530
|
*/
|
|
2262
|
-
|
|
2263
|
-
return this.httpClient.get(this.core.serviceUri + '/
|
|
2531
|
+
newOTP(id) {
|
|
2532
|
+
return this.httpClient.get(this.core.serviceUri + '/otp/new/?id=' + id);
|
|
2264
2533
|
}
|
|
2534
|
+
////
|
|
2535
|
+
// CONTEXT (re-published from ClipperCoreService for consumer convenience)
|
|
2536
|
+
////
|
|
2265
2537
|
/**
|
|
2266
|
-
*
|
|
2267
|
-
*
|
|
2268
|
-
* @returns
|
|
2538
|
+
* Persists the current login context to `localStorage`.
|
|
2539
|
+
* Delegates to `ClipperCoreService.storeContext()`.
|
|
2540
|
+
* @returns void
|
|
2269
2541
|
*/
|
|
2270
|
-
|
|
2271
|
-
|
|
2542
|
+
storeContext() {
|
|
2543
|
+
this.core.storeContext();
|
|
2272
2544
|
}
|
|
2273
2545
|
/**
|
|
2274
|
-
*
|
|
2275
|
-
*
|
|
2276
|
-
*
|
|
2546
|
+
* Clears every trace of the current session on this device: the stored context, the logged-in
|
|
2547
|
+
* signal and the session-storage keys. Does NOT call the server — use {@link logout} for that.
|
|
2548
|
+
* Delegates to `ClipperCoreService.clear()`.
|
|
2549
|
+
* @param clearOAuthToken - Whether to drop the legacy OAuth token too. Defaults to `true`.
|
|
2550
|
+
* @returns void
|
|
2277
2551
|
*/
|
|
2278
|
-
|
|
2279
|
-
|
|
2552
|
+
clear(clearOAuthToken = true) {
|
|
2553
|
+
this.core.clear(clearOAuthToken);
|
|
2554
|
+
}
|
|
2555
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: ClipperLoginService, deps: [], target: i0.ɵɵFactoryTarget.Service }); }
|
|
2556
|
+
static { this.ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.1.3", ngImport: i0, type: ClipperLoginService }); }
|
|
2557
|
+
}
|
|
2558
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: ClipperLoginService, decorators: [{
|
|
2559
|
+
type: Service
|
|
2560
|
+
}] });
|
|
2561
|
+
|
|
2562
|
+
/**
|
|
2563
|
+
* Shared state and bootstrap for the Clipper application.
|
|
2564
|
+
*
|
|
2565
|
+
* Holds every piece of state used by more than one feature service (service URI,
|
|
2566
|
+
* flags, login context, dashboard, working-documents bag, channels and the various
|
|
2567
|
+
* UI signals) plus the low-level helpers that mutate that state.
|
|
2568
|
+
*
|
|
2569
|
+
* The feature services (`ClipperLoginService`, `ClipperDocumentsService`, ...) inject
|
|
2570
|
+
* this service to read shared state and call shared helpers. `ClipperService` (the
|
|
2571
|
+
* barrel/facade) re-exports the state declared here.
|
|
2572
|
+
*/
|
|
2573
|
+
class ClipperCoreService {
|
|
2574
|
+
constructor() {
|
|
2575
|
+
this.httpClient = inject(HttpClient);
|
|
2576
|
+
this.destroyRef = inject(DestroyRef);
|
|
2577
|
+
this.broadcastService = inject(BroadcastService);
|
|
2578
|
+
/** Used to lazily resolve feature services inside `initialize`, avoiding circular DI. */
|
|
2579
|
+
this.injector = inject(Injector);
|
|
2580
|
+
this.broadcastInitialized = false;
|
|
2581
|
+
this._serviceUri = '';
|
|
2582
|
+
this._flags = ClipperServiceFlags.None;
|
|
2583
|
+
/**
|
|
2584
|
+
* Seeded from the stored context, which is the only trace a session leaves on this device: the
|
|
2585
|
+
* auth cookie is HttpOnly and unreadable from here. It means "there WAS a session", never "there
|
|
2586
|
+
* is one" -- `ClipperLoginService.initialize` validates it against `/session/me` before it is
|
|
2587
|
+
* trusted, and a failure there ends as a 401 that clears the session.
|
|
2588
|
+
*
|
|
2589
|
+
* It used to be seeded from a `clipper_oauth_token` session-storage key, which nothing has
|
|
2590
|
+
* written since the provider flow moved server-side: the flag was therefore false at every
|
|
2591
|
+
* bootstrap and the whole restore path could never run.
|
|
2592
|
+
*/
|
|
2593
|
+
this._loggedIn = signal(localStorage.getItem('clipper_context') !== null, /* @ts-ignore */
|
|
2594
|
+
...(ngDevMode ? [{ debugName: "_loggedIn" }] : /* istanbul ignore next */ []));
|
|
2595
|
+
this.loggedIn = this._loggedIn.asReadonly();
|
|
2596
|
+
this.loggingIn = signal(false, /* @ts-ignore */
|
|
2597
|
+
...(ngDevMode ? [{ debugName: "loggingIn" }] : /* istanbul ignore next */ []));
|
|
2598
|
+
this.snapshot = signal(undefined, /* @ts-ignore */
|
|
2599
|
+
...(ngDevMode ? [{ debugName: "snapshot" }] : /* istanbul ignore next */ []));
|
|
2600
|
+
this.supportsRS = signal(false, /* @ts-ignore */
|
|
2601
|
+
...(ngDevMode ? [{ debugName: "supportsRS" }] : /* istanbul ignore next */ []));
|
|
2602
|
+
this.referencesSnapshot = signal(undefined, /* @ts-ignore */
|
|
2603
|
+
...(ngDevMode ? [{ debugName: "referencesSnapshot" }] : /* istanbul ignore next */ []));
|
|
2604
|
+
this.dashboard = new ClipperDashboard();
|
|
2605
|
+
this.bag = signal([], /* @ts-ignore */
|
|
2606
|
+
...(ngDevMode ? [{ debugName: "bag" }] : /* istanbul ignore next */ []));
|
|
2607
|
+
this.bagTotal = computed(() => this.bag().length, /* @ts-ignore */
|
|
2608
|
+
...(ngDevMode ? [{ debugName: "bagTotal" }] : /* istanbul ignore next */ []));
|
|
2609
|
+
this.visible = signal(false, /* @ts-ignore */
|
|
2610
|
+
...(ngDevMode ? [{ debugName: "visible" }] : /* istanbul ignore next */ []));
|
|
2611
|
+
this.availableChannels = signal([], /* @ts-ignore */
|
|
2612
|
+
...(ngDevMode ? [{ debugName: "availableChannels" }] : /* istanbul ignore next */ []));
|
|
2613
|
+
this.activeChannels = computed(() => {
|
|
2614
|
+
return this.availableChannels()?.filter(x => !x.suspended && !x.disabled && x.active === true);
|
|
2615
|
+
}, /* @ts-ignore */
|
|
2616
|
+
...(ngDevMode ? [{ debugName: "activeChannels" }] : /* istanbul ignore next */ []));
|
|
2617
|
+
this.allowTags = signal(false, /* @ts-ignore */
|
|
2618
|
+
...(ngDevMode ? [{ debugName: "allowTags" }] : /* istanbul ignore next */ []));
|
|
2619
|
+
}
|
|
2620
|
+
/** @returns The URI of the Clipper web application, or `undefined` if not set. */
|
|
2621
|
+
get appUri() {
|
|
2622
|
+
return this._appUri;
|
|
2623
|
+
}
|
|
2624
|
+
/** @returns The base URI of the Clipper REST API. */
|
|
2625
|
+
get serviceUri() {
|
|
2626
|
+
return this._serviceUri;
|
|
2627
|
+
}
|
|
2628
|
+
/** @returns The active feature flags. */
|
|
2629
|
+
get flags() {
|
|
2630
|
+
return this._flags;
|
|
2280
2631
|
}
|
|
2281
2632
|
/**
|
|
2282
|
-
*
|
|
2283
|
-
* @
|
|
2284
|
-
* @returns An observable emitting the API result wrapping the last update string.
|
|
2633
|
+
* Lazily loads the login context from `localStorage` on first access.
|
|
2634
|
+
* @returns The current login context, or `undefined` if not authenticated.
|
|
2285
2635
|
*/
|
|
2286
|
-
|
|
2287
|
-
|
|
2636
|
+
get loginInfo() {
|
|
2637
|
+
if (!this._loginInfo) {
|
|
2638
|
+
const loginInfo = localStorage.getItem('clipper_context');
|
|
2639
|
+
if (loginInfo) {
|
|
2640
|
+
try {
|
|
2641
|
+
this._loginInfo = JSON.parse(loginInfo);
|
|
2642
|
+
}
|
|
2643
|
+
catch { }
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
return this._loginInfo;
|
|
2288
2647
|
}
|
|
2648
|
+
////
|
|
2649
|
+
// SHARED MUTATORS
|
|
2650
|
+
////
|
|
2289
2651
|
/**
|
|
2290
|
-
*
|
|
2291
|
-
*
|
|
2292
|
-
*
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
switch (mode) {
|
|
2299
|
-
case ClipperQueryReferencesMode.ReferencesIn:
|
|
2300
|
-
case ClipperQueryReferencesMode.ReferencesOut:
|
|
2301
|
-
params.mode = mode;
|
|
2302
|
-
return this.httpClient.post(this.core.serviceUri + '/documents/references', params);
|
|
2303
|
-
case ClipperQueryReferencesMode.ChangesIn:
|
|
2304
|
-
case ClipperQueryReferencesMode.ChangesOut:
|
|
2305
|
-
return this.httpClient.post(this.core.serviceUri + '/documents/changes', params);
|
|
2306
|
-
case ClipperQueryReferencesMode.Juris:
|
|
2307
|
-
return this.httpClient.post(this.core.serviceUri + '/documents/juris', params);
|
|
2308
|
-
default:
|
|
2309
|
-
return null;
|
|
2310
|
-
}
|
|
2311
|
-
}
|
|
2312
|
-
/**
|
|
2313
|
-
* Retrieves the facets for a document references query.
|
|
2314
|
-
* @param params - The references search parameters; `mode` defaults to `ReferencesIn`.
|
|
2315
|
-
* @returns An observable emitting the API result wrapping the search facets, or `null` for an unsupported mode.
|
|
2652
|
+
* Ensures a login-info object exists and returns it for further mutation.
|
|
2653
|
+
* Replaces the inline `if (!this._loginInfo) { ... }` guards now that the field
|
|
2654
|
+
* is private to this service.
|
|
2655
|
+
*
|
|
2656
|
+
* Reads through the getter, never the field: the context is loaded from storage lazily, so a
|
|
2657
|
+
* caller that mutates before anyone has read would find the field empty and start from a blank
|
|
2658
|
+
* object, silently dropping a perfectly good stored context.
|
|
2659
|
+
* @returns The existing login-info object, or a freshly created empty one.
|
|
2316
2660
|
*/
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
case ClipperQueryReferencesMode.ReferencesIn:
|
|
2321
|
-
case ClipperQueryReferencesMode.ReferencesOut:
|
|
2322
|
-
params.mode = mode;
|
|
2323
|
-
return this.httpClient.post(this.core.serviceUri + '/documents/references/facets', params);
|
|
2324
|
-
case ClipperQueryReferencesMode.ChangesIn:
|
|
2325
|
-
case ClipperQueryReferencesMode.ChangesOut:
|
|
2326
|
-
return this.httpClient.post(this.core.serviceUri + '/documents/changes/facets', params);
|
|
2327
|
-
case ClipperQueryReferencesMode.Juris:
|
|
2328
|
-
return this.httpClient.post(this.core.serviceUri + '/documents/juris/facets', params);
|
|
2329
|
-
default: return null;
|
|
2661
|
+
ensureLoginInfo() {
|
|
2662
|
+
if (!this.loginInfo) {
|
|
2663
|
+
this._loginInfo = { context: undefined };
|
|
2330
2664
|
}
|
|
2665
|
+
return this._loginInfo;
|
|
2331
2666
|
}
|
|
2332
2667
|
/**
|
|
2333
|
-
*
|
|
2334
|
-
*
|
|
2335
|
-
|
|
2336
|
-
preRender() {
|
|
2337
|
-
return this.httpClient.get(this.core.serviceUri + '/documents/pre-render?nocache=' + SystemUtils.generateUUID());
|
|
2338
|
-
}
|
|
2339
|
-
/**
|
|
2340
|
-
* Gets the jurisprudence articles for a document query.
|
|
2341
|
-
* @param params - The document search parameters.
|
|
2342
|
-
* @returns An observable emitting the API result wrapping the search result.
|
|
2668
|
+
* Sets the logged-in state. The backing signal is read-only to consumers, so the
|
|
2669
|
+
* feature services use this method instead.
|
|
2670
|
+
* @param value - The new logged-in state.
|
|
2343
2671
|
*/
|
|
2344
|
-
|
|
2345
|
-
|
|
2672
|
+
setLoggedIn(value) {
|
|
2673
|
+
this._loggedIn.set(value);
|
|
2346
2674
|
}
|
|
2347
2675
|
/**
|
|
2348
|
-
*
|
|
2349
|
-
*
|
|
2350
|
-
*
|
|
2676
|
+
* Persists the current login context to `localStorage`, but only when there is a context to
|
|
2677
|
+
* persist. A login-info object with no `context` is not a session: it is the empty shell
|
|
2678
|
+
* `ensureLoginInfo` hands out before anything has been written into it, and storing it would leave
|
|
2679
|
+
* behind a key whose mere presence makes the next bootstrap declare the user logged in.
|
|
2351
2680
|
*/
|
|
2352
|
-
|
|
2353
|
-
|
|
2681
|
+
storeContext() {
|
|
2682
|
+
// Through the getter, never the field: the context is loaded from storage lazily, so a caller
|
|
2683
|
+
// that stores before anyone has read would otherwise find the field empty and DELETE a
|
|
2684
|
+
// perfectly good stored context.
|
|
2685
|
+
if (!this.loginInfo?.context) {
|
|
2686
|
+
// Nothing to store. Falling through would call `JSON.stringify(undefined)`, whose result is
|
|
2687
|
+
// the value `undefined`, which local storage writes as the nine-letter STRING "undefined" --
|
|
2688
|
+
// a key that exists, parses to nothing, and makes the next bootstrap believe a session was
|
|
2689
|
+
// left behind here. Removing it says the truth instead.
|
|
2690
|
+
localStorage.removeItem('clipper_context');
|
|
2691
|
+
return;
|
|
2692
|
+
}
|
|
2693
|
+
localStorage.setItem('clipper_context', JSON.stringify(this._loginInfo));
|
|
2354
2694
|
}
|
|
2355
2695
|
/**
|
|
2356
|
-
*
|
|
2357
|
-
*
|
|
2358
|
-
*
|
|
2696
|
+
* Updates the stored login context with the values from a fresh login result.
|
|
2697
|
+
*
|
|
2698
|
+
* Each half is written only when the server actually sent it. An absent `context` or `settings`
|
|
2699
|
+
* means "not carried by this response", never "the user has none": overwriting on absence is what
|
|
2700
|
+
* made a page refresh empty the channel list, because `/session/me` answers without the settings
|
|
2701
|
+
* and this method wrote that emptiness -- through `storeContext`, onto the disk, where it survived
|
|
2702
|
+
* the reload it was born in. An EMPTY array is a different matter and is stored as it comes: that
|
|
2703
|
+
* is the server saying the subscription really is gone.
|
|
2704
|
+
* @param result - The login result containing the new user context and channel settings.
|
|
2359
2705
|
*/
|
|
2360
|
-
|
|
2361
|
-
|
|
2706
|
+
updateContext(result) {
|
|
2707
|
+
const info = this.ensureLoginInfo();
|
|
2708
|
+
if (result.context) {
|
|
2709
|
+
info.context = result.context;
|
|
2710
|
+
}
|
|
2711
|
+
if (result.settings) {
|
|
2712
|
+
info.channels = result.settings;
|
|
2713
|
+
}
|
|
2714
|
+
this.storeContext();
|
|
2362
2715
|
}
|
|
2363
2716
|
/**
|
|
2364
|
-
*
|
|
2365
|
-
* @returns An observable emitting the API result wrapping the list of topics.
|
|
2717
|
+
* Rebuilds the `availableChannels` signal from the current login context.
|
|
2366
2718
|
*/
|
|
2367
|
-
|
|
2368
|
-
|
|
2719
|
+
initializeChannels() {
|
|
2720
|
+
if (this.loginInfo) {
|
|
2721
|
+
const channels = [];
|
|
2722
|
+
this.loginInfo.channels?.forEach(n => {
|
|
2723
|
+
const channelSubscription = this.loginInfo?.context?.channels?.find(x => x.channel === n.channelId);
|
|
2724
|
+
n.isSuspended = channelSubscription?.isSuspended === true;
|
|
2725
|
+
const channel = ClipperChannels.find(x => x.value === n.channelId);
|
|
2726
|
+
if (channel) {
|
|
2727
|
+
channel.disabled = !n.isActive;
|
|
2728
|
+
channel.suspended = n.isSuspended === true;
|
|
2729
|
+
channel.active = n.isActive === true && n.isEnabled === true;
|
|
2730
|
+
channels.push(channel);
|
|
2731
|
+
}
|
|
2732
|
+
});
|
|
2733
|
+
this.availableChannels.set(channels);
|
|
2734
|
+
}
|
|
2369
2735
|
}
|
|
2370
2736
|
/**
|
|
2371
|
-
*
|
|
2372
|
-
* @returns An observable emitting the API result wrapping the topics folder tree.
|
|
2737
|
+
* Resets the login state, clears the stored login info, and broadcasts `LOGOUT_COMPLETED`.
|
|
2373
2738
|
*/
|
|
2374
|
-
|
|
2375
|
-
|
|
2739
|
+
reset() {
|
|
2740
|
+
// Clear login info, in memory AND on disk: `loginInfo` re-reads local storage whenever the
|
|
2741
|
+
// field is empty, so leaving the stored copy behind would resurrect the previous user.
|
|
2742
|
+
this._loginInfo = undefined;
|
|
2743
|
+
localStorage.removeItem('clipper_context');
|
|
2744
|
+
// Logged out
|
|
2745
|
+
this._loggedIn.set(false);
|
|
2746
|
+
// Reset channels
|
|
2747
|
+
this.availableChannels.set([]);
|
|
2748
|
+
// Notify
|
|
2749
|
+
this.broadcastService.sendMessage(ClipperMessages.LOGOUT_COMPLETED);
|
|
2376
2750
|
}
|
|
2377
2751
|
/**
|
|
2378
|
-
*
|
|
2379
|
-
* @
|
|
2752
|
+
* Clears all session-storage authentication keys and resets the login state.
|
|
2753
|
+
* @param clearOAuthToken - When `true`, the OAuth bearer token is also removed.
|
|
2380
2754
|
*/
|
|
2381
|
-
|
|
2382
|
-
|
|
2755
|
+
clear(clearOAuthToken = false) {
|
|
2756
|
+
// Clear local storage
|
|
2757
|
+
sessionStorage.removeItem('clipper_auth');
|
|
2758
|
+
sessionStorage.removeItem('clipper_oauth');
|
|
2759
|
+
if (clearOAuthToken) {
|
|
2760
|
+
sessionStorage.removeItem('clipper_oauth_token');
|
|
2761
|
+
}
|
|
2762
|
+
// Reset login
|
|
2763
|
+
this.reset();
|
|
2383
2764
|
}
|
|
2384
|
-
|
|
2385
|
-
//
|
|
2386
|
-
|
|
2765
|
+
////
|
|
2766
|
+
// BOOTSTRAP
|
|
2767
|
+
////
|
|
2387
2768
|
/**
|
|
2388
|
-
*
|
|
2389
|
-
* @
|
|
2769
|
+
* Initialises the service with the API base URI, optional app URI, and feature flags.
|
|
2770
|
+
* @param serviceUri - The base URI of the Clipper REST API.
|
|
2771
|
+
* @param appUri - Optional URI of the Clipper web application (used to build document links).
|
|
2772
|
+
* @param flags - Feature flags that control service behaviour. Defaults to `ClipperServiceFlags.None`.
|
|
2390
2773
|
*/
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
.
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2774
|
+
initialize(serviceUri, appUri, flags = ClipperServiceFlags.None) {
|
|
2775
|
+
// Create unique client and machine id
|
|
2776
|
+
if (!sessionStorage.getItem('clipper_client_id')) {
|
|
2777
|
+
sessionStorage.setItem('clipper_client_id', (flags && ClipperServiceFlags.Embedded) > 0
|
|
2778
|
+
? 'embedded'
|
|
2779
|
+
: SystemUtils.generateUUID());
|
|
2780
|
+
}
|
|
2781
|
+
// Initialize
|
|
2782
|
+
this._serviceUri = serviceUri;
|
|
2783
|
+
this._appUri = appUri;
|
|
2784
|
+
this._flags = flags;
|
|
2785
|
+
// React to message broadcasting
|
|
2786
|
+
if (!this.broadcastInitialized) {
|
|
2787
|
+
this.broadcastInitialized = true;
|
|
2788
|
+
this.broadcastService.getMessage()
|
|
2789
|
+
.pipe(takeUntilDestroyed(this.destroyRef))
|
|
2790
|
+
.subscribe(message => {
|
|
2791
|
+
if (message.id === ClipperMessages.LOGOUT) {
|
|
2792
|
+
if (this.loggedIn()) {
|
|
2793
|
+
this.injector.get(ClipperLoginService).logout().subscribe(r => {
|
|
2794
|
+
if (!r.success) {
|
|
2795
|
+
if (r.message) {
|
|
2796
|
+
this.broadcastService.sendMessage(ClipperMessages.ERROR,
|
|
2797
|
+
// The surrounding markup is ours; the server's message is escaped, because
|
|
2798
|
+
// the dialog renders this string as HTML.
|
|
2799
|
+
{ message: "<p>" + escapeHtml(r.message) + "</p><br><br><hr><p class='small'><i>Per eliminare la configurazione di Clipper accedere a:<br><b>menu > personalizza > collegamenti</b></i></p>" });
|
|
2407
2800
|
}
|
|
2408
|
-
n.title1 = documentInfo.Title1 ?? documentInfo.Title2;
|
|
2409
|
-
n.title2 = documentInfo.Title1 ? documentInfo.Title2 : undefined;
|
|
2410
2801
|
}
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
}
|
|
2418
|
-
});
|
|
2419
|
-
}
|
|
2420
|
-
/**
|
|
2421
|
-
* Adds one or more documents to the working documents bag.
|
|
2422
|
-
* @param documentIds - The IDs of the documents to add.
|
|
2423
|
-
* @returns The subscription to the add-to-bag request.
|
|
2424
|
-
*/
|
|
2425
|
-
addToBag(documentIds) {
|
|
2426
|
-
return this.httpClient
|
|
2427
|
-
.post(this.core.serviceUri + '/documents/working/add', { documentIds: documentIds })
|
|
2428
|
-
.subscribe((r) => {
|
|
2429
|
-
if (!r.success) {
|
|
2430
|
-
this.broadcastService.sendMessage(ClipperMessages.LOGIN_CHANGED);
|
|
2431
|
-
}
|
|
2432
|
-
else {
|
|
2433
|
-
if (r.value) {
|
|
2434
|
-
const newItems = [];
|
|
2435
|
-
r.value.forEach(n => {
|
|
2436
|
-
if (n.documentDescriptor) {
|
|
2437
|
-
const documentInfo = JSON.parse(n.documentDescriptor);
|
|
2438
|
-
if (documentInfo) {
|
|
2439
|
-
n.documentId = documentInfo.DocumentId || documentInfo.Id;
|
|
2440
|
-
n.title1 = documentInfo.Title1 ?? documentInfo.Title2;
|
|
2441
|
-
n.title2 = documentInfo.Title1 ? documentInfo.Title2 : null;
|
|
2802
|
+
else {
|
|
2803
|
+
if ((this.flags & ClipperServiceFlags.DisplayConnectionStateMessages) > 0) {
|
|
2804
|
+
this.broadcastService.sendMessage(ClipperMessages.SUCCESS_TOAST, { message: "Disconnesso da Clipper", icon: 'power_off', duration: 1500 });
|
|
2805
|
+
}
|
|
2806
|
+
// Empty bag
|
|
2807
|
+
this.bag.set([]);
|
|
2442
2808
|
}
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
this.core.bag.update((values) => [...values, ...newItems]);
|
|
2809
|
+
});
|
|
2810
|
+
}
|
|
2811
|
+
else {
|
|
2812
|
+
// No session to close server-side, but the device must still be left clean.
|
|
2813
|
+
this.clear(true);
|
|
2449
2814
|
}
|
|
2450
|
-
this.broadcastService.sendMessage(ClipperMessages.SUCCESS_TOAST, { message: "Operazione completata con successo.", icon: 'check', duration: 1500 });
|
|
2451
2815
|
}
|
|
2452
|
-
}
|
|
2453
|
-
}
|
|
2816
|
+
});
|
|
2817
|
+
}
|
|
2818
|
+
// A page refresh is NOT handled here. Announcing LOGIN_COMPLETED from a stored context would
|
|
2819
|
+
// declare a session before anyone has asked the server whether it is still alive, and the
|
|
2820
|
+
// consumer would then treat the inevitable 401 as "your session just expired" rather than as a
|
|
2821
|
+
// bootstrap that never got off the ground. `ClipperLoginService.initialize` -- which
|
|
2822
|
+
// `ClipperService.initialize` runs right after this method -- validates the context against
|
|
2823
|
+
// `/session/me` first and announces the login only if it holds.
|
|
2454
2824
|
}
|
|
2455
2825
|
/**
|
|
2456
|
-
*
|
|
2457
|
-
* @param documentId - The ID of the document to remove.
|
|
2458
|
-
* @returns The subscription to the remove-from-bag request.
|
|
2826
|
+
* Ping
|
|
2459
2827
|
*/
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
.
|
|
2463
|
-
.subscribe(
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2828
|
+
ping() {
|
|
2829
|
+
this.httpClient.get(this._serviceUri + '/ping?nocache=' + SystemUtils.generateUUID())
|
|
2830
|
+
.pipe(catchError$1(() => { return EMPTY; }))
|
|
2831
|
+
.subscribe();
|
|
2832
|
+
}
|
|
2833
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: ClipperCoreService, deps: [], target: i0.ɵɵFactoryTarget.Service }); }
|
|
2834
|
+
static { this.ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.1.3", ngImport: i0, type: ClipperCoreService }); }
|
|
2835
|
+
}
|
|
2836
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: ClipperCoreService, decorators: [{
|
|
2837
|
+
type: Service
|
|
2838
|
+
}] });
|
|
2839
|
+
|
|
2840
|
+
/**
|
|
2841
|
+
* The two pieces of account state the document views actually move: which channels are
|
|
2842
|
+
* active, and the unread counters on the dashboard.
|
|
2843
|
+
*
|
|
2844
|
+
* Everything else an account can do — password reset and recovery, settings, channel
|
|
2845
|
+
* activation towards the server, trial info, user links, loading the dashboard — belongs to
|
|
2846
|
+
* the application that owns the account, not to a library whose job is showing documents.
|
|
2847
|
+
* It was removed on 2026-08-25 together with the archive, calendar and collaboration worlds;
|
|
2848
|
+
* a product that needs those endpoints calls them itself.
|
|
2849
|
+
*
|
|
2850
|
+
* What is left touches no HTTP: both methods only mutate the shared state in
|
|
2851
|
+
* `ClipperCoreService` and announce the change on the broadcast bus.
|
|
2852
|
+
*/
|
|
2853
|
+
class ClipperAccountService {
|
|
2854
|
+
constructor() {
|
|
2855
|
+
this.broadcastService = inject(BroadcastService);
|
|
2856
|
+
this.core = inject(ClipperCoreService);
|
|
2478
2857
|
}
|
|
2479
2858
|
/**
|
|
2480
|
-
*
|
|
2481
|
-
* @
|
|
2859
|
+
* Toggles the active state of available channels based on the supplied selection list.
|
|
2860
|
+
* @param values - The selected channel items; channels absent from this list are deactivated.
|
|
2482
2861
|
*/
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
if (!r.success) {
|
|
2488
|
-
this.broadcastService.sendMessage(ClipperMessages.ERROR, { message: r.message });
|
|
2489
|
-
}
|
|
2490
|
-
else {
|
|
2491
|
-
this.core.bag.set([]);
|
|
2492
|
-
this.broadcastService.sendMessage(ClipperMessages.SUCCESS_TOAST, { message: "Operazione completata con successo.", icon: 'check', duration: 1500 });
|
|
2862
|
+
updateChannels(values) {
|
|
2863
|
+
this.core.availableChannels().forEach(channel => {
|
|
2864
|
+
if (!channel.disabled) {
|
|
2865
|
+
channel.active = values?.findIndex(x => x.value === channel.value) !== -1;
|
|
2493
2866
|
}
|
|
2494
2867
|
});
|
|
2495
2868
|
}
|
|
2496
2869
|
/**
|
|
2497
|
-
*
|
|
2498
|
-
* @param module - The Clipper module whose
|
|
2499
|
-
* @
|
|
2500
|
-
|
|
2501
|
-
loadSearches(module) {
|
|
2502
|
-
return this.httpClient
|
|
2503
|
-
.get(this.core.serviceUri + '/documents/searches/?module=' + module);
|
|
2504
|
-
}
|
|
2505
|
-
/**
|
|
2506
|
-
* Persists a user search configuration on the server.
|
|
2507
|
-
* @param params - The search configuration to save.
|
|
2508
|
-
* @returns An observable emitting the API result wrapping the saved search.
|
|
2509
|
-
*/
|
|
2510
|
-
saveSearch(params) {
|
|
2511
|
-
return this.httpClient
|
|
2512
|
-
.post(this.core.serviceUri + '/documents/searches/save', params);
|
|
2513
|
-
}
|
|
2514
|
-
/**
|
|
2515
|
-
* Deletes a saved search by its ID.
|
|
2516
|
-
* @param id - The ID of the saved search to remove.
|
|
2517
|
-
* @returns An observable emitting the API result wrapping the number of deleted searches.
|
|
2870
|
+
* Adjusts the unread-item counter for the given module and broadcasts a dashboard update.
|
|
2871
|
+
* @param module - The Clipper module whose counter should be adjusted.
|
|
2872
|
+
* @param model - Optional document model to further scope the counter update.
|
|
2873
|
+
* @param increment - The signed increment to apply (use negative values to decrement).
|
|
2518
2874
|
*/
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2875
|
+
updateUnreadItems(module, model, increment) {
|
|
2876
|
+
increment ??= 0;
|
|
2877
|
+
if (increment !== 0) {
|
|
2878
|
+
this.core.dashboard.updateUnreadItems(module, model, increment);
|
|
2879
|
+
}
|
|
2880
|
+
this.broadcastService.sendMessage(ClipperMessages.COMMAND_DASHBOARD_UPDATED);
|
|
2522
2881
|
}
|
|
2523
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type:
|
|
2524
|
-
static { this.ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.1.3", ngImport: i0, type:
|
|
2882
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: ClipperAccountService, deps: [], target: i0.ɵɵFactoryTarget.Service }); }
|
|
2883
|
+
static { this.ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.1.3", ngImport: i0, type: ClipperAccountService }); }
|
|
2525
2884
|
}
|
|
2526
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type:
|
|
2885
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: ClipperAccountService, decorators: [{
|
|
2527
2886
|
type: Service
|
|
2528
2887
|
}] });
|
|
2529
2888
|
|
|
2530
2889
|
/**
|
|
2531
|
-
*
|
|
2532
|
-
*
|
|
2890
|
+
* Document search, references, export and metadata, plus the dashboard counters,
|
|
2891
|
+
* taxonomy/topics/tags lookups, the working-documents "bag" and saved searches.
|
|
2533
2892
|
*/
|
|
2534
|
-
class
|
|
2893
|
+
class ClipperDocumentsService {
|
|
2535
2894
|
constructor() {
|
|
2536
2895
|
this.httpClient = inject(HttpClient);
|
|
2537
2896
|
this.broadcastService = inject(BroadcastService);
|
|
2538
|
-
this.splashService = inject(SplashService);
|
|
2539
2897
|
this.core = inject(ClipperCoreService);
|
|
2540
2898
|
}
|
|
2899
|
+
/////
|
|
2900
|
+
// DOCUMENTS
|
|
2901
|
+
/////
|
|
2541
2902
|
/**
|
|
2542
|
-
*
|
|
2543
|
-
* @param
|
|
2544
|
-
|
|
2545
|
-
autoLogin(onSuccess) {
|
|
2546
|
-
this.login(undefined, undefined, true)
|
|
2547
|
-
.subscribe({
|
|
2548
|
-
next: r => {
|
|
2549
|
-
if (!r.success) {
|
|
2550
|
-
this.broadcastService.sendMessage(ClipperMessages.ERROR, { message: r.message });
|
|
2551
|
-
}
|
|
2552
|
-
else {
|
|
2553
|
-
if (!r.value.requiresMfa) {
|
|
2554
|
-
this.broadcastService.sendMessage(ClipperMessages.SUCCESS_TOAST, { message: "Connesso a Clipper", icon: 'power', duration: 1500 });
|
|
2555
|
-
}
|
|
2556
|
-
if (onSuccess) {
|
|
2557
|
-
onSuccess(r.value);
|
|
2558
|
-
}
|
|
2559
|
-
}
|
|
2560
|
-
},
|
|
2561
|
-
error: () => { this.broadcastService.sendMessage(ClipperMessages.ERROR, { message: "Clipper non disponibile." }); }
|
|
2562
|
-
});
|
|
2563
|
-
}
|
|
2564
|
-
/**
|
|
2565
|
-
* Performs an automatic logout and clears the stored session.
|
|
2566
|
-
* @param onSuccess - Optional callback invoked after a successful logout.
|
|
2567
|
-
*/
|
|
2568
|
-
autoLogout(onSuccess) {
|
|
2569
|
-
this.logout()
|
|
2570
|
-
.subscribe({
|
|
2571
|
-
next: r => {
|
|
2572
|
-
if (!r.success) {
|
|
2573
|
-
if (r.message) {
|
|
2574
|
-
this.broadcastService.sendMessage(ClipperMessages.ERROR, { message: r.message });
|
|
2575
|
-
}
|
|
2576
|
-
this.broadcastService.sendMessage(ClipperMessages.LOGIN_CHANGED);
|
|
2577
|
-
}
|
|
2578
|
-
else {
|
|
2579
|
-
this.broadcastService.sendMessage(ClipperMessages.SUCCESS_TOAST, { message: "Disconnesso da Clipper", icon: 'power_off', duration: 1500 });
|
|
2580
|
-
}
|
|
2581
|
-
},
|
|
2582
|
-
error: () => { },
|
|
2583
|
-
complete: () => {
|
|
2584
|
-
if (onSuccess) {
|
|
2585
|
-
onSuccess();
|
|
2586
|
-
}
|
|
2587
|
-
}
|
|
2588
|
-
});
|
|
2589
|
-
}
|
|
2590
|
-
/**
|
|
2591
|
-
* Authenticates the user against the Clipper API, supporting both credential-based and
|
|
2592
|
-
* OAuth2 login flows.
|
|
2593
|
-
* @param email - The user's email address (credential login only).
|
|
2594
|
-
* @param password - The user's password (credential login only).
|
|
2595
|
-
* @param remember - When `true`, the session is remembered across browser restarts.
|
|
2596
|
-
* @param oauth - The OAuth2 provider type, when authenticating via SSO.
|
|
2597
|
-
* @param oauthAccessToken - The OAuth2 bearer token. Defaults to the value stored in session storage.
|
|
2598
|
-
* @returns An observable emitting the API result wrapping the login result.
|
|
2903
|
+
* Queries documents matching the given search parameters.
|
|
2904
|
+
* @param params - The document search parameters.
|
|
2905
|
+
* @returns An observable emitting the API result wrapping the search result.
|
|
2599
2906
|
*/
|
|
2600
|
-
|
|
2601
|
-
this.
|
|
2602
|
-
return this.httpClient
|
|
2603
|
-
.post(this.core.serviceUri + '/login2', {
|
|
2604
|
-
user: oauth ? null : email,
|
|
2605
|
-
password: oauth ? null : password,
|
|
2606
|
-
remember: remember,
|
|
2607
|
-
oauth: oauth
|
|
2608
|
-
}, {
|
|
2609
|
-
headers: !oauth || !oauthAccessToken
|
|
2610
|
-
? new HttpHeaders()
|
|
2611
|
-
: new HttpHeaders()
|
|
2612
|
-
.set("Authorization", 'Bearer ' + oauthAccessToken)
|
|
2613
|
-
})
|
|
2614
|
-
.pipe(catchError$1(err => {
|
|
2615
|
-
return throwError(() => err);
|
|
2616
|
-
}), map((r) => {
|
|
2617
|
-
if (r.success) {
|
|
2618
|
-
const info = this.core.ensureLoginInfo();
|
|
2619
|
-
info.oauth = oauth;
|
|
2620
|
-
info.remember = remember;
|
|
2621
|
-
if (!oauth && r.value.requiresMfa) {
|
|
2622
|
-
// Notify login is pending
|
|
2623
|
-
this.broadcastService.sendMessage(ClipperMessages.LOGIN_PENDING);
|
|
2624
|
-
}
|
|
2625
|
-
else {
|
|
2626
|
-
// Complete login
|
|
2627
|
-
this.completeLogin(r.value);
|
|
2628
|
-
}
|
|
2629
|
-
}
|
|
2630
|
-
return r;
|
|
2631
|
-
}));
|
|
2907
|
+
query(params) {
|
|
2908
|
+
return this.httpClient.post(this.core.serviceUri + '/documents', params);
|
|
2632
2909
|
}
|
|
2633
2910
|
/**
|
|
2634
|
-
*
|
|
2635
|
-
*
|
|
2636
|
-
* @
|
|
2911
|
+
* Retrieves the facets for a document query.
|
|
2912
|
+
* @param params - The document search parameters.
|
|
2913
|
+
* @returns An observable emitting the API result wrapping the search facets.
|
|
2637
2914
|
*/
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
this.core.updateContext(result);
|
|
2641
|
-
this.core.setLoggedIn(!result.context?.isTemporary);
|
|
2642
|
-
this.core.loggingIn.set(false);
|
|
2643
|
-
// Initialize channels
|
|
2644
|
-
this.core.initializeChannels();
|
|
2645
|
-
// Notify
|
|
2646
|
-
this.broadcastService.sendMessage(ClipperMessages.LOGIN_COMPLETED);
|
|
2915
|
+
queryFacets(params) {
|
|
2916
|
+
return this.httpClient.post(this.core.serviceUri + '/documents/facets', params);
|
|
2647
2917
|
}
|
|
2648
2918
|
/**
|
|
2649
|
-
*
|
|
2650
|
-
* @param
|
|
2651
|
-
* @returns An observable emitting the API result wrapping the
|
|
2919
|
+
* Updates the state of one or more documents.
|
|
2920
|
+
* @param params - The document state update parameters.
|
|
2921
|
+
* @returns An observable emitting the API result wrapping the number of updated documents.
|
|
2652
2922
|
*/
|
|
2653
|
-
|
|
2654
|
-
return this.httpClient
|
|
2655
|
-
.post(this.core.serviceUri + '/login/confirm/' + code, {})
|
|
2656
|
-
.pipe(catchError$1((err) => {
|
|
2657
|
-
return throwError(() => err);
|
|
2658
|
-
}), map((r) => {
|
|
2659
|
-
if (r.success) {
|
|
2660
|
-
this.completeLogin(r.value);
|
|
2661
|
-
}
|
|
2662
|
-
return r;
|
|
2663
|
-
}));
|
|
2923
|
+
updateState(params) {
|
|
2924
|
+
return this.httpClient.post(this.core.serviceUri + '/documents/state/update', params);
|
|
2664
2925
|
}
|
|
2665
2926
|
/**
|
|
2666
|
-
*
|
|
2667
|
-
* @param
|
|
2668
|
-
* @returns An observable
|
|
2927
|
+
* Exports a single document in PDF format.
|
|
2928
|
+
* @param id - The ID of the document to export.
|
|
2929
|
+
* @returns An observable emitting the PDF binary content as a blob.
|
|
2669
2930
|
*/
|
|
2670
|
-
|
|
2671
|
-
return this.httpClient.
|
|
2672
|
-
.pipe(finalize(() => {
|
|
2673
|
-
this.core.clear(true);
|
|
2674
|
-
// Clean up
|
|
2675
|
-
localStorage.removeItem('clipper_context');
|
|
2676
|
-
}), catchError$1((_e) => {
|
|
2677
|
-
return of([]);
|
|
2678
|
-
}));
|
|
2931
|
+
exportPdf(id) {
|
|
2932
|
+
return this.httpClient.get(this.core.serviceUri + '/documents/export/' + id, { responseType: 'blob' });
|
|
2679
2933
|
}
|
|
2680
2934
|
/**
|
|
2681
|
-
*
|
|
2682
|
-
* @param
|
|
2683
|
-
* @returns An observable emitting the
|
|
2935
|
+
* Exports a document list (query or selected items), or exports deadlines as ICS.
|
|
2936
|
+
* @param params - The export parameters.
|
|
2937
|
+
* @returns An observable emitting the exported content as a blob.
|
|
2684
2938
|
*/
|
|
2685
|
-
|
|
2686
|
-
return this.httpClient.
|
|
2939
|
+
export(params) {
|
|
2940
|
+
return this.httpClient.post(this.core.serviceUri + '/documents/export', params, { responseType: 'blob' });
|
|
2687
2941
|
}
|
|
2688
|
-
////
|
|
2689
|
-
// CONTEXT (re-published from ClipperCoreService for consumer convenience)
|
|
2690
|
-
////
|
|
2691
2942
|
/**
|
|
2692
|
-
*
|
|
2693
|
-
*
|
|
2943
|
+
* Sends document links by email.
|
|
2944
|
+
* @param params - The send-by-email parameters including recipients and documents.
|
|
2945
|
+
* @returns An observable emitting the API result wrapping the number of sent items.
|
|
2694
2946
|
*/
|
|
2695
|
-
|
|
2696
|
-
this.core.
|
|
2947
|
+
sendTo(params) {
|
|
2948
|
+
return this.httpClient.post(this.core.serviceUri + '/documents/send', params);
|
|
2697
2949
|
}
|
|
2698
2950
|
/**
|
|
2699
|
-
*
|
|
2700
|
-
*
|
|
2701
|
-
* @
|
|
2951
|
+
* Retrieves the full document report page.
|
|
2952
|
+
* @param id - The ID of the document.
|
|
2953
|
+
* @returns An observable emitting the report content as a blob.
|
|
2702
2954
|
*/
|
|
2703
|
-
|
|
2704
|
-
this.core.
|
|
2955
|
+
report(id) {
|
|
2956
|
+
return this.httpClient.get(this.core.serviceUri + '/documents/report/' + id, { responseType: 'blob' });
|
|
2705
2957
|
}
|
|
2706
2958
|
/**
|
|
2707
|
-
*
|
|
2708
|
-
*
|
|
2709
|
-
* @
|
|
2959
|
+
* Gets the comment associated with a document.
|
|
2960
|
+
* @param id - The ID of the document.
|
|
2961
|
+
* @returns An observable emitting the API result wrapping the comment text.
|
|
2710
2962
|
*/
|
|
2711
|
-
|
|
2712
|
-
this.core.
|
|
2713
|
-
}
|
|
2714
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: ClipperLoginService, deps: [], target: i0.ɵɵFactoryTarget.Service }); }
|
|
2715
|
-
static { this.ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.1.3", ngImport: i0, type: ClipperLoginService }); }
|
|
2716
|
-
}
|
|
2717
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: ClipperLoginService, decorators: [{
|
|
2718
|
-
type: Service
|
|
2719
|
-
}] });
|
|
2720
|
-
|
|
2721
|
-
/**
|
|
2722
|
-
* Shared state and bootstrap for the Clipper application.
|
|
2723
|
-
*
|
|
2724
|
-
* Holds every piece of state used by more than one feature service (service URI,
|
|
2725
|
-
* flags, login context, dashboard, working-documents bag, channels and the various
|
|
2726
|
-
* UI signals) plus the low-level helpers that mutate that state.
|
|
2727
|
-
*
|
|
2728
|
-
* The feature services (`ClipperLoginService`, `ClipperDocumentsService`, ...) inject
|
|
2729
|
-
* this service to read shared state and call shared helpers. `ClipperService` (the
|
|
2730
|
-
* barrel/facade) re-exports the state declared here.
|
|
2731
|
-
*/
|
|
2732
|
-
class ClipperCoreService {
|
|
2733
|
-
constructor() {
|
|
2734
|
-
this.httpClient = inject(HttpClient);
|
|
2735
|
-
this.destroyRef = inject(DestroyRef);
|
|
2736
|
-
this.broadcastService = inject(BroadcastService);
|
|
2737
|
-
/** Used to lazily resolve feature services inside `initialize`, avoiding circular DI. */
|
|
2738
|
-
this.injector = inject(Injector);
|
|
2739
|
-
this.broadcastInitialized = false;
|
|
2740
|
-
this._serviceUri = '';
|
|
2741
|
-
this._flags = ClipperServiceFlags.None;
|
|
2742
|
-
this._loggedIn = signal(sessionStorage.getItem("clipper_oauth_token") !== null, /* @ts-ignore */
|
|
2743
|
-
...(ngDevMode ? [{ debugName: "_loggedIn" }] : /* istanbul ignore next */ []));
|
|
2744
|
-
this.loggedIn = this._loggedIn.asReadonly();
|
|
2745
|
-
this.loggingIn = signal(false, /* @ts-ignore */
|
|
2746
|
-
...(ngDevMode ? [{ debugName: "loggingIn" }] : /* istanbul ignore next */ []));
|
|
2747
|
-
this.snapshot = signal(undefined, /* @ts-ignore */
|
|
2748
|
-
...(ngDevMode ? [{ debugName: "snapshot" }] : /* istanbul ignore next */ []));
|
|
2749
|
-
this.supportsRS = signal(false, /* @ts-ignore */
|
|
2750
|
-
...(ngDevMode ? [{ debugName: "supportsRS" }] : /* istanbul ignore next */ []));
|
|
2751
|
-
this.referencesSnapshot = signal(undefined, /* @ts-ignore */
|
|
2752
|
-
...(ngDevMode ? [{ debugName: "referencesSnapshot" }] : /* istanbul ignore next */ []));
|
|
2753
|
-
this.dashboard = new ClipperDashboard();
|
|
2754
|
-
this.bag = signal([], /* @ts-ignore */
|
|
2755
|
-
...(ngDevMode ? [{ debugName: "bag" }] : /* istanbul ignore next */ []));
|
|
2756
|
-
this.bagTotal = computed(() => this.bag().length, /* @ts-ignore */
|
|
2757
|
-
...(ngDevMode ? [{ debugName: "bagTotal" }] : /* istanbul ignore next */ []));
|
|
2758
|
-
this.visible = signal(false, /* @ts-ignore */
|
|
2759
|
-
...(ngDevMode ? [{ debugName: "visible" }] : /* istanbul ignore next */ []));
|
|
2760
|
-
this.availableChannels = signal([], /* @ts-ignore */
|
|
2761
|
-
...(ngDevMode ? [{ debugName: "availableChannels" }] : /* istanbul ignore next */ []));
|
|
2762
|
-
this.activeChannels = computed(() => {
|
|
2763
|
-
return this.availableChannels()?.filter(x => !x.suspended && !x.disabled && x.active === true);
|
|
2764
|
-
}, /* @ts-ignore */
|
|
2765
|
-
...(ngDevMode ? [{ debugName: "activeChannels" }] : /* istanbul ignore next */ []));
|
|
2766
|
-
this.allowTags = signal(false, /* @ts-ignore */
|
|
2767
|
-
...(ngDevMode ? [{ debugName: "allowTags" }] : /* istanbul ignore next */ []));
|
|
2963
|
+
comment(id) {
|
|
2964
|
+
return this.httpClient.get(this.core.serviceUri + '/documents/comment/' + id);
|
|
2768
2965
|
}
|
|
2769
|
-
/**
|
|
2770
|
-
|
|
2771
|
-
|
|
2966
|
+
/**
|
|
2967
|
+
* Gets the info for a document.
|
|
2968
|
+
* @param id - The ID of the document.
|
|
2969
|
+
* @returns An observable emitting the API result wrapping the document info.
|
|
2970
|
+
*/
|
|
2971
|
+
info(id) {
|
|
2972
|
+
return this.httpClient.get(this.core.serviceUri + '/documents/info/' + id);
|
|
2772
2973
|
}
|
|
2773
|
-
/**
|
|
2774
|
-
|
|
2775
|
-
|
|
2974
|
+
/**
|
|
2975
|
+
* Gets the structure (index) of a document.
|
|
2976
|
+
* @param id - The ID of the document.
|
|
2977
|
+
* @returns An observable emitting the API result wrapping the document structure.
|
|
2978
|
+
*/
|
|
2979
|
+
index(id) {
|
|
2980
|
+
return this.httpClient.get(this.core.serviceUri + '/documents/structure/' + id);
|
|
2776
2981
|
}
|
|
2777
|
-
/**
|
|
2778
|
-
|
|
2779
|
-
|
|
2982
|
+
/**
|
|
2983
|
+
* Gets the last-update metadata for a document.
|
|
2984
|
+
*
|
|
2985
|
+
* The response carries two things, not one: the last-update string and the shared notes attached
|
|
2986
|
+
* to the document. The second was long left undeclared here, so a consumer of this library could
|
|
2987
|
+
* not see data the API had already sent it.
|
|
2988
|
+
* @param id - The ID of the document.
|
|
2989
|
+
* @returns An observable emitting the API result wrapping the last update string and its notes.
|
|
2990
|
+
*/
|
|
2991
|
+
metadata(id) {
|
|
2992
|
+
return this.httpClient.get(this.core.serviceUri + '/documents/metadata/' + id);
|
|
2780
2993
|
}
|
|
2781
2994
|
/**
|
|
2782
|
-
*
|
|
2783
|
-
* @
|
|
2995
|
+
* Queries document references, changes or jurisprudence depending on the requested mode.
|
|
2996
|
+
* @param params - The references search parameters; `mode` defaults to `ReferencesIn`.
|
|
2997
|
+
* @returns An observable emitting the API result wrapping the search result, or `null` for an unsupported mode.
|
|
2784
2998
|
*/
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2999
|
+
references(params) {
|
|
3000
|
+
let mode = params.mode;
|
|
3001
|
+
if (!mode)
|
|
3002
|
+
mode = ClipperQueryReferencesMode.ReferencesIn;
|
|
3003
|
+
switch (mode) {
|
|
3004
|
+
case ClipperQueryReferencesMode.ReferencesIn:
|
|
3005
|
+
case ClipperQueryReferencesMode.ReferencesOut:
|
|
3006
|
+
params.mode = mode;
|
|
3007
|
+
return this.httpClient.post(this.core.serviceUri + '/documents/references', params);
|
|
3008
|
+
case ClipperQueryReferencesMode.ChangesIn:
|
|
3009
|
+
case ClipperQueryReferencesMode.ChangesOut:
|
|
3010
|
+
return this.httpClient.post(this.core.serviceUri + '/documents/changes', params);
|
|
3011
|
+
case ClipperQueryReferencesMode.Juris:
|
|
3012
|
+
return this.httpClient.post(this.core.serviceUri + '/documents/juris', params);
|
|
3013
|
+
default:
|
|
3014
|
+
return null;
|
|
2794
3015
|
}
|
|
2795
|
-
return this._loginInfo;
|
|
2796
3016
|
}
|
|
2797
|
-
////
|
|
2798
|
-
// SHARED MUTATORS
|
|
2799
|
-
////
|
|
2800
3017
|
/**
|
|
2801
|
-
*
|
|
2802
|
-
*
|
|
2803
|
-
*
|
|
2804
|
-
* @returns The existing login-info object, or a freshly created empty one.
|
|
3018
|
+
* Retrieves the facets for a document references query.
|
|
3019
|
+
* @param params - The references search parameters; `mode` defaults to `ReferencesIn`.
|
|
3020
|
+
* @returns An observable emitting the API result wrapping the search facets, or `null` for an unsupported mode.
|
|
2805
3021
|
*/
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
3022
|
+
referencesFacets(params) {
|
|
3023
|
+
let mode = params.mode ?? ClipperQueryReferencesMode.ReferencesIn;
|
|
3024
|
+
switch (mode) {
|
|
3025
|
+
case ClipperQueryReferencesMode.ReferencesIn:
|
|
3026
|
+
case ClipperQueryReferencesMode.ReferencesOut:
|
|
3027
|
+
params.mode = mode;
|
|
3028
|
+
return this.httpClient.post(this.core.serviceUri + '/documents/references/facets', params);
|
|
3029
|
+
case ClipperQueryReferencesMode.ChangesIn:
|
|
3030
|
+
case ClipperQueryReferencesMode.ChangesOut:
|
|
3031
|
+
return this.httpClient.post(this.core.serviceUri + '/documents/changes/facets', params);
|
|
3032
|
+
case ClipperQueryReferencesMode.Juris:
|
|
3033
|
+
return this.httpClient.post(this.core.serviceUri + '/documents/juris/facets', params);
|
|
3034
|
+
default: return null;
|
|
2809
3035
|
}
|
|
2810
|
-
return this._loginInfo;
|
|
2811
3036
|
}
|
|
2812
3037
|
/**
|
|
2813
|
-
*
|
|
2814
|
-
*
|
|
2815
|
-
* @param value - The new logged-in state.
|
|
3038
|
+
* Wraps document rendering to allow token refresh.
|
|
3039
|
+
* @returns An observable emitting the API result wrapping a boolean readiness flag.
|
|
2816
3040
|
*/
|
|
2817
|
-
|
|
2818
|
-
this.
|
|
3041
|
+
preRender() {
|
|
3042
|
+
return this.httpClient.get(this.core.serviceUri + '/documents/pre-render?nocache=' + SystemUtils.generateUUID());
|
|
2819
3043
|
}
|
|
2820
3044
|
/**
|
|
2821
|
-
*
|
|
3045
|
+
* Gets the jurisprudence articles for a document query.
|
|
3046
|
+
* @param params - The document search parameters.
|
|
3047
|
+
* @returns An observable emitting the API result wrapping the search result.
|
|
2822
3048
|
*/
|
|
2823
|
-
|
|
2824
|
-
|
|
3049
|
+
jurisArticles(params) {
|
|
3050
|
+
return this.httpClient.post(this.core.serviceUri + '/documents/juris/articles', params);
|
|
2825
3051
|
}
|
|
2826
3052
|
/**
|
|
2827
|
-
*
|
|
2828
|
-
* @param
|
|
3053
|
+
* Gets a deadlines snapshot based on the supplied deadlines.
|
|
3054
|
+
* @param params - The calendar search parameters.
|
|
3055
|
+
* @returns An observable emitting the API result wrapping the calendar snapshot result.
|
|
2829
3056
|
*/
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
info.context = result.context;
|
|
2833
|
-
info.channels = result.settings;
|
|
2834
|
-
this.storeContext();
|
|
3057
|
+
deadlinesSnapshot(params) {
|
|
3058
|
+
return this.httpClient.post(this.core.serviceUri + '/documents/calendar/snapshot', params);
|
|
2835
3059
|
}
|
|
2836
3060
|
/**
|
|
2837
|
-
*
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
const channelSubscription = this.loginInfo?.context?.channels?.find(x => x.channel === n.channelId);
|
|
2844
|
-
n.isSuspended = channelSubscription?.isSuspended === true;
|
|
2845
|
-
const channel = ClipperChannels.find(x => x.value === n.channelId);
|
|
2846
|
-
if (channel) {
|
|
2847
|
-
channel.disabled = !n.isActive;
|
|
2848
|
-
channel.suspended = n.isSuspended === true;
|
|
2849
|
-
channel.active = n.isActive === true && n.isEnabled === true;
|
|
2850
|
-
channels.push(channel);
|
|
2851
|
-
}
|
|
2852
|
-
});
|
|
2853
|
-
this.availableChannels.set(channels);
|
|
2854
|
-
}
|
|
3061
|
+
* Retrieves the taxonomy.
|
|
3062
|
+
* @param params - Optional taxonomy parameters. Defaults to `{ model: 0, countItems: false }`.
|
|
3063
|
+
* @returns An observable emitting the API result wrapping the taxonomy folder tree.
|
|
3064
|
+
*/
|
|
3065
|
+
getTaxonomy(params) {
|
|
3066
|
+
return this.httpClient.post(this.core.serviceUri + '/taxonomy', params ?? { model: 0, countItems: false });
|
|
2855
3067
|
}
|
|
2856
3068
|
/**
|
|
2857
|
-
*
|
|
3069
|
+
* Retrieves the topics as a flat list.
|
|
3070
|
+
* @returns An observable emitting the API result wrapping the list of topics.
|
|
2858
3071
|
*/
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
this._loginInfo = undefined;
|
|
2862
|
-
// Logged out
|
|
2863
|
-
this._loggedIn.set(false);
|
|
2864
|
-
// Reset channels
|
|
2865
|
-
this.availableChannels.set([]);
|
|
2866
|
-
// Notify
|
|
2867
|
-
this.broadcastService.sendMessage(ClipperMessages.LOGOUT_COMPLETED);
|
|
3072
|
+
getTopics() {
|
|
3073
|
+
return this.httpClient.get(this.core.serviceUri + '/topics');
|
|
2868
3074
|
}
|
|
2869
3075
|
/**
|
|
2870
|
-
*
|
|
2871
|
-
* @
|
|
3076
|
+
* Retrieves the topics as a tree.
|
|
3077
|
+
* @returns An observable emitting the API result wrapping the topics folder tree.
|
|
2872
3078
|
*/
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
sessionStorage.removeItem('clipper_auth');
|
|
2876
|
-
sessionStorage.removeItem('clipper_oauth');
|
|
2877
|
-
if (clearOAuthToken) {
|
|
2878
|
-
sessionStorage.removeItem('clipper_oauth_token');
|
|
2879
|
-
}
|
|
2880
|
-
// Reset login
|
|
2881
|
-
this.reset();
|
|
3079
|
+
getTopicsAsTree() {
|
|
3080
|
+
return this.httpClient.get(this.core.serviceUri + '/topics2');
|
|
2882
3081
|
}
|
|
2883
|
-
////
|
|
2884
|
-
// BOOTSTRAP
|
|
2885
|
-
////
|
|
2886
3082
|
/**
|
|
2887
|
-
*
|
|
2888
|
-
* @
|
|
2889
|
-
* @param appUri - Optional URI of the Clipper web application (used to build document links).
|
|
2890
|
-
* @param flags - Feature flags that control service behaviour. Defaults to `ClipperServiceFlags.None`.
|
|
3083
|
+
* Retrieves the tags.
|
|
3084
|
+
* @returns An observable emitting the API result wrapping the list of tags.
|
|
2891
3085
|
*/
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
.
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
else {
|
|
2919
|
-
if ((this.flags & ClipperServiceFlags.DisplayConnectionStateMessages) > 0) {
|
|
2920
|
-
this.broadcastService.sendMessage(ClipperMessages.SUCCESS_TOAST, { message: "Connesso a Clipper", icon: 'power', duration: 1500 });
|
|
3086
|
+
getTags() {
|
|
3087
|
+
return this.httpClient.get(this.core.serviceUri + '/tags');
|
|
3088
|
+
}
|
|
3089
|
+
///
|
|
3090
|
+
// BAG
|
|
3091
|
+
///
|
|
3092
|
+
/**
|
|
3093
|
+
* Loads the working documents and populates the shared bag.
|
|
3094
|
+
* @returns The subscription to the working-documents request.
|
|
3095
|
+
*/
|
|
3096
|
+
loadBag() {
|
|
3097
|
+
return this.httpClient
|
|
3098
|
+
.get(this.core.serviceUri + '/documents/working')
|
|
3099
|
+
.subscribe(r => {
|
|
3100
|
+
if (!r.success) {
|
|
3101
|
+
this.broadcastService.sendMessage(ClipperMessages.ERROR, { message: r.message });
|
|
3102
|
+
}
|
|
3103
|
+
else {
|
|
3104
|
+
if (r.value) {
|
|
3105
|
+
const items = [];
|
|
3106
|
+
r.value.forEach(n => {
|
|
3107
|
+
if (n.documentDescriptor) {
|
|
3108
|
+
const documentInfo = JSON.parse(n.documentDescriptor);
|
|
3109
|
+
if (documentInfo) {
|
|
3110
|
+
if (!n.documentId) {
|
|
3111
|
+
n.documentId = documentInfo.DocumentId || documentInfo.Id;
|
|
2921
3112
|
}
|
|
2922
|
-
|
|
2923
|
-
|
|
3113
|
+
n.title1 = documentInfo.Title1 ?? documentInfo.Title2;
|
|
3114
|
+
n.title2 = documentInfo.Title1 ? documentInfo.Title2 : undefined;
|
|
2924
3115
|
}
|
|
2925
|
-
|
|
2926
|
-
|
|
3116
|
+
n.documentDescriptor = undefined;
|
|
3117
|
+
items.push(n);
|
|
3118
|
+
}
|
|
2927
3119
|
});
|
|
3120
|
+
this.core.bag.set(items);
|
|
2928
3121
|
}
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
3122
|
+
}
|
|
3123
|
+
});
|
|
3124
|
+
}
|
|
3125
|
+
/**
|
|
3126
|
+
* Adds one or more documents to the working documents bag.
|
|
3127
|
+
* @param documentIds - The IDs of the documents to add.
|
|
3128
|
+
* @returns The subscription to the add-to-bag request.
|
|
3129
|
+
*/
|
|
3130
|
+
addToBag(documentIds) {
|
|
3131
|
+
return this.httpClient
|
|
3132
|
+
.post(this.core.serviceUri + '/documents/working/add', { documentIds: documentIds })
|
|
3133
|
+
.subscribe((r) => {
|
|
3134
|
+
// A failure needs no broadcast here: `clipperAuthInterceptor` already turns an expired
|
|
3135
|
+
// session into an `ERROR_401` message and reports every other status the same way.
|
|
3136
|
+
if (r.success) {
|
|
3137
|
+
if (r.value) {
|
|
3138
|
+
const newItems = [];
|
|
3139
|
+
r.value.forEach(n => {
|
|
3140
|
+
if (n.documentDescriptor) {
|
|
3141
|
+
const documentInfo = JSON.parse(n.documentDescriptor);
|
|
3142
|
+
if (documentInfo) {
|
|
3143
|
+
n.documentId = documentInfo.DocumentId || documentInfo.Id;
|
|
3144
|
+
n.title1 = documentInfo.Title1 ?? documentInfo.Title2;
|
|
3145
|
+
n.title2 = documentInfo.Title1 ? documentInfo.Title2 : null;
|
|
2943
3146
|
}
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
3147
|
+
n.documentDescriptor = undefined;
|
|
3148
|
+
newItems.push(n);
|
|
3149
|
+
}
|
|
3150
|
+
});
|
|
3151
|
+
if (newItems.length > 0) {
|
|
3152
|
+
this.core.bag.update((values) => [...values, ...newItems]);
|
|
2948
3153
|
}
|
|
3154
|
+
this.broadcastService.sendMessage(ClipperMessages.SUCCESS_TOAST, { message: "Operazione completata con successo.", icon: 'check', duration: 1500 });
|
|
2949
3155
|
}
|
|
2950
|
-
}
|
|
2951
|
-
}
|
|
2952
|
-
// Eveluate current session storage in case of page refresh (F5)
|
|
2953
|
-
if (this.loggedIn()) {
|
|
2954
|
-
// Auto login
|
|
2955
|
-
this.loggingIn.set(false);
|
|
2956
|
-
// Initialize channels
|
|
2957
|
-
this.initializeChannels();
|
|
2958
|
-
// Notify
|
|
2959
|
-
this.broadcastService.sendMessage(ClipperMessages.LOGIN_COMPLETED);
|
|
2960
|
-
}
|
|
3156
|
+
}
|
|
3157
|
+
});
|
|
2961
3158
|
}
|
|
2962
3159
|
/**
|
|
2963
|
-
*
|
|
3160
|
+
* Removes a document from the working documents bag.
|
|
3161
|
+
* @param documentId - The ID of the document to remove.
|
|
3162
|
+
* @returns The subscription to the remove-from-bag request.
|
|
2964
3163
|
*/
|
|
2965
|
-
|
|
2966
|
-
this.httpClient
|
|
2967
|
-
.
|
|
2968
|
-
.subscribe()
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
* the application that owns the account, not to a library whose job is showing documents.
|
|
2984
|
-
* It was removed on 2026-08-25 together with the archive, calendar and collaboration worlds;
|
|
2985
|
-
* a product that needs those endpoints calls them itself.
|
|
2986
|
-
*
|
|
2987
|
-
* What is left touches no HTTP: both methods only mutate the shared state in
|
|
2988
|
-
* `ClipperCoreService` and announce the change on the broadcast bus.
|
|
2989
|
-
*/
|
|
2990
|
-
class ClipperAccountService {
|
|
2991
|
-
constructor() {
|
|
2992
|
-
this.broadcastService = inject(BroadcastService);
|
|
2993
|
-
this.core = inject(ClipperCoreService);
|
|
3164
|
+
removeFromBag(documentId) {
|
|
3165
|
+
return this.httpClient
|
|
3166
|
+
.post(this.core.serviceUri + '/documents/working/delete', { documentIds: [documentId] })
|
|
3167
|
+
.subscribe((r) => {
|
|
3168
|
+
if (!r.success) {
|
|
3169
|
+
this.broadcastService.sendMessage(ClipperMessages.ERROR, { message: r.message });
|
|
3170
|
+
}
|
|
3171
|
+
else {
|
|
3172
|
+
const p = this.core.bag().findIndex((n) => n.documentId === documentId);
|
|
3173
|
+
if (p !== -1) {
|
|
3174
|
+
this.core.bag.update((values) => {
|
|
3175
|
+
values.splice(p, 1);
|
|
3176
|
+
return [...values];
|
|
3177
|
+
});
|
|
3178
|
+
this.broadcastService.sendMessage(ClipperMessages.SUCCESS_TOAST, { message: "Operazione completata con successo.", icon: 'check', duration: 1500 });
|
|
3179
|
+
}
|
|
3180
|
+
}
|
|
3181
|
+
});
|
|
2994
3182
|
}
|
|
2995
3183
|
/**
|
|
2996
|
-
*
|
|
2997
|
-
* @
|
|
3184
|
+
* Clears all working documents from the bag.
|
|
3185
|
+
* @returns The subscription to the clear-bag request.
|
|
2998
3186
|
*/
|
|
2999
|
-
|
|
3000
|
-
this.
|
|
3001
|
-
|
|
3002
|
-
|
|
3187
|
+
clearBag() {
|
|
3188
|
+
return this.httpClient
|
|
3189
|
+
.post(this.core.serviceUri + '/documents/working/delete', { deleteAll: true })
|
|
3190
|
+
.subscribe((r) => {
|
|
3191
|
+
if (!r.success) {
|
|
3192
|
+
this.broadcastService.sendMessage(ClipperMessages.ERROR, { message: r.message });
|
|
3193
|
+
}
|
|
3194
|
+
else {
|
|
3195
|
+
this.core.bag.set([]);
|
|
3196
|
+
this.broadcastService.sendMessage(ClipperMessages.SUCCESS_TOAST, { message: "Operazione completata con successo.", icon: 'check', duration: 1500 });
|
|
3003
3197
|
}
|
|
3004
3198
|
});
|
|
3005
3199
|
}
|
|
3006
3200
|
/**
|
|
3007
|
-
*
|
|
3008
|
-
* @param module - The Clipper module whose
|
|
3009
|
-
* @
|
|
3010
|
-
* @param increment - The signed increment to apply (use negative values to decrement).
|
|
3201
|
+
* Retrieves the saved searches for the given module.
|
|
3202
|
+
* @param module - The Clipper module whose saved searches to load.
|
|
3203
|
+
* @returns An observable emitting the API result wrapping the list of saved searches.
|
|
3011
3204
|
*/
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
this.core.dashboard.updateUnreadItems(module, model, increment);
|
|
3016
|
-
}
|
|
3017
|
-
this.broadcastService.sendMessage(ClipperMessages.COMMAND_DASHBOARD_UPDATED);
|
|
3205
|
+
loadSearches(module) {
|
|
3206
|
+
return this.httpClient
|
|
3207
|
+
.get(this.core.serviceUri + '/documents/searches/?module=' + module);
|
|
3018
3208
|
}
|
|
3019
|
-
|
|
3020
|
-
|
|
3209
|
+
/**
|
|
3210
|
+
* Persists a user search configuration on the server.
|
|
3211
|
+
* @param params - The search configuration to save.
|
|
3212
|
+
* @returns An observable emitting the API result wrapping the saved search.
|
|
3213
|
+
*/
|
|
3214
|
+
saveSearch(params) {
|
|
3215
|
+
return this.httpClient
|
|
3216
|
+
.post(this.core.serviceUri + '/documents/searches/save', params);
|
|
3217
|
+
}
|
|
3218
|
+
/**
|
|
3219
|
+
* Deletes a saved search by its ID.
|
|
3220
|
+
* @param id - The ID of the saved search to remove.
|
|
3221
|
+
* @returns An observable emitting the API result wrapping the number of deleted searches.
|
|
3222
|
+
*/
|
|
3223
|
+
deleteSearch(id) {
|
|
3224
|
+
return this.httpClient
|
|
3225
|
+
.post(this.core.serviceUri + '/documents/searches/delete', { id: id });
|
|
3226
|
+
}
|
|
3227
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: ClipperDocumentsService, deps: [], target: i0.ɵɵFactoryTarget.Service }); }
|
|
3228
|
+
static { this.ɵprov = i0.ɵɵngDeclareService({ minVersion: "22.0.0", version: "22.1.3", ngImport: i0, type: ClipperDocumentsService }); }
|
|
3021
3229
|
}
|
|
3022
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type:
|
|
3230
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: ClipperDocumentsService, decorators: [{
|
|
3023
3231
|
type: Service
|
|
3024
3232
|
}] });
|
|
3025
3233
|
|
|
@@ -3034,7 +3242,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
|
|
|
3034
3242
|
class ClipperService {
|
|
3035
3243
|
constructor() {
|
|
3036
3244
|
this.core = inject(ClipperCoreService);
|
|
3037
|
-
/** Authentication: login/logout,
|
|
3245
|
+
/** Authentication: login/logout, e-mailed code confirmation, session restore, OTP. */
|
|
3038
3246
|
this.session = inject(ClipperLoginService);
|
|
3039
3247
|
/** Document search, references, export, working-documents bag and saved searches. */
|
|
3040
3248
|
this.documents = inject(ClipperDocumentsService);
|
|
@@ -3080,13 +3288,20 @@ class ClipperService {
|
|
|
3080
3288
|
// BOOTSTRAP
|
|
3081
3289
|
////
|
|
3082
3290
|
/**
|
|
3083
|
-
* Initialises the application with the API base URI, optional app URI, and feature flags
|
|
3291
|
+
* Initialises the application with the API base URI, optional app URI, and feature flags, then
|
|
3292
|
+
* validates any session that survived a page refresh.
|
|
3084
3293
|
* @param serviceUri - The base URI of the Clipper REST API.
|
|
3085
3294
|
* @param appUri - Optional URI of the Clipper web application (used to build document links).
|
|
3086
3295
|
* @param flags - Feature flags that control service behaviour. Defaults to `ClipperServiceFlags.None`.
|
|
3296
|
+
* @returns void
|
|
3087
3297
|
*/
|
|
3088
3298
|
initialize(serviceUri, appUri, flags = ClipperServiceFlags.None) {
|
|
3089
3299
|
this.core.initialize(serviceUri, appUri, flags);
|
|
3300
|
+
// Second, and in this order: the core sets the base URI that `/session/me` is built from, and
|
|
3301
|
+
// it no longer declares a session from what local storage holds. Validating that context is
|
|
3302
|
+
// what `ClipperLoginService.initialize` does, and doing it from here means a consumer keeps
|
|
3303
|
+
// calling one method to bootstrap, exactly as before.
|
|
3304
|
+
this.session.initialize();
|
|
3090
3305
|
}
|
|
3091
3306
|
/**
|
|
3092
3307
|
* Pings the Clipper REST API to keep the session/token alive.
|