strapi-plugin-oidc 1.3.1 → 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 CHANGED
@@ -11,80 +11,125 @@
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_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
- ## How to Login
54
+ ## Login
67
55
 
68
- Once configured, you can initiate the OIDC login flow by navigating to:
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
- (e.g., `http://localhost:1337/strapi-plugin-oidc/oidc` for local development).
58
+ ## Admin Settings
72
59
 
73
- When the **Enforce OIDC Login** option is enabled in the Admin Settings, the standard Strapi admin login page will be automatically redirected to this URL.
60
+ Manage the plugin under **Settings OIDC Plugin**.
74
61
 
75
- ## Admin Settings
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
- Once the plugin is installed and configured, you can manage the OIDC settings from the Strapi Admin Panel under **Settings** > **OIDC Plugin**.
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
- - **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, and login button are hidden on 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.
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 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!
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-CFmg9Kxl.mjs");
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, Field, Button, Divider, Thead, Tr, Th, Tbody, Td, Dialog, IconButton, Pagination, PreviousLink, PageLink, NextLink, Table, Alert } from "@strapi/design-system";
6
- import { Plus, Trash, WarningCircle, Information } from "@strapi/icons";
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-D1ypRUlq.mjs";
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-BqyGGX8X.js"));
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
  }