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 CHANGED
@@ -11,80 +11,124 @@
11
11
  </p>
12
12
  </div>
13
13
 
14
- A Strapi plugin that provides OpenID Connect (OIDC) authentication functionality for the Strapi Admin Panel.
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
- To enable and configure the plugin, update your `config/plugins.js` (or `config/plugins.ts`) file with your OIDC provider's settings.
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
- // --- Required ---
41
- OIDC_CLIENT_ID: '[Client ID from OpenID Provider]',
42
- OIDC_CLIENT_SECRET: '[Client Secret from OpenID Provider]',
43
- OIDC_REDIRECT_URI: '[Your Strapi URL]/strapi-plugin-oidc/oidc/callback',
44
- OIDC_AUTHORIZATION_ENDPOINT: '[Authorization Endpoint]',
45
- OIDC_TOKEN_ENDPOINT: '[Token Endpoint]',
46
- OIDC_USER_INFO_ENDPOINT: '[User Info Endpoint]',
47
-
48
- // --- Defaults provided only set if your provider differs ---
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
+ // Optionaldefaults shown
49
40
  OIDC_SCOPES: 'openid profile email',
50
41
  OIDC_GRANT_TYPE: 'authorization_code',
51
- OIDC_FAMILY_NAME_FIELD: 'family_name', // OIDC claim for the user's surname
52
- OIDC_GIVEN_NAME_FIELD: 'given_name', // OIDC claim for the user's first name
53
-
54
- // --- Optional ---
55
- OIDC_USER_INFO_ENDPOINT_WITH_AUTH_HEADER: false, // true = Bearer token header, false = query param
56
- OIDC_LOGOUT_URL: '', // OIDC provider logout URL; omit to return to Strapi login instead
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
- ## How to Login
53
+ ## Login
67
54
 
68
- Once configured, you can initiate the OIDC login flow by navigating to:
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
- (e.g., `http://localhost:1337/strapi-plugin-oidc/oidc` for local development).
57
+ ## Admin Settings
72
58
 
73
- When the **Enforce OIDC Login** option is enabled in the Admin Settings, the standard login fields are removed from the login page and only the SSO button remains — click it to start the OIDC flow.
59
+ Manage the plugin under **Settings OIDC Plugin**.
74
60
 
75
- ## Admin Settings
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
- Once the plugin is installed and configured, you can manage the OIDC settings from the Strapi Admin Panel under **Settings** > **OIDC Plugin**.
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
- - **Whitelist Management**: Restrict login to specific users by adding their email addresses to the whitelist. You can also whitelist entire email domains (e.g., `*@company.com`). If the whitelist is empty, any user who successfully authenticates via your OIDC provider will be able to log in and an account will be automatically created for them.
80
- - **Default Role Assignment**: Select the default Strapi admin role that will be assigned to newly created users when they log in for the first time via OIDC.
81
- - **SSO Login Button**: A "Login via SSO" button is always injected into the Strapi login page, allowing users to authenticate via OIDC. The button text is configurable via the `OIDC_SSO_BUTTON_TEXT` config option.
82
- - **Enforce OIDC Login**: When enabled, the standard email/password fields, remember me checkbox, login button, and forgot-password link are removed from the login page, leaving only the SSO button. All direct login API calls are also blocked server-side. _(Note: This option is automatically disabled and grayed out if your whitelist is empty to prevent accidentally locking everyone out of the admin panel)._
83
- - **`OIDC_ENFORCE` config override**: Setting `OIDC_ENFORCE: true` or `OIDC_ENFORCE: false` in your plugin config takes priority over the Admin UI toggle and locks it. Set `OIDC_ENFORCE: false` in your config to regain access if you are ever locked out, then restart Strapi.
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 the original [`strapi-plugin-sso`](https://github.com/yasudacloud/strapi-plugin-sso) created by **yasudacloud**. Huge thanks to them for creating the foundation of this plugin!
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-Cse9ex24.js");
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
- const LocalizedDate = ({ date }) => {
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 onSaveEmail = react.useCallback(async () => {
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
- await onSave(emailText, selectedRoles);
88
+ onSave(emailText, selectedRoles);
78
89
  setEmail("");
79
90
  setSelectedRoles([]);
80
91
  }
81
92
  }, [email, selectedRoles, users, onSave, formatMessage, toggleNotification]);
82
- const isValidEmail = react.useCallback(() => {
83
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
84
- return emailRegex.test(email);
85
- }, [email]);
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 getRoleNames = (roleIds) => roleIds.map((roleId) => {
136
- const r = roles.find((ro) => String(ro.id) === String(roleId));
137
- return r ? r.name : roleId;
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(JSON.parse(JSON.stringify(response.data)));
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(JSON.parse(JSON.stringify(response.data.whitelistUsers)));
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 = async (email, selectedRoles) => {
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 = async (email) => {
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(JSON.parse(JSON.stringify(oidcRoles)));
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(JSON.parse(JSON.stringify(getResponse.data.whitelistUsers)));
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
  }