@goplusvn/core 0.1.71 → 0.1.73
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/branding/__tests__/identity.test.ts +101 -0
- package/src/branding/__tests__/logo-route.test.ts +143 -0
- package/src/branding/identity.ts +74 -0
- package/src/branding/index.ts +36 -0
- package/src/branding/logo-route.ts +249 -0
- package/src/branding/pwa.tsx +146 -0
- package/src/branding/types.ts +45 -0
- 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/styles/base.css +16 -0
- 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/src/ui/errors/error-view.tsx +80 -87
- package/src/ui/errors/index.ts +6 -1
- package/src/ui/errors/not-found-view.tsx +89 -16
- package/src/ui/errors/status-page.tsx +132 -0
- package/src/ui/layout/logo.tsx +5 -0
- package/src/ui/pages/not-found.tsx +11 -27
- package/templates/starter-app/AGENTS.md +1 -1
- package/templates/starter-app/next.config.mjs +8 -0
- package/templates/starter-app/package.json +1 -0
- 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/public/images/illustrations/characters/character-01.svg +33 -0
- package/templates/starter-app/public/images/illustrations/characters/character-02.svg +30 -0
- package/templates/starter-app/public/images/illustrations/characters/character-03.svg +30 -0
- package/templates/starter-app/public/images/illustrations/characters/character-04.svg +27 -0
- package/templates/starter-app/public/images/logos/goeat_logo.png +0 -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 +38 -11
- 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 +27 -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/branding/logo/route.ts +25 -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/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/app/layout.tsx +42 -1
- package/templates/starter-app/src/app/manifest.ts +23 -19
- 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/configs/tenant.ts +6 -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 +13 -2
- package/templates/starter-app/src/providers/theme-provider.tsx +4 -3
- package/templates/starter-app/src/proxy.ts +3 -1
- 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,26 +1,30 @@
|
|
|
1
|
+
import { buildAppManifest, resolveBranding } from "@goerp/core/branding"
|
|
2
|
+
|
|
1
3
|
import type { MetadataRoute } from "next"
|
|
2
4
|
|
|
5
|
+
import { i18n } from "@/configs/i18n"
|
|
3
6
|
import { tenant } from "@/configs/tenant"
|
|
7
|
+
import { getSystemCompany } from "@/server/services/company-service"
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
|
-
* Web App Manifest — mức PWA "cài được lên màn hình chính"
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
+
* Web App Manifest — mức PWA "cài được lên màn hình chính", sinh ĐỘNG theo hồ
|
|
11
|
+
* sơ công ty: đổi tên/logo ở /admin/system/company là tên app và icon trên màn
|
|
12
|
+
* hình chính đổi theo, không cần build lại.
|
|
13
|
+
*
|
|
14
|
+
* Offline/service-worker CHƯA nằm ở đây — app cần chạy không mạng (nhà máy
|
|
15
|
+
* sóng yếu) thì thêm serwist ở tầng app, đó là quyết định riêng từng dự án.
|
|
10
16
|
*/
|
|
11
|
-
export
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
],
|
|
25
|
-
}
|
|
17
|
+
export const dynamic = "force-dynamic"
|
|
18
|
+
|
|
19
|
+
export default async function manifest(): Promise<MetadataRoute.Manifest> {
|
|
20
|
+
const company = await getSystemCompany()
|
|
21
|
+
const identity = resolveBranding({
|
|
22
|
+
company,
|
|
23
|
+
tenant,
|
|
24
|
+
// start_url có locale sẵn: "/" bắt mỗi lần mở app đi thêm chặng redirect
|
|
25
|
+
// server, chặng đó không vẽ được gì ngoài màn trắng.
|
|
26
|
+
startUrl: `/${i18n.defaultLocale}`,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
return buildAppManifest(identity)
|
|
26
30
|
}
|
|
@@ -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
|
)
|
|
@@ -8,7 +8,12 @@ export const tenant: TenantConfig = {
|
|
|
8
8
|
id: "starter",
|
|
9
9
|
name: "GoERP Starter",
|
|
10
10
|
branding: {
|
|
11
|
-
//
|
|
11
|
+
// Logo MẶC ĐỊNH của app mới init — file có thật trong public/ của template
|
|
12
|
+
// (goerp-init chép cả public/), nên không 404 như lần trước khi default này
|
|
13
|
+
// là hằng số hardcode trong component Logo. Admin upload logo ở
|
|
14
|
+
// /admin/system/company thì `Company.logoUrl` ĐÈ lên nó (xem
|
|
15
|
+
// src/providers/index.tsx); xoá dòng này thì Logo rơi về monogram chữ cái.
|
|
16
|
+
logo: "/images/logos/goeat_logo.png",
|
|
12
17
|
companyName: "GoERP Starter",
|
|
13
18
|
// Dòng phụ dưới tên app ở sidebar/đăng nhập — để "" là ẩn hẳn.
|
|
14
19
|
tagline: "",
|
|
@@ -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.
|
|
@@ -7,11 +7,13 @@ import { ErrorDialogProvider } from "@goerp/core/ui/feedback"
|
|
|
7
7
|
import { SidebarProvider } from "@goerp/core/ui/primitives"
|
|
8
8
|
import { Toaster } from "sonner"
|
|
9
9
|
|
|
10
|
+
import type { TenantConfig } from "@goerp/core/providers"
|
|
10
11
|
import type { LocaleType } from "@goerp/core/types"
|
|
11
12
|
import type { ReactNode } from "react"
|
|
12
13
|
|
|
13
|
-
import { tenant } from "@/configs/tenant"
|
|
14
14
|
import { navigations } from "@/data/navigations"
|
|
15
|
+
|
|
16
|
+
import { tenant as staticTenant } from "@/configs/tenant"
|
|
15
17
|
import { authBridgeClient } from "@/lib/auth-client"
|
|
16
18
|
|
|
17
19
|
import { ModeProvider } from "./mode-provider"
|
|
@@ -20,14 +22,23 @@ import { ThemeProvider } from "./theme-provider"
|
|
|
20
22
|
export function Providers({
|
|
21
23
|
children,
|
|
22
24
|
locale,
|
|
25
|
+
tenant = staticTenant,
|
|
23
26
|
}: {
|
|
24
27
|
children: ReactNode
|
|
25
28
|
locale: LocaleType
|
|
29
|
+
/**
|
|
30
|
+
* Bản tenant đã áp hồ sơ công ty từ DB — layout server dựng bằng
|
|
31
|
+
* `resolveBranding` + `tenantWithBranding` của `@goerp/core/branding`.
|
|
32
|
+
* Thứ tự thương hiệu: HỒ SƠ CÔNG TY (admin upload) → tenant config tĩnh →
|
|
33
|
+
* logo GoEat mặc định trong `public/`. Bỏ trống ⇒ dùng cấu hình tĩnh.
|
|
34
|
+
*/
|
|
35
|
+
tenant?: TenantConfig
|
|
26
36
|
}) {
|
|
27
37
|
return (
|
|
28
38
|
<SettingsProvider locale={locale}>
|
|
29
39
|
{/* TenantProvider cấp branding (logo / tên / màu chủ đạo) cho Logo và cả
|
|
30
|
-
vỏ app.
|
|
40
|
+
vỏ app. Logo/tên đến từ "Hồ sơ công ty"; màu chủ đạo và tagline vẫn ở
|
|
41
|
+
src/configs/tenant.ts. */}
|
|
31
42
|
<TenantProvider tenant={tenant}>
|
|
32
43
|
<ModeProvider>
|
|
33
44
|
<ThemeProvider>
|
|
@@ -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 ở
|
|
@@ -24,7 +24,9 @@ export const proxy = createAuthProxy({
|
|
|
24
24
|
// và cả đường NGOÀI /api (máy móc không có cookie kiểu /pub, /iclock của
|
|
25
25
|
// máy chấm công). Mỗi entry phải ghi rõ nó tự bảo vệ bằng gì; guardrail
|
|
26
26
|
// `auth/route-outside-api-declared` sẽ bắt route ngoài /api chưa khai ở đây.
|
|
27
|
-
|
|
27
|
+
// /api/branding: logo công ty phục vụ favicon + trang đăng nhập — hai bề mặt
|
|
28
|
+
// render TRƯỚC khi có phiên. Route chỉ lộ đúng cột logo, không đọc gì khác.
|
|
29
|
+
publicPrefixes: ["/api/better-auth", "/api/public", "/api/branding"],
|
|
28
30
|
publicPages: [...i18n.locales.map((l) => `/${l}/sign-in`), "/sign-in"],
|
|
29
31
|
signInPath: `/${i18n.defaultLocale}/sign-in`,
|
|
30
32
|
homePath: `/${i18n.defaultLocale}`,
|
|
@@ -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,
|