@plone-collective/volto-identity 1.0.0-alpha.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/.changelog.draft +261 -0
- package/.release-it.json +34 -0
- package/CHANGELOG.md +269 -0
- package/LICENSE +21 -0
- package/README.md +228 -0
- package/babel.config.js +17 -0
- package/locales/de/LC_MESSAGES/volto.po +1162 -0
- package/locales/en/LC_MESSAGES/volto.po +1162 -0
- package/locales/es/LC_MESSAGES/volto.po +1169 -0
- package/locales/pt_BR/LC_MESSAGES/volto.po +1167 -0
- package/locales/volto.pot +1164 -0
- package/news/.changelog_template.jinja +15 -0
- package/news/.gitkeep +0 -0
- package/package.json +66 -0
- package/public/.gitkeep +0 -0
- package/src/actions/actions.test.ts +226 -0
- package/src/actions/index.ts +530 -0
- package/src/components/.gitkeep +0 -0
- package/src/components/Applications/Applications.tsx +169 -0
- package/src/components/Applications/ApplicationsPanel.scss +59 -0
- package/src/components/Applications/ApplicationsPanel.stories.tsx +82 -0
- package/src/components/Applications/ApplicationsPanel.test.tsx +235 -0
- package/src/components/Applications/ApplicationsPanel.tsx +342 -0
- package/src/components/Callback/Callback.scss +17 -0
- package/src/components/Callback/Callback.stories.tsx +43 -0
- package/src/components/Callback/Callback.test.tsx +262 -0
- package/src/components/Callback/Callback.tsx +170 -0
- package/src/components/Consent/Consent.test.tsx +75 -0
- package/src/components/Consent/Consent.tsx +98 -0
- package/src/components/Consent/ConsentPanel.scss +37 -0
- package/src/components/Consent/ConsentPanel.stories.tsx +80 -0
- package/src/components/Consent/ConsentPanel.test.tsx +138 -0
- package/src/components/Consent/ConsentPanel.tsx +162 -0
- package/src/components/ControlPanel/ClientsControlPanel.stories.tsx +38 -0
- package/src/components/ControlPanel/ClientsControlPanel.tsx +321 -0
- package/src/components/ControlPanel/ClientsPanel.scss +21 -0
- package/src/components/ControlPanel/ClientsPanel.stories.tsx +71 -0
- package/src/components/ControlPanel/ClientsPanel.test.tsx +267 -0
- package/src/components/ControlPanel/ClientsPanel.tsx +386 -0
- package/src/components/ControlPanel/ConfirmModal.stories.tsx +35 -0
- package/src/components/ControlPanel/ConfirmModal.test.tsx +66 -0
- package/src/components/ControlPanel/ConfirmModal.tsx +68 -0
- package/src/components/ControlPanel/ProviderForm.stories.tsx +107 -0
- package/src/components/ControlPanel/ProvidersControlPanel.scss +26 -0
- package/src/components/ControlPanel/ProvidersControlPanel.stories.tsx +88 -0
- package/src/components/ControlPanel/ProvidersControlPanel.tsx +600 -0
- package/src/components/ControlPanel/RenderUsers.stories.tsx +112 -0
- package/src/components/ControlPanel/RenderUsers.test.tsx +145 -0
- package/src/components/ControlPanel/RenderUsers.tsx +263 -0
- package/src/components/ControlPanel/SecretReveal.scss +32 -0
- package/src/components/ControlPanel/SecretReveal.stories.tsx +33 -0
- package/src/components/ControlPanel/SecretReveal.test.tsx +101 -0
- package/src/components/ControlPanel/SecretReveal.tsx +105 -0
- package/src/components/ControlPanel/UserAccount.stories.tsx +69 -0
- package/src/components/ControlPanel/UserAccount.test.tsx +117 -0
- package/src/components/ControlPanel/UserAccount.tsx +125 -0
- package/src/components/ControlPanel/UserAccountPanel.scss +38 -0
- package/src/components/ControlPanel/UserAccountPanel.stories.tsx +77 -0
- package/src/components/ControlPanel/UserAccountPanel.test.tsx +173 -0
- package/src/components/ControlPanel/UserAccountPanel.tsx +273 -0
- package/src/components/FirstLogin/FirstLogin.scss +12 -0
- package/src/components/FirstLogin/FirstLogin.stories.tsx +50 -0
- package/src/components/FirstLogin/FirstLogin.tsx +94 -0
- package/src/components/Identities/Identities.stories.tsx +51 -0
- package/src/components/Identities/Identities.test.tsx +134 -0
- package/src/components/Identities/Identities.tsx +217 -0
- package/src/components/Identities/IdentitiesList.scss +49 -0
- package/src/components/Identities/IdentitiesList.stories.tsx +71 -0
- package/src/components/Identities/IdentitiesList.test.tsx +218 -0
- package/src/components/Identities/IdentitiesList.tsx +207 -0
- package/src/components/Identities/ProfileEmails.scss +62 -0
- package/src/components/Identities/ProfileEmails.stories.tsx +75 -0
- package/src/components/Identities/ProfileEmails.test.tsx +132 -0
- package/src/components/Identities/ProfileEmails.tsx +217 -0
- package/src/components/Login/Login.stories.tsx +63 -0
- package/src/components/Login/Login.test.tsx +189 -0
- package/src/components/Login/Login.tsx +163 -0
- package/src/components/Login/LoginForm.scss +39 -0
- package/src/components/Login/LoginForm.stories.tsx +112 -0
- package/src/components/Login/LoginForm.test.tsx +483 -0
- package/src/components/Login/LoginForm.tsx +321 -0
- package/src/components/Login/LoginOverlay.scss +44 -0
- package/src/components/Login/LoginOverlay.stories.tsx +73 -0
- package/src/components/Login/LoginOverlay.test.tsx +98 -0
- package/src/components/Login/LoginOverlay.tsx +107 -0
- package/src/components/Login/LoginPanel.scss +50 -0
- package/src/components/Login/LoginPanel.stories.tsx +40 -0
- package/src/components/Login/LoginPanel.tsx +60 -0
- package/src/components/Login/MagicLinkForm.scss +25 -0
- package/src/components/Login/MagicLinkForm.stories.tsx +27 -0
- package/src/components/Login/MagicLinkForm.tsx +133 -0
- package/src/components/Login/PasswordForm.scss +82 -0
- package/src/components/Login/PasswordForm.stories.tsx +26 -0
- package/src/components/Login/PasswordForm.tsx +159 -0
- package/src/components/Login/ProviderButton.scss +103 -0
- package/src/components/Login/ProviderButton.stories.tsx +74 -0
- package/src/components/Login/ProviderButton.test.tsx +99 -0
- package/src/components/Login/ProviderButton.tsx +97 -0
- package/src/components/ProfileGate/ProfileGate.stories.tsx +118 -0
- package/src/components/ProfileGate/ProfileGate.test.tsx +370 -0
- package/src/components/ProfileGate/ProfileGate.tsx +172 -0
- package/src/components/Toolbar/PersonalTools.stories.tsx +111 -0
- package/src/components/Toolbar/PersonalTools.test.tsx +165 -0
- package/src/components/Toolbar/PersonalTools.tsx +177 -0
- package/src/components/Toolbar/Toolbar.jsx +766 -0
- package/src/components/UserMenu/ApplicationsMenuItem.stories.tsx +62 -0
- package/src/components/UserMenu/ApplicationsMenuItem.test.tsx +109 -0
- package/src/components/UserMenu/ApplicationsMenuItem.tsx +82 -0
- package/src/components/UserMenu/IdentitiesMenuItem.stories.tsx +35 -0
- package/src/components/UserMenu/IdentitiesMenuItem.test.tsx +103 -0
- package/src/components/UserMenu/IdentitiesMenuItem.tsx +34 -0
- package/src/components/UserMenu/MenuItem.stories.tsx +32 -0
- package/src/components/UserMenu/MenuItem.tsx +48 -0
- package/src/components/UserMenu/ProfileMenuItem.stories.tsx +76 -0
- package/src/components/UserMenu/ProfileMenuItem.test.tsx +90 -0
- package/src/components/UserMenu/ProfileMenuItem.tsx +60 -0
- package/src/components/UserMenu/UserAvatar.scss +23 -0
- package/src/components/UserMenu/UserAvatar.stories.tsx +121 -0
- package/src/components/UserMenu/UserAvatar.test.tsx +120 -0
- package/src/components/UserMenu/UserAvatar.tsx +119 -0
- package/src/components/UserMenu/UserMenuPlugs.stories.tsx +101 -0
- package/src/components/UserMenu/UserMenuPlugs.test.tsx +202 -0
- package/src/components/UserMenu/UserMenuPlugs.tsx +142 -0
- package/src/components/UserMenu/UserProfileLoader.test.tsx +211 -0
- package/src/components/UserMenu/UserProfileLoader.tsx +54 -0
- package/src/components/Views/GroupView.scss +36 -0
- package/src/components/Views/GroupView.stories.tsx +98 -0
- package/src/components/Views/GroupView.test.tsx +137 -0
- package/src/components/Views/GroupView.tsx +178 -0
- package/src/components/Views/ProfileView.scss +27 -0
- package/src/components/Views/ProfileView.stories.tsx +34 -0
- package/src/components/Views/ProfileView.test.tsx +82 -0
- package/src/components/Views/ProfileView.tsx +100 -0
- package/src/components/Widgets/ProviderIconWidget.scss +21 -0
- package/src/components/Widgets/ProviderIconWidget.stories.tsx +30 -0
- package/src/components/Widgets/ProviderIconWidget.test.tsx +55 -0
- package/src/components/Widgets/ProviderIconWidget.tsx +143 -0
- package/src/config/menu.ts +35 -0
- package/src/config/reducers.ts +7 -0
- package/src/config/routes.test.ts +177 -0
- package/src/config/routes.ts +143 -0
- package/src/config/settings.test.ts +34 -0
- package/src/config/settings.ts +72 -0
- package/src/config/views.ts +30 -0
- package/src/config/widgets.ts +25 -0
- package/src/constants/ActionTypes.ts +42 -0
- package/src/constants/vocabularies.ts +20 -0
- package/src/customizations/volto/components/manage/Controlpanels/Users/RenderUsers.tsx +23 -0
- package/src/customizations/volto/components/manage/Toolbar/PersonalTools.tsx +21 -0
- package/src/customizations/volto/components/manage/Toolbar/Toolbar.jsx +21 -0
- package/src/helpers/avatar.test.ts +79 -0
- package/src/helpers/avatar.ts +85 -0
- package/src/helpers/callback.test.ts +60 -0
- package/src/helpers/callback.ts +71 -0
- package/src/helpers/clientSchema.test.ts +182 -0
- package/src/helpers/clientSchema.ts +181 -0
- package/src/helpers/firstLogin.test.ts +90 -0
- package/src/helpers/firstLogin.ts +76 -0
- package/src/helpers/groupmap.test.ts +81 -0
- package/src/helpers/groupmap.ts +50 -0
- package/src/helpers/identities.ts +22 -0
- package/src/helpers/navigate.test.ts +132 -0
- package/src/helpers/navigate.ts +126 -0
- package/src/helpers/profileGate.test.ts +150 -0
- package/src/helpers/profileGate.ts +191 -0
- package/src/helpers/profileSource.ts +41 -0
- package/src/helpers/propertymap.test.ts +71 -0
- package/src/helpers/propertymap.ts +43 -0
- package/src/helpers/providerSchema.test.ts +410 -0
- package/src/helpers/providerSchema.ts +362 -0
- package/src/helpers/returnUrl.test.ts +42 -0
- package/src/helpers/returnUrl.ts +41 -0
- package/src/helpers/rowmap.ts +110 -0
- package/src/helpers/showPloneLogin.test.ts +78 -0
- package/src/helpers/showPloneLogin.ts +55 -0
- package/src/helpers/token.test.ts +62 -0
- package/src/helpers/token.ts +71 -0
- package/src/index.ts +22 -0
- package/src/reducers/index.ts +369 -0
- package/src/reducers/reducers.test.ts +369 -0
- package/src/stories/fixtures.tsx +640 -0
- package/src/storybook/withPage.test.tsx +90 -0
- package/src/storybook/withPage.tsx +110 -0
- package/src/storybook/withUserMenu.test.tsx +78 -0
- package/src/storybook/withUserMenu.tsx +136 -0
- package/src/styles.css +262 -0
- package/src/testing.tsx +86 -0
- package/src/types.ts +441 -0
- package/towncrier.toml +38 -0
- package/tsconfig.json +22 -0
- package/vitest.config.mjs +47 -0
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared fixtures and decorators for the stories.
|
|
3
|
+
*
|
|
4
|
+
* Two things every story in this package needs. The data is the shape the
|
|
5
|
+
* backend actually returns, copied from `types` rather than invented, so a
|
|
6
|
+
* story that renders is evidence the component can render what it will really
|
|
7
|
+
* be given. And the connected components need a store: Storybook's preview
|
|
8
|
+
* wraps stories in an `IntlProvider` and a router, but not a redux
|
|
9
|
+
* `Provider`, and half of this add-on reads `useSelector`.
|
|
10
|
+
*
|
|
11
|
+
* The store is inert on purpose. A story is a picture of one state, so
|
|
12
|
+
* dispatching does nothing and the state never changes -- which is also what
|
|
13
|
+
* makes a loading state or an error state something you can simply ask for.
|
|
14
|
+
* @module stories/fixtures
|
|
15
|
+
*/
|
|
16
|
+
import React from 'react';
|
|
17
|
+
import type { ReactNode } from 'react';
|
|
18
|
+
import { Provider } from 'react-redux';
|
|
19
|
+
|
|
20
|
+
import LoginPanel from '../components/Login/LoginPanel';
|
|
21
|
+
|
|
22
|
+
import type {
|
|
23
|
+
ConfiguredProvider,
|
|
24
|
+
ConsentRequest,
|
|
25
|
+
Driver,
|
|
26
|
+
Identity,
|
|
27
|
+
LoginProvider,
|
|
28
|
+
OAuthClient,
|
|
29
|
+
OAuthGrants,
|
|
30
|
+
SigningKeyRing,
|
|
31
|
+
UserAccount,
|
|
32
|
+
UserProfile,
|
|
33
|
+
ProfileEmail,
|
|
34
|
+
} from '../types';
|
|
35
|
+
|
|
36
|
+
/** A request that has finished with data. */
|
|
37
|
+
export const LOADED = { loading: false, loaded: true, error: null };
|
|
38
|
+
|
|
39
|
+
/** A request still in flight. */
|
|
40
|
+
export const LOADING = { loading: true, loaded: false, error: null };
|
|
41
|
+
|
|
42
|
+
/** A request that was refused. */
|
|
43
|
+
export const FAILED = { loading: false, loaded: false, error: { status: 401 } };
|
|
44
|
+
|
|
45
|
+
export const GOOGLE: LoginProvider = {
|
|
46
|
+
'@id': '/@login-providers/google',
|
|
47
|
+
id: 'google',
|
|
48
|
+
title: 'Google',
|
|
49
|
+
driver: 'google',
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const GITHUB: LoginProvider = {
|
|
53
|
+
'@id': '/@login-providers/github',
|
|
54
|
+
id: 'github',
|
|
55
|
+
title: 'GitHub',
|
|
56
|
+
driver: 'github',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const KEYCLOAK: LoginProvider = {
|
|
60
|
+
'@id': '/@login-providers/keycloak',
|
|
61
|
+
id: 'keycloak',
|
|
62
|
+
title: 'Sign in with Keycloak',
|
|
63
|
+
driver: 'oidc-generic',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const EMAIL: LoginProvider = {
|
|
67
|
+
'@id': '/@login-providers/email',
|
|
68
|
+
id: 'email',
|
|
69
|
+
title: 'Email',
|
|
70
|
+
driver: 'email',
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const PROVIDERS = [GOOGLE, GITHUB, KEYCLOAK];
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* A provider carrying the look an operator gave it.
|
|
77
|
+
*
|
|
78
|
+
* The icon is a real SVG rather than a placeholder, because the point of the
|
|
79
|
+
* story is that the button is drawn from it: a stand-in string would render
|
|
80
|
+
* an empty box and prove nothing.
|
|
81
|
+
*/
|
|
82
|
+
export const STYLED: LoginProvider = {
|
|
83
|
+
'@id': '/@login-providers/acme',
|
|
84
|
+
id: 'acme',
|
|
85
|
+
title: 'Acme SSO',
|
|
86
|
+
driver: 'oidc-generic',
|
|
87
|
+
icon:
|
|
88
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">' +
|
|
89
|
+
'<path d="M8 1l7 13H1z"/></svg>',
|
|
90
|
+
background_color: '#4b3f72',
|
|
91
|
+
foreground_color: '#ffffff',
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/** A profile's addresses: one proved, one not. */
|
|
95
|
+
export const PROFILE_EMAILS: ProfileEmail[] = [
|
|
96
|
+
{ address: 'erico@plone.org', verified: true, preferred: true },
|
|
97
|
+
{ address: 'erico@example.com', verified: false, preferred: false },
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
export const IDENTITIES: Identity[] = [
|
|
101
|
+
{
|
|
102
|
+
'@id': '/@identities/google:1234',
|
|
103
|
+
provider: 'google',
|
|
104
|
+
subject: '1234567890',
|
|
105
|
+
title: 'Google',
|
|
106
|
+
created: '2026-01-14T09:12:00+00:00',
|
|
107
|
+
last_login: '2026-08-21T18:03:00+00:00',
|
|
108
|
+
can_unlink: true,
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
'@id': '/@identities/github:99',
|
|
112
|
+
provider: 'github',
|
|
113
|
+
subject: '99',
|
|
114
|
+
title: 'GitHub',
|
|
115
|
+
created: '2026-03-02T11:40:00+00:00',
|
|
116
|
+
last_login: null,
|
|
117
|
+
can_unlink: true,
|
|
118
|
+
},
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
/** The last way in: unlinking it would lock the user out. */
|
|
122
|
+
export const ONLY_IDENTITY: Identity[] = [
|
|
123
|
+
{ ...IDENTITIES[0], can_unlink: false },
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* What one entry of `@identity-drivers` sends: `IOIDCSettings`, serialized.
|
|
128
|
+
*
|
|
129
|
+
* An ordinary JSON schema, because that is what the backend produces now --
|
|
130
|
+
* `plone.restapi`'s own `get_fieldsets` / `get_jsonschema_properties` /
|
|
131
|
+
* `get_fieldset_infos`, the same three calls that answer `@controlpanels`.
|
|
132
|
+
* Titles arrive translated and widgets arrive decided, which is why nothing
|
|
133
|
+
* here carries a `secret` flag or an `order`: a secret is a `Password` field
|
|
134
|
+
* and the order is the order the properties are in.
|
|
135
|
+
*/
|
|
136
|
+
export const OIDC_DRIVER: Driver = {
|
|
137
|
+
id: 'oidc-generic',
|
|
138
|
+
title: 'OpenID Connect',
|
|
139
|
+
schema: {
|
|
140
|
+
properties: {
|
|
141
|
+
issuer: {
|
|
142
|
+
type: 'string',
|
|
143
|
+
title: 'Issuer URL',
|
|
144
|
+
description:
|
|
145
|
+
'Discovery is fetched from <issuer>/.well-known/openid-configuration.',
|
|
146
|
+
},
|
|
147
|
+
client_id: {
|
|
148
|
+
type: 'string',
|
|
149
|
+
title: 'Client ID',
|
|
150
|
+
description: 'The identifier this provider issued for this site.',
|
|
151
|
+
},
|
|
152
|
+
client_secret: {
|
|
153
|
+
type: 'string',
|
|
154
|
+
title: 'Client secret',
|
|
155
|
+
description: 'Write-only. It is never sent back by any endpoint here.',
|
|
156
|
+
widget: 'password',
|
|
157
|
+
},
|
|
158
|
+
scope: {
|
|
159
|
+
type: 'array',
|
|
160
|
+
title: 'Scope',
|
|
161
|
+
description: 'One permission per entry.',
|
|
162
|
+
widget: 'token',
|
|
163
|
+
},
|
|
164
|
+
userid_source: {
|
|
165
|
+
type: 'string',
|
|
166
|
+
title: 'Userid taken from',
|
|
167
|
+
choices: [
|
|
168
|
+
['uuid', 'A random id'],
|
|
169
|
+
['username', "The provider's username"],
|
|
170
|
+
['email', 'The email address'],
|
|
171
|
+
['subject', "The provider's subject identifier"],
|
|
172
|
+
],
|
|
173
|
+
},
|
|
174
|
+
group_claim: {
|
|
175
|
+
type: 'string',
|
|
176
|
+
title: 'Groups arrive in the claim',
|
|
177
|
+
description:
|
|
178
|
+
'Which claim carries the group names this provider asserts.',
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
required: ['issuer', 'client_id', 'client_secret'],
|
|
182
|
+
fieldsets: [
|
|
183
|
+
{
|
|
184
|
+
id: 'default',
|
|
185
|
+
title: 'Default',
|
|
186
|
+
fields: [
|
|
187
|
+
'issuer',
|
|
188
|
+
'client_id',
|
|
189
|
+
'client_secret',
|
|
190
|
+
'scope',
|
|
191
|
+
'userid_source',
|
|
192
|
+
'group_claim',
|
|
193
|
+
],
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* A driver whose providers have no groups.
|
|
201
|
+
*
|
|
202
|
+
* The real GitHub driver is this case, and the difference is worth having in
|
|
203
|
+
* the fixtures: the group mapping is offered for a driver that declares a
|
|
204
|
+
* group claim and hidden for one that does not, so a story showing only the
|
|
205
|
+
* first would never show the second. `IGitHubSettings` extends the OAuth2
|
|
206
|
+
* base and adds nothing, so the absence here is the absence there.
|
|
207
|
+
*/
|
|
208
|
+
const NO_GROUPS_SCHEMA: Driver['schema'] = {
|
|
209
|
+
properties: Object.fromEntries(
|
|
210
|
+
Object.entries(OIDC_DRIVER.schema.properties ?? {}).filter(
|
|
211
|
+
([name]) => name !== 'group_claim' && name !== 'issuer',
|
|
212
|
+
),
|
|
213
|
+
),
|
|
214
|
+
required: ['client_id', 'client_secret'],
|
|
215
|
+
fieldsets: [
|
|
216
|
+
{
|
|
217
|
+
id: 'default',
|
|
218
|
+
title: 'Default',
|
|
219
|
+
fields: ['client_id', 'client_secret', 'scope', 'userid_source'],
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
export const DRIVERS: Driver[] = [
|
|
225
|
+
OIDC_DRIVER,
|
|
226
|
+
{ id: 'google', title: 'Google', schema: OIDC_DRIVER.schema },
|
|
227
|
+
{ id: 'github', title: 'GitHub', schema: NO_GROUPS_SCHEMA },
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* What `@identity-providers` sends alongside the listing: `IProviderRecords`.
|
|
232
|
+
*
|
|
233
|
+
* The provider's own half of the form -- everything true of a provider
|
|
234
|
+
* whatever its driver. The panel renders this and the chosen driver's schema
|
|
235
|
+
* together, which is why a story needs both.
|
|
236
|
+
*/
|
|
237
|
+
export const PROVIDER_SCHEMA = {
|
|
238
|
+
type: 'object',
|
|
239
|
+
properties: {
|
|
240
|
+
// Served, and dropped again by `providerSchema`: a provider's storage
|
|
241
|
+
// has a driver and two maps, and none of the three is renderable as
|
|
242
|
+
// described. Kept here because a fixture that quietly leaves them out
|
|
243
|
+
// would stop the stories from showing what the composition does.
|
|
244
|
+
driver: { type: 'string', title: 'Driver' },
|
|
245
|
+
propertymap: { type: 'object', title: 'Property map' },
|
|
246
|
+
groupmap: { type: 'object', title: 'Group map' },
|
|
247
|
+
title: { type: 'string', title: 'Title' },
|
|
248
|
+
enabled: { type: 'boolean', title: 'Enabled' },
|
|
249
|
+
show_in_login: { type: 'boolean', title: 'Show on the login screen' },
|
|
250
|
+
order: { type: 'integer', title: 'Order' },
|
|
251
|
+
icon: { type: 'string', title: 'Icon', widget: 'provider_icon' },
|
|
252
|
+
background_color: {
|
|
253
|
+
type: 'string',
|
|
254
|
+
title: 'Background colour',
|
|
255
|
+
widget: 'color_picker',
|
|
256
|
+
},
|
|
257
|
+
foreground_color: {
|
|
258
|
+
type: 'string',
|
|
259
|
+
title: 'Foreground colour',
|
|
260
|
+
widget: 'color_picker',
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
required: [],
|
|
264
|
+
fieldsets: [
|
|
265
|
+
{
|
|
266
|
+
id: 'default',
|
|
267
|
+
title: 'Default',
|
|
268
|
+
fields: [
|
|
269
|
+
'driver',
|
|
270
|
+
'title',
|
|
271
|
+
'enabled',
|
|
272
|
+
'show_in_login',
|
|
273
|
+
'order',
|
|
274
|
+
'propertymap',
|
|
275
|
+
'groupmap',
|
|
276
|
+
],
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
id: 'style',
|
|
280
|
+
title: 'Style',
|
|
281
|
+
fields: ['icon', 'background_color', 'foreground_color'],
|
|
282
|
+
},
|
|
283
|
+
],
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
export const CONFIGURED: ConfiguredProvider[] = [
|
|
287
|
+
{
|
|
288
|
+
'@id': '/@identity-providers/keycloak',
|
|
289
|
+
id: 'keycloak',
|
|
290
|
+
driver: 'oidc-generic',
|
|
291
|
+
title: 'Sign in with Keycloak',
|
|
292
|
+
enabled: true,
|
|
293
|
+
show_in_login: true,
|
|
294
|
+
config: {
|
|
295
|
+
client_id: 'plone',
|
|
296
|
+
client_secret: '••••••••',
|
|
297
|
+
issuer: 'https://id.example.org/realms/main',
|
|
298
|
+
scope: 'openid email profile',
|
|
299
|
+
},
|
|
300
|
+
propertymap: {
|
|
301
|
+
preferred_username: 'username',
|
|
302
|
+
'address.formatted': 'location',
|
|
303
|
+
},
|
|
304
|
+
groupmap: {
|
|
305
|
+
'plone-editors': 'Site Administrators',
|
|
306
|
+
'plone-staff': 'Reviewers',
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
'@id': '/@identity-providers/github',
|
|
311
|
+
id: 'github',
|
|
312
|
+
driver: 'github',
|
|
313
|
+
title: 'GitHub',
|
|
314
|
+
enabled: false,
|
|
315
|
+
show_in_login: false,
|
|
316
|
+
config: { client_id: 'Iv1.0123456789abcdef', client_secret: '••••••••' },
|
|
317
|
+
propertymap: {},
|
|
318
|
+
groupmap: {},
|
|
319
|
+
},
|
|
320
|
+
];
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* The vocabulary the property-map editor reads, already loaded.
|
|
324
|
+
*
|
|
325
|
+
* Served by the backend from the site's live user schema, so a story shows
|
|
326
|
+
* a representative subset rather than a list this file owns.
|
|
327
|
+
*/
|
|
328
|
+
export const USER_FIELDS_STATE = {
|
|
329
|
+
'pas.plugins.identity.UserFields': {
|
|
330
|
+
loaded: true,
|
|
331
|
+
loading: false,
|
|
332
|
+
items: [
|
|
333
|
+
{ value: 'description', label: 'Biography' },
|
|
334
|
+
{ value: 'email', label: 'Email' },
|
|
335
|
+
{ value: 'fullname', label: 'Full Name' },
|
|
336
|
+
{ value: 'home_page', label: 'Home page' },
|
|
337
|
+
{ value: 'location', label: 'Location' },
|
|
338
|
+
{ value: 'username', label: 'Username' },
|
|
339
|
+
],
|
|
340
|
+
itemsTotal: 6,
|
|
341
|
+
},
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* The vocabulary the group-map editor reads, already loaded.
|
|
346
|
+
*
|
|
347
|
+
* Every group PAS knows on the site, which is why the local half of a group
|
|
348
|
+
* mapping is a picker: a group that does not exist here grants nothing, and
|
|
349
|
+
* that is a typo worth catching in the form rather than in a log line.
|
|
350
|
+
*/
|
|
351
|
+
export const GROUPS_STATE = {
|
|
352
|
+
'pas.plugins.identity.Groups': {
|
|
353
|
+
loaded: true,
|
|
354
|
+
loading: false,
|
|
355
|
+
items: [
|
|
356
|
+
{ value: 'Administrators', label: 'Administrators' },
|
|
357
|
+
{ value: 'Reviewers', label: 'Reviewers' },
|
|
358
|
+
{ value: 'Site Administrators', label: 'Site Administrators' },
|
|
359
|
+
],
|
|
360
|
+
itemsTotal: 3,
|
|
361
|
+
},
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
export const CLIENT: OAuthClient = {
|
|
365
|
+
'@id': '/@identity-clients/intranet',
|
|
366
|
+
client_id: 'intranet',
|
|
367
|
+
title: 'Intranet',
|
|
368
|
+
redirect_uris: ['https://intranet.example.org/login-identity'],
|
|
369
|
+
grant_types: ['authorization_code', 'refresh_token'],
|
|
370
|
+
scope: ['openid', 'email', 'profile'],
|
|
371
|
+
auth_method: 'client_secret_post',
|
|
372
|
+
public: false,
|
|
373
|
+
enabled: true,
|
|
374
|
+
service_user: '',
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
export const CLIENTS: OAuthClient[] = [
|
|
378
|
+
CLIENT,
|
|
379
|
+
{
|
|
380
|
+
...CLIENT,
|
|
381
|
+
'@id': '/@identity-clients/kiosk',
|
|
382
|
+
client_id: 'kiosk',
|
|
383
|
+
title: 'Lobby kiosk',
|
|
384
|
+
auth_method: 'none',
|
|
385
|
+
public: true,
|
|
386
|
+
enabled: false,
|
|
387
|
+
},
|
|
388
|
+
];
|
|
389
|
+
|
|
390
|
+
/** A client as it comes back from the one response that carries a secret. */
|
|
391
|
+
export const MINTED_CLIENT: OAuthClient = {
|
|
392
|
+
...CLIENT,
|
|
393
|
+
secret: 'Yb3nT7qk-2sV1pE0xR8wL4mZ6aQ9cJ5dN0gH2fK1uS',
|
|
394
|
+
notice:
|
|
395
|
+
'This secret is shown once and is not recoverable. Store it before leaving this page.',
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
/** The applications a user has authorized, as `@oauth-grants` lists them. */
|
|
399
|
+
export const GRANTS: OAuthGrants = {
|
|
400
|
+
'@id': 'http://id.localhost/@oauth-grants',
|
|
401
|
+
access_token_ttl: 900,
|
|
402
|
+
items: [
|
|
403
|
+
{
|
|
404
|
+
'@id': 'http://id.localhost/@oauth-grants/demo-rp',
|
|
405
|
+
client_id: 'demo-rp',
|
|
406
|
+
title: 'Plone Content Site',
|
|
407
|
+
registered: true,
|
|
408
|
+
enabled: true,
|
|
409
|
+
granted_at: '2026-08-24T09:15:00+00:00',
|
|
410
|
+
scopes: [
|
|
411
|
+
{ id: 'openid', claims: [] },
|
|
412
|
+
{
|
|
413
|
+
id: 'profile',
|
|
414
|
+
claims: ['name', 'preferred_username', 'picture', 'description'],
|
|
415
|
+
},
|
|
416
|
+
{ id: 'email', claims: ['email', 'email_verified'] },
|
|
417
|
+
],
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
'@id': 'http://id.localhost/@oauth-grants/reporting',
|
|
421
|
+
client_id: 'reporting',
|
|
422
|
+
title: 'Nightly reporting job',
|
|
423
|
+
registered: true,
|
|
424
|
+
enabled: true,
|
|
425
|
+
granted_at: '2026-06-02T22:40:00+00:00',
|
|
426
|
+
scopes: [{ id: 'openid', claims: [] }],
|
|
427
|
+
},
|
|
428
|
+
],
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
/** A pending authorization request, as `@oauth-consent` describes one. */
|
|
432
|
+
export const CONSENT_REQUEST: ConsentRequest = {
|
|
433
|
+
'@id': 'http://id.localhost/@oauth-consent',
|
|
434
|
+
client: { id: 'demo-rp', title: 'Plone Content Site' },
|
|
435
|
+
user: { id: 'alice', label: 'Alice Liddell' },
|
|
436
|
+
scopes: [
|
|
437
|
+
{ id: 'openid', claims: [] },
|
|
438
|
+
{ id: 'profile', claims: ['name', 'preferred_username', 'picture'] },
|
|
439
|
+
{ id: 'email', claims: ['email', 'email_verified'] },
|
|
440
|
+
],
|
|
441
|
+
authorize_url: 'http://id.localhost/@@oauth-authorize',
|
|
442
|
+
params: {
|
|
443
|
+
response_type: 'code',
|
|
444
|
+
client_id: 'demo-rp',
|
|
445
|
+
redirect_uri: 'http://plone.localhost/login-identity',
|
|
446
|
+
scope: 'openid profile email',
|
|
447
|
+
state: 'a1b2c3',
|
|
448
|
+
},
|
|
449
|
+
authenticator: 'a-plone-protect-token',
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
export const KEYRING: SigningKeyRing = {
|
|
453
|
+
'@id': '/@identity-keys',
|
|
454
|
+
algorithm: 'RS256',
|
|
455
|
+
ring_size: 2,
|
|
456
|
+
jwks_uri: 'https://example.org/@@oauth-jwks',
|
|
457
|
+
items_total: 2,
|
|
458
|
+
items: [
|
|
459
|
+
{ kid: 'q7Yk2mVx', active: true },
|
|
460
|
+
{ kid: 'b1Rn8dTs', active: false },
|
|
461
|
+
],
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Wrap a story in a redux store holding exactly the state it wants.
|
|
466
|
+
*
|
|
467
|
+
* @param state The store's whole state, as the component's selectors read it.
|
|
468
|
+
* @returns A Storybook decorator.
|
|
469
|
+
*/
|
|
470
|
+
/**
|
|
471
|
+
* One account as `@user-account` serves it.
|
|
472
|
+
*
|
|
473
|
+
* Shared, because two stories show it: the panel on its own, and the control
|
|
474
|
+
* panel page that fetches it.
|
|
475
|
+
*/
|
|
476
|
+
export const USER_ACCOUNT: UserAccount = {
|
|
477
|
+
'@id': '/@user-account/erico',
|
|
478
|
+
userid: 'erico',
|
|
479
|
+
fullname: 'Érico Andrei',
|
|
480
|
+
profile_url: '/identity-profiles/erico',
|
|
481
|
+
identities: [
|
|
482
|
+
{
|
|
483
|
+
provider: 'github',
|
|
484
|
+
title: 'GitHub',
|
|
485
|
+
subject: '99',
|
|
486
|
+
created: '2026-03-02T11:40:00+00:00',
|
|
487
|
+
last_login: '2026-08-21T18:03:00+00:00',
|
|
488
|
+
provider_configured: true,
|
|
489
|
+
provider_enabled: true,
|
|
490
|
+
groups: ['site-editors'],
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
provider: 'email',
|
|
494
|
+
title: 'Email',
|
|
495
|
+
subject: 'erico@plone.org',
|
|
496
|
+
created: '2026-01-14T09:12:00+00:00',
|
|
497
|
+
last_login: null,
|
|
498
|
+
provider_configured: true,
|
|
499
|
+
provider_enabled: true,
|
|
500
|
+
groups: [],
|
|
501
|
+
},
|
|
502
|
+
],
|
|
503
|
+
emails: [
|
|
504
|
+
{ address: 'erico@plone.org', verified: true, preferred: true },
|
|
505
|
+
{ address: 'erico@example.com', verified: false, preferred: false },
|
|
506
|
+
],
|
|
507
|
+
last_authenticated: '2026-08-21T18:03:00+00:00',
|
|
508
|
+
events_total: 2,
|
|
509
|
+
events: [
|
|
510
|
+
{
|
|
511
|
+
event: 'authenticated',
|
|
512
|
+
provider: 'github',
|
|
513
|
+
success: true,
|
|
514
|
+
timestamp: '2026-08-21T18:03:00+00:00',
|
|
515
|
+
detail: {},
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
event: 'magic-link-sent',
|
|
519
|
+
provider: 'email',
|
|
520
|
+
success: true,
|
|
521
|
+
timestamp: '2026-08-20T09:00:00+00:00',
|
|
522
|
+
detail: {},
|
|
523
|
+
},
|
|
524
|
+
],
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Render a story inside the login card, at the real page's dimensions.
|
|
529
|
+
*
|
|
530
|
+
* `LoginForm`, `PasswordForm` and `MagicLinkForm` are never seen anywhere but
|
|
531
|
+
* inside `LoginPanel`, and the panel is what sizes them: the card is
|
|
532
|
+
* `--identity-login-width` wide and the forms lay themselves out against
|
|
533
|
+
* that. On Storybook's full-width canvas they stretched to whatever the
|
|
534
|
+
* viewport was, so a story could look fine and the page wrong -- and two
|
|
535
|
+
* forms meant to be indistinguishable could not be compared at all.
|
|
536
|
+
*
|
|
537
|
+
* The real component rather than a `<div>` of the same width, so the stories
|
|
538
|
+
* cannot drift from the page: a change to the card's width or padding shows
|
|
539
|
+
* up here without anybody remembering to copy it.
|
|
540
|
+
*
|
|
541
|
+
* @param description The strip under the heading, which names what is below
|
|
542
|
+
* -- the real page picks between two sentences depending on what a site has
|
|
543
|
+
* configured, so a story showing only the password form passes the other.
|
|
544
|
+
* @returns A Storybook decorator.
|
|
545
|
+
*/
|
|
546
|
+
export function withLoginCard(
|
|
547
|
+
description = 'Choose how you would like to sign in.',
|
|
548
|
+
) {
|
|
549
|
+
const Decorator = (Story: () => ReactNode) => (
|
|
550
|
+
<LoginPanel title="Log in" description={description}>
|
|
551
|
+
{Story()}
|
|
552
|
+
</LoginPanel>
|
|
553
|
+
);
|
|
554
|
+
return Decorator;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* The store slices Volto's own chrome reads, which no story is about.
|
|
559
|
+
*
|
|
560
|
+
* Five pages here portal Volto's `Toolbar`, and it selects these four out of
|
|
561
|
+
* the store whatever the page is. A story that left them out did not render
|
|
562
|
+
* at all -- the failure was a TypeError from inside Volto, which reads as the
|
|
563
|
+
* add-on being broken rather than as a fixture missing furniture.
|
|
564
|
+
*
|
|
565
|
+
* Exactly four, established by removing each in turn: `apierror`, `intl` and
|
|
566
|
+
* `workflow` were guesses and are not read.
|
|
567
|
+
*/
|
|
568
|
+
export const VOLTO_CHROME = {
|
|
569
|
+
actions: { actions: {} },
|
|
570
|
+
content: { data: null },
|
|
571
|
+
types: { types: [] },
|
|
572
|
+
userSession: { token: null },
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* A store for a story, over the chrome every page needs.
|
|
577
|
+
*
|
|
578
|
+
* The story's own state wins, so a story about being signed in can say so by
|
|
579
|
+
* passing its own `userSession`.
|
|
580
|
+
*/
|
|
581
|
+
export function withStore(state: Record<string, unknown>) {
|
|
582
|
+
const merged = { ...VOLTO_CHROME, ...state };
|
|
583
|
+
const store = {
|
|
584
|
+
getState: () => merged,
|
|
585
|
+
dispatch: (action: unknown) => action,
|
|
586
|
+
subscribe: () => () => {},
|
|
587
|
+
};
|
|
588
|
+
const Decorator = (Story: () => ReactNode) => (
|
|
589
|
+
<Provider store={store as never}>{Story()}</Provider>
|
|
590
|
+
);
|
|
591
|
+
return Decorator;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* A portrait, inline.
|
|
596
|
+
*
|
|
597
|
+
* A data URI rather than a URL: a story has to render the same whether or
|
|
598
|
+
* not Storybook can reach the network, and a portrait that silently 404s
|
|
599
|
+
* would show the fallback while claiming to show the picture.
|
|
600
|
+
*/
|
|
601
|
+
export const PORTRAIT =
|
|
602
|
+
'data:image/svg+xml;utf8,' +
|
|
603
|
+
encodeURIComponent(
|
|
604
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96">
|
|
605
|
+
<rect width="96" height="96" fill="#0083be"/>
|
|
606
|
+
<circle cx="48" cy="36" r="18" fill="#bfe3f2"/>
|
|
607
|
+
<path d="M12 96c0-20 16-32 36-32s36 12 36 32z" fill="#bfe3f2"/>
|
|
608
|
+
</svg>`,
|
|
609
|
+
);
|
|
610
|
+
|
|
611
|
+
/** The signed-in user, as `@users/<userid>` describes them. */
|
|
612
|
+
export const USER: UserProfile = {
|
|
613
|
+
'@id': 'https://example.org/@users/alice',
|
|
614
|
+
id: 'alice',
|
|
615
|
+
username: 'alice@example.org',
|
|
616
|
+
fullname: 'Alice Liddell',
|
|
617
|
+
email: 'alice@example.org',
|
|
618
|
+
portrait: null,
|
|
619
|
+
source: 'identity_profile',
|
|
620
|
+
identities: [],
|
|
621
|
+
profile_url: 'https://example.org/identity-profiles/alice',
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Wrap a story in a store holding one signed-in user.
|
|
626
|
+
*
|
|
627
|
+
* The common case for anything in the user menu, which is why it is here
|
|
628
|
+
* rather than repeated per story.
|
|
629
|
+
*
|
|
630
|
+
* @param user The user, or null for an anonymous visitor.
|
|
631
|
+
* @returns A Storybook decorator.
|
|
632
|
+
*/
|
|
633
|
+
export function withUser(user: Partial<UserProfile> | null) {
|
|
634
|
+
return withStore({
|
|
635
|
+
userProfile: {
|
|
636
|
+
...LOADED,
|
|
637
|
+
data: user === null ? null : { ...USER, ...user },
|
|
638
|
+
},
|
|
639
|
+
});
|
|
640
|
+
}
|