@honuware/ui 0.1.0

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/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # @honuware/ui
2
+
3
+ Reusable, brand-free Angular UI + server-access framework — the shared component library behind the Knotty Yoga app, packaged for reuse by other Angular apps that talk to a compatible ("honuware") backend.
4
+
5
+ Published as **eight secondary entry points** under `@honuware/ui/*`; import only what you need — each is independently tree-shakeable (`sideEffects: false`).
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @honuware/ui
11
+ ```
12
+
13
+ Peer dependencies (Angular 21 — install if you don't already have them): `@angular/animations`, `@angular/cdk`, `@angular/common`, `@angular/core`, `@angular/forms`, `@angular/material`, `@angular/router`, and `rxjs`. The library's only runtime dependency is `tslib`.
14
+
15
+ ## Entry points
16
+
17
+ | Entry | What's in it |
18
+ |---|---|
19
+ | `@honuware/ui/foundation` | `ToastService`, `hw-confirm-dialog`, fuse animations, microsecond date-formatting utils, `sanitizeReturnUrl`. No internal deps. |
20
+ | `@honuware/ui/access` | The narrow `CrudAccess` / `AuthAccess` / `PhotoAccess` interfaces + `HONUWARE_*_ACCESS` tokens; framework DTOs (`DatabaseSchema`, `DataResults`, `ColumnDataInfo`, admin/photo types, `UserInfo`/`LoginInfo`…); the RFC 7807 stack (`ProblemDetails`, `ErrorService`); `CsrfInterceptor`; `PhotoUrlBuilder`; the request-serializing proxy + default HTTP implementations; and `provideHonuwareAccess()`. |
21
+ | `@honuware/ui/controls` | Schema-driven field controls (`hw-simple-text` / `-long-text` / `-simple-bool` / `-simple-enum` / `-simple-date`), the `hw-composite-control` type dispatcher, and `hw-fk-picker`. |
22
+ | `@honuware/ui/photos` | `hw-photo-upload` — drag-drop, client-side resize/re-encode to JPEG, deferred-upload mode, `userMode` avatar path. |
23
+ | `@honuware/ui/auth` | `AuthService` + `AuthData`/permission helpers, the five route guards, `ErrorInterceptor`, the login/register/verify pages (`hw-login` / `-register` / `-verify`), the `AUTH_ROUTES` config token, and the `tryTokenLoginInitializer` bootstrap factory. |
24
+ | `@honuware/ui/crud` | The generic table editor: `DatabaseSchemaService`, table-binding utils, the `hw-table-view-control` / `hw-composite-row-control` containers, the TableView/Edit/New pages, and the `CRUD_EDITOR_ROUTES` token. |
25
+ | `@honuware/ui/square` | `SquarePaymentService` (Web Payments SDK loader + card tokenizer) behind the `SQUARE_CONFIG` token. Depends on nothing internal. |
26
+ | `@honuware/ui/testing` | In-memory mocks for tests — `MockCrudAccess` (schema-driven store), `MockAuthAccess` (session simulator), `MockPhotoAccess`, and `provideHonuwareAccessMock()`. |
27
+
28
+ **Layering** (an entry may import only the entries below it): foundation → access → {controls, photos, auth} → crud; `square` is standalone; `testing` depends on access + auth.
29
+
30
+ ## Configuring the framework
31
+
32
+ Library components inject **narrow tokens** rather than a concrete client, so you wire them once.
33
+
34
+ ### Server access
35
+
36
+ ```ts
37
+ import { provideHonuwareAccess } from '@honuware/ui/access';
38
+
39
+ providers: [
40
+ // 'http' (default): the built-in request-serializing proxy over the standard
41
+ // honuware endpoints, off a base path (HONUWARE_API_BASE, default '/api').
42
+ provideHonuwareAccess(),
43
+ ]
44
+ ```
45
+
46
+ Or point the three `HONUWARE_{CRUD,AUTH,PHOTO}_ACCESS` tokens at your own client. For tests, use mock mode:
47
+
48
+ ```ts
49
+ import { provideHonuwareAccessMock } from '@honuware/ui/testing';
50
+ providers: [provideHonuwareAccessMock({ crud: { schema } })]
51
+ ```
52
+
53
+ ### Config tokens
54
+
55
+ - **`AUTH_ROUTES`** (`@honuware/ui/auth`) — where login/register/change-password live + the post-login `returnUrl` allowlist. Guards, the auth pages, and `ErrorInterceptor` read it.
56
+ - **`CRUD_EDITOR_ROUTES`** (`@honuware/ui/crud`) — the base path the generic table editor is mounted at.
57
+ - **`SQUARE_CONFIG`** (`@honuware/ui/square`) — the Square `applicationId` / `locationId` / `scriptUrl`.
58
+ - **`tryTokenLoginInitializer`** (`@honuware/ui/auth`) — wire into an `APP_INITIALIZER` (`deps: [AuthService]`) for silent re-auth at bootstrap.
59
+
60
+ Each token has a sensible root-provided default, so the minimal setup is `provideHonuwareAccess()` + a Material theme.
61
+
62
+ ## Styling
63
+
64
+ The components use **Angular Material**, so a consumer **must** set up a Material theme — otherwise the components render unstyled:
65
+
66
+ ```scss
67
+ // styles.scss
68
+ @use '@angular/material' as mat;
69
+ // … your mat.theme(...) definition / include
70
+ ```
71
+
72
+ The library reads **no CSS custom properties** — you don't need to define any `--vars`. Component styles are self-contained (inlined at build); there is no global stylesheet to import.
73
+
74
+ **Caveat (pre-"Website Makeover").** A handful of component styles still hardcode colors — an M2 indigo/pink palette assumption, plus semantic colors like `#f44336` (errors) and `#d1d5db` (card borders). A consumer whose Material theme differs will see these until the makeover moves them onto theme / CSS-variable hooks.
75
+
76
+ ## License
77
+
78
+ Apache-2.0
@@ -0,0 +1,557 @@
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, Inject, Injectable, inject } from '@angular/core';
3
+ import { Observable } from 'rxjs';
4
+ import { map } from 'rxjs/operators';
5
+ import * as i1 from '@angular/common/http';
6
+
7
+ // Base path for the honuware server API. Relative '/api' matches every
8
+ // current deployment shape (Angular dev proxy, same-origin Nginx/CloudFront);
9
+ // a consumer hosting the API elsewhere overrides this token.
10
+ const HONUWARE_API_BASE = new InjectionToken('HONUWARE_API_BASE', {
11
+ providedIn: 'root',
12
+ factory: () => '/api',
13
+ });
14
+
15
+ // Default HttpClient implementation of the framework CRUD surface against the
16
+ // standard honuware endpoints. New consumers get this via
17
+ // provideHonuwareAccess() (wrapped in the serializing proxy); knottyyoga keeps
18
+ // its own ServerAccessNetwork. Endpoints mirror the app's network layer.
19
+ class CrudHttpAccess {
20
+ http;
21
+ base;
22
+ constructor(http, base) {
23
+ this.http = http;
24
+ this.base = base;
25
+ }
26
+ addItem(body) {
27
+ return this.http.post(`${this.base}/add_item`, body, { withCredentials: true }).pipe(map(() => void 0));
28
+ }
29
+ addItemFetchPrimaryKey(body) {
30
+ return this.http.post(`${this.base}/add_item_fetch_primary_key`, body, { withCredentials: true, responseType: 'text' }).pipe(map((response) => {
31
+ const obj = JSON.parse(response);
32
+ return obj[Object.keys(obj)[0]];
33
+ }));
34
+ }
35
+ getTableRows(tableName) {
36
+ return this.http.get(`${this.base}/get_table_rows/${tableName}`, { withCredentials: true });
37
+ }
38
+ getRowsByColumn(tableName, columnName, ascending, pageSize, pageNumber) {
39
+ const asc = ascending ? 1 : 0;
40
+ return this.http.get(`${this.base}/get_rows_by_column/${tableName}/${columnName}/${asc}/${pageSize}/${pageNumber}`, { withCredentials: true });
41
+ }
42
+ getFilteredTableRows(tableName, columnName, ascending, pageSize, page, filterPairs) {
43
+ return this.http.post(`${this.base}/get_filtered_table_rows`, {
44
+ table_name: tableName,
45
+ column_name: columnName,
46
+ asc: ascending,
47
+ page_size: pageSize,
48
+ page,
49
+ filter_pairs: filterPairs,
50
+ }, { withCredentials: true });
51
+ }
52
+ getRow(tableName, columnName, columnValue) {
53
+ return this.http.get(`${this.base}/get_row/${tableName}/${columnName}/${columnValue}`, { withCredentials: true });
54
+ }
55
+ getRowByValues(tableName, filterPairs) {
56
+ return this.http.post(`${this.base}/get_row_by_values`, {
57
+ table_name: tableName,
58
+ filter_pairs: filterPairs,
59
+ }, { withCredentials: true });
60
+ }
61
+ updateItem(body) {
62
+ return this.http.post(`${this.base}/update_item`, body, { withCredentials: true }).pipe(map(() => void 0));
63
+ }
64
+ deleteItem(tableName, columnName, columnValue) {
65
+ return this.http.get(`${this.base}/delete_item/${tableName}/${columnName}/${columnValue}`, { withCredentials: true })
66
+ .pipe(map(() => void 0));
67
+ }
68
+ getDbSchema() {
69
+ return this.http.get(`${this.base}/get_db_schema`, { withCredentials: true });
70
+ }
71
+ getFkOptions(tableName, searchText, pageSize) {
72
+ return this.http.post(`${this.base}/get_fk_options`, {
73
+ table_name: tableName,
74
+ search_text: searchText,
75
+ page_size: pageSize,
76
+ }, { withCredentials: true });
77
+ }
78
+ resolveFkDisplay(parentTableName, values) {
79
+ return this.http.post(`${this.base}/resolve_fk_display`, {
80
+ parent_table_name: parentTableName,
81
+ values,
82
+ }, { withCredentials: true });
83
+ }
84
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: CrudHttpAccess, deps: [{ token: i1.HttpClient }, { token: HONUWARE_API_BASE }], target: i0.ɵɵFactoryTarget.Injectable });
85
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: CrudHttpAccess, providedIn: 'root' });
86
+ }
87
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: CrudHttpAccess, decorators: [{
88
+ type: Injectable,
89
+ args: [{ providedIn: 'root' }]
90
+ }], ctorParameters: () => [{ type: i1.HttpClient }, { type: undefined, decorators: [{
91
+ type: Inject,
92
+ args: [HONUWARE_API_BASE]
93
+ }] }] });
94
+
95
+ // Default HttpClient implementation of the framework auth/account surface.
96
+ class AuthHttpAccess {
97
+ http;
98
+ base;
99
+ constructor(http, base) {
100
+ this.http = http;
101
+ this.base = base;
102
+ }
103
+ register(firstName, lastName, email, password) {
104
+ return this.http.post(`${this.base}/register`, {
105
+ first_name: firstName,
106
+ last_name: lastName,
107
+ email,
108
+ password,
109
+ }, { withCredentials: true }).pipe(map(() => void 0));
110
+ }
111
+ verify(email, secret) {
112
+ return this.http.post(`${this.base}/verify`, { email, secret }, { withCredentials: true }).pipe(map(() => void 0));
113
+ }
114
+ login(loginInfo) {
115
+ return this.http.post(`${this.base}/login`, loginInfo, { withCredentials: true }).pipe(map(() => void 0));
116
+ }
117
+ logout() {
118
+ return this.http.post(`${this.base}/logout`, null, { withCredentials: true }).pipe(map(() => void 0));
119
+ }
120
+ me() {
121
+ return this.http.get(`${this.base}/me`, { withCredentials: true }).pipe(map(() => void 0));
122
+ }
123
+ remember() {
124
+ return this.http.post(`${this.base}/remember`, null, { withCredentials: true }).pipe(map(() => void 0));
125
+ }
126
+ getUserInfo() {
127
+ return this.http.post(`${this.base}/get_user_info`, null, { withCredentials: true });
128
+ }
129
+ setUserInfo(userInfo) {
130
+ return this.http.post(`${this.base}/set_user_info`, userInfo, { withCredentials: true }).pipe(map(() => void 0));
131
+ }
132
+ updateUserPassword(passwordInfo) {
133
+ return this.http.post(`${this.base}/update_user_password`, passwordInfo, { withCredentials: true }).pipe(map(() => void 0));
134
+ }
135
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: AuthHttpAccess, deps: [{ token: i1.HttpClient }, { token: HONUWARE_API_BASE }], target: i0.ɵɵFactoryTarget.Injectable });
136
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: AuthHttpAccess, providedIn: 'root' });
137
+ }
138
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: AuthHttpAccess, decorators: [{
139
+ type: Injectable,
140
+ args: [{ providedIn: 'root' }]
141
+ }], ctorParameters: () => [{ type: i1.HttpClient }, { type: undefined, decorators: [{
142
+ type: Inject,
143
+ args: [HONUWARE_API_BASE]
144
+ }] }] });
145
+
146
+ // Default HttpClient implementation of the framework photo surface.
147
+ class PhotoHttpAccess {
148
+ http;
149
+ base;
150
+ constructor(http, base) {
151
+ this.http = http;
152
+ this.base = base;
153
+ }
154
+ uploadPhoto(tableName, tableItemId, imageData, imageType) {
155
+ const type = imageType.split('/').pop() || imageType;
156
+ const url = `${this.base}/upload_photo/${encodeURIComponent(tableName)}/${tableItemId}/${encodeURIComponent(type)}`;
157
+ return this.http.post(url, imageData, { withCredentials: true });
158
+ }
159
+ uploadUserPhoto(imageData, imageType) {
160
+ const type = imageType.split('/').pop() || imageType;
161
+ const url = `${this.base}/upload_user_photo/${encodeURIComponent(type)}`;
162
+ return this.http.post(url, imageData, { withCredentials: true });
163
+ }
164
+ deletePhoto(tableName, tableItemId) {
165
+ const url = `${this.base}/delete_photo/${encodeURIComponent(tableName)}/${tableItemId}`;
166
+ return this.http.post(url, null, { withCredentials: true }).pipe(map(() => void 0));
167
+ }
168
+ hasPhoto(tableName, tableItemId) {
169
+ const url = `${this.base}/has_photo/${encodeURIComponent(tableName)}/${tableItemId}`;
170
+ return this.http.get(url, { withCredentials: true });
171
+ }
172
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: PhotoHttpAccess, deps: [{ token: i1.HttpClient }, { token: HONUWARE_API_BASE }], target: i0.ɵɵFactoryTarget.Injectable });
173
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: PhotoHttpAccess, providedIn: 'root' });
174
+ }
175
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: PhotoHttpAccess, decorators: [{
176
+ type: Injectable,
177
+ args: [{ providedIn: 'root' }]
178
+ }], ctorParameters: () => [{ type: i1.HttpClient }, { type: undefined, decorators: [{
179
+ type: Inject,
180
+ args: [HONUWARE_API_BASE]
181
+ }] }] });
182
+
183
+ // Request-serializing façade over the default HTTP implementations. A honuware
184
+ // server allows one transaction per session and rejects concurrent requests,
185
+ // so every call is queued and run one at a time (the library twin of the app's
186
+ // ServerAccessProxy). This is the default a new consumer gets via
187
+ // provideHonuwareAccess({ mode: 'http' }); knottyyoga wires its own proxy.
188
+ class HonuwareAccessProxy {
189
+ crud;
190
+ auth;
191
+ photo;
192
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
193
+ queue = [];
194
+ busy = false;
195
+ constructor(crud, auth, photo) {
196
+ this.crud = crud;
197
+ this.auth = auth;
198
+ this.photo = photo;
199
+ }
200
+ serialize(factory) {
201
+ return new Observable((subscriber) => {
202
+ this.queue.push({ factory, subscriber });
203
+ this.pump();
204
+ });
205
+ }
206
+ pump() {
207
+ if (this.busy)
208
+ return;
209
+ const next = this.queue.shift();
210
+ if (!next)
211
+ return;
212
+ this.busy = true;
213
+ next.factory().subscribe({
214
+ next: (value) => next.subscriber.next(value),
215
+ error: (err) => { next.subscriber.error(err); this.busy = false; this.pump(); },
216
+ complete: () => { next.subscriber.complete(); this.busy = false; this.pump(); },
217
+ });
218
+ }
219
+ // --- CrudAccess ---
220
+ addItem(body) { return this.serialize(() => this.crud.addItem(body)); }
221
+ addItemFetchPrimaryKey(body) { return this.serialize(() => this.crud.addItemFetchPrimaryKey(body)); }
222
+ getTableRows(tableName) { return this.serialize(() => this.crud.getTableRows(tableName)); }
223
+ getRowsByColumn(t, c, a, s, p) { return this.serialize(() => this.crud.getRowsByColumn(t, c, a, s, p)); }
224
+ getFilteredTableRows(t, c, a, s, p, f) { return this.serialize(() => this.crud.getFilteredTableRows(t, c, a, s, p, f)); }
225
+ getRow(t, c, v) { return this.serialize(() => this.crud.getRow(t, c, v)); }
226
+ getRowByValues(t, f) { return this.serialize(() => this.crud.getRowByValues(t, f)); }
227
+ updateItem(body) { return this.serialize(() => this.crud.updateItem(body)); }
228
+ deleteItem(t, c, v) { return this.serialize(() => this.crud.deleteItem(t, c, v)); }
229
+ getDbSchema() { return this.serialize(() => this.crud.getDbSchema()); }
230
+ getFkOptions(t, s, p) { return this.serialize(() => this.crud.getFkOptions(t, s, p)); }
231
+ resolveFkDisplay(t, v) { return this.serialize(() => this.crud.resolveFkDisplay(t, v)); }
232
+ // --- AuthAccess ---
233
+ register(f, l, e, p) { return this.serialize(() => this.auth.register(f, l, e, p)); }
234
+ verify(email, secret) { return this.serialize(() => this.auth.verify(email, secret)); }
235
+ login(loginInfo) { return this.serialize(() => this.auth.login(loginInfo)); }
236
+ logout() { return this.serialize(() => this.auth.logout()); }
237
+ me() { return this.serialize(() => this.auth.me()); }
238
+ remember() { return this.serialize(() => this.auth.remember()); }
239
+ getUserInfo() { return this.serialize(() => this.auth.getUserInfo()); }
240
+ setUserInfo(userInfo) { return this.serialize(() => this.auth.setUserInfo(userInfo)); }
241
+ updateUserPassword(passwordInfo) { return this.serialize(() => this.auth.updateUserPassword(passwordInfo)); }
242
+ // --- PhotoAccess ---
243
+ uploadPhoto(t, id, data, type) { return this.serialize(() => this.photo.uploadPhoto(t, id, data, type)); }
244
+ uploadUserPhoto(data, type) { return this.serialize(() => this.photo.uploadUserPhoto(data, type)); }
245
+ deletePhoto(t, id) { return this.serialize(() => this.photo.deletePhoto(t, id)); }
246
+ hasPhoto(t, id) { return this.serialize(() => this.photo.hasPhoto(t, id)); }
247
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: HonuwareAccessProxy, deps: [{ token: CrudHttpAccess }, { token: AuthHttpAccess }, { token: PhotoHttpAccess }], target: i0.ɵɵFactoryTarget.Injectable });
248
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: HonuwareAccessProxy, providedIn: 'root' });
249
+ }
250
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: HonuwareAccessProxy, decorators: [{
251
+ type: Injectable,
252
+ args: [{ providedIn: 'root' }]
253
+ }], ctorParameters: () => [{ type: CrudHttpAccess }, { type: AuthHttpAccess }, { type: PhotoHttpAccess }] });
254
+
255
+ // Narrow injection seams over the server-access layer. Framework-generic
256
+ // consumers (controls, auth, photos, the generic CRUD editor) inject these
257
+ // instead of a full ~250-method app interface, so a foreign app can satisfy
258
+ // them with a 25-method implementation.
259
+ //
260
+ // The default resolves to HonuwareAccessProxy — the request-serializing façade
261
+ // over the default HTTP implementations — so a consumer that does nothing gets
262
+ // working, correctly-serialized access to the standard honuware endpoints. An
263
+ // app that has its own request-serializing client (like knottyyoga's
264
+ // ServerAccessProxy) overrides these tokens to point at it.
265
+ const HONUWARE_CRUD_ACCESS = new InjectionToken('HONUWARE_CRUD_ACCESS', { providedIn: 'root', factory: () => inject(HonuwareAccessProxy) });
266
+ const HONUWARE_AUTH_ACCESS = new InjectionToken('HONUWARE_AUTH_ACCESS', { providedIn: 'root', factory: () => inject(HonuwareAccessProxy) });
267
+ const HONUWARE_PHOTO_ACCESS = new InjectionToken('HONUWARE_PHOTO_ACCESS', { providedIn: 'root', factory: () => inject(HonuwareAccessProxy) });
268
+ // Convenience wiring for a consumer's app config. `mode: 'http'` (default) is
269
+ // equivalent to the token defaults; `mode: 'mock'` requires the caller to
270
+ // supply crud/auth/photo implementations (the testing entry point provides a
271
+ // wrapper that fills them in).
272
+ function provideHonuwareAccess(config = {}) {
273
+ if (config.mode === 'mock') {
274
+ if (!config.crud || !config.auth || !config.photo) {
275
+ throw new Error('provideHonuwareAccess({ mode: "mock" }) requires crud, auth, and photo implementations.');
276
+ }
277
+ return [
278
+ { provide: HONUWARE_CRUD_ACCESS, useValue: config.crud },
279
+ { provide: HONUWARE_AUTH_ACCESS, useValue: config.auth },
280
+ { provide: HONUWARE_PHOTO_ACCESS, useValue: config.photo },
281
+ ];
282
+ }
283
+ return [
284
+ { provide: HONUWARE_CRUD_ACCESS, useExisting: HonuwareAccessProxy },
285
+ { provide: HONUWARE_AUTH_ACCESS, useExisting: HonuwareAccessProxy },
286
+ { provide: HONUWARE_PHOTO_ACCESS, useExisting: HonuwareAccessProxy },
287
+ ];
288
+ }
289
+
290
+ // Builds the <img src> URLs for server-scaled photos, so no component
291
+ // string-concatenates API paths. Callers append their own cache-busting query
292
+ // params when they need them.
293
+ class PhotoUrlBuilder {
294
+ apiBase;
295
+ constructor(apiBase) {
296
+ this.apiBase = apiBase;
297
+ }
298
+ scaledPhotoUrl(tableName, tableItemId, width, height) {
299
+ return `${this.apiBase}/get_scaled_photo/${tableName}/${tableItemId}/${width}/${height}`;
300
+ }
301
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: PhotoUrlBuilder, deps: [{ token: HONUWARE_API_BASE }], target: i0.ɵɵFactoryTarget.Injectable });
302
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: PhotoUrlBuilder, providedIn: 'root' });
303
+ }
304
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: PhotoUrlBuilder, decorators: [{
305
+ type: Injectable,
306
+ args: [{ providedIn: 'root' }]
307
+ }], ctorParameters: () => [{ type: undefined, decorators: [{
308
+ type: Inject,
309
+ args: [HONUWARE_API_BASE]
310
+ }] }] });
311
+
312
+ /** Error type constants matching server-side ErrorCodes */
313
+ const ErrorTypes = {
314
+ VALIDATION_ERROR: 'validation_error',
315
+ BAD_REQUEST: 'bad_request',
316
+ NOT_AUTHENTICATED: 'not_authenticated',
317
+ INVALID_CREDENTIALS: 'invalid_credentials',
318
+ SESSION_EXPIRED: 'session_expired',
319
+ NOT_AUTHORIZED: 'not_authorized',
320
+ NOT_FOUND: 'not_found',
321
+ IDEMPOTENCY_CONFLICT: 'idempotency_conflict',
322
+ INTERNAL_ERROR: 'internal_error',
323
+ PAYMENT_DECLINED: 'payment_declined',
324
+ PAYMENT_FAILED: 'payment_failed',
325
+ VOUCHER_INVALID: 'voucher_invalid',
326
+ SEATS_EXCEEDED: 'seats_exceeded',
327
+ BOOKING_CONFLICT: 'booking_conflict',
328
+ };
329
+ /** Check if an error response is in RFC 7807 format */
330
+ function isProblemDetails(error) {
331
+ return (typeof error === 'object' &&
332
+ error !== null &&
333
+ 'type' in error &&
334
+ 'status' in error &&
335
+ typeof error.type === 'string' &&
336
+ typeof error.status === 'number');
337
+ }
338
+
339
+ class ErrorService {
340
+ /**
341
+ * Parse an HTTP error response into RFC 7807 ProblemDetails format.
342
+ * Handles both new RFC 7807 responses and legacy plain-text errors.
343
+ */
344
+ parseError(error) {
345
+ // Check if response is already RFC 7807 format
346
+ if (isProblemDetails(error.error)) {
347
+ return error.error;
348
+ }
349
+ // Fallback for legacy plain-text errors
350
+ return {
351
+ type: this.inferTypeFromStatus(error.status),
352
+ title: this.getTitleForStatus(error.status),
353
+ status: error.status,
354
+ detail: this.extractDetailFromError(error),
355
+ };
356
+ }
357
+ /**
358
+ * Get user-friendly message from a ProblemDetails error.
359
+ * The detail field should already be user-friendly.
360
+ */
361
+ getUserFriendlyMessage(error) {
362
+ return error.detail || error.title || 'An error occurred';
363
+ }
364
+ inferTypeFromStatus(status) {
365
+ switch (status) {
366
+ case 400:
367
+ return 'bad_request';
368
+ case 401:
369
+ return 'not_authenticated';
370
+ case 403:
371
+ return 'not_authorized';
372
+ case 404:
373
+ return 'not_found';
374
+ case 409:
375
+ return 'idempotency_conflict';
376
+ case 402:
377
+ return 'payment_failed';
378
+ default:
379
+ return 'internal_error';
380
+ }
381
+ }
382
+ getTitleForStatus(status) {
383
+ switch (status) {
384
+ case 400:
385
+ return 'Bad Request';
386
+ case 401:
387
+ return 'Authentication Required';
388
+ case 403:
389
+ return 'Not Authorized';
390
+ case 404:
391
+ return 'Not Found';
392
+ case 409:
393
+ return 'Conflict';
394
+ case 402:
395
+ return 'Payment Failed';
396
+ case 500:
397
+ return 'Internal Server Error';
398
+ default:
399
+ return 'Error';
400
+ }
401
+ }
402
+ extractDetailFromError(error) {
403
+ // If error body is a string, use it directly
404
+ if (typeof error.error === 'string' && error.error.length > 0) {
405
+ return error.error;
406
+ }
407
+ // If there's an error message property
408
+ if (error.error?.message) {
409
+ return error.error.message;
410
+ }
411
+ // Use HTTP status text as fallback
412
+ if (error.statusText && error.statusText !== 'Unknown Error') {
413
+ return error.statusText;
414
+ }
415
+ // Final fallback
416
+ return 'An unexpected error occurred. Please try again later.';
417
+ }
418
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: ErrorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
419
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: ErrorService, providedIn: 'root' });
420
+ }
421
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: ErrorService, decorators: [{
422
+ type: Injectable,
423
+ args: [{
424
+ providedIn: 'root',
425
+ }]
426
+ }] });
427
+
428
+ const CSRF_COOKIE_NAME = 'csrft';
429
+ const CSRF_HEADER_NAME = 'X-CSRF-Token';
430
+ const STATE_CHANGING_METHODS = new Set([
431
+ 'POST',
432
+ 'PUT',
433
+ 'PATCH',
434
+ 'DELETE',
435
+ ]);
436
+ /**
437
+ * Phase 4.3 of the security review: companion to the server-side
438
+ * CsrfGuard middleware.
439
+ *
440
+ * On every state-changing request, read the `csrft` cookie that the
441
+ * backend set at login time (Phase 4.1), and copy its value into the
442
+ * `X-CSRF-Token` header. The double-submit pattern: an attacker on
443
+ * a different origin cannot read the cookie value across origins, so
444
+ * they can't forge a matching header.
445
+ *
446
+ * GETs / HEADs / OPTIONS pass through untouched. Requests that fire
447
+ * before the user has logged in (no cookie present) pass through
448
+ * unmodified — the server-side guard exempts the bootstrap endpoints
449
+ * (login / register / remember / verify) explicitly, so these still
450
+ * succeed.
451
+ */
452
+ class CsrfInterceptor {
453
+ intercept(request, next) {
454
+ if (!STATE_CHANGING_METHODS.has(request.method.toUpperCase())) {
455
+ return next.handle(request);
456
+ }
457
+ const token = this.readCsrfCookie();
458
+ if (!token) {
459
+ return next.handle(request);
460
+ }
461
+ const cloned = request.clone({
462
+ setHeaders: { [CSRF_HEADER_NAME]: token },
463
+ });
464
+ return next.handle(cloned);
465
+ }
466
+ readCsrfCookie() {
467
+ if (typeof document === 'undefined' || !document.cookie)
468
+ return null;
469
+ const pairs = document.cookie.split(';');
470
+ for (const pair of pairs) {
471
+ const eq = pair.indexOf('=');
472
+ if (eq < 0)
473
+ continue;
474
+ const key = pair.slice(0, eq).trim();
475
+ if (key === CSRF_COOKIE_NAME) {
476
+ return pair.slice(eq + 1).trim();
477
+ }
478
+ }
479
+ return null;
480
+ }
481
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: CsrfInterceptor, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
482
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: CsrfInterceptor });
483
+ }
484
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.18", ngImport: i0, type: CsrfInterceptor, decorators: [{
485
+ type: Injectable
486
+ }] });
487
+
488
+ function ColumnDataInfoFromJSON(obj) {
489
+ const input = obj;
490
+ function parseString(val) {
491
+ if (val === null)
492
+ return undefined;
493
+ return typeof val === 'string' ? val : undefined;
494
+ }
495
+ function parseBool(val) {
496
+ if (val === null)
497
+ return undefined;
498
+ if (val === 't')
499
+ return true;
500
+ if (val === 'f')
501
+ return false;
502
+ return undefined;
503
+ }
504
+ function parseNumber(val) {
505
+ if (val === null)
506
+ return undefined;
507
+ if (typeof val === 'string') {
508
+ const num = Number(val);
509
+ return isNaN(num) ? undefined : num;
510
+ }
511
+ return undefined;
512
+ }
513
+ function parseStringArray(val) {
514
+ if (!Array.isArray(val))
515
+ return undefined;
516
+ return val.filter((v) => typeof v === 'string');
517
+ }
518
+ return {
519
+ column_name: parseString(input['column_name']),
520
+ type: parseString(input['type']),
521
+ primary_key: parseBool(input['primary_key']),
522
+ unique: parseBool(input['unique']),
523
+ nullable: parseBool(input['nullable']),
524
+ column_friendly_name: parseString(input['column_friendly_name']),
525
+ label: parseString(input['label']),
526
+ hint: parseString(input['hint']),
527
+ place_holder: parseString(input['place_holder']),
528
+ regex: parseString(input['regex']),
529
+ html_input_type: parseString(input['html_input_type']),
530
+ required: parseBool(input['required']),
531
+ max_length: parseNumber(input['max_length']),
532
+ default_value: parseString(input['default_value']),
533
+ rows: parseNumber(input['rows']),
534
+ hidden: parseBool(input['hidden']),
535
+ readonly: parseBool(input['readonly']),
536
+ enum_values: parseStringArray(input['enum_values']),
537
+ };
538
+ }
539
+
540
+ /*
541
+ * @honuware/ui/access
542
+ *
543
+ * The framework server-access seam: the narrow CrudAccess / AuthAccess /
544
+ * PhotoAccess interfaces + injection tokens, the framework DTO types
545
+ * (ColumnDataInfo + JSON conversion, DataResults, DatabaseSchema, TableSchema,
546
+ * ForeignKeyInfo, admin.types, photo.types), the RFC 7807 stack (ApiError +
547
+ * ErrorService), CsrfInterceptor, PhotoUrlBuilder, the request-serializing
548
+ * proxy, and the default HTTP implementations (provideHonuwareAccess).
549
+ */
550
+ // Interfaces + request/response types
551
+
552
+ /**
553
+ * Generated bundle index. Do not edit.
554
+ */
555
+
556
+ export { AuthHttpAccess, ColumnDataInfoFromJSON, CrudHttpAccess, CsrfInterceptor, ErrorService, ErrorTypes, HONUWARE_API_BASE, HONUWARE_AUTH_ACCESS, HONUWARE_CRUD_ACCESS, HONUWARE_PHOTO_ACCESS, HonuwareAccessProxy, PhotoHttpAccess, PhotoUrlBuilder, isProblemDetails, provideHonuwareAccess };
557
+ //# sourceMappingURL=honuware-ui-access.mjs.map