@goplusvn/core 0.1.71 → 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/package.json +2 -1
- package/src/auth/index.ts +5 -1
- package/src/configs/entities/departments.config.ts +1 -0
- package/src/configs/entities/material-categories.config.ts +1 -0
- 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/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/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/rules/design.ts +3 -2
- package/src/types/index.ts +75 -10
- 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/templates/starter-app/AGENTS.md +1 -1
- 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/scripts/rbac-sync.ts +49 -21
- package/templates/starter-app/src/__tests__/architecture.test.ts +3 -2
- package/templates/starter-app/src/app/[lang]/(main)/actions/page.tsx +4 -4
- package/templates/starter-app/src/app/[lang]/(main)/admin/system/cache/page.tsx +1 -2
- package/templates/starter-app/src/app/[lang]/(main)/admin/system/company/actions.ts +7 -5
- package/templates/starter-app/src/app/[lang]/(main)/admin/system/company/company-profile-form.tsx +20 -9
- package/templates/starter-app/src/app/[lang]/(main)/admin/system/company/page.tsx +2 -1
- package/templates/starter-app/src/app/[lang]/(main)/crud/[entity]/page.tsx +4 -2
- package/templates/starter-app/src/app/[lang]/(main)/layout.tsx +2 -1
- package/templates/starter-app/src/app/[lang]/(main)/page.tsx +4 -4
- package/templates/starter-app/src/app/[lang]/(main)/resources/page.tsx +5 -5
- 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)/user/profile/actions.ts +4 -2
- package/templates/starter-app/src/app/[lang]/(main)/user/profile/page.tsx +3 -1
- package/templates/starter-app/src/app/[lang]/(main)/user/profile/profile-client-page.tsx +13 -5
- package/templates/starter-app/src/app/[lang]/(main)/users/page.tsx +3 -2
- package/templates/starter-app/src/app/[lang]/layout.tsx +2 -2
- 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/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/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 +1 -1
- 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 +5 -5
- package/templates/starter-app/src/app/api/security/sessions/revoke-user/route.ts +1 -1
- package/templates/starter-app/src/app/api/security/sessions/route.ts +2 -2
- package/templates/starter-app/src/app/api/system-categories/route.ts +39 -14
- package/templates/starter-app/src/app/api/system-category-groups/route.ts +32 -14
- 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 +20 -12
- package/templates/starter-app/src/app/api/users/route.ts +11 -9
- package/templates/starter-app/src/app/globals.css +0 -1
- 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 +1 -1
- package/templates/starter-app/src/configs/entities/job-titles.config.ts +2 -0
- package/templates/starter-app/src/configs/entities/system-alerts.config.ts +2 -0
- package/templates/starter-app/src/configs/entities/users.config.ts +15 -2
- package/templates/starter-app/src/configs/i18n.ts +6 -6
- package/templates/starter-app/src/configs/permissions/menu-tree.ts +1 -1
- package/templates/starter-app/src/data/dictionary.ts +2 -1
- package/templates/starter-app/src/data/navigations.ts +12 -2
- package/templates/starter-app/src/lib/action-guard.ts +3 -4
- 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/branch-scope.ts +1 -1
- package/templates/starter-app/src/lib/crud/index.ts +5 -12
- 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 +13 -5
- 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/theme-provider.tsx +4 -3
- package/templates/starter-app/src/server/services/company-service.ts +4 -2
- 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 +2 -2
- 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 +8 -0
- package/templates/starter-app/vitest.config.ts +17 -1
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { NextResponse } from "next/server"
|
|
2
|
+
import { getUsersList } from "@/server/services/user-service"
|
|
2
3
|
import bcrypt from "bcryptjs"
|
|
3
4
|
|
|
4
5
|
import { apiHandler } from "@/lib/api-handler"
|
|
5
6
|
import { serverError } from "@/lib/errors/server-error"
|
|
6
7
|
import { db } from "@/lib/prisma"
|
|
7
|
-
import { getUsersList } from "@/server/services/user-service"
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* /api/users — danh sách + tạo người dùng.
|
|
@@ -27,10 +27,12 @@ export const GET = apiHandler(
|
|
|
27
27
|
})
|
|
28
28
|
return NextResponse.json(result)
|
|
29
29
|
} catch (error) {
|
|
30
|
-
return serverError(error, req, {
|
|
30
|
+
return serverError(error, req, {
|
|
31
|
+
message: "Lỗi tải danh sách người dùng",
|
|
32
|
+
})
|
|
31
33
|
}
|
|
32
34
|
},
|
|
33
|
-
{ resource: "user", action: "view" }
|
|
35
|
+
{ resource: "user", action: "view" }
|
|
34
36
|
)
|
|
35
37
|
|
|
36
38
|
export const POST = apiHandler(
|
|
@@ -52,20 +54,20 @@ export const POST = apiHandler(
|
|
|
52
54
|
if (!name) {
|
|
53
55
|
return NextResponse.json(
|
|
54
56
|
{ error: "Tên là bắt buộc", field: "name" },
|
|
55
|
-
{ status: 400 }
|
|
57
|
+
{ status: 400 }
|
|
56
58
|
)
|
|
57
59
|
}
|
|
58
60
|
// Template dùng email làm định danh đăng nhập (User.email là unique).
|
|
59
61
|
if (!email) {
|
|
60
62
|
return NextResponse.json(
|
|
61
63
|
{ error: "Email là bắt buộc", field: "email" },
|
|
62
|
-
{ status: 400 }
|
|
64
|
+
{ status: 400 }
|
|
63
65
|
)
|
|
64
66
|
}
|
|
65
67
|
if (password && password.length < 6) {
|
|
66
68
|
return NextResponse.json(
|
|
67
69
|
{ error: "Mật khẩu phải có ít nhất 6 ký tự", field: "password" },
|
|
68
|
-
{ status: 400 }
|
|
70
|
+
{ status: 400 }
|
|
69
71
|
)
|
|
70
72
|
}
|
|
71
73
|
|
|
@@ -73,7 +75,7 @@ export const POST = apiHandler(
|
|
|
73
75
|
if (existing) {
|
|
74
76
|
return NextResponse.json(
|
|
75
77
|
{ error: "Email đã tồn tại", field: "email" },
|
|
76
|
-
{ status: 400 }
|
|
78
|
+
{ status: 400 }
|
|
77
79
|
)
|
|
78
80
|
}
|
|
79
81
|
|
|
@@ -129,11 +131,11 @@ export const POST = apiHandler(
|
|
|
129
131
|
|
|
130
132
|
return NextResponse.json(
|
|
131
133
|
{ id: created.id, name: created.name, email: created.email },
|
|
132
|
-
{ status: 201 }
|
|
134
|
+
{ status: 201 }
|
|
133
135
|
)
|
|
134
136
|
} catch (error) {
|
|
135
137
|
return serverError(error, req, { message: "Lỗi tạo người dùng" })
|
|
136
138
|
}
|
|
137
139
|
},
|
|
138
|
-
{ resource: "user", action: "create" }
|
|
140
|
+
{ resource: "user", action: "create" }
|
|
139
141
|
)
|
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
/* Tailwind v4 must scan core's component classes (this app's own src is
|
|
9
9
|
auto-scanned). */
|
|
10
10
|
@source "../../node_modules/@goerp/core/src/**/*.tsx";
|
|
11
|
-
@source "../../node_modules/@goplusvn/core/src/**/*.tsx";
|
|
12
11
|
|
|
13
12
|
/* App brand overrides. Core's default brand is indigo. Re-skin either here
|
|
14
13
|
statically, or at runtime via `branding.primaryColor` on <TenantProvider>. */
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
"use client"
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
|
|
3
|
+
import { NotificationBell } from "@goerp/core/notification/ui"
|
|
4
|
+
import { MainLayout } from "@goerp/core/ui"
|
|
5
|
+
|
|
6
|
+
import type { NavigationType } from "@goerp/core/types"
|
|
6
7
|
|
|
7
8
|
export function MainLayoutWrapper({
|
|
8
9
|
children,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { departmentsConfig } from "@goerp/core/configs/entities"
|
|
2
|
-
|
|
3
1
|
import type { EntityConfig } from "@goerp/core/types"
|
|
4
2
|
|
|
3
|
+
import { departmentsConfig } from "@goerp/core/configs/entities"
|
|
4
|
+
|
|
5
5
|
const CRUD_ENDPOINT = "/api/crud/departments"
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -18,7 +18,10 @@ export const departmentConfig: EntityConfig = {
|
|
|
18
18
|
// generic endpoint (core's default points at /api/departments).
|
|
19
19
|
fields: departmentsConfig.fields.map((field) =>
|
|
20
20
|
field.dataSource?.type === "api"
|
|
21
|
-
? {
|
|
22
|
-
|
|
21
|
+
? {
|
|
22
|
+
...field,
|
|
23
|
+
dataSource: { ...field.dataSource, endpoint: CRUD_ENDPOINT },
|
|
24
|
+
}
|
|
25
|
+
: field
|
|
23
26
|
),
|
|
24
27
|
}
|
|
@@ -5,7 +5,7 @@ import { jobTitlesConfig } from "./job-titles.config"
|
|
|
5
5
|
import { systemAlertsConfig } from "./system-alerts.config"
|
|
6
6
|
|
|
7
7
|
// Đăng ký entity → cấu hình. Khóa = số nhiều, khớp URL /crud/<key> + apiEndpoint.
|
|
8
|
-
// Thêm entity mới: import config rồi thêm 1 dòng ở đây (
|
|
8
|
+
// Thêm entity mới: import config rồi thêm 1 dòng ở đây (khai `modelName` trong config).
|
|
9
9
|
export const entityConfigs: Record<string, EntityConfig> = {
|
|
10
10
|
departments: departmentConfig,
|
|
11
11
|
"job-titles": jobTitlesConfig,
|
|
@@ -11,6 +11,8 @@ export const jobTitlesConfig: EntityConfig = {
|
|
|
11
11
|
icon: Briefcase,
|
|
12
12
|
description: "Quản lý danh sách chức danh trong tổ chức",
|
|
13
13
|
apiEndpoint: "/api/crud/job-titles",
|
|
14
|
+
// Tên model Prisma — engine core đọc từ đây, không cần MODEL_MAP ở lib/crud.
|
|
15
|
+
modelName: "jobTitle",
|
|
14
16
|
idField: "id",
|
|
15
17
|
displayField: "name",
|
|
16
18
|
|
|
@@ -14,6 +14,8 @@ export const systemAlertsConfig: EntityConfig = {
|
|
|
14
14
|
icon: Bell,
|
|
15
15
|
description: "Quản lý các thông báo và cảnh báo hệ thống",
|
|
16
16
|
apiEndpoint: "/api/crud/system-alerts",
|
|
17
|
+
// Tên model Prisma — engine core đọc từ đây, không cần MODEL_MAP ở lib/crud.
|
|
18
|
+
modelName: "systemAlert",
|
|
17
19
|
idField: "id",
|
|
18
20
|
displayField: "title",
|
|
19
21
|
|
|
@@ -20,13 +20,26 @@ export const usersConfig: EntityConfig = {
|
|
|
20
20
|
iconName: "Users",
|
|
21
21
|
description: "Quản lý tài khoản đăng nhập, vai trò và chi nhánh",
|
|
22
22
|
apiEndpoint: "/api/users",
|
|
23
|
+
modelName: "user",
|
|
23
24
|
idField: "id",
|
|
24
25
|
displayField: "name",
|
|
25
26
|
|
|
26
27
|
fields: [
|
|
27
|
-
{
|
|
28
|
+
{
|
|
29
|
+
name: "id",
|
|
30
|
+
label: "ID",
|
|
31
|
+
type: "text",
|
|
32
|
+
hideInForm: true,
|
|
33
|
+
hideInTable: true,
|
|
34
|
+
},
|
|
28
35
|
{ name: "name", label: "Họ tên", type: "text", required: true },
|
|
29
|
-
{
|
|
36
|
+
{
|
|
37
|
+
name: "email",
|
|
38
|
+
label: "Email",
|
|
39
|
+
type: "email",
|
|
40
|
+
required: true,
|
|
41
|
+
width: 200,
|
|
42
|
+
},
|
|
30
43
|
{
|
|
31
44
|
name: "roleNames",
|
|
32
45
|
label: "Vai trò",
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
export const i18n = {
|
|
2
|
-
defaultLocale:
|
|
3
|
-
locales: [
|
|
2
|
+
defaultLocale: "vi",
|
|
3
|
+
locales: ["vi", "en"] as const,
|
|
4
4
|
localeDirection: {
|
|
5
|
-
vi:
|
|
6
|
-
en:
|
|
5
|
+
vi: "ltr",
|
|
6
|
+
en: "ltr",
|
|
7
7
|
} as const,
|
|
8
8
|
localeNames: {
|
|
9
|
-
vi:
|
|
10
|
-
en:
|
|
9
|
+
vi: "vietnamese",
|
|
10
|
+
en: "english",
|
|
11
11
|
} as const,
|
|
12
12
|
} as const
|
|
@@ -33,5 +33,5 @@ export const MENU_TREE: MenuTreeSection[] = navigations
|
|
|
33
33
|
|
|
34
34
|
/** resource → href của trang chính (ActionCatalogPage dùng để link chéo). */
|
|
35
35
|
export const RESOURCE_PAGES: Record<string, string> = Object.fromEntries(
|
|
36
|
-
MENU_TREE.flatMap((s) => s.items.map((i) => [i.resource, i.href ?? ""]))
|
|
36
|
+
MENU_TREE.flatMap((s) => s.items.map((i) => [i.resource, i.href ?? ""]))
|
|
37
37
|
)
|
|
@@ -67,7 +67,8 @@ export const dictionary: DictionaryType = {
|
|
|
67
67
|
deleted: "{{entity}} đã được xóa thành công",
|
|
68
68
|
bulkDeleted: "{{count}} {{entities}} đã được xóa thành công",
|
|
69
69
|
deleteConfirm: "Bạn có chắc chắn muốn xóa {{entity}} này không?",
|
|
70
|
-
bulkDeleteConfirm:
|
|
70
|
+
bulkDeleteConfirm:
|
|
71
|
+
"Bạn có chắc chắn muốn xóa {{count}} {{entities}} không?",
|
|
71
72
|
deleteFailed: "Không thể xóa {{entity}}",
|
|
72
73
|
saveFailed: "Không thể lưu {{entity}}",
|
|
73
74
|
loadFailed: "Không thể tải dữ liệu. Vui lòng thử lại.",
|
|
@@ -37,14 +37,24 @@ export const navigations: NavigationType[] = [
|
|
|
37
37
|
title: "Hệ thống",
|
|
38
38
|
iconName: "ShieldCheck",
|
|
39
39
|
items: [
|
|
40
|
-
{
|
|
40
|
+
{
|
|
41
|
+
title: "Người dùng",
|
|
42
|
+
iconName: "Users",
|
|
43
|
+
href: "/users",
|
|
44
|
+
resource: "user",
|
|
45
|
+
},
|
|
41
46
|
{
|
|
42
47
|
title: "Phiên đăng nhập",
|
|
43
48
|
iconName: "KeyRound",
|
|
44
49
|
href: "/security/sessions",
|
|
45
50
|
resource: "user",
|
|
46
51
|
},
|
|
47
|
-
{
|
|
52
|
+
{
|
|
53
|
+
title: "Vai trò",
|
|
54
|
+
iconName: "ShieldCheck",
|
|
55
|
+
href: "/roles",
|
|
56
|
+
resource: "role",
|
|
57
|
+
},
|
|
48
58
|
{
|
|
49
59
|
title: "Tài nguyên",
|
|
50
60
|
iconName: "Boxes",
|
|
@@ -14,13 +14,12 @@ import { getSession } from "@/lib/auth"
|
|
|
14
14
|
*/
|
|
15
15
|
export async function requirePermission(
|
|
16
16
|
resource: string,
|
|
17
|
-
action = "view"
|
|
17
|
+
action = "view"
|
|
18
18
|
): Promise<Session> {
|
|
19
19
|
const session = await getSession()
|
|
20
20
|
if (!session?.user) throw new Error("Unauthorized")
|
|
21
|
-
|
|
22
|
-
if (!checkPermission(s, resource, action)) {
|
|
21
|
+
if (!checkPermission(session, resource, action)) {
|
|
23
22
|
throw new Error(`Forbidden: ${resource}:${action}`)
|
|
24
23
|
}
|
|
25
|
-
return session
|
|
24
|
+
return session
|
|
26
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
|
|
|
@@ -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,15 +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
|
-
"system-alerts": "systemAlert",
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export const crudService = createServerCrudService({
|
|
13
|
-
prisma: db,
|
|
14
|
-
getModelName: (entity) => getModelName(entity, MODEL_MAP),
|
|
15
|
-
})
|
|
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
|
|
@@ -71,10 +71,18 @@ export const db = rawClient
|
|
|
71
71
|
rawClient,
|
|
72
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
|
|
73
73
|
// nghiệp vụ bị nhấn chìm (vinhhoa đo được 1 bảng chiếm 64% số dòng audit).
|
|
74
|
-
skipModels: [
|
|
75
|
-
|
|
74
|
+
skipModels: [
|
|
75
|
+
"AuditLog",
|
|
76
|
+
"ErrorLog",
|
|
77
|
+
"Session",
|
|
78
|
+
"Account",
|
|
79
|
+
"Verification",
|
|
80
|
+
],
|
|
81
|
+
})
|
|
76
82
|
)
|
|
77
|
-
.$extends(
|
|
83
|
+
.$extends(
|
|
84
|
+
createBranchGuardExtension({ models: GUARDED_MODELS })
|
|
85
|
+
) as unknown as PrismaClient
|
|
78
86
|
|
|
79
87
|
// Core đọc SystemConfig qua service này. Thiếu dòng dưới thì mọi getSettings()
|
|
80
88
|
// lặng lẽ trả giá trị mặc định và saveSettings() ném lỗi.
|
|
@@ -10,8 +10,9 @@ import { Toaster } from "sonner"
|
|
|
10
10
|
import type { LocaleType } from "@goerp/core/types"
|
|
11
11
|
import type { ReactNode } from "react"
|
|
12
12
|
|
|
13
|
-
import { tenant } from "@/configs/tenant"
|
|
14
13
|
import { navigations } from "@/data/navigations"
|
|
14
|
+
|
|
15
|
+
import { tenant } from "@/configs/tenant"
|
|
15
16
|
import { authBridgeClient } from "@/lib/auth-client"
|
|
16
17
|
|
|
17
18
|
import { ModeProvider } from "./mode-provider"
|
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect } from "react"
|
|
4
4
|
import { useSettings } from "@goerp/core/hooks"
|
|
5
|
+
|
|
6
|
+
import type { ReactNode } from "react"
|
|
7
|
+
|
|
5
8
|
import {
|
|
6
|
-
applyBrandThemeTokens,
|
|
7
9
|
THEME_PRESETS,
|
|
10
|
+
applyBrandThemeTokens,
|
|
8
11
|
} from "@goerp/core/providers/brand-theme"
|
|
9
12
|
|
|
10
|
-
import type { ReactNode } from "react"
|
|
11
|
-
|
|
12
13
|
/**
|
|
13
14
|
* Áp màu thương hiệu người dùng chọn trong Customizer vào CSS token
|
|
14
15
|
* (--primary, --ring, --sidebar-background…). Bảng màu calibrate sẵn nằm ở
|
|
@@ -34,7 +34,7 @@ export interface CompanyProfileInput {
|
|
|
34
34
|
|
|
35
35
|
export async function updateSystemCompany(
|
|
36
36
|
data: CompanyProfileInput,
|
|
37
|
-
updatedBy: string
|
|
37
|
+
updatedBy: string
|
|
38
38
|
) {
|
|
39
39
|
const existing = await db.company.findFirst({ orderBy: { createdAt: "asc" } })
|
|
40
40
|
if (existing) {
|
|
@@ -43,5 +43,7 @@ export async function updateSystemCompany(
|
|
|
43
43
|
data: { ...data, updatedBy },
|
|
44
44
|
})
|
|
45
45
|
}
|
|
46
|
-
return db.company.create({
|
|
46
|
+
return db.company.create({
|
|
47
|
+
data: { ...data, createdBy: updatedBy, updatedBy },
|
|
48
|
+
})
|
|
47
49
|
}
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import { configureNotificationService } from "@goerp/core/notification"
|
|
2
|
+
|
|
3
|
+
import type { NotificationDb } from "@goerp/core/notification"
|
|
4
|
+
|
|
5
|
+
import { db } from "@/lib/prisma"
|
|
6
|
+
|
|
1
7
|
/**
|
|
2
8
|
* Thông báo trong ứng dụng (chuông ở thanh trên). Engine ở
|
|
3
9
|
* @goerp/core/notification, bảng `notifications` + `push_subscriptions` ship
|
|
@@ -8,12 +14,6 @@
|
|
|
8
14
|
* push vào bundle. Core đã fire-and-forget + catch nên push lỗi không làm hỏng
|
|
9
15
|
* nghiệp vụ đang chạy.
|
|
10
16
|
*/
|
|
11
|
-
import {
|
|
12
|
-
configureNotificationService,
|
|
13
|
-
type NotificationDb,
|
|
14
|
-
} from "@goerp/core/notification"
|
|
15
|
-
|
|
16
|
-
import { db } from "@/lib/prisma"
|
|
17
17
|
|
|
18
18
|
configureNotificationService({
|
|
19
19
|
db: db as unknown as NotificationDb,
|