@goplusvn/core 0.1.59 → 0.1.60
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/PLATFORM.md +18 -0
- package/bin/goerp-init.mjs +141 -0
- package/package.json +4 -4
- package/src/cron/db-cron-manager.ts +3 -3
- package/src/cron/index.ts +2 -2
- package/src/{infrastructure/cron/cron-manager.ts → cron/simple-cron-job.ts} +1 -1
- package/src/infrastructure/__tests__/architecture-verification.spec.ts +13 -97
- package/src/infrastructure/index.ts +4 -7
- package/src/ui/management/index.ts +3 -2
- package/templates/starter-app/.dockerignore +41 -0
- package/templates/starter-app/.env.example +25 -0
- package/templates/starter-app/AGENTS.md +52 -0
- package/templates/starter-app/Dockerfile +74 -0
- package/templates/starter-app/README.md +141 -0
- package/templates/starter-app/gitignore +9 -0
- package/templates/starter-app/next.config.mjs +50 -0
- package/templates/starter-app/package.json +55 -0
- package/templates/starter-app/postcss.config.mjs +5 -0
- package/templates/starter-app/prisma/migrations/20260801000000_init/migration.sql +504 -0
- package/templates/starter-app/prisma/migrations/20260801091814_goerp_audit_logs_0001_init/migration.sql +33 -0
- package/templates/starter-app/prisma/migrations/20260801091815_goerp_background_tasks_0001_init/migration.sql +23 -0
- package/templates/starter-app/prisma/migrations/20260801091816_goerp_error_logs_0001_init/migration.sql +32 -0
- package/templates/starter-app/prisma/migrations/20260801091817_goerp_notifications_0001_init/migration.sql +59 -0
- package/templates/starter-app/prisma/migrations/20260801091818_goerp_system_jobs_0001_init/migration.sql +47 -0
- package/templates/starter-app/prisma/schema/auth.prisma +87 -0
- package/templates/starter-app/prisma/schema/domain.prisma +24 -0
- package/templates/starter-app/prisma/schema/goerp-audit-logs.prisma +23 -0
- package/templates/starter-app/prisma/schema/goerp-background-tasks.prisma +25 -0
- package/templates/starter-app/prisma/schema/goerp-error-logs.prisma +31 -0
- package/templates/starter-app/prisma/schema/goerp-notifications.prisma +49 -0
- package/templates/starter-app/prisma/schema/goerp-system-jobs.prisma +45 -0
- package/templates/starter-app/prisma/schema/organization.prisma +31 -0
- package/templates/starter-app/prisma/schema/rbac.prisma +100 -0
- package/templates/starter-app/prisma/schema/schema.prisma +8 -0
- package/templates/starter-app/prisma/schema/system.prisma +22 -0
- package/templates/starter-app/prisma/seed.ts +127 -0
- package/templates/starter-app/prisma.config.ts +20 -0
- package/templates/starter-app/public/.gitkeep +2 -0
- package/templates/starter-app/scripts/rbac-sync.ts +235 -0
- package/templates/starter-app/src/__tests__/architecture.test.ts +151 -0
- package/templates/starter-app/src/app/[lang]/(main)/admin/system/audit/page.tsx +24 -0
- package/templates/starter-app/src/app/[lang]/(main)/admin/system/error-logs/page.tsx +21 -0
- package/templates/starter-app/src/app/[lang]/(main)/admin/system/jobs/page.tsx +18 -0
- package/templates/starter-app/src/app/[lang]/(main)/admin/system/settings/page.tsx +22 -0
- package/templates/starter-app/src/app/[lang]/(main)/crud/[entity]/page.tsx +41 -0
- package/templates/starter-app/src/app/[lang]/(main)/layout.tsx +13 -0
- package/templates/starter-app/src/app/[lang]/(main)/notifications/page.tsx +13 -0
- package/templates/starter-app/src/app/[lang]/(main)/page.tsx +130 -0
- package/templates/starter-app/src/app/[lang]/(main)/roles/[id]/edit/page.tsx +60 -0
- package/templates/starter-app/src/app/[lang]/(main)/roles/new/page.tsx +41 -0
- package/templates/starter-app/src/app/[lang]/(main)/roles/page.tsx +48 -0
- package/templates/starter-app/src/app/[lang]/(main)/tasks/page.tsx +13 -0
- package/templates/starter-app/src/app/[lang]/(plain)/layout.tsx +10 -0
- package/templates/starter-app/src/app/[lang]/(plain)/sign-in/page.tsx +39 -0
- package/templates/starter-app/src/app/[lang]/layout.tsx +21 -0
- package/templates/starter-app/src/app/api/admin/system/audit/route.ts +60 -0
- package/templates/starter-app/src/app/api/admin/system/jobs/[name]/history/route.ts +39 -0
- package/templates/starter-app/src/app/api/admin/system/jobs/route.ts +96 -0
- package/templates/starter-app/src/app/api/admin/system/settings/all/route.ts +17 -0
- package/templates/starter-app/src/app/api/admin/system/settings/create/route.ts +19 -0
- package/templates/starter-app/src/app/api/admin/system/settings/delete/route.ts +20 -0
- package/templates/starter-app/src/app/api/admin/system/settings/route.ts +49 -0
- package/templates/starter-app/src/app/api/admin/system/settings/toggle-status/route.ts +28 -0
- package/templates/starter-app/src/app/api/admin/system/settings/update/route.ts +24 -0
- package/templates/starter-app/src/app/api/admin/system/settings/update-full/route.ts +23 -0
- package/templates/starter-app/src/app/api/better-auth/[...all]/route.ts +13 -0
- package/templates/starter-app/src/app/api/crud/[entity]/[id]/route.ts +13 -0
- package/templates/starter-app/src/app/api/crud/[entity]/route.ts +14 -0
- package/templates/starter-app/src/app/api/error-logs/[id]/route.ts +23 -0
- package/templates/starter-app/src/app/api/error-logs/route.ts +124 -0
- package/templates/starter-app/src/app/api/files/[...key]/route.ts +25 -0
- package/templates/starter-app/src/app/api/notifications/read/route.ts +29 -0
- package/templates/starter-app/src/app/api/notifications/route.ts +26 -0
- package/templates/starter-app/src/app/api/notifications/unread-count/route.ts +14 -0
- package/templates/starter-app/src/app/api/rbac/permissions-version/route.ts +11 -0
- package/templates/starter-app/src/app/api/roles/[id]/route.ts +14 -0
- package/templates/starter-app/src/app/api/roles/route.ts +18 -0
- package/templates/starter-app/src/app/api/tasks/[id]/download/route.ts +52 -0
- package/templates/starter-app/src/app/api/tasks/route.ts +37 -0
- package/templates/starter-app/src/app/api/upload/route.ts +15 -0
- package/templates/starter-app/src/app/globals.css +15 -0
- package/templates/starter-app/src/app/layout.tsx +16 -0
- package/templates/starter-app/src/app/page.tsx +8 -0
- package/templates/starter-app/src/components/layout/main-layout-wrapper.tsx +30 -0
- package/templates/starter-app/src/configs/entities/department.config.ts +24 -0
- package/templates/starter-app/src/configs/entities/index.ts +13 -0
- package/templates/starter-app/src/configs/i18n.ts +12 -0
- package/templates/starter-app/src/configs/permissions/index.ts +45 -0
- package/templates/starter-app/src/configs/permissions/master-data.permissions.ts +31 -0
- package/templates/starter-app/src/configs/permissions/system.permissions.ts +131 -0
- package/templates/starter-app/src/configs/permissions/types.ts +63 -0
- package/templates/starter-app/src/configs/tenant.ts +18 -0
- package/templates/starter-app/src/data/dictionary.ts +8 -0
- package/templates/starter-app/src/data/navigations.ts +61 -0
- package/templates/starter-app/src/instrumentation.ts +105 -0
- package/templates/starter-app/src/lib/api-handler.ts +157 -0
- package/templates/starter-app/src/lib/auth-client.ts +57 -0
- package/templates/starter-app/src/lib/auth.ts +62 -0
- package/templates/starter-app/src/lib/better-auth.ts +107 -0
- package/templates/starter-app/src/lib/branch-scope.ts +53 -0
- package/templates/starter-app/src/lib/cron/db-cron-manager.ts +42 -0
- package/templates/starter-app/src/lib/crud/index.ts +13 -0
- package/templates/starter-app/src/lib/errors/app-error.ts +2 -0
- package/templates/starter-app/src/lib/errors/error-handler.ts +5 -0
- package/templates/starter-app/src/lib/errors/log-server-error.ts +15 -0
- package/templates/starter-app/src/lib/errors/server-error.ts +11 -0
- package/templates/starter-app/src/lib/logger.ts +30 -0
- package/templates/starter-app/src/lib/page-guard.ts +35 -0
- package/templates/starter-app/src/lib/prisma.ts +80 -0
- package/templates/starter-app/src/lib/rbac/access.ts +87 -0
- package/templates/starter-app/src/lib/storage.ts +28 -0
- package/templates/starter-app/src/providers/index.tsx +54 -0
- package/templates/starter-app/src/providers/mode-provider.tsx +31 -0
- package/templates/starter-app/src/providers/theme-provider.tsx +21 -0
- package/templates/starter-app/src/proxy.ts +45 -0
- package/templates/starter-app/src/server/services/notification-service.ts +31 -0
- package/templates/starter-app/src/server/services/system-config-service.ts +163 -0
- package/templates/starter-app/src/server/tasks/handlers/export-departments.ts +58 -0
- package/templates/starter-app/src/server/tasks/index.ts +16 -0
- package/templates/starter-app/src/server/tasks/task-runner.ts +40 -0
- package/templates/starter-app/src/types/session.ts +29 -0
- package/templates/starter-app/tsconfig.json +47 -0
- package/templates/starter-app/vitest.config.ts +17 -0
- package/src/infrastructure/cron/index.ts +0 -6
- package/src/infrastructure/event-bus/event-bus.ts +0 -145
- package/src/infrastructure/event-bus/index.ts +0 -2
- package/src/infrastructure/event-bus/types.ts +0 -22
- package/src/infrastructure/lock/decorators.ts +0 -67
- package/src/infrastructure/lock/index.ts +0 -2
- package/src/infrastructure/lock/lock-manager.ts +0 -33
- package/src/plugin/apps-registry.ts +0 -97
- package/src/plugin/index.ts +0 -5
- package/src/plugin/types.ts +0 -41
- package/src/ui/management/audit-log-page.tsx +0 -14
- package/src/ui/management/job-management.tsx +0 -308
- package/src/workflow/activity-timeline.tsx +0 -412
- package/src/workflow/approval-workflow.tsx +0 -31
- package/src/workflow/index.ts +0 -2
- /package/src/{infrastructure/cron → cron}/types.ts +0 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { notFound } from "next/navigation"
|
|
2
|
+
import { getCrudPermissions } from "@goerp/core/crud/server"
|
|
3
|
+
import { RoleFormPage } from "@goerp/core/rbac/pages"
|
|
4
|
+
import {
|
|
5
|
+
getActionsForPermissionMatrix,
|
|
6
|
+
getResourcesForPermissionMatrix,
|
|
7
|
+
} from "@goerp/core/rbac/resource-service"
|
|
8
|
+
|
|
9
|
+
import type { Metadata } from "next"
|
|
10
|
+
|
|
11
|
+
import { dictionary } from "@/data/dictionary"
|
|
12
|
+
import { requirePageAccess } from "@/lib/page-guard"
|
|
13
|
+
import { db } from "@/lib/prisma"
|
|
14
|
+
|
|
15
|
+
export const metadata: Metadata = { title: "Sửa vai trò" }
|
|
16
|
+
export const dynamic = "force-dynamic"
|
|
17
|
+
|
|
18
|
+
export default async function EditRolePage({
|
|
19
|
+
params,
|
|
20
|
+
}: {
|
|
21
|
+
params: Promise<{ lang: string; id: string }>
|
|
22
|
+
}) {
|
|
23
|
+
const { lang, id } = await params
|
|
24
|
+
const session = await requirePageAccess(lang, "role", "update")
|
|
25
|
+
|
|
26
|
+
const [permissions, resources, actions, role] = await Promise.all([
|
|
27
|
+
getCrudPermissions(session, "role"),
|
|
28
|
+
getResourcesForPermissionMatrix(db),
|
|
29
|
+
getActionsForPermissionMatrix(db),
|
|
30
|
+
db.role.findUnique({
|
|
31
|
+
where: { id },
|
|
32
|
+
include: { rolePermissions: { select: { resourceCode: true, actionCode: true } } },
|
|
33
|
+
}),
|
|
34
|
+
])
|
|
35
|
+
|
|
36
|
+
if (!role) notFound()
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<RoleFormPage
|
|
40
|
+
dictionary={dictionary}
|
|
41
|
+
permissions={permissions}
|
|
42
|
+
lang={lang}
|
|
43
|
+
resources={resources}
|
|
44
|
+
actions={actions}
|
|
45
|
+
mode="edit"
|
|
46
|
+
initialData={{
|
|
47
|
+
id: role.id,
|
|
48
|
+
name: role.name,
|
|
49
|
+
code: role.code,
|
|
50
|
+
description: role.description ?? undefined,
|
|
51
|
+
status: role.status,
|
|
52
|
+
// Ma trận quyền của core đọc dạng "action:resource" — đảo thứ tự là
|
|
53
|
+
// không ô nào tick, mà cũng chẳng có lỗi nào hiện ra.
|
|
54
|
+
permissions: role.rolePermissions.map(
|
|
55
|
+
(rp) => `${rp.actionCode}:${rp.resourceCode}`,
|
|
56
|
+
),
|
|
57
|
+
}}
|
|
58
|
+
/>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { getCrudPermissions } from "@goerp/core/crud/server"
|
|
2
|
+
import { RoleFormPage } from "@goerp/core/rbac/pages"
|
|
3
|
+
import {
|
|
4
|
+
getActionsForPermissionMatrix,
|
|
5
|
+
getResourcesForPermissionMatrix,
|
|
6
|
+
} from "@goerp/core/rbac/resource-service"
|
|
7
|
+
|
|
8
|
+
import type { Metadata } from "next"
|
|
9
|
+
|
|
10
|
+
import { dictionary } from "@/data/dictionary"
|
|
11
|
+
import { requirePageAccess } from "@/lib/page-guard"
|
|
12
|
+
import { db } from "@/lib/prisma"
|
|
13
|
+
|
|
14
|
+
export const metadata: Metadata = { title: "Thêm vai trò" }
|
|
15
|
+
export const dynamic = "force-dynamic"
|
|
16
|
+
|
|
17
|
+
export default async function NewRolePage({
|
|
18
|
+
params,
|
|
19
|
+
}: {
|
|
20
|
+
params: Promise<{ lang: string }>
|
|
21
|
+
}) {
|
|
22
|
+
const { lang } = await params
|
|
23
|
+
const session = await requirePageAccess(lang, "role", "create")
|
|
24
|
+
|
|
25
|
+
const [permissions, resources, actions] = await Promise.all([
|
|
26
|
+
getCrudPermissions(session, "role"),
|
|
27
|
+
getResourcesForPermissionMatrix(db),
|
|
28
|
+
getActionsForPermissionMatrix(db),
|
|
29
|
+
])
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<RoleFormPage
|
|
33
|
+
dictionary={dictionary}
|
|
34
|
+
permissions={permissions}
|
|
35
|
+
lang={lang}
|
|
36
|
+
resources={resources}
|
|
37
|
+
actions={actions}
|
|
38
|
+
mode="create"
|
|
39
|
+
/>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { getCrudPermissions } from "@goerp/core/crud/server"
|
|
2
|
+
import { RoleListPage } from "@goerp/core/rbac/pages"
|
|
3
|
+
import {
|
|
4
|
+
getActionsForPermissionMatrix,
|
|
5
|
+
getResourcesForPermissionMatrix,
|
|
6
|
+
} from "@goerp/core/rbac/resource-service"
|
|
7
|
+
import { getRolesData } from "@goerp/core/rbac/role-service"
|
|
8
|
+
|
|
9
|
+
import type { Metadata } from "next"
|
|
10
|
+
|
|
11
|
+
import { dictionary } from "@/data/dictionary"
|
|
12
|
+
import { requirePageAccess } from "@/lib/page-guard"
|
|
13
|
+
import { db } from "@/lib/prisma"
|
|
14
|
+
|
|
15
|
+
export const metadata: Metadata = { title: "Vai trò" }
|
|
16
|
+
export const dynamic = "force-dynamic"
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Danh sách vai trò — màn hình sẵn của core, dữ liệu do trang nạp và truyền
|
|
20
|
+
* vào dạng props (core không tự truy vấn DB). Ma trận quyền hiển thị từ bảng
|
|
21
|
+
* `Resource`/`Action`, tức là từ những gì `pnpm rbac-sync` đã đồng bộ.
|
|
22
|
+
*/
|
|
23
|
+
export default async function RolesPage({
|
|
24
|
+
params,
|
|
25
|
+
}: {
|
|
26
|
+
params: Promise<{ lang: string }>
|
|
27
|
+
}) {
|
|
28
|
+
const { lang } = await params
|
|
29
|
+
const session = await requirePageAccess(lang, "role")
|
|
30
|
+
|
|
31
|
+
const [permissions, initialData, resources, actions] = await Promise.all([
|
|
32
|
+
getCrudPermissions(session, "role"),
|
|
33
|
+
getRolesData(db, { page: 1, pageSize: 20 }),
|
|
34
|
+
getResourcesForPermissionMatrix(db),
|
|
35
|
+
getActionsForPermissionMatrix(db),
|
|
36
|
+
])
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<RoleListPage
|
|
40
|
+
dictionary={dictionary}
|
|
41
|
+
permissions={permissions}
|
|
42
|
+
lang={lang}
|
|
43
|
+
initialData={initialData}
|
|
44
|
+
resources={resources}
|
|
45
|
+
actions={actions}
|
|
46
|
+
/>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { TaskListClient } from "@goerp/core/tasks/ui"
|
|
2
|
+
|
|
3
|
+
import type { Metadata } from "next"
|
|
4
|
+
|
|
5
|
+
export const metadata: Metadata = { title: "Tác vụ nền" }
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Tác vụ nền của chính người dùng (xuất/nhập file dài hơi). /api/tasks lọc
|
|
9
|
+
* theo `createdBy` nên trang này cũng không cần quyền riêng.
|
|
10
|
+
*/
|
|
11
|
+
export default function TasksPage() {
|
|
12
|
+
return <TaskListClient />
|
|
13
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ReactNode } from "react"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Vỏ TRỐNG — dành cho các trang không có sidebar/topbar: đăng nhập, trang in,
|
|
5
|
+
* màn hình công khai. Vẫn nằm trong [lang] nên vẫn có Providers (theme, tenant,
|
|
6
|
+
* auth bridge) — SignInForm của core cần chúng.
|
|
7
|
+
*/
|
|
8
|
+
export default function PlainLayout({ children }: { children: ReactNode }) {
|
|
9
|
+
return <main className="min-h-screen w-full">{children}</main>
|
|
10
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Auth,
|
|
3
|
+
AuthDescription,
|
|
4
|
+
AuthForm,
|
|
5
|
+
AuthHeader,
|
|
6
|
+
AuthTitle,
|
|
7
|
+
SignInForm,
|
|
8
|
+
} from "@goerp/core/ui/auth"
|
|
9
|
+
|
|
10
|
+
import type { Metadata } from "next"
|
|
11
|
+
|
|
12
|
+
import wallpaper from "@goerp/core/assets/erp_wallpaper.png"
|
|
13
|
+
|
|
14
|
+
import { dictionary } from "@/data/dictionary"
|
|
15
|
+
|
|
16
|
+
export const metadata: Metadata = { title: "Đăng nhập" }
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Trang đăng nhập = ghép sẵn từ core. `SignInForm` gọi qua AuthBridge (xem
|
|
20
|
+
* src/lib/auth-client.ts) nên không biết gì về Better Auth — đổi thư viện auth
|
|
21
|
+
* chỉ cần sửa bridge, trang này giữ nguyên.
|
|
22
|
+
*
|
|
23
|
+
* Đổi ảnh nền bằng cách trỏ `imgSrc` sang file trong public/ của app.
|
|
24
|
+
*/
|
|
25
|
+
export default function SignInPage() {
|
|
26
|
+
return (
|
|
27
|
+
<Auth imgSrc={wallpaper} dictionary={dictionary}>
|
|
28
|
+
<AuthHeader>
|
|
29
|
+
<AuthTitle>Đăng nhập</AuthTitle>
|
|
30
|
+
<AuthDescription>
|
|
31
|
+
Nhập email và mật khẩu để vào hệ thống.
|
|
32
|
+
</AuthDescription>
|
|
33
|
+
</AuthHeader>
|
|
34
|
+
<AuthForm>
|
|
35
|
+
<SignInForm />
|
|
36
|
+
</AuthForm>
|
|
37
|
+
</Auth>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { LocaleType } from "@goerp/core/types"
|
|
2
|
+
import type { ReactNode } from "react"
|
|
3
|
+
|
|
4
|
+
import { Providers } from "@/providers"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Không truyền session xuống Providers: Better Auth lấy session ở CLIENT qua
|
|
8
|
+
* authClient.useSession() (bridge). Server component nào cần session thì tự gọi
|
|
9
|
+
* getSession() — nó đã được React cache theo request.
|
|
10
|
+
*/
|
|
11
|
+
export default async function LangLayout({
|
|
12
|
+
children,
|
|
13
|
+
params,
|
|
14
|
+
}: {
|
|
15
|
+
children: ReactNode
|
|
16
|
+
params: Promise<{ lang: string }>
|
|
17
|
+
}) {
|
|
18
|
+
const { lang } = await params
|
|
19
|
+
|
|
20
|
+
return <Providers locale={lang as LocaleType}>{children}</Providers>
|
|
21
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { NextResponse } from "next/server"
|
|
2
|
+
|
|
3
|
+
import { apiHandler } from "@/lib/api-handler"
|
|
4
|
+
import { serverError } from "@/lib/errors/server-error"
|
|
5
|
+
import { db } from "@/lib/prisma"
|
|
6
|
+
|
|
7
|
+
/** Nhật ký thao tác cho trang /admin/system/audit (SystemAuditPage của core). */
|
|
8
|
+
export const GET = apiHandler(
|
|
9
|
+
async (req) => {
|
|
10
|
+
try {
|
|
11
|
+
const sp = new URL(req.url).searchParams
|
|
12
|
+
const action = sp.get("action") || undefined
|
|
13
|
+
const resource = sp.get("resource") || undefined
|
|
14
|
+
const userId = sp.get("userId") || undefined
|
|
15
|
+
const search = sp.get("search") || undefined
|
|
16
|
+
const startDate = sp.get("startDate")
|
|
17
|
+
const endDate = sp.get("endDate")
|
|
18
|
+
const skip = parseInt(sp.get("skip") || "0", 10)
|
|
19
|
+
const take = Math.min(parseInt(sp.get("take") || "50", 10), 100)
|
|
20
|
+
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
22
|
+
const where: Record<string, any> = {}
|
|
23
|
+
if (action && action !== "all") where.action = action
|
|
24
|
+
if (resource && resource !== "all") where.resource = resource
|
|
25
|
+
if (userId) where.userId = userId
|
|
26
|
+
if (search) {
|
|
27
|
+
where.OR = [
|
|
28
|
+
{ description: { contains: search, mode: "insensitive" } },
|
|
29
|
+
{ resourceId: { contains: search, mode: "insensitive" } },
|
|
30
|
+
{ action: { contains: search, mode: "insensitive" } },
|
|
31
|
+
{ resource: { contains: search, mode: "insensitive" } },
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
if (startDate || endDate) {
|
|
35
|
+
where.createdAt = {}
|
|
36
|
+
if (startDate) where.createdAt.gte = new Date(startDate)
|
|
37
|
+
if (endDate) where.createdAt.lte = new Date(endDate)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const [logs, total] = await Promise.all([
|
|
41
|
+
db.auditLog.findMany({
|
|
42
|
+
where,
|
|
43
|
+
orderBy: { createdAt: "desc" },
|
|
44
|
+
skip,
|
|
45
|
+
take,
|
|
46
|
+
include: { user: { select: { id: true, name: true, email: true } } },
|
|
47
|
+
}),
|
|
48
|
+
db.auditLog.count({ where }),
|
|
49
|
+
])
|
|
50
|
+
|
|
51
|
+
return NextResponse.json({
|
|
52
|
+
data: logs,
|
|
53
|
+
meta: { total, skip, take, hasMore: skip + take < total },
|
|
54
|
+
})
|
|
55
|
+
} catch (error) {
|
|
56
|
+
return serverError(error, req, { message: "Không tải được nhật ký thao tác" })
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
{ resource: "audit-log", action: "view" },
|
|
60
|
+
)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { NextResponse } from "next/server"
|
|
2
|
+
|
|
3
|
+
import { apiHandler } from "@/lib/api-handler"
|
|
4
|
+
import { serverError } from "@/lib/errors/server-error"
|
|
5
|
+
import { db } from "@/lib/prisma"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Lịch sử chạy của một job. Mỗi dòng có `actions` — nhật ký diễn biến do chính
|
|
9
|
+
* job ghi qua `getJobExecutionContext().log()`, thứ cho biết lần chạy đó thực
|
|
10
|
+
* sự làm được gì chứ không chỉ thành công/thất bại.
|
|
11
|
+
*/
|
|
12
|
+
export const GET = apiHandler(
|
|
13
|
+
async (req, { params }) => {
|
|
14
|
+
try {
|
|
15
|
+
const { name } = await params
|
|
16
|
+
const sp = new URL(req.url).searchParams
|
|
17
|
+
const skip = parseInt(sp.get("skip") || "0", 10)
|
|
18
|
+
const take = Math.min(parseInt(sp.get("take") || "20", 10), 100)
|
|
19
|
+
|
|
20
|
+
const [logs, total] = await Promise.all([
|
|
21
|
+
db.jobExecutionLog.findMany({
|
|
22
|
+
where: { jobName: name },
|
|
23
|
+
orderBy: { startedAt: "desc" },
|
|
24
|
+
skip,
|
|
25
|
+
take,
|
|
26
|
+
}),
|
|
27
|
+
db.jobExecutionLog.count({ where: { jobName: name } }),
|
|
28
|
+
])
|
|
29
|
+
|
|
30
|
+
return NextResponse.json({
|
|
31
|
+
data: logs,
|
|
32
|
+
meta: { total, skip, take, hasMore: skip + take < total },
|
|
33
|
+
})
|
|
34
|
+
} catch (error) {
|
|
35
|
+
return serverError(error, req, { message: "Không tải được lịch sử job" })
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
{ resource: "system-job", action: "view" },
|
|
39
|
+
)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { NextResponse } from "next/server"
|
|
2
|
+
|
|
3
|
+
import { apiHandler } from "@/lib/api-handler"
|
|
4
|
+
import { cronManager } from "@/lib/cron/db-cron-manager"
|
|
5
|
+
import { serverError } from "@/lib/errors/server-error"
|
|
6
|
+
import { db } from "@/lib/prisma"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Danh sách job cho trang /admin/system/jobs (SystemJobsPage của core).
|
|
10
|
+
*
|
|
11
|
+
* Hợp nhất HAI nguồn: bảng `system_jobs` (nguồn sự thật của lịch + cờ bật/tắt,
|
|
12
|
+
* còn nguyên sau khi restart) và registry trong RAM của tiến trình đang phục vụ
|
|
13
|
+
* request (biết job có đang chạy không). Tiến trình này có thể chưa chạy
|
|
14
|
+
* `register()` — khi đó `inMemory=false` và nút chạy tay sẽ không tác dụng, nên
|
|
15
|
+
* cờ đó được trả về cho UI biết đường hiển thị.
|
|
16
|
+
*/
|
|
17
|
+
export const GET = apiHandler(
|
|
18
|
+
async (req) => {
|
|
19
|
+
try {
|
|
20
|
+
const memoryJobs = new Map(cronManager.getJobsInfo().map((j) => [j.name, j]))
|
|
21
|
+
const dbJobs = new Map(
|
|
22
|
+
(await db.systemJob.findMany({ orderBy: { name: "asc" } })).map((j) => [j.name, j]),
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
const names = [...new Set([...dbJobs.keys(), ...memoryJobs.keys()])].sort((a, b) =>
|
|
26
|
+
a.localeCompare(b),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
const jobs = names.map((name) => {
|
|
30
|
+
const mem = memoryJobs.get(name)
|
|
31
|
+
const row = dbJobs.get(name)
|
|
32
|
+
return {
|
|
33
|
+
name,
|
|
34
|
+
cronTime: row?.cronTime ?? mem?.cronTime ?? "",
|
|
35
|
+
isRunning: mem?.isRunning ?? false,
|
|
36
|
+
nextDate: mem?.nextDate ?? null,
|
|
37
|
+
inMemory: !!mem,
|
|
38
|
+
enabled: row?.enabled ?? mem?.isRunning ?? false,
|
|
39
|
+
status: row?.status ?? (mem?.isRunning ? "idle" : "stopped"),
|
|
40
|
+
lastRun: row?.lastRun ?? null,
|
|
41
|
+
nextRun: row?.nextRun ?? null,
|
|
42
|
+
error: row?.error ?? null,
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
return NextResponse.json({ data: jobs })
|
|
47
|
+
} catch (error) {
|
|
48
|
+
return serverError(error, req, { message: "Không tải được danh sách job" })
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
{ resource: "system-job", action: "view" },
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
/** POST { name, action: "toggle" | "run" | "remove" }. */
|
|
55
|
+
export const POST = apiHandler(
|
|
56
|
+
async (req) => {
|
|
57
|
+
try {
|
|
58
|
+
const { name, action } = await req.json()
|
|
59
|
+
if (!name) {
|
|
60
|
+
return NextResponse.json({ error: "Thiếu tên job" }, { status: 400 })
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (action === "toggle") {
|
|
64
|
+
if (!(await cronManager.toggleJob(name))) {
|
|
65
|
+
return NextResponse.json({ error: `Không tìm thấy job "${name}"` }, { status: 404 })
|
|
66
|
+
}
|
|
67
|
+
const info = cronManager.getJobsInfo().find((j) => j.name === name)
|
|
68
|
+
return NextResponse.json({
|
|
69
|
+
success: true,
|
|
70
|
+
message: `Job "${name}" đã ${info?.isRunning ? "bật" : "tắt"}`,
|
|
71
|
+
isRunning: info?.isRunning,
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (action === "run") {
|
|
76
|
+
if (!(await cronManager.runJobNow(name))) {
|
|
77
|
+
return NextResponse.json({ error: `Không tìm thấy job "${name}"` }, { status: 404 })
|
|
78
|
+
}
|
|
79
|
+
return NextResponse.json({ success: true, message: `Đã chạy tay job "${name}"` })
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (action === "remove") {
|
|
83
|
+
cronManager.removeJob(name)
|
|
84
|
+
return NextResponse.json({ success: true, message: `Đã gỡ job "${name}"` })
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return NextResponse.json(
|
|
88
|
+
{ error: "action phải là 'toggle', 'run' hoặc 'remove'" },
|
|
89
|
+
{ status: 400 },
|
|
90
|
+
)
|
|
91
|
+
} catch (error) {
|
|
92
|
+
return serverError(error, req, { message: "Không thao tác được với job" })
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
{ resource: "system-job", action: "update" },
|
|
96
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { NextResponse } from "next/server"
|
|
2
|
+
|
|
3
|
+
import { apiHandler } from "@/lib/api-handler"
|
|
4
|
+
import { serverError } from "@/lib/errors/server-error"
|
|
5
|
+
import { listConfigs } from "@/server/services/system-config-service"
|
|
6
|
+
|
|
7
|
+
/** Toàn bộ cấu hình đang bật — bảng chính của trang cài đặt hệ thống. */
|
|
8
|
+
export const GET = apiHandler(
|
|
9
|
+
async (req) => {
|
|
10
|
+
try {
|
|
11
|
+
return NextResponse.json({ data: await listConfigs() })
|
|
12
|
+
} catch (error) {
|
|
13
|
+
return serverError(error, req, { message: "Không tải được cấu hình" })
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
{ resource: "system-setting", action: "view" },
|
|
17
|
+
)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { NextResponse } from "next/server"
|
|
2
|
+
|
|
3
|
+
import { apiHandler } from "@/lib/api-handler"
|
|
4
|
+
import { serverError } from "@/lib/errors/server-error"
|
|
5
|
+
import { configErrorInfo, createConfig } from "@/server/services/system-config-service"
|
|
6
|
+
|
|
7
|
+
export const POST = apiHandler(
|
|
8
|
+
async (req, { session }) => {
|
|
9
|
+
try {
|
|
10
|
+
const data = await createConfig(await req.json(), session.user.id)
|
|
11
|
+
return NextResponse.json({ success: true, data })
|
|
12
|
+
} catch (error) {
|
|
13
|
+
const known = configErrorInfo(error)
|
|
14
|
+
if (known) return NextResponse.json({ error: known.message }, { status: known.status })
|
|
15
|
+
return serverError(error, req, { message: "Không tạo được cấu hình" })
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
{ resource: "system-setting", action: "create" },
|
|
19
|
+
)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { NextResponse } from "next/server"
|
|
2
|
+
|
|
3
|
+
import { apiHandler } from "@/lib/api-handler"
|
|
4
|
+
import { serverError } from "@/lib/errors/server-error"
|
|
5
|
+
import { configErrorInfo, deleteConfig } from "@/server/services/system-config-service"
|
|
6
|
+
|
|
7
|
+
export const POST = apiHandler(
|
|
8
|
+
async (req) => {
|
|
9
|
+
try {
|
|
10
|
+
const { key } = await req.json()
|
|
11
|
+
await deleteConfig(key)
|
|
12
|
+
return NextResponse.json({ success: true, message: "Đã xóa cấu hình" })
|
|
13
|
+
} catch (error) {
|
|
14
|
+
const known = configErrorInfo(error)
|
|
15
|
+
if (known) return NextResponse.json({ error: known.message }, { status: known.status })
|
|
16
|
+
return serverError(error, req, { message: "Không xóa được cấu hình" })
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
{ resource: "system-setting", action: "delete" },
|
|
20
|
+
)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { NextResponse } from "next/server"
|
|
2
|
+
import { SettingsService } from "@goerp/core/system/services/settings-service"
|
|
3
|
+
|
|
4
|
+
import { apiHandler } from "@/lib/api-handler"
|
|
5
|
+
import { serverError } from "@/lib/errors/server-error"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Nhóm cấu hình có KIỂU (lưu dạng JSON dưới một key) — khác với
|
|
9
|
+
* /settings/all vốn liệt kê từng dòng key-value thô.
|
|
10
|
+
*
|
|
11
|
+
* Thêm nhóm của app: khai thêm nhánh `type` bên dưới và dùng
|
|
12
|
+
* `SettingsService.getSettings/saveSettings` với key riêng.
|
|
13
|
+
*/
|
|
14
|
+
export const GET = apiHandler(
|
|
15
|
+
async (req) => {
|
|
16
|
+
try {
|
|
17
|
+
const [audit, notification] = await Promise.all([
|
|
18
|
+
SettingsService.getAuditSettings(),
|
|
19
|
+
SettingsService.getNotificationSettings(),
|
|
20
|
+
])
|
|
21
|
+
return NextResponse.json({ data: { audit, notification } })
|
|
22
|
+
} catch (error) {
|
|
23
|
+
return serverError(error, req, { message: "Không tải được cấu hình" })
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
{ resource: "system-setting", action: "view" },
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
export const POST = apiHandler(
|
|
30
|
+
async (req) => {
|
|
31
|
+
try {
|
|
32
|
+
const { type, settings } = await req.json()
|
|
33
|
+
|
|
34
|
+
if (type === "audit") {
|
|
35
|
+
await SettingsService.saveAuditSettings(settings)
|
|
36
|
+
return NextResponse.json({ success: true, message: "Đã lưu cấu hình nhật ký" })
|
|
37
|
+
}
|
|
38
|
+
if (type === "notification") {
|
|
39
|
+
await SettingsService.saveNotificationSettings(settings)
|
|
40
|
+
return NextResponse.json({ success: true, message: "Đã lưu cấu hình thông báo" })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return NextResponse.json({ error: "Loại cấu hình không hợp lệ" }, { status: 400 })
|
|
44
|
+
} catch (error) {
|
|
45
|
+
return serverError(error, req, { message: "Không lưu được cấu hình" })
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
{ resource: "system-setting", action: "update" },
|
|
49
|
+
)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { NextResponse } from "next/server"
|
|
2
|
+
|
|
3
|
+
import { apiHandler } from "@/lib/api-handler"
|
|
4
|
+
import { serverError } from "@/lib/errors/server-error"
|
|
5
|
+
import {
|
|
6
|
+
configErrorInfo,
|
|
7
|
+
toggleConfigStatus,
|
|
8
|
+
} from "@/server/services/system-config-service"
|
|
9
|
+
|
|
10
|
+
/** Bật/tạm ngưng một cấu hình mà không xóa nó. */
|
|
11
|
+
export const POST = apiHandler(
|
|
12
|
+
async (req, { session }) => {
|
|
13
|
+
try {
|
|
14
|
+
const { key, status } = await req.json()
|
|
15
|
+
const data = await toggleConfigStatus(key, status, session.user.id)
|
|
16
|
+
return NextResponse.json({
|
|
17
|
+
success: true,
|
|
18
|
+
data,
|
|
19
|
+
message: status === "active" ? "Đã kích hoạt cấu hình" : "Đã tạm ngưng cấu hình",
|
|
20
|
+
})
|
|
21
|
+
} catch (error) {
|
|
22
|
+
const known = configErrorInfo(error)
|
|
23
|
+
if (known) return NextResponse.json({ error: known.message }, { status: known.status })
|
|
24
|
+
return serverError(error, req, { message: "Không cập nhật được trạng thái" })
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
{ resource: "system-setting", action: "update" },
|
|
28
|
+
)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { NextResponse } from "next/server"
|
|
2
|
+
|
|
3
|
+
import { apiHandler } from "@/lib/api-handler"
|
|
4
|
+
import { serverError } from "@/lib/errors/server-error"
|
|
5
|
+
import {
|
|
6
|
+
configErrorInfo,
|
|
7
|
+
updateConfigValue,
|
|
8
|
+
} from "@/server/services/system-config-service"
|
|
9
|
+
|
|
10
|
+
/** Đổi mỗi giá trị của một cấu hình theo key. */
|
|
11
|
+
export const POST = apiHandler(
|
|
12
|
+
async (req, { session }) => {
|
|
13
|
+
try {
|
|
14
|
+
const { key, value } = await req.json()
|
|
15
|
+
const data = await updateConfigValue(key, value, session.user.id)
|
|
16
|
+
return NextResponse.json({ success: true, data })
|
|
17
|
+
} catch (error) {
|
|
18
|
+
const known = configErrorInfo(error)
|
|
19
|
+
if (known) return NextResponse.json({ error: known.message }, { status: known.status })
|
|
20
|
+
return serverError(error, req, { message: "Không cập nhật được cấu hình" })
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
{ resource: "system-setting", action: "update" },
|
|
24
|
+
)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { NextResponse } from "next/server"
|
|
2
|
+
|
|
3
|
+
import { apiHandler } from "@/lib/api-handler"
|
|
4
|
+
import { serverError } from "@/lib/errors/server-error"
|
|
5
|
+
import {
|
|
6
|
+
configErrorInfo,
|
|
7
|
+
updateConfigFull,
|
|
8
|
+
} from "@/server/services/system-config-service"
|
|
9
|
+
|
|
10
|
+
/** Sửa toàn bộ thuộc tính của một cấu hình (dialog "Sửa"). */
|
|
11
|
+
export const POST = apiHandler(
|
|
12
|
+
async (req, { session }) => {
|
|
13
|
+
try {
|
|
14
|
+
const data = await updateConfigFull(await req.json(), session.user.id)
|
|
15
|
+
return NextResponse.json({ success: true, data })
|
|
16
|
+
} catch (error) {
|
|
17
|
+
const known = configErrorInfo(error)
|
|
18
|
+
if (known) return NextResponse.json({ error: known.message }, { status: known.status })
|
|
19
|
+
return serverError(error, req, { message: "Không cập nhật được cấu hình" })
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
{ resource: "system-setting", action: "update" },
|
|
23
|
+
)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { toNextJsHandler } from "better-auth/next-js"
|
|
2
|
+
|
|
3
|
+
import { getAuth } from "@/lib/better-auth"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Toàn bộ endpoint của Better Auth (sign-in, sign-out, get-session…). Prefix
|
|
7
|
+
* `/api/better-auth` nằm trong danh sách public của src/proxy.ts vì các
|
|
8
|
+
* endpoint này tự xác thực — không có nó thì cổng default-deny chặn luôn cả
|
|
9
|
+
* đường đăng nhập.
|
|
10
|
+
*/
|
|
11
|
+
export const { GET, POST } = toNextJsHandler((req: Request) =>
|
|
12
|
+
getAuth().handler(req),
|
|
13
|
+
)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createCrudItemHandlers } from "@goerp/core/crud/server"
|
|
2
|
+
|
|
3
|
+
import { getSession } from "@/lib/auth"
|
|
4
|
+
import { getEntityConfig } from "@/configs/entities"
|
|
5
|
+
import { serverError } from "@/lib/errors/server-error"
|
|
6
|
+
import { crudService } from "@/lib/crud"
|
|
7
|
+
|
|
8
|
+
export const { GET, PUT, PATCH, DELETE } = createCrudItemHandlers({
|
|
9
|
+
getSession,
|
|
10
|
+
getEntityConfig,
|
|
11
|
+
service: crudService,
|
|
12
|
+
onError: serverError,
|
|
13
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createCrudCollectionHandlers } from "@goerp/core/crud/server"
|
|
2
|
+
|
|
3
|
+
import { getSession } from "@/lib/auth"
|
|
4
|
+
import { getEntityConfig } from "@/configs/entities"
|
|
5
|
+
import { serverError } from "@/lib/errors/server-error"
|
|
6
|
+
import { crudService } from "@/lib/crud"
|
|
7
|
+
|
|
8
|
+
// GET danh sách + POST tạo. Gác quyền + query đều ở core.
|
|
9
|
+
export const { GET, POST } = createCrudCollectionHandlers({
|
|
10
|
+
getSession,
|
|
11
|
+
getEntityConfig,
|
|
12
|
+
service: crudService,
|
|
13
|
+
onError: serverError,
|
|
14
|
+
})
|