@7365admin1/layer-common 3.2.1-staging.72 → 3.2.2
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/CHANGELOG.md +39 -0
- package/components/EntryPassInformation.vue +4 -4
- package/components/FeedbackMain.vue +7 -7
- package/components/HidAccessLogDashboard.vue +44 -119
- package/components/HidIdentityMapping.vue +975 -0
- package/components/HidIntercomManagement.vue +201 -808
- package/components/HidQrCodeConfiguration.vue +95 -907
- package/components/HidServiceSettingsPanel.vue +1 -20
- package/components/HidUserEnrollment.vue +81 -392
- package/components/Layout/NavigationDrawer.vue +2 -43
- package/components/NavigationItem.vue +0 -9
- package/components/Nfc/NFCPatrolRouteMain.vue +7 -52
- package/components/OnlineFormConfigurationForm.vue +224 -620
- package/components/OnlineFormFill.vue +8 -36
- package/components/OnlineFormsConfiguration.vue +2 -6
- package/components/QrTemplate/CreditCardLandscape.vue +12 -19
- package/components/QrTemplate/PrintDialog.vue +196 -209
- package/components/SiteSettings.vue +2 -4
- package/components/VisitorForm.vue +78 -201
- package/components/VisitorManagement.vue +2 -1
- package/composables/useComment.ts +3 -3
- package/composables/useHidAmico.ts +0 -127
- package/composables/useHidNavigation.ts +7 -0
- package/composables/useOnlineFormPages.ts +0 -2
- package/composables/useSiteSettings.ts +0 -50
- package/composables/useWebUsb.ts +37 -127
- package/middleware/member.ts +4 -0
- package/package.json +1 -2
- package/pages/[org]/[site]/access-mgmt/identity-mapping/index.vue +23 -0
- package/pages/[org]/[site]/access-mgmt/intercom/index.vue +1 -1
- package/plugins/secure-member.client.ts +56 -21
- package/plugins/vuetify.ts +9 -37
- package/types/site.d.ts +0 -71
- package/.changeset/camera-wall-gated-endpoint.md +0 -47
- package/.changeset/camera-wall-plain-english.md +0 -60
- package/.changeset/camera-wall-selection-and-guidance.md +0 -65
- package/.changeset/dark-primary-contrast.md +0 -39
- package/components/CameraWall.vue +0 -1034
- package/components/CameraWallTile.vue +0 -818
- package/components/HidCameraCaptureDialog.vue +0 -199
- package/composables/useSipWebPhone.ts +0 -311
- package/utils/camera-wall.test.ts +0 -571
- package/utils/camera-wall.ts +0 -926
- package/utils/theme.test.ts +0 -201
- package/utils/theme.ts +0 -136
|
@@ -99,54 +99,6 @@ export default function () {
|
|
|
99
99
|
});
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
/**
|
|
103
|
-
* The monitoring wall for one site.
|
|
104
|
-
*
|
|
105
|
-
* A different endpoint from `getAllSiteCameras`, and the difference is the
|
|
106
|
-
* point. That one is the Settings panel's paginated CRUD list; this one is
|
|
107
|
-
* purpose-built for a wall and is what the React Native app already uses:
|
|
108
|
-
*
|
|
109
|
-
* - it is **site-scoped and authorised server-side** — the caller must be a
|
|
110
|
-
* member of the site, of its owning organisation, or work for an
|
|
111
|
-
* organisation engaged to serve it;
|
|
112
|
-
* - it returns `type: "ip"` cameras only, so an ANPR unit can never land on a
|
|
113
|
-
* monitoring wall;
|
|
114
|
-
* - it returns each camera's **capability descriptor** and the server's own
|
|
115
|
-
* `unavailableReason`, so a tile explains itself instead of showing a black
|
|
116
|
-
* rectangle;
|
|
117
|
-
* - it never returns a credential, and it contacts no device.
|
|
118
|
-
*
|
|
119
|
-
* Unpaginated by design: a wall shows a site's cameras, and paging them was
|
|
120
|
-
* an artefact of borrowing the CRUD list.
|
|
121
|
-
*/
|
|
122
|
-
async function getSiteWall(siteId: string) {
|
|
123
|
-
return await useNuxtApp().$api<Record<string, any>>(
|
|
124
|
-
`/api/site-cameras/site/${siteId}/wall`,
|
|
125
|
-
{ method: "GET" }
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Health for the cameras currently on screen.
|
|
131
|
-
*
|
|
132
|
-
* Same site scoping as the wall, and the ids are narrowed to the visible
|
|
133
|
-
* tiles on purpose: this is the one camera call that can reach a device, and
|
|
134
|
-
* asking about cameras nobody is looking at spends a real budget for nothing.
|
|
135
|
-
*
|
|
136
|
-
* **Reachability is answered per RECORDER and cached there**, so a wall of
|
|
137
|
-
* twelve channels on one device is one connect rather than twelve, and
|
|
138
|
-
* several supervisors watching the same wall is still one. That is what makes
|
|
139
|
-
* it safe to ask on a timer at all — but only a slow one: the server caches
|
|
140
|
-
* for `healthCacheSeconds`, and asking faster than the cache buys nothing and
|
|
141
|
-
* costs round trips.
|
|
142
|
-
*/
|
|
143
|
-
async function getSiteHealth(siteId: string, cameraIds: string[]) {
|
|
144
|
-
return await useNuxtApp().$api<Record<string, any>>(
|
|
145
|
-
`/api/site-cameras/site/${siteId}/health`,
|
|
146
|
-
{ method: "GET", query: { ids: cameraIds.join(",") } }
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
102
|
async function updateSiteInformation(
|
|
151
103
|
siteId: string,
|
|
152
104
|
payload: { bgImage: string; description: string; docs: { id: string; name: string }[] }
|
|
@@ -223,8 +175,6 @@ export default function () {
|
|
|
223
175
|
updateSite,
|
|
224
176
|
addCamera,
|
|
225
177
|
getAllSiteCameras,
|
|
226
|
-
getSiteWall,
|
|
227
|
-
getSiteHealth,
|
|
228
178
|
setSiteGuardPosts,
|
|
229
179
|
updateSiteCamera,
|
|
230
180
|
deleteSiteCameraById,
|
package/composables/useWebUsb.ts
CHANGED
|
@@ -11,10 +11,6 @@ interface USBDevice {
|
|
|
11
11
|
close(): Promise<void>;
|
|
12
12
|
selectConfiguration(configurationValue: number): Promise<void>;
|
|
13
13
|
claimInterface(interfaceNumber: number): Promise<void>;
|
|
14
|
-
selectAlternateInterface(
|
|
15
|
-
interfaceNumber: number,
|
|
16
|
-
alternateSetting: number,
|
|
17
|
-
): Promise<void>;
|
|
18
14
|
releaseInterface(interfaceNumber: number): Promise<void>;
|
|
19
15
|
transferOut(
|
|
20
16
|
endpointNumber: number,
|
|
@@ -23,27 +19,14 @@ interface USBDevice {
|
|
|
23
19
|
configuration: {
|
|
24
20
|
interfaces: Array<{
|
|
25
21
|
interfaceNumber: number;
|
|
26
|
-
alternate: USBAlternateInterface;
|
|
27
22
|
alternates: Array<{
|
|
28
|
-
alternateSetting: number;
|
|
29
|
-
interfaceClass: number;
|
|
30
23
|
endpoints: Array<{
|
|
31
24
|
endpointNumber: number;
|
|
32
25
|
direction: "in" | "out";
|
|
33
26
|
}>;
|
|
34
27
|
}>;
|
|
35
28
|
}>;
|
|
36
|
-
}
|
|
37
|
-
configurations: Array<{ configurationValue: number }>;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
interface USBAlternateInterface {
|
|
41
|
-
alternateSetting: number;
|
|
42
|
-
interfaceClass: number;
|
|
43
|
-
endpoints: Array<{
|
|
44
|
-
endpointNumber: number;
|
|
45
|
-
direction: "in" | "out";
|
|
46
|
-
}>;
|
|
29
|
+
};
|
|
47
30
|
}
|
|
48
31
|
|
|
49
32
|
interface USBOutTransferResult {
|
|
@@ -56,86 +39,13 @@ declare global {
|
|
|
56
39
|
usb: {
|
|
57
40
|
getDevices(): Promise<USBDevice[]>;
|
|
58
41
|
requestDevice(options: {
|
|
59
|
-
filters: Array<{
|
|
60
|
-
vendorId?: number;
|
|
61
|
-
productId?: number;
|
|
62
|
-
classCode?: number;
|
|
63
|
-
}>;
|
|
42
|
+
filters: Array<{ vendorId?: number; productId?: number }>;
|
|
64
43
|
}): Promise<USBDevice>;
|
|
65
44
|
};
|
|
66
45
|
}
|
|
67
46
|
}
|
|
68
47
|
|
|
69
48
|
export default function useWebUsb() {
|
|
70
|
-
const printerConnections = new WeakMap<
|
|
71
|
-
USBDevice,
|
|
72
|
-
{ interfaceNumber: number; endpointNumber: number }
|
|
73
|
-
>();
|
|
74
|
-
|
|
75
|
-
const protectedInterfaceClasses = new Set([0x01, 0x03, 0x08, 0x0b, 0x0e, 0x10, 0xe0]);
|
|
76
|
-
|
|
77
|
-
function getErrorMessage(error: unknown, fallback: string) {
|
|
78
|
-
return error instanceof Error && error.message ? error.message : fallback;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function findPrinterInterface(device: USBDevice) {
|
|
82
|
-
const candidates = (device.configuration?.interfaces || []).flatMap((usbInterface) =>
|
|
83
|
-
usbInterface.alternates.flatMap((alternate) => {
|
|
84
|
-
const outputEndpoint = alternate.endpoints.find((endpoint) => endpoint.direction === "out");
|
|
85
|
-
if (!outputEndpoint || protectedInterfaceClasses.has(alternate.interfaceClass)) return [];
|
|
86
|
-
|
|
87
|
-
return [{
|
|
88
|
-
interfaceNumber: usbInterface.interfaceNumber,
|
|
89
|
-
alternateSetting: alternate.alternateSetting,
|
|
90
|
-
activeAlternateSetting: usbInterface.alternate.alternateSetting,
|
|
91
|
-
interfaceClass: alternate.interfaceClass,
|
|
92
|
-
endpointNumber: outputEndpoint.endpointNumber,
|
|
93
|
-
}];
|
|
94
|
-
}),
|
|
95
|
-
);
|
|
96
|
-
|
|
97
|
-
candidates.sort((left, right) => {
|
|
98
|
-
const priority = (interfaceClass: number) => interfaceClass === 0x07 ? 0 : interfaceClass === 0xff ? 1 : 2;
|
|
99
|
-
return priority(left.interfaceClass) - priority(right.interfaceClass);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
return candidates[0] || null;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
async function preparePrinterDevice(device: USBDevice) {
|
|
106
|
-
await device.open();
|
|
107
|
-
if (!device.configuration) {
|
|
108
|
-
await device.selectConfiguration(device.configurations[0]?.configurationValue || 1);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const printerInterface = findPrinterInterface(device);
|
|
112
|
-
if (!printerInterface) {
|
|
113
|
-
throw new Error(
|
|
114
|
-
"The selected USB device is not a compatible printer. Select the receipt printer instead of a Bluetooth, camera, keyboard, or other system device.",
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
await device.claimInterface(printerInterface.interfaceNumber);
|
|
119
|
-
if (printerInterface.alternateSetting !== printerInterface.activeAlternateSetting) {
|
|
120
|
-
await device.selectAlternateInterface(
|
|
121
|
-
printerInterface.interfaceNumber,
|
|
122
|
-
printerInterface.alternateSetting,
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
printerConnections.set(device, {
|
|
127
|
-
interfaceNumber: printerInterface.interfaceNumber,
|
|
128
|
-
endpointNumber: printerInterface.endpointNumber,
|
|
129
|
-
});
|
|
130
|
-
return device;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function getPrinterConnection(device: USBDevice) {
|
|
134
|
-
const connection = printerConnections.get(device);
|
|
135
|
-
if (!connection) throw new Error("Printer interface is not connected");
|
|
136
|
-
return connection;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
49
|
const isWebUsbSupported = computed(() => {
|
|
140
50
|
return typeof navigator !== "undefined" && "usb" in navigator;
|
|
141
51
|
});
|
|
@@ -165,9 +75,7 @@ export default function useWebUsb() {
|
|
|
165
75
|
throw new Error("Web USB is not supported in this browser");
|
|
166
76
|
}
|
|
167
77
|
try {
|
|
168
|
-
const device = await navigator.usb.requestDevice({
|
|
169
|
-
filters: [{ classCode: 0x07 }, { classCode: 0xff }],
|
|
170
|
-
});
|
|
78
|
+
const device = await navigator.usb.requestDevice({ filters: [] });
|
|
171
79
|
return {
|
|
172
80
|
vendorId: device.vendorId,
|
|
173
81
|
productId: device.productId,
|
|
@@ -176,12 +84,12 @@ export default function useWebUsb() {
|
|
|
176
84
|
serialNumber: device.serialNumber || "Unknown",
|
|
177
85
|
deviceId: `${device.vendorId}:${device.productId}`,
|
|
178
86
|
};
|
|
179
|
-
} catch (error:
|
|
180
|
-
if (error
|
|
87
|
+
} catch (error: any) {
|
|
88
|
+
if (error.name === "NotFoundError") {
|
|
181
89
|
throw new Error("No device selected");
|
|
182
90
|
}
|
|
183
91
|
console.error("Error requesting USB device:", error);
|
|
184
|
-
throw new Error(
|
|
92
|
+
throw new Error("Failed to access USB device");
|
|
185
93
|
}
|
|
186
94
|
};
|
|
187
95
|
|
|
@@ -202,17 +110,14 @@ export default function useWebUsb() {
|
|
|
202
110
|
});
|
|
203
111
|
}
|
|
204
112
|
console.log("[WebUSB] Opening device:", device.vendorId, device.productId);
|
|
205
|
-
await
|
|
113
|
+
await device.open();
|
|
114
|
+
await device.selectConfiguration(1);
|
|
115
|
+
await device.claimInterface(0);
|
|
206
116
|
console.log("[WebUSB] Device opened and interface claimed.");
|
|
207
117
|
return device;
|
|
208
|
-
} catch (error
|
|
118
|
+
} catch (error) {
|
|
209
119
|
console.error("Error connecting to USB device:", error);
|
|
210
|
-
|
|
211
|
-
await device?.close();
|
|
212
|
-
} catch (closeError) {
|
|
213
|
-
console.error("Error closing rejected USB device:", closeError);
|
|
214
|
-
}
|
|
215
|
-
throw new Error(getErrorMessage(error, "Failed to connect to USB printer"));
|
|
120
|
+
throw new Error("Failed to connect to USB device");
|
|
216
121
|
}
|
|
217
122
|
};
|
|
218
123
|
|
|
@@ -268,20 +173,15 @@ export default function useWebUsb() {
|
|
|
268
173
|
? "Connection and print test successful"
|
|
269
174
|
: "Connection successful but print test failed",
|
|
270
175
|
};
|
|
271
|
-
} catch (error:
|
|
176
|
+
} catch (error: any) {
|
|
272
177
|
return {
|
|
273
178
|
success: false,
|
|
274
|
-
error:
|
|
179
|
+
error: error.message,
|
|
275
180
|
message: "Connection failed",
|
|
276
181
|
};
|
|
277
182
|
} finally {
|
|
278
183
|
if (device) {
|
|
279
184
|
try {
|
|
280
|
-
const connection = printerConnections.get(device);
|
|
281
|
-
if (connection) {
|
|
282
|
-
await device.releaseInterface(connection.interfaceNumber);
|
|
283
|
-
printerConnections.delete(device);
|
|
284
|
-
}
|
|
285
185
|
await device.close();
|
|
286
186
|
} catch (closeError) {
|
|
287
187
|
console.error("Error closing device:", closeError);
|
|
@@ -305,16 +205,21 @@ export default function useWebUsb() {
|
|
|
305
205
|
];
|
|
306
206
|
|
|
307
207
|
const testData = new Uint8Array(testCommands);
|
|
308
|
-
const
|
|
309
|
-
|
|
208
|
+
const usbInterface = device.configuration.interfaces[0];
|
|
209
|
+
const alternate = usbInterface.alternates[0];
|
|
210
|
+
const outputEndpoint = alternate.endpoints.find(
|
|
211
|
+
(ep: any) => ep.direction === "out",
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
if (!outputEndpoint) {
|
|
215
|
+
throw new Error("No output endpoint found");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
await device.transferOut(outputEndpoint.endpointNumber, testData);
|
|
310
219
|
return { success: true, message: "Test print sent successfully" };
|
|
311
|
-
} catch (error:
|
|
220
|
+
} catch (error: any) {
|
|
312
221
|
console.error("Test print error:", error);
|
|
313
|
-
return {
|
|
314
|
-
success: false,
|
|
315
|
-
error: getErrorMessage(error, "Test print failed"),
|
|
316
|
-
message: "Test print failed",
|
|
317
|
-
};
|
|
222
|
+
return { success: false, error: error.message, message: "Test print failed" };
|
|
318
223
|
}
|
|
319
224
|
};
|
|
320
225
|
|
|
@@ -449,8 +354,13 @@ export default function useWebUsb() {
|
|
|
449
354
|
) => {
|
|
450
355
|
try {
|
|
451
356
|
console.log("[WebUSB] printQrCode start — doorLevel:", doorLevel, "liftLevel:", liftLevel, "company:", companyName, "address:", address);
|
|
452
|
-
const
|
|
453
|
-
|
|
357
|
+
const usbInterface = device.configuration.interfaces[0];
|
|
358
|
+
const alternate = usbInterface.alternates[0];
|
|
359
|
+
const outputEndpoint = alternate.endpoints.find(
|
|
360
|
+
(ep: any) => ep.direction === "out",
|
|
361
|
+
);
|
|
362
|
+
if (!outputEndpoint) throw new Error("No output endpoint found");
|
|
363
|
+
console.log("[WebUSB] Using endpoint:", outputEndpoint.endpointNumber);
|
|
454
364
|
|
|
455
365
|
const canvas = await createReceiptLayout(
|
|
456
366
|
urlImage,
|
|
@@ -465,17 +375,17 @@ export default function useWebUsb() {
|
|
|
465
375
|
const rasterData = canvasToRaster(canvas);
|
|
466
376
|
console.log("[WebUSB] Raster data size:", rasterData.byteLength, "bytes");
|
|
467
377
|
|
|
468
|
-
await device.transferOut(endpointNumber, rasterData.buffer);
|
|
378
|
+
await device.transferOut(outputEndpoint.endpointNumber, rasterData.buffer);
|
|
469
379
|
console.log("[WebUSB] Raster data sent.");
|
|
470
380
|
|
|
471
381
|
const CUT = new Uint8Array([0x1d, 0x56, 0x41, 0x10]);
|
|
472
|
-
await device.transferOut(endpointNumber, CUT.buffer);
|
|
382
|
+
await device.transferOut(outputEndpoint.endpointNumber, CUT.buffer);
|
|
473
383
|
console.log("[WebUSB] Cut command sent.");
|
|
474
384
|
|
|
475
385
|
return { success: true, message: "QR print sent successfully" };
|
|
476
|
-
} catch (error:
|
|
386
|
+
} catch (error: any) {
|
|
477
387
|
console.error("[WebUSB] printQrCode error:", error);
|
|
478
|
-
return { success: false, error:
|
|
388
|
+
return { success: false, error: error.message };
|
|
479
389
|
}
|
|
480
390
|
};
|
|
481
391
|
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@7365admin1/layer-common",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "3.2.
|
|
5
|
+
"version": "3.2.2",
|
|
6
6
|
"author": "7365admin1",
|
|
7
7
|
"main": "./nuxt.config.ts",
|
|
8
8
|
"publishConfig": {
|
|
@@ -41,7 +41,6 @@
|
|
|
41
41
|
"qrcode": "^1.5.4",
|
|
42
42
|
"qrcode.vue": "^3.4.1",
|
|
43
43
|
"sass": "^1.80.6",
|
|
44
|
-
"sip.js": "0.21.2",
|
|
45
44
|
"socket.io-client": "^4.8.3",
|
|
46
45
|
"vue-draggable-next": "^2.3.0",
|
|
47
46
|
"vue3-signature": "^0.2.4",
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-container fluid>
|
|
3
|
+
<HidEnabledGate
|
|
4
|
+
:site="siteId"
|
|
5
|
+
:org="orgId"
|
|
6
|
+
message="Enable HID as a service for this site before mapping identities."
|
|
7
|
+
>
|
|
8
|
+
<HidIdentityMapping :site="siteId" :org="orgId" />
|
|
9
|
+
</HidEnabledGate>
|
|
10
|
+
</v-container>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<script setup lang="ts">
|
|
14
|
+
definePageMeta({
|
|
15
|
+
layout: "default",
|
|
16
|
+
middleware: ["01-auth", "02-org"],
|
|
17
|
+
memberOnly: true,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const route = useRoute();
|
|
21
|
+
const siteId = computed(() => String(route.params.site ?? ""));
|
|
22
|
+
const orgId = computed(() => String(route.params.org ?? ""));
|
|
23
|
+
</script>
|
|
@@ -12,40 +12,75 @@ export default defineNuxtPlugin(() => {
|
|
|
12
12
|
|
|
13
13
|
const { userAppRole, id, orgNature } = useLocalSetup();
|
|
14
14
|
|
|
15
|
-
router.
|
|
15
|
+
router.afterEach(async (to) => {
|
|
16
16
|
const isMember = to.meta?.memberOnly;
|
|
17
17
|
|
|
18
18
|
if (!isMember) return;
|
|
19
19
|
|
|
20
20
|
const APP = useRuntimeConfig().public.APP;
|
|
21
|
-
const org =
|
|
22
|
-
(
|
|
21
|
+
const org = computed(
|
|
22
|
+
() =>
|
|
23
|
+
(to.params.org as string) || (to.params.organization as string) || ""
|
|
24
|
+
);
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
console.log('[secure-member-plugin', 'org', org.value)
|
|
27
|
+
|
|
28
|
+
if (!hexSchema.safeParse(org.value).success) {
|
|
29
|
+
return router.replace({ name: "require-organization-membership" });
|
|
26
30
|
}
|
|
27
31
|
|
|
28
|
-
const userId = useCookie("user").value ?? "";
|
|
29
|
-
|
|
32
|
+
const userId = computed(() => useCookie("user").value ?? "");
|
|
33
|
+
|
|
34
|
+
const { data: userMemberData, error: userMemberError } =
|
|
35
|
+
await useLazyAsyncData(
|
|
36
|
+
"plugin-get-member-by-id-" + userId.value + "-" + APP + "-" + org.value,
|
|
37
|
+
() => getByUserType(userId.value, APP, org.value),
|
|
38
|
+
{ watch: [userId] }
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
watchEffect(() => {
|
|
42
|
+
if (userMemberError.value) {
|
|
43
|
+
console.log('running-secure-member-redirect-plugin')
|
|
44
|
+
navigateTo(
|
|
45
|
+
{
|
|
46
|
+
name: "index",
|
|
47
|
+
},
|
|
48
|
+
{ replace: true }
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const roleId = ref("roleId");
|
|
30
54
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
55
|
+
watchEffect(() => {
|
|
56
|
+
if (userMemberData.value) {
|
|
57
|
+
id.value = userMemberData.value.org ?? "";
|
|
58
|
+
roleId.value = userMemberData.value.role ?? "roleId";
|
|
59
|
+
}
|
|
60
|
+
});
|
|
34
61
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
]
|
|
62
|
+
const { data: getOrgByIdReq } = await useLazyAsyncData(
|
|
63
|
+
"plugin-get-org-by-id-" + org.value,
|
|
64
|
+
() => getById(org.value),
|
|
65
|
+
{ watch: [org] }
|
|
66
|
+
);
|
|
39
67
|
|
|
40
|
-
|
|
41
|
-
|
|
68
|
+
watchEffect(() => {
|
|
69
|
+
if (getOrgByIdReq.value) {
|
|
70
|
+
orgNature.value = getOrgByIdReq.value.nature ?? "";
|
|
42
71
|
}
|
|
72
|
+
});
|
|
43
73
|
|
|
44
|
-
|
|
45
|
-
|
|
74
|
+
const { data: getRoleByIdReq } = await useLazyAsyncData(
|
|
75
|
+
"plugin-get-role-by-id-" + roleId.value,
|
|
76
|
+
() => getRoleById(roleId.value),
|
|
77
|
+
{ watch: [roleId] }
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
watchEffect(() => {
|
|
81
|
+
if (getRoleByIdReq.value) {
|
|
82
|
+
userAppRole.value = getRoleByIdReq.value;
|
|
46
83
|
}
|
|
47
|
-
}
|
|
48
|
-
return { name: "index" };
|
|
49
|
-
}
|
|
84
|
+
});
|
|
50
85
|
});
|
|
51
86
|
});
|
package/plugins/vuetify.ts
CHANGED
|
@@ -42,8 +42,6 @@ import {
|
|
|
42
42
|
} from "vuetify-pro-tiptap";
|
|
43
43
|
import "vuetify-pro-tiptap/style.css";
|
|
44
44
|
|
|
45
|
-
import { LIGHT_THEME, DARK_THEME } from "../utils/theme";
|
|
46
|
-
|
|
47
45
|
export default defineNuxtPlugin((app) => {
|
|
48
46
|
const vuetify = createVuetify({
|
|
49
47
|
defaults: {
|
|
@@ -71,43 +69,17 @@ export default defineNuxtPlugin((app) => {
|
|
|
71
69
|
// hint: "This field is required",
|
|
72
70
|
// },
|
|
73
71
|
},
|
|
74
|
-
/**
|
|
75
|
-
* THE PRODUCT HAS TWO THEMES, AND UNTIL NOW ONLY ONE OF THEM WAS OURS.
|
|
76
|
-
*
|
|
77
|
-
* Every app shows a light/dark toggle in the app bar (Layout/Header.vue),
|
|
78
|
-
* and it sets Vuetify's built-in "light" and "dark" themes. Those were left
|
|
79
|
-
* at Vuetify's factory values, so one click on any screen in any of the
|
|
80
|
-
* eleven apps threw the brand away: `primary` stopped being the Seven365
|
|
81
|
-
* navy and became Material blue #2196F3 - and `primary` is the navigation
|
|
82
|
-
* drawer's fill - while `primary-button` and `text-primary`, used in around
|
|
83
|
-
* eighty places across the layer and the apps, do not exist in a stock
|
|
84
|
-
* theme at all, so those components lost their colour to an undefined CSS
|
|
85
|
-
* variable. There was also no way back to the brand: the toggle only ever
|
|
86
|
-
* flipped between the two stock themes.
|
|
87
|
-
*
|
|
88
|
-
* The fix is to make the stock names OURS rather than to add new names.
|
|
89
|
-
* Several components already branch on `theme.global.name === "dark"` (this
|
|
90
|
-
* layer's FormDialog, property-management's facility icons), so a
|
|
91
|
-
* differently-named dark theme would have left every one of them silently
|
|
92
|
-
* on their light branch inside a dark app. Overriding `light` and `dark`
|
|
93
|
-
* means the toggle, `v-theme-provider theme="light"`, the `plain-dark`
|
|
94
|
-
* layout and all of those comparisons keep working, and start being right.
|
|
95
|
-
*
|
|
96
|
-
* The colours themselves, and every ratio they were chosen for, are in
|
|
97
|
-
* `utils/theme.ts`, which `utils/theme.test.ts` measures on every run.
|
|
98
|
-
*/
|
|
99
72
|
theme: {
|
|
100
|
-
defaultTheme: "
|
|
73
|
+
defaultTheme: "iservice365",
|
|
101
74
|
themes: {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
iservice365: LIGHT_THEME,
|
|
75
|
+
iservice365: {
|
|
76
|
+
dark: false,
|
|
77
|
+
colors: {
|
|
78
|
+
primary: "#042134",
|
|
79
|
+
"primary-button": "#1867C0",
|
|
80
|
+
"text-primary": "#052439",
|
|
81
|
+
},
|
|
82
|
+
},
|
|
111
83
|
},
|
|
112
84
|
},
|
|
113
85
|
icons: {
|
package/types/site.d.ts
CHANGED
|
@@ -1,76 +1,5 @@
|
|
|
1
1
|
declare type TSiteCreate = Pick<TSite, "name" | "description" | "orgId">;
|
|
2
2
|
|
|
3
|
-
declare type THidQrCodeFormat = "0" | "1" | "2";
|
|
4
|
-
|
|
5
|
-
declare type THidPermissionCategory =
|
|
6
|
-
| "resident"
|
|
7
|
-
| "property_management"
|
|
8
|
-
| "service_provider";
|
|
9
|
-
|
|
10
|
-
declare type THidPermissionAssignment = {
|
|
11
|
-
subjectId: string;
|
|
12
|
-
category: THidPermissionCategory;
|
|
13
|
-
name: string;
|
|
14
|
-
subtitle?: string;
|
|
15
|
-
intercom: boolean;
|
|
16
|
-
readerId?: string;
|
|
17
|
-
location?: string;
|
|
18
|
-
locationKey?: string;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
declare type THidSitePermissions = {
|
|
22
|
-
orgId: string;
|
|
23
|
-
site: string;
|
|
24
|
-
readerId: string;
|
|
25
|
-
location: string;
|
|
26
|
-
assignments: THidPermissionAssignment[];
|
|
27
|
-
counts: {
|
|
28
|
-
resident: number;
|
|
29
|
-
propertyManagement: number;
|
|
30
|
-
serviceProvider: number;
|
|
31
|
-
intercom: number;
|
|
32
|
-
};
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
declare type THidPermissionCandidate = THidPermissionAssignment & {
|
|
36
|
-
selected: boolean;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
declare type THidQrCodePassConfig = {
|
|
40
|
-
enabled: boolean;
|
|
41
|
-
onlineMode?: boolean;
|
|
42
|
-
readerId: string;
|
|
43
|
-
qrFormat: THidQrCodeFormat;
|
|
44
|
-
identificationMethods?: {
|
|
45
|
-
facial: boolean;
|
|
46
|
-
card: boolean;
|
|
47
|
-
qrCode: boolean;
|
|
48
|
-
idPassword: boolean;
|
|
49
|
-
pin: boolean;
|
|
50
|
-
bluetooth: boolean;
|
|
51
|
-
};
|
|
52
|
-
printer: {
|
|
53
|
-
vendorId: string;
|
|
54
|
-
productId: string;
|
|
55
|
-
};
|
|
56
|
-
template: {
|
|
57
|
-
header: string;
|
|
58
|
-
subtext: string;
|
|
59
|
-
};
|
|
60
|
-
validityMinutes: number | null;
|
|
61
|
-
updatedAt?: string;
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
declare type TSiteMetadata = {
|
|
65
|
-
block?: number;
|
|
66
|
-
guardPosts?: number;
|
|
67
|
-
gracePeriod?: number;
|
|
68
|
-
incidentCounter?: number;
|
|
69
|
-
incidentLogo?: string;
|
|
70
|
-
services?: Record<string, unknown>[];
|
|
71
|
-
hidQrCodePass?: THidQrCodePassConfig;
|
|
72
|
-
};
|
|
73
|
-
|
|
74
3
|
|
|
75
4
|
declare type TSite = {
|
|
76
5
|
_id?: string;
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
"@7365admin1/layer-common": patch
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
Draw the camera wall from the gated wall endpoint, and show the server's own
|
|
6
|
-
reason on a tile.
|
|
7
|
-
|
|
8
|
-
`CameraWall` fetched its cameras from `GET /api/site-cameras` - the Settings
|
|
9
|
-
panel's paginated CRUD list, which took its site from an optional query
|
|
10
|
-
parameter and checked nothing about the caller. Any signed-in user from any
|
|
11
|
-
organisation could read every camera in the estate through it, `host` and
|
|
12
|
-
`username` included.
|
|
13
|
-
|
|
14
|
-
It now calls `GET /api/site-cameras/site/:siteId/wall`, which already exists,
|
|
15
|
-
which the React Native monitoring app was built against, and which is
|
|
16
|
-
authorised server-side: the caller must be a member of the site, of its owning
|
|
17
|
-
organisation, or work for an organisation actively engaged to serve it. That
|
|
18
|
-
endpoint also returns `type: "ip"` cameras only and each camera's capability
|
|
19
|
-
descriptor, so a wall cannot be handed an ANPR unit and a tile can explain
|
|
20
|
-
itself.
|
|
21
|
-
|
|
22
|
-
Two consequences worth stating:
|
|
23
|
-
|
|
24
|
-
- **The pager is gone.** The wall endpoint returns the site's cameras in one
|
|
25
|
-
answer. Paging a video wall was an artefact of borrowing the CRUD list.
|
|
26
|
-
- **A tile now prefers the server's `unavailableReason`** over the reason it
|
|
27
|
-
used to work out itself. The server knows things the browser cannot - chiefly
|
|
28
|
-
"No recorder is configured for this camera's relay", which is the true answer
|
|
29
|
-
for every camera in the estate until the API host is configured, and which the
|
|
30
|
-
wall used to replace with its own guess. Offline still beats everything, since
|
|
31
|
-
it explains every tile at once.
|
|
32
|
-
|
|
33
|
-
No permission gate was invented here. Each application still gates its own menu
|
|
34
|
-
entry; this component draws what it is given, and the server decides.
|
|
35
|
-
|
|
36
|
-
Two smaller things in the same area:
|
|
37
|
-
|
|
38
|
-
- **A refused wall now says so.** The catch-all message told every failure to
|
|
39
|
-
"check your connection", which sends somebody who simply may not see that
|
|
40
|
-
site off to debug their wifi. A 401/403/404 now reads "You do not have access
|
|
41
|
-
to this site's cameras." The server answers "not yours" and "does not exist"
|
|
42
|
-
identically, so this wording does not distinguish them either.
|
|
43
|
-
- **`middleware/member.ts` is removed.** It read a cookie into an unused
|
|
44
|
-
variable and did nothing else - a file named like a membership gate that was
|
|
45
|
-
not one. No page in any of the eleven web apps referenced it. The real check
|
|
46
|
-
is `plugins/secure-member.client.ts`, driven by `memberOnly` page meta, which
|
|
47
|
-
is untouched.
|