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