strapi-plugin-oidc 1.6.6 → 1.7.1
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 +526 -173
- package/dist/server/index.mjs +526 -173
- 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;
|
|
@@ -411,6 +477,21 @@ const OIDC_ERROR_DISPATCH = {
|
|
|
411
477
|
key: "sign_in_unknown"
|
|
412
478
|
}
|
|
413
479
|
};
|
|
480
|
+
function getClientIp(ctx) {
|
|
481
|
+
const cfConnectingIp = ctx.get("CF-Connecting-IP");
|
|
482
|
+
if (cfConnectingIp) {
|
|
483
|
+
return cfConnectingIp.split(",")[0].trim();
|
|
484
|
+
}
|
|
485
|
+
const forwardedFor = ctx.get("X-Forwarded-For");
|
|
486
|
+
if (forwardedFor) {
|
|
487
|
+
return forwardedFor.split(",")[0].trim();
|
|
488
|
+
}
|
|
489
|
+
const realIp = ctx.get("X-Real-IP");
|
|
490
|
+
if (realIp) {
|
|
491
|
+
return realIp.trim();
|
|
492
|
+
}
|
|
493
|
+
return ctx.ip;
|
|
494
|
+
}
|
|
414
495
|
const REQUIRED_CONFIG_KEYS = [
|
|
415
496
|
"OIDC_CLIENT_ID",
|
|
416
497
|
"OIDC_CLIENT_SECRET",
|
|
@@ -554,62 +635,99 @@ async function updateUserRoles(user, currentRoleIds, newRoleIds) {
|
|
|
554
635
|
throw updateErr;
|
|
555
636
|
}
|
|
556
637
|
}
|
|
557
|
-
async function
|
|
558
|
-
const
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
638
|
+
async function resolveRolesFromGroups(candidateNames) {
|
|
639
|
+
const matchedRoles = await strapi.db.query("admin::role").findMany({
|
|
640
|
+
where: { name: { $in: candidateNames } },
|
|
641
|
+
select: ["id", "name"]
|
|
642
|
+
});
|
|
643
|
+
const nameToId = new Map(matchedRoles.map((r) => [r.name, String(r.id)]));
|
|
644
|
+
const roles2 = [];
|
|
645
|
+
for (const name of candidateNames) {
|
|
646
|
+
const id = nameToId.get(name);
|
|
647
|
+
if (id) roles2.push(id);
|
|
562
648
|
}
|
|
563
|
-
|
|
649
|
+
return {
|
|
650
|
+
roles: roles2,
|
|
651
|
+
fromGroupMapping: true,
|
|
652
|
+
resolvedRoleNames: matchedRoles.map((r) => r.name)
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
async function resolveRolesFromDefaults(roleService2) {
|
|
656
|
+
const oidcRolesResult = await roleService2.oidcRoles();
|
|
657
|
+
const roles2 = oidcRolesResult?.roles || [];
|
|
658
|
+
if (roles2.length === 0) {
|
|
659
|
+
return { roles: roles2, fromGroupMapping: false, resolvedRoleNames: [] };
|
|
660
|
+
}
|
|
661
|
+
const records = await strapi.db.query("admin::role").findMany({
|
|
662
|
+
where: { id: { $in: roles2.map(Number) } },
|
|
663
|
+
select: ["id", "name"]
|
|
664
|
+
});
|
|
665
|
+
return {
|
|
666
|
+
roles: roles2,
|
|
667
|
+
fromGroupMapping: false,
|
|
668
|
+
resolvedRoleNames: records.map((r) => r.name)
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
async function resolveRoles(userResponseData, config2, roleService2) {
|
|
564
672
|
const candidateNames = collectGroupMapRoleNames(userResponseData, config2);
|
|
565
|
-
let roles2 = [];
|
|
566
|
-
let fromGroupMapping = false;
|
|
567
|
-
let resolvedRoleNames = [];
|
|
568
673
|
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
|
-
}
|
|
674
|
+
return resolveRolesFromGroups(candidateNames);
|
|
590
675
|
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
676
|
+
return resolveRolesFromDefaults(roleService2);
|
|
677
|
+
}
|
|
678
|
+
async function ensureUser(userService, oauthService2, email, userResponseData, config2, ctx, resolved) {
|
|
679
|
+
const existing = await userService.findOneByEmail(email, ["roles"]);
|
|
680
|
+
if (!existing) {
|
|
595
681
|
try {
|
|
596
|
-
user = await registerNewUser(
|
|
682
|
+
const user = await registerNewUser(
|
|
683
|
+
oauthService2,
|
|
684
|
+
email,
|
|
685
|
+
userResponseData,
|
|
686
|
+
config2,
|
|
687
|
+
ctx,
|
|
688
|
+
resolved.roles
|
|
689
|
+
);
|
|
690
|
+
return { user, userCreated: true, rolesUpdated: true };
|
|
597
691
|
} catch (e) {
|
|
598
692
|
const msg = e instanceof Error ? e.message : String(e);
|
|
599
693
|
throw new OidcError("user_creation_failed", msg, e);
|
|
600
694
|
}
|
|
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
695
|
}
|
|
696
|
+
if (!resolved.fromGroupMapping || resolved.roles.length === 0) {
|
|
697
|
+
return { user: existing, userCreated: false, rolesUpdated: false };
|
|
698
|
+
}
|
|
699
|
+
const currentRoleIds = new Set((existing.roles ?? []).map((r) => String(r.id)));
|
|
700
|
+
if (!rolesChanged(currentRoleIds, new Set(resolved.roles))) {
|
|
701
|
+
return { user: existing, userCreated: false, rolesUpdated: false };
|
|
702
|
+
}
|
|
703
|
+
await updateUserRoles(existing, currentRoleIds, resolved.roles);
|
|
704
|
+
return { user: existing, userCreated: false, rolesUpdated: true };
|
|
705
|
+
}
|
|
706
|
+
async function handleUserAuthentication(userService, oauthService2, roleService2, whitelistService2, userResponseData, config2, ctx) {
|
|
707
|
+
const email = String(userResponseData.email ?? "").toLowerCase();
|
|
708
|
+
if (!email || !isValidEmail(email)) {
|
|
709
|
+
throw new OidcError("invalid_email", errorMessages.INVALID_EMAIL);
|
|
710
|
+
}
|
|
711
|
+
await whitelistService2.checkWhitelistForEmail(email);
|
|
712
|
+
const resolved = await resolveRoles(userResponseData, config2, roleService2);
|
|
713
|
+
const { user, userCreated, rolesUpdated } = await ensureUser(
|
|
714
|
+
userService,
|
|
715
|
+
oauthService2,
|
|
716
|
+
email,
|
|
717
|
+
userResponseData,
|
|
718
|
+
config2,
|
|
719
|
+
ctx,
|
|
720
|
+
resolved
|
|
721
|
+
);
|
|
610
722
|
const jwtToken = await oauthService2.generateToken(user, ctx);
|
|
611
723
|
oauthService2.triggerSignInSuccess(user);
|
|
612
|
-
return {
|
|
724
|
+
return {
|
|
725
|
+
activateUser: user,
|
|
726
|
+
jwtToken,
|
|
727
|
+
userCreated,
|
|
728
|
+
rolesUpdated,
|
|
729
|
+
resolvedRoleNames: resolved.resolvedRoleNames
|
|
730
|
+
};
|
|
613
731
|
}
|
|
614
732
|
function classifyOidcError(e, userInfo) {
|
|
615
733
|
const kind = e instanceof OidcError ? e.kind : "unknown";
|
|
@@ -628,26 +746,76 @@ function classifyOidcError(e, userInfo) {
|
|
|
628
746
|
params
|
|
629
747
|
};
|
|
630
748
|
}
|
|
631
|
-
|
|
632
|
-
const config2 = configValidation();
|
|
633
|
-
const userService = getAdminUserService();
|
|
634
|
-
const oauthService2 = getOauthService();
|
|
635
|
-
const roleService2 = getRoleService();
|
|
636
|
-
const whitelistService2 = getWhitelistService();
|
|
637
|
-
const auditLog2 = getAuditLogService();
|
|
638
|
-
if (!ctx.query.code) {
|
|
639
|
-
await auditLog2.log({ action: "missing_code", ip: ctx.ip });
|
|
640
|
-
return ctx.send(oauthService2.renderSignUpError(userFacingMessages.missing_code));
|
|
641
|
-
}
|
|
749
|
+
function readAndClearPkceCookies(ctx) {
|
|
642
750
|
const oidcState = ctx.cookies.get("oidc_state");
|
|
643
751
|
const codeVerifier = ctx.cookies.get("oidc_code_verifier");
|
|
644
752
|
const oidcNonce = ctx.cookies.get("oidc_nonce");
|
|
645
753
|
ctx.cookies.set("oidc_state", null);
|
|
646
754
|
ctx.cookies.set("oidc_code_verifier", null);
|
|
647
755
|
ctx.cookies.set("oidc_nonce", null);
|
|
756
|
+
return { oidcState, codeVerifier, oidcNonce };
|
|
757
|
+
}
|
|
758
|
+
async function logSuccessfulAuth(auditLog2, ctx, user, userCreated, rolesUpdated, resolvedRoleNames) {
|
|
759
|
+
const roles2 = resolvedRoleNames.join(", ");
|
|
760
|
+
const entries = [
|
|
761
|
+
auditLog2.log({
|
|
762
|
+
action: "login_success",
|
|
763
|
+
email: user.email,
|
|
764
|
+
ip: getClientIp(ctx),
|
|
765
|
+
detailsKey: rolesUpdated ? "roles_updated" : void 0,
|
|
766
|
+
detailsParams: rolesUpdated ? { roles: roles2 } : void 0
|
|
767
|
+
})
|
|
768
|
+
];
|
|
769
|
+
if (userCreated) {
|
|
770
|
+
entries.push(
|
|
771
|
+
auditLog2.log({
|
|
772
|
+
action: "user_created",
|
|
773
|
+
email: user.email,
|
|
774
|
+
ip: getClientIp(ctx),
|
|
775
|
+
detailsKey: "user_created",
|
|
776
|
+
detailsParams: { roles: roles2 }
|
|
777
|
+
})
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
await Promise.all(entries);
|
|
781
|
+
}
|
|
782
|
+
async function handleCallbackError(e, userInfo, auditLog2, oauthService2, ctx) {
|
|
783
|
+
const errorInfo = classifyOidcError(e, userInfo);
|
|
784
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
785
|
+
await auditLog2.log({
|
|
786
|
+
action: errorInfo.action,
|
|
787
|
+
email: userInfo?.email,
|
|
788
|
+
ip: getClientIp(ctx),
|
|
789
|
+
detailsKey: errorInfo.action,
|
|
790
|
+
detailsParams: errorInfo.action === "login_failure" ? { message } : void 0
|
|
791
|
+
});
|
|
792
|
+
strapi.log.error({
|
|
793
|
+
code: errorInfo.code,
|
|
794
|
+
phase: "oidc_callback",
|
|
795
|
+
message: e instanceof Error ? e.message : "Unknown sign-in error",
|
|
796
|
+
detail: errorInfo.key ? getErrorDetail(errorInfo.key, errorInfo.params) : void 0,
|
|
797
|
+
email: userInfo?.email
|
|
798
|
+
});
|
|
799
|
+
const locale = negotiateLocale(ctx.request.headers["accept-language"]);
|
|
800
|
+
ctx.send(oauthService2.renderSignUpError(userFacingMessages(locale).signInError, locale));
|
|
801
|
+
}
|
|
802
|
+
async function oidcSignInCallback(ctx) {
|
|
803
|
+
const config2 = configValidation();
|
|
804
|
+
const oauthService2 = getOauthService();
|
|
805
|
+
const auditLog2 = getAuditLogService();
|
|
806
|
+
const locale = negotiateLocale(ctx.request.headers["accept-language"]);
|
|
807
|
+
if (!ctx.query.code) {
|
|
808
|
+
await auditLog2.log({ action: "missing_code", ip: getClientIp(ctx) });
|
|
809
|
+
return ctx.send(
|
|
810
|
+
oauthService2.renderSignUpError(userFacingMessages(locale).missing_code, locale)
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
const { oidcState, codeVerifier, oidcNonce } = readAndClearPkceCookies(ctx);
|
|
648
814
|
if (!ctx.query.state || ctx.query.state !== oidcState) {
|
|
649
|
-
await auditLog2.log({ action: "state_mismatch", ip: ctx
|
|
650
|
-
return ctx.send(
|
|
815
|
+
await auditLog2.log({ action: "state_mismatch", ip: getClientIp(ctx) });
|
|
816
|
+
return ctx.send(
|
|
817
|
+
oauthService2.renderSignUpError(userFacingMessages(locale).invalid_state, locale)
|
|
818
|
+
);
|
|
651
819
|
}
|
|
652
820
|
const params = new URLSearchParams({
|
|
653
821
|
code: ctx.query.code,
|
|
@@ -661,67 +829,53 @@ async function oidcSignInCallback(ctx) {
|
|
|
661
829
|
try {
|
|
662
830
|
const exchangeResult = await exchangeTokenAndFetchUserInfo(config2, params, oidcNonce ?? "");
|
|
663
831
|
userInfo = exchangeResult.userInfo;
|
|
664
|
-
const accessToken = exchangeResult.accessToken;
|
|
665
832
|
const isProduction = strapi.config.get("environment") === "production";
|
|
666
|
-
ctx.
|
|
833
|
+
const secureFlag = isProduction && ctx.request.secure;
|
|
834
|
+
ctx.cookies.set("oidc_access_token", exchangeResult.accessToken, {
|
|
667
835
|
httpOnly: true,
|
|
668
836
|
maxAge: 3e5,
|
|
669
|
-
secure:
|
|
837
|
+
secure: secureFlag,
|
|
670
838
|
sameSite: "lax"
|
|
671
839
|
});
|
|
672
840
|
const { activateUser, jwtToken, userCreated, rolesUpdated, resolvedRoleNames } = await handleUserAuthentication(
|
|
673
|
-
|
|
841
|
+
getAdminUserService(),
|
|
674
842
|
oauthService2,
|
|
675
|
-
|
|
676
|
-
|
|
843
|
+
getRoleService(),
|
|
844
|
+
getWhitelistService(),
|
|
677
845
|
userInfo,
|
|
678
846
|
config2,
|
|
679
847
|
ctx
|
|
680
848
|
);
|
|
681
|
-
|
|
849
|
+
ctx.cookies.set("oidc_user_email", activateUser.email, {
|
|
682
850
|
httpOnly: true,
|
|
683
851
|
path: "/",
|
|
684
|
-
secure:
|
|
852
|
+
secure: secureFlag,
|
|
685
853
|
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
854
|
});
|
|
855
|
+
await logSuccessfulAuth(
|
|
856
|
+
auditLog2,
|
|
857
|
+
ctx,
|
|
858
|
+
activateUser,
|
|
859
|
+
userCreated,
|
|
860
|
+
rolesUpdated,
|
|
861
|
+
resolvedRoleNames
|
|
862
|
+
);
|
|
704
863
|
const nonce = randomUUID();
|
|
705
|
-
const html = oauthService2.renderSignUpSuccess(jwtToken, activateUser, nonce);
|
|
706
864
|
ctx.set("Content-Security-Policy", `script-src 'nonce-${nonce}'`);
|
|
707
|
-
ctx.send(
|
|
865
|
+
ctx.send(oauthService2.renderSignUpSuccess(jwtToken, activateUser, nonce, locale));
|
|
708
866
|
} 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
|
|
867
|
+
await handleCallbackError(e, userInfo, auditLog2, oauthService2, ctx);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
async function isProviderSessionActive(userinfoEndpoint, accessToken) {
|
|
871
|
+
try {
|
|
872
|
+
const response = await fetch(userinfoEndpoint, {
|
|
873
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
874
|
+
signal: AbortSignal.timeout(LOGOUT_USERINFO_TIMEOUT_MS)
|
|
723
875
|
});
|
|
724
|
-
|
|
876
|
+
return response.ok;
|
|
877
|
+
} catch {
|
|
878
|
+
return false;
|
|
725
879
|
}
|
|
726
880
|
}
|
|
727
881
|
async function logout(ctx) {
|
|
@@ -729,38 +883,27 @@ async function logout(ctx) {
|
|
|
729
883
|
const auditLog2 = getAuditLogService();
|
|
730
884
|
const logoutUrl = config2.OIDC_END_SESSION_ENDPOINT;
|
|
731
885
|
const adminPanelUrl = strapi.config.get("admin.url", "/admin");
|
|
886
|
+
const loginUrl = `${adminPanelUrl}/auth/login`;
|
|
732
887
|
const isOidcSession = !!ctx.cookies.get("oidc_authenticated");
|
|
733
888
|
const accessToken = ctx.cookies.get("oidc_access_token");
|
|
734
889
|
const userEmail = ctx.cookies.get("oidc_user_email") ?? void 0;
|
|
735
890
|
clearAuthCookies(strapi, ctx);
|
|
736
|
-
if (
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
891
|
+
if (!isOidcSession) {
|
|
892
|
+
return ctx.redirect(loginUrl);
|
|
893
|
+
}
|
|
894
|
+
const logAudit = (action) => userEmail ? auditLog2.log({ action, email: userEmail, ip: getClientIp(ctx) }) : Promise.resolve();
|
|
895
|
+
if (logoutUrl && accessToken) {
|
|
896
|
+
const active = await isProviderSessionActive(config2.OIDC_USERINFO_ENDPOINT, accessToken);
|
|
897
|
+
if (active) {
|
|
898
|
+
logAudit("logout").catch(() => {
|
|
741
899
|
});
|
|
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`);
|
|
900
|
+
return ctx.redirect(logoutUrl);
|
|
755
901
|
}
|
|
902
|
+
await logAudit("session_expired");
|
|
903
|
+
return ctx.redirect(loginUrl);
|
|
756
904
|
}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
}
|
|
760
|
-
if (logoutUrl && isOidcSession) {
|
|
761
|
-
return ctx.redirect(logoutUrl);
|
|
762
|
-
}
|
|
763
|
-
ctx.redirect(`${adminPanelUrl}/auth/login`);
|
|
905
|
+
await logAudit("logout");
|
|
906
|
+
ctx.redirect(logoutUrl || loginUrl);
|
|
764
907
|
}
|
|
765
908
|
const oidc = {
|
|
766
909
|
oidcSignIn,
|
|
@@ -935,11 +1078,149 @@ const whitelist = {
|
|
|
935
1078
|
importUsers,
|
|
936
1079
|
exportWhitelist
|
|
937
1080
|
};
|
|
1081
|
+
const AUDIT_ACTIONS = [
|
|
1082
|
+
"login_success",
|
|
1083
|
+
"login_failure",
|
|
1084
|
+
"missing_code",
|
|
1085
|
+
"state_mismatch",
|
|
1086
|
+
"nonce_mismatch",
|
|
1087
|
+
"token_exchange_failed",
|
|
1088
|
+
"whitelist_rejected",
|
|
1089
|
+
"logout",
|
|
1090
|
+
"session_expired",
|
|
1091
|
+
"user_created"
|
|
1092
|
+
];
|
|
1093
|
+
const ISO_UTC_DATETIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
1094
|
+
function isIsoUtcDatetime(value) {
|
|
1095
|
+
return typeof value === "string" && ISO_UTC_DATETIME.test(value);
|
|
1096
|
+
}
|
|
1097
|
+
const ALLOWED_FIELDS = /* @__PURE__ */ new Set(["action", "email", "ip", "createdAt"]);
|
|
1098
|
+
const STRING_OPERATORS = /* @__PURE__ */ new Set([
|
|
1099
|
+
"$eq",
|
|
1100
|
+
"$contains",
|
|
1101
|
+
"$endsWith",
|
|
1102
|
+
"$null",
|
|
1103
|
+
"$notNull"
|
|
1104
|
+
]);
|
|
1105
|
+
const DATE_OPERATORS = /* @__PURE__ */ new Set(["$gte", "$lt", "$lte", "$between", "$in"]);
|
|
1106
|
+
const ENUM_OPERATORS = /* @__PURE__ */ new Set(["$eq", "$in"]);
|
|
1107
|
+
function isPlainObject(value) {
|
|
1108
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
1109
|
+
const proto = Object.getPrototypeOf(value);
|
|
1110
|
+
return proto === Object.prototype || proto === null;
|
|
1111
|
+
}
|
|
1112
|
+
function isStringOperator(op) {
|
|
1113
|
+
return STRING_OPERATORS.has(op);
|
|
1114
|
+
}
|
|
1115
|
+
function isDateOperator(op) {
|
|
1116
|
+
return DATE_OPERATORS.has(op);
|
|
1117
|
+
}
|
|
1118
|
+
function isEnumOperator(op) {
|
|
1119
|
+
return ENUM_OPERATORS.has(op);
|
|
1120
|
+
}
|
|
1121
|
+
function isAuditAction(value) {
|
|
1122
|
+
return AUDIT_ACTIONS.includes(value);
|
|
1123
|
+
}
|
|
1124
|
+
class ValidationError extends Error {
|
|
1125
|
+
constructor(message) {
|
|
1126
|
+
super(message);
|
|
1127
|
+
this.name = "ValidationError";
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
function requireType(field, op, value, check, expected) {
|
|
1131
|
+
if (!check) {
|
|
1132
|
+
throw new ValidationError(`Operator "${op}" for field "${field}" requires ${expected}`);
|
|
1133
|
+
}
|
|
1134
|
+
return value;
|
|
1135
|
+
}
|
|
1136
|
+
function parseActionOperator(op, opValue) {
|
|
1137
|
+
if (!isEnumOperator(op)) {
|
|
1138
|
+
throw new ValidationError(`Unknown operator "${op}" for field "action"`);
|
|
1139
|
+
}
|
|
1140
|
+
if (op === "$in") {
|
|
1141
|
+
requireType("action", op, opValue, Array.isArray(opValue), "an array value");
|
|
1142
|
+
for (const v of opValue) {
|
|
1143
|
+
if (!isAuditAction(v)) {
|
|
1144
|
+
throw new ValidationError(
|
|
1145
|
+
`Invalid action value "${v}" — must be one of: ${AUDIT_ACTIONS.join(", ")}`
|
|
1146
|
+
);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
return opValue;
|
|
1150
|
+
}
|
|
1151
|
+
if (!isAuditAction(opValue)) {
|
|
1152
|
+
throw new ValidationError(
|
|
1153
|
+
`Invalid action value "${opValue}" — must be one of: ${AUDIT_ACTIONS.join(", ")}`
|
|
1154
|
+
);
|
|
1155
|
+
}
|
|
1156
|
+
return opValue;
|
|
1157
|
+
}
|
|
1158
|
+
function parseCreatedAtOperator(op, opValue) {
|
|
1159
|
+
if (!isDateOperator(op)) {
|
|
1160
|
+
throw new ValidationError(`Unknown operator "${op}" for field "createdAt"`);
|
|
1161
|
+
}
|
|
1162
|
+
const expected = 'an ISO-8601 UTC datetime string (e.g. "2024-01-15T00:00:00.000Z")';
|
|
1163
|
+
if (op === "$between") {
|
|
1164
|
+
const isTuple = Array.isArray(opValue) && opValue.length === 2;
|
|
1165
|
+
requireType("createdAt", op, opValue, isTuple, "a tuple [start, end]");
|
|
1166
|
+
const [a, b] = opValue;
|
|
1167
|
+
requireType("createdAt", op, opValue, isIsoUtcDatetime(a) && isIsoUtcDatetime(b), expected);
|
|
1168
|
+
return opValue;
|
|
1169
|
+
}
|
|
1170
|
+
if (op === "$in") {
|
|
1171
|
+
requireType("createdAt", op, opValue, Array.isArray(opValue), "an array value");
|
|
1172
|
+
for (const v of opValue) {
|
|
1173
|
+
requireType("createdAt", op, v, isIsoUtcDatetime(v), expected);
|
|
1174
|
+
}
|
|
1175
|
+
return opValue;
|
|
1176
|
+
}
|
|
1177
|
+
return requireType("createdAt", op, opValue, isIsoUtcDatetime(opValue), expected);
|
|
1178
|
+
}
|
|
1179
|
+
function parseStringFieldOperator(field, op, opValue) {
|
|
1180
|
+
if (!isStringOperator(op)) {
|
|
1181
|
+
throw new ValidationError(`Unknown operator "${op}" for field "${field}"`);
|
|
1182
|
+
}
|
|
1183
|
+
if (op === "$null" || op === "$notNull") {
|
|
1184
|
+
return requireType(field, op, opValue, typeof opValue === "boolean", "a boolean value");
|
|
1185
|
+
}
|
|
1186
|
+
return requireType(field, op, opValue, typeof opValue === "string", "a string value");
|
|
1187
|
+
}
|
|
1188
|
+
function parseFieldOperators(field, fieldValue) {
|
|
1189
|
+
if (!isPlainObject(fieldValue)) {
|
|
1190
|
+
throw new ValidationError(
|
|
1191
|
+
`Filter field "${field}" must be an object of operators, got ${typeof fieldValue}`
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
1194
|
+
const parsed = {};
|
|
1195
|
+
for (const [op, opValue] of Object.entries(fieldValue)) {
|
|
1196
|
+
if (field === "action") parsed[op] = parseActionOperator(op, opValue);
|
|
1197
|
+
else if (field === "createdAt") parsed[op] = parseCreatedAtOperator(op, opValue);
|
|
1198
|
+
else parsed[op] = parseStringFieldOperator(field, op, opValue);
|
|
1199
|
+
}
|
|
1200
|
+
return Object.keys(parsed).length > 0 ? parsed : null;
|
|
1201
|
+
}
|
|
1202
|
+
function parseAuditLogFilters(query) {
|
|
1203
|
+
if (!isPlainObject(query)) return {};
|
|
1204
|
+
const result = {};
|
|
1205
|
+
const filters = query.filters;
|
|
1206
|
+
if (filters === void 0) return result;
|
|
1207
|
+
if (!isPlainObject(filters)) {
|
|
1208
|
+
throw new ValidationError(`"filters" must be an object, got ${typeof filters}`);
|
|
1209
|
+
}
|
|
1210
|
+
for (const [field, fieldValue] of Object.entries(filters)) {
|
|
1211
|
+
if (!ALLOWED_FIELDS.has(field)) {
|
|
1212
|
+
throw new ValidationError(`Unknown filter field: "${field}"`);
|
|
1213
|
+
}
|
|
1214
|
+
const parsed = parseFieldOperators(field, fieldValue);
|
|
1215
|
+
if (parsed) result[field] = parsed;
|
|
1216
|
+
}
|
|
1217
|
+
return result;
|
|
1218
|
+
}
|
|
938
1219
|
const EXPORT_PAGE_SIZE = 500;
|
|
939
|
-
async function* ndjsonRowStream(service) {
|
|
1220
|
+
async function* ndjsonRowStream(service, filters) {
|
|
940
1221
|
let page = 1;
|
|
941
1222
|
while (true) {
|
|
942
|
-
const { results } = await service.find({ page, pageSize: EXPORT_PAGE_SIZE });
|
|
1223
|
+
const { results } = await service.find({ page, pageSize: EXPORT_PAGE_SIZE, filters });
|
|
943
1224
|
if (results.length === 0) return;
|
|
944
1225
|
let chunk = "";
|
|
945
1226
|
for (const row of results) {
|
|
@@ -956,28 +1237,35 @@ async function* ndjsonRowStream(service) {
|
|
|
956
1237
|
page++;
|
|
957
1238
|
}
|
|
958
1239
|
}
|
|
959
|
-
function errorAwareNdjsonStream(service) {
|
|
960
|
-
const gen = ndjsonRowStream(service);
|
|
1240
|
+
function errorAwareNdjsonStream(strapi2, service, filters) {
|
|
1241
|
+
const gen = ndjsonRowStream(service, filters);
|
|
961
1242
|
const readable = Readable.from(gen);
|
|
962
1243
|
readable.on("error", (err) => {
|
|
963
|
-
|
|
1244
|
+
strapi2.log.error({ phase: "audit_log_export", err }, "NDJSON export stream failed");
|
|
964
1245
|
});
|
|
965
1246
|
return readable;
|
|
966
1247
|
}
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1248
|
+
function parseFiltersOr400(ctx) {
|
|
1249
|
+
try {
|
|
1250
|
+
return parseAuditLogFilters(ctx.query);
|
|
1251
|
+
} catch (err) {
|
|
1252
|
+
ctx.status = 400;
|
|
1253
|
+
ctx.body = { message: err instanceof ValidationError ? err.message : "Invalid filters" };
|
|
1254
|
+
return null;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
async function find(ctx) {
|
|
1258
|
+
const filters = parseFiltersOr400(ctx);
|
|
1259
|
+
if (!filters) return;
|
|
970
1260
|
const page = Math.max(1, Number(ctx.query.page) || 1);
|
|
971
1261
|
const pageSize = Math.min(100, Math.max(1, Number(ctx.query.pageSize) || 25));
|
|
972
|
-
|
|
973
|
-
ctx.body = result;
|
|
974
|
-
});
|
|
1262
|
+
ctx.body = await getAuditLogService().find({ page, pageSize, filters });
|
|
975
1263
|
}
|
|
976
1264
|
async function exportLogs(ctx) {
|
|
977
|
-
|
|
1265
|
+
const filters = parseFiltersOr400(ctx);
|
|
1266
|
+
if (!filters) return;
|
|
978
1267
|
setNdjsonAttachmentHeaders(ctx, "strapi-oidc-audit-log");
|
|
979
|
-
|
|
980
|
-
ctx.body = errorAwareNdjsonStream(service);
|
|
1268
|
+
ctx.body = errorAwareNdjsonStream(ctx.strapi, getAuditLogService(), filters);
|
|
981
1269
|
}
|
|
982
1270
|
async function clearAll(ctx) {
|
|
983
1271
|
await getAuditLogService().clearAll();
|
|
@@ -998,7 +1286,7 @@ const rateLimitMap = /* @__PURE__ */ new Map();
|
|
|
998
1286
|
const RATE_LIMIT_WINDOW = 6e4;
|
|
999
1287
|
const MAX_REQUESTS = 1e3;
|
|
1000
1288
|
function getRateLimitKey(ctx) {
|
|
1001
|
-
const ip = ctx
|
|
1289
|
+
const ip = getClientIp(ctx);
|
|
1002
1290
|
const ua = ctx.request.header["user-agent"] ?? "";
|
|
1003
1291
|
const uaHash = createHash("sha256").update(ua).digest("hex").slice(0, 16);
|
|
1004
1292
|
return `${ip}:${uaHash}`;
|
|
@@ -1191,10 +1479,10 @@ const routes = {
|
|
|
1191
1479
|
}
|
|
1192
1480
|
};
|
|
1193
1481
|
const policies = {};
|
|
1194
|
-
function renderHtmlTemplate(title, content) {
|
|
1482
|
+
function renderHtmlTemplate(title, content, locale = "en") {
|
|
1195
1483
|
return `
|
|
1196
1484
|
<!doctype html>
|
|
1197
|
-
<html lang="
|
|
1485
|
+
<html lang="${locale}">
|
|
1198
1486
|
<head>
|
|
1199
1487
|
<meta charset="utf-8">
|
|
1200
1488
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
@@ -1373,9 +1661,10 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
1373
1661
|
provider: "strapi-plugin-oidc"
|
|
1374
1662
|
});
|
|
1375
1663
|
},
|
|
1376
|
-
renderSignUpSuccess(jwtToken, user, nonce) {
|
|
1664
|
+
renderSignUpSuccess(jwtToken, user, nonce, locale = "en") {
|
|
1377
1665
|
const config2 = strapi2.config.get("plugin::strapi-plugin-oidc");
|
|
1378
1666
|
const isRememberMe = !!config2?.REMEMBER_ME;
|
|
1667
|
+
const messages = authPageMessages(locale);
|
|
1379
1668
|
const content = `
|
|
1380
1669
|
<noscript>
|
|
1381
1670
|
<div class="card">
|
|
@@ -1384,8 +1673,8 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
1384
1673
|
<path d="M20 6 9 17l-5-5"/>
|
|
1385
1674
|
</svg>
|
|
1386
1675
|
</div>
|
|
1387
|
-
<h1
|
|
1388
|
-
<p
|
|
1676
|
+
<h1>${messages.noscriptHeading}</h1>
|
|
1677
|
+
<p>${messages.noscriptBody}</p>
|
|
1389
1678
|
</div>
|
|
1390
1679
|
</noscript>
|
|
1391
1680
|
<script nonce="${nonce}">
|
|
@@ -1399,9 +1688,10 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
1399
1688
|
location.href = '${strapi2.config.admin.url}'
|
|
1400
1689
|
})
|
|
1401
1690
|
<\/script>`;
|
|
1402
|
-
return renderHtmlTemplate(
|
|
1691
|
+
return renderHtmlTemplate(messages.authenticatingTitle, content, locale);
|
|
1403
1692
|
},
|
|
1404
|
-
renderSignUpError(message) {
|
|
1693
|
+
renderSignUpError(message, locale = "en") {
|
|
1694
|
+
const messages = authPageMessages(locale);
|
|
1405
1695
|
const safeMessage = String(message).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1406
1696
|
const content = `
|
|
1407
1697
|
<div class="card">
|
|
@@ -1412,11 +1702,11 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
1412
1702
|
<path d="M12 17h.01"/>
|
|
1413
1703
|
</svg>
|
|
1414
1704
|
</div>
|
|
1415
|
-
<h1
|
|
1705
|
+
<h1>${messages.errorTitle}</h1>
|
|
1416
1706
|
<p>${safeMessage}</p>
|
|
1417
|
-
<a href="${strapi2.config.admin.url}" class="btn"
|
|
1707
|
+
<a href="${strapi2.config.admin.url}" class="btn">${messages.returnToLogin}</a>
|
|
1418
1708
|
</div>`;
|
|
1419
|
-
return renderHtmlTemplate(
|
|
1709
|
+
return renderHtmlTemplate(messages.errorTitle, content, locale);
|
|
1420
1710
|
},
|
|
1421
1711
|
async generateToken(user, ctx) {
|
|
1422
1712
|
const sessionManager = strapi2.sessionManager;
|
|
@@ -1580,6 +1870,58 @@ function translateDetails(key, params) {
|
|
|
1580
1870
|
if (!translation) return null;
|
|
1581
1871
|
return interpolate(translation, params);
|
|
1582
1872
|
}
|
|
1873
|
+
const STRING_OP_MAP = {
|
|
1874
|
+
$eq: (v) => v,
|
|
1875
|
+
$contains: (v) => ({ $containsi: v }),
|
|
1876
|
+
$endsWith: (v) => ({ $endsWith: v }),
|
|
1877
|
+
$null: (v) => v === true ? null : void 0,
|
|
1878
|
+
$notNull: (v) => v === true ? { $notNull: true } : void 0
|
|
1879
|
+
};
|
|
1880
|
+
const DATE_OP_MAP = {
|
|
1881
|
+
$gte: (v) => ({ $gte: v }),
|
|
1882
|
+
$lt: (v) => ({ $lt: v }),
|
|
1883
|
+
$lte: (v) => ({ $lte: v }),
|
|
1884
|
+
$between: (v) => ({ $between: v })
|
|
1885
|
+
// $in is handled separately: each ISO day-start is expanded to a [day, day+1) range.
|
|
1886
|
+
};
|
|
1887
|
+
const DAY_MS = 864e5;
|
|
1888
|
+
function nextDayIso(iso) {
|
|
1889
|
+
return new Date(new Date(iso).getTime() + DAY_MS).toISOString();
|
|
1890
|
+
}
|
|
1891
|
+
function expandCreatedAtInToDayRanges(days) {
|
|
1892
|
+
const ranges = days.map((d) => ({ createdAt: { $gte: d, $lt: nextDayIso(d) } }));
|
|
1893
|
+
return ranges.length === 1 ? ranges[0] : { $or: ranges };
|
|
1894
|
+
}
|
|
1895
|
+
const ACTION_OP_MAP = {
|
|
1896
|
+
$eq: (v) => v,
|
|
1897
|
+
$in: (v) => ({ $in: v })
|
|
1898
|
+
};
|
|
1899
|
+
function mapFieldFilter(conditions, field, filter, opMap) {
|
|
1900
|
+
for (const [op, value] of Object.entries(filter)) {
|
|
1901
|
+
const transform = opMap[op];
|
|
1902
|
+
if (!transform) continue;
|
|
1903
|
+
const result = transform(value);
|
|
1904
|
+
if (result !== void 0) conditions.push({ [field]: result });
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
function buildWhereClause(filters) {
|
|
1908
|
+
const conditions = [];
|
|
1909
|
+
if (filters.action) mapFieldFilter(conditions, "action", filters.action, ACTION_OP_MAP);
|
|
1910
|
+
if (filters.email) mapFieldFilter(conditions, "email", filters.email, STRING_OP_MAP);
|
|
1911
|
+
if (filters.ip) mapFieldFilter(conditions, "ip", filters.ip, STRING_OP_MAP);
|
|
1912
|
+
if (filters.createdAt) {
|
|
1913
|
+
const { $in: inDays, ...rest } = filters.createdAt;
|
|
1914
|
+
if (Array.isArray(inDays) && inDays.length > 0) {
|
|
1915
|
+
conditions.push(expandCreatedAtInToDayRanges(inDays));
|
|
1916
|
+
}
|
|
1917
|
+
if (Object.keys(rest).length > 0) {
|
|
1918
|
+
mapFieldFilter(conditions, "createdAt", rest, DATE_OP_MAP);
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
if (conditions.length === 0) return {};
|
|
1922
|
+
if (conditions.length === 1) return conditions[0];
|
|
1923
|
+
return { $and: conditions };
|
|
1924
|
+
}
|
|
1583
1925
|
function auditLogService({ strapi: strapi2 }) {
|
|
1584
1926
|
return {
|
|
1585
1927
|
async log({ action, email, ip, detailsKey, detailsParams }) {
|
|
@@ -1604,20 +1946,31 @@ function auditLogService({ strapi: strapi2 }) {
|
|
|
1604
1946
|
},
|
|
1605
1947
|
async find({
|
|
1606
1948
|
page = 1,
|
|
1607
|
-
pageSize = 25
|
|
1949
|
+
pageSize = 25,
|
|
1950
|
+
filters
|
|
1608
1951
|
} = {}) {
|
|
1609
|
-
const
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1952
|
+
const where = filters ? buildWhereClause(filters) : {};
|
|
1953
|
+
const dbQuery = strapi2.db.query("plugin::strapi-plugin-oidc.audit-log");
|
|
1954
|
+
const [rows, total] = await Promise.all([
|
|
1955
|
+
dbQuery.findMany({
|
|
1956
|
+
where,
|
|
1957
|
+
orderBy: [{ createdAt: "desc" }],
|
|
1958
|
+
limit: pageSize,
|
|
1959
|
+
offset: (page - 1) * pageSize
|
|
1960
|
+
}),
|
|
1961
|
+
dbQuery.count({ where })
|
|
1962
|
+
]);
|
|
1618
1963
|
return {
|
|
1619
|
-
results
|
|
1620
|
-
|
|
1964
|
+
results: rows.map((row) => ({
|
|
1965
|
+
...row,
|
|
1966
|
+
details: row.detailsKey ? translateDetails(row.detailsKey, row.detailsParams) : null
|
|
1967
|
+
})),
|
|
1968
|
+
pagination: {
|
|
1969
|
+
page,
|
|
1970
|
+
pageSize,
|
|
1971
|
+
total,
|
|
1972
|
+
pageCount: Math.ceil(total / pageSize)
|
|
1973
|
+
}
|
|
1621
1974
|
};
|
|
1622
1975
|
},
|
|
1623
1976
|
async clearAll() {
|
|
@@ -1629,7 +1982,7 @@ function auditLogService({ strapi: strapi2 }) {
|
|
|
1629
1982
|
} while (deletedCount === BATCH_SIZE);
|
|
1630
1983
|
},
|
|
1631
1984
|
async cleanup(retentionDays) {
|
|
1632
|
-
const cutoff = new Date(Date.now() - retentionDays *
|
|
1985
|
+
const cutoff = new Date(Date.now() - retentionDays * DAY_MS);
|
|
1633
1986
|
await strapi2.db.query("plugin::strapi-plugin-oidc.audit-log").deleteMany({ where: { createdAt: { $lt: cutoff } } });
|
|
1634
1987
|
}
|
|
1635
1988
|
};
|