strapi-plugin-oidc 1.3.2 → 1.4.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/README.md +92 -44
- package/dist/admin/{index-BqyGGX8X.js → index-BnFRueNv.js} +163 -33
- package/dist/admin/{index-CFmg9Kxl.mjs → index-CY4s-vtv.mjs} +167 -37
- package/dist/admin/{index-Cse9ex24.js → index-RMgj1w0B.js} +15 -2
- package/dist/admin/{index-D1ypRUlq.mjs → index-ZRaWWFUL.mjs} +15 -2
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/server/index.js +257 -219
- package/dist/server/index.mjs +257 -219
- package/package.json +11 -4
package/README.md
CHANGED
|
@@ -11,80 +11,124 @@
|
|
|
11
11
|
</p>
|
|
12
12
|
</div>
|
|
13
13
|
|
|
14
|
-
A Strapi plugin that provides OpenID Connect (OIDC) authentication
|
|
15
|
-
|
|
16
|
-
This plugin allows your administrators to log in to the Strapi administration interface using external OIDC identity providers such as Zitadel, Keycloak, Auth0, AWS Cognito, and others.
|
|
14
|
+
A Strapi plugin that provides OpenID Connect (OIDC) authentication for the Strapi Admin Panel. Supports Keycloak, Auth0, Okta, Azure AD, Authentik, Authelia, and any other OpenID Connect provider.
|
|
17
15
|
|
|
18
16
|
## Installation
|
|
19
17
|
|
|
20
|
-
You can install the plugin via `npm` or `yarn`:
|
|
21
|
-
|
|
22
18
|
```bash
|
|
23
|
-
# Using npm
|
|
24
19
|
npm install strapi-plugin-oidc
|
|
25
|
-
|
|
26
|
-
# Using yarn
|
|
27
|
-
yarn add strapi-plugin-oidc
|
|
28
20
|
```
|
|
29
21
|
|
|
30
22
|
## Configuration
|
|
31
23
|
|
|
32
|
-
|
|
24
|
+
Add the plugin to `config/plugins.js` (or `.ts`):
|
|
33
25
|
|
|
34
26
|
```javascript
|
|
35
27
|
module.exports = ({ env }) => ({
|
|
36
|
-
// ...
|
|
37
28
|
'strapi-plugin-oidc': {
|
|
38
29
|
enabled: true,
|
|
39
30
|
config: {
|
|
40
|
-
//
|
|
41
|
-
OIDC_CLIENT_ID: '
|
|
42
|
-
OIDC_CLIENT_SECRET: '
|
|
43
|
-
OIDC_REDIRECT_URI: '
|
|
44
|
-
OIDC_AUTHORIZATION_ENDPOINT: '
|
|
45
|
-
OIDC_TOKEN_ENDPOINT: '
|
|
46
|
-
OIDC_USER_INFO_ENDPOINT: '
|
|
47
|
-
|
|
48
|
-
//
|
|
31
|
+
// Required
|
|
32
|
+
OIDC_CLIENT_ID: env('OIDC_CLIENT_ID'),
|
|
33
|
+
OIDC_CLIENT_SECRET: env('OIDC_CLIENT_SECRET'),
|
|
34
|
+
OIDC_REDIRECT_URI: env('OIDC_REDIRECT_URI'), // https://your-strapi.com/strapi-plugin-oidc/oidc/callback
|
|
35
|
+
OIDC_AUTHORIZATION_ENDPOINT: env('OIDC_AUTHORIZATION_ENDPOINT'),
|
|
36
|
+
OIDC_TOKEN_ENDPOINT: env('OIDC_TOKEN_ENDPOINT'),
|
|
37
|
+
OIDC_USER_INFO_ENDPOINT: env('OIDC_USER_INFO_ENDPOINT'),
|
|
38
|
+
|
|
39
|
+
// Optional — defaults shown
|
|
49
40
|
OIDC_SCOPES: 'openid profile email',
|
|
50
41
|
OIDC_GRANT_TYPE: 'authorization_code',
|
|
51
|
-
OIDC_FAMILY_NAME_FIELD: 'family_name',
|
|
52
|
-
OIDC_GIVEN_NAME_FIELD: 'given_name',
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
OIDC_SSO_BUTTON_TEXT: 'Login via SSO', // Text on the SSO button injected into the login page
|
|
58
|
-
OIDC_ENFORCE: null, // null = use Admin UI setting; true/false = override it in config
|
|
59
|
-
REMEMBER_ME: false, // true = persist session across browser restarts, using Strapi's built-in refresh token duration
|
|
42
|
+
OIDC_FAMILY_NAME_FIELD: 'family_name',
|
|
43
|
+
OIDC_GIVEN_NAME_FIELD: 'given_name',
|
|
44
|
+
OIDC_LOGOUT_URL: '', // Provider logout URL; omit to redirect to Strapi login
|
|
45
|
+
OIDC_SSO_BUTTON_TEXT: 'Login via SSO',
|
|
46
|
+
OIDC_ENFORCE: null, // null = use Admin UI toggle; true/false = override in config
|
|
47
|
+
REMEMBER_ME: false, // Persist session across browser restarts
|
|
60
48
|
},
|
|
61
49
|
},
|
|
62
|
-
// ...
|
|
63
50
|
});
|
|
64
51
|
```
|
|
65
52
|
|
|
66
|
-
##
|
|
53
|
+
## Login
|
|
67
54
|
|
|
68
|
-
|
|
69
|
-
`http://<your-strapi-domain>/strapi-plugin-oidc/oidc`
|
|
55
|
+
Navigate to `/strapi-plugin-oidc/oidc` to start the OIDC flow, or click the **Login via SSO** button that is always injected into the Strapi login page.
|
|
70
56
|
|
|
71
|
-
|
|
57
|
+
## Admin Settings
|
|
72
58
|
|
|
73
|
-
|
|
59
|
+
Manage the plugin under **Settings → OIDC Plugin**.
|
|
74
60
|
|
|
75
|
-
|
|
61
|
+
**Default Roles** — Select which Strapi admin role(s) are assigned to new users on first login.
|
|
62
|
+
|
|
63
|
+
**Whitelist** — Restrict access to specific email addresses. When the whitelist is enabled, only listed emails can log in. When empty, any successfully authenticated OIDC user gets an account. The whitelist supports:
|
|
64
|
+
|
|
65
|
+
- Adding individual emails with optional role overrides
|
|
66
|
+
- JSON import / export (see [format](#import-format) below)
|
|
67
|
+
- Bulk delete with confirmation
|
|
68
|
+
- Unsaved changes are held in the UI until **Save Changes** is clicked
|
|
69
|
+
|
|
70
|
+
**Enforce OIDC Login** — Removes the standard email/password fields from the login page and blocks direct login API calls server-side. Automatically disabled when the whitelist is empty to prevent lockout.
|
|
71
|
+
|
|
72
|
+
- The toggle is grayed out and locked when `OIDC_ENFORCE` is set in config.
|
|
73
|
+
- **Lockout recovery**: set `OIDC_ENFORCE: false` in your plugin config and restart Strapi. This writes through to the database so removing the variable afterwards keeps the setting.
|
|
74
|
+
|
|
75
|
+
## Whitelist API
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
The whitelist can be managed programmatically using a Strapi **API token** (Settings → API Tokens → Full Access). All endpoints are under `/api/strapi-plugin-oidc` and require `Authorization: Bearer <token>`.
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
79
|
+
| Method | Path | Description |
|
|
80
|
+
| -------- | ------------------------------------------ | ---------------------- |
|
|
81
|
+
| `GET` | `/api/strapi-plugin-oidc/whitelist` | List all entries |
|
|
82
|
+
| `POST` | `/api/strapi-plugin-oidc/whitelist` | Add one or more emails |
|
|
83
|
+
| `POST` | `/api/strapi-plugin-oidc/whitelist/import` | Bulk import |
|
|
84
|
+
| `DELETE` | `/api/strapi-plugin-oidc/whitelist/:id` | Remove by ID |
|
|
85
|
+
| `DELETE` | `/api/strapi-plugin-oidc/whitelist` | Remove all entries |
|
|
86
|
+
|
|
87
|
+
API calls write directly to the database — there is no unsaved state.
|
|
88
|
+
|
|
89
|
+
### Import format
|
|
90
|
+
|
|
91
|
+
Accepted by both the API import endpoint and the Admin UI import button. `roles` is optional and accepts role **names** (recommended) or numeric IDs. If the email already exists as a Strapi admin user, their current roles are used automatically.
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
[
|
|
95
|
+
{ "email": "alice@example.com", "roles": ["Editor"] },
|
|
96
|
+
{ "email": "bob@example.com", "roles": ["Editor", "Author"] },
|
|
97
|
+
{ "email": "carol@example.com" }
|
|
98
|
+
]
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Duplicate emails within the payload and emails already in the whitelist are silently skipped.
|
|
102
|
+
|
|
103
|
+
### Examples
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
# List
|
|
107
|
+
curl -H "Authorization: Bearer <token>" \
|
|
108
|
+
http://localhost:1337/api/strapi-plugin-oidc/whitelist
|
|
109
|
+
|
|
110
|
+
# Add
|
|
111
|
+
curl -X POST -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
|
|
112
|
+
-d '{"email": "user@example.com", "roles": ["Editor"]}' \
|
|
113
|
+
http://localhost:1337/api/strapi-plugin-oidc/whitelist
|
|
114
|
+
|
|
115
|
+
# Bulk import
|
|
116
|
+
curl -X POST -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
|
|
117
|
+
-d '{"users": [{"email": "a@example.com", "roles": ["Editor"]}, {"email": "b@example.com"}]}' \
|
|
118
|
+
http://localhost:1337/api/strapi-plugin-oidc/whitelist/import
|
|
119
|
+
|
|
120
|
+
# Delete one
|
|
121
|
+
curl -X DELETE -H "Authorization: Bearer <token>" \
|
|
122
|
+
http://localhost:1337/api/strapi-plugin-oidc/whitelist/42
|
|
123
|
+
|
|
124
|
+
# Delete all
|
|
125
|
+
curl -X DELETE -H "Authorization: Bearer <token>" \
|
|
126
|
+
http://localhost:1337/api/strapi-plugin-oidc/whitelist
|
|
127
|
+
```
|
|
84
128
|
|
|
85
129
|
## Credits & Changes
|
|
86
130
|
|
|
87
|
-
This plugin is a hard fork of
|
|
131
|
+
This plugin is a hard fork of [`strapi-plugin-sso`](https://github.com/yasudacloud/strapi-plugin-sso) by **yasudacloud**. Huge thanks to them for creating the foundation of this plugin!
|
|
88
132
|
|
|
89
133
|
### Changes made to the original codebase:
|
|
90
134
|
|
|
@@ -92,10 +136,14 @@ This plugin is a hard fork of the original [`strapi-plugin-sso`](https://github.
|
|
|
92
136
|
- Redesigned the Whitelist and Role management UI (switched to native Strapi cards, added pagination, etc.).
|
|
93
137
|
- Added an OIDC logout redirect URL.
|
|
94
138
|
- Added an option to "Enforce OIDC login" with an admin toggle (automatically disabled if the whitelist is empty).
|
|
95
|
-
- Added "Remember Me" support for OIDC sessions, using Strapi's built-in refresh token duration and idle lifespan.
|
|
96
139
|
- Migrated the testing framework to Vitest and added comprehensive test coverage for controllers and services.
|
|
97
140
|
- Cleaned up dead code and unused dependencies to improve maintainability.
|
|
98
141
|
- Upgraded to use newer versions of Node.js.
|
|
99
142
|
- Added styled success and error pages.
|
|
100
143
|
- Always injects a "Login via SSO" button on the Strapi login page. Button text is configurable via `OIDC_SSO_BUTTON_TEXT`. When enforcement is on, standard login fields are hidden so only the SSO button is visible.
|
|
144
|
+
- Whitelist improvements:
|
|
145
|
+
- JSON import and export (uses human-readable role names).
|
|
146
|
+
- Bulk delete all entries with a confirmation dialog.
|
|
147
|
+
- Unsaved changes confirmation when navigating away from the settings page.
|
|
148
|
+
- Programmatic API for managing the whitelist via Strapi API tokens (list, register, import, delete, delete all).
|
|
101
149
|
- Added misc. quality of life improvements and bug fixes.
|
|
@@ -7,7 +7,7 @@ const react = require("react");
|
|
|
7
7
|
const designSystem = require("@strapi/design-system");
|
|
8
8
|
const icons = require("@strapi/icons");
|
|
9
9
|
const reactIntl = require("react-intl");
|
|
10
|
-
const index = require("./index-
|
|
10
|
+
const index = require("./index-RMgj1w0B.js");
|
|
11
11
|
const styled = require("styled-components");
|
|
12
12
|
const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
|
|
13
13
|
const styled__default = /* @__PURE__ */ _interopDefault(styled);
|
|
@@ -39,7 +39,7 @@ const CustomTable = styled__default.default(designSystem.Table)`
|
|
|
39
39
|
font-size: 1.3rem !important;
|
|
40
40
|
}
|
|
41
41
|
`;
|
|
42
|
-
|
|
42
|
+
function LocalizedDate({ date }) {
|
|
43
43
|
const userLocale = navigator.language || "en-US";
|
|
44
44
|
return new Intl.DateTimeFormat(userLocale, {
|
|
45
45
|
year: "numeric",
|
|
@@ -48,7 +48,7 @@ const LocalizedDate = ({ date }) => {
|
|
|
48
48
|
hour: "2-digit",
|
|
49
49
|
minute: "2-digit"
|
|
50
50
|
}).format(new Date(date));
|
|
51
|
-
}
|
|
51
|
+
}
|
|
52
52
|
function Whitelist({
|
|
53
53
|
users,
|
|
54
54
|
roles,
|
|
@@ -56,17 +56,28 @@ function Whitelist({
|
|
|
56
56
|
useWhitelist,
|
|
57
57
|
loading,
|
|
58
58
|
onSave,
|
|
59
|
-
onDelete
|
|
59
|
+
onDelete,
|
|
60
|
+
onDeleteAll,
|
|
61
|
+
onImport,
|
|
62
|
+
onExport
|
|
60
63
|
}) {
|
|
61
64
|
const [email, setEmail] = react.useState("");
|
|
62
65
|
const [selectedRoles, setSelectedRoles] = react.useState([]);
|
|
63
66
|
const [page, setPage] = react.useState(1);
|
|
64
67
|
const { formatMessage } = reactIntl.useIntl();
|
|
65
68
|
const { toggleNotification } = admin.useNotification();
|
|
69
|
+
const fileInputRef = react.useRef(null);
|
|
66
70
|
const PAGE_SIZE = 10;
|
|
67
71
|
const pageCount = Math.ceil(users.length / PAGE_SIZE) || 1;
|
|
68
72
|
const paginatedUsers = users.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
|
|
69
|
-
const
|
|
73
|
+
const getRoleNames = (roleIds) => roleIds.map((roleId) => {
|
|
74
|
+
const r = roles.find((ro) => String(ro.id) === String(roleId));
|
|
75
|
+
return r ? r.name : roleId;
|
|
76
|
+
}).join(", ");
|
|
77
|
+
const defaultRoleNames = getRoleNames(oidcRoles.flatMap((oidc) => oidc.role ?? []));
|
|
78
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
79
|
+
const isValidEmail = emailRegex.test(email);
|
|
80
|
+
const onSaveEmail = react.useCallback(() => {
|
|
70
81
|
const emailText = email.trim();
|
|
71
82
|
if (users.some((user) => user.email === emailText)) {
|
|
72
83
|
toggleNotification({
|
|
@@ -74,18 +85,98 @@ function Whitelist({
|
|
|
74
85
|
message: formatMessage(index.getTrad("whitelist.error.unique"))
|
|
75
86
|
});
|
|
76
87
|
} else {
|
|
77
|
-
|
|
88
|
+
onSave(emailText, selectedRoles);
|
|
78
89
|
setEmail("");
|
|
79
90
|
setSelectedRoles([]);
|
|
80
91
|
}
|
|
81
92
|
}, [email, selectedRoles, users, onSave, formatMessage, toggleNotification]);
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
93
|
+
const handleImport = react.useCallback(
|
|
94
|
+
async (e) => {
|
|
95
|
+
const file = e.target.files?.[0];
|
|
96
|
+
if (!fileInputRef.current) return;
|
|
97
|
+
fileInputRef.current.value = "";
|
|
98
|
+
if (!file) return;
|
|
99
|
+
try {
|
|
100
|
+
const text = await file.text();
|
|
101
|
+
const parsed = JSON.parse(text);
|
|
102
|
+
if (!Array.isArray(parsed)) throw new Error();
|
|
103
|
+
const entries = parsed.filter((item) => item?.email).map((item) => ({
|
|
104
|
+
email: String(item.email),
|
|
105
|
+
roles: Array.isArray(item.roles) ? item.roles : []
|
|
106
|
+
}));
|
|
107
|
+
const count = await onImport(entries);
|
|
108
|
+
if (count === 0) {
|
|
109
|
+
toggleNotification({
|
|
110
|
+
type: "info",
|
|
111
|
+
message: formatMessage(index.getTrad("whitelist.import.none"))
|
|
112
|
+
});
|
|
113
|
+
} else {
|
|
114
|
+
toggleNotification({
|
|
115
|
+
type: "success",
|
|
116
|
+
message: formatMessage(index.getTrad("whitelist.import.success"), { count })
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
toggleNotification({
|
|
121
|
+
type: "warning",
|
|
122
|
+
message: formatMessage(index.getTrad("whitelist.import.error"))
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
[onImport, formatMessage, toggleNotification]
|
|
127
|
+
);
|
|
86
128
|
return /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { children: [
|
|
87
129
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { tag: "p", variant: "omega", textColor: "neutral600", marginBottom: 4, children: formatMessage(index.getTrad("whitelist.description")) }),
|
|
88
130
|
useWhitelist && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
131
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", alignItems: "center", marginBottom: 4, children: [
|
|
132
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", children: formatMessage(index.getTrad("whitelist.count"), { count: users.length }) }),
|
|
133
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, children: [
|
|
134
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
135
|
+
designSystem.Button,
|
|
136
|
+
{
|
|
137
|
+
size: "S",
|
|
138
|
+
variant: "tertiary",
|
|
139
|
+
startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Download, {}),
|
|
140
|
+
onClick: onExport,
|
|
141
|
+
disabled: users.length === 0,
|
|
142
|
+
children: formatMessage(index.getTrad("whitelist.export"))
|
|
143
|
+
}
|
|
144
|
+
),
|
|
145
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
146
|
+
designSystem.Button,
|
|
147
|
+
{
|
|
148
|
+
size: "S",
|
|
149
|
+
variant: "tertiary",
|
|
150
|
+
startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Upload, {}),
|
|
151
|
+
onClick: () => fileInputRef.current?.click(),
|
|
152
|
+
children: formatMessage(index.getTrad("whitelist.import"))
|
|
153
|
+
}
|
|
154
|
+
),
|
|
155
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
156
|
+
"input",
|
|
157
|
+
{
|
|
158
|
+
ref: fileInputRef,
|
|
159
|
+
type: "file",
|
|
160
|
+
accept: ".json,application/json",
|
|
161
|
+
style: { display: "none" },
|
|
162
|
+
onChange: handleImport
|
|
163
|
+
}
|
|
164
|
+
),
|
|
165
|
+
users.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Dialog.Root, { children: [
|
|
166
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Dialog.Trigger, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { size: "S", variant: "danger-light", startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Trash, {}), children: formatMessage(index.getTrad("whitelist.delete.all.label")) }) }),
|
|
167
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Dialog.Content, { children: [
|
|
168
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Dialog.Header, { children: formatMessage(index.getTrad("whitelist.delete.all.title")) }),
|
|
169
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Dialog.Body, { icon: /* @__PURE__ */ jsxRuntime.jsx(icons.WarningCircle, { fill: "danger600" }), children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textColor: "neutral800", textAlign: "center", children: formatMessage(index.getTrad("whitelist.delete.all.description"), {
|
|
170
|
+
count: users.length
|
|
171
|
+
}) }) }) }),
|
|
172
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Dialog.Footer, { children: [
|
|
173
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Dialog.Cancel, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { fullWidth: true, variant: "tertiary", children: formatMessage(index.getTrad("page.cancel")) }) }),
|
|
174
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Dialog.Action, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { fullWidth: true, variant: "danger", onClick: onDeleteAll, children: formatMessage(index.getTrad("whitelist.delete.all.label")) }) })
|
|
175
|
+
] })
|
|
176
|
+
] })
|
|
177
|
+
] })
|
|
178
|
+
] })
|
|
179
|
+
] }),
|
|
89
180
|
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 4, marginTop: 5, marginBottom: 5, alignItems: "flex-start", children: [
|
|
90
181
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { style: { flex: 1 }, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Root, { children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
91
182
|
designSystem.Field.Input,
|
|
@@ -93,7 +184,7 @@ function Whitelist({
|
|
|
93
184
|
type: "text",
|
|
94
185
|
disabled: loading,
|
|
95
186
|
value: email,
|
|
96
|
-
hasError: Boolean(email && !isValidEmail
|
|
187
|
+
hasError: Boolean(email && !isValidEmail),
|
|
97
188
|
onChange: (e) => setEmail(e.currentTarget.value),
|
|
98
189
|
placeholder: formatMessage(index.getTrad("whitelist.email.placeholder"))
|
|
99
190
|
}
|
|
@@ -115,7 +206,7 @@ function Whitelist({
|
|
|
115
206
|
{
|
|
116
207
|
size: "L",
|
|
117
208
|
startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Plus, {}),
|
|
118
|
-
disabled: loading || email.trim() === "" || !isValidEmail
|
|
209
|
+
disabled: loading || email.trim() === "" || !isValidEmail,
|
|
119
210
|
loading,
|
|
120
211
|
onClick: onSaveEmail,
|
|
121
212
|
children: formatMessage(index.getTrad("page.add"))
|
|
@@ -132,22 +223,16 @@ function Whitelist({
|
|
|
132
223
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Th, { style: { paddingRight: 0 }, children: " " })
|
|
133
224
|
] }) }),
|
|
134
225
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Tbody, { children: users.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Tr, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { colSpan: 5, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", padding: 4, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { textColor: "neutral600", children: formatMessage(index.getTrad("whitelist.table.empty")) }) }) }) }) : paginatedUsers.map((user, index$1) => {
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}).join(", ");
|
|
139
|
-
let userRolesNames = getRoleNames(user.roles || []);
|
|
140
|
-
if (!userRolesNames) {
|
|
141
|
-
const defaultRolesIds = oidcRoles.reduce((acc, oidc) => {
|
|
142
|
-
if (oidc.role) acc.push(...oidc.role);
|
|
143
|
-
return acc;
|
|
144
|
-
}, []);
|
|
145
|
-
userRolesNames = getRoleNames(defaultRolesIds);
|
|
146
|
-
}
|
|
226
|
+
const explicitRoleNames = getRoleNames(user.roles || []);
|
|
227
|
+
const isDefault = !explicitRoleNames && Boolean(defaultRoleNames);
|
|
228
|
+
const userRolesNames = explicitRoleNames || defaultRoleNames;
|
|
147
229
|
return /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Tr, { children: [
|
|
148
230
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: index$1 + 1 + (page - 1) * PAGE_SIZE }),
|
|
149
231
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: user.email }),
|
|
150
|
-
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: userRolesNames
|
|
232
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: userRolesNames ? /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { gap: 2, alignItems: "center", children: [
|
|
233
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: userRolesNames }),
|
|
234
|
+
isDefault && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral500", children: "(Default)" })
|
|
235
|
+
] }) : "-" }),
|
|
151
236
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { children: /* @__PURE__ */ jsxRuntime.jsx(LocalizedDate, { date: user.createdAt }) }),
|
|
152
237
|
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Td, { style: { paddingRight: 0 }, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
153
238
|
designSystem.Flex,
|
|
@@ -350,8 +435,11 @@ function CustomSwitch({ checked, onChange, label, disabled }) {
|
|
|
350
435
|
label && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", fontWeight: "bold", textColor: disabled ? "neutral500" : "neutral800", children: label })
|
|
351
436
|
] });
|
|
352
437
|
}
|
|
438
|
+
function deepClone(value) {
|
|
439
|
+
return JSON.parse(JSON.stringify(value));
|
|
440
|
+
}
|
|
353
441
|
function useOidcSettings() {
|
|
354
|
-
const { get, put } = admin.useFetchClient();
|
|
442
|
+
const { get, put, post } = admin.useFetchClient();
|
|
355
443
|
const [loading, setLoading] = react.useState(false);
|
|
356
444
|
const [showSuccess, setSuccess] = react.useState(false);
|
|
357
445
|
const [showError, setError] = react.useState(false);
|
|
@@ -369,14 +457,14 @@ function useOidcSettings() {
|
|
|
369
457
|
react.useEffect(() => {
|
|
370
458
|
get(`/strapi-plugin-oidc/oidc-roles`).then((response) => {
|
|
371
459
|
setOIDCRoles(response.data);
|
|
372
|
-
setInitialOIDCRoles(
|
|
460
|
+
setInitialOIDCRoles(deepClone(response.data));
|
|
373
461
|
});
|
|
374
462
|
get(`/admin/roles`).then((response) => {
|
|
375
463
|
setRoles(response.data.data);
|
|
376
464
|
});
|
|
377
465
|
get("/strapi-plugin-oidc/whitelist").then((response) => {
|
|
378
466
|
setUsers(response.data.whitelistUsers);
|
|
379
|
-
setInitialUsers(
|
|
467
|
+
setInitialUsers(deepClone(response.data.whitelistUsers));
|
|
380
468
|
setUseWhitelist(response.data.useWhitelist);
|
|
381
469
|
setInitialUseWhitelist(response.data.useWhitelist);
|
|
382
470
|
setEnforceOIDC(response.data.enforceOIDC);
|
|
@@ -390,17 +478,44 @@ function useOidcSettings() {
|
|
|
390
478
|
);
|
|
391
479
|
setOIDCRoles(updatedRoles);
|
|
392
480
|
};
|
|
393
|
-
const onRegisterWhitelist =
|
|
481
|
+
const onRegisterWhitelist = (email, selectedRoles) => {
|
|
394
482
|
const newUser = { email, roles: selectedRoles, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
395
483
|
setUsers([...users, newUser]);
|
|
396
484
|
};
|
|
397
|
-
const onDeleteWhitelist =
|
|
485
|
+
const onDeleteWhitelist = (email) => {
|
|
398
486
|
const updatedUsers = users.filter((u) => u.email !== email);
|
|
399
487
|
setUsers(updatedUsers);
|
|
400
488
|
if (useWhitelist && updatedUsers.length === 0) {
|
|
401
489
|
setEnforceOIDC(false);
|
|
402
490
|
}
|
|
403
491
|
};
|
|
492
|
+
const onDeleteAll = () => {
|
|
493
|
+
setUsers([]);
|
|
494
|
+
if (useWhitelist) setEnforceOIDC(false);
|
|
495
|
+
};
|
|
496
|
+
const onImport = async (entries) => {
|
|
497
|
+
const response = await post("/strapi-plugin-oidc/whitelist/import", { users: entries });
|
|
498
|
+
const refreshed = await get("/strapi-plugin-oidc/whitelist");
|
|
499
|
+
setUsers(refreshed.data.whitelistUsers);
|
|
500
|
+
setInitialUsers(deepClone(refreshed.data.whitelistUsers));
|
|
501
|
+
return response.data.importedCount;
|
|
502
|
+
};
|
|
503
|
+
const onExport = () => {
|
|
504
|
+
const roleMap = new Map(roles.map((r) => [String(r.id), r.name]));
|
|
505
|
+
const data = users.map(({ email, roles: userRoles }) => ({
|
|
506
|
+
email,
|
|
507
|
+
roles: (userRoles || []).map((id) => roleMap.get(String(id)) ?? id)
|
|
508
|
+
}));
|
|
509
|
+
const now = /* @__PURE__ */ new Date();
|
|
510
|
+
const datetime = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, "0")}${String(now.getDate()).padStart(2, "0")}_${String(now.getHours()).padStart(2, "0")}${String(now.getMinutes()).padStart(2, "0")}${String(now.getSeconds()).padStart(2, "0")}`;
|
|
511
|
+
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
|
|
512
|
+
const url = URL.createObjectURL(blob);
|
|
513
|
+
const a = document.createElement("a");
|
|
514
|
+
a.href = url;
|
|
515
|
+
a.download = `strapi-oidc-whitelist-${datetime}.json`;
|
|
516
|
+
a.click();
|
|
517
|
+
URL.revokeObjectURL(url);
|
|
518
|
+
};
|
|
404
519
|
const onToggleWhitelist = (e) => {
|
|
405
520
|
const checked = e.target.checked;
|
|
406
521
|
setUseWhitelist(checked);
|
|
@@ -428,12 +543,12 @@ function useOidcSettings() {
|
|
|
428
543
|
useWhitelist,
|
|
429
544
|
enforceOIDC
|
|
430
545
|
});
|
|
431
|
-
setInitialOIDCRoles(
|
|
546
|
+
setInitialOIDCRoles(deepClone(oidcRoles));
|
|
432
547
|
setInitialUseWhitelist(useWhitelist);
|
|
433
548
|
setInitialEnforceOIDC(enforceOIDC);
|
|
434
549
|
get("/strapi-plugin-oidc/whitelist").then((getResponse) => {
|
|
435
550
|
setUsers(getResponse.data.whitelistUsers);
|
|
436
|
-
setInitialUsers(
|
|
551
|
+
setInitialUsers(deepClone(getResponse.data.whitelistUsers));
|
|
437
552
|
});
|
|
438
553
|
if (syncResponse.data?.matchedExistingUsersCount > 0) {
|
|
439
554
|
setMatched(syncResponse.data.matchedExistingUsersCount);
|
|
@@ -472,6 +587,9 @@ function useOidcSettings() {
|
|
|
472
587
|
onChangeRole,
|
|
473
588
|
onRegisterWhitelist,
|
|
474
589
|
onDeleteWhitelist,
|
|
590
|
+
onDeleteAll,
|
|
591
|
+
onImport,
|
|
592
|
+
onExport,
|
|
475
593
|
onToggleWhitelist,
|
|
476
594
|
onToggleEnforce,
|
|
477
595
|
onSaveAll
|
|
@@ -481,6 +599,7 @@ function useOidcSettings() {
|
|
|
481
599
|
function HomePage$1() {
|
|
482
600
|
const { formatMessage } = reactIntl.useIntl();
|
|
483
601
|
const { state, actions } = useOidcSettings();
|
|
602
|
+
const blocker = reactRouterDom.useBlocker(state.isDirty);
|
|
484
603
|
return /* @__PURE__ */ jsxRuntime.jsxs(admin.Page.Protect, { permissions: [{ action: "plugin::strapi-plugin-oidc.read", subject: null }], children: [
|
|
485
604
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
486
605
|
admin.Layouts.Header,
|
|
@@ -525,7 +644,10 @@ function HomePage$1() {
|
|
|
525
644
|
oidcRoles: state.oidcRoles,
|
|
526
645
|
useWhitelist: state.useWhitelist,
|
|
527
646
|
onSave: actions.onRegisterWhitelist,
|
|
528
|
-
onDelete: actions.onDeleteWhitelist
|
|
647
|
+
onDelete: actions.onDeleteWhitelist,
|
|
648
|
+
onDeleteAll: actions.onDeleteAll,
|
|
649
|
+
onImport: actions.onImport,
|
|
650
|
+
onExport: actions.onExport
|
|
529
651
|
}
|
|
530
652
|
)
|
|
531
653
|
] }),
|
|
@@ -564,6 +686,14 @@ function HomePage$1() {
|
|
|
564
686
|
children: formatMessage(index.getTrad("page.save"))
|
|
565
687
|
}
|
|
566
688
|
) })
|
|
689
|
+
] }) }),
|
|
690
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Dialog.Root, { open: blocker.state === "blocked", children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Dialog.Content, { children: [
|
|
691
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Dialog.Header, { children: formatMessage(index.getTrad("unsaved.title")) }),
|
|
692
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Dialog.Body, { children: formatMessage(index.getTrad("unsaved.description")) }),
|
|
693
|
+
/* @__PURE__ */ jsxRuntime.jsxs(designSystem.Dialog.Footer, { children: [
|
|
694
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Dialog.Cancel, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "tertiary", onClick: () => blocker.reset?.(), children: formatMessage(index.getTrad("unsaved.cancel")) }) }),
|
|
695
|
+
/* @__PURE__ */ jsxRuntime.jsx(designSystem.Dialog.Action, { children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { variant: "danger", onClick: () => blocker.proceed?.(), children: formatMessage(index.getTrad("unsaved.confirm")) }) })
|
|
696
|
+
] })
|
|
567
697
|
] }) })
|
|
568
698
|
] });
|
|
569
699
|
}
|