strapi-plugin-oidc 1.5.2 → 1.6.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.
@@ -24,18 +24,31 @@ function resolveEnforceOIDC(strapi2, dbValue) {
24
24
  if (configValue !== null) return configValue;
25
25
  return dbValue ?? false;
26
26
  }
27
+ const PLUGIN_UID = "plugin::strapi-plugin-oidc";
28
+ const DEFAULT_RETENTION_DAYS = 90;
29
+ function getPluginConfig() {
30
+ return strapi.config.get(PLUGIN_UID);
31
+ }
32
+ function getRetentionDays() {
33
+ const config2 = getPluginConfig();
34
+ return Number(config2.AUDIT_LOG_RETENTION_DAYS ?? DEFAULT_RETENTION_DAYS);
35
+ }
36
+ function isAuditLogEnabled() {
37
+ return getRetentionDays() !== 0;
38
+ }
27
39
  async function bootstrap({ strapi: strapi2 }) {
40
+ const adminUrl = strapi2.config.get("admin.url", "/admin");
41
+ const authRoutes = [
42
+ `${adminUrl}/login`,
43
+ `${adminUrl}/register`,
44
+ `${adminUrl}/register-admin`,
45
+ `${adminUrl}/forgot-password`,
46
+ `${adminUrl}/reset-password`
47
+ ];
48
+ const tokenRefreshPath = `${adminUrl}/token/refresh`;
28
49
  const enforceOidcMiddleware = async (ctx, next) => {
29
- const adminUrl = strapi2.config.get("admin.url", "/admin");
30
- const authRoutes = [
31
- `${adminUrl}/login`,
32
- `${adminUrl}/register`,
33
- `${adminUrl}/register-admin`,
34
- `${adminUrl}/forgot-password`,
35
- `${adminUrl}/reset-password`
36
- ];
37
50
  const isPostAuth = authRoutes.includes(ctx.request.path) && ctx.request.method === "POST";
38
- const isTokenRefresh = ctx.request.path === `${adminUrl}/token/refresh` && ctx.request.method === "POST";
51
+ const isTokenRefresh = ctx.request.path === tokenRefreshPath && ctx.request.method === "POST";
39
52
  if (isPostAuth || isTokenRefresh) {
40
53
  try {
41
54
  const whitelistService2 = strapi2.plugin("strapi-plugin-oidc").service("whitelist");
@@ -116,14 +129,12 @@ async function bootstrap({ strapi: strapi2 }) {
116
129
  where: { oauth_type: "4" }
117
130
  });
118
131
  if (oidcRoleCount === 0) {
119
- const editorRole = await strapi2.query("admin::role").findOne({
120
- where: { code: "strapi-editor" }
121
- });
122
- if (editorRole) {
132
+ const defaultRole = await strapi2.query("admin::role").findOne({ where: { code: "strapi-editor" } }) ?? await strapi2.query("admin::role").findOne({});
133
+ if (defaultRole) {
123
134
  await strapi2.query("plugin::strapi-plugin-oidc.roles").create({
124
135
  data: {
125
136
  oauth_type: "4",
126
- roles: [editorRole.id.toString()]
137
+ roles: [defaultRole.id.toString()]
127
138
  }
128
139
  });
129
140
  }
@@ -131,25 +142,18 @@ async function bootstrap({ strapi: strapi2 }) {
131
142
  } catch (err) {
132
143
  strapi2.log.warn("Could not initialize default OIDC role:", err.message);
133
144
  }
134
- strapi2.db.lifecycles.subscribe({
135
- models: ["admin::user"],
136
- async afterUpdate(event) {
137
- const { result } = event;
138
- if (!result?.email) return;
139
- const query = strapi2.query("plugin::strapi-plugin-oidc.whitelists");
140
- const whitelistEntry = await query.findOne({ where: { email: result.email } });
141
- if (!whitelistEntry) return;
142
- const userWithRoles = await strapi2.query("admin::user").findOne({
143
- where: { id: result.id },
144
- populate: ["roles"]
145
- });
146
- if (userWithRoles?.roles) {
147
- const roleIds = userWithRoles.roles.map((r) => r.id.toString());
148
- await query.update({
149
- where: { id: whitelistEntry.id },
150
- data: { roles: roleIds }
151
- });
152
- }
145
+ strapi2.cron.add({
146
+ "strapi-plugin-oidc-audit-log-cleanup": {
147
+ task: async () => {
148
+ try {
149
+ const retentionDays = getRetentionDays();
150
+ await strapi2.plugin("strapi-plugin-oidc").service("auditLog").cleanup(retentionDays);
151
+ } catch (err) {
152
+ strapi2.log.warn("[strapi-plugin-oidc] Audit log cleanup failed:", err.message);
153
+ }
154
+ },
155
+ options: { rule: "0 0 * * *" }
156
+ // daily at midnight
153
157
  }
154
158
  });
155
159
  }
@@ -170,41 +174,58 @@ const config = {
170
174
  OIDC_GIVEN_NAME_FIELD: "given_name",
171
175
  OIDC_END_SESSION_ENDPOINT: "",
172
176
  OIDC_SSO_BUTTON_TEXT: "Login via SSO",
173
- OIDC_ENFORCE: null
177
+ OIDC_ENFORCE: null,
174
178
  // null = use DB setting; true/false = override DB (useful for lockout recovery)
179
+ AUDIT_LOG_RETENTION_DAYS: 90,
180
+ OIDC_GROUP_FIELD: "groups",
181
+ OIDC_GROUP_ROLE_MAP: "{}"
175
182
  },
176
183
  validator() {
177
184
  }
178
185
  };
179
- const info$2 = { "singularName": "roles", "pluralName": "oidc-roles", "collectionName": "oidc-roles", "displayName": "oidc-role", "description": "" };
186
+ const info$3 = { "singularName": "roles", "pluralName": "oidc-roles", "collectionName": "oidc-roles", "displayName": "oidc-role", "description": "" };
187
+ const options$2 = { "draftAndPublish": false };
188
+ const pluginOptions$2 = { "content-manager": { "visible": false }, "content-type-builder": { "visible": false } };
189
+ const attributes$2 = { "oauth_type": { "type": "string", "configurable": false, "required": true }, "roles": { "type": "json", "configurable": false } };
190
+ const schema$2 = {
191
+ info: info$3,
192
+ options: options$2,
193
+ pluginOptions: pluginOptions$2,
194
+ attributes: attributes$2
195
+ };
196
+ const roles = {
197
+ schema: schema$2
198
+ };
199
+ const info$2 = { "singularName": "whitelists", "pluralName": "whitelists", "collectionName": "whitelists", "displayName": "whitelist", "description": "" };
180
200
  const options$1 = { "draftAndPublish": false };
181
201
  const pluginOptions$1 = { "content-manager": { "visible": false }, "content-type-builder": { "visible": false } };
182
- const attributes$1 = { "oauth_type": { "type": "string", "configurable": false, "required": true }, "roles": { "type": "json", "configurable": false } };
202
+ const attributes$1 = { "email": { "type": "string", "configurable": false, "required": true, "unique": true } };
183
203
  const schema$1 = {
184
204
  info: info$2,
185
205
  options: options$1,
186
206
  pluginOptions: pluginOptions$1,
187
207
  attributes: attributes$1
188
208
  };
189
- const roles = {
209
+ const whitelists = {
190
210
  schema: schema$1
191
211
  };
192
- const info$1 = { "singularName": "whitelists", "pluralName": "whitelists", "collectionName": "whitelists", "displayName": "whitelist", "description": "" };
212
+ const info$1 = { "singularName": "audit-log", "pluralName": "audit-logs", "collectionName": "audit_logs", "displayName": "OIDC Audit Log" };
193
213
  const options = { "draftAndPublish": false };
194
214
  const pluginOptions = { "content-manager": { "visible": false }, "content-type-builder": { "visible": false } };
195
- const attributes = { "email": { "type": "string", "configurable": false, "required": true, "unique": true }, "roles": { "type": "json", "configurable": false } };
215
+ const attributes = { "action": { "type": "string", "required": true }, "email": { "type": "string" }, "ip": { "type": "string" }, "detailsKey": { "type": "string" }, "detailsParams": { "type": "json" } };
196
216
  const schema = {
197
217
  info: info$1,
198
218
  options,
199
219
  pluginOptions,
200
220
  attributes
201
221
  };
202
- const whitelists = {
222
+ const auditLog$1 = {
203
223
  schema
204
224
  };
205
225
  const contentTypes = {
206
226
  roles,
207
- whitelists
227
+ whitelists,
228
+ "audit-log": auditLog$1
208
229
  };
209
230
  function getExpiredCookieOptions(strapi2, ctx) {
210
231
  const isProduction = strapi2.config.get("environment") === "production";
@@ -222,7 +243,131 @@ function clearAuthCookies(strapi2, ctx) {
222
243
  const options2 = getExpiredCookieOptions(strapi2, ctx);
223
244
  ctx.cookies.set("strapi_admin_refresh", "", options2);
224
245
  ctx.cookies.set("oidc_authenticated", "", { ...options2, path: "/" });
246
+ ctx.cookies.set("oidc_access_token", "", { ...options2, path: "/" });
247
+ ctx.cookies.set("oidc_user_email", "", { ...options2, path: "/" });
225
248
  }
249
+ const errorCodes = {
250
+ TOKEN_EXCHANGE_FAILED: "TOKEN_EXCHANGE_FAILED",
251
+ USERINFO_FETCH_FAILED: "USERINFO_FETCH_FAILED",
252
+ ID_TOKEN_PARSE_FAILED: "ID_TOKEN_PARSE_FAILED",
253
+ NONCE_MISMATCH: "NONCE_MISMATCH",
254
+ ROLE_UPDATE_FAILED: "ROLE_UPDATE_FAILED",
255
+ USER_CREATION_FAILED: "USER_CREATION_FAILED",
256
+ WHITELIST_CHECK_FAILED: "WHITELIST_CHECK_FAILED"
257
+ };
258
+ function getErrorDetail(key, params) {
259
+ switch (key) {
260
+ case "token_exchange_failed":
261
+ return `Token exchange failed with HTTP status ${params?.status ?? "unknown"}`;
262
+ case "userinfo_fetch_failed":
263
+ return `UserInfo endpoint returned HTTP ${params?.status ?? "unknown"}`;
264
+ case "role_update_failed":
265
+ return `Role update failed for user ${params?.userId}: ${params?.error ?? "unknown"}`;
266
+ case "user_creation_failed":
267
+ return `User creation failed for ${params?.email}: ${params?.error ?? "unknown"}`;
268
+ case "id_token_parse_failed":
269
+ return `ID token parse failed: ${params?.error ?? "unknown"}`;
270
+ case "sign_in_unknown":
271
+ return `Unknown sign-in error: ${params?.error ?? "unknown"}`;
272
+ default:
273
+ return void 0;
274
+ }
275
+ }
276
+ const en = {
277
+ "global.plugins.strapi-plugin-oidc": "OIDC Plugin",
278
+ "page.title": "Configure OIDC default role(s) and access controls.",
279
+ "roles.notes": "Select the default role(s) assigned to new users upon their first login. This setting does not affect existing users.",
280
+ "page.save": "Save Changes",
281
+ "page.save.success": "Updated settings",
282
+ "page.save.error": "Update failed.",
283
+ "page.add": "Add",
284
+ "page.cancel": "Cancel",
285
+ "page.ok": "OK",
286
+ "roles.title": "Default Role(s)",
287
+ "roles.placeholder": "Select default role(s)",
288
+ "whitelist.title": "Whitelist",
289
+ "whitelist.error.unique": "Already registered email address.",
290
+ "whitelist.description": "Restrict OIDC authentication to specific email addresses. Users receive roles from the default OIDC role mapping or their OIDC group.",
291
+ "alert.title.success": "Success",
292
+ "alert.title.error": "Error",
293
+ "alert.title.info": "Info",
294
+ "pagination.previous": "Go to previous page",
295
+ "pagination.page": "Go to page {page}",
296
+ "pagination.next": "Go to next page",
297
+ "whitelist.table.no": "No.",
298
+ "whitelist.table.email": "Email",
299
+ "whitelist.table.created": "Created At",
300
+ "whitelist.delete.title": "Confirmation",
301
+ "whitelist.delete.description": "Are you sure you want to delete:",
302
+ "whitelist.delete.note": "This will not delete the user account in Strapi.",
303
+ "whitelist.toggle.enabled": "Enabled",
304
+ "whitelist.toggle.disabled": "Disabled",
305
+ "whitelist.email.placeholder": "Email address",
306
+ "whitelist.roles.placeholder": "Select specific role(s)",
307
+ "whitelist.table.roles": "Role(s)",
308
+ "whitelist.table.empty": "No email addresses",
309
+ "whitelist.delete.label": "Delete",
310
+ "page.title.oidc": "OIDC",
311
+ "enforce.title": "Enforce OIDC Login",
312
+ "enforce.toggle.enabled": "Enabled",
313
+ "enforce.toggle.disabled": "Disabled",
314
+ "enforce.warning": "Make sure OIDC is setup correctly before saving changes, you won't be able to login normally.",
315
+ "enforce.config.info": "Enforcement is controlled by the OIDC_ENFORCE config variable and cannot be changed here.",
316
+ "login.settings.title": "Login Settings",
317
+ "login.sso": "Login via SSO",
318
+ "whitelist.count": "{count, plural, one {# entry} other {# entries}}",
319
+ "whitelist.import": "Import",
320
+ "whitelist.export": "Export",
321
+ "whitelist.delete.all.label": "Delete All",
322
+ "whitelist.delete.all.title": "Delete All Entries",
323
+ "whitelist.delete.all.description": "This will permanently remove all {count, plural, one {# entry} other {# entries}} from the whitelist. Unsaved changes will be lost.",
324
+ "whitelist.import.error": "Invalid file — expected a JSON array of objects with an email field.",
325
+ "whitelist.import.success": "Imported {count, plural, one {# new entry} other {# new entries}}.",
326
+ "whitelist.import.none": "No new entries — all emails are already in the whitelist.",
327
+ "unsaved.title": "Unsaved Changes",
328
+ "unsaved.description": "You have unsaved changes that will be lost if you leave. Do you want to continue?",
329
+ "unsaved.confirm": "Leave",
330
+ "unsaved.cancel": "Stay",
331
+ "whitelist.table.roles.default": "(Default)",
332
+ "auditlog.title": "Audit Logs",
333
+ "auditlog.export": "Download",
334
+ "auditlog.table.timestamp": "Timestamp",
335
+ "auditlog.table.action": "Action",
336
+ "auditlog.table.email": "Email",
337
+ "auditlog.table.ip": "IP",
338
+ "auditlog.table.details": "Details",
339
+ "auditlog.table.empty": "No audit log entries",
340
+ "auditlog.clear": "Clear Logs",
341
+ "auditlog.clear.title": "Clear All Logs",
342
+ "auditlog.clear.description": "This will permanently delete all {count, plural, one {# audit log entry} other {# audit log entries}}. This action cannot be undone.",
343
+ "auditlog.clear.success": "Audit logs cleared",
344
+ "auditlog.clear.error": "Failed to clear audit logs",
345
+ "auditlog.export.error": "Failed to export audit logs",
346
+ "auditlog.action.login_success": "User successfully authenticated via OIDC and was granted access.",
347
+ "auditlog.action.user_created": "A new Strapi admin account was created for this user on their first OIDC login.",
348
+ "auditlog.action.logout": "User logged out and their OIDC session was ended.",
349
+ "auditlog.action.session_expired": "The OIDC access token had expired by the time the user logged out, so the provider session could not be terminated remotely.",
350
+ "auditlog.action.login_failure": "An unexpected error occurred during the OIDC login flow.",
351
+ "auditlog.action.missing_code": "The OIDC callback was received without an authorisation code. This may indicate a misconfigured provider or a tampered request.",
352
+ "auditlog.action.state_mismatch": "The state parameter in the callback did not match the one stored in the session. This may indicate a CSRF attempt or an expired login session.",
353
+ "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
+ "auditlog.action.token_exchange_failed": "The authorisation code could not be exchanged for tokens. The OIDC provider rejected the request.",
355
+ "auditlog.action.whitelist_rejected": "The user's email address is not on the whitelist. Access was denied.",
356
+ "user.missing_code": "Authorisation code was not received from the OIDC provider.",
357
+ "user.invalid_state": "State parameter mismatch. Please restart the login flow.",
358
+ "user.signInError": "Authentication failed. Please try again."
359
+ };
360
+ const userFacingMessages = {
361
+ get missing_code() {
362
+ return en["user.missing_code"];
363
+ },
364
+ get invalid_state() {
365
+ return en["user.invalid_state"];
366
+ },
367
+ get signInError() {
368
+ return en["user.signInError"];
369
+ }
370
+ };
226
371
  const REQUIRED_CONFIG_KEYS = [
227
372
  "OIDC_CLIENT_ID",
228
373
  "OIDC_CLIENT_SECRET",
@@ -260,15 +405,16 @@ async function oidcSignIn(ctx) {
260
405
  ctx.cookies.set("oidc_code_verifier", codeVerifier, cookieOptions);
261
406
  ctx.cookies.set("oidc_state", state, cookieOptions);
262
407
  ctx.cookies.set("oidc_nonce", nonce, cookieOptions);
263
- const params = new URLSearchParams();
264
- params.append("response_type", "code");
265
- params.append("client_id", OIDC_CLIENT_ID);
266
- params.append("redirect_uri", OIDC_REDIRECT_URI);
267
- params.append("scope", OIDC_SCOPE);
268
- params.append("code_challenge", codeChallenge);
269
- params.append("code_challenge_method", "S256");
270
- params.append("state", state);
271
- params.append("nonce", nonce);
408
+ const params = new URLSearchParams({
409
+ response_type: "code",
410
+ client_id: OIDC_CLIENT_ID,
411
+ redirect_uri: OIDC_REDIRECT_URI,
412
+ scope: OIDC_SCOPE,
413
+ code_challenge: codeChallenge,
414
+ code_challenge_method: "S256",
415
+ state,
416
+ nonce
417
+ });
272
418
  const authorizationUrl = `${OIDC_AUTHORIZATION_ENDPOINT}?${params.toString()}`;
273
419
  ctx.set("Location", authorizationUrl);
274
420
  return ctx.send({}, 302);
@@ -303,16 +449,38 @@ async function exchangeTokenAndFetchUserInfo(config2, params, expectedNonce) {
303
449
  if (!userResponse.ok) {
304
450
  throw new Error("Failed to fetch user info");
305
451
  }
306
- return userResponse.json();
452
+ const userInfo = await userResponse.json();
453
+ return { userInfo, accessToken: tokenData.access_token };
307
454
  }
308
- async function registerNewUser(userService, oauthService2, roleService2, email, userResponseData, whitelistUser, config2, ctx) {
309
- let roles2 = [];
310
- if (whitelistUser?.roles?.length > 0) {
311
- roles2 = whitelistUser.roles;
312
- } else {
313
- const oidcRoles = await roleService2.oidcRoles();
314
- roles2 = oidcRoles?.roles || [];
455
+ function resolveRolesFromGroups(userInfo, config2, availableRoles) {
456
+ const rawGroups = userInfo[config2.OIDC_GROUP_FIELD];
457
+ if (!Array.isArray(rawGroups) || rawGroups.length === 0) return [];
458
+ const groups = rawGroups.filter((g) => typeof g === "string");
459
+ const raw = config2.OIDC_GROUP_ROLE_MAP;
460
+ let groupRoleMap;
461
+ try {
462
+ groupRoleMap = typeof raw === "string" ? JSON.parse(raw) : raw;
463
+ } catch {
464
+ return [];
315
465
  }
466
+ const roleIdSet = /* @__PURE__ */ new Set();
467
+ for (const group of groups) {
468
+ const roleNames = groupRoleMap[group];
469
+ if (!roleNames) continue;
470
+ for (const name of roleNames) {
471
+ const match = availableRoles.find((r) => r.name === name);
472
+ if (match) roleIdSet.add(String(match.id));
473
+ }
474
+ }
475
+ return [...roleIdSet];
476
+ }
477
+ async function resolveRoles(userInfo, config2, roleService2, availableRoles) {
478
+ const groupRoles = resolveRolesFromGroups(userInfo, config2, availableRoles);
479
+ if (groupRoles.length > 0) return groupRoles;
480
+ const oidcRoles = await roleService2.oidcRoles();
481
+ return oidcRoles?.roles || [];
482
+ }
483
+ async function registerNewUser(oauthService2, email, userResponseData, config2, ctx, roles2) {
316
484
  const defaultLocale = oauthService2.localeFindByHeader(
317
485
  ctx.request.headers
318
486
  );
@@ -327,21 +495,106 @@ async function registerNewUser(userService, oauthService2, roleService2, email,
327
495
  return activateUser;
328
496
  }
329
497
  async function handleUserAuthentication(userService, oauthService2, roleService2, whitelistService2, userResponseData, config2, ctx) {
330
- const email = String(userResponseData.email).toLowerCase();
331
- const whitelistUser = await whitelistService2.checkWhitelistForEmail(email);
332
- const activateUser = await userService.findOneByEmail(email) ?? await registerNewUser(
333
- userService,
334
- oauthService2,
335
- roleService2,
336
- email,
337
- userResponseData,
338
- whitelistUser,
339
- config2,
340
- ctx
341
- );
498
+ const rawEmail = String(userResponseData.email ?? "");
499
+ const email = rawEmail.toLowerCase();
500
+ if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
501
+ throw new Error("Invalid email address received from OIDC provider");
502
+ }
503
+ await whitelistService2.checkWhitelistForEmail(email);
504
+ const allRoles = await strapi.db.query("admin::role").findMany();
505
+ const roles2 = await resolveRoles(userResponseData, config2, roleService2, allRoles);
506
+ const resolvedRoleNames = allRoles.filter((r) => roles2.includes(String(r.id))).map((r) => r.name);
507
+ let userCreated = false;
508
+ let rolesUpdated = false;
509
+ let activateUser = await userService.findOneByEmail(email);
510
+ if (!activateUser) {
511
+ activateUser = await registerNewUser(oauthService2, email, userResponseData, config2, ctx, roles2);
512
+ userCreated = true;
513
+ } else if (roles2.length > 0) {
514
+ const currentRoleIds = new Set((activateUser.roles ?? []).map((r) => String(r.id)));
515
+ const newRoleIds = new Set(roles2);
516
+ const rolesChanged = currentRoleIds.size !== newRoleIds.size || [...newRoleIds].some((id) => !currentRoleIds.has(id));
517
+ if (rolesChanged) {
518
+ try {
519
+ strapi.log.info(
520
+ `[OIDC] Roles updated for user ${activateUser.id}: [${[...currentRoleIds].join(",")}] -> [${roles2.join(",")}]`
521
+ );
522
+ await strapi.db.query("admin::user").update({
523
+ where: { id: activateUser.id },
524
+ data: { roles: roles2 }
525
+ });
526
+ rolesUpdated = true;
527
+ } catch (updateErr) {
528
+ strapi.log.error({
529
+ code: errorCodes.ROLE_UPDATE_FAILED,
530
+ userId: activateUser.id,
531
+ detail: getErrorDetail("role_update_failed", {
532
+ userId: activateUser.id,
533
+ error: updateErr.message
534
+ })
535
+ });
536
+ throw updateErr;
537
+ }
538
+ }
539
+ }
342
540
  const jwtToken = await oauthService2.generateToken(activateUser, ctx);
343
541
  oauthService2.triggerSignInSuccess(activateUser);
344
- return { activateUser, jwtToken };
542
+ return { activateUser, jwtToken, userCreated, rolesUpdated, resolvedRoleNames };
543
+ }
544
+ function classifyOidcError(msg, userInfo) {
545
+ const errorMap = [
546
+ {
547
+ test: (m) => m.includes("whitelist"),
548
+ result: {
549
+ action: "whitelist_rejected",
550
+ code: errorCodes.WHITELIST_CHECK_FAILED,
551
+ key: "whitelist_rejected"
552
+ }
553
+ },
554
+ {
555
+ test: (m) => m === "Nonce mismatch",
556
+ result: { action: "nonce_mismatch", code: errorCodes.NONCE_MISMATCH }
557
+ },
558
+ {
559
+ test: (m) => m === "Token exchange failed",
560
+ result: { action: "token_exchange_failed", code: errorCodes.TOKEN_EXCHANGE_FAILED }
561
+ },
562
+ {
563
+ test: (m) => m === "Failed to fetch user info",
564
+ result: {
565
+ action: "login_failure",
566
+ code: errorCodes.USERINFO_FETCH_FAILED,
567
+ key: "userinfo_fetch_failed"
568
+ }
569
+ },
570
+ {
571
+ test: (m) => m === "Failed to parse ID token",
572
+ result: {
573
+ action: "login_failure",
574
+ code: errorCodes.ID_TOKEN_PARSE_FAILED,
575
+ key: "id_token_parse_failed",
576
+ params: { error: msg }
577
+ }
578
+ },
579
+ {
580
+ test: (m) => m === "User creation failed" || m.includes("createUser"),
581
+ result: {
582
+ action: "login_failure",
583
+ code: errorCodes.USER_CREATION_FAILED,
584
+ key: "user_creation_failed",
585
+ params: userInfo?.email ? { email: userInfo.email, error: msg } : void 0
586
+ }
587
+ }
588
+ ];
589
+ for (const { test, result } of errorMap) {
590
+ if (test(msg)) return result;
591
+ }
592
+ return {
593
+ action: "login_failure",
594
+ code: errorCodes.TOKEN_EXCHANGE_FAILED,
595
+ key: "sign_in_unknown",
596
+ params: { error: msg || "unknown" }
597
+ };
345
598
  }
346
599
  async function oidcSignInCallback(ctx) {
347
600
  const config2 = configValidation();
@@ -349,8 +602,10 @@ async function oidcSignInCallback(ctx) {
349
602
  const oauthService2 = strapi.plugin("strapi-plugin-oidc").service("oauth");
350
603
  const roleService2 = strapi.plugin("strapi-plugin-oidc").service("role");
351
604
  const whitelistService2 = strapi.plugin("strapi-plugin-oidc").service("whitelist");
605
+ const auditLog2 = strapi.plugin("strapi-plugin-oidc").service("auditLog");
352
606
  if (!ctx.query.code) {
353
- return ctx.send(oauthService2.renderSignUpError("code Not Found"));
607
+ await auditLog2.log({ action: "missing_code", ip: ctx.ip });
608
+ return ctx.send(oauthService2.renderSignUpError(userFacingMessages.missing_code));
354
609
  }
355
610
  const oidcState = ctx.cookies.get("oidc_state");
356
611
  const codeVerifier = ctx.cookies.get("oidc_code_verifier");
@@ -359,53 +614,129 @@ async function oidcSignInCallback(ctx) {
359
614
  ctx.cookies.set("oidc_code_verifier", null);
360
615
  ctx.cookies.set("oidc_nonce", null);
361
616
  if (!ctx.query.state || ctx.query.state !== oidcState) {
362
- return ctx.send(oauthService2.renderSignUpError("Invalid state"));
617
+ await auditLog2.log({ action: "state_mismatch", ip: ctx.ip });
618
+ return ctx.send(oauthService2.renderSignUpError(userFacingMessages.invalid_state));
363
619
  }
364
- const params = new URLSearchParams();
365
- params.append("code", ctx.query.code);
366
- params.append("client_id", config2.OIDC_CLIENT_ID);
367
- params.append("client_secret", config2.OIDC_CLIENT_SECRET);
368
- params.append("redirect_uri", config2.OIDC_REDIRECT_URI);
369
- params.append("grant_type", config2.OIDC_GRANT_TYPE);
370
- params.append("code_verifier", codeVerifier ?? "");
620
+ const params = new URLSearchParams({
621
+ code: ctx.query.code,
622
+ client_id: config2.OIDC_CLIENT_ID,
623
+ client_secret: config2.OIDC_CLIENT_SECRET,
624
+ redirect_uri: config2.OIDC_REDIRECT_URI,
625
+ grant_type: config2.OIDC_GRANT_TYPE,
626
+ code_verifier: codeVerifier ?? ""
627
+ });
628
+ let userInfo;
371
629
  try {
372
- const userResponseData = await exchangeTokenAndFetchUserInfo(config2, params, oidcNonce ?? "");
373
- const { activateUser, jwtToken } = await handleUserAuthentication(
630
+ const exchangeResult = await exchangeTokenAndFetchUserInfo(config2, params, oidcNonce ?? "");
631
+ userInfo = exchangeResult.userInfo;
632
+ const accessToken = exchangeResult.accessToken;
633
+ const isProduction = strapi.config.get("environment") === "production";
634
+ ctx.cookies.set("oidc_access_token", accessToken, {
635
+ httpOnly: true,
636
+ maxAge: 3e5,
637
+ // 5 minutes — matches typical provider access token lifetime
638
+ secure: isProduction && ctx.request.secure,
639
+ sameSite: "lax"
640
+ });
641
+ const { activateUser, jwtToken, userCreated, rolesUpdated, resolvedRoleNames } = await handleUserAuthentication(
374
642
  userService,
375
643
  oauthService2,
376
644
  roleService2,
377
645
  whitelistService2,
378
- userResponseData,
646
+ userInfo,
379
647
  config2,
380
648
  ctx
381
649
  );
650
+ const identityCookieOptions = {
651
+ httpOnly: true,
652
+ path: "/",
653
+ secure: isProduction && ctx.request.secure,
654
+ sameSite: "lax"
655
+ };
656
+ ctx.cookies.set("oidc_user_email", activateUser.email, identityCookieOptions);
657
+ if (userCreated) {
658
+ await auditLog2.log({
659
+ action: "user_created",
660
+ email: activateUser.email,
661
+ ip: ctx.ip,
662
+ detailsKey: "user_created",
663
+ detailsParams: { roles: resolvedRoleNames.join(", ") }
664
+ });
665
+ }
666
+ await auditLog2.log({
667
+ action: "login_success",
668
+ email: activateUser.email,
669
+ ip: ctx.ip,
670
+ detailsKey: rolesUpdated ? "roles_updated" : void 0,
671
+ detailsParams: rolesUpdated ? { roles: resolvedRoleNames.join(", ") } : void 0
672
+ });
382
673
  const nonce = node_crypto.randomUUID();
383
674
  const html = oauthService2.renderSignUpSuccess(jwtToken, activateUser, nonce);
384
675
  ctx.set("Content-Security-Policy", `script-src 'nonce-${nonce}'`);
385
676
  ctx.send(html);
386
677
  } catch (e) {
387
- console.error("ERROR CAUGHT IN OIDC SIGNIN:", e);
388
- ctx.send(oauthService2.renderSignUpError("Authentication failed. Please try again."));
678
+ const msg = e.message ?? "";
679
+ const errorInfo = classifyOidcError(msg, userInfo);
680
+ await auditLog2.log({
681
+ action: errorInfo.action,
682
+ email: userInfo?.email,
683
+ ip: ctx.ip,
684
+ detailsKey: errorInfo.action,
685
+ detailsParams: errorInfo.action === "login_failure" ? { message: msg } : void 0
686
+ });
687
+ strapi.log.error({
688
+ code: errorInfo.code,
689
+ phase: "oidc_callback",
690
+ message: msg || "Unknown sign-in error",
691
+ detail: errorInfo.key ? getErrorDetail(errorInfo.key, errorInfo.params) : void 0,
692
+ email: userInfo?.email
693
+ });
694
+ ctx.send(oauthService2.renderSignUpError(userFacingMessages.signInError));
389
695
  }
390
696
  }
391
697
  async function logout(ctx) {
392
698
  const config2 = strapi.config.get("plugin::strapi-plugin-oidc");
699
+ const auditLog2 = strapi.plugin("strapi-plugin-oidc").service("auditLog");
393
700
  const logoutUrl = config2.OIDC_END_SESSION_ENDPOINT;
701
+ const adminPanelUrl = strapi.config.get("admin.url", "/admin");
394
702
  const isOidcSession = !!ctx.cookies.get("oidc_authenticated");
703
+ const accessToken = ctx.cookies.get("oidc_access_token");
704
+ const userEmail = ctx.cookies.get("oidc_user_email") ?? void 0;
395
705
  clearAuthCookies(strapi, ctx);
706
+ if (logoutUrl && isOidcSession && accessToken) {
707
+ try {
708
+ const response = await fetch(config2.OIDC_USERINFO_ENDPOINT, {
709
+ headers: { Authorization: `Bearer ${accessToken}` }
710
+ });
711
+ if (response.ok) {
712
+ if (userEmail)
713
+ auditLog2.log({ action: "logout", email: userEmail, ip: ctx.ip }).catch(() => {
714
+ });
715
+ return ctx.redirect(logoutUrl);
716
+ }
717
+ if (userEmail)
718
+ await auditLog2.log({ action: "session_expired", email: userEmail, ip: ctx.ip });
719
+ return ctx.redirect(`${adminPanelUrl}/auth/login`);
720
+ } catch {
721
+ if (userEmail)
722
+ await auditLog2.log({ action: "session_expired", email: userEmail, ip: ctx.ip });
723
+ return ctx.redirect(`${adminPanelUrl}/auth/login`);
724
+ }
725
+ }
726
+ if (isOidcSession && userEmail) {
727
+ await auditLog2.log({ action: "logout", email: userEmail, ip: ctx.ip });
728
+ }
396
729
  if (logoutUrl && isOidcSession) {
397
- ctx.redirect(logoutUrl);
398
- } else {
399
- const adminPanelUrl = strapi.config.get("admin.url", "/admin");
400
- ctx.redirect(`${adminPanelUrl}/auth/login`);
730
+ return ctx.redirect(logoutUrl);
401
731
  }
732
+ ctx.redirect(`${adminPanelUrl}/auth/login`);
402
733
  }
403
734
  const oidc = {
404
735
  oidcSignIn,
405
736
  oidcSignInCallback,
406
737
  logout
407
738
  };
408
- async function find(ctx) {
739
+ async function find$1(ctx) {
409
740
  const roleService2 = strapi.plugin("strapi-plugin-oidc").service("role");
410
741
  const roles2 = await roleService2.find();
411
742
  const oidcConstants = roleService2.getOidcRoles();
@@ -424,14 +755,23 @@ async function update(ctx) {
424
755
  await roleService2.update(roles2);
425
756
  ctx.send({}, 204);
426
757
  } catch (e) {
427
- console.error(e);
758
+ strapi.log.error(e);
428
759
  ctx.send({}, 400);
429
760
  }
430
761
  }
431
762
  const role = {
432
- find,
763
+ find: find$1,
433
764
  update
434
765
  };
766
+ function formatDatetimeForFilename(date) {
767
+ const year = date.getFullYear();
768
+ const month = String(date.getMonth() + 1).padStart(2, "0");
769
+ const day = String(date.getDate()).padStart(2, "0");
770
+ const hours = String(date.getHours()).padStart(2, "0");
771
+ const minutes = String(date.getMinutes()).padStart(2, "0");
772
+ const seconds = String(date.getSeconds()).padStart(2, "0");
773
+ return `${year}${month}${day}_${hours}${minutes}${seconds}`;
774
+ }
435
775
  function getWhitelistService() {
436
776
  return strapi.plugin("strapi-plugin-oidc").service("whitelist");
437
777
  }
@@ -443,7 +783,8 @@ async function info(ctx) {
443
783
  useWhitelist: settings.useWhitelist,
444
784
  enforceOIDC: resolveEnforceOIDC(strapi, settings.enforceOIDC),
445
785
  enforceOIDCConfig: getEnforceOIDCConfig(strapi),
446
- whitelistUsers
786
+ whitelistUsers,
787
+ auditLogEnabled: isAuditLogEnabled()
447
788
  };
448
789
  }
449
790
  async function updateSettings(ctx) {
@@ -469,32 +810,25 @@ async function publicSettings(ctx) {
469
810
  };
470
811
  }
471
812
  async function register(ctx) {
472
- const { email, roles: roles2 } = ctx.request.body;
813
+ const { email } = ctx.request.body;
473
814
  if (!email) {
474
815
  ctx.body = { message: "Please enter a valid email address" };
475
816
  return;
476
817
  }
477
818
  const rawEmails = Array.isArray(email) ? email : email.split(",");
478
819
  const emailList = rawEmails.map((e) => String(e).trim().toLowerCase()).filter(Boolean);
479
- const existingUsers = await strapi.query("admin::user").findMany({
480
- where: { email: { $in: emailList } },
481
- populate: ["roles"]
482
- });
483
- const existingUsersByEmail = new Map(existingUsers.map((u) => [u.email, u]));
484
820
  const whitelistService2 = getWhitelistService();
485
821
  let matchedExistingUsersCount = 0;
486
822
  for (const singleEmail of emailList) {
487
- const existingUser = existingUsersByEmail.get(singleEmail);
488
- let finalRoles = roles2;
489
- if (existingUser?.roles) {
490
- finalRoles = existingUser.roles.map((r) => String(r.id));
491
- matchedExistingUsersCount++;
492
- }
823
+ const existingUser = await strapi.query("admin::user").findOne({
824
+ where: { email: singleEmail }
825
+ });
826
+ if (existingUser) matchedExistingUsersCount++;
493
827
  const alreadyWhitelisted = await strapi.query("plugin::strapi-plugin-oidc.whitelists").findOne({
494
828
  where: { email: singleEmail }
495
829
  });
496
830
  if (!alreadyWhitelisted) {
497
- await whitelistService2.registerUser(singleEmail, finalRoles);
831
+ await whitelistService2.registerUser(singleEmail);
498
832
  }
499
833
  }
500
834
  ctx.body = { matchedExistingUsersCount };
@@ -509,81 +843,52 @@ async function deleteAll(ctx) {
509
843
  await strapi.query("plugin::strapi-plugin-oidc.whitelists").deleteMany({});
510
844
  ctx.body = {};
511
845
  }
846
+ async function exportWhitelist(ctx) {
847
+ const datetime = formatDatetimeForFilename(/* @__PURE__ */ new Date());
848
+ ctx.set("Content-Type", "application/json");
849
+ ctx.set("Content-Disposition", `attachment; filename="strapi-oidc-whitelist-${datetime}.json"`);
850
+ const whitelistService2 = getWhitelistService();
851
+ const users = await whitelistService2.getUsers();
852
+ ctx.body = users.map((u) => ({ email: u.email }));
853
+ }
512
854
  async function importUsers(ctx) {
513
855
  const { users } = ctx.request.body;
514
856
  if (!Array.isArray(users)) {
515
857
  ctx.status = 400;
516
- ctx.body = { error: "Expected { users: [{email, roles}] }" };
858
+ ctx.body = { error: "Expected { users: [{email}] }" };
517
859
  return;
518
860
  }
519
- const allRoles = await strapi.query("admin::role").findMany({});
520
- const roleNameToId = new Map(allRoles.map((r) => [r.name, String(r.id)]));
521
- const resolveRole = (nameOrId) => roleNameToId.get(nameOrId) ?? nameOrId;
522
- const normalized = users.filter((u) => u?.email).map((u) => ({
523
- email: String(u.email).trim().toLowerCase(),
524
- roles: (Array.isArray(u.roles) ? u.roles : []).map(resolveRole)
525
- }));
526
- const seen = /* @__PURE__ */ new Set();
527
- const deduped = normalized.filter((u) => {
528
- if (seen.has(u.email)) return false;
529
- seen.add(u.email);
530
- return true;
531
- });
532
- const strapiUsers = await strapi.query("admin::user").findMany({
533
- where: { email: { $in: deduped.map((u) => u.email) } },
534
- populate: ["roles"]
535
- });
536
- const strapiUserMap = new Map(strapiUsers.map((u) => [u.email, u]));
861
+ const normalized = users.filter((u) => u?.email).map((u) => String(u.email).trim().toLowerCase()).filter((email) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email));
862
+ const deduped = [...new Set(normalized)];
537
863
  const whitelistService2 = getWhitelistService();
538
864
  const existing = await whitelistService2.getUsers();
539
865
  const existingEmails = new Set(existing.map((u) => u.email));
540
866
  let importedCount = 0;
541
- for (const user of deduped) {
542
- if (existingEmails.has(user.email)) continue;
543
- const strapiUser = strapiUserMap.get(user.email);
544
- const finalRoles = strapiUser?.roles?.length ? strapiUser.roles.map((r) => String(r.id)) : user.roles;
545
- await whitelistService2.registerUser(user.email, finalRoles);
867
+ for (const email of deduped) {
868
+ if (existingEmails.has(email)) continue;
869
+ await whitelistService2.registerUser(email);
546
870
  importedCount++;
547
871
  }
548
872
  ctx.body = { importedCount };
549
873
  }
550
874
  async function syncUsers(ctx) {
551
875
  const { users: rawUsers } = ctx.request.body;
552
- const users = rawUsers.map((u) => ({ ...u, email: String(u.email).toLowerCase() }));
876
+ const emails = rawUsers.map((u) => String(u.email).toLowerCase()).filter((e) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e));
553
877
  const whitelistService2 = getWhitelistService();
554
878
  const currentUsers = await whitelistService2.getUsers();
555
- let matchedExistingUsersCount = 0;
556
- const emailsToSync = users.map((u) => u.email);
557
- const existingStrapiUsers = await strapi.query("admin::user").findMany({
558
- where: { email: { $in: emailsToSync } },
559
- populate: ["roles"]
560
- });
561
- const syncEmailSet = new Set(emailsToSync);
879
+ const syncEmailSet = new Set(emails);
562
880
  const currentUsersByEmail = new Map(currentUsers.map((u) => [u.email, u]));
563
- const strapiUsersByEmail = new Map(existingStrapiUsers.map((u) => [u.email, u]));
564
881
  for (const currUser of currentUsers) {
565
882
  if (!syncEmailSet.has(currUser.email)) {
566
883
  await whitelistService2.removeUser(currUser.id);
567
884
  }
568
885
  }
569
- for (const user of users) {
570
- const currUser = currentUsersByEmail.get(user.email);
571
- let finalRoles = user.roles;
572
- if (!currUser) {
573
- const existingStrapiUser = strapiUsersByEmail.get(user.email);
574
- if (existingStrapiUser?.roles) {
575
- finalRoles = existingStrapiUser.roles.map((r) => String(r.id));
576
- matchedExistingUsersCount++;
577
- }
578
- await whitelistService2.registerUser(user.email, finalRoles);
579
- } else {
580
- await strapi.query("plugin::strapi-plugin-oidc.whitelists").update({
581
- where: { id: currUser.id },
582
- data: { roles: finalRoles }
583
- });
886
+ for (const email of emails) {
887
+ if (!currentUsersByEmail.has(email)) {
888
+ await whitelistService2.registerUser(email);
584
889
  }
585
890
  }
586
- ctx.body = { matchedExistingUsersCount };
891
+ ctx.body = { matchedExistingUsersCount: 0 };
587
892
  }
588
893
  const whitelist = {
589
894
  info,
@@ -593,39 +898,99 @@ const whitelist = {
593
898
  removeEmail,
594
899
  deleteAll,
595
900
  syncUsers,
596
- importUsers
901
+ importUsers,
902
+ exportWhitelist
903
+ };
904
+ function getAuditLogService() {
905
+ return strapi.plugin("strapi-plugin-oidc").service("auditLog");
906
+ }
907
+ async function find(ctx) {
908
+ const page = Math.max(1, Number(ctx.query.page) || 1);
909
+ const pageSize = Math.min(100, Math.max(1, Number(ctx.query.pageSize) || 25));
910
+ ctx.body = await getAuditLogService().find({ page, pageSize });
911
+ }
912
+ async function exportLogs(ctx) {
913
+ const datetime = formatDatetimeForFilename(/* @__PURE__ */ new Date());
914
+ ctx.set("Content-Type", "application/json");
915
+ ctx.set("Content-Disposition", `attachment; filename="strapi-oidc-audit-log-${datetime}.json"`);
916
+ const service = getAuditLogService();
917
+ const PAGE_SIZE = 1e3;
918
+ const allRows = [];
919
+ let page = 1;
920
+ while (true) {
921
+ const { results } = await service.find({ page, pageSize: PAGE_SIZE });
922
+ for (const row of results) {
923
+ allRows.push({
924
+ id: row.id,
925
+ createdAt: row.createdAt,
926
+ action: row.action,
927
+ email: row.email ?? null,
928
+ ip: row.ip ?? null,
929
+ details: row.details
930
+ });
931
+ }
932
+ if (results.length < PAGE_SIZE) break;
933
+ page++;
934
+ }
935
+ ctx.body = allRows.map((row) => ({
936
+ datetime: row.createdAt,
937
+ action: row.action,
938
+ email: row.email,
939
+ ip: row.ip,
940
+ details: row.details
941
+ }));
942
+ }
943
+ async function clearAll(ctx) {
944
+ await getAuditLogService().clearAll();
945
+ ctx.status = 204;
946
+ }
947
+ const auditLog = {
948
+ find,
949
+ export: exportLogs,
950
+ clearAll
597
951
  };
598
952
  const controllers = {
599
953
  oidc,
600
954
  role,
601
- whitelist
955
+ whitelist,
956
+ auditLog
602
957
  };
603
958
  const rateLimitMap = /* @__PURE__ */ new Map();
604
959
  const RATE_LIMIT_WINDOW = 6e4;
605
- const MAX_REQUESTS = 20;
606
- const rateLimitMiddleware = async (ctx, next) => {
960
+ const MAX_REQUESTS = 1e3;
961
+ function getRateLimitKey(ctx) {
607
962
  const ip = ctx.request.ip;
963
+ const ua = ctx.request.header["user-agent"] ?? "";
964
+ const uaHash = node_crypto.createHash("sha256").update(ua).digest("hex").slice(0, 16);
965
+ return `${ip}:${uaHash}`;
966
+ }
967
+ function rateLimitMiddleware(ctx, next) {
968
+ const key = getRateLimitKey(ctx);
608
969
  const now = Date.now();
609
970
  const windowStart = now - RATE_LIMIT_WINDOW;
610
- const requestStamps = (rateLimitMap.get(ip) || []).filter((timestamp) => timestamp > windowStart);
971
+ const requestStamps = (rateLimitMap.get(key) || []).filter(
972
+ (timestamp) => timestamp > windowStart
973
+ );
611
974
  if (requestStamps.length >= MAX_REQUESTS) {
612
975
  ctx.status = 429;
613
976
  ctx.body = "Too Many Requests";
614
977
  return;
615
978
  }
616
979
  requestStamps.push(now);
617
- rateLimitMap.set(ip, requestStamps);
618
- await next();
619
- };
620
- const adminPolicies = (action) => ({
621
- policies: [
622
- "admin::isAuthenticatedAdmin",
623
- {
624
- name: "admin::hasPermissions",
625
- config: { actions: [`plugin::strapi-plugin-oidc.${action}`] }
626
- }
627
- ]
628
- });
980
+ rateLimitMap.set(key, requestStamps);
981
+ return next();
982
+ }
983
+ function adminPolicies(action) {
984
+ return {
985
+ policies: [
986
+ "admin::isAuthenticatedAdmin",
987
+ {
988
+ name: "admin::hasPermissions",
989
+ config: { actions: [`plugin::strapi-plugin-oidc.${action}`] }
990
+ }
991
+ ]
992
+ };
993
+ }
629
994
  const routes = {
630
995
  admin: {
631
996
  type: "admin",
@@ -707,6 +1072,30 @@ const routes = {
707
1072
  path: "/whitelist",
708
1073
  handler: "whitelist.deleteAll",
709
1074
  config: adminPolicies("update")
1075
+ },
1076
+ {
1077
+ method: "GET",
1078
+ path: "/whitelist/export",
1079
+ handler: "whitelist.exportWhitelist",
1080
+ config: adminPolicies("read")
1081
+ },
1082
+ {
1083
+ method: "GET",
1084
+ path: "/audit-logs",
1085
+ handler: "auditLog.find",
1086
+ config: { policies: ["admin::isAuthenticatedAdmin"] }
1087
+ },
1088
+ {
1089
+ method: "GET",
1090
+ path: "/audit-logs/export",
1091
+ handler: "auditLog.export",
1092
+ config: { policies: ["admin::isAuthenticatedAdmin"] }
1093
+ },
1094
+ {
1095
+ method: "DELETE",
1096
+ path: "/audit-logs",
1097
+ handler: "auditLog.clearAll",
1098
+ config: { policies: ["admin::isAuthenticatedAdmin"] }
710
1099
  }
711
1100
  ]
712
1101
  },
@@ -740,6 +1129,26 @@ const routes = {
740
1129
  method: "DELETE",
741
1130
  path: "/whitelist",
742
1131
  handler: "whitelist.deleteAll"
1132
+ },
1133
+ {
1134
+ method: "GET",
1135
+ path: "/whitelist/export",
1136
+ handler: "whitelist.exportWhitelist"
1137
+ },
1138
+ {
1139
+ method: "GET",
1140
+ path: "/audit-logs",
1141
+ handler: "auditLog.find"
1142
+ },
1143
+ {
1144
+ method: "GET",
1145
+ path: "/audit-logs/export",
1146
+ handler: "auditLog.export"
1147
+ },
1148
+ {
1149
+ method: "DELETE",
1150
+ path: "/audit-logs",
1151
+ handler: "auditLog.clearAll"
743
1152
  }
744
1153
  ]
745
1154
  }
@@ -1110,9 +1519,9 @@ function whitelistService({ strapi: strapi2 }) {
1110
1519
  async getUsers() {
1111
1520
  return getWhitelistQuery().findMany();
1112
1521
  },
1113
- async registerUser(email, roles2) {
1522
+ async registerUser(email) {
1114
1523
  await getWhitelistQuery().create({
1115
- data: { email, roles: roles2 }
1524
+ data: { email }
1116
1525
  });
1117
1526
  },
1118
1527
  async removeUser(id) {
@@ -1135,10 +1544,73 @@ function whitelistService({ strapi: strapi2 }) {
1135
1544
  }
1136
1545
  };
1137
1546
  }
1547
+ function interpolate(str, params) {
1548
+ if (!params) return str;
1549
+ return str.replace(/\{(\w+)\}/g, (_, key) => String(params[key] ?? `{${key}}`));
1550
+ }
1551
+ function translateDetails(key, params) {
1552
+ const translation = en[`audit.${key}`];
1553
+ if (!translation) return null;
1554
+ return interpolate(translation, params);
1555
+ }
1556
+ function auditLogService({ strapi: strapi2 }) {
1557
+ return {
1558
+ async log({ action, email, ip, detailsKey, detailsParams }) {
1559
+ if (!isAuditLogEnabled()) return;
1560
+ await strapi2.db.query("plugin::strapi-plugin-oidc.audit-log").create({
1561
+ data: {
1562
+ action,
1563
+ email: email ?? null,
1564
+ ip: ip ?? null,
1565
+ detailsKey: detailsKey ?? null,
1566
+ detailsParams: detailsParams ?? null
1567
+ }
1568
+ });
1569
+ const eventHub = strapi2.serviceMap?.get?.("eventHub") ?? strapi2.eventHub;
1570
+ if (eventHub) {
1571
+ eventHub.emit(`strapi-plugin-oidc::auth.${action}`, {
1572
+ email,
1573
+ ip,
1574
+ provider: "strapi-plugin-oidc"
1575
+ });
1576
+ }
1577
+ },
1578
+ async find({ page = 1, pageSize = 25 } = {}) {
1579
+ const result = await strapi2.db.query("plugin::strapi-plugin-oidc.audit-log").findPage({
1580
+ sort: { createdAt: "desc" },
1581
+ page,
1582
+ pageSize
1583
+ });
1584
+ const results = result.results.map((row) => ({
1585
+ ...row,
1586
+ details: row.detailsKey ? translateDetails(row.detailsKey, row.detailsParams) : null
1587
+ }));
1588
+ return {
1589
+ results,
1590
+ pagination: result.pagination
1591
+ };
1592
+ },
1593
+ async clearAll() {
1594
+ const BATCH_SIZE = 1e3;
1595
+ let deletedCount;
1596
+ do {
1597
+ const result = await strapi2.db.query("plugin::strapi-plugin-oidc.audit-log").deleteMany({ limit: BATCH_SIZE });
1598
+ deletedCount = result.count;
1599
+ } while (deletedCount === BATCH_SIZE);
1600
+ },
1601
+ async cleanup(retentionDays) {
1602
+ const cutoff = new Date(Date.now() - retentionDays * 864e5);
1603
+ await strapi2.db.query("plugin::strapi-plugin-oidc.audit-log").deleteMany({
1604
+ where: { createdAt: { $lt: cutoff } }
1605
+ });
1606
+ }
1607
+ };
1608
+ }
1138
1609
  const services = {
1139
1610
  oauth: oauthService,
1140
1611
  role: roleService,
1141
- whitelist: whitelistService
1612
+ whitelist: whitelistService,
1613
+ auditLog: auditLogService
1142
1614
  };
1143
1615
  const index = {
1144
1616
  register: register$1,