@sonicjs-cms/core 2.10.1 → 2.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{app-Ozl9agJG.d.cts → app-COElO4Rm.d.cts} +6 -1
- package/dist/{app-Ozl9agJG.d.ts → app-COElO4Rm.d.ts} +6 -1
- package/dist/{chunk-CJYFSKH7.js → chunk-2MXF4RYZ.js} +3 -3
- package/dist/{chunk-CJYFSKH7.js.map → chunk-2MXF4RYZ.js.map} +1 -1
- package/dist/{chunk-MNFY6DWY.cjs → chunk-56GUBLJE.cjs} +7 -7
- package/dist/{chunk-MNFY6DWY.cjs.map → chunk-56GUBLJE.cjs.map} +1 -1
- package/dist/{chunk-YXTFJPMN.js → chunk-6R6LAUR7.js} +3 -3
- package/dist/{chunk-YXTFJPMN.js.map → chunk-6R6LAUR7.js.map} +1 -1
- package/dist/{chunk-JJS7JZCH.js → chunk-76TX6XND.js} +4 -2
- package/dist/chunk-76TX6XND.js.map +1 -0
- package/dist/{chunk-EAJJHE5F.cjs → chunk-AG3SIPP7.cjs} +9 -2
- package/dist/chunk-AG3SIPP7.cjs.map +1 -0
- package/dist/{chunk-74XCYEI7.js → chunk-BWZBKLOC.js} +3 -3
- package/dist/{chunk-74XCYEI7.js.map → chunk-BWZBKLOC.js.map} +1 -1
- package/dist/{chunk-BUPNX3ZM.js → chunk-H3XXBAMO.js} +50 -2
- package/dist/chunk-H3XXBAMO.js.map +1 -0
- package/dist/{chunk-LTKV7AE5.cjs → chunk-H4NHRZ6Y.cjs} +4 -2
- package/dist/chunk-H4NHRZ6Y.cjs.map +1 -0
- package/dist/{chunk-JFMBYQTC.js → chunk-HXIYYE57.js} +6 -6
- package/dist/{chunk-JFMBYQTC.js.map → chunk-HXIYYE57.js.map} +1 -1
- package/dist/{chunk-TWCQVJ6M.cjs → chunk-I6FFGQIT.cjs} +50 -2
- package/dist/chunk-I6FFGQIT.cjs.map +1 -0
- package/dist/{chunk-SDAGUFOF.js → chunk-NDFHQOPP.js} +4318 -3820
- package/dist/chunk-NDFHQOPP.js.map +1 -0
- package/dist/{chunk-LFAQUR7P.cjs → chunk-NZWFCUDA.cjs} +26 -2
- package/dist/chunk-NZWFCUDA.cjs.map +1 -0
- package/dist/{chunk-KYGRJCZM.cjs → chunk-QTFKZBLC.cjs} +3 -2
- package/dist/chunk-QTFKZBLC.cjs.map +1 -0
- package/dist/{chunk-LOUJRBXV.js → chunk-QXOZI5Q2.js} +3 -2
- package/dist/chunk-QXOZI5Q2.js.map +1 -0
- package/dist/{chunk-3G7XX4UI.cjs → chunk-RBXFXT7H.cjs} +9 -9
- package/dist/{chunk-3G7XX4UI.cjs.map → chunk-RBXFXT7H.cjs.map} +1 -1
- package/dist/{chunk-E2GKK5HX.cjs → chunk-RXNLGINR.cjs} +3 -3
- package/dist/{chunk-E2GKK5HX.cjs.map → chunk-RXNLGINR.cjs.map} +1 -1
- package/dist/{chunk-VJCLJH3X.js → chunk-TBJY2FF7.js} +26 -2
- package/dist/chunk-TBJY2FF7.js.map +1 -0
- package/dist/{chunk-FW5CGNM2.js → chunk-U3ZMGBVC.js} +9 -2
- package/dist/chunk-U3ZMGBVC.js.map +1 -0
- package/dist/{chunk-5GO3AMON.cjs → chunk-VHNTCB2X.cjs} +10 -10
- package/dist/{chunk-5GO3AMON.cjs.map → chunk-VHNTCB2X.cjs.map} +1 -1
- package/dist/{chunk-HGKBMUYY.cjs → chunk-ZV6ZCJ74.cjs} +4498 -3996
- package/dist/chunk-ZV6ZCJ74.cjs.map +1 -0
- package/dist/index.cjs +2469 -240
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +175 -7
- package/dist/index.d.ts +175 -7
- package/dist/index.js +2294 -84
- package/dist/index.js.map +1 -1
- package/dist/middleware.cjs +29 -29
- package/dist/middleware.d.cts +1 -1
- package/dist/middleware.d.ts +1 -1
- package/dist/middleware.js +3 -3
- package/dist/migrations-USSEHJC7.js +4 -0
- package/dist/{migrations-ADK6YNM2.js.map → migrations-USSEHJC7.js.map} +1 -1
- package/dist/migrations-ZE6IZNLB.cjs +13 -0
- package/dist/{migrations-EM2D6EG2.cjs.map → migrations-ZE6IZNLB.cjs.map} +1 -1
- package/dist/{plugin-0Xogrln-.d.cts → plugin-DDYetMF-.d.cts} +1 -0
- package/dist/{plugin-0Xogrln-.d.ts → plugin-DDYetMF-.d.ts} +1 -0
- package/dist/{plugin-bootstrap-CD63DZ-p.d.ts → plugin-bootstrap-CZ1GDum7.d.ts} +803 -2
- package/dist/{plugin-bootstrap-B8PXeGj_.d.cts → plugin-bootstrap-DVGLQrcO.d.cts} +803 -2
- package/dist/{plugin-manager-GcIeb226.d.cts → plugin-manager-BoM3Q7o7.d.cts} +1 -1
- package/dist/{plugin-manager-Clf2gXwj.d.ts → plugin-manager-Efx9RyDX.d.ts} +1 -1
- package/dist/plugins.cjs +10 -10
- package/dist/plugins.d.cts +2 -2
- package/dist/plugins.d.ts +2 -2
- package/dist/plugins.js +2 -2
- package/dist/routes.cjs +30 -30
- package/dist/routes.d.cts +1 -1
- package/dist/routes.d.ts +1 -1
- package/dist/routes.js +7 -7
- package/dist/services.cjs +39 -39
- package/dist/services.d.cts +1 -1
- package/dist/services.d.ts +1 -1
- package/dist/services.js +3 -3
- package/dist/templates.cjs +19 -19
- package/dist/templates.js +2 -2
- package/dist/types.cjs +2 -2
- package/dist/types.d.cts +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/utils.cjs +11 -11
- package/dist/utils.js +1 -1
- package/migrations/034_security_audit_plugin.sql +27 -0
- package/package.json +1 -1
- package/dist/chunk-BUPNX3ZM.js.map +0 -1
- package/dist/chunk-EAJJHE5F.cjs.map +0 -1
- package/dist/chunk-FW5CGNM2.js.map +0 -1
- package/dist/chunk-HGKBMUYY.cjs.map +0 -1
- package/dist/chunk-JJS7JZCH.js.map +0 -1
- package/dist/chunk-KYGRJCZM.cjs.map +0 -1
- package/dist/chunk-LFAQUR7P.cjs.map +0 -1
- package/dist/chunk-LOUJRBXV.js.map +0 -1
- package/dist/chunk-LTKV7AE5.cjs.map +0 -1
- package/dist/chunk-SDAGUFOF.js.map +0 -1
- package/dist/chunk-TWCQVJ6M.cjs.map +0 -1
- package/dist/chunk-VJCLJH3X.js.map +0 -1
- package/dist/migrations-ADK6YNM2.js +0 -4
- package/dist/migrations-EM2D6EG2.cjs +0 -13
package/dist/index.js
CHANGED
|
@@ -1,26 +1,27 @@
|
|
|
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-
|
|
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-
|
|
3
|
-
import { SettingsService, setAppInstance, schema_exports } from './chunk-
|
|
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-
|
|
5
|
-
import { requireAuth, AuthManager, metricsMiddleware, bootstrapMiddleware, securityHeadersMiddleware, csrfProtection } from './chunk-
|
|
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-
|
|
7
|
-
|
|
8
|
-
export {
|
|
9
|
-
export {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
export {
|
|
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-NDFHQOPP.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-NDFHQOPP.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-HXIYYE57.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-HXIYYE57.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-U3ZMGBVC.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';
|
|
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-
|
|
16
|
-
export { QueryFilterBuilder, SONICJS_VERSION, TemplateRenderer, buildQuery, getCoreVersion, renderTemplate, templateRenderer } from './chunk-
|
|
16
|
+
import { package_default, getCoreVersion } from './chunk-6R6LAUR7.js';
|
|
17
|
+
export { QueryFilterBuilder, SONICJS_VERSION, TemplateRenderer, buildQuery, getCoreVersion, renderTemplate, templateRenderer } from './chunk-6R6LAUR7.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';
|
|
20
|
-
export { HOOKS } from './chunk-
|
|
21
|
+
export { HOOKS } from './chunk-QXOZI5Q2.js';
|
|
21
22
|
import './chunk-V4OQ3NZ2.js';
|
|
22
23
|
import { Hono } from 'hono';
|
|
23
|
-
import { setCookie } from 'hono/cookie';
|
|
24
|
+
import { setCookie, getCookie } from 'hono/cookie';
|
|
24
25
|
import { z } from 'zod';
|
|
25
26
|
import { drizzle } from 'drizzle-orm/d1';
|
|
26
27
|
|
|
@@ -1826,7 +1827,8 @@ function createOTPLoginPlugin() {
|
|
|
1826
1827
|
email: normalizedEmail,
|
|
1827
1828
|
ipAddress,
|
|
1828
1829
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1829
|
-
appName: siteName
|
|
1830
|
+
appName: siteName,
|
|
1831
|
+
logoUrl: settings.logoUrl || ""
|
|
1830
1832
|
});
|
|
1831
1833
|
const emailPlugin2 = await db.prepare(`
|
|
1832
1834
|
SELECT settings FROM plugins WHERE id = 'email'
|
|
@@ -2001,6 +2003,567 @@ function createOTPLoginPlugin() {
|
|
|
2001
2003
|
}
|
|
2002
2004
|
var otpLoginPlugin = createOTPLoginPlugin();
|
|
2003
2005
|
|
|
2006
|
+
// src/plugins/core-plugins/oauth-providers/oauth-service.ts
|
|
2007
|
+
var GITHUB_PROVIDER = {
|
|
2008
|
+
id: "github",
|
|
2009
|
+
name: "GitHub",
|
|
2010
|
+
authorizeUrl: "https://github.com/login/oauth/authorize",
|
|
2011
|
+
tokenUrl: "https://github.com/login/oauth/access_token",
|
|
2012
|
+
userInfoUrl: "https://api.github.com/user",
|
|
2013
|
+
scopes: ["read:user", "user:email"],
|
|
2014
|
+
mapProfile: (profile) => ({
|
|
2015
|
+
providerAccountId: String(profile.id),
|
|
2016
|
+
email: profile.email || "",
|
|
2017
|
+
name: profile.name || profile.login || "",
|
|
2018
|
+
avatar: profile.avatar_url || void 0
|
|
2019
|
+
})
|
|
2020
|
+
};
|
|
2021
|
+
var GOOGLE_PROVIDER = {
|
|
2022
|
+
id: "google",
|
|
2023
|
+
name: "Google",
|
|
2024
|
+
authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
2025
|
+
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
2026
|
+
userInfoUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
|
|
2027
|
+
scopes: ["openid", "email", "profile"],
|
|
2028
|
+
mapProfile: (profile) => ({
|
|
2029
|
+
providerAccountId: String(profile.id),
|
|
2030
|
+
email: profile.email || "",
|
|
2031
|
+
name: profile.name || "",
|
|
2032
|
+
avatar: profile.picture || void 0
|
|
2033
|
+
})
|
|
2034
|
+
};
|
|
2035
|
+
var BUILT_IN_PROVIDERS = {
|
|
2036
|
+
github: GITHUB_PROVIDER,
|
|
2037
|
+
google: GOOGLE_PROVIDER
|
|
2038
|
+
};
|
|
2039
|
+
var OAuthService = class {
|
|
2040
|
+
constructor(db) {
|
|
2041
|
+
this.db = db;
|
|
2042
|
+
}
|
|
2043
|
+
/**
|
|
2044
|
+
* Build the authorization redirect URL for a provider.
|
|
2045
|
+
*/
|
|
2046
|
+
buildAuthorizeUrl(provider, clientId, redirectUri, state) {
|
|
2047
|
+
const params = new URLSearchParams({
|
|
2048
|
+
client_id: clientId,
|
|
2049
|
+
redirect_uri: redirectUri,
|
|
2050
|
+
response_type: "code",
|
|
2051
|
+
scope: provider.scopes.join(" "),
|
|
2052
|
+
state
|
|
2053
|
+
});
|
|
2054
|
+
if (provider.id === "google") {
|
|
2055
|
+
params.set("access_type", "offline");
|
|
2056
|
+
params.set("prompt", "consent");
|
|
2057
|
+
}
|
|
2058
|
+
return `${provider.authorizeUrl}?${params.toString()}`;
|
|
2059
|
+
}
|
|
2060
|
+
/**
|
|
2061
|
+
* Exchange authorization code for tokens using native fetch.
|
|
2062
|
+
*/
|
|
2063
|
+
async exchangeCode(provider, clientId, clientSecret, code, redirectUri) {
|
|
2064
|
+
const body = {
|
|
2065
|
+
client_id: clientId,
|
|
2066
|
+
client_secret: clientSecret,
|
|
2067
|
+
code,
|
|
2068
|
+
redirect_uri: redirectUri,
|
|
2069
|
+
grant_type: "authorization_code"
|
|
2070
|
+
};
|
|
2071
|
+
const response = await fetch(provider.tokenUrl, {
|
|
2072
|
+
method: "POST",
|
|
2073
|
+
headers: {
|
|
2074
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
2075
|
+
"Accept": "application/json"
|
|
2076
|
+
},
|
|
2077
|
+
body: new URLSearchParams(body).toString()
|
|
2078
|
+
});
|
|
2079
|
+
if (!response.ok) {
|
|
2080
|
+
const errorText = await response.text();
|
|
2081
|
+
throw new Error(`Token exchange failed (${response.status}): ${errorText}`);
|
|
2082
|
+
}
|
|
2083
|
+
const data = await response.json();
|
|
2084
|
+
if (data.error) {
|
|
2085
|
+
throw new Error(`Token exchange error: ${data.error_description || data.error}`);
|
|
2086
|
+
}
|
|
2087
|
+
return {
|
|
2088
|
+
access_token: data.access_token,
|
|
2089
|
+
refresh_token: data.refresh_token,
|
|
2090
|
+
expires_in: data.expires_in ? Number(data.expires_in) : void 0
|
|
2091
|
+
};
|
|
2092
|
+
}
|
|
2093
|
+
/**
|
|
2094
|
+
* Fetch user profile from the provider's userinfo endpoint.
|
|
2095
|
+
*/
|
|
2096
|
+
async fetchUserProfile(provider, accessToken) {
|
|
2097
|
+
const headers = {
|
|
2098
|
+
"Authorization": `Bearer ${accessToken}`,
|
|
2099
|
+
"Accept": "application/json"
|
|
2100
|
+
};
|
|
2101
|
+
if (provider.id === "github") {
|
|
2102
|
+
headers["Authorization"] = `token ${accessToken}`;
|
|
2103
|
+
}
|
|
2104
|
+
const response = await fetch(provider.userInfoUrl, { headers });
|
|
2105
|
+
if (!response.ok) {
|
|
2106
|
+
throw new Error(`Failed to fetch user profile (${response.status})`);
|
|
2107
|
+
}
|
|
2108
|
+
const profile = await response.json();
|
|
2109
|
+
if (provider.id === "github" && !profile.email) {
|
|
2110
|
+
const emailResponse = await fetch("https://api.github.com/user/emails", {
|
|
2111
|
+
headers: {
|
|
2112
|
+
"Authorization": `token ${accessToken}`,
|
|
2113
|
+
"Accept": "application/json"
|
|
2114
|
+
}
|
|
2115
|
+
});
|
|
2116
|
+
if (emailResponse.ok) {
|
|
2117
|
+
const emails = await emailResponse.json();
|
|
2118
|
+
const primaryEmail = emails.find((e) => e.primary && e.verified);
|
|
2119
|
+
if (primaryEmail) {
|
|
2120
|
+
profile.email = primaryEmail.email;
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
return provider.mapProfile(profile);
|
|
2125
|
+
}
|
|
2126
|
+
// ─── Database Operations ────────────────────────────────────────────────
|
|
2127
|
+
/**
|
|
2128
|
+
* Find an existing OAuth account link.
|
|
2129
|
+
*/
|
|
2130
|
+
async findOAuthAccount(provider, providerAccountId) {
|
|
2131
|
+
return await this.db.prepare(`
|
|
2132
|
+
SELECT * FROM oauth_accounts
|
|
2133
|
+
WHERE provider = ? AND provider_account_id = ?
|
|
2134
|
+
`).bind(provider, providerAccountId).first();
|
|
2135
|
+
}
|
|
2136
|
+
/**
|
|
2137
|
+
* Find all OAuth accounts for a user.
|
|
2138
|
+
*/
|
|
2139
|
+
async findUserOAuthAccounts(userId) {
|
|
2140
|
+
const result = await this.db.prepare(`
|
|
2141
|
+
SELECT * FROM oauth_accounts WHERE user_id = ?
|
|
2142
|
+
`).bind(userId).all();
|
|
2143
|
+
return result.results || [];
|
|
2144
|
+
}
|
|
2145
|
+
/**
|
|
2146
|
+
* Create a new OAuth account link.
|
|
2147
|
+
*/
|
|
2148
|
+
async createOAuthAccount(params) {
|
|
2149
|
+
const id = crypto.randomUUID();
|
|
2150
|
+
const now = Date.now();
|
|
2151
|
+
await this.db.prepare(`
|
|
2152
|
+
INSERT INTO oauth_accounts (
|
|
2153
|
+
id, user_id, provider, provider_account_id,
|
|
2154
|
+
access_token, refresh_token, token_expires_at,
|
|
2155
|
+
profile_data, created_at, updated_at
|
|
2156
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2157
|
+
`).bind(
|
|
2158
|
+
id,
|
|
2159
|
+
params.userId,
|
|
2160
|
+
params.provider,
|
|
2161
|
+
params.providerAccountId,
|
|
2162
|
+
params.accessToken,
|
|
2163
|
+
params.refreshToken || null,
|
|
2164
|
+
params.tokenExpiresAt || null,
|
|
2165
|
+
params.profileData || null,
|
|
2166
|
+
now,
|
|
2167
|
+
now
|
|
2168
|
+
).run();
|
|
2169
|
+
return {
|
|
2170
|
+
id,
|
|
2171
|
+
user_id: params.userId,
|
|
2172
|
+
provider: params.provider,
|
|
2173
|
+
provider_account_id: params.providerAccountId,
|
|
2174
|
+
access_token: params.accessToken,
|
|
2175
|
+
refresh_token: params.refreshToken || null,
|
|
2176
|
+
token_expires_at: params.tokenExpiresAt || null,
|
|
2177
|
+
profile_data: params.profileData || null,
|
|
2178
|
+
created_at: now,
|
|
2179
|
+
updated_at: now
|
|
2180
|
+
};
|
|
2181
|
+
}
|
|
2182
|
+
/**
|
|
2183
|
+
* Update tokens for an existing OAuth account.
|
|
2184
|
+
*/
|
|
2185
|
+
async updateOAuthTokens(id, accessToken, refreshToken, tokenExpiresAt) {
|
|
2186
|
+
await this.db.prepare(`
|
|
2187
|
+
UPDATE oauth_accounts
|
|
2188
|
+
SET access_token = ?, refresh_token = ?, token_expires_at = ?, updated_at = ?
|
|
2189
|
+
WHERE id = ?
|
|
2190
|
+
`).bind(accessToken, refreshToken || null, tokenExpiresAt || null, Date.now(), id).run();
|
|
2191
|
+
}
|
|
2192
|
+
/**
|
|
2193
|
+
* Unlink an OAuth account from a user (only if they have another auth method).
|
|
2194
|
+
*/
|
|
2195
|
+
async unlinkOAuthAccount(userId, provider) {
|
|
2196
|
+
const user = await this.db.prepare(`
|
|
2197
|
+
SELECT password_hash FROM users WHERE id = ?
|
|
2198
|
+
`).bind(userId).first();
|
|
2199
|
+
const otherLinks = await this.db.prepare(`
|
|
2200
|
+
SELECT COUNT(*) as count FROM oauth_accounts
|
|
2201
|
+
WHERE user_id = ? AND provider != ?
|
|
2202
|
+
`).bind(userId, provider).first();
|
|
2203
|
+
const hasPassword = !!user?.password_hash;
|
|
2204
|
+
const hasOtherLinks = (otherLinks?.count || 0) > 0;
|
|
2205
|
+
if (!hasPassword && !hasOtherLinks) {
|
|
2206
|
+
return false;
|
|
2207
|
+
}
|
|
2208
|
+
await this.db.prepare(`
|
|
2209
|
+
DELETE FROM oauth_accounts WHERE user_id = ? AND provider = ?
|
|
2210
|
+
`).bind(userId, provider).run();
|
|
2211
|
+
return true;
|
|
2212
|
+
}
|
|
2213
|
+
/**
|
|
2214
|
+
* Find a user by email.
|
|
2215
|
+
*/
|
|
2216
|
+
async findUserByEmail(email) {
|
|
2217
|
+
return await this.db.prepare(`
|
|
2218
|
+
SELECT id, email, role, is_active, first_name, last_name
|
|
2219
|
+
FROM users WHERE email = ?
|
|
2220
|
+
`).bind(email.toLowerCase()).first();
|
|
2221
|
+
}
|
|
2222
|
+
/**
|
|
2223
|
+
* Create a new user from an OAuth profile.
|
|
2224
|
+
*/
|
|
2225
|
+
async createUserFromOAuth(profile) {
|
|
2226
|
+
const id = crypto.randomUUID();
|
|
2227
|
+
const now = Date.now();
|
|
2228
|
+
const email = profile.email.toLowerCase();
|
|
2229
|
+
const nameParts = (profile.name || email.split("@")[0] || "User").split(" ");
|
|
2230
|
+
const firstName = nameParts[0] || "User";
|
|
2231
|
+
const lastName = nameParts.slice(1).join(" ") || "";
|
|
2232
|
+
const username = email.split("@")[0] || id.substring(0, 8);
|
|
2233
|
+
const existing = await this.db.prepare(
|
|
2234
|
+
"SELECT id FROM users WHERE username = ?"
|
|
2235
|
+
).bind(username).first();
|
|
2236
|
+
const finalUsername = existing ? `${username}-${id.substring(0, 6)}` : username;
|
|
2237
|
+
await this.db.prepare(`
|
|
2238
|
+
INSERT INTO users (
|
|
2239
|
+
id, email, username, first_name, last_name,
|
|
2240
|
+
password_hash, role, avatar, is_active, created_at, updated_at
|
|
2241
|
+
) VALUES (?, ?, ?, ?, ?, NULL, 'viewer', ?, 1, ?, ?)
|
|
2242
|
+
`).bind(
|
|
2243
|
+
id,
|
|
2244
|
+
email,
|
|
2245
|
+
finalUsername,
|
|
2246
|
+
firstName,
|
|
2247
|
+
lastName,
|
|
2248
|
+
profile.avatar || null,
|
|
2249
|
+
now,
|
|
2250
|
+
now
|
|
2251
|
+
).run();
|
|
2252
|
+
return id;
|
|
2253
|
+
}
|
|
2254
|
+
/**
|
|
2255
|
+
* Generate a cryptographically random state parameter for CSRF protection.
|
|
2256
|
+
*/
|
|
2257
|
+
generateState() {
|
|
2258
|
+
const bytes = new Uint8Array(32);
|
|
2259
|
+
crypto.getRandomValues(bytes);
|
|
2260
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
2261
|
+
}
|
|
2262
|
+
};
|
|
2263
|
+
|
|
2264
|
+
// src/plugins/core-plugins/oauth-providers/index.ts
|
|
2265
|
+
var STATE_COOKIE_NAME = "oauth_state";
|
|
2266
|
+
var STATE_COOKIE_MAX_AGE = 600;
|
|
2267
|
+
function createOAuthProvidersPlugin() {
|
|
2268
|
+
const builder = PluginBuilder.create({
|
|
2269
|
+
name: "oauth-providers",
|
|
2270
|
+
version: "1.0.0-beta.1",
|
|
2271
|
+
description: "OAuth2/OIDC social login with GitHub, Google, and more"
|
|
2272
|
+
});
|
|
2273
|
+
builder.metadata({
|
|
2274
|
+
author: {
|
|
2275
|
+
name: "SonicJS Team",
|
|
2276
|
+
email: "team@sonicjs.com"
|
|
2277
|
+
},
|
|
2278
|
+
license: "MIT",
|
|
2279
|
+
compatibility: "^2.0.0"
|
|
2280
|
+
});
|
|
2281
|
+
function getCallbackUrl(c, provider) {
|
|
2282
|
+
const proto = c.req.header("x-forwarded-proto") || "https";
|
|
2283
|
+
const host = c.req.header("host") || "localhost";
|
|
2284
|
+
return `${proto}://${host}/auth/oauth/${provider}/callback`;
|
|
2285
|
+
}
|
|
2286
|
+
async function loadSettings(db) {
|
|
2287
|
+
const row = await db.prepare(
|
|
2288
|
+
`SELECT settings FROM plugins WHERE id = 'oauth-providers'`
|
|
2289
|
+
).first();
|
|
2290
|
+
if (!row?.settings) return null;
|
|
2291
|
+
try {
|
|
2292
|
+
return JSON.parse(row.settings);
|
|
2293
|
+
} catch {
|
|
2294
|
+
return null;
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
function getProviderCredentials(settings, providerId) {
|
|
2298
|
+
if (!settings?.providers?.[providerId]) return null;
|
|
2299
|
+
const p = settings.providers[providerId];
|
|
2300
|
+
if (!p.enabled || !p.clientId || !p.clientSecret) return null;
|
|
2301
|
+
return { clientId: p.clientId, clientSecret: p.clientSecret };
|
|
2302
|
+
}
|
|
2303
|
+
const oauthAPI = new Hono();
|
|
2304
|
+
oauthAPI.get("/:provider", async (c) => {
|
|
2305
|
+
try {
|
|
2306
|
+
const providerId = c.req.param("provider");
|
|
2307
|
+
const providerConfig = BUILT_IN_PROVIDERS[providerId];
|
|
2308
|
+
if (!providerConfig) {
|
|
2309
|
+
return c.json({ error: `Unknown OAuth provider: ${providerId}` }, 400);
|
|
2310
|
+
}
|
|
2311
|
+
const db = c.env.DB;
|
|
2312
|
+
const settings = await loadSettings(db);
|
|
2313
|
+
const creds = getProviderCredentials(settings, providerId);
|
|
2314
|
+
if (!creds) {
|
|
2315
|
+
return c.json({
|
|
2316
|
+
error: `OAuth provider "${providerId}" is not configured or not enabled`
|
|
2317
|
+
}, 400);
|
|
2318
|
+
}
|
|
2319
|
+
const oauthService = new OAuthService(db);
|
|
2320
|
+
const state = oauthService.generateState();
|
|
2321
|
+
const redirectUri = getCallbackUrl(c, providerId);
|
|
2322
|
+
setCookie(c, STATE_COOKIE_NAME, state, {
|
|
2323
|
+
httpOnly: true,
|
|
2324
|
+
secure: true,
|
|
2325
|
+
sameSite: "Lax",
|
|
2326
|
+
// Lax required for OAuth redirect flow
|
|
2327
|
+
maxAge: STATE_COOKIE_MAX_AGE,
|
|
2328
|
+
path: "/auth/oauth"
|
|
2329
|
+
});
|
|
2330
|
+
const authorizeUrl = oauthService.buildAuthorizeUrl(
|
|
2331
|
+
providerConfig,
|
|
2332
|
+
creds.clientId,
|
|
2333
|
+
redirectUri,
|
|
2334
|
+
state
|
|
2335
|
+
);
|
|
2336
|
+
return c.redirect(authorizeUrl);
|
|
2337
|
+
} catch (error) {
|
|
2338
|
+
console.error("OAuth authorize error:", error);
|
|
2339
|
+
return c.json({ error: "Failed to initiate OAuth flow" }, 500);
|
|
2340
|
+
}
|
|
2341
|
+
});
|
|
2342
|
+
oauthAPI.get("/:provider/callback", async (c) => {
|
|
2343
|
+
try {
|
|
2344
|
+
const providerId = c.req.param("provider");
|
|
2345
|
+
const providerConfig = BUILT_IN_PROVIDERS[providerId];
|
|
2346
|
+
if (!providerConfig) {
|
|
2347
|
+
return c.redirect("/auth/login?error=Unknown OAuth provider");
|
|
2348
|
+
}
|
|
2349
|
+
const stateParam = c.req.query("state");
|
|
2350
|
+
const stateCookie = getCookie(c, STATE_COOKIE_NAME);
|
|
2351
|
+
if (!stateParam || !stateCookie || stateParam !== stateCookie) {
|
|
2352
|
+
return c.redirect("/auth/login?error=Invalid OAuth state. Please try again.");
|
|
2353
|
+
}
|
|
2354
|
+
setCookie(c, STATE_COOKIE_NAME, "", {
|
|
2355
|
+
httpOnly: true,
|
|
2356
|
+
secure: true,
|
|
2357
|
+
sameSite: "Lax",
|
|
2358
|
+
maxAge: 0,
|
|
2359
|
+
path: "/auth/oauth"
|
|
2360
|
+
});
|
|
2361
|
+
const errorParam = c.req.query("error");
|
|
2362
|
+
if (errorParam) {
|
|
2363
|
+
const errorDesc = c.req.query("error_description") || errorParam;
|
|
2364
|
+
return c.redirect(`/auth/login?error=${encodeURIComponent(errorDesc)}`);
|
|
2365
|
+
}
|
|
2366
|
+
const code = c.req.query("code");
|
|
2367
|
+
if (!code) {
|
|
2368
|
+
return c.redirect("/auth/login?error=No authorization code received");
|
|
2369
|
+
}
|
|
2370
|
+
const db = c.env.DB;
|
|
2371
|
+
const settings = await loadSettings(db);
|
|
2372
|
+
const creds = getProviderCredentials(settings, providerId);
|
|
2373
|
+
if (!creds) {
|
|
2374
|
+
return c.redirect("/auth/login?error=OAuth provider not configured");
|
|
2375
|
+
}
|
|
2376
|
+
const oauthService = new OAuthService(db);
|
|
2377
|
+
const redirectUri = getCallbackUrl(c, providerId);
|
|
2378
|
+
const tokens = await oauthService.exchangeCode(
|
|
2379
|
+
providerConfig,
|
|
2380
|
+
creds.clientId,
|
|
2381
|
+
creds.clientSecret,
|
|
2382
|
+
code,
|
|
2383
|
+
redirectUri
|
|
2384
|
+
);
|
|
2385
|
+
const profile = await oauthService.fetchUserProfile(providerConfig, tokens.access_token);
|
|
2386
|
+
if (!profile.email) {
|
|
2387
|
+
return c.redirect("/auth/login?error=Could not retrieve email from OAuth provider. Please ensure your email is public or grant email permission.");
|
|
2388
|
+
}
|
|
2389
|
+
const tokenExpiresAt = tokens.expires_in ? Date.now() + tokens.expires_in * 1e3 : null;
|
|
2390
|
+
const existingOAuth = await oauthService.findOAuthAccount(providerId, profile.providerAccountId);
|
|
2391
|
+
if (existingOAuth) {
|
|
2392
|
+
await oauthService.updateOAuthTokens(
|
|
2393
|
+
existingOAuth.id,
|
|
2394
|
+
tokens.access_token,
|
|
2395
|
+
tokens.refresh_token,
|
|
2396
|
+
tokenExpiresAt ?? void 0
|
|
2397
|
+
);
|
|
2398
|
+
const user = await db.prepare(
|
|
2399
|
+
"SELECT id, email, role, is_active FROM users WHERE id = ?"
|
|
2400
|
+
).bind(existingOAuth.user_id).first();
|
|
2401
|
+
if (!user || !user.is_active) {
|
|
2402
|
+
return c.redirect("/auth/login?error=Account is deactivated");
|
|
2403
|
+
}
|
|
2404
|
+
const jwt2 = await AuthManager.generateToken(
|
|
2405
|
+
user.id,
|
|
2406
|
+
user.email,
|
|
2407
|
+
user.role,
|
|
2408
|
+
c.env.JWT_SECRET
|
|
2409
|
+
);
|
|
2410
|
+
AuthManager.setAuthCookie(c, jwt2, { sameSite: "Lax" });
|
|
2411
|
+
return c.redirect("/admin");
|
|
2412
|
+
}
|
|
2413
|
+
const existingUser = await oauthService.findUserByEmail(profile.email);
|
|
2414
|
+
if (existingUser) {
|
|
2415
|
+
if (!existingUser.is_active) {
|
|
2416
|
+
return c.redirect("/auth/login?error=Account is deactivated");
|
|
2417
|
+
}
|
|
2418
|
+
await oauthService.createOAuthAccount({
|
|
2419
|
+
userId: existingUser.id,
|
|
2420
|
+
provider: providerId,
|
|
2421
|
+
providerAccountId: profile.providerAccountId,
|
|
2422
|
+
accessToken: tokens.access_token,
|
|
2423
|
+
refreshToken: tokens.refresh_token,
|
|
2424
|
+
tokenExpiresAt: tokenExpiresAt ?? void 0,
|
|
2425
|
+
profileData: JSON.stringify(profile)
|
|
2426
|
+
});
|
|
2427
|
+
const jwt2 = await AuthManager.generateToken(
|
|
2428
|
+
existingUser.id,
|
|
2429
|
+
existingUser.email,
|
|
2430
|
+
existingUser.role,
|
|
2431
|
+
c.env.JWT_SECRET
|
|
2432
|
+
);
|
|
2433
|
+
AuthManager.setAuthCookie(c, jwt2, { sameSite: "Lax" });
|
|
2434
|
+
return c.redirect("/admin");
|
|
2435
|
+
}
|
|
2436
|
+
const newUserId = await oauthService.createUserFromOAuth(profile);
|
|
2437
|
+
await oauthService.createOAuthAccount({
|
|
2438
|
+
userId: newUserId,
|
|
2439
|
+
provider: providerId,
|
|
2440
|
+
providerAccountId: profile.providerAccountId,
|
|
2441
|
+
accessToken: tokens.access_token,
|
|
2442
|
+
refreshToken: tokens.refresh_token,
|
|
2443
|
+
tokenExpiresAt: tokenExpiresAt ?? void 0,
|
|
2444
|
+
profileData: JSON.stringify(profile)
|
|
2445
|
+
});
|
|
2446
|
+
const jwt = await AuthManager.generateToken(
|
|
2447
|
+
newUserId,
|
|
2448
|
+
profile.email.toLowerCase(),
|
|
2449
|
+
"viewer",
|
|
2450
|
+
c.env.JWT_SECRET
|
|
2451
|
+
);
|
|
2452
|
+
AuthManager.setAuthCookie(c, jwt, { sameSite: "Lax" });
|
|
2453
|
+
return c.redirect("/admin");
|
|
2454
|
+
} catch (error) {
|
|
2455
|
+
console.error("OAuth callback error:", error);
|
|
2456
|
+
const message = error instanceof Error ? error.message : "OAuth authentication failed";
|
|
2457
|
+
return c.redirect(`/auth/login?error=${encodeURIComponent(message)}`);
|
|
2458
|
+
}
|
|
2459
|
+
});
|
|
2460
|
+
oauthAPI.post("/link", async (c) => {
|
|
2461
|
+
try {
|
|
2462
|
+
const user = c.get("user");
|
|
2463
|
+
if (!user) {
|
|
2464
|
+
return c.json({ error: "Authentication required" }, 401);
|
|
2465
|
+
}
|
|
2466
|
+
const body = await c.req.json();
|
|
2467
|
+
const { provider } = body;
|
|
2468
|
+
if (!provider || !BUILT_IN_PROVIDERS[provider]) {
|
|
2469
|
+
return c.json({ error: "Invalid provider" }, 400);
|
|
2470
|
+
}
|
|
2471
|
+
const db = c.env.DB;
|
|
2472
|
+
const settings = await loadSettings(db);
|
|
2473
|
+
const creds = getProviderCredentials(settings, provider);
|
|
2474
|
+
if (!creds) {
|
|
2475
|
+
return c.json({ error: `OAuth provider "${provider}" is not configured` }, 400);
|
|
2476
|
+
}
|
|
2477
|
+
const oauthService = new OAuthService(db);
|
|
2478
|
+
const state = oauthService.generateState();
|
|
2479
|
+
const redirectUri = getCallbackUrl(c, provider);
|
|
2480
|
+
setCookie(c, STATE_COOKIE_NAME, state, {
|
|
2481
|
+
httpOnly: true,
|
|
2482
|
+
secure: true,
|
|
2483
|
+
sameSite: "Lax",
|
|
2484
|
+
maxAge: STATE_COOKIE_MAX_AGE,
|
|
2485
|
+
path: "/auth/oauth"
|
|
2486
|
+
});
|
|
2487
|
+
const authorizeUrl = oauthService.buildAuthorizeUrl(
|
|
2488
|
+
BUILT_IN_PROVIDERS[provider],
|
|
2489
|
+
creds.clientId,
|
|
2490
|
+
redirectUri,
|
|
2491
|
+
state
|
|
2492
|
+
);
|
|
2493
|
+
return c.json({ redirectUrl: authorizeUrl });
|
|
2494
|
+
} catch (error) {
|
|
2495
|
+
console.error("OAuth link error:", error);
|
|
2496
|
+
return c.json({ error: "Failed to initiate account linking" }, 500);
|
|
2497
|
+
}
|
|
2498
|
+
});
|
|
2499
|
+
oauthAPI.post("/unlink", async (c) => {
|
|
2500
|
+
try {
|
|
2501
|
+
const user = c.get("user");
|
|
2502
|
+
if (!user) {
|
|
2503
|
+
return c.json({ error: "Authentication required" }, 401);
|
|
2504
|
+
}
|
|
2505
|
+
const body = await c.req.json();
|
|
2506
|
+
const { provider } = body;
|
|
2507
|
+
if (!provider) {
|
|
2508
|
+
return c.json({ error: "Provider is required" }, 400);
|
|
2509
|
+
}
|
|
2510
|
+
const db = c.env.DB;
|
|
2511
|
+
const oauthService = new OAuthService(db);
|
|
2512
|
+
const success = await oauthService.unlinkOAuthAccount(user.userId, provider);
|
|
2513
|
+
if (!success) {
|
|
2514
|
+
return c.json({
|
|
2515
|
+
error: "Cannot unlink the only authentication method. Set a password first."
|
|
2516
|
+
}, 400);
|
|
2517
|
+
}
|
|
2518
|
+
return c.json({ success: true, message: `${provider} account unlinked` });
|
|
2519
|
+
} catch (error) {
|
|
2520
|
+
console.error("OAuth unlink error:", error);
|
|
2521
|
+
return c.json({ error: "Failed to unlink account" }, 500);
|
|
2522
|
+
}
|
|
2523
|
+
});
|
|
2524
|
+
oauthAPI.get("/accounts", async (c) => {
|
|
2525
|
+
try {
|
|
2526
|
+
const user = c.get("user");
|
|
2527
|
+
if (!user) {
|
|
2528
|
+
return c.json({ error: "Authentication required" }, 401);
|
|
2529
|
+
}
|
|
2530
|
+
const db = c.env.DB;
|
|
2531
|
+
const oauthService = new OAuthService(db);
|
|
2532
|
+
const accounts = await oauthService.findUserOAuthAccounts(user.userId);
|
|
2533
|
+
return c.json({
|
|
2534
|
+
accounts: accounts.map((a) => ({
|
|
2535
|
+
provider: a.provider,
|
|
2536
|
+
providerAccountId: a.provider_account_id,
|
|
2537
|
+
linkedAt: a.created_at
|
|
2538
|
+
}))
|
|
2539
|
+
});
|
|
2540
|
+
} catch (error) {
|
|
2541
|
+
console.error("OAuth accounts error:", error);
|
|
2542
|
+
return c.json({ error: "Failed to fetch linked accounts" }, 500);
|
|
2543
|
+
}
|
|
2544
|
+
});
|
|
2545
|
+
builder.addRoute("/auth/oauth", oauthAPI, {
|
|
2546
|
+
description: "OAuth2 social login endpoints",
|
|
2547
|
+
requiresAuth: false,
|
|
2548
|
+
priority: 100
|
|
2549
|
+
});
|
|
2550
|
+
builder.addMenuItem("OAuth Providers", "/admin/plugins/oauth-providers", {
|
|
2551
|
+
icon: "shield",
|
|
2552
|
+
order: 86,
|
|
2553
|
+
permissions: ["oauth:manage"]
|
|
2554
|
+
});
|
|
2555
|
+
builder.lifecycle({
|
|
2556
|
+
activate: async () => {
|
|
2557
|
+
console.info("\u2705 OAuth Providers plugin activated");
|
|
2558
|
+
},
|
|
2559
|
+
deactivate: async () => {
|
|
2560
|
+
console.info("\u274C OAuth Providers plugin deactivated");
|
|
2561
|
+
}
|
|
2562
|
+
});
|
|
2563
|
+
return builder.build();
|
|
2564
|
+
}
|
|
2565
|
+
var oauthProvidersPlugin = createOAuthProvidersPlugin();
|
|
2566
|
+
|
|
2004
2567
|
// src/plugins/core-plugins/ai-search-plugin/services/embedding.service.ts
|
|
2005
2568
|
var EmbeddingService = class {
|
|
2006
2569
|
constructor(ai) {
|
|
@@ -4131,74 +4694,1704 @@ function renderMagicLinkEmail(magicLink, expiryMinutes) {
|
|
|
4131
4694
|
}
|
|
4132
4695
|
createMagicLinkAuthPlugin();
|
|
4133
4696
|
|
|
4134
|
-
// src/plugins/
|
|
4135
|
-
var
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
kvEnabled: true,
|
|
4141
|
-
memoryEnabled: true,
|
|
4142
|
-
namespace: "content",
|
|
4143
|
-
invalidateOn: ["content.update", "content.delete", "content.publish"],
|
|
4144
|
-
version: "v1"
|
|
4145
|
-
},
|
|
4146
|
-
// User data (medium read, medium write)
|
|
4147
|
-
user: {
|
|
4148
|
-
ttl: 900,
|
|
4149
|
-
// 15 minutes
|
|
4150
|
-
kvEnabled: true,
|
|
4151
|
-
memoryEnabled: true,
|
|
4152
|
-
namespace: "user",
|
|
4153
|
-
invalidateOn: ["user.update", "user.delete", "auth.login"],
|
|
4154
|
-
version: "v1"
|
|
4155
|
-
},
|
|
4156
|
-
// Configuration (high read, very low write)
|
|
4157
|
-
config: {
|
|
4158
|
-
ttl: 7200,
|
|
4159
|
-
// 2 hours
|
|
4160
|
-
kvEnabled: true,
|
|
4161
|
-
memoryEnabled: true,
|
|
4162
|
-
namespace: "config",
|
|
4163
|
-
invalidateOn: ["config.update", "plugin.activate", "plugin.deactivate"],
|
|
4164
|
-
version: "v1"
|
|
4165
|
-
},
|
|
4166
|
-
// Media metadata (high read, low write)
|
|
4167
|
-
media: {
|
|
4168
|
-
ttl: 3600,
|
|
4169
|
-
// 1 hour
|
|
4170
|
-
kvEnabled: true,
|
|
4171
|
-
memoryEnabled: true,
|
|
4172
|
-
namespace: "media",
|
|
4173
|
-
invalidateOn: ["media.upload", "media.delete", "media.update"],
|
|
4174
|
-
version: "v1"
|
|
4175
|
-
},
|
|
4176
|
-
// API responses (very high read, low write)
|
|
4177
|
-
api: {
|
|
4178
|
-
ttl: 300,
|
|
4179
|
-
// 5 minutes
|
|
4180
|
-
kvEnabled: true,
|
|
4181
|
-
memoryEnabled: true,
|
|
4182
|
-
namespace: "api",
|
|
4183
|
-
invalidateOn: ["content.update", "content.publish"],
|
|
4184
|
-
version: "v1"
|
|
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
|
|
4185
4703
|
},
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
namespace: "session",
|
|
4194
|
-
invalidateOn: ["auth.logout"],
|
|
4195
|
-
version: "v1"
|
|
4704
|
+
bruteForce: {
|
|
4705
|
+
enabled: true,
|
|
4706
|
+
maxFailedAttemptsPerIP: 10,
|
|
4707
|
+
maxFailedAttemptsPerEmail: 5,
|
|
4708
|
+
windowMinutes: 15,
|
|
4709
|
+
lockoutDurationMinutes: 30,
|
|
4710
|
+
alertThreshold: 20
|
|
4196
4711
|
},
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
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
|
+
|
|
6327
|
+
// src/plugins/cache/services/cache-config.ts
|
|
6328
|
+
var CACHE_CONFIGS = {
|
|
6329
|
+
// Content (high read, low write)
|
|
6330
|
+
content: {
|
|
6331
|
+
ttl: 3600,
|
|
6332
|
+
// 1 hour
|
|
6333
|
+
kvEnabled: true,
|
|
6334
|
+
memoryEnabled: true,
|
|
6335
|
+
namespace: "content",
|
|
6336
|
+
invalidateOn: ["content.update", "content.delete", "content.publish"],
|
|
6337
|
+
version: "v1"
|
|
6338
|
+
},
|
|
6339
|
+
// User data (medium read, medium write)
|
|
6340
|
+
user: {
|
|
6341
|
+
ttl: 900,
|
|
6342
|
+
// 15 minutes
|
|
6343
|
+
kvEnabled: true,
|
|
6344
|
+
memoryEnabled: true,
|
|
6345
|
+
namespace: "user",
|
|
6346
|
+
invalidateOn: ["user.update", "user.delete", "auth.login"],
|
|
6347
|
+
version: "v1"
|
|
6348
|
+
},
|
|
6349
|
+
// Configuration (high read, very low write)
|
|
6350
|
+
config: {
|
|
6351
|
+
ttl: 7200,
|
|
6352
|
+
// 2 hours
|
|
6353
|
+
kvEnabled: true,
|
|
6354
|
+
memoryEnabled: true,
|
|
6355
|
+
namespace: "config",
|
|
6356
|
+
invalidateOn: ["config.update", "plugin.activate", "plugin.deactivate"],
|
|
6357
|
+
version: "v1"
|
|
6358
|
+
},
|
|
6359
|
+
// Media metadata (high read, low write)
|
|
6360
|
+
media: {
|
|
6361
|
+
ttl: 3600,
|
|
6362
|
+
// 1 hour
|
|
6363
|
+
kvEnabled: true,
|
|
6364
|
+
memoryEnabled: true,
|
|
6365
|
+
namespace: "media",
|
|
6366
|
+
invalidateOn: ["media.upload", "media.delete", "media.update"],
|
|
6367
|
+
version: "v1"
|
|
6368
|
+
},
|
|
6369
|
+
// API responses (very high read, low write)
|
|
6370
|
+
api: {
|
|
6371
|
+
ttl: 300,
|
|
6372
|
+
// 5 minutes
|
|
6373
|
+
kvEnabled: true,
|
|
6374
|
+
memoryEnabled: true,
|
|
6375
|
+
namespace: "api",
|
|
6376
|
+
invalidateOn: ["content.update", "content.publish"],
|
|
6377
|
+
version: "v1"
|
|
6378
|
+
},
|
|
6379
|
+
// Session data (very high read, medium write)
|
|
6380
|
+
session: {
|
|
6381
|
+
ttl: 1800,
|
|
6382
|
+
// 30 minutes
|
|
6383
|
+
kvEnabled: false,
|
|
6384
|
+
// Only in-memory for sessions
|
|
6385
|
+
memoryEnabled: true,
|
|
6386
|
+
namespace: "session",
|
|
6387
|
+
invalidateOn: ["auth.logout"],
|
|
6388
|
+
version: "v1"
|
|
6389
|
+
},
|
|
6390
|
+
// Plugin data
|
|
6391
|
+
plugin: {
|
|
6392
|
+
ttl: 3600,
|
|
6393
|
+
// 1 hour
|
|
6394
|
+
kvEnabled: true,
|
|
4202
6395
|
memoryEnabled: true,
|
|
4203
6396
|
namespace: "plugin",
|
|
4204
6397
|
invalidateOn: ["plugin.activate", "plugin.deactivate", "plugin.update"],
|
|
@@ -6003,6 +8196,7 @@ function createSonicJSApp(config = {}) {
|
|
|
6003
8196
|
app2.use("*", middleware);
|
|
6004
8197
|
}
|
|
6005
8198
|
}
|
|
8199
|
+
app2.use("/admin/*", pluginMenuMiddleware());
|
|
6006
8200
|
app2.route("/api", api_default);
|
|
6007
8201
|
app2.route("/api/media", api_media_default);
|
|
6008
8202
|
app2.route("/api/system", api_system_default);
|
|
@@ -6018,12 +8212,28 @@ function createSonicJSApp(config = {}) {
|
|
|
6018
8212
|
app2.route("/admin/seed-data", createSeedDataAdminRoutes());
|
|
6019
8213
|
app2.route("/admin/content", admin_content_default);
|
|
6020
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
|
+
}
|
|
6021
8221
|
if (aiSearchPlugin.routes && aiSearchPlugin.routes.length > 0) {
|
|
6022
8222
|
for (const route of aiSearchPlugin.routes) {
|
|
6023
8223
|
app2.route(route.path, route.handler);
|
|
6024
8224
|
}
|
|
6025
8225
|
}
|
|
6026
8226
|
app2.route("/admin/cache", cache_default.getRoutes());
|
|
8227
|
+
if (oauthProvidersPlugin.routes && oauthProvidersPlugin.routes.length > 0) {
|
|
8228
|
+
for (const route of oauthProvidersPlugin.routes) {
|
|
8229
|
+
app2.route(route.path, route.handler);
|
|
8230
|
+
}
|
|
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
|
+
}
|
|
6027
8237
|
if (otpLoginPlugin.routes && otpLoginPlugin.routes.length > 0) {
|
|
6028
8238
|
for (const route of otpLoginPlugin.routes) {
|
|
6029
8239
|
app2.route(route.path, route.handler);
|
|
@@ -6119,6 +8329,6 @@ function createDb(d1) {
|
|
|
6119
8329
|
// src/index.ts
|
|
6120
8330
|
var VERSION = package_default.version;
|
|
6121
8331
|
|
|
6122
|
-
export { VERSION, createDb, createSonicJSApp, setupCoreMiddleware, setupCoreRoutes };
|
|
8332
|
+
export { BUILT_IN_PROVIDERS, OAuthService, VERSION, createDb, createOAuthProvidersPlugin, createSonicJSApp, oauthProvidersPlugin, setupCoreMiddleware, setupCoreRoutes };
|
|
6123
8333
|
//# sourceMappingURL=index.js.map
|
|
6124
8334
|
//# sourceMappingURL=index.js.map
|