@jmruthers/pace-core 0.5.136 → 0.5.139
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-JXFCA2BJ.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-NOHEVYVX.js → chunk-5JMOHWDI.js} +417 -319
- package/dist/chunk-5JMOHWDI.js.map +1 -0
- package/dist/{chunk-FHWWBIHA.js → chunk-6DXZ6V5Q.js} +5 -5
- 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-BJPBT3CU.js +21 -0
- package/dist/chunk-BJPBT3CU.js.map +1 -0
- package/dist/{chunk-L6PGMCMD.js → chunk-BOOI7GK2.js} +38 -12
- package/dist/chunk-BOOI7GK2.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-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-HJGGOMQ6.js → chunk-TLT2ZR3L.js} +147 -103
- package/dist/chunk-TLT2ZR3L.js.map +1 -0
- 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 +84 -15
- 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/rbac/hooks/useCan.test.ts +24 -0
- package/src/rbac/hooks/usePermissions.ts +49 -12
- 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-L6PGMCMD.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-JXFCA2BJ.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-FHWWBIHA.js.map → chunk-6DXZ6V5Q.js.map} +0 -0
- /package/dist/{chunk-2TWNJ46Y.js.map → chunk-6LAAY47Q.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,377 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Secure Data Access Utility
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Utils/SecureDataAccess
|
|
5
|
+
* @since 0.4.0
|
|
6
|
+
*
|
|
7
|
+
* Secure data access utilities that enforce organisation context for all database operations.
|
|
8
|
+
* Prevents data leakage between organisations and ensures proper access validation.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
12
|
+
|
|
13
|
+
// Generic database record type
|
|
14
|
+
export interface DatabaseRecord {
|
|
15
|
+
id: string;
|
|
16
|
+
organisation_id: string;
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Generic data for insert/update operations
|
|
21
|
+
export interface DatabaseData {
|
|
22
|
+
[key: string]: unknown;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Generic filters for queries
|
|
26
|
+
export interface DatabaseFilters {
|
|
27
|
+
[key: string]: unknown;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Secure query options
|
|
31
|
+
export interface SecureQueryOptions {
|
|
32
|
+
table: string;
|
|
33
|
+
select: string;
|
|
34
|
+
organisationId: string;
|
|
35
|
+
filters?: DatabaseFilters;
|
|
36
|
+
orderBy?: string;
|
|
37
|
+
limit?: number;
|
|
38
|
+
offset?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface SecureDataAccess {
|
|
42
|
+
// Secure query methods
|
|
43
|
+
secureQuery: <T extends DatabaseRecord = DatabaseRecord>(options: SecureQueryOptions) => Promise<T[]>;
|
|
44
|
+
secureSingleQuery: <T extends DatabaseRecord = DatabaseRecord>(options: SecureQueryOptions) => Promise<T | null>;
|
|
45
|
+
|
|
46
|
+
// Secure mutation methods
|
|
47
|
+
secureInsert: <T extends DatabaseRecord = DatabaseRecord>(table: string, data: DatabaseData, organisationId: string) => Promise<T | null>;
|
|
48
|
+
secureUpdate: <T extends DatabaseRecord = DatabaseRecord>(table: string, data: DatabaseData, filters: DatabaseFilters, organisationId: string) => Promise<T | null>;
|
|
49
|
+
secureDelete: (table: string, filters: DatabaseFilters, organisationId: string) => Promise<boolean>;
|
|
50
|
+
|
|
51
|
+
// Organisation-scoped queries
|
|
52
|
+
queryByOrganisation: <T extends DatabaseRecord = DatabaseRecord>(table: string, select: string, organisationId: string, filters?: DatabaseFilters) => Promise<T[]>;
|
|
53
|
+
|
|
54
|
+
// Validation helpers
|
|
55
|
+
validateOrganisationContext: (organisationId: string) => void;
|
|
56
|
+
ensureOrganisationColumn: (table: string) => boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface SecureQueryBuilder {
|
|
60
|
+
table: string;
|
|
61
|
+
select: string;
|
|
62
|
+
organisationId: string;
|
|
63
|
+
filters?: DatabaseFilters;
|
|
64
|
+
orderBy?: string;
|
|
65
|
+
limit?: number;
|
|
66
|
+
offset?: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Create a secure data access instance
|
|
71
|
+
* @param supabase - Supabase client instance
|
|
72
|
+
* @param organisationId - Current organisation context
|
|
73
|
+
* @param isSuperAdmin - Whether user has super admin privileges
|
|
74
|
+
* @returns Secure data access utilities
|
|
75
|
+
*/
|
|
76
|
+
export const createSecureDataAccess = (
|
|
77
|
+
supabase: SupabaseClient,
|
|
78
|
+
organisationId: string,
|
|
79
|
+
isSuperAdmin: boolean = false
|
|
80
|
+
): SecureDataAccess => {
|
|
81
|
+
|
|
82
|
+
// Validate organisation context
|
|
83
|
+
const validateOrganisationContext = (orgId: string): void => {
|
|
84
|
+
if (!orgId) {
|
|
85
|
+
throw new Error('Organisation context is required for secure data access');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!isSuperAdmin && !orgId) {
|
|
89
|
+
throw new Error('Organisation context is mandatory for non-super admin users');
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Check if table has organisation_id column
|
|
94
|
+
const ensureOrganisationColumn = (table: string): boolean => {
|
|
95
|
+
// This is a simplified check - in production you might want to cache this
|
|
96
|
+
const tablesWithOrganisation = [
|
|
97
|
+
'event', 'organisation_settings',
|
|
98
|
+
'rbac_event_app_roles', 'rbac_organisation_roles',
|
|
99
|
+
// SECURITY: Phase 2 additions - complete organisation table mapping
|
|
100
|
+
'organisation_audit_log', 'organisation_invitations', 'organisation_app_access',
|
|
101
|
+
// SECURITY: Emergency additions for Phase 1 fixes
|
|
102
|
+
'cake_meal', 'cake_mealtype', 'pace_person', 'pace_member',
|
|
103
|
+
// SECURITY: Phase 3A additions - medical and personal data
|
|
104
|
+
'medi_profile', 'medi_condition', 'medi_diet', 'medi_action_plan', 'medi_profile_versions',
|
|
105
|
+
'pace_consent', 'pace_contact', 'pace_id_documents', 'pace_qualifications',
|
|
106
|
+
'form_responses', 'form_response_values', 'forms',
|
|
107
|
+
// SECURITY: Phase 3B additions - remaining critical tables
|
|
108
|
+
'invoice', 'line_item', 'credit_balance', 'payment_method',
|
|
109
|
+
'form_contexts', 'form_field_config', 'form_fields',
|
|
110
|
+
'cake_delivery', 'cake_diettype', 'cake_diner', 'cake_dish', 'cake_item',
|
|
111
|
+
'cake_logistics', 'cake_mealplan', 'cake_package', 'cake_recipe', 'cake_supplier',
|
|
112
|
+
'cake_supply', 'cake_unit', 'event_app_access', 'base_application', 'base_questions'
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
return tablesWithOrganisation.includes(table);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Build secure query with organisation context
|
|
119
|
+
const buildSecureQuery = (options: SecureQueryBuilder) => {
|
|
120
|
+
const { table, select, organisationId: orgId, filters, orderBy, limit, offset } = options;
|
|
121
|
+
|
|
122
|
+
validateOrganisationContext(orgId);
|
|
123
|
+
|
|
124
|
+
let query = supabase
|
|
125
|
+
.from(table)
|
|
126
|
+
.select(select);
|
|
127
|
+
|
|
128
|
+
// Add organisation filter (unless super admin)
|
|
129
|
+
if (!isSuperAdmin && ensureOrganisationColumn(table)) {
|
|
130
|
+
query = query.eq('organisation_id', orgId);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Add additional filters
|
|
134
|
+
if (filters) {
|
|
135
|
+
Object.entries(filters).forEach(([key, value]) => {
|
|
136
|
+
if (value !== undefined && value !== null) {
|
|
137
|
+
// Handle qualified column names (e.g., 'users.role')
|
|
138
|
+
const columnName = key.includes('.') ? key.split('.').pop()! : key;
|
|
139
|
+
query = query.eq(columnName, value);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Add ordering
|
|
145
|
+
if (orderBy) {
|
|
146
|
+
// Only use the column name, not a qualified name
|
|
147
|
+
const orderByColumn = orderBy.split('.').pop();
|
|
148
|
+
if (orderByColumn) {
|
|
149
|
+
query = query.order(orderByColumn);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Add pagination
|
|
154
|
+
if (limit) {
|
|
155
|
+
query = query.limit(limit);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (offset) {
|
|
159
|
+
query = query.range(offset, offset + (limit || 10) - 1);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return query;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// Secure query for multiple results
|
|
166
|
+
const secureQuery = async <T extends DatabaseRecord = DatabaseRecord>(options: SecureQueryOptions): Promise<T[]> => {
|
|
167
|
+
const { table, select, organisationId: orgId, filters, orderBy, limit, offset } = options;
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
const query = buildSecureQuery({
|
|
171
|
+
table,
|
|
172
|
+
select,
|
|
173
|
+
organisationId: orgId,
|
|
174
|
+
filters,
|
|
175
|
+
orderBy,
|
|
176
|
+
limit,
|
|
177
|
+
offset
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const { data, error } = await query;
|
|
181
|
+
|
|
182
|
+
if (error) {
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Ensure data is an array and not an error type
|
|
187
|
+
if (Array.isArray(data)) {
|
|
188
|
+
return data as unknown as T[];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return [];
|
|
192
|
+
} catch (error) {
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// Secure query for single result
|
|
198
|
+
const secureSingleQuery = async <T extends DatabaseRecord = DatabaseRecord>(options: SecureQueryOptions): Promise<T | null> => {
|
|
199
|
+
const { table, select, organisationId: orgId, filters, orderBy, limit, offset } = options;
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
const query = buildSecureQuery({
|
|
203
|
+
table,
|
|
204
|
+
select,
|
|
205
|
+
organisationId: orgId,
|
|
206
|
+
filters,
|
|
207
|
+
orderBy,
|
|
208
|
+
limit,
|
|
209
|
+
offset
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const { data, error } = await query.single();
|
|
213
|
+
|
|
214
|
+
if (error) {
|
|
215
|
+
if (error.code === 'PGRST116') {
|
|
216
|
+
// No rows returned
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
throw error;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Ensure data is not an error type
|
|
223
|
+
if (data && typeof data === 'object' && !('code' in data)) {
|
|
224
|
+
return data as unknown as T;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return null;
|
|
228
|
+
} catch (error) {
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// Secure insert with organisation context
|
|
234
|
+
const secureInsert = async <T extends DatabaseRecord = DatabaseRecord>(
|
|
235
|
+
table: string,
|
|
236
|
+
data: DatabaseData,
|
|
237
|
+
organisationId: string
|
|
238
|
+
): Promise<T | null> => {
|
|
239
|
+
validateOrganisationContext(organisationId);
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
const insertData = {
|
|
243
|
+
...data,
|
|
244
|
+
organisation_id: organisationId
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const { data: result, error } = await supabase
|
|
248
|
+
.from(table)
|
|
249
|
+
.insert(insertData)
|
|
250
|
+
.select()
|
|
251
|
+
.single();
|
|
252
|
+
|
|
253
|
+
if (error) {
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return result;
|
|
258
|
+
} catch (error) {
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
// Secure update with organisation context
|
|
264
|
+
const secureUpdate = async <T extends DatabaseRecord = DatabaseRecord>(
|
|
265
|
+
table: string,
|
|
266
|
+
data: DatabaseData,
|
|
267
|
+
filters: DatabaseFilters,
|
|
268
|
+
organisationId: string
|
|
269
|
+
): Promise<T | null> => {
|
|
270
|
+
validateOrganisationContext(organisationId);
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
let query = supabase
|
|
274
|
+
.from(table)
|
|
275
|
+
.update(data);
|
|
276
|
+
|
|
277
|
+
// Add organisation filter (unless super admin)
|
|
278
|
+
if (!isSuperAdmin && ensureOrganisationColumn(table)) {
|
|
279
|
+
query = query.eq('organisation_id', organisationId);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Add additional filters
|
|
283
|
+
if (filters) {
|
|
284
|
+
Object.entries(filters).forEach(([key, value]) => {
|
|
285
|
+
if (value !== undefined && value !== null) {
|
|
286
|
+
query = query.eq(key, value);
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const { data: result, error } = await query.select().single();
|
|
292
|
+
|
|
293
|
+
if (error) {
|
|
294
|
+
throw error;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return result;
|
|
298
|
+
} catch (error) {
|
|
299
|
+
throw error;
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
// Secure delete with organisation context
|
|
304
|
+
const secureDelete = async (
|
|
305
|
+
table: string,
|
|
306
|
+
filters: DatabaseFilters,
|
|
307
|
+
organisationId: string
|
|
308
|
+
): Promise<boolean> => {
|
|
309
|
+
validateOrganisationContext(organisationId);
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
let query = supabase
|
|
313
|
+
.from(table)
|
|
314
|
+
.delete();
|
|
315
|
+
|
|
316
|
+
// Add organisation filter (unless super admin)
|
|
317
|
+
if (!isSuperAdmin && ensureOrganisationColumn(table)) {
|
|
318
|
+
query = query.eq('organisation_id', organisationId);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Add additional filters
|
|
322
|
+
if (filters) {
|
|
323
|
+
Object.entries(filters).forEach(([key, value]) => {
|
|
324
|
+
if (value !== undefined && value !== null) {
|
|
325
|
+
query = query.eq(key, value);
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const { error } = await query;
|
|
331
|
+
|
|
332
|
+
if (error) {
|
|
333
|
+
throw error;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return true;
|
|
337
|
+
} catch (error) {
|
|
338
|
+
throw error;
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
// Organisation-scoped query helper
|
|
343
|
+
const queryByOrganisation = async <T extends DatabaseRecord = DatabaseRecord>(
|
|
344
|
+
table: string,
|
|
345
|
+
select: string,
|
|
346
|
+
organisationId: string,
|
|
347
|
+
filters?: DatabaseFilters
|
|
348
|
+
): Promise<T[]> => {
|
|
349
|
+
return secureQuery<T>({
|
|
350
|
+
table,
|
|
351
|
+
select,
|
|
352
|
+
organisationId,
|
|
353
|
+
filters
|
|
354
|
+
});
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
secureQuery,
|
|
359
|
+
secureSingleQuery,
|
|
360
|
+
secureInsert,
|
|
361
|
+
secureUpdate,
|
|
362
|
+
secureDelete,
|
|
363
|
+
queryByOrganisation,
|
|
364
|
+
validateOrganisationContext,
|
|
365
|
+
ensureOrganisationColumn
|
|
366
|
+
};
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Hook for secure data access
|
|
371
|
+
* @returns Secure data access utilities
|
|
372
|
+
*/
|
|
373
|
+
export const useSecureDataAccess = (): SecureDataAccess => {
|
|
374
|
+
// This would typically get the context from providers
|
|
375
|
+
// For now, we'll create a placeholder that can be used with explicit parameters
|
|
376
|
+
throw new Error('useSecureDataAccess must be used with explicit parameters. Use createSecureDataAccess instead.');
|
|
377
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Secure error handling utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { AuthError, AuthErrorCode, RequestId } from '../types/unified';
|
|
6
|
+
|
|
7
|
+
export class SecureError extends Error {
|
|
8
|
+
public readonly code: AuthErrorCode;
|
|
9
|
+
public readonly statusCode: number;
|
|
10
|
+
public readonly userMessage: string;
|
|
11
|
+
|
|
12
|
+
constructor(message: string, code: AuthErrorCode, statusCode: number = 500, userMessage?: string) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = 'SecureError';
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.statusCode = statusCode;
|
|
17
|
+
this.userMessage = userMessage || message;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createSecureError(
|
|
22
|
+
message: string,
|
|
23
|
+
code: AuthErrorCode,
|
|
24
|
+
statusCode: number = 500,
|
|
25
|
+
userMessage?: string
|
|
26
|
+
): SecureError {
|
|
27
|
+
return new SecureError(message, code, statusCode, userMessage);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function convertToAuthError(error: SecureError | Error | unknown): AuthError {
|
|
31
|
+
if (error instanceof SecureError) {
|
|
32
|
+
return {
|
|
33
|
+
message: error.message,
|
|
34
|
+
code: error.code,
|
|
35
|
+
user_message: error.userMessage,
|
|
36
|
+
__isAuthError: true,
|
|
37
|
+
name: 'AuthError',
|
|
38
|
+
timestamp: Date.now()
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (error instanceof Error) {
|
|
43
|
+
return {
|
|
44
|
+
message: error.message,
|
|
45
|
+
code: AuthErrorCode.UNKNOWN_ERROR,
|
|
46
|
+
user_message: error.message,
|
|
47
|
+
__isAuthError: true,
|
|
48
|
+
name: 'AuthError',
|
|
49
|
+
timestamp: Date.now()
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
message: 'Unknown error occurred',
|
|
55
|
+
code: AuthErrorCode.UNKNOWN_ERROR,
|
|
56
|
+
user_message: 'Unknown error occurred',
|
|
57
|
+
__isAuthError: true,
|
|
58
|
+
name: 'AuthError',
|
|
59
|
+
timestamp: Date.now()
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function isSecureError(error: unknown): error is SecureError {
|
|
64
|
+
return error instanceof SecureError;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function sanitizeError(error: unknown): AuthError {
|
|
68
|
+
return convertToAuthError(error);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function generateRequestId(): RequestId {
|
|
72
|
+
return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` as RequestId;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function logSecurityEvent(event: string, details?: unknown): void {
|
|
76
|
+
// In production, this would send to a proper logging service
|
|
77
|
+
// For now, we'll log to console.warn for testing purposes
|
|
78
|
+
console.warn(`[SECURITY] ${event}`, details);
|
|
79
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* @file Secure Storage Utilities
|
|
4
|
+
* @description Encrypted storage wrapper for sensitive data
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface SecureStorageOptions {
|
|
8
|
+
encrypt?: boolean;
|
|
9
|
+
expiry?: number; // TTL in milliseconds
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Secure storage implementation with encryption support
|
|
14
|
+
*/
|
|
15
|
+
class SecureStorageImpl {
|
|
16
|
+
private encryptionKey: CryptoKey | null = null;
|
|
17
|
+
private initialized = false;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Initialize secure storage with encryption
|
|
21
|
+
*/
|
|
22
|
+
async init(): Promise<void> {
|
|
23
|
+
if (this.initialized) return;
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
// Check if Web Crypto API is available
|
|
27
|
+
if (window.crypto && window.crypto.subtle) {
|
|
28
|
+
// Generate or retrieve encryption key
|
|
29
|
+
const keyData = localStorage.getItem('_sec_key');
|
|
30
|
+
if (keyData) {
|
|
31
|
+
try {
|
|
32
|
+
const keyBuffer = this.base64ToArrayBuffer(keyData);
|
|
33
|
+
this.encryptionKey = await window.crypto.subtle.importKey(
|
|
34
|
+
'raw',
|
|
35
|
+
keyBuffer,
|
|
36
|
+
{ name: 'AES-GCM' },
|
|
37
|
+
false,
|
|
38
|
+
['encrypt', 'decrypt']
|
|
39
|
+
);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
await this.generateNewKey();
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
await this.generateNewKey();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
this.initialized = true;
|
|
48
|
+
} catch (error) {
|
|
49
|
+
this.initialized = true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Store item securely
|
|
55
|
+
*/
|
|
56
|
+
async setItem(
|
|
57
|
+
key: string,
|
|
58
|
+
value: string,
|
|
59
|
+
options: SecureStorageOptions = {}
|
|
60
|
+
): Promise<void> {
|
|
61
|
+
await this.init();
|
|
62
|
+
|
|
63
|
+
const data = {
|
|
64
|
+
value,
|
|
65
|
+
timestamp: Date.now(),
|
|
66
|
+
expiry: options.expiry ? Date.now() + options.expiry : undefined,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const serialized = JSON.stringify(data);
|
|
70
|
+
|
|
71
|
+
if (options.encrypt && this.encryptionKey) {
|
|
72
|
+
try {
|
|
73
|
+
const encrypted = await this.encrypt(serialized);
|
|
74
|
+
localStorage.setItem(`_sec_${key}`, encrypted);
|
|
75
|
+
return;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
// Silent fail - store as plain text
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
localStorage.setItem(key, serialized);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Retrieve item securely
|
|
86
|
+
*/
|
|
87
|
+
async getItem(key: string): Promise<string | null> {
|
|
88
|
+
await this.init();
|
|
89
|
+
|
|
90
|
+
// Try encrypted storage first
|
|
91
|
+
const encryptedData = localStorage.getItem(`_sec_${key}`);
|
|
92
|
+
if (encryptedData && this.encryptionKey) {
|
|
93
|
+
try {
|
|
94
|
+
const decrypted = await this.decrypt(encryptedData);
|
|
95
|
+
const parsed = JSON.parse(decrypted);
|
|
96
|
+
|
|
97
|
+
// Check expiry
|
|
98
|
+
if (parsed.expiry && Date.now() > parsed.expiry) {
|
|
99
|
+
await this.removeItem(key);
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return parsed.value;
|
|
104
|
+
} catch (error) {
|
|
105
|
+
// Silent fail - try plain storage
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Fallback to plain storage
|
|
110
|
+
const plainData = localStorage.getItem(key);
|
|
111
|
+
if (!plainData) return null;
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const parsed = JSON.parse(plainData);
|
|
115
|
+
|
|
116
|
+
// Check expiry
|
|
117
|
+
if (parsed.expiry && Date.now() > parsed.expiry) {
|
|
118
|
+
await this.removeItem(key);
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return parsed.value || plainData;
|
|
123
|
+
} catch (error) {
|
|
124
|
+
// If parsing fails, return as-is (backward compatibility)
|
|
125
|
+
return plainData;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Remove item
|
|
131
|
+
*/
|
|
132
|
+
async removeItem(key: string): Promise<void> {
|
|
133
|
+
localStorage.removeItem(key);
|
|
134
|
+
localStorage.removeItem(`_sec_${key}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Clear all secure storage
|
|
139
|
+
*/
|
|
140
|
+
async clear(): Promise<void> {
|
|
141
|
+
const keys = Object.keys(localStorage);
|
|
142
|
+
for (const key of keys) {
|
|
143
|
+
if (key.startsWith('_sec_')) {
|
|
144
|
+
localStorage.removeItem(key);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Generate new encryption key
|
|
151
|
+
*/
|
|
152
|
+
private async generateNewKey(): Promise<void> {
|
|
153
|
+
if (!window.crypto?.subtle) return;
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
this.encryptionKey = await window.crypto.subtle.generateKey(
|
|
157
|
+
{ name: 'AES-GCM', length: 256 },
|
|
158
|
+
true,
|
|
159
|
+
['encrypt', 'decrypt']
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
// Export and store key
|
|
163
|
+
const exportedKey = await window.crypto.subtle.exportKey('raw', this.encryptionKey);
|
|
164
|
+
const keyData = this.arrayBufferToBase64(exportedKey);
|
|
165
|
+
localStorage.setItem('_sec_key', keyData);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
// Silent fail - encryption not available
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Encrypt data
|
|
173
|
+
*/
|
|
174
|
+
private async encrypt(data: string): Promise<string> {
|
|
175
|
+
if (!this.encryptionKey || !window.crypto?.subtle) {
|
|
176
|
+
throw new Error('Encryption not available');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const encoder = new TextEncoder();
|
|
180
|
+
const dataBuffer = encoder.encode(data);
|
|
181
|
+
const iv = window.crypto.getRandomValues(new Uint8Array(12));
|
|
182
|
+
|
|
183
|
+
const encrypted = await window.crypto.subtle.encrypt(
|
|
184
|
+
{ name: 'AES-GCM', iv },
|
|
185
|
+
this.encryptionKey,
|
|
186
|
+
dataBuffer
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
// Combine IV and encrypted data
|
|
190
|
+
const combined = new Uint8Array(iv.length + encrypted.byteLength);
|
|
191
|
+
combined.set(iv);
|
|
192
|
+
combined.set(new Uint8Array(encrypted), iv.length);
|
|
193
|
+
|
|
194
|
+
return this.arrayBufferToBase64(combined.buffer);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Decrypt data
|
|
199
|
+
*/
|
|
200
|
+
private async decrypt(encryptedData: string): Promise<string> {
|
|
201
|
+
if (!this.encryptionKey || !window.crypto?.subtle) {
|
|
202
|
+
throw new Error('Decryption not available');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const combined = this.base64ToArrayBuffer(encryptedData);
|
|
206
|
+
const iv = combined.slice(0, 12);
|
|
207
|
+
const encrypted = combined.slice(12);
|
|
208
|
+
|
|
209
|
+
const decrypted = await window.crypto.subtle.decrypt(
|
|
210
|
+
{ name: 'AES-GCM', iv },
|
|
211
|
+
this.encryptionKey,
|
|
212
|
+
encrypted
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const decoder = new TextDecoder();
|
|
216
|
+
return decoder.decode(decrypted);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Convert ArrayBuffer to base64
|
|
221
|
+
*/
|
|
222
|
+
private arrayBufferToBase64(buffer: ArrayBuffer): string {
|
|
223
|
+
const bytes = new Uint8Array(buffer);
|
|
224
|
+
let binary = '';
|
|
225
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
226
|
+
binary += String.fromCharCode(bytes[i]);
|
|
227
|
+
}
|
|
228
|
+
return btoa(binary);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Convert base64 to ArrayBuffer
|
|
233
|
+
*/
|
|
234
|
+
private base64ToArrayBuffer(base64: string): ArrayBuffer {
|
|
235
|
+
const binary = atob(base64);
|
|
236
|
+
const bytes = new Uint8Array(binary.length);
|
|
237
|
+
for (let i = 0; i < binary.length; i++) {
|
|
238
|
+
bytes[i] = binary.charCodeAt(i);
|
|
239
|
+
}
|
|
240
|
+
return bytes.buffer;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export const secureStorage = new SecureStorageImpl();
|