strapi-plugin-oidc 1.3.2 → 1.4.0
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 +93 -44
- package/dist/admin/{index-D1ypRUlq.mjs → index-8hB6LKml.mjs} +15 -2
- package/dist/admin/{index-CFmg9Kxl.mjs → index-BTTGSnuQ.mjs} +142 -9
- package/dist/admin/{index-Cse9ex24.js → index-CZDixCh4.js} +15 -2
- package/dist/admin/{index-BqyGGX8X.js → index-QZkv75Xp.js} +138 -5
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/server/index.js +169 -126
- package/dist/server/index.mjs +169 -126
- package/package.json +11 -4
package/README.md
CHANGED
|
@@ -11,80 +11,125 @@
|
|
|
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
|
-
|
|
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_USER_INFO_ENDPOINT_WITH_AUTH_HEADER: false, // true = Bearer header, false = query param
|
|
45
|
+
OIDC_LOGOUT_URL: '', // Provider logout URL; omit to redirect to Strapi login
|
|
46
|
+
OIDC_SSO_BUTTON_TEXT: 'Login via SSO',
|
|
47
|
+
OIDC_ENFORCE: null, // null = use Admin UI toggle; true/false = override in config
|
|
48
|
+
REMEMBER_ME: false, // Persist session across browser restarts
|
|
60
49
|
},
|
|
61
50
|
},
|
|
62
|
-
// ...
|
|
63
51
|
});
|
|
64
52
|
```
|
|
65
53
|
|
|
66
|
-
##
|
|
54
|
+
## Login
|
|
67
55
|
|
|
68
|
-
|
|
69
|
-
`http://<your-strapi-domain>/strapi-plugin-oidc/oidc`
|
|
56
|
+
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
57
|
|
|
71
|
-
|
|
58
|
+
## Admin Settings
|
|
72
59
|
|
|
73
|
-
|
|
60
|
+
Manage the plugin under **Settings → OIDC Plugin**.
|
|
74
61
|
|
|
75
|
-
|
|
62
|
+
**Default Roles** — Select which Strapi admin role(s) are assigned to new users on first login.
|
|
63
|
+
|
|
64
|
+
**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:
|
|
65
|
+
|
|
66
|
+
- Adding individual emails with optional role overrides
|
|
67
|
+
- JSON import / export (see [format](#import-format) below)
|
|
68
|
+
- Bulk delete with confirmation
|
|
69
|
+
- Unsaved changes are held in the UI until **Save Changes** is clicked
|
|
70
|
+
|
|
71
|
+
**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.
|
|
72
|
+
|
|
73
|
+
- The toggle is grayed out and locked when `OIDC_ENFORCE` is set in config.
|
|
74
|
+
- **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.
|
|
75
|
+
|
|
76
|
+
## Whitelist API
|
|
76
77
|
|
|
77
|
-
|
|
78
|
+
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
79
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
80
|
+
| Method | Path | Description |
|
|
81
|
+
| -------- | ------------------------------------------ | ---------------------- |
|
|
82
|
+
| `GET` | `/api/strapi-plugin-oidc/whitelist` | List all entries |
|
|
83
|
+
| `POST` | `/api/strapi-plugin-oidc/whitelist` | Add one or more emails |
|
|
84
|
+
| `POST` | `/api/strapi-plugin-oidc/whitelist/import` | Bulk import |
|
|
85
|
+
| `DELETE` | `/api/strapi-plugin-oidc/whitelist/:id` | Remove by ID |
|
|
86
|
+
| `DELETE` | `/api/strapi-plugin-oidc/whitelist` | Remove all entries |
|
|
87
|
+
|
|
88
|
+
API calls write directly to the database — there is no unsaved state.
|
|
89
|
+
|
|
90
|
+
### Import format
|
|
91
|
+
|
|
92
|
+
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.
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
[
|
|
96
|
+
{ "email": "alice@example.com", "roles": ["Editor"] },
|
|
97
|
+
{ "email": "bob@example.com", "roles": ["Editor", "Author"] },
|
|
98
|
+
{ "email": "carol@example.com" }
|
|
99
|
+
]
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Duplicate emails within the payload and emails already in the whitelist are silently skipped.
|
|
103
|
+
|
|
104
|
+
### Examples
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
# List
|
|
108
|
+
curl -H "Authorization: Bearer <token>" \
|
|
109
|
+
http://localhost:1337/api/strapi-plugin-oidc/whitelist
|
|
110
|
+
|
|
111
|
+
# Add
|
|
112
|
+
curl -X POST -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
|
|
113
|
+
-d '{"email": "user@example.com", "roles": ["Editor"]}' \
|
|
114
|
+
http://localhost:1337/api/strapi-plugin-oidc/whitelist
|
|
115
|
+
|
|
116
|
+
# Bulk import
|
|
117
|
+
curl -X POST -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
|
|
118
|
+
-d '{"users": [{"email": "a@example.com", "roles": ["Editor"]}, {"email": "b@example.com"}]}' \
|
|
119
|
+
http://localhost:1337/api/strapi-plugin-oidc/whitelist/import
|
|
120
|
+
|
|
121
|
+
# Delete one
|
|
122
|
+
curl -X DELETE -H "Authorization: Bearer <token>" \
|
|
123
|
+
http://localhost:1337/api/strapi-plugin-oidc/whitelist/42
|
|
124
|
+
|
|
125
|
+
# Delete all
|
|
126
|
+
curl -X DELETE -H "Authorization: Bearer <token>" \
|
|
127
|
+
http://localhost:1337/api/strapi-plugin-oidc/whitelist
|
|
128
|
+
```
|
|
84
129
|
|
|
85
130
|
## Credits & Changes
|
|
86
131
|
|
|
87
|
-
This plugin is a hard fork of
|
|
132
|
+
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
133
|
|
|
89
134
|
### Changes made to the original codebase:
|
|
90
135
|
|
|
@@ -92,10 +137,14 @@ This plugin is a hard fork of the original [`strapi-plugin-sso`](https://github.
|
|
|
92
137
|
- Redesigned the Whitelist and Role management UI (switched to native Strapi cards, added pagination, etc.).
|
|
93
138
|
- Added an OIDC logout redirect URL.
|
|
94
139
|
- 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
140
|
- Migrated the testing framework to Vitest and added comprehensive test coverage for controllers and services.
|
|
97
141
|
- Cleaned up dead code and unused dependencies to improve maintainability.
|
|
98
142
|
- Upgraded to use newer versions of Node.js.
|
|
99
143
|
- Added styled success and error pages.
|
|
100
144
|
- 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.
|
|
145
|
+
- Whitelist improvements:
|
|
146
|
+
- JSON import and export (uses human-readable role names).
|
|
147
|
+
- Bulk delete all entries with a confirmation dialog.
|
|
148
|
+
- Unsaved changes confirmation when navigating away from the settings page.
|
|
149
|
+
- Programmatic API for managing the whitelist via Strapi API tokens (list, register, import, delete, delete all).
|
|
101
150
|
- Added misc. quality of life improvements and bug fixes.
|
|
@@ -78,7 +78,20 @@ const en = {
|
|
|
78
78
|
"enforce.warning": "Make sure OIDC is setup correctly before saving changes, you won't be able to login normally.",
|
|
79
79
|
"enforce.config.info": "Enforcement is controlled by the OIDC_ENFORCE config variable and cannot be changed here.",
|
|
80
80
|
"login.settings.title": "Login Settings",
|
|
81
|
-
"login.sso": "Login via SSO"
|
|
81
|
+
"login.sso": "Login via SSO",
|
|
82
|
+
"whitelist.count": "{count, plural, one {# entry} other {# entries}}",
|
|
83
|
+
"whitelist.import": "Import",
|
|
84
|
+
"whitelist.export": "Export",
|
|
85
|
+
"whitelist.delete.all.label": "Delete All",
|
|
86
|
+
"whitelist.delete.all.title": "Delete All Entries",
|
|
87
|
+
"whitelist.delete.all.description": "This will permanently remove all {count, plural, one {# entry} other {# entries}} from the whitelist. Unsaved changes will be lost.",
|
|
88
|
+
"whitelist.import.error": "Invalid file — expected a JSON array of objects with an email field.",
|
|
89
|
+
"whitelist.import.success": "Imported {count, plural, one {# new entry} other {# new entries}}.",
|
|
90
|
+
"whitelist.import.none": "No new entries — all emails are already in the whitelist.",
|
|
91
|
+
"unsaved.title": "Unsaved Changes",
|
|
92
|
+
"unsaved.description": "You have unsaved changes that will be lost if you leave. Do you want to continue?",
|
|
93
|
+
"unsaved.confirm": "Leave",
|
|
94
|
+
"unsaved.cancel": "Stay"
|
|
82
95
|
};
|
|
83
96
|
function getTrad(id) {
|
|
84
97
|
const pluginIdWithId = `${pluginId}.${id}`;
|
|
@@ -109,7 +122,7 @@ const index = {
|
|
|
109
122
|
defaultMessage: "Configuration"
|
|
110
123
|
},
|
|
111
124
|
Component: async () => {
|
|
112
|
-
return await import("./index-
|
|
125
|
+
return await import("./index-BTTGSnuQ.mjs");
|
|
113
126
|
},
|
|
114
127
|
permissions: [{ action: "plugin::strapi-plugin-oidc.read", subject: null }]
|
|
115
128
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { jsxs, Fragment, jsx } from "react/jsx-runtime";
|
|
2
|
-
import { Routes, Route } from "react-router-dom";
|
|
2
|
+
import { useBlocker, Routes, Route } from "react-router-dom";
|
|
3
3
|
import { useNotification, useFetchClient, Page, Layouts } from "@strapi/strapi/admin";
|
|
4
|
-
import { useState, useCallback, useEffect, memo } from "react";
|
|
5
|
-
import { Typography, Flex, Box, MultiSelect, MultiSelectOption,
|
|
6
|
-
import {
|
|
4
|
+
import { useState, useRef, useCallback, useEffect, memo } from "react";
|
|
5
|
+
import { Typography, Flex, Box, MultiSelect, MultiSelectOption, Button, Dialog, Field, Divider, Thead, Tr, Th, Tbody, Td, IconButton, Pagination, PreviousLink, PageLink, NextLink, Table, Alert } from "@strapi/design-system";
|
|
6
|
+
import { Download, Upload, Trash, WarningCircle, Plus, Information } from "@strapi/icons";
|
|
7
7
|
import { useIntl } from "react-intl";
|
|
8
|
-
import { g as getTrad } from "./index-
|
|
8
|
+
import { g as getTrad } from "./index-8hB6LKml.mjs";
|
|
9
9
|
import styled from "styled-components";
|
|
10
10
|
function Role({ oidcRoles, roles, onChangeRole }) {
|
|
11
11
|
const { formatMessage } = useIntl();
|
|
@@ -52,13 +52,17 @@ function Whitelist({
|
|
|
52
52
|
useWhitelist,
|
|
53
53
|
loading,
|
|
54
54
|
onSave,
|
|
55
|
-
onDelete
|
|
55
|
+
onDelete,
|
|
56
|
+
onDeleteAll,
|
|
57
|
+
onImport,
|
|
58
|
+
onExport
|
|
56
59
|
}) {
|
|
57
60
|
const [email, setEmail] = useState("");
|
|
58
61
|
const [selectedRoles, setSelectedRoles] = useState([]);
|
|
59
62
|
const [page, setPage] = useState(1);
|
|
60
63
|
const { formatMessage } = useIntl();
|
|
61
64
|
const { toggleNotification } = useNotification();
|
|
65
|
+
const fileInputRef = useRef(null);
|
|
62
66
|
const PAGE_SIZE = 10;
|
|
63
67
|
const pageCount = Math.ceil(users.length / PAGE_SIZE) || 1;
|
|
64
68
|
const paginatedUsers = users.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
|
|
@@ -79,9 +83,93 @@ function Whitelist({
|
|
|
79
83
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
80
84
|
return emailRegex.test(email);
|
|
81
85
|
}, [email]);
|
|
86
|
+
const handleImport = useCallback(
|
|
87
|
+
async (e) => {
|
|
88
|
+
const file = e.target.files?.[0];
|
|
89
|
+
if (!fileInputRef.current) return;
|
|
90
|
+
fileInputRef.current.value = "";
|
|
91
|
+
if (!file) return;
|
|
92
|
+
try {
|
|
93
|
+
const text = await file.text();
|
|
94
|
+
const parsed = JSON.parse(text);
|
|
95
|
+
if (!Array.isArray(parsed)) throw new Error();
|
|
96
|
+
const entries = parsed.filter((item) => item?.email).map((item) => ({
|
|
97
|
+
email: String(item.email),
|
|
98
|
+
roles: Array.isArray(item.roles) ? item.roles : []
|
|
99
|
+
}));
|
|
100
|
+
const count = await onImport(entries);
|
|
101
|
+
if (count === 0) {
|
|
102
|
+
toggleNotification({
|
|
103
|
+
type: "info",
|
|
104
|
+
message: formatMessage(getTrad("whitelist.import.none"))
|
|
105
|
+
});
|
|
106
|
+
} else {
|
|
107
|
+
toggleNotification({
|
|
108
|
+
type: "success",
|
|
109
|
+
message: formatMessage(getTrad("whitelist.import.success"), { count })
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
} catch {
|
|
113
|
+
toggleNotification({
|
|
114
|
+
type: "warning",
|
|
115
|
+
message: formatMessage(getTrad("whitelist.import.error"))
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
[onImport, formatMessage, toggleNotification]
|
|
120
|
+
);
|
|
82
121
|
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
83
122
|
/* @__PURE__ */ jsx(Typography, { tag: "p", variant: "omega", textColor: "neutral600", marginBottom: 4, children: formatMessage(getTrad("whitelist.description")) }),
|
|
84
123
|
useWhitelist && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
124
|
+
/* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", marginBottom: 4, children: [
|
|
125
|
+
/* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", children: formatMessage(getTrad("whitelist.count"), { count: users.length }) }),
|
|
126
|
+
/* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
|
|
127
|
+
/* @__PURE__ */ jsx(
|
|
128
|
+
Button,
|
|
129
|
+
{
|
|
130
|
+
size: "S",
|
|
131
|
+
variant: "tertiary",
|
|
132
|
+
startIcon: /* @__PURE__ */ jsx(Download, {}),
|
|
133
|
+
onClick: onExport,
|
|
134
|
+
disabled: users.length === 0,
|
|
135
|
+
children: formatMessage(getTrad("whitelist.export"))
|
|
136
|
+
}
|
|
137
|
+
),
|
|
138
|
+
/* @__PURE__ */ jsx(
|
|
139
|
+
Button,
|
|
140
|
+
{
|
|
141
|
+
size: "S",
|
|
142
|
+
variant: "tertiary",
|
|
143
|
+
startIcon: /* @__PURE__ */ jsx(Upload, {}),
|
|
144
|
+
onClick: () => fileInputRef.current?.click(),
|
|
145
|
+
children: formatMessage(getTrad("whitelist.import"))
|
|
146
|
+
}
|
|
147
|
+
),
|
|
148
|
+
/* @__PURE__ */ jsx(
|
|
149
|
+
"input",
|
|
150
|
+
{
|
|
151
|
+
ref: fileInputRef,
|
|
152
|
+
type: "file",
|
|
153
|
+
accept: ".json,application/json",
|
|
154
|
+
style: { display: "none" },
|
|
155
|
+
onChange: handleImport
|
|
156
|
+
}
|
|
157
|
+
),
|
|
158
|
+
users.length > 0 && /* @__PURE__ */ jsxs(Dialog.Root, { children: [
|
|
159
|
+
/* @__PURE__ */ jsx(Dialog.Trigger, { children: /* @__PURE__ */ jsx(Button, { size: "S", variant: "danger-light", startIcon: /* @__PURE__ */ jsx(Trash, {}), children: formatMessage(getTrad("whitelist.delete.all.label")) }) }),
|
|
160
|
+
/* @__PURE__ */ jsxs(Dialog.Content, { children: [
|
|
161
|
+
/* @__PURE__ */ jsx(Dialog.Header, { children: formatMessage(getTrad("whitelist.delete.all.title")) }),
|
|
162
|
+
/* @__PURE__ */ jsx(Dialog.Body, { icon: /* @__PURE__ */ jsx(WarningCircle, { fill: "danger600" }), children: /* @__PURE__ */ jsx(Flex, { justifyContent: "center", children: /* @__PURE__ */ jsx(Typography, { textColor: "neutral800", textAlign: "center", children: formatMessage(getTrad("whitelist.delete.all.description"), {
|
|
163
|
+
count: users.length
|
|
164
|
+
}) }) }) }),
|
|
165
|
+
/* @__PURE__ */ jsxs(Dialog.Footer, { children: [
|
|
166
|
+
/* @__PURE__ */ jsx(Dialog.Cancel, { children: /* @__PURE__ */ jsx(Button, { fullWidth: true, variant: "tertiary", children: formatMessage(getTrad("page.cancel")) }) }),
|
|
167
|
+
/* @__PURE__ */ jsx(Dialog.Action, { children: /* @__PURE__ */ jsx(Button, { fullWidth: true, variant: "danger", onClick: onDeleteAll, children: formatMessage(getTrad("whitelist.delete.all.label")) }) })
|
|
168
|
+
] })
|
|
169
|
+
] })
|
|
170
|
+
] })
|
|
171
|
+
] })
|
|
172
|
+
] }),
|
|
85
173
|
/* @__PURE__ */ jsxs(Flex, { gap: 4, marginTop: 5, marginBottom: 5, alignItems: "flex-start", children: [
|
|
86
174
|
/* @__PURE__ */ jsx(Box, { style: { flex: 1 }, children: /* @__PURE__ */ jsx(Field.Root, { children: /* @__PURE__ */ jsx(
|
|
87
175
|
Field.Input,
|
|
@@ -133,17 +221,22 @@ function Whitelist({
|
|
|
133
221
|
return r ? r.name : roleId;
|
|
134
222
|
}).join(", ");
|
|
135
223
|
let userRolesNames = getRoleNames(user.roles || []);
|
|
224
|
+
let isDefault = false;
|
|
136
225
|
if (!userRolesNames) {
|
|
137
226
|
const defaultRolesIds = oidcRoles.reduce((acc, oidc) => {
|
|
138
227
|
if (oidc.role) acc.push(...oidc.role);
|
|
139
228
|
return acc;
|
|
140
229
|
}, []);
|
|
141
230
|
userRolesNames = getRoleNames(defaultRolesIds);
|
|
231
|
+
isDefault = Boolean(userRolesNames);
|
|
142
232
|
}
|
|
143
233
|
return /* @__PURE__ */ jsxs(Tr, { children: [
|
|
144
234
|
/* @__PURE__ */ jsx(Td, { children: index + 1 + (page - 1) * PAGE_SIZE }),
|
|
145
235
|
/* @__PURE__ */ jsx(Td, { children: user.email }),
|
|
146
|
-
/* @__PURE__ */ jsx(Td, { children: userRolesNames
|
|
236
|
+
/* @__PURE__ */ jsx(Td, { children: userRolesNames ? /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
|
|
237
|
+
/* @__PURE__ */ jsx("span", { children: userRolesNames }),
|
|
238
|
+
isDefault && /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral500", children: "(Default)" })
|
|
239
|
+
] }) : "-" }),
|
|
147
240
|
/* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(LocalizedDate, { date: user.createdAt }) }),
|
|
148
241
|
/* @__PURE__ */ jsx(Td, { style: { paddingRight: 0 }, children: /* @__PURE__ */ jsx(
|
|
149
242
|
Flex,
|
|
@@ -347,7 +440,7 @@ function CustomSwitch({ checked, onChange, label, disabled }) {
|
|
|
347
440
|
] });
|
|
348
441
|
}
|
|
349
442
|
function useOidcSettings() {
|
|
350
|
-
const { get, put } = useFetchClient();
|
|
443
|
+
const { get, put, post } = useFetchClient();
|
|
351
444
|
const [loading, setLoading] = useState(false);
|
|
352
445
|
const [showSuccess, setSuccess] = useState(false);
|
|
353
446
|
const [showError, setError] = useState(false);
|
|
@@ -397,6 +490,31 @@ function useOidcSettings() {
|
|
|
397
490
|
setEnforceOIDC(false);
|
|
398
491
|
}
|
|
399
492
|
};
|
|
493
|
+
const onDeleteAll = () => {
|
|
494
|
+
setUsers([]);
|
|
495
|
+
if (useWhitelist) setEnforceOIDC(false);
|
|
496
|
+
};
|
|
497
|
+
const onImport = async (entries) => {
|
|
498
|
+
const response = await post("/strapi-plugin-oidc/whitelist/import", { users: entries });
|
|
499
|
+
const refreshed = await get("/strapi-plugin-oidc/whitelist");
|
|
500
|
+
setUsers(refreshed.data.whitelistUsers);
|
|
501
|
+
setInitialUsers(JSON.parse(JSON.stringify(refreshed.data.whitelistUsers)));
|
|
502
|
+
return response.data.importedCount;
|
|
503
|
+
};
|
|
504
|
+
const onExport = () => {
|
|
505
|
+
const roleMap = new Map(roles.map((r) => [String(r.id), r.name]));
|
|
506
|
+
const data = users.map(({ email, roles: userRoles }) => ({
|
|
507
|
+
email,
|
|
508
|
+
roles: (userRoles || []).map((id) => roleMap.get(String(id)) ?? id)
|
|
509
|
+
}));
|
|
510
|
+
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
|
|
511
|
+
const url = URL.createObjectURL(blob);
|
|
512
|
+
const a = document.createElement("a");
|
|
513
|
+
a.href = url;
|
|
514
|
+
a.download = "whitelist.json";
|
|
515
|
+
a.click();
|
|
516
|
+
URL.revokeObjectURL(url);
|
|
517
|
+
};
|
|
400
518
|
const onToggleWhitelist = (e) => {
|
|
401
519
|
const checked = e.target.checked;
|
|
402
520
|
setUseWhitelist(checked);
|
|
@@ -468,6 +586,9 @@ function useOidcSettings() {
|
|
|
468
586
|
onChangeRole,
|
|
469
587
|
onRegisterWhitelist,
|
|
470
588
|
onDeleteWhitelist,
|
|
589
|
+
onDeleteAll,
|
|
590
|
+
onImport,
|
|
591
|
+
onExport,
|
|
471
592
|
onToggleWhitelist,
|
|
472
593
|
onToggleEnforce,
|
|
473
594
|
onSaveAll
|
|
@@ -477,6 +598,7 @@ function useOidcSettings() {
|
|
|
477
598
|
function HomePage() {
|
|
478
599
|
const { formatMessage } = useIntl();
|
|
479
600
|
const { state, actions } = useOidcSettings();
|
|
601
|
+
const blocker = useBlocker(state.isDirty);
|
|
480
602
|
return /* @__PURE__ */ jsxs(Page.Protect, { permissions: [{ action: "plugin::strapi-plugin-oidc.read", subject: null }], children: [
|
|
481
603
|
/* @__PURE__ */ jsx(
|
|
482
604
|
Layouts.Header,
|
|
@@ -521,7 +643,10 @@ function HomePage() {
|
|
|
521
643
|
oidcRoles: state.oidcRoles,
|
|
522
644
|
useWhitelist: state.useWhitelist,
|
|
523
645
|
onSave: actions.onRegisterWhitelist,
|
|
524
|
-
onDelete: actions.onDeleteWhitelist
|
|
646
|
+
onDelete: actions.onDeleteWhitelist,
|
|
647
|
+
onDeleteAll: actions.onDeleteAll,
|
|
648
|
+
onImport: actions.onImport,
|
|
649
|
+
onExport: actions.onExport
|
|
525
650
|
}
|
|
526
651
|
)
|
|
527
652
|
] }),
|
|
@@ -560,6 +685,14 @@ function HomePage() {
|
|
|
560
685
|
children: formatMessage(getTrad("page.save"))
|
|
561
686
|
}
|
|
562
687
|
) })
|
|
688
|
+
] }) }),
|
|
689
|
+
/* @__PURE__ */ jsx(Dialog.Root, { open: blocker.state === "blocked", children: /* @__PURE__ */ jsxs(Dialog.Content, { children: [
|
|
690
|
+
/* @__PURE__ */ jsx(Dialog.Header, { children: formatMessage(getTrad("unsaved.title")) }),
|
|
691
|
+
/* @__PURE__ */ jsx(Dialog.Body, { children: formatMessage(getTrad("unsaved.description")) }),
|
|
692
|
+
/* @__PURE__ */ jsxs(Dialog.Footer, { children: [
|
|
693
|
+
/* @__PURE__ */ jsx(Dialog.Cancel, { children: /* @__PURE__ */ jsx(Button, { variant: "tertiary", onClick: () => blocker.reset?.(), children: formatMessage(getTrad("unsaved.cancel")) }) }),
|
|
694
|
+
/* @__PURE__ */ jsx(Dialog.Action, { children: /* @__PURE__ */ jsx(Button, { variant: "danger", onClick: () => blocker.proceed?.(), children: formatMessage(getTrad("unsaved.confirm")) }) })
|
|
695
|
+
] })
|
|
563
696
|
] }) })
|
|
564
697
|
] });
|
|
565
698
|
}
|
|
@@ -79,7 +79,20 @@ const en = {
|
|
|
79
79
|
"enforce.warning": "Make sure OIDC is setup correctly before saving changes, you won't be able to login normally.",
|
|
80
80
|
"enforce.config.info": "Enforcement is controlled by the OIDC_ENFORCE config variable and cannot be changed here.",
|
|
81
81
|
"login.settings.title": "Login Settings",
|
|
82
|
-
"login.sso": "Login via SSO"
|
|
82
|
+
"login.sso": "Login via SSO",
|
|
83
|
+
"whitelist.count": "{count, plural, one {# entry} other {# entries}}",
|
|
84
|
+
"whitelist.import": "Import",
|
|
85
|
+
"whitelist.export": "Export",
|
|
86
|
+
"whitelist.delete.all.label": "Delete All",
|
|
87
|
+
"whitelist.delete.all.title": "Delete All Entries",
|
|
88
|
+
"whitelist.delete.all.description": "This will permanently remove all {count, plural, one {# entry} other {# entries}} from the whitelist. Unsaved changes will be lost.",
|
|
89
|
+
"whitelist.import.error": "Invalid file — expected a JSON array of objects with an email field.",
|
|
90
|
+
"whitelist.import.success": "Imported {count, plural, one {# new entry} other {# new entries}}.",
|
|
91
|
+
"whitelist.import.none": "No new entries — all emails are already in the whitelist.",
|
|
92
|
+
"unsaved.title": "Unsaved Changes",
|
|
93
|
+
"unsaved.description": "You have unsaved changes that will be lost if you leave. Do you want to continue?",
|
|
94
|
+
"unsaved.confirm": "Leave",
|
|
95
|
+
"unsaved.cancel": "Stay"
|
|
83
96
|
};
|
|
84
97
|
function getTrad(id) {
|
|
85
98
|
const pluginIdWithId = `${pluginId}.${id}`;
|
|
@@ -110,7 +123,7 @@ const index = {
|
|
|
110
123
|
defaultMessage: "Configuration"
|
|
111
124
|
},
|
|
112
125
|
Component: async () => {
|
|
113
|
-
return await Promise.resolve().then(() => require("./index-
|
|
126
|
+
return await Promise.resolve().then(() => require("./index-QZkv75Xp.js"));
|
|
114
127
|
},
|
|
115
128
|
permissions: [{ action: "plugin::strapi-plugin-oidc.read", subject: null }]
|
|
116
129
|
}
|