@goplusvn/core 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/src/assets/erp_wallpaper.png +0 -0
- package/src/assets/goeat_logo.png +0 -0
- package/src/audit/audit-manager.ts +139 -0
- package/src/audit/index.ts +11 -0
- package/src/audit/memory-audit-logger.ts +86 -0
- package/src/audit/types.ts +50 -0
- package/src/auth/auth-service.ts +97 -0
- package/src/auth/index.ts +266 -0
- package/src/code-generation/index.ts +69 -0
- package/src/configs/auth-routes.ts +17 -0
- package/src/configs/crud.ts +136 -0
- package/src/configs/data/navigations.ts +781 -0
- package/src/configs/data/oauth-links.ts +10 -0
- package/src/configs/entities/material-categories.config.ts +125 -0
- package/src/configs/i18n.ts +12 -0
- package/src/configs/index.ts +26 -0
- package/src/configs/status.ts +25 -0
- package/src/configs/themes.ts +100 -0
- package/src/crud/components/crud-bulk-actions.tsx +91 -0
- package/src/crud/components/crud-card-view.tsx +241 -0
- package/src/crud/components/crud-context.tsx +122 -0
- package/src/crud/components/crud-delete-dialog.tsx +145 -0
- package/src/crud/components/crud-dialog.tsx +406 -0
- package/src/crud/components/crud-empty-state.tsx +104 -0
- package/src/crud/components/crud-export-button.tsx +170 -0
- package/src/crud/components/crud-field-renderer.tsx +653 -0
- package/src/crud/components/crud-filter-chips.tsx +102 -0
- package/src/crud/components/crud-filters/checkbox-filter.tsx +97 -0
- package/src/crud/components/crud-filters/datetime-filter.tsx +83 -0
- package/src/crud/components/crud-filters/filter-builder.tsx +66 -0
- package/src/crud/components/crud-filters/index.tsx +76 -0
- package/src/crud/components/crud-filters/radio-filter.tsx +86 -0
- package/src/crud/components/crud-filters/select-filter.tsx +141 -0
- package/src/crud/components/crud-filters/text-filter.tsx +86 -0
- package/src/crud/components/crud-form.tsx +642 -0
- package/src/crud/components/crud-import-dialog.tsx +440 -0
- package/src/crud/components/crud-infinite-scroll.tsx +116 -0
- package/src/crud/components/crud-page.tsx +1017 -0
- package/src/crud/components/crud-provider.tsx +277 -0
- package/src/crud/components/crud-row-actions.tsx +189 -0
- package/src/crud/components/crud-search.tsx +82 -0
- package/src/crud/components/crud-sheet.tsx +336 -0
- package/src/crud/components/crud-table-skeleton.tsx +26 -0
- package/src/crud/components/crud-table-toolbar.tsx +91 -0
- package/src/crud/components/crud-table.tsx +352 -0
- package/src/crud/components/crud-virtual-table.tsx +55 -0
- package/src/crud/components/index.tsx +20 -0
- package/src/crud/crud-filters/checkbox-filter.tsx +87 -0
- package/src/crud/crud-filters/datetime-filter.tsx +82 -0
- package/src/crud/crud-filters/filter-builder.tsx +64 -0
- package/src/crud/crud-filters/index.tsx +78 -0
- package/src/crud/crud-filters/radio-filter.tsx +79 -0
- package/src/crud/crud-filters/select-filter.tsx +148 -0
- package/src/crud/crud-filters/text-filter.tsx +81 -0
- package/src/crud/index.ts +43 -0
- package/src/crud/lib/crud-service.test.ts +334 -0
- package/src/crud/lib/crud-service.ts +358 -0
- package/src/crud/lib/crud-utils.test.ts +354 -0
- package/src/crud/lib/crud-utils.ts +299 -0
- package/src/crud/lib/crud-validator.ts +247 -0
- package/src/crud/lib/data-loader.ts +234 -0
- package/src/crud/lib/field-calculator.ts +241 -0
- package/src/crud/lib/field-formatter.ts +240 -0
- package/src/crud/lib/import-export-service.test.ts +290 -0
- package/src/crud/lib/import-export-service.ts +352 -0
- package/src/crud/lib/import-server-utils.ts +109 -0
- package/src/crud/lib/lazy-loader.ts +241 -0
- package/src/crud/lib/parse-filters.ts +85 -0
- package/src/crud/lib/permissions.ts +52 -0
- package/src/crud/lib/serialize-config.ts +60 -0
- package/src/crud/lib/stream-loader.ts +145 -0
- package/src/crud/lib/translate-config.ts +335 -0
- package/src/crud/lib/types.ts +11 -0
- package/src/crud/pages/entity-crud-page.tsx +144 -0
- package/src/crud/server.ts +8 -0
- package/src/home/constants.tsx +142 -0
- package/src/home/feature-showcase.tsx +171 -0
- package/src/home/home-page.tsx +191 -0
- package/src/home/hooks/index.ts +1 -0
- package/src/home/hooks/useWidgetPreferences.ts +167 -0
- package/src/home/index.ts +33 -0
- package/src/home/quick-access-dialog.tsx +271 -0
- package/src/home/quick-access-menu.tsx +267 -0
- package/src/home/types.ts +140 -0
- package/src/home/welcome-card.tsx +92 -0
- package/src/home/widget-container.tsx +258 -0
- package/src/home/widgets/base-widget.tsx +200 -0
- package/src/home/widgets/customers-widget.tsx +74 -0
- package/src/home/widgets/index.ts +6 -0
- package/src/home/widgets/orders-widget.tsx +87 -0
- package/src/home/widgets/revenue-widget.tsx +71 -0
- package/src/home/widgets/stock-widget.tsx +109 -0
- package/src/hooks/index.tsx +598 -0
- package/src/hooks/use-tenant.test.tsx +30 -0
- package/src/hooks/use-tenant.ts +5 -0
- package/src/index.ts +17 -0
- package/src/infrastructure/__tests__/architecture-verification.spec.ts +103 -0
- package/src/infrastructure/api-service.ts +317 -0
- package/src/infrastructure/cache/cache-manager.ts +107 -0
- package/src/infrastructure/cache/cache.ts +120 -0
- package/src/infrastructure/cache/index.ts +8 -0
- package/src/infrastructure/cache/types.ts +48 -0
- package/src/infrastructure/cron/cron-manager.ts +239 -0
- package/src/infrastructure/cron/index.ts +6 -0
- package/src/infrastructure/cron/types.ts +41 -0
- package/src/infrastructure/event-bus/event-bus.ts +145 -0
- package/src/infrastructure/event-bus/index.ts +2 -0
- package/src/infrastructure/event-bus/types.ts +22 -0
- package/src/infrastructure/index.ts +32 -0
- package/src/infrastructure/lock/decorators.ts +67 -0
- package/src/infrastructure/lock/index.ts +2 -0
- package/src/infrastructure/lock/lock-manager.ts +33 -0
- package/src/infrastructure/logger/index.ts +2 -0
- package/src/infrastructure/logger/logger.ts +96 -0
- package/src/infrastructure/logger/types.ts +25 -0
- package/src/layout/index.tsx +185 -0
- package/src/navigation/index.ts +91 -0
- package/src/notification/index.ts +14 -0
- package/src/notification/notification-service.ts +120 -0
- package/src/notification/storage/in-memory.ts +56 -0
- package/src/notification/storage/index.ts +1 -0
- package/src/notification/types.ts +51 -0
- package/src/organization/branch-service.ts +299 -0
- package/src/organization/branches.config.ts +154 -0
- package/src/organization/index.ts +5 -0
- package/src/plugin/apps-registry.ts +97 -0
- package/src/plugin/index.ts +5 -0
- package/src/plugin/types.ts +41 -0
- package/src/providers/index.tsx +109 -0
- package/src/providers/tenant-provider.tsx +45 -0
- package/src/rbac/components/roles/role-card.tsx +158 -0
- package/src/rbac/components/roles/role-stats-cards.tsx +29 -0
- package/src/rbac/components/roles/role-toolbar.tsx +123 -0
- package/src/rbac/hooks/use-role-operations.ts +159 -0
- package/src/rbac/hooks/use-roles-data.ts +59 -0
- package/src/rbac/index.ts +297 -0
- package/src/rbac/lib/permission-helpers.ts +63 -0
- package/src/rbac/pages/action-list-page.tsx +25 -0
- package/src/rbac/pages/resource-list-page.tsx +25 -0
- package/src/rbac/pages/role-list-page.tsx +378 -0
- package/src/rbac/permission-service.ts +140 -0
- package/src/rbac/permissions.ts +135 -0
- package/src/rbac/resource-service.ts +115 -0
- package/src/rbac/resource-validator.ts +119 -0
- package/src/rbac/role-service.ts +165 -0
- package/src/rbac/server.ts +16 -0
- package/src/rbac/types.ts +38 -0
- package/src/schemas/action.schema.ts +66 -0
- package/src/schemas/branch.schema.ts +52 -0
- package/src/schemas/coming-soon-schema.ts +9 -0
- package/src/schemas/company.schema.ts +44 -0
- package/src/schemas/forgot-passward-schema.ts +9 -0
- package/src/schemas/index.ts +30 -0
- package/src/schemas/material-category.schema.ts +43 -0
- package/src/schemas/material-pricing.schema.ts +74 -0
- package/src/schemas/material.schema.ts +76 -0
- package/src/schemas/materials.ts +52 -0
- package/src/schemas/new-passward-schema.ts +15 -0
- package/src/schemas/partner-company.schema.ts +149 -0
- package/src/schemas/register-schema.ts +36 -0
- package/src/schemas/resource.schema.ts +133 -0
- package/src/schemas/role.schema.ts +11 -0
- package/src/schemas/sign-in-schema.ts +24 -0
- package/src/schemas/supplier-pricing.schema.ts +15 -0
- package/src/schemas/supplier.schema.ts +120 -0
- package/src/schemas/system-category-group.schema.ts +67 -0
- package/src/schemas/system-category.schema.ts +77 -0
- package/src/schemas/system-config.schema.ts +118 -0
- package/src/schemas/uom.schema.ts +75 -0
- package/src/schemas/user-supplier.schema.ts +179 -0
- package/src/schemas/user.schema.ts +18 -0
- package/src/schemas/verify-email-schema.ts +9 -0
- package/src/schemas/warehouse.schema.ts +49 -0
- package/src/system/components/categories/category-list.tsx +529 -0
- package/src/system/components/categories/category-manager.tsx +89 -0
- package/src/system/components/categories/group-sidebar.tsx +308 -0
- package/src/system/components/settings/setting-dialogs.tsx +197 -0
- package/src/system/components/settings/setting-field.tsx +291 -0
- package/src/system/components/settings/setting-form-dialog.tsx +308 -0
- package/src/system/components/settings/settings-groups.ts +80 -0
- package/src/system/components/settings/settings-search.tsx +71 -0
- package/src/system/components/settings/settings-section.tsx +74 -0
- package/src/system/components/settings/settings-sidebar.tsx +81 -0
- package/src/system/constants.ts +3 -0
- package/src/system/index.ts +150 -0
- package/src/system/job-manager.ts +176 -0
- package/src/system/pages/components/categories/category-list.tsx +537 -0
- package/src/system/pages/components/categories/category-manager.tsx +90 -0
- package/src/system/pages/components/categories/group-sidebar.tsx +311 -0
- package/src/system/pages/components/settings/sales-rules-settings.tsx +222 -0
- package/src/system/pages/components/settings/setting-dialogs.tsx +197 -0
- package/src/system/pages/components/settings/setting-field.tsx +292 -0
- package/src/system/pages/components/settings/setting-form-dialog.tsx +308 -0
- package/src/system/pages/components/settings/settings-groups.ts +87 -0
- package/src/system/pages/components/settings/settings-page.tsx +372 -0
- package/src/system/pages/components/settings/settings-search.tsx +71 -0
- package/src/system/pages/components/settings/settings-section.tsx +74 -0
- package/src/system/pages/components/settings/settings-sidebar.tsx +81 -0
- package/src/system/pages/components/settings/system-settings.tsx +244 -0
- package/src/system/pages/system-category-page.tsx +15 -0
- package/src/system/pages/system-settings-page.tsx +380 -0
- package/src/system/schemas/system-category-group.schema.ts +46 -0
- package/src/system/schemas/system-category.schema.ts +56 -0
- package/src/system/services/settings-service.ts +127 -0
- package/src/system/services/system-category-service.ts +63 -0
- package/src/system/types.ts +45 -0
- package/src/types/index.ts +703 -0
- package/src/ui/auth/auth-layout.tsx +135 -0
- package/src/ui/auth/forgot-password-form.tsx +98 -0
- package/src/ui/auth/index.tsx +7 -0
- package/src/ui/auth/new-password-form.tsx +107 -0
- package/src/ui/auth/oauth-links.tsx +30 -0
- package/src/ui/auth/register-form.tsx +202 -0
- package/src/ui/auth/sign-in-form.tsx +238 -0
- package/src/ui/auth/verify-email-form.tsx +104 -0
- package/src/ui/crud/index.tsx +10 -0
- package/src/ui/data-display/accordion.tsx +65 -0
- package/src/ui/data-display/aspect-ratio.tsx +11 -0
- package/src/ui/data-display/avatar.tsx +163 -0
- package/src/ui/data-display/bento-grid.tsx +77 -0
- package/src/ui/data-display/carousel.tsx +249 -0
- package/src/ui/data-display/chart.tsx +363 -0
- package/src/ui/data-display/code-block-highlight.tsx +54 -0
- package/src/ui/data-display/collapsible.tsx +42 -0
- package/src/ui/data-display/compact-stat-bar.tsx +149 -0
- package/src/ui/data-display/data-table/data-table-context.tsx +255 -0
- package/src/ui/data-display/data-table/data-table-empty-state.tsx +133 -0
- package/src/ui/data-display/data-table/data-table-skeleton.tsx +145 -0
- package/src/ui/data-display/data-table/data-table-toolbar.tsx +353 -0
- package/src/ui/data-display/data-table/data-table.tsx +597 -0
- package/src/ui/data-display/data-table/index.ts +44 -0
- package/src/ui/data-display/data-table-column-header.tsx +75 -0
- package/src/ui/data-display/data-table-pagination.tsx +130 -0
- package/src/ui/data-display/data-table-view-options.tsx +59 -0
- package/src/ui/data-display/formatted-number-input.tsx +210 -0
- package/src/ui/data-display/highlight.tsx +20 -0
- package/src/ui/data-display/hover-card.tsx +48 -0
- package/src/ui/data-display/index.tsx +50 -0
- package/src/ui/data-display/iphone-15-pro.tsx +114 -0
- package/src/ui/data-display/kanban/index.ts +4 -0
- package/src/ui/data-display/kanban/kanban-board.tsx +192 -0
- package/src/ui/data-display/kanban/kanban-column.tsx +74 -0
- package/src/ui/data-display/kanban/kanban-item.tsx +50 -0
- package/src/ui/data-display/kanban/kanban-types.ts +21 -0
- package/src/ui/data-display/kpi-card.tsx +68 -0
- package/src/ui/data-display/media-grid.tsx +110 -0
- package/src/ui/data-display/safari.tsx +175 -0
- package/src/ui/data-display/show-more-text.tsx +55 -0
- package/src/ui/data-display/tabs.tsx +68 -0
- package/src/ui/data-display/timeline.tsx +256 -0
- package/src/ui/feedback/alert.tsx +60 -0
- package/src/ui/feedback/context-menu.tsx +245 -0
- package/src/ui/feedback/drawer.tsx +132 -0
- package/src/ui/feedback/error-dialog.tsx +273 -0
- package/src/ui/feedback/index.tsx +183 -0
- package/src/ui/feedback/progress.tsx +32 -0
- package/src/ui/feedback/sheet.tsx +148 -0
- package/src/ui/feedback/sonner.tsx +36 -0
- package/src/ui/forms/command.tsx +157 -0
- package/src/ui/forms/date-picker.tsx +73 -0
- package/src/ui/forms/date-range-picker.tsx +76 -0
- package/src/ui/forms/date-time-picker.tsx +109 -0
- package/src/ui/forms/editor/editor-menu-bar.tsx +394 -0
- package/src/ui/forms/editor/index.tsx +130 -0
- package/src/ui/forms/editor/multi-select-example.tsx +1234 -0
- package/src/ui/forms/emoji-picker.tsx +109 -0
- package/src/ui/forms/file-dropzone.tsx +169 -0
- package/src/ui/forms/file-thumbnail.tsx +29 -0
- package/src/ui/forms/index.tsx +201 -0
- package/src/ui/forms/input-file.tsx +99 -0
- package/src/ui/forms/input-group.tsx +46 -0
- package/src/ui/forms/input-otp.tsx +81 -0
- package/src/ui/forms/input-phone.tsx +172 -0
- package/src/ui/forms/input-spin.tsx +116 -0
- package/src/ui/forms/input-tags.tsx +219 -0
- package/src/ui/forms/input-time.tsx +42 -0
- package/src/ui/forms/multi-select.tsx +629 -0
- package/src/ui/forms/multiple-date-picker.tsx +74 -0
- package/src/ui/forms/radio-group.tsx +42 -0
- package/src/ui/forms/rating.tsx +158 -0
- package/src/ui/forms/time-picker.tsx +57 -0
- package/src/ui/index.tsx +17 -0
- package/src/ui/layout/animated-list.tsx +77 -0
- package/src/ui/layout/animated-sidebar.tsx +294 -0
- package/src/ui/layout/command-menu.tsx +355 -0
- package/src/ui/layout/customizer.tsx +324 -0
- package/src/ui/layout/footer.tsx +43 -0
- package/src/ui/layout/full-screen-toggle.tsx +52 -0
- package/src/ui/layout/header-breadcrumb.tsx +77 -0
- package/src/ui/layout/horizontal-layout-header.tsx +83 -0
- package/src/ui/layout/horizontal-layout.tsx +50 -0
- package/src/ui/layout/index.tsx +25 -0
- package/src/ui/layout/language-dropdown.tsx +103 -0
- package/src/ui/layout/logo.tsx +63 -0
- package/src/ui/layout/main-layout.tsx +57 -0
- package/src/ui/layout/mode-dropdown.tsx +58 -0
- package/src/ui/layout/notification-dropdown.tsx +127 -0
- package/src/ui/layout/page-tabs.tsx +306 -0
- package/src/ui/layout/route-cache.tsx +214 -0
- package/src/ui/layout/sidebar-group-icon-menu.tsx +195 -0
- package/src/ui/layout/sidebar.tsx +279 -0
- package/src/ui/layout/tab-content-cache.tsx +201 -0
- package/src/ui/layout/tab-navigation-provider.tsx +536 -0
- package/src/ui/layout/toggle-mobile-sidebar.tsx +33 -0
- package/src/ui/layout/top-bar-header-menubar.tsx +412 -0
- package/src/ui/layout/user-dropdown.tsx +188 -0
- package/src/ui/layout/vertical-layout-header.tsx +65 -0
- package/src/ui/layout/vertical-layout.tsx +47 -0
- package/src/ui/management/audit-log-page.tsx +209 -0
- package/src/ui/management/cache-management.tsx +349 -0
- package/src/ui/management/index.ts +3 -0
- package/src/ui/management/job-management.tsx +308 -0
- package/src/ui/pages/not-found.tsx +30 -0
- package/src/ui/primitives/badge.tsx +66 -0
- package/src/ui/primitives/breadcrumb.tsx +103 -0
- package/src/ui/primitives/button.tsx +129 -0
- package/src/ui/primitives/calendar.tsx +74 -0
- package/src/ui/primitives/card.tsx +86 -0
- package/src/ui/primitives/checkbox.tsx +31 -0
- package/src/ui/primitives/client.ts +30 -0
- package/src/ui/primitives/combobox.tsx +290 -0
- package/src/ui/primitives/dialog.tsx +121 -0
- package/src/ui/primitives/dropdown-menu.tsx +239 -0
- package/src/ui/primitives/dynamic-icon.tsx +24 -0
- package/src/ui/primitives/index.tsx +134 -0
- package/src/ui/primitives/input-number.tsx +131 -0
- package/src/ui/primitives/input.tsx +22 -0
- package/src/ui/primitives/keyboard.tsx +23 -0
- package/src/ui/primitives/label.tsx +24 -0
- package/src/ui/primitives/menubar.tsx +262 -0
- package/src/ui/primitives/navigation-menu.tsx +157 -0
- package/src/ui/primitives/pagination.tsx +118 -0
- package/src/ui/primitives/popover.tsx +56 -0
- package/src/ui/primitives/prefetch-link.tsx +60 -0
- package/src/ui/primitives/resizable.tsx +59 -0
- package/src/ui/primitives/scroll-area.tsx +63 -0
- package/src/ui/primitives/select.tsx +172 -0
- package/src/ui/primitives/separator.tsx +51 -0
- package/src/ui/primitives/sidebar.tsx +844 -0
- package/src/ui/primitives/slider.tsx +27 -0
- package/src/ui/primitives/status-badge.tsx +47 -0
- package/src/ui/primitives/sticky-layout.tsx +50 -0
- package/src/ui/primitives/switch.tsx +29 -0
- package/src/ui/primitives/table.tsx +116 -0
- package/src/ui/primitives/tabs.tsx +55 -0
- package/src/ui/primitives/toggle-group.tsx +70 -0
- package/src/ui/primitives/toggle.tsx +47 -0
- package/src/ui/primitives/tooltip.tsx +59 -0
- package/src/user/components/dangerous-zone.tsx +34 -0
- package/src/user/components/delete-account-form.tsx +40 -0
- package/src/user/components/index.ts +4 -0
- package/src/user/components/profile-info-form.tsx +390 -0
- package/src/user/components/profile-info.tsx +32 -0
- package/src/user/components/unified-profile-dialog.tsx +1019 -0
- package/src/user/components/user-stats.tsx +27 -0
- package/src/user/components/user-toolbar.tsx +137 -0
- package/src/user/components/users-card-view.tsx +253 -0
- package/src/user/index.ts +11 -0
- package/src/user/pages/user-list-page.tsx +234 -0
- package/src/user/pages/users-client-page.tsx +385 -0
- package/src/user/profile-page.tsx +19 -0
- package/src/user/schemas.ts +68 -0
- package/src/user/types.ts +34 -0
- package/src/user/user-service.ts +538 -0
- package/src/utils/index.ts +906 -0
- package/src/workflow/activity-timeline.tsx +412 -0
- package/src/workflow/approval-workflow.tsx +31 -0
- package/src/workflow/index.ts +2 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goplusvn/core",
|
|
3
3
|
"description": "GoPlusVN Platform Kit - ERP kernel: layout, RBAC, CRUD, multi-tenant, system pages",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.1",
|
|
5
5
|
"private": false,
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"registry": "https://registry.npmjs.org",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"sideEffects": false,
|
|
14
14
|
"files": [
|
|
15
15
|
"dist",
|
|
16
|
+
"src",
|
|
16
17
|
"README.md",
|
|
17
18
|
"CHANGELOG.md"
|
|
18
19
|
],
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AuditLog,
|
|
3
|
+
AuditLogger,
|
|
4
|
+
AuditManagerOptions,
|
|
5
|
+
CreateAuditInput,
|
|
6
|
+
} from "./types";
|
|
7
|
+
import { createLogger } from "../infrastructure/logger";
|
|
8
|
+
import { MemoryAuditLogger } from "./memory-audit-logger";
|
|
9
|
+
|
|
10
|
+
const logger = createLogger("AuditManager");
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Console-based audit logger (default)
|
|
14
|
+
* In production, replace with database or external service
|
|
15
|
+
*/
|
|
16
|
+
class ConsoleAuditLogger implements AuditLogger {
|
|
17
|
+
async log(entry: AuditLog): Promise<void> {
|
|
18
|
+
logger.info(`AUDIT: ${entry.action} ${entry.resource}`, {
|
|
19
|
+
id: entry.id,
|
|
20
|
+
resourceId: entry.resourceId,
|
|
21
|
+
userId: entry.userId,
|
|
22
|
+
changes: entry.changes,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* AuditManager - tracks actions for compliance and debugging
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* import { auditManager } from '@goerp/core/audit';
|
|
33
|
+
*
|
|
34
|
+
* // Log an action
|
|
35
|
+
* await auditManager.log({
|
|
36
|
+
* action: 'update',
|
|
37
|
+
* resource: 'purchase-order',
|
|
38
|
+
* resourceId: '123',
|
|
39
|
+
* userId: session.user.id,
|
|
40
|
+
* changes: { status: { old: 'pending', new: 'approved' } }
|
|
41
|
+
* });
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
class AuditManagerImpl {
|
|
45
|
+
private auditLogger: AuditLogger;
|
|
46
|
+
private registeredActions: Set<string>;
|
|
47
|
+
|
|
48
|
+
constructor(options: AuditManagerOptions = {}) {
|
|
49
|
+
this.auditLogger = options.logger || new MemoryAuditLogger();
|
|
50
|
+
this.registeredActions = new Set(
|
|
51
|
+
options.actions || ["create", "update", "delete", "login", "logout"],
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Set custom audit logger */
|
|
56
|
+
setLogger(auditLogger: AuditLogger): void {
|
|
57
|
+
this.auditLogger = auditLogger;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Register action for automatic auditing */
|
|
61
|
+
registerAction(action: string): void {
|
|
62
|
+
this.registeredActions.add(action);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Check if action is registered for auditing */
|
|
66
|
+
isRegistered(action: string): boolean {
|
|
67
|
+
return this.registeredActions.has(action);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Log an audit entry */
|
|
71
|
+
async log(input: CreateAuditInput): Promise<void> {
|
|
72
|
+
const entry: AuditLog = {
|
|
73
|
+
id: crypto.randomUUID(),
|
|
74
|
+
...input,
|
|
75
|
+
createdAt: new Date(),
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
await this.auditLogger.log(entry);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
logger.error("Failed to log audit entry", { error: String(error) });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get logs if the logger supports it (specifically MemoryAuditLogger)
|
|
87
|
+
*/
|
|
88
|
+
getLogs(filter: any = {}): AuditLog[] {
|
|
89
|
+
if ("getLogs" in this.auditLogger) {
|
|
90
|
+
return (this.auditLogger as any).getLogs(filter);
|
|
91
|
+
}
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Create middleware for automatic API route auditing */
|
|
96
|
+
middleware() {
|
|
97
|
+
return async (
|
|
98
|
+
ctx: {
|
|
99
|
+
action?: { actionName: string; resourceName: string };
|
|
100
|
+
state?: { currentUser?: { id: string }; currentRole?: string };
|
|
101
|
+
request?: { ip?: string; header?: Record<string, string> };
|
|
102
|
+
status?: number;
|
|
103
|
+
},
|
|
104
|
+
next: () => Promise<void>,
|
|
105
|
+
): Promise<void> => {
|
|
106
|
+
const startTime = Date.now();
|
|
107
|
+
let error: Error | null = null;
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
await next();
|
|
111
|
+
} catch (e) {
|
|
112
|
+
error = e as Error;
|
|
113
|
+
throw e;
|
|
114
|
+
} finally {
|
|
115
|
+
if (ctx.action && this.isRegistered(ctx.action.actionName)) {
|
|
116
|
+
await this.log({
|
|
117
|
+
action: ctx.action.actionName,
|
|
118
|
+
resource: ctx.action.resourceName,
|
|
119
|
+
userId: ctx.state?.currentUser?.id,
|
|
120
|
+
roleName: ctx.state?.currentRole,
|
|
121
|
+
ip: ctx.request?.ip,
|
|
122
|
+
userAgent: ctx.request?.header?.["user-agent"],
|
|
123
|
+
status: ctx.status || (error ? 500 : 200),
|
|
124
|
+
metadata: {
|
|
125
|
+
duration: Date.now() - startTime,
|
|
126
|
+
...(error && { error: error.message }),
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Singleton instance
|
|
136
|
+
export const auditManager = new AuditManagerImpl();
|
|
137
|
+
|
|
138
|
+
// Export class for testing
|
|
139
|
+
export { AuditManagerImpl, ConsoleAuditLogger, MemoryAuditLogger };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { AuditLog, AuditLogger } from "./types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Filter options for retrieving audit logs
|
|
5
|
+
*/
|
|
6
|
+
export interface AuditLogFilter {
|
|
7
|
+
fromDate?: Date;
|
|
8
|
+
toDate?: Date;
|
|
9
|
+
userId?: string;
|
|
10
|
+
action?: string;
|
|
11
|
+
resource?: string;
|
|
12
|
+
type?: "info" | "warning" | "error";
|
|
13
|
+
limit?: number;
|
|
14
|
+
offset?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* In-memory audit logger that supports retrieval
|
|
19
|
+
*/
|
|
20
|
+
export class MemoryAuditLogger implements AuditLogger {
|
|
21
|
+
private logs: AuditLog[] = [];
|
|
22
|
+
private readonly maxLogs: number;
|
|
23
|
+
|
|
24
|
+
constructor(maxLogs = 1000) {
|
|
25
|
+
this.maxLogs = maxLogs;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async log(entry: AuditLog): Promise<void> {
|
|
29
|
+
// Add to beginning of array
|
|
30
|
+
this.logs.unshift(entry);
|
|
31
|
+
|
|
32
|
+
// Trim if exceeds max size
|
|
33
|
+
if (this.logs.length > this.maxLogs) {
|
|
34
|
+
this.logs = this.logs.slice(0, this.maxLogs);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get logs with filtering
|
|
40
|
+
*/
|
|
41
|
+
getLogs(filter: AuditLogFilter = {}): AuditLog[] {
|
|
42
|
+
let filtered = this.logs;
|
|
43
|
+
|
|
44
|
+
if (filter.fromDate) {
|
|
45
|
+
filtered = filtered.filter((log) => log.createdAt >= filter.fromDate!);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (filter.toDate) {
|
|
49
|
+
filtered = filtered.filter((log) => log.createdAt <= filter.toDate!);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (filter.userId) {
|
|
53
|
+
filtered = filtered.filter((log) => log.userId === filter.userId);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (filter.action) {
|
|
57
|
+
filtered = filtered.filter((log) => log.action === filter.action);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (filter.resource) {
|
|
61
|
+
filtered = filtered.filter((log) => log.resource === filter.resource);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// "type" filter is a fuzzy mapping based on status code or action name for demo purposes
|
|
65
|
+
if (filter.type) {
|
|
66
|
+
filtered = filtered.filter((log) => {
|
|
67
|
+
if (filter.type === "error") return (log.status || 200) >= 400;
|
|
68
|
+
if (filter.type === "warning")
|
|
69
|
+
return (log.status || 200) >= 300 && (log.status || 200) < 400;
|
|
70
|
+
return (log.status || 200) < 300;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const offset = filter.offset || 0;
|
|
75
|
+
const limit = filter.limit || 50;
|
|
76
|
+
|
|
77
|
+
return filtered.slice(offset, offset + limit);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Clear all logs
|
|
82
|
+
*/
|
|
83
|
+
clear(): void {
|
|
84
|
+
this.logs = [];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Audit Types
|
|
2
|
+
export interface AuditLog {
|
|
3
|
+
id: string;
|
|
4
|
+
/** Action type: create, update, delete, login, logout, custom */
|
|
5
|
+
action: string;
|
|
6
|
+
/** Resource/entity name (e.g., 'purchase-order', 'user') */
|
|
7
|
+
resource: string;
|
|
8
|
+
/** Resource ID */
|
|
9
|
+
resourceId?: string;
|
|
10
|
+
/** User who performed the action */
|
|
11
|
+
userId?: string;
|
|
12
|
+
/** User's role at time of action */
|
|
13
|
+
roleName?: string;
|
|
14
|
+
/** Changes made (for update actions) */
|
|
15
|
+
changes?: Record<string, { old: unknown; new: unknown }>;
|
|
16
|
+
/** Additional metadata */
|
|
17
|
+
metadata?: Record<string, unknown>;
|
|
18
|
+
/** Request IP address */
|
|
19
|
+
ip?: string;
|
|
20
|
+
/** User agent */
|
|
21
|
+
userAgent?: string;
|
|
22
|
+
/** HTTP status code */
|
|
23
|
+
status?: number;
|
|
24
|
+
/** Timestamp */
|
|
25
|
+
createdAt: Date;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface CreateAuditInput {
|
|
29
|
+
action: string;
|
|
30
|
+
resource: string;
|
|
31
|
+
resourceId?: string;
|
|
32
|
+
userId?: string;
|
|
33
|
+
roleName?: string;
|
|
34
|
+
changes?: Record<string, { old: unknown; new: unknown }>;
|
|
35
|
+
metadata?: Record<string, unknown>;
|
|
36
|
+
ip?: string;
|
|
37
|
+
userAgent?: string;
|
|
38
|
+
status?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface AuditLogger {
|
|
42
|
+
log(entry: AuditLog): Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface AuditManagerOptions {
|
|
46
|
+
/** Custom logger implementation */
|
|
47
|
+
logger?: AuditLogger;
|
|
48
|
+
/** Actions to automatically audit */
|
|
49
|
+
actions?: string[];
|
|
50
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Define the shape of the Prisma Client required by this service
|
|
2
|
+
export interface AuthPrismaClient {
|
|
3
|
+
user: any;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// Dynamic import bcryptjs to avoid Edge Runtime issues
|
|
7
|
+
export async function verifyPassword(
|
|
8
|
+
password: string,
|
|
9
|
+
hash: string,
|
|
10
|
+
): Promise<boolean> {
|
|
11
|
+
try {
|
|
12
|
+
const bcrypt = await import("bcryptjs");
|
|
13
|
+
return bcrypt.compareSync(password, hash);
|
|
14
|
+
} catch (error) {
|
|
15
|
+
console.error("Error verifying password", error);
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface AuthenticatedUser {
|
|
21
|
+
id: string;
|
|
22
|
+
email: string | null;
|
|
23
|
+
name: string;
|
|
24
|
+
avatar: string | null;
|
|
25
|
+
status: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface AuthError {
|
|
29
|
+
message: string;
|
|
30
|
+
email?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Authenticate user with email and password
|
|
35
|
+
* Returns user data if successful, throws error if failed
|
|
36
|
+
*/
|
|
37
|
+
export async function authenticateUser(
|
|
38
|
+
db: AuthPrismaClient,
|
|
39
|
+
email: string,
|
|
40
|
+
password: string,
|
|
41
|
+
): Promise<AuthenticatedUser> {
|
|
42
|
+
try {
|
|
43
|
+
// Query user from database by email
|
|
44
|
+
const user = await db.user.findUnique({
|
|
45
|
+
where: { email: email.toLowerCase().trim() },
|
|
46
|
+
select: {
|
|
47
|
+
id: true,
|
|
48
|
+
email: true,
|
|
49
|
+
name: true,
|
|
50
|
+
avatar: true,
|
|
51
|
+
password: true,
|
|
52
|
+
isActive: true,
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Check if user exists
|
|
57
|
+
if (!user) {
|
|
58
|
+
throw new Error("Invalid email or password");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Check if user account is suspended/inactive
|
|
62
|
+
if (!user.isActive) {
|
|
63
|
+
throw new Error("ACCOUNT_SUSPENDED");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Verify password
|
|
67
|
+
if (!user.password) {
|
|
68
|
+
throw new Error("Invalid email or password");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const isPasswordValid = await verifyPassword(password, user.password);
|
|
72
|
+
if (!isPasswordValid) {
|
|
73
|
+
throw new Error("Invalid email or password");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Update last login time
|
|
77
|
+
await db.user.update({
|
|
78
|
+
where: { id: user.id },
|
|
79
|
+
data: { lastLoginAt: new Date() },
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Return user data (without password)
|
|
83
|
+
return {
|
|
84
|
+
id: user.id,
|
|
85
|
+
name: user.name || "",
|
|
86
|
+
email: user.email,
|
|
87
|
+
avatar: user.avatar || null,
|
|
88
|
+
status: "ONLINE",
|
|
89
|
+
};
|
|
90
|
+
} catch (error) {
|
|
91
|
+
// Re-throw error with proper message
|
|
92
|
+
if (error instanceof Error) {
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
throw new Error("Error signing in");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
// @goerp/core/auth
|
|
2
|
+
// Authentication and RBAC utilities for GoERP
|
|
3
|
+
|
|
4
|
+
import type { Session } from "next-auth";
|
|
5
|
+
|
|
6
|
+
export * from "./auth-service";
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// Types
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
export interface Permission {
|
|
13
|
+
resourceCode: string;
|
|
14
|
+
actionCode: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ExtendedUser {
|
|
18
|
+
id: string;
|
|
19
|
+
name?: string | null;
|
|
20
|
+
email?: string | null;
|
|
21
|
+
image?: string | null;
|
|
22
|
+
roles?: string[];
|
|
23
|
+
permissions?: Permission[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface UserSession {
|
|
27
|
+
user: ExtendedUser;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface CrudPermissionResult {
|
|
31
|
+
create: boolean;
|
|
32
|
+
view: boolean;
|
|
33
|
+
update: boolean;
|
|
34
|
+
delete: boolean;
|
|
35
|
+
export: boolean;
|
|
36
|
+
import: boolean;
|
|
37
|
+
approve: boolean;
|
|
38
|
+
reject: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// Constants
|
|
43
|
+
// ============================================================================
|
|
44
|
+
|
|
45
|
+
const ADMIN_ROLE_CODE = "admin";
|
|
46
|
+
const BYPASS_AUTH =
|
|
47
|
+
process.env.BYPASS_AUTH === "true" || process.env.BYPASS_AUTH === "1";
|
|
48
|
+
|
|
49
|
+
// Action code mapping
|
|
50
|
+
const ACTION_CODES: Record<string, string> = {
|
|
51
|
+
create: "create",
|
|
52
|
+
view: "view",
|
|
53
|
+
update: "update",
|
|
54
|
+
delete: "delete",
|
|
55
|
+
export: "export",
|
|
56
|
+
import: "import",
|
|
57
|
+
approve: "approve",
|
|
58
|
+
reject: "reject",
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get action code from action name
|
|
63
|
+
*/
|
|
64
|
+
export function getActionCode(action: string): string {
|
|
65
|
+
return ACTION_CODES[action] || action;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ============================================================================
|
|
69
|
+
// Permission Functions
|
|
70
|
+
// ============================================================================
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get all permissions from session
|
|
74
|
+
*/
|
|
75
|
+
export function getUserPermissions(session: Session | null): Permission[] {
|
|
76
|
+
if (!session?.user) return [];
|
|
77
|
+
const user = session.user as ExtendedUser;
|
|
78
|
+
|
|
79
|
+
if (!user.permissions) {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
return user.permissions;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get CRUD permissions from session for a specific entity
|
|
87
|
+
*/
|
|
88
|
+
export function getCrudPermissionsFromSession(
|
|
89
|
+
session: Session | null,
|
|
90
|
+
entity: string,
|
|
91
|
+
): CrudPermissionResult {
|
|
92
|
+
// TEMPORARY: Return all permissions as true if bypass is enabled
|
|
93
|
+
if (BYPASS_AUTH) {
|
|
94
|
+
return {
|
|
95
|
+
create: true,
|
|
96
|
+
view: true,
|
|
97
|
+
update: true,
|
|
98
|
+
delete: true,
|
|
99
|
+
export: true,
|
|
100
|
+
import: true,
|
|
101
|
+
approve: true,
|
|
102
|
+
reject: true,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!session?.user) {
|
|
107
|
+
return {
|
|
108
|
+
create: false,
|
|
109
|
+
view: false,
|
|
110
|
+
update: false,
|
|
111
|
+
delete: false,
|
|
112
|
+
export: false,
|
|
113
|
+
import: false,
|
|
114
|
+
approve: false,
|
|
115
|
+
reject: false,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const user = session.user as ExtendedUser;
|
|
120
|
+
|
|
121
|
+
if (!user.id) {
|
|
122
|
+
return {
|
|
123
|
+
create: false,
|
|
124
|
+
view: false,
|
|
125
|
+
update: false,
|
|
126
|
+
delete: false,
|
|
127
|
+
export: false,
|
|
128
|
+
import: false,
|
|
129
|
+
approve: false,
|
|
130
|
+
reject: false,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Check admin role first (fastest check)
|
|
135
|
+
const isAdmin =
|
|
136
|
+
user.roles?.includes(ADMIN_ROLE_CODE) ||
|
|
137
|
+
user.roles?.includes("SUPER_ADMIN");
|
|
138
|
+
if (isAdmin) {
|
|
139
|
+
return {
|
|
140
|
+
create: true,
|
|
141
|
+
view: true,
|
|
142
|
+
update: true,
|
|
143
|
+
delete: true,
|
|
144
|
+
export: true,
|
|
145
|
+
import: true,
|
|
146
|
+
approve: true,
|
|
147
|
+
reject: true,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Pre-compute permission keys for fast lookup
|
|
152
|
+
const permissions = user.permissions || [];
|
|
153
|
+
const permissionKeys = new Set(
|
|
154
|
+
permissions.map((p) => `${p.resourceCode}:${p.actionCode}`),
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const hasPermission = (action: string) => {
|
|
158
|
+
const key = `${entity}:${action}`;
|
|
159
|
+
return permissionKeys.has(key);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
create: hasPermission(getActionCode("create")),
|
|
164
|
+
view: hasPermission(getActionCode("view")),
|
|
165
|
+
update: hasPermission(getActionCode("update")),
|
|
166
|
+
delete: hasPermission(getActionCode("delete")),
|
|
167
|
+
export: hasPermission(getActionCode("export")),
|
|
168
|
+
import: hasPermission(getActionCode("import")),
|
|
169
|
+
approve: hasPermission(getActionCode("approve")),
|
|
170
|
+
reject: hasPermission(getActionCode("reject")),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Check if user has a specific permission
|
|
176
|
+
*/
|
|
177
|
+
export function checkPermission(
|
|
178
|
+
session: Session | null,
|
|
179
|
+
resourceCode: string,
|
|
180
|
+
actionCode: string,
|
|
181
|
+
): boolean {
|
|
182
|
+
if (BYPASS_AUTH) {
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (!session?.user) return false;
|
|
187
|
+
const user = session.user as ExtendedUser;
|
|
188
|
+
|
|
189
|
+
if (!user.permissions || user.permissions.length === 0) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Admin role bypass
|
|
194
|
+
if (
|
|
195
|
+
user.roles?.includes(ADMIN_ROLE_CODE) ||
|
|
196
|
+
user.roles?.includes("SUPER_ADMIN")
|
|
197
|
+
) {
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return user.permissions.some(
|
|
202
|
+
(p) => p.resourceCode === resourceCode && p.actionCode === actionCode,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Alias for checkPermission
|
|
208
|
+
*/
|
|
209
|
+
export function hasPermission(
|
|
210
|
+
session: Session | null,
|
|
211
|
+
resourceCode: string,
|
|
212
|
+
actionCode: string,
|
|
213
|
+
): boolean {
|
|
214
|
+
return checkPermission(session, resourceCode, actionCode);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Require permission - throw error if not authorized
|
|
219
|
+
*/
|
|
220
|
+
export function requirePermission(
|
|
221
|
+
session: Session | null,
|
|
222
|
+
resourceCode: string,
|
|
223
|
+
actionCode: string,
|
|
224
|
+
): void {
|
|
225
|
+
if (!checkPermission(session, resourceCode, actionCode)) {
|
|
226
|
+
throw new Error(
|
|
227
|
+
`Unauthorized: User does not have permission ${actionCode} on ${resourceCode}`,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Check if user has a specific role
|
|
234
|
+
*/
|
|
235
|
+
export function hasRole(session: Session | null, roleCode: string): boolean {
|
|
236
|
+
if (!session?.user) return false;
|
|
237
|
+
const user = session.user as ExtendedUser;
|
|
238
|
+
|
|
239
|
+
if (!user.roles) {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
return user.roles.includes(roleCode);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Check if user has any of the specified roles
|
|
247
|
+
*/
|
|
248
|
+
export function hasAnyRole(
|
|
249
|
+
session: Session | null,
|
|
250
|
+
roleCodes: string[],
|
|
251
|
+
): boolean {
|
|
252
|
+
if (!session?.user) return false;
|
|
253
|
+
const user = session.user as ExtendedUser;
|
|
254
|
+
|
|
255
|
+
if (!user.roles) {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
return roleCodes.some((role) => user.roles!.includes(role));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Check if user is admin
|
|
263
|
+
*/
|
|
264
|
+
export function isAdmin(session: Session | null): boolean {
|
|
265
|
+
return hasRole(session, ADMIN_ROLE_CODE);
|
|
266
|
+
}
|