@thinhnguyencth1204/nextcli 0.2.1 → 0.4.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 (98) hide show
  1. package/README.md +6 -2
  2. package/dist/cli.js +778 -101
  3. package/package.json +2 -1
  4. package/templates/next-base/PROJECT_STRUCTURE.md +88 -0
  5. package/templates/next-base/SETUP.md +86 -0
  6. package/templates/next-base/bun.lock +1443 -0
  7. package/templates/next-base/components.json +21 -0
  8. package/templates/next-base/messages/vi/auth.json +42 -0
  9. package/templates/next-base/messages/vi/common.json +34 -0
  10. package/templates/next-base/messages/vi/example.json +10 -0
  11. package/templates/next-base/next-env.d.ts +3 -1
  12. package/templates/next-base/next.config.ts +11 -1
  13. package/templates/next-base/nextcli.json +8 -0
  14. package/templates/next-base/package.json +21 -1
  15. package/templates/next-base/postcss.config.mjs +5 -0
  16. package/templates/next-base/prisma/migrations/20260612000000_init/migration.sql +104 -0
  17. package/templates/next-base/prisma/migrations/migration_lock.toml +3 -0
  18. package/templates/next-base/prisma/schema.prisma +23 -9
  19. package/templates/next-base/public/logo.svg +4 -0
  20. package/templates/next-base/src/app/(auth)/change-password/layout.tsx +21 -0
  21. package/templates/next-base/src/app/(auth)/change-password/page.tsx +14 -0
  22. package/templates/next-base/src/app/(auth)/layout.tsx +9 -0
  23. package/templates/next-base/src/app/(auth)/sign-in/layout.tsx +17 -0
  24. package/templates/next-base/src/app/(auth)/sign-in/page.tsx +6 -3
  25. package/templates/next-base/src/app/(dashboard)/account/page.tsx +9 -5
  26. package/templates/next-base/src/app/(dashboard)/dashboard/page.tsx +17 -0
  27. package/templates/next-base/src/app/(dashboard)/example/page.tsx +5 -2
  28. package/templates/next-base/src/app/(dashboard)/layout.tsx +22 -0
  29. package/templates/next-base/src/app/api/v1/auth/change-password/route.ts +55 -0
  30. package/templates/next-base/src/app/api/v1/auth/login/route.ts +15 -5
  31. package/templates/next-base/src/app/api/v1/auth/me/route.ts +17 -19
  32. package/templates/next-base/src/app/api/v1/users/[id]/route.ts +104 -0
  33. package/templates/next-base/src/app/api/v1/users/route.ts +58 -0
  34. package/templates/next-base/src/app/globals.css +111 -0
  35. package/templates/next-base/src/app/layout.tsx +24 -10
  36. package/templates/next-base/src/app/page.tsx +2 -18
  37. package/templates/next-base/src/components/branding/logo.tsx +27 -0
  38. package/templates/next-base/src/components/layout/private/app-sidebar.tsx +44 -0
  39. package/templates/next-base/src/components/layout/private/dashboard-layout.tsx +54 -0
  40. package/templates/next-base/src/components/layout/private/locale-switcher.tsx +45 -0
  41. package/templates/next-base/src/components/layout/private/nav-sidebar.tsx +55 -0
  42. package/templates/next-base/src/components/layout/private/nav-user.tsx +99 -0
  43. package/templates/next-base/src/components/providers/theme-provider.tsx +11 -0
  44. package/templates/next-base/src/components/ui/alert-dialog.tsx +11 -0
  45. package/templates/next-base/src/components/ui/avatar.tsx +45 -0
  46. package/templates/next-base/src/components/ui/badge.tsx +29 -0
  47. package/templates/next-base/src/components/ui/button.tsx +47 -7
  48. package/templates/next-base/src/components/ui/card.tsx +54 -0
  49. package/templates/next-base/src/components/ui/data-table/data-table-column-header.tsx +23 -0
  50. package/templates/next-base/src/components/ui/data-table/data-table-filter-list.tsx +3 -0
  51. package/templates/next-base/src/components/ui/data-table/data-table-pagination.tsx +35 -0
  52. package/templates/next-base/src/components/ui/data-table/data-table-skeleton.tsx +11 -0
  53. package/templates/next-base/src/components/ui/data-table/data-table-toolbar.tsx +14 -0
  54. package/templates/next-base/src/components/ui/data-table/data-table-view-options.tsx +3 -0
  55. package/templates/next-base/src/components/ui/data-table/data-table.tsx +72 -0
  56. package/templates/next-base/src/components/ui/dialog.tsx +105 -0
  57. package/templates/next-base/src/components/ui/dropdown-menu.tsx +44 -0
  58. package/templates/next-base/src/components/ui/input.tsx +19 -0
  59. package/templates/next-base/src/components/ui/label.tsx +15 -0
  60. package/templates/next-base/src/components/ui/popover.tsx +30 -0
  61. package/templates/next-base/src/components/ui/scroll-area.tsx +47 -0
  62. package/templates/next-base/src/components/ui/select.tsx +76 -0
  63. package/templates/next-base/src/components/ui/separator.tsx +23 -0
  64. package/templates/next-base/src/components/ui/sheet.tsx +117 -0
  65. package/templates/next-base/src/components/ui/sidebar.tsx +215 -0
  66. package/templates/next-base/src/components/ui/skeleton.tsx +10 -0
  67. package/templates/next-base/src/components/ui/sonner.tsx +3 -0
  68. package/templates/next-base/src/components/ui/table.tsx +54 -0
  69. package/templates/next-base/src/components/ui/tabs.tsx +52 -0
  70. package/templates/next-base/src/components/ui/textarea.tsx +17 -0
  71. package/templates/next-base/src/components/ui/tooltip.tsx +26 -0
  72. package/templates/next-base/src/config/branding.ts +14 -0
  73. package/templates/next-base/src/data/sidebar-modules.ts +11 -0
  74. package/templates/next-base/src/example/components/example-table.tsx +25 -40
  75. package/templates/next-base/src/features/auth/components/account-panel.tsx +32 -14
  76. package/templates/next-base/src/features/auth/components/change-password-form.tsx +82 -0
  77. package/templates/next-base/src/features/auth/components/sign-in-form.tsx +53 -35
  78. package/templates/next-base/src/features/auth/validations.ts +7 -1
  79. package/templates/next-base/src/features/users/services.ts +132 -0
  80. package/templates/next-base/src/features/users/validations.ts +21 -0
  81. package/templates/next-base/src/hooks/index.ts +1 -1
  82. package/templates/next-base/src/hooks/table/use-data-table.ts +33 -0
  83. package/templates/next-base/src/hooks/use-mobile.ts +25 -0
  84. package/templates/next-base/src/i18n/config.ts +7 -0
  85. package/templates/next-base/src/i18n/namespaces.ts +5 -0
  86. package/templates/next-base/src/i18n/request.ts +19 -2
  87. package/templates/next-base/src/instrumentation.ts +14 -0
  88. package/templates/next-base/src/lib/auth-client.ts +2 -2
  89. package/templates/next-base/src/lib/auth.ts +2 -2
  90. package/templates/next-base/src/lib/bootstrap.ts +96 -0
  91. package/templates/next-base/src/lib/constants.ts +7 -0
  92. package/templates/next-base/src/lib/prisma.ts +11 -1
  93. package/templates/next-base/src/lib/rbac.ts +62 -0
  94. package/templates/next-base/src/types/data-table.ts +4 -0
  95. package/templates/next-base/src/types/index.ts +2 -0
  96. package/templates/next-base/tsconfig.json +29 -7
  97. package/templates/next-base/middleware.ts +0 -10
  98. package/templates/next-base/src/app/styles.css +0 -12
@@ -0,0 +1,14 @@
1
+ export async function register() {
2
+ if (process.env.NEXT_RUNTIME !== "nodejs") {
3
+ return;
4
+ }
5
+
6
+ try {
7
+ const { runBootstrap } = await import("@/lib/bootstrap");
8
+ await runBootstrap();
9
+ } catch (error) {
10
+ const message =
11
+ error instanceof Error ? error.message : "Unknown instrumentation error";
12
+ console.warn(`[instrumentation] Bootstrap failed: ${message}`);
13
+ }
14
+ }
@@ -1,7 +1,7 @@
1
1
  import { createAuthClient } from "better-auth/react";
2
- import { jwtClient } from "better-auth/client/plugins";
2
+ import { jwtClient, usernameClient } from "better-auth/client/plugins";
3
3
 
4
4
  export const authClient = createAuthClient({
5
5
  baseURL: process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000",
6
- plugins: [jwtClient()],
6
+ plugins: [jwtClient(), usernameClient()],
7
7
  });
@@ -1,6 +1,6 @@
1
1
  import { betterAuth } from "better-auth/minimal";
2
2
  import { prismaAdapter } from "better-auth/adapters/prisma";
3
- import { jwt } from "better-auth/plugins";
3
+ import { jwt, username } from "better-auth/plugins";
4
4
  import prisma from "@/lib/prisma";
5
5
 
6
6
  const socialProviders = {
@@ -12,7 +12,7 @@ export const auth = betterAuth({
12
12
  database: prismaAdapter(prisma, {
13
13
  provider: "postgresql",
14
14
  }),
15
- plugins: [jwt()],
15
+ plugins: [jwt(), username()],
16
16
  emailAndPassword: {
17
17
  enabled: true,
18
18
  },
@@ -0,0 +1,96 @@
1
+ import type { PrismaClient } from "@prisma/client";
2
+ import {
3
+ ADMIN_ROLE_LEVEL,
4
+ ADMIN_ROLE_NAME,
5
+ INTERNAL_EMAIL_DOMAIN,
6
+ SUPER_ADMIN_USERNAME,
7
+ } from "@/lib/constants";
8
+ import { auth } from "@/lib/auth";
9
+ import prisma from "@/lib/prisma";
10
+
11
+ const DEFAULT_ADMIN_PASSWORD = "admin";
12
+
13
+ function hasRoleModel(client: PrismaClient): boolean {
14
+ const delegate = (
15
+ client as PrismaClient & { role?: { findUnique?: unknown } }
16
+ ).role;
17
+ return typeof delegate?.findUnique === "function";
18
+ }
19
+
20
+ function logBootstrapSkip(message: string): void {
21
+ console.warn(`[bootstrap] ${message}`);
22
+ }
23
+
24
+ export async function runBootstrap(): Promise<void> {
25
+ if (!hasRoleModel(prisma)) {
26
+ logBootstrapSkip(
27
+ "Prisma client is missing the Role model. Run: bun run db:generate && bun run db:migrate",
28
+ );
29
+ return;
30
+ }
31
+
32
+ try {
33
+ let adminRole = await prisma.role.findUnique({
34
+ where: { name: ADMIN_ROLE_NAME },
35
+ });
36
+
37
+ if (!adminRole) {
38
+ adminRole = await prisma.role.create({
39
+ data: {
40
+ name: ADMIN_ROLE_NAME,
41
+ level: ADMIN_ROLE_LEVEL,
42
+ },
43
+ });
44
+ }
45
+
46
+ const existingAdmin = await prisma.user.findUnique({
47
+ where: { username: SUPER_ADMIN_USERNAME },
48
+ });
49
+
50
+ if (existingAdmin) {
51
+ if (!existingAdmin.roleId) {
52
+ await prisma.user.update({
53
+ where: { id: existingAdmin.id },
54
+ data: { roleId: adminRole.id },
55
+ });
56
+ }
57
+ return;
58
+ }
59
+
60
+ try {
61
+ await auth.api.signUpEmail({
62
+ body: {
63
+ email: `${SUPER_ADMIN_USERNAME}@${INTERNAL_EMAIL_DOMAIN}`,
64
+ password: DEFAULT_ADMIN_PASSWORD,
65
+ name: "Administrator",
66
+ username: SUPER_ADMIN_USERNAME,
67
+ displayUsername: SUPER_ADMIN_USERNAME,
68
+ },
69
+ });
70
+ } catch {
71
+ // User may already exist from a partial bootstrap run.
72
+ }
73
+
74
+ const adminUser = await prisma.user.findUnique({
75
+ where: { username: SUPER_ADMIN_USERNAME },
76
+ });
77
+
78
+ if (!adminUser) {
79
+ return;
80
+ }
81
+
82
+ await prisma.user.update({
83
+ where: { id: adminUser.id },
84
+ data: {
85
+ roleId: adminRole.id,
86
+ requirePasswordChange: true,
87
+ },
88
+ });
89
+ } catch (error) {
90
+ const message =
91
+ error instanceof Error ? error.message : "Unknown bootstrap error";
92
+ logBootstrapSkip(
93
+ `Skipped admin seed (${message}). Ensure Postgres is running and run: bun run db:migrate`,
94
+ );
95
+ }
96
+ }
@@ -0,0 +1,7 @@
1
+ /** Built-in super-admin account username — protected from RBAC mutations. */
2
+ export const SUPER_ADMIN_USERNAME = "admin";
3
+
4
+ export const ADMIN_ROLE_NAME = "admin";
5
+ export const ADMIN_ROLE_LEVEL = 100;
6
+
7
+ export const INTERNAL_EMAIL_DOMAIN = "internal.local";
@@ -1,10 +1,20 @@
1
+ import { PrismaPg } from "@prisma/adapter-pg";
1
2
  import { PrismaClient } from "@prisma/client";
3
+ import { Pool } from "pg";
2
4
 
3
5
  declare global {
4
6
  var prisma: PrismaClient | undefined;
5
7
  }
6
8
 
7
- const prisma = global.prisma ?? new PrismaClient();
9
+ const connectionString = process.env.DATABASE_URL;
10
+ if (!connectionString) {
11
+ throw new Error(
12
+ "DATABASE_URL is missing. Please set it in your environment.",
13
+ );
14
+ }
15
+
16
+ const adapter = new PrismaPg(new Pool({ connectionString }));
17
+ const prisma = global.prisma ?? new PrismaClient({ adapter });
8
18
 
9
19
  if (process.env.NODE_ENV !== "production") {
10
20
  global.prisma = prisma;
@@ -0,0 +1,62 @@
1
+ import { headers } from "next/headers";
2
+ import type { Role, User } from "@prisma/client";
3
+ import { auth } from "@/lib/auth";
4
+ import prisma from "@/lib/prisma";
5
+ import { SUPER_ADMIN_USERNAME } from "@/lib/constants";
6
+
7
+ export type SessionUser = User & { role: Role | null };
8
+
9
+ export async function getSessionUser(
10
+ requestHeaders?: Headers,
11
+ ): Promise<SessionUser | null> {
12
+ const session = await auth.api.getSession({
13
+ headers: requestHeaders ?? (await headers()),
14
+ });
15
+
16
+ if (!session?.user?.id) {
17
+ return null;
18
+ }
19
+
20
+ return prisma.user.findUnique({
21
+ where: { id: session.user.id },
22
+ include: { role: true },
23
+ });
24
+ }
25
+
26
+ export function isSuperAdmin(user: Pick<User, "username">): boolean {
27
+ return user.username === SUPER_ADMIN_USERNAME;
28
+ }
29
+
30
+ export function canActOnUser(
31
+ actor: SessionUser,
32
+ target: SessionUser,
33
+ ): boolean {
34
+ if (isSuperAdmin(target)) {
35
+ return false;
36
+ }
37
+
38
+ if (!actor.role || !target.role) {
39
+ return false;
40
+ }
41
+
42
+ return actor.role.level > target.role.level;
43
+ }
44
+
45
+ export function canAssignRole(
46
+ actor: SessionUser,
47
+ targetRole: Pick<Role, "level">,
48
+ ): boolean {
49
+ if (!actor.role) {
50
+ return false;
51
+ }
52
+
53
+ return actor.role.level > targetRole.level;
54
+ }
55
+
56
+ export function requireAuthenticated(
57
+ user: SessionUser | null,
58
+ ): asserts user is SessionUser {
59
+ if (!user) {
60
+ throw new Error("UNAUTHORIZED");
61
+ }
62
+ }
@@ -0,0 +1,4 @@
1
+ export type DataTablePaginationState = {
2
+ pageIndex: number;
3
+ pageSize: number;
4
+ };
@@ -38,3 +38,5 @@ export type ApiErrorResponse = {
38
38
  };
39
39
 
40
40
  export type ApiResponse<T> = ApiSuccess<T> | ApiErrorResponse;
41
+
42
+ export * from "@/types/data-table";
@@ -1,7 +1,11 @@
1
1
  {
2
2
  "compilerOptions": {
3
3
  "target": "ES2022",
4
- "lib": ["dom", "dom.iterable", "esnext"],
4
+ "lib": [
5
+ "dom",
6
+ "dom.iterable",
7
+ "esnext"
8
+ ],
5
9
  "allowJs": false,
6
10
  "skipLibCheck": true,
7
11
  "strict": true,
@@ -11,14 +15,32 @@
11
15
  "moduleResolution": "bundler",
12
16
  "resolveJsonModule": true,
13
17
  "isolatedModules": true,
14
- "jsx": "preserve",
15
- "types": ["node", "react", "react-dom"],
18
+ "jsx": "react-jsx",
19
+ "types": [
20
+ "node",
21
+ "react",
22
+ "react-dom"
23
+ ],
16
24
  "incremental": true,
17
- "plugins": [{ "name": "next" }],
25
+ "plugins": [
26
+ {
27
+ "name": "next"
28
+ }
29
+ ],
18
30
  "paths": {
19
- "@/*": ["./src/*"]
31
+ "@/*": [
32
+ "./src/*"
33
+ ]
20
34
  }
21
35
  },
22
- "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
23
- "exclude": ["node_modules"]
36
+ "include": [
37
+ "next-env.d.ts",
38
+ "**/*.ts",
39
+ "**/*.tsx",
40
+ ".next/types/**/*.ts",
41
+ ".next/dev/types/**/*.ts"
42
+ ],
43
+ "exclude": [
44
+ "node_modules"
45
+ ]
24
46
  }
@@ -1,10 +0,0 @@
1
- import createMiddleware from "next-intl/middleware";
2
-
3
- export default createMiddleware({
4
- locales: ["en", "vi"],
5
- defaultLocale: "en",
6
- });
7
-
8
- export const config = {
9
- matcher: ["/((?!api|_next|.*\\..*).*)"],
10
- };
@@ -1,12 +0,0 @@
1
- :root {
2
- color-scheme: light dark;
3
- }
4
-
5
- * {
6
- box-sizing: border-box;
7
- }
8
-
9
- body {
10
- margin: 0;
11
- font-family: Arial, sans-serif;
12
- }