@goplusvn/core 0.1.59 → 0.1.61
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/crud/lib/mutation-builder.ts +105 -0
- package/src/crud/lib/query-builder.ts +119 -0
- package/src/crud/server-service.ts +35 -163
- 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,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 { db } from "@/lib/prisma"
|
|
6
|
+
|
|
7
|
+
/** Đánh dấu đã/chưa xử lý một dòng lỗi. */
|
|
8
|
+
export const PATCH = apiHandler(
|
|
9
|
+
async (req, { params }) => {
|
|
10
|
+
try {
|
|
11
|
+
const { id } = await params
|
|
12
|
+
const { resolved } = await req.json()
|
|
13
|
+
const log = await db.errorLog.update({
|
|
14
|
+
where: { id },
|
|
15
|
+
data: { resolved: Boolean(resolved) },
|
|
16
|
+
})
|
|
17
|
+
return NextResponse.json({ success: true, log })
|
|
18
|
+
} catch (error) {
|
|
19
|
+
return serverError(error, req, { message: "Không cập nhật được trạng thái lỗi" })
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
{ resource: "error-log", action: "update" },
|
|
23
|
+
)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { NextResponse } from "next/server"
|
|
2
|
+
|
|
3
|
+
import type { NextRequest } from "next/server"
|
|
4
|
+
|
|
5
|
+
import { apiHandler } from "@/lib/api-handler"
|
|
6
|
+
import { serverError } from "@/lib/errors/server-error"
|
|
7
|
+
import { db } from "@/lib/prisma"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Nhật ký lỗi cho trang /admin/system/error-logs (ErrorLogsPage của core).
|
|
11
|
+
* Lỗi được GHI ở nơi khác (`serverError` / `onRequestError`); route này chỉ đọc
|
|
12
|
+
* và dọn.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
16
|
+
type Where = Record<string, any>
|
|
17
|
+
|
|
18
|
+
export const GET = apiHandler(
|
|
19
|
+
async (req) => {
|
|
20
|
+
try {
|
|
21
|
+
const sp = new URL(req.url).searchParams
|
|
22
|
+
const search = sp.get("search") || undefined
|
|
23
|
+
const severity = sp.get("severity") || undefined
|
|
24
|
+
const module = sp.get("module") || undefined
|
|
25
|
+
const resolved = sp.get("resolved")
|
|
26
|
+
const startDate = sp.get("startDate")
|
|
27
|
+
const endDate = sp.get("endDate")
|
|
28
|
+
const skip = parseInt(sp.get("skip") || "0", 10)
|
|
29
|
+
const take = Math.min(parseInt(sp.get("take") || "50", 10), 100)
|
|
30
|
+
|
|
31
|
+
const where: Where = {}
|
|
32
|
+
if (search) {
|
|
33
|
+
where.OR = [
|
|
34
|
+
{ message: { contains: search, mode: "insensitive" } },
|
|
35
|
+
{ errorId: { contains: search, mode: "insensitive" } },
|
|
36
|
+
{ code: { contains: search, mode: "insensitive" } },
|
|
37
|
+
{ module: { contains: search, mode: "insensitive" } },
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
if (severity && severity !== "all") where.severity = severity
|
|
41
|
+
if (module && module !== "all") where.module = module
|
|
42
|
+
if (resolved === "true") where.resolved = true
|
|
43
|
+
else if (resolved === "false") where.resolved = false
|
|
44
|
+
if (startDate || endDate) {
|
|
45
|
+
where.createdAt = {}
|
|
46
|
+
if (startDate) where.createdAt.gte = new Date(startDate)
|
|
47
|
+
if (endDate) where.createdAt.lte = new Date(endDate)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const startOfToday = new Date()
|
|
51
|
+
startOfToday.setHours(0, 0, 0, 0)
|
|
52
|
+
|
|
53
|
+
const [logs, total, todayCount, unresolvedCount, moduleStats] = await Promise.all([
|
|
54
|
+
db.errorLog.findMany({ where, orderBy: { lastSeenAt: "desc" }, skip, take }),
|
|
55
|
+
db.errorLog.count({ where }),
|
|
56
|
+
db.errorLog.count({ where: { createdAt: { gte: startOfToday } } }),
|
|
57
|
+
db.errorLog.count({ where: { resolved: false } }),
|
|
58
|
+
db.errorLog.groupBy({
|
|
59
|
+
by: ["module"],
|
|
60
|
+
_count: { module: true },
|
|
61
|
+
where: { resolved: false },
|
|
62
|
+
orderBy: { _count: { module: "desc" } },
|
|
63
|
+
take: 8,
|
|
64
|
+
}),
|
|
65
|
+
])
|
|
66
|
+
|
|
67
|
+
// ErrorLog không có quan hệ tới User (lỗi có thể xảy ra khi chưa đăng
|
|
68
|
+
// nhập) — tra tên riêng thay vì include.
|
|
69
|
+
const userIds = [...new Set(logs.map((l) => l.userId).filter(Boolean))] as string[]
|
|
70
|
+
const nameById = new Map<string, string>()
|
|
71
|
+
if (userIds.length) {
|
|
72
|
+
const users = await db.user.findMany({
|
|
73
|
+
where: { id: { in: userIds } },
|
|
74
|
+
select: { id: true, name: true },
|
|
75
|
+
})
|
|
76
|
+
for (const u of users) nameById.set(u.id, u.name || u.id)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return NextResponse.json({
|
|
80
|
+
data: logs.map((l) => ({
|
|
81
|
+
...l,
|
|
82
|
+
userName: l.userId ? (nameById.get(l.userId) ?? l.userId) : null,
|
|
83
|
+
})),
|
|
84
|
+
meta: {
|
|
85
|
+
total,
|
|
86
|
+
skip,
|
|
87
|
+
take,
|
|
88
|
+
hasMore: skip + take < total,
|
|
89
|
+
todayCount,
|
|
90
|
+
unresolvedCount,
|
|
91
|
+
moduleStats: moduleStats.map((m) => ({
|
|
92
|
+
module: m.module || "unknown",
|
|
93
|
+
count: m._count.module,
|
|
94
|
+
})),
|
|
95
|
+
},
|
|
96
|
+
})
|
|
97
|
+
} catch (error) {
|
|
98
|
+
return serverError(error, req, { message: "Không tải được nhật ký lỗi" })
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
{ resource: "error-log", action: "view" },
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
/** Dọn log: ?mode=resolved (đã xử lý) hoặc ?mode=older_than&days=30. */
|
|
105
|
+
export const DELETE = apiHandler(
|
|
106
|
+
async (req: NextRequest) => {
|
|
107
|
+
try {
|
|
108
|
+
const sp = new URL(req.url).searchParams
|
|
109
|
+
const mode = sp.get("mode")
|
|
110
|
+
const days = parseInt(sp.get("days") || "30", 10)
|
|
111
|
+
|
|
112
|
+
const where =
|
|
113
|
+
mode === "resolved"
|
|
114
|
+
? { resolved: true }
|
|
115
|
+
: { createdAt: { lt: new Date(Date.now() - days * 864e5) } }
|
|
116
|
+
|
|
117
|
+
const { count } = await db.errorLog.deleteMany({ where })
|
|
118
|
+
return NextResponse.json({ success: true, deleted: count })
|
|
119
|
+
} catch (error) {
|
|
120
|
+
return serverError(error, req, { message: "Không xóa được nhật ký lỗi" })
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
{ resource: "error-log", action: "delete" },
|
|
124
|
+
)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createFileProxyHandler } from "@/lib/storage"
|
|
2
|
+
|
|
3
|
+
import { apiHandler } from "@/lib/api-handler"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* GET /api/files/[...key] — phục vụ tập tin từ kho qua app server.
|
|
7
|
+
*
|
|
8
|
+
* Đi qua app chứ không cho trình duyệt gọi thẳng vào kho, vì hai lẽ: MinIO/S3
|
|
9
|
+
* nội bộ thường không mở ra Internet, và mỗi object cần luật quyền riêng.
|
|
10
|
+
*
|
|
11
|
+
* Hiện tại chỉ gác PHIÊN. Khi kho có tập tin nhạy cảm, thêm seam `authorize`:
|
|
12
|
+
*
|
|
13
|
+
* createFileProxyHandler<Session>({
|
|
14
|
+
* authorize: ({ key, session }) => {
|
|
15
|
+
* if (key.startsWith("hop-dong/")) {
|
|
16
|
+
* return checkPermission(session, "contract", "view")
|
|
17
|
+
* }
|
|
18
|
+
* return true
|
|
19
|
+
* },
|
|
20
|
+
* })
|
|
21
|
+
*
|
|
22
|
+
* Trả `false` → 403; trả `{ status: 404 }` khi không muốn lộ cả sự tồn tại
|
|
23
|
+
* của key.
|
|
24
|
+
*/
|
|
25
|
+
export const GET = apiHandler<{ key: string[] }>(createFileProxyHandler())
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { NextResponse } from "next/server"
|
|
2
|
+
|
|
3
|
+
import { apiHandler } from "@/lib/api-handler"
|
|
4
|
+
import { serverError } from "@/lib/errors/server-error"
|
|
5
|
+
import { markAllRead, markRead } from "@/server/services/notification-service"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* POST /api/notifications/read
|
|
9
|
+
* { ids: string[] } → đánh dấu đã đọc các tin đó
|
|
10
|
+
* { all: true } → đánh dấu đã đọc tất cả
|
|
11
|
+
* Luôn scope theo session.user.id — không ai đọc hộ hộp thư người khác.
|
|
12
|
+
*/
|
|
13
|
+
export const POST = apiHandler(async (req, { session }) => {
|
|
14
|
+
try {
|
|
15
|
+
const body = await req.json().catch(() => ({}))
|
|
16
|
+
const userId = session.user.id
|
|
17
|
+
|
|
18
|
+
if (body?.all === true) {
|
|
19
|
+
return NextResponse.json({ count: await markAllRead(userId) })
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const ids: string[] = Array.isArray(body?.ids)
|
|
23
|
+
? body.ids.filter((id: unknown): id is string => typeof id === "string")
|
|
24
|
+
: []
|
|
25
|
+
return NextResponse.json({ count: await markRead(userId, ids) })
|
|
26
|
+
} catch (error) {
|
|
27
|
+
return serverError(error, req, { message: "Không cập nhật được thông báo" })
|
|
28
|
+
}
|
|
29
|
+
})
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { NextResponse } from "next/server"
|
|
2
|
+
|
|
3
|
+
import { apiHandler } from "@/lib/api-handler"
|
|
4
|
+
import { serverError } from "@/lib/errors/server-error"
|
|
5
|
+
import { listNotifications } from "@/server/services/notification-service"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* GET /api/notifications?cursor=&take=&filter=unread|all
|
|
9
|
+
* Hộp thư của CHÍNH người đang đăng nhập — luôn scope theo session.user.id,
|
|
10
|
+
* nên chỉ cần đăng nhập chứ không cần quyền trên resource nào.
|
|
11
|
+
*/
|
|
12
|
+
export const GET = apiHandler(async (req, { session }) => {
|
|
13
|
+
try {
|
|
14
|
+
const sp = new URL(req.url).searchParams
|
|
15
|
+
const takeRaw = Number(sp.get("take"))
|
|
16
|
+
|
|
17
|
+
const result = await listNotifications(session.user.id, {
|
|
18
|
+
cursor: sp.get("cursor") || undefined,
|
|
19
|
+
take: Number.isFinite(takeRaw) && takeRaw > 0 ? takeRaw : undefined,
|
|
20
|
+
filter: sp.get("filter") === "unread" ? "unread" : "all",
|
|
21
|
+
})
|
|
22
|
+
return NextResponse.json(result)
|
|
23
|
+
} catch (error) {
|
|
24
|
+
return serverError(error, req, { message: "Không tải được thông báo" })
|
|
25
|
+
}
|
|
26
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { NextResponse } from "next/server"
|
|
2
|
+
|
|
3
|
+
import { apiHandler } from "@/lib/api-handler"
|
|
4
|
+
import { serverError } from "@/lib/errors/server-error"
|
|
5
|
+
import { getUnreadCount } from "@/server/services/notification-service"
|
|
6
|
+
|
|
7
|
+
/** GET /api/notifications/unread-count → { count }. Endpoint nhẹ cho chuông poll. */
|
|
8
|
+
export const GET = apiHandler(async (req, { session }) => {
|
|
9
|
+
try {
|
|
10
|
+
return NextResponse.json({ count: await getUnreadCount(session.user.id) })
|
|
11
|
+
} catch (error) {
|
|
12
|
+
return serverError(error, req, { message: "Không đếm được thông báo" })
|
|
13
|
+
}
|
|
14
|
+
})
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createPermissionsVersionHandler } from "@goerp/core/rbac/route-handlers"
|
|
2
|
+
|
|
3
|
+
import { getSession } from "@/lib/auth"
|
|
4
|
+
import { db } from "@/lib/prisma"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* GET /api/rbac/permissions-version — client poll (PermissionsVersionWatcher
|
|
8
|
+
* của core). Số version đổi ⇒ trình duyệt tự nạp lại phiên, nên admin sửa
|
|
9
|
+
* quyền là người dùng thấy ngay, không phải đăng xuất/đăng nhập lại.
|
|
10
|
+
*/
|
|
11
|
+
export const { GET } = createPermissionsVersionHandler({ prisma: db, getSession })
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { getCrudPermissions } from "@goerp/core/crud/server"
|
|
2
|
+
import { createRoleItemHandlers } from "@goerp/core/rbac/route-handlers"
|
|
3
|
+
|
|
4
|
+
import { getSession } from "@/lib/auth"
|
|
5
|
+
import { serverError } from "@/lib/errors/server-error"
|
|
6
|
+
import { db } from "@/lib/prisma"
|
|
7
|
+
|
|
8
|
+
/** GET / PUT (thay toàn bộ bộ quyền) / DELETE (chặn nếu còn người dùng). */
|
|
9
|
+
export const { GET, PUT, DELETE } = createRoleItemHandlers({
|
|
10
|
+
prisma: db,
|
|
11
|
+
getSession,
|
|
12
|
+
getCrudPermissions,
|
|
13
|
+
onError: (error, req) => serverError(error, req, { message: "Lỗi xử lý vai trò" }),
|
|
14
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { getCrudPermissions } from "@goerp/core/crud/server"
|
|
2
|
+
import { createRolesCollectionHandlers } from "@goerp/core/rbac/route-handlers"
|
|
3
|
+
|
|
4
|
+
import { getSession } from "@/lib/auth"
|
|
5
|
+
import { serverError } from "@/lib/errors/server-error"
|
|
6
|
+
import { db } from "@/lib/prisma"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* GET (danh sách + số người dùng) và POST (tạo vai trò kèm bộ quyền) — cả hai
|
|
10
|
+
* đến từ factory của core, app chỉ tiêm prisma + nguồn session. Đổi vai trò
|
|
11
|
+
* cũng bump `permissions-version` để phiên đang mở tự nhận quyền mới.
|
|
12
|
+
*/
|
|
13
|
+
export const { GET, POST } = createRolesCollectionHandlers({
|
|
14
|
+
prisma: db,
|
|
15
|
+
getSession,
|
|
16
|
+
getCrudPermissions,
|
|
17
|
+
onError: (error, req) => serverError(error, req, { message: "Lỗi xử lý vai trò" }),
|
|
18
|
+
})
|
|
@@ -0,0 +1,52 @@
|
|
|
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
|
+
* GET /api/tasks/[id]/download[?file=error] — tải file kết quả.
|
|
9
|
+
*
|
|
10
|
+
* CHỦ tác vụ mới tải được (`createdBy` trong mệnh đề where, không phải kiểm tra
|
|
11
|
+
* sau khi đọc): file xuất thường là dữ liệu kinh doanh, đừng để đoán id là lấy
|
|
12
|
+
* được. `?file=error` lấy file liệt kê dòng lỗi của tác vụ nhập.
|
|
13
|
+
*/
|
|
14
|
+
export const GET = apiHandler(async (req, { session, params }) => {
|
|
15
|
+
try {
|
|
16
|
+
const { id } = await params
|
|
17
|
+
const task = await db.backgroundTask.findFirst({
|
|
18
|
+
where: { id, createdBy: session.user.id },
|
|
19
|
+
})
|
|
20
|
+
if (!task || task.status !== "success") {
|
|
21
|
+
return NextResponse.json(
|
|
22
|
+
{ error: "Không tìm thấy tác vụ hoặc tác vụ chưa hoàn tất" },
|
|
23
|
+
{ status: 404 },
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const result = (task.result ?? {}) as {
|
|
28
|
+
fileKey?: string
|
|
29
|
+
fileName?: string
|
|
30
|
+
contentType?: string
|
|
31
|
+
errorFileKey?: string
|
|
32
|
+
errorFileName?: string
|
|
33
|
+
}
|
|
34
|
+
const wantError = new URL(req.url).searchParams.get("file") === "error"
|
|
35
|
+
const fileKey = wantError ? result.errorFileKey : result.fileKey
|
|
36
|
+
const fileName = (wantError ? result.errorFileName : result.fileName) || "download"
|
|
37
|
+
if (!fileKey) {
|
|
38
|
+
return NextResponse.json({ error: "Tác vụ không có file kết quả" }, { status: 404 })
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const { readTaskFile } = await import("@/server/tasks")
|
|
42
|
+
const buffer = await readTaskFile(fileKey)
|
|
43
|
+
return new NextResponse(new Uint8Array(buffer), {
|
|
44
|
+
headers: {
|
|
45
|
+
"Content-Type": result.contentType || "application/octet-stream",
|
|
46
|
+
"Content-Disposition": `attachment; filename="${encodeURIComponent(fileName)}"`,
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
} catch (error) {
|
|
50
|
+
return serverError(error, req, { message: "Không tải được file tác vụ" })
|
|
51
|
+
}
|
|
52
|
+
})
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
* GET /api/tasks?take=20 — tác vụ nền của CHÍNH người đang đăng nhập, mới nhất
|
|
9
|
+
* trước. Không khai resource: đây là dữ liệu cá nhân, chỉ cần đăng nhập; phạm
|
|
10
|
+
* vi được ép bằng `createdBy` chứ không bằng quyền.
|
|
11
|
+
*/
|
|
12
|
+
export const GET = apiHandler(async (req, { session }) => {
|
|
13
|
+
try {
|
|
14
|
+
const takeRaw = Number(new URL(req.url).searchParams.get("take"))
|
|
15
|
+
const take = Number.isFinite(takeRaw) && takeRaw > 0 ? Math.min(takeRaw, 100) : 20
|
|
16
|
+
|
|
17
|
+
const tasks = await db.backgroundTask.findMany({
|
|
18
|
+
where: { createdBy: session.user.id },
|
|
19
|
+
orderBy: { createdAt: "desc" },
|
|
20
|
+
take,
|
|
21
|
+
select: {
|
|
22
|
+
id: true,
|
|
23
|
+
type: true,
|
|
24
|
+
title: true,
|
|
25
|
+
status: true,
|
|
26
|
+
progress: true,
|
|
27
|
+
result: true,
|
|
28
|
+
error: true,
|
|
29
|
+
createdAt: true,
|
|
30
|
+
finishedAt: true,
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
return NextResponse.json({ tasks })
|
|
34
|
+
} catch (error) {
|
|
35
|
+
return serverError(error, req, { message: "Không tải được danh sách tác vụ" })
|
|
36
|
+
}
|
|
37
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createUploadHandler } from "@/lib/storage"
|
|
2
|
+
|
|
3
|
+
import { apiHandler } from "@/lib/api-handler"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* POST /api/upload — nhận tập tin đính kèm, trả `{ url, key }`.
|
|
7
|
+
*
|
|
8
|
+
* Engine ở `@goerp/core/storage`: giới hạn 25MB, whitelist đuôi, làm sạch tên
|
|
9
|
+
* (giữ dấu tiếng Việt), gom theo thư mục ngày. URL trả về LUÔN là
|
|
10
|
+
* `/api/files/<key>` dù kho đang là đĩa local hay S3 — đổi kho về sau không
|
|
11
|
+
* làm hỏng URL đã lưu trong DB.
|
|
12
|
+
*
|
|
13
|
+
* Đổi hạn mức/định dạng: `createUploadHandler({ maxBytes, allowedExtensions })`.
|
|
14
|
+
*/
|
|
15
|
+
export const POST = apiHandler(createUploadHandler())
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@import "tw-animate-css";
|
|
3
|
+
|
|
4
|
+
/* The whole design system (Radix → shadcn → Plane tokens, status, charts,
|
|
5
|
+
sidebar, scrollbar) comes from core in one import. */
|
|
6
|
+
@import "@goerp/core/styles/base.css";
|
|
7
|
+
|
|
8
|
+
/* Tailwind v4 must scan core's component classes (this app's own src is
|
|
9
|
+
auto-scanned). */
|
|
10
|
+
@source "../../node_modules/@goerp/core/src/**/*.tsx";
|
|
11
|
+
@source "../../node_modules/@goplusvn/core/src/**/*.tsx";
|
|
12
|
+
|
|
13
|
+
/* App brand overrides. Core's default brand is indigo. Re-skin either here
|
|
14
|
+
statically, or at runtime via `branding.primaryColor` on <TenantProvider>. */
|
|
15
|
+
/* :root { --primary: 262 83% 58%; --sidebar-background: 262 60% 20%; } */
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ReactNode } from "react"
|
|
2
|
+
|
|
3
|
+
import "./globals.css"
|
|
4
|
+
|
|
5
|
+
export default function RootLayout({ children }: { children: ReactNode }) {
|
|
6
|
+
return (
|
|
7
|
+
<html lang="vi" suppressHydrationWarning>
|
|
8
|
+
<body
|
|
9
|
+
suppressHydrationWarning
|
|
10
|
+
className="bg-background text-foreground antialiased"
|
|
11
|
+
>
|
|
12
|
+
{children}
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { NavigationType } from '@goerp/core/types'
|
|
4
|
+
import { NotificationBell } from '@goerp/core/notification/ui'
|
|
5
|
+
import { MainLayout } from '@goerp/core/ui'
|
|
6
|
+
|
|
7
|
+
export function MainLayoutWrapper({
|
|
8
|
+
children,
|
|
9
|
+
dictionary,
|
|
10
|
+
navigation,
|
|
11
|
+
}: {
|
|
12
|
+
children: React.ReactNode
|
|
13
|
+
dictionary: Record<string, unknown>
|
|
14
|
+
navigation: NavigationType[]
|
|
15
|
+
}) {
|
|
16
|
+
return (
|
|
17
|
+
<MainLayout
|
|
18
|
+
dictionary={dictionary}
|
|
19
|
+
navigation={navigation}
|
|
20
|
+
onGlobalSearch={async () => []}
|
|
21
|
+
searchResults={[]}
|
|
22
|
+
searchLoading={false}
|
|
23
|
+
// Chuông đọc /api/notifications* — bỏ prop này thì người dùng không có
|
|
24
|
+
// đường nào thấy thông báo mà notify() đã ghi.
|
|
25
|
+
notificationSlot={<NotificationBell />}
|
|
26
|
+
>
|
|
27
|
+
{children}
|
|
28
|
+
</MainLayout>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { departmentsConfig } from "@goerp/core/configs/entities"
|
|
2
|
+
|
|
3
|
+
import type { EntityConfig } from "@goerp/core/types"
|
|
4
|
+
|
|
5
|
+
const CRUD_ENDPOINT = "/api/crud/departments"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Reuse core's canonical "Phòng ban" config; override only what's app-specific.
|
|
9
|
+
* Feed this to the core CRUD engine (@goerp/core/crud) to get a full
|
|
10
|
+
* table/form/import/export page for the Department entity.
|
|
11
|
+
*/
|
|
12
|
+
export const departmentConfig: EntityConfig = {
|
|
13
|
+
...departmentsConfig,
|
|
14
|
+
// Trỏ vào route CRUD generic (/api/crud/[entity]) — được phục vụ bởi engine core
|
|
15
|
+
// qua src/lib/crud + src/app/api/crud. Đổi nếu bạn tự viết route riêng.
|
|
16
|
+
apiEndpoint: CRUD_ENDPOINT,
|
|
17
|
+
// Repoint the self-referencing "parent department" dropdown at the same
|
|
18
|
+
// generic endpoint (core's default points at /api/departments).
|
|
19
|
+
fields: departmentsConfig.fields.map((field) =>
|
|
20
|
+
field.dataSource?.type === "api"
|
|
21
|
+
? { ...field, dataSource: { ...field.dataSource, endpoint: CRUD_ENDPOINT } }
|
|
22
|
+
: field,
|
|
23
|
+
),
|
|
24
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { EntityConfig } from "@goerp/core/types"
|
|
2
|
+
|
|
3
|
+
import { departmentConfig } from "./department.config"
|
|
4
|
+
|
|
5
|
+
// Đăng ký entity → cấu hình. Khóa = số nhiều, khớp URL /crud/<key> + apiEndpoint.
|
|
6
|
+
// Thêm entity mới: import config rồi thêm 1 dòng ở đây.
|
|
7
|
+
export const entityConfigs: Record<string, EntityConfig> = {
|
|
8
|
+
departments: departmentConfig,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getEntityConfig(entity: string): EntityConfig | undefined {
|
|
12
|
+
return entityConfigs[entity]
|
|
13
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import masterDataPermissions from "./master-data.permissions"
|
|
2
|
+
import systemPermissions from "./system.permissions"
|
|
3
|
+
|
|
4
|
+
import type { FeaturePermissions, LookupPolicy, ResourceDeclaration } from "./types"
|
|
5
|
+
|
|
6
|
+
export type { FeaturePermissions, LookupPolicy, ResourceDeclaration }
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Registry hợp nhất — thêm phân hệ mới thì import file khai báo và nối vào
|
|
10
|
+
* mảng dưới đây. Giữ file này DỮ LIỆU THUẦN (xem types.ts).
|
|
11
|
+
*/
|
|
12
|
+
export const permissionRegistry: readonly FeaturePermissions[] = [
|
|
13
|
+
systemPermissions,
|
|
14
|
+
masterDataPermissions,
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
const resourceByCode = new Map<string, ResourceDeclaration>()
|
|
18
|
+
const featureByResourceCode = new Map<string, string>()
|
|
19
|
+
for (const feature of permissionRegistry) {
|
|
20
|
+
for (const resource of feature.resources) {
|
|
21
|
+
const owner = featureByResourceCode.get(resource.code)
|
|
22
|
+
if (owner) {
|
|
23
|
+
// Trùng mã resource là lỗi cấu hình im lặng nguy hiểm nhất: hai phân hệ
|
|
24
|
+
// tưởng mình sở hữu cùng một quyền. Ném ngay lúc nạp module.
|
|
25
|
+
throw new Error(
|
|
26
|
+
`Permission registry: resource "${resource.code}" khai trùng ở "${feature.feature}" và "${owner}"`,
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
resourceByCode.set(resource.code, resource)
|
|
30
|
+
featureByResourceCode.set(resource.code, feature.feature)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getRegistryResource(code: string): ResourceDeclaration | undefined {
|
|
35
|
+
return resourceByCode.get(code)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getRegistryResourceCodes(): string[] {
|
|
39
|
+
return [...resourceByCode.keys()]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Resource nào cho phép mọi người đã đăng nhập đọc để đổ combo/picker. */
|
|
43
|
+
export function isLookupOpenToAuthenticated(code: string): boolean {
|
|
44
|
+
return resourceByCode.get(code)?.lookupPolicy === "authenticated"
|
|
45
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { FeaturePermissions } from "./types"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Danh mục nghiệp vụ của app — MẪU. Xoá Department và khai resource của bạn
|
|
5
|
+
* theo đúng khuôn: mã resource ở đây phải trùng chuỗi bạn truyền vào
|
|
6
|
+
* checkPermission và trùng `permissionResource` trong EntityConfig, nếu không
|
|
7
|
+
* nút bấm sẽ ẩn vĩnh viễn mà không báo lỗi.
|
|
8
|
+
*/
|
|
9
|
+
const masterDataPermissions: FeaturePermissions = {
|
|
10
|
+
feature: "master-data",
|
|
11
|
+
description: "Danh mục dùng chung",
|
|
12
|
+
resources: [
|
|
13
|
+
{
|
|
14
|
+
code: "department",
|
|
15
|
+
name: "Phòng ban",
|
|
16
|
+
group: "Danh mục",
|
|
17
|
+
icon: "Building2",
|
|
18
|
+
order: 1,
|
|
19
|
+
actions: ["view", "create", "update", "delete", "export", "import"],
|
|
20
|
+
// Phòng ban xuất hiện trong nhiều combo (hồ sơ nhân sự, phân bổ chi phí…)
|
|
21
|
+
// nên cho mọi người đăng nhập đọc được danh sách để đổ picker.
|
|
22
|
+
lookupPolicy: "authenticated",
|
|
23
|
+
defaultGrants: {
|
|
24
|
+
admin: "*",
|
|
25
|
+
staff: ["view", "export"],
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default masterDataPermissions
|