@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
package/src/testing.tsx
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test-only rendering.
|
|
3
|
+
*
|
|
4
|
+
* Every component in this package formats its strings through `react-intl`,
|
|
5
|
+
* which means every one of them needs an `IntlProvider` somewhere above it or
|
|
6
|
+
* it throws on the first render. Storybook's preview supplies one; this is its
|
|
7
|
+
* counterpart for the tests, so no test file has to know that.
|
|
8
|
+
*
|
|
9
|
+
* Testing Library is re-exported so a test file still has a single import.
|
|
10
|
+
* @module testing
|
|
11
|
+
*/
|
|
12
|
+
import React from 'react';
|
|
13
|
+
import type { ReactElement, ReactNode } from 'react';
|
|
14
|
+
import { createIntl, IntlProvider as BareIntlProvider } from 'react-intl';
|
|
15
|
+
import { MemoryRouter } from 'react-router-dom';
|
|
16
|
+
import {
|
|
17
|
+
render as renderBare,
|
|
18
|
+
type RenderOptions,
|
|
19
|
+
type RenderResult,
|
|
20
|
+
} from '@testing-library/react';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* No message catalogue on purpose.
|
|
24
|
+
*
|
|
25
|
+
* The tests assert on what a reader sees, which for a locale with nothing
|
|
26
|
+
* translated is each descriptor's `defaultMessage` -- exactly the English the
|
|
27
|
+
* components used to hard-code. `onError` is silenced because "no message for
|
|
28
|
+
* this id" is the expected state here, not a failure.
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* `react-intl` 3.12's provider, given its children back.
|
|
32
|
+
*
|
|
33
|
+
* The version Volto pins types `IntlProvider` as a class component whose
|
|
34
|
+
* props never mention `children` -- React 17's types supplied that
|
|
35
|
+
* implicitly and React 18's do not, so every use of it is a type error and
|
|
36
|
+
* none of them is a bug. The cast restores the prop the component has always
|
|
37
|
+
* accepted and changes nothing at runtime. Exported because the tests that
|
|
38
|
+
* bring their own provider need the same repair.
|
|
39
|
+
*/
|
|
40
|
+
export const IntlProvider = BareIntlProvider as React.ComponentType<
|
|
41
|
+
React.ComponentProps<typeof BareIntlProvider> & { children?: ReactNode }
|
|
42
|
+
>;
|
|
43
|
+
|
|
44
|
+
const WithIntl = ({ children }: { children: ReactNode }) => (
|
|
45
|
+
<IntlProvider locale="en" defaultLocale="en" messages={{}} onError={() => {}}>
|
|
46
|
+
{/* A router as well, for the same reason: components in this package
|
|
47
|
+
link with `<Link>` rather than throwing the application away with a
|
|
48
|
+
plain anchor, and `<Link>` outside a Router throws on first render.
|
|
49
|
+
A test that needs to *observe* navigation still brings its own
|
|
50
|
+
`MemoryRouter` and asserts on its location. */}
|
|
51
|
+
<MemoryRouter>{children}</MemoryRouter>
|
|
52
|
+
</IntlProvider>
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Render as Testing Library does, inside an `IntlProvider`.
|
|
57
|
+
*
|
|
58
|
+
* @param ui The element under test.
|
|
59
|
+
* @param options Testing Library's own options.
|
|
60
|
+
* @returns Whatever `@testing-library/react`'s `render` returns.
|
|
61
|
+
*/
|
|
62
|
+
export function render(
|
|
63
|
+
ui: ReactElement,
|
|
64
|
+
options?: RenderOptions,
|
|
65
|
+
): RenderResult {
|
|
66
|
+
return renderBare(ui, { wrapper: WithIntl, ...options });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* An `IntlShape` for code that formats messages outside a component.
|
|
71
|
+
*
|
|
72
|
+
* The schema helpers build form labels in plain modules, so they take an
|
|
73
|
+
* `intl` rather than calling `useIntl`. Tests need one without rendering
|
|
74
|
+
* anything, and `createIntl` is how `react-intl` provides it.
|
|
75
|
+
*
|
|
76
|
+
* English with no catalogue, exactly as `WithIntl` renders: every message
|
|
77
|
+
* falls back to its `defaultMessage`, which is what the assertions read.
|
|
78
|
+
*/
|
|
79
|
+
export const testIntl = createIntl({
|
|
80
|
+
locale: 'en',
|
|
81
|
+
defaultLocale: 'en',
|
|
82
|
+
messages: {},
|
|
83
|
+
onError: () => {},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
export * from '@testing-library/react';
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shapes the backend actually returns.
|
|
3
|
+
* @module types
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* How a provider's button should look.
|
|
8
|
+
*
|
|
9
|
+
* Served on the public listing, so it carries presentation and nothing else:
|
|
10
|
+
* an icon, and the two colours the button is drawn in. Every field may be
|
|
11
|
+
* empty, and empty means "use the frontend's own styling" rather than a
|
|
12
|
+
* placeholder every provider shares.
|
|
13
|
+
*/
|
|
14
|
+
export interface ProviderStyle {
|
|
15
|
+
/**
|
|
16
|
+
* SVG source, sanitized by the backend on the way in.
|
|
17
|
+
*
|
|
18
|
+
* Rendered inline rather than as an `<img>`, which is what lets it inherit
|
|
19
|
+
* the button's colour. The backend keeps an allowlist of elements and
|
|
20
|
+
* attributes and refuses anything that references a URL, which is what
|
|
21
|
+
* makes inlining it safe -- see `core/svg.py`.
|
|
22
|
+
*/
|
|
23
|
+
icon?: string;
|
|
24
|
+
background_color?: string;
|
|
25
|
+
foreground_color?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** One provider offered on the login page, from `@login-providers`. */
|
|
29
|
+
export interface LoginProvider extends ProviderStyle {
|
|
30
|
+
'@id': string;
|
|
31
|
+
id: string;
|
|
32
|
+
title: string;
|
|
33
|
+
driver: string;
|
|
34
|
+
/**
|
|
35
|
+
* Whether a user may start a link against this from a form.
|
|
36
|
+
*
|
|
37
|
+
* False for magic link: the address it would prove is whatever was typed,
|
|
38
|
+
* and the addresses this site verifies are the ones already on a person's
|
|
39
|
+
* profile. The backend leaves such providers out of `available` as well;
|
|
40
|
+
* this is here so a client can explain the absence rather than only
|
|
41
|
+
* observe it.
|
|
42
|
+
*/
|
|
43
|
+
supports_manual_link?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Answer from `@login-providers/<id>`: where to send the browser. */
|
|
47
|
+
export interface AuthorizeRedirect {
|
|
48
|
+
provider: string;
|
|
49
|
+
authorize_url: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Answer from `@identity-callback` and `@magic-link-confirm`. */
|
|
53
|
+
export interface TokenResponse {
|
|
54
|
+
token: string;
|
|
55
|
+
came_from?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Answer from `POST @identities` for a provider with no authorize URL.
|
|
60
|
+
*
|
|
61
|
+
* The email provider's "provider" is a mailbox, so there is nowhere to send
|
|
62
|
+
* the browser: the flow continues when the link in the message is clicked.
|
|
63
|
+
*/
|
|
64
|
+
export interface EmailLinkSent {
|
|
65
|
+
provider: string;
|
|
66
|
+
sent: true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Either shape `POST @identities` answers with. */
|
|
70
|
+
export type LinkStarted = AuthorizeRedirect | EmailLinkSent;
|
|
71
|
+
|
|
72
|
+
/** Answer from `@magic-link-confirm` when the token was minted for linking. */
|
|
73
|
+
export interface IdentityLinked {
|
|
74
|
+
linked: {
|
|
75
|
+
provider: string;
|
|
76
|
+
subject: string;
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Everything `@magic-link-confirm` can answer with. */
|
|
81
|
+
export type ConfirmResponse = TokenResponse | IdentityLinked;
|
|
82
|
+
|
|
83
|
+
/** Reducer state shared by every request this add-on makes. */
|
|
84
|
+
export interface RequestState {
|
|
85
|
+
loading: boolean;
|
|
86
|
+
loaded: boolean;
|
|
87
|
+
error: unknown | null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** One identity the signed-in user owns, from `@identities`. */
|
|
91
|
+
export interface Identity {
|
|
92
|
+
'@id': string;
|
|
93
|
+
provider: string;
|
|
94
|
+
subject: string;
|
|
95
|
+
title: string;
|
|
96
|
+
created: string;
|
|
97
|
+
last_login: string | null;
|
|
98
|
+
/** False when this is the user's last way in. */
|
|
99
|
+
can_unlink: boolean;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** A driver and the form it needs, from `@identity-drivers`. */
|
|
103
|
+
/**
|
|
104
|
+
* A JSON schema as `plone.restapi` serializes an interface.
|
|
105
|
+
*
|
|
106
|
+
* The shape every Plone form is built from, and the reason this add-on no
|
|
107
|
+
* longer describes fields of its own.
|
|
108
|
+
*/
|
|
109
|
+
export interface JsonSchema {
|
|
110
|
+
properties?: Record<string, Record<string, unknown>>;
|
|
111
|
+
required?: string[];
|
|
112
|
+
fieldsets?: { id: string; title: string; fields: string[] }[];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* A composed form schema, as Volto's `Form` consumes one.
|
|
117
|
+
*
|
|
118
|
+
* The same three keys as `JsonSchema` and none of them optional, which is the
|
|
119
|
+
* whole difference: a `JsonSchema` is what arrived over the wire and may be
|
|
120
|
+
* missing anything, while this is what a helper in this package built out of
|
|
121
|
+
* one, and `Form` renders nothing at all when handed a schema without
|
|
122
|
+
* `properties`.
|
|
123
|
+
*/
|
|
124
|
+
export interface VoltoSchema {
|
|
125
|
+
properties: Record<string, Record<string, unknown>>;
|
|
126
|
+
required: string[];
|
|
127
|
+
fieldsets: { id: string; title: string; fields: string[] }[];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface Driver {
|
|
131
|
+
id: string;
|
|
132
|
+
title: string;
|
|
133
|
+
/**
|
|
134
|
+
* The driver's settings, as an ordinary JSON schema.
|
|
135
|
+
*
|
|
136
|
+
* Serialized by `plone.restapi` from the driver's `settings_schema`, so it
|
|
137
|
+
* carries `properties`, `required`, `fieldsets` and widget hints, already
|
|
138
|
+
* translated. The frontend composes it into the provider form and has no
|
|
139
|
+
* opinion about what is in it.
|
|
140
|
+
*/
|
|
141
|
+
schema: JsonSchema;
|
|
142
|
+
/**
|
|
143
|
+
* Claim path to user field, seeded into a new provider's mapping.
|
|
144
|
+
*
|
|
145
|
+
* A starting point rather than a rule: it is written into the form like
|
|
146
|
+
* anything typed there, and an operator edits or empties it before saving.
|
|
147
|
+
*/
|
|
148
|
+
default_propertymap?: Record<string, string>;
|
|
149
|
+
/**
|
|
150
|
+
* Provider group name to local group id, seeded into a new provider's map.
|
|
151
|
+
*
|
|
152
|
+
* Almost always empty, and honestly so: group names are a fact about one
|
|
153
|
+
* deployment's directory, not about a driver.
|
|
154
|
+
*/
|
|
155
|
+
default_groupmap?: Record<string, string>;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** A configured provider as the control panel sees it. */
|
|
159
|
+
export interface ConfiguredProvider extends ProviderStyle {
|
|
160
|
+
'@id': string;
|
|
161
|
+
id: string;
|
|
162
|
+
driver: string;
|
|
163
|
+
title: string;
|
|
164
|
+
/** Whether the provider may be used at all -- to sign in, and to link. */
|
|
165
|
+
enabled: boolean;
|
|
166
|
+
/**
|
|
167
|
+
* Whether the login screen offers a button for it.
|
|
168
|
+
*
|
|
169
|
+
* A separate question from `enabled`. An enabled provider that is not
|
|
170
|
+
* shown stays linkable from a user's own sign-in methods and still signs
|
|
171
|
+
* in an account already linked to it, which is what a staff-only provider
|
|
172
|
+
* looks like.
|
|
173
|
+
*/
|
|
174
|
+
show_in_login: boolean;
|
|
175
|
+
config: Record<string, unknown>;
|
|
176
|
+
/** Claim path to Plone user field, applied on every login. */
|
|
177
|
+
propertymap: Record<string, string>;
|
|
178
|
+
/**
|
|
179
|
+
* Provider group name to local group id, applied on every login.
|
|
180
|
+
*
|
|
181
|
+
* Empty grants nothing. Only groups this provider granted are ever taken
|
|
182
|
+
* back, so a group granted locally survives every sign-in.
|
|
183
|
+
*/
|
|
184
|
+
groupmap: Record<string, string>;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Answer from the per-provider connection check. */
|
|
188
|
+
export interface ConnectionCheck {
|
|
189
|
+
ok: boolean;
|
|
190
|
+
error?: string;
|
|
191
|
+
authorization_endpoint?: string;
|
|
192
|
+
token_endpoint?: string;
|
|
193
|
+
has_jwks?: boolean;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Answer from `@my-profile`: where the caller's Profile is, and how far along. */
|
|
197
|
+
export interface MyProfile {
|
|
198
|
+
'@id': string;
|
|
199
|
+
userid: string;
|
|
200
|
+
/** Absolute URL of the Profile, or null when the user has none. */
|
|
201
|
+
profile: string | null;
|
|
202
|
+
/** Workflow state, or null when the user has no Profile. */
|
|
203
|
+
review_state: string | null;
|
|
204
|
+
/**
|
|
205
|
+
* Required fields the Profile has no value for.
|
|
206
|
+
*
|
|
207
|
+
* Why `review_state` is `incomplete`, and what the gate tells the user it
|
|
208
|
+
* wants. Empty for a complete Profile and for a user without one.
|
|
209
|
+
*/
|
|
210
|
+
missing: string[];
|
|
211
|
+
/**
|
|
212
|
+
* The addresses the Profile carries, and which of them are verified.
|
|
213
|
+
*
|
|
214
|
+
* Optional because a client may be talking to a backend that predates it,
|
|
215
|
+
* and every consumer therefore has to tolerate its absence -- an empty
|
|
216
|
+
* list and a missing key mean the same thing here.
|
|
217
|
+
*/
|
|
218
|
+
emails?: ProfileEmail[];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* One address on a Profile.
|
|
223
|
+
*
|
|
224
|
+
* `verified` means this site holds the address as proved: either the person
|
|
225
|
+
* followed a magic link sent here, or a provider the operator marked as
|
|
226
|
+
* trusted vouched for it. A provider nobody marked still counts for nothing.
|
|
227
|
+
* `preferred` marks the one the Profile's `email` resolves to, so a page can
|
|
228
|
+
* show it without reimplementing the rule that picks it -- the first verified
|
|
229
|
+
* address, or the first address at all.
|
|
230
|
+
*/
|
|
231
|
+
export interface ProfileEmail {
|
|
232
|
+
address: string;
|
|
233
|
+
verified: boolean;
|
|
234
|
+
preferred: boolean;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* A user as `@users/<userid>` describes them once this add-on is installed.
|
|
239
|
+
*
|
|
240
|
+
* Only the fields this package adds or reads are named; the endpoint carries
|
|
241
|
+
* the whole of Plone's own user representation alongside them.
|
|
242
|
+
*/
|
|
243
|
+
export interface UserProfile {
|
|
244
|
+
'@id': string;
|
|
245
|
+
id: string;
|
|
246
|
+
/** The name this user signs in with; not the same as the userid. */
|
|
247
|
+
username?: string | null;
|
|
248
|
+
fullname?: string;
|
|
249
|
+
email?: string;
|
|
250
|
+
/** Portrait URL, when the user has uploaded one. */
|
|
251
|
+
portrait?: string | null;
|
|
252
|
+
/** The PAS plugin the userid came from, as PAS itself resolved it. */
|
|
253
|
+
source: string | null;
|
|
254
|
+
/** External identities linked to this account. */
|
|
255
|
+
identities: Identity[];
|
|
256
|
+
/** The user's Profile, when they have one. */
|
|
257
|
+
profile_url: string | null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** One member of a group, from `@group-members/<id>`. */
|
|
261
|
+
export interface GroupMember {
|
|
262
|
+
'@id': string;
|
|
263
|
+
id: string;
|
|
264
|
+
fullname: string;
|
|
265
|
+
login: string;
|
|
266
|
+
profile_url: string | null;
|
|
267
|
+
/**
|
|
268
|
+
* The groups this person is actually in.
|
|
269
|
+
*
|
|
270
|
+
* Which is how a listing can account for somebody being on it: a member of
|
|
271
|
+
* an inner group appears in the outer group's list, and this says through
|
|
272
|
+
* which.
|
|
273
|
+
*/
|
|
274
|
+
through: string[];
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** A group named in a nesting, with enough to render a link. */
|
|
278
|
+
export interface NestedGroup {
|
|
279
|
+
'@id': string;
|
|
280
|
+
id: string;
|
|
281
|
+
title: string;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** What `@group-members/<id>` answers with. */
|
|
285
|
+
export interface GroupMembers {
|
|
286
|
+
'@id': string;
|
|
287
|
+
group: string;
|
|
288
|
+
items_total: number;
|
|
289
|
+
items: GroupMember[];
|
|
290
|
+
/** Groups nested inside this one, at any depth. */
|
|
291
|
+
nested_groups: NestedGroup[];
|
|
292
|
+
/** Groups this one is nested inside, as stored rather than as closed. */
|
|
293
|
+
parent_groups: NestedGroup[];
|
|
294
|
+
batching?: Record<string, string>;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** One identity as an administrator sees it, from `@user-account/<id>`. */
|
|
298
|
+
export interface AccountIdentity extends ProviderStyle {
|
|
299
|
+
provider: string;
|
|
300
|
+
title: string;
|
|
301
|
+
subject: string;
|
|
302
|
+
created: string;
|
|
303
|
+
last_login: string | null;
|
|
304
|
+
/** Whether the provider still exists in this site's configuration. */
|
|
305
|
+
provider_configured: boolean;
|
|
306
|
+
/** Whether it is enabled. An identity against a disabled provider cannot
|
|
307
|
+
* sign anybody in, and looks like a broken login rather than a setting. */
|
|
308
|
+
provider_enabled: boolean;
|
|
309
|
+
/** Groups this provider granted at the last sign-in. */
|
|
310
|
+
groups: string[];
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** One recorded authentication event. */
|
|
314
|
+
export interface AuditEvent {
|
|
315
|
+
event: string;
|
|
316
|
+
provider: string;
|
|
317
|
+
success: boolean;
|
|
318
|
+
timestamp: string;
|
|
319
|
+
detail: Record<string, unknown>;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* How one account gets in, and when it last did.
|
|
324
|
+
*
|
|
325
|
+
* `@users/<id>` carries identities as bare ids; this names and styles them,
|
|
326
|
+
* says whether each provider is still usable, and adds the two things Plone
|
|
327
|
+
* records nowhere: when the person last authenticated, and by what route.
|
|
328
|
+
*/
|
|
329
|
+
export interface UserAccount {
|
|
330
|
+
'@id': string;
|
|
331
|
+
userid: string;
|
|
332
|
+
fullname: string;
|
|
333
|
+
profile_url: string | null;
|
|
334
|
+
identities: AccountIdentity[];
|
|
335
|
+
emails: ProfileEmail[];
|
|
336
|
+
/**
|
|
337
|
+
* ISO 8601, or null.
|
|
338
|
+
*
|
|
339
|
+
* Null is not "never signed in": the audit log is bounded, so an account
|
|
340
|
+
* dormant longer than the retention period has had its entries purged.
|
|
341
|
+
*/
|
|
342
|
+
last_authenticated: string | null;
|
|
343
|
+
events_total: number;
|
|
344
|
+
events: AuditEvent[];
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** One registered OAuth client: who may log in *to* this site. */
|
|
348
|
+
export interface OAuthClient {
|
|
349
|
+
'@id': string;
|
|
350
|
+
client_id: string;
|
|
351
|
+
title: string;
|
|
352
|
+
redirect_uris: string[];
|
|
353
|
+
grant_types: string[];
|
|
354
|
+
scope: string[];
|
|
355
|
+
auth_method: string;
|
|
356
|
+
public: boolean;
|
|
357
|
+
enabled: boolean;
|
|
358
|
+
service_user: string;
|
|
359
|
+
/**
|
|
360
|
+
* The plaintext secret. Present only in the response that minted it, at
|
|
361
|
+
* registration or rotation, and never readable again: the server stores a
|
|
362
|
+
* hash. Anything holding this has to show it to the operator at once.
|
|
363
|
+
*/
|
|
364
|
+
secret?: string;
|
|
365
|
+
/** The server's own words about that, shown alongside the secret. */
|
|
366
|
+
notice?: string;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* A pending authorization request, as `@oauth-consent` describes it.
|
|
371
|
+
*
|
|
372
|
+
* Nothing here is a decision. The answer goes back to `authorize_url` with
|
|
373
|
+
* `params` and the `authenticator`, and the authorization endpoint re-runs
|
|
374
|
+
* every check before acting on it.
|
|
375
|
+
*/
|
|
376
|
+
export interface ConsentRequest {
|
|
377
|
+
'@id': string;
|
|
378
|
+
/** The application asking. */
|
|
379
|
+
client: { id: string; title: string };
|
|
380
|
+
/** Who would be agreeing, so a forgotten session is visible. */
|
|
381
|
+
user: { id: string; label: string };
|
|
382
|
+
/** What is being asked for, in the order the client asked. */
|
|
383
|
+
scopes: { id: string; claims: string[] }[];
|
|
384
|
+
/** Where the answer goes. */
|
|
385
|
+
authorize_url: string;
|
|
386
|
+
/** The authorization request, to hand back unchanged. */
|
|
387
|
+
params: Record<string, string>;
|
|
388
|
+
/** plone.protect's token, bound to this user. */
|
|
389
|
+
authenticator: string;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* One application a user has authorized, as `@oauth-grants` lists them.
|
|
394
|
+
*
|
|
395
|
+
* The mirror image of an `Identity`: that is a provider they sign in *with*,
|
|
396
|
+
* this is an application they signed in *to*.
|
|
397
|
+
*/
|
|
398
|
+
export interface OAuthGrant {
|
|
399
|
+
'@id': string;
|
|
400
|
+
client_id: string;
|
|
401
|
+
/** What the consent screen called it; the id when nothing else is left. */
|
|
402
|
+
title: string;
|
|
403
|
+
/** Whether the client is still registered on this site. */
|
|
404
|
+
registered: boolean;
|
|
405
|
+
enabled: boolean;
|
|
406
|
+
/** ISO 8601, when the agreement was last given. */
|
|
407
|
+
granted_at: string;
|
|
408
|
+
/** What was agreed to, and what each scope releases. */
|
|
409
|
+
scopes: { id: string; claims: string[] }[];
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** What `@oauth-grants` answers with. */
|
|
413
|
+
export interface OAuthGrants {
|
|
414
|
+
'@id': string;
|
|
415
|
+
items: OAuthGrant[];
|
|
416
|
+
/**
|
|
417
|
+
* Seconds an access token already minted may still be accepted.
|
|
418
|
+
*
|
|
419
|
+
* Withdrawing consent cannot reach one: they are self-encoded with no
|
|
420
|
+
* denylist. The screen says so rather than implying a cutoff.
|
|
421
|
+
*/
|
|
422
|
+
access_token_ttl: number;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** One key in the signing ring. Metadata only; never key material. */
|
|
426
|
+
export interface SigningKey {
|
|
427
|
+
kid: string;
|
|
428
|
+
/** Whether this is the key currently signing. */
|
|
429
|
+
active: boolean;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** The signing ring, as the admin API describes it. */
|
|
433
|
+
export interface SigningKeyRing {
|
|
434
|
+
'@id': string;
|
|
435
|
+
algorithm: string;
|
|
436
|
+
/** How many keys the ring holds before the oldest is dropped. */
|
|
437
|
+
ring_size: number;
|
|
438
|
+
jwks_uri: string;
|
|
439
|
+
items_total: number;
|
|
440
|
+
items: SigningKey[];
|
|
441
|
+
}
|
package/towncrier.toml
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[tool.towncrier]
|
|
2
|
+
filename = "CHANGELOG.md"
|
|
3
|
+
directory = "news/"
|
|
4
|
+
title_format = "## {version} ({project_date})"
|
|
5
|
+
underlines = ["", "", ""]
|
|
6
|
+
template = "./news/.changelog_template.jinja"
|
|
7
|
+
start_string = "<!-- towncrier release notes start -->\n"
|
|
8
|
+
issue_format = "[#{issue}](https://github.com/collective/pas-plugins-identity/issues/{issue})"
|
|
9
|
+
|
|
10
|
+
[[tool.towncrier.type]]
|
|
11
|
+
directory = "breaking"
|
|
12
|
+
name = "Breaking"
|
|
13
|
+
showcontent = true
|
|
14
|
+
|
|
15
|
+
[[tool.towncrier.type]]
|
|
16
|
+
directory = "feature"
|
|
17
|
+
name = "Feature"
|
|
18
|
+
showcontent = true
|
|
19
|
+
|
|
20
|
+
[[tool.towncrier.type]]
|
|
21
|
+
directory = "bugfix"
|
|
22
|
+
name = "Bugfix"
|
|
23
|
+
showcontent = true
|
|
24
|
+
|
|
25
|
+
[[tool.towncrier.type]]
|
|
26
|
+
directory = "internal"
|
|
27
|
+
name = "Internal"
|
|
28
|
+
showcontent = true
|
|
29
|
+
|
|
30
|
+
[[tool.towncrier.type]]
|
|
31
|
+
directory = "documentation"
|
|
32
|
+
name = "Documentation"
|
|
33
|
+
showcontent = true
|
|
34
|
+
|
|
35
|
+
[[tool.towncrier.type]]
|
|
36
|
+
directory = "tests"
|
|
37
|
+
name = "Tests"
|
|
38
|
+
showcontent = true
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"esModuleInterop": true,
|
|
4
|
+
"skipLibCheck": true,
|
|
5
|
+
"target": "es2022",
|
|
6
|
+
"allowJs": true,
|
|
7
|
+
"resolveJsonModule": true,
|
|
8
|
+
"moduleDetection": "force",
|
|
9
|
+
"isolatedModules": true,
|
|
10
|
+
"verbatimModuleSyntax": true,
|
|
11
|
+
"module": "preserve",
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"lib": ["es2022", "dom", "dom.iterable"],
|
|
14
|
+
"jsx": "react-jsx",
|
|
15
|
+
"paths": {
|
|
16
|
+
"@plone/volto/*": ["../../core/packages/volto/src/*"],
|
|
17
|
+
"@plone-collective/volto-identity/*": ["./src/*"]
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"],
|
|
21
|
+
"exclude": ["node_modules", "build", "public", "coverage"]
|
|
22
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
import voltoVitestConfig from '@plone/volto/vitest.config.mjs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
|
|
8
|
+
const COMPONENTS_SRC = path.resolve(
|
|
9
|
+
__dirname,
|
|
10
|
+
'../../core/packages/components/src',
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Volto's shared aliases map `@plone/components` onto that package's `src`,
|
|
15
|
+
* so an import written the way a real build needs it --
|
|
16
|
+
* `@plone/components/src/styles/basic/Foo.css`, which is how volto-authomatic
|
|
17
|
+
* imports widget stylesheets -- would resolve to `src/src/...` here and fail.
|
|
18
|
+
*
|
|
19
|
+
* This maps the full subpath, and does it *before* spreading Volto's aliases
|
|
20
|
+
* so the longer, more specific key is matched first.
|
|
21
|
+
*/
|
|
22
|
+
const withComponentsSrc = (project) => ({
|
|
23
|
+
...project,
|
|
24
|
+
resolve: {
|
|
25
|
+
...project.resolve,
|
|
26
|
+
alias: {
|
|
27
|
+
'@plone/components/src': COMPONENTS_SRC,
|
|
28
|
+
...(project.resolve?.alias ?? {}),
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export default defineConfig({
|
|
34
|
+
...voltoVitestConfig,
|
|
35
|
+
resolve: {
|
|
36
|
+
...voltoVitestConfig.resolve,
|
|
37
|
+
alias: {
|
|
38
|
+
'@plone/components/src': COMPONENTS_SRC,
|
|
39
|
+
...(voltoVitestConfig.resolve?.alias ?? {}),
|
|
40
|
+
'@plone/volto': path.resolve(__dirname, '../../core/packages/volto/src'),
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
test: {
|
|
44
|
+
...voltoVitestConfig.test,
|
|
45
|
+
projects: (voltoVitestConfig.test?.projects ?? []).map(withComponentsSrc),
|
|
46
|
+
},
|
|
47
|
+
});
|