@goplusvn/core 0.1.70 → 0.1.72
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/CHANGELOG.md +45 -0
- package/bin/goerp-init.mjs +15 -0
- package/package.json +2 -1
- package/src/auth/index.ts +5 -1
- package/src/auth/proxy-gate.ts +21 -2
- package/src/configs/entities/departments.config.ts +1 -0
- package/src/configs/entities/material-categories.config.ts +1 -0
- package/src/cron/__tests__/cron-schedule.test.ts +76 -0
- package/src/cron/cron-schedule.ts +128 -0
- package/src/cron/simple-cron-job.ts +83 -69
- package/src/crud/components/crud-export-button.tsx +3 -2
- package/src/crud/components/crud-page.tsx +56 -25
- package/src/crud/components/crud-row-actions.tsx +16 -4
- package/src/crud/components/crud-table.tsx +13 -1
- package/src/crud/crud-route-handlers.test.ts +352 -0
- package/src/crud/crud-route-handlers.ts +260 -13
- package/src/crud/index.ts +3 -0
- package/src/crud/lib/coerce.test.ts +118 -0
- package/src/crud/lib/coerce.ts +95 -0
- package/src/crud/lib/crud-utils.ts +4 -0
- package/src/crud/lib/entity-endpoints.test.ts +41 -0
- package/src/crud/lib/entity-endpoints.ts +23 -0
- package/src/crud/lib/errors.ts +14 -0
- package/src/crud/lib/import-request.ts +99 -0
- package/src/crud/lib/mutation-builder.test.ts +114 -0
- package/src/crud/lib/mutation-builder.ts +27 -34
- package/src/crud/lib/permissions.test.ts +57 -0
- package/src/crud/lib/permissions.ts +25 -13
- package/src/crud/lib/query-builder.ts +13 -6
- package/src/crud/lib/translate-config.ts +16 -2
- package/src/crud/pages/entity-crud-page.tsx +11 -8
- package/src/crud/server-service.test.ts +112 -0
- package/src/crud/server-service.ts +52 -27
- package/src/crud/server.ts +11 -1
- package/src/guardrails/index.ts +1 -0
- package/src/guardrails/rules/auth.ts +54 -1
- package/src/guardrails/rules/design.ts +3 -2
- package/src/guardrails/types.ts +17 -8
- package/src/providers/brand-theme.ts +20 -0
- package/src/rbac/pages/permission-catalog-pages.tsx +3 -1
- package/src/security/index.ts +2 -0
- package/src/security/pages/sessions-page.tsx +516 -0
- package/src/types/index.ts +75 -10
- package/src/ui/auth/sign-in-form.tsx +64 -9
- package/src/ui/data-display/__tests__/use-client-pagination.test.ts +59 -0
- package/src/ui/data-display/data-table/__tests__/data-table-row-memo.test.tsx +96 -0
- package/src/ui/data-display/data-table/data-table-context.tsx +5 -0
- package/src/ui/data-display/data-table/data-table.tsx +68 -23
- package/src/ui/data-display/data-table-pagination.tsx +25 -8
- package/src/ui/data-display/index.tsx +1 -0
- package/src/ui/data-display/use-client-pagination.ts +48 -0
- package/src/ui/errors/error-view.tsx +164 -0
- package/src/ui/errors/index.ts +2 -0
- package/src/ui/errors/not-found-view.tsx +44 -0
- package/src/ui/index.tsx +1 -0
- package/src/ui/layout/logo.tsx +54 -19
- package/src/user/pages/users-client-page.tsx +12 -7
- package/templates/starter-app/.env.example +4 -0
- package/templates/starter-app/AGENTS.md +1 -1
- package/templates/starter-app/README.md +6 -2
- package/templates/starter-app/husky/pre-commit +10 -0
- package/templates/starter-app/package.json +27 -2
- package/templates/starter-app/prisma/migrations/20260807115347_platform_pages/migration.sql +87 -0
- package/templates/starter-app/prisma/migrations/20260808003811_company_profile/migration.sql +25 -0
- package/templates/starter-app/prisma/schema/organization.prisma +20 -0
- package/templates/starter-app/prisma/schema/system.prisma +74 -0
- package/templates/starter-app/prisma/seed.ts +7 -5
- package/templates/starter-app/scripts/migration-new.mjs +89 -0
- package/templates/starter-app/scripts/rbac-sync.ts +49 -21
- package/templates/starter-app/src/__tests__/architecture.test.ts +11 -2
- package/templates/starter-app/src/app/[lang]/(main)/actions/page.tsx +37 -0
- package/templates/starter-app/src/app/[lang]/(main)/admin/system/cache/page.tsx +68 -0
- package/templates/starter-app/src/app/[lang]/(main)/admin/system/company/actions.ts +33 -0
- package/templates/starter-app/src/app/[lang]/(main)/admin/system/company/company-profile-form.tsx +195 -0
- package/templates/starter-app/src/app/[lang]/(main)/admin/system/company/page.tsx +29 -0
- package/templates/starter-app/src/app/[lang]/(main)/crud/[entity]/page.tsx +4 -2
- package/templates/starter-app/src/app/[lang]/(main)/error.tsx +24 -0
- package/templates/starter-app/src/app/[lang]/(main)/layout.tsx +2 -1
- package/templates/starter-app/src/app/[lang]/(main)/not-found.tsx +6 -0
- package/templates/starter-app/src/app/[lang]/(main)/page.tsx +13 -3
- package/templates/starter-app/src/app/[lang]/(main)/resources/page.tsx +39 -0
- package/templates/starter-app/src/app/[lang]/(main)/roles/[id]/edit/page.tsx +5 -2
- package/templates/starter-app/src/app/[lang]/(main)/roles/new/page.tsx +1 -0
- package/templates/starter-app/src/app/[lang]/(main)/roles/page.tsx +1 -0
- package/templates/starter-app/src/app/[lang]/(main)/security/sessions/page.tsx +24 -0
- package/templates/starter-app/src/app/[lang]/(main)/system-categories/page.tsx +30 -0
- package/templates/starter-app/src/app/[lang]/(main)/user/profile/actions.ts +62 -0
- package/templates/starter-app/src/app/[lang]/(main)/user/profile/page.tsx +41 -0
- package/templates/starter-app/src/app/[lang]/(main)/user/profile/profile-client-page.tsx +157 -0
- package/templates/starter-app/src/app/[lang]/(main)/users/page.tsx +69 -0
- package/templates/starter-app/src/app/[lang]/[...not-found]/page.tsx +9 -0
- package/templates/starter-app/src/app/[lang]/error.tsx +26 -0
- package/templates/starter-app/src/app/[lang]/layout.tsx +2 -2
- package/templates/starter-app/src/app/[lang]/not-found.tsx +9 -0
- package/templates/starter-app/src/app/api/admin/system/audit/route.ts +4 -3
- package/templates/starter-app/src/app/api/admin/system/jobs/[name]/history/route.ts +1 -1
- package/templates/starter-app/src/app/api/admin/system/jobs/route.ts +31 -12
- package/templates/starter-app/src/app/api/admin/system/settings/all/route.ts +2 -2
- package/templates/starter-app/src/app/api/admin/system/settings/create/route.ts +10 -3
- package/templates/starter-app/src/app/api/admin/system/settings/delete/route.ts +10 -3
- package/templates/starter-app/src/app/api/admin/system/settings/route.ts +14 -5
- package/templates/starter-app/src/app/api/admin/system/settings/toggle-status/route.ts +16 -7
- package/templates/starter-app/src/app/api/admin/system/settings/update/route.ts +12 -6
- package/templates/starter-app/src/app/api/admin/system/settings/update-full/route.ts +12 -6
- package/templates/starter-app/src/app/api/better-auth/[...all]/route.ts +1 -1
- package/templates/starter-app/src/app/api/branches/route.ts +26 -0
- package/templates/starter-app/src/app/api/crud/[entity]/[id]/route.ts +2 -2
- package/templates/starter-app/src/app/api/crud/[entity]/export/route.ts +16 -0
- package/templates/starter-app/src/app/api/crud/[entity]/import/route.ts +17 -0
- package/templates/starter-app/src/app/api/crud/[entity]/route.ts +2 -2
- package/templates/starter-app/src/app/api/departments/route.ts +26 -0
- package/templates/starter-app/src/app/api/error-logs/[id]/route.ts +4 -2
- package/templates/starter-app/src/app/api/error-logs/route.ts +26 -19
- package/templates/starter-app/src/app/api/files/[...key]/route.ts +1 -2
- package/templates/starter-app/src/app/api/job-titles/route.ts +46 -0
- package/templates/starter-app/src/app/api/notifications/read/route.ts +1 -1
- package/templates/starter-app/src/app/api/notifications/route.ts +1 -1
- package/templates/starter-app/src/app/api/notifications/unread-count/route.ts +1 -1
- package/templates/starter-app/src/app/api/rbac/permissions-version/route.ts +4 -1
- package/templates/starter-app/src/app/api/roles/[id]/route.ts +2 -1
- package/templates/starter-app/src/app/api/roles/route.ts +2 -1
- package/templates/starter-app/src/app/api/security/sessions/[id]/route.ts +52 -0
- package/templates/starter-app/src/app/api/security/sessions/revoke-user/route.ts +44 -0
- package/templates/starter-app/src/app/api/security/sessions/route.ts +101 -0
- package/templates/starter-app/src/app/api/suppliers/route.ts +13 -0
- package/templates/starter-app/src/app/api/system-categories/route.ts +179 -0
- package/templates/starter-app/src/app/api/system-category-groups/route.ts +149 -0
- package/templates/starter-app/src/app/api/tasks/[id]/download/route.ts +7 -3
- package/templates/starter-app/src/app/api/tasks/route.ts +5 -2
- package/templates/starter-app/src/app/api/upload/route.ts +1 -2
- package/templates/starter-app/src/app/api/users/[id]/route.ts +230 -0
- package/templates/starter-app/src/app/api/users/route.ts +141 -0
- package/templates/starter-app/src/app/global-error.tsx +135 -0
- package/templates/starter-app/src/app/globals.css +0 -1
- package/templates/starter-app/src/app/icon.svg +6 -0
- package/templates/starter-app/src/app/manifest.ts +26 -0
- package/templates/starter-app/src/components/layout/main-layout-wrapper.tsx +5 -4
- package/templates/starter-app/src/configs/entities/department.config.ts +7 -4
- package/templates/starter-app/src/configs/entities/index.ts +5 -1
- package/templates/starter-app/src/configs/entities/job-titles.config.ts +121 -0
- package/templates/starter-app/src/configs/entities/system-alerts.config.ts +93 -0
- package/templates/starter-app/src/configs/entities/users.config.ts +80 -0
- package/templates/starter-app/src/configs/i18n.ts +6 -6
- package/templates/starter-app/src/configs/permissions/index.ts +37 -0
- package/templates/starter-app/src/configs/permissions/master-data.permissions.ts +11 -0
- package/templates/starter-app/src/configs/permissions/menu-tree.ts +37 -0
- package/templates/starter-app/src/configs/permissions/system.permissions.ts +29 -0
- package/templates/starter-app/src/configs/tenant.ts +2 -1
- package/templates/starter-app/src/data/dictionary.ts +77 -4
- package/templates/starter-app/src/data/navigations.ts +60 -1
- package/templates/starter-app/src/instrumentation.ts +5 -16
- package/templates/starter-app/src/lib/action-guard.ts +25 -0
- package/templates/starter-app/src/lib/api-handler.ts +27 -19
- package/templates/starter-app/src/lib/auth.ts +3 -1
- package/templates/starter-app/src/lib/better-auth.ts +8 -0
- package/templates/starter-app/src/lib/branch-scope.ts +1 -1
- package/templates/starter-app/src/lib/crud/index.ts +5 -10
- package/templates/starter-app/src/lib/errors/log-server-error.ts +5 -4
- package/templates/starter-app/src/lib/logger.ts +11 -4
- package/templates/starter-app/src/lib/page-guard.ts +2 -3
- package/templates/starter-app/src/lib/prisma.ts +16 -7
- package/templates/starter-app/src/lib/rbac/access.ts +2 -2
- package/templates/starter-app/src/lib/storage.ts +3 -1
- package/templates/starter-app/src/providers/index.tsx +2 -1
- package/templates/starter-app/src/providers/mode-provider.tsx +13 -18
- package/templates/starter-app/src/providers/theme-provider.tsx +27 -12
- package/templates/starter-app/src/proxy.ts +8 -3
- package/templates/starter-app/src/server/services/company-service.ts +49 -0
- package/templates/starter-app/src/server/services/notification-service.ts +6 -6
- package/templates/starter-app/src/server/services/system-config-service.ts +21 -9
- package/templates/starter-app/src/server/services/user-service.ts +104 -0
- package/templates/starter-app/src/server/tasks/handlers/export-departments.ts +15 -9
- package/templates/starter-app/src/server/tasks/task-runner.ts +6 -7
- package/templates/starter-app/tsconfig.json +10 -0
- package/templates/starter-app/vitest.config.ts +17 -1
|
@@ -43,3 +43,40 @@ export function getRegistryResourceCodes(): string[] {
|
|
|
43
43
|
export function isLookupOpenToAuthenticated(code: string): boolean {
|
|
44
44
|
return resourceByCode.get(code)?.lookupPolicy === "authenticated"
|
|
45
45
|
}
|
|
46
|
+
|
|
47
|
+
/** Metadata action cho trang phân quyền / catalog (nhãn tiếng Việt). */
|
|
48
|
+
export interface ActionMetaEntry {
|
|
49
|
+
label?: string
|
|
50
|
+
description?: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const BASE_ACTION_META: Record<string, ActionMetaEntry> = {
|
|
54
|
+
view: { label: "Xem" },
|
|
55
|
+
create: { label: "Tạo" },
|
|
56
|
+
update: { label: "Sửa" },
|
|
57
|
+
delete: { label: "Xóa" },
|
|
58
|
+
export: { label: "Xuất" },
|
|
59
|
+
import: { label: "Nhập" },
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let actionMetaCache: Record<string, ActionMetaEntry> | null = null
|
|
63
|
+
|
|
64
|
+
export function getActionMetaMap(): Record<string, ActionMetaEntry> {
|
|
65
|
+
if (actionMetaCache) return actionMetaCache
|
|
66
|
+
const map: Record<string, ActionMetaEntry> = { ...BASE_ACTION_META }
|
|
67
|
+
for (const feature of permissionRegistry) {
|
|
68
|
+
for (const a of feature.customActions ?? []) {
|
|
69
|
+
map[a.code] = { label: a.name, description: a.description }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
actionMetaCache = map
|
|
73
|
+
return map
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function getLookupPolicyMap(): Record<string, LookupPolicy> {
|
|
77
|
+
const map: Record<string, LookupPolicy> = {}
|
|
78
|
+
resourceByCode.forEach((r, code) => {
|
|
79
|
+
map[code] = r.lookupPolicy ?? "view-only"
|
|
80
|
+
})
|
|
81
|
+
return map
|
|
82
|
+
}
|
|
@@ -25,6 +25,17 @@ const masterDataPermissions: FeaturePermissions = {
|
|
|
25
25
|
staff: ["view", "export"],
|
|
26
26
|
},
|
|
27
27
|
},
|
|
28
|
+
{
|
|
29
|
+
code: "job-title",
|
|
30
|
+
name: "Chức danh",
|
|
31
|
+
group: "Danh mục",
|
|
32
|
+
icon: "Briefcase",
|
|
33
|
+
order: 2,
|
|
34
|
+
actions: ["view", "create", "update", "delete", "export", "import"],
|
|
35
|
+
// Chức danh đổ vào picker hồ sơ nhân sự.
|
|
36
|
+
lookupPolicy: "authenticated",
|
|
37
|
+
defaultGrants: { admin: "*", staff: ["view"] },
|
|
38
|
+
},
|
|
28
39
|
],
|
|
29
40
|
}
|
|
30
41
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { navigations } from "@/data/navigations"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CÂY MENU cho các màn phân quyền (RoleFormPage, ResourceCatalogPage) — dựng
|
|
5
|
+
* từ navigations. PURE DATA (serializable), dùng được cả server lẫn client.
|
|
6
|
+
*/
|
|
7
|
+
export interface MenuTreeItem {
|
|
8
|
+
title: string
|
|
9
|
+
href?: string
|
|
10
|
+
icon?: string
|
|
11
|
+
resource: string
|
|
12
|
+
}
|
|
13
|
+
export interface MenuTreeSection {
|
|
14
|
+
title: string
|
|
15
|
+
icon?: string
|
|
16
|
+
items: MenuTreeItem[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const MENU_TREE: MenuTreeSection[] = navigations
|
|
20
|
+
.map((section) => ({
|
|
21
|
+
title: section.title,
|
|
22
|
+
icon: (section as { iconName?: string }).iconName,
|
|
23
|
+
items: ((section.items ?? []) as unknown as Array<Record<string, unknown>>)
|
|
24
|
+
.filter((item) => item.resource && item.href)
|
|
25
|
+
.map((item) => ({
|
|
26
|
+
title: item.title as string,
|
|
27
|
+
href: item.href as string,
|
|
28
|
+
icon: item.iconName as string | undefined,
|
|
29
|
+
resource: item.resource as string,
|
|
30
|
+
})),
|
|
31
|
+
}))
|
|
32
|
+
.filter((s) => s.items.length > 0)
|
|
33
|
+
|
|
34
|
+
/** resource → href của trang chính (ActionCatalogPage dùng để link chéo). */
|
|
35
|
+
export const RESOURCE_PAGES: Record<string, string> = Object.fromEntries(
|
|
36
|
+
MENU_TREE.flatMap((s) => s.items.map((i) => [i.resource, i.href ?? ""]))
|
|
37
|
+
)
|
|
@@ -64,6 +64,35 @@ const systemPermissions: FeaturePermissions = {
|
|
|
64
64
|
},
|
|
65
65
|
|
|
66
66
|
// ── Cấu hình ─────────────────────────────────────────────────────────
|
|
67
|
+
{
|
|
68
|
+
code: "system-category",
|
|
69
|
+
name: "Danh mục hệ thống",
|
|
70
|
+
group: "Hệ thống",
|
|
71
|
+
icon: "FolderTree",
|
|
72
|
+
order: 17,
|
|
73
|
+
actions: ["view", "create", "update", "delete"],
|
|
74
|
+
// Danh mục dùng chung đổ vào form/picker toàn app.
|
|
75
|
+
lookupPolicy: "authenticated",
|
|
76
|
+
defaultGrants: { admin: "*" },
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
code: "system-alert",
|
|
80
|
+
name: "Cảnh báo hệ thống",
|
|
81
|
+
group: "Hệ thống",
|
|
82
|
+
icon: "Bell",
|
|
83
|
+
order: 18,
|
|
84
|
+
actions: ["view", "create", "update", "delete", "export"],
|
|
85
|
+
defaultGrants: { admin: "*" },
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
code: "system-cache",
|
|
89
|
+
name: "Bộ nhớ đệm",
|
|
90
|
+
group: "Hệ thống",
|
|
91
|
+
icon: "Database",
|
|
92
|
+
order: 19,
|
|
93
|
+
actions: ["view", "update"],
|
|
94
|
+
defaultGrants: { admin: "*" },
|
|
95
|
+
},
|
|
67
96
|
{
|
|
68
97
|
code: "system-setting",
|
|
69
98
|
name: "Cấu hình chung",
|
|
@@ -10,7 +10,8 @@ export const tenant: TenantConfig = {
|
|
|
10
10
|
branding: {
|
|
11
11
|
// logo: "/logo.png",
|
|
12
12
|
companyName: "GoERP Starter",
|
|
13
|
-
|
|
13
|
+
// Dòng phụ dưới tên app ở sidebar/đăng nhập — để "" là ẩn hẳn.
|
|
14
|
+
tagline: "",
|
|
14
15
|
// primaryColor: "262 83% 58%", // HSL triple → reskins --primary at runtime
|
|
15
16
|
// favicon: "/favicon.ico",
|
|
16
17
|
},
|
|
@@ -1,8 +1,81 @@
|
|
|
1
1
|
import type { DictionaryType } from "@goerp/core/hooks"
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* i18n dictionary
|
|
5
|
-
*
|
|
6
|
-
* `
|
|
4
|
+
* i18n dictionary của shell + CRUD engine.
|
|
5
|
+
*
|
|
6
|
+
* Block `crud` là BẮT BUỘC có: engine CRUD của core tra nhãn qua các key
|
|
7
|
+
* `crud.common.*` / `crud.messages.*` — thiếu thì UI hiện nguyên raw key
|
|
8
|
+
* (vd chip trạng thái hiện "crud.common.options.active" thay vì "Hoạt động").
|
|
9
|
+
* Sidebar thì fall back về title trong navigations nên `navigation` để trống
|
|
10
|
+
* được; localize dần bằng cách thêm `{ navigation: {...}, label: {...} }`.
|
|
7
11
|
*/
|
|
8
|
-
export const dictionary: DictionaryType = {
|
|
12
|
+
export const dictionary: DictionaryType = {
|
|
13
|
+
crud: {
|
|
14
|
+
common: {
|
|
15
|
+
create: "Tạo",
|
|
16
|
+
edit: "Sửa",
|
|
17
|
+
delete: "Xóa",
|
|
18
|
+
save: "Lưu",
|
|
19
|
+
saving: "Đang lưu...",
|
|
20
|
+
cancel: "Hủy",
|
|
21
|
+
submit: "Gửi",
|
|
22
|
+
update: "Cập nhật",
|
|
23
|
+
search: "Tìm kiếm...",
|
|
24
|
+
filter: "Lọc",
|
|
25
|
+
filters: "Bộ lọc",
|
|
26
|
+
export: "Xuất",
|
|
27
|
+
import: "Nhập",
|
|
28
|
+
reset: "Đặt lại",
|
|
29
|
+
clear: "Xóa",
|
|
30
|
+
clearAll: "Xóa tất cả",
|
|
31
|
+
actions: "Hành động",
|
|
32
|
+
noData: "Không có dữ liệu",
|
|
33
|
+
loading: "Đang tải...",
|
|
34
|
+
success: "Thành công",
|
|
35
|
+
error: "Lỗi",
|
|
36
|
+
confirm: "Xác nhận",
|
|
37
|
+
close: "Đóng",
|
|
38
|
+
detail: "Chi tiết",
|
|
39
|
+
selectAll: "Chọn tất cả",
|
|
40
|
+
selected: "đã chọn",
|
|
41
|
+
item: "mục",
|
|
42
|
+
items: "mục",
|
|
43
|
+
add: "Thêm",
|
|
44
|
+
creating: "Đang tạo...",
|
|
45
|
+
updating: "Đang cập nhật...",
|
|
46
|
+
deleting: "Đang xóa...",
|
|
47
|
+
savedSuccessfully: "Đã lưu thành công!",
|
|
48
|
+
createdSuccessfully: "Đã tạo thành công!",
|
|
49
|
+
updatedSuccessfully: "Đã cập nhật thành công!",
|
|
50
|
+
deleteSelected: "Xóa đã chọn",
|
|
51
|
+
addNew: "Thêm mới",
|
|
52
|
+
confirmDeleteTitle: "Xóa {{entity}}?",
|
|
53
|
+
confirmBulkDeleteTitle: "Xóa {{count}} {{entities}}?",
|
|
54
|
+
confirmDeleteDescription:
|
|
55
|
+
'Bạn có chắc chắn muốn xóa "{{name}}"? Hành động này không thể hoàn tác.',
|
|
56
|
+
confirmBulkDeleteDescription:
|
|
57
|
+
"Bạn có chắc chắn muốn xóa {{count}} {{entities}}? Hành động này không thể hoàn tác.",
|
|
58
|
+
cannotBeUndone: "Hành động này không thể hoàn tác.",
|
|
59
|
+
options: {
|
|
60
|
+
active: "Hoạt động",
|
|
61
|
+
inactive: "Tạm ngưng",
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
messages: {
|
|
65
|
+
created: "{{entity}} đã được tạo thành công",
|
|
66
|
+
updated: "{{entity}} đã được cập nhật thành công",
|
|
67
|
+
deleted: "{{entity}} đã được xóa thành công",
|
|
68
|
+
bulkDeleted: "{{count}} {{entities}} đã được xóa thành công",
|
|
69
|
+
deleteConfirm: "Bạn có chắc chắn muốn xóa {{entity}} này không?",
|
|
70
|
+
bulkDeleteConfirm:
|
|
71
|
+
"Bạn có chắc chắn muốn xóa {{count}} {{entities}} không?",
|
|
72
|
+
deleteFailed: "Không thể xóa {{entity}}",
|
|
73
|
+
saveFailed: "Không thể lưu {{entity}}",
|
|
74
|
+
loadFailed: "Không thể tải dữ liệu. Vui lòng thử lại.",
|
|
75
|
+
exportSuccess: 'File "{{filename}}" đã được tải xuống thành công',
|
|
76
|
+
exportFailed: "Xuất dữ liệu thất bại. Vui lòng thử lại.",
|
|
77
|
+
importSuccess: "{{count}} {{entities}} đã được nhập thành công",
|
|
78
|
+
importFailed: "Nhập dữ liệu thất bại. Vui lòng thử lại.",
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
}
|
|
@@ -25,19 +25,78 @@ export const navigations: NavigationType[] = [
|
|
|
25
25
|
href: "/crud/departments",
|
|
26
26
|
resource: "department",
|
|
27
27
|
},
|
|
28
|
+
{
|
|
29
|
+
title: "Chức danh",
|
|
30
|
+
iconName: "Briefcase",
|
|
31
|
+
href: "/crud/job-titles",
|
|
32
|
+
resource: "job-title",
|
|
33
|
+
},
|
|
28
34
|
],
|
|
29
35
|
},
|
|
30
36
|
{
|
|
31
37
|
title: "Hệ thống",
|
|
32
38
|
iconName: "ShieldCheck",
|
|
33
39
|
items: [
|
|
34
|
-
{
|
|
40
|
+
{
|
|
41
|
+
title: "Người dùng",
|
|
42
|
+
iconName: "Users",
|
|
43
|
+
href: "/users",
|
|
44
|
+
resource: "user",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
title: "Phiên đăng nhập",
|
|
48
|
+
iconName: "KeyRound",
|
|
49
|
+
href: "/security/sessions",
|
|
50
|
+
resource: "user",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
title: "Vai trò",
|
|
54
|
+
iconName: "ShieldCheck",
|
|
55
|
+
href: "/roles",
|
|
56
|
+
resource: "role",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
title: "Tài nguyên",
|
|
60
|
+
iconName: "Boxes",
|
|
61
|
+
href: "/resources",
|
|
62
|
+
resource: "resource",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
title: "Hành động",
|
|
66
|
+
iconName: "MousePointerClick",
|
|
67
|
+
href: "/actions",
|
|
68
|
+
resource: "action",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
title: "Hồ sơ công ty",
|
|
72
|
+
iconName: "Building",
|
|
73
|
+
href: "/admin/system/company",
|
|
74
|
+
resource: "system-setting",
|
|
75
|
+
},
|
|
35
76
|
{
|
|
36
77
|
title: "Cấu hình",
|
|
37
78
|
iconName: "Settings2",
|
|
38
79
|
href: "/admin/system/settings",
|
|
39
80
|
resource: "system-setting",
|
|
40
81
|
},
|
|
82
|
+
{
|
|
83
|
+
title: "Danh mục hệ thống",
|
|
84
|
+
iconName: "FolderTree",
|
|
85
|
+
href: "/system-categories",
|
|
86
|
+
resource: "system-category",
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
title: "Cảnh báo hệ thống",
|
|
90
|
+
iconName: "Bell",
|
|
91
|
+
href: "/crud/system-alerts",
|
|
92
|
+
resource: "system-alert",
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
title: "Bộ nhớ đệm",
|
|
96
|
+
iconName: "Database",
|
|
97
|
+
href: "/admin/system/cache",
|
|
98
|
+
resource: "system-cache",
|
|
99
|
+
},
|
|
41
100
|
{
|
|
42
101
|
title: "Nhật ký hoạt động",
|
|
43
102
|
iconName: "Scroll",
|
|
@@ -42,23 +42,12 @@ export async function register() {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
|
-
*
|
|
46
|
-
* "0 1 * * *"
|
|
47
|
-
*
|
|
48
|
-
* chạy đúng giờ mong muốn.
|
|
45
|
+
* Từ core 0.1.71, biểu thức cron 5 trường chạy ĐÚNG GIỜ ĐỊA PHƯƠNG của tiến
|
|
46
|
+
* trình ("0 1 * * *" = 1 giờ sáng thật — Dockerfile đã set TZ=Asia/Ho_Chi_Minh)
|
|
47
|
+
* nên job hằng ngày cứ khai lịch thẳng, KHÔNG cần mẹo gác giờ nữa.
|
|
49
48
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* cronManager.addJob({
|
|
54
|
-
* name: "daily-report",
|
|
55
|
-
* cronTime: "0 * * * *",
|
|
56
|
-
* start: true,
|
|
57
|
-
* onTick: async () => {
|
|
58
|
-
* if (!isLocalHour(1)) return
|
|
59
|
-
* await sendDailyReport()
|
|
60
|
-
* },
|
|
61
|
-
* })
|
|
49
|
+
* Hàm dưới chỉ còn cho ca đặc thù: job muốn gác theo MỘT múi giờ KHÁC múi giờ
|
|
50
|
+
* tiến trình (vd server chạy UTC nhưng nghiệp vụ theo giờ VN).
|
|
62
51
|
*/
|
|
63
52
|
export function isLocalHour(hour: number, timeZone = "Asia/Ho_Chi_Minh"): boolean {
|
|
64
53
|
return (
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { checkPermission } from "@goerp/core/auth"
|
|
2
|
+
|
|
3
|
+
import type { Session } from "@/types/session"
|
|
4
|
+
|
|
5
|
+
import { getSession } from "@/lib/auth"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Cổng cho SERVER ACTION — tương đương apiHandler({resource, action}) của tầng
|
|
9
|
+
* API. Action là endpoint POST thật sự (mọi client đã đăng nhập gọi được) nên
|
|
10
|
+
* PHẢI gác quyền; guardrail `auth/server-action-bare-session` cấm gọi
|
|
11
|
+
* getSession() trần trong file "use server" để ép đi qua đây.
|
|
12
|
+
*
|
|
13
|
+
* const session = await requirePermission("system-setting", "update")
|
|
14
|
+
*/
|
|
15
|
+
export async function requirePermission(
|
|
16
|
+
resource: string,
|
|
17
|
+
action = "view"
|
|
18
|
+
): Promise<Session> {
|
|
19
|
+
const session = await getSession()
|
|
20
|
+
if (!session?.user) throw new Error("Unauthorized")
|
|
21
|
+
if (!checkPermission(session, resource, action)) {
|
|
22
|
+
throw new Error(`Forbidden: ${resource}:${action}`)
|
|
23
|
+
}
|
|
24
|
+
return session
|
|
25
|
+
}
|
|
@@ -15,13 +15,11 @@
|
|
|
15
15
|
import { NextResponse } from "next/server"
|
|
16
16
|
import { withAuditContext } from "@goerp/core/audit"
|
|
17
17
|
import { checkPermission as coreCheckPermission } from "@goerp/core/auth"
|
|
18
|
-
import {
|
|
19
|
-
createApiHandler,
|
|
20
|
-
type ApiHandlerMiddleware,
|
|
21
|
-
} from "@goerp/core/auth/api-handler"
|
|
18
|
+
import { createApiHandler } from "@goerp/core/auth/api-handler"
|
|
22
19
|
|
|
23
|
-
import type { NextRequest } from "next/server"
|
|
24
20
|
import type { Session } from "@/types/session"
|
|
21
|
+
import type { ApiHandlerMiddleware } from "@goerp/core/auth/api-handler"
|
|
22
|
+
import type { NextRequest } from "next/server"
|
|
25
23
|
|
|
26
24
|
import { getRegistryResource } from "@/configs/permissions"
|
|
27
25
|
import { getSession } from "@/lib/auth"
|
|
@@ -40,7 +38,7 @@ interface HandlerContext<TParams = Record<string, string>> {
|
|
|
40
38
|
|
|
41
39
|
type Handler<TParams = Record<string, string>> = (
|
|
42
40
|
req: NextRequest,
|
|
43
|
-
ctx: HandlerContext<TParams
|
|
41
|
+
ctx: HandlerContext<TParams>
|
|
44
42
|
) => Promise<Response>
|
|
45
43
|
|
|
46
44
|
interface ApiHandlerOptions {
|
|
@@ -70,7 +68,10 @@ function assertResourceDeclared(resource: string) {
|
|
|
70
68
|
* trong lib/prisma.ts đọc context này; thiếu nó thì mọi dòng audit_logs đều
|
|
71
69
|
* ghi tác nhân rỗng.
|
|
72
70
|
*/
|
|
73
|
-
const auditContext: ApiHandlerMiddleware<Session> = async (
|
|
71
|
+
const auditContext: ApiHandlerMiddleware<Session> = async (
|
|
72
|
+
next,
|
|
73
|
+
{ req, session }
|
|
74
|
+
) => {
|
|
74
75
|
if (!session) return next()
|
|
75
76
|
const ip =
|
|
76
77
|
req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ||
|
|
@@ -78,7 +79,7 @@ const auditContext: ApiHandlerMiddleware<Session> = async (next, { req, session
|
|
|
78
79
|
undefined
|
|
79
80
|
return (await withAuditContext(
|
|
80
81
|
{ userId: session.user.id, userName: session.user.name, ip },
|
|
81
|
-
() => next()
|
|
82
|
+
() => next()
|
|
82
83
|
)) as Response
|
|
83
84
|
}
|
|
84
85
|
|
|
@@ -87,11 +88,14 @@ const auditContext: ApiHandlerMiddleware<Session> = async (next, { req, session
|
|
|
87
88
|
* biết user được xem chi nhánh nào. Phạm vi được tính LƯỜI — request không đụng
|
|
88
89
|
* model nào bị guard thì `resolve` không bao giờ chạy.
|
|
89
90
|
*/
|
|
90
|
-
const branchScope: ApiHandlerMiddleware<Session> = async (
|
|
91
|
+
const branchScope: ApiHandlerMiddleware<Session> = async (
|
|
92
|
+
next,
|
|
93
|
+
{ session }
|
|
94
|
+
) => {
|
|
91
95
|
if (!session) return next()
|
|
92
96
|
return (await runWithBranchScope(
|
|
93
97
|
{ resolve: () => getBranchScope(session) },
|
|
94
|
-
() => next()
|
|
98
|
+
() => next()
|
|
95
99
|
)) as Response
|
|
96
100
|
}
|
|
97
101
|
|
|
@@ -108,7 +112,8 @@ async function onApiError(error: unknown, req: Request): Promise<Response> {
|
|
|
108
112
|
userAgent: req.headers.get("user-agent") || undefined,
|
|
109
113
|
})
|
|
110
114
|
|
|
111
|
-
const message =
|
|
115
|
+
const message =
|
|
116
|
+
error instanceof Error ? error.message : "Internal Server Error"
|
|
112
117
|
return NextResponse.json(
|
|
113
118
|
{
|
|
114
119
|
error: message,
|
|
@@ -116,9 +121,14 @@ async function onApiError(error: unknown, req: Request): Promise<Response> {
|
|
|
116
121
|
// đang in — production thì tuyệt đối không lộ.
|
|
117
122
|
...(process.env.NODE_ENV === "production"
|
|
118
123
|
? {}
|
|
119
|
-
: {
|
|
124
|
+
: {
|
|
125
|
+
debug: {
|
|
126
|
+
message,
|
|
127
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
128
|
+
},
|
|
129
|
+
}),
|
|
120
130
|
},
|
|
121
|
-
{ status: 500 }
|
|
131
|
+
{ status: 500 }
|
|
122
132
|
)
|
|
123
133
|
}
|
|
124
134
|
|
|
@@ -128,9 +138,7 @@ const coreApiHandler = createApiHandler<Session>({
|
|
|
128
138
|
const session = await getSession()
|
|
129
139
|
return session?.user ? session : null
|
|
130
140
|
},
|
|
131
|
-
checkPermission:
|
|
132
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
133
|
-
coreCheckPermission(session as any, resource, action),
|
|
141
|
+
checkPermission: coreCheckPermission,
|
|
134
142
|
assertResource: assertResourceDeclared,
|
|
135
143
|
wrap: [auditContext, branchScope],
|
|
136
144
|
onError: onApiError,
|
|
@@ -138,7 +146,7 @@ const coreApiHandler = createApiHandler<Session>({
|
|
|
138
146
|
|
|
139
147
|
export function apiHandler<TParams = Record<string, string>>(
|
|
140
148
|
handler: Handler<TParams>,
|
|
141
|
-
options?: ApiHandlerOptions
|
|
149
|
+
options?: ApiHandlerOptions
|
|
142
150
|
) {
|
|
143
151
|
const route = coreApiHandler(
|
|
144
152
|
(req, ctx) =>
|
|
@@ -147,11 +155,11 @@ export function apiHandler<TParams = Record<string, string>>(
|
|
|
147
155
|
session: ctx.session ?? ({} as Session),
|
|
148
156
|
params: ctx.params as Promise<TParams>,
|
|
149
157
|
}),
|
|
150
|
-
options
|
|
158
|
+
options
|
|
151
159
|
)
|
|
152
160
|
return async (
|
|
153
161
|
req: NextRequest,
|
|
154
162
|
// Route tĩnh (không có segment động) được Next gọi mà không kèm ctx.
|
|
155
|
-
routeCtx?: { params: Promise<TParams> }
|
|
163
|
+
routeCtx?: { params: Promise<TParams> }
|
|
156
164
|
): Promise<Response> => route(req, routeCtx as never)
|
|
157
165
|
}
|
|
@@ -50,7 +50,9 @@ export const getSession = cache(async (): Promise<Session | null> => {
|
|
|
50
50
|
return {
|
|
51
51
|
user: result.user,
|
|
52
52
|
expires:
|
|
53
|
-
expiresAt instanceof Date
|
|
53
|
+
expiresAt instanceof Date
|
|
54
|
+
? expiresAt.toISOString()
|
|
55
|
+
: String(expiresAt ?? ""),
|
|
54
56
|
} as unknown as Session
|
|
55
57
|
})
|
|
56
58
|
|
|
@@ -6,6 +6,7 @@ import { prismaAdapter } from "better-auth/adapters/prisma"
|
|
|
6
6
|
import { APIError } from "better-auth/api"
|
|
7
7
|
import { customSession } from "better-auth/plugins"
|
|
8
8
|
|
|
9
|
+
import { tenant } from "@/configs/tenant"
|
|
9
10
|
import { db } from "@/lib/prisma"
|
|
10
11
|
import { loadUserAccess } from "@/lib/rbac/access"
|
|
11
12
|
|
|
@@ -28,6 +29,13 @@ function buildAuth() {
|
|
|
28
29
|
secret: process.env.BETTER_AUTH_SECRET,
|
|
29
30
|
database: prismaAdapter(db, { provider: "postgresql" }),
|
|
30
31
|
|
|
32
|
+
// Cookie mang tên riêng của app (`<tenant.id>.session_token`): cookie theo
|
|
33
|
+
// HOST không phân biệt port, nên dev nhiều app GoERP trên cùng localhost mà
|
|
34
|
+
// dùng tên mặc định `better-auth.*` là app này mở khoá proxy app kia.
|
|
35
|
+
// proxy.ts phải truyền CÙNG prefix vào getSessionCookie — đổi một nơi thì
|
|
36
|
+
// đổi cả hai.
|
|
37
|
+
advanced: { cookiePrefix: tenant.id },
|
|
38
|
+
|
|
31
39
|
rateLimit: {
|
|
32
40
|
enabled: true,
|
|
33
41
|
window: 60,
|
|
@@ -27,7 +27,7 @@ import type { Session } from "@/types/session"
|
|
|
27
27
|
export function canViewAllBranches(session: Session): boolean {
|
|
28
28
|
return Boolean(
|
|
29
29
|
session.user?.roles?.includes("admin") ||
|
|
30
|
-
checkPermission(session
|
|
30
|
+
checkPermission(session, "branch", "view-all-branches")
|
|
31
31
|
)
|
|
32
32
|
}
|
|
33
33
|
|
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
import { createServerCrudService
|
|
1
|
+
import { createServerCrudService } from "@goerp/core/crud/server"
|
|
2
2
|
|
|
3
3
|
import { db } from "@/lib/prisma"
|
|
4
4
|
|
|
5
|
-
// Engine CRUD generic ở CORE — app chỉ tiêm prisma
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export const crudService = createServerCrudService({
|
|
11
|
-
prisma: db,
|
|
12
|
-
getModelName: (entity) => getModelName(entity, MODEL_MAP),
|
|
13
|
-
})
|
|
5
|
+
// Engine CRUD generic ở CORE — app chỉ tiêm prisma. Tên model Prisma khai
|
|
6
|
+
// ngay trong từng entity config (`modelName: "jobTitle"`…), không còn
|
|
7
|
+
// MODEL_MAP song song ở đây.
|
|
8
|
+
export const crudService = createServerCrudService({ prisma: db })
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { createErrorLogger } from "@goerp/core/errors/server-error"
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
ErrorLoggerDb,
|
|
5
|
+
ServerErrorContext,
|
|
5
6
|
} from "@goerp/core/errors/server-error"
|
|
6
7
|
|
|
7
8
|
import { db } from "@/lib/prisma"
|
|
@@ -12,9 +12,12 @@ type LogContext = Record<string, unknown>
|
|
|
12
12
|
* được stack — bọc lại ở đây thay vì bắt mỗi call site tự trải ra.
|
|
13
13
|
*/
|
|
14
14
|
export const logger = {
|
|
15
|
-
info: (message: string, context?: LogContext) =>
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
info: (message: string, context?: LogContext) =>
|
|
16
|
+
coreLogger.info(message, context),
|
|
17
|
+
warn: (message: string, context?: LogContext) =>
|
|
18
|
+
coreLogger.warn(message, context),
|
|
19
|
+
debug: (message: string, context?: LogContext) =>
|
|
20
|
+
coreLogger.debug(message, context),
|
|
18
21
|
error: (message: string, error?: unknown, context?: LogContext) =>
|
|
19
22
|
coreLogger.error(message, {
|
|
20
23
|
...context,
|
|
@@ -22,7 +25,11 @@ export const logger = {
|
|
|
22
25
|
? {
|
|
23
26
|
error:
|
|
24
27
|
error instanceof Error
|
|
25
|
-
? {
|
|
28
|
+
? {
|
|
29
|
+
name: error.name,
|
|
30
|
+
message: error.message,
|
|
31
|
+
stack: error.stack,
|
|
32
|
+
}
|
|
26
33
|
: error,
|
|
27
34
|
}
|
|
28
35
|
: {}),
|
|
@@ -22,14 +22,13 @@ import { getSession } from "@/lib/auth"
|
|
|
22
22
|
export async function requirePageAccess(
|
|
23
23
|
lang: string,
|
|
24
24
|
resource: string,
|
|
25
|
-
action = "view"
|
|
25
|
+
action = "view"
|
|
26
26
|
): Promise<Session> {
|
|
27
27
|
const session = await getSession()
|
|
28
28
|
const locale = lang || i18n.defaultLocale
|
|
29
29
|
|
|
30
30
|
if (!session?.user) redirect(`/${locale}/sign-in`)
|
|
31
|
-
|
|
32
|
-
if (!checkPermission(session as any, resource, action)) redirect(`/${locale}`)
|
|
31
|
+
if (!checkPermission(session, resource, action)) redirect(`/${locale}`)
|
|
33
32
|
|
|
34
33
|
return session
|
|
35
34
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { PrismaPg } from "@prisma/adapter-pg"
|
|
2
|
-
import { Prisma, PrismaClient } from "@prisma/client"
|
|
3
1
|
import { createAuditExtension } from "@goerp/core/audit"
|
|
4
2
|
import { createBranchGuardExtension } from "@goerp/core/branch-scope"
|
|
5
3
|
import { configureSettingsService } from "@goerp/core/system/services/settings-service"
|
|
4
|
+
import { PrismaPg } from "@prisma/adapter-pg"
|
|
5
|
+
import { Prisma, PrismaClient } from "@prisma/client"
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* COMPOSITION ROOT của app — nơi duy nhất tạo Prisma client và cắm dây cho các
|
|
@@ -14,8 +14,9 @@ import { configureSettingsService } from "@goerp/core/system/services/settings-s
|
|
|
14
14
|
* 3. bọc branch-guard (lưới an toàn phạm vi chi nhánh cho thao tác ĐỌC);
|
|
15
15
|
* 4. gọi configureSettingsService(db) để core đọc SystemConfig thật.
|
|
16
16
|
*
|
|
17
|
-
* Các seam còn lại
|
|
18
|
-
* src/server/
|
|
17
|
+
* Các seam còn lại cấu hình ở chỗ chúng sống: tasks → src/server/tasks/,
|
|
18
|
+
* notifications → src/server/services/notification-service.ts, cron →
|
|
19
|
+
* src/lib/cron/db-cron-manager.ts (khởi động từ src/instrumentation.ts).
|
|
19
20
|
*/
|
|
20
21
|
const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient }
|
|
21
22
|
|
|
@@ -70,10 +71,18 @@ export const db = rawClient
|
|
|
70
71
|
rawClient,
|
|
71
72
|
// Bảng ghi rất nhiều mà không có giá trị điều tra thì loại ra, kẻo dấu vết
|
|
72
73
|
// nghiệp vụ bị nhấn chìm (vinhhoa đo được 1 bảng chiếm 64% số dòng audit).
|
|
73
|
-
skipModels: [
|
|
74
|
-
|
|
74
|
+
skipModels: [
|
|
75
|
+
"AuditLog",
|
|
76
|
+
"ErrorLog",
|
|
77
|
+
"Session",
|
|
78
|
+
"Account",
|
|
79
|
+
"Verification",
|
|
80
|
+
],
|
|
81
|
+
})
|
|
75
82
|
)
|
|
76
|
-
.$extends(
|
|
83
|
+
.$extends(
|
|
84
|
+
createBranchGuardExtension({ models: GUARDED_MODELS })
|
|
85
|
+
) as unknown as PrismaClient
|
|
77
86
|
|
|
78
87
|
// Core đọc SystemConfig qua service này. Thiếu dòng dưới thì mọi getSettings()
|
|
79
88
|
// lặng lẽ trả giá trị mặc định và saveSettings() ném lỗi.
|