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.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;
|
|
@@ -417,6 +483,21 @@ const OIDC_ERROR_DISPATCH = {
|
|
|
417
483
|
key: "sign_in_unknown"
|
|
418
484
|
}
|
|
419
485
|
};
|
|
486
|
+
function getClientIp(ctx) {
|
|
487
|
+
const cfConnectingIp = ctx.get("CF-Connecting-IP");
|
|
488
|
+
if (cfConnectingIp) {
|
|
489
|
+
return cfConnectingIp.split(",")[0].trim();
|
|
490
|
+
}
|
|
491
|
+
const forwardedFor = ctx.get("X-Forwarded-For");
|
|
492
|
+
if (forwardedFor) {
|
|
493
|
+
return forwardedFor.split(",")[0].trim();
|
|
494
|
+
}
|
|
495
|
+
const realIp = ctx.get("X-Real-IP");
|
|
496
|
+
if (realIp) {
|
|
497
|
+
return realIp.trim();
|
|
498
|
+
}
|
|
499
|
+
return ctx.ip;
|
|
500
|
+
}
|
|
420
501
|
const REQUIRED_CONFIG_KEYS = [
|
|
421
502
|
"OIDC_CLIENT_ID",
|
|
422
503
|
"OIDC_CLIENT_SECRET",
|
|
@@ -560,62 +641,99 @@ async function updateUserRoles(user, currentRoleIds, newRoleIds) {
|
|
|
560
641
|
throw updateErr;
|
|
561
642
|
}
|
|
562
643
|
}
|
|
563
|
-
async function
|
|
564
|
-
const
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
644
|
+
async function resolveRolesFromGroups(candidateNames) {
|
|
645
|
+
const matchedRoles = await strapi.db.query("admin::role").findMany({
|
|
646
|
+
where: { name: { $in: candidateNames } },
|
|
647
|
+
select: ["id", "name"]
|
|
648
|
+
});
|
|
649
|
+
const nameToId = new Map(matchedRoles.map((r) => [r.name, String(r.id)]));
|
|
650
|
+
const roles2 = [];
|
|
651
|
+
for (const name of candidateNames) {
|
|
652
|
+
const id = nameToId.get(name);
|
|
653
|
+
if (id) roles2.push(id);
|
|
568
654
|
}
|
|
569
|
-
|
|
655
|
+
return {
|
|
656
|
+
roles: roles2,
|
|
657
|
+
fromGroupMapping: true,
|
|
658
|
+
resolvedRoleNames: matchedRoles.map((r) => r.name)
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
async function resolveRolesFromDefaults(roleService2) {
|
|
662
|
+
const oidcRolesResult = await roleService2.oidcRoles();
|
|
663
|
+
const roles2 = oidcRolesResult?.roles || [];
|
|
664
|
+
if (roles2.length === 0) {
|
|
665
|
+
return { roles: roles2, fromGroupMapping: false, resolvedRoleNames: [] };
|
|
666
|
+
}
|
|
667
|
+
const records = await strapi.db.query("admin::role").findMany({
|
|
668
|
+
where: { id: { $in: roles2.map(Number) } },
|
|
669
|
+
select: ["id", "name"]
|
|
670
|
+
});
|
|
671
|
+
return {
|
|
672
|
+
roles: roles2,
|
|
673
|
+
fromGroupMapping: false,
|
|
674
|
+
resolvedRoleNames: records.map((r) => r.name)
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
async function resolveRoles(userResponseData, config2, roleService2) {
|
|
570
678
|
const candidateNames = collectGroupMapRoleNames(userResponseData, config2);
|
|
571
|
-
let roles2 = [];
|
|
572
|
-
let fromGroupMapping = false;
|
|
573
|
-
let resolvedRoleNames = [];
|
|
574
679
|
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
|
-
}
|
|
680
|
+
return resolveRolesFromGroups(candidateNames);
|
|
596
681
|
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
682
|
+
return resolveRolesFromDefaults(roleService2);
|
|
683
|
+
}
|
|
684
|
+
async function ensureUser(userService, oauthService2, email, userResponseData, config2, ctx, resolved) {
|
|
685
|
+
const existing = await userService.findOneByEmail(email, ["roles"]);
|
|
686
|
+
if (!existing) {
|
|
601
687
|
try {
|
|
602
|
-
user = await registerNewUser(
|
|
688
|
+
const user = await registerNewUser(
|
|
689
|
+
oauthService2,
|
|
690
|
+
email,
|
|
691
|
+
userResponseData,
|
|
692
|
+
config2,
|
|
693
|
+
ctx,
|
|
694
|
+
resolved.roles
|
|
695
|
+
);
|
|
696
|
+
return { user, userCreated: true, rolesUpdated: true };
|
|
603
697
|
} catch (e) {
|
|
604
698
|
const msg = e instanceof Error ? e.message : String(e);
|
|
605
699
|
throw new OidcError("user_creation_failed", msg, e);
|
|
606
700
|
}
|
|
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
701
|
}
|
|
702
|
+
if (!resolved.fromGroupMapping || resolved.roles.length === 0) {
|
|
703
|
+
return { user: existing, userCreated: false, rolesUpdated: false };
|
|
704
|
+
}
|
|
705
|
+
const currentRoleIds = new Set((existing.roles ?? []).map((r) => String(r.id)));
|
|
706
|
+
if (!rolesChanged(currentRoleIds, new Set(resolved.roles))) {
|
|
707
|
+
return { user: existing, userCreated: false, rolesUpdated: false };
|
|
708
|
+
}
|
|
709
|
+
await updateUserRoles(existing, currentRoleIds, resolved.roles);
|
|
710
|
+
return { user: existing, userCreated: false, rolesUpdated: true };
|
|
711
|
+
}
|
|
712
|
+
async function handleUserAuthentication(userService, oauthService2, roleService2, whitelistService2, userResponseData, config2, ctx) {
|
|
713
|
+
const email = String(userResponseData.email ?? "").toLowerCase();
|
|
714
|
+
if (!email || !isValidEmail(email)) {
|
|
715
|
+
throw new OidcError("invalid_email", errorMessages.INVALID_EMAIL);
|
|
716
|
+
}
|
|
717
|
+
await whitelistService2.checkWhitelistForEmail(email);
|
|
718
|
+
const resolved = await resolveRoles(userResponseData, config2, roleService2);
|
|
719
|
+
const { user, userCreated, rolesUpdated } = await ensureUser(
|
|
720
|
+
userService,
|
|
721
|
+
oauthService2,
|
|
722
|
+
email,
|
|
723
|
+
userResponseData,
|
|
724
|
+
config2,
|
|
725
|
+
ctx,
|
|
726
|
+
resolved
|
|
727
|
+
);
|
|
616
728
|
const jwtToken = await oauthService2.generateToken(user, ctx);
|
|
617
729
|
oauthService2.triggerSignInSuccess(user);
|
|
618
|
-
return {
|
|
730
|
+
return {
|
|
731
|
+
activateUser: user,
|
|
732
|
+
jwtToken,
|
|
733
|
+
userCreated,
|
|
734
|
+
rolesUpdated,
|
|
735
|
+
resolvedRoleNames: resolved.resolvedRoleNames
|
|
736
|
+
};
|
|
619
737
|
}
|
|
620
738
|
function classifyOidcError(e, userInfo) {
|
|
621
739
|
const kind = e instanceof OidcError ? e.kind : "unknown";
|
|
@@ -634,26 +752,76 @@ function classifyOidcError(e, userInfo) {
|
|
|
634
752
|
params
|
|
635
753
|
};
|
|
636
754
|
}
|
|
637
|
-
|
|
638
|
-
const config2 = configValidation();
|
|
639
|
-
const userService = getAdminUserService();
|
|
640
|
-
const oauthService2 = getOauthService();
|
|
641
|
-
const roleService2 = getRoleService();
|
|
642
|
-
const whitelistService2 = getWhitelistService();
|
|
643
|
-
const auditLog2 = getAuditLogService();
|
|
644
|
-
if (!ctx.query.code) {
|
|
645
|
-
await auditLog2.log({ action: "missing_code", ip: ctx.ip });
|
|
646
|
-
return ctx.send(oauthService2.renderSignUpError(userFacingMessages.missing_code));
|
|
647
|
-
}
|
|
755
|
+
function readAndClearPkceCookies(ctx) {
|
|
648
756
|
const oidcState = ctx.cookies.get("oidc_state");
|
|
649
757
|
const codeVerifier = ctx.cookies.get("oidc_code_verifier");
|
|
650
758
|
const oidcNonce = ctx.cookies.get("oidc_nonce");
|
|
651
759
|
ctx.cookies.set("oidc_state", null);
|
|
652
760
|
ctx.cookies.set("oidc_code_verifier", null);
|
|
653
761
|
ctx.cookies.set("oidc_nonce", null);
|
|
762
|
+
return { oidcState, codeVerifier, oidcNonce };
|
|
763
|
+
}
|
|
764
|
+
async function logSuccessfulAuth(auditLog2, ctx, user, userCreated, rolesUpdated, resolvedRoleNames) {
|
|
765
|
+
const roles2 = resolvedRoleNames.join(", ");
|
|
766
|
+
const entries = [
|
|
767
|
+
auditLog2.log({
|
|
768
|
+
action: "login_success",
|
|
769
|
+
email: user.email,
|
|
770
|
+
ip: getClientIp(ctx),
|
|
771
|
+
detailsKey: rolesUpdated ? "roles_updated" : void 0,
|
|
772
|
+
detailsParams: rolesUpdated ? { roles: roles2 } : void 0
|
|
773
|
+
})
|
|
774
|
+
];
|
|
775
|
+
if (userCreated) {
|
|
776
|
+
entries.push(
|
|
777
|
+
auditLog2.log({
|
|
778
|
+
action: "user_created",
|
|
779
|
+
email: user.email,
|
|
780
|
+
ip: getClientIp(ctx),
|
|
781
|
+
detailsKey: "user_created",
|
|
782
|
+
detailsParams: { roles: roles2 }
|
|
783
|
+
})
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
await Promise.all(entries);
|
|
787
|
+
}
|
|
788
|
+
async function handleCallbackError(e, userInfo, auditLog2, oauthService2, ctx) {
|
|
789
|
+
const errorInfo = classifyOidcError(e, userInfo);
|
|
790
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
791
|
+
await auditLog2.log({
|
|
792
|
+
action: errorInfo.action,
|
|
793
|
+
email: userInfo?.email,
|
|
794
|
+
ip: getClientIp(ctx),
|
|
795
|
+
detailsKey: errorInfo.action,
|
|
796
|
+
detailsParams: errorInfo.action === "login_failure" ? { message } : void 0
|
|
797
|
+
});
|
|
798
|
+
strapi.log.error({
|
|
799
|
+
code: errorInfo.code,
|
|
800
|
+
phase: "oidc_callback",
|
|
801
|
+
message: e instanceof Error ? e.message : "Unknown sign-in error",
|
|
802
|
+
detail: errorInfo.key ? getErrorDetail(errorInfo.key, errorInfo.params) : void 0,
|
|
803
|
+
email: userInfo?.email
|
|
804
|
+
});
|
|
805
|
+
const locale = negotiateLocale(ctx.request.headers["accept-language"]);
|
|
806
|
+
ctx.send(oauthService2.renderSignUpError(userFacingMessages(locale).signInError, locale));
|
|
807
|
+
}
|
|
808
|
+
async function oidcSignInCallback(ctx) {
|
|
809
|
+
const config2 = configValidation();
|
|
810
|
+
const oauthService2 = getOauthService();
|
|
811
|
+
const auditLog2 = getAuditLogService();
|
|
812
|
+
const locale = negotiateLocale(ctx.request.headers["accept-language"]);
|
|
813
|
+
if (!ctx.query.code) {
|
|
814
|
+
await auditLog2.log({ action: "missing_code", ip: getClientIp(ctx) });
|
|
815
|
+
return ctx.send(
|
|
816
|
+
oauthService2.renderSignUpError(userFacingMessages(locale).missing_code, locale)
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
const { oidcState, codeVerifier, oidcNonce } = readAndClearPkceCookies(ctx);
|
|
654
820
|
if (!ctx.query.state || ctx.query.state !== oidcState) {
|
|
655
|
-
await auditLog2.log({ action: "state_mismatch", ip: ctx
|
|
656
|
-
return ctx.send(
|
|
821
|
+
await auditLog2.log({ action: "state_mismatch", ip: getClientIp(ctx) });
|
|
822
|
+
return ctx.send(
|
|
823
|
+
oauthService2.renderSignUpError(userFacingMessages(locale).invalid_state, locale)
|
|
824
|
+
);
|
|
657
825
|
}
|
|
658
826
|
const params = new URLSearchParams({
|
|
659
827
|
code: ctx.query.code,
|
|
@@ -667,67 +835,53 @@ async function oidcSignInCallback(ctx) {
|
|
|
667
835
|
try {
|
|
668
836
|
const exchangeResult = await exchangeTokenAndFetchUserInfo(config2, params, oidcNonce ?? "");
|
|
669
837
|
userInfo = exchangeResult.userInfo;
|
|
670
|
-
const accessToken = exchangeResult.accessToken;
|
|
671
838
|
const isProduction = strapi.config.get("environment") === "production";
|
|
672
|
-
ctx.
|
|
839
|
+
const secureFlag = isProduction && ctx.request.secure;
|
|
840
|
+
ctx.cookies.set("oidc_access_token", exchangeResult.accessToken, {
|
|
673
841
|
httpOnly: true,
|
|
674
842
|
maxAge: 3e5,
|
|
675
|
-
secure:
|
|
843
|
+
secure: secureFlag,
|
|
676
844
|
sameSite: "lax"
|
|
677
845
|
});
|
|
678
846
|
const { activateUser, jwtToken, userCreated, rolesUpdated, resolvedRoleNames } = await handleUserAuthentication(
|
|
679
|
-
|
|
847
|
+
getAdminUserService(),
|
|
680
848
|
oauthService2,
|
|
681
|
-
|
|
682
|
-
|
|
849
|
+
getRoleService(),
|
|
850
|
+
getWhitelistService(),
|
|
683
851
|
userInfo,
|
|
684
852
|
config2,
|
|
685
853
|
ctx
|
|
686
854
|
);
|
|
687
|
-
|
|
855
|
+
ctx.cookies.set("oidc_user_email", activateUser.email, {
|
|
688
856
|
httpOnly: true,
|
|
689
857
|
path: "/",
|
|
690
|
-
secure:
|
|
858
|
+
secure: secureFlag,
|
|
691
859
|
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
860
|
});
|
|
861
|
+
await logSuccessfulAuth(
|
|
862
|
+
auditLog2,
|
|
863
|
+
ctx,
|
|
864
|
+
activateUser,
|
|
865
|
+
userCreated,
|
|
866
|
+
rolesUpdated,
|
|
867
|
+
resolvedRoleNames
|
|
868
|
+
);
|
|
710
869
|
const nonce = node_crypto.randomUUID();
|
|
711
|
-
const html = oauthService2.renderSignUpSuccess(jwtToken, activateUser, nonce);
|
|
712
870
|
ctx.set("Content-Security-Policy", `script-src 'nonce-${nonce}'`);
|
|
713
|
-
ctx.send(
|
|
871
|
+
ctx.send(oauthService2.renderSignUpSuccess(jwtToken, activateUser, nonce, locale));
|
|
714
872
|
} 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
|
|
873
|
+
await handleCallbackError(e, userInfo, auditLog2, oauthService2, ctx);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
async function isProviderSessionActive(userinfoEndpoint, accessToken) {
|
|
877
|
+
try {
|
|
878
|
+
const response = await fetch(userinfoEndpoint, {
|
|
879
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
880
|
+
signal: AbortSignal.timeout(LOGOUT_USERINFO_TIMEOUT_MS)
|
|
729
881
|
});
|
|
730
|
-
|
|
882
|
+
return response.ok;
|
|
883
|
+
} catch {
|
|
884
|
+
return false;
|
|
731
885
|
}
|
|
732
886
|
}
|
|
733
887
|
async function logout(ctx) {
|
|
@@ -735,38 +889,27 @@ async function logout(ctx) {
|
|
|
735
889
|
const auditLog2 = getAuditLogService();
|
|
736
890
|
const logoutUrl = config2.OIDC_END_SESSION_ENDPOINT;
|
|
737
891
|
const adminPanelUrl = strapi.config.get("admin.url", "/admin");
|
|
892
|
+
const loginUrl = `${adminPanelUrl}/auth/login`;
|
|
738
893
|
const isOidcSession = !!ctx.cookies.get("oidc_authenticated");
|
|
739
894
|
const accessToken = ctx.cookies.get("oidc_access_token");
|
|
740
895
|
const userEmail = ctx.cookies.get("oidc_user_email") ?? void 0;
|
|
741
896
|
clearAuthCookies(strapi, ctx);
|
|
742
|
-
if (
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
897
|
+
if (!isOidcSession) {
|
|
898
|
+
return ctx.redirect(loginUrl);
|
|
899
|
+
}
|
|
900
|
+
const logAudit = (action) => userEmail ? auditLog2.log({ action, email: userEmail, ip: getClientIp(ctx) }) : Promise.resolve();
|
|
901
|
+
if (logoutUrl && accessToken) {
|
|
902
|
+
const active = await isProviderSessionActive(config2.OIDC_USERINFO_ENDPOINT, accessToken);
|
|
903
|
+
if (active) {
|
|
904
|
+
logAudit("logout").catch(() => {
|
|
747
905
|
});
|
|
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`);
|
|
906
|
+
return ctx.redirect(logoutUrl);
|
|
761
907
|
}
|
|
908
|
+
await logAudit("session_expired");
|
|
909
|
+
return ctx.redirect(loginUrl);
|
|
762
910
|
}
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
}
|
|
766
|
-
if (logoutUrl && isOidcSession) {
|
|
767
|
-
return ctx.redirect(logoutUrl);
|
|
768
|
-
}
|
|
769
|
-
ctx.redirect(`${adminPanelUrl}/auth/login`);
|
|
911
|
+
await logAudit("logout");
|
|
912
|
+
ctx.redirect(logoutUrl || loginUrl);
|
|
770
913
|
}
|
|
771
914
|
const oidc = {
|
|
772
915
|
oidcSignIn,
|
|
@@ -941,11 +1084,149 @@ const whitelist = {
|
|
|
941
1084
|
importUsers,
|
|
942
1085
|
exportWhitelist
|
|
943
1086
|
};
|
|
1087
|
+
const AUDIT_ACTIONS = [
|
|
1088
|
+
"login_success",
|
|
1089
|
+
"login_failure",
|
|
1090
|
+
"missing_code",
|
|
1091
|
+
"state_mismatch",
|
|
1092
|
+
"nonce_mismatch",
|
|
1093
|
+
"token_exchange_failed",
|
|
1094
|
+
"whitelist_rejected",
|
|
1095
|
+
"logout",
|
|
1096
|
+
"session_expired",
|
|
1097
|
+
"user_created"
|
|
1098
|
+
];
|
|
1099
|
+
const ISO_UTC_DATETIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
1100
|
+
function isIsoUtcDatetime(value) {
|
|
1101
|
+
return typeof value === "string" && ISO_UTC_DATETIME.test(value);
|
|
1102
|
+
}
|
|
1103
|
+
const ALLOWED_FIELDS = /* @__PURE__ */ new Set(["action", "email", "ip", "createdAt"]);
|
|
1104
|
+
const STRING_OPERATORS = /* @__PURE__ */ new Set([
|
|
1105
|
+
"$eq",
|
|
1106
|
+
"$contains",
|
|
1107
|
+
"$endsWith",
|
|
1108
|
+
"$null",
|
|
1109
|
+
"$notNull"
|
|
1110
|
+
]);
|
|
1111
|
+
const DATE_OPERATORS = /* @__PURE__ */ new Set(["$gte", "$lt", "$lte", "$between", "$in"]);
|
|
1112
|
+
const ENUM_OPERATORS = /* @__PURE__ */ new Set(["$eq", "$in"]);
|
|
1113
|
+
function isPlainObject(value) {
|
|
1114
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
1115
|
+
const proto = Object.getPrototypeOf(value);
|
|
1116
|
+
return proto === Object.prototype || proto === null;
|
|
1117
|
+
}
|
|
1118
|
+
function isStringOperator(op) {
|
|
1119
|
+
return STRING_OPERATORS.has(op);
|
|
1120
|
+
}
|
|
1121
|
+
function isDateOperator(op) {
|
|
1122
|
+
return DATE_OPERATORS.has(op);
|
|
1123
|
+
}
|
|
1124
|
+
function isEnumOperator(op) {
|
|
1125
|
+
return ENUM_OPERATORS.has(op);
|
|
1126
|
+
}
|
|
1127
|
+
function isAuditAction(value) {
|
|
1128
|
+
return AUDIT_ACTIONS.includes(value);
|
|
1129
|
+
}
|
|
1130
|
+
class ValidationError extends Error {
|
|
1131
|
+
constructor(message) {
|
|
1132
|
+
super(message);
|
|
1133
|
+
this.name = "ValidationError";
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
function requireType(field, op, value, check, expected) {
|
|
1137
|
+
if (!check) {
|
|
1138
|
+
throw new ValidationError(`Operator "${op}" for field "${field}" requires ${expected}`);
|
|
1139
|
+
}
|
|
1140
|
+
return value;
|
|
1141
|
+
}
|
|
1142
|
+
function parseActionOperator(op, opValue) {
|
|
1143
|
+
if (!isEnumOperator(op)) {
|
|
1144
|
+
throw new ValidationError(`Unknown operator "${op}" for field "action"`);
|
|
1145
|
+
}
|
|
1146
|
+
if (op === "$in") {
|
|
1147
|
+
requireType("action", op, opValue, Array.isArray(opValue), "an array value");
|
|
1148
|
+
for (const v of opValue) {
|
|
1149
|
+
if (!isAuditAction(v)) {
|
|
1150
|
+
throw new ValidationError(
|
|
1151
|
+
`Invalid action value "${v}" — must be one of: ${AUDIT_ACTIONS.join(", ")}`
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
return opValue;
|
|
1156
|
+
}
|
|
1157
|
+
if (!isAuditAction(opValue)) {
|
|
1158
|
+
throw new ValidationError(
|
|
1159
|
+
`Invalid action value "${opValue}" — must be one of: ${AUDIT_ACTIONS.join(", ")}`
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
return opValue;
|
|
1163
|
+
}
|
|
1164
|
+
function parseCreatedAtOperator(op, opValue) {
|
|
1165
|
+
if (!isDateOperator(op)) {
|
|
1166
|
+
throw new ValidationError(`Unknown operator "${op}" for field "createdAt"`);
|
|
1167
|
+
}
|
|
1168
|
+
const expected = 'an ISO-8601 UTC datetime string (e.g. "2024-01-15T00:00:00.000Z")';
|
|
1169
|
+
if (op === "$between") {
|
|
1170
|
+
const isTuple = Array.isArray(opValue) && opValue.length === 2;
|
|
1171
|
+
requireType("createdAt", op, opValue, isTuple, "a tuple [start, end]");
|
|
1172
|
+
const [a, b] = opValue;
|
|
1173
|
+
requireType("createdAt", op, opValue, isIsoUtcDatetime(a) && isIsoUtcDatetime(b), expected);
|
|
1174
|
+
return opValue;
|
|
1175
|
+
}
|
|
1176
|
+
if (op === "$in") {
|
|
1177
|
+
requireType("createdAt", op, opValue, Array.isArray(opValue), "an array value");
|
|
1178
|
+
for (const v of opValue) {
|
|
1179
|
+
requireType("createdAt", op, v, isIsoUtcDatetime(v), expected);
|
|
1180
|
+
}
|
|
1181
|
+
return opValue;
|
|
1182
|
+
}
|
|
1183
|
+
return requireType("createdAt", op, opValue, isIsoUtcDatetime(opValue), expected);
|
|
1184
|
+
}
|
|
1185
|
+
function parseStringFieldOperator(field, op, opValue) {
|
|
1186
|
+
if (!isStringOperator(op)) {
|
|
1187
|
+
throw new ValidationError(`Unknown operator "${op}" for field "${field}"`);
|
|
1188
|
+
}
|
|
1189
|
+
if (op === "$null" || op === "$notNull") {
|
|
1190
|
+
return requireType(field, op, opValue, typeof opValue === "boolean", "a boolean value");
|
|
1191
|
+
}
|
|
1192
|
+
return requireType(field, op, opValue, typeof opValue === "string", "a string value");
|
|
1193
|
+
}
|
|
1194
|
+
function parseFieldOperators(field, fieldValue) {
|
|
1195
|
+
if (!isPlainObject(fieldValue)) {
|
|
1196
|
+
throw new ValidationError(
|
|
1197
|
+
`Filter field "${field}" must be an object of operators, got ${typeof fieldValue}`
|
|
1198
|
+
);
|
|
1199
|
+
}
|
|
1200
|
+
const parsed = {};
|
|
1201
|
+
for (const [op, opValue] of Object.entries(fieldValue)) {
|
|
1202
|
+
if (field === "action") parsed[op] = parseActionOperator(op, opValue);
|
|
1203
|
+
else if (field === "createdAt") parsed[op] = parseCreatedAtOperator(op, opValue);
|
|
1204
|
+
else parsed[op] = parseStringFieldOperator(field, op, opValue);
|
|
1205
|
+
}
|
|
1206
|
+
return Object.keys(parsed).length > 0 ? parsed : null;
|
|
1207
|
+
}
|
|
1208
|
+
function parseAuditLogFilters(query) {
|
|
1209
|
+
if (!isPlainObject(query)) return {};
|
|
1210
|
+
const result = {};
|
|
1211
|
+
const filters = query.filters;
|
|
1212
|
+
if (filters === void 0) return result;
|
|
1213
|
+
if (!isPlainObject(filters)) {
|
|
1214
|
+
throw new ValidationError(`"filters" must be an object, got ${typeof filters}`);
|
|
1215
|
+
}
|
|
1216
|
+
for (const [field, fieldValue] of Object.entries(filters)) {
|
|
1217
|
+
if (!ALLOWED_FIELDS.has(field)) {
|
|
1218
|
+
throw new ValidationError(`Unknown filter field: "${field}"`);
|
|
1219
|
+
}
|
|
1220
|
+
const parsed = parseFieldOperators(field, fieldValue);
|
|
1221
|
+
if (parsed) result[field] = parsed;
|
|
1222
|
+
}
|
|
1223
|
+
return result;
|
|
1224
|
+
}
|
|
944
1225
|
const EXPORT_PAGE_SIZE = 500;
|
|
945
|
-
async function* ndjsonRowStream(service) {
|
|
1226
|
+
async function* ndjsonRowStream(service, filters) {
|
|
946
1227
|
let page = 1;
|
|
947
1228
|
while (true) {
|
|
948
|
-
const { results } = await service.find({ page, pageSize: EXPORT_PAGE_SIZE });
|
|
1229
|
+
const { results } = await service.find({ page, pageSize: EXPORT_PAGE_SIZE, filters });
|
|
949
1230
|
if (results.length === 0) return;
|
|
950
1231
|
let chunk = "";
|
|
951
1232
|
for (const row of results) {
|
|
@@ -962,28 +1243,35 @@ async function* ndjsonRowStream(service) {
|
|
|
962
1243
|
page++;
|
|
963
1244
|
}
|
|
964
1245
|
}
|
|
965
|
-
function errorAwareNdjsonStream(service) {
|
|
966
|
-
const gen = ndjsonRowStream(service);
|
|
1246
|
+
function errorAwareNdjsonStream(strapi2, service, filters) {
|
|
1247
|
+
const gen = ndjsonRowStream(service, filters);
|
|
967
1248
|
const readable = node_stream.Readable.from(gen);
|
|
968
1249
|
readable.on("error", (err) => {
|
|
969
|
-
|
|
1250
|
+
strapi2.log.error({ phase: "audit_log_export", err }, "NDJSON export stream failed");
|
|
970
1251
|
});
|
|
971
1252
|
return readable;
|
|
972
1253
|
}
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1254
|
+
function parseFiltersOr400(ctx) {
|
|
1255
|
+
try {
|
|
1256
|
+
return parseAuditLogFilters(ctx.query);
|
|
1257
|
+
} catch (err) {
|
|
1258
|
+
ctx.status = 400;
|
|
1259
|
+
ctx.body = { message: err instanceof ValidationError ? err.message : "Invalid filters" };
|
|
1260
|
+
return null;
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
async function find(ctx) {
|
|
1264
|
+
const filters = parseFiltersOr400(ctx);
|
|
1265
|
+
if (!filters) return;
|
|
976
1266
|
const page = Math.max(1, Number(ctx.query.page) || 1);
|
|
977
1267
|
const pageSize = Math.min(100, Math.max(1, Number(ctx.query.pageSize) || 25));
|
|
978
|
-
|
|
979
|
-
ctx.body = result;
|
|
980
|
-
});
|
|
1268
|
+
ctx.body = await getAuditLogService().find({ page, pageSize, filters });
|
|
981
1269
|
}
|
|
982
1270
|
async function exportLogs(ctx) {
|
|
983
|
-
|
|
1271
|
+
const filters = parseFiltersOr400(ctx);
|
|
1272
|
+
if (!filters) return;
|
|
984
1273
|
setNdjsonAttachmentHeaders(ctx, "strapi-oidc-audit-log");
|
|
985
|
-
|
|
986
|
-
ctx.body = errorAwareNdjsonStream(service);
|
|
1274
|
+
ctx.body = errorAwareNdjsonStream(ctx.strapi, getAuditLogService(), filters);
|
|
987
1275
|
}
|
|
988
1276
|
async function clearAll(ctx) {
|
|
989
1277
|
await getAuditLogService().clearAll();
|
|
@@ -1004,7 +1292,7 @@ const rateLimitMap = /* @__PURE__ */ new Map();
|
|
|
1004
1292
|
const RATE_LIMIT_WINDOW = 6e4;
|
|
1005
1293
|
const MAX_REQUESTS = 1e3;
|
|
1006
1294
|
function getRateLimitKey(ctx) {
|
|
1007
|
-
const ip = ctx
|
|
1295
|
+
const ip = getClientIp(ctx);
|
|
1008
1296
|
const ua = ctx.request.header["user-agent"] ?? "";
|
|
1009
1297
|
const uaHash = node_crypto.createHash("sha256").update(ua).digest("hex").slice(0, 16);
|
|
1010
1298
|
return `${ip}:${uaHash}`;
|
|
@@ -1197,10 +1485,10 @@ const routes = {
|
|
|
1197
1485
|
}
|
|
1198
1486
|
};
|
|
1199
1487
|
const policies = {};
|
|
1200
|
-
function renderHtmlTemplate(title, content) {
|
|
1488
|
+
function renderHtmlTemplate(title, content, locale = "en") {
|
|
1201
1489
|
return `
|
|
1202
1490
|
<!doctype html>
|
|
1203
|
-
<html lang="
|
|
1491
|
+
<html lang="${locale}">
|
|
1204
1492
|
<head>
|
|
1205
1493
|
<meta charset="utf-8">
|
|
1206
1494
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
@@ -1379,9 +1667,10 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
1379
1667
|
provider: "strapi-plugin-oidc"
|
|
1380
1668
|
});
|
|
1381
1669
|
},
|
|
1382
|
-
renderSignUpSuccess(jwtToken, user, nonce) {
|
|
1670
|
+
renderSignUpSuccess(jwtToken, user, nonce, locale = "en") {
|
|
1383
1671
|
const config2 = strapi2.config.get("plugin::strapi-plugin-oidc");
|
|
1384
1672
|
const isRememberMe = !!config2?.REMEMBER_ME;
|
|
1673
|
+
const messages = authPageMessages(locale);
|
|
1385
1674
|
const content = `
|
|
1386
1675
|
<noscript>
|
|
1387
1676
|
<div class="card">
|
|
@@ -1390,8 +1679,8 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
1390
1679
|
<path d="M20 6 9 17l-5-5"/>
|
|
1391
1680
|
</svg>
|
|
1392
1681
|
</div>
|
|
1393
|
-
<h1
|
|
1394
|
-
<p
|
|
1682
|
+
<h1>${messages.noscriptHeading}</h1>
|
|
1683
|
+
<p>${messages.noscriptBody}</p>
|
|
1395
1684
|
</div>
|
|
1396
1685
|
</noscript>
|
|
1397
1686
|
<script nonce="${nonce}">
|
|
@@ -1405,9 +1694,10 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
1405
1694
|
location.href = '${strapi2.config.admin.url}'
|
|
1406
1695
|
})
|
|
1407
1696
|
<\/script>`;
|
|
1408
|
-
return renderHtmlTemplate(
|
|
1697
|
+
return renderHtmlTemplate(messages.authenticatingTitle, content, locale);
|
|
1409
1698
|
},
|
|
1410
|
-
renderSignUpError(message) {
|
|
1699
|
+
renderSignUpError(message, locale = "en") {
|
|
1700
|
+
const messages = authPageMessages(locale);
|
|
1411
1701
|
const safeMessage = String(message).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1412
1702
|
const content = `
|
|
1413
1703
|
<div class="card">
|
|
@@ -1418,11 +1708,11 @@ function oauthService({ strapi: strapi2 }) {
|
|
|
1418
1708
|
<path d="M12 17h.01"/>
|
|
1419
1709
|
</svg>
|
|
1420
1710
|
</div>
|
|
1421
|
-
<h1
|
|
1711
|
+
<h1>${messages.errorTitle}</h1>
|
|
1422
1712
|
<p>${safeMessage}</p>
|
|
1423
|
-
<a href="${strapi2.config.admin.url}" class="btn"
|
|
1713
|
+
<a href="${strapi2.config.admin.url}" class="btn">${messages.returnToLogin}</a>
|
|
1424
1714
|
</div>`;
|
|
1425
|
-
return renderHtmlTemplate(
|
|
1715
|
+
return renderHtmlTemplate(messages.errorTitle, content, locale);
|
|
1426
1716
|
},
|
|
1427
1717
|
async generateToken(user, ctx) {
|
|
1428
1718
|
const sessionManager = strapi2.sessionManager;
|
|
@@ -1586,6 +1876,58 @@ function translateDetails(key, params) {
|
|
|
1586
1876
|
if (!translation) return null;
|
|
1587
1877
|
return interpolate(translation, params);
|
|
1588
1878
|
}
|
|
1879
|
+
const STRING_OP_MAP = {
|
|
1880
|
+
$eq: (v) => v,
|
|
1881
|
+
$contains: (v) => ({ $containsi: v }),
|
|
1882
|
+
$endsWith: (v) => ({ $endsWith: v }),
|
|
1883
|
+
$null: (v) => v === true ? null : void 0,
|
|
1884
|
+
$notNull: (v) => v === true ? { $notNull: true } : void 0
|
|
1885
|
+
};
|
|
1886
|
+
const DATE_OP_MAP = {
|
|
1887
|
+
$gte: (v) => ({ $gte: v }),
|
|
1888
|
+
$lt: (v) => ({ $lt: v }),
|
|
1889
|
+
$lte: (v) => ({ $lte: v }),
|
|
1890
|
+
$between: (v) => ({ $between: v })
|
|
1891
|
+
// $in is handled separately: each ISO day-start is expanded to a [day, day+1) range.
|
|
1892
|
+
};
|
|
1893
|
+
const DAY_MS = 864e5;
|
|
1894
|
+
function nextDayIso(iso) {
|
|
1895
|
+
return new Date(new Date(iso).getTime() + DAY_MS).toISOString();
|
|
1896
|
+
}
|
|
1897
|
+
function expandCreatedAtInToDayRanges(days) {
|
|
1898
|
+
const ranges = days.map((d) => ({ createdAt: { $gte: d, $lt: nextDayIso(d) } }));
|
|
1899
|
+
return ranges.length === 1 ? ranges[0] : { $or: ranges };
|
|
1900
|
+
}
|
|
1901
|
+
const ACTION_OP_MAP = {
|
|
1902
|
+
$eq: (v) => v,
|
|
1903
|
+
$in: (v) => ({ $in: v })
|
|
1904
|
+
};
|
|
1905
|
+
function mapFieldFilter(conditions, field, filter, opMap) {
|
|
1906
|
+
for (const [op, value] of Object.entries(filter)) {
|
|
1907
|
+
const transform = opMap[op];
|
|
1908
|
+
if (!transform) continue;
|
|
1909
|
+
const result = transform(value);
|
|
1910
|
+
if (result !== void 0) conditions.push({ [field]: result });
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
function buildWhereClause(filters) {
|
|
1914
|
+
const conditions = [];
|
|
1915
|
+
if (filters.action) mapFieldFilter(conditions, "action", filters.action, ACTION_OP_MAP);
|
|
1916
|
+
if (filters.email) mapFieldFilter(conditions, "email", filters.email, STRING_OP_MAP);
|
|
1917
|
+
if (filters.ip) mapFieldFilter(conditions, "ip", filters.ip, STRING_OP_MAP);
|
|
1918
|
+
if (filters.createdAt) {
|
|
1919
|
+
const { $in: inDays, ...rest } = filters.createdAt;
|
|
1920
|
+
if (Array.isArray(inDays) && inDays.length > 0) {
|
|
1921
|
+
conditions.push(expandCreatedAtInToDayRanges(inDays));
|
|
1922
|
+
}
|
|
1923
|
+
if (Object.keys(rest).length > 0) {
|
|
1924
|
+
mapFieldFilter(conditions, "createdAt", rest, DATE_OP_MAP);
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
if (conditions.length === 0) return {};
|
|
1928
|
+
if (conditions.length === 1) return conditions[0];
|
|
1929
|
+
return { $and: conditions };
|
|
1930
|
+
}
|
|
1589
1931
|
function auditLogService({ strapi: strapi2 }) {
|
|
1590
1932
|
return {
|
|
1591
1933
|
async log({ action, email, ip, detailsKey, detailsParams }) {
|
|
@@ -1610,20 +1952,31 @@ function auditLogService({ strapi: strapi2 }) {
|
|
|
1610
1952
|
},
|
|
1611
1953
|
async find({
|
|
1612
1954
|
page = 1,
|
|
1613
|
-
pageSize = 25
|
|
1955
|
+
pageSize = 25,
|
|
1956
|
+
filters
|
|
1614
1957
|
} = {}) {
|
|
1615
|
-
const
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1958
|
+
const where = filters ? buildWhereClause(filters) : {};
|
|
1959
|
+
const dbQuery = strapi2.db.query("plugin::strapi-plugin-oidc.audit-log");
|
|
1960
|
+
const [rows, total] = await Promise.all([
|
|
1961
|
+
dbQuery.findMany({
|
|
1962
|
+
where,
|
|
1963
|
+
orderBy: [{ createdAt: "desc" }],
|
|
1964
|
+
limit: pageSize,
|
|
1965
|
+
offset: (page - 1) * pageSize
|
|
1966
|
+
}),
|
|
1967
|
+
dbQuery.count({ where })
|
|
1968
|
+
]);
|
|
1624
1969
|
return {
|
|
1625
|
-
results
|
|
1626
|
-
|
|
1970
|
+
results: rows.map((row) => ({
|
|
1971
|
+
...row,
|
|
1972
|
+
details: row.detailsKey ? translateDetails(row.detailsKey, row.detailsParams) : null
|
|
1973
|
+
})),
|
|
1974
|
+
pagination: {
|
|
1975
|
+
page,
|
|
1976
|
+
pageSize,
|
|
1977
|
+
total,
|
|
1978
|
+
pageCount: Math.ceil(total / pageSize)
|
|
1979
|
+
}
|
|
1627
1980
|
};
|
|
1628
1981
|
},
|
|
1629
1982
|
async clearAll() {
|
|
@@ -1635,7 +1988,7 @@ function auditLogService({ strapi: strapi2 }) {
|
|
|
1635
1988
|
} while (deletedCount === BATCH_SIZE);
|
|
1636
1989
|
},
|
|
1637
1990
|
async cleanup(retentionDays) {
|
|
1638
|
-
const cutoff = new Date(Date.now() - retentionDays *
|
|
1991
|
+
const cutoff = new Date(Date.now() - retentionDays * DAY_MS);
|
|
1639
1992
|
await strapi2.db.query("plugin::strapi-plugin-oidc.audit-log").deleteMany({ where: { createdAt: { $lt: cutoff } } });
|
|
1640
1993
|
}
|
|
1641
1994
|
};
|