@webiny/app-admin 6.5.0 → 6.5.1-beta.1
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/base/Base/DefaultFieldRenderers.js +4 -0
- package/base/Base/DefaultFieldRenderers.js.map +1 -1
- package/base/Base/FieldRenderers/BirthdayInputRenderer.d.ts +21 -0
- package/base/Base/FieldRenderers/BirthdayInputRenderer.js +39 -0
- package/base/Base/FieldRenderers/BirthdayInputRenderer.js.map +1 -0
- package/base/Base/FieldRenderers/BirthdayInputRenderer.test.d.ts +1 -0
- package/base/Base/FieldRenderers/BirthdayInputRenderer.test.js +123 -0
- package/base/Base/FieldRenderers/BirthdayInputRenderer.test.js.map +1 -0
- package/base/Base/UserMenu/SwitchTenant.d.ts +20 -0
- package/base/Base/UserMenu/SwitchTenant.js +45 -0
- package/base/Base/UserMenu/SwitchTenant.js.map +1 -0
- package/base/Base/UserMenu.js +4 -0
- package/base/Base/UserMenu.js.map +1 -1
- package/domain/Identity.d.ts +8 -0
- package/domain/Identity.js +6 -0
- package/domain/Identity.js.map +1 -1
- package/exports/admin/security.d.ts +4 -1
- package/exports/admin/security.js +4 -1
- package/features/formModel/FieldBuilder.test.js +126 -1
- package/features/formModel/FieldBuilder.test.js.map +1 -1
- package/features/formModel/fieldTypes/DateTimeFieldType.d.ts +65 -4
- package/features/formModel/fieldTypes/DateTimeFieldType.js +70 -8
- package/features/formModel/fieldTypes/DateTimeFieldType.js.map +1 -1
- package/features/formModel/fieldTypes/dateLimits.d.ts +37 -0
- package/features/formModel/fieldTypes/dateLimits.js +64 -0
- package/features/formModel/fieldTypes/dateLimits.js.map +1 -0
- package/features/formModel/fieldTypes/dateLimits.test.d.ts +1 -0
- package/features/formModel/fieldTypes/dateLimits.test.js +60 -0
- package/features/formModel/fieldTypes/dateLimits.test.js.map +1 -0
- package/features/formModel/renderers.d.ts +1 -0
- package/features/formModel/renderers.js +1 -0
- package/features/security/LogIn/LogInError.d.ts +15 -0
- package/features/security/LogIn/LogInError.js +25 -0
- package/features/security/LogIn/LogInError.js.map +1 -0
- package/features/security/LogIn/LogInGateway.d.ts +2 -2
- package/features/security/LogIn/LogInGateway.js +22 -5
- package/features/security/LogIn/LogInGateway.js.map +1 -1
- package/features/security/LogIn/LogInRepository.d.ts +1 -2
- package/features/security/LogIn/LogInRepository.js +6 -2
- package/features/security/LogIn/LogInRepository.js.map +1 -1
- package/features/security/LogIn/LogInUseCase.d.ts +7 -2
- package/features/security/LogIn/LogInUseCase.js +53 -3
- package/features/security/LogIn/LogInUseCase.js.map +1 -1
- package/features/security/LogIn/LogInUseCase.test.d.ts +1 -0
- package/features/security/LogIn/LogInUseCase.test.js +321 -0
- package/features/security/LogIn/LogInUseCase.test.js.map +1 -0
- package/features/security/LogIn/abstractions.d.ts +48 -3
- package/features/security/LogIn/abstractions.js.map +1 -1
- package/features/security/LogIn/createLoginMutation.js +40 -27
- package/features/security/LogIn/createLoginMutation.js.map +1 -1
- package/features/security/LogIn/feature.js +2 -0
- package/features/security/LogIn/feature.js.map +1 -1
- package/features/security/LogIn/index.d.ts +1 -0
- package/features/security/LogIn/index.js +1 -0
- package/features/security/LoginTenantResolver/LoginTenantResolver.d.ts +14 -0
- package/features/security/LoginTenantResolver/LoginTenantResolver.js +52 -0
- package/features/security/LoginTenantResolver/LoginTenantResolver.js.map +1 -0
- package/features/security/LoginTenantResolver/abstractions.d.ts +38 -0
- package/features/security/LoginTenantResolver/abstractions.js +5 -0
- package/features/security/LoginTenantResolver/abstractions.js.map +1 -0
- package/features/security/LoginTenantResolver/feature.d.ts +3 -0
- package/features/security/LoginTenantResolver/feature.js +17 -0
- package/features/security/LoginTenantResolver/feature.js.map +1 -0
- package/features/security/LoginTenantResolver/index.d.ts +2 -0
- package/features/security/LoginTenantResolver/index.js +2 -0
- package/package.json +24 -24
- package/presentation/security/components/LoginNoTenantAccess.d.ts +27 -0
- package/presentation/security/components/LoginNoTenantAccess.js +22 -0
- package/presentation/security/components/LoginNoTenantAccess.js.map +1 -0
- package/presentation/security/components/LoginTenantPicker.d.ts +30 -0
- package/presentation/security/components/LoginTenantPicker.js +32 -0
- package/presentation/security/components/LoginTenantPicker.js.map +1 -0
- package/presentation/security/hooks/useAuthentication.d.ts +2 -2
- package/presentation/security/hooks/useAuthentication.js.map +1 -1
- package/presentation/sortable/SortablePresenter.js +3 -2
- package/presentation/sortable/SortablePresenter.js.map +1 -1
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { Container } from "@webiny/di";
|
|
3
|
+
import { MainGraphQLClient } from "@webiny/app/features/mainGraphQLClient/index.js";
|
|
4
|
+
import { LocalStorage } from "@webiny/app/features/localStorage/index.js";
|
|
5
|
+
import { LogInFeature } from "./feature.js";
|
|
6
|
+
import { AuthenticationContextFeature } from "../AuthenticationContext/feature.js";
|
|
7
|
+
import { IdentityContextFeature } from "../IdentityContext/feature.js";
|
|
8
|
+
import { TenancyFeature } from "../../tenancy/feature.js";
|
|
9
|
+
const createLocalStorage = (initial = {})=>{
|
|
10
|
+
const values = new Map(Object.entries(initial));
|
|
11
|
+
return {
|
|
12
|
+
get: (key)=>values.get(key),
|
|
13
|
+
set: (key, value)=>{
|
|
14
|
+
values.set(key, value);
|
|
15
|
+
},
|
|
16
|
+
remove: (key)=>{
|
|
17
|
+
values.delete(key);
|
|
18
|
+
},
|
|
19
|
+
clear: ()=>values.clear(),
|
|
20
|
+
keys: ()=>Array.from(values.keys())
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
const identityDto = {
|
|
24
|
+
__typename: "SecurityIdentity",
|
|
25
|
+
id: "user-1",
|
|
26
|
+
type: "admin",
|
|
27
|
+
displayName: "User 1",
|
|
28
|
+
roles: [],
|
|
29
|
+
teams: [],
|
|
30
|
+
permissions: [
|
|
31
|
+
{
|
|
32
|
+
name: "*"
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
profile: {
|
|
36
|
+
firstName: "",
|
|
37
|
+
lastName: "",
|
|
38
|
+
email: "user-1@webiny.local"
|
|
39
|
+
},
|
|
40
|
+
currentTenant: {
|
|
41
|
+
id: "acme",
|
|
42
|
+
name: "Acme"
|
|
43
|
+
},
|
|
44
|
+
defaultTenant: {
|
|
45
|
+
id: "root",
|
|
46
|
+
name: "Root"
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const loginResponse = (login)=>({
|
|
50
|
+
security: {
|
|
51
|
+
login: {
|
|
52
|
+
data: login.data ?? null,
|
|
53
|
+
error: login.error ?? null
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
const tenantSelectionRequired = (tenants)=>loginResponse({
|
|
58
|
+
data: {
|
|
59
|
+
__typename: "SecurityLoginTenantSelection",
|
|
60
|
+
tenants
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
describe("LogIn feature: tenant resolution", ()=>{
|
|
64
|
+
let container;
|
|
65
|
+
let execute;
|
|
66
|
+
let storage;
|
|
67
|
+
const setup = (storedTenant)=>{
|
|
68
|
+
container = new Container();
|
|
69
|
+
storage = createLocalStorage(storedTenant ? {
|
|
70
|
+
tenantId: storedTenant
|
|
71
|
+
} : {});
|
|
72
|
+
execute = vi.fn();
|
|
73
|
+
container.registerInstance(LocalStorage, storage);
|
|
74
|
+
container.registerInstance(MainGraphQLClient, {
|
|
75
|
+
execute
|
|
76
|
+
});
|
|
77
|
+
TenancyFeature.register(container);
|
|
78
|
+
AuthenticationContextFeature.register(container);
|
|
79
|
+
IdentityContextFeature.register(container);
|
|
80
|
+
LogInFeature.register(container);
|
|
81
|
+
};
|
|
82
|
+
const login = ()=>{
|
|
83
|
+
const { useCase } = LogInFeature.resolve(container);
|
|
84
|
+
return useCase.execute({
|
|
85
|
+
idTokenProvider: async ()=>"token"
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
const currentTenant = ()=>TenancyFeature.resolve(container).service.getCurrentTenant();
|
|
89
|
+
beforeEach(()=>{
|
|
90
|
+
setup();
|
|
91
|
+
});
|
|
92
|
+
it("logs in on the current tenant when the user has access to it", async ()=>{
|
|
93
|
+
setup("acme");
|
|
94
|
+
execute.mockResolvedValue(loginResponse({
|
|
95
|
+
data: identityDto
|
|
96
|
+
}));
|
|
97
|
+
const result = await login();
|
|
98
|
+
expect(result).toEqual({
|
|
99
|
+
type: "loggedIn"
|
|
100
|
+
});
|
|
101
|
+
expect(currentTenant()).toBe("acme");
|
|
102
|
+
expect(execute).toHaveBeenCalledTimes(1);
|
|
103
|
+
});
|
|
104
|
+
it("offers tenant switching only to a user with more than one tenant", async ()=>{
|
|
105
|
+
const identity = ()=>IdentityContextFeature.resolve(container).identityContext.getIdentity();
|
|
106
|
+
const tenants = [
|
|
107
|
+
{
|
|
108
|
+
id: "acme",
|
|
109
|
+
name: "Acme"
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: "beta",
|
|
113
|
+
name: "Beta"
|
|
114
|
+
}
|
|
115
|
+
];
|
|
116
|
+
setup("acme");
|
|
117
|
+
execute.mockResolvedValue(loginResponse({
|
|
118
|
+
data: {
|
|
119
|
+
...identityDto,
|
|
120
|
+
tenants
|
|
121
|
+
}
|
|
122
|
+
}));
|
|
123
|
+
await login();
|
|
124
|
+
expect(identity().tenants).toEqual(tenants);
|
|
125
|
+
expect(identity().canSwitchTenant).toBe(true);
|
|
126
|
+
setup("acme");
|
|
127
|
+
execute.mockResolvedValue(loginResponse({
|
|
128
|
+
data: {
|
|
129
|
+
...identityDto,
|
|
130
|
+
tenants: [
|
|
131
|
+
tenants[0]
|
|
132
|
+
]
|
|
133
|
+
}
|
|
134
|
+
}));
|
|
135
|
+
await login();
|
|
136
|
+
expect(identity().canSwitchTenant).toBe(false);
|
|
137
|
+
setup("acme");
|
|
138
|
+
execute.mockResolvedValue(loginResponse({
|
|
139
|
+
data: identityDto
|
|
140
|
+
}));
|
|
141
|
+
await login();
|
|
142
|
+
expect(identity().tenants).toEqual([]);
|
|
143
|
+
expect(identity().canSwitchTenant).toBe(false);
|
|
144
|
+
setup("root");
|
|
145
|
+
execute.mockResolvedValue(loginResponse({
|
|
146
|
+
data: {
|
|
147
|
+
...identityDto,
|
|
148
|
+
tenants: [
|
|
149
|
+
{
|
|
150
|
+
id: "root",
|
|
151
|
+
name: "Root"
|
|
152
|
+
},
|
|
153
|
+
...tenants
|
|
154
|
+
]
|
|
155
|
+
}
|
|
156
|
+
}));
|
|
157
|
+
await login();
|
|
158
|
+
expect(identity().canSwitchTenant).toBe(false);
|
|
159
|
+
});
|
|
160
|
+
it("sends a root user to root instead of the tenant picker", async ()=>{
|
|
161
|
+
setup("deep");
|
|
162
|
+
execute.mockResolvedValue(tenantSelectionRequired([
|
|
163
|
+
{
|
|
164
|
+
id: "acme",
|
|
165
|
+
name: "Acme"
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
id: "root",
|
|
169
|
+
name: "Root"
|
|
170
|
+
}
|
|
171
|
+
]));
|
|
172
|
+
const result = await login();
|
|
173
|
+
expect(result).toEqual({
|
|
174
|
+
type: "switchingTenant",
|
|
175
|
+
tenantId: "root"
|
|
176
|
+
});
|
|
177
|
+
expect(currentTenant()).toBe("root");
|
|
178
|
+
});
|
|
179
|
+
it("switches to the only tenant the user can access, and remembers it", async ()=>{
|
|
180
|
+
const onChange = vi.fn();
|
|
181
|
+
TenancyFeature.resolve(container).service.onTenantChange(onChange);
|
|
182
|
+
execute.mockResolvedValue(tenantSelectionRequired([
|
|
183
|
+
{
|
|
184
|
+
id: "acme",
|
|
185
|
+
name: "Acme"
|
|
186
|
+
}
|
|
187
|
+
]));
|
|
188
|
+
const result = await login();
|
|
189
|
+
expect(result).toEqual({
|
|
190
|
+
type: "switchingTenant",
|
|
191
|
+
tenantId: "acme"
|
|
192
|
+
});
|
|
193
|
+
expect(currentTenant()).toBe("acme");
|
|
194
|
+
expect(storage.get("tenantId")).toBe("acme");
|
|
195
|
+
expect(onChange).toHaveBeenCalledWith("acme");
|
|
196
|
+
});
|
|
197
|
+
it("asks the user to pick when they can access several tenants", async ()=>{
|
|
198
|
+
const tenants = [
|
|
199
|
+
{
|
|
200
|
+
id: "acme",
|
|
201
|
+
name: "Acme"
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
id: "beta",
|
|
205
|
+
name: "Beta"
|
|
206
|
+
}
|
|
207
|
+
];
|
|
208
|
+
execute.mockResolvedValue(tenantSelectionRequired(tenants));
|
|
209
|
+
const result = await login();
|
|
210
|
+
expect(result).toEqual({
|
|
211
|
+
type: "selectTenant",
|
|
212
|
+
tenants
|
|
213
|
+
});
|
|
214
|
+
expect(currentTenant()).toBe("root");
|
|
215
|
+
});
|
|
216
|
+
it("reports no access when the user has no tenant", async ()=>{
|
|
217
|
+
execute.mockResolvedValue(tenantSelectionRequired([]));
|
|
218
|
+
expect(await login()).toEqual({
|
|
219
|
+
type: "noTenantAccess"
|
|
220
|
+
});
|
|
221
|
+
execute.mockResolvedValue(loginResponse({
|
|
222
|
+
error: {
|
|
223
|
+
code: "Security/Identity/NotAuthorized",
|
|
224
|
+
message: "Missing user profile!"
|
|
225
|
+
}
|
|
226
|
+
}));
|
|
227
|
+
expect(await login()).toEqual({
|
|
228
|
+
type: "noTenantAccess"
|
|
229
|
+
});
|
|
230
|
+
expect(currentTenant()).toBe("root");
|
|
231
|
+
});
|
|
232
|
+
it("sends a user from a `?tenantId` they can't access to their own tenant", async ()=>{
|
|
233
|
+
setup("beta");
|
|
234
|
+
execute.mockResolvedValue(tenantSelectionRequired([
|
|
235
|
+
{
|
|
236
|
+
id: "acme",
|
|
237
|
+
name: "Acme"
|
|
238
|
+
}
|
|
239
|
+
]));
|
|
240
|
+
const result = await login();
|
|
241
|
+
expect(result).toEqual({
|
|
242
|
+
type: "switchingTenant",
|
|
243
|
+
tenantId: "acme"
|
|
244
|
+
});
|
|
245
|
+
expect(currentTenant()).toBe("acme");
|
|
246
|
+
});
|
|
247
|
+
it("throws instead of switching to the tenant it's already on", async ()=>{
|
|
248
|
+
setup("acme");
|
|
249
|
+
const onChange = vi.fn();
|
|
250
|
+
TenancyFeature.resolve(container).service.onTenantChange(onChange);
|
|
251
|
+
execute.mockResolvedValue(tenantSelectionRequired([
|
|
252
|
+
{
|
|
253
|
+
id: "acme",
|
|
254
|
+
name: "Acme"
|
|
255
|
+
}
|
|
256
|
+
]));
|
|
257
|
+
await expect(login()).rejects.toMatchObject({
|
|
258
|
+
code: "Security/Identity/TenantSelectionFailed"
|
|
259
|
+
});
|
|
260
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
261
|
+
});
|
|
262
|
+
it.each([
|
|
263
|
+
[
|
|
264
|
+
"deleted",
|
|
265
|
+
{
|
|
266
|
+
message: "Unable to load tenant!",
|
|
267
|
+
code: "Tenancy/TenantNotFound"
|
|
268
|
+
}
|
|
269
|
+
],
|
|
270
|
+
[
|
|
271
|
+
"disabled",
|
|
272
|
+
{
|
|
273
|
+
message: "Tenant is disabled!",
|
|
274
|
+
code: "Tenancy/TenantDisabled"
|
|
275
|
+
}
|
|
276
|
+
]
|
|
277
|
+
])("retries through root when the stored tenant is %s", async (_, requestError)=>{
|
|
278
|
+
setup("old-tenant");
|
|
279
|
+
execute.mockRejectedValue(requestError);
|
|
280
|
+
const result = await login();
|
|
281
|
+
expect(result).toEqual({
|
|
282
|
+
type: "switchingTenant",
|
|
283
|
+
tenantId: "root"
|
|
284
|
+
});
|
|
285
|
+
expect(currentTenant()).toBe("root");
|
|
286
|
+
});
|
|
287
|
+
it("doesn't loop when root itself can't be loaded", async ()=>{
|
|
288
|
+
setup("root");
|
|
289
|
+
execute.mockRejectedValue({
|
|
290
|
+
message: "Tenant is disabled!",
|
|
291
|
+
code: "Tenancy/TenantDisabled"
|
|
292
|
+
});
|
|
293
|
+
await expect(login()).rejects.toMatchObject({
|
|
294
|
+
code: "Tenancy/TenantDisabled"
|
|
295
|
+
});
|
|
296
|
+
expect(currentTenant()).toBe("root");
|
|
297
|
+
});
|
|
298
|
+
it("throws errors that aren't about the tenant, with their message", async ()=>{
|
|
299
|
+
execute.mockResolvedValue(loginResponse({
|
|
300
|
+
error: {
|
|
301
|
+
code: "SOMETHING",
|
|
302
|
+
message: "Something broke."
|
|
303
|
+
}
|
|
304
|
+
}));
|
|
305
|
+
await expect(login()).rejects.toMatchObject({
|
|
306
|
+
code: "SOMETHING",
|
|
307
|
+
message: "Something broke."
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
it("throws when the user has no permissions on the tenant", async ()=>{
|
|
311
|
+
execute.mockResolvedValue(loginResponse({
|
|
312
|
+
data: {
|
|
313
|
+
...identityDto,
|
|
314
|
+
permissions: []
|
|
315
|
+
}
|
|
316
|
+
}));
|
|
317
|
+
await expect(login()).rejects.toThrow("You have no permissions on this tenant!");
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
//# sourceMappingURL=LogInUseCase.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"features/security/LogIn/LogInUseCase.test.js","sources":["../../../../src/features/security/LogIn/LogInUseCase.test.ts"],"sourcesContent":["import { describe, it, expect, vi, beforeEach } from \"vitest\";\nimport { Container } from \"@webiny/di\";\nimport { MainGraphQLClient } from \"@webiny/app/features/mainGraphQLClient/index.js\";\nimport { LocalStorage } from \"@webiny/app/features/localStorage/index.js\";\nimport { LogInFeature } from \"./feature.js\";\nimport { AuthenticationContextFeature } from \"~/features/security/AuthenticationContext/feature.js\";\nimport { IdentityContextFeature } from \"~/features/security/IdentityContext/feature.js\";\nimport { TenancyFeature } from \"~/features/tenancy/feature.js\";\n\nconst createLocalStorage = (initial: Record<string, any> = {}): LocalStorage.Interface => {\n const values = new Map<string, any>(Object.entries(initial));\n return {\n get: key => values.get(key),\n set: (key, value) => {\n values.set(key, value);\n },\n remove: key => {\n values.delete(key);\n },\n clear: () => values.clear(),\n keys: () => Array.from(values.keys())\n };\n};\n\nconst identityDto = {\n __typename: \"SecurityIdentity\",\n id: \"user-1\",\n type: \"admin\",\n displayName: \"User 1\",\n roles: [],\n teams: [],\n permissions: [{ name: \"*\" }],\n profile: { firstName: \"\", lastName: \"\", email: \"user-1@webiny.local\" },\n currentTenant: { id: \"acme\", name: \"Acme\" },\n defaultTenant: { id: \"root\", name: \"Root\" }\n};\n\nconst loginResponse = (login: { data?: any; error?: any }) => {\n return { security: { login: { data: login.data ?? null, error: login.error ?? null } } };\n};\n\nconst tenantSelectionRequired = (tenants: { id: string; name: string }[]) => {\n return loginResponse({ data: { __typename: \"SecurityLoginTenantSelection\", tenants } });\n};\n\ndescribe(\"LogIn feature: tenant resolution\", () => {\n let container: Container;\n let execute: ReturnType<typeof vi.fn>;\n let storage: LocalStorage.Interface;\n\n const setup = (storedTenant?: string) => {\n container = new Container();\n storage = createLocalStorage(storedTenant ? { tenantId: storedTenant } : {});\n execute = vi.fn();\n\n container.registerInstance(LocalStorage, storage);\n container.registerInstance(MainGraphQLClient, {\n execute\n } as unknown as MainGraphQLClient.Interface);\n\n TenancyFeature.register(container);\n AuthenticationContextFeature.register(container);\n IdentityContextFeature.register(container);\n LogInFeature.register(container);\n };\n\n const login = () => {\n const { useCase } = LogInFeature.resolve(container);\n return useCase.execute({ idTokenProvider: async () => \"token\" });\n };\n\n const currentTenant = () => TenancyFeature.resolve(container).service.getCurrentTenant();\n\n beforeEach(() => {\n setup();\n });\n\n it(\"logs in on the current tenant when the user has access to it\", async () => {\n setup(\"acme\");\n execute.mockResolvedValue(loginResponse({ data: identityDto }));\n\n const result = await login();\n\n expect(result).toEqual({ type: \"loggedIn\" });\n expect(currentTenant()).toBe(\"acme\");\n expect(execute).toHaveBeenCalledTimes(1);\n });\n\n it(\"offers tenant switching only to a user with more than one tenant\", async () => {\n const identity = () =>\n IdentityContextFeature.resolve(container).identityContext.getIdentity();\n const tenants = [\n { id: \"acme\", name: \"Acme\" },\n { id: \"beta\", name: \"Beta\" }\n ];\n\n setup(\"acme\");\n execute.mockResolvedValue(loginResponse({ data: { ...identityDto, tenants } }));\n await login();\n expect(identity().tenants).toEqual(tenants);\n expect(identity().canSwitchTenant).toBe(true);\n\n setup(\"acme\");\n execute.mockResolvedValue(\n loginResponse({ data: { ...identityDto, tenants: [tenants[0]] } })\n );\n await login();\n expect(identity().canSwitchTenant).toBe(false);\n\n // An API without the tenant list (before this change) offers no switching.\n setup(\"acme\");\n execute.mockResolvedValue(loginResponse({ data: identityDto }));\n await login();\n expect(identity().tenants).toEqual([]);\n expect(identity().canSwitchTenant).toBe(false);\n\n // Root users open tenants through Tenant Manager, even when they have records in several.\n setup(\"root\");\n execute.mockResolvedValue(\n loginResponse({\n data: { ...identityDto, tenants: [{ id: \"root\", name: \"Root\" }, ...tenants] }\n })\n );\n await login();\n expect(identity().canSwitchTenant).toBe(false);\n });\n\n it(\"sends a root user to root instead of the tenant picker\", async () => {\n // A tenant below a child of root: the parent lookup doesn't reach the root record.\n setup(\"deep\");\n execute.mockResolvedValue(\n tenantSelectionRequired([\n { id: \"acme\", name: \"Acme\" },\n { id: \"root\", name: \"Root\" }\n ])\n );\n\n const result = await login();\n\n expect(result).toEqual({ type: \"switchingTenant\", tenantId: \"root\" });\n expect(currentTenant()).toBe(\"root\");\n });\n\n it(\"switches to the only tenant the user can access, and remembers it\", async () => {\n const onChange = vi.fn();\n TenancyFeature.resolve(container).service.onTenantChange(onChange);\n execute.mockResolvedValue(tenantSelectionRequired([{ id: \"acme\", name: \"Acme\" }]));\n\n const result = await login();\n\n expect(result).toEqual({ type: \"switchingTenant\", tenantId: \"acme\" });\n expect(currentTenant()).toBe(\"acme\");\n expect(storage.get(\"tenantId\")).toBe(\"acme\");\n // The tenancy provider reloads the app on this change.\n expect(onChange).toHaveBeenCalledWith(\"acme\");\n });\n\n it(\"asks the user to pick when they can access several tenants\", async () => {\n const tenants = [\n { id: \"acme\", name: \"Acme\" },\n { id: \"beta\", name: \"Beta\" }\n ];\n execute.mockResolvedValue(tenantSelectionRequired(tenants));\n\n const result = await login();\n\n expect(result).toEqual({ type: \"selectTenant\", tenants });\n expect(currentTenant()).toBe(\"root\");\n });\n\n it(\"reports no access when the user has no tenant\", async () => {\n execute.mockResolvedValue(tenantSelectionRequired([]));\n expect(await login()).toEqual({ type: \"noTenantAccess\" });\n\n execute.mockResolvedValue(\n loginResponse({\n error: { code: \"Security/Identity/NotAuthorized\", message: \"Missing user profile!\" }\n })\n );\n expect(await login()).toEqual({ type: \"noTenantAccess\" });\n expect(currentTenant()).toBe(\"root\");\n });\n\n it(\"sends a user from a `?tenantId` they can't access to their own tenant\", async () => {\n // The tenancy provider has already stored the `?tenantId` tenant.\n setup(\"beta\");\n execute.mockResolvedValue(tenantSelectionRequired([{ id: \"acme\", name: \"Acme\" }]));\n\n const result = await login();\n\n expect(result).toEqual({ type: \"switchingTenant\", tenantId: \"acme\" });\n expect(currentTenant()).toBe(\"acme\");\n });\n\n it(\"throws instead of switching to the tenant it's already on\", async () => {\n setup(\"acme\");\n const onChange = vi.fn();\n TenancyFeature.resolve(container).service.onTenantChange(onChange);\n execute.mockResolvedValue(tenantSelectionRequired([{ id: \"acme\", name: \"Acme\" }]));\n\n await expect(login()).rejects.toMatchObject({\n code: \"Security/Identity/TenantSelectionFailed\"\n });\n expect(onChange).not.toHaveBeenCalled();\n });\n\n it.each([\n [\"deleted\", { message: \"Unable to load tenant!\", code: \"Tenancy/TenantNotFound\" }],\n [\"disabled\", { message: \"Tenant is disabled!\", code: \"Tenancy/TenantDisabled\" }]\n ])(\"retries through root when the stored tenant is %s\", async (_, requestError) => {\n setup(\"old-tenant\");\n execute.mockRejectedValue(requestError);\n\n const result = await login();\n\n expect(result).toEqual({ type: \"switchingTenant\", tenantId: \"root\" });\n expect(currentTenant()).toBe(\"root\");\n });\n\n it(\"doesn't loop when root itself can't be loaded\", async () => {\n setup(\"root\");\n execute.mockRejectedValue({\n message: \"Tenant is disabled!\",\n code: \"Tenancy/TenantDisabled\"\n });\n\n await expect(login()).rejects.toMatchObject({ code: \"Tenancy/TenantDisabled\" });\n expect(currentTenant()).toBe(\"root\");\n });\n\n it(\"throws errors that aren't about the tenant, with their message\", async () => {\n execute.mockResolvedValue(\n loginResponse({ error: { code: \"SOMETHING\", message: \"Something broke.\" } })\n );\n\n await expect(login()).rejects.toMatchObject({\n code: \"SOMETHING\",\n message: \"Something broke.\"\n });\n });\n\n it(\"throws when the user has no permissions on the tenant\", async () => {\n execute.mockResolvedValue(loginResponse({ data: { ...identityDto, permissions: [] } }));\n\n await expect(login()).rejects.toThrow(\"You have no permissions on this tenant!\");\n });\n});\n"],"names":["createLocalStorage","initial","values","Map","Object","key","value","Array","identityDto","loginResponse","login","tenantSelectionRequired","tenants","describe","container","execute","storage","setup","storedTenant","Container","vi","LocalStorage","MainGraphQLClient","TenancyFeature","AuthenticationContextFeature","IdentityContextFeature","LogInFeature","useCase","currentTenant","beforeEach","it","result","expect","identity","onChange","_","requestError"],"mappings":"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"}
|
|
@@ -5,13 +5,36 @@ export interface ILoginParams {
|
|
|
5
5
|
idTokenProvider: AuthenticationContext.IdTokenProvider;
|
|
6
6
|
logoutCallback?: AuthenticationContext.LogoutCallback;
|
|
7
7
|
}
|
|
8
|
+
export type LogInResult =
|
|
9
|
+
/** The identity is set: the user is logged in. */
|
|
10
|
+
{
|
|
11
|
+
type: "loggedIn";
|
|
12
|
+
}
|
|
13
|
+
/** The current tenant was changed, and the app reloads to log in on it. */
|
|
14
|
+
| {
|
|
15
|
+
type: "switchingTenant";
|
|
16
|
+
tenantId: string;
|
|
17
|
+
}
|
|
18
|
+
/** The user has to pick one of these tenants. */
|
|
19
|
+
| {
|
|
20
|
+
type: "selectTenant";
|
|
21
|
+
tenants: Tenant[];
|
|
22
|
+
}
|
|
23
|
+
/** The user can't access any tenant. */
|
|
24
|
+
| {
|
|
25
|
+
type: "noTenantAccess";
|
|
26
|
+
};
|
|
8
27
|
export interface ILogInUseCase {
|
|
9
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Log in on the current tenant. Errors that aren't about the tenant are thrown.
|
|
30
|
+
*/
|
|
31
|
+
execute(params: ILoginParams): Promise<LogInResult>;
|
|
10
32
|
}
|
|
11
33
|
export declare const LogInUseCase: import("@webiny/di").Abstraction<ILogInUseCase>;
|
|
12
34
|
export declare namespace LogInUseCase {
|
|
13
35
|
type Interface = ILogInUseCase;
|
|
14
36
|
type Params = ILoginParams;
|
|
37
|
+
type Result = LogInResult;
|
|
15
38
|
}
|
|
16
39
|
export interface IIdentityDTO {
|
|
17
40
|
id: string;
|
|
@@ -22,22 +45,44 @@ export interface IIdentityDTO {
|
|
|
22
45
|
permissions: Identity.Permission[];
|
|
23
46
|
currentTenant: Tenant;
|
|
24
47
|
defaultTenant: Tenant;
|
|
48
|
+
/** The enabled tenants in which the identity has a user record. */
|
|
49
|
+
tenants?: Tenant[];
|
|
25
50
|
profile: Identity.Profile;
|
|
26
51
|
}
|
|
52
|
+
/** What `security.login` returned: the identity, or the tenants the user has to log into instead. */
|
|
53
|
+
export type LogInStep<TIdentity> =
|
|
54
|
+
/** The user is logged in on the current tenant. */
|
|
55
|
+
{
|
|
56
|
+
step: "done";
|
|
57
|
+
identity: TIdentity;
|
|
58
|
+
}
|
|
59
|
+
/** The user has no access to the current tenant, but has access to these. */
|
|
60
|
+
| {
|
|
61
|
+
step: "selectTenant";
|
|
62
|
+
tenants: Tenant[];
|
|
63
|
+
};
|
|
27
64
|
export interface ILogInRepository {
|
|
28
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Log in on the current tenant. A failed login throws a `LogInError`.
|
|
67
|
+
*/
|
|
68
|
+
login(): Promise<LogInStep<Identity>>;
|
|
29
69
|
}
|
|
30
70
|
export declare const LogInRepository: import("@webiny/di").Abstraction<ILogInRepository>;
|
|
31
71
|
export declare namespace LogInRepository {
|
|
32
72
|
type Interface = ILogInRepository;
|
|
33
73
|
type IdentityDTO = IIdentityDTO;
|
|
74
|
+
type Result = LogInStep<Identity>;
|
|
34
75
|
}
|
|
35
76
|
export interface ILogInGateway {
|
|
36
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Run `security.login`. An error in the response, or a failed request, throws a `LogInError`.
|
|
79
|
+
*/
|
|
80
|
+
execute(): Promise<LogInStep<IIdentityDTO>>;
|
|
37
81
|
}
|
|
38
82
|
export declare const LogInGateway: import("@webiny/di").Abstraction<ILogInGateway>;
|
|
39
83
|
export declare namespace LogInGateway {
|
|
40
84
|
type Interface = ILogInGateway;
|
|
85
|
+
type Result = LogInStep<IIdentityDTO>;
|
|
41
86
|
}
|
|
42
87
|
export interface IIdentityMapper {
|
|
43
88
|
toIdentity(dto: IIdentityDTO): Identity;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"features/security/LogIn/abstractions.js","sources":["../../../../src/features/security/LogIn/abstractions.ts"],"sourcesContent":["import { createAbstraction } from \"@webiny/feature/admin\";\nimport { AuthenticationContext } from \"~/features/security/AuthenticationContext/index.js\";\nimport type { Identity, IRole, ITeam } from \"~/domain/Identity.js\";\nimport type { Tenant } from \"~/features/tenancy/abstractions.js\";\n\n// Use Case\nexport interface ILoginParams {\n idTokenProvider: AuthenticationContext.IdTokenProvider;\n logoutCallback?: AuthenticationContext.LogoutCallback;\n}\n\nexport interface ILogInUseCase {\n execute(params: ILoginParams): Promise<
|
|
1
|
+
{"version":3,"file":"features/security/LogIn/abstractions.js","sources":["../../../../src/features/security/LogIn/abstractions.ts"],"sourcesContent":["import { createAbstraction } from \"@webiny/feature/admin\";\nimport { AuthenticationContext } from \"~/features/security/AuthenticationContext/index.js\";\nimport type { Identity, IRole, ITeam } from \"~/domain/Identity.js\";\nimport type { Tenant } from \"~/features/tenancy/abstractions.js\";\n\n// Use Case\nexport interface ILoginParams {\n idTokenProvider: AuthenticationContext.IdTokenProvider;\n logoutCallback?: AuthenticationContext.LogoutCallback;\n}\n\nexport type LogInResult =\n /** The identity is set: the user is logged in. */\n | { type: \"loggedIn\" }\n /** The current tenant was changed, and the app reloads to log in on it. */\n | { type: \"switchingTenant\"; tenantId: string }\n /** The user has to pick one of these tenants. */\n | { type: \"selectTenant\"; tenants: Tenant[] }\n /** The user can't access any tenant. */\n | { type: \"noTenantAccess\" };\n\nexport interface ILogInUseCase {\n /**\n * Log in on the current tenant. Errors that aren't about the tenant are thrown.\n */\n execute(params: ILoginParams): Promise<LogInResult>;\n}\nexport const LogInUseCase = createAbstraction<ILogInUseCase>(\"LogInUseCase\");\n\nexport namespace LogInUseCase {\n export type Interface = ILogInUseCase;\n export type Params = ILoginParams;\n export type Result = LogInResult;\n}\n\n// Repository\nexport interface IIdentityDTO {\n id: string;\n type: string;\n displayName: string;\n roles: IRole[];\n teams: ITeam[];\n permissions: Identity.Permission[];\n currentTenant: Tenant;\n defaultTenant: Tenant;\n /** The enabled tenants in which the identity has a user record. */\n tenants?: Tenant[];\n profile: Identity.Profile;\n}\n\n/** What `security.login` returned: the identity, or the tenants the user has to log into instead. */\nexport type LogInStep<TIdentity> =\n /** The user is logged in on the current tenant. */\n | { step: \"done\"; identity: TIdentity }\n /** The user has no access to the current tenant, but has access to these. */\n | { step: \"selectTenant\"; tenants: Tenant[] };\n\nexport interface ILogInRepository {\n /**\n * Log in on the current tenant. A failed login throws a `LogInError`.\n */\n login(): Promise<LogInStep<Identity>>;\n}\n\nexport const LogInRepository = createAbstraction<ILogInRepository>(\"LogInRepository\");\n\nexport namespace LogInRepository {\n export type Interface = ILogInRepository;\n export type IdentityDTO = IIdentityDTO;\n export type Result = LogInStep<Identity>;\n}\n\n// Gateway\nexport interface ILogInGateway {\n /**\n * Run `security.login`. An error in the response, or a failed request, throws a `LogInError`.\n */\n execute(): Promise<LogInStep<IIdentityDTO>>;\n}\n\nexport const LogInGateway = createAbstraction<ILogInGateway>(\"LogInGateway\");\n\nexport namespace LogInGateway {\n export type Interface = ILogInGateway;\n export type Result = LogInStep<IIdentityDTO>;\n}\n\n// Identity Mapper\nexport interface IIdentityMapper {\n toIdentity(dto: IIdentityDTO): Identity;\n}\n\nexport const IdentityMapper = createAbstraction<IIdentityMapper>(\"IdentityMapper\");\n\nexport namespace IdentityMapper {\n export type Interface = IIdentityMapper;\n}\n"],"names":["LogInUseCase","createAbstraction","LogInRepository","LogInGateway","IdentityMapper"],"mappings":";AA2BO,MAAMA,eAAeC,kBAAiC;AAqCtD,MAAMC,kBAAkBD,kBAAoC;AAgB5D,MAAME,eAAeF,kBAAiC;AAYtD,MAAMG,iBAAiBH,kBAAmC"}
|
|
@@ -3,36 +3,49 @@ const createLoginMutation = ()=>`
|
|
|
3
3
|
security {
|
|
4
4
|
login {
|
|
5
5
|
data {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
type
|
|
9
|
-
roles {
|
|
6
|
+
__typename
|
|
7
|
+
... on SecurityIdentity {
|
|
10
8
|
id
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
displayName
|
|
10
|
+
type
|
|
11
|
+
roles {
|
|
12
|
+
id
|
|
13
|
+
slug
|
|
14
|
+
name
|
|
15
|
+
}
|
|
16
|
+
teams {
|
|
17
|
+
id
|
|
18
|
+
slug
|
|
19
|
+
name
|
|
20
|
+
}
|
|
21
|
+
profile {
|
|
22
|
+
firstName
|
|
23
|
+
lastName
|
|
24
|
+
email
|
|
25
|
+
avatar
|
|
26
|
+
external
|
|
27
|
+
createdOn
|
|
28
|
+
}
|
|
29
|
+
currentTenant {
|
|
30
|
+
id
|
|
31
|
+
name
|
|
32
|
+
}
|
|
33
|
+
defaultTenant {
|
|
34
|
+
id
|
|
35
|
+
name
|
|
36
|
+
}
|
|
37
|
+
tenants {
|
|
38
|
+
id
|
|
39
|
+
name
|
|
40
|
+
}
|
|
41
|
+
permissions
|
|
13
42
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
profile {
|
|
20
|
-
firstName
|
|
21
|
-
lastName
|
|
22
|
-
email
|
|
23
|
-
avatar
|
|
24
|
-
external
|
|
25
|
-
createdOn
|
|
26
|
-
}
|
|
27
|
-
currentTenant {
|
|
28
|
-
id
|
|
29
|
-
name
|
|
30
|
-
}
|
|
31
|
-
defaultTenant {
|
|
32
|
-
id
|
|
33
|
-
name
|
|
43
|
+
... on SecurityLoginTenantSelection {
|
|
44
|
+
tenants {
|
|
45
|
+
id
|
|
46
|
+
name
|
|
47
|
+
}
|
|
34
48
|
}
|
|
35
|
-
permissions
|
|
36
49
|
}
|
|
37
50
|
error {
|
|
38
51
|
code
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"features/security/LogIn/createLoginMutation.js","sources":["../../../../src/features/security/LogIn/createLoginMutation.ts"],"sourcesContent":["export const createLoginMutation = () => {\n return /* GraphQL */ `\n mutation Login {\n security {\n login {\n data {\n id\n
|
|
1
|
+
{"version":3,"file":"features/security/LogIn/createLoginMutation.js","sources":["../../../../src/features/security/LogIn/createLoginMutation.ts"],"sourcesContent":["export const createLoginMutation = () => {\n return /* GraphQL */ `\n mutation Login {\n security {\n login {\n data {\n __typename\n ... on SecurityIdentity {\n id\n displayName\n type\n roles {\n id\n slug\n name\n }\n teams {\n id\n slug\n name\n }\n profile {\n firstName\n lastName\n email\n avatar\n external\n createdOn\n }\n currentTenant {\n id\n name\n }\n defaultTenant {\n id\n name\n }\n tenants {\n id\n name\n }\n permissions\n }\n ... on SecurityLoginTenantSelection {\n tenants {\n id\n name\n }\n }\n }\n error {\n code\n message\n data\n }\n }\n }\n }\n `;\n};\n"],"names":["createLoginMutation"],"mappings":"AAAO,MAAMA,sBAAsB,IACV,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAyDtB,CAAC"}
|
|
@@ -4,6 +4,7 @@ import { LogInUseCase as external_LogInUseCase_js_LogInUseCase } from "./LogInUs
|
|
|
4
4
|
import { IdentityMapper } from "./IdentityMapper.js";
|
|
5
5
|
import { LogInGateway } from "./LogInGateway.js";
|
|
6
6
|
import { LogInRepository } from "./LogInRepository.js";
|
|
7
|
+
import { LoginTenantResolverFeature } from "../LoginTenantResolver/index.js";
|
|
7
8
|
const LogInFeature = createFeature({
|
|
8
9
|
name: "LogIn",
|
|
9
10
|
register (container) {
|
|
@@ -11,6 +12,7 @@ const LogInFeature = createFeature({
|
|
|
11
12
|
container.register(IdentityMapper).inSingletonScope();
|
|
12
13
|
container.register(LogInGateway).inSingletonScope();
|
|
13
14
|
container.register(LogInRepository).inSingletonScope();
|
|
15
|
+
LoginTenantResolverFeature.register(container);
|
|
14
16
|
},
|
|
15
17
|
resolve (container) {
|
|
16
18
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"features/security/LogIn/feature.js","sources":["../../../../src/features/security/LogIn/feature.ts"],"sourcesContent":["import { createFeature } from \"@webiny/feature/admin\";\nimport { LogInUseCase as UseCase } from \"./abstractions.js\";\nimport { LogInUseCase } from \"./LogInUseCase.js\";\nimport { IdentityMapper } from \"./IdentityMapper.js\";\nimport { LogInGateway } from \"./LogInGateway.js\";\nimport { LogInRepository } from \"./LogInRepository.js\";\n\nexport const LogInFeature = createFeature({\n name: \"LogIn\",\n register(container) {\n container.register(LogInUseCase);\n container.register(IdentityMapper).inSingletonScope();\n container.register(LogInGateway).inSingletonScope();\n container.register(LogInRepository).inSingletonScope();\n },\n resolve(container) {\n return {\n useCase: container.resolve(UseCase)\n };\n }\n});\n"],"names":["LogInFeature","createFeature","container","LogInUseCase","IdentityMapper","LogInGateway","LogInRepository","UseCase"],"mappings":"
|
|
1
|
+
{"version":3,"file":"features/security/LogIn/feature.js","sources":["../../../../src/features/security/LogIn/feature.ts"],"sourcesContent":["import { createFeature } from \"@webiny/feature/admin\";\nimport { LogInUseCase as UseCase } from \"./abstractions.js\";\nimport { LogInUseCase } from \"./LogInUseCase.js\";\nimport { IdentityMapper } from \"./IdentityMapper.js\";\nimport { LogInGateway } from \"./LogInGateway.js\";\nimport { LogInRepository } from \"./LogInRepository.js\";\nimport { LoginTenantResolverFeature } from \"~/features/security/LoginTenantResolver/index.js\";\n\nexport const LogInFeature = createFeature({\n name: \"LogIn\",\n register(container) {\n container.register(LogInUseCase);\n container.register(IdentityMapper).inSingletonScope();\n container.register(LogInGateway).inSingletonScope();\n container.register(LogInRepository).inSingletonScope();\n LoginTenantResolverFeature.register(container);\n },\n resolve(container) {\n return {\n useCase: container.resolve(UseCase)\n };\n }\n});\n"],"names":["LogInFeature","createFeature","container","LogInUseCase","IdentityMapper","LogInGateway","LogInRepository","LoginTenantResolverFeature","UseCase"],"mappings":";;;;;;;AAQO,MAAMA,eAAeC,cAAc;IACtC,MAAM;IACN,UAASC,SAAS;QACdA,UAAU,QAAQ,CAACC;QACnBD,UAAU,QAAQ,CAACE,gBAAgB,gBAAgB;QACnDF,UAAU,QAAQ,CAACG,cAAc,gBAAgB;QACjDH,UAAU,QAAQ,CAACI,iBAAiB,gBAAgB;QACpDC,2BAA2B,QAAQ,CAACL;IACxC;IACA,SAAQA,SAAS;QACb,OAAO;YACH,SAASA,UAAU,OAAO,CAACM;QAC/B;IACJ;AACJ"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { LoginTenantResolver as Abstraction } from "./abstractions.js";
|
|
2
|
+
import { TenantContext } from "../../../features/tenancy/abstractions.js";
|
|
3
|
+
import type { Tenant } from "../../../features/tenancy/abstractions.js";
|
|
4
|
+
import type { LogInError } from "../../../features/security/LogIn/index.js";
|
|
5
|
+
declare class LoginTenantResolverImpl implements Abstraction.Interface {
|
|
6
|
+
private tenantContext;
|
|
7
|
+
constructor(tenantContext: TenantContext.Interface);
|
|
8
|
+
resolveTenants(tenants: Tenant[]): Promise<Abstraction.Resolution>;
|
|
9
|
+
resolveError(error: LogInError): Promise<Abstraction.Resolution>;
|
|
10
|
+
}
|
|
11
|
+
export declare const LoginTenantResolver: typeof LoginTenantResolverImpl & {
|
|
12
|
+
__abstraction: import("@webiny/di").Abstraction<import("./abstractions.js").ILoginTenantResolver>;
|
|
13
|
+
};
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { LoginTenantResolver } from "./abstractions.js";
|
|
2
|
+
import { TenantContext } from "../../tenancy/abstractions.js";
|
|
3
|
+
const ROOT_TENANT = "root";
|
|
4
|
+
const UNAVAILABLE_TENANT_CODES = [
|
|
5
|
+
"Tenancy/TenantNotFound",
|
|
6
|
+
"Tenancy/TenantDisabled"
|
|
7
|
+
];
|
|
8
|
+
class LoginTenantResolverImpl {
|
|
9
|
+
constructor(tenantContext){
|
|
10
|
+
this.tenantContext = tenantContext;
|
|
11
|
+
}
|
|
12
|
+
async resolveTenants(tenants) {
|
|
13
|
+
if (tenants.some((tenant)=>tenant.id === ROOT_TENANT)) return {
|
|
14
|
+
type: "switchTenant",
|
|
15
|
+
tenantId: ROOT_TENANT
|
|
16
|
+
};
|
|
17
|
+
if (1 === tenants.length) return {
|
|
18
|
+
type: "switchTenant",
|
|
19
|
+
tenantId: tenants[0].id
|
|
20
|
+
};
|
|
21
|
+
if (tenants.length > 1) return {
|
|
22
|
+
type: "selectTenant",
|
|
23
|
+
tenants
|
|
24
|
+
};
|
|
25
|
+
return {
|
|
26
|
+
type: "noTenantAccess"
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
async resolveError(error) {
|
|
30
|
+
if ("Security/Identity/NotAuthorized" === error.code) return {
|
|
31
|
+
type: "noTenantAccess"
|
|
32
|
+
};
|
|
33
|
+
if (UNAVAILABLE_TENANT_CODES.includes(error.code)) {
|
|
34
|
+
if (this.tenantContext.getCurrentTenant() !== ROOT_TENANT) return {
|
|
35
|
+
type: "switchTenant",
|
|
36
|
+
tenantId: ROOT_TENANT
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
type: "unresolved"
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const LoginTenantResolver_LoginTenantResolver = LoginTenantResolver.createImplementation({
|
|
45
|
+
implementation: LoginTenantResolverImpl,
|
|
46
|
+
dependencies: [
|
|
47
|
+
TenantContext
|
|
48
|
+
]
|
|
49
|
+
});
|
|
50
|
+
export { LoginTenantResolver_LoginTenantResolver as LoginTenantResolver };
|
|
51
|
+
|
|
52
|
+
//# sourceMappingURL=LoginTenantResolver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"features/security/LoginTenantResolver/LoginTenantResolver.js","sources":["../../../../src/features/security/LoginTenantResolver/LoginTenantResolver.ts"],"sourcesContent":["import { LoginTenantResolver as Abstraction } from \"./abstractions.js\";\nimport { TenantContext } from \"~/features/tenancy/abstractions.js\";\nimport type { Tenant } from \"~/features/tenancy/abstractions.js\";\nimport type { LogInError } from \"~/features/security/LogIn/index.js\";\n\nconst ROOT_TENANT = \"root\";\n\n// The API couldn't load the tenant from the `x-tenant` header: it was deleted or disabled.\nconst UNAVAILABLE_TENANT_CODES = [\"Tenancy/TenantNotFound\", \"Tenancy/TenantDisabled\"];\n\nclass LoginTenantResolverImpl implements Abstraction.Interface {\n constructor(private tenantContext: TenantContext.Interface) {}\n\n async resolveTenants(tenants: Tenant[]): Promise<Abstraction.Resolution> {\n // Root users never pick a tenant: they log in on root and open tenants through Tenant Manager.\n if (tenants.some(tenant => tenant.id === ROOT_TENANT)) {\n return { type: \"switchTenant\", tenantId: ROOT_TENANT };\n }\n\n if (tenants.length === 1) {\n return { type: \"switchTenant\", tenantId: tenants[0].id };\n }\n\n if (tenants.length > 1) {\n return { type: \"selectTenant\", tenants };\n }\n\n return { type: \"noTenantAccess\" };\n }\n\n async resolveError(error: LogInError): Promise<Abstraction.Resolution> {\n if (error.code === \"Security/Identity/NotAuthorized\") {\n return { type: \"noTenantAccess\" };\n }\n\n // A stale tenant (from localStorage or `?tenantId`) fails every request. Root always loads,\n // so this retries through root at most once.\n if (UNAVAILABLE_TENANT_CODES.includes(error.code)) {\n if (this.tenantContext.getCurrentTenant() !== ROOT_TENANT) {\n return { type: \"switchTenant\", tenantId: ROOT_TENANT };\n }\n }\n\n return { type: \"unresolved\" };\n }\n}\n\nexport const LoginTenantResolver = Abstraction.createImplementation({\n implementation: LoginTenantResolverImpl,\n dependencies: [TenantContext]\n});\n"],"names":["ROOT_TENANT","UNAVAILABLE_TENANT_CODES","LoginTenantResolverImpl","tenantContext","tenants","tenant","error","LoginTenantResolver","Abstraction","TenantContext"],"mappings":";;AAKA,MAAMA,cAAc;AAGpB,MAAMC,2BAA2B;IAAC;IAA0B;CAAyB;AAErF,MAAMC;IACF,YAAoBC,aAAsC,CAAE;aAAxCA,aAAa,GAAbA;IAAyC;IAE7D,MAAM,eAAeC,OAAiB,EAAmC;QAErE,IAAIA,QAAQ,IAAI,CAACC,CAAAA,SAAUA,OAAO,EAAE,KAAKL,cACrC,OAAO;YAAE,MAAM;YAAgB,UAAUA;QAAY;QAGzD,IAAII,AAAmB,MAAnBA,QAAQ,MAAM,EACd,OAAO;YAAE,MAAM;YAAgB,UAAUA,OAAO,CAAC,EAAE,CAAC,EAAE;QAAC;QAG3D,IAAIA,QAAQ,MAAM,GAAG,GACjB,OAAO;YAAE,MAAM;YAAgBA;QAAQ;QAG3C,OAAO;YAAE,MAAM;QAAiB;IACpC;IAEA,MAAM,aAAaE,KAAiB,EAAmC;QACnE,IAAIA,AAAe,sCAAfA,MAAM,IAAI,EACV,OAAO;YAAE,MAAM;QAAiB;QAKpC,IAAIL,yBAAyB,QAAQ,CAACK,MAAM,IAAI,GAC5C;YAAA,IAAI,IAAI,CAAC,aAAa,CAAC,gBAAgB,OAAON,aAC1C,OAAO;gBAAE,MAAM;gBAAgB,UAAUA;YAAY;QACzD;QAGJ,OAAO;YAAE,MAAM;QAAa;IAChC;AACJ;AAEO,MAAMO,0CAAsBC,oBAAAA,oBAAgC,CAAC;IAChE,gBAAgBN;IAChB,cAAc;QAACO;KAAc;AACjC"}
|