@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.
- package/CHANGELOG.md +45 -0
- package/bin/goerp-init.mjs +15 -0
- package/package.json +2 -1
- package/src/auth/index.ts +5 -1
- package/src/auth/proxy-gate.ts +21 -2
- package/src/configs/entities/departments.config.ts +1 -0
- package/src/configs/entities/material-categories.config.ts +1 -0
- package/src/cron/__tests__/cron-schedule.test.ts +76 -0
- package/src/cron/cron-schedule.ts +128 -0
- package/src/cron/simple-cron-job.ts +83 -69
- package/src/crud/components/crud-export-button.tsx +3 -2
- package/src/crud/components/crud-page.tsx +56 -25
- package/src/crud/components/crud-row-actions.tsx +16 -4
- package/src/crud/components/crud-table.tsx +13 -1
- package/src/crud/crud-route-handlers.test.ts +352 -0
- package/src/crud/crud-route-handlers.ts +260 -13
- package/src/crud/index.ts +3 -0
- package/src/crud/lib/coerce.test.ts +118 -0
- package/src/crud/lib/coerce.ts +95 -0
- package/src/crud/lib/crud-utils.ts +4 -0
- package/src/crud/lib/entity-endpoints.test.ts +41 -0
- package/src/crud/lib/entity-endpoints.ts +23 -0
- package/src/crud/lib/errors.ts +14 -0
- package/src/crud/lib/import-request.ts +99 -0
- package/src/crud/lib/mutation-builder.test.ts +114 -0
- package/src/crud/lib/mutation-builder.ts +27 -34
- package/src/crud/lib/permissions.test.ts +57 -0
- package/src/crud/lib/permissions.ts +25 -13
- package/src/crud/lib/query-builder.ts +13 -6
- package/src/crud/lib/translate-config.ts +16 -2
- package/src/crud/pages/entity-crud-page.tsx +11 -8
- package/src/crud/server-service.test.ts +112 -0
- package/src/crud/server-service.ts +52 -27
- package/src/crud/server.ts +11 -1
- package/src/guardrails/index.ts +1 -0
- package/src/guardrails/rules/auth.ts +54 -1
- package/src/guardrails/rules/design.ts +3 -2
- package/src/guardrails/types.ts +17 -8
- package/src/providers/brand-theme.ts +20 -0
- package/src/rbac/pages/permission-catalog-pages.tsx +3 -1
- package/src/security/index.ts +2 -0
- package/src/security/pages/sessions-page.tsx +516 -0
- package/src/types/index.ts +75 -10
- package/src/ui/auth/sign-in-form.tsx +64 -9
- package/src/ui/data-display/__tests__/use-client-pagination.test.ts +59 -0
- package/src/ui/data-display/data-table/__tests__/data-table-row-memo.test.tsx +96 -0
- package/src/ui/data-display/data-table/data-table-context.tsx +5 -0
- package/src/ui/data-display/data-table/data-table.tsx +68 -23
- package/src/ui/data-display/data-table-pagination.tsx +25 -8
- package/src/ui/data-display/index.tsx +1 -0
- package/src/ui/data-display/use-client-pagination.ts +48 -0
- package/src/ui/errors/error-view.tsx +164 -0
- package/src/ui/errors/index.ts +2 -0
- package/src/ui/errors/not-found-view.tsx +44 -0
- package/src/ui/index.tsx +1 -0
- package/src/ui/layout/logo.tsx +54 -19
- package/src/user/pages/users-client-page.tsx +12 -7
- package/templates/starter-app/.env.example +4 -0
- package/templates/starter-app/AGENTS.md +1 -1
- package/templates/starter-app/README.md +6 -2
- package/templates/starter-app/husky/pre-commit +10 -0
- package/templates/starter-app/package.json +27 -2
- package/templates/starter-app/prisma/migrations/20260807115347_platform_pages/migration.sql +87 -0
- package/templates/starter-app/prisma/migrations/20260808003811_company_profile/migration.sql +25 -0
- package/templates/starter-app/prisma/schema/organization.prisma +20 -0
- package/templates/starter-app/prisma/schema/system.prisma +74 -0
- package/templates/starter-app/prisma/seed.ts +7 -5
- package/templates/starter-app/scripts/migration-new.mjs +89 -0
- package/templates/starter-app/scripts/rbac-sync.ts +49 -21
- package/templates/starter-app/src/__tests__/architecture.test.ts +11 -2
- package/templates/starter-app/src/app/[lang]/(main)/actions/page.tsx +37 -0
- package/templates/starter-app/src/app/[lang]/(main)/admin/system/cache/page.tsx +68 -0
- package/templates/starter-app/src/app/[lang]/(main)/admin/system/company/actions.ts +33 -0
- package/templates/starter-app/src/app/[lang]/(main)/admin/system/company/company-profile-form.tsx +195 -0
- package/templates/starter-app/src/app/[lang]/(main)/admin/system/company/page.tsx +29 -0
- package/templates/starter-app/src/app/[lang]/(main)/crud/[entity]/page.tsx +4 -2
- package/templates/starter-app/src/app/[lang]/(main)/error.tsx +24 -0
- package/templates/starter-app/src/app/[lang]/(main)/layout.tsx +2 -1
- package/templates/starter-app/src/app/[lang]/(main)/not-found.tsx +6 -0
- package/templates/starter-app/src/app/[lang]/(main)/page.tsx +13 -3
- package/templates/starter-app/src/app/[lang]/(main)/resources/page.tsx +39 -0
- package/templates/starter-app/src/app/[lang]/(main)/roles/[id]/edit/page.tsx +5 -2
- package/templates/starter-app/src/app/[lang]/(main)/roles/new/page.tsx +1 -0
- package/templates/starter-app/src/app/[lang]/(main)/roles/page.tsx +1 -0
- package/templates/starter-app/src/app/[lang]/(main)/security/sessions/page.tsx +24 -0
- package/templates/starter-app/src/app/[lang]/(main)/system-categories/page.tsx +30 -0
- package/templates/starter-app/src/app/[lang]/(main)/user/profile/actions.ts +62 -0
- package/templates/starter-app/src/app/[lang]/(main)/user/profile/page.tsx +41 -0
- package/templates/starter-app/src/app/[lang]/(main)/user/profile/profile-client-page.tsx +157 -0
- package/templates/starter-app/src/app/[lang]/(main)/users/page.tsx +69 -0
- package/templates/starter-app/src/app/[lang]/[...not-found]/page.tsx +9 -0
- package/templates/starter-app/src/app/[lang]/error.tsx +26 -0
- package/templates/starter-app/src/app/[lang]/layout.tsx +2 -2
- package/templates/starter-app/src/app/[lang]/not-found.tsx +9 -0
- package/templates/starter-app/src/app/api/admin/system/audit/route.ts +4 -3
- package/templates/starter-app/src/app/api/admin/system/jobs/[name]/history/route.ts +1 -1
- package/templates/starter-app/src/app/api/admin/system/jobs/route.ts +31 -12
- package/templates/starter-app/src/app/api/admin/system/settings/all/route.ts +2 -2
- package/templates/starter-app/src/app/api/admin/system/settings/create/route.ts +10 -3
- package/templates/starter-app/src/app/api/admin/system/settings/delete/route.ts +10 -3
- package/templates/starter-app/src/app/api/admin/system/settings/route.ts +14 -5
- package/templates/starter-app/src/app/api/admin/system/settings/toggle-status/route.ts +16 -7
- package/templates/starter-app/src/app/api/admin/system/settings/update/route.ts +12 -6
- package/templates/starter-app/src/app/api/admin/system/settings/update-full/route.ts +12 -6
- package/templates/starter-app/src/app/api/better-auth/[...all]/route.ts +1 -1
- package/templates/starter-app/src/app/api/branches/route.ts +26 -0
- package/templates/starter-app/src/app/api/crud/[entity]/[id]/route.ts +2 -2
- package/templates/starter-app/src/app/api/crud/[entity]/export/route.ts +16 -0
- package/templates/starter-app/src/app/api/crud/[entity]/import/route.ts +17 -0
- package/templates/starter-app/src/app/api/crud/[entity]/route.ts +2 -2
- package/templates/starter-app/src/app/api/departments/route.ts +26 -0
- package/templates/starter-app/src/app/api/error-logs/[id]/route.ts +4 -2
- package/templates/starter-app/src/app/api/error-logs/route.ts +26 -19
- package/templates/starter-app/src/app/api/files/[...key]/route.ts +1 -2
- package/templates/starter-app/src/app/api/job-titles/route.ts +46 -0
- package/templates/starter-app/src/app/api/notifications/read/route.ts +1 -1
- package/templates/starter-app/src/app/api/notifications/route.ts +1 -1
- package/templates/starter-app/src/app/api/notifications/unread-count/route.ts +1 -1
- package/templates/starter-app/src/app/api/rbac/permissions-version/route.ts +4 -1
- package/templates/starter-app/src/app/api/roles/[id]/route.ts +2 -1
- package/templates/starter-app/src/app/api/roles/route.ts +2 -1
- package/templates/starter-app/src/app/api/security/sessions/[id]/route.ts +52 -0
- package/templates/starter-app/src/app/api/security/sessions/revoke-user/route.ts +44 -0
- package/templates/starter-app/src/app/api/security/sessions/route.ts +101 -0
- package/templates/starter-app/src/app/api/suppliers/route.ts +13 -0
- package/templates/starter-app/src/app/api/system-categories/route.ts +179 -0
- package/templates/starter-app/src/app/api/system-category-groups/route.ts +149 -0
- package/templates/starter-app/src/app/api/tasks/[id]/download/route.ts +7 -3
- package/templates/starter-app/src/app/api/tasks/route.ts +5 -2
- package/templates/starter-app/src/app/api/upload/route.ts +1 -2
- package/templates/starter-app/src/app/api/users/[id]/route.ts +230 -0
- package/templates/starter-app/src/app/api/users/route.ts +141 -0
- package/templates/starter-app/src/app/global-error.tsx +135 -0
- package/templates/starter-app/src/app/globals.css +0 -1
- package/templates/starter-app/src/app/icon.svg +6 -0
- package/templates/starter-app/src/app/manifest.ts +26 -0
- package/templates/starter-app/src/components/layout/main-layout-wrapper.tsx +5 -4
- package/templates/starter-app/src/configs/entities/department.config.ts +7 -4
- package/templates/starter-app/src/configs/entities/index.ts +5 -1
- package/templates/starter-app/src/configs/entities/job-titles.config.ts +121 -0
- package/templates/starter-app/src/configs/entities/system-alerts.config.ts +93 -0
- package/templates/starter-app/src/configs/entities/users.config.ts +80 -0
- package/templates/starter-app/src/configs/i18n.ts +6 -6
- package/templates/starter-app/src/configs/permissions/index.ts +37 -0
- package/templates/starter-app/src/configs/permissions/master-data.permissions.ts +11 -0
- package/templates/starter-app/src/configs/permissions/menu-tree.ts +37 -0
- package/templates/starter-app/src/configs/permissions/system.permissions.ts +29 -0
- package/templates/starter-app/src/configs/tenant.ts +2 -1
- package/templates/starter-app/src/data/dictionary.ts +77 -4
- package/templates/starter-app/src/data/navigations.ts +60 -1
- package/templates/starter-app/src/instrumentation.ts +5 -16
- package/templates/starter-app/src/lib/action-guard.ts +25 -0
- package/templates/starter-app/src/lib/api-handler.ts +27 -19
- package/templates/starter-app/src/lib/auth.ts +3 -1
- package/templates/starter-app/src/lib/better-auth.ts +8 -0
- package/templates/starter-app/src/lib/branch-scope.ts +1 -1
- package/templates/starter-app/src/lib/crud/index.ts +5 -10
- package/templates/starter-app/src/lib/errors/log-server-error.ts +5 -4
- package/templates/starter-app/src/lib/logger.ts +11 -4
- package/templates/starter-app/src/lib/page-guard.ts +2 -3
- package/templates/starter-app/src/lib/prisma.ts +16 -7
- package/templates/starter-app/src/lib/rbac/access.ts +2 -2
- package/templates/starter-app/src/lib/storage.ts +3 -1
- package/templates/starter-app/src/providers/index.tsx +2 -1
- package/templates/starter-app/src/providers/mode-provider.tsx +13 -18
- package/templates/starter-app/src/providers/theme-provider.tsx +27 -12
- package/templates/starter-app/src/proxy.ts +8 -3
- package/templates/starter-app/src/server/services/company-service.ts +49 -0
- package/templates/starter-app/src/server/services/notification-service.ts +6 -6
- package/templates/starter-app/src/server/services/system-config-service.ts +21 -9
- package/templates/starter-app/src/server/services/user-service.ts +104 -0
- package/templates/starter-app/src/server/tasks/handlers/export-departments.ts +15 -9
- package/templates/starter-app/src/server/tasks/task-runner.ts +6 -7
- package/templates/starter-app/tsconfig.json +10 -0
- 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 {
|
|
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 {
|
|
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, {
|
|
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
|
|
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 (
|
|
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] =
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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 = [
|
|
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
|
)
|
|
@@ -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({
|
|
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) =>
|
|
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) =>
|
|
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
|
+
})
|