@goplusvn/core 0.1.70 → 0.1.72

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/bin/goerp-init.mjs +15 -0
  3. package/package.json +2 -1
  4. package/src/auth/index.ts +5 -1
  5. package/src/auth/proxy-gate.ts +21 -2
  6. package/src/configs/entities/departments.config.ts +1 -0
  7. package/src/configs/entities/material-categories.config.ts +1 -0
  8. package/src/cron/__tests__/cron-schedule.test.ts +76 -0
  9. package/src/cron/cron-schedule.ts +128 -0
  10. package/src/cron/simple-cron-job.ts +83 -69
  11. package/src/crud/components/crud-export-button.tsx +3 -2
  12. package/src/crud/components/crud-page.tsx +56 -25
  13. package/src/crud/components/crud-row-actions.tsx +16 -4
  14. package/src/crud/components/crud-table.tsx +13 -1
  15. package/src/crud/crud-route-handlers.test.ts +352 -0
  16. package/src/crud/crud-route-handlers.ts +260 -13
  17. package/src/crud/index.ts +3 -0
  18. package/src/crud/lib/coerce.test.ts +118 -0
  19. package/src/crud/lib/coerce.ts +95 -0
  20. package/src/crud/lib/crud-utils.ts +4 -0
  21. package/src/crud/lib/entity-endpoints.test.ts +41 -0
  22. package/src/crud/lib/entity-endpoints.ts +23 -0
  23. package/src/crud/lib/errors.ts +14 -0
  24. package/src/crud/lib/import-request.ts +99 -0
  25. package/src/crud/lib/mutation-builder.test.ts +114 -0
  26. package/src/crud/lib/mutation-builder.ts +27 -34
  27. package/src/crud/lib/permissions.test.ts +57 -0
  28. package/src/crud/lib/permissions.ts +25 -13
  29. package/src/crud/lib/query-builder.ts +13 -6
  30. package/src/crud/lib/translate-config.ts +16 -2
  31. package/src/crud/pages/entity-crud-page.tsx +11 -8
  32. package/src/crud/server-service.test.ts +112 -0
  33. package/src/crud/server-service.ts +52 -27
  34. package/src/crud/server.ts +11 -1
  35. package/src/guardrails/index.ts +1 -0
  36. package/src/guardrails/rules/auth.ts +54 -1
  37. package/src/guardrails/rules/design.ts +3 -2
  38. package/src/guardrails/types.ts +17 -8
  39. package/src/providers/brand-theme.ts +20 -0
  40. package/src/rbac/pages/permission-catalog-pages.tsx +3 -1
  41. package/src/security/index.ts +2 -0
  42. package/src/security/pages/sessions-page.tsx +516 -0
  43. package/src/types/index.ts +75 -10
  44. package/src/ui/auth/sign-in-form.tsx +64 -9
  45. package/src/ui/data-display/__tests__/use-client-pagination.test.ts +59 -0
  46. package/src/ui/data-display/data-table/__tests__/data-table-row-memo.test.tsx +96 -0
  47. package/src/ui/data-display/data-table/data-table-context.tsx +5 -0
  48. package/src/ui/data-display/data-table/data-table.tsx +68 -23
  49. package/src/ui/data-display/data-table-pagination.tsx +25 -8
  50. package/src/ui/data-display/index.tsx +1 -0
  51. package/src/ui/data-display/use-client-pagination.ts +48 -0
  52. package/src/ui/errors/error-view.tsx +164 -0
  53. package/src/ui/errors/index.ts +2 -0
  54. package/src/ui/errors/not-found-view.tsx +44 -0
  55. package/src/ui/index.tsx +1 -0
  56. package/src/ui/layout/logo.tsx +54 -19
  57. package/src/user/pages/users-client-page.tsx +12 -7
  58. package/templates/starter-app/.env.example +4 -0
  59. package/templates/starter-app/AGENTS.md +1 -1
  60. package/templates/starter-app/README.md +6 -2
  61. package/templates/starter-app/husky/pre-commit +10 -0
  62. package/templates/starter-app/package.json +27 -2
  63. package/templates/starter-app/prisma/migrations/20260807115347_platform_pages/migration.sql +87 -0
  64. package/templates/starter-app/prisma/migrations/20260808003811_company_profile/migration.sql +25 -0
  65. package/templates/starter-app/prisma/schema/organization.prisma +20 -0
  66. package/templates/starter-app/prisma/schema/system.prisma +74 -0
  67. package/templates/starter-app/prisma/seed.ts +7 -5
  68. package/templates/starter-app/scripts/migration-new.mjs +89 -0
  69. package/templates/starter-app/scripts/rbac-sync.ts +49 -21
  70. package/templates/starter-app/src/__tests__/architecture.test.ts +11 -2
  71. package/templates/starter-app/src/app/[lang]/(main)/actions/page.tsx +37 -0
  72. package/templates/starter-app/src/app/[lang]/(main)/admin/system/cache/page.tsx +68 -0
  73. package/templates/starter-app/src/app/[lang]/(main)/admin/system/company/actions.ts +33 -0
  74. package/templates/starter-app/src/app/[lang]/(main)/admin/system/company/company-profile-form.tsx +195 -0
  75. package/templates/starter-app/src/app/[lang]/(main)/admin/system/company/page.tsx +29 -0
  76. package/templates/starter-app/src/app/[lang]/(main)/crud/[entity]/page.tsx +4 -2
  77. package/templates/starter-app/src/app/[lang]/(main)/error.tsx +24 -0
  78. package/templates/starter-app/src/app/[lang]/(main)/layout.tsx +2 -1
  79. package/templates/starter-app/src/app/[lang]/(main)/not-found.tsx +6 -0
  80. package/templates/starter-app/src/app/[lang]/(main)/page.tsx +13 -3
  81. package/templates/starter-app/src/app/[lang]/(main)/resources/page.tsx +39 -0
  82. package/templates/starter-app/src/app/[lang]/(main)/roles/[id]/edit/page.tsx +5 -2
  83. package/templates/starter-app/src/app/[lang]/(main)/roles/new/page.tsx +1 -0
  84. package/templates/starter-app/src/app/[lang]/(main)/roles/page.tsx +1 -0
  85. package/templates/starter-app/src/app/[lang]/(main)/security/sessions/page.tsx +24 -0
  86. package/templates/starter-app/src/app/[lang]/(main)/system-categories/page.tsx +30 -0
  87. package/templates/starter-app/src/app/[lang]/(main)/user/profile/actions.ts +62 -0
  88. package/templates/starter-app/src/app/[lang]/(main)/user/profile/page.tsx +41 -0
  89. package/templates/starter-app/src/app/[lang]/(main)/user/profile/profile-client-page.tsx +157 -0
  90. package/templates/starter-app/src/app/[lang]/(main)/users/page.tsx +69 -0
  91. package/templates/starter-app/src/app/[lang]/[...not-found]/page.tsx +9 -0
  92. package/templates/starter-app/src/app/[lang]/error.tsx +26 -0
  93. package/templates/starter-app/src/app/[lang]/layout.tsx +2 -2
  94. package/templates/starter-app/src/app/[lang]/not-found.tsx +9 -0
  95. package/templates/starter-app/src/app/api/admin/system/audit/route.ts +4 -3
  96. package/templates/starter-app/src/app/api/admin/system/jobs/[name]/history/route.ts +1 -1
  97. package/templates/starter-app/src/app/api/admin/system/jobs/route.ts +31 -12
  98. package/templates/starter-app/src/app/api/admin/system/settings/all/route.ts +2 -2
  99. package/templates/starter-app/src/app/api/admin/system/settings/create/route.ts +10 -3
  100. package/templates/starter-app/src/app/api/admin/system/settings/delete/route.ts +10 -3
  101. package/templates/starter-app/src/app/api/admin/system/settings/route.ts +14 -5
  102. package/templates/starter-app/src/app/api/admin/system/settings/toggle-status/route.ts +16 -7
  103. package/templates/starter-app/src/app/api/admin/system/settings/update/route.ts +12 -6
  104. package/templates/starter-app/src/app/api/admin/system/settings/update-full/route.ts +12 -6
  105. package/templates/starter-app/src/app/api/better-auth/[...all]/route.ts +1 -1
  106. package/templates/starter-app/src/app/api/branches/route.ts +26 -0
  107. package/templates/starter-app/src/app/api/crud/[entity]/[id]/route.ts +2 -2
  108. package/templates/starter-app/src/app/api/crud/[entity]/export/route.ts +16 -0
  109. package/templates/starter-app/src/app/api/crud/[entity]/import/route.ts +17 -0
  110. package/templates/starter-app/src/app/api/crud/[entity]/route.ts +2 -2
  111. package/templates/starter-app/src/app/api/departments/route.ts +26 -0
  112. package/templates/starter-app/src/app/api/error-logs/[id]/route.ts +4 -2
  113. package/templates/starter-app/src/app/api/error-logs/route.ts +26 -19
  114. package/templates/starter-app/src/app/api/files/[...key]/route.ts +1 -2
  115. package/templates/starter-app/src/app/api/job-titles/route.ts +46 -0
  116. package/templates/starter-app/src/app/api/notifications/read/route.ts +1 -1
  117. package/templates/starter-app/src/app/api/notifications/route.ts +1 -1
  118. package/templates/starter-app/src/app/api/notifications/unread-count/route.ts +1 -1
  119. package/templates/starter-app/src/app/api/rbac/permissions-version/route.ts +4 -1
  120. package/templates/starter-app/src/app/api/roles/[id]/route.ts +2 -1
  121. package/templates/starter-app/src/app/api/roles/route.ts +2 -1
  122. package/templates/starter-app/src/app/api/security/sessions/[id]/route.ts +52 -0
  123. package/templates/starter-app/src/app/api/security/sessions/revoke-user/route.ts +44 -0
  124. package/templates/starter-app/src/app/api/security/sessions/route.ts +101 -0
  125. package/templates/starter-app/src/app/api/suppliers/route.ts +13 -0
  126. package/templates/starter-app/src/app/api/system-categories/route.ts +179 -0
  127. package/templates/starter-app/src/app/api/system-category-groups/route.ts +149 -0
  128. package/templates/starter-app/src/app/api/tasks/[id]/download/route.ts +7 -3
  129. package/templates/starter-app/src/app/api/tasks/route.ts +5 -2
  130. package/templates/starter-app/src/app/api/upload/route.ts +1 -2
  131. package/templates/starter-app/src/app/api/users/[id]/route.ts +230 -0
  132. package/templates/starter-app/src/app/api/users/route.ts +141 -0
  133. package/templates/starter-app/src/app/global-error.tsx +135 -0
  134. package/templates/starter-app/src/app/globals.css +0 -1
  135. package/templates/starter-app/src/app/icon.svg +6 -0
  136. package/templates/starter-app/src/app/manifest.ts +26 -0
  137. package/templates/starter-app/src/components/layout/main-layout-wrapper.tsx +5 -4
  138. package/templates/starter-app/src/configs/entities/department.config.ts +7 -4
  139. package/templates/starter-app/src/configs/entities/index.ts +5 -1
  140. package/templates/starter-app/src/configs/entities/job-titles.config.ts +121 -0
  141. package/templates/starter-app/src/configs/entities/system-alerts.config.ts +93 -0
  142. package/templates/starter-app/src/configs/entities/users.config.ts +80 -0
  143. package/templates/starter-app/src/configs/i18n.ts +6 -6
  144. package/templates/starter-app/src/configs/permissions/index.ts +37 -0
  145. package/templates/starter-app/src/configs/permissions/master-data.permissions.ts +11 -0
  146. package/templates/starter-app/src/configs/permissions/menu-tree.ts +37 -0
  147. package/templates/starter-app/src/configs/permissions/system.permissions.ts +29 -0
  148. package/templates/starter-app/src/configs/tenant.ts +2 -1
  149. package/templates/starter-app/src/data/dictionary.ts +77 -4
  150. package/templates/starter-app/src/data/navigations.ts +60 -1
  151. package/templates/starter-app/src/instrumentation.ts +5 -16
  152. package/templates/starter-app/src/lib/action-guard.ts +25 -0
  153. package/templates/starter-app/src/lib/api-handler.ts +27 -19
  154. package/templates/starter-app/src/lib/auth.ts +3 -1
  155. package/templates/starter-app/src/lib/better-auth.ts +8 -0
  156. package/templates/starter-app/src/lib/branch-scope.ts +1 -1
  157. package/templates/starter-app/src/lib/crud/index.ts +5 -10
  158. package/templates/starter-app/src/lib/errors/log-server-error.ts +5 -4
  159. package/templates/starter-app/src/lib/logger.ts +11 -4
  160. package/templates/starter-app/src/lib/page-guard.ts +2 -3
  161. package/templates/starter-app/src/lib/prisma.ts +16 -7
  162. package/templates/starter-app/src/lib/rbac/access.ts +2 -2
  163. package/templates/starter-app/src/lib/storage.ts +3 -1
  164. package/templates/starter-app/src/providers/index.tsx +2 -1
  165. package/templates/starter-app/src/providers/mode-provider.tsx +13 -18
  166. package/templates/starter-app/src/providers/theme-provider.tsx +27 -12
  167. package/templates/starter-app/src/proxy.ts +8 -3
  168. package/templates/starter-app/src/server/services/company-service.ts +49 -0
  169. package/templates/starter-app/src/server/services/notification-service.ts +6 -6
  170. package/templates/starter-app/src/server/services/system-config-service.ts +21 -9
  171. package/templates/starter-app/src/server/services/user-service.ts +104 -0
  172. package/templates/starter-app/src/server/tasks/handlers/export-departments.ts +15 -9
  173. package/templates/starter-app/src/server/tasks/task-runner.ts +6 -7
  174. package/templates/starter-app/tsconfig.json +10 -0
  175. package/templates/starter-app/vitest.config.ts +17 -1
@@ -1,9 +1,9 @@
1
1
  import { createCrudItemHandlers } from "@goerp/core/crud/server"
2
2
 
3
- import { getSession } from "@/lib/auth"
4
3
  import { getEntityConfig } from "@/configs/entities"
5
- import { serverError } from "@/lib/errors/server-error"
4
+ import { getSession } from "@/lib/auth"
6
5
  import { crudService } from "@/lib/crud"
6
+ import { serverError } from "@/lib/errors/server-error"
7
7
 
8
8
  export const { GET, PUT, PATCH, DELETE } = createCrudItemHandlers({
9
9
  getSession,
@@ -0,0 +1,16 @@
1
+ import { createCrudExportHandler } from "@goerp/core/crud/server"
2
+
3
+ import { getEntityConfig } from "@/configs/entities"
4
+ import { getSession } from "@/lib/auth"
5
+ import { crudService } from "@/lib/crud"
6
+ import { serverError } from "@/lib/errors/server-error"
7
+
8
+ // Xuất xlsx/csv/json theo đúng search/sort/filter đang áp trên bảng. Nút
9
+ // Export của CrudPage mặc định trỏ về đây (`${apiEndpoint}/export`); gác
10
+ // quyền `<resource>:export` ở core.
11
+ export const { GET } = createCrudExportHandler({
12
+ getSession,
13
+ getEntityConfig,
14
+ service: crudService,
15
+ onError: serverError,
16
+ })
@@ -0,0 +1,17 @@
1
+ import { createCrudImportHandler } from "@goerp/core/crud/server"
2
+
3
+ import { getEntityConfig } from "@/configs/entities"
4
+ import { getSession } from "@/lib/auth"
5
+ import { crudService } from "@/lib/crud"
6
+ import { serverError } from "@/lib/errors/server-error"
7
+
8
+ // Nhập từ file (multipart của ImportDialog) hoặc JSON array. Mỗi dòng đi qua
9
+ // service.create như tạo tay — coercion/validate/audit của engine áp đủ; dòng
10
+ // lỗi gom vào ImportResult.errors, không chặn dòng sau. Gác quyền
11
+ // `<resource>:import` ở core.
12
+ export const { POST } = createCrudImportHandler({
13
+ getSession,
14
+ getEntityConfig,
15
+ service: crudService,
16
+ onError: serverError,
17
+ })
@@ -1,9 +1,9 @@
1
1
  import { createCrudCollectionHandlers } from "@goerp/core/crud/server"
2
2
 
3
- import { getSession } from "@/lib/auth"
4
3
  import { getEntityConfig } from "@/configs/entities"
5
- import { serverError } from "@/lib/errors/server-error"
4
+ import { getSession } from "@/lib/auth"
6
5
  import { crudService } from "@/lib/crud"
6
+ import { serverError } from "@/lib/errors/server-error"
7
7
 
8
8
  // GET danh sách + POST tạo. Gác quyền + query đều ở core.
9
9
  export const { GET, POST } = createCrudCollectionHandlers({
@@ -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 { db } from "@/lib/prisma"
6
+
7
+ /**
8
+ * Lookup phòng ban cho picker (UnifiedProfileDialog của core fetch cứng
9
+ * /api/departments?status=active). Registry khai `department` với lookupPolicy
10
+ * "authenticated" nên chỉ cần đăng nhập, không gác resource/action.
11
+ *
12
+ * Trang quản lý phòng ban vẫn đi qua /api/crud/departments (CRUD engine) —
13
+ * route này CHỈ phục vụ combo.
14
+ */
15
+ export const GET = apiHandler(async (req) => {
16
+ try {
17
+ const departments = await db.department.findMany({
18
+ where: { status: "active" },
19
+ select: { id: true, code: true, name: true },
20
+ orderBy: { order: "asc" },
21
+ })
22
+ return NextResponse.json({ data: departments })
23
+ } catch (error) {
24
+ return serverError(error, req, { message: "Lỗi tải danh sách phòng ban" })
25
+ }
26
+ })
@@ -16,8 +16,10 @@ export const PATCH = apiHandler(
16
16
  })
17
17
  return NextResponse.json({ success: true, log })
18
18
  } catch (error) {
19
- return serverError(error, req, { message: "Không cập nhật được trạng thái lỗi" })
19
+ return serverError(error, req, {
20
+ message: "Không cập nhật được trạng thái lỗi",
21
+ })
20
22
  }
21
23
  },
22
- { resource: "error-log", action: "update" },
24
+ { resource: "error-log", action: "update" }
23
25
  )
@@ -12,7 +12,6 @@ import { db } from "@/lib/prisma"
12
12
  * và dọn.
13
13
  */
14
14
 
15
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
15
  type Where = Record<string, any>
17
16
 
18
17
  export const GET = apiHandler(
@@ -21,7 +20,7 @@ export const GET = apiHandler(
21
20
  const sp = new URL(req.url).searchParams
22
21
  const search = sp.get("search") || undefined
23
22
  const severity = sp.get("severity") || undefined
24
- const module = sp.get("module") || undefined
23
+ const moduleFilter = sp.get("module") || undefined
25
24
  const resolved = sp.get("resolved")
26
25
  const startDate = sp.get("startDate")
27
26
  const endDate = sp.get("endDate")
@@ -38,7 +37,7 @@ export const GET = apiHandler(
38
37
  ]
39
38
  }
40
39
  if (severity && severity !== "all") where.severity = severity
41
- if (module && module !== "all") where.module = module
40
+ if (moduleFilter && moduleFilter !== "all") where.module = moduleFilter
42
41
  if (resolved === "true") where.resolved = true
43
42
  else if (resolved === "false") where.resolved = false
44
43
  if (startDate || endDate) {
@@ -50,23 +49,31 @@ export const GET = apiHandler(
50
49
  const startOfToday = new Date()
51
50
  startOfToday.setHours(0, 0, 0, 0)
52
51
 
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
- ])
52
+ const [logs, total, todayCount, unresolvedCount, moduleStats] =
53
+ await Promise.all([
54
+ db.errorLog.findMany({
55
+ where,
56
+ orderBy: { lastSeenAt: "desc" },
57
+ skip,
58
+ take,
59
+ }),
60
+ db.errorLog.count({ where }),
61
+ db.errorLog.count({ where: { createdAt: { gte: startOfToday } } }),
62
+ db.errorLog.count({ where: { resolved: false } }),
63
+ db.errorLog.groupBy({
64
+ by: ["module"],
65
+ _count: { module: true },
66
+ where: { resolved: false },
67
+ orderBy: { _count: { module: "desc" } },
68
+ take: 8,
69
+ }),
70
+ ])
66
71
 
67
72
  // ErrorLog không có quan hệ tới User (lỗi có thể xảy ra khi chưa đăng
68
73
  // nhập) — tra tên riêng thay vì include.
69
- const userIds = [...new Set(logs.map((l) => l.userId).filter(Boolean))] as string[]
74
+ const userIds = [
75
+ ...new Set(logs.map((l) => l.userId).filter(Boolean)),
76
+ ] as string[]
70
77
  const nameById = new Map<string, string>()
71
78
  if (userIds.length) {
72
79
  const users = await db.user.findMany({
@@ -98,7 +105,7 @@ export const GET = apiHandler(
98
105
  return serverError(error, req, { message: "Không tải được nhật ký lỗi" })
99
106
  }
100
107
  },
101
- { resource: "error-log", action: "view" },
108
+ { resource: "error-log", action: "view" }
102
109
  )
103
110
 
104
111
  /** Dọn log: ?mode=resolved (đã xử lý) hoặc ?mode=older_than&days=30. */
@@ -120,5 +127,5 @@ export const DELETE = apiHandler(
120
127
  return serverError(error, req, { message: "Không xóa được nhật ký lỗi" })
121
128
  }
122
129
  },
123
- { resource: "error-log", action: "delete" },
130
+ { resource: "error-log", action: "delete" }
124
131
  )
@@ -1,6 +1,5 @@
1
- import { createFileProxyHandler } from "@/lib/storage"
2
-
3
1
  import { apiHandler } from "@/lib/api-handler"
2
+ import { createFileProxyHandler } from "@/lib/storage"
4
3
 
5
4
  /**
6
5
  * GET /api/files/[...key] — phục vụ tập tin từ kho qua app server.
@@ -0,0 +1,46 @@
1
+ import { NextResponse } from "next/server"
2
+
3
+ import { apiHandler } from "@/lib/api-handler"
4
+ import { db } from "@/lib/prisma"
5
+
6
+ /**
7
+ * GET danh sách chức danh — endpoint đọc gọn cho picker/dataSource. Màn hình
8
+ * quản lý dùng CRUD generic (/crud/job-titles → /api/crud/job-titles).
9
+ */
10
+ export const GET = apiHandler(
11
+ async (req) => {
12
+ const { searchParams } = new URL(req.url)
13
+ const page = Math.max(Number(searchParams.get("page") || 1), 1)
14
+ const pageSize = Math.min(Number(searchParams.get("pageSize") || 100), 100)
15
+ const search = (searchParams.get("q") || searchParams.get("search"))?.trim()
16
+ const status = searchParams.get("status")?.trim()
17
+
18
+ const where = {
19
+ AND: [
20
+ search
21
+ ? {
22
+ OR: [
23
+ { code: { contains: search, mode: "insensitive" as const } },
24
+ { name: { contains: search, mode: "insensitive" as const } },
25
+ ],
26
+ }
27
+ : {},
28
+ status && status !== "all" ? { status } : {},
29
+ ],
30
+ }
31
+
32
+ const [total, items] = await Promise.all([
33
+ db.jobTitle.count({ where }),
34
+ db.jobTitle.findMany({
35
+ where,
36
+ orderBy: [{ level: "desc" }, { name: "asc" }],
37
+ skip: (page - 1) * pageSize,
38
+ take: pageSize,
39
+ }),
40
+ ])
41
+
42
+ return NextResponse.json({ total, page, pageSize, items })
43
+ }
44
+ // Combo tra cứu — registry khai lookupPolicy "authenticated" cho job-title:
45
+ // ai đăng nhập cũng đổ được picker, không đòi quyền view của trang quản lý.
46
+ )
@@ -1,8 +1,8 @@
1
1
  import { NextResponse } from "next/server"
2
+ import { markAllRead, markRead } from "@/server/services/notification-service"
2
3
 
3
4
  import { apiHandler } from "@/lib/api-handler"
4
5
  import { serverError } from "@/lib/errors/server-error"
5
- import { markAllRead, markRead } from "@/server/services/notification-service"
6
6
 
7
7
  /**
8
8
  * POST /api/notifications/read
@@ -1,8 +1,8 @@
1
1
  import { NextResponse } from "next/server"
2
+ import { listNotifications } from "@/server/services/notification-service"
2
3
 
3
4
  import { apiHandler } from "@/lib/api-handler"
4
5
  import { serverError } from "@/lib/errors/server-error"
5
- import { listNotifications } from "@/server/services/notification-service"
6
6
 
7
7
  /**
8
8
  * GET /api/notifications?cursor=&take=&filter=unread|all
@@ -1,8 +1,8 @@
1
1
  import { NextResponse } from "next/server"
2
+ import { getUnreadCount } from "@/server/services/notification-service"
2
3
 
3
4
  import { apiHandler } from "@/lib/api-handler"
4
5
  import { serverError } from "@/lib/errors/server-error"
5
- import { getUnreadCount } from "@/server/services/notification-service"
6
6
 
7
7
  /** GET /api/notifications/unread-count → { count }. Endpoint nhẹ cho chuông poll. */
8
8
  export const GET = apiHandler(async (req, { session }) => {
@@ -8,4 +8,7 @@ import { db } from "@/lib/prisma"
8
8
  * của core). Số version đổi ⇒ trình duyệt tự nạp lại phiên, nên admin sửa
9
9
  * quyền là người dùng thấy ngay, không phải đăng xuất/đăng nhập lại.
10
10
  */
11
- export const { GET } = createPermissionsVersionHandler({ prisma: db, getSession })
11
+ export const { GET } = createPermissionsVersionHandler({
12
+ prisma: db,
13
+ getSession,
14
+ })
@@ -10,5 +10,6 @@ export const { GET, PUT, DELETE } = createRoleItemHandlers({
10
10
  prisma: db,
11
11
  getSession,
12
12
  getCrudPermissions,
13
- onError: (error, req) => serverError(error, req, { message: "Lỗi xử lý vai trò" }),
13
+ onError: (error, req) =>
14
+ serverError(error, req, { message: "Lỗi xử lý vai trò" }),
14
15
  })
@@ -14,5 +14,6 @@ export const { GET, POST } = createRolesCollectionHandlers({
14
14
  prisma: db,
15
15
  getSession,
16
16
  getCrudPermissions,
17
- onError: (error, req) => serverError(error, req, { message: "Lỗi xử lý vai trò" }),
17
+ onError: (error, req) =>
18
+ serverError(error, req, { message: "Lỗi xử lý vai trò" }),
18
19
  })
@@ -0,0 +1,52 @@
1
+ import { NextResponse } from "next/server"
2
+ import { checkPermission } from "@goerp/core/auth"
3
+
4
+ import { apiHandler } from "@/lib/api-handler"
5
+ import { getAuth } from "@/lib/better-auth"
6
+ import { serverError } from "@/lib/errors/server-error"
7
+ import { db } from "@/lib/prisma"
8
+
9
+ /**
10
+ * DELETE /api/security/sessions/[id] — thu hồi 1 phiên đăng nhập.
11
+ * - Phiên của mình: được thu hồi (trừ phiên HIỆN TẠI — dùng nút Đăng xuất).
12
+ * - Phiên user khác: cần quyền user:update (thao tác quản trị).
13
+ * Xóa row `sessions` = phiên chết ngay (session DB-backed).
14
+ */
15
+ export const DELETE = apiHandler(async (req, { session, params }) => {
16
+ try {
17
+ const { id } = await params
18
+ const row = await db.session.findUnique({
19
+ where: { id },
20
+ select: { id: true, userId: true, token: true },
21
+ })
22
+ if (!row) {
23
+ return NextResponse.json(
24
+ { error: "Phiên không tồn tại" },
25
+ { status: 404 }
26
+ )
27
+ }
28
+
29
+ const isMine = row.userId === session.user.id
30
+
31
+ if (!isMine && !checkPermission(session, "user", "update")) {
32
+ return NextResponse.json(
33
+ { error: "Bạn không có quyền thu hồi phiên của người dùng khác" },
34
+ { status: 403 }
35
+ )
36
+ }
37
+
38
+ // Không tự thu hồi phiên đang dùng (tránh tự bắn vào chân — dùng Đăng xuất)
39
+ const current = await getAuth().api.getSession({ headers: req.headers })
40
+ if (current?.session?.token && row.token === current.session.token) {
41
+ return NextResponse.json(
42
+ { error: "Không thể thu hồi phiên hiện tại — hãy dùng Đăng xuất" },
43
+ { status: 400 }
44
+ )
45
+ }
46
+
47
+ await db.session.delete({ where: { id } })
48
+ return NextResponse.json({ ok: true })
49
+ } catch (error) {
50
+ return serverError(error, req, { message: "Không thu hồi được phiên" })
51
+ }
52
+ })
@@ -0,0 +1,44 @@
1
+ import { NextResponse } from "next/server"
2
+
3
+ import { apiHandler } from "@/lib/api-handler"
4
+ import { getAuth } from "@/lib/better-auth"
5
+ import { serverError } from "@/lib/errors/server-error"
6
+ import { db } from "@/lib/prisma"
7
+
8
+ /**
9
+ * POST /api/security/sessions/revoke-user — QUẢN TRỊ: đăng xuất MỌI thiết bị
10
+ * của 1 người dùng (xóa toàn bộ row `sessions` của user đó → chết ngay).
11
+ * - Cần user:update (thao tác quản trị).
12
+ * - Nếu tự thu hồi chính mình: giữ lại phiên HIỆN TẠI (không tự bắn văng).
13
+ * Dùng khi: nghi ngờ lộ tài khoản, thu máy, cho nghỉ việc...
14
+ */
15
+ export const POST = apiHandler(
16
+ async (req, { session }) => {
17
+ try {
18
+ const body = await req.json().catch(() => null)
19
+ const userId = typeof body?.userId === "string" ? body.userId : ""
20
+ if (!userId) {
21
+ return NextResponse.json({ error: "Thiếu userId" }, { status: 400 })
22
+ }
23
+
24
+ // Tự thu hồi chính mình → chừa phiên hiện tại
25
+ let excludeToken: string | undefined
26
+ if (userId === session.user.id) {
27
+ const current = await getAuth().api.getSession({ headers: req.headers })
28
+ excludeToken = current?.session?.token ?? undefined
29
+ }
30
+
31
+ const r = await db.session.deleteMany({
32
+ where: {
33
+ userId,
34
+ ...(excludeToken ? { token: { not: excludeToken } } : {}),
35
+ },
36
+ })
37
+
38
+ return NextResponse.json({ revoked: r.count })
39
+ } catch (error) {
40
+ return serverError(error, req, { message: "Không thu hồi được phiên" })
41
+ }
42
+ },
43
+ { resource: "user", action: "update" }
44
+ )
@@ -0,0 +1,101 @@
1
+ import { NextResponse } from "next/server"
2
+
3
+ import { apiHandler } from "@/lib/api-handler"
4
+ import { getAuth } from "@/lib/better-auth"
5
+ import { serverError } from "@/lib/errors/server-error"
6
+ import { db } from "@/lib/prisma"
7
+
8
+ /**
9
+ * GET /api/security/sessions — QUẢN TRỊ phiên đăng nhập toàn hệ thống
10
+ * (bảng `sessions` của Better Auth). Gate user:view. Search: tên/email user,
11
+ * IP, user-agent. Phân trang chuẩn {items,total,page,pageSize}. KHÔNG trả
12
+ * token (nhạy cảm) — thu hồi dùng id.
13
+ */
14
+ export const GET = apiHandler(
15
+ async (req, { session }) => {
16
+ try {
17
+ const sp = req.nextUrl.searchParams
18
+ const search = sp.get("search")?.trim() || ""
19
+ const page = Math.max(Number(sp.get("page") || 1), 1)
20
+ const pageSize = Math.min(
21
+ Math.max(Number(sp.get("pageSize") || 20), 1),
22
+ 100
23
+ )
24
+
25
+ // Token của phiên hiện tại (đánh dấu "thiết bị này" — so sánh server-side,
26
+ // không lộ token ra client)
27
+ const current = await getAuth().api.getSession({ headers: req.headers })
28
+ const currentToken = current?.session?.token ?? null
29
+
30
+ const where = {
31
+ expiresAt: { gt: new Date() }, // chỉ phiên còn hiệu lực
32
+ ...(search
33
+ ? {
34
+ OR: [
35
+ {
36
+ ipAddress: { contains: search, mode: "insensitive" as const },
37
+ },
38
+ {
39
+ userAgent: { contains: search, mode: "insensitive" as const },
40
+ },
41
+ {
42
+ user: {
43
+ OR: [
44
+ {
45
+ name: {
46
+ contains: search,
47
+ mode: "insensitive" as const,
48
+ },
49
+ },
50
+ {
51
+ email: {
52
+ contains: search,
53
+ mode: "insensitive" as const,
54
+ },
55
+ },
56
+ ],
57
+ },
58
+ },
59
+ ],
60
+ }
61
+ : {}),
62
+ }
63
+
64
+ const [total, rows] = await Promise.all([
65
+ db.session.count({ where }),
66
+ db.session.findMany({
67
+ where,
68
+ include: { user: { select: { name: true, email: true } } },
69
+ orderBy: { updatedAt: "desc" },
70
+ skip: (page - 1) * pageSize,
71
+ take: pageSize,
72
+ }),
73
+ ])
74
+
75
+ return NextResponse.json({
76
+ items: rows.map((r) => ({
77
+ id: r.id,
78
+ userId: r.userId,
79
+ userName: r.user?.name ?? null,
80
+ userEmail: r.user?.email ?? null,
81
+ ipAddress: r.ipAddress,
82
+ userAgent: r.userAgent,
83
+ createdAt: r.createdAt,
84
+ updatedAt: r.updatedAt,
85
+ expiresAt: r.expiresAt,
86
+ isCurrent: currentToken !== null && r.token === currentToken,
87
+ isMine: r.userId === session.user.id,
88
+ impersonatedBy: null,
89
+ })),
90
+ total,
91
+ page,
92
+ pageSize,
93
+ })
94
+ } catch (error) {
95
+ return serverError(error, req, {
96
+ message: "Không tải được danh sách phiên",
97
+ })
98
+ }
99
+ },
100
+ { resource: "user", action: "view" }
101
+ )
@@ -0,0 +1,13 @@
1
+ import { NextResponse } from "next/server"
2
+
3
+ import { apiHandler } from "@/lib/api-handler"
4
+
5
+ /**
6
+ * STUB — template không có model Supplier. UnifiedProfileDialog của core
7
+ * fetch cứng /api/suppliers?pageSize=100 (tab "Nhà cung cấp" khi userType =
8
+ * supplier); trả rỗng để dialog không vỡ. App nào có nhà cung cấp thì thay
9
+ * bằng query thật (shape: { data: [{ id, name }] }).
10
+ */
11
+ export const GET = apiHandler(async () => {
12
+ return NextResponse.json({ data: [] })
13
+ })