strapi-plugin-oidc 1.6.5 → 1.6.6

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
@@ -79,7 +79,7 @@ Manage the plugin under **Settings → OIDC Plugin**.
79
79
  - Bulk delete with confirmation
80
80
  - Unsaved changes are held in the UI until **Save Changes** is clicked
81
81
 
82
- **Audit Logs** — Every authentication event is recorded in the plugin's audit log table and visible in the **Audit Logs** section at the bottom of the settings page. A **Download** button exports all records as JSON, compatible with SIEM tools and log processors. Setting `AUDIT_LOG_RETENTION_DAYS` to `0` disables audit logging entirely. Otherwise records older than the configured value (default: 90 days) are automatically purged by a daily cron job. The audit log is also accessible [via API](#audit-log-api).
82
+ **Audit Logs** — Every authentication event is recorded in the plugin's audit log table and visible in the **Audit Logs** section at the bottom of the settings page. A **Download** button exports all records as NDJSON (newline-delimited JSON), compatible with SIEM tools and log processors. Setting `AUDIT_LOG_RETENTION_DAYS` to `0` disables audit logging entirely. Otherwise records older than the configured value (default: 90 days) are automatically purged by a daily cron job. The audit log is also accessible [via API](#audit-log-api).
83
83
 
84
84
  **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.
85
85
 
@@ -187,10 +187,10 @@ curl -X DELETE -H "Authorization: Bearer <token>" \
187
187
 
188
188
  Audit log entries can be fetched programmatically using a Strapi **API token** (Settings → API Tokens → Full Access). Endpoints are under `/api/strapi-plugin-oidc` and require `Authorization: Bearer <token>`.
189
189
 
190
- | Method | Path | Description |
191
- | ------ | ------------------------------------------- | ----------------------------- |
192
- | `GET` | `/api/strapi-plugin-oidc/audit-logs` | Paginated list of log entries |
193
- | `GET` | `/api/strapi-plugin-oidc/audit-logs/export` | All records as JSON download |
190
+ | Method | Path | Description |
191
+ | ------ | ------------------------------------------- | ------------------------------ |
192
+ | `GET` | `/api/strapi-plugin-oidc/audit-logs` | Paginated list of log entries |
193
+ | `GET` | `/api/strapi-plugin-oidc/audit-logs/export` | All records as NDJSON download |
194
194
 
195
195
  ### Query parameters (`GET /audit-logs`)
196
196
 
@@ -238,10 +238,10 @@ Each event is also emitted on Strapi's internal eventHub as `strapi-plugin-oidc:
238
238
  curl -H "Authorization: Bearer <token>" \
239
239
  "http://localhost:1337/api/strapi-plugin-oidc/audit-logs?page=1&pageSize=50"
240
240
 
241
- # JSON export
241
+ # NDJSON export
242
242
  curl -H "Authorization: Bearer <token>" \
243
243
  http://localhost:1337/api/strapi-plugin-oidc/audit-logs/export \
244
- -o oidc-audit-log.json
244
+ -o oidc-audit-log.ndjson
245
245
  ```
246
246
 
247
247
  ## Credits & Changes
@@ -1,11 +1,11 @@
1
1
  import { jsxs, Fragment, jsx } from "react/jsx-runtime";
2
2
  import { useBlocker, Routes, Route } from "react-router-dom";
3
3
  import { useNotification, useFetchClient, Page, Layouts } from "@strapi/strapi/admin";
4
- import { useState, useRef, useCallback, useEffect, memo } from "react";
4
+ import { useState, useRef, useCallback, useEffect, useReducer, useMemo, memo } from "react";
5
5
  import { Typography, Flex, Box, MultiSelect, MultiSelectOption, Dialog, Button, Table, Pagination, PreviousLink, NextLink, PageLink, Field, Divider, Thead, Tr, Th, Tbody, Td, IconButton, Tooltip, Alert } from "@strapi/design-system";
6
6
  import { WarningCircle, Download, Upload, Trash, Plus, Information } from "@strapi/icons";
7
7
  import { useIntl } from "react-intl";
8
- import { g as getTrad } from "./index-DgUClS5s.mjs";
8
+ import { g as getTrad } from "./index-D2aMSVmR.mjs";
9
9
  import styled from "styled-components";
10
10
  function Role({ oidcRoles, roles, onChangeRole }) {
11
11
  const { formatMessage } = useIntl();
@@ -367,8 +367,7 @@ function AuditLog() {
367
367
  });
368
368
  return;
369
369
  }
370
- const text = await response.text();
371
- const blob = new Blob([text], { type: "application/x-ndjson" });
370
+ const blob = await response.blob();
372
371
  const url = URL.createObjectURL(blob);
373
372
  const a = document.createElement("a");
374
373
  a.href = url;
@@ -571,75 +570,201 @@ function formatDatetimeForFilename(date) {
571
570
  const seconds = String(date.getSeconds()).padStart(2, "0");
572
571
  return `${year}${month}${day}_${hours}${minutes}${seconds}`;
573
572
  }
574
- function deepClone(value) {
575
- return JSON.parse(JSON.stringify(value));
573
+ function downloadJson(basename, data) {
574
+ const datetime = formatDatetimeForFilename(/* @__PURE__ */ new Date());
575
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
576
+ const url = URL.createObjectURL(blob);
577
+ const a = document.createElement("a");
578
+ a.href = url;
579
+ a.download = `${basename}-${datetime}.json`;
580
+ a.click();
581
+ URL.revokeObjectURL(url);
582
+ }
583
+ const initialState = {
584
+ current: {
585
+ oidcRoles: [],
586
+ users: [],
587
+ useWhitelist: false,
588
+ enforceOIDC: false
589
+ },
590
+ initial: {
591
+ oidcRoles: [],
592
+ users: [],
593
+ useWhitelist: false,
594
+ enforceOIDC: false
595
+ },
596
+ roles: [],
597
+ enforceOIDCConfig: null,
598
+ auditLogEnabled: true,
599
+ loading: false,
600
+ showSuccess: false,
601
+ showError: false
602
+ };
603
+ function reducer(state, action) {
604
+ switch (action.type) {
605
+ case "hydrate/roles":
606
+ return { ...state, roles: action.roles };
607
+ case "hydrate/oidcRoles": {
608
+ const snapshot = { oidcRoles: action.oidcRoles };
609
+ return {
610
+ ...state,
611
+ current: { ...state.current, ...snapshot },
612
+ initial: { ...state.initial, ...snapshot }
613
+ };
614
+ }
615
+ case "hydrate/whitelist": {
616
+ const snapshot = {
617
+ oidcRoles: action.snapshot.oidcRoles ?? state.current.oidcRoles,
618
+ users: action.snapshot.users ?? state.current.users,
619
+ useWhitelist: action.snapshot.useWhitelist ?? state.current.useWhitelist,
620
+ enforceOIDC: action.snapshot.enforceOIDC ?? state.current.enforceOIDC
621
+ };
622
+ return {
623
+ ...state,
624
+ current: snapshot,
625
+ initial: structuredClone(snapshot),
626
+ enforceOIDCConfig: action.enforceOIDCConfig,
627
+ auditLogEnabled: action.auditLogEnabled
628
+ };
629
+ }
630
+ case "patch/oidcRole":
631
+ return {
632
+ ...state,
633
+ current: {
634
+ ...state.current,
635
+ oidcRoles: state.current.oidcRoles.map(
636
+ (role) => role.oauth_type === action.oidcId ? { ...role, role: action.values } : role
637
+ )
638
+ }
639
+ };
640
+ case "user/add":
641
+ return {
642
+ ...state,
643
+ current: {
644
+ ...state.current,
645
+ users: [
646
+ ...state.current.users,
647
+ { email: action.email, createdAt: (/* @__PURE__ */ new Date()).toISOString() }
648
+ ]
649
+ }
650
+ };
651
+ case "user/delete": {
652
+ const updatedUsers = state.current.users.filter((u) => u.email !== action.email);
653
+ const updated = { users: updatedUsers };
654
+ let enforceOIDC = state.current.enforceOIDC;
655
+ if (state.current.useWhitelist && updatedUsers.length === 0) {
656
+ enforceOIDC = false;
657
+ }
658
+ return {
659
+ ...state,
660
+ current: { ...state.current, ...updated, enforceOIDC }
661
+ };
662
+ }
663
+ case "users/clear": {
664
+ const updated = { users: [] };
665
+ let enforceOIDC = state.current.enforceOIDC;
666
+ if (state.current.useWhitelist) {
667
+ enforceOIDC = false;
668
+ }
669
+ return {
670
+ ...state,
671
+ current: { ...state.current, ...updated, enforceOIDC }
672
+ };
673
+ }
674
+ case "users/replace":
675
+ return {
676
+ ...state,
677
+ current: { ...state.current, users: action.users }
678
+ };
679
+ case "toggle/useWhitelist": {
680
+ const useWhitelist = action.value;
681
+ let enforceOIDC = state.current.enforceOIDC;
682
+ if (useWhitelist && state.current.users.length === 0) {
683
+ enforceOIDC = false;
684
+ }
685
+ return {
686
+ ...state,
687
+ current: { ...state.current, useWhitelist, enforceOIDC }
688
+ };
689
+ }
690
+ case "toggle/enforceOIDC":
691
+ return {
692
+ ...state,
693
+ current: { ...state.current, enforceOIDC: action.value }
694
+ };
695
+ case "commit":
696
+ return {
697
+ ...state,
698
+ initial: structuredClone(
699
+ action.snapshot ? { ...state.current, ...action.snapshot } : state.current
700
+ )
701
+ };
702
+ case "loading":
703
+ return { ...state, loading: action.value };
704
+ case "flash/success":
705
+ return { ...state, showSuccess: true };
706
+ case "flash/error":
707
+ return { ...state, showError: true };
708
+ case "flash/clear":
709
+ return {
710
+ ...state,
711
+ showSuccess: action.kind === "success" ? false : state.showSuccess,
712
+ showError: action.kind === "error" ? false : state.showError
713
+ };
714
+ default:
715
+ return state;
716
+ }
717
+ }
718
+ function isDirtyPrimitive(a, b) {
719
+ return a !== b;
720
+ }
721
+ function isDirtyArray(a, b) {
722
+ return JSON.stringify(a) !== JSON.stringify(b);
576
723
  }
577
724
  function useOidcSettings() {
578
725
  const { get, put, post } = useFetchClient();
579
- const [loading, setLoading] = useState(false);
580
- const [showSuccess, setSuccess] = useState(false);
581
- const [showError, setError] = useState(false);
582
- const [initialOidcRoles, setInitialOIDCRoles] = useState([]);
583
- const [oidcRoles, setOIDCRoles] = useState([]);
584
- const [roles, setRoles] = useState([]);
585
- const [initialUseWhitelist, setInitialUseWhitelist] = useState(false);
586
- const [useWhitelist, setUseWhitelist] = useState(false);
587
- const [initialEnforceOIDC, setInitialEnforceOIDC] = useState(false);
588
- const [enforceOIDC, setEnforceOIDC] = useState(false);
589
- const [enforceOIDCConfig, setEnforceOIDCConfig] = useState(null);
590
- const [initialUsers, setInitialUsers] = useState([]);
591
- const [users, setUsers] = useState([]);
592
- const [whitelistResponse, setWhitelistResponse] = useState({});
726
+ const [state, dispatch] = useReducer(reducer, initialState);
593
727
  useEffect(() => {
594
728
  get(`/strapi-plugin-oidc/oidc-roles`).then((response) => {
595
- setOIDCRoles(response.data);
596
- setInitialOIDCRoles(deepClone(response.data));
729
+ dispatch({ type: "hydrate/oidcRoles", oidcRoles: response.data });
597
730
  });
598
731
  get(`/admin/roles`).then((response) => {
599
- setRoles(response.data.data);
732
+ dispatch({ type: "hydrate/roles", roles: response.data.data });
600
733
  });
601
734
  get("/strapi-plugin-oidc/whitelist").then((response) => {
602
735
  const data = response.data;
603
- setWhitelistResponse(data);
604
- setUsers(data.whitelistUsers);
605
- setInitialUsers(deepClone(data.whitelistUsers));
606
- setUseWhitelist(data.useWhitelist);
607
- setInitialUseWhitelist(data.useWhitelist);
608
- setEnforceOIDC(data.enforceOIDC);
609
- setInitialEnforceOIDC(data.enforceOIDC);
610
- setEnforceOIDCConfig(data.enforceOIDCConfig ?? null);
736
+ dispatch({
737
+ type: "hydrate/whitelist",
738
+ snapshot: {
739
+ users: data.whitelistUsers,
740
+ useWhitelist: data.useWhitelist,
741
+ enforceOIDC: data.enforceOIDC
742
+ },
743
+ enforceOIDCConfig: data.enforceOIDCConfig ?? null,
744
+ auditLogEnabled: data.auditLogEnabled ?? true
745
+ });
611
746
  });
612
747
  }, [get]);
613
748
  const onChangeRole = useCallback((values, oidcId) => {
614
- setOIDCRoles(
615
- (prev) => prev.map((role) => role.oauth_type === oidcId ? { ...role, role: values } : role)
616
- );
749
+ dispatch({ type: "patch/oidcRole", oidcId, values });
617
750
  }, []);
618
751
  const onRegisterWhitelist = useCallback((email) => {
619
- setUsers((prev) => [...prev, { email, createdAt: (/* @__PURE__ */ new Date()).toISOString() }]);
752
+ dispatch({ type: "user/add", email });
753
+ }, []);
754
+ const onDeleteWhitelist = useCallback((email) => {
755
+ dispatch({ type: "user/delete", email });
620
756
  }, []);
621
- const onDeleteWhitelist = useCallback(
622
- (email) => {
623
- setUsers((prev) => {
624
- const updated = prev.filter((u) => u.email !== email);
625
- if (useWhitelist && updated.length === 0) setEnforceOIDC(false);
626
- return updated;
627
- });
628
- },
629
- [useWhitelist]
630
- );
631
757
  const onDeleteAll = useCallback(() => {
632
- setUsers([]);
633
- if (useWhitelist) setEnforceOIDC(false);
634
- }, [useWhitelist]);
758
+ dispatch({ type: "users/clear" });
759
+ }, []);
635
760
  const onImport = useCallback(
636
761
  async (emails) => {
637
762
  const response = await post("/strapi-plugin-oidc/whitelist/import", {
638
763
  users: emails.map((e) => ({ email: e }))
639
764
  });
640
765
  const refreshed = await get("/strapi-plugin-oidc/whitelist");
641
- setUsers(refreshed.data.whitelistUsers);
642
- setInitialUsers(deepClone(refreshed.data.whitelistUsers));
766
+ dispatch({ type: "users/replace", users: refreshed.data.whitelistUsers });
767
+ dispatch({ type: "commit" });
643
768
  return response.data.importedCount;
644
769
  },
645
770
  [post, get]
@@ -647,74 +772,77 @@ function useOidcSettings() {
647
772
  const onExport = useCallback(async () => {
648
773
  const response = await get("/strapi-plugin-oidc/whitelist/export");
649
774
  const data = response.data;
650
- const datetime = formatDatetimeForFilename(/* @__PURE__ */ new Date());
651
- const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
652
- const url = URL.createObjectURL(blob);
653
- const a = document.createElement("a");
654
- a.href = url;
655
- a.download = `strapi-oidc-whitelist-${datetime}.json`;
656
- a.click();
657
- URL.revokeObjectURL(url);
775
+ downloadJson("strapi-oidc-whitelist", data);
658
776
  }, [get]);
659
- const onToggleWhitelist = useCallback(
660
- (e) => {
661
- const checked = e.target.checked;
662
- setUseWhitelist(checked);
663
- if (checked && users.length === 0) setEnforceOIDC(false);
664
- },
665
- [users.length]
666
- );
777
+ const onToggleWhitelist = useCallback((e) => {
778
+ dispatch({ type: "toggle/useWhitelist", value: e.target.checked });
779
+ }, []);
667
780
  const onToggleEnforce = useCallback((e) => {
668
- setEnforceOIDC(e.target.checked);
781
+ dispatch({ type: "toggle/enforceOIDC", value: e.target.checked });
669
782
  }, []);
670
- const isDirty = useWhitelist !== initialUseWhitelist || enforceOIDC !== initialEnforceOIDC || JSON.stringify(oidcRoles) !== JSON.stringify(initialOidcRoles) || JSON.stringify(users) !== JSON.stringify(initialUsers);
783
+ const isDirty = useMemo(
784
+ () => isDirtyPrimitive(state.current.useWhitelist, state.initial.useWhitelist) || isDirtyPrimitive(state.current.enforceOIDC, state.initial.enforceOIDC) || isDirtyArray(state.current.oidcRoles, state.initial.oidcRoles) || isDirtyArray(state.current.users, state.initial.users),
785
+ [state.current, state.initial]
786
+ );
671
787
  const onSaveAll = useCallback(async () => {
672
- setLoading(true);
788
+ dispatch({ type: "loading", value: true });
673
789
  try {
674
- await put("/strapi-plugin-oidc/oidc-roles", {
675
- roles: oidcRoles.map((role) => ({ oauth_type: role.oauth_type, role: role.role }))
676
- });
677
- await put("/strapi-plugin-oidc/whitelist/sync", {
678
- users: users.map((u) => ({ email: u.email }))
679
- });
680
- await put("/strapi-plugin-oidc/whitelist/settings", { useWhitelist, enforceOIDC });
681
- setInitialOIDCRoles(deepClone(oidcRoles));
682
- setInitialUseWhitelist(useWhitelist);
683
- setInitialEnforceOIDC(enforceOIDC);
684
- get("/strapi-plugin-oidc/whitelist").then((getResponse) => {
685
- const data = getResponse.data;
686
- setWhitelistResponse(data);
687
- setUsers(data.whitelistUsers);
688
- setInitialUsers(deepClone(data.whitelistUsers));
790
+ await Promise.all([
791
+ put("/strapi-plugin-oidc/oidc-roles", {
792
+ roles: state.current.oidcRoles.map((role) => ({
793
+ oauth_type: role.oauth_type,
794
+ role: role.role
795
+ }))
796
+ }),
797
+ put("/strapi-plugin-oidc/whitelist/sync", {
798
+ users: state.current.users.map((u) => ({ email: u.email }))
799
+ }),
800
+ put("/strapi-plugin-oidc/whitelist/settings", {
801
+ useWhitelist: state.current.useWhitelist,
802
+ enforceOIDC: state.current.enforceOIDC
803
+ })
804
+ ]);
805
+ dispatch({ type: "commit" });
806
+ const getResponse = await get("/strapi-plugin-oidc/whitelist");
807
+ const data = getResponse.data;
808
+ dispatch({
809
+ type: "hydrate/whitelist",
810
+ snapshot: {
811
+ users: data.whitelistUsers,
812
+ useWhitelist: data.useWhitelist,
813
+ enforceOIDC: data.enforceOIDC
814
+ },
815
+ enforceOIDCConfig: data.enforceOIDCConfig ?? null,
816
+ auditLogEnabled: data.auditLogEnabled ?? true
689
817
  });
690
- setSuccess(true);
691
- setTimeout(() => setSuccess(false), 3e3);
818
+ dispatch({ type: "flash/success" });
819
+ setTimeout(() => dispatch({ type: "flash/clear", kind: "success" }), 3e3);
692
820
  } catch (e) {
693
821
  console.error(e);
694
- setError(true);
695
- setTimeout(() => setError(false), 3e3);
822
+ dispatch({ type: "flash/error" });
823
+ setTimeout(() => dispatch({ type: "flash/clear", kind: "error" }), 3e3);
696
824
  } finally {
697
- setLoading(false);
825
+ dispatch({ type: "loading", value: false });
698
826
  }
699
- }, [put, get, oidcRoles, users, useWhitelist, enforceOIDC]);
827
+ }, [put, get, state.current]);
700
828
  return {
701
829
  state: {
702
- loading,
703
- showSuccess,
704
- showError,
705
- oidcRoles,
706
- roles,
707
- useWhitelist,
708
- enforceOIDC,
709
- enforceOIDCConfig,
710
- initialEnforceOIDC,
711
- users,
830
+ loading: state.loading,
831
+ showSuccess: state.showSuccess,
832
+ showError: state.showError,
833
+ oidcRoles: state.current.oidcRoles,
834
+ roles: state.roles,
835
+ useWhitelist: state.current.useWhitelist,
836
+ enforceOIDC: state.current.enforceOIDC,
837
+ enforceOIDCConfig: state.enforceOIDCConfig,
838
+ initialEnforceOIDC: state.initial.enforceOIDC,
839
+ users: state.current.users,
712
840
  isDirty,
713
- auditLogEnabled: whitelistResponse.auditLogEnabled ?? true
841
+ auditLogEnabled: state.auditLogEnabled
714
842
  },
715
843
  actions: {
716
- setSuccess,
717
- setError,
844
+ setSuccess: (val) => dispatch({ type: val ? "flash/success" : "flash/clear", kind: "success" }),
845
+ setError: (val) => dispatch({ type: val ? "flash/error" : "flash/clear", kind: "error" }),
718
846
  onChangeRole,
719
847
  onRegisterWhitelist,
720
848
  onDeleteWhitelist,
@@ -22,9 +22,6 @@ const pluginPkg = {
22
22
  strapi
23
23
  };
24
24
  const pluginId = pluginPkg.name.replace(/^@strapi\/plugin-/i, "");
25
- function getTranslation(id) {
26
- return `${pluginId}.${id}`;
27
- }
28
25
  function Initializer({ setPlugin }) {
29
26
  const ref = useRef();
30
27
  ref.current = setPlugin;
@@ -148,7 +145,7 @@ const index = {
148
145
  defaultMessage: "Configuration"
149
146
  },
150
147
  Component: async () => {
151
- return await import("./index-HQ2uuypE.mjs");
148
+ return await import("./index-CKyNupYU.mjs");
152
149
  },
153
150
  permissions: [{ action: "plugin::strapi-plugin-oidc.read", subject: null }]
154
151
  }
@@ -261,7 +258,7 @@ const index = {
261
258
  async registerTrads({ locales }) {
262
259
  const transformKeys = (data) => Object.fromEntries(
263
260
  Object.entries(data).map(([key, value]) => [
264
- key.startsWith("global.") ? key : getTranslation(key),
261
+ key.startsWith("global.") ? key : `${pluginId}.${key}`,
265
262
  value
266
263
  ])
267
264
  );