@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 +78 -0
- package/fesm2022/honuware-ui-access.mjs +557 -0
- package/fesm2022/honuware-ui-access.mjs.map +1 -0
- package/fesm2022/honuware-ui-auth.mjs +503 -0
- package/fesm2022/honuware-ui-auth.mjs.map +1 -0
- package/fesm2022/honuware-ui-controls.mjs +624 -0
- package/fesm2022/honuware-ui-controls.mjs.map +1 -0
- package/fesm2022/honuware-ui-crud.mjs +1101 -0
- package/fesm2022/honuware-ui-crud.mjs.map +1 -0
- package/fesm2022/honuware-ui-foundation.mjs +745 -0
- package/fesm2022/honuware-ui-foundation.mjs.map +1 -0
- package/fesm2022/honuware-ui-photos.mjs +238 -0
- package/fesm2022/honuware-ui-photos.mjs.map +1 -0
- package/fesm2022/honuware-ui-square.mjs +128 -0
- package/fesm2022/honuware-ui-square.mjs.map +1 -0
- package/fesm2022/honuware-ui-testing.mjs +325 -0
- package/fesm2022/honuware-ui-testing.mjs.map +1 -0
- package/fesm2022/honuware-ui.mjs +17 -0
- package/fesm2022/honuware-ui.mjs.map +1 -0
- package/package.json +80 -0
- package/types/honuware-ui-access.d.ts +382 -0
- package/types/honuware-ui-auth.d.ts +164 -0
- package/types/honuware-ui-controls.d.ts +136 -0
- package/types/honuware-ui-crud.d.ts +287 -0
- package/types/honuware-ui-foundation.d.ts +89 -0
- package/types/honuware-ui-photos.d.ts +46 -0
- package/types/honuware-ui-square.d.ts +65 -0
- package/types/honuware-ui-testing.d.ts +126 -0
- package/types/honuware-ui.d.ts +3 -0
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
|