@thinhnguyencth1204/nextcli 0.8.0 → 1.0.0

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 (112) hide show
  1. package/README.md +27 -24
  2. package/dist/cli.js +168 -107
  3. package/package.json +1 -1
  4. package/templates/features/api/src/lib/api/axios.ts +1 -90
  5. package/templates/features/auth/messages/vi/auth.json +2 -1
  6. package/templates/features/auth/src/app/(auth)/change-password/page.tsx +5 -4
  7. package/templates/features/auth/src/app/(auth)/layout.tsx +2 -5
  8. package/templates/features/auth/src/app/(auth)/sign-in/page.tsx +5 -4
  9. package/templates/features/auth/src/app/api/v1/auth/login/route.ts +24 -29
  10. package/templates/features/auth/src/app/api/v1/auth/logout/route.ts +0 -5
  11. package/templates/features/auth/src/components/layout/auth/auth-shell.tsx +24 -0
  12. package/templates/features/auth/src/features/auth/components/account-panel.tsx +15 -3
  13. package/templates/features/auth/src/features/auth/components/change-password-form.tsx +27 -30
  14. package/templates/features/auth/src/features/auth/components/sign-in-form.tsx +33 -42
  15. package/templates/features/auth/src/lib/auth/client.ts +2 -2
  16. package/templates/features/auth/src/lib/auth/server.ts +2 -2
  17. package/templates/features/dashboard/src/app/(dashboard)/account/page.tsx +9 -7
  18. package/templates/features/dashboard/src/app/(dashboard)/dashboard/page.tsx +24 -10
  19. package/templates/features/dashboard/src/components/layout/private/app-sidebar.tsx +1 -13
  20. package/templates/features/dashboard/src/components/layout/private/dashboard-layout.tsx +31 -22
  21. package/templates/features/dashboard/src/components/layout/private/page-shell.tsx +40 -0
  22. package/templates/features/database/prisma/schema.prisma +1 -0
  23. package/templates/features/example/messages/vi/example.json +11 -1
  24. package/templates/features/example/src/app/(dashboard)/example/page.tsx +92 -3
  25. package/templates/features/example/src/example/components/example-table.tsx +15 -2
  26. package/templates/next-base/.env +16 -0
  27. package/templates/next-base/.env.development +16 -0
  28. package/templates/next-base/.env.example +16 -0
  29. package/templates/next-base/SETUP.md +62 -10
  30. package/templates/next-base/bun.lock +407 -0
  31. package/templates/next-base/messages/vi/auth.json +43 -0
  32. package/templates/next-base/messages/vi/common.json +53 -0
  33. package/templates/next-base/messages/vi/example.json +20 -0
  34. package/templates/next-base/next-env.d.ts +1 -1
  35. package/templates/next-base/next.config.ts +4 -1
  36. package/templates/next-base/nextcli.json +12 -4
  37. package/templates/next-base/package.json +24 -5
  38. package/templates/next-base/prisma/schema.prisma +85 -0
  39. package/templates/next-base/prisma.config.ts +16 -0
  40. package/templates/next-base/src/app/(auth)/.gitkeep +1 -0
  41. package/templates/next-base/src/app/(auth)/change-password/layout.tsx +21 -0
  42. package/templates/next-base/src/app/(auth)/change-password/page.tsx +15 -0
  43. package/templates/next-base/src/app/(auth)/layout.tsx +6 -0
  44. package/templates/next-base/src/app/(auth)/sign-in/layout.tsx +17 -0
  45. package/templates/next-base/src/app/(auth)/sign-in/page.tsx +15 -0
  46. package/templates/next-base/src/app/(dashboard)/account/page.tsx +20 -0
  47. package/templates/next-base/src/app/(dashboard)/dashboard/page.tsx +31 -0
  48. package/templates/next-base/src/app/(dashboard)/example/page.tsx +102 -0
  49. package/templates/next-base/src/app/(dashboard)/layout.tsx +22 -0
  50. package/templates/next-base/src/app/api/auth/[...all]/route.ts +4 -0
  51. package/templates/next-base/src/app/api/v1/auth/change-password/route.ts +55 -0
  52. package/templates/next-base/src/app/api/v1/auth/login/route.ts +65 -0
  53. package/templates/next-base/src/app/api/v1/auth/logout/route.ts +23 -0
  54. package/templates/next-base/src/app/api/v1/auth/me/route.ts +24 -0
  55. package/templates/next-base/src/app/api/v1/example/route.ts +34 -0
  56. package/templates/next-base/src/app/api/v1/users/[id]/route.ts +104 -0
  57. package/templates/next-base/src/app/api/v1/users/route.ts +58 -0
  58. package/templates/next-base/src/app/layout.tsx +14 -6
  59. package/templates/next-base/src/app/page.tsx +2 -25
  60. package/templates/next-base/src/components/branding/logo.tsx +27 -4
  61. package/templates/next-base/src/components/layout/auth/auth-shell.tsx +24 -0
  62. package/templates/next-base/src/components/layout/private/app-sidebar.tsx +32 -0
  63. package/templates/next-base/src/components/layout/private/dashboard-layout.tsx +63 -0
  64. package/templates/next-base/src/components/layout/private/locale-switcher.tsx +45 -0
  65. package/templates/next-base/src/components/layout/private/nav-sidebar.tsx +55 -0
  66. package/templates/next-base/src/components/layout/private/nav-user.tsx +99 -0
  67. package/templates/next-base/src/components/layout/private/page-shell.tsx +40 -0
  68. package/templates/next-base/src/components/providers/query-provider.tsx +17 -0
  69. package/templates/next-base/src/components/ui/data-table/data-table-column-header.tsx +23 -0
  70. package/templates/next-base/src/components/ui/data-table/data-table-filter-list.tsx +3 -0
  71. package/templates/next-base/src/components/ui/data-table/data-table-pagination.tsx +35 -0
  72. package/templates/next-base/src/components/ui/data-table/data-table-skeleton.tsx +11 -0
  73. package/templates/next-base/src/components/ui/data-table/data-table-toolbar.tsx +14 -0
  74. package/templates/next-base/src/components/ui/data-table/data-table-view-options.tsx +3 -0
  75. package/templates/next-base/src/components/ui/data-table/data-table.tsx +72 -0
  76. package/templates/next-base/src/components/ui/sidebar.tsx +215 -0
  77. package/templates/next-base/src/data/sidebar-modules.ts +11 -0
  78. package/templates/next-base/src/example/api/use-example.ts +21 -0
  79. package/templates/next-base/src/example/api/use-mutations.ts +20 -0
  80. package/templates/next-base/src/example/components/example-table.tsx +64 -0
  81. package/templates/next-base/src/example/services.ts +9 -0
  82. package/templates/next-base/src/example/validations.ts +8 -0
  83. package/templates/next-base/src/features/auth/components/account-panel.tsx +92 -0
  84. package/templates/next-base/src/features/auth/components/change-password-form.tsx +79 -0
  85. package/templates/next-base/src/features/auth/components/sign-in-form.tsx +86 -0
  86. package/templates/next-base/src/features/auth/validations.ts +14 -0
  87. package/templates/next-base/src/features/users/services.ts +132 -0
  88. package/templates/next-base/src/features/users/validations.ts +21 -0
  89. package/templates/next-base/src/hooks/table/use-data-table.ts +33 -0
  90. package/templates/next-base/src/hooks/use-mobile.ts +25 -0
  91. package/templates/next-base/src/i18n/config.ts +7 -0
  92. package/templates/next-base/src/i18n/namespaces.ts +5 -0
  93. package/templates/next-base/src/i18n/request.ts +25 -0
  94. package/templates/next-base/src/instrumentation.ts +14 -0
  95. package/templates/next-base/src/lib/api/axios.ts +56 -0
  96. package/templates/next-base/src/lib/api/response.ts +45 -0
  97. package/templates/next-base/src/lib/auth/bootstrap.ts +95 -0
  98. package/templates/next-base/src/lib/auth/client.ts +7 -0
  99. package/templates/next-base/src/lib/auth/index.ts +1 -0
  100. package/templates/next-base/src/lib/auth/rbac.ts +59 -0
  101. package/templates/next-base/src/lib/auth/server.ts +21 -0
  102. package/templates/next-base/src/lib/constants.ts +10 -0
  103. package/templates/next-base/src/lib/db/prisma.ts +23 -0
  104. package/templates/next-base/src/lib/prisma.ts +23 -0
  105. package/templates/next-base/src/lib/supabase/client.ts +6 -0
  106. package/templates/next-base/src/lib/supabase/rich-text-image-sync.ts +28 -0
  107. package/templates/next-base/src/lib/supabase/storage-config.ts +69 -0
  108. package/templates/next-base/src/lib/supabase/storage.ts +164 -0
  109. package/templates/next-base/src/types/data-table.ts +4 -0
  110. package/templates/features/api/src/lib/api/token-store.ts +0 -13
  111. package/templates/features/auth/src/app/api/v1/auth/refresh/route.ts +0 -32
  112. package/templates/features/auth/src/lib/auth/cookies.ts +0 -15
@@ -0,0 +1,43 @@
1
+ {
2
+ "signInPage": {
3
+ "title": "Đăng nhập",
4
+ "description": "Dùng tên đăng nhập và mật khẩu để truy cập hệ thống."
5
+ },
6
+ "signInForm": {
7
+ "username": "Tên đăng nhập",
8
+ "password": "Mật khẩu",
9
+ "usernamePlaceholder": "admin",
10
+ "passwordPlaceholder": "********",
11
+ "submit": "Đăng nhập",
12
+ "submitting": "Đang đăng nhập...",
13
+ "invalidInput": "Vui lòng nhập tên đăng nhập và mật khẩu hợp lệ.",
14
+ "success": "Đăng nhập thành công.",
15
+ "failed": "Đăng nhập thất bại."
16
+ },
17
+ "changePasswordPage": {
18
+ "title": "Đổi mật khẩu",
19
+ "description": "Bạn cần đổi mật khẩu trước khi tiếp tục sử dụng hệ thống."
20
+ },
21
+ "changePasswordForm": {
22
+ "currentPassword": "Mật khẩu hiện tại",
23
+ "newPassword": "Mật khẩu mới",
24
+ "submit": "Cập nhật mật khẩu",
25
+ "submitting": "Đang cập nhật...",
26
+ "invalidInput": "Vui lòng nhập mật khẩu hợp lệ (tối thiểu 8 ký tự).",
27
+ "success": "Đổi mật khẩu thành công.",
28
+ "failed": "Không thể đổi mật khẩu."
29
+ },
30
+ "account": {
31
+ "title": "Tài khoản",
32
+ "description": "Xem thông tin phiên đăng nhập hiện tại.",
33
+ "panelTitle": "Thông tin tài khoản",
34
+ "loading": "Đang tải thông tin tài khoản...",
35
+ "noSession": "Chưa có phiên đăng nhập hoạt động.",
36
+ "userId": "Mã người dùng",
37
+ "username": "Tên đăng nhập",
38
+ "email": "Email",
39
+ "name": "Tên",
40
+ "na": "N/A",
41
+ "goToSignIn": "Đi tới trang đăng nhập"
42
+ }
43
+ }
@@ -0,0 +1,53 @@
1
+ {
2
+ "appName": "__PROJECT_NAME__",
3
+ "sidebar": {
4
+ "groupGeneral": "Tổng quan",
5
+ "dashboard": "Bảng điều khiển",
6
+ "example": "Ví dụ",
7
+ "account": "Tài khoản"
8
+ },
9
+ "header": {
10
+ "themeLight": "Sáng",
11
+ "themeDark": "Tối",
12
+ "themeSystem": "Theo hệ thống",
13
+ "toggleTheme": "Chuyển giao diện"
14
+ },
15
+ "dashboardPage": {
16
+ "title": "Bảng điều khiển",
17
+ "description": "Tổng quan nhanh về hoạt động hệ thống.",
18
+ "placeholder": "Thêm widget và số liệu của bạn tại đây.",
19
+ "stats": {
20
+ "users": {
21
+ "title": "Người dùng",
22
+ "value": "—"
23
+ },
24
+ "records": {
25
+ "title": "Bản ghi",
26
+ "value": "—"
27
+ },
28
+ "activity": {
29
+ "title": "Hoạt động hôm nay",
30
+ "value": "—"
31
+ }
32
+ }
33
+ },
34
+ "userMenu": {
35
+ "title": "Tài khoản",
36
+ "anonymousName": "Người dùng",
37
+ "anonymousEmail": "user@example.com",
38
+ "appearance": "Giao diện",
39
+ "logout": "Đăng xuất"
40
+ },
41
+ "locale": {
42
+ "label": "Ngôn ngữ",
43
+ "vi": "Tiếng Việt",
44
+ "en": "English",
45
+ "ja": "日本語",
46
+ "ko": "한국어"
47
+ },
48
+ "table": {
49
+ "noResults": "Không có dữ liệu.",
50
+ "previous": "Trước",
51
+ "next": "Sau"
52
+ }
53
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "page": {
3
+ "title": "Ví dụ",
4
+ "description": "Trang mẫu với bảng dữ liệu, thẻ nội dung và nút thêm bản ghi.",
5
+ "add": "Thêm mới",
6
+ "createTitle": "Thêm bản ghi ví dụ",
7
+ "nameLabel": "Tên",
8
+ "descriptionLabel": "Mô tả",
9
+ "createSubmit": "Lưu",
10
+ "creating": "Đang lưu...",
11
+ "createSuccess": "Đã thêm bản ghi ví dụ.",
12
+ "createFailed": "Không thể thêm bản ghi ví dụ.",
13
+ "invalidInput": "Vui lòng nhập tên hợp lệ."
14
+ },
15
+ "table": {
16
+ "name": "Tên",
17
+ "description": "Mô tả",
18
+ "loading": "Đang tải dữ liệu ví dụ..."
19
+ }
20
+ }
@@ -1,6 +1,6 @@
1
1
  /// <reference types="next" />
2
2
  /// <reference types="next/image-types/global" />
3
- import "./.next/types/routes.d.ts";
3
+ import "./.next/dev/types/routes.d.ts";
4
4
 
5
5
  // NOTE: This file should not be edited
6
6
  // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
@@ -1,4 +1,5 @@
1
1
  import type { NextConfig } from "next";
2
+ import createNextIntlPlugin from "next-intl/plugin";
2
3
  import path from "node:path";
3
4
  import { fileURLToPath } from "node:url";
4
5
 
@@ -11,4 +12,6 @@ const nextConfig: NextConfig = {
11
12
  },
12
13
  };
13
14
 
14
- export default nextConfig;
15
+ const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts");
16
+
17
+ export default withNextIntl(nextConfig);
@@ -1,8 +1,16 @@
1
1
  {
2
2
  "cli": "__NEXTCLI_VERSION__",
3
3
  "defaultLocale": "vi",
4
- "locales": [],
5
- "namespaces": [],
6
- "modules": [],
7
- "features": []
4
+ "locales": ["vi"],
5
+ "namespaces": ["common", "auth", "example"],
6
+ "modules": [
7
+ "database",
8
+ "supabase",
9
+ "auth",
10
+ "api",
11
+ "i18n",
12
+ "dashboard",
13
+ "example"
14
+ ],
15
+ "features": ["example"]
8
16
  }
@@ -7,9 +7,20 @@
7
7
  "dev": "next dev",
8
8
  "build": "next build",
9
9
  "start": "next start",
10
- "lint": "eslint ."
10
+ "lint": "eslint .",
11
+ "postinstall": "prisma generate",
12
+ "db:generate": "prisma generate",
13
+ "db:migrate": "prisma migrate dev",
14
+ "db:studio": "prisma studio"
11
15
  },
12
16
  "dependencies": {
17
+ "@dnd-kit/core": "^6.3.1",
18
+ "@lexical/history": "^0.45.0",
19
+ "@lexical/react": "^0.45.0",
20
+ "@lexical/rich-text": "^0.45.0",
21
+ "@lexical/utils": "^0.45.0",
22
+ "@prisma/adapter-pg": "^7.8.0",
23
+ "@prisma/client": "^7.8.0",
13
24
  "@radix-ui/react-avatar": "^1.1.10",
14
25
  "@radix-ui/react-dialog": "^1.1.15",
15
26
  "@radix-ui/react-dropdown-menu": "^2.1.16",
@@ -21,16 +32,22 @@
21
32
  "@radix-ui/react-slot": "^1.2.3",
22
33
  "@radix-ui/react-tabs": "^1.1.13",
23
34
  "@radix-ui/react-tooltip": "^1.2.8",
35
+ "@supabase/supabase-js": "^2.44.2",
36
+ "@tanstack/react-query": "^5.56.2",
37
+ "@tanstack/react-query-devtools": "^5.56.2",
38
+ "@tanstack/react-table": "^8.20.5",
39
+ "axios": "^1.7.7",
40
+ "better-auth": "^1.6.11",
24
41
  "class-variance-authority": "^0.7.0",
25
42
  "clsx": "^2.1.1",
26
- "@lexical/history": "^0.45.0",
27
- "@lexical/react": "^0.45.0",
28
- "@lexical/rich-text": "^0.45.0",
29
- "@lexical/utils": "^0.45.0",
43
+ "date-fns": "^3.6.0",
30
44
  "lexical": "^0.45.0",
31
45
  "lucide-react": "^0.525.0",
32
46
  "next": "^16.1.6",
47
+ "next-intl": "^4.13.0",
33
48
  "next-themes": "^0.4.6",
49
+ "nuqs": "^2.8.1",
50
+ "pg": "^8.16.3",
34
51
  "react": "^19.0.0",
35
52
  "react-dom": "^19.0.0",
36
53
  "sonner": "^1.7.1",
@@ -43,8 +60,10 @@
43
60
  "@types/node": "^22.7.4",
44
61
  "@types/react": "^19.0.0",
45
62
  "@types/react-dom": "^19.0.0",
63
+ "dotenv": "^17.4.2",
46
64
  "eslint": "^9.11.1",
47
65
  "eslint-config-next": "^16.1.6",
66
+ "prisma": "^7.8.0",
48
67
  "tailwindcss": "^4.1.11",
49
68
  "typescript": "^5.6.2"
50
69
  }
@@ -0,0 +1,85 @@
1
+ generator client {
2
+ provider = "prisma-client-js"
3
+ }
4
+
5
+ datasource db {
6
+ provider = "postgresql"
7
+ }
8
+
9
+ model Role {
10
+ id String @id @default(cuid())
11
+ name String @unique
12
+ level Int
13
+ createdAt DateTime @default(now())
14
+ updatedAt DateTime @updatedAt
15
+ users User[]
16
+ }
17
+
18
+ model User {
19
+ id String @id @default(cuid())
20
+ email String?
21
+ username String @unique
22
+ displayUsername String?
23
+ name String?
24
+ image String?
25
+ emailVerified Boolean @default(false)
26
+ requirePasswordChange Boolean @default(false)
27
+ roleId String?
28
+ role Role? @relation(fields: [roleId], references: [id], onDelete: SetNull)
29
+ createdAt DateTime @default(now())
30
+ updatedAt DateTime @updatedAt
31
+ sessions Session[]
32
+ accounts Account[]
33
+ }
34
+ model Session {
35
+ id String @id @default(cuid())
36
+ token String @unique
37
+ expiresAt DateTime
38
+ ipAddress String?
39
+ userAgent String?
40
+ userId String
41
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
42
+ createdAt DateTime @default(now())
43
+ updatedAt DateTime @updatedAt
44
+ }
45
+
46
+ model Account {
47
+ id String @id @default(cuid())
48
+ accountId String
49
+ providerId String
50
+ userId String
51
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
52
+ accessToken String?
53
+ refreshToken String?
54
+ idToken String?
55
+ accessTokenExpiresAt DateTime?
56
+ refreshTokenExpiresAt DateTime?
57
+ scope String?
58
+ password String?
59
+ createdAt DateTime @default(now())
60
+ updatedAt DateTime @updatedAt
61
+
62
+ @@unique([providerId, accountId])
63
+ }
64
+
65
+ model Verification {
66
+ id String @id @default(cuid())
67
+ identifier String
68
+ value String
69
+ expiresAt DateTime
70
+ createdAt DateTime? @default(now())
71
+ updatedAt DateTime? @updatedAt
72
+ }
73
+
74
+ // Optional Chatbox models are appended by NexTCLI only when adding the chat feature.
75
+ // Review generated schema changes before running migrations on production databases.
76
+
77
+ // Starter demo table for template onboarding only.
78
+ // Rename/remove this `Example` model before production migration.
79
+ model Example {
80
+ id String @id @default(cuid())
81
+ name String
82
+ description String?
83
+ createdAt DateTime @default(now())
84
+ updatedAt DateTime @updatedAt
85
+ }
@@ -0,0 +1,16 @@
1
+ import "dotenv/config";
2
+ import { defineConfig, env } from "prisma/config";
3
+
4
+ type Env = {
5
+ DIRECT_URL: string;
6
+ };
7
+
8
+ export default defineConfig({
9
+ schema: "prisma/schema.prisma",
10
+ migrations: {
11
+ path: "prisma/migrations",
12
+ },
13
+ datasource: {
14
+ url: env<Env>("DIRECT_URL"),
15
+ },
16
+ });
@@ -0,0 +1,21 @@
1
+ import type { ReactNode } from "react";
2
+ import { redirect } from "next/navigation";
3
+ import { getSessionUser } from "@/lib/auth/rbac";
4
+
5
+ export default async function ChangePasswordLayout({
6
+ children,
7
+ }: {
8
+ children: ReactNode;
9
+ }) {
10
+ const user = await getSessionUser();
11
+
12
+ if (!user) {
13
+ redirect("/sign-in");
14
+ }
15
+
16
+ if (!user.requirePasswordChange) {
17
+ redirect("/dashboard");
18
+ }
19
+
20
+ return <>{children}</>;
21
+ }
@@ -0,0 +1,15 @@
1
+ import { useTranslations } from "next-intl";
2
+ import { ChangePasswordForm } from "@/features/auth/components/change-password-form";
3
+
4
+ export default function ChangePasswordPage() {
5
+ const t = useTranslations("auth.changePasswordPage");
6
+
7
+ return (
8
+ <div className="space-y-6">
9
+ <div className="text-center sm:text-left">
10
+ <h1 className="text-2xl font-semibold tracking-tight">{t("title")}</h1>
11
+ </div>
12
+ <ChangePasswordForm />
13
+ </div>
14
+ );
15
+ }
@@ -0,0 +1,6 @@
1
+ import type { ReactNode } from "react";
2
+ import { AuthShell } from "@/components/layout/auth/auth-shell";
3
+
4
+ export default function AuthRouteLayout({ children }: { children: ReactNode }) {
5
+ return <AuthShell>{children}</AuthShell>;
6
+ }
@@ -0,0 +1,17 @@
1
+ import type { ReactNode } from "react";
2
+ import { redirect } from "next/navigation";
3
+ import { getSessionUser } from "@/lib/auth/rbac";
4
+
5
+ export default async function SignInLayout({ children }: { children: ReactNode }) {
6
+ const user = await getSessionUser();
7
+
8
+ if (user?.requirePasswordChange) {
9
+ redirect("/change-password");
10
+ }
11
+
12
+ if (user) {
13
+ redirect("/dashboard");
14
+ }
15
+
16
+ return children;
17
+ }
@@ -0,0 +1,15 @@
1
+ import { useTranslations } from "next-intl";
2
+ import { SignInForm } from "@/features/auth/components/sign-in-form";
3
+
4
+ export default function SignInPage() {
5
+ const t = useTranslations("auth.signInPage");
6
+
7
+ return (
8
+ <div className="space-y-6">
9
+ <div className="text-center sm:text-left">
10
+ <h1 className="text-2xl font-semibold tracking-tight">{t("title")}</h1>
11
+ </div>
12
+ <SignInForm />
13
+ </div>
14
+ );
15
+ }
@@ -0,0 +1,20 @@
1
+ import Link from "next/link";
2
+ import { useTranslations } from "next-intl";
3
+ import { AccountPanel } from "@/features/auth/components/account-panel";
4
+ import { PageShell } from "@/components/layout/private/page-shell";
5
+ import { Button } from "@/components/ui/button";
6
+
7
+ export default function AccountPage() {
8
+ const t = useTranslations("auth.account");
9
+
10
+ return (
11
+ <PageShell title={t("title")} description={t("description")}>
12
+ <div className="space-y-4">
13
+ <AccountPanel />
14
+ <Button asChild variant="outline">
15
+ <Link href="/sign-in">{t("goToSignIn")}</Link>
16
+ </Button>
17
+ </div>
18
+ </PageShell>
19
+ );
20
+ }
@@ -0,0 +1,31 @@
1
+ import { useTranslations } from "next-intl";
2
+ import { PageShell } from "@/components/layout/private/page-shell";
3
+
4
+ export default function DashboardPage() {
5
+ const t = useTranslations("common.dashboardPage");
6
+
7
+ const stats = [
8
+ { title: t("stats.users.title"), value: t("stats.users.value") },
9
+ { title: t("stats.records.title"), value: t("stats.records.value") },
10
+ { title: t("stats.activity.title"), value: t("stats.activity.value") },
11
+ ];
12
+
13
+ return (
14
+ <PageShell title={t("title")} description={t("description")}>
15
+ <div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
16
+ {stats.map((stat) => (
17
+ <div
18
+ key={stat.title}
19
+ className="overflow-hidden rounded-lg border bg-card p-6 shadow-sm"
20
+ >
21
+ <h3 className="text-base font-semibold leading-6">{stat.title}</h3>
22
+ <p className="mt-2 text-3xl font-bold tracking-tight text-primary">
23
+ {stat.value}
24
+ </p>
25
+ </div>
26
+ ))}
27
+ </div>
28
+ <p className="mt-8 text-sm text-muted-foreground">{t("placeholder")}</p>
29
+ </PageShell>
30
+ );
31
+ }
@@ -0,0 +1,102 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import type { FormEvent } from "react";
5
+ import { useTranslations } from "next-intl";
6
+ import { Plus } from "lucide-react";
7
+ import { toast } from "sonner";
8
+ import { useCreateExample } from "@/example/api/use-mutations";
9
+ import { createExampleSchema } from "@/example/validations";
10
+ import { ExampleTable } from "@/example/components/example-table";
11
+ import { PageShell } from "@/components/layout/private/page-shell";
12
+ import { Button } from "@/components/ui/button";
13
+ import {
14
+ Dialog,
15
+ DialogContent,
16
+ DialogFooter,
17
+ DialogHeader,
18
+ DialogTitle,
19
+ DialogTrigger,
20
+ } from "@/components/ui/dialog";
21
+ import { Input } from "@/components/ui/input";
22
+ import { Label } from "@/components/ui/label";
23
+
24
+ export default function ExamplePage() {
25
+ const t = useTranslations("example.page");
26
+ const createExample = useCreateExample();
27
+ const [open, setOpen] = useState(false);
28
+ const [name, setName] = useState("");
29
+ const [description, setDescription] = useState("");
30
+
31
+ const onSubmit = async (event: FormEvent<HTMLFormElement>) => {
32
+ event.preventDefault();
33
+
34
+ const parsed = createExampleSchema.safeParse({ name, description });
35
+ if (!parsed.success) {
36
+ toast.error(t("invalidInput"));
37
+ return;
38
+ }
39
+
40
+ try {
41
+ await createExample.mutateAsync(parsed.data);
42
+ toast.success(t("createSuccess"));
43
+ setName("");
44
+ setDescription("");
45
+ setOpen(false);
46
+ } catch (error) {
47
+ const message =
48
+ error instanceof Error ? error.message : t("createFailed");
49
+ toast.error(message);
50
+ }
51
+ };
52
+
53
+ return (
54
+ <PageShell
55
+ title={t("title")}
56
+ description={t("description")}
57
+ actions={
58
+ <Dialog open={open} onOpenChange={setOpen}>
59
+ <DialogTrigger asChild>
60
+ <Button>
61
+ <Plus className="mr-2 h-4 w-4" />
62
+ {t("add")}
63
+ </Button>
64
+ </DialogTrigger>
65
+ <DialogContent>
66
+ <DialogHeader>
67
+ <DialogTitle>{t("createTitle")}</DialogTitle>
68
+ </DialogHeader>
69
+ <form onSubmit={onSubmit} className="space-y-4">
70
+ <div className="space-y-2">
71
+ <Label htmlFor="example-name">{t("nameLabel")}</Label>
72
+ <Input
73
+ id="example-name"
74
+ value={name}
75
+ onChange={(event) => setName(event.target.value)}
76
+ required
77
+ />
78
+ </div>
79
+ <div className="space-y-2">
80
+ <Label htmlFor="example-description">
81
+ {t("descriptionLabel")}
82
+ </Label>
83
+ <Input
84
+ id="example-description"
85
+ value={description}
86
+ onChange={(event) => setDescription(event.target.value)}
87
+ />
88
+ </div>
89
+ <DialogFooter>
90
+ <Button type="submit" disabled={createExample.isPending}>
91
+ {createExample.isPending ? t("creating") : t("createSubmit")}
92
+ </Button>
93
+ </DialogFooter>
94
+ </form>
95
+ </DialogContent>
96
+ </Dialog>
97
+ }
98
+ >
99
+ <ExampleTable />
100
+ </PageShell>
101
+ );
102
+ }
@@ -0,0 +1,22 @@
1
+ import type { ReactNode } from "react";
2
+ import { redirect } from "next/navigation";
3
+ import { DashboardLayout } from "@/components/layout/private/dashboard-layout";
4
+ import { getSessionUser } from "@/lib/auth/rbac";
5
+
6
+ export default async function DashboardRouteLayout({
7
+ children,
8
+ }: {
9
+ children: ReactNode;
10
+ }) {
11
+ const user = await getSessionUser();
12
+
13
+ if (!user) {
14
+ redirect("/sign-in");
15
+ }
16
+
17
+ if (user.requirePasswordChange) {
18
+ redirect("/change-password");
19
+ }
20
+
21
+ return <DashboardLayout>{children}</DashboardLayout>;
22
+ }
@@ -0,0 +1,4 @@
1
+ import { toNextJsHandler } from "better-auth/next-js";
2
+ import { auth } from "@/lib/auth";
3
+
4
+ export const { GET, POST } = toNextJsHandler(auth);
@@ -0,0 +1,55 @@
1
+ import { changePasswordSchema } from "@/features/auth/validations";
2
+ import { fail, ok } from "@/lib/api/response";
3
+ import { auth } from "@/lib/auth";
4
+ import { getSessionUser } from "@/lib/auth/rbac";
5
+ import prisma from "@/lib/db/prisma";
6
+
7
+ const authBaseUrl =
8
+ process.env.BETTER_AUTH_URL ??
9
+ process.env.NEXT_PUBLIC_APP_URL ??
10
+ "http://localhost:3000";
11
+
12
+ export async function POST(request: Request) {
13
+ const actor = await getSessionUser(request.headers);
14
+ if (!actor) {
15
+ return fail("UNAUTHORIZED", "Unauthorized", { status: 401 });
16
+ }
17
+
18
+ const payload = await request.json().catch(() => null);
19
+ const parsed = changePasswordSchema.safeParse(payload);
20
+
21
+ if (!parsed.success) {
22
+ return fail("VALIDATION_ERROR", "Invalid password payload.", {
23
+ status: 400,
24
+ details: parsed.error.flatten(),
25
+ });
26
+ }
27
+
28
+ const verifyResponse = await fetch(`${authBaseUrl}/api/auth/sign-in/username`, {
29
+ method: "POST",
30
+ headers: { "Content-Type": "application/json" },
31
+ body: JSON.stringify({
32
+ username: actor.username,
33
+ password: parsed.data.currentPassword,
34
+ }),
35
+ });
36
+
37
+ if (!verifyResponse.ok) {
38
+ return fail("UNAUTHORIZED", "Current password is incorrect.", { status: 401 });
39
+ }
40
+
41
+ await auth.api.changePassword({
42
+ body: {
43
+ currentPassword: parsed.data.currentPassword,
44
+ newPassword: parsed.data.newPassword,
45
+ },
46
+ headers: request.headers,
47
+ });
48
+
49
+ await prisma.user.update({
50
+ where: { id: actor.id },
51
+ data: { requirePasswordChange: false },
52
+ });
53
+
54
+ return ok({ updated: true });
55
+ }