@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.
- package/README.md +6 -2
- package/dist/cli.js +778 -101
- package/package.json +2 -1
- package/templates/next-base/PROJECT_STRUCTURE.md +88 -0
- package/templates/next-base/SETUP.md +86 -0
- package/templates/next-base/bun.lock +1443 -0
- package/templates/next-base/components.json +21 -0
- package/templates/next-base/messages/vi/auth.json +42 -0
- package/templates/next-base/messages/vi/common.json +34 -0
- package/templates/next-base/messages/vi/example.json +10 -0
- package/templates/next-base/next-env.d.ts +3 -1
- package/templates/next-base/next.config.ts +11 -1
- package/templates/next-base/nextcli.json +8 -0
- package/templates/next-base/package.json +21 -1
- package/templates/next-base/postcss.config.mjs +5 -0
- package/templates/next-base/prisma/migrations/20260612000000_init/migration.sql +104 -0
- package/templates/next-base/prisma/migrations/migration_lock.toml +3 -0
- package/templates/next-base/prisma/schema.prisma +23 -9
- package/templates/next-base/public/logo.svg +4 -0
- package/templates/next-base/src/app/(auth)/change-password/layout.tsx +21 -0
- package/templates/next-base/src/app/(auth)/change-password/page.tsx +14 -0
- package/templates/next-base/src/app/(auth)/layout.tsx +9 -0
- package/templates/next-base/src/app/(auth)/sign-in/layout.tsx +17 -0
- package/templates/next-base/src/app/(auth)/sign-in/page.tsx +6 -3
- package/templates/next-base/src/app/(dashboard)/account/page.tsx +9 -5
- package/templates/next-base/src/app/(dashboard)/dashboard/page.tsx +17 -0
- package/templates/next-base/src/app/(dashboard)/example/page.tsx +5 -2
- package/templates/next-base/src/app/(dashboard)/layout.tsx +22 -0
- package/templates/next-base/src/app/api/v1/auth/change-password/route.ts +55 -0
- package/templates/next-base/src/app/api/v1/auth/login/route.ts +15 -5
- package/templates/next-base/src/app/api/v1/auth/me/route.ts +17 -19
- package/templates/next-base/src/app/api/v1/users/[id]/route.ts +104 -0
- package/templates/next-base/src/app/api/v1/users/route.ts +58 -0
- package/templates/next-base/src/app/globals.css +111 -0
- package/templates/next-base/src/app/layout.tsx +24 -10
- package/templates/next-base/src/app/page.tsx +2 -18
- package/templates/next-base/src/components/branding/logo.tsx +27 -0
- package/templates/next-base/src/components/layout/private/app-sidebar.tsx +44 -0
- package/templates/next-base/src/components/layout/private/dashboard-layout.tsx +54 -0
- package/templates/next-base/src/components/layout/private/locale-switcher.tsx +45 -0
- package/templates/next-base/src/components/layout/private/nav-sidebar.tsx +55 -0
- package/templates/next-base/src/components/layout/private/nav-user.tsx +99 -0
- package/templates/next-base/src/components/providers/theme-provider.tsx +11 -0
- package/templates/next-base/src/components/ui/alert-dialog.tsx +11 -0
- package/templates/next-base/src/components/ui/avatar.tsx +45 -0
- package/templates/next-base/src/components/ui/badge.tsx +29 -0
- package/templates/next-base/src/components/ui/button.tsx +47 -7
- package/templates/next-base/src/components/ui/card.tsx +54 -0
- package/templates/next-base/src/components/ui/data-table/data-table-column-header.tsx +23 -0
- package/templates/next-base/src/components/ui/data-table/data-table-filter-list.tsx +3 -0
- package/templates/next-base/src/components/ui/data-table/data-table-pagination.tsx +35 -0
- package/templates/next-base/src/components/ui/data-table/data-table-skeleton.tsx +11 -0
- package/templates/next-base/src/components/ui/data-table/data-table-toolbar.tsx +14 -0
- package/templates/next-base/src/components/ui/data-table/data-table-view-options.tsx +3 -0
- package/templates/next-base/src/components/ui/data-table/data-table.tsx +72 -0
- package/templates/next-base/src/components/ui/dialog.tsx +105 -0
- package/templates/next-base/src/components/ui/dropdown-menu.tsx +44 -0
- package/templates/next-base/src/components/ui/input.tsx +19 -0
- package/templates/next-base/src/components/ui/label.tsx +15 -0
- package/templates/next-base/src/components/ui/popover.tsx +30 -0
- package/templates/next-base/src/components/ui/scroll-area.tsx +47 -0
- package/templates/next-base/src/components/ui/select.tsx +76 -0
- package/templates/next-base/src/components/ui/separator.tsx +23 -0
- package/templates/next-base/src/components/ui/sheet.tsx +117 -0
- package/templates/next-base/src/components/ui/sidebar.tsx +215 -0
- package/templates/next-base/src/components/ui/skeleton.tsx +10 -0
- package/templates/next-base/src/components/ui/sonner.tsx +3 -0
- package/templates/next-base/src/components/ui/table.tsx +54 -0
- package/templates/next-base/src/components/ui/tabs.tsx +52 -0
- package/templates/next-base/src/components/ui/textarea.tsx +17 -0
- package/templates/next-base/src/components/ui/tooltip.tsx +26 -0
- package/templates/next-base/src/config/branding.ts +14 -0
- package/templates/next-base/src/data/sidebar-modules.ts +11 -0
- package/templates/next-base/src/example/components/example-table.tsx +25 -40
- package/templates/next-base/src/features/auth/components/account-panel.tsx +32 -14
- package/templates/next-base/src/features/auth/components/change-password-form.tsx +82 -0
- package/templates/next-base/src/features/auth/components/sign-in-form.tsx +53 -35
- package/templates/next-base/src/features/auth/validations.ts +7 -1
- package/templates/next-base/src/features/users/services.ts +132 -0
- package/templates/next-base/src/features/users/validations.ts +21 -0
- package/templates/next-base/src/hooks/index.ts +1 -1
- package/templates/next-base/src/hooks/table/use-data-table.ts +33 -0
- package/templates/next-base/src/hooks/use-mobile.ts +25 -0
- package/templates/next-base/src/i18n/config.ts +7 -0
- package/templates/next-base/src/i18n/namespaces.ts +5 -0
- package/templates/next-base/src/i18n/request.ts +19 -2
- package/templates/next-base/src/instrumentation.ts +14 -0
- package/templates/next-base/src/lib/auth-client.ts +2 -2
- package/templates/next-base/src/lib/auth.ts +2 -2
- package/templates/next-base/src/lib/bootstrap.ts +96 -0
- package/templates/next-base/src/lib/constants.ts +7 -0
- package/templates/next-base/src/lib/prisma.ts +11 -1
- package/templates/next-base/src/lib/rbac.ts +62 -0
- package/templates/next-base/src/types/data-table.ts +4 -0
- package/templates/next-base/src/types/index.ts +2 -0
- package/templates/next-base/tsconfig.json +29 -7
- package/templates/next-base/middleware.ts +0 -10
- 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
|
+
}
|
|
@@ -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
|
|
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
|
+
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
3
|
"target": "ES2022",
|
|
4
|
-
"lib": [
|
|
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": "
|
|
15
|
-
"types": [
|
|
18
|
+
"jsx": "react-jsx",
|
|
19
|
+
"types": [
|
|
20
|
+
"node",
|
|
21
|
+
"react",
|
|
22
|
+
"react-dom"
|
|
23
|
+
],
|
|
16
24
|
"incremental": true,
|
|
17
|
-
"plugins": [
|
|
25
|
+
"plugins": [
|
|
26
|
+
{
|
|
27
|
+
"name": "next"
|
|
28
|
+
}
|
|
29
|
+
],
|
|
18
30
|
"paths": {
|
|
19
|
-
"@/*": [
|
|
31
|
+
"@/*": [
|
|
32
|
+
"./src/*"
|
|
33
|
+
]
|
|
20
34
|
}
|
|
21
35
|
},
|
|
22
|
-
"include": [
|
|
23
|
-
|
|
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
|
}
|