@7365admin1/layer-common 4.83.0 → 4.84.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/CHANGELOG.md +10 -4
- package/components/MemberMain.vue +2 -1
- package/components/RolePermissionMain.vue +4 -2
- package/package.json +1 -1
- package/plugins/API.ts +16 -0
- package/utils/access-suspended.ts +18 -0
- package/utils/role.ts +15 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @iservice365/layer-common
|
|
2
2
|
|
|
3
|
+
## 4.84.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 57abba1: Suspended access: when the API answers ACCESS_SUSPENDED (every membership of the person is suspended), the app signs out and the sign-in page shows the server's message. Role names are unique per site, so role pickers and the roles table show "Admin · St. Regis Residences" (`roleLabel`), and the Members role chip uses the same "Role · Site" form.
|
|
8
|
+
|
|
3
9
|
## 4.83.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
|
@@ -1968,11 +1974,11 @@
|
|
|
1968
1974
|
`utils/console-tier.ts` + `composables/useConsoleTier.ts` mirror the server's
|
|
1969
1975
|
own rule from two endpoints the console already calls, unprojected:
|
|
1970
1976
|
|
|
1971
|
-
|
|
1972
|
-
|
|
1977
|
+
GET /api/members/user/:user/app/admin the Seven365 staff membership
|
|
1978
|
+
GET /api/roles/id/:role that membership's role document
|
|
1973
1979
|
|
|
1974
|
-
|
|
1975
|
-
|
|
1980
|
+
owner = member.type === "admin" && role.type === "admin" && role.default === true
|
|
1981
|
+
staff = member.type === "admin" && role.type === "admin"
|
|
1976
1982
|
|
|
1977
1983
|
`role.default` is the marker because it is the only property of a platform
|
|
1978
1984
|
staff role no API caller can set - `role.controller.ts` validates create and
|
|
@@ -374,7 +374,7 @@
|
|
|
374
374
|
v-model="selectedRole"
|
|
375
375
|
:items="pickerRoles"
|
|
376
376
|
item-value="_id"
|
|
377
|
-
item-title="
|
|
377
|
+
:item-title="roleLabel"
|
|
378
378
|
density="comfortable"
|
|
379
379
|
:rules="[requiredRule]"
|
|
380
380
|
:error-messages="
|
|
@@ -427,6 +427,7 @@ import {
|
|
|
427
427
|
safeReturnTo,
|
|
428
428
|
limitedSites,
|
|
429
429
|
withCurrentRole,
|
|
430
|
+
roleLabel,
|
|
430
431
|
} from "../utils/role";
|
|
431
432
|
import { formatLastActive } from "../utils/last-active";
|
|
432
433
|
import { formatMemberDate, memberAccess, memberPerson } from "../utils/member-row";
|
|
@@ -58,6 +58,8 @@
|
|
|
58
58
|
else. Two kinds share that list and they are governed differently,
|
|
59
59
|
so the row has to say which it is.
|
|
60
60
|
-->
|
|
61
|
+
<!-- "Admin · St. Regis Residences": names are unique per site (2026-10-01) -->
|
|
62
|
+
<template #item.name="{ item }">{{ roleLabel(item) }}</template>
|
|
61
63
|
<template #item.scope="{ item }">
|
|
62
64
|
<StatusChip
|
|
63
65
|
:dot="false"
|
|
@@ -164,7 +166,7 @@
|
|
|
164
166
|
<v-select
|
|
165
167
|
v-model="memberReassignments[member._id]"
|
|
166
168
|
:items="deletionPreview.replacementRoles"
|
|
167
|
-
item-title="
|
|
169
|
+
:item-title="roleLabel"
|
|
168
170
|
item-value="_id"
|
|
169
171
|
label="Reassign role"
|
|
170
172
|
density="comfortable"
|
|
@@ -204,7 +206,7 @@
|
|
|
204
206
|
|
|
205
207
|
<script setup lang="ts">
|
|
206
208
|
// Relative import: this layer's `utils/` is not auto-imported into consumers.
|
|
207
|
-
import { safeReturnTo, showSiteField } from "../utils/role";
|
|
209
|
+
import { roleLabel, safeReturnTo, showSiteField } from "../utils/role";
|
|
208
210
|
import { holdsPermission } from "../utils/permission-spellings";
|
|
209
211
|
import {
|
|
210
212
|
catalogueForOrg,
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@7365admin1/layer-common",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "4.
|
|
5
|
+
"version": "4.84.0",
|
|
6
6
|
"author": "7365admin1",
|
|
7
7
|
"main": "./nuxt.config.ts",
|
|
8
8
|
"//files": "What a consumer extending this layer actually loads. Without this npm ships the whole working tree - the changesets, the CI workflows, the render harness in tools/ and any scratch directory that happened to exist at publish time. Nuxt resolves a layer by directory, so every runtime directory below has to stay listed; adding a new top-level runtime directory means adding it here too.",
|
package/plugins/API.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { isPermissionRefusal } from "../utils/permission-refusal";
|
|
2
|
+
import { accessSuspendedMessage, SIGNIN_NOTICE_COOKIE } from "../utils/access-suspended";
|
|
2
3
|
|
|
3
4
|
export default defineNuxtPlugin((nuxtApp) => {
|
|
4
5
|
const { cookieConfig } = useRuntimeConfig().public;
|
|
6
|
+
let signingOut = false;
|
|
5
7
|
|
|
6
8
|
const api = $fetch.create({
|
|
7
9
|
baseURL: "/",
|
|
@@ -32,6 +34,20 @@ export default defineNuxtPlugin((nuxtApp) => {
|
|
|
32
34
|
// 401 + "You do not have permission ..." (vehicle writes and every other
|
|
33
35
|
// `requireSiteReach` gate). See utils/permission-refusal.ts.
|
|
34
36
|
onResponseError({ response }) {
|
|
37
|
+
// Every membership suspended (2026-10-01): the server already ended the
|
|
38
|
+
// session. Sign out here and let the sign-in page say why, once.
|
|
39
|
+
const suspended = accessSuspendedMessage((response as any)?._data);
|
|
40
|
+
if (suspended && import.meta.client && !signingOut) {
|
|
41
|
+
signingOut = true;
|
|
42
|
+
void nuxtApp.runWithContext(() => {
|
|
43
|
+
useCookie(SIGNIN_NOTICE_COOKIE, { ...cookieConfig, maxAge: 300 }).value = suspended;
|
|
44
|
+
for (const name of ["sid", "user", "organization"]) useCookie(name, cookieConfig).value = null;
|
|
45
|
+
const { APP_MAIN, APP_NAME } = useRuntimeConfig().public as Record<string, string>;
|
|
46
|
+
if (String(APP_NAME ?? "").toLowerCase() === "main" || !APP_MAIN) return navigateTo("/sign-in");
|
|
47
|
+
window.location.href = `${APP_MAIN}/sign-in`;
|
|
48
|
+
});
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
35
51
|
if (!isPermissionRefusal(response?.status, (response as any)?._data)) return;
|
|
36
52
|
if (!import.meta.client) return;
|
|
37
53
|
const refresh = (nuxtApp as any).$refreshAppRole;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Suspended = no access (owner 2026-10-01). When every membership of the
|
|
3
|
+
* signed-in person is suspended the API answers any request with 401 +
|
|
4
|
+
* `code: "ACCESS_SUSPENDED"` and the message "Your access has been suspended
|
|
5
|
+
* by <org>. Contact your administrator." (core `suspendedSessionGuard`). The
|
|
6
|
+
* session is already dead server-side; the app signs out and the sign-in page
|
|
7
|
+
* (web-app-main) shows the message once, from this cookie.
|
|
8
|
+
*/
|
|
9
|
+
export const ACCESS_SUSPENDED_CODE = "ACCESS_SUSPENDED";
|
|
10
|
+
export const SIGNIN_NOTICE_COOKIE = "signin-notice";
|
|
11
|
+
|
|
12
|
+
export function accessSuspendedMessage(data: unknown): string | null {
|
|
13
|
+
const body = data as { code?: unknown; message?: unknown } | null | undefined;
|
|
14
|
+
if (body?.code !== ACCESS_SUSPENDED_CODE) return null;
|
|
15
|
+
return typeof body.message === "string" && body.message
|
|
16
|
+
? body.message
|
|
17
|
+
: "Your access has been suspended. Contact your administrator.";
|
|
18
|
+
}
|
package/utils/role.ts
CHANGED
|
@@ -9,6 +9,19 @@
|
|
|
9
9
|
export const showSiteField = (siteState: boolean, type: string): boolean =>
|
|
10
10
|
siteState && type !== "admin";
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* A role's name with its site, "Admin · St. Regis Residences" (owner
|
|
14
|
+
* 2026-10-01: role names are unique per SITE, so two sites may both have an
|
|
15
|
+
* "Admin" - two independent roles - and every list that can show both must
|
|
16
|
+
* say which is which). Organisation-level roles (no site) show the name alone.
|
|
17
|
+
* `siteName` comes from the role list (`GET /api/roles`, core 2026-10-01).
|
|
18
|
+
*/
|
|
19
|
+
export function roleLabel(role: { name?: unknown; siteName?: unknown; site?: unknown } | null | undefined): string {
|
|
20
|
+
const name = typeof role?.name === "string" ? role.name : "";
|
|
21
|
+
const site = typeof role?.siteName === "string" ? role.siteName.trim() : "";
|
|
22
|
+
return role?.site && site ? `${name} · ${site}` : name;
|
|
23
|
+
}
|
|
24
|
+
|
|
12
25
|
/**
|
|
13
26
|
* The Assign Role picker's list, with the member's CURRENT role always in it.
|
|
14
27
|
*
|
|
@@ -50,13 +63,13 @@ export function memberRoleChip(
|
|
|
50
63
|
const limited = limitedSites(item);
|
|
51
64
|
if (orgLevel && limited.length) {
|
|
52
65
|
return {
|
|
53
|
-
text: limited.map((l) => `${l.roleName || "No role"}
|
|
66
|
+
text: limited.map((l) => `${l.roleName || "No role"} · ${l.siteName || "a site"}`).join(", "),
|
|
54
67
|
tone: "info",
|
|
55
68
|
};
|
|
56
69
|
}
|
|
57
70
|
if (orgLevel && item?.roleState === "other-site") {
|
|
58
71
|
const site = typeof item?.roleSiteName === "string" && item.roleSiteName ? item.roleSiteName : "";
|
|
59
|
-
return { text: site ? `${name || "Role"}
|
|
72
|
+
return { text: site ? `${name || "Role"} · ${site}` : name || "Role", tone: "info" };
|
|
60
73
|
}
|
|
61
74
|
switch (item?.roleState) {
|
|
62
75
|
case "missing":
|