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.
@@ -2,6 +2,7 @@
2
2
  Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
3
3
  const node_crypto = require("node:crypto");
4
4
  const pkceChallenge = require("pkce-challenge");
5
+ const node_stream = require("node:stream");
5
6
  const strapiUtils = require("@strapi/utils");
6
7
  const generator = require("generate-password");
7
8
  const _interopDefault = (e) => e && e.__esModule ? e : { default: e };
@@ -36,6 +37,12 @@ function getRetentionDays() {
36
37
  function isAuditLogEnabled() {
37
38
  return getRetentionDays() !== 0;
38
39
  }
40
+ const PLUGIN_NAME = "strapi-plugin-oidc";
41
+ const getOauthService = () => strapi.plugin(PLUGIN_NAME).service("oauth");
42
+ const getRoleService = () => strapi.plugin(PLUGIN_NAME).service("role");
43
+ const getWhitelistService = () => strapi.plugin(PLUGIN_NAME).service("whitelist");
44
+ const getAuditLogService = () => strapi.plugin(PLUGIN_NAME).service("auditLog");
45
+ const getAdminUserService = () => strapi.service("admin::user");
39
46
  const AUTH_ROUTES = ["login", "register", "register-admin", "forgot-password", "reset-password"];
40
47
  async function bootstrap({ strapi: strapi2 }) {
41
48
  const adminUrl = strapi2.config.get("admin.url", "/admin");
@@ -47,7 +54,7 @@ async function bootstrap({ strapi: strapi2 }) {
47
54
  const isTokenRefresh = path === tokenRefreshPath;
48
55
  if (isAuthRoute && isPost || isTokenRefresh) {
49
56
  try {
50
- const whitelistService2 = strapi2.plugin("strapi-plugin-oidc").service("whitelist");
57
+ const whitelistService2 = getWhitelistService();
51
58
  const settings = await whitelistService2.getSettings();
52
59
  const enforceOIDC = resolveEnforceOIDC(strapi2, settings?.enforceOIDC);
53
60
  if (enforceOIDC && isAuthRoute && isPost) {
@@ -95,7 +102,7 @@ async function bootstrap({ strapi: strapi2 }) {
95
102
  const enforceOIDCConfig = getEnforceOIDCConfig(strapi2);
96
103
  if (enforceOIDCConfig !== null) {
97
104
  try {
98
- const whitelistService2 = strapi2.plugin("strapi-plugin-oidc").service("whitelist");
105
+ const whitelistService2 = getWhitelistService();
99
106
  const settings = await whitelistService2.getSettings();
100
107
  if (settings.enforceOIDC !== enforceOIDCConfig) {
101
108
  await whitelistService2.setSettings({ ...settings, enforceOIDC: enforceOIDCConfig });
@@ -125,7 +132,7 @@ async function bootstrap({ strapi: strapi2 }) {
125
132
  task: async () => {
126
133
  try {
127
134
  const retentionDays = getRetentionDays();
128
- await strapi2.plugin("strapi-plugin-oidc").service("auditLog").cleanup(retentionDays);
135
+ await getAuditLogService().cleanup(retentionDays);
129
136
  } catch (err) {
130
137
  strapi2.log.warn("[strapi-plugin-oidc] Audit log cleanup failed:", err.message);
131
138
  }
@@ -219,9 +226,14 @@ function getExpiredCookieOptions(strapi2, ctx) {
219
226
  function clearAuthCookies(strapi2, ctx) {
220
227
  const options2 = getExpiredCookieOptions(strapi2, ctx);
221
228
  ctx.cookies.set("strapi_admin_refresh", "", options2);
222
- ctx.cookies.set("oidc_authenticated", "", { ...options2, path: "/" });
223
- ctx.cookies.set("oidc_access_token", "", { ...options2, path: "/" });
224
- ctx.cookies.set("oidc_user_email", "", { ...options2, path: "/" });
229
+ const rootPathOptions = { ...options2, path: "/" };
230
+ for (const name of ["oidc_authenticated", "oidc_access_token", "oidc_user_email"]) {
231
+ ctx.cookies.set(name, "", rootPathOptions);
232
+ }
233
+ }
234
+ const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
235
+ function isValidEmail(email) {
236
+ return EMAIL_REGEX.test(email);
225
237
  }
226
238
  const errorCodes = {
227
239
  TOKEN_EXCHANGE_FAILED: "TOKEN_EXCHANGE_FAILED",
@@ -358,6 +370,53 @@ const userFacingMessages = {
358
370
  return en["user.signInError"];
359
371
  }
360
372
  };
373
+ class OidcError extends Error {
374
+ kind;
375
+ cause;
376
+ constructor(kind, message, cause) {
377
+ super(message);
378
+ this.name = "OidcError";
379
+ this.kind = kind;
380
+ this.cause = cause;
381
+ }
382
+ }
383
+ const OIDC_ERROR_DISPATCH = {
384
+ nonce_mismatch: { action: "nonce_mismatch", code: errorCodes.NONCE_MISMATCH },
385
+ token_exchange_failed: {
386
+ action: "token_exchange_failed",
387
+ code: errorCodes.TOKEN_EXCHANGE_FAILED
388
+ },
389
+ id_token_parse_failed: {
390
+ action: "login_failure",
391
+ code: errorCodes.ID_TOKEN_PARSE_FAILED,
392
+ key: "id_token_parse_failed"
393
+ },
394
+ userinfo_fetch_failed: {
395
+ action: "login_failure",
396
+ code: errorCodes.USERINFO_FETCH_FAILED,
397
+ key: "userinfo_fetch_failed"
398
+ },
399
+ user_creation_failed: {
400
+ action: "login_failure",
401
+ code: errorCodes.USER_CREATION_FAILED,
402
+ key: "user_creation_failed"
403
+ },
404
+ whitelist_rejected: {
405
+ action: "whitelist_rejected",
406
+ code: errorCodes.WHITELIST_CHECK_FAILED,
407
+ key: "whitelist_rejected"
408
+ },
409
+ invalid_email: {
410
+ action: "login_failure",
411
+ code: errorCodes.TOKEN_EXCHANGE_FAILED,
412
+ key: "sign_in_unknown"
413
+ },
414
+ unknown: {
415
+ action: "login_failure",
416
+ code: errorCodes.TOKEN_EXCHANGE_FAILED,
417
+ key: "sign_in_unknown"
418
+ }
419
+ };
361
420
  const REQUIRED_CONFIG_KEYS = [
362
421
  "OIDC_CLIENT_ID",
363
422
  "OIDC_CLIENT_SECRET",
@@ -370,6 +429,7 @@ const REQUIRED_CONFIG_KEYS = [
370
429
  "OIDC_GIVEN_NAME_FIELD",
371
430
  "OIDC_AUTHORIZATION_ENDPOINT"
372
431
  ];
432
+ const LOGOUT_USERINFO_TIMEOUT_MS = 3e3;
373
433
  function configValidation() {
374
434
  const config2 = strapi.config.get("plugin::strapi-plugin-oidc");
375
435
  const missing = REQUIRED_CONFIG_KEYS.filter((key) => !config2[key]);
@@ -387,7 +447,6 @@ async function oidcSignIn(ctx) {
387
447
  const cookieOptions = {
388
448
  httpOnly: true,
389
449
  maxAge: 6e5,
390
- // 10 minutes
391
450
  secure: isProduction && ctx.request.secure,
392
451
  sameSite: "lax"
393
452
  };
@@ -417,7 +476,7 @@ async function exchangeTokenAndFetchUserInfo(config2, params, expectedNonce) {
417
476
  }
418
477
  });
419
478
  if (!response.ok) {
420
- throw new Error(errorMessages.TOKEN_EXCHANGE_FAILED);
479
+ throw new OidcError("token_exchange_failed", errorMessages.TOKEN_EXCHANGE_FAILED);
421
480
  }
422
481
  const tokenData = await response.json();
423
482
  if (tokenData.id_token) {
@@ -425,23 +484,23 @@ async function exchangeTokenAndFetchUserInfo(config2, params, expectedNonce) {
425
484
  const payloadB64 = tokenData.id_token.split(".")[1];
426
485
  const idTokenPayload = JSON.parse(Buffer.from(payloadB64, "base64url").toString("utf8"));
427
486
  if (idTokenPayload.nonce !== expectedNonce) {
428
- throw new Error(errorMessages.NONCE_MISMATCH);
487
+ throw new OidcError("nonce_mismatch", errorMessages.NONCE_MISMATCH);
429
488
  }
430
489
  } catch (e) {
431
- if (e.message === "Nonce mismatch") throw e;
432
- throw new Error(errorMessages.ID_TOKEN_PARSE_FAILED);
490
+ if (e instanceof OidcError && e.kind === "nonce_mismatch") throw e;
491
+ throw new OidcError("id_token_parse_failed", errorMessages.ID_TOKEN_PARSE_FAILED, e);
433
492
  }
434
493
  }
435
494
  const userResponse = await fetch(config2.OIDC_USERINFO_ENDPOINT, {
436
495
  headers: { Authorization: `Bearer ${tokenData.access_token}` }
437
496
  });
438
497
  if (!userResponse.ok) {
439
- throw new Error(errorMessages.USERINFO_FETCH_FAILED);
498
+ throw new OidcError("userinfo_fetch_failed", errorMessages.USERINFO_FETCH_FAILED);
440
499
  }
441
500
  const userInfo = await userResponse.json();
442
501
  return { userInfo, accessToken: tokenData.access_token };
443
502
  }
444
- function resolveRolesFromGroups(userInfo, config2, availableRoles) {
503
+ function collectGroupMapRoleNames(userInfo, config2) {
445
504
  const rawGroups = userInfo[config2.OIDC_GROUP_FIELD];
446
505
  if (!Array.isArray(rawGroups) || rawGroups.length === 0) return [];
447
506
  const groups = rawGroups.filter((g) => typeof g === "string");
@@ -452,22 +511,15 @@ function resolveRolesFromGroups(userInfo, config2, availableRoles) {
452
511
  } catch {
453
512
  return [];
454
513
  }
455
- const roleIdSet = /* @__PURE__ */ new Set();
514
+ const roleNameSet = /* @__PURE__ */ new Set();
456
515
  for (const group of groups) {
457
516
  const roleNames = groupRoleMap[group];
458
517
  if (!roleNames) continue;
459
518
  for (const name of roleNames) {
460
- const match = availableRoles.find((r) => r.name === name);
461
- if (match) roleIdSet.add(String(match.id));
519
+ roleNameSet.add(name);
462
520
  }
463
521
  }
464
- return [...roleIdSet];
465
- }
466
- async function resolveRoles(userInfo, config2, roleService2, availableRoles) {
467
- const groupRoles = resolveRolesFromGroups(userInfo, config2, availableRoles);
468
- if (groupRoles.length > 0) return { roles: groupRoles, fromGroupMapping: true };
469
- const oidcRoles = await roleService2.oidcRoles();
470
- return { roles: oidcRoles?.roles || [], fromGroupMapping: false };
522
+ return [...roleNameSet];
471
523
  }
472
524
  async function registerNewUser(oauthService2, email, userResponseData, config2, ctx, roles2) {
473
525
  const defaultLocale = oauthService2.localeFindByHeader(
@@ -485,10 +537,7 @@ async function registerNewUser(oauthService2, email, userResponseData, config2,
485
537
  }
486
538
  function rolesChanged(current, next) {
487
539
  if (current.size !== next.size) return true;
488
- for (const id of next) {
489
- if (!current.has(id)) return true;
490
- }
491
- return false;
540
+ return [...next].some((id) => !current.has(id));
492
541
  }
493
542
  async function updateUserRoles(user, currentRoleIds, newRoleIds) {
494
543
  try {
@@ -514,27 +563,51 @@ async function updateUserRoles(user, currentRoleIds, newRoleIds) {
514
563
  async function handleUserAuthentication(userService, oauthService2, roleService2, whitelistService2, userResponseData, config2, ctx) {
515
564
  const rawEmail = String(userResponseData.email ?? "");
516
565
  const email = rawEmail.toLowerCase();
517
- if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
518
- throw new Error(errorMessages.INVALID_EMAIL);
566
+ if (!email || !isValidEmail(email)) {
567
+ throw new OidcError("invalid_email", errorMessages.INVALID_EMAIL);
519
568
  }
520
569
  await whitelistService2.checkWhitelistForEmail(email);
521
- const allRoles = await strapi.db.query("admin::role").findMany();
522
- const { roles: roles2, fromGroupMapping } = await resolveRoles(
523
- userResponseData,
524
- config2,
525
- roleService2,
526
- allRoles
527
- );
528
- const resolvedRoleNames = allRoles.filter((r) => roles2.includes(String(r.id))).map((r) => r.name);
570
+ const candidateNames = collectGroupMapRoleNames(userResponseData, config2);
571
+ let roles2 = [];
572
+ let fromGroupMapping = false;
573
+ let resolvedRoleNames = [];
574
+ if (candidateNames.length > 0) {
575
+ const matchedRoles = await strapi.db.query("admin::role").findMany({
576
+ where: { name: { $in: candidateNames } },
577
+ select: ["id", "name"]
578
+ });
579
+ const nameToId = new Map(matchedRoles.map((r) => [r.name, String(r.id)]));
580
+ for (const name of candidateNames) {
581
+ const id = nameToId.get(name);
582
+ if (id) roles2.push(id);
583
+ }
584
+ resolvedRoleNames = matchedRoles.map((r) => r.name);
585
+ fromGroupMapping = true;
586
+ } else {
587
+ const oidcRolesResult = await roleService2.oidcRoles();
588
+ roles2 = oidcRolesResult?.roles || [];
589
+ if (roles2.length > 0) {
590
+ const oidcRoleRecords = await strapi.db.query("admin::role").findMany({
591
+ where: { id: { $in: roles2.map(Number) } },
592
+ select: ["id", "name"]
593
+ });
594
+ resolvedRoleNames = oidcRoleRecords.map((r) => r.name);
595
+ }
596
+ }
529
597
  let userCreated = false;
530
598
  let rolesUpdated = false;
531
599
  let user = await userService.findOneByEmail(email, ["roles"]);
532
600
  if (!user) {
533
- user = await registerNewUser(oauthService2, email, userResponseData, config2, ctx, roles2);
601
+ try {
602
+ user = await registerNewUser(oauthService2, email, userResponseData, config2, ctx, roles2);
603
+ } catch (e) {
604
+ const msg = e instanceof Error ? e.message : String(e);
605
+ throw new OidcError("user_creation_failed", msg, e);
606
+ }
534
607
  userCreated = true;
535
608
  rolesUpdated = true;
536
609
  } else if (fromGroupMapping && roles2.length > 0) {
537
- const currentRoleIds = new Set(user.roles.map((r) => String(r.id)));
610
+ const currentRoleIds = new Set((user.roles ?? []).map((r) => String(r.id)));
538
611
  if (rolesChanged(currentRoleIds, new Set(roles2))) {
539
612
  await updateUserRoles(user, currentRoleIds, roles2);
540
613
  rolesUpdated = true;
@@ -544,55 +617,30 @@ async function handleUserAuthentication(userService, oauthService2, roleService2
544
617
  oauthService2.triggerSignInSuccess(user);
545
618
  return { activateUser: user, jwtToken, userCreated, rolesUpdated, resolvedRoleNames };
546
619
  }
547
- function classifyOidcError(msg, userInfo) {
548
- if (msg.includes("whitelist")) {
549
- return {
550
- action: "whitelist_rejected",
551
- code: errorCodes.WHITELIST_CHECK_FAILED,
552
- key: "whitelist_rejected"
553
- };
554
- }
555
- if (msg === "Nonce mismatch")
556
- return { action: "nonce_mismatch", code: errorCodes.NONCE_MISMATCH };
557
- if (msg === "Token exchange failed")
558
- return { action: "token_exchange_failed", code: errorCodes.TOKEN_EXCHANGE_FAILED };
559
- if (msg === "Failed to fetch user info") {
560
- return {
561
- action: "login_failure",
562
- code: errorCodes.USERINFO_FETCH_FAILED,
563
- key: "userinfo_fetch_failed"
564
- };
565
- }
566
- if (msg === "Failed to parse ID token") {
567
- return {
568
- action: "login_failure",
569
- code: errorCodes.ID_TOKEN_PARSE_FAILED,
570
- key: "id_token_parse_failed",
571
- params: { error: msg }
572
- };
573
- }
574
- if (msg === "User creation failed" || msg.includes("createUser")) {
575
- return {
576
- action: "login_failure",
577
- code: errorCodes.USER_CREATION_FAILED,
578
- key: "user_creation_failed",
579
- params: userInfo?.email ? { email: userInfo.email, error: msg } : void 0
580
- };
620
+ function classifyOidcError(e, userInfo) {
621
+ const kind = e instanceof OidcError ? e.kind : "unknown";
622
+ const dispatch = OIDC_ERROR_DISPATCH[kind];
623
+ const msg = e instanceof Error ? e.message : String(e);
624
+ let params;
625
+ if (kind === "id_token_parse_failed" || kind === "unknown") {
626
+ params = { error: msg };
627
+ } else if (kind === "user_creation_failed" && userInfo?.email) {
628
+ params = { email: userInfo.email, error: msg };
581
629
  }
582
630
  return {
583
- action: "login_failure",
584
- code: errorCodes.TOKEN_EXCHANGE_FAILED,
585
- key: "sign_in_unknown",
586
- params: { error: msg || "unknown" }
631
+ action: dispatch.action,
632
+ code: dispatch.code,
633
+ key: dispatch.key,
634
+ params
587
635
  };
588
636
  }
589
637
  async function oidcSignInCallback(ctx) {
590
638
  const config2 = configValidation();
591
- const userService = strapi.service("admin::user");
592
- const oauthService2 = strapi.plugin("strapi-plugin-oidc").service("oauth");
593
- const roleService2 = strapi.plugin("strapi-plugin-oidc").service("role");
594
- const whitelistService2 = strapi.plugin("strapi-plugin-oidc").service("whitelist");
595
- const auditLog2 = strapi.plugin("strapi-plugin-oidc").service("auditLog");
639
+ const userService = getAdminUserService();
640
+ const oauthService2 = getOauthService();
641
+ const roleService2 = getRoleService();
642
+ const whitelistService2 = getWhitelistService();
643
+ const auditLog2 = getAuditLogService();
596
644
  if (!ctx.query.code) {
597
645
  await auditLog2.log({ action: "missing_code", ip: ctx.ip });
598
646
  return ctx.send(oauthService2.renderSignUpError(userFacingMessages.missing_code));
@@ -624,7 +672,6 @@ async function oidcSignInCallback(ctx) {
624
672
  ctx.cookies.set("oidc_access_token", accessToken, {
625
673
  httpOnly: true,
626
674
  maxAge: 3e5,
627
- // 5 minutes — matches typical provider access token lifetime
628
675
  secure: isProduction && ctx.request.secure,
629
676
  sameSite: "lax"
630
677
  });
@@ -665,19 +712,18 @@ async function oidcSignInCallback(ctx) {
665
712
  ctx.set("Content-Security-Policy", `script-src 'nonce-${nonce}'`);
666
713
  ctx.send(html);
667
714
  } catch (e) {
668
- const msg = e.message ?? "";
669
- const errorInfo = classifyOidcError(msg, userInfo);
715
+ const errorInfo = classifyOidcError(e, userInfo);
670
716
  await auditLog2.log({
671
717
  action: errorInfo.action,
672
718
  email: userInfo?.email,
673
719
  ip: ctx.ip,
674
720
  detailsKey: errorInfo.action,
675
- detailsParams: errorInfo.action === "login_failure" ? { message: msg } : void 0
721
+ detailsParams: errorInfo.action === "login_failure" ? { message: e instanceof Error ? e.message : String(e) } : void 0
676
722
  });
677
723
  strapi.log.error({
678
724
  code: errorInfo.code,
679
725
  phase: "oidc_callback",
680
- message: msg || "Unknown sign-in error",
726
+ message: e instanceof Error ? e.message : "Unknown sign-in error",
681
727
  detail: errorInfo.key ? getErrorDetail(errorInfo.key, errorInfo.params) : void 0,
682
728
  email: userInfo?.email
683
729
  });
@@ -686,7 +732,7 @@ async function oidcSignInCallback(ctx) {
686
732
  }
687
733
  async function logout(ctx) {
688
734
  const config2 = strapi.config.get("plugin::strapi-plugin-oidc");
689
- const auditLog2 = strapi.plugin("strapi-plugin-oidc").service("auditLog");
735
+ const auditLog2 = getAuditLogService();
690
736
  const logoutUrl = config2.OIDC_END_SESSION_ENDPOINT;
691
737
  const adminPanelUrl = strapi.config.get("admin.url", "/admin");
692
738
  const isOidcSession = !!ctx.cookies.get("oidc_authenticated");
@@ -696,7 +742,8 @@ async function logout(ctx) {
696
742
  if (logoutUrl && isOidcSession && accessToken) {
697
743
  try {
698
744
  const response = await fetch(config2.OIDC_USERINFO_ENDPOINT, {
699
- headers: { Authorization: `Bearer ${accessToken}` }
745
+ headers: { Authorization: `Bearer ${accessToken}` },
746
+ signal: AbortSignal.timeout(LOGOUT_USERINFO_TIMEOUT_MS)
700
747
  });
701
748
  if (response.ok) {
702
749
  if (userEmail)
@@ -727,7 +774,7 @@ const oidc = {
727
774
  logout
728
775
  };
729
776
  async function find$1(ctx) {
730
- const roleService2 = strapi.plugin("strapi-plugin-oidc").service("role");
777
+ const roleService2 = getRoleService();
731
778
  const roles2 = await roleService2.find();
732
779
  const oidcConstants = roleService2.getOidcRoles();
733
780
  for (const oidc2 of oidcConstants) {
@@ -741,7 +788,7 @@ async function find$1(ctx) {
741
788
  async function update(ctx) {
742
789
  try {
743
790
  const { roles: roles2 } = ctx.request.body;
744
- const roleService2 = strapi.plugin("strapi-plugin-oidc").service("role");
791
+ const roleService2 = getRoleService();
745
792
  await roleService2.update(roles2);
746
793
  ctx.send({}, 204);
747
794
  } catch (e) {
@@ -762,8 +809,17 @@ function formatDatetimeForFilename(date) {
762
809
  const seconds = String(date.getSeconds()).padStart(2, "0");
763
810
  return `${year}${month}${day}_${hours}${minutes}${seconds}`;
764
811
  }
765
- function getWhitelistService() {
766
- return strapi.plugin("strapi-plugin-oidc").service("whitelist");
812
+ function setJsonAttachmentHeaders(ctx, basename) {
813
+ const datetime = formatDatetimeForFilename(/* @__PURE__ */ new Date());
814
+ ctx.set("Content-Type", "application/json");
815
+ ctx.set("Content-Disposition", `attachment; filename="${basename}-${datetime}.json"`);
816
+ }
817
+ function setNdjsonAttachmentHeaders(ctx, basename) {
818
+ const datetime = formatDatetimeForFilename(/* @__PURE__ */ new Date());
819
+ ctx.set("Content-Type", "application/x-ndjson; charset=utf-8");
820
+ ctx.set("Content-Disposition", `attachment; filename="${basename}-${datetime}.ndjson"`);
821
+ ctx.set("Cache-Control", "no-store");
822
+ ctx.set("X-Content-Type-Options", "nosniff");
767
823
  }
768
824
  async function info(ctx) {
769
825
  const whitelistService2 = getWhitelistService();
@@ -778,8 +834,9 @@ async function info(ctx) {
778
834
  };
779
835
  }
780
836
  async function updateSettings(ctx) {
781
- const { useWhitelist } = ctx.request.body;
782
- let { enforceOIDC } = ctx.request.body;
837
+ const body = ctx.request.body;
838
+ const { useWhitelist } = body;
839
+ let { enforceOIDC } = body;
783
840
  const whitelistService2 = getWhitelistService();
784
841
  if (useWhitelist && enforceOIDC) {
785
842
  const users = await whitelistService2.getUsers();
@@ -808,11 +865,9 @@ async function register(ctx) {
808
865
  const rawEmails = Array.isArray(email) ? email : email.split(",");
809
866
  const emailList = rawEmails.map((e) => String(e).trim().toLowerCase()).filter(Boolean);
810
867
  const whitelistService2 = getWhitelistService();
811
- let matchedExistingUsersCount = 0;
868
+ const matchedExistingUsersCount = await whitelistService2.countAdminUsersByEmails(emailList);
812
869
  for (const singleEmail of emailList) {
813
- const existingUser = await strapi.query("admin::user").findOne({ where: { email: singleEmail } });
814
- if (existingUser) matchedExistingUsersCount++;
815
- const alreadyWhitelisted = await strapi.query("plugin::strapi-plugin-oidc.whitelists").findOne({ where: { email: singleEmail } });
870
+ const alreadyWhitelisted = await whitelistService2.hasUser(singleEmail);
816
871
  if (!alreadyWhitelisted) {
817
872
  await whitelistService2.registerUser(singleEmail);
818
873
  }
@@ -826,13 +881,12 @@ async function removeEmail(ctx) {
826
881
  ctx.body = {};
827
882
  }
828
883
  async function deleteAll(ctx) {
829
- await strapi.query("plugin::strapi-plugin-oidc.whitelists").deleteMany({});
884
+ const whitelistService2 = getWhitelistService();
885
+ await whitelistService2.deleteAllUsers();
830
886
  ctx.body = {};
831
887
  }
832
888
  async function exportWhitelist(ctx) {
833
- const datetime = formatDatetimeForFilename(/* @__PURE__ */ new Date());
834
- ctx.set("Content-Type", "application/json");
835
- ctx.set("Content-Disposition", `attachment; filename="strapi-oidc-whitelist-${datetime}.json"`);
889
+ setJsonAttachmentHeaders(ctx, "strapi-oidc-whitelist");
836
890
  const whitelistService2 = getWhitelistService();
837
891
  const users = await whitelistService2.getUsers();
838
892
  ctx.body = users.map((u) => ({ email: u.email }));
@@ -844,7 +898,7 @@ async function importUsers(ctx) {
844
898
  ctx.body = { error: "Expected { users: [{email}] }" };
845
899
  return;
846
900
  }
847
- const normalized = users.filter((u) => u?.email).map((u) => String(u.email).trim().toLowerCase()).filter((email) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email));
901
+ const normalized = users.filter((u) => u?.email).map((u) => String(u.email).trim().toLowerCase()).filter(isValidEmail);
848
902
  const deduped = [...new Set(normalized)];
849
903
  const whitelistService2 = getWhitelistService();
850
904
  const existing = await whitelistService2.getUsers();
@@ -859,7 +913,7 @@ async function importUsers(ctx) {
859
913
  }
860
914
  async function syncUsers(ctx) {
861
915
  const { users: rawUsers } = ctx.request.body;
862
- const emails = rawUsers.map((u) => String(u.email).toLowerCase()).filter((e) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e));
916
+ const emails = rawUsers.map((u) => String(u.email).toLowerCase()).filter(isValidEmail);
863
917
  const whitelistService2 = getWhitelistService();
864
918
  const currentUsers = await whitelistService2.getUsers();
865
919
  const syncEmailSet = new Set(emails);
@@ -887,44 +941,49 @@ const whitelist = {
887
941
  importUsers,
888
942
  exportWhitelist
889
943
  };
890
- function getAuditLogService() {
891
- return strapi.plugin("strapi-plugin-oidc").service("auditLog");
892
- }
893
- async function find(ctx) {
894
- const page = Math.max(1, Number(ctx.query.page) || 1);
895
- const pageSize = Math.min(100, Math.max(1, Number(ctx.query.pageSize) || 25));
896
- ctx.body = await getAuditLogService().find({ page, pageSize });
897
- }
898
- async function exportLogs(ctx) {
899
- const datetime = formatDatetimeForFilename(/* @__PURE__ */ new Date());
900
- ctx.set("Content-Type", "application/json");
901
- ctx.set("Content-Disposition", `attachment; filename="strapi-oidc-audit-log-${datetime}.json"`);
902
- const service = getAuditLogService();
903
- const PAGE_SIZE = 1e3;
904
- const allRows = [];
944
+ const EXPORT_PAGE_SIZE = 500;
945
+ async function* ndjsonRowStream(service) {
905
946
  let page = 1;
906
947
  while (true) {
907
- const { results } = await service.find({ page, pageSize: PAGE_SIZE });
948
+ const { results } = await service.find({ page, pageSize: EXPORT_PAGE_SIZE });
949
+ if (results.length === 0) return;
950
+ let chunk = "";
908
951
  for (const row of results) {
909
- allRows.push({
910
- id: row.id,
911
- createdAt: row.createdAt,
952
+ chunk += JSON.stringify({
953
+ datetime: row.createdAt,
912
954
  action: row.action,
913
955
  email: row.email ?? null,
914
956
  ip: row.ip ?? null,
915
957
  details: row.details
916
- });
958
+ }) + "\n";
917
959
  }
918
- if (results.length < PAGE_SIZE) break;
960
+ yield Buffer.from(chunk, "utf8");
961
+ if (results.length < EXPORT_PAGE_SIZE) return;
919
962
  page++;
920
963
  }
921
- ctx.body = allRows.map((row) => ({
922
- datetime: row.createdAt,
923
- action: row.action,
924
- email: row.email,
925
- ip: row.ip,
926
- details: row.details
927
- }));
964
+ }
965
+ function errorAwareNdjsonStream(service) {
966
+ const gen = ndjsonRowStream(service);
967
+ const readable = node_stream.Readable.from(gen);
968
+ readable.on("error", (err) => {
969
+ strapi$1.log.error({ phase: "audit_log_export", err }, "NDJSON export stream failed");
970
+ });
971
+ return readable;
972
+ }
973
+ let strapi$1;
974
+ function find(ctx) {
975
+ strapi$1 = ctx.strapi;
976
+ const page = Math.max(1, Number(ctx.query.page) || 1);
977
+ const pageSize = Math.min(100, Math.max(1, Number(ctx.query.pageSize) || 25));
978
+ return getAuditLogService().find({ page, pageSize }).then((result) => {
979
+ ctx.body = result;
980
+ });
981
+ }
982
+ async function exportLogs(ctx) {
983
+ strapi$1 = ctx.strapi;
984
+ setNdjsonAttachmentHeaders(ctx, "strapi-oidc-audit-log");
985
+ const service = getAuditLogService();
986
+ ctx.body = errorAwareNdjsonStream(service);
928
987
  }
929
988
  async function clearAll(ctx) {
930
989
  await getAuditLogService().clearAll();
@@ -1159,7 +1218,7 @@ function renderHtmlTemplate(title, content) {
1159
1218
  --icon-color: #d02b20;
1160
1219
  --success-bg: #eafbe7;
1161
1220
  --success-color: #328048;
1162
- --shadow: 0 1px 4px rgba(33, 33, 52, 0.1);
1221
+ --shadow: 0 1px 4 rgba(33, 33, 52, 0.1);
1163
1222
  }
1164
1223
  @media (prefers-color-scheme: dark) {
1165
1224
  :root {
@@ -1174,7 +1233,7 @@ function renderHtmlTemplate(title, content) {
1174
1233
  --icon-color: #f23628;
1175
1234
  --success-bg: #1c3523;
1176
1235
  --success-color: #55ca76;
1177
- --shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
1236
+ --shadow: 0 1px 4 rgba(0, 0, 0, 0.5);
1178
1237
  }
1179
1238
  }
1180
1239
  body {
@@ -1259,14 +1318,11 @@ function oauthService({ strapi: strapi2 }) {
1259
1318
  return {
1260
1319
  async createUser(email, lastname, firstname, locale, roles2 = []) {
1261
1320
  const userService = strapi2.service("admin::user");
1262
- if (/[A-Z]/.test(email)) {
1263
- const dbUser = await userService.findOneByEmail(email.toLocaleLowerCase());
1264
- if (dbUser) return dbUser;
1265
- }
1321
+ const normalizedEmail = email.toLowerCase();
1266
1322
  const createdUser = await userService.create({
1267
1323
  firstname: firstname || "unset",
1268
1324
  lastname: lastname || "",
1269
- email: email.toLocaleLowerCase(),
1325
+ email: normalizedEmail,
1270
1326
  roles: roles2,
1271
1327
  preferedLanguage: locale
1272
1328
  });
@@ -1297,35 +1353,35 @@ function oauthService({ strapi: strapi2 }) {
1297
1353
  },
1298
1354
  async triggerWebHook(user) {
1299
1355
  let ENTRY_CREATE;
1300
- const webhookStore = strapi2.serviceMap.get("webhookStore");
1301
- const eventHub = strapi2.serviceMap.get("eventHub");
1356
+ const webhookStore = strapi2.serviceMap?.get("webhookStore");
1357
+ const eventHub = strapi2.serviceMap?.get("eventHub");
1302
1358
  if (webhookStore) {
1303
1359
  ENTRY_CREATE = webhookStore.allowedEvents.get("ENTRY_CREATE");
1304
1360
  }
1305
1361
  const modelDef = strapi2.getModel("admin::user");
1306
1362
  const sanitizedEntity = await strapiUtils__default.default.sanitize.sanitizers.defaultSanitizeOutput(
1307
- {
1308
- schema: modelDef,
1309
- getModel: (uid2) => strapi2.getModel(uid2)
1310
- },
1363
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1364
+ { schema: modelDef, getModel: (uid2) => strapi2.getModel(uid2) },
1365
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1311
1366
  user
1312
1367
  );
1313
- eventHub.emit(ENTRY_CREATE, {
1368
+ eventHub?.emit(ENTRY_CREATE ?? "entry.create", {
1314
1369
  model: modelDef.modelName,
1315
1370
  entry: sanitizedEntity
1316
1371
  });
1317
1372
  },
1318
1373
  triggerSignInSuccess(user) {
1319
- delete user.password;
1320
- const eventHub = strapi2.serviceMap.get("eventHub");
1321
- eventHub.emit("admin.auth.success", {
1322
- user,
1374
+ const userCopy = { ...user };
1375
+ delete userCopy.password;
1376
+ const eventHub = strapi2.serviceMap?.get("eventHub");
1377
+ eventHub?.emit("admin.auth.success", {
1378
+ user: userCopy,
1323
1379
  provider: "strapi-plugin-oidc"
1324
1380
  });
1325
1381
  },
1326
1382
  renderSignUpSuccess(jwtToken, user, nonce) {
1327
1383
  const config2 = strapi2.config.get("plugin::strapi-plugin-oidc");
1328
- const isRememberMe = !!config2["REMEMBER_ME"];
1384
+ const isRememberMe = !!config2?.REMEMBER_ME;
1329
1385
  const content = `
1330
1386
  <noscript>
1331
1387
  <div class="card">
@@ -1376,12 +1432,15 @@ function oauthService({ strapi: strapi2 }) {
1376
1432
  const userId = String(user.id);
1377
1433
  const deviceId = node_crypto.randomUUID();
1378
1434
  const config2 = strapi2.config.get("plugin::strapi-plugin-oidc");
1379
- const rememberMe = !!config2["REMEMBER_ME"];
1380
- const { token: refreshToken, absoluteExpiresAt } = await sessionManager(
1381
- "admin"
1382
- ).generateRefreshToken(userId, deviceId, {
1383
- type: rememberMe ? "refresh" : "session"
1384
- });
1435
+ const rememberMe = !!config2?.REMEMBER_ME;
1436
+ const smAdmin = sessionManager("admin");
1437
+ const { token: refreshToken, absoluteExpiresAt } = await smAdmin.generateRefreshToken(
1438
+ userId,
1439
+ deviceId,
1440
+ {
1441
+ type: rememberMe ? "refresh" : "session"
1442
+ }
1443
+ );
1385
1444
  const isProduction = strapi2.config.get("environment") === "production";
1386
1445
  const domain = strapi2.config.get("admin.auth.cookie.domain") || strapi2.config.get("admin.auth.domain");
1387
1446
  const path = strapi2.config.get("admin.auth.cookie.path", "/admin");
@@ -1398,7 +1457,6 @@ function oauthService({ strapi: strapi2 }) {
1398
1457
  const idleLifespanSec = strapi2.config.get(
1399
1458
  "admin.auth.sessions.idleRefreshTokenLifespan",
1400
1459
  1209600
1401
- // 14 days — Strapi default
1402
1460
  );
1403
1461
  const idleMs = idleLifespanSec * 1e3;
1404
1462
  const absoluteMs = new Date(absoluteExpiresAt).getTime() - Date.now();
@@ -1408,7 +1466,7 @@ function oauthService({ strapi: strapi2 }) {
1408
1466
  }
1409
1467
  ctx.cookies.set("strapi_admin_refresh", refreshToken, cookieOptions);
1410
1468
  ctx.cookies.set("oidc_authenticated", "1", { ...cookieOptions, path: "/" });
1411
- const accessResult = await sessionManager("admin").generateAccessToken(refreshToken);
1469
+ const accessResult = await smAdmin.generateAccessToken(refreshToken);
1412
1470
  if ("error" in accessResult) {
1413
1471
  throw new Error(accessResult.error);
1414
1472
  }
@@ -1499,8 +1557,23 @@ function whitelistService({ strapi: strapi2 }) {
1499
1557
  const result = await getWhitelistQuery().findOne({
1500
1558
  where: { email }
1501
1559
  });
1502
- if (!result) throw new Error(errorMessages.WHITELIST_NOT_PRESENT);
1560
+ if (!result) throw new OidcError("whitelist_rejected", errorMessages.WHITELIST_NOT_PRESENT);
1503
1561
  return result;
1562
+ },
1563
+ async hasUser(email) {
1564
+ const row = await getWhitelistQuery().findOne({ where: { email }, select: ["id"] });
1565
+ return !!row;
1566
+ },
1567
+ async deleteAllUsers() {
1568
+ await getWhitelistQuery().deleteMany({});
1569
+ },
1570
+ async countAdminUsersByEmails(emails) {
1571
+ if (emails.length === 0) return 0;
1572
+ const rows = await strapi2.query("admin::user").findMany({
1573
+ where: { email: { $in: emails } },
1574
+ select: ["id"]
1575
+ });
1576
+ return rows.length;
1504
1577
  }
1505
1578
  };
1506
1579
  }
@@ -1535,7 +1608,10 @@ function auditLogService({ strapi: strapi2 }) {
1535
1608
  });
1536
1609
  }
1537
1610
  },
1538
- async find({ page = 1, pageSize = 25 } = {}) {
1611
+ async find({
1612
+ page = 1,
1613
+ pageSize = 25
1614
+ } = {}) {
1539
1615
  const result = await strapi2.db.query("plugin::strapi-plugin-oidc.audit-log").findPage({
1540
1616
  sort: { createdAt: "desc" },
1541
1617
  page,