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.
@@ -1,5 +1,6 @@
1
1
  import { randomUUID, randomBytes, createHash } from "node:crypto";
2
2
  import pkceChallenge from "pkce-challenge";
3
+ import { Readable } from "node:stream";
3
4
  import strapiUtils from "@strapi/utils";
4
5
  import generator from "generate-password";
5
6
  function register$1() {
@@ -30,6 +31,12 @@ function getRetentionDays() {
30
31
  function isAuditLogEnabled() {
31
32
  return getRetentionDays() !== 0;
32
33
  }
34
+ const PLUGIN_NAME = "strapi-plugin-oidc";
35
+ const getOauthService = () => strapi.plugin(PLUGIN_NAME).service("oauth");
36
+ const getRoleService = () => strapi.plugin(PLUGIN_NAME).service("role");
37
+ const getWhitelistService = () => strapi.plugin(PLUGIN_NAME).service("whitelist");
38
+ const getAuditLogService = () => strapi.plugin(PLUGIN_NAME).service("auditLog");
39
+ const getAdminUserService = () => strapi.service("admin::user");
33
40
  const AUTH_ROUTES = ["login", "register", "register-admin", "forgot-password", "reset-password"];
34
41
  async function bootstrap({ strapi: strapi2 }) {
35
42
  const adminUrl = strapi2.config.get("admin.url", "/admin");
@@ -41,7 +48,7 @@ async function bootstrap({ strapi: strapi2 }) {
41
48
  const isTokenRefresh = path === tokenRefreshPath;
42
49
  if (isAuthRoute && isPost || isTokenRefresh) {
43
50
  try {
44
- const whitelistService2 = strapi2.plugin("strapi-plugin-oidc").service("whitelist");
51
+ const whitelistService2 = getWhitelistService();
45
52
  const settings = await whitelistService2.getSettings();
46
53
  const enforceOIDC = resolveEnforceOIDC(strapi2, settings?.enforceOIDC);
47
54
  if (enforceOIDC && isAuthRoute && isPost) {
@@ -89,7 +96,7 @@ async function bootstrap({ strapi: strapi2 }) {
89
96
  const enforceOIDCConfig = getEnforceOIDCConfig(strapi2);
90
97
  if (enforceOIDCConfig !== null) {
91
98
  try {
92
- const whitelistService2 = strapi2.plugin("strapi-plugin-oidc").service("whitelist");
99
+ const whitelistService2 = getWhitelistService();
93
100
  const settings = await whitelistService2.getSettings();
94
101
  if (settings.enforceOIDC !== enforceOIDCConfig) {
95
102
  await whitelistService2.setSettings({ ...settings, enforceOIDC: enforceOIDCConfig });
@@ -119,7 +126,7 @@ async function bootstrap({ strapi: strapi2 }) {
119
126
  task: async () => {
120
127
  try {
121
128
  const retentionDays = getRetentionDays();
122
- await strapi2.plugin("strapi-plugin-oidc").service("auditLog").cleanup(retentionDays);
129
+ await getAuditLogService().cleanup(retentionDays);
123
130
  } catch (err) {
124
131
  strapi2.log.warn("[strapi-plugin-oidc] Audit log cleanup failed:", err.message);
125
132
  }
@@ -213,9 +220,14 @@ function getExpiredCookieOptions(strapi2, ctx) {
213
220
  function clearAuthCookies(strapi2, ctx) {
214
221
  const options2 = getExpiredCookieOptions(strapi2, ctx);
215
222
  ctx.cookies.set("strapi_admin_refresh", "", options2);
216
- ctx.cookies.set("oidc_authenticated", "", { ...options2, path: "/" });
217
- ctx.cookies.set("oidc_access_token", "", { ...options2, path: "/" });
218
- ctx.cookies.set("oidc_user_email", "", { ...options2, path: "/" });
223
+ const rootPathOptions = { ...options2, path: "/" };
224
+ for (const name of ["oidc_authenticated", "oidc_access_token", "oidc_user_email"]) {
225
+ ctx.cookies.set(name, "", rootPathOptions);
226
+ }
227
+ }
228
+ const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
229
+ function isValidEmail(email) {
230
+ return EMAIL_REGEX.test(email);
219
231
  }
220
232
  const errorCodes = {
221
233
  TOKEN_EXCHANGE_FAILED: "TOKEN_EXCHANGE_FAILED",
@@ -352,6 +364,53 @@ const userFacingMessages = {
352
364
  return en["user.signInError"];
353
365
  }
354
366
  };
367
+ class OidcError extends Error {
368
+ kind;
369
+ cause;
370
+ constructor(kind, message, cause) {
371
+ super(message);
372
+ this.name = "OidcError";
373
+ this.kind = kind;
374
+ this.cause = cause;
375
+ }
376
+ }
377
+ const OIDC_ERROR_DISPATCH = {
378
+ nonce_mismatch: { action: "nonce_mismatch", code: errorCodes.NONCE_MISMATCH },
379
+ token_exchange_failed: {
380
+ action: "token_exchange_failed",
381
+ code: errorCodes.TOKEN_EXCHANGE_FAILED
382
+ },
383
+ id_token_parse_failed: {
384
+ action: "login_failure",
385
+ code: errorCodes.ID_TOKEN_PARSE_FAILED,
386
+ key: "id_token_parse_failed"
387
+ },
388
+ userinfo_fetch_failed: {
389
+ action: "login_failure",
390
+ code: errorCodes.USERINFO_FETCH_FAILED,
391
+ key: "userinfo_fetch_failed"
392
+ },
393
+ user_creation_failed: {
394
+ action: "login_failure",
395
+ code: errorCodes.USER_CREATION_FAILED,
396
+ key: "user_creation_failed"
397
+ },
398
+ whitelist_rejected: {
399
+ action: "whitelist_rejected",
400
+ code: errorCodes.WHITELIST_CHECK_FAILED,
401
+ key: "whitelist_rejected"
402
+ },
403
+ invalid_email: {
404
+ action: "login_failure",
405
+ code: errorCodes.TOKEN_EXCHANGE_FAILED,
406
+ key: "sign_in_unknown"
407
+ },
408
+ unknown: {
409
+ action: "login_failure",
410
+ code: errorCodes.TOKEN_EXCHANGE_FAILED,
411
+ key: "sign_in_unknown"
412
+ }
413
+ };
355
414
  const REQUIRED_CONFIG_KEYS = [
356
415
  "OIDC_CLIENT_ID",
357
416
  "OIDC_CLIENT_SECRET",
@@ -364,6 +423,7 @@ const REQUIRED_CONFIG_KEYS = [
364
423
  "OIDC_GIVEN_NAME_FIELD",
365
424
  "OIDC_AUTHORIZATION_ENDPOINT"
366
425
  ];
426
+ const LOGOUT_USERINFO_TIMEOUT_MS = 3e3;
367
427
  function configValidation() {
368
428
  const config2 = strapi.config.get("plugin::strapi-plugin-oidc");
369
429
  const missing = REQUIRED_CONFIG_KEYS.filter((key) => !config2[key]);
@@ -381,7 +441,6 @@ async function oidcSignIn(ctx) {
381
441
  const cookieOptions = {
382
442
  httpOnly: true,
383
443
  maxAge: 6e5,
384
- // 10 minutes
385
444
  secure: isProduction && ctx.request.secure,
386
445
  sameSite: "lax"
387
446
  };
@@ -411,7 +470,7 @@ async function exchangeTokenAndFetchUserInfo(config2, params, expectedNonce) {
411
470
  }
412
471
  });
413
472
  if (!response.ok) {
414
- throw new Error(errorMessages.TOKEN_EXCHANGE_FAILED);
473
+ throw new OidcError("token_exchange_failed", errorMessages.TOKEN_EXCHANGE_FAILED);
415
474
  }
416
475
  const tokenData = await response.json();
417
476
  if (tokenData.id_token) {
@@ -419,23 +478,23 @@ async function exchangeTokenAndFetchUserInfo(config2, params, expectedNonce) {
419
478
  const payloadB64 = tokenData.id_token.split(".")[1];
420
479
  const idTokenPayload = JSON.parse(Buffer.from(payloadB64, "base64url").toString("utf8"));
421
480
  if (idTokenPayload.nonce !== expectedNonce) {
422
- throw new Error(errorMessages.NONCE_MISMATCH);
481
+ throw new OidcError("nonce_mismatch", errorMessages.NONCE_MISMATCH);
423
482
  }
424
483
  } catch (e) {
425
- if (e.message === "Nonce mismatch") throw e;
426
- throw new Error(errorMessages.ID_TOKEN_PARSE_FAILED);
484
+ if (e instanceof OidcError && e.kind === "nonce_mismatch") throw e;
485
+ throw new OidcError("id_token_parse_failed", errorMessages.ID_TOKEN_PARSE_FAILED, e);
427
486
  }
428
487
  }
429
488
  const userResponse = await fetch(config2.OIDC_USERINFO_ENDPOINT, {
430
489
  headers: { Authorization: `Bearer ${tokenData.access_token}` }
431
490
  });
432
491
  if (!userResponse.ok) {
433
- throw new Error(errorMessages.USERINFO_FETCH_FAILED);
492
+ throw new OidcError("userinfo_fetch_failed", errorMessages.USERINFO_FETCH_FAILED);
434
493
  }
435
494
  const userInfo = await userResponse.json();
436
495
  return { userInfo, accessToken: tokenData.access_token };
437
496
  }
438
- function resolveRolesFromGroups(userInfo, config2, availableRoles) {
497
+ function collectGroupMapRoleNames(userInfo, config2) {
439
498
  const rawGroups = userInfo[config2.OIDC_GROUP_FIELD];
440
499
  if (!Array.isArray(rawGroups) || rawGroups.length === 0) return [];
441
500
  const groups = rawGroups.filter((g) => typeof g === "string");
@@ -446,22 +505,15 @@ function resolveRolesFromGroups(userInfo, config2, availableRoles) {
446
505
  } catch {
447
506
  return [];
448
507
  }
449
- const roleIdSet = /* @__PURE__ */ new Set();
508
+ const roleNameSet = /* @__PURE__ */ new Set();
450
509
  for (const group of groups) {
451
510
  const roleNames = groupRoleMap[group];
452
511
  if (!roleNames) continue;
453
512
  for (const name of roleNames) {
454
- const match = availableRoles.find((r) => r.name === name);
455
- if (match) roleIdSet.add(String(match.id));
513
+ roleNameSet.add(name);
456
514
  }
457
515
  }
458
- return [...roleIdSet];
459
- }
460
- async function resolveRoles(userInfo, config2, roleService2, availableRoles) {
461
- const groupRoles = resolveRolesFromGroups(userInfo, config2, availableRoles);
462
- if (groupRoles.length > 0) return { roles: groupRoles, fromGroupMapping: true };
463
- const oidcRoles = await roleService2.oidcRoles();
464
- return { roles: oidcRoles?.roles || [], fromGroupMapping: false };
516
+ return [...roleNameSet];
465
517
  }
466
518
  async function registerNewUser(oauthService2, email, userResponseData, config2, ctx, roles2) {
467
519
  const defaultLocale = oauthService2.localeFindByHeader(
@@ -479,10 +531,7 @@ async function registerNewUser(oauthService2, email, userResponseData, config2,
479
531
  }
480
532
  function rolesChanged(current, next) {
481
533
  if (current.size !== next.size) return true;
482
- for (const id of next) {
483
- if (!current.has(id)) return true;
484
- }
485
- return false;
534
+ return [...next].some((id) => !current.has(id));
486
535
  }
487
536
  async function updateUserRoles(user, currentRoleIds, newRoleIds) {
488
537
  try {
@@ -508,27 +557,51 @@ async function updateUserRoles(user, currentRoleIds, newRoleIds) {
508
557
  async function handleUserAuthentication(userService, oauthService2, roleService2, whitelistService2, userResponseData, config2, ctx) {
509
558
  const rawEmail = String(userResponseData.email ?? "");
510
559
  const email = rawEmail.toLowerCase();
511
- if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
512
- throw new Error(errorMessages.INVALID_EMAIL);
560
+ if (!email || !isValidEmail(email)) {
561
+ throw new OidcError("invalid_email", errorMessages.INVALID_EMAIL);
513
562
  }
514
563
  await whitelistService2.checkWhitelistForEmail(email);
515
- const allRoles = await strapi.db.query("admin::role").findMany();
516
- const { roles: roles2, fromGroupMapping } = await resolveRoles(
517
- userResponseData,
518
- config2,
519
- roleService2,
520
- allRoles
521
- );
522
- const resolvedRoleNames = allRoles.filter((r) => roles2.includes(String(r.id))).map((r) => r.name);
564
+ const candidateNames = collectGroupMapRoleNames(userResponseData, config2);
565
+ let roles2 = [];
566
+ let fromGroupMapping = false;
567
+ let resolvedRoleNames = [];
568
+ if (candidateNames.length > 0) {
569
+ const matchedRoles = await strapi.db.query("admin::role").findMany({
570
+ where: { name: { $in: candidateNames } },
571
+ select: ["id", "name"]
572
+ });
573
+ const nameToId = new Map(matchedRoles.map((r) => [r.name, String(r.id)]));
574
+ for (const name of candidateNames) {
575
+ const id = nameToId.get(name);
576
+ if (id) roles2.push(id);
577
+ }
578
+ resolvedRoleNames = matchedRoles.map((r) => r.name);
579
+ fromGroupMapping = true;
580
+ } else {
581
+ const oidcRolesResult = await roleService2.oidcRoles();
582
+ roles2 = oidcRolesResult?.roles || [];
583
+ if (roles2.length > 0) {
584
+ const oidcRoleRecords = await strapi.db.query("admin::role").findMany({
585
+ where: { id: { $in: roles2.map(Number) } },
586
+ select: ["id", "name"]
587
+ });
588
+ resolvedRoleNames = oidcRoleRecords.map((r) => r.name);
589
+ }
590
+ }
523
591
  let userCreated = false;
524
592
  let rolesUpdated = false;
525
593
  let user = await userService.findOneByEmail(email, ["roles"]);
526
594
  if (!user) {
527
- user = await registerNewUser(oauthService2, email, userResponseData, config2, ctx, roles2);
595
+ try {
596
+ user = await registerNewUser(oauthService2, email, userResponseData, config2, ctx, roles2);
597
+ } catch (e) {
598
+ const msg = e instanceof Error ? e.message : String(e);
599
+ throw new OidcError("user_creation_failed", msg, e);
600
+ }
528
601
  userCreated = true;
529
602
  rolesUpdated = true;
530
603
  } else if (fromGroupMapping && roles2.length > 0) {
531
- const currentRoleIds = new Set(user.roles.map((r) => String(r.id)));
604
+ const currentRoleIds = new Set((user.roles ?? []).map((r) => String(r.id)));
532
605
  if (rolesChanged(currentRoleIds, new Set(roles2))) {
533
606
  await updateUserRoles(user, currentRoleIds, roles2);
534
607
  rolesUpdated = true;
@@ -538,55 +611,30 @@ async function handleUserAuthentication(userService, oauthService2, roleService2
538
611
  oauthService2.triggerSignInSuccess(user);
539
612
  return { activateUser: user, jwtToken, userCreated, rolesUpdated, resolvedRoleNames };
540
613
  }
541
- function classifyOidcError(msg, userInfo) {
542
- if (msg.includes("whitelist")) {
543
- return {
544
- action: "whitelist_rejected",
545
- code: errorCodes.WHITELIST_CHECK_FAILED,
546
- key: "whitelist_rejected"
547
- };
548
- }
549
- if (msg === "Nonce mismatch")
550
- return { action: "nonce_mismatch", code: errorCodes.NONCE_MISMATCH };
551
- if (msg === "Token exchange failed")
552
- return { action: "token_exchange_failed", code: errorCodes.TOKEN_EXCHANGE_FAILED };
553
- if (msg === "Failed to fetch user info") {
554
- return {
555
- action: "login_failure",
556
- code: errorCodes.USERINFO_FETCH_FAILED,
557
- key: "userinfo_fetch_failed"
558
- };
559
- }
560
- if (msg === "Failed to parse ID token") {
561
- return {
562
- action: "login_failure",
563
- code: errorCodes.ID_TOKEN_PARSE_FAILED,
564
- key: "id_token_parse_failed",
565
- params: { error: msg }
566
- };
567
- }
568
- if (msg === "User creation failed" || msg.includes("createUser")) {
569
- return {
570
- action: "login_failure",
571
- code: errorCodes.USER_CREATION_FAILED,
572
- key: "user_creation_failed",
573
- params: userInfo?.email ? { email: userInfo.email, error: msg } : void 0
574
- };
614
+ function classifyOidcError(e, userInfo) {
615
+ const kind = e instanceof OidcError ? e.kind : "unknown";
616
+ const dispatch = OIDC_ERROR_DISPATCH[kind];
617
+ const msg = e instanceof Error ? e.message : String(e);
618
+ let params;
619
+ if (kind === "id_token_parse_failed" || kind === "unknown") {
620
+ params = { error: msg };
621
+ } else if (kind === "user_creation_failed" && userInfo?.email) {
622
+ params = { email: userInfo.email, error: msg };
575
623
  }
576
624
  return {
577
- action: "login_failure",
578
- code: errorCodes.TOKEN_EXCHANGE_FAILED,
579
- key: "sign_in_unknown",
580
- params: { error: msg || "unknown" }
625
+ action: dispatch.action,
626
+ code: dispatch.code,
627
+ key: dispatch.key,
628
+ params
581
629
  };
582
630
  }
583
631
  async function oidcSignInCallback(ctx) {
584
632
  const config2 = configValidation();
585
- const userService = strapi.service("admin::user");
586
- const oauthService2 = strapi.plugin("strapi-plugin-oidc").service("oauth");
587
- const roleService2 = strapi.plugin("strapi-plugin-oidc").service("role");
588
- const whitelistService2 = strapi.plugin("strapi-plugin-oidc").service("whitelist");
589
- const auditLog2 = strapi.plugin("strapi-plugin-oidc").service("auditLog");
633
+ const userService = getAdminUserService();
634
+ const oauthService2 = getOauthService();
635
+ const roleService2 = getRoleService();
636
+ const whitelistService2 = getWhitelistService();
637
+ const auditLog2 = getAuditLogService();
590
638
  if (!ctx.query.code) {
591
639
  await auditLog2.log({ action: "missing_code", ip: ctx.ip });
592
640
  return ctx.send(oauthService2.renderSignUpError(userFacingMessages.missing_code));
@@ -618,7 +666,6 @@ async function oidcSignInCallback(ctx) {
618
666
  ctx.cookies.set("oidc_access_token", accessToken, {
619
667
  httpOnly: true,
620
668
  maxAge: 3e5,
621
- // 5 minutes — matches typical provider access token lifetime
622
669
  secure: isProduction && ctx.request.secure,
623
670
  sameSite: "lax"
624
671
  });
@@ -659,19 +706,18 @@ async function oidcSignInCallback(ctx) {
659
706
  ctx.set("Content-Security-Policy", `script-src 'nonce-${nonce}'`);
660
707
  ctx.send(html);
661
708
  } catch (e) {
662
- const msg = e.message ?? "";
663
- const errorInfo = classifyOidcError(msg, userInfo);
709
+ const errorInfo = classifyOidcError(e, userInfo);
664
710
  await auditLog2.log({
665
711
  action: errorInfo.action,
666
712
  email: userInfo?.email,
667
713
  ip: ctx.ip,
668
714
  detailsKey: errorInfo.action,
669
- detailsParams: errorInfo.action === "login_failure" ? { message: msg } : void 0
715
+ detailsParams: errorInfo.action === "login_failure" ? { message: e instanceof Error ? e.message : String(e) } : void 0
670
716
  });
671
717
  strapi.log.error({
672
718
  code: errorInfo.code,
673
719
  phase: "oidc_callback",
674
- message: msg || "Unknown sign-in error",
720
+ message: e instanceof Error ? e.message : "Unknown sign-in error",
675
721
  detail: errorInfo.key ? getErrorDetail(errorInfo.key, errorInfo.params) : void 0,
676
722
  email: userInfo?.email
677
723
  });
@@ -680,7 +726,7 @@ async function oidcSignInCallback(ctx) {
680
726
  }
681
727
  async function logout(ctx) {
682
728
  const config2 = strapi.config.get("plugin::strapi-plugin-oidc");
683
- const auditLog2 = strapi.plugin("strapi-plugin-oidc").service("auditLog");
729
+ const auditLog2 = getAuditLogService();
684
730
  const logoutUrl = config2.OIDC_END_SESSION_ENDPOINT;
685
731
  const adminPanelUrl = strapi.config.get("admin.url", "/admin");
686
732
  const isOidcSession = !!ctx.cookies.get("oidc_authenticated");
@@ -690,7 +736,8 @@ async function logout(ctx) {
690
736
  if (logoutUrl && isOidcSession && accessToken) {
691
737
  try {
692
738
  const response = await fetch(config2.OIDC_USERINFO_ENDPOINT, {
693
- headers: { Authorization: `Bearer ${accessToken}` }
739
+ headers: { Authorization: `Bearer ${accessToken}` },
740
+ signal: AbortSignal.timeout(LOGOUT_USERINFO_TIMEOUT_MS)
694
741
  });
695
742
  if (response.ok) {
696
743
  if (userEmail)
@@ -721,7 +768,7 @@ const oidc = {
721
768
  logout
722
769
  };
723
770
  async function find$1(ctx) {
724
- const roleService2 = strapi.plugin("strapi-plugin-oidc").service("role");
771
+ const roleService2 = getRoleService();
725
772
  const roles2 = await roleService2.find();
726
773
  const oidcConstants = roleService2.getOidcRoles();
727
774
  for (const oidc2 of oidcConstants) {
@@ -735,7 +782,7 @@ async function find$1(ctx) {
735
782
  async function update(ctx) {
736
783
  try {
737
784
  const { roles: roles2 } = ctx.request.body;
738
- const roleService2 = strapi.plugin("strapi-plugin-oidc").service("role");
785
+ const roleService2 = getRoleService();
739
786
  await roleService2.update(roles2);
740
787
  ctx.send({}, 204);
741
788
  } catch (e) {
@@ -756,8 +803,17 @@ function formatDatetimeForFilename(date) {
756
803
  const seconds = String(date.getSeconds()).padStart(2, "0");
757
804
  return `${year}${month}${day}_${hours}${minutes}${seconds}`;
758
805
  }
759
- function getWhitelistService() {
760
- return strapi.plugin("strapi-plugin-oidc").service("whitelist");
806
+ function setJsonAttachmentHeaders(ctx, basename) {
807
+ const datetime = formatDatetimeForFilename(/* @__PURE__ */ new Date());
808
+ ctx.set("Content-Type", "application/json");
809
+ ctx.set("Content-Disposition", `attachment; filename="${basename}-${datetime}.json"`);
810
+ }
811
+ function setNdjsonAttachmentHeaders(ctx, basename) {
812
+ const datetime = formatDatetimeForFilename(/* @__PURE__ */ new Date());
813
+ ctx.set("Content-Type", "application/x-ndjson; charset=utf-8");
814
+ ctx.set("Content-Disposition", `attachment; filename="${basename}-${datetime}.ndjson"`);
815
+ ctx.set("Cache-Control", "no-store");
816
+ ctx.set("X-Content-Type-Options", "nosniff");
761
817
  }
762
818
  async function info(ctx) {
763
819
  const whitelistService2 = getWhitelistService();
@@ -772,8 +828,9 @@ async function info(ctx) {
772
828
  };
773
829
  }
774
830
  async function updateSettings(ctx) {
775
- const { useWhitelist } = ctx.request.body;
776
- let { enforceOIDC } = ctx.request.body;
831
+ const body = ctx.request.body;
832
+ const { useWhitelist } = body;
833
+ let { enforceOIDC } = body;
777
834
  const whitelistService2 = getWhitelistService();
778
835
  if (useWhitelist && enforceOIDC) {
779
836
  const users = await whitelistService2.getUsers();
@@ -802,11 +859,9 @@ async function register(ctx) {
802
859
  const rawEmails = Array.isArray(email) ? email : email.split(",");
803
860
  const emailList = rawEmails.map((e) => String(e).trim().toLowerCase()).filter(Boolean);
804
861
  const whitelistService2 = getWhitelistService();
805
- let matchedExistingUsersCount = 0;
862
+ const matchedExistingUsersCount = await whitelistService2.countAdminUsersByEmails(emailList);
806
863
  for (const singleEmail of emailList) {
807
- const existingUser = await strapi.query("admin::user").findOne({ where: { email: singleEmail } });
808
- if (existingUser) matchedExistingUsersCount++;
809
- const alreadyWhitelisted = await strapi.query("plugin::strapi-plugin-oidc.whitelists").findOne({ where: { email: singleEmail } });
864
+ const alreadyWhitelisted = await whitelistService2.hasUser(singleEmail);
810
865
  if (!alreadyWhitelisted) {
811
866
  await whitelistService2.registerUser(singleEmail);
812
867
  }
@@ -820,13 +875,12 @@ async function removeEmail(ctx) {
820
875
  ctx.body = {};
821
876
  }
822
877
  async function deleteAll(ctx) {
823
- await strapi.query("plugin::strapi-plugin-oidc.whitelists").deleteMany({});
878
+ const whitelistService2 = getWhitelistService();
879
+ await whitelistService2.deleteAllUsers();
824
880
  ctx.body = {};
825
881
  }
826
882
  async function exportWhitelist(ctx) {
827
- const datetime = formatDatetimeForFilename(/* @__PURE__ */ new Date());
828
- ctx.set("Content-Type", "application/json");
829
- ctx.set("Content-Disposition", `attachment; filename="strapi-oidc-whitelist-${datetime}.json"`);
883
+ setJsonAttachmentHeaders(ctx, "strapi-oidc-whitelist");
830
884
  const whitelistService2 = getWhitelistService();
831
885
  const users = await whitelistService2.getUsers();
832
886
  ctx.body = users.map((u) => ({ email: u.email }));
@@ -838,7 +892,7 @@ async function importUsers(ctx) {
838
892
  ctx.body = { error: "Expected { users: [{email}] }" };
839
893
  return;
840
894
  }
841
- const normalized = users.filter((u) => u?.email).map((u) => String(u.email).trim().toLowerCase()).filter((email) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email));
895
+ const normalized = users.filter((u) => u?.email).map((u) => String(u.email).trim().toLowerCase()).filter(isValidEmail);
842
896
  const deduped = [...new Set(normalized)];
843
897
  const whitelistService2 = getWhitelistService();
844
898
  const existing = await whitelistService2.getUsers();
@@ -853,7 +907,7 @@ async function importUsers(ctx) {
853
907
  }
854
908
  async function syncUsers(ctx) {
855
909
  const { users: rawUsers } = ctx.request.body;
856
- const emails = rawUsers.map((u) => String(u.email).toLowerCase()).filter((e) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e));
910
+ const emails = rawUsers.map((u) => String(u.email).toLowerCase()).filter(isValidEmail);
857
911
  const whitelistService2 = getWhitelistService();
858
912
  const currentUsers = await whitelistService2.getUsers();
859
913
  const syncEmailSet = new Set(emails);
@@ -881,44 +935,49 @@ const whitelist = {
881
935
  importUsers,
882
936
  exportWhitelist
883
937
  };
884
- function getAuditLogService() {
885
- return strapi.plugin("strapi-plugin-oidc").service("auditLog");
886
- }
887
- async function find(ctx) {
888
- const page = Math.max(1, Number(ctx.query.page) || 1);
889
- const pageSize = Math.min(100, Math.max(1, Number(ctx.query.pageSize) || 25));
890
- ctx.body = await getAuditLogService().find({ page, pageSize });
891
- }
892
- async function exportLogs(ctx) {
893
- const datetime = formatDatetimeForFilename(/* @__PURE__ */ new Date());
894
- ctx.set("Content-Type", "application/json");
895
- ctx.set("Content-Disposition", `attachment; filename="strapi-oidc-audit-log-${datetime}.json"`);
896
- const service = getAuditLogService();
897
- const PAGE_SIZE = 1e3;
898
- const allRows = [];
938
+ const EXPORT_PAGE_SIZE = 500;
939
+ async function* ndjsonRowStream(service) {
899
940
  let page = 1;
900
941
  while (true) {
901
- const { results } = await service.find({ page, pageSize: PAGE_SIZE });
942
+ const { results } = await service.find({ page, pageSize: EXPORT_PAGE_SIZE });
943
+ if (results.length === 0) return;
944
+ let chunk = "";
902
945
  for (const row of results) {
903
- allRows.push({
904
- id: row.id,
905
- createdAt: row.createdAt,
946
+ chunk += JSON.stringify({
947
+ datetime: row.createdAt,
906
948
  action: row.action,
907
949
  email: row.email ?? null,
908
950
  ip: row.ip ?? null,
909
951
  details: row.details
910
- });
952
+ }) + "\n";
911
953
  }
912
- if (results.length < PAGE_SIZE) break;
954
+ yield Buffer.from(chunk, "utf8");
955
+ if (results.length < EXPORT_PAGE_SIZE) return;
913
956
  page++;
914
957
  }
915
- ctx.body = allRows.map((row) => ({
916
- datetime: row.createdAt,
917
- action: row.action,
918
- email: row.email,
919
- ip: row.ip,
920
- details: row.details
921
- }));
958
+ }
959
+ function errorAwareNdjsonStream(service) {
960
+ const gen = ndjsonRowStream(service);
961
+ const readable = Readable.from(gen);
962
+ readable.on("error", (err) => {
963
+ strapi$1.log.error({ phase: "audit_log_export", err }, "NDJSON export stream failed");
964
+ });
965
+ return readable;
966
+ }
967
+ let strapi$1;
968
+ function find(ctx) {
969
+ strapi$1 = ctx.strapi;
970
+ const page = Math.max(1, Number(ctx.query.page) || 1);
971
+ const pageSize = Math.min(100, Math.max(1, Number(ctx.query.pageSize) || 25));
972
+ return getAuditLogService().find({ page, pageSize }).then((result) => {
973
+ ctx.body = result;
974
+ });
975
+ }
976
+ async function exportLogs(ctx) {
977
+ strapi$1 = ctx.strapi;
978
+ setNdjsonAttachmentHeaders(ctx, "strapi-oidc-audit-log");
979
+ const service = getAuditLogService();
980
+ ctx.body = errorAwareNdjsonStream(service);
922
981
  }
923
982
  async function clearAll(ctx) {
924
983
  await getAuditLogService().clearAll();
@@ -1153,7 +1212,7 @@ function renderHtmlTemplate(title, content) {
1153
1212
  --icon-color: #d02b20;
1154
1213
  --success-bg: #eafbe7;
1155
1214
  --success-color: #328048;
1156
- --shadow: 0 1px 4px rgba(33, 33, 52, 0.1);
1215
+ --shadow: 0 1px 4 rgba(33, 33, 52, 0.1);
1157
1216
  }
1158
1217
  @media (prefers-color-scheme: dark) {
1159
1218
  :root {
@@ -1168,7 +1227,7 @@ function renderHtmlTemplate(title, content) {
1168
1227
  --icon-color: #f23628;
1169
1228
  --success-bg: #1c3523;
1170
1229
  --success-color: #55ca76;
1171
- --shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
1230
+ --shadow: 0 1px 4 rgba(0, 0, 0, 0.5);
1172
1231
  }
1173
1232
  }
1174
1233
  body {
@@ -1253,14 +1312,11 @@ function oauthService({ strapi: strapi2 }) {
1253
1312
  return {
1254
1313
  async createUser(email, lastname, firstname, locale, roles2 = []) {
1255
1314
  const userService = strapi2.service("admin::user");
1256
- if (/[A-Z]/.test(email)) {
1257
- const dbUser = await userService.findOneByEmail(email.toLocaleLowerCase());
1258
- if (dbUser) return dbUser;
1259
- }
1315
+ const normalizedEmail = email.toLowerCase();
1260
1316
  const createdUser = await userService.create({
1261
1317
  firstname: firstname || "unset",
1262
1318
  lastname: lastname || "",
1263
- email: email.toLocaleLowerCase(),
1319
+ email: normalizedEmail,
1264
1320
  roles: roles2,
1265
1321
  preferedLanguage: locale
1266
1322
  });
@@ -1291,35 +1347,35 @@ function oauthService({ strapi: strapi2 }) {
1291
1347
  },
1292
1348
  async triggerWebHook(user) {
1293
1349
  let ENTRY_CREATE;
1294
- const webhookStore = strapi2.serviceMap.get("webhookStore");
1295
- const eventHub = strapi2.serviceMap.get("eventHub");
1350
+ const webhookStore = strapi2.serviceMap?.get("webhookStore");
1351
+ const eventHub = strapi2.serviceMap?.get("eventHub");
1296
1352
  if (webhookStore) {
1297
1353
  ENTRY_CREATE = webhookStore.allowedEvents.get("ENTRY_CREATE");
1298
1354
  }
1299
1355
  const modelDef = strapi2.getModel("admin::user");
1300
1356
  const sanitizedEntity = await strapiUtils.sanitize.sanitizers.defaultSanitizeOutput(
1301
- {
1302
- schema: modelDef,
1303
- getModel: (uid2) => strapi2.getModel(uid2)
1304
- },
1357
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1358
+ { schema: modelDef, getModel: (uid2) => strapi2.getModel(uid2) },
1359
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1305
1360
  user
1306
1361
  );
1307
- eventHub.emit(ENTRY_CREATE, {
1362
+ eventHub?.emit(ENTRY_CREATE ?? "entry.create", {
1308
1363
  model: modelDef.modelName,
1309
1364
  entry: sanitizedEntity
1310
1365
  });
1311
1366
  },
1312
1367
  triggerSignInSuccess(user) {
1313
- delete user.password;
1314
- const eventHub = strapi2.serviceMap.get("eventHub");
1315
- eventHub.emit("admin.auth.success", {
1316
- user,
1368
+ const userCopy = { ...user };
1369
+ delete userCopy.password;
1370
+ const eventHub = strapi2.serviceMap?.get("eventHub");
1371
+ eventHub?.emit("admin.auth.success", {
1372
+ user: userCopy,
1317
1373
  provider: "strapi-plugin-oidc"
1318
1374
  });
1319
1375
  },
1320
1376
  renderSignUpSuccess(jwtToken, user, nonce) {
1321
1377
  const config2 = strapi2.config.get("plugin::strapi-plugin-oidc");
1322
- const isRememberMe = !!config2["REMEMBER_ME"];
1378
+ const isRememberMe = !!config2?.REMEMBER_ME;
1323
1379
  const content = `
1324
1380
  <noscript>
1325
1381
  <div class="card">
@@ -1370,12 +1426,15 @@ function oauthService({ strapi: strapi2 }) {
1370
1426
  const userId = String(user.id);
1371
1427
  const deviceId = randomUUID();
1372
1428
  const config2 = strapi2.config.get("plugin::strapi-plugin-oidc");
1373
- const rememberMe = !!config2["REMEMBER_ME"];
1374
- const { token: refreshToken, absoluteExpiresAt } = await sessionManager(
1375
- "admin"
1376
- ).generateRefreshToken(userId, deviceId, {
1377
- type: rememberMe ? "refresh" : "session"
1378
- });
1429
+ const rememberMe = !!config2?.REMEMBER_ME;
1430
+ const smAdmin = sessionManager("admin");
1431
+ const { token: refreshToken, absoluteExpiresAt } = await smAdmin.generateRefreshToken(
1432
+ userId,
1433
+ deviceId,
1434
+ {
1435
+ type: rememberMe ? "refresh" : "session"
1436
+ }
1437
+ );
1379
1438
  const isProduction = strapi2.config.get("environment") === "production";
1380
1439
  const domain = strapi2.config.get("admin.auth.cookie.domain") || strapi2.config.get("admin.auth.domain");
1381
1440
  const path = strapi2.config.get("admin.auth.cookie.path", "/admin");
@@ -1392,7 +1451,6 @@ function oauthService({ strapi: strapi2 }) {
1392
1451
  const idleLifespanSec = strapi2.config.get(
1393
1452
  "admin.auth.sessions.idleRefreshTokenLifespan",
1394
1453
  1209600
1395
- // 14 days — Strapi default
1396
1454
  );
1397
1455
  const idleMs = idleLifespanSec * 1e3;
1398
1456
  const absoluteMs = new Date(absoluteExpiresAt).getTime() - Date.now();
@@ -1402,7 +1460,7 @@ function oauthService({ strapi: strapi2 }) {
1402
1460
  }
1403
1461
  ctx.cookies.set("strapi_admin_refresh", refreshToken, cookieOptions);
1404
1462
  ctx.cookies.set("oidc_authenticated", "1", { ...cookieOptions, path: "/" });
1405
- const accessResult = await sessionManager("admin").generateAccessToken(refreshToken);
1463
+ const accessResult = await smAdmin.generateAccessToken(refreshToken);
1406
1464
  if ("error" in accessResult) {
1407
1465
  throw new Error(accessResult.error);
1408
1466
  }
@@ -1493,8 +1551,23 @@ function whitelistService({ strapi: strapi2 }) {
1493
1551
  const result = await getWhitelistQuery().findOne({
1494
1552
  where: { email }
1495
1553
  });
1496
- if (!result) throw new Error(errorMessages.WHITELIST_NOT_PRESENT);
1554
+ if (!result) throw new OidcError("whitelist_rejected", errorMessages.WHITELIST_NOT_PRESENT);
1497
1555
  return result;
1556
+ },
1557
+ async hasUser(email) {
1558
+ const row = await getWhitelistQuery().findOne({ where: { email }, select: ["id"] });
1559
+ return !!row;
1560
+ },
1561
+ async deleteAllUsers() {
1562
+ await getWhitelistQuery().deleteMany({});
1563
+ },
1564
+ async countAdminUsersByEmails(emails) {
1565
+ if (emails.length === 0) return 0;
1566
+ const rows = await strapi2.query("admin::user").findMany({
1567
+ where: { email: { $in: emails } },
1568
+ select: ["id"]
1569
+ });
1570
+ return rows.length;
1498
1571
  }
1499
1572
  };
1500
1573
  }
@@ -1529,7 +1602,10 @@ function auditLogService({ strapi: strapi2 }) {
1529
1602
  });
1530
1603
  }
1531
1604
  },
1532
- async find({ page = 1, pageSize = 25 } = {}) {
1605
+ async find({
1606
+ page = 1,
1607
+ pageSize = 25
1608
+ } = {}) {
1533
1609
  const result = await strapi2.db.query("plugin::strapi-plugin-oidc.audit-log").findPage({
1534
1610
  sort: { createdAt: "desc" },
1535
1611
  page,