@thinhnguyencth1204/nextcli 0.7.0 → 0.9.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 +37 -24
- package/dist/cli.js +168 -107
- package/package.json +5 -3
- package/templates/features/supabase/src/lib/supabase/rich-text-image-sync.ts +28 -0
- package/templates/next-base/.env +16 -0
- package/templates/next-base/.env.development +16 -0
- package/templates/next-base/.env.example +16 -0
- package/templates/next-base/PROJECT_STRUCTURE.md +29 -18
- package/templates/next-base/SETUP.md +62 -10
- package/templates/next-base/bun.lock +59 -414
- 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 +1 -1
- package/templates/next-base/next.config.ts +4 -1
- package/templates/next-base/nextcli.json +12 -4
- package/templates/next-base/package.json +25 -1
- package/templates/next-base/prisma/schema.prisma +84 -0
- package/templates/next-base/prisma.config.ts +16 -0
- package/templates/next-base/src/app/(auth)/.gitkeep +1 -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 +14 -0
- package/templates/next-base/src/app/(dashboard)/account/page.tsx +18 -0
- package/templates/next-base/src/app/(dashboard)/dashboard/page.tsx +17 -0
- package/templates/next-base/src/app/(dashboard)/example/page.tsx +13 -0
- package/templates/next-base/src/app/(dashboard)/layout.tsx +22 -0
- package/templates/next-base/src/app/api/auth/[...all]/route.ts +4 -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 +70 -0
- package/templates/next-base/src/app/api/v1/auth/logout/route.ts +28 -0
- package/templates/next-base/src/app/api/v1/auth/me/route.ts +24 -0
- package/templates/next-base/src/app/api/v1/auth/refresh/route.ts +32 -0
- package/templates/next-base/src/app/api/v1/example/route.ts +34 -0
- 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/blog-demo/page.tsx +9 -0
- package/templates/next-base/src/app/globals.css +57 -0
- package/templates/next-base/src/app/layout.tsx +14 -6
- package/templates/next-base/src/app/page.tsx +2 -25
- 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/query-provider.tsx +17 -0
- package/templates/next-base/src/components/rich-text/adapters/textarea-field.tsx +50 -0
- package/templates/next-base/src/components/rich-text/client-only.tsx +23 -0
- package/templates/next-base/src/components/rich-text/editor-field.tsx +62 -0
- package/templates/next-base/src/components/rich-text/examples/blog-rich-text-demo.tsx +218 -0
- package/templates/next-base/src/components/rich-text/index.ts +11 -0
- package/templates/next-base/src/components/rich-text/lexical/extension.ts +37 -0
- package/templates/next-base/src/components/rich-text/lexical/nodes/image-node.tsx +187 -0
- package/templates/next-base/src/components/rich-text/lexical/plugins/image-plugin.tsx +40 -0
- package/templates/next-base/src/components/rich-text/lexical/plugins/initial-state-plugin.tsx +26 -0
- package/templates/next-base/src/components/rich-text/lexical/plugins/on-change-plugin.tsx +26 -0
- package/templates/next-base/src/components/rich-text/lexical/plugins/toolbar-plugin.tsx +190 -0
- package/templates/next-base/src/components/rich-text/lexical/rich-text-editor.tsx +121 -0
- package/templates/next-base/src/components/rich-text/lexical/theme.ts +18 -0
- package/templates/next-base/src/components/rich-text/rich-text-renderer.tsx +72 -0
- package/templates/next-base/src/components/rich-text/types.ts +60 -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/sidebar.tsx +215 -0
- package/templates/next-base/src/data/sidebar-modules.ts +11 -0
- package/templates/next-base/src/example/api/use-example.ts +21 -0
- package/templates/next-base/src/example/api/use-mutations.ts +20 -0
- package/templates/next-base/src/example/components/example-table.tsx +51 -0
- package/templates/next-base/src/example/services.ts +9 -0
- package/templates/next-base/src/example/validations.ts +8 -0
- package/templates/next-base/src/features/auth/components/account-panel.tsx +80 -0
- 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 +95 -0
- package/templates/next-base/src/features/auth/validations.ts +14 -0
- 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 +25 -0
- package/templates/next-base/src/instrumentation.ts +14 -0
- package/templates/next-base/src/lib/api/axios.ts +145 -0
- package/templates/next-base/src/lib/api/response.ts +45 -0
- package/templates/next-base/src/lib/api/token-store.ts +13 -0
- package/templates/next-base/src/lib/auth/bootstrap.ts +95 -0
- package/templates/next-base/src/lib/auth/client.ts +7 -0
- package/templates/next-base/src/lib/auth/cookies.ts +15 -0
- package/templates/next-base/src/lib/auth/index.ts +1 -0
- package/templates/next-base/src/lib/auth/rbac.ts +59 -0
- package/templates/next-base/src/lib/auth/server.ts +21 -0
- package/templates/next-base/src/lib/constants.ts +10 -0
- package/templates/next-base/src/lib/db/prisma.ts +23 -0
- package/templates/next-base/src/lib/prisma.ts +23 -0
- package/templates/next-base/src/lib/rich-text/default-image-removal.ts +10 -0
- package/templates/next-base/src/lib/rich-text/image-urls.ts +41 -0
- package/templates/next-base/src/lib/rich-text/index.ts +12 -0
- package/templates/next-base/src/lib/rich-text/supabase-url.ts +67 -0
- package/templates/next-base/src/lib/rich-text/sync-removed-images.ts +48 -0
- package/templates/next-base/src/lib/supabase/client.ts +6 -0
- package/templates/next-base/src/lib/supabase/rich-text-image-sync.ts +28 -0
- package/templates/next-base/src/lib/supabase/storage-config.ts +69 -0
- package/templates/next-base/src/lib/supabase/storage.ts +164 -0
- package/templates/next-base/src/types/data-table.ts +4 -0
- package/templates/next-base/src/types/index.ts +0 -2
- package/templates/next-base/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,42 @@
|
|
|
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
|
+
"missingAccessToken": "Không tìm thấy access token.",
|
|
15
|
+
"success": "Đăng nhập thành công.",
|
|
16
|
+
"failed": "Đăng nhập thất bại."
|
|
17
|
+
},
|
|
18
|
+
"changePasswordPage": {
|
|
19
|
+
"title": "Đổi mật khẩu",
|
|
20
|
+
"description": "Bạn cần đổi mật khẩu trước khi tiếp tục sử dụng hệ thống."
|
|
21
|
+
},
|
|
22
|
+
"changePasswordForm": {
|
|
23
|
+
"currentPassword": "Mật khẩu hiện tại",
|
|
24
|
+
"newPassword": "Mật khẩu mới",
|
|
25
|
+
"submit": "Cập nhật mật khẩu",
|
|
26
|
+
"submitting": "Đang cập nhật...",
|
|
27
|
+
"invalidInput": "Vui lòng nhập mật khẩu hợp lệ (tối thiểu 8 ký tự).",
|
|
28
|
+
"success": "Đổi mật khẩu thành công.",
|
|
29
|
+
"failed": "Không thể đổi mật khẩu."
|
|
30
|
+
},
|
|
31
|
+
"account": {
|
|
32
|
+
"title": "Tài khoản",
|
|
33
|
+
"loading": "Đang tải thông tin tài khoản...",
|
|
34
|
+
"noSession": "Chưa có phiên đăng nhập hoạt động.",
|
|
35
|
+
"userId": "Mã người dùng",
|
|
36
|
+
"username": "Tên đăng nhập",
|
|
37
|
+
"email": "Email",
|
|
38
|
+
"name": "Tên",
|
|
39
|
+
"na": "N/A",
|
|
40
|
+
"goToSignIn": "Đi tới trang đăng nhập"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
"userMenu": {
|
|
16
|
+
"title": "Tài khoản",
|
|
17
|
+
"anonymousName": "Người dùng",
|
|
18
|
+
"anonymousEmail": "user@example.com",
|
|
19
|
+
"appearance": "Giao diện",
|
|
20
|
+
"logout": "Đăng xuất"
|
|
21
|
+
},
|
|
22
|
+
"locale": {
|
|
23
|
+
"label": "Ngôn ngữ",
|
|
24
|
+
"vi": "Tiếng Việt",
|
|
25
|
+
"en": "English",
|
|
26
|
+
"ja": "日本語",
|
|
27
|
+
"ko": "한국어"
|
|
28
|
+
},
|
|
29
|
+
"table": {
|
|
30
|
+
"noResults": "Không có dữ liệu.",
|
|
31
|
+
"previous": "Trước",
|
|
32
|
+
"next": "Sau"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference types="next" />
|
|
2
2
|
/// <reference types="next/image-types/global" />
|
|
3
|
-
import "./.next/
|
|
3
|
+
import "./.next/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
|
-
|
|
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
|
-
|
|
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,11 +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",
|
|
43
|
+
"date-fns": "^3.6.0",
|
|
44
|
+
"lexical": "^0.45.0",
|
|
26
45
|
"lucide-react": "^0.525.0",
|
|
27
46
|
"next": "^16.1.6",
|
|
47
|
+
"next-intl": "^4.13.0",
|
|
28
48
|
"next-themes": "^0.4.6",
|
|
49
|
+
"nuqs": "^2.8.1",
|
|
50
|
+
"pg": "^8.16.3",
|
|
29
51
|
"react": "^19.0.0",
|
|
30
52
|
"react-dom": "^19.0.0",
|
|
31
53
|
"sonner": "^1.7.1",
|
|
@@ -38,8 +60,10 @@
|
|
|
38
60
|
"@types/node": "^22.7.4",
|
|
39
61
|
"@types/react": "^19.0.0",
|
|
40
62
|
"@types/react-dom": "^19.0.0",
|
|
63
|
+
"dotenv": "^17.4.2",
|
|
41
64
|
"eslint": "^9.11.1",
|
|
42
65
|
"eslint-config-next": "^16.1.6",
|
|
66
|
+
"prisma": "^7.8.0",
|
|
43
67
|
"tailwindcss": "^4.1.11",
|
|
44
68
|
"typescript": "^5.6.2"
|
|
45
69
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
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
|
+
// Optional Chatbox models are appended by NexTCLI only when adding the chat feature.
|
|
74
|
+
// Review generated schema changes before running migrations on production databases.
|
|
75
|
+
|
|
76
|
+
// Starter demo table for template onboarding only.
|
|
77
|
+
// Rename/remove this `Example` model before production migration.
|
|
78
|
+
model Example {
|
|
79
|
+
id String @id @default(cuid())
|
|
80
|
+
name String
|
|
81
|
+
description String?
|
|
82
|
+
createdAt DateTime @default(now())
|
|
83
|
+
updatedAt DateTime @updatedAt
|
|
84
|
+
}
|
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -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,14 @@
|
|
|
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
|
+
<main className="space-y-3">
|
|
9
|
+
<h1 className="text-2xl font-semibold">{t("title")}</h1>
|
|
10
|
+
<p className="text-sm text-muted-foreground">{t("description")}</p>
|
|
11
|
+
<ChangePasswordForm />
|
|
12
|
+
</main>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export default function AuthRouteLayout({ children }: { children: ReactNode }) {
|
|
4
|
+
return (
|
|
5
|
+
<div className="flex min-h-screen items-center justify-center p-4">
|
|
6
|
+
<div className="w-full max-w-md">{children}</div>
|
|
7
|
+
</div>
|
|
8
|
+
);
|
|
9
|
+
}
|
|
@@ -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,14 @@
|
|
|
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
|
+
<main className="space-y-3">
|
|
9
|
+
<h1 className="text-2xl font-semibold">{t("title")}</h1>
|
|
10
|
+
<p className="text-sm text-muted-foreground">{t("description")}</p>
|
|
11
|
+
<SignInForm />
|
|
12
|
+
</main>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import { useTranslations } from "next-intl";
|
|
3
|
+
import { AccountPanel } from "@/features/auth/components/account-panel";
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
5
|
+
|
|
6
|
+
export default function AccountPage() {
|
|
7
|
+
const t = useTranslations("auth.account");
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<main className="space-y-4">
|
|
11
|
+
<h1 className="text-2xl font-semibold">{t("title")}</h1>
|
|
12
|
+
<AccountPanel />
|
|
13
|
+
<Button asChild variant="outline">
|
|
14
|
+
<Link href="/sign-in">{t("goToSignIn")}</Link>
|
|
15
|
+
</Button>
|
|
16
|
+
</main>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useTranslations } from "next-intl";
|
|
2
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
3
|
+
|
|
4
|
+
export default function DashboardPage() {
|
|
5
|
+
const t = useTranslations("common.sidebar");
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<Card>
|
|
9
|
+
<CardHeader>
|
|
10
|
+
<CardTitle>{t("dashboard")}</CardTitle>
|
|
11
|
+
</CardHeader>
|
|
12
|
+
<CardContent className="text-sm text-muted-foreground">
|
|
13
|
+
Starter dashboard page. Add your own widgets and metrics here.
|
|
14
|
+
</CardContent>
|
|
15
|
+
</Card>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useTranslations } from "next-intl";
|
|
2
|
+
import { ExampleTable } from "@/example/components/example-table";
|
|
3
|
+
|
|
4
|
+
export default function ExamplePage() {
|
|
5
|
+
const t = useTranslations("example.page");
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<main className="space-y-4">
|
|
9
|
+
<h1 className="text-2xl font-semibold">{t("title")}</h1>
|
|
10
|
+
<ExampleTable />
|
|
11
|
+
</main>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -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,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
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { getRefreshCookieName, refreshCookieOptions } from "@/lib/auth/cookies";
|
|
2
|
+
import { fail, ok } from "@/lib/api/response";
|
|
3
|
+
import prisma from "@/lib/db/prisma";
|
|
4
|
+
|
|
5
|
+
const authBaseUrl =
|
|
6
|
+
process.env.BETTER_AUTH_URL ??
|
|
7
|
+
process.env.NEXT_PUBLIC_APP_URL ??
|
|
8
|
+
"http://localhost:3000";
|
|
9
|
+
|
|
10
|
+
export async function POST(request: Request) {
|
|
11
|
+
const payload = (await request.json().catch(() => ({}))) as {
|
|
12
|
+
username?: string;
|
|
13
|
+
password?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
if (!payload.username || !payload.password) {
|
|
17
|
+
return fail("BAD_REQUEST", "Username and password are required.", {
|
|
18
|
+
status: 400,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const signInResponse = await fetch(`${authBaseUrl}/api/auth/sign-in/username`, {
|
|
23
|
+
method: "POST",
|
|
24
|
+
headers: {
|
|
25
|
+
"Content-Type": "application/json",
|
|
26
|
+
},
|
|
27
|
+
body: JSON.stringify({
|
|
28
|
+
username: payload.username,
|
|
29
|
+
password: payload.password,
|
|
30
|
+
}),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (!signInResponse.ok) {
|
|
34
|
+
return fail("UNAUTHORIZED", "Invalid credentials.", {
|
|
35
|
+
status: signInResponse.status,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const cookieHeader = signInResponse.headers.get("set-cookie");
|
|
40
|
+
|
|
41
|
+
const tokenResponse = await fetch(`${authBaseUrl}/api/auth/token`, {
|
|
42
|
+
method: "GET",
|
|
43
|
+
headers: cookieHeader ? { cookie: cookieHeader } : undefined,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (!tokenResponse.ok) {
|
|
47
|
+
return fail("UPSTREAM_ERROR", "Unable to issue access token.", {
|
|
48
|
+
status: tokenResponse.status,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const tokenPayload = await tokenResponse.json();
|
|
53
|
+
|
|
54
|
+
const dbUser = await prisma.user.findUnique({
|
|
55
|
+
where: { username: payload.username },
|
|
56
|
+
select: { requirePasswordChange: true },
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const response = ok({
|
|
60
|
+
accessToken: tokenPayload.token,
|
|
61
|
+
requirePasswordChange: dbUser?.requirePasswordChange ?? false,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (cookieHeader) {
|
|
65
|
+
response.headers.set("set-cookie", cookieHeader);
|
|
66
|
+
}
|
|
67
|
+
response.cookies.set(getRefreshCookieName(), crypto.randomUUID(), refreshCookieOptions());
|
|
68
|
+
|
|
69
|
+
return response;
|
|
70
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { getRefreshCookieName, refreshCookieOptions } from "@/lib/auth/cookies";
|
|
2
|
+
import { fail, ok } from "@/lib/api/response";
|
|
3
|
+
|
|
4
|
+
const authBaseUrl =
|
|
5
|
+
process.env.BETTER_AUTH_URL ??
|
|
6
|
+
process.env.NEXT_PUBLIC_APP_URL ??
|
|
7
|
+
"http://localhost:3000";
|
|
8
|
+
|
|
9
|
+
export async function POST(request: Request) {
|
|
10
|
+
const incomingCookie = request.headers.get("cookie") ?? "";
|
|
11
|
+
const signOutResponse = await fetch(`${authBaseUrl}/api/auth/sign-out`, {
|
|
12
|
+
method: "POST",
|
|
13
|
+
headers: {
|
|
14
|
+
cookie: incomingCookie,
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const response = signOutResponse.ok
|
|
19
|
+
? ok({ loggedOut: true })
|
|
20
|
+
: fail("UPSTREAM_ERROR", "Failed to sign out.", {
|
|
21
|
+
status: signOutResponse.status,
|
|
22
|
+
});
|
|
23
|
+
response.cookies.set(getRefreshCookieName(), "", {
|
|
24
|
+
...refreshCookieOptions(),
|
|
25
|
+
maxAge: 0,
|
|
26
|
+
});
|
|
27
|
+
return response;
|
|
28
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { fail, ok } from "@/lib/api/response";
|
|
2
|
+
import { getSessionUser } from "@/lib/auth/rbac";
|
|
3
|
+
|
|
4
|
+
export async function GET(request: Request) {
|
|
5
|
+
const user = await getSessionUser(request.headers);
|
|
6
|
+
|
|
7
|
+
if (!user) {
|
|
8
|
+
return fail("UNAUTHORIZED", "Unauthorized", { status: 401 });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return ok({
|
|
12
|
+
user: {
|
|
13
|
+
id: user.id,
|
|
14
|
+
username: user.username,
|
|
15
|
+
displayUsername: user.displayUsername,
|
|
16
|
+
name: user.name,
|
|
17
|
+
email: user.email,
|
|
18
|
+
requirePasswordChange: user.requirePasswordChange,
|
|
19
|
+
role: user.role
|
|
20
|
+
? { id: user.role.id, name: user.role.name, level: user.role.level }
|
|
21
|
+
: null,
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { getRefreshCookieName } from "@/lib/auth/cookies";
|
|
2
|
+
import { fail, ok } from "@/lib/api/response";
|
|
3
|
+
|
|
4
|
+
const authBaseUrl =
|
|
5
|
+
process.env.BETTER_AUTH_URL ??
|
|
6
|
+
process.env.NEXT_PUBLIC_APP_URL ??
|
|
7
|
+
"http://localhost:3000";
|
|
8
|
+
|
|
9
|
+
export async function POST(request: Request) {
|
|
10
|
+
const incomingCookie = request.headers.get("cookie") ?? "";
|
|
11
|
+
if (!incomingCookie.includes(getRefreshCookieName())) {
|
|
12
|
+
return fail("UNAUTHORIZED", "Refresh cookie is missing.", { status: 401 });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const tokenResponse = await fetch(`${authBaseUrl}/api/auth/token`, {
|
|
16
|
+
method: "GET",
|
|
17
|
+
headers: {
|
|
18
|
+
cookie: incomingCookie,
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
if (!tokenResponse.ok) {
|
|
23
|
+
return fail("UNAUTHORIZED", "Failed to refresh access token.", {
|
|
24
|
+
status: tokenResponse.status,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const tokenPayload = await tokenResponse.json();
|
|
29
|
+
return ok({
|
|
30
|
+
accessToken: tokenPayload.token,
|
|
31
|
+
});
|
|
32
|
+
}
|