strapi-plugin-oidc 1.6.6 → 1.7.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 +43 -13
- package/dist/admin/index-C2KZ4QxC.js +4381 -0
- package/dist/admin/{index-DVjS4hOr.js → index-DB7zjuHj.js} +22 -2
- package/dist/admin/{index-D2aMSVmR.mjs → index-D_ZKgByO.mjs} +22 -2
- package/dist/admin/index-UvfJxIgI.mjs +4379 -0
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/server/index.js +508 -170
- package/dist/server/index.mjs +508 -170
- package/package.json +3 -2
- package/dist/admin/index-CKyNupYU.mjs +0 -969
- package/dist/admin/index-DUFtPEHD.js +0 -971
package/dist/server/index.js
CHANGED
|
@@ -284,6 +284,7 @@ const en = {
|
|
|
284
284
|
"page.save.error": "Update failed.",
|
|
285
285
|
"page.add": "Add",
|
|
286
286
|
"page.cancel": "Cancel",
|
|
287
|
+
"common.remove": "Remove {label}",
|
|
287
288
|
"page.ok": "OK",
|
|
288
289
|
"roles.title": "Default Role(s)",
|
|
289
290
|
"roles.placeholder": "Select default role(s)",
|
|
@@ -315,7 +316,7 @@ const en = {
|
|
|
315
316
|
"enforce.config.info": "Enforcement is controlled by the OIDC_ENFORCE config variable and cannot be changed here.",
|
|
316
317
|
"login.settings.title": "Login Settings",
|
|
317
318
|
"login.sso": "Login via SSO",
|
|
318
|
-
"
|
|
319
|
+
"pagination.total": "{count, plural, one {# entry} other {# entries}}",
|
|
319
320
|
"whitelist.import": "Import",
|
|
320
321
|
"whitelist.export": "Export",
|
|
321
322
|
"whitelist.delete.all.label": "Delete All",
|
|
@@ -343,6 +344,20 @@ const en = {
|
|
|
343
344
|
"auditlog.clear.success": "Audit logs cleared",
|
|
344
345
|
"auditlog.clear.error": "Failed to clear audit logs",
|
|
345
346
|
"auditlog.export.error": "Failed to export audit logs",
|
|
347
|
+
"auditlog.filters": "Filters",
|
|
348
|
+
"auditlog.filters.action": "Action",
|
|
349
|
+
"auditlog.filters.email": "Email",
|
|
350
|
+
"auditlog.filters.ip": "IP address",
|
|
351
|
+
"auditlog.filters.createdAt": "Date",
|
|
352
|
+
"auditlog.filters.clear": "Clear filters",
|
|
353
|
+
"auditlog.filters.empty": "No entries match the current filters",
|
|
354
|
+
"auditlog.calendar.prevMonth": "Previous month",
|
|
355
|
+
"auditlog.calendar.nextMonth": "Next month",
|
|
356
|
+
"auditlog.calendar.state.today": "today",
|
|
357
|
+
"auditlog.calendar.state.selected": "selected",
|
|
358
|
+
"auditlog.calendar.state.alreadyAdded": "already added",
|
|
359
|
+
"auditlog.calendar.state.future": "unavailable, future date",
|
|
360
|
+
"auditlog.calendar.dayWithState": "{date}, {state}",
|
|
346
361
|
"auditlog.action.login_success": "User successfully authenticated via OIDC and was granted access.",
|
|
347
362
|
"auditlog.action.user_created": "A new Strapi admin account was created for this user on their first OIDC login.",
|
|
348
363
|
"auditlog.action.logout": "User logged out and their OIDC session was ended.",
|
|
@@ -353,23 +368,74 @@ const en = {
|
|
|
353
368
|
"auditlog.action.nonce_mismatch": "The nonce in the ID token did not match the one generated at login. This may indicate a token replay attack.",
|
|
354
369
|
"auditlog.action.token_exchange_failed": "The authorisation code could not be exchanged for tokens. The OIDC provider rejected the request.",
|
|
355
370
|
"auditlog.action.whitelist_rejected": "The user's email address is not on the whitelist. Access was denied.",
|
|
371
|
+
"auth.page.authenticating.title": "Authenticating...",
|
|
372
|
+
"auth.page.authenticating.noscript.heading": "JavaScript Required",
|
|
373
|
+
"auth.page.authenticating.noscript.body": "JavaScript must be enabled for authentication to complete.",
|
|
374
|
+
"auth.page.error.title": "Authentication Failed",
|
|
375
|
+
"auth.page.error.returnToLogin": "Return to Login",
|
|
356
376
|
"user.missing_code": "Authorisation code was not received from the OIDC provider.",
|
|
357
377
|
"user.invalid_state": "State parameter mismatch. Please restart the login flow.",
|
|
358
378
|
"user.signInError": "Authentication failed. Please try again.",
|
|
359
379
|
"settings.section": "OIDC",
|
|
360
380
|
"settings.configuration": "Configuration"
|
|
361
381
|
};
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
return
|
|
382
|
+
const __vite_glob_0_0 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
383
|
+
__proto__: null,
|
|
384
|
+
default: en
|
|
385
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
386
|
+
const modules = /* @__PURE__ */ Object.assign({ "../translations/locales/en.json": __vite_glob_0_0 });
|
|
387
|
+
const locales = Object.fromEntries(
|
|
388
|
+
Object.entries(modules).map(([path, mod]) => {
|
|
389
|
+
const code = path.match(/\/([^/]+)\.json$/)?.[1];
|
|
390
|
+
return [code ?? "", mod.default];
|
|
391
|
+
})
|
|
392
|
+
);
|
|
393
|
+
Object.keys(locales).filter(Boolean);
|
|
394
|
+
const DEFAULT_LOCALE = "en";
|
|
395
|
+
function parseAcceptLanguage(header) {
|
|
396
|
+
return header.split(",").map((part) => {
|
|
397
|
+
const [tag, ...params] = part.trim().split(";");
|
|
398
|
+
const qParam = params.find((p) => p.trim().startsWith("q="));
|
|
399
|
+
const q = qParam ? parseFloat(qParam.trim().slice(2)) : 1;
|
|
400
|
+
return { tag: tag.toLowerCase(), q: Number.isFinite(q) ? q : 1 };
|
|
401
|
+
}).filter((entry) => entry.tag).sort((a, b) => b.q - a.q);
|
|
402
|
+
}
|
|
403
|
+
function negotiateLocale(acceptLanguage) {
|
|
404
|
+
if (!acceptLanguage) return DEFAULT_LOCALE;
|
|
405
|
+
for (const { tag } of parseAcceptLanguage(acceptLanguage)) {
|
|
406
|
+
if (locales[tag]) return tag;
|
|
407
|
+
const base = tag.split("-")[0];
|
|
408
|
+
if (locales[base]) return base;
|
|
371
409
|
}
|
|
372
|
-
|
|
410
|
+
return DEFAULT_LOCALE;
|
|
411
|
+
}
|
|
412
|
+
function t(locale, key, fallback) {
|
|
413
|
+
return locales[locale]?.[key] ?? locales[DEFAULT_LOCALE]?.[key] ?? fallback ?? key;
|
|
414
|
+
}
|
|
415
|
+
const userFacingMessages = (locale) => ({
|
|
416
|
+
missing_code: t(
|
|
417
|
+
locale,
|
|
418
|
+
"user.missing_code",
|
|
419
|
+
"Authorisation code was not received from the OIDC provider."
|
|
420
|
+
),
|
|
421
|
+
invalid_state: t(
|
|
422
|
+
locale,
|
|
423
|
+
"user.invalid_state",
|
|
424
|
+
"State parameter mismatch. Please restart the login flow."
|
|
425
|
+
),
|
|
426
|
+
signInError: t(locale, "user.signInError", "Authentication failed. Please try again.")
|
|
427
|
+
});
|
|
428
|
+
const authPageMessages = (locale) => ({
|
|
429
|
+
authenticatingTitle: t(locale, "auth.page.authenticating.title", "Authenticating..."),
|
|
430
|
+
noscriptHeading: t(locale, "auth.page.authenticating.noscript.heading", "JavaScript Required"),
|
|
431
|
+
noscriptBody: t(
|
|
432
|
+
locale,
|
|
433
|
+
"auth.page.authenticating.noscript.body",
|
|
434
|
+
"JavaScript must be enabled for authentication to complete."
|
|
435
|
+
),
|
|
436
|
+
errorTitle: t(locale, "auth.page.error.title", "Authentication Failed"),
|
|
437
|
+
returnToLogin: t(locale, "auth.page.error.returnToLogin", "Return to Login")
|
|
438
|
+
});
|
|
373
439
|
class OidcError extends Error {
|
|
374
440
|
kind;
|
|
375
441
|
cause;
|
|
@@ -560,62 +626,99 @@ async function updateUserRoles(user, currentRoleIds, newRoleIds) {
|
|
|
560
626
|
throw updateErr;
|
|
561
627
|
}
|
|
562
628
|
}
|
|
563
|
-
async function
|
|
564
|
-
const
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
629
|
+
async function resolveRolesFromGroups(candidateNames) {
|
|
630
|
+
const matchedRoles = await strapi.db.query("admin::role").findMany({
|
|
631
|
+
where: { name: { $in: candidateNames } },
|
|
632
|
+
select: ["id", "name"]
|
|
633
|
+
});
|
|
634
|
+
const nameToId = new Map(matchedRoles.map((r) => [r.name, String(r.id)]));
|
|
635
|
+
const roles2 = [];
|
|
636
|
+
for (const name of candidateNames) {
|
|
637
|
+
const id = nameToId.get(name);
|
|
638
|
+
if (id) roles2.push(id);
|
|
568
639
|
}
|
|
569
|
-
|
|
640
|
+
return {
|
|
641
|
+
roles: roles2,
|
|
642
|
+
fromGroupMapping: true,
|
|
643
|
+
resolvedRoleNames: matchedRoles.map((r) => r.name)
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
async function resolveRolesFromDefaults(roleService2) {
|
|
647
|
+
const oidcRolesResult = await roleService2.oidcRoles();
|
|
648
|
+
const roles2 = oidcRolesResult?.roles || [];
|
|
649
|
+
if (roles2.length === 0) {
|
|
650
|
+
return { roles: roles2, fromGroupMapping: false, resolvedRoleNames: [] };
|
|
651
|
+
}
|
|
652
|
+
const records = await strapi.db.query("admin::role").findMany({
|
|
653
|
+
where: { id: { $in: roles2.map(Number) } },
|
|
654
|
+
select: ["id", "name"]
|
|
655
|
+
});
|
|
656
|
+
return {
|
|
657
|
+
roles: roles2,
|
|
658
|
+
fromGroupMapping: false,
|
|
659
|
+
resolvedRoleNames: records.map((r) => r.name)
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
async function resolveRoles(userResponseData, config2, roleService2) {
|
|
570
663
|
const candidateNames = collectGroupMapRoleNames(userResponseData, config2);
|
|
571
|
-
let roles2 = [];
|
|
572
|
-
let fromGroupMapping = false;
|
|
573
|
-
let resolvedRoleNames = [];
|
|
574
664
|
if (candidateNames.length > 0) {
|
|
575
|
-
|
|
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
|
-
}
|
|
665
|
+
return resolveRolesFromGroups(candidateNames);
|
|
596
666
|
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
667
|
+
return resolveRolesFromDefaults(roleService2);
|
|
668
|
+
}
|
|
669
|
+
async function ensureUser(userService, oauthService2, email, userResponseData, config2, ctx, resolved) {
|
|
670
|
+
const existing = await userService.findOneByEmail(email, ["roles"]);
|
|
671
|
+
if (!existing) {
|
|
601
672
|
try {
|
|
602
|
-
user = await registerNewUser(
|
|
673
|
+
const user = await registerNewUser(
|
|
674
|
+
oauthService2,
|
|
675
|
+
email,
|
|
676
|
+
userResponseData,
|
|
677
|
+
config2,
|
|
678
|
+
ctx,
|
|
679
|
+
resolved.roles
|
|
680
|
+
);
|
|
681
|
+
return { user, userCreated: true, rolesUpdated: true };
|
|
603
682
|
} catch (e) {
|
|
604
683
|
const msg = e instanceof Error ? e.message : String(e);
|
|
605
684
|
throw new OidcError("user_creation_failed", msg, e);
|
|
606
685
|
}
|
|
607
|
-
userCreated = true;
|
|
608
|
-
rolesUpdated = true;
|
|
609
|
-
} else if (fromGroupMapping && roles2.length > 0) {
|
|
610
|
-
const currentRoleIds = new Set((user.roles ?? []).map((r) => String(r.id)));
|
|
611
|
-
if (rolesChanged(currentRoleIds, new Set(roles2))) {
|
|
612
|
-
await updateUserRoles(user, currentRoleIds, roles2);
|
|
613
|
-
rolesUpdated = true;
|
|
614
|
-
}
|
|
615
686
|
}
|
|
687
|
+
if (!resolved.fromGroupMapping || resolved.roles.length === 0) {
|
|
688
|
+
return { user: existing, userCreated: false, rolesUpdated: false };
|
|
689
|
+
}
|
|
690
|
+
const currentRoleIds = new Set((existing.roles ?? []).map((r) => String(r.id)));
|
|
691
|
+
if (!rolesChanged(currentRoleIds, new Set(resolved.roles))) {
|
|
692
|
+
return { user: existing, userCreated: false, rolesUpdated: false };
|
|
693
|
+
}
|
|
694
|
+
await updateUserRoles(existing, currentRoleIds, resolved.roles);
|
|
695
|
+
return { user: existing, userCreated: false, rolesUpdated: true };
|
|
696
|
+
}
|
|
697
|
+
async function handleUserAuthentication(userService, oauthService2, roleService2, whitelistService2, userResponseData, config2, ctx) {
|
|
698
|
+
const email = String(userResponseData.email ?? "").toLowerCase();
|
|
699
|
+
if (!email || !isValidEmail(email)) {
|
|
700
|
+
throw new OidcError("invalid_email", errorMessages.INVALID_EMAIL);
|
|
701
|
+
}
|
|
702
|
+
await whitelistService2.checkWhitelistForEmail(email);
|
|
703
|
+
const resolved = await resolveRoles(userResponseData, config2, roleService2);
|
|
704
|
+
const { user, userCreated, rolesUpdated } = await ensureUser(
|
|
705
|
+
userService,
|
|
706
|
+
oauthService2,
|
|
707
|
+
email,
|
|
708
|
+
userResponseData,
|
|
709
|
+
config2,
|
|
710
|
+
ctx,
|
|
711
|
+
resolved
|
|
712
|
+
);
|
|
616
713
|
const jwtToken = await oauthService2.generateToken(user, ctx);
|
|
617
714
|
oauthService2.triggerSignInSuccess(user);
|
|
618
|
-
return {
|
|
715
|
+
return {
|
|
716
|
+
activateUser: user,
|
|
717
|
+
jwtToken,
|
|
718
|
+
userCreated,
|
|
719
|
+
rolesUpdated,
|
|
720
|
+
resolvedRoleNames: resolved.resolvedRoleNames
|
|
721
|
+
};
|
|
619
722
|
}
|
|
620
723
|
function classifyOidcError(e, userInfo) {
|
|
621
724
|
const kind = e instanceof OidcError ? e.kind : "unknown";
|
|
@@ -634,26 +737,76 @@ function classifyOidcError(e, userInfo) {
|
|
|
634
737
|
params
|
|
635
738
|
};
|
|
636
739
|
}
|
|
740
|
+
function readAndClearPkceCookies(ctx) {
|
|
741
|
+
const oidcState = ctx.cookies.get("oidc_state");
|
|
742
|
+
const codeVerifier = ctx.cookies.get("oidc_code_verifier");
|
|
743
|
+
const oidcNonce = ctx.cookies.get("oidc_nonce");
|
|
744
|
+
ctx.cookies.set("oidc_state", null);
|
|
745
|
+
ctx.cookies.set("oidc_code_verifier", null);
|
|
746
|
+
ctx.cookies.set("oidc_nonce", null);
|
|
747
|
+
return { oidcState, codeVerifier, oidcNonce };
|
|
748
|
+
}
|
|
749
|
+
async function logSuccessfulAuth(auditLog2, ctx, user, userCreated, rolesUpdated, resolvedRoleNames) {
|
|
750
|
+
const roles2 = resolvedRoleNames.join(", ");
|
|
751
|
+
const entries = [
|
|
752
|
+
auditLog2.log({
|
|
753
|
+
action: "login_success",
|
|
754
|
+
email: user.email,
|
|
755
|
+
ip: ctx.ip,
|
|
756
|
+
detailsKey: rolesUpdated ? "roles_updated" : void 0,
|
|
757
|
+
detailsParams: rolesUpdated ? { roles: roles2 } : void 0
|
|
758
|
+
})
|
|
759
|
+
];
|
|
760
|
+
if (userCreated) {
|
|
761
|
+
entries.push(
|
|
762
|
+
auditLog2.log({
|
|
763
|
+
action: "user_created",
|
|
764
|
+
email: user.email,
|
|
765
|
+
ip: ctx.ip,
|
|
766
|
+
detailsKey: "user_created",
|
|
767
|
+
detailsParams: { roles: roles2 }
|
|
768
|
+
})
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
await Promise.all(entries);
|
|
772
|
+
}
|
|
773
|
+
async function handleCallbackError(e, userInfo, auditLog2, oauthService2, ctx) {
|
|
774
|
+
const errorInfo = classifyOidcError(e, userInfo);
|
|
775
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
776
|
+
await auditLog2.log({
|
|
777
|
+
action: errorInfo.action,
|
|
778
|
+
email: userInfo?.email,
|
|
779
|
+
ip: ctx.ip,
|
|
780
|
+
detailsKey: errorInfo.action,
|
|
781
|
+
detailsParams: errorInfo.action === "login_failure" ? { message } : void 0
|
|
782
|
+
});
|
|
783
|
+
strapi.log.error({
|
|
784
|
+
code: errorInfo.code,
|
|
785
|
+
phase: "oidc_callback",
|
|
786
|
+
message: e instanceof Error ? e.message : "Unknown sign-in error",
|
|
787
|
+
detail: errorInfo.key ? getErrorDetail(errorInfo.key, errorInfo.params) : void 0,
|
|
788
|
+
email: userInfo?.email
|
|
789
|
+
});
|
|
790
|
+
const locale = negotiateLocale(ctx.request.headers["accept-language"]);
|
|
791
|
+
ctx.send(oauthService2.renderSignUpError(userFacingMessages(locale).signInError, locale));
|
|
792
|
+
}
|
|
637
793
|
async function oidcSignInCallback(ctx) {
|
|
638
794
|
const config2 = configValidation();
|
|
639
|
-
const userService = getAdminUserService();
|
|
640
795
|
const oauthService2 = getOauthService();
|
|
641
|
-
const roleService2 = getRoleService();
|
|
642
|
-
const whitelistService2 = getWhitelistService();
|
|
643
796
|
const auditLog2 = getAuditLogService();
|
|
797
|
+
const locale = negotiateLocale(ctx.request.headers["accept-language"]);
|
|
644
798
|
if (!ctx.query.code) {
|
|
645
799
|
await auditLog2.log({ action: "missing_code", ip: ctx.ip });
|
|
646
|
-
return ctx.send(
|
|
800
|
+
return ctx.send(
|
|
801
|
+
oauthService2.renderSignUpError(userFacingMessages(locale).missing_code, locale)
|
|
802
|
+
);
|
|
647
803
|
}
|
|
648
|
-
const oidcState = ctx
|
|
649
|
-
const codeVerifier = ctx.cookies.get("oidc_code_verifier");
|
|
650
|
-
const oidcNonce = ctx.cookies.get("oidc_nonce");
|
|
651
|
-
ctx.cookies.set("oidc_state", null);
|
|
652
|
-
ctx.cookies.set("oidc_code_verifier", null);
|
|
653
|
-
ctx.cookies.set("oidc_nonce", null);
|
|
804
|
+
const { oidcState, codeVerifier, oidcNonce } = readAndClearPkceCookies(ctx);
|
|
654
805
|
if (!ctx.query.state || ctx.query.state !== oidcState) {
|
|
655
806
|
await auditLog2.log({ action: "state_mismatch", ip: ctx.ip });
|
|
656
|
-
return ctx.send(
|
|
807
|
+
return ctx.send(
|
|
808
|
+
oauthService2.renderSignUpError(userFacingMessages(locale).invalid_state, locale)
|
|
809
|
+
);
|
|
657
810
|
}
|
|
658
811
|
const params = new URLSearchParams({
|
|
659
812
|
code: ctx.query.code,
|
|
@@ -667,67 +820,53 @@ async function oidcSignInCallback(ctx) {
|
|
|
667
820
|
try {
|
|
668
821
|
const exchangeResult = await exchangeTokenAndFetchUserInfo(config2, params, oidcNonce ?? "");
|
|
669
822
|
userInfo = exchangeResult.userInfo;
|
|
670
|
-
const accessToken = exchangeResult.accessToken;
|
|
671
823
|
const isProduction = strapi.config.get("environment") === "production";
|
|
672
|
-
ctx.
|
|
824
|
+
const secureFlag = isProduction && ctx.request.secure;
|
|
825
|
+
ctx.cookies.set("oidc_access_token", exchangeResult.accessToken, {
|
|
673
826
|
httpOnly: true,
|
|
674
827
|
maxAge: 3e5,
|
|
675
|
-
secure:
|
|
828
|
+
secure: secureFlag,
|
|
676
829
|
sameSite: "lax"
|
|
677
830
|
});
|
|
678
831
|
const { activateUser, jwtToken, userCreated, rolesUpdated, resolvedRoleNames } = await handleUserAuthentication(
|
|
679
|
-
|
|
832
|
+
getAdminUserService(),
|
|
680
833
|
oauthService2,
|
|
681
|
-
|
|
682
|
-
|
|
834
|
+
getRoleService(),
|
|
835
|
+
getWhitelistService(),
|
|
683
836
|
userInfo,
|
|
684
837
|
config2,
|
|
685
838
|
ctx
|
|
686
839
|
);
|
|
687
|
-
|
|
840
|
+
ctx.cookies.set("oidc_user_email", activateUser.email, {
|
|
688
841
|
httpOnly: true,
|
|
689
842
|
path: "/",
|
|
690
|
-
secure:
|
|
843
|
+
secure: secureFlag,
|
|
691
844
|
sameSite: "lax"
|
|
692
|
-
};
|
|
693
|
-
ctx.cookies.set("oidc_user_email", activateUser.email, identityCookieOptions);
|
|
694
|
-
if (userCreated) {
|
|
695
|
-
await auditLog2.log({
|
|
696
|
-
action: "user_created",
|
|
697
|
-
email: activateUser.email,
|
|
698
|
-
ip: ctx.ip,
|
|
699
|
-
detailsKey: "user_created",
|
|
700
|
-
detailsParams: { roles: resolvedRoleNames.join(", ") }
|
|
701
|
-
});
|
|
702
|
-
}
|
|
703
|
-
await auditLog2.log({
|
|
704
|
-
action: "login_success",
|
|
705
|
-
email: activateUser.email,
|
|
706
|
-
ip: ctx.ip,
|
|
707
|
-
detailsKey: rolesUpdated ? "roles_updated" : void 0,
|
|
708
|
-
detailsParams: rolesUpdated ? { roles: resolvedRoleNames.join(", ") } : void 0
|
|
709
845
|
});
|
|
846
|
+
await logSuccessfulAuth(
|
|
847
|
+
auditLog2,
|
|
848
|
+
ctx,
|
|
849
|
+
activateUser,
|
|
850
|
+
userCreated,
|
|
851
|
+
rolesUpdated,
|
|
852
|
+
resolvedRoleNames
|
|
853
|
+
);
|
|
710
854
|
const nonce = node_crypto.randomUUID();
|
|
711
|
-
const html = oauthService2.renderSignUpSuccess(jwtToken, activateUser, nonce);
|
|
712
855
|
ctx.set("Content-Security-Policy", `script-src 'nonce-${nonce}'`);
|
|
713
|
-
ctx.send(
|
|
856
|
+
ctx.send(oauthService2.renderSignUpSuccess(jwtToken, activateUser, nonce, locale));
|
|
714
857
|
} catch (e) {
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
strapi.log.error({
|
|
724
|
-
code: errorInfo.code,
|
|
725
|
-
phase: "oidc_callback",
|
|
726
|
-
message: e instanceof Error ? e.message : "Unknown sign-in error",
|
|
727
|
-
detail: errorInfo.key ? getErrorDetail(errorInfo.key, errorInfo.params) : void 0,
|
|
728
|
-
email: userInfo?.email
|
|
858
|
+
await handleCallbackError(e, userInfo, auditLog2, oauthService2, ctx);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
async function isProviderSessionActive(userinfoEndpoint, accessToken) {
|
|
862
|
+
try {
|
|
863
|
+
const response = await fetch(userinfoEndpoint, {
|
|
864
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
865
|
+
signal: AbortSignal.timeout(LOGOUT_USERINFO_TIMEOUT_MS)
|
|
729
866
|
});
|
|
730
|
-
|
|
867
|
+
return response.ok;
|
|
868
|
+
} catch {
|
|
869
|
+
return false;
|
|
731
870
|
}
|
|
732
871
|
}
|
|
733
872
|
async function logout(ctx) {
|
|
@@ -735,38 +874,27 @@ async function logout(ctx) {
|
|
|
735
874
|
const auditLog2 = getAuditLogService();
|
|
736
875
|
const logoutUrl = config2.OIDC_END_SESSION_ENDPOINT;
|
|
737
876
|
const adminPanelUrl = strapi.config.get("admin.url", "/admin");
|
|
877
|
+
const loginUrl = `${adminPanelUrl}/auth/login`;
|
|
738
878
|
const isOidcSession = !!ctx.cookies.get("oidc_authenticated");
|
|
739
879
|
const accessToken = ctx.cookies.get("oidc_access_token");
|
|
740
880
|
const userEmail = ctx.cookies.get("oidc_user_email") ?? void 0;
|
|
741
881
|
clearAuthCookies(strapi, ctx);
|
|
742
|
-
if (
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
882
|
+
if (!isOidcSession) {
|
|
883
|
+
return ctx.redirect(loginUrl);
|
|
884
|
+
}
|
|
885
|
+
const logAudit = (action) => userEmail ? auditLog2.log({ action, email: userEmail, ip: ctx.ip }) : Promise.resolve();
|
|
886
|
+
if (logoutUrl && accessToken) {
|
|
887
|
+
const active = await isProviderSessionActive(config2.OIDC_USERINFO_ENDPOINT, accessToken);
|
|
888
|
+
if (active) {
|
|
889
|
+
logAudit("logout").catch(() => {
|
|
747
890
|
});
|
|
748
|
-
|
|
749
|
-
if (userEmail)
|
|
750
|
-
auditLog2.log({ action: "logout", email: userEmail, ip: ctx.ip }).catch(() => {
|
|
751
|
-
});
|
|
752
|
-
return ctx.redirect(logoutUrl);
|
|
753
|
-
}
|
|
754
|
-
if (userEmail)
|
|
755
|
-
await auditLog2.log({ action: "session_expired", email: userEmail, ip: ctx.ip });
|
|
756
|
-
return ctx.redirect(`${adminPanelUrl}/auth/login`);
|
|
757
|
-
} catch {
|
|
758
|
-
if (userEmail)
|
|
759
|
-
await auditLog2.log({ action: "session_expired", email: userEmail, ip: ctx.ip });
|
|
760
|
-
return ctx.redirect(`${adminPanelUrl}/auth/login`);
|
|
891
|
+
return ctx.redirect(logoutUrl);
|
|
761
892
|
}
|
|
893
|
+
await logAudit("session_expired");
|
|
894
|
+
return ctx.redirect(loginUrl);
|
|
762
895
|
}
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
}
|
|
766
|
-
if (logoutUrl && isOidcSession) {
|
|
767
|
-
return ctx.redirect(logoutUrl);
|
|
768
|
-
}
|
|
769
|
-
ctx.redirect(`${adminPanelUrl}/auth/login`);
|
|
896
|
+
await logAudit("logout");
|
|
897
|
+
ctx.redirect(logoutUrl || loginUrl);
|
|
770
898
|
}
|
|
771
899
|
const oidc = {
|
|
772
900
|
oidcSignIn,
|
|
@@ -941,11 +1069,149 @@ const whitelist = {
|
|
|
941
1069
|
importUsers,
|
|
942
1070
|
exportWhitelist
|
|
943
1071
|
};
|
|
1072
|
+
const AUDIT_ACTIONS = [
|
|
1073
|
+
"login_success",
|
|
1074
|
+
"login_failure",
|
|
1075
|
+
"missing_code",
|
|
1076
|
+
"state_mismatch",
|
|
1077
|
+
"nonce_mismatch",
|
|
1078
|
+
"token_exchange_failed",
|
|
1079
|
+
"whitelist_rejected",
|
|
1080
|
+
"logout",
|
|
1081
|
+
"session_expired",
|
|
1082
|
+
"user_created"
|
|
1083
|
+
];
|
|
1084
|
+
const ISO_UTC_DATETIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
1085
|
+
function isIsoUtcDatetime(value) {
|
|
1086
|
+
return typeof value === "string" && ISO_UTC_DATETIME.test(value);
|
|
1087
|
+
}
|
|
1088
|
+
const ALLOWED_FIELDS = /* @__PURE__ */ new Set(["action", "email", "ip", "createdAt"]);
|
|
1089
|
+
const STRING_OPERATORS = /* @__PURE__ */ new Set([
|
|
1090
|
+
"$eq",
|
|
1091
|
+
"$contains",
|
|
1092
|
+
"$endsWith",
|
|
1093
|
+
"$null",
|
|
1094
|
+
"$notNull"
|
|
1095
|
+
]);
|
|
1096
|
+
const DATE_OPERATORS = /* @__PURE__ */ new Set(["$gte", "$lt", "$lte", "$between", "$in"]);
|
|
1097
|
+
const ENUM_OPERATORS = /* @__PURE__ */ new Set(["$eq", "$in"]);
|
|
1098
|
+
function isPlainObject(value) {
|
|
1099
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
1100
|
+
const proto = Object.getPrototypeOf(value);
|
|
1101
|
+
return proto === Object.prototype || proto === null;
|
|
1102
|
+
}
|
|
1103
|
+
function isStringOperator(op) {
|
|
1104
|
+
return STRING_OPERATORS.has(op);
|
|
1105
|
+
}
|
|
1106
|
+
function isDateOperator(op) {
|
|
1107
|
+
return DATE_OPERATORS.has(op);
|
|
1108
|
+
}
|
|
1109
|
+
function isEnumOperator(op) {
|
|
1110
|
+
return ENUM_OPERATORS.has(op);
|
|
1111
|
+
}
|
|
1112
|
+
function isAuditAction(value) {
|
|
1113
|
+
return AUDIT_ACTIONS.includes(value);
|
|
1114
|
+
}
|
|
1115
|
+
class ValidationError extends Error {
|
|
1116
|
+
constructor(message) {
|
|
1117
|
+
super(message);
|
|
1118
|
+
this.name = "ValidationError";
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
function requireType(field, op, value, check, expected) {
|
|
1122
|
+
if (!check) {
|
|
1123
|
+
throw new ValidationError(`Operator "${op}" for field "${field}" requires ${expected}`);
|
|
1124
|
+
}
|
|
1125
|
+
return value;
|
|
1126
|
+
}
|
|
1127
|
+
function parseActionOperator(op, opValue) {
|
|
1128
|
+
if (!isEnumOperator(op)) {
|
|
1129
|
+
throw new ValidationError(`Unknown operator "${op}" for field "action"`);
|
|
1130
|
+
}
|
|
1131
|
+
if (op === "$in") {
|
|
1132
|
+
requireType("action", op, opValue, Array.isArray(opValue), "an array value");
|
|
1133
|
+
for (const v of opValue) {
|
|
1134
|
+
if (!isAuditAction(v)) {
|
|
1135
|
+
throw new ValidationError(
|
|
1136
|
+
`Invalid action value "${v}" — must be one of: ${AUDIT_ACTIONS.join(", ")}`
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
return opValue;
|
|
1141
|
+
}
|
|
1142
|
+
if (!isAuditAction(opValue)) {
|
|
1143
|
+
throw new ValidationError(
|
|
1144
|
+
`Invalid action value "${opValue}" — must be one of: ${AUDIT_ACTIONS.join(", ")}`
|
|
1145
|
+
);
|
|
1146
|
+
}
|
|
1147
|
+
return opValue;
|
|
1148
|
+
}
|
|
1149
|
+
function parseCreatedAtOperator(op, opValue) {
|
|
1150
|
+
if (!isDateOperator(op)) {
|
|
1151
|
+
throw new ValidationError(`Unknown operator "${op}" for field "createdAt"`);
|
|
1152
|
+
}
|
|
1153
|
+
const expected = 'an ISO-8601 UTC datetime string (e.g. "2024-01-15T00:00:00.000Z")';
|
|
1154
|
+
if (op === "$between") {
|
|
1155
|
+
const isTuple = Array.isArray(opValue) && opValue.length === 2;
|
|
1156
|
+
requireType("createdAt", op, opValue, isTuple, "a tuple [start, end]");
|
|
1157
|
+
const [a, b] = opValue;
|
|
1158
|
+
requireType("createdAt", op, opValue, isIsoUtcDatetime(a) && isIsoUtcDatetime(b), expected);
|
|
1159
|
+
return opValue;
|
|
1160
|
+
}
|
|
1161
|
+
if (op === "$in") {
|
|
1162
|
+
requireType("createdAt", op, opValue, Array.isArray(opValue), "an array value");
|
|
1163
|
+
for (const v of opValue) {
|
|
1164
|
+
requireType("createdAt", op, v, isIsoUtcDatetime(v), expected);
|
|
1165
|
+
}
|
|
1166
|
+
return opValue;
|
|
1167
|
+
}
|
|
1168
|
+
return requireType("createdAt", op, opValue, isIsoUtcDatetime(opValue), expected);
|
|
1169
|
+
}
|
|
1170
|
+
function parseStringFieldOperator(field, op, opValue) {
|
|
1171
|
+
if (!isStringOperator(op)) {
|
|
1172
|
+
throw new ValidationError(`Unknown operator "${op}" for field "${field}"`);
|
|
1173
|
+
}
|
|
1174
|
+
if (op === "$null" || op === "$notNull") {
|
|
1175
|
+
return requireType(field, op, opValue, typeof opValue === "boolean", "a boolean value");
|
|
1176
|
+
}
|
|
1177
|
+
return requireType(field, op, opValue, typeof opValue === "string", "a string value");
|
|
1178
|
+
}
|
|
1179
|
+
function parseFieldOperators(field, fieldValue) {
|
|
1180
|
+
if (!isPlainObject(fieldValue)) {
|
|
1181
|
+
throw new ValidationError(
|
|
1182
|
+
`Filter field "${field}" must be an object of operators, got ${typeof fieldValue}`
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
const parsed = {};
|
|
1186
|
+
for (const [op, opValue] of Object.entries(fieldValue)) {
|
|
1187
|
+
if (field === "action") parsed[op] = parseActionOperator(op, opValue);
|
|
1188
|
+
else if (field === "createdAt") parsed[op] = parseCreatedAtOperator(op, opValue);
|
|
1189
|
+
else parsed[op] = parseStringFieldOperator(field, op, opValue);
|
|
1190
|
+
}
|
|
1191
|
+
return Object.keys(parsed).length > 0 ? parsed : null;
|
|
1192
|
+
}
|
|
1193
|
+
function parseAuditLogFilters(query) {
|
|
1194
|
+
if (!isPlainObject(query)) return {};
|
|
1195
|
+
const result = {};
|
|
1196
|
+
const filters = query.filters;
|
|
1197
|
+
if (filters === void 0) return result;
|
|
1198
|
+
if (!isPlainObject(filters)) {
|
|
1199
|
+
throw new ValidationError(`"filters" must be an object, got ${typeof filters}`);
|
|
1200
|
+
}
|
|
1201
|
+
for (const [field, fieldValue] of Object.entries(filters)) {
|
|
1202
|
+
if (!ALLOWED_FIELDS.has(field)) {
|
|
1203
|
+
throw new ValidationError(`Unknown filter field: "${field}"`);
|
|
1204
|
+
}
|
|
1205
|
+
const parsed = parseFieldOperators(field, fieldValue);
|
|
1206
|
+
if (parsed) result[field] = parsed;
|
|
1207
|
+
}
|
|
1208
|
+
return result;
|
|
1209
|
+
}
|
|
944
1210
|
const EXPORT_PAGE_SIZE = 500;
|
|
945
|
-
async function* ndjsonRowStream(service) {
|
|
1211
|
+
async function* ndjsonRowStream(service, filters) {
|
|
946
1212
|
let page = 1;
|
|
947
1213
|
while (true) {
|
|
948
|
-
const { results } = await service.find({ page, pageSize: EXPORT_PAGE_SIZE });
|
|
1214
|
+
const { results } = await service.find({ page, pageSize: EXPORT_PAGE_SIZE, filters });
|
|
949
1215
|
if (results.length === 0) return;
|
|
950
1216
|
let chunk = "";
|
|
951
1217
|
for (const row of results) {
|
|
@@ -962,28 +1228,35 @@ async function* ndjsonRowStream(service) {
|
|
|
962
1228
|
page++;
|
|
963
1229
|
}
|
|
964
1230
|
}
|
|
965
|
-
function errorAwareNdjsonStream(service) {
|
|
966
|
-
const gen = ndjsonRowStream(service);
|
|
1231
|
+
function errorAwareNdjsonStream(strapi2, service, filters) {
|
|
1232
|
+
const gen = ndjsonRowStream(service, filters);
|
|
967
1233
|
const readable = node_stream.Readable.from(gen);
|
|
968
1234
|
readable.on("error", (err) => {
|
|
969
|
-
|
|
1235
|
+
strapi2.log.error({ phase: "audit_log_export", err }, "NDJSON export stream failed");
|
|
970
1236
|
});
|
|
971
1237
|
return readable;
|
|
972
1238
|
}
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1239
|
+
function parseFiltersOr400(ctx) {
|
|
1240
|
+
try {
|
|
1241
|
+
return parseAuditLogFilters(ctx.query);
|
|
1242
|
+
} catch (err) {
|
|
1243
|
+
ctx.status = 400;
|
|
1244
|
+
ctx.body = { message: err instanceof ValidationError ? err.message : "Invalid filters" };
|
|
1245
|
+
return null;
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
async function find(ctx) {
|
|
1249
|
+
const filters = parseFiltersOr400(ctx);
|
|
1250
|
+
if (!filters) return;
|
|
976
1251
|
const page = Math.max(1, Number(ctx.query.page) || 1);
|
|
977
1252
|
const pageSize = Math.min(100, Math.max(1, Number(ctx.query.pageSize) || 25));
|
|
978
|
-
|
|
979
|
-
ctx.body = result;
|
|
980
|
-
});
|
|
1253
|
+
ctx.body = await getAuditLogService().find({ page, pageSize, filters });
|
|
981
1254
|
}
|
|
982
1255
|
async function exportLogs(ctx) {
|
|
983
|
-
|
|
1256
|
+
const filters = parseFiltersOr400(ctx);
|
|
1257
|
+
if (!filters) return;
|
|
984
1258
|
setNdjsonAttachmentHeaders(ctx, "strapi-oidc-audit-log");
|
|
985
|
-
|
|
986
|
-
ctx.body = errorAwareNdjsonStream(service);
|
|
1259
|
+
ctx.body = errorAwareNdjsonStream(ctx.strapi, getAuditLogService(), filters);
|
|
987
1260
|
}
|
|
988
1261
|
async function clearAll(ctx) {
|
|
989
1262
|
await getAuditLogService().clearAll();
|
|
@@ -1197,10 +1470,10 @@ const routes = {
|
|
|
1197
1470
|
}
|
|
1198
1471
|
};
|
|
1199
1472
|
const policies = {};
|
|
1200
|
-
function renderHtmlTemplate(title, content) {
|
|
1473
|
+
function renderHtmlTemplate(title, content, locale = "en") {
|
|
1201
1474
|
return `
|
|
1202
1475
|
<!doctype html>
|
|
1203
|
-
<html lang="
|
|
1476
|
+
<html lang="${locale}">
|
|
1204
1477
|
<head>
|
|
1205
1478
|
<meta charset="utf-8">
|
|
1206
1479
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
@@ -1379,9 +1652,10 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
1379
1652
|
provider: "strapi-plugin-oidc"
|
|
1380
1653
|
});
|
|
1381
1654
|
},
|
|
1382
|
-
renderSignUpSuccess(jwtToken, user, nonce) {
|
|
1655
|
+
renderSignUpSuccess(jwtToken, user, nonce, locale = "en") {
|
|
1383
1656
|
const config2 = strapi2.config.get("plugin::strapi-plugin-oidc");
|
|
1384
1657
|
const isRememberMe = !!config2?.REMEMBER_ME;
|
|
1658
|
+
const messages = authPageMessages(locale);
|
|
1385
1659
|
const content = `
|
|
1386
1660
|
<noscript>
|
|
1387
1661
|
<div class="card">
|
|
@@ -1390,8 +1664,8 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
1390
1664
|
<path d="M20 6 9 17l-5-5"/>
|
|
1391
1665
|
</svg>
|
|
1392
1666
|
</div>
|
|
1393
|
-
<h1
|
|
1394
|
-
<p
|
|
1667
|
+
<h1>${messages.noscriptHeading}</h1>
|
|
1668
|
+
<p>${messages.noscriptBody}</p>
|
|
1395
1669
|
</div>
|
|
1396
1670
|
</noscript>
|
|
1397
1671
|
<script nonce="${nonce}">
|
|
@@ -1405,9 +1679,10 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
1405
1679
|
location.href = '${strapi2.config.admin.url}'
|
|
1406
1680
|
})
|
|
1407
1681
|
<\/script>`;
|
|
1408
|
-
return renderHtmlTemplate(
|
|
1682
|
+
return renderHtmlTemplate(messages.authenticatingTitle, content, locale);
|
|
1409
1683
|
},
|
|
1410
|
-
renderSignUpError(message) {
|
|
1684
|
+
renderSignUpError(message, locale = "en") {
|
|
1685
|
+
const messages = authPageMessages(locale);
|
|
1411
1686
|
const safeMessage = String(message).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1412
1687
|
const content = `
|
|
1413
1688
|
<div class="card">
|
|
@@ -1418,11 +1693,11 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
1418
1693
|
<path d="M12 17h.01"/>
|
|
1419
1694
|
</svg>
|
|
1420
1695
|
</div>
|
|
1421
|
-
<h1
|
|
1696
|
+
<h1>${messages.errorTitle}</h1>
|
|
1422
1697
|
<p>${safeMessage}</p>
|
|
1423
|
-
<a href="${strapi2.config.admin.url}" class="btn"
|
|
1698
|
+
<a href="${strapi2.config.admin.url}" class="btn">${messages.returnToLogin}</a>
|
|
1424
1699
|
</div>`;
|
|
1425
|
-
return renderHtmlTemplate(
|
|
1700
|
+
return renderHtmlTemplate(messages.errorTitle, content, locale);
|
|
1426
1701
|
},
|
|
1427
1702
|
async generateToken(user, ctx) {
|
|
1428
1703
|
const sessionManager = strapi2.sessionManager;
|
|
@@ -1586,6 +1861,58 @@ function translateDetails(key, params) {
|
|
|
1586
1861
|
if (!translation) return null;
|
|
1587
1862
|
return interpolate(translation, params);
|
|
1588
1863
|
}
|
|
1864
|
+
const STRING_OP_MAP = {
|
|
1865
|
+
$eq: (v) => v,
|
|
1866
|
+
$contains: (v) => ({ $containsi: v }),
|
|
1867
|
+
$endsWith: (v) => ({ $endsWith: v }),
|
|
1868
|
+
$null: (v) => v === true ? null : void 0,
|
|
1869
|
+
$notNull: (v) => v === true ? { $notNull: true } : void 0
|
|
1870
|
+
};
|
|
1871
|
+
const DATE_OP_MAP = {
|
|
1872
|
+
$gte: (v) => ({ $gte: v }),
|
|
1873
|
+
$lt: (v) => ({ $lt: v }),
|
|
1874
|
+
$lte: (v) => ({ $lte: v }),
|
|
1875
|
+
$between: (v) => ({ $between: v })
|
|
1876
|
+
// $in is handled separately: each ISO day-start is expanded to a [day, day+1) range.
|
|
1877
|
+
};
|
|
1878
|
+
const DAY_MS = 864e5;
|
|
1879
|
+
function nextDayIso(iso) {
|
|
1880
|
+
return new Date(new Date(iso).getTime() + DAY_MS).toISOString();
|
|
1881
|
+
}
|
|
1882
|
+
function expandCreatedAtInToDayRanges(days) {
|
|
1883
|
+
const ranges = days.map((d) => ({ createdAt: { $gte: d, $lt: nextDayIso(d) } }));
|
|
1884
|
+
return ranges.length === 1 ? ranges[0] : { $or: ranges };
|
|
1885
|
+
}
|
|
1886
|
+
const ACTION_OP_MAP = {
|
|
1887
|
+
$eq: (v) => v,
|
|
1888
|
+
$in: (v) => ({ $in: v })
|
|
1889
|
+
};
|
|
1890
|
+
function mapFieldFilter(conditions, field, filter, opMap) {
|
|
1891
|
+
for (const [op, value] of Object.entries(filter)) {
|
|
1892
|
+
const transform = opMap[op];
|
|
1893
|
+
if (!transform) continue;
|
|
1894
|
+
const result = transform(value);
|
|
1895
|
+
if (result !== void 0) conditions.push({ [field]: result });
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
function buildWhereClause(filters) {
|
|
1899
|
+
const conditions = [];
|
|
1900
|
+
if (filters.action) mapFieldFilter(conditions, "action", filters.action, ACTION_OP_MAP);
|
|
1901
|
+
if (filters.email) mapFieldFilter(conditions, "email", filters.email, STRING_OP_MAP);
|
|
1902
|
+
if (filters.ip) mapFieldFilter(conditions, "ip", filters.ip, STRING_OP_MAP);
|
|
1903
|
+
if (filters.createdAt) {
|
|
1904
|
+
const { $in: inDays, ...rest } = filters.createdAt;
|
|
1905
|
+
if (Array.isArray(inDays) && inDays.length > 0) {
|
|
1906
|
+
conditions.push(expandCreatedAtInToDayRanges(inDays));
|
|
1907
|
+
}
|
|
1908
|
+
if (Object.keys(rest).length > 0) {
|
|
1909
|
+
mapFieldFilter(conditions, "createdAt", rest, DATE_OP_MAP);
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
if (conditions.length === 0) return {};
|
|
1913
|
+
if (conditions.length === 1) return conditions[0];
|
|
1914
|
+
return { $and: conditions };
|
|
1915
|
+
}
|
|
1589
1916
|
function auditLogService({ strapi: strapi2 }) {
|
|
1590
1917
|
return {
|
|
1591
1918
|
async log({ action, email, ip, detailsKey, detailsParams }) {
|
|
@@ -1610,20 +1937,31 @@ function auditLogService({ strapi: strapi2 }) {
|
|
|
1610
1937
|
},
|
|
1611
1938
|
async find({
|
|
1612
1939
|
page = 1,
|
|
1613
|
-
pageSize = 25
|
|
1940
|
+
pageSize = 25,
|
|
1941
|
+
filters
|
|
1614
1942
|
} = {}) {
|
|
1615
|
-
const
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1943
|
+
const where = filters ? buildWhereClause(filters) : {};
|
|
1944
|
+
const dbQuery = strapi2.db.query("plugin::strapi-plugin-oidc.audit-log");
|
|
1945
|
+
const [rows, total] = await Promise.all([
|
|
1946
|
+
dbQuery.findMany({
|
|
1947
|
+
where,
|
|
1948
|
+
orderBy: [{ createdAt: "desc" }],
|
|
1949
|
+
limit: pageSize,
|
|
1950
|
+
offset: (page - 1) * pageSize
|
|
1951
|
+
}),
|
|
1952
|
+
dbQuery.count({ where })
|
|
1953
|
+
]);
|
|
1624
1954
|
return {
|
|
1625
|
-
results
|
|
1626
|
-
|
|
1955
|
+
results: rows.map((row) => ({
|
|
1956
|
+
...row,
|
|
1957
|
+
details: row.detailsKey ? translateDetails(row.detailsKey, row.detailsParams) : null
|
|
1958
|
+
})),
|
|
1959
|
+
pagination: {
|
|
1960
|
+
page,
|
|
1961
|
+
pageSize,
|
|
1962
|
+
total,
|
|
1963
|
+
pageCount: Math.ceil(total / pageSize)
|
|
1964
|
+
}
|
|
1627
1965
|
};
|
|
1628
1966
|
},
|
|
1629
1967
|
async clearAll() {
|
|
@@ -1635,7 +1973,7 @@ function auditLogService({ strapi: strapi2 }) {
|
|
|
1635
1973
|
} while (deletedCount === BATCH_SIZE);
|
|
1636
1974
|
},
|
|
1637
1975
|
async cleanup(retentionDays) {
|
|
1638
|
-
const cutoff = new Date(Date.now() - retentionDays *
|
|
1976
|
+
const cutoff = new Date(Date.now() - retentionDays * DAY_MS);
|
|
1639
1977
|
await strapi2.db.query("plugin::strapi-plugin-oidc.audit-log").deleteMany({ where: { createdAt: { $lt: cutoff } } });
|
|
1640
1978
|
}
|
|
1641
1979
|
};
|