@jmruthers/pace-core 0.5.136 → 0.5.137
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/{DataTable-CYOHOX3O.js → DataTable-6M4L6BI2.js} +10 -9
- package/dist/{EventLogo-801uofbR.d.ts → EventLogo-rFL_kRjk.d.ts} +73 -1
- package/dist/{UnifiedAuthProvider-5E5TUNMS.js → UnifiedAuthProvider-XIQQ7LVU.js} +4 -5
- package/dist/{chunk-YLKIDTUK.js → chunk-22WKWKRX.js} +4 -4
- package/dist/{chunk-TVYPTYOY.js → chunk-4C7EXCAR.js} +60 -24
- package/dist/chunk-4C7EXCAR.js.map +1 -0
- package/dist/{chunk-2TWNJ46Y.js → chunk-6LAAY47Q.js} +2 -2
- package/dist/{chunk-444EZN6N.js → chunk-7QCC6MCP.js} +88 -1
- package/dist/chunk-7QCC6MCP.js.map +1 -0
- package/dist/{chunk-FHWWBIHA.js → chunk-BCIBECNB.js} +5 -5
- package/dist/chunk-BJPBT3CU.js +21 -0
- package/dist/chunk-BJPBT3CU.js.map +1 -0
- package/dist/{chunk-L6PGMCMD.js → chunk-BLCXZEYF.js} +3 -3
- package/dist/{chunk-HJGGOMQ6.js → chunk-HAWZXGR2.js} +147 -103
- package/dist/chunk-HAWZXGR2.js.map +1 -0
- package/dist/{chunk-XARJS7CD.js → chunk-INQLMHPF.js} +2 -2
- package/dist/chunk-JISYG63F.js +70 -0
- package/dist/chunk-JISYG63F.js.map +1 -0
- package/dist/{chunk-NOHEVYVX.js → chunk-KYRHUBIU.js} +417 -319
- package/dist/chunk-KYRHUBIU.js.map +1 -0
- package/dist/{chunk-SL2YQDR6.js → chunk-MA6EPSGZ.js} +2 -2
- package/dist/{chunk-5DPZ5EAT.js → chunk-OWAG3GSU.js} +1 -3
- package/dist/{chunk-LTV3XIJJ.js → chunk-T6JN6LH6.js} +4 -4
- package/dist/{chunk-4MT5BGGL.js → chunk-YCWDTTUK.js} +4 -6
- package/dist/{chunk-4MT5BGGL.js.map → chunk-YCWDTTUK.js.map} +1 -1
- package/dist/components.d.ts +1 -1
- package/dist/components.js +12 -11
- package/dist/components.js.map +1 -1
- package/dist/hooks.js +8 -9
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +15 -14
- package/dist/index.js.map +1 -1
- package/dist/providers.js +3 -4
- package/dist/rbac/index.js +8 -9
- package/dist/schema-DTDZQe2u.d.ts +28 -0
- package/dist/types.d.ts +152 -3
- package/dist/types.js +51 -16
- package/dist/types.js.map +1 -1
- package/dist/utils.d.ts +89 -4
- package/dist/utils.js +214 -96
- package/dist/utils.js.map +1 -1
- package/dist/validation.d.ts +1 -343
- package/dist/validation.js +3 -100
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +1 -1
- package/docs/api/classes/MissingUserContextError.md +1 -1
- package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
- package/docs/api/classes/PermissionDeniedError.md +1 -1
- package/docs/api/classes/PublicErrorBoundary.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +1 -1
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +1 -1
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +1 -1
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/BadgeProps.md +27 -0
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CardProps.md +1 -1
- package/docs/api/interfaces/ColorPalette.md +1 -1
- package/docs/api/interfaces/ColorShade.md +1 -1
- package/docs/api/interfaces/DataAccessRecord.md +1 -1
- package/docs/api/interfaces/DataRecord.md +1 -1
- package/docs/api/interfaces/DataTableAction.md +1 -1
- package/docs/api/interfaces/DataTableColumn.md +1 -1
- package/docs/api/interfaces/DataTableProps.md +1 -1
- package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/EventAppRoleData.md +1 -1
- package/docs/api/interfaces/EventLogoProps.md +1 -1
- package/docs/api/interfaces/ExportColumn.md +1 -1
- package/docs/api/interfaces/ExportOptions.md +1 -1
- package/docs/api/interfaces/FileDisplayProps.md +1 -1
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +1 -1
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
- package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
- package/docs/api/interfaces/PageAccessRecord.md +1 -1
- package/docs/api/interfaces/PagePermissionContextType.md +1 -1
- package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
- package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RoleManagementResult.md +1 -1
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/SwitchProps.md +1 -1
- package/docs/api/interfaces/ToastActionElement.md +1 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +1 -1
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +79 -10
- package/docs/architecture/README.md +0 -1
- package/docs/styles/README.md +0 -2
- package/examples/RBAC/CompleteRBACExample.tsx +324 -0
- package/examples/RBAC/EventBasedApp.tsx +239 -0
- package/examples/RBAC/PermissionExample.tsx +151 -0
- package/examples/RBAC/index.ts +13 -0
- package/examples/public-pages/CorrectPublicPageImplementation.tsx +301 -0
- package/examples/public-pages/PublicEventPage.tsx +274 -0
- package/examples/public-pages/PublicPageApp.tsx +308 -0
- package/examples/public-pages/PublicPageUsageExample.tsx +216 -0
- package/examples/public-pages/index.ts +14 -0
- package/package.json +1 -10
- package/src/__tests__/TEST_STANDARD.md +92 -0
- package/src/components/Badge/Badge.test.tsx +314 -0
- package/src/components/Badge/Badge.tsx +304 -0
- package/src/components/Badge/index.ts +3 -0
- package/src/components/DataTable/__tests__/DataTableCore.test-setup.ts +217 -0
- package/src/components/DataTable/__tests__/styles.test.ts +1 -1
- package/src/components/DataTable/components/ColumnFilter.tsx +8 -4
- package/src/components/DataTable/components/DataTableBody.tsx +461 -0
- package/src/components/DataTable/components/DraggableColumnHeader.tsx +144 -0
- package/src/components/DataTable/components/FilterRow.tsx +9 -3
- package/src/components/DataTable/components/PaginationControls.tsx +1 -0
- package/src/components/DataTable/components/VirtualizedDataTable.tsx +513 -0
- package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +14 -68
- package/src/components/DataTable/components/__tests__/ColumnFilter.test.tsx +62 -0
- package/src/components/DataTable/components/__tests__/FilterRow.test.tsx +43 -0
- package/src/components/DataTable/core/ActionManager.ts +235 -0
- package/src/components/DataTable/core/ColumnManager.ts +205 -0
- package/src/components/DataTable/core/DataManager.ts +188 -0
- package/src/components/DataTable/core/DataTableContext.tsx +181 -0
- package/src/components/DataTable/core/LocalDataAdapter.ts +273 -0
- package/src/components/DataTable/core/PluginRegistry.ts +229 -0
- package/src/components/DataTable/core/StateManager.ts +311 -0
- package/src/components/DataTable/core/interfaces.ts +338 -0
- package/src/components/DataTable/styles.ts +27 -6
- package/src/components/DataTable/utils/__tests__/columnUtils.test.ts +94 -0
- package/src/components/DataTable/utils/columnUtils.ts +40 -0
- package/src/components/DataTable/utils/debugTools.ts +609 -0
- package/src/components/DataTable/utils/index.ts +1 -0
- package/src/components/Dialog/README.md +804 -0
- package/src/components/Dialog/utils/__tests__/safeHtml.unit.test.ts +611 -0
- package/src/components/Dialog/utils/safeHtml.ts +185 -0
- package/src/components/Footer/Footer.test.tsx +1 -1
- package/src/components/Form/Form.test.tsx +1 -1
- package/src/components/Form/FormErrorSummary.tsx +113 -0
- package/src/components/Form/FormFieldset.tsx +127 -0
- package/src/components/Form/FormLiveRegion.tsx +198 -0
- package/src/components/LoginForm/LoginForm.test.tsx +1 -1
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +76 -10
- package/src/components/PaceLoginPage/PaceLoginPage.tsx +1 -1
- package/src/components/PasswordReset/PasswordResetForm.test.tsx +597 -0
- package/src/components/PasswordReset/PasswordResetForm.tsx +201 -0
- package/src/components/PublicLayout/PublicPageDebugger.tsx +104 -0
- package/src/components/PublicLayout/PublicPageDiagnostic.tsx +162 -0
- package/src/components/PublicLayout/__tests__/PublicPageFooter.test.tsx +1 -1
- package/src/components/Select/Select.test.tsx +1 -1
- package/src/components/Select/Select.tsx +20 -8
- package/src/components/Table/__tests__/Table.test.tsx +1 -1
- package/src/components/index.ts +3 -0
- package/src/hooks/__tests__/useFileUrl.unit.test.ts +83 -85
- package/src/index.ts +4 -0
- package/src/styles/core.css +3 -0
- package/src/utils/appConfig.ts +47 -0
- package/src/utils/appIdResolver.test.ts +499 -0
- package/src/utils/appIdResolver.ts +130 -0
- package/src/utils/appNameResolver.simple.test.ts +212 -0
- package/src/utils/appNameResolver.test.ts +121 -0
- package/src/utils/appNameResolver.ts +191 -0
- package/src/utils/audit.ts +127 -0
- package/src/utils/auth-utils.ts +96 -0
- package/src/utils/bundleAnalysis.ts +129 -0
- package/src/utils/cn.ts +7 -0
- package/src/utils/debugLogger.ts +67 -0
- package/src/utils/deviceFingerprint.ts +215 -0
- package/src/utils/dynamicUtils.ts +105 -0
- package/src/utils/file-reference.test.ts +788 -0
- package/src/utils/file-reference.ts +519 -0
- package/src/utils/formatDate.test.ts +237 -0
- package/src/utils/formatting.ts +133 -0
- package/src/utils/index.ts +7 -0
- package/src/utils/lazyLoad.tsx +44 -0
- package/src/utils/logger.ts +179 -0
- package/src/utils/organisationContext.test.ts +322 -0
- package/src/utils/organisationContext.ts +153 -0
- package/src/utils/performanceBenchmark.ts +64 -0
- package/src/utils/performanceBudgets.ts +110 -0
- package/src/utils/permissionTypes.ts +37 -0
- package/src/utils/permissionUtils.test.ts +393 -0
- package/src/utils/permissionUtils.ts +34 -0
- package/src/utils/sanitization.ts +264 -0
- package/src/utils/schemaUtils.ts +37 -0
- package/src/utils/secureDataAccess.test.ts +711 -0
- package/src/utils/secureDataAccess.ts +377 -0
- package/src/utils/secureErrors.ts +79 -0
- package/src/utils/secureStorage.ts +244 -0
- package/src/utils/security.ts +156 -0
- package/src/utils/securityMonitor.ts +45 -0
- package/src/utils/sessionTracking.ts +126 -0
- package/src/utils/validation.ts +111 -0
- package/src/utils/validationUtils.ts +120 -0
- package/src/validation/index.ts +2 -2
- package/dist/chunk-444EZN6N.js.map +0 -1
- package/dist/chunk-APIBCTL2.js +0 -670
- package/dist/chunk-APIBCTL2.js.map +0 -1
- package/dist/chunk-HJGGOMQ6.js.map +0 -1
- package/dist/chunk-K2WWTH7O.js +0 -94
- package/dist/chunk-K2WWTH7O.js.map +0 -1
- package/dist/chunk-LMC26NLJ.js +0 -84
- package/dist/chunk-LMC26NLJ.js.map +0 -1
- package/dist/chunk-NOHEVYVX.js.map +0 -1
- package/dist/chunk-TVYPTYOY.js.map +0 -1
- package/dist/validation-8npbysjg.d.ts +0 -177
- /package/dist/{DataTable-CYOHOX3O.js.map → DataTable-6M4L6BI2.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-5E5TUNMS.js.map → UnifiedAuthProvider-XIQQ7LVU.js.map} +0 -0
- /package/dist/{chunk-YLKIDTUK.js.map → chunk-22WKWKRX.js.map} +0 -0
- /package/dist/{chunk-2TWNJ46Y.js.map → chunk-6LAAY47Q.js.map} +0 -0
- /package/dist/{chunk-FHWWBIHA.js.map → chunk-BCIBECNB.js.map} +0 -0
- /package/dist/{chunk-L6PGMCMD.js.map → chunk-BLCXZEYF.js.map} +0 -0
- /package/dist/{chunk-XARJS7CD.js.map → chunk-INQLMHPF.js.map} +0 -0
- /package/dist/{chunk-SL2YQDR6.js.map → chunk-MA6EPSGZ.js.map} +0 -0
- /package/dist/{chunk-5DPZ5EAT.js.map → chunk-OWAG3GSU.js.map} +0 -0
- /package/dist/{chunk-LTV3XIJJ.js.map → chunk-T6JN6LH6.js.map} +0 -0
- /package/examples/{components → components 2}/DataTable/HierarchicalActionsExample.tsx +0 -0
- /package/examples/{components → components 2}/DataTable/HierarchicalExample.tsx +0 -0
- /package/examples/{components → components 2}/DataTable/InitialPageSizeExample.tsx +0 -0
- /package/examples/{components → components 2}/DataTable/PerformanceExample.tsx +0 -0
- /package/examples/{components → components 2}/DataTable/index.ts +0 -0
- /package/examples/{components → components 2}/Dialog/BasicHtmlTest.tsx +0 -0
- /package/examples/{components → components 2}/Dialog/DebugHtmlExample.tsx +0 -0
- /package/examples/{components → components 2}/Dialog/HtmlDialogExample.tsx +0 -0
- /package/examples/{components → components 2}/Dialog/ScrollableDialogExample.tsx +0 -0
- /package/examples/{components → components 2}/Dialog/SimpleHtmlTest.tsx +0 -0
- /package/examples/{components → components 2}/Dialog/SmartDialogExample.tsx +0 -0
- /package/examples/{components → components 2}/Dialog/index.ts +0 -0
- /package/examples/{components → components 2}/index.ts +0 -0
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Permission Utils Tests
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Utils/PermissionUtils
|
|
5
|
+
* @since 1.0.0
|
|
6
|
+
*
|
|
7
|
+
* Comprehensive tests for permission utility functions covering all critical functionality.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it, expect } from 'vitest';
|
|
11
|
+
import {
|
|
12
|
+
transformPermissionMapToBoolean,
|
|
13
|
+
hasPermission,
|
|
14
|
+
hasAnyPermission,
|
|
15
|
+
hasAllPermissions
|
|
16
|
+
} from './permissionUtils';
|
|
17
|
+
|
|
18
|
+
describe('Permission Utils', () => {
|
|
19
|
+
describe('transformPermissionMapToBoolean', () => {
|
|
20
|
+
it('transforms string values correctly', () => {
|
|
21
|
+
const permissions = {
|
|
22
|
+
'read:users': 'true',
|
|
23
|
+
'write:users': 'false',
|
|
24
|
+
'delete:users': '',
|
|
25
|
+
'manage:users': 'yes',
|
|
26
|
+
'view:users': 'no'
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const result = transformPermissionMapToBoolean(permissions);
|
|
30
|
+
|
|
31
|
+
expect(result).toEqual({
|
|
32
|
+
'read:users': true,
|
|
33
|
+
'write:users': false,
|
|
34
|
+
'delete:users': false,
|
|
35
|
+
'manage:users': true,
|
|
36
|
+
'view:users': true
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('transforms boolean values correctly', () => {
|
|
41
|
+
const permissions = {
|
|
42
|
+
'read:users': true,
|
|
43
|
+
'write:users': false,
|
|
44
|
+
'delete:users': true,
|
|
45
|
+
'manage:users': false
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const result = transformPermissionMapToBoolean(permissions);
|
|
49
|
+
|
|
50
|
+
expect(result).toEqual({
|
|
51
|
+
'read:users': true,
|
|
52
|
+
'write:users': false,
|
|
53
|
+
'delete:users': true,
|
|
54
|
+
'manage:users': false
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('transforms mixed types correctly', () => {
|
|
59
|
+
const permissions = {
|
|
60
|
+
'read:users': 'true',
|
|
61
|
+
'write:users': false,
|
|
62
|
+
'delete:users': 1,
|
|
63
|
+
'manage:users': 0,
|
|
64
|
+
'view:users': null,
|
|
65
|
+
'edit:users': undefined
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const result = transformPermissionMapToBoolean(permissions);
|
|
69
|
+
|
|
70
|
+
expect(result).toEqual({
|
|
71
|
+
'read:users': true,
|
|
72
|
+
'write:users': false,
|
|
73
|
+
'delete:users': true,
|
|
74
|
+
'manage:users': false,
|
|
75
|
+
'view:users': false,
|
|
76
|
+
'edit:users': false
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('handles empty object', () => {
|
|
81
|
+
const permissions = {};
|
|
82
|
+
|
|
83
|
+
const result = transformPermissionMapToBoolean(permissions);
|
|
84
|
+
|
|
85
|
+
expect(result).toEqual({});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('handles null and undefined values', () => {
|
|
89
|
+
const permissions = {
|
|
90
|
+
'read:users': null,
|
|
91
|
+
'write:users': undefined,
|
|
92
|
+
'delete:users': false,
|
|
93
|
+
'manage:users': true
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const result = transformPermissionMapToBoolean(permissions);
|
|
97
|
+
|
|
98
|
+
expect(result).toEqual({
|
|
99
|
+
'read:users': false,
|
|
100
|
+
'write:users': false,
|
|
101
|
+
'delete:users': false,
|
|
102
|
+
'manage:users': true
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('handles numeric values', () => {
|
|
107
|
+
const permissions = {
|
|
108
|
+
'read:users': 1,
|
|
109
|
+
'write:users': 0,
|
|
110
|
+
'delete:users': -1,
|
|
111
|
+
'manage:users': 42
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const result = transformPermissionMapToBoolean(permissions);
|
|
115
|
+
|
|
116
|
+
expect(result).toEqual({
|
|
117
|
+
'read:users': true,
|
|
118
|
+
'write:users': false,
|
|
119
|
+
'delete:users': true,
|
|
120
|
+
'manage:users': true
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('handles case-insensitive string values', () => {
|
|
125
|
+
const permissions = {
|
|
126
|
+
'read:users': 'TRUE',
|
|
127
|
+
'write:users': 'FALSE',
|
|
128
|
+
'delete:users': 'True',
|
|
129
|
+
'manage:users': 'False'
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const result = transformPermissionMapToBoolean(permissions);
|
|
133
|
+
|
|
134
|
+
expect(result).toEqual({
|
|
135
|
+
'read:users': true,
|
|
136
|
+
'write:users': false,
|
|
137
|
+
'delete:users': true,
|
|
138
|
+
'manage:users': false
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe('hasPermission', () => {
|
|
144
|
+
it('returns true for granted permission', () => {
|
|
145
|
+
const permissions = {
|
|
146
|
+
'read:users': true,
|
|
147
|
+
'write:users': false,
|
|
148
|
+
'delete:users': true
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
expect(hasPermission(permissions, 'read:users')).toBe(true);
|
|
152
|
+
expect(hasPermission(permissions, 'delete:users')).toBe(true);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('returns false for denied permission', () => {
|
|
156
|
+
const permissions = {
|
|
157
|
+
'read:users': true,
|
|
158
|
+
'write:users': false,
|
|
159
|
+
'delete:users': true
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
expect(hasPermission(permissions, 'write:users')).toBe(false);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('returns false for non-existent permission', () => {
|
|
166
|
+
const permissions = {
|
|
167
|
+
'read:users': true,
|
|
168
|
+
'write:users': false
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
expect(hasPermission(permissions, 'manage:users')).toBe(false);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('handles empty permissions object', () => {
|
|
175
|
+
const permissions = {};
|
|
176
|
+
|
|
177
|
+
expect(hasPermission(permissions, 'read:users')).toBe(false);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('handles null/undefined permission', () => {
|
|
181
|
+
const permissions = {
|
|
182
|
+
'read:users': true,
|
|
183
|
+
'write:users': false
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
expect(hasPermission(permissions, null as any)).toBe(false);
|
|
187
|
+
expect(hasPermission(permissions, undefined as any)).toBe(false);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe('hasAnyPermission', () => {
|
|
192
|
+
it('returns true when at least one permission is granted', () => {
|
|
193
|
+
const permissions = {
|
|
194
|
+
'read:users': true,
|
|
195
|
+
'write:users': false,
|
|
196
|
+
'delete:users': false
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
expect(hasAnyPermission(permissions, ['read:users', 'write:users'])).toBe(true);
|
|
200
|
+
expect(hasAnyPermission(permissions, ['write:users', 'delete:users', 'read:users'])).toBe(true);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('returns false when no permissions are granted', () => {
|
|
204
|
+
const permissions = {
|
|
205
|
+
'read:users': false,
|
|
206
|
+
'write:users': false,
|
|
207
|
+
'delete:users': false
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
expect(hasAnyPermission(permissions, ['read:users', 'write:users'])).toBe(false);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('returns false for empty permission list', () => {
|
|
214
|
+
const permissions = {
|
|
215
|
+
'read:users': true,
|
|
216
|
+
'write:users': false
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
expect(hasAnyPermission(permissions, [])).toBe(false);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('returns false when permission list contains non-existent permissions', () => {
|
|
223
|
+
const permissions = {
|
|
224
|
+
'read:users': true,
|
|
225
|
+
'write:users': false
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
expect(hasAnyPermission(permissions, ['manage:users', 'admin:users'])).toBe(false);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('handles mixed permission states', () => {
|
|
232
|
+
const permissions = {
|
|
233
|
+
'read:users': true,
|
|
234
|
+
'write:users': false,
|
|
235
|
+
'delete:users': true,
|
|
236
|
+
'manage:users': false
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
expect(hasAnyPermission(permissions, ['read:users', 'write:users'])).toBe(true);
|
|
240
|
+
expect(hasAnyPermission(permissions, ['write:users', 'manage:users'])).toBe(false);
|
|
241
|
+
expect(hasAnyPermission(permissions, ['delete:users', 'manage:users'])).toBe(true);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('handles null/undefined permission list', () => {
|
|
245
|
+
const permissions = {
|
|
246
|
+
'read:users': true,
|
|
247
|
+
'write:users': false
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
expect(hasAnyPermission(permissions, null as any)).toBe(false);
|
|
251
|
+
expect(hasAnyPermission(permissions, undefined as any)).toBe(false);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
describe('hasAllPermissions', () => {
|
|
256
|
+
it('returns true when all permissions are granted', () => {
|
|
257
|
+
const permissions = {
|
|
258
|
+
'read:users': true,
|
|
259
|
+
'write:users': true,
|
|
260
|
+
'delete:users': true
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
expect(hasAllPermissions(permissions, ['read:users', 'write:users', 'delete:users'])).toBe(true);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('returns false when at least one permission is denied', () => {
|
|
267
|
+
const permissions = {
|
|
268
|
+
'read:users': true,
|
|
269
|
+
'write:users': false,
|
|
270
|
+
'delete:users': true
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
expect(hasAllPermissions(permissions, ['read:users', 'write:users'])).toBe(false);
|
|
274
|
+
expect(hasAllPermissions(permissions, ['read:users', 'write:users', 'delete:users'])).toBe(false);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('returns true for empty permission list', () => {
|
|
278
|
+
const permissions = {
|
|
279
|
+
'read:users': true,
|
|
280
|
+
'write:users': false
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
expect(hasAllPermissions(permissions, [])).toBe(true);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('returns false when permission list contains non-existent permissions', () => {
|
|
287
|
+
const permissions = {
|
|
288
|
+
'read:users': true,
|
|
289
|
+
'write:users': true
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
expect(hasAllPermissions(permissions, ['read:users', 'manage:users'])).toBe(false);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('handles mixed permission states', () => {
|
|
296
|
+
const permissions = {
|
|
297
|
+
'read:users': true,
|
|
298
|
+
'write:users': true,
|
|
299
|
+
'delete:users': false,
|
|
300
|
+
'manage:users': true
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
expect(hasAllPermissions(permissions, ['read:users', 'write:users'])).toBe(true);
|
|
304
|
+
expect(hasAllPermissions(permissions, ['read:users', 'write:users', 'delete:users'])).toBe(false);
|
|
305
|
+
expect(hasAllPermissions(permissions, ['read:users', 'manage:users'])).toBe(true);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('handles null/undefined permission list', () => {
|
|
309
|
+
const permissions = {
|
|
310
|
+
'read:users': true,
|
|
311
|
+
'write:users': false
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
expect(hasAllPermissions(permissions, null as any)).toBe(false);
|
|
315
|
+
expect(hasAllPermissions(permissions, undefined as any)).toBe(false);
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
describe('Edge Cases', () => {
|
|
320
|
+
it('handles very large permission objects', () => {
|
|
321
|
+
const permissions: Record<string, boolean> = {};
|
|
322
|
+
for (let i = 0; i < 1000; i++) {
|
|
323
|
+
permissions[`permission:${i}`] = i % 2 === 0;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const result = transformPermissionMapToBoolean(permissions);
|
|
327
|
+
expect(Object.keys(result)).toHaveLength(1000);
|
|
328
|
+
expect(result['permission:0']).toBe(true);
|
|
329
|
+
expect(result['permission:1']).toBe(false);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('handles special characters in permission names', () => {
|
|
333
|
+
const permissions = {
|
|
334
|
+
'read:users@domain.com': true,
|
|
335
|
+
'write:users#special': false,
|
|
336
|
+
'delete:users$with$symbols': true,
|
|
337
|
+
'manage:users.with.dots': false
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
const result = transformPermissionMapToBoolean(permissions);
|
|
341
|
+
expect(result['read:users@domain.com']).toBe(true);
|
|
342
|
+
expect(result['write:users#special']).toBe(false);
|
|
343
|
+
expect(result['delete:users$with$symbols']).toBe(true);
|
|
344
|
+
expect(result['manage:users.with.dots']).toBe(false);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('handles unicode characters in permission names', () => {
|
|
348
|
+
const permissions = {
|
|
349
|
+
'read:用户': true,
|
|
350
|
+
'write:ユーザー': false,
|
|
351
|
+
'delete:مستخدم': true
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const result = transformPermissionMapToBoolean(permissions);
|
|
355
|
+
expect(result['read:用户']).toBe(true);
|
|
356
|
+
expect(result['write:ユーザー']).toBe(false);
|
|
357
|
+
expect(result['delete:مستخدم']).toBe(true);
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
describe('Type Safety', () => {
|
|
362
|
+
it('maintains type safety with TypeScript', () => {
|
|
363
|
+
const permissions: Record<string, boolean> = {
|
|
364
|
+
'read:users': true,
|
|
365
|
+
'write:users': false
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
// These should compile without errors
|
|
369
|
+
const hasRead = hasPermission(permissions, 'read:users');
|
|
370
|
+
const hasAny = hasAnyPermission(permissions, ['read:users', 'write:users']);
|
|
371
|
+
const hasAll = hasAllPermissions(permissions, ['read:users']);
|
|
372
|
+
|
|
373
|
+
expect(typeof hasRead).toBe('boolean');
|
|
374
|
+
expect(typeof hasAny).toBe('boolean');
|
|
375
|
+
expect(typeof hasAll).toBe('boolean');
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('handles mixed input types gracefully', () => {
|
|
379
|
+
const permissions = {
|
|
380
|
+
'read:users': 'true',
|
|
381
|
+
'write:users': 1,
|
|
382
|
+
'delete:users': false,
|
|
383
|
+
'manage:users': null
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
const result = transformPermissionMapToBoolean(permissions);
|
|
387
|
+
expect(typeof result['read:users']).toBe('boolean');
|
|
388
|
+
expect(typeof result['write:users']).toBe('boolean');
|
|
389
|
+
expect(typeof result['delete:users']).toBe('boolean');
|
|
390
|
+
expect(typeof result['manage:users']).toBe('boolean');
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* Permission utilities for transforming and managing permissions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export function transformPermissionMapToBoolean(permissions: Record<string, unknown>): Record<string, boolean> {
|
|
7
|
+
const result: Record<string, boolean> = {};
|
|
8
|
+
|
|
9
|
+
Object.entries(permissions).forEach(([key, value]) => {
|
|
10
|
+
if (typeof value === 'string') {
|
|
11
|
+
// Handle string values - 'false' should be false, empty strings are false, other non-empty strings are true
|
|
12
|
+
result[key] = value !== '' && value.toLowerCase() !== 'false';
|
|
13
|
+
} else {
|
|
14
|
+
result[key] = Boolean(value);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function hasPermission(permissions: Record<string, boolean>, permission: string): boolean {
|
|
22
|
+
return Boolean(permissions[permission]);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function hasAnyPermission(permissions: Record<string, boolean>, permissionList: string[] | null | undefined): boolean {
|
|
26
|
+
if (!permissionList || permissionList.length === 0) return false;
|
|
27
|
+
return permissionList.some(permission => hasPermission(permissions, permission));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function hasAllPermissions(permissions: Record<string, boolean>, permissionList: string[] | null | undefined): boolean {
|
|
31
|
+
if (!permissionList) return false;
|
|
32
|
+
if (permissionList.length === 0) return true;
|
|
33
|
+
return permissionList.every(permission => hasPermission(permissions, permission));
|
|
34
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* @file Input Sanitization Layer
|
|
4
|
+
* @package @jmruthers/pace-core
|
|
5
|
+
* @module Security
|
|
6
|
+
* @since 0.1.0
|
|
7
|
+
*
|
|
8
|
+
* Comprehensive input sanitization utilities to prevent XSS, injection attacks,
|
|
9
|
+
* and other security vulnerabilities.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Sanitization options for different contexts
|
|
16
|
+
*/
|
|
17
|
+
export interface SanitizationOptions {
|
|
18
|
+
allowHtml?: boolean;
|
|
19
|
+
allowedTags?: string[];
|
|
20
|
+
maxLength?: number;
|
|
21
|
+
trim?: boolean;
|
|
22
|
+
removeScripts?: boolean;
|
|
23
|
+
removeEvents?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Default sanitization options
|
|
28
|
+
*/
|
|
29
|
+
const DEFAULT_OPTIONS: SanitizationOptions = {
|
|
30
|
+
allowHtml: false,
|
|
31
|
+
allowedTags: [],
|
|
32
|
+
maxLength: 1000,
|
|
33
|
+
trim: true,
|
|
34
|
+
removeScripts: true,
|
|
35
|
+
removeEvents: true
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Sanitizes user input by removing potentially dangerous characters and patterns
|
|
40
|
+
*/
|
|
41
|
+
export function sanitizeUserInput(input: string, options: SanitizationOptions = {}): string {
|
|
42
|
+
if (typeof input !== 'string') {
|
|
43
|
+
return '';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
47
|
+
let sanitized = input;
|
|
48
|
+
|
|
49
|
+
// Trim whitespace if requested
|
|
50
|
+
if (opts.trim) {
|
|
51
|
+
sanitized = sanitized.trim();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Enforce maximum length
|
|
55
|
+
if (opts.maxLength && sanitized.length > opts.maxLength) {
|
|
56
|
+
sanitized = sanitized.substring(0, opts.maxLength);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Remove or escape HTML if not allowed
|
|
60
|
+
if (!opts.allowHtml) {
|
|
61
|
+
sanitized = sanitized
|
|
62
|
+
.replace(/</g, '<')
|
|
63
|
+
.replace(/>/g, '>')
|
|
64
|
+
.replace(/"/g, '"')
|
|
65
|
+
.replace(/'/g, ''')
|
|
66
|
+
.replace(/\//g, '/');
|
|
67
|
+
} else if (opts.allowedTags && opts.allowedTags.length > 0) {
|
|
68
|
+
// If HTML is allowed, only permit specific tags
|
|
69
|
+
const allowedTagsRegex = new RegExp(`<(?!\/?(?:${opts.allowedTags.join('|')})\s*\/?>)[^>]+>`, 'gi');
|
|
70
|
+
sanitized = sanitized.replace(allowedTagsRegex, '');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Remove script tags and javascript: protocols
|
|
74
|
+
if (opts.removeScripts) {
|
|
75
|
+
sanitized = sanitized
|
|
76
|
+
.replace(/<script[^>]*>.*?<\/script>/gi, '')
|
|
77
|
+
.replace(/javascript:/gi, '')
|
|
78
|
+
.replace(/vbscript:/gi, '')
|
|
79
|
+
.replace(/data:/gi, '');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Remove event handlers
|
|
83
|
+
if (opts.removeEvents) {
|
|
84
|
+
sanitized = sanitized.replace(/on\w+\s*=/gi, '');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return sanitized;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Sanitizes email addresses
|
|
92
|
+
*/
|
|
93
|
+
export function sanitizeEmail(email: string): string {
|
|
94
|
+
if (typeof email !== 'string') {
|
|
95
|
+
return '';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return email
|
|
99
|
+
.trim()
|
|
100
|
+
.toLowerCase()
|
|
101
|
+
.replace(/[^\w@.-]/g, ''); // Only allow word characters, @, ., and -
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Sanitizes phone numbers
|
|
106
|
+
*/
|
|
107
|
+
export function sanitizePhoneNumber(phone: string): string {
|
|
108
|
+
if (typeof phone !== 'string') {
|
|
109
|
+
return '';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return phone.replace(/[^\d+\-\s()]/g, '').trim();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Sanitizes URLs
|
|
117
|
+
*/
|
|
118
|
+
export function sanitizeUrl(url: string): string {
|
|
119
|
+
if (typeof url !== 'string') {
|
|
120
|
+
return '';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const sanitized = url.trim();
|
|
124
|
+
|
|
125
|
+
// Only allow http(s) and ftp protocols
|
|
126
|
+
if (!/^https?:\/\/|^ftp:\/\//i.test(sanitized)) {
|
|
127
|
+
return '';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Remove javascript: and other dangerous protocols
|
|
131
|
+
if (/javascript:|data:|vbscript:/i.test(sanitized)) {
|
|
132
|
+
return '';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return sanitized;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Sanitizes file names
|
|
140
|
+
*/
|
|
141
|
+
export function sanitizeFileName(fileName: string): string {
|
|
142
|
+
if (typeof fileName !== 'string') {
|
|
143
|
+
return '';
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return fileName
|
|
147
|
+
.trim()
|
|
148
|
+
.replace(/[<>:"/\\|?*]/g, '') // Remove invalid file name characters
|
|
149
|
+
.replace(/\.\./g, '') // Remove directory traversal attempts
|
|
150
|
+
.substring(0, 255); // Limit length
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Sanitizes SQL input to prevent injection
|
|
155
|
+
*/
|
|
156
|
+
export function sanitizeSqlInput(input: string): string {
|
|
157
|
+
if (typeof input !== 'string') {
|
|
158
|
+
return '';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return input
|
|
162
|
+
.replace(/['";\\]/g, '') // Remove SQL special characters
|
|
163
|
+
.replace(/--.*$/gm, '') // Remove SQL comments
|
|
164
|
+
.replace(/\/\*.*?\*\//g, '') // Remove SQL block comments
|
|
165
|
+
.trim();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Validates and sanitizes form data using Zod schemas
|
|
170
|
+
*/
|
|
171
|
+
export function sanitizeFormData<T>(
|
|
172
|
+
data: unknown,
|
|
173
|
+
schema: z.ZodSchema<T>,
|
|
174
|
+
sanitizationRules?: Record<string, SanitizationOptions>
|
|
175
|
+
): { success: boolean; data?: T; error?: string } {
|
|
176
|
+
try {
|
|
177
|
+
// First, sanitize string fields if rules are provided
|
|
178
|
+
if (sanitizationRules && typeof data === 'object' && data !== null) {
|
|
179
|
+
const sanitizedData = { ...data } as Record<string, unknown>;
|
|
180
|
+
|
|
181
|
+
Object.entries(sanitizationRules).forEach(([field, options]) => {
|
|
182
|
+
if (typeof sanitizedData[field] === 'string') {
|
|
183
|
+
sanitizedData[field] = sanitizeUserInput(sanitizedData[field] as string, options);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
data = sanitizedData;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Then validate with Zod schema
|
|
191
|
+
const result = schema.parse(data);
|
|
192
|
+
return { success: true, data: result };
|
|
193
|
+
} catch (error) {
|
|
194
|
+
if (error instanceof z.ZodError) {
|
|
195
|
+
return {
|
|
196
|
+
success: false,
|
|
197
|
+
error: error.errors.map(e => e.message).join(', ')
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
success: false,
|
|
202
|
+
error: 'Validation failed'
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Content Security Policy (CSP) utilities
|
|
209
|
+
*/
|
|
210
|
+
export const CSP_DIRECTIVES = {
|
|
211
|
+
default: "default-src 'self'",
|
|
212
|
+
script: "script-src 'self' 'unsafe-inline'",
|
|
213
|
+
style: "style-src 'self' 'unsafe-inline'",
|
|
214
|
+
img: "img-src 'self' data: https:",
|
|
215
|
+
font: "font-src 'self'",
|
|
216
|
+
connect: "connect-src 'self'",
|
|
217
|
+
frame: "frame-src 'none'"
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
export function generateCSPHeader(customDirectives?: Partial<typeof CSP_DIRECTIVES>): string {
|
|
221
|
+
const directives = { ...CSP_DIRECTIVES, ...customDirectives };
|
|
222
|
+
return Object.values(directives).join('; ');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Rate limiting utilities
|
|
227
|
+
*/
|
|
228
|
+
export class RateLimiter {
|
|
229
|
+
private attempts: Map<string, { count: number; resetTime: number }> = new Map();
|
|
230
|
+
|
|
231
|
+
constructor(
|
|
232
|
+
private maxAttempts: number = 5,
|
|
233
|
+
private windowMs: number = 15 * 60 * 1000 // 15 minutes
|
|
234
|
+
) {}
|
|
235
|
+
|
|
236
|
+
isAllowed(identifier: string): boolean {
|
|
237
|
+
const now = Date.now();
|
|
238
|
+
const record = this.attempts.get(identifier);
|
|
239
|
+
|
|
240
|
+
if (!record || now > record.resetTime) {
|
|
241
|
+
this.attempts.set(identifier, { count: 1, resetTime: now + this.windowMs });
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (record.count >= this.maxAttempts) {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
record.count++;
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
getRemainingAttempts(identifier: string): number {
|
|
254
|
+
const record = this.attempts.get(identifier);
|
|
255
|
+
if (!record || Date.now() > record.resetTime) {
|
|
256
|
+
return this.maxAttempts;
|
|
257
|
+
}
|
|
258
|
+
return Math.max(0, this.maxAttempts - record.count);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
reset(identifier: string): void {
|
|
262
|
+
this.attempts.delete(identifier);
|
|
263
|
+
}
|
|
264
|
+
}
|