@sonicjs-cms/core 2.11.0 → 2.12.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.
Files changed (78) hide show
  1. package/dist/{app-Ozl9agJG.d.cts → app-COElO4Rm.d.cts} +6 -1
  2. package/dist/{app-Ozl9agJG.d.ts → app-COElO4Rm.d.ts} +6 -1
  3. package/dist/{chunk-JTQBNSZX.js → chunk-3V2CQFIR.js} +4176 -3805
  4. package/dist/chunk-3V2CQFIR.js.map +1 -0
  5. package/dist/{chunk-BUU2US2Z.js → chunk-673VROB3.js} +3 -3
  6. package/dist/{chunk-BUU2US2Z.js.map → chunk-673VROB3.js.map} +1 -1
  7. package/dist/{chunk-H55AYIRI.js → chunk-6C6W54QP.js} +17 -3
  8. package/dist/chunk-6C6W54QP.js.map +1 -0
  9. package/dist/{chunk-JJS7JZCH.js → chunk-76TX6XND.js} +4 -2
  10. package/dist/chunk-76TX6XND.js.map +1 -0
  11. package/dist/{chunk-74XCYEI7.js → chunk-BWZBKLOC.js} +3 -3
  12. package/dist/{chunk-74XCYEI7.js.map → chunk-BWZBKLOC.js.map} +1 -1
  13. package/dist/{chunk-ASAEJ4B7.cjs → chunk-DHTCZZUB.cjs} +4376 -4001
  14. package/dist/chunk-DHTCZZUB.cjs.map +1 -0
  15. package/dist/{chunk-NMLFKXWW.js → chunk-H3XXBAMO.js} +15 -2
  16. package/dist/chunk-H3XXBAMO.js.map +1 -0
  17. package/dist/{chunk-LTKV7AE5.cjs → chunk-H4NHRZ6Y.cjs} +4 -2
  18. package/dist/chunk-H4NHRZ6Y.cjs.map +1 -0
  19. package/dist/{chunk-B2ASV5RD.cjs → chunk-HBUFGLEX.cjs} +10 -10
  20. package/dist/{chunk-B2ASV5RD.cjs.map → chunk-HBUFGLEX.cjs.map} +1 -1
  21. package/dist/{chunk-6BVLPACH.cjs → chunk-I6FFGQIT.cjs} +15 -2
  22. package/dist/chunk-I6FFGQIT.cjs.map +1 -0
  23. package/dist/{chunk-QLPFENZ2.cjs → chunk-IKBAY2M2.cjs} +3 -3
  24. package/dist/{chunk-QLPFENZ2.cjs.map → chunk-IKBAY2M2.cjs.map} +1 -1
  25. package/dist/{chunk-LFAQUR7P.cjs → chunk-NZWFCUDA.cjs} +26 -2
  26. package/dist/chunk-NZWFCUDA.cjs.map +1 -0
  27. package/dist/{chunk-3G7XX4UI.cjs → chunk-RBXFXT7H.cjs} +9 -9
  28. package/dist/{chunk-3G7XX4UI.cjs.map → chunk-RBXFXT7H.cjs.map} +1 -1
  29. package/dist/{chunk-VJCLJH3X.js → chunk-TBJY2FF7.js} +26 -2
  30. package/dist/chunk-TBJY2FF7.js.map +1 -0
  31. package/dist/{chunk-GKRGDJGG.js → chunk-UFWE3MEJ.js} +6 -6
  32. package/dist/{chunk-GKRGDJGG.js.map → chunk-UFWE3MEJ.js.map} +1 -1
  33. package/dist/{chunk-DE5YTNCD.cjs → chunk-XK3TKOLQ.cjs} +17 -3
  34. package/dist/chunk-XK3TKOLQ.cjs.map +1 -0
  35. package/dist/index.cjs +1828 -170
  36. package/dist/index.cjs.map +1 -1
  37. package/dist/index.d.cts +53 -5
  38. package/dist/index.d.ts +53 -5
  39. package/dist/index.js +1656 -13
  40. package/dist/index.js.map +1 -1
  41. package/dist/middleware.cjs +29 -29
  42. package/dist/middleware.d.cts +1 -1
  43. package/dist/middleware.d.ts +1 -1
  44. package/dist/middleware.js +3 -3
  45. package/dist/migrations-AH2XIFSA.js +4 -0
  46. package/dist/{migrations-UFVJTPVT.js.map → migrations-AH2XIFSA.js.map} +1 -1
  47. package/dist/migrations-MIZFGFNS.cjs +13 -0
  48. package/dist/{migrations-VNYOSUNE.cjs.map → migrations-MIZFGFNS.cjs.map} +1 -1
  49. package/dist/{plugin-bootstrap-DXBAYaqM.d.ts → plugin-bootstrap-CZ1GDum7.d.ts} +802 -1
  50. package/dist/{plugin-bootstrap-DCXpeQVb.d.cts → plugin-bootstrap-DVGLQrcO.d.cts} +802 -1
  51. package/dist/routes.cjs +30 -30
  52. package/dist/routes.d.cts +1 -1
  53. package/dist/routes.d.ts +1 -1
  54. package/dist/routes.js +7 -7
  55. package/dist/services.cjs +39 -39
  56. package/dist/services.d.cts +1 -1
  57. package/dist/services.d.ts +1 -1
  58. package/dist/services.js +3 -3
  59. package/dist/templates.cjs +19 -19
  60. package/dist/templates.js +2 -2
  61. package/dist/utils.cjs +11 -11
  62. package/dist/utils.js +1 -1
  63. package/migrations/032_user_profiles.sql +1 -0
  64. package/migrations/034_security_audit_plugin.sql +27 -0
  65. package/migrations/035_user_profiles_data_column.sql +17 -0
  66. package/package.json +1 -1
  67. package/dist/chunk-6BVLPACH.cjs.map +0 -1
  68. package/dist/chunk-ASAEJ4B7.cjs.map +0 -1
  69. package/dist/chunk-DE5YTNCD.cjs.map +0 -1
  70. package/dist/chunk-H55AYIRI.js.map +0 -1
  71. package/dist/chunk-JJS7JZCH.js.map +0 -1
  72. package/dist/chunk-JTQBNSZX.js.map +0 -1
  73. package/dist/chunk-LFAQUR7P.cjs.map +0 -1
  74. package/dist/chunk-LTKV7AE5.cjs.map +0 -1
  75. package/dist/chunk-NMLFKXWW.js.map +0 -1
  76. package/dist/chunk-VJCLJH3X.js.map +0 -1
  77. package/dist/migrations-UFVJTPVT.js +0 -4
  78. package/dist/migrations-VNYOSUNE.cjs +0 -13
package/dist/index.js CHANGED
@@ -1,19 +1,20 @@
1
- import { renderConfirmationDialog, getConfirmationDialogScript, api_default, api_media_default, api_system_default, admin_api_default, router, adminCollectionsRoutes, adminFormsRoutes, adminSettingsRoutes, public_forms_default, router2, admin_content_default, adminMediaRoutes, adminPluginRoutes, adminLogsRoutes, userRoutes, auth_default, test_cleanup_default } from './chunk-JTQBNSZX.js';
2
- export { ROUTES_INFO, admin_api_default as adminApiRoutes, adminCheckboxRoutes, admin_code_examples_default as adminCodeExamplesRoutes, adminCollectionsRoutes, admin_content_default as adminContentRoutes, router as adminDashboardRoutes, adminDesignRoutes, adminLogsRoutes, adminMediaRoutes, adminPluginRoutes, adminSettingsRoutes, admin_testimonials_default as adminTestimonialsRoutes, userRoutes as adminUsersRoutes, api_content_crud_default as apiContentCrudRoutes, api_media_default as apiMediaRoutes, api_default as apiRoutes, api_system_default as apiSystemRoutes, auth_default as authRoutes } from './chunk-JTQBNSZX.js';
3
- import { SettingsService, setAppInstance, schema_exports } from './chunk-VJCLJH3X.js';
4
- export { Logger, apiTokens, collections, content, contentVersions, getLogger, initLogger, insertCollectionSchema, insertContentSchema, insertLogConfigSchema, insertMediaSchema, insertPluginActivityLogSchema, insertPluginAssetSchema, insertPluginHookSchema, insertPluginRouteSchema, insertPluginSchema, insertSystemLogSchema, insertUserSchema, insertWorkflowHistorySchema, logConfig, media, pluginActivityLog, pluginAssets, pluginHooks, pluginRoutes, plugins, selectCollectionSchema, selectContentSchema, selectLogConfigSchema, selectMediaSchema, selectPluginActivityLogSchema, selectPluginAssetSchema, selectPluginHookSchema, selectPluginRouteSchema, selectPluginSchema, selectSystemLogSchema, selectUserSchema, selectWorkflowHistorySchema, systemLogs, users, workflowHistory } from './chunk-VJCLJH3X.js';
5
- import { requireAuth, AuthManager, metricsMiddleware, bootstrapMiddleware, securityHeadersMiddleware, csrfProtection } from './chunk-GKRGDJGG.js';
6
- export { AuthManager, PermissionManager, bootstrapMiddleware, cacheHeaders, compressionMiddleware, detailedLoggingMiddleware, getActivePlugins, isPluginActive, logActivity, loggingMiddleware, optionalAuth, performanceLoggingMiddleware, requireActivePlugin, requireActivePlugins, requireAnyPermission, requireAuth, requirePermission, requireRole, securityHeadersMiddleware as securityHeaders, securityLoggingMiddleware } from './chunk-GKRGDJGG.js';
7
- export { PluginBootstrapService, PluginService as PluginServiceClass, backfillFormSubmissions, cleanupRemovedCollections, createContentFromSubmission, deriveCollectionSchemaFromFormio, deriveSubmissionTitle, fullCollectionSync, getAvailableCollectionNames, getManagedCollections, isCollectionManaged, loadCollectionConfig, loadCollectionConfigs, mapFormStatusToContentStatus, registerCollections, syncAllFormCollections, syncCollection, syncCollections, syncFormCollection, validateCollectionConfig } from './chunk-NMLFKXWW.js';
8
- export { MigrationService } from './chunk-H55AYIRI.js';
9
- export { renderFilterBar } from './chunk-74XCYEI7.js';
10
- import { init_admin_layout_catalyst_template, renderAdminLayout, renderAdminLayoutCatalyst } from './chunk-JJS7JZCH.js';
11
- export { getConfirmationDialogScript, renderAlert, renderConfirmationDialog, renderForm, renderFormField, renderPagination, renderTable } from './chunk-JJS7JZCH.js';
1
+ import { renderConfirmationDialog, getConfirmationDialogScript, api_default, api_media_default, api_system_default, admin_api_default, router, adminCollectionsRoutes, adminFormsRoutes, adminSettingsRoutes, public_forms_default, router2, admin_content_default, adminMediaRoutes, userProfilesPlugin, adminPluginRoutes, adminLogsRoutes, userRoutes, auth_default, test_cleanup_default } from './chunk-3V2CQFIR.js';
2
+ export { ROUTES_INFO, admin_api_default as adminApiRoutes, adminCheckboxRoutes, admin_code_examples_default as adminCodeExamplesRoutes, adminCollectionsRoutes, admin_content_default as adminContentRoutes, router as adminDashboardRoutes, adminDesignRoutes, adminLogsRoutes, adminMediaRoutes, adminPluginRoutes, adminSettingsRoutes, admin_testimonials_default as adminTestimonialsRoutes, userRoutes as adminUsersRoutes, api_content_crud_default as apiContentCrudRoutes, api_media_default as apiMediaRoutes, api_default as apiRoutes, api_system_default as apiSystemRoutes, auth_default as authRoutes, createUserProfilesPlugin, defineUserProfile, getUserProfileConfig, userProfilesPlugin } from './chunk-3V2CQFIR.js';
3
+ import { SettingsService, setAppInstance, schema_exports } from './chunk-TBJY2FF7.js';
4
+ export { Logger, apiTokens, collections, content, contentVersions, getLogger, initLogger, insertCollectionSchema, insertContentSchema, insertLogConfigSchema, insertMediaSchema, insertPluginActivityLogSchema, insertPluginAssetSchema, insertPluginHookSchema, insertPluginRouteSchema, insertPluginSchema, insertSystemLogSchema, insertUserSchema, insertWorkflowHistorySchema, logConfig, media, pluginActivityLog, pluginAssets, pluginHooks, pluginRoutes, plugins, selectCollectionSchema, selectContentSchema, selectLogConfigSchema, selectMediaSchema, selectPluginActivityLogSchema, selectPluginAssetSchema, selectPluginHookSchema, selectPluginRouteSchema, selectPluginSchema, selectSystemLogSchema, selectUserSchema, selectWorkflowHistorySchema, systemLogs, users, workflowHistory } from './chunk-TBJY2FF7.js';
5
+ import { requireAuth, AuthManager, metricsMiddleware, bootstrapMiddleware, securityHeadersMiddleware, csrfProtection } from './chunk-UFWE3MEJ.js';
6
+ export { AuthManager, PermissionManager, bootstrapMiddleware, cacheHeaders, compressionMiddleware, detailedLoggingMiddleware, getActivePlugins, isPluginActive, logActivity, loggingMiddleware, optionalAuth, performanceLoggingMiddleware, requireActivePlugin, requireActivePlugins, requireAnyPermission, requireAuth, requirePermission, requireRole, securityHeadersMiddleware as securityHeaders, securityLoggingMiddleware } from './chunk-UFWE3MEJ.js';
7
+ import { PluginService } from './chunk-H3XXBAMO.js';
8
+ export { PluginBootstrapService, PluginService as PluginServiceClass, backfillFormSubmissions, cleanupRemovedCollections, createContentFromSubmission, deriveCollectionSchemaFromFormio, deriveSubmissionTitle, fullCollectionSync, getAvailableCollectionNames, getManagedCollections, isCollectionManaged, loadCollectionConfig, loadCollectionConfigs, mapFormStatusToContentStatus, registerCollections, syncAllFormCollections, syncCollection, syncCollections, syncFormCollection, validateCollectionConfig } from './chunk-H3XXBAMO.js';
9
+ export { MigrationService } from './chunk-6C6W54QP.js';
10
+ export { renderFilterBar } from './chunk-BWZBKLOC.js';
11
+ import { init_admin_layout_catalyst_template, renderAdminLayout, renderAdminLayoutCatalyst } from './chunk-76TX6XND.js';
12
+ export { getConfirmationDialogScript, renderAlert, renderConfirmationDialog, renderForm, renderFormField, renderPagination, renderTable } from './chunk-76TX6XND.js';
12
13
  export { HookSystemImpl, HookUtils, PluginManager as PluginManagerClass, PluginRegistryImpl, PluginValidator as PluginValidatorClass, ScopedHookSystem as ScopedHookSystemClass } from './chunk-2MXF4RYZ.js';
13
14
  import { PluginBuilder } from './chunk-J5WGMRSU.js';
14
15
  export { PluginBuilder, PluginHelpers } from './chunk-J5WGMRSU.js';
15
- import { package_default, getCoreVersion } from './chunk-BUU2US2Z.js';
16
- export { QueryFilterBuilder, SONICJS_VERSION, TemplateRenderer, buildQuery, getCoreVersion, renderTemplate, templateRenderer } from './chunk-BUU2US2Z.js';
16
+ import { package_default, getCoreVersion } from './chunk-673VROB3.js';
17
+ export { QueryFilterBuilder, SONICJS_VERSION, TemplateRenderer, buildQuery, getCoreVersion, renderTemplate, templateRenderer } from './chunk-673VROB3.js';
17
18
  import './chunk-X7ZAEI5S.js';
18
19
  export { metricsTracker } from './chunk-FICTAGD4.js';
19
20
  export { escapeHtml, sanitizeInput, sanitizeObject } from './chunk-TQABQWOP.js';
@@ -4693,6 +4694,1636 @@ function renderMagicLinkEmail(magicLink, expiryMinutes) {
4693
4694
  }
4694
4695
  createMagicLinkAuthPlugin();
4695
4696
 
4697
+ // src/plugins/core-plugins/security-audit-plugin/types.ts
4698
+ var DEFAULT_SETTINGS2 = {
4699
+ retention: {
4700
+ daysToKeep: 90,
4701
+ maxEvents: 1e5,
4702
+ autoPurge: true
4703
+ },
4704
+ bruteForce: {
4705
+ enabled: true,
4706
+ maxFailedAttemptsPerIP: 10,
4707
+ maxFailedAttemptsPerEmail: 5,
4708
+ windowMinutes: 15,
4709
+ lockoutDurationMinutes: 30,
4710
+ alertThreshold: 20
4711
+ },
4712
+ logging: {
4713
+ logSuccessfulLogins: true,
4714
+ logLogouts: true,
4715
+ logRegistrations: true,
4716
+ logPasswordResets: true,
4717
+ logPermissionDenied: true
4718
+ }
4719
+ };
4720
+
4721
+ // src/plugins/core-plugins/security-audit-plugin/services/security-audit-service.ts
4722
+ var SecurityAuditService = class {
4723
+ constructor(db, settings = DEFAULT_SETTINGS2) {
4724
+ this.db = db;
4725
+ this.settings = settings;
4726
+ }
4727
+ async logEvent(event) {
4728
+ const id = crypto.randomUUID();
4729
+ const now = Date.now();
4730
+ await this.db.prepare(`
4731
+ INSERT INTO security_events (id, event_type, severity, user_id, email, ip_address, user_agent, country_code, request_path, request_method, details, fingerprint, blocked, created_at)
4732
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
4733
+ `).bind(
4734
+ id,
4735
+ event.eventType,
4736
+ event.severity || "info",
4737
+ event.userId || null,
4738
+ event.email || null,
4739
+ event.ipAddress || null,
4740
+ event.userAgent || null,
4741
+ event.countryCode || null,
4742
+ event.requestPath || null,
4743
+ event.requestMethod || null,
4744
+ event.details ? JSON.stringify(event.details) : null,
4745
+ event.fingerprint || null,
4746
+ event.blocked ? 1 : 0,
4747
+ now
4748
+ ).run();
4749
+ return id;
4750
+ }
4751
+ async getEvents(filters = {}) {
4752
+ const conditions = [];
4753
+ const params = [];
4754
+ if (filters.eventType) {
4755
+ if (Array.isArray(filters.eventType)) {
4756
+ conditions.push(`event_type IN (${filters.eventType.map(() => "?").join(",")})`);
4757
+ params.push(...filters.eventType);
4758
+ } else {
4759
+ conditions.push("event_type = ?");
4760
+ params.push(filters.eventType);
4761
+ }
4762
+ }
4763
+ if (filters.severity) {
4764
+ if (Array.isArray(filters.severity)) {
4765
+ conditions.push(`severity IN (${filters.severity.map(() => "?").join(",")})`);
4766
+ params.push(...filters.severity);
4767
+ } else {
4768
+ conditions.push("severity = ?");
4769
+ params.push(filters.severity);
4770
+ }
4771
+ }
4772
+ if (filters.email) {
4773
+ conditions.push("email LIKE ?");
4774
+ params.push(`%${filters.email}%`);
4775
+ }
4776
+ if (filters.ipAddress) {
4777
+ conditions.push("ip_address LIKE ?");
4778
+ params.push(`%${filters.ipAddress}%`);
4779
+ }
4780
+ if (filters.search) {
4781
+ conditions.push("(email LIKE ? OR ip_address LIKE ? OR details LIKE ?)");
4782
+ params.push(`%${filters.search}%`, `%${filters.search}%`, `%${filters.search}%`);
4783
+ }
4784
+ if (filters.startDate) {
4785
+ conditions.push("created_at >= ?");
4786
+ params.push(filters.startDate);
4787
+ }
4788
+ if (filters.endDate) {
4789
+ conditions.push("created_at <= ?");
4790
+ params.push(filters.endDate);
4791
+ }
4792
+ if (filters.blocked !== void 0) {
4793
+ conditions.push("blocked = ?");
4794
+ params.push(filters.blocked ? 1 : 0);
4795
+ }
4796
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
4797
+ const sortBy = filters.sortBy || "created_at";
4798
+ const sortOrder = filters.sortOrder || "desc";
4799
+ const page = filters.page || 1;
4800
+ const limit = filters.limit || 50;
4801
+ const offset = (page - 1) * limit;
4802
+ const countResult = await this.db.prepare(
4803
+ `SELECT COUNT(*) as count FROM security_events ${where}`
4804
+ ).bind(...params).first();
4805
+ const total = countResult?.count || 0;
4806
+ const results = await this.db.prepare(
4807
+ `SELECT * FROM security_events ${where} ORDER BY ${sortBy} ${sortOrder} LIMIT ? OFFSET ?`
4808
+ ).bind(...params, limit, offset).all();
4809
+ const events = (results.results || []).map((row) => ({
4810
+ id: row.id,
4811
+ eventType: row.event_type,
4812
+ severity: row.severity,
4813
+ userId: row.user_id,
4814
+ email: row.email,
4815
+ ipAddress: row.ip_address,
4816
+ userAgent: row.user_agent,
4817
+ countryCode: row.country_code,
4818
+ requestPath: row.request_path,
4819
+ requestMethod: row.request_method,
4820
+ details: row.details ? JSON.parse(row.details) : null,
4821
+ fingerprint: row.fingerprint,
4822
+ blocked: !!row.blocked,
4823
+ createdAt: row.created_at
4824
+ }));
4825
+ return { events, total };
4826
+ }
4827
+ async getEvent(id) {
4828
+ const row = await this.db.prepare(
4829
+ "SELECT * FROM security_events WHERE id = ?"
4830
+ ).bind(id).first();
4831
+ if (!row) return null;
4832
+ return {
4833
+ id: row.id,
4834
+ eventType: row.event_type,
4835
+ severity: row.severity,
4836
+ userId: row.user_id,
4837
+ email: row.email,
4838
+ ipAddress: row.ip_address,
4839
+ userAgent: row.user_agent,
4840
+ countryCode: row.country_code,
4841
+ requestPath: row.request_path,
4842
+ requestMethod: row.request_method,
4843
+ details: row.details ? JSON.parse(row.details) : null,
4844
+ fingerprint: row.fingerprint,
4845
+ blocked: !!row.blocked,
4846
+ createdAt: row.created_at
4847
+ };
4848
+ }
4849
+ async getStats() {
4850
+ const now = Date.now();
4851
+ const h24 = now - 24 * 60 * 60 * 1e3;
4852
+ const h48 = now - 48 * 60 * 60 * 1e3;
4853
+ const totalResult = await this.db.prepare(
4854
+ "SELECT COUNT(*) as count FROM security_events"
4855
+ ).first();
4856
+ const failed24hResult = await this.db.prepare(
4857
+ "SELECT COUNT(*) as count FROM security_events WHERE event_type = 'login_failure' AND created_at >= ?"
4858
+ ).bind(h24).first();
4859
+ const failedPrior24hResult = await this.db.prepare(
4860
+ "SELECT COUNT(*) as count FROM security_events WHERE event_type = 'login_failure' AND created_at >= ? AND created_at < ?"
4861
+ ).bind(h48, h24).first();
4862
+ const failed24h = failed24hResult?.count || 0;
4863
+ const failedPrior24h = failedPrior24hResult?.count || 0;
4864
+ const trend = failedPrior24h > 0 ? Math.round((failed24h - failedPrior24h) / failedPrior24h * 100) : failed24h > 0 ? 100 : 0;
4865
+ const lockoutWindow = now - this.settings.bruteForce.lockoutDurationMinutes * 60 * 1e3;
4866
+ const lockoutsResult = await this.db.prepare(
4867
+ "SELECT COUNT(DISTINCT ip_address) as count FROM security_events WHERE event_type = 'account_lockout' AND created_at >= ?"
4868
+ ).bind(lockoutWindow).first();
4869
+ const windowStart = now - this.settings.bruteForce.windowMinutes * 60 * 1e3;
4870
+ const flaggedResult = await this.db.prepare(
4871
+ `SELECT COUNT(*) as count FROM (
4872
+ SELECT ip_address FROM security_events
4873
+ WHERE event_type = 'login_failure' AND created_at >= ?
4874
+ GROUP BY ip_address HAVING COUNT(*) >= ?
4875
+ )`
4876
+ ).bind(windowStart, this.settings.bruteForce.maxFailedAttemptsPerIP).first();
4877
+ const typeResults = await this.db.prepare(
4878
+ "SELECT event_type, COUNT(*) as count FROM security_events WHERE created_at >= ? GROUP BY event_type"
4879
+ ).bind(h24).all();
4880
+ const eventsByType = {};
4881
+ for (const row of typeResults.results || []) {
4882
+ eventsByType[row.event_type] = row.count;
4883
+ }
4884
+ const severityResults = await this.db.prepare(
4885
+ "SELECT severity, COUNT(*) as count FROM security_events WHERE created_at >= ? GROUP BY severity"
4886
+ ).bind(h24).all();
4887
+ const eventsBySeverity = {};
4888
+ for (const row of severityResults.results || []) {
4889
+ eventsBySeverity[row.severity] = row.count;
4890
+ }
4891
+ return {
4892
+ totalEvents: totalResult?.count || 0,
4893
+ failedLogins24h: failed24h,
4894
+ failedLoginsTrend: trend,
4895
+ activeLockouts: lockoutsResult?.count || 0,
4896
+ flaggedIPs: flaggedResult?.count || 0,
4897
+ eventsByType,
4898
+ eventsBySeverity
4899
+ };
4900
+ }
4901
+ async getTopIPs(limit = 10) {
4902
+ const now = Date.now();
4903
+ const h24 = now - 24 * 60 * 60 * 1e3;
4904
+ const results = await this.db.prepare(`
4905
+ SELECT
4906
+ ip_address,
4907
+ country_code,
4908
+ COUNT(*) as failed_attempts,
4909
+ MAX(created_at) as last_seen
4910
+ FROM security_events
4911
+ WHERE event_type = 'login_failure' AND created_at >= ?
4912
+ GROUP BY ip_address
4913
+ ORDER BY failed_attempts DESC
4914
+ LIMIT ?
4915
+ `).bind(h24, limit).all();
4916
+ const lockoutWindow = now - this.settings.bruteForce.lockoutDurationMinutes * 60 * 1e3;
4917
+ const lockoutResults = await this.db.prepare(
4918
+ "SELECT DISTINCT ip_address FROM security_events WHERE event_type = 'account_lockout' AND created_at >= ?"
4919
+ ).bind(lockoutWindow).all();
4920
+ const lockedIPs = new Set((lockoutResults.results || []).map((r) => r.ip_address));
4921
+ return (results.results || []).map((row) => ({
4922
+ ipAddress: row.ip_address,
4923
+ countryCode: row.country_code,
4924
+ failedAttempts: row.failed_attempts,
4925
+ lastSeen: row.last_seen,
4926
+ locked: lockedIPs.has(row.ip_address)
4927
+ }));
4928
+ }
4929
+ async getHourlyTrend(hours = 24) {
4930
+ const now = Date.now();
4931
+ const start = now - hours * 60 * 60 * 1e3;
4932
+ const buckets = [];
4933
+ for (let i = 0; i < hours; i++) {
4934
+ const bucketStart = start + i * 60 * 60 * 1e3;
4935
+ const date = new Date(bucketStart);
4936
+ buckets.push({
4937
+ hour: `${date.getUTCHours().toString().padStart(2, "0")}:00`,
4938
+ count: 0
4939
+ });
4940
+ }
4941
+ const results = await this.db.prepare(`
4942
+ SELECT
4943
+ CAST((created_at - ?) / 3600000 AS INTEGER) as bucket,
4944
+ COUNT(*) as count
4945
+ FROM security_events
4946
+ WHERE event_type = 'login_failure' AND created_at >= ?
4947
+ GROUP BY bucket
4948
+ ORDER BY bucket
4949
+ `).bind(start, start).all();
4950
+ for (const row of results.results || []) {
4951
+ const idx = row.bucket;
4952
+ if (idx >= 0 && idx < buckets.length) {
4953
+ buckets[idx].count = row.count;
4954
+ }
4955
+ }
4956
+ return buckets;
4957
+ }
4958
+ async purgeOldEvents(daysToKeep) {
4959
+ const days = daysToKeep || this.settings.retention.daysToKeep;
4960
+ const cutoff = Date.now() - days * 24 * 60 * 60 * 1e3;
4961
+ const result = await this.db.prepare(
4962
+ "DELETE FROM security_events WHERE created_at < ?"
4963
+ ).bind(cutoff).run();
4964
+ return result.meta?.changes || 0;
4965
+ }
4966
+ async getRecentCriticalEvents(limit = 20) {
4967
+ const results = await this.db.prepare(
4968
+ "SELECT * FROM security_events WHERE severity = 'critical' ORDER BY created_at DESC LIMIT ?"
4969
+ ).bind(limit).all();
4970
+ return (results.results || []).map((row) => ({
4971
+ id: row.id,
4972
+ eventType: row.event_type,
4973
+ severity: row.severity,
4974
+ userId: row.user_id,
4975
+ email: row.email,
4976
+ ipAddress: row.ip_address,
4977
+ userAgent: row.user_agent,
4978
+ countryCode: row.country_code,
4979
+ requestPath: row.request_path,
4980
+ requestMethod: row.request_method,
4981
+ details: row.details ? JSON.parse(row.details) : null,
4982
+ fingerprint: row.fingerprint,
4983
+ blocked: !!row.blocked,
4984
+ createdAt: row.created_at
4985
+ }));
4986
+ }
4987
+ };
4988
+
4989
+ // src/plugins/core-plugins/security-audit-plugin/components/dashboard-page.ts
4990
+ init_admin_layout_catalyst_template();
4991
+ function formatTimestamp(ts) {
4992
+ const date = new Date(ts);
4993
+ const now = Date.now();
4994
+ const diff = now - ts;
4995
+ if (diff < 6e4) return "just now";
4996
+ if (diff < 36e5) return `${Math.floor(diff / 6e4)}m ago`;
4997
+ if (diff < 864e5) return `${Math.floor(diff / 36e5)}h ago`;
4998
+ return date.toLocaleDateString("en-US", { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
4999
+ }
5000
+ function severityBadge(severity) {
5001
+ const colors = {
5002
+ info: "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400",
5003
+ warning: "bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400",
5004
+ critical: "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400"
5005
+ };
5006
+ return `<span class="inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ${colors[severity] || colors.info}">${severity}</span>`;
5007
+ }
5008
+ function eventTypeBadge(type) {
5009
+ const labels = {
5010
+ login_success: "Login OK",
5011
+ login_failure: "Login Failed",
5012
+ registration: "Registration",
5013
+ account_lockout: "Lockout",
5014
+ suspicious_activity: "Suspicious",
5015
+ logout: "Logout",
5016
+ password_reset_request: "Password Reset",
5017
+ permission_denied: "Access Denied"
5018
+ };
5019
+ return labels[type] || type;
5020
+ }
5021
+ function trendArrow(trend) {
5022
+ if (trend > 0) return `<span class="text-red-500">+${trend}%</span>`;
5023
+ if (trend < 0) return `<span class="text-emerald-500">${trend}%</span>`;
5024
+ return `<span class="text-zinc-400">0%</span>`;
5025
+ }
5026
+ function renderBarChart(data) {
5027
+ if (data.length === 0) return '<p class="text-zinc-500 text-sm">No data available</p>';
5028
+ const max = Math.max(...data.map((d) => d.count), 1);
5029
+ const bars = data.map((d) => {
5030
+ const height = Math.max(d.count / max * 100, 2);
5031
+ const color = d.count === 0 ? "bg-zinc-200 dark:bg-zinc-700" : d.count >= max * 0.75 ? "bg-red-500" : d.count >= max * 0.5 ? "bg-amber-500" : "bg-cyan-500";
5032
+ return `
5033
+ <div class="flex flex-col items-center flex-1 min-w-0 group relative">
5034
+ <div class="w-full flex flex-col items-center justify-end" style="height: 120px">
5035
+ <div class="absolute bottom-8 hidden group-hover:block bg-zinc-900 text-white text-xs rounded px-2 py-1 whitespace-nowrap z-10">
5036
+ ${d.hour}: ${d.count} failed
5037
+ </div>
5038
+ <div class="${color} w-full max-w-[12px] rounded-t transition-all" style="height: ${height}%"></div>
5039
+ </div>
5040
+ <span class="text-[9px] text-zinc-400 mt-1 ${data.length > 12 ? "hidden sm:block" : ""}">${d.hour}</span>
5041
+ </div>
5042
+ `;
5043
+ }).join("");
5044
+ return `<div class="flex items-end gap-px">${bars}</div>`;
5045
+ }
5046
+ function renderSecurityDashboard(data) {
5047
+ const { stats, topIPs, hourlyTrend, recentCritical, user, version, dynamicMenuItems } = data;
5048
+ const content2 = `
5049
+ <div>
5050
+ <div class="sm:flex sm:items-center sm:justify-between mb-6">
5051
+ <div class="sm:flex-auto">
5052
+ <h1 class="text-2xl/8 font-semibold text-zinc-950 dark:text-white sm:text-xl/8">Security Dashboard</h1>
5053
+ <p class="mt-2 text-sm/6 text-zinc-500 dark:text-zinc-400">
5054
+ Monitor login attempts, brute-force detection, and security events.
5055
+ </p>
5056
+ </div>
5057
+ <div class="mt-4 sm:mt-0 sm:ml-16 flex gap-x-2">
5058
+ <a href="/admin/plugins/security-audit/events"
5059
+ class="inline-flex items-center justify-center rounded-lg bg-white dark:bg-zinc-800 px-3.5 py-2.5 text-sm font-semibold text-zinc-950 dark:text-white hover:bg-zinc-50 dark:hover:bg-zinc-700 ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 transition-colors shadow-sm">
5060
+ View Event Log
5061
+ </a>
5062
+ <a href="/api/security-audit/export?format=csv"
5063
+ class="inline-flex items-center justify-center rounded-lg bg-zinc-950 dark:bg-white px-3.5 py-2.5 text-sm font-semibold text-white dark:text-zinc-950 hover:bg-zinc-800 dark:hover:bg-zinc-100 transition-colors shadow-sm">
5064
+ Export CSV
5065
+ </a>
5066
+ </div>
5067
+ </div>
5068
+
5069
+ <!-- Summary Cards -->
5070
+ <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4 mb-6">
5071
+ <div class="rounded-xl bg-white/80 dark:bg-zinc-900/80 backdrop-blur-xl p-5 ring-1 ring-zinc-950/5 dark:ring-white/10 shadow-sm">
5072
+ <p class="text-sm font-medium text-zinc-500 dark:text-zinc-400">Total Events</p>
5073
+ <p class="mt-2 text-3xl font-bold text-zinc-950 dark:text-white">${stats.totalEvents.toLocaleString()}</p>
5074
+ </div>
5075
+ <div class="rounded-xl bg-white/80 dark:bg-zinc-900/80 backdrop-blur-xl p-5 ring-1 ring-zinc-950/5 dark:ring-white/10 shadow-sm">
5076
+ <p class="text-sm font-medium text-zinc-500 dark:text-zinc-400">Failed Logins (24h)</p>
5077
+ <p class="mt-2 text-3xl font-bold text-zinc-950 dark:text-white">
5078
+ ${stats.failedLogins24h}
5079
+ <span class="ml-2 text-sm font-normal">${trendArrow(stats.failedLoginsTrend)}</span>
5080
+ </p>
5081
+ </div>
5082
+ <div class="rounded-xl bg-white/80 dark:bg-zinc-900/80 backdrop-blur-xl p-5 ring-1 ring-zinc-950/5 dark:ring-white/10 shadow-sm">
5083
+ <p class="text-sm font-medium text-zinc-500 dark:text-zinc-400">Active Lockouts</p>
5084
+ <p class="mt-2 text-3xl font-bold ${stats.activeLockouts > 0 ? "text-red-600 dark:text-red-400" : "text-zinc-950 dark:text-white"}">${stats.activeLockouts}</p>
5085
+ </div>
5086
+ <div class="rounded-xl bg-white/80 dark:bg-zinc-900/80 backdrop-blur-xl p-5 ring-1 ring-zinc-950/5 dark:ring-white/10 shadow-sm">
5087
+ <p class="text-sm font-medium text-zinc-500 dark:text-zinc-400">Flagged IPs</p>
5088
+ <p class="mt-2 text-3xl font-bold ${stats.flaggedIPs > 0 ? "text-amber-600 dark:text-amber-400" : "text-zinc-950 dark:text-white"}">${stats.flaggedIPs}</p>
5089
+ </div>
5090
+ </div>
5091
+
5092
+ <!-- Charts Row -->
5093
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
5094
+ <!-- Failed Login Trend -->
5095
+ <div class="rounded-xl bg-white/80 dark:bg-zinc-900/80 backdrop-blur-xl p-5 ring-1 ring-zinc-950/5 dark:ring-white/10 shadow-sm">
5096
+ <h2 class="text-sm font-semibold text-zinc-950 dark:text-white mb-4">Failed Login Attempts (24h)</h2>
5097
+ ${renderBarChart(hourlyTrend)}
5098
+ </div>
5099
+
5100
+ <!-- Events by Type -->
5101
+ <div class="rounded-xl bg-white/80 dark:bg-zinc-900/80 backdrop-blur-xl p-5 ring-1 ring-zinc-950/5 dark:ring-white/10 shadow-sm">
5102
+ <h2 class="text-sm font-semibold text-zinc-950 dark:text-white mb-4">Events by Type (24h)</h2>
5103
+ <div class="space-y-3">
5104
+ ${Object.entries(stats.eventsByType).length === 0 ? '<p class="text-zinc-500 text-sm">No events in the last 24 hours</p>' : Object.entries(stats.eventsByType).sort(([, a], [, b]) => b - a).map(([type, count]) => {
5105
+ const total = Object.values(stats.eventsByType).reduce((s, v) => s + v, 0);
5106
+ const pct = total > 0 ? Math.round(count / total * 100) : 0;
5107
+ return `
5108
+ <div>
5109
+ <div class="flex justify-between text-sm mb-1">
5110
+ <span class="text-zinc-600 dark:text-zinc-300">${eventTypeBadge(type)}</span>
5111
+ <span class="text-zinc-500 dark:text-zinc-400">${count}</span>
5112
+ </div>
5113
+ <div class="w-full bg-zinc-100 dark:bg-zinc-800 rounded-full h-1.5">
5114
+ <div class="h-1.5 rounded-full ${type === "login_failure" ? "bg-red-500" : type === "login_success" ? "bg-emerald-500" : "bg-cyan-500"}" style="width: ${pct}%"></div>
5115
+ </div>
5116
+ </div>
5117
+ `;
5118
+ }).join("")}
5119
+ </div>
5120
+ </div>
5121
+ </div>
5122
+
5123
+ <!-- Top IPs and Recent Critical -->
5124
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
5125
+ <!-- Top IPs -->
5126
+ <div class="rounded-xl bg-white/80 dark:bg-zinc-900/80 backdrop-blur-xl ring-1 ring-zinc-950/5 dark:ring-white/10 shadow-sm overflow-hidden">
5127
+ <div class="p-5 border-b border-zinc-100 dark:border-zinc-800">
5128
+ <h2 class="text-sm font-semibold text-zinc-950 dark:text-white">Top IPs by Failed Logins (24h)</h2>
5129
+ </div>
5130
+ ${topIPs.length === 0 ? '<div class="p-5"><p class="text-zinc-500 text-sm">No failed login attempts</p></div>' : `<table class="min-w-full">
5131
+ <thead>
5132
+ <tr class="border-b border-zinc-100 dark:border-zinc-800">
5133
+ <th class="px-5 py-3 text-left text-xs font-medium text-zinc-500 uppercase">IP Address</th>
5134
+ <th class="px-5 py-3 text-left text-xs font-medium text-zinc-500 uppercase">Country</th>
5135
+ <th class="px-5 py-3 text-right text-xs font-medium text-zinc-500 uppercase">Attempts</th>
5136
+ <th class="px-5 py-3 text-right text-xs font-medium text-zinc-500 uppercase">Status</th>
5137
+ </tr>
5138
+ </thead>
5139
+ <tbody>
5140
+ ${topIPs.map((ip) => `
5141
+ <tr class="border-b border-zinc-50 dark:border-zinc-800/50 hover:bg-zinc-50 dark:hover:bg-zinc-800/50">
5142
+ <td class="px-5 py-3 text-sm font-mono text-zinc-900 dark:text-zinc-100">${ip.ipAddress}</td>
5143
+ <td class="px-5 py-3 text-sm text-zinc-600 dark:text-zinc-400">${ip.countryCode || "-"}</td>
5144
+ <td class="px-5 py-3 text-sm text-right font-semibold ${ip.failedAttempts >= 10 ? "text-red-600 dark:text-red-400" : "text-zinc-900 dark:text-zinc-100"}">${ip.failedAttempts}</td>
5145
+ <td class="px-5 py-3 text-sm text-right">
5146
+ ${ip.locked ? '<span class="inline-flex items-center rounded-md bg-red-100 dark:bg-red-900/30 px-2 py-1 text-xs font-medium text-red-700 dark:text-red-400">Locked</span>' : '<span class="inline-flex items-center rounded-md bg-emerald-100 dark:bg-emerald-900/30 px-2 py-1 text-xs font-medium text-emerald-700 dark:text-emerald-400">Active</span>'}
5147
+ </td>
5148
+ </tr>
5149
+ `).join("")}
5150
+ </tbody>
5151
+ </table>`}
5152
+ </div>
5153
+
5154
+ <!-- Recent Critical Events -->
5155
+ <div class="rounded-xl bg-white/80 dark:bg-zinc-900/80 backdrop-blur-xl ring-1 ring-zinc-950/5 dark:ring-white/10 shadow-sm overflow-hidden">
5156
+ <div class="p-5 border-b border-zinc-100 dark:border-zinc-800">
5157
+ <h2 class="text-sm font-semibold text-zinc-950 dark:text-white">Recent Critical Events</h2>
5158
+ </div>
5159
+ ${recentCritical.length === 0 ? '<div class="p-5"><p class="text-zinc-500 text-sm">No critical events</p></div>' : `<div class="divide-y divide-zinc-100 dark:divide-zinc-800">
5160
+ ${recentCritical.slice(0, 10).map((event) => `
5161
+ <div class="px-5 py-3 hover:bg-zinc-50 dark:hover:bg-zinc-800/50">
5162
+ <div class="flex items-center justify-between">
5163
+ <div class="flex items-center gap-2">
5164
+ ${severityBadge(event.severity)}
5165
+ <span class="text-sm font-medium text-zinc-900 dark:text-zinc-100">${eventTypeBadge(event.eventType)}</span>
5166
+ </div>
5167
+ <span class="text-xs text-zinc-400">${formatTimestamp(event.createdAt)}</span>
5168
+ </div>
5169
+ <div class="mt-1 text-xs text-zinc-500 dark:text-zinc-400">
5170
+ ${event.ipAddress ? `IP: ${event.ipAddress}` : ""}
5171
+ ${event.email ? ` | ${event.email}` : ""}
5172
+ </div>
5173
+ </div>
5174
+ `).join("")}
5175
+ </div>`}
5176
+ </div>
5177
+ </div>
5178
+ </div>
5179
+ `;
5180
+ const layoutData = {
5181
+ title: "Security Dashboard",
5182
+ pageTitle: "Security Dashboard",
5183
+ currentPath: "/admin/plugins/security-audit",
5184
+ user,
5185
+ content: content2,
5186
+ version,
5187
+ dynamicMenuItems
5188
+ };
5189
+ return renderAdminLayoutCatalyst(layoutData);
5190
+ }
5191
+
5192
+ // src/plugins/core-plugins/security-audit-plugin/components/event-log-page.ts
5193
+ init_admin_layout_catalyst_template();
5194
+ function formatTimestamp2(ts) {
5195
+ const date = new Date(ts);
5196
+ return date.toLocaleDateString("en-US", {
5197
+ month: "short",
5198
+ day: "numeric",
5199
+ year: "numeric",
5200
+ hour: "2-digit",
5201
+ minute: "2-digit",
5202
+ second: "2-digit"
5203
+ });
5204
+ }
5205
+ function severityBadge2(severity) {
5206
+ const colors = {
5207
+ info: "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400",
5208
+ warning: "bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400",
5209
+ critical: "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400"
5210
+ };
5211
+ return `<span class="inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ${colors[severity] || colors.info}">${severity}</span>`;
5212
+ }
5213
+ function eventTypeBadge2(type) {
5214
+ const colors = {
5215
+ login_success: "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400",
5216
+ login_failure: "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400",
5217
+ registration: "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400",
5218
+ account_lockout: "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400",
5219
+ suspicious_activity: "bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400",
5220
+ logout: "bg-zinc-100 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-400",
5221
+ password_reset_request: "bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400",
5222
+ permission_denied: "bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400"
5223
+ };
5224
+ const labels = {
5225
+ login_success: "Login OK",
5226
+ login_failure: "Login Failed",
5227
+ registration: "Registration",
5228
+ account_lockout: "Lockout",
5229
+ suspicious_activity: "Suspicious",
5230
+ logout: "Logout",
5231
+ password_reset_request: "Password Reset",
5232
+ permission_denied: "Access Denied"
5233
+ };
5234
+ const color = colors[type] || "bg-zinc-100 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-400";
5235
+ return `<span class="inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ${color}">${labels[type] || type}</span>`;
5236
+ }
5237
+ function buildFilterUrl(filters, overrides = {}) {
5238
+ const params = new URLSearchParams();
5239
+ if (filters.eventType && !overrides.type) params.set("type", String(filters.eventType));
5240
+ if (filters.severity && !overrides.severity) params.set("severity", String(filters.severity));
5241
+ if (filters.email && !overrides.email) params.set("email", filters.email);
5242
+ if (filters.ipAddress && !overrides.ip) params.set("ip", filters.ipAddress);
5243
+ if (filters.search && !overrides.search) params.set("search", filters.search);
5244
+ for (const [key, value] of Object.entries(overrides)) {
5245
+ if (value) params.set(key, value);
5246
+ }
5247
+ const qs = params.toString();
5248
+ return `/admin/plugins/security-audit/events${qs ? "?" + qs : ""}`;
5249
+ }
5250
+ function renderEventLogPage(data) {
5251
+ const { events, pagination, filters, user, version, dynamicMenuItems } = data;
5252
+ const content2 = `
5253
+ <div>
5254
+ <div class="sm:flex sm:items-center sm:justify-between mb-6">
5255
+ <div class="sm:flex-auto">
5256
+ <h1 class="text-2xl/8 font-semibold text-zinc-950 dark:text-white sm:text-xl/8">Security Event Log</h1>
5257
+ <p class="mt-2 text-sm/6 text-zinc-500 dark:text-zinc-400">
5258
+ Browse and filter all security events. Showing ${pagination.startItem}-${pagination.endItem} of ${pagination.totalItems}.
5259
+ </p>
5260
+ </div>
5261
+ <div class="mt-4 sm:mt-0 sm:ml-16 flex gap-x-2">
5262
+ <a href="/admin/plugins/security-audit"
5263
+ class="inline-flex items-center justify-center rounded-lg bg-white dark:bg-zinc-800 px-3.5 py-2.5 text-sm font-semibold text-zinc-950 dark:text-white hover:bg-zinc-50 dark:hover:bg-zinc-700 ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 transition-colors shadow-sm">
5264
+ Dashboard
5265
+ </a>
5266
+ </div>
5267
+ </div>
5268
+
5269
+ <!-- Filters -->
5270
+ <div class="rounded-xl bg-white/80 dark:bg-zinc-900/80 backdrop-blur-xl p-5 ring-1 ring-zinc-950/5 dark:ring-white/10 shadow-sm mb-6">
5271
+ <form method="GET" action="/admin/plugins/security-audit/events" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4">
5272
+ <div>
5273
+ <label class="block text-xs font-medium text-zinc-500 dark:text-zinc-400 mb-1">Event Type</label>
5274
+ <select name="type" class="w-full rounded-lg border-0 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-900 dark:text-white ring-1 ring-inset ring-zinc-300 dark:ring-zinc-700 focus:ring-2 focus:ring-cyan-500">
5275
+ <option value="">All Types</option>
5276
+ <option value="login_success" ${filters.eventType === "login_success" ? "selected" : ""}>Login Success</option>
5277
+ <option value="login_failure" ${filters.eventType === "login_failure" ? "selected" : ""}>Login Failure</option>
5278
+ <option value="registration" ${filters.eventType === "registration" ? "selected" : ""}>Registration</option>
5279
+ <option value="account_lockout" ${filters.eventType === "account_lockout" ? "selected" : ""}>Account Lockout</option>
5280
+ <option value="suspicious_activity" ${filters.eventType === "suspicious_activity" ? "selected" : ""}>Suspicious Activity</option>
5281
+ <option value="logout" ${filters.eventType === "logout" ? "selected" : ""}>Logout</option>
5282
+ </select>
5283
+ </div>
5284
+ <div>
5285
+ <label class="block text-xs font-medium text-zinc-500 dark:text-zinc-400 mb-1">Severity</label>
5286
+ <select name="severity" class="w-full rounded-lg border-0 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-900 dark:text-white ring-1 ring-inset ring-zinc-300 dark:ring-zinc-700 focus:ring-2 focus:ring-cyan-500">
5287
+ <option value="">All Severities</option>
5288
+ <option value="info" ${filters.severity === "info" ? "selected" : ""}>Info</option>
5289
+ <option value="warning" ${filters.severity === "warning" ? "selected" : ""}>Warning</option>
5290
+ <option value="critical" ${filters.severity === "critical" ? "selected" : ""}>Critical</option>
5291
+ </select>
5292
+ </div>
5293
+ <div>
5294
+ <label class="block text-xs font-medium text-zinc-500 dark:text-zinc-400 mb-1">Email</label>
5295
+ <input type="text" name="email" value="${filters.email || ""}" placeholder="Filter by email"
5296
+ class="w-full rounded-lg border-0 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-900 dark:text-white ring-1 ring-inset ring-zinc-300 dark:ring-zinc-700 focus:ring-2 focus:ring-cyan-500 placeholder:text-zinc-400">
5297
+ </div>
5298
+ <div>
5299
+ <label class="block text-xs font-medium text-zinc-500 dark:text-zinc-400 mb-1">IP Address</label>
5300
+ <input type="text" name="ip" value="${filters.ipAddress || ""}" placeholder="Filter by IP"
5301
+ class="w-full rounded-lg border-0 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-900 dark:text-white ring-1 ring-inset ring-zinc-300 dark:ring-zinc-700 focus:ring-2 focus:ring-cyan-500 placeholder:text-zinc-400">
5302
+ </div>
5303
+ <div class="flex items-end gap-2">
5304
+ <button type="submit"
5305
+ class="flex-1 rounded-lg bg-cyan-600 px-3 py-2 text-sm font-semibold text-white hover:bg-cyan-500 transition-colors shadow-sm">
5306
+ Filter
5307
+ </button>
5308
+ <a href="/admin/plugins/security-audit/events"
5309
+ class="rounded-lg bg-zinc-100 dark:bg-zinc-800 px-3 py-2 text-sm font-medium text-zinc-600 dark:text-zinc-400 hover:bg-zinc-200 dark:hover:bg-zinc-700 transition-colors">
5310
+ Clear
5311
+ </a>
5312
+ </div>
5313
+ </form>
5314
+ </div>
5315
+
5316
+ <!-- Events Table -->
5317
+ <div class="rounded-xl bg-white/80 dark:bg-zinc-900/80 backdrop-blur-xl ring-1 ring-zinc-950/5 dark:ring-white/10 shadow-sm overflow-hidden">
5318
+ ${events.length === 0 ? `<div class="p-12 text-center">
5319
+ <svg class="mx-auto h-12 w-12 text-zinc-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
5320
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
5321
+ </svg>
5322
+ <h3 class="mt-2 text-sm font-semibold text-zinc-900 dark:text-white">No events found</h3>
5323
+ <p class="mt-1 text-sm text-zinc-500">No security events match your current filters.</p>
5324
+ </div>` : `<div class="overflow-x-auto">
5325
+ <table class="min-w-full divide-y divide-zinc-200 dark:divide-zinc-800">
5326
+ <thead>
5327
+ <tr>
5328
+ <th class="px-4 py-3 text-left text-xs font-medium text-zinc-500 uppercase tracking-wider">Time</th>
5329
+ <th class="px-4 py-3 text-left text-xs font-medium text-zinc-500 uppercase tracking-wider">Type</th>
5330
+ <th class="px-4 py-3 text-left text-xs font-medium text-zinc-500 uppercase tracking-wider">Severity</th>
5331
+ <th class="px-4 py-3 text-left text-xs font-medium text-zinc-500 uppercase tracking-wider">Email</th>
5332
+ <th class="px-4 py-3 text-left text-xs font-medium text-zinc-500 uppercase tracking-wider">IP Address</th>
5333
+ <th class="px-4 py-3 text-left text-xs font-medium text-zinc-500 uppercase tracking-wider">Country</th>
5334
+ <th class="px-4 py-3 text-left text-xs font-medium text-zinc-500 uppercase tracking-wider">Status</th>
5335
+ </tr>
5336
+ </thead>
5337
+ <tbody class="divide-y divide-zinc-100 dark:divide-zinc-800">
5338
+ ${events.map((event) => `
5339
+ <tr class="hover:bg-zinc-50 dark:hover:bg-zinc-800/50 cursor-pointer" onclick="this.querySelector('.event-details').classList.toggle('hidden')">
5340
+ <td class="px-4 py-3 text-sm text-zinc-600 dark:text-zinc-300 whitespace-nowrap">${formatTimestamp2(event.createdAt)}</td>
5341
+ <td class="px-4 py-3">${eventTypeBadge2(event.eventType)}</td>
5342
+ <td class="px-4 py-3">${severityBadge2(event.severity)}</td>
5343
+ <td class="px-4 py-3 text-sm text-zinc-600 dark:text-zinc-300 max-w-[200px] truncate">${event.email || "-"}</td>
5344
+ <td class="px-4 py-3 text-sm font-mono text-zinc-600 dark:text-zinc-300">${event.ipAddress || "-"}</td>
5345
+ <td class="px-4 py-3 text-sm text-zinc-600 dark:text-zinc-300">${event.countryCode || "-"}</td>
5346
+ <td class="px-4 py-3">
5347
+ ${event.blocked ? '<span class="inline-flex items-center rounded-md bg-red-100 dark:bg-red-900/30 px-2 py-1 text-xs font-medium text-red-700 dark:text-red-400">Blocked</span>' : '<span class="inline-flex items-center rounded-md bg-emerald-100 dark:bg-emerald-900/30 px-2 py-1 text-xs font-medium text-emerald-700 dark:text-emerald-400">Allowed</span>'}
5348
+ </td>
5349
+ </tr>
5350
+ <tr class="event-details hidden">
5351
+ <td colspan="7" class="px-4 py-3 bg-zinc-50 dark:bg-zinc-800/30">
5352
+ <div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-xs">
5353
+ <div><span class="font-medium text-zinc-500">Event ID:</span> <span class="text-zinc-700 dark:text-zinc-300 font-mono">${event.id.substring(0, 8)}...</span></div>
5354
+ <div><span class="font-medium text-zinc-500">User Agent:</span> <span class="text-zinc-700 dark:text-zinc-300 truncate block max-w-[300px]">${event.userAgent || "-"}</span></div>
5355
+ <div><span class="font-medium text-zinc-500">Path:</span> <span class="text-zinc-700 dark:text-zinc-300">${event.requestPath || "-"}</span></div>
5356
+ <div><span class="font-medium text-zinc-500">Fingerprint:</span> <span class="text-zinc-700 dark:text-zinc-300 font-mono">${event.fingerprint || "-"}</span></div>
5357
+ ${event.details ? `<div class="col-span-full"><span class="font-medium text-zinc-500">Details:</span> <pre class="text-zinc-700 dark:text-zinc-300 mt-1 bg-zinc-100 dark:bg-zinc-900 rounded p-2 overflow-x-auto">${JSON.stringify(event.details, null, 2)}</pre></div>` : ""}
5358
+ </div>
5359
+ </td>
5360
+ </tr>
5361
+ `).join("")}
5362
+ </tbody>
5363
+ </table>
5364
+ </div>`}
5365
+
5366
+ <!-- Pagination -->
5367
+ ${pagination.totalPages > 1 ? `
5368
+ <div class="flex items-center justify-between border-t border-zinc-200 dark:border-zinc-800 px-4 py-3">
5369
+ <div class="text-sm text-zinc-500">
5370
+ Page ${pagination.currentPage} of ${pagination.totalPages}
5371
+ </div>
5372
+ <div class="flex gap-1">
5373
+ ${pagination.currentPage > 1 ? `
5374
+ <a href="${buildFilterUrl(filters, { page: String(pagination.currentPage - 1) })}"
5375
+ class="rounded-lg px-3 py-1.5 text-sm font-medium text-zinc-600 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors">
5376
+ Previous
5377
+ </a>
5378
+ ` : ""}
5379
+ ${pagination.currentPage < pagination.totalPages ? `
5380
+ <a href="${buildFilterUrl(filters, { page: String(pagination.currentPage + 1) })}"
5381
+ class="rounded-lg px-3 py-1.5 text-sm font-medium text-zinc-600 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors">
5382
+ Next
5383
+ </a>
5384
+ ` : ""}
5385
+ </div>
5386
+ </div>
5387
+ ` : ""}
5388
+ </div>
5389
+ </div>
5390
+ `;
5391
+ const layoutData = {
5392
+ title: "Security Event Log",
5393
+ pageTitle: "Security Event Log",
5394
+ currentPath: "/admin/plugins/security-audit/events",
5395
+ user,
5396
+ content: content2,
5397
+ version,
5398
+ dynamicMenuItems
5399
+ };
5400
+ return renderAdminLayoutCatalyst(layoutData);
5401
+ }
5402
+
5403
+ // src/plugins/core-plugins/security-audit-plugin/components/settings-page.ts
5404
+ init_admin_layout_catalyst_template();
5405
+ function renderSecuritySettingsPage(data) {
5406
+ const { settings, user, version, message, dynamicMenuItems } = data;
5407
+ const content2 = `
5408
+ <div>
5409
+ <div class="sm:flex sm:items-center sm:justify-between mb-6">
5410
+ <div class="sm:flex-auto">
5411
+ <h1 class="text-2xl/8 font-semibold text-zinc-950 dark:text-white sm:text-xl/8">Security Audit Settings</h1>
5412
+ <p class="mt-2 text-sm/6 text-zinc-500 dark:text-zinc-400">
5413
+ Configure brute-force detection thresholds, event logging, and data retention.
5414
+ </p>
5415
+ </div>
5416
+ <div class="mt-4 sm:mt-0 sm:ml-16 flex gap-x-2">
5417
+ <a href="/admin/plugins/security-audit"
5418
+ class="inline-flex items-center justify-center rounded-lg bg-white dark:bg-zinc-800 px-3.5 py-2.5 text-sm font-semibold text-zinc-950 dark:text-white hover:bg-zinc-50 dark:hover:bg-zinc-700 ring-1 ring-inset ring-zinc-950/10 dark:ring-white/10 transition-colors shadow-sm">
5419
+ Dashboard
5420
+ </a>
5421
+ </div>
5422
+ </div>
5423
+
5424
+ ${message ? `
5425
+ <div class="mb-6 rounded-lg bg-emerald-50 dark:bg-emerald-900/20 p-4 ring-1 ring-emerald-200 dark:ring-emerald-800">
5426
+ <p class="text-sm text-emerald-800 dark:text-emerald-300">${message}</p>
5427
+ </div>
5428
+ ` : ""}
5429
+
5430
+ <form method="POST" action="/admin/plugins/security-audit/settings"
5431
+ class="space-y-6"
5432
+ hx-post="/admin/plugins/security-audit/settings"
5433
+ hx-swap="none"
5434
+ hx-on::after-request="if(event.detail.successful) { window.showNotification && window.showNotification('Settings saved', 'success'); } else { window.showNotification && window.showNotification('Failed to save', 'error'); }">
5435
+
5436
+ <!-- Brute Force Detection -->
5437
+ <div class="rounded-xl bg-white/80 dark:bg-zinc-900/80 backdrop-blur-xl p-6 ring-1 ring-zinc-950/5 dark:ring-white/10 shadow-sm">
5438
+ <h2 class="text-base font-semibold text-zinc-950 dark:text-white mb-4">Brute-Force Detection</h2>
5439
+ <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
5440
+ <div>
5441
+ <label class="flex items-center gap-2 mb-4">
5442
+ <input type="checkbox" name="bruteForce.enabled" value="true" ${settings.bruteForce.enabled ? "checked" : ""}
5443
+ class="rounded border-zinc-300 text-cyan-600 focus:ring-cyan-500">
5444
+ <span class="text-sm font-medium text-zinc-700 dark:text-zinc-300">Enable brute-force detection</span>
5445
+ </label>
5446
+ </div>
5447
+ <div></div><div></div>
5448
+ <div>
5449
+ <label class="block text-xs font-medium text-zinc-500 dark:text-zinc-400 mb-1">Max Failed Attempts per IP</label>
5450
+ <input type="number" name="bruteForce.maxFailedAttemptsPerIP" value="${settings.bruteForce.maxFailedAttemptsPerIP}" min="1" max="100"
5451
+ class="w-full rounded-lg border-0 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-900 dark:text-white ring-1 ring-inset ring-zinc-300 dark:ring-zinc-700 focus:ring-2 focus:ring-cyan-500">
5452
+ </div>
5453
+ <div>
5454
+ <label class="block text-xs font-medium text-zinc-500 dark:text-zinc-400 mb-1">Max Failed Attempts per Email</label>
5455
+ <input type="number" name="bruteForce.maxFailedAttemptsPerEmail" value="${settings.bruteForce.maxFailedAttemptsPerEmail}" min="1" max="100"
5456
+ class="w-full rounded-lg border-0 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-900 dark:text-white ring-1 ring-inset ring-zinc-300 dark:ring-zinc-700 focus:ring-2 focus:ring-cyan-500">
5457
+ </div>
5458
+ <div>
5459
+ <label class="block text-xs font-medium text-zinc-500 dark:text-zinc-400 mb-1">Window (minutes)</label>
5460
+ <input type="number" name="bruteForce.windowMinutes" value="${settings.bruteForce.windowMinutes}" min="1" max="1440"
5461
+ class="w-full rounded-lg border-0 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-900 dark:text-white ring-1 ring-inset ring-zinc-300 dark:ring-zinc-700 focus:ring-2 focus:ring-cyan-500">
5462
+ </div>
5463
+ <div>
5464
+ <label class="block text-xs font-medium text-zinc-500 dark:text-zinc-400 mb-1">Lockout Duration (minutes)</label>
5465
+ <input type="number" name="bruteForce.lockoutDurationMinutes" value="${settings.bruteForce.lockoutDurationMinutes}" min="1" max="1440"
5466
+ class="w-full rounded-lg border-0 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-900 dark:text-white ring-1 ring-inset ring-zinc-300 dark:ring-zinc-700 focus:ring-2 focus:ring-cyan-500">
5467
+ </div>
5468
+ <div>
5469
+ <label class="block text-xs font-medium text-zinc-500 dark:text-zinc-400 mb-1">Alert Threshold</label>
5470
+ <input type="number" name="bruteForce.alertThreshold" value="${settings.bruteForce.alertThreshold}" min="1" max="1000"
5471
+ class="w-full rounded-lg border-0 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-900 dark:text-white ring-1 ring-inset ring-zinc-300 dark:ring-zinc-700 focus:ring-2 focus:ring-cyan-500">
5472
+ <p class="mt-1 text-xs text-zinc-400">Events above this count trigger critical severity</p>
5473
+ </div>
5474
+ </div>
5475
+ </div>
5476
+
5477
+ <!-- Event Logging -->
5478
+ <div class="rounded-xl bg-white/80 dark:bg-zinc-900/80 backdrop-blur-xl p-6 ring-1 ring-zinc-950/5 dark:ring-white/10 shadow-sm">
5479
+ <h2 class="text-base font-semibold text-zinc-950 dark:text-white mb-4">Event Logging</h2>
5480
+ <div class="space-y-3">
5481
+ <label class="flex items-center gap-2">
5482
+ <input type="checkbox" name="logging.logSuccessfulLogins" value="true" ${settings.logging.logSuccessfulLogins ? "checked" : ""}
5483
+ class="rounded border-zinc-300 text-cyan-600 focus:ring-cyan-500">
5484
+ <span class="text-sm text-zinc-700 dark:text-zinc-300">Log successful logins</span>
5485
+ </label>
5486
+ <label class="flex items-center gap-2">
5487
+ <input type="checkbox" name="logging.logLogouts" value="true" ${settings.logging.logLogouts ? "checked" : ""}
5488
+ class="rounded border-zinc-300 text-cyan-600 focus:ring-cyan-500">
5489
+ <span class="text-sm text-zinc-700 dark:text-zinc-300">Log logouts</span>
5490
+ </label>
5491
+ <label class="flex items-center gap-2">
5492
+ <input type="checkbox" name="logging.logRegistrations" value="true" ${settings.logging.logRegistrations ? "checked" : ""}
5493
+ class="rounded border-zinc-300 text-cyan-600 focus:ring-cyan-500">
5494
+ <span class="text-sm text-zinc-700 dark:text-zinc-300">Log registrations</span>
5495
+ </label>
5496
+ <label class="flex items-center gap-2">
5497
+ <input type="checkbox" name="logging.logPasswordResets" value="true" ${settings.logging.logPasswordResets ? "checked" : ""}
5498
+ class="rounded border-zinc-300 text-cyan-600 focus:ring-cyan-500">
5499
+ <span class="text-sm text-zinc-700 dark:text-zinc-300">Log password resets</span>
5500
+ </label>
5501
+ <label class="flex items-center gap-2">
5502
+ <input type="checkbox" name="logging.logPermissionDenied" value="true" ${settings.logging.logPermissionDenied ? "checked" : ""}
5503
+ class="rounded border-zinc-300 text-cyan-600 focus:ring-cyan-500">
5504
+ <span class="text-sm text-zinc-700 dark:text-zinc-300">Log permission denied events</span>
5505
+ </label>
5506
+ </div>
5507
+ </div>
5508
+
5509
+ <!-- Data Retention -->
5510
+ <div class="rounded-xl bg-white/80 dark:bg-zinc-900/80 backdrop-blur-xl p-6 ring-1 ring-zinc-950/5 dark:ring-white/10 shadow-sm">
5511
+ <h2 class="text-base font-semibold text-zinc-950 dark:text-white mb-4">Data Retention</h2>
5512
+ <div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
5513
+ <div>
5514
+ <label class="block text-xs font-medium text-zinc-500 dark:text-zinc-400 mb-1">Days to Keep</label>
5515
+ <input type="number" name="retention.daysToKeep" value="${settings.retention.daysToKeep}" min="1" max="365"
5516
+ class="w-full rounded-lg border-0 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-900 dark:text-white ring-1 ring-inset ring-zinc-300 dark:ring-zinc-700 focus:ring-2 focus:ring-cyan-500">
5517
+ </div>
5518
+ <div>
5519
+ <label class="block text-xs font-medium text-zinc-500 dark:text-zinc-400 mb-1">Max Events</label>
5520
+ <input type="number" name="retention.maxEvents" value="${settings.retention.maxEvents}" min="1000" max="1000000"
5521
+ class="w-full rounded-lg border-0 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-900 dark:text-white ring-1 ring-inset ring-zinc-300 dark:ring-zinc-700 focus:ring-2 focus:ring-cyan-500">
5522
+ </div>
5523
+ <div>
5524
+ <label class="flex items-center gap-2 mt-5">
5525
+ <input type="checkbox" name="retention.autoPurge" value="true" ${settings.retention.autoPurge ? "checked" : ""}
5526
+ class="rounded border-zinc-300 text-cyan-600 focus:ring-cyan-500">
5527
+ <span class="text-sm text-zinc-700 dark:text-zinc-300">Auto-purge old events</span>
5528
+ </label>
5529
+ </div>
5530
+ </div>
5531
+ </div>
5532
+
5533
+ <!-- Actions -->
5534
+ <div class="flex items-center justify-between">
5535
+ <button type="button"
5536
+ onclick="if(confirm('Purge events older than retention period?')) fetch('/api/security-audit/events/purge', {method:'POST',headers:{'Content-Type':'application/json'}}).then(r=>r.json()).then(d=>window.showNotification && window.showNotification('Purged '+d.deleted+' events','success'))"
5537
+ class="rounded-lg bg-red-50 dark:bg-red-900/20 px-4 py-2.5 text-sm font-medium text-red-700 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900/40 ring-1 ring-red-200 dark:ring-red-800 transition-colors">
5538
+ Purge Old Events
5539
+ </button>
5540
+ <button type="submit"
5541
+ class="rounded-lg bg-cyan-600 px-6 py-2.5 text-sm font-semibold text-white hover:bg-cyan-500 transition-colors shadow-sm">
5542
+ Save Settings
5543
+ </button>
5544
+ </div>
5545
+ </form>
5546
+ </div>
5547
+ `;
5548
+ const layoutData = {
5549
+ title: "Security Audit Settings",
5550
+ pageTitle: "Security Audit Settings",
5551
+ currentPath: "/admin/plugins/security-audit/settings",
5552
+ user,
5553
+ content: content2,
5554
+ version,
5555
+ dynamicMenuItems
5556
+ };
5557
+ return renderAdminLayoutCatalyst(layoutData);
5558
+ }
5559
+
5560
+ // src/plugins/core-plugins/security-audit-plugin/routes/admin.ts
5561
+ var adminRoutes2 = new Hono();
5562
+ adminRoutes2.use("*", requireAuth());
5563
+ adminRoutes2.use("*", async (c, next) => {
5564
+ const user = c.get("user");
5565
+ if (user?.role !== "admin") {
5566
+ return c.text("Access denied", 403);
5567
+ }
5568
+ return next();
5569
+ });
5570
+ async function getSettings(db) {
5571
+ try {
5572
+ const pluginService = new PluginService(db);
5573
+ const plugin2 = await pluginService.getPlugin("security-audit");
5574
+ if (plugin2?.settings) {
5575
+ const settings = typeof plugin2.settings === "string" ? JSON.parse(plugin2.settings) : plugin2.settings;
5576
+ return { ...DEFAULT_SETTINGS2, ...settings };
5577
+ }
5578
+ } catch {
5579
+ }
5580
+ return DEFAULT_SETTINGS2;
5581
+ }
5582
+ adminRoutes2.get("/", async (c) => {
5583
+ const db = c.env.DB;
5584
+ const user = c.get("user");
5585
+ const settings = await getSettings(db);
5586
+ const service = new SecurityAuditService(db, settings);
5587
+ const [stats, topIPs, hourlyTrend, recentCritical] = await Promise.all([
5588
+ service.getStats(),
5589
+ service.getTopIPs(10),
5590
+ service.getHourlyTrend(24),
5591
+ service.getRecentCriticalEvents(20)
5592
+ ]);
5593
+ const pageData = {
5594
+ stats,
5595
+ topIPs,
5596
+ hourlyTrend,
5597
+ recentCritical,
5598
+ user: user ? { name: user.email, email: user.email, role: user.role } : void 0,
5599
+ version: c.get("appVersion"),
5600
+ dynamicMenuItems: c.get("pluginMenuItems")
5601
+ };
5602
+ return c.html(renderSecurityDashboard(pageData));
5603
+ });
5604
+ adminRoutes2.get("/events", async (c) => {
5605
+ const db = c.env.DB;
5606
+ const user = c.get("user");
5607
+ const settings = await getSettings(db);
5608
+ const service = new SecurityAuditService(db, settings);
5609
+ const page = parseInt(c.req.query("page") || "1");
5610
+ const limit = 50;
5611
+ const filters = {
5612
+ eventType: c.req.query("type") || void 0,
5613
+ severity: c.req.query("severity") || void 0,
5614
+ email: c.req.query("email") || void 0,
5615
+ ipAddress: c.req.query("ip") || void 0,
5616
+ search: c.req.query("search") || void 0,
5617
+ page,
5618
+ limit
5619
+ };
5620
+ const { events, total } = await service.getEvents(filters);
5621
+ const totalPages = Math.ceil(total / limit);
5622
+ const pageData = {
5623
+ events,
5624
+ pagination: {
5625
+ currentPage: page,
5626
+ totalPages,
5627
+ totalItems: total,
5628
+ itemsPerPage: limit,
5629
+ startItem: total === 0 ? 0 : (page - 1) * limit + 1,
5630
+ endItem: Math.min(page * limit, total)
5631
+ },
5632
+ filters,
5633
+ user: user ? { name: user.email, email: user.email, role: user.role } : void 0,
5634
+ version: c.get("appVersion"),
5635
+ dynamicMenuItems: c.get("pluginMenuItems")
5636
+ };
5637
+ return c.html(renderEventLogPage(pageData));
5638
+ });
5639
+ adminRoutes2.get("/settings", async (c) => {
5640
+ const db = c.env.DB;
5641
+ const user = c.get("user");
5642
+ const settings = await getSettings(db);
5643
+ const pageData = {
5644
+ settings,
5645
+ user: user ? { name: user.email, email: user.email, role: user.role } : void 0,
5646
+ version: c.get("appVersion"),
5647
+ message: c.req.query("message") || void 0,
5648
+ dynamicMenuItems: c.get("pluginMenuItems")
5649
+ };
5650
+ return c.html(renderSecuritySettingsPage(pageData));
5651
+ });
5652
+ adminRoutes2.post("/settings", async (c) => {
5653
+ const db = c.env.DB;
5654
+ const body = await c.req.parseBody();
5655
+ const settings = {
5656
+ bruteForce: {
5657
+ enabled: body["bruteForce.enabled"] === "true",
5658
+ maxFailedAttemptsPerIP: parseInt(body["bruteForce.maxFailedAttemptsPerIP"]) || 10,
5659
+ maxFailedAttemptsPerEmail: parseInt(body["bruteForce.maxFailedAttemptsPerEmail"]) || 5,
5660
+ windowMinutes: parseInt(body["bruteForce.windowMinutes"]) || 15,
5661
+ lockoutDurationMinutes: parseInt(body["bruteForce.lockoutDurationMinutes"]) || 30,
5662
+ alertThreshold: parseInt(body["bruteForce.alertThreshold"]) || 20
5663
+ },
5664
+ logging: {
5665
+ logSuccessfulLogins: body["logging.logSuccessfulLogins"] === "true",
5666
+ logLogouts: body["logging.logLogouts"] === "true",
5667
+ logRegistrations: body["logging.logRegistrations"] === "true",
5668
+ logPasswordResets: body["logging.logPasswordResets"] === "true",
5669
+ logPermissionDenied: body["logging.logPermissionDenied"] === "true"
5670
+ },
5671
+ retention: {
5672
+ daysToKeep: parseInt(body["retention.daysToKeep"]) || 90,
5673
+ maxEvents: parseInt(body["retention.maxEvents"]) || 1e5,
5674
+ autoPurge: body["retention.autoPurge"] === "true"
5675
+ }
5676
+ };
5677
+ const pluginService = new PluginService(db);
5678
+ await pluginService.updatePluginSettings("security-audit", settings);
5679
+ if (c.req.header("HX-Request")) {
5680
+ return c.json({ success: true });
5681
+ }
5682
+ return c.redirect("/admin/plugins/security-audit/settings?message=Settings saved successfully");
5683
+ });
5684
+
5685
+ // src/plugins/core-plugins/security-audit-plugin/services/brute-force-detector.ts
5686
+ var KV_PREFIX = "security:bf:";
5687
+ var LOCK_PREFIX = "security:locked:";
5688
+ var BruteForceDetector = class {
5689
+ constructor(kv, settings) {
5690
+ this.kv = kv;
5691
+ this.settings = settings || DEFAULT_SETTINGS2.bruteForce;
5692
+ }
5693
+ settings;
5694
+ async recordFailedAttempt(ip, email) {
5695
+ if (!this.settings.enabled) {
5696
+ return { ipCount: 0, emailCount: 0, shouldLockIP: false, shouldLockEmail: false, isSuspicious: false };
5697
+ }
5698
+ const windowMs = this.settings.windowMinutes * 60 * 1e3;
5699
+ const ipKey = `${KV_PREFIX}ip:${ip}`;
5700
+ const ipCount = await this.incrementCounter(ipKey, windowMs);
5701
+ const emailKey = `${KV_PREFIX}email:${email}`;
5702
+ const emailCount = await this.incrementCounter(emailKey, windowMs);
5703
+ const ipEmailsKey = `${KV_PREFIX}ip-emails:${ip}`;
5704
+ await this.addToSet(ipEmailsKey, email, windowMs);
5705
+ const emailsFromIP = await this.getSetSize(ipEmailsKey);
5706
+ const isSuspicious = emailsFromIP >= 5;
5707
+ const shouldLockIP = ipCount >= this.settings.maxFailedAttemptsPerIP;
5708
+ const shouldLockEmail = emailCount >= this.settings.maxFailedAttemptsPerEmail;
5709
+ return { ipCount, emailCount, shouldLockIP, shouldLockEmail, isSuspicious };
5710
+ }
5711
+ async isLocked(ip, email) {
5712
+ if (!this.settings.enabled) {
5713
+ return { locked: false };
5714
+ }
5715
+ const ipLocked = await this.kv.get(`${LOCK_PREFIX}ip:${ip}`);
5716
+ if (ipLocked) {
5717
+ return { locked: true, reason: "IP address temporarily locked due to excessive failed login attempts" };
5718
+ }
5719
+ const emailLocked = await this.kv.get(`${LOCK_PREFIX}email:${email}`);
5720
+ if (emailLocked) {
5721
+ return { locked: true, reason: "Account temporarily locked due to excessive failed login attempts" };
5722
+ }
5723
+ return { locked: false };
5724
+ }
5725
+ async lockIP(ip) {
5726
+ const ttl = this.settings.lockoutDurationMinutes * 60;
5727
+ await this.kv.put(`${LOCK_PREFIX}ip:${ip}`, JSON.stringify({
5728
+ lockedAt: Date.now(),
5729
+ reason: "brute_force_ip"
5730
+ }), { expirationTtl: ttl });
5731
+ }
5732
+ async lockEmail(email) {
5733
+ const ttl = this.settings.lockoutDurationMinutes * 60;
5734
+ await this.kv.put(`${LOCK_PREFIX}email:${email}`, JSON.stringify({
5735
+ lockedAt: Date.now(),
5736
+ reason: "brute_force_email"
5737
+ }), { expirationTtl: ttl });
5738
+ }
5739
+ async unlockIP(ip) {
5740
+ await this.kv.delete(`${LOCK_PREFIX}ip:${ip}`);
5741
+ }
5742
+ async unlockEmail(email) {
5743
+ await this.kv.delete(`${LOCK_PREFIX}email:${email}`);
5744
+ }
5745
+ async getActiveLockouts() {
5746
+ const ipLocks = await this.kv.list({ prefix: `${LOCK_PREFIX}ip:` });
5747
+ const emailLocks = await this.kv.list({ prefix: `${LOCK_PREFIX}email:` });
5748
+ const lockouts = [];
5749
+ for (const key of ipLocks.keys) {
5750
+ const data = await this.kv.get(key.name);
5751
+ if (data) {
5752
+ const parsed = JSON.parse(data);
5753
+ lockouts.push({
5754
+ key: key.name,
5755
+ type: "ip",
5756
+ value: key.name.replace(`${LOCK_PREFIX}ip:`, ""),
5757
+ lockedAt: parsed.lockedAt
5758
+ });
5759
+ }
5760
+ }
5761
+ for (const key of emailLocks.keys) {
5762
+ const data = await this.kv.get(key.name);
5763
+ if (data) {
5764
+ const parsed = JSON.parse(data);
5765
+ lockouts.push({
5766
+ key: key.name,
5767
+ type: "email",
5768
+ value: key.name.replace(`${LOCK_PREFIX}email:`, ""),
5769
+ lockedAt: parsed.lockedAt
5770
+ });
5771
+ }
5772
+ }
5773
+ return lockouts;
5774
+ }
5775
+ async releaseLockout(key) {
5776
+ await this.kv.delete(key);
5777
+ }
5778
+ isAboveAlertThreshold(count) {
5779
+ return count >= this.settings.alertThreshold;
5780
+ }
5781
+ async incrementCounter(key, windowMs) {
5782
+ const existing = await this.kv.get(key);
5783
+ const now = Date.now();
5784
+ let entries = [];
5785
+ if (existing) {
5786
+ try {
5787
+ entries = JSON.parse(existing);
5788
+ } catch {
5789
+ entries = [];
5790
+ }
5791
+ }
5792
+ const cutoff = now - windowMs;
5793
+ entries = entries.filter((ts) => ts > cutoff);
5794
+ entries.push(now);
5795
+ const ttlSeconds = Math.ceil(windowMs / 1e3);
5796
+ await this.kv.put(key, JSON.stringify(entries), { expirationTtl: ttlSeconds });
5797
+ return entries.length;
5798
+ }
5799
+ async addToSet(key, value, windowMs) {
5800
+ const existing = await this.kv.get(key);
5801
+ let set = {};
5802
+ const now = Date.now();
5803
+ const cutoff = now - windowMs;
5804
+ if (existing) {
5805
+ try {
5806
+ set = JSON.parse(existing);
5807
+ } catch {
5808
+ set = {};
5809
+ }
5810
+ }
5811
+ for (const [k, ts] of Object.entries(set)) {
5812
+ if (ts < cutoff) delete set[k];
5813
+ }
5814
+ set[value] = now;
5815
+ const ttlSeconds = Math.ceil(windowMs / 1e3);
5816
+ await this.kv.put(key, JSON.stringify(set), { expirationTtl: ttlSeconds });
5817
+ }
5818
+ async getSetSize(key) {
5819
+ const existing = await this.kv.get(key);
5820
+ if (!existing) return 0;
5821
+ try {
5822
+ const set = JSON.parse(existing);
5823
+ return Object.keys(set).length;
5824
+ } catch {
5825
+ return 0;
5826
+ }
5827
+ }
5828
+ };
5829
+
5830
+ // src/plugins/core-plugins/security-audit-plugin/routes/api.ts
5831
+ var apiRoutes2 = new Hono();
5832
+ apiRoutes2.use("*", requireAuth());
5833
+ apiRoutes2.use("*", async (c, next) => {
5834
+ const user = c.get("user");
5835
+ if (user?.role !== "admin") {
5836
+ return c.json({ error: "Access denied" }, 403);
5837
+ }
5838
+ return next();
5839
+ });
5840
+ async function getSettings2(db) {
5841
+ try {
5842
+ const pluginService = new PluginService(db);
5843
+ const plugin2 = await pluginService.getPlugin("security-audit");
5844
+ if (plugin2?.settings) {
5845
+ const settings = typeof plugin2.settings === "string" ? JSON.parse(plugin2.settings) : plugin2.settings;
5846
+ return { ...DEFAULT_SETTINGS2, ...settings };
5847
+ }
5848
+ } catch {
5849
+ }
5850
+ return DEFAULT_SETTINGS2;
5851
+ }
5852
+ apiRoutes2.get("/events", async (c) => {
5853
+ const db = c.env.DB;
5854
+ const settings = await getSettings2(db);
5855
+ const service = new SecurityAuditService(db, settings);
5856
+ const filters = {
5857
+ eventType: c.req.query("type"),
5858
+ severity: c.req.query("severity"),
5859
+ email: c.req.query("email") || void 0,
5860
+ ipAddress: c.req.query("ip") || void 0,
5861
+ search: c.req.query("search") || void 0,
5862
+ startDate: c.req.query("start") ? parseInt(c.req.query("start")) : void 0,
5863
+ endDate: c.req.query("end") ? parseInt(c.req.query("end")) : void 0,
5864
+ page: c.req.query("page") ? parseInt(c.req.query("page")) : 1,
5865
+ limit: c.req.query("limit") ? Math.min(parseInt(c.req.query("limit")), 100) : 50,
5866
+ sortBy: c.req.query("sortBy") || "created_at",
5867
+ sortOrder: c.req.query("sortOrder") || "desc"
5868
+ };
5869
+ const result = await service.getEvents(filters);
5870
+ return c.json(result);
5871
+ });
5872
+ apiRoutes2.get("/events/:id", async (c) => {
5873
+ const db = c.env.DB;
5874
+ const settings = await getSettings2(db);
5875
+ const service = new SecurityAuditService(db, settings);
5876
+ const event = await service.getEvent(c.req.param("id"));
5877
+ if (!event) {
5878
+ return c.json({ error: "Event not found" }, 404);
5879
+ }
5880
+ return c.json(event);
5881
+ });
5882
+ apiRoutes2.get("/stats", async (c) => {
5883
+ const db = c.env.DB;
5884
+ const settings = await getSettings2(db);
5885
+ const service = new SecurityAuditService(db, settings);
5886
+ const stats = await service.getStats();
5887
+ return c.json(stats);
5888
+ });
5889
+ apiRoutes2.get("/stats/ips", async (c) => {
5890
+ const db = c.env.DB;
5891
+ const settings = await getSettings2(db);
5892
+ const service = new SecurityAuditService(db, settings);
5893
+ const limit = c.req.query("limit") ? parseInt(c.req.query("limit")) : 10;
5894
+ const ips = await service.getTopIPs(limit);
5895
+ return c.json(ips);
5896
+ });
5897
+ apiRoutes2.get("/stats/trend", async (c) => {
5898
+ const db = c.env.DB;
5899
+ const settings = await getSettings2(db);
5900
+ const service = new SecurityAuditService(db, settings);
5901
+ const hours = c.req.query("hours") ? parseInt(c.req.query("hours")) : 24;
5902
+ const trend = await service.getHourlyTrend(hours);
5903
+ return c.json(trend);
5904
+ });
5905
+ apiRoutes2.get("/lockouts", async (c) => {
5906
+ const kv = c.env.CACHE_KV;
5907
+ const db = c.env.DB;
5908
+ const settings = await getSettings2(db);
5909
+ const detector = new BruteForceDetector(kv, settings.bruteForce);
5910
+ const lockouts = await detector.getActiveLockouts();
5911
+ return c.json(lockouts);
5912
+ });
5913
+ apiRoutes2.delete("/lockouts/:key", async (c) => {
5914
+ const kv = c.env.CACHE_KV;
5915
+ const key = decodeURIComponent(c.req.param("key"));
5916
+ const db = c.env.DB;
5917
+ const settings = await getSettings2(db);
5918
+ const detector = new BruteForceDetector(kv, settings.bruteForce);
5919
+ await detector.releaseLockout(key);
5920
+ return c.json({ success: true });
5921
+ });
5922
+ apiRoutes2.post("/events/purge", async (c) => {
5923
+ const db = c.env.DB;
5924
+ const settings = await getSettings2(db);
5925
+ const service = new SecurityAuditService(db, settings);
5926
+ const body = await c.req.json().catch(() => ({}));
5927
+ const deleted = await service.purgeOldEvents(body.daysToKeep);
5928
+ return c.json({ success: true, deleted });
5929
+ });
5930
+ apiRoutes2.get("/export", async (c) => {
5931
+ const db = c.env.DB;
5932
+ const settings = await getSettings2(db);
5933
+ const service = new SecurityAuditService(db, settings);
5934
+ const format = c.req.query("format") || "json";
5935
+ const filters = {
5936
+ eventType: c.req.query("type"),
5937
+ severity: c.req.query("severity"),
5938
+ startDate: c.req.query("start") ? parseInt(c.req.query("start")) : void 0,
5939
+ endDate: c.req.query("end") ? parseInt(c.req.query("end")) : void 0,
5940
+ limit: 1e4,
5941
+ page: 1
5942
+ };
5943
+ const { events } = await service.getEvents(filters);
5944
+ if (format === "csv") {
5945
+ const headers = ["id", "event_type", "severity", "email", "ip_address", "country_code", "blocked", "created_at"];
5946
+ const csvRows = [headers.join(",")];
5947
+ for (const event of events) {
5948
+ csvRows.push([
5949
+ event.id,
5950
+ event.eventType,
5951
+ event.severity,
5952
+ event.email || "",
5953
+ event.ipAddress || "",
5954
+ event.countryCode || "",
5955
+ event.blocked ? "1" : "0",
5956
+ new Date(event.createdAt).toISOString()
5957
+ ].map((v) => `"${String(v).replace(/"/g, '""')}"`).join(","));
5958
+ }
5959
+ return new Response(csvRows.join("\n"), {
5960
+ headers: {
5961
+ "Content-Type": "text/csv",
5962
+ "Content-Disposition": `attachment; filename="security-events-${Date.now()}.csv"`
5963
+ }
5964
+ });
5965
+ }
5966
+ return c.json(events);
5967
+ });
5968
+
5969
+ // src/plugins/core-plugins/security-audit-plugin/middleware/audit-middleware.ts
5970
+ function extractRequestInfo(c) {
5971
+ const ip = c.req.header("cf-connecting-ip") || c.req.header("x-forwarded-for")?.split(",")[0]?.trim() || "unknown";
5972
+ const userAgent = c.req.header("user-agent") || "unknown";
5973
+ const countryCode = c.req.header("cf-ipcountry") || null;
5974
+ const path = new URL(c.req.url).pathname;
5975
+ const method = c.req.method;
5976
+ return { ip, userAgent, countryCode, path, method };
5977
+ }
5978
+ function generateFingerprint(ip, userAgent) {
5979
+ const str = `${ip}:${userAgent}`;
5980
+ let hash = 0;
5981
+ for (let i = 0; i < str.length; i++) {
5982
+ const char = str.charCodeAt(i);
5983
+ hash = (hash << 5) - hash + char;
5984
+ hash |= 0;
5985
+ }
5986
+ return Math.abs(hash).toString(36);
5987
+ }
5988
+ async function getPluginSettings(db) {
5989
+ try {
5990
+ const pluginService = new PluginService(db);
5991
+ const plugin2 = await pluginService.getPlugin("security-audit");
5992
+ if (plugin2?.settings) {
5993
+ const settings = typeof plugin2.settings === "string" ? JSON.parse(plugin2.settings) : plugin2.settings;
5994
+ return { ...DEFAULT_SETTINGS2, ...settings };
5995
+ }
5996
+ } catch {
5997
+ }
5998
+ return DEFAULT_SETTINGS2;
5999
+ }
6000
+ async function isPluginActive2(db) {
6001
+ try {
6002
+ const result = await db.prepare(
6003
+ "SELECT status FROM plugins WHERE id = 'security-audit'"
6004
+ ).first();
6005
+ return result?.status === "active";
6006
+ } catch {
6007
+ return false;
6008
+ }
6009
+ }
6010
+ function securityAuditMiddleware() {
6011
+ return async (c, next) => {
6012
+ const path = new URL(c.req.url).pathname;
6013
+ if (!path.startsWith("/auth/")) {
6014
+ return next();
6015
+ }
6016
+ const db = c.env.DB;
6017
+ if (!await isPluginActive2(db)) {
6018
+ return next();
6019
+ }
6020
+ const settings = await getPluginSettings(db);
6021
+ const { ip, userAgent, countryCode, method } = extractRequestInfo(c);
6022
+ const fingerprint = generateFingerprint(ip, userAgent);
6023
+ const isLoginPost = (path === "/auth/login" || path === "/auth/login/form") && method === "POST";
6024
+ let preExtractedEmail = "";
6025
+ if (isLoginPost) {
6026
+ try {
6027
+ if (path === "/auth/login/form") {
6028
+ const clonedReq = c.req.raw.clone();
6029
+ const formData = await clonedReq.formData();
6030
+ preExtractedEmail = (formData.get("email") || "").toLowerCase();
6031
+ } else {
6032
+ const body = await c.req.json();
6033
+ preExtractedEmail = body?.email?.toLowerCase() || "";
6034
+ }
6035
+ } catch {
6036
+ }
6037
+ if (preExtractedEmail && settings.bruteForce.enabled) {
6038
+ const detector = new BruteForceDetector(c.env.CACHE_KV, settings.bruteForce);
6039
+ const lockStatus = await detector.isLocked(ip, preExtractedEmail);
6040
+ if (lockStatus.locked) {
6041
+ const service = new SecurityAuditService(db, settings);
6042
+ const logPromise2 = service.logEvent({
6043
+ eventType: "login_failure",
6044
+ severity: "warning",
6045
+ email: preExtractedEmail,
6046
+ ipAddress: ip,
6047
+ userAgent,
6048
+ countryCode: countryCode || void 0,
6049
+ requestPath: path,
6050
+ requestMethod: method,
6051
+ fingerprint,
6052
+ blocked: true,
6053
+ details: { reason: lockStatus.reason }
6054
+ });
6055
+ if (c.executionCtx?.waitUntil) {
6056
+ c.executionCtx.waitUntil(logPromise2);
6057
+ }
6058
+ return c.json({
6059
+ error: lockStatus.reason || "Too many failed attempts. Please try again later."
6060
+ }, 429);
6061
+ }
6062
+ }
6063
+ }
6064
+ await next();
6065
+ const logPromise = logAuthEvent(c, db, settings, ip, userAgent, countryCode, fingerprint, path, method, preExtractedEmail);
6066
+ if (c.executionCtx?.waitUntil) {
6067
+ c.executionCtx.waitUntil(logPromise);
6068
+ }
6069
+ };
6070
+ }
6071
+ async function logAuthEvent(c, db, settings, ip, userAgent, countryCode, fingerprint, path, method, preExtractedEmail = "") {
6072
+ try {
6073
+ const service = new SecurityAuditService(db, settings);
6074
+ const status = c.res.status;
6075
+ const isLoginPost = (path === "/auth/login" || path === "/auth/login/form") && method === "POST";
6076
+ const isFormLogin = path === "/auth/login/form";
6077
+ if (isLoginPost) {
6078
+ let loginSucceeded;
6079
+ if (isFormLogin) {
6080
+ const hxRedirect = c.res.headers.get("HX-Redirect");
6081
+ const setCookieHeader = c.res.headers.get("set-cookie") || "";
6082
+ loginSucceeded = !!(hxRedirect?.includes("/admin") || setCookieHeader.includes("auth_token"));
6083
+ } else {
6084
+ loginSucceeded = status === 200;
6085
+ }
6086
+ if (loginSucceeded) {
6087
+ if (!settings.logging.logSuccessfulLogins) return;
6088
+ let email = preExtractedEmail;
6089
+ let userId = "";
6090
+ if (!isFormLogin) {
6091
+ try {
6092
+ const cloned = c.res.clone();
6093
+ const body = await cloned.json();
6094
+ email = body?.user?.email || email;
6095
+ userId = body?.user?.id || "";
6096
+ } catch {
6097
+ }
6098
+ }
6099
+ await service.logEvent({
6100
+ eventType: "login_success",
6101
+ severity: "info",
6102
+ userId: userId || void 0,
6103
+ email: email || void 0,
6104
+ ipAddress: ip,
6105
+ userAgent,
6106
+ countryCode: countryCode || void 0,
6107
+ requestPath: path,
6108
+ requestMethod: method,
6109
+ fingerprint
6110
+ });
6111
+ } else {
6112
+ const email = preExtractedEmail;
6113
+ await service.logEvent({
6114
+ eventType: "login_failure",
6115
+ severity: "warning",
6116
+ email: email || void 0,
6117
+ ipAddress: ip,
6118
+ userAgent,
6119
+ countryCode: countryCode || void 0,
6120
+ requestPath: path,
6121
+ requestMethod: method,
6122
+ fingerprint,
6123
+ details: { statusCode: status }
6124
+ });
6125
+ if (email && settings.bruteForce.enabled) {
6126
+ const detector = new BruteForceDetector(c.env.CACHE_KV, settings.bruteForce);
6127
+ const result = await detector.recordFailedAttempt(ip, email);
6128
+ if (result.shouldLockIP) {
6129
+ await detector.lockIP(ip);
6130
+ await service.logEvent({
6131
+ eventType: "account_lockout",
6132
+ severity: "critical",
6133
+ email,
6134
+ ipAddress: ip,
6135
+ userAgent,
6136
+ countryCode: countryCode || void 0,
6137
+ requestPath: path,
6138
+ requestMethod: method,
6139
+ fingerprint,
6140
+ details: { reason: "brute_force_ip", attemptCount: result.ipCount }
6141
+ });
6142
+ }
6143
+ if (result.shouldLockEmail) {
6144
+ await detector.lockEmail(email);
6145
+ await service.logEvent({
6146
+ eventType: "account_lockout",
6147
+ severity: "critical",
6148
+ email,
6149
+ ipAddress: ip,
6150
+ userAgent,
6151
+ countryCode: countryCode || void 0,
6152
+ requestPath: path,
6153
+ requestMethod: method,
6154
+ fingerprint,
6155
+ details: { reason: "brute_force_email", attemptCount: result.emailCount }
6156
+ });
6157
+ }
6158
+ if (result.isSuspicious) {
6159
+ await service.logEvent({
6160
+ eventType: "suspicious_activity",
6161
+ severity: "critical",
6162
+ ipAddress: ip,
6163
+ userAgent,
6164
+ countryCode: countryCode || void 0,
6165
+ requestPath: path,
6166
+ requestMethod: method,
6167
+ fingerprint,
6168
+ details: { reason: "multiple_emails_from_ip", ipCount: result.ipCount }
6169
+ });
6170
+ }
6171
+ }
6172
+ }
6173
+ }
6174
+ if (path === "/auth/register" && method === "POST" && settings.logging.logRegistrations) {
6175
+ if (status === 201 || status === 200) {
6176
+ let email = "";
6177
+ let userId = "";
6178
+ try {
6179
+ const cloned = c.res.clone();
6180
+ const body = await cloned.json();
6181
+ email = body?.user?.email || "";
6182
+ userId = body?.user?.id || "";
6183
+ } catch {
6184
+ }
6185
+ await service.logEvent({
6186
+ eventType: "registration",
6187
+ severity: "info",
6188
+ userId: userId || void 0,
6189
+ email: email || void 0,
6190
+ ipAddress: ip,
6191
+ userAgent,
6192
+ countryCode: countryCode || void 0,
6193
+ requestPath: path,
6194
+ requestMethod: method,
6195
+ fingerprint
6196
+ });
6197
+ }
6198
+ }
6199
+ if (path === "/auth/logout" && settings.logging.logLogouts) {
6200
+ const user = c.get("user");
6201
+ await service.logEvent({
6202
+ eventType: "logout",
6203
+ severity: "info",
6204
+ userId: user?.userId,
6205
+ email: user?.email,
6206
+ ipAddress: ip,
6207
+ userAgent,
6208
+ countryCode: countryCode || void 0,
6209
+ requestPath: path,
6210
+ requestMethod: method,
6211
+ fingerprint
6212
+ });
6213
+ }
6214
+ } catch (error) {
6215
+ console.error("[SecurityAudit] Error logging auth event:", error);
6216
+ }
6217
+ }
6218
+
6219
+ // src/plugins/core-plugins/security-audit-plugin/index.ts
6220
+ function createSecurityAuditPlugin() {
6221
+ const builder = PluginBuilder.create({
6222
+ name: "security-audit",
6223
+ version: "1.0.0-beta.1",
6224
+ description: "Security event logging, brute-force detection, and analytics dashboard"
6225
+ });
6226
+ builder.metadata({
6227
+ author: { name: "SonicJS Team" },
6228
+ license: "MIT"
6229
+ });
6230
+ builder.addRoute("/admin/plugins/security-audit", adminRoutes2, {
6231
+ description: "Security audit dashboard and admin pages",
6232
+ requiresAuth: true,
6233
+ priority: 50
6234
+ });
6235
+ builder.addRoute("/api/security-audit", apiRoutes2, {
6236
+ description: "Security audit API endpoints",
6237
+ requiresAuth: true,
6238
+ priority: 50
6239
+ });
6240
+ builder.addMenuItem("Security", "/admin/plugins/security-audit", {
6241
+ icon: `<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/></svg>`,
6242
+ order: 85
6243
+ });
6244
+ builder.lifecycle({
6245
+ install: async (context) => {
6246
+ console.log("[SecurityAudit] Plugin installed");
6247
+ },
6248
+ activate: async (context) => {
6249
+ console.log("[SecurityAudit] Plugin activated");
6250
+ },
6251
+ deactivate: async (context) => {
6252
+ console.log("[SecurityAudit] Plugin deactivated");
6253
+ },
6254
+ uninstall: async (context) => {
6255
+ console.log("[SecurityAudit] Plugin uninstalled");
6256
+ }
6257
+ });
6258
+ return builder.build();
6259
+ }
6260
+ var securityAuditPlugin = createSecurityAuditPlugin();
6261
+
6262
+ // src/middleware/plugin-menu.ts
6263
+ var MENU_PLUGINS = [
6264
+ securityAuditPlugin
6265
+ ];
6266
+ var MARKER = "<!-- DYNAMIC_PLUGIN_MENU -->";
6267
+ function renderMenuItem(item, currentPath) {
6268
+ const isActive = currentPath === item.path || currentPath.startsWith(item.path);
6269
+ const fallbackIcon = `<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>`;
6270
+ return `
6271
+ <span class="relative">
6272
+ ${isActive ? '<span class="absolute inset-y-2 -left-4 w-0.5 rounded-full bg-cyan-500 dark:bg-cyan-400"></span>' : ""}
6273
+ <a
6274
+ href="${item.path}"
6275
+ class="flex w-full items-center gap-3 rounded-lg px-2 py-2.5 text-left text-sm/5 font-medium ${isActive ? "text-zinc-950 dark:text-white" : "text-zinc-950 hover:bg-zinc-950/5 dark:text-white dark:hover:bg-white/5"}"
6276
+ ${isActive ? 'data-current="true"' : ""}
6277
+ >
6278
+ <span class="shrink-0 ${isActive ? "fill-zinc-950 dark:fill-white" : "fill-zinc-500 dark:fill-zinc-400"}">
6279
+ ${item.icon || fallbackIcon}
6280
+ </span>
6281
+ <span class="truncate">${item.label}</span>
6282
+ </a>
6283
+ </span>`;
6284
+ }
6285
+ function pluginMenuMiddleware() {
6286
+ return async (c, next) => {
6287
+ const path = new URL(c.req.url).pathname;
6288
+ if (!path.startsWith("/admin")) {
6289
+ return next();
6290
+ }
6291
+ let activeMenuItems = [];
6292
+ try {
6293
+ const db = c.env.DB;
6294
+ const pluginNames = MENU_PLUGINS.map((p) => p.name);
6295
+ if (pluginNames.length > 0) {
6296
+ const placeholders = pluginNames.map(() => "?").join(",");
6297
+ const result = await db.prepare(
6298
+ `SELECT name FROM plugins WHERE name IN (${placeholders}) AND status = 'active'`
6299
+ ).bind(...pluginNames).all();
6300
+ const activeNames = new Set((result.results || []).map((r) => r.name));
6301
+ for (const plugin2 of MENU_PLUGINS) {
6302
+ if (activeNames.has(plugin2.name) && plugin2.menuItems) {
6303
+ activeMenuItems.push(...plugin2.menuItems);
6304
+ }
6305
+ }
6306
+ activeMenuItems.sort((a, b) => (a.order || 0) - (b.order || 0));
6307
+ }
6308
+ } catch {
6309
+ }
6310
+ c.set("pluginMenuItems", activeMenuItems.map((m) => ({ label: m.label, path: m.path, icon: m.icon || "" })));
6311
+ await next();
6312
+ if (activeMenuItems.length > 0 && c.res.headers.get("content-type")?.includes("text/html")) {
6313
+ const status = c.res.status;
6314
+ const headers = new Headers(c.res.headers);
6315
+ const html = await c.res.text();
6316
+ if (html.includes(MARKER)) {
6317
+ const renderedItems = activeMenuItems.map((item) => renderMenuItem(item, path)).join("");
6318
+ const newHtml = html.split(MARKER).join(renderedItems);
6319
+ c.res = new Response(newHtml, { status, headers });
6320
+ } else {
6321
+ c.res = new Response(html, { status, headers });
6322
+ }
6323
+ }
6324
+ };
6325
+ }
6326
+
4696
6327
  // src/plugins/cache/services/cache-config.ts
4697
6328
  var CACHE_CONFIGS = {
4698
6329
  // Content (high read, low write)
@@ -6565,6 +8196,7 @@ function createSonicJSApp(config = {}) {
6565
8196
  app2.use("*", middleware);
6566
8197
  }
6567
8198
  }
8199
+ app2.use("/admin/*", pluginMenuMiddleware());
6568
8200
  app2.route("/api", api_default);
6569
8201
  app2.route("/api/media", api_media_default);
6570
8202
  app2.route("/api/system", api_system_default);
@@ -6580,6 +8212,12 @@ function createSonicJSApp(config = {}) {
6580
8212
  app2.route("/admin/seed-data", createSeedDataAdminRoutes());
6581
8213
  app2.route("/admin/content", admin_content_default);
6582
8214
  app2.route("/admin/media", adminMediaRoutes);
8215
+ app2.use("/auth/*", securityAuditMiddleware());
8216
+ if (securityAuditPlugin.routes && securityAuditPlugin.routes.length > 0) {
8217
+ for (const route of securityAuditPlugin.routes) {
8218
+ app2.route(route.path, route.handler);
8219
+ }
8220
+ }
6583
8221
  if (aiSearchPlugin.routes && aiSearchPlugin.routes.length > 0) {
6584
8222
  for (const route of aiSearchPlugin.routes) {
6585
8223
  app2.route(route.path, route.handler);
@@ -6591,6 +8229,11 @@ function createSonicJSApp(config = {}) {
6591
8229
  app2.route(route.path, route.handler);
6592
8230
  }
6593
8231
  }
8232
+ if (userProfilesPlugin.routes && userProfilesPlugin.routes.length > 0) {
8233
+ for (const route of userProfilesPlugin.routes) {
8234
+ app2.route(route.path, route.handler);
8235
+ }
8236
+ }
6594
8237
  if (otpLoginPlugin.routes && otpLoginPlugin.routes.length > 0) {
6595
8238
  for (const route of otpLoginPlugin.routes) {
6596
8239
  app2.route(route.path, route.handler);