@7365admin1/layer-common 4.0.3-staging.234 → 4.0.3-staging.236
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.
|
@@ -92,6 +92,16 @@
|
|
|
92
92
|
font-family: var(--font-sans, Roboto, sans-serif);
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
/* `[class*="text-h"]` is a substring match, so the rule above would also
|
|
96
|
+
catch an icon: `text-h6` and friends are used as SIZE classes directly on
|
|
97
|
+
`<v-icon>` (DeliveryCompany, PasswordConfirmation, OvernightParking-
|
|
98
|
+
Management), and `text-high-emphasis` matches too. The re-pointed family
|
|
99
|
+
would then beat `.material-symbols-outlined`, the ligature would fall back,
|
|
100
|
+
and the icon would draw as its literal name - hygiene saw the red word
|
|
101
|
+
"error" on its work-order list. `:not(.material-symbols-outlined)` keeps
|
|
102
|
+
icons out of the match set; web-app-hygiene `517eaeb` and web-app-account
|
|
103
|
+
#26 carry the same guard on their local copies. */
|
|
104
|
+
|
|
95
105
|
/* ------------------------------------------------------------------ */
|
|
96
106
|
/* The icon face. */
|
|
97
107
|
/* ------------------------------------------------------------------ */
|
|
@@ -109,8 +119,18 @@
|
|
|
109
119
|
* through `.mdi:before`, which we no longer emit. Only `text-transform` has
|
|
110
120
|
* to be stated: `.v-btn` uppercases its contents, and an uppercased "close"
|
|
111
121
|
* matches no ligature, so the icon would render as the word.
|
|
122
|
+
*
|
|
123
|
+
* The scoped selectors are load-bearing, not belt-and-braces: an app bundle
|
|
124
|
+
* emits Vuetify's typography utilities twice, and the second
|
|
125
|
+
* `.text-h6 { font-family: Roboto }` lands AFTER this rule, winning the
|
|
126
|
+
* 0,1,0 tie on source order. An icon sized with `text-h6` then drew the
|
|
127
|
+
* word in Roboto even with the re-point above guarded (measured in
|
|
128
|
+
* web-app-account's bundle). `.v-application`-scoped, the family is 0,2,0
|
|
129
|
+
* and survives any typography size class on the icon.
|
|
112
130
|
*/
|
|
113
|
-
.material-symbols-outlined
|
|
131
|
+
.material-symbols-outlined,
|
|
132
|
+
.v-application .material-symbols-outlined,
|
|
133
|
+
.v-overlay-container .material-symbols-outlined {
|
|
114
134
|
font-family: "Material Symbols Outlined";
|
|
115
135
|
font-style: normal;
|
|
116
136
|
font-variation-settings:
|
|
@@ -1,12 +1,38 @@
|
|
|
1
1
|
export function useLocalSetup() {
|
|
2
2
|
const userAppRole = useState<TRole | null>("userAppRole", () => null);
|
|
3
3
|
|
|
4
|
+
// Where the `userAppRole` lookup has got to. `userAppRole` on its own cannot
|
|
5
|
+
// say this, and conflating the two states below is what caused both halves of
|
|
6
|
+
// the permission bug:
|
|
7
|
+
//
|
|
8
|
+
// "idle" | "loading" -- nobody has resolved a role yet, or one is in
|
|
9
|
+
// flight. NOT an answer. A gate read here must not
|
|
10
|
+
// grant, and an app that cares should render a
|
|
11
|
+
// loading state rather than guess.
|
|
12
|
+
// "resolved" -- the lookup finished. `userAppRole` is either the
|
|
13
|
+
// role, or `null` because this account genuinely
|
|
14
|
+
// holds no member document for this app. Seven365
|
|
15
|
+
// staff-console accounts and residents never hold
|
|
16
|
+
// one, and "no role" is the correct final answer for
|
|
17
|
+
// them -- not a failure, and not a reason to redirect.
|
|
18
|
+
//
|
|
19
|
+
// A failed request drops back to "idle", never to "resolved", so a transient
|
|
20
|
+
// blip retries on the next navigation instead of denying for the whole session.
|
|
21
|
+
const userAppRoleStatus = useState<"idle" | "loading" | "resolved">(
|
|
22
|
+
"userAppRoleStatus",
|
|
23
|
+
() => "idle"
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const isAppRoleResolved = computed(() => userAppRoleStatus.value === "resolved");
|
|
27
|
+
|
|
4
28
|
const id = useState<string | null>("memberShipOrgId", () => null);
|
|
5
29
|
|
|
6
30
|
const orgNature = useState<string>("orgNature", () => "");
|
|
7
31
|
|
|
8
32
|
return {
|
|
9
33
|
userAppRole,
|
|
34
|
+
userAppRoleStatus,
|
|
35
|
+
isAppRoleResolved,
|
|
10
36
|
id,
|
|
11
37
|
orgNature,
|
|
12
38
|
};
|
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.0.3-staging.
|
|
5
|
+
"version": "4.0.3-staging.236",
|
|
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.",
|
|
@@ -4,20 +4,82 @@ const hexSchema = z
|
|
|
4
4
|
.string()
|
|
5
5
|
.regex(/^[0-9a-fA-F]{24}$/, "Invalid organization ID");
|
|
6
6
|
|
|
7
|
+
function statusOf(error: any) {
|
|
8
|
+
return error?.statusCode ?? error?.status ?? error?.response?.status;
|
|
9
|
+
}
|
|
10
|
+
|
|
7
11
|
export default defineNuxtPlugin(() => {
|
|
8
12
|
const router = useRouter();
|
|
9
13
|
const { getByUserType } = useMember();
|
|
10
14
|
const { getRoleById } = useRole();
|
|
11
15
|
const { getById } = useOrg();
|
|
12
16
|
|
|
13
|
-
const { userAppRole, id, orgNature } = useLocalSetup();
|
|
17
|
+
const { userAppRole, userAppRoleStatus, id, orgNature } = useLocalSetup();
|
|
18
|
+
|
|
19
|
+
// Resolved once per signed-in session, not once per navigation. Keyed by the
|
|
20
|
+
// user cookie so signing in as somebody else re-resolves.
|
|
21
|
+
let resolvedFor: string | null = null;
|
|
22
|
+
let inFlight: Promise<void> | null = null;
|
|
23
|
+
let hasMemberDocument = false;
|
|
24
|
+
|
|
25
|
+
// Deliberately never redirects and never throws.
|
|
26
|
+
//
|
|
27
|
+
// `GET /api/members/user/:id/app/:type` returns 404 for any account holding no
|
|
28
|
+
// member document for this app -- the Seven365 staff console and residents
|
|
29
|
+
// both do -- and that is a real answer ("no organisation role"), not an error.
|
|
30
|
+
// Treating it as an error is what would bounce those accounts off their own
|
|
31
|
+
// pages; leaving it unanswered is what let gates fail open.
|
|
32
|
+
async function resolveAppRole(userId: string) {
|
|
33
|
+
if (resolvedFor === userId) return;
|
|
34
|
+
if (inFlight) return inFlight;
|
|
35
|
+
|
|
36
|
+
userAppRoleStatus.value = "loading";
|
|
37
|
+
inFlight = run(userId).finally(() => {
|
|
38
|
+
inFlight = null;
|
|
39
|
+
});
|
|
40
|
+
return inFlight;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function run(userId: string) {
|
|
44
|
+
const APP = useRuntimeConfig().public.APP;
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
// The org query is not sent: the endpoint keys on user + app type only, so
|
|
48
|
+
// the role does not depend on which organisation is in the current route.
|
|
49
|
+
const member = await getByUserType(userId, APP);
|
|
50
|
+
hasMemberDocument = true;
|
|
51
|
+
id.value = member.org ?? "";
|
|
52
|
+
userAppRole.value = member.role ? await getRoleById(member.role) : null;
|
|
53
|
+
} catch (error) {
|
|
54
|
+
userAppRole.value = null;
|
|
55
|
+
hasMemberDocument = false;
|
|
56
|
+
// Anything that is not a 404 is transient. Stay unresolved so the next
|
|
57
|
+
// navigation tries again rather than denying for the rest of the session.
|
|
58
|
+
if (statusOf(error) !== 404) {
|
|
59
|
+
userAppRoleStatus.value = "idle";
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
resolvedFor = userId;
|
|
65
|
+
userAppRoleStatus.value = "resolved";
|
|
66
|
+
}
|
|
14
67
|
|
|
15
68
|
router.beforeEach(async (to) => {
|
|
69
|
+
const userId = useCookie("user").value ?? "";
|
|
70
|
+
|
|
71
|
+
// Resolve for EVERY authenticated route, not only `memberOnly` ones. Gates
|
|
72
|
+
// are read on pages that carry no such flag -- the staff console cannot
|
|
73
|
+
// carry it at all, it has no organisation in the route -- and until this
|
|
74
|
+
// ran there the role stayed null for the life of the page: gates that
|
|
75
|
+
// default open granted everything, gates that fail closed denied everything.
|
|
76
|
+
// Awaited, so no gate is ever evaluated against an unresolved role.
|
|
77
|
+
if (userId) await resolveAppRole(userId);
|
|
78
|
+
|
|
16
79
|
const isMember = to.meta?.memberOnly;
|
|
17
80
|
|
|
18
81
|
if (!isMember) return;
|
|
19
82
|
|
|
20
|
-
const APP = useRuntimeConfig().public.APP;
|
|
21
83
|
const org =
|
|
22
84
|
(to.params.org as string) || (to.params.organization as string) || "";
|
|
23
85
|
|
|
@@ -25,27 +87,19 @@ export default defineNuxtPlugin(() => {
|
|
|
25
87
|
return { name: "require-organization-membership" };
|
|
26
88
|
}
|
|
27
89
|
|
|
28
|
-
const userId = useCookie("user").value ?? "";
|
|
29
90
|
if (!userId) return { name: "index" };
|
|
30
91
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
getById(org),
|
|
37
|
-
userMemberData.role ? getRoleById(userMemberData.role) : null,
|
|
38
|
-
]);
|
|
39
|
-
|
|
40
|
-
if (orgResult.status === "fulfilled" && orgResult.value) {
|
|
41
|
-
orgNature.value = orgResult.value.nature ?? "";
|
|
42
|
-
}
|
|
92
|
+
// Same bounce as before for an account with no member document. Gated on a
|
|
93
|
+
// finished lookup so a failed request no longer throws the user out.
|
|
94
|
+
if (userAppRoleStatus.value === "resolved" && !hasMemberDocument) {
|
|
95
|
+
return { name: "index" };
|
|
96
|
+
}
|
|
43
97
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
98
|
+
try {
|
|
99
|
+
const orgResult = await getById(org);
|
|
100
|
+
if (orgResult) orgNature.value = orgResult.nature ?? "";
|
|
47
101
|
} catch (error) {
|
|
48
|
-
|
|
102
|
+
// Cosmetic only, and it never blocked navigation before either.
|
|
49
103
|
}
|
|
50
104
|
});
|
|
51
105
|
});
|