@goplusvn/core 0.1.69 → 0.1.71

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 (87) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/bin/goerp-init.mjs +15 -0
  3. package/package.json +1 -1
  4. package/src/auth/proxy-gate.ts +21 -2
  5. package/src/cron/__tests__/cron-schedule.test.ts +76 -0
  6. package/src/cron/cron-schedule.ts +128 -0
  7. package/src/cron/simple-cron-job.ts +83 -69
  8. package/src/crud/components/crud-table.tsx +13 -1
  9. package/src/crud/lib/translate-config.ts +16 -2
  10. package/src/guardrails/index.ts +1 -0
  11. package/src/guardrails/rules/auth.ts +54 -1
  12. package/src/guardrails/types.ts +17 -8
  13. package/src/print/print-styles.tsx +4 -1
  14. package/src/providers/brand-theme.ts +20 -0
  15. package/src/rbac/pages/permission-catalog-pages.tsx +3 -1
  16. package/src/security/index.ts +2 -0
  17. package/src/security/pages/sessions-page.tsx +516 -0
  18. package/src/ui/auth/sign-in-form.tsx +64 -9
  19. package/src/ui/errors/error-view.tsx +164 -0
  20. package/src/ui/errors/index.ts +2 -0
  21. package/src/ui/errors/not-found-view.tsx +44 -0
  22. package/src/ui/index.tsx +1 -0
  23. package/src/ui/layout/logo.tsx +54 -19
  24. package/src/user/pages/users-client-page.tsx +12 -7
  25. package/templates/starter-app/.env.example +4 -0
  26. package/templates/starter-app/README.md +6 -2
  27. package/templates/starter-app/husky/pre-commit +10 -0
  28. package/templates/starter-app/package.json +27 -2
  29. package/templates/starter-app/prisma/schema/organization.prisma +20 -0
  30. package/templates/starter-app/prisma/schema/system.prisma +74 -0
  31. package/templates/starter-app/prisma/seed.ts +7 -5
  32. package/templates/starter-app/scripts/migration-new.mjs +89 -0
  33. package/templates/starter-app/src/__tests__/architecture.test.ts +8 -0
  34. package/templates/starter-app/src/app/[lang]/(main)/actions/page.tsx +37 -0
  35. package/templates/starter-app/src/app/[lang]/(main)/admin/system/cache/page.tsx +69 -0
  36. package/templates/starter-app/src/app/[lang]/(main)/admin/system/company/actions.ts +31 -0
  37. package/templates/starter-app/src/app/[lang]/(main)/admin/system/company/company-profile-form.tsx +184 -0
  38. package/templates/starter-app/src/app/[lang]/(main)/admin/system/company/page.tsx +28 -0
  39. package/templates/starter-app/src/app/[lang]/(main)/error.tsx +24 -0
  40. package/templates/starter-app/src/app/[lang]/(main)/not-found.tsx +6 -0
  41. package/templates/starter-app/src/app/[lang]/(main)/page.tsx +10 -0
  42. package/templates/starter-app/src/app/[lang]/(main)/resources/page.tsx +39 -0
  43. package/templates/starter-app/src/app/[lang]/(main)/security/sessions/page.tsx +24 -0
  44. package/templates/starter-app/src/app/[lang]/(main)/system-categories/page.tsx +30 -0
  45. package/templates/starter-app/src/app/[lang]/(main)/user/profile/actions.ts +60 -0
  46. package/templates/starter-app/src/app/[lang]/(main)/user/profile/page.tsx +39 -0
  47. package/templates/starter-app/src/app/[lang]/(main)/user/profile/profile-client-page.tsx +149 -0
  48. package/templates/starter-app/src/app/[lang]/(main)/users/page.tsx +68 -0
  49. package/templates/starter-app/src/app/[lang]/[...not-found]/page.tsx +9 -0
  50. package/templates/starter-app/src/app/[lang]/error.tsx +26 -0
  51. package/templates/starter-app/src/app/[lang]/not-found.tsx +9 -0
  52. package/templates/starter-app/src/app/api/branches/route.ts +26 -0
  53. package/templates/starter-app/src/app/api/departments/route.ts +26 -0
  54. package/templates/starter-app/src/app/api/job-titles/route.ts +46 -0
  55. package/templates/starter-app/src/app/api/security/sessions/[id]/route.ts +52 -0
  56. package/templates/starter-app/src/app/api/security/sessions/revoke-user/route.ts +44 -0
  57. package/templates/starter-app/src/app/api/security/sessions/route.ts +101 -0
  58. package/templates/starter-app/src/app/api/suppliers/route.ts +13 -0
  59. package/templates/starter-app/src/app/api/system-categories/route.ts +154 -0
  60. package/templates/starter-app/src/app/api/system-category-groups/route.ts +131 -0
  61. package/templates/starter-app/src/app/api/users/[id]/route.ts +222 -0
  62. package/templates/starter-app/src/app/api/users/route.ts +139 -0
  63. package/templates/starter-app/src/app/global-error.tsx +135 -0
  64. package/templates/starter-app/src/app/icon.svg +6 -0
  65. package/templates/starter-app/src/app/manifest.ts +26 -0
  66. package/templates/starter-app/src/configs/entities/index.ts +5 -1
  67. package/templates/starter-app/src/configs/entities/job-titles.config.ts +119 -0
  68. package/templates/starter-app/src/configs/entities/system-alerts.config.ts +91 -0
  69. package/templates/starter-app/src/configs/entities/users.config.ts +67 -0
  70. package/templates/starter-app/src/configs/permissions/index.ts +37 -0
  71. package/templates/starter-app/src/configs/permissions/master-data.permissions.ts +11 -0
  72. package/templates/starter-app/src/configs/permissions/menu-tree.ts +37 -0
  73. package/templates/starter-app/src/configs/permissions/system.permissions.ts +29 -0
  74. package/templates/starter-app/src/configs/tenant.ts +2 -1
  75. package/templates/starter-app/src/data/dictionary.ts +76 -4
  76. package/templates/starter-app/src/data/navigations.ts +49 -0
  77. package/templates/starter-app/src/instrumentation.ts +5 -16
  78. package/templates/starter-app/src/lib/action-guard.ts +26 -0
  79. package/templates/starter-app/src/lib/better-auth.ts +8 -0
  80. package/templates/starter-app/src/lib/crud/index.ts +2 -0
  81. package/templates/starter-app/src/lib/prisma.ts +3 -2
  82. package/templates/starter-app/src/providers/mode-provider.tsx +13 -18
  83. package/templates/starter-app/src/providers/theme-provider.tsx +27 -13
  84. package/templates/starter-app/src/proxy.ts +8 -3
  85. package/templates/starter-app/src/server/services/company-service.ts +47 -0
  86. package/templates/starter-app/src/server/services/user-service.ts +104 -0
  87. package/templates/starter-app/tsconfig.json +2 -0
@@ -42,6 +42,26 @@ function publicPrefixes(ctx: GuardrailContext): string[] {
42
42
  );
43
43
  }
44
44
 
45
+ /**
46
+ * MỌI tiền tố trong danh sách công khai của proxy.ts — kể cả ngoài /api
47
+ * (/pub, /iclock, webhook…). Chỉ đọc bên trong mảng `publicPrefixes:`/
48
+ * `publicApiPrefixes:` chứ không vơ mọi chuỗi (signInPath/homePath không phải
49
+ * tuyên bố công khai).
50
+ */
51
+ function declaredPublicPrefixes(ctx: GuardrailContext): string[] {
52
+ if (!ctx.existsInSrc("proxy.ts")) return [];
53
+ const code = ctx.readCodeRel("proxy.ts");
54
+ const out: string[] = [];
55
+ for (const m of code.matchAll(
56
+ /public(?:Api)?Prefixes\s*:\s*\[([^\]]*)\]/g,
57
+ )) {
58
+ for (const s of m[1].matchAll(/["'](\/[^"']*)["']/g)) {
59
+ out.push(s[1].replace(/\/$/, ""));
60
+ }
61
+ }
62
+ return out;
63
+ }
64
+
45
65
  function gateSources(ctx: GuardrailContext, code: string): string[] {
46
66
  const targets = [...code.matchAll(/export \* from ["']@\/([^"']+)["']/g)].map(
47
67
  (m) => m[1],
@@ -77,7 +97,8 @@ export const authRules: GuardrailRule[] = [
77
97
  // Hai lối dựng đều hợp lệ: factory của core, hoặc bản tự dựng của app.
78
98
  // Ràng cứng vào một lối là bắt app phải giống hệt vinhhoa về CÁCH VIẾT,
79
99
  // trong khi thứ cần ràng là TÍNH CHẤT: /api chặn trước, công khai phải khai.
80
- const viaCore = /createAuthProxy\s*\(/.test(code) && /publicApiPrefixes/.test(code);
100
+ const viaCore =
101
+ /createAuthProxy\s*\(/.test(code) && /public(?:Api)?Prefixes/.test(code);
81
102
  const handRolled =
82
103
  /getSessionCookie/.test(code) &&
83
104
  /pathname\.startsWith\(["']\/api["']\)/.test(code) &&
@@ -151,6 +172,38 @@ export const authRules: GuardrailRule[] = [
151
172
  },
152
173
  },
153
174
 
175
+ {
176
+ id: "auth/route-outside-api-declared",
177
+ title: "route.ts NGOÀI app/api phải khai tiền tố trong danh sách công khai của proxy, hoặc tự gác",
178
+ why:
179
+ "Endpoint server ngoài /api (route thiết bị /pub, /iclock của spartronics; " +
180
+ "webhook đặt ngoài /api…) từng VÔ HÌNH với mọi thước: rule api-route-gated " +
181
+ "chỉ quét app/api/**, còn proxy mặc-định-chặn thì các route này lại cần được " +
182
+ "MỞ — một bề mặt công khai không ai review thấy.",
183
+ fix: "Thêm tiền tố vào `publicPrefixes` trong src/proxy.ts kèm ghi rõ nó tự xác thực bằng gì (allowlist serial, HMAC…), hoặc bọc cổng như route API thường.",
184
+ run(ctx) {
185
+ const routes = ctx.files
186
+ .map(ctx.rel)
187
+ .filter(
188
+ (rel) =>
189
+ rel.startsWith("app/") &&
190
+ rel.endsWith("/route.ts") &&
191
+ !rel.startsWith("app/api/"),
192
+ );
193
+ if (routes.length === 0) return null;
194
+ const publics = declaredPublicPrefixes(ctx);
195
+ const allowed = new Set(
196
+ ctx.options.allowlists["auth/route-outside-api-declared"] ?? [],
197
+ );
198
+ return routes.filter((rel) => {
199
+ if (allowed.has(rel)) return false;
200
+ const url = routeUrl(rel);
201
+ if (publics.some((p) => url === p || url.startsWith(`${p}/`))) return false;
202
+ return !gateSources(ctx, ctx.readCodeRel(rel)).some((src) => GATED.test(src));
203
+ });
204
+ },
205
+ },
206
+
154
207
  forbidPattern({
155
208
  id: "auth/server-action-bare-session",
156
209
  title: "file 'use server' không gọi getSession trần — dùng requirePermission",
@@ -81,9 +81,9 @@ export interface GuardrailOptions {
81
81
  * Registry quyền của app (`src/configs/permissions`). Thiếu thì nhóm rule
82
82
  * `rbac/*` tự bỏ qua.
83
83
  */
84
- permissionRegistry?: PermissionFeatureLike[];
84
+ permissionRegistry?: readonly PermissionFeatureLike[];
85
85
  /** Cây menu (`src/data/navigations`). Thiếu thì rule nav tự bỏ qua. */
86
- navigations?: NavigationGroupLike[];
86
+ navigations?: readonly NavigationGroupLike[];
87
87
  /**
88
88
  * Bộ action ai cũng hiểu. PHẢI trùng danh sách trong `scripts/rbac-sync.ts`
89
89
  * của app — lệch nhau thì test xanh mà seed ném lỗi lúc deploy.
@@ -158,17 +158,26 @@ export type ResolvedGuardrailOptions = GuardrailOptions &
158
158
  * guardrails không kéo theo phụ thuộc ngược lên tầng config.
159
159
  */
160
160
  export interface PermissionFeatureLike {
161
- resources: Array<{
161
+ resources: ReadonlyArray<{
162
162
  code: string;
163
- actions: string[];
164
- defaultGrants?: Record<string, string[] | "*">;
163
+ actions: readonly string[];
164
+ defaultGrants?: Readonly<Record<string, readonly string[] | "*">>;
165
165
  }>;
166
- customActions?: Array<{ code: string }>;
166
+ customActions?: ReadonlyArray<{ code: string }>;
167
+ }
168
+
169
+ /**
170
+ * CHỈ khai trường rule thật sự đọc, KHÔNG index signature: một interface cụ
171
+ * thể (NavigationType của app) gán vào đây được theo structural typing, còn
172
+ * `[key: string]: unknown` thì bắt app phải có index signature — chính là lỗi
173
+ * TS mà template dính ngay sau init (pilot spartronics 2026-08-07).
174
+ */
175
+ export interface NavigationItemLike {
176
+ resource?: string;
167
177
  }
168
178
 
169
179
  export interface NavigationGroupLike {
170
- items?: Array<{ resource?: string; [key: string]: unknown }>;
171
- [key: string]: unknown;
180
+ items?: readonly NavigationItemLike[];
172
181
  }
173
182
 
174
183
  /** Kết quả chạy một rule — dùng cho `goerp doctor` (không cần vitest). */
@@ -20,7 +20,10 @@ export function PrintStyles({ pageSize = "A4" }: PrintStylesProps) {
20
20
  }
21
21
  case "A5-Landscape":
22
22
  return {
23
- size: "A5 landscape",
23
+ // Kích thước tường minh thay cho "A5 landscape": một số driver máy in
24
+ // (PC cửa hàng) bỏ qua từ khóa landscape rồi in dọc + scale fill,
25
+ // cắt mất nửa phải trang.
26
+ size: "210mm 148.5mm",
24
27
  maxWidth: "210mm", // width
25
28
  padding: "12mm 12mm", // increased top padding from 8mm
26
29
  fontSize: "10pt",
@@ -20,6 +20,26 @@ export interface BrandThemeColor {
20
20
  sidebarDark: string;
21
21
  }
22
22
 
23
+ /**
24
+ * Bảng màu thương hiệu CALIBRATE SẴN, khớp 1-1 với swatch trong
25
+ * `configs/themes.ts` — app mới dùng thẳng (theme-provider của starter-app),
26
+ * app muốn nắn tay từng hue (như vinhhoa) thì giữ bảng riêng và bỏ qua cái này.
27
+ */
28
+ export const THEME_PRESETS: Record<string, BrandThemeColor> = {
29
+ zinc: { primary: "235 12% 27%", primaryDark: "235 10% 62%", fg: "0 0% 98%", sidebar: "235 18% 15%", sidebarDark: "235 16% 9%" },
30
+ slate: { primary: "213 30% 42%", primaryDark: "213 26% 64%", fg: "0 0% 100%", sidebar: "214 36% 19%", sidebarDark: "214 32% 12%" },
31
+ stone: { primary: "26 20% 40%", primaryDark: "28 16% 62%", fg: "0 0% 98%", sidebar: "26 28% 18%", sidebarDark: "26 24% 11%" },
32
+ gray: { primary: "218 9% 48%", primaryDark: "218 9% 68%", fg: "0 0% 100%", sidebar: "218 16% 19%", sidebarDark: "218 14% 12%" },
33
+ neutral: { primary: "0 0% 42%", primaryDark: "0 0% 66%", fg: "0 0% 100%", sidebar: "0 0% 16%", sidebarDark: "0 0% 9%" },
34
+ red: { primary: "4 78% 50%", primaryDark: "6 84% 62%", fg: "0 0% 100%", sidebar: "356 62% 28%", sidebarDark: "356 58% 16%" },
35
+ rose: { primary: "338 80% 53%", primaryDark: "340 80% 63%", fg: "0 0% 100%", sidebar: "338 56% 28%", sidebarDark: "338 54% 16%" },
36
+ orange: { primary: "26 88% 47%", primaryDark: "30 92% 58%", fg: "0 0% 100%", sidebar: "20 75% 28%", sidebarDark: "22 72% 15%" },
37
+ green: { primary: "151 66% 38%", primaryDark: "150 58% 48%", fg: "0 0% 100%", sidebar: "156 70% 16%", sidebarDark: "158 68% 9%" },
38
+ blue: { primary: "233 74% 44%", primaryDark: "230 62% 60%", fg: "0 0% 100%", sidebar: "233.9 88.8% 17.5%", sidebarDark: "234 50% 10%" },
39
+ yellow: { primary: "42 92% 46%", primaryDark: "46 96% 56%", fg: "40 60% 12%", sidebar: "40 72% 25%", sidebarDark: "42 70% 14%" },
40
+ violet: { primary: "262 72% 52%", primaryDark: "263 72% 64%", fg: "0 0% 100%", sidebar: "264 56% 25%", sidebarDark: "265 52% 15%" },
41
+ };
42
+
23
43
  /** Dịch lightness của "H S% L%" (cho hover/active). */
24
44
  export function shiftLightness(triple: string, delta: number): string {
25
45
  const parts = triple.trim().split(/\s+/);
@@ -567,7 +567,9 @@ export function ResourceCatalogPage({
567
567
  ?.description ||
568
568
  actionName(code)
569
569
  }
570
- className="inline-flex items-center gap-1 rounded-md border border-primary/40 bg-primary/10 px-1.5 py-0.5 text-[11px] font-medium text-primary"
570
+ // Check màu đã tự nói lên trạng thái không cần thêm khung
571
+ // viền/nền màu (nhiễu khi cả dãy chip đứng cạnh nhau).
572
+ className="inline-flex items-center gap-1 px-1 py-0.5 text-[11px] font-medium text-foreground"
571
573
  >
572
574
  <span
573
575
  className={cn(
@@ -0,0 +1,2 @@
1
+ // @goerp/core/security — trang quản trị bảo mật dùng chung (Better Auth).
2
+ export { SessionsPage, type SessionsPageProps } from "./pages/sessions-page"