@jskit-ai/users-core 0.1.4
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/package.descriptor.mjs +464 -0
- package/package.json +35 -0
- package/src/server/UsersCoreServiceProvider.js +74 -0
- package/src/server/accountNotifications/accountNotificationsActions.js +39 -0
- package/src/server/accountNotifications/accountNotificationsService.js +41 -0
- package/src/server/accountNotifications/bootAccountNotificationsRoutes.js +41 -0
- package/src/server/accountNotifications/registerAccountNotifications.js +39 -0
- package/src/server/accountPreferences/accountPreferencesActions.js +39 -0
- package/src/server/accountPreferences/accountPreferencesService.js +41 -0
- package/src/server/accountPreferences/bootAccountPreferencesRoutes.js +41 -0
- package/src/server/accountPreferences/registerAccountPreferences.js +39 -0
- package/src/server/accountProfile/accountProfileActions.js +137 -0
- package/src/server/accountProfile/accountProfileService.js +124 -0
- package/src/server/accountProfile/avatarService.js +141 -0
- package/src/server/accountProfile/avatarStorageService.js +132 -0
- package/src/server/accountProfile/bootAccountProfileRoutes.js +166 -0
- package/src/server/accountProfile/registerAccountProfile.js +62 -0
- package/src/server/accountProfile/registerAvatarMultipartSupport.js +43 -0
- package/src/server/accountSecurity/accountSecurityActions.js +144 -0
- package/src/server/accountSecurity/accountSecurityService.js +103 -0
- package/src/server/accountSecurity/bootAccountSecurityRoutes.js +183 -0
- package/src/server/accountSecurity/registerAccountSecurity.js +31 -0
- package/src/server/common/README.md +21 -0
- package/src/server/common/contributors/README.md +11 -0
- package/src/server/common/contributors/workspaceActionContextContributor.js +79 -0
- package/src/server/common/contributors/workspaceAuthPolicyContextResolver.js +34 -0
- package/src/server/common/contributors/workspaceRouteVisibilityResolver.js +79 -0
- package/src/server/common/diTokens.js +21 -0
- package/src/server/common/formatters/README.md +11 -0
- package/src/server/common/formatters/accountAvatarFormatter.js +42 -0
- package/src/server/common/formatters/accountSecurityStatusFormatter.js +71 -0
- package/src/server/common/formatters/accountSettingsResponseFormatter.js +62 -0
- package/src/server/common/formatters/workspaceFormatter.js +46 -0
- package/src/server/common/registerCommonRepositories.js +45 -0
- package/src/server/common/registerSharedApi.js +9 -0
- package/src/server/common/repositories/README.md +24 -0
- package/src/server/common/repositories/repositoryUtils.js +50 -0
- package/src/server/common/repositories/userProfilesRepository.js +251 -0
- package/src/server/common/repositories/userSettingsRepository.js +179 -0
- package/src/server/common/repositories/workspaceInvitesRepository.js +172 -0
- package/src/server/common/repositories/workspaceMembershipsRepository.js +157 -0
- package/src/server/common/repositories/workspacesRepository.js +183 -0
- package/src/server/common/routes/README.md +11 -0
- package/src/server/common/services/README.md +12 -0
- package/src/server/common/services/accountContextService.js +31 -0
- package/src/server/common/services/authProfileSyncService.js +128 -0
- package/src/server/common/services/workspaceContextService.js +270 -0
- package/src/server/common/support/deepFreeze.js +17 -0
- package/src/server/common/support/realtimeServiceEvents.js +94 -0
- package/src/server/common/support/resolveActionUser.js +11 -0
- package/src/server/common/support/workspaceRoutePaths.js +17 -0
- package/src/server/common/validators/README.md +11 -0
- package/src/server/common/validators/authenticatedUserValidator.js +42 -0
- package/src/server/common/validators/routeParamsValidator.js +62 -0
- package/src/server/consoleSettings/bootConsoleSettingsRoutes.js +64 -0
- package/src/server/consoleSettings/consoleService.js +36 -0
- package/src/server/consoleSettings/consoleSettingsActions.js +55 -0
- package/src/server/consoleSettings/consoleSettingsRepository.js +111 -0
- package/src/server/consoleSettings/consoleSettingsService.js +40 -0
- package/src/server/consoleSettings/registerConsoleSettings.js +57 -0
- package/src/server/registerWorkspaceBootstrap.js +36 -0
- package/src/server/registerWorkspaceCore.js +95 -0
- package/src/server/support/resolveWorkspace.js +16 -0
- package/src/server/support/workspaceActionSurfaces.js +135 -0
- package/src/server/support/workspaceInvitationsPolicy.js +45 -0
- package/src/server/support/workspaceRouteInput.js +22 -0
- package/src/server/workspaceBootstrapContributor.js +401 -0
- package/src/server/workspaceDirectory/bootWorkspaceDirectoryRoutes.js +73 -0
- package/src/server/workspaceDirectory/registerWorkspaceDirectory.js +19 -0
- package/src/server/workspaceDirectory/workspaceDirectoryActions.js +65 -0
- package/src/server/workspaceMembers/bootWorkspaceMembers.js +238 -0
- package/src/server/workspaceMembers/registerWorkspaceMembers.js +112 -0
- package/src/server/workspaceMembers/workspaceMembersActions.js +186 -0
- package/src/server/workspaceMembers/workspaceMembersService.js +210 -0
- package/src/server/workspacePendingInvitations/bootWorkspacePendingInvitations.js +63 -0
- package/src/server/workspacePendingInvitations/registerWorkspacePendingInvitations.js +128 -0
- package/src/server/workspacePendingInvitations/workspacePendingInvitationsActions.js +74 -0
- package/src/server/workspacePendingInvitations/workspacePendingInvitationsService.js +137 -0
- package/src/server/workspaceSettings/bootWorkspaceSettings.js +77 -0
- package/src/server/workspaceSettings/registerWorkspaceSettings.js +67 -0
- package/src/server/workspaceSettings/workspaceSettingsActions.js +72 -0
- package/src/server/workspaceSettings/workspaceSettingsRepository.js +135 -0
- package/src/server/workspaceSettings/workspaceSettingsService.js +65 -0
- package/src/shared/events/usersEvents.js +19 -0
- package/src/shared/index.js +91 -0
- package/src/shared/operationMessages.js +16 -0
- package/src/shared/resources/consoleSettingsFields.js +55 -0
- package/src/shared/resources/consoleSettingsResource.js +139 -0
- package/src/shared/resources/resolveGlobalArrayRegistry.js +6 -0
- package/src/shared/resources/userProfileResource.js +148 -0
- package/src/shared/resources/userSettingsFields.js +71 -0
- package/src/shared/resources/userSettingsResource.js +416 -0
- package/src/shared/resources/workspaceMembersResource.js +352 -0
- package/src/shared/resources/workspacePendingInvitationsResource.js +87 -0
- package/src/shared/resources/workspaceResource.js +149 -0
- package/src/shared/resources/workspaceSettingsFields.js +60 -0
- package/src/shared/resources/workspaceSettingsResource.js +178 -0
- package/src/shared/roles.js +136 -0
- package/src/shared/settings.js +31 -0
- package/src/shared/support/usersApiPaths.js +34 -0
- package/src/shared/support/usersVisibility.js +45 -0
- package/src/shared/support/workspacePathModel.js +145 -0
- package/src/shared/tenancyMode.js +35 -0
- package/src/shared/tenancyProfile.js +73 -0
- package/templates/config/workspaceRoles.js +30 -0
- package/templates/migrations/users_core_console_owner.cjs +39 -0
- package/templates/migrations/users_core_initial.cjs +118 -0
- package/templates/migrations/users_core_profile_username.cjs +98 -0
- package/templates/packages/main/src/shared/resources/consoleSettingsFields.js +11 -0
- package/templates/packages/main/src/shared/resources/userSettingsFields.js +138 -0
- package/templates/packages/main/src/shared/resources/workspaceSettingsFields.js +105 -0
- package/test/authProfileSyncService.test.js +119 -0
- package/test/avatarService.test.js +114 -0
- package/test/avatarStorageService.test.js +61 -0
- package/test/consoleService.test.js +57 -0
- package/test/consoleSettingsService.test.js +86 -0
- package/test/exportsContract.test.js +38 -0
- package/test/registerAvatarMultipartSupport.test.js +64 -0
- package/test/registerServiceRealtimeEvents.test.js +160 -0
- package/test/registerWorkspaceDirectory.test.js +26 -0
- package/test/registerWorkspaceSettings.test.js +44 -0
- package/test/resourcesCanonical.test.js +90 -0
- package/test/roles.test.js +74 -0
- package/test/settingsFieldRegistriesSingleton.test.js +24 -0
- package/test/tenancyProfile.test.js +67 -0
- package/test/userSettingsResource.test.js +31 -0
- package/test/usersApiPaths.test.js +31 -0
- package/test/usersRouteRequestInputValidator.test.js +556 -0
- package/test/usersRouteResources.test.js +113 -0
- package/test/usersRouteValidators.test.js +49 -0
- package/test/usersVisibility.test.js +22 -0
- package/test/workspaceActionContextContributor.test.js +251 -0
- package/test/workspaceActionSurfaces.test.js +105 -0
- package/test/workspaceAuthPolicyContextResolver.test.js +119 -0
- package/test/workspaceBootstrapContributor.test.js +466 -0
- package/test/workspaceInvitationsPolicy.test.js +71 -0
- package/test/workspaceInvitesRepository.test.js +111 -0
- package/test/workspaceMembersService.test.js +400 -0
- package/test/workspacePathModel.test.js +93 -0
- package/test/workspacePendingInvitationsResource.test.js +38 -0
- package/test/workspacePendingInvitationsService.test.js +151 -0
- package/test/workspaceRouteVisibilityResolver.test.js +83 -0
- package/test/workspaceService.test.js +480 -0
- package/test/workspaceSettingsActions.test.js +42 -0
- package/test/workspaceSettingsRepository.test.js +156 -0
- package/test/workspaceSettingsResource.test.js +156 -0
- package/test/workspaceSettingsService.test.js +120 -0
- package/test-support/registerDefaultSettingsFields.js +3 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { Type } from "@fastify/type-provider-typebox";
|
|
2
|
+
import { withStandardErrorResponses } from "@jskit-ai/http-runtime/shared/validators/errorResponses";
|
|
3
|
+
import { KERNEL_TOKENS } from "@jskit-ai/kernel/shared/support/tokens";
|
|
4
|
+
import { userSettingsResource } from "../../shared/resources/userSettingsResource.js";
|
|
5
|
+
|
|
6
|
+
function bootAccountSecurityRoutes(app) {
|
|
7
|
+
if (!app || typeof app.make !== "function") {
|
|
8
|
+
throw new Error("bootAccountSecurityRoutes requires application make().");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const router = app.make(KERNEL_TOKENS.HttpRouter);
|
|
12
|
+
const authService = app.make("authService");
|
|
13
|
+
|
|
14
|
+
router.register(
|
|
15
|
+
"POST",
|
|
16
|
+
"/api/settings/security/change-password",
|
|
17
|
+
{
|
|
18
|
+
auth: "required",
|
|
19
|
+
meta: {
|
|
20
|
+
tags: ["settings"],
|
|
21
|
+
summary: "Set or change authenticated user's password"
|
|
22
|
+
},
|
|
23
|
+
bodyValidator: userSettingsResource.operations.passwordChange.bodyValidator,
|
|
24
|
+
responseValidators: withStandardErrorResponses(
|
|
25
|
+
{
|
|
26
|
+
200: userSettingsResource.operations.passwordChange.outputValidator
|
|
27
|
+
},
|
|
28
|
+
{ includeValidation400: true }
|
|
29
|
+
),
|
|
30
|
+
rateLimit: {
|
|
31
|
+
max: 10,
|
|
32
|
+
timeWindow: "1 minute"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
async function (request, reply) {
|
|
36
|
+
const result = await request.executeAction({
|
|
37
|
+
actionId: "settings.security.password.change",
|
|
38
|
+
input: {
|
|
39
|
+
payload: request.input.body
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (result?.session && typeof authService.writeSessionCookies === "function") {
|
|
44
|
+
authService.writeSessionCookies(reply, result.session);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
reply.code(200).send({
|
|
48
|
+
ok: true,
|
|
49
|
+
message: result?.message || "Password updated."
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
router.register(
|
|
55
|
+
"PATCH",
|
|
56
|
+
"/api/settings/security/methods/password",
|
|
57
|
+
{
|
|
58
|
+
auth: "required",
|
|
59
|
+
meta: {
|
|
60
|
+
tags: ["settings"],
|
|
61
|
+
summary: "Enable or disable password sign-in method"
|
|
62
|
+
},
|
|
63
|
+
bodyValidator: userSettingsResource.operations.passwordMethodToggle.bodyValidator,
|
|
64
|
+
responseValidators: withStandardErrorResponses(
|
|
65
|
+
{
|
|
66
|
+
200: userSettingsResource.operations.passwordMethodToggle.outputValidator
|
|
67
|
+
},
|
|
68
|
+
{ includeValidation400: true }
|
|
69
|
+
),
|
|
70
|
+
rateLimit: {
|
|
71
|
+
max: 20,
|
|
72
|
+
timeWindow: "1 minute"
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
async function (request, reply) {
|
|
76
|
+
const response = await request.executeAction({
|
|
77
|
+
actionId: "settings.security.password_method.toggle",
|
|
78
|
+
input: {
|
|
79
|
+
payload: request.input.body
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
reply.code(200).send(response);
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
router.register(
|
|
88
|
+
"GET",
|
|
89
|
+
"/api/settings/security/oauth/:provider/start",
|
|
90
|
+
{
|
|
91
|
+
auth: "required",
|
|
92
|
+
csrfProtection: false,
|
|
93
|
+
meta: {
|
|
94
|
+
tags: ["settings"],
|
|
95
|
+
summary: "Start linking an OAuth provider for authenticated user"
|
|
96
|
+
},
|
|
97
|
+
paramsValidator: userSettingsResource.operations.oauthLinkStart.paramsValidator,
|
|
98
|
+
queryValidator: userSettingsResource.operations.oauthLinkStart.queryValidator,
|
|
99
|
+
responseValidators: withStandardErrorResponses(
|
|
100
|
+
{
|
|
101
|
+
302: { schema: Type.Unknown() }
|
|
102
|
+
},
|
|
103
|
+
{ includeValidation400: true }
|
|
104
|
+
),
|
|
105
|
+
rateLimit: {
|
|
106
|
+
max: 20,
|
|
107
|
+
timeWindow: "1 minute"
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
async function (request, reply) {
|
|
111
|
+
const result = await request.executeAction({
|
|
112
|
+
actionId: "settings.security.oauth.link.start",
|
|
113
|
+
input: {
|
|
114
|
+
provider: request.input.params.provider,
|
|
115
|
+
returnTo: request.input.query.returnTo
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
reply.redirect(result.url);
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
router.register(
|
|
124
|
+
"DELETE",
|
|
125
|
+
"/api/settings/security/oauth/:provider",
|
|
126
|
+
{
|
|
127
|
+
auth: "required",
|
|
128
|
+
meta: {
|
|
129
|
+
tags: ["settings"],
|
|
130
|
+
summary: "Unlink an OAuth provider from authenticated account"
|
|
131
|
+
},
|
|
132
|
+
paramsValidator: userSettingsResource.operations.oauthUnlink.paramsValidator,
|
|
133
|
+
responseValidators: withStandardErrorResponses(
|
|
134
|
+
{
|
|
135
|
+
200: userSettingsResource.operations.oauthUnlink.outputValidator
|
|
136
|
+
},
|
|
137
|
+
{ includeValidation400: true }
|
|
138
|
+
),
|
|
139
|
+
rateLimit: {
|
|
140
|
+
max: 20,
|
|
141
|
+
timeWindow: "1 minute"
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
async function (request, reply) {
|
|
145
|
+
const response = await request.executeAction({
|
|
146
|
+
actionId: "settings.security.oauth.unlink",
|
|
147
|
+
input: {
|
|
148
|
+
provider: request.input.params.provider
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
reply.code(200).send(response);
|
|
153
|
+
}
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
router.register(
|
|
157
|
+
"POST",
|
|
158
|
+
"/api/settings/security/logout-others",
|
|
159
|
+
{
|
|
160
|
+
auth: "required",
|
|
161
|
+
meta: {
|
|
162
|
+
tags: ["settings"],
|
|
163
|
+
summary: "Sign out from other active sessions"
|
|
164
|
+
},
|
|
165
|
+
responseValidators: withStandardErrorResponses({
|
|
166
|
+
200: userSettingsResource.operations.logoutOtherSessions.outputValidator
|
|
167
|
+
}),
|
|
168
|
+
rateLimit: {
|
|
169
|
+
max: 20,
|
|
170
|
+
timeWindow: "1 minute"
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
async function (request, reply) {
|
|
174
|
+
const response = await request.executeAction({
|
|
175
|
+
actionId: "settings.security.sessions.logout_others",
|
|
176
|
+
input: {}
|
|
177
|
+
});
|
|
178
|
+
reply.code(200).send(response);
|
|
179
|
+
}
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export { bootAccountSecurityRoutes };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { withActionDefaults } from "@jskit-ai/kernel/shared/actions";
|
|
2
|
+
import { createService as createAccountSecurityService } from "./accountSecurityService.js";
|
|
3
|
+
import { accountSecurityActions } from "./accountSecurityActions.js";
|
|
4
|
+
|
|
5
|
+
const USERS_ACCOUNT_SECURITY_SERVICE_TOKEN = "users.accountSecurity.service";
|
|
6
|
+
|
|
7
|
+
function registerAccountSecurity(app) {
|
|
8
|
+
if (!app || typeof app.singleton !== "function" || typeof app.actions !== "function") {
|
|
9
|
+
throw new Error("registerAccountSecurity requires application singleton()/actions().");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
app.singleton(USERS_ACCOUNT_SECURITY_SERVICE_TOKEN, (scope) => {
|
|
13
|
+
const authService = scope.has("authService") ? scope.make("authService") : null;
|
|
14
|
+
return createAccountSecurityService({
|
|
15
|
+
userSettingsRepository: scope.make("userSettingsRepository"),
|
|
16
|
+
userProfilesRepository: scope.make("userProfilesRepository"),
|
|
17
|
+
authService
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
app.actions(
|
|
22
|
+
withActionDefaults(accountSecurityActions, {
|
|
23
|
+
domain: "settings",
|
|
24
|
+
dependencies: {
|
|
25
|
+
accountSecurityService: USERS_ACCOUNT_SECURITY_SERVICE_TOKEN
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export { registerAccountSecurity };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Server Common
|
|
2
|
+
|
|
3
|
+
This directory contains server-only runtime pieces reused by multiple slices in `users-core`.
|
|
4
|
+
|
|
5
|
+
Use these folders:
|
|
6
|
+
- `repositories/`: shared repositories and repository-only helpers.
|
|
7
|
+
- `services/`: shared domain services consumed by multiple slices.
|
|
8
|
+
- `contributors/`: shared action-context/bootstrap contributors.
|
|
9
|
+
- `validators/`: shared request/response validators used by multiple adapters.
|
|
10
|
+
- `formatters/`: shared payload formatters/projections for transport output.
|
|
11
|
+
- `routes/`: shared route schema maps used by more than one route adapter.
|
|
12
|
+
|
|
13
|
+
Keep these files here:
|
|
14
|
+
- `diTokens.js`: shared DI tokens used across slices.
|
|
15
|
+
- `registerCommonRepositories.js`: shared repository bindings.
|
|
16
|
+
- `registerSharedApi.js`: shared API metadata registration.
|
|
17
|
+
|
|
18
|
+
Do not put these in `common/`:
|
|
19
|
+
- feature-only actions/services/repositories/controllers
|
|
20
|
+
- one-off route payload shapes used by a single feature
|
|
21
|
+
- UI/client code
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# `contributors/`
|
|
2
|
+
|
|
3
|
+
Put shared runtime contributors here (for action context or bootstrap payload composition).
|
|
4
|
+
|
|
5
|
+
Allowed:
|
|
6
|
+
- contributors reused by multiple slices/actions
|
|
7
|
+
- contributor logic that delegates domain work to services
|
|
8
|
+
|
|
9
|
+
Not allowed:
|
|
10
|
+
- feature-specific contributors used by one slice only
|
|
11
|
+
- direct repository access when a service already owns that domain
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import {
|
|
2
|
+
normalizeObject,
|
|
3
|
+
requireServiceMethod
|
|
4
|
+
} from "@jskit-ai/kernel/shared/actions/actionContributorHelpers";
|
|
5
|
+
import {
|
|
6
|
+
normalizeScopedRouteVisibility,
|
|
7
|
+
USERS_ROUTE_VISIBILITY_PUBLIC,
|
|
8
|
+
USERS_ROUTE_VISIBILITY_WORKSPACE,
|
|
9
|
+
USERS_ROUTE_VISIBILITY_WORKSPACE_USER
|
|
10
|
+
} from "../../../shared/support/usersVisibility.js";
|
|
11
|
+
import { resolveActionUser } from "../support/resolveActionUser.js";
|
|
12
|
+
|
|
13
|
+
const WORKSPACE_CONTEXT_ACTION_IDS = Object.freeze([
|
|
14
|
+
"workspace.roles.list",
|
|
15
|
+
"workspace.settings.read",
|
|
16
|
+
"workspace.settings.update",
|
|
17
|
+
"workspace.members.list",
|
|
18
|
+
"workspace.member.role.update",
|
|
19
|
+
"workspace.member.remove",
|
|
20
|
+
"workspace.invites.list",
|
|
21
|
+
"workspace.invite.create",
|
|
22
|
+
"workspace.invite.revoke"
|
|
23
|
+
]);
|
|
24
|
+
const WORKSPACE_VISIBILITY_ACTION_CONTEXT_SET = new Set([
|
|
25
|
+
USERS_ROUTE_VISIBILITY_WORKSPACE,
|
|
26
|
+
USERS_ROUTE_VISIBILITY_WORKSPACE_USER
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
function createWorkspaceActionContextContributor({ workspaceService } = {}) {
|
|
30
|
+
const contributorId = "users.workspace.context";
|
|
31
|
+
|
|
32
|
+
requireServiceMethod(workspaceService, "resolveWorkspaceContextForUserBySlug", contributorId);
|
|
33
|
+
|
|
34
|
+
return Object.freeze({
|
|
35
|
+
contributorId,
|
|
36
|
+
async contribute({ actionId, input, context, request } = {}) {
|
|
37
|
+
const payload = normalizeObject(input);
|
|
38
|
+
if (!Object.hasOwn(payload, "workspaceSlug")) {
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const actionName = String(actionId || "").trim();
|
|
43
|
+
const hasLegacyWorkspaceActionId = WORKSPACE_CONTEXT_ACTION_IDS.includes(actionName);
|
|
44
|
+
const routeVisibility = normalizeScopedRouteVisibility(request?.routeOptions?.config?.visibility, {
|
|
45
|
+
fallback: USERS_ROUTE_VISIBILITY_PUBLIC
|
|
46
|
+
});
|
|
47
|
+
const hasWorkspaceRouteVisibility = WORKSPACE_VISIBILITY_ACTION_CONTEXT_SET.has(routeVisibility);
|
|
48
|
+
if (!hasLegacyWorkspaceActionId && !hasWorkspaceRouteVisibility) {
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const resolvedWorkspaceContext = await workspaceService.resolveWorkspaceContextForUserBySlug(
|
|
53
|
+
resolveActionUser(context, payload),
|
|
54
|
+
payload.workspaceSlug,
|
|
55
|
+
{ request }
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const contribution = {
|
|
59
|
+
requestMeta: {
|
|
60
|
+
resolvedWorkspaceContext
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
if (!context?.workspace) {
|
|
65
|
+
contribution.workspace = resolvedWorkspaceContext.workspace;
|
|
66
|
+
}
|
|
67
|
+
if (!context?.membership) {
|
|
68
|
+
contribution.membership = resolvedWorkspaceContext.membership;
|
|
69
|
+
}
|
|
70
|
+
if (!Array.isArray(context?.permissions) || context.permissions.length < 1) {
|
|
71
|
+
contribution.permissions = resolvedWorkspaceContext.permissions;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return contribution;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export { createWorkspaceActionContextContributor };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
+
|
|
3
|
+
function createWorkspaceAuthPolicyContextResolver({ workspaceService } = {}) {
|
|
4
|
+
if (!workspaceService || typeof workspaceService.resolveWorkspaceContextForUserBySlug !== "function") {
|
|
5
|
+
throw new Error(
|
|
6
|
+
"workspace auth policy context resolver requires workspaceService.resolveWorkspaceContextForUserBySlug()."
|
|
7
|
+
);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return async function resolveWorkspaceAuthPolicyContext({ request, actor, meta } = {}) {
|
|
11
|
+
const contextPolicy = normalizeText(meta?.contextPolicy || "none").toLowerCase() || "none";
|
|
12
|
+
const permission = normalizeText(meta?.permission);
|
|
13
|
+
if (contextPolicy === "none" && !permission) {
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const workspaceSlug = normalizeText(request?.params?.workspaceSlug).toLowerCase();
|
|
18
|
+
if (!workspaceSlug || !actor) {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const resolvedWorkspaceContext = await workspaceService.resolveWorkspaceContextForUserBySlug(actor, workspaceSlug, {
|
|
23
|
+
request
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
workspace: resolvedWorkspaceContext?.workspace || null,
|
|
28
|
+
membership: resolvedWorkspaceContext?.membership || null,
|
|
29
|
+
permissions: Array.isArray(resolvedWorkspaceContext?.permissions) ? resolvedWorkspaceContext.permissions : []
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export { createWorkspaceAuthPolicyContextResolver };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { normalizeOpaqueId, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
+
import { parsePositiveInteger } from "@jskit-ai/kernel/server/runtime";
|
|
3
|
+
|
|
4
|
+
function buildVisibilityContribution({ visibility, scopeOwnerId = 0, userOwnerId = null } = {}) {
|
|
5
|
+
const requiresActorScope = visibility === "workspace_user";
|
|
6
|
+
const contribution = {
|
|
7
|
+
scopeKind: requiresActorScope ? "workspace_user" : "workspace",
|
|
8
|
+
requiresActorScope
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
if (scopeOwnerId > 0) {
|
|
12
|
+
contribution.scopeOwnerId = scopeOwnerId;
|
|
13
|
+
}
|
|
14
|
+
if (requiresActorScope && userOwnerId != null) {
|
|
15
|
+
contribution.userOwnerId = userOwnerId;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return contribution;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function createWorkspaceRouteVisibilityResolver({ workspaceService } = {}) {
|
|
22
|
+
if (!workspaceService || typeof workspaceService.resolveWorkspaceContextForUserBySlug !== "function") {
|
|
23
|
+
throw new Error("workspace route visibility resolver requires workspaceService.resolveWorkspaceContextForUserBySlug().");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return Object.freeze({
|
|
27
|
+
resolverId: "users.workspace.visibility",
|
|
28
|
+
async resolve({ visibility, context, request, input } = {}) {
|
|
29
|
+
if (visibility !== "workspace" && visibility !== "workspace_user") {
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const actor = context?.actor || request?.user || null;
|
|
34
|
+
const userOwnerId = normalizeOpaqueId(actor?.id);
|
|
35
|
+
const workspace =
|
|
36
|
+
context?.workspace || context?.requestMeta?.resolvedWorkspaceContext?.workspace || request?.workspace || null;
|
|
37
|
+
const scopeOwnerId = parsePositiveInteger(workspace?.id);
|
|
38
|
+
if (!scopeOwnerId) {
|
|
39
|
+
const workspaceSlug = normalizeText(input?.workspaceSlug).toLowerCase();
|
|
40
|
+
|
|
41
|
+
if (!workspaceSlug || !actor) {
|
|
42
|
+
return visibility === "workspace_user"
|
|
43
|
+
? buildVisibilityContribution({
|
|
44
|
+
visibility,
|
|
45
|
+
userOwnerId
|
|
46
|
+
})
|
|
47
|
+
: {};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const resolvedWorkspaceContext = await workspaceService.resolveWorkspaceContextForUserBySlug(actor, workspaceSlug, {
|
|
51
|
+
request
|
|
52
|
+
});
|
|
53
|
+
const resolvedWorkspaceOwnerId = parsePositiveInteger(resolvedWorkspaceContext?.workspace?.id);
|
|
54
|
+
if (!resolvedWorkspaceOwnerId) {
|
|
55
|
+
return visibility === "workspace_user"
|
|
56
|
+
? buildVisibilityContribution({
|
|
57
|
+
visibility,
|
|
58
|
+
userOwnerId
|
|
59
|
+
})
|
|
60
|
+
: {};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return buildVisibilityContribution({
|
|
64
|
+
visibility,
|
|
65
|
+
scopeOwnerId: resolvedWorkspaceOwnerId,
|
|
66
|
+
userOwnerId
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return buildVisibilityContribution({
|
|
71
|
+
visibility,
|
|
72
|
+
scopeOwnerId,
|
|
73
|
+
userOwnerId
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export { createWorkspaceRouteVisibilityResolver };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const USERS_WORKSPACE_PENDING_INVITATIONS_SERVICE_TOKEN = "users.workspace.pending-invitations.service";
|
|
2
|
+
const USERS_WORKSPACE_ENABLED_TOKEN = "users.workspace.enabled";
|
|
3
|
+
const USERS_WORKSPACE_TENANCY_ENABLED_TOKEN = "users.workspace.tenancy.enabled";
|
|
4
|
+
const USERS_WORKSPACE_INVITATIONS_ENABLED_TOKEN = "users.workspace.invitations.enabled";
|
|
5
|
+
const USERS_WORKSPACE_SELF_CREATE_ENABLED_TOKEN = "users.workspace.self-create.enabled";
|
|
6
|
+
const USERS_TENANCY_PROFILE_TOKEN = "users.tenancy.profile";
|
|
7
|
+
const USERS_AVATAR_STORAGE_SERVICE_TOKEN = "users.avatar.storage.service";
|
|
8
|
+
const USERS_AVATAR_SERVICE_TOKEN = "users.avatar.service";
|
|
9
|
+
const USERS_PROFILE_SYNC_SERVICE_TOKEN = "users.profile.sync.service";
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
USERS_WORKSPACE_PENDING_INVITATIONS_SERVICE_TOKEN,
|
|
13
|
+
USERS_WORKSPACE_ENABLED_TOKEN,
|
|
14
|
+
USERS_WORKSPACE_TENANCY_ENABLED_TOKEN,
|
|
15
|
+
USERS_WORKSPACE_INVITATIONS_ENABLED_TOKEN,
|
|
16
|
+
USERS_WORKSPACE_SELF_CREATE_ENABLED_TOKEN,
|
|
17
|
+
USERS_TENANCY_PROFILE_TOKEN,
|
|
18
|
+
USERS_AVATAR_STORAGE_SERVICE_TOKEN,
|
|
19
|
+
USERS_AVATAR_SERVICE_TOKEN,
|
|
20
|
+
USERS_PROFILE_SYNC_SERVICE_TOKEN
|
|
21
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { normalizeLowerText } from "@jskit-ai/kernel/shared/actions/textNormalization";
|
|
3
|
+
import { DEFAULT_USER_SETTINGS } from "../../../shared/settings.js";
|
|
4
|
+
|
|
5
|
+
const ACCOUNT_AVATAR_FILE_PATH = "/api/settings/profile/avatar";
|
|
6
|
+
|
|
7
|
+
function createGravatarUrl(email, size = 64) {
|
|
8
|
+
const normalizedEmail = normalizeLowerText(email);
|
|
9
|
+
const hash = createHash("sha256").update(normalizedEmail).digest("hex");
|
|
10
|
+
return `https://www.gravatar.com/avatar/${hash}?d=mp&s=${Number(size) || 64}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function createUploadedAvatarUrl(profile = {}) {
|
|
14
|
+
const storageKey = String(profile?.avatarStorageKey || "").trim();
|
|
15
|
+
if (!storageKey) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const avatarVersion = String(profile?.avatarVersion || "").trim();
|
|
20
|
+
if (!avatarVersion) {
|
|
21
|
+
return ACCOUNT_AVATAR_FILE_PATH;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return `${ACCOUNT_AVATAR_FILE_PATH}?v=${encodeURIComponent(avatarVersion)}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function accountAvatarFormatter(profile, settings) {
|
|
28
|
+
const size = Number(settings?.avatarSize || DEFAULT_USER_SETTINGS.avatarSize);
|
|
29
|
+
const uploadedUrl = createUploadedAvatarUrl(profile);
|
|
30
|
+
const gravatarUrl = createGravatarUrl(profile?.email, size);
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
uploadedUrl,
|
|
34
|
+
gravatarUrl,
|
|
35
|
+
effectiveUrl: uploadedUrl || gravatarUrl,
|
|
36
|
+
hasUploadedAvatar: Boolean(uploadedUrl),
|
|
37
|
+
size,
|
|
38
|
+
version: profile?.avatarVersion || null
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export { accountAvatarFormatter };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { normalizeText } from "@jskit-ai/kernel/shared/actions/textNormalization";
|
|
2
|
+
import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
|
|
3
|
+
|
|
4
|
+
function normalizeMfa(source) {
|
|
5
|
+
const mfaSource = isRecord(source?.mfa) ? source.mfa : {};
|
|
6
|
+
const methods = [];
|
|
7
|
+
for (const entry of Array.isArray(mfaSource.methods) ? mfaSource.methods : []) {
|
|
8
|
+
const normalized = normalizeText(entry);
|
|
9
|
+
if (!normalized) {
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
methods.push(normalized);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
status: normalizeText(mfaSource.status) || "not_enabled",
|
|
17
|
+
enrolled: Boolean(mfaSource.enrolled),
|
|
18
|
+
methods
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizeAuthMethods(sourceMethods) {
|
|
23
|
+
const methods = [];
|
|
24
|
+
let enabledMethodsCount = 0;
|
|
25
|
+
|
|
26
|
+
for (const method of Array.isArray(sourceMethods) ? sourceMethods : []) {
|
|
27
|
+
const normalizedMethod = {
|
|
28
|
+
id: normalizeText(method?.id),
|
|
29
|
+
kind: normalizeText(method?.kind),
|
|
30
|
+
provider: method?.provider == null ? null : normalizeText(method.provider),
|
|
31
|
+
label: normalizeText(method?.label || method?.id),
|
|
32
|
+
configured: method?.configured === true,
|
|
33
|
+
enabled: method?.enabled === true,
|
|
34
|
+
canEnable: method?.canEnable === true,
|
|
35
|
+
canDisable: method?.canDisable === true,
|
|
36
|
+
supportsSecretUpdate: method?.supportsSecretUpdate === true,
|
|
37
|
+
requiresCurrentPassword: method?.requiresCurrentPassword === true
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
if (normalizedMethod.enabled) {
|
|
41
|
+
enabledMethodsCount += 1;
|
|
42
|
+
}
|
|
43
|
+
methods.push(normalizedMethod);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
methods,
|
|
48
|
+
enabledMethodsCount
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function accountSecurityStatusFormatter(securityStatus = {}) {
|
|
53
|
+
const source = isRecord(securityStatus) ? securityStatus : {};
|
|
54
|
+
const authPolicy = isRecord(source.authPolicy) ? source.authPolicy : {};
|
|
55
|
+
const { methods: authMethods, enabledMethodsCount } = normalizeAuthMethods(source.authMethods);
|
|
56
|
+
const minimumEnabledMethods = Number(authPolicy.minimumEnabledMethods);
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
mfa: normalizeMfa(source),
|
|
60
|
+
sessions: {
|
|
61
|
+
canSignOutOtherDevices: true
|
|
62
|
+
},
|
|
63
|
+
authPolicy: {
|
|
64
|
+
minimumEnabledMethods: minimumEnabledMethods > 0 ? minimumEnabledMethods : 1,
|
|
65
|
+
enabledMethodsCount
|
|
66
|
+
},
|
|
67
|
+
authMethods
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export { accountSecurityStatusFormatter };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/actions/textNormalization";
|
|
2
|
+
import {
|
|
3
|
+
USER_SETTINGS_SECTIONS,
|
|
4
|
+
userSettingsFields
|
|
5
|
+
} from "../../../shared/resources/userSettingsFields.js";
|
|
6
|
+
import { accountAvatarFormatter } from "./accountAvatarFormatter.js";
|
|
7
|
+
import { accountSecurityStatusFormatter } from "./accountSecurityStatusFormatter.js";
|
|
8
|
+
|
|
9
|
+
function resolveAuthProfileSettings(authService) {
|
|
10
|
+
if (!authService || typeof authService.getSettingsProfileAuthInfo !== "function") {
|
|
11
|
+
return {
|
|
12
|
+
emailManagedBy: "auth",
|
|
13
|
+
emailChangeFlow: "auth"
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const authProfileSettings = authService.getSettingsProfileAuthInfo();
|
|
18
|
+
return {
|
|
19
|
+
emailManagedBy: normalizeLowerText(authProfileSettings?.emailManagedBy) || "auth",
|
|
20
|
+
emailChangeFlow: normalizeLowerText(authProfileSettings?.emailChangeFlow) || "auth"
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function formatUserSettingsSection(section, settings = {}) {
|
|
25
|
+
const source = settings && typeof settings === "object" ? settings : {};
|
|
26
|
+
const formatted = {};
|
|
27
|
+
|
|
28
|
+
for (const field of userSettingsFields) {
|
|
29
|
+
if (field.section !== section) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const rawValue = Object.hasOwn(source, field.key)
|
|
33
|
+
? source[field.key]
|
|
34
|
+
: field.resolveDefault({
|
|
35
|
+
settings: source
|
|
36
|
+
});
|
|
37
|
+
formatted[field.key] = field.normalizeOutput(rawValue, {
|
|
38
|
+
settings: source
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return formatted;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function accountSettingsResponseFormatter({ profile, settings, securityStatus, authService }) {
|
|
46
|
+
const authProfileSettings = resolveAuthProfileSettings(authService);
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
profile: {
|
|
50
|
+
displayName: normalizeText(profile?.displayName),
|
|
51
|
+
email: normalizeLowerText(profile?.email),
|
|
52
|
+
emailManagedBy: authProfileSettings.emailManagedBy,
|
|
53
|
+
emailChangeFlow: authProfileSettings.emailChangeFlow,
|
|
54
|
+
avatar: accountAvatarFormatter(profile, settings)
|
|
55
|
+
},
|
|
56
|
+
security: accountSecurityStatusFormatter(securityStatus),
|
|
57
|
+
preferences: formatUserSettingsSection(USER_SETTINGS_SECTIONS.PREFERENCES, settings),
|
|
58
|
+
notifications: formatUserSettingsSection(USER_SETTINGS_SECTIONS.NOTIFICATIONS, settings)
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export { accountSettingsResponseFormatter };
|