strapi-plugin-oidc 1.5.3 → 1.6.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.
@@ -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";
@@ -223,7 +244,130 @@ function clearAuthCookies(strapi2, ctx) {
223
244
  ctx.cookies.set("strapi_admin_refresh", "", options2);
224
245
  ctx.cookies.set("oidc_authenticated", "", { ...options2, path: "/" });
225
246
  ctx.cookies.set("oidc_access_token", "", { ...options2, path: "/" });
247
+ ctx.cookies.set("oidc_user_email", "", { ...options2, path: "/" });
226
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
+ };
227
371
  const REQUIRED_CONFIG_KEYS = [
228
372
  "OIDC_CLIENT_ID",
229
373
  "OIDC_CLIENT_SECRET",
@@ -261,15 +405,16 @@ async function oidcSignIn(ctx) {
261
405
  ctx.cookies.set("oidc_code_verifier", codeVerifier, cookieOptions);
262
406
  ctx.cookies.set("oidc_state", state, cookieOptions);
263
407
  ctx.cookies.set("oidc_nonce", nonce, cookieOptions);
264
- const params = new URLSearchParams();
265
- params.append("response_type", "code");
266
- params.append("client_id", OIDC_CLIENT_ID);
267
- params.append("redirect_uri", OIDC_REDIRECT_URI);
268
- params.append("scope", OIDC_SCOPE);
269
- params.append("code_challenge", codeChallenge);
270
- params.append("code_challenge_method", "S256");
271
- params.append("state", state);
272
- 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
+ });
273
418
  const authorizationUrl = `${OIDC_AUTHORIZATION_ENDPOINT}?${params.toString()}`;
274
419
  ctx.set("Location", authorizationUrl);
275
420
  return ctx.send({}, 302);
@@ -307,14 +452,35 @@ async function exchangeTokenAndFetchUserInfo(config2, params, expectedNonce) {
307
452
  const userInfo = await userResponse.json();
308
453
  return { userInfo, accessToken: tokenData.access_token };
309
454
  }
310
- async function registerNewUser(userService, oauthService2, roleService2, email, userResponseData, whitelistUser, config2, ctx) {
311
- let roles2 = [];
312
- if (whitelistUser?.roles?.length > 0) {
313
- roles2 = whitelistUser.roles;
314
- } else {
315
- const oidcRoles = await roleService2.oidcRoles();
316
- 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 [];
317
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) {
318
484
  const defaultLocale = oauthService2.localeFindByHeader(
319
485
  ctx.request.headers
320
486
  );
@@ -329,21 +495,106 @@ async function registerNewUser(userService, oauthService2, roleService2, email,
329
495
  return activateUser;
330
496
  }
331
497
  async function handleUserAuthentication(userService, oauthService2, roleService2, whitelistService2, userResponseData, config2, ctx) {
332
- const email = String(userResponseData.email).toLowerCase();
333
- const whitelistUser = await whitelistService2.checkWhitelistForEmail(email);
334
- const activateUser = await userService.findOneByEmail(email) ?? await registerNewUser(
335
- userService,
336
- oauthService2,
337
- roleService2,
338
- email,
339
- userResponseData,
340
- whitelistUser,
341
- config2,
342
- ctx
343
- );
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
+ }
344
540
  const jwtToken = await oauthService2.generateToken(activateUser, ctx);
345
541
  oauthService2.triggerSignInSuccess(activateUser);
346
- 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
+ };
347
598
  }
348
599
  async function oidcSignInCallback(ctx) {
349
600
  const config2 = configValidation();
@@ -351,8 +602,10 @@ async function oidcSignInCallback(ctx) {
351
602
  const oauthService2 = strapi.plugin("strapi-plugin-oidc").service("oauth");
352
603
  const roleService2 = strapi.plugin("strapi-plugin-oidc").service("role");
353
604
  const whitelistService2 = strapi.plugin("strapi-plugin-oidc").service("whitelist");
605
+ const auditLog2 = strapi.plugin("strapi-plugin-oidc").service("auditLog");
354
606
  if (!ctx.query.code) {
355
- 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));
356
609
  }
357
610
  const oidcState = ctx.cookies.get("oidc_state");
358
611
  const codeVerifier = ctx.cookies.get("oidc_code_verifier");
@@ -361,21 +614,22 @@ async function oidcSignInCallback(ctx) {
361
614
  ctx.cookies.set("oidc_code_verifier", null);
362
615
  ctx.cookies.set("oidc_nonce", null);
363
616
  if (!ctx.query.state || ctx.query.state !== oidcState) {
364
- 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));
365
619
  }
366
- const params = new URLSearchParams();
367
- params.append("code", ctx.query.code);
368
- params.append("client_id", config2.OIDC_CLIENT_ID);
369
- params.append("client_secret", config2.OIDC_CLIENT_SECRET);
370
- params.append("redirect_uri", config2.OIDC_REDIRECT_URI);
371
- params.append("grant_type", config2.OIDC_GRANT_TYPE);
372
- 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;
373
629
  try {
374
- const { userInfo, accessToken } = await exchangeTokenAndFetchUserInfo(
375
- config2,
376
- params,
377
- oidcNonce ?? ""
378
- );
630
+ const exchangeResult = await exchangeTokenAndFetchUserInfo(config2, params, oidcNonce ?? "");
631
+ userInfo = exchangeResult.userInfo;
632
+ const accessToken = exchangeResult.accessToken;
379
633
  const isProduction = strapi.config.get("environment") === "production";
380
634
  ctx.cookies.set("oidc_access_token", accessToken, {
381
635
  httpOnly: true,
@@ -384,7 +638,7 @@ async function oidcSignInCallback(ctx) {
384
638
  secure: isProduction && ctx.request.secure,
385
639
  sameSite: "lax"
386
640
  });
387
- const { activateUser, jwtToken } = await handleUserAuthentication(
641
+ const { activateUser, jwtToken, userCreated, rolesUpdated, resolvedRoleNames } = await handleUserAuthentication(
388
642
  userService,
389
643
  oauthService2,
390
644
  roleService2,
@@ -393,21 +647,61 @@ async function oidcSignInCallback(ctx) {
393
647
  config2,
394
648
  ctx
395
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
+ });
396
673
  const nonce = node_crypto.randomUUID();
397
674
  const html = oauthService2.renderSignUpSuccess(jwtToken, activateUser, nonce);
398
675
  ctx.set("Content-Security-Policy", `script-src 'nonce-${nonce}'`);
399
676
  ctx.send(html);
400
677
  } catch (e) {
401
- console.error("ERROR CAUGHT IN OIDC SIGNIN:", e);
402
- 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));
403
695
  }
404
696
  }
405
697
  async function logout(ctx) {
406
698
  const config2 = strapi.config.get("plugin::strapi-plugin-oidc");
699
+ const auditLog2 = strapi.plugin("strapi-plugin-oidc").service("auditLog");
407
700
  const logoutUrl = config2.OIDC_END_SESSION_ENDPOINT;
408
701
  const adminPanelUrl = strapi.config.get("admin.url", "/admin");
409
702
  const isOidcSession = !!ctx.cookies.get("oidc_authenticated");
410
703
  const accessToken = ctx.cookies.get("oidc_access_token");
704
+ const userEmail = ctx.cookies.get("oidc_user_email") ?? void 0;
411
705
  clearAuthCookies(strapi, ctx);
412
706
  if (logoutUrl && isOidcSession && accessToken) {
413
707
  try {
@@ -415,11 +709,22 @@ async function logout(ctx) {
415
709
  headers: { Authorization: `Bearer ${accessToken}` }
416
710
  });
417
711
  if (response.ok) {
712
+ if (userEmail)
713
+ auditLog2.log({ action: "logout", email: userEmail, ip: ctx.ip }).catch(() => {
714
+ });
418
715
  return ctx.redirect(logoutUrl);
419
716
  }
717
+ if (userEmail)
718
+ await auditLog2.log({ action: "session_expired", email: userEmail, ip: ctx.ip });
719
+ return ctx.redirect(`${adminPanelUrl}/auth/login`);
420
720
  } catch {
721
+ if (userEmail)
722
+ await auditLog2.log({ action: "session_expired", email: userEmail, ip: ctx.ip });
723
+ return ctx.redirect(`${adminPanelUrl}/auth/login`);
421
724
  }
422
- return ctx.redirect(`${adminPanelUrl}/auth/login`);
725
+ }
726
+ if (isOidcSession && userEmail) {
727
+ await auditLog2.log({ action: "logout", email: userEmail, ip: ctx.ip });
423
728
  }
424
729
  if (logoutUrl && isOidcSession) {
425
730
  return ctx.redirect(logoutUrl);
@@ -431,7 +736,7 @@ const oidc = {
431
736
  oidcSignInCallback,
432
737
  logout
433
738
  };
434
- async function find(ctx) {
739
+ async function find$1(ctx) {
435
740
  const roleService2 = strapi.plugin("strapi-plugin-oidc").service("role");
436
741
  const roles2 = await roleService2.find();
437
742
  const oidcConstants = roleService2.getOidcRoles();
@@ -450,14 +755,23 @@ async function update(ctx) {
450
755
  await roleService2.update(roles2);
451
756
  ctx.send({}, 204);
452
757
  } catch (e) {
453
- console.error(e);
758
+ strapi.log.error(e);
454
759
  ctx.send({}, 400);
455
760
  }
456
761
  }
457
762
  const role = {
458
- find,
763
+ find: find$1,
459
764
  update
460
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
+ }
461
775
  function getWhitelistService() {
462
776
  return strapi.plugin("strapi-plugin-oidc").service("whitelist");
463
777
  }
@@ -469,7 +783,8 @@ async function info(ctx) {
469
783
  useWhitelist: settings.useWhitelist,
470
784
  enforceOIDC: resolveEnforceOIDC(strapi, settings.enforceOIDC),
471
785
  enforceOIDCConfig: getEnforceOIDCConfig(strapi),
472
- whitelistUsers
786
+ whitelistUsers,
787
+ auditLogEnabled: isAuditLogEnabled()
473
788
  };
474
789
  }
475
790
  async function updateSettings(ctx) {
@@ -495,121 +810,85 @@ async function publicSettings(ctx) {
495
810
  };
496
811
  }
497
812
  async function register(ctx) {
498
- const { email, roles: roles2 } = ctx.request.body;
813
+ const { email } = ctx.request.body;
499
814
  if (!email) {
500
815
  ctx.body = { message: "Please enter a valid email address" };
501
816
  return;
502
817
  }
503
818
  const rawEmails = Array.isArray(email) ? email : email.split(",");
504
819
  const emailList = rawEmails.map((e) => String(e).trim().toLowerCase()).filter(Boolean);
505
- const existingUsers = await strapi.query("admin::user").findMany({
506
- where: { email: { $in: emailList } },
507
- populate: ["roles"]
508
- });
509
- const existingUsersByEmail = new Map(existingUsers.map((u) => [u.email, u]));
510
820
  const whitelistService2 = getWhitelistService();
511
821
  let matchedExistingUsersCount = 0;
512
822
  for (const singleEmail of emailList) {
513
- const existingUser = existingUsersByEmail.get(singleEmail);
514
- let finalRoles = roles2;
515
- if (existingUser?.roles) {
516
- finalRoles = existingUser.roles.map((r) => String(r.id));
517
- matchedExistingUsersCount++;
518
- }
823
+ const existingUser = await strapi.query("admin::user").findOne({
824
+ where: { email: singleEmail }
825
+ });
826
+ if (existingUser) matchedExistingUsersCount++;
519
827
  const alreadyWhitelisted = await strapi.query("plugin::strapi-plugin-oidc.whitelists").findOne({
520
828
  where: { email: singleEmail }
521
829
  });
522
830
  if (!alreadyWhitelisted) {
523
- await whitelistService2.registerUser(singleEmail, finalRoles);
831
+ await whitelistService2.registerUser(singleEmail);
524
832
  }
525
833
  }
526
834
  ctx.body = { matchedExistingUsersCount };
527
835
  }
528
836
  async function removeEmail(ctx) {
529
- const { id } = ctx.params;
837
+ const { email } = ctx.params;
530
838
  const whitelistService2 = getWhitelistService();
531
- await whitelistService2.removeUser(id);
839
+ await whitelistService2.removeUser(email);
532
840
  ctx.body = {};
533
841
  }
534
842
  async function deleteAll(ctx) {
535
843
  await strapi.query("plugin::strapi-plugin-oidc.whitelists").deleteMany({});
536
844
  ctx.body = {};
537
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
+ }
538
854
  async function importUsers(ctx) {
539
855
  const { users } = ctx.request.body;
540
856
  if (!Array.isArray(users)) {
541
857
  ctx.status = 400;
542
- ctx.body = { error: "Expected { users: [{email, roles}] }" };
858
+ ctx.body = { error: "Expected { users: [{email}] }" };
543
859
  return;
544
860
  }
545
- const allRoles = await strapi.query("admin::role").findMany({});
546
- const roleNameToId = new Map(allRoles.map((r) => [r.name, String(r.id)]));
547
- const resolveRole = (nameOrId) => roleNameToId.get(nameOrId) ?? nameOrId;
548
- const normalized = users.filter((u) => u?.email).map((u) => ({
549
- email: String(u.email).trim().toLowerCase(),
550
- roles: (Array.isArray(u.roles) ? u.roles : []).map(resolveRole)
551
- }));
552
- const seen = /* @__PURE__ */ new Set();
553
- const deduped = normalized.filter((u) => {
554
- if (seen.has(u.email)) return false;
555
- seen.add(u.email);
556
- return true;
557
- });
558
- const strapiUsers = await strapi.query("admin::user").findMany({
559
- where: { email: { $in: deduped.map((u) => u.email) } },
560
- populate: ["roles"]
561
- });
562
- 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)];
563
863
  const whitelistService2 = getWhitelistService();
564
864
  const existing = await whitelistService2.getUsers();
565
865
  const existingEmails = new Set(existing.map((u) => u.email));
566
866
  let importedCount = 0;
567
- for (const user of deduped) {
568
- if (existingEmails.has(user.email)) continue;
569
- const strapiUser = strapiUserMap.get(user.email);
570
- const finalRoles = strapiUser?.roles?.length ? strapiUser.roles.map((r) => String(r.id)) : user.roles;
571
- await whitelistService2.registerUser(user.email, finalRoles);
867
+ for (const email of deduped) {
868
+ if (existingEmails.has(email)) continue;
869
+ await whitelistService2.registerUser(email);
572
870
  importedCount++;
573
871
  }
574
872
  ctx.body = { importedCount };
575
873
  }
576
874
  async function syncUsers(ctx) {
577
875
  const { users: rawUsers } = ctx.request.body;
578
- 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));
579
877
  const whitelistService2 = getWhitelistService();
580
878
  const currentUsers = await whitelistService2.getUsers();
581
- let matchedExistingUsersCount = 0;
582
- const emailsToSync = users.map((u) => u.email);
583
- const existingStrapiUsers = await strapi.query("admin::user").findMany({
584
- where: { email: { $in: emailsToSync } },
585
- populate: ["roles"]
586
- });
587
- const syncEmailSet = new Set(emailsToSync);
879
+ const syncEmailSet = new Set(emails);
588
880
  const currentUsersByEmail = new Map(currentUsers.map((u) => [u.email, u]));
589
- const strapiUsersByEmail = new Map(existingStrapiUsers.map((u) => [u.email, u]));
590
881
  for (const currUser of currentUsers) {
591
882
  if (!syncEmailSet.has(currUser.email)) {
592
- await whitelistService2.removeUser(currUser.id);
883
+ await whitelistService2.removeUser(currUser.email);
593
884
  }
594
885
  }
595
- for (const user of users) {
596
- const currUser = currentUsersByEmail.get(user.email);
597
- let finalRoles = user.roles;
598
- if (!currUser) {
599
- const existingStrapiUser = strapiUsersByEmail.get(user.email);
600
- if (existingStrapiUser?.roles) {
601
- finalRoles = existingStrapiUser.roles.map((r) => String(r.id));
602
- matchedExistingUsersCount++;
603
- }
604
- await whitelistService2.registerUser(user.email, finalRoles);
605
- } else {
606
- await strapi.query("plugin::strapi-plugin-oidc.whitelists").update({
607
- where: { id: currUser.id },
608
- data: { roles: finalRoles }
609
- });
886
+ for (const email of emails) {
887
+ if (!currentUsersByEmail.has(email)) {
888
+ await whitelistService2.registerUser(email);
610
889
  }
611
890
  }
612
- ctx.body = { matchedExistingUsersCount };
891
+ ctx.body = { matchedExistingUsersCount: 0 };
613
892
  }
614
893
  const whitelist = {
615
894
  info,
@@ -619,39 +898,99 @@ const whitelist = {
619
898
  removeEmail,
620
899
  deleteAll,
621
900
  syncUsers,
622
- 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
623
951
  };
624
952
  const controllers = {
625
953
  oidc,
626
954
  role,
627
- whitelist
955
+ whitelist,
956
+ auditLog
628
957
  };
629
958
  const rateLimitMap = /* @__PURE__ */ new Map();
630
959
  const RATE_LIMIT_WINDOW = 6e4;
631
- const MAX_REQUESTS = 20;
632
- const rateLimitMiddleware = async (ctx, next) => {
960
+ const MAX_REQUESTS = 1e3;
961
+ function getRateLimitKey(ctx) {
633
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);
634
969
  const now = Date.now();
635
970
  const windowStart = now - RATE_LIMIT_WINDOW;
636
- const requestStamps = (rateLimitMap.get(ip) || []).filter((timestamp) => timestamp > windowStart);
971
+ const requestStamps = (rateLimitMap.get(key) || []).filter(
972
+ (timestamp) => timestamp > windowStart
973
+ );
637
974
  if (requestStamps.length >= MAX_REQUESTS) {
638
975
  ctx.status = 429;
639
976
  ctx.body = "Too Many Requests";
640
977
  return;
641
978
  }
642
979
  requestStamps.push(now);
643
- rateLimitMap.set(ip, requestStamps);
644
- await next();
645
- };
646
- const adminPolicies = (action) => ({
647
- policies: [
648
- "admin::isAuthenticatedAdmin",
649
- {
650
- name: "admin::hasPermissions",
651
- config: { actions: [`plugin::strapi-plugin-oidc.${action}`] }
652
- }
653
- ]
654
- });
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
+ }
655
994
  const routes = {
656
995
  admin: {
657
996
  type: "admin",
@@ -724,7 +1063,7 @@ const routes = {
724
1063
  },
725
1064
  {
726
1065
  method: "DELETE",
727
- path: "/whitelist/:id",
1066
+ path: "/whitelist/:email",
728
1067
  handler: "whitelist.removeEmail",
729
1068
  config: adminPolicies("update")
730
1069
  },
@@ -733,6 +1072,30 @@ const routes = {
733
1072
  path: "/whitelist",
734
1073
  handler: "whitelist.deleteAll",
735
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"] }
736
1099
  }
737
1100
  ]
738
1101
  },
@@ -759,13 +1122,33 @@ const routes = {
759
1122
  },
760
1123
  {
761
1124
  method: "DELETE",
762
- path: "/whitelist/:id",
1125
+ path: "/whitelist/:email",
763
1126
  handler: "whitelist.removeEmail"
764
1127
  },
765
1128
  {
766
1129
  method: "DELETE",
767
1130
  path: "/whitelist",
768
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"
769
1152
  }
770
1153
  ]
771
1154
  }
@@ -1136,14 +1519,14 @@ function whitelistService({ strapi: strapi2 }) {
1136
1519
  async getUsers() {
1137
1520
  return getWhitelistQuery().findMany();
1138
1521
  },
1139
- async registerUser(email, roles2) {
1522
+ async registerUser(email) {
1140
1523
  await getWhitelistQuery().create({
1141
- data: { email, roles: roles2 }
1524
+ data: { email }
1142
1525
  });
1143
1526
  },
1144
- async removeUser(id) {
1145
- await getWhitelistQuery().delete({
1146
- where: { id }
1527
+ async removeUser(email) {
1528
+ await strapi2.db.query("plugin::strapi-plugin-oidc.whitelists").deleteMany({
1529
+ where: { email }
1147
1530
  });
1148
1531
  },
1149
1532
  async checkWhitelistForEmail(email) {
@@ -1161,10 +1544,73 @@ function whitelistService({ strapi: strapi2 }) {
1161
1544
  }
1162
1545
  };
1163
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
+ }
1164
1609
  const services = {
1165
1610
  oauth: oauthService,
1166
1611
  role: roleService,
1167
- whitelist: whitelistService
1612
+ whitelist: whitelistService,
1613
+ auditLog: auditLogService
1168
1614
  };
1169
1615
  const index = {
1170
1616
  register: register$1,