@thinhnguyencth1204/nextcli 0.6.1 → 0.8.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 (122) hide show
  1. package/README.md +68 -47
  2. package/dist/cli.js +1002 -753
  3. package/package.json +6 -2
  4. package/templates/{next-base/src/lib/axios-instance.ts → features/api/src/lib/api/axios.ts} +7 -2
  5. package/templates/{next-base/src/lib/api-response.ts → features/api/src/lib/api/response.ts} +1 -5
  6. package/templates/{next-base → features/auth}/src/app/(auth)/change-password/layout.tsx +1 -1
  7. package/templates/{next-base → features/auth}/src/app/(auth)/sign-in/layout.tsx +1 -1
  8. package/templates/{next-base → features/auth}/src/app/api/v1/auth/change-password/route.ts +3 -3
  9. package/templates/{next-base → features/auth}/src/app/api/v1/auth/login/route.ts +3 -3
  10. package/templates/{next-base → features/auth}/src/app/api/v1/auth/logout/route.ts +2 -2
  11. package/templates/{next-base → features/auth}/src/app/api/v1/auth/me/route.ts +2 -2
  12. package/templates/{next-base → features/auth}/src/app/api/v1/auth/refresh/route.ts +2 -2
  13. package/templates/{next-base → features/auth}/src/app/api/v1/users/[id]/route.ts +3 -3
  14. package/templates/{next-base → features/auth}/src/app/api/v1/users/route.ts +3 -3
  15. package/templates/{next-base → features/auth}/src/features/auth/components/account-panel.tsx +1 -1
  16. package/templates/{next-base → features/auth}/src/features/auth/components/change-password-form.tsx +1 -1
  17. package/templates/{next-base → features/auth}/src/features/auth/components/sign-in-form.tsx +2 -2
  18. package/templates/{next-base → features/auth}/src/features/users/services.ts +1 -1
  19. package/templates/{next-base → features/auth}/src/instrumentation.ts +1 -1
  20. package/templates/{next-base/src/lib → features/auth/src/lib/auth}/bootstrap.ts +2 -3
  21. package/templates/features/auth/src/lib/auth/index.ts +1 -0
  22. package/templates/{next-base/src/lib → features/auth/src/lib/auth}/rbac.ts +2 -5
  23. package/templates/{next-base/src/lib/auth.ts → features/auth/src/lib/auth/server.ts} +2 -1
  24. package/templates/{next-base → features/auth}/src/lib/constants.ts +3 -0
  25. package/templates/features/chat/src/app/api/v1/chat/route.ts +1 -1
  26. package/templates/features/chat/src/features/chat/api/use-chat-history.ts +1 -1
  27. package/templates/features/chat/src/features/chat/api/use-send-message.ts +1 -1
  28. package/templates/{next-base → features/dashboard}/src/app/(dashboard)/layout.tsx +1 -1
  29. package/templates/features/dashboard/src/app/page.tsx +5 -0
  30. package/templates/{next-base → features/dashboard}/src/components/layout/private/nav-user.tsx +1 -1
  31. package/templates/{next-base → features/database}/prisma/schema.prisma +0 -12
  32. package/templates/{next-base → features/database}/prisma.config.ts +2 -2
  33. package/templates/features/database/src/lib/prisma.ts +23 -0
  34. package/templates/{next-base → features/example}/src/app/api/v1/example/route.ts +2 -2
  35. package/templates/{next-base → features/example}/src/example/api/use-example.ts +1 -1
  36. package/templates/{next-base → features/example}/src/example/api/use-mutations.ts +1 -1
  37. package/templates/{next-base → features/example}/src/example/services.ts +1 -1
  38. package/templates/features/i18n/next.config.ts +17 -0
  39. package/templates/features/i18n/src/app/layout.tsx +42 -0
  40. package/templates/features/supabase/src/lib/supabase/rich-text-image-sync.ts +28 -0
  41. package/templates/next-base/.env +0 -14
  42. package/templates/next-base/.env.development +0 -14
  43. package/templates/next-base/.env.example +0 -14
  44. package/templates/next-base/PROJECT_STRUCTURE.md +43 -54
  45. package/templates/next-base/SETUP.md +12 -60
  46. package/templates/next-base/bun.lock +59 -397
  47. package/templates/next-base/next-env.d.ts +1 -1
  48. package/templates/next-base/next.config.ts +1 -4
  49. package/templates/next-base/nextcli.json +3 -3
  50. package/templates/next-base/package.json +6 -21
  51. package/templates/next-base/src/app/blog-demo/page.tsx +9 -0
  52. package/templates/next-base/src/app/globals.css +57 -0
  53. package/templates/next-base/src/app/layout.tsx +6 -14
  54. package/templates/next-base/src/app/page.tsx +25 -2
  55. package/templates/next-base/src/components/rich-text/adapters/textarea-field.tsx +50 -0
  56. package/templates/next-base/src/components/rich-text/client-only.tsx +23 -0
  57. package/templates/next-base/src/components/rich-text/editor-field.tsx +62 -0
  58. package/templates/next-base/src/components/rich-text/examples/blog-rich-text-demo.tsx +218 -0
  59. package/templates/next-base/src/components/rich-text/index.ts +11 -0
  60. package/templates/next-base/src/components/rich-text/lexical/extension.ts +37 -0
  61. package/templates/next-base/src/components/rich-text/lexical/nodes/image-node.tsx +187 -0
  62. package/templates/next-base/src/components/rich-text/lexical/plugins/image-plugin.tsx +40 -0
  63. package/templates/next-base/src/components/rich-text/lexical/plugins/initial-state-plugin.tsx +26 -0
  64. package/templates/next-base/src/components/rich-text/lexical/plugins/on-change-plugin.tsx +26 -0
  65. package/templates/next-base/src/components/rich-text/lexical/plugins/toolbar-plugin.tsx +190 -0
  66. package/templates/next-base/src/components/rich-text/lexical/rich-text-editor.tsx +121 -0
  67. package/templates/next-base/src/components/rich-text/lexical/theme.ts +18 -0
  68. package/templates/next-base/src/components/rich-text/rich-text-renderer.tsx +72 -0
  69. package/templates/next-base/src/components/rich-text/types.ts +60 -0
  70. package/templates/next-base/src/hooks/index.ts +1 -1
  71. package/templates/next-base/src/lib/rich-text/default-image-removal.ts +10 -0
  72. package/templates/next-base/src/lib/rich-text/image-urls.ts +41 -0
  73. package/templates/next-base/src/lib/rich-text/index.ts +12 -0
  74. package/templates/next-base/src/lib/rich-text/supabase-url.ts +67 -0
  75. package/templates/next-base/src/lib/rich-text/sync-removed-images.ts +48 -0
  76. package/templates/next-base/src/types/index.ts +0 -2
  77. package/templates/next-base/tsconfig.tsbuildinfo +1 -0
  78. package/templates/next-base/prisma/migrations/20260612000000_init/migration.sql +0 -104
  79. package/templates/next-base/prisma/migrations/migration_lock.toml +0 -3
  80. package/templates/next-base/src/app/(auth)/.gitkeep +0 -1
  81. /package/templates/{next-base → features/api}/src/components/providers/query-provider.tsx +0 -0
  82. /package/templates/{next-base/src/lib → features/api/src/lib/api}/token-store.ts +0 -0
  83. /package/templates/{next-base → features/auth}/messages/vi/auth.json +0 -0
  84. /package/templates/{next-base/prisma/migrations → features/auth/src/app/(auth)}/.gitkeep +0 -0
  85. /package/templates/{next-base → features/auth}/src/app/(auth)/change-password/page.tsx +0 -0
  86. /package/templates/{next-base → features/auth}/src/app/(auth)/layout.tsx +0 -0
  87. /package/templates/{next-base → features/auth}/src/app/(auth)/sign-in/page.tsx +0 -0
  88. /package/templates/{next-base → features/auth}/src/app/api/auth/[...all]/route.ts +0 -0
  89. /package/templates/{next-base → features/auth}/src/features/auth/validations.ts +0 -0
  90. /package/templates/{next-base → features/auth}/src/features/users/validations.ts +0 -0
  91. /package/templates/{next-base/src/lib/auth-client.ts → features/auth/src/lib/auth/client.ts} +0 -0
  92. /package/templates/{next-base/src/lib/auth-cookies.ts → features/auth/src/lib/auth/cookies.ts} +0 -0
  93. /package/templates/{next-base → features/dashboard}/src/app/(dashboard)/account/page.tsx +0 -0
  94. /package/templates/{next-base → features/dashboard}/src/app/(dashboard)/dashboard/page.tsx +0 -0
  95. /package/templates/{next-base → features/dashboard}/src/components/layout/private/app-sidebar.tsx +0 -0
  96. /package/templates/{next-base → features/dashboard}/src/components/layout/private/dashboard-layout.tsx +0 -0
  97. /package/templates/{next-base → features/dashboard}/src/components/layout/private/nav-sidebar.tsx +0 -0
  98. /package/templates/{next-base → features/dashboard}/src/components/ui/data-table/data-table-column-header.tsx +0 -0
  99. /package/templates/{next-base → features/dashboard}/src/components/ui/data-table/data-table-filter-list.tsx +0 -0
  100. /package/templates/{next-base → features/dashboard}/src/components/ui/data-table/data-table-pagination.tsx +0 -0
  101. /package/templates/{next-base → features/dashboard}/src/components/ui/data-table/data-table-skeleton.tsx +0 -0
  102. /package/templates/{next-base → features/dashboard}/src/components/ui/data-table/data-table-toolbar.tsx +0 -0
  103. /package/templates/{next-base → features/dashboard}/src/components/ui/data-table/data-table-view-options.tsx +0 -0
  104. /package/templates/{next-base → features/dashboard}/src/components/ui/data-table/data-table.tsx +0 -0
  105. /package/templates/{next-base → features/dashboard}/src/components/ui/sidebar.tsx +0 -0
  106. /package/templates/{next-base → features/dashboard}/src/data/sidebar-modules.ts +0 -0
  107. /package/templates/{next-base → features/dashboard}/src/hooks/table/use-data-table.ts +0 -0
  108. /package/templates/{next-base → features/dashboard}/src/hooks/use-mobile.ts +0 -0
  109. /package/templates/{next-base → features/dashboard}/src/types/data-table.ts +0 -0
  110. /package/templates/{next-base/src/lib → features/database/src/lib/db}/prisma.ts +0 -0
  111. /package/templates/{next-base → features/example}/messages/vi/example.json +0 -0
  112. /package/templates/{next-base → features/example}/src/app/(dashboard)/example/page.tsx +0 -0
  113. /package/templates/{next-base → features/example}/src/example/components/example-table.tsx +0 -0
  114. /package/templates/{next-base → features/example}/src/example/validations.ts +0 -0
  115. /package/templates/{next-base → features/i18n}/messages/vi/common.json +0 -0
  116. /package/templates/{next-base → features/i18n}/src/components/layout/private/locale-switcher.tsx +0 -0
  117. /package/templates/{next-base → features/i18n}/src/i18n/config.ts +0 -0
  118. /package/templates/{next-base → features/i18n}/src/i18n/namespaces.ts +0 -0
  119. /package/templates/{next-base → features/i18n}/src/i18n/request.ts +0 -0
  120. /package/templates/{next-base → features/supabase}/src/lib/supabase/client.ts +0 -0
  121. /package/templates/{next-base → features/supabase}/src/lib/supabase/storage-config.ts +0 -0
  122. /package/templates/{next-base → features/supabase}/src/lib/supabase/storage.ts +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thinhnguyencth1204/nextcli",
3
- "version": "0.6.1",
3
+ "version": "0.8.0",
4
4
  "description": "CLI scaffolder for outsourced Next.js projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,8 +15,11 @@
15
15
  "build": "tsup",
16
16
  "dev": "tsup --watch",
17
17
  "typecheck": "tsc --noEmit",
18
+ "test": "bun test tests",
19
+ "test:add-feature": "bun run build && bun test tests/core",
20
+ "test:rich-text": "bun test tests/templates/next-base",
18
21
  "smoke": "node dist/cli.js --help",
19
- "smoke:full": "bun run scripts/pre-publish-smoke.ts",
22
+ "smoke:full": "bun run build && bun run test && bun run scripts/pre-publish-smoke.ts",
20
23
  "prepublishOnly": "npm run build"
21
24
  },
22
25
  "keywords": [
@@ -38,6 +41,7 @@
38
41
  },
39
42
  "devDependencies": {
40
43
  "@types/node": "^22.15.29",
44
+ "lexical": "^0.45.0",
41
45
  "tsup": "^8.1.0",
42
46
  "typescript": "^5.5.4"
43
47
  }
@@ -1,5 +1,9 @@
1
1
  import axios from "axios";
2
- import { clearAccessToken, getAccessToken, setAccessToken } from "@/lib/token-store";
2
+ import {
3
+ clearAccessToken,
4
+ getAccessToken,
5
+ setAccessToken,
6
+ } from "@/lib/api/token-store";
3
7
  import type { ApiErrorResponse, ApiSuccess } from "@/types";
4
8
 
5
9
  const baseURL = process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000";
@@ -48,7 +52,8 @@ async function refreshAccessToken(): Promise<string | null> {
48
52
  const response = await publicApi.post("/api/v1/auth/refresh", null, {
49
53
  withCredentials: true,
50
54
  });
51
- const token = (response.data as ApiSuccess<{ accessToken: string }>).data?.accessToken;
55
+ const token = (response.data as ApiSuccess<{ accessToken: string }>).data
56
+ ?.accessToken;
52
57
  if (!token) {
53
58
  clearAccessToken();
54
59
  return null;
@@ -30,11 +30,7 @@ export function ok<T>(data: T, init: SuccessInit = {}) {
30
30
  return NextResponse.json(payload, { status: init.status ?? 200 });
31
31
  }
32
32
 
33
- export function fail(
34
- code: ErrorCode,
35
- message: string,
36
- init: FailInit = {},
37
- ) {
33
+ export function fail(code: ErrorCode, message: string, init: FailInit = {}) {
38
34
  const payload: ApiErrorResponse = {
39
35
  success: false,
40
36
  error: {
@@ -1,6 +1,6 @@
1
1
  import type { ReactNode } from "react";
2
2
  import { redirect } from "next/navigation";
3
- import { getSessionUser } from "@/lib/rbac";
3
+ import { getSessionUser } from "@/lib/auth/rbac";
4
4
 
5
5
  export default async function ChangePasswordLayout({
6
6
  children,
@@ -1,6 +1,6 @@
1
1
  import type { ReactNode } from "react";
2
2
  import { redirect } from "next/navigation";
3
- import { getSessionUser } from "@/lib/rbac";
3
+ import { getSessionUser } from "@/lib/auth/rbac";
4
4
 
5
5
  export default async function SignInLayout({ children }: { children: ReactNode }) {
6
6
  const user = await getSessionUser();
@@ -1,8 +1,8 @@
1
1
  import { changePasswordSchema } from "@/features/auth/validations";
2
- import { fail, ok } from "@/lib/api-response";
2
+ import { fail, ok } from "@/lib/api/response";
3
3
  import { auth } from "@/lib/auth";
4
- import { getSessionUser } from "@/lib/rbac";
5
- import prisma from "@/lib/prisma";
4
+ import { getSessionUser } from "@/lib/auth/rbac";
5
+ import prisma from "@/lib/db/prisma";
6
6
 
7
7
  const authBaseUrl =
8
8
  process.env.BETTER_AUTH_URL ??
@@ -1,6 +1,6 @@
1
- import { getRefreshCookieName, refreshCookieOptions } from "@/lib/auth-cookies";
2
- import { fail, ok } from "@/lib/api-response";
3
- import prisma from "@/lib/prisma";
1
+ import { getRefreshCookieName, refreshCookieOptions } from "@/lib/auth/cookies";
2
+ import { fail, ok } from "@/lib/api/response";
3
+ import prisma from "@/lib/db/prisma";
4
4
 
5
5
  const authBaseUrl =
6
6
  process.env.BETTER_AUTH_URL ??
@@ -1,5 +1,5 @@
1
- import { getRefreshCookieName, refreshCookieOptions } from "@/lib/auth-cookies";
2
- import { fail, ok } from "@/lib/api-response";
1
+ import { getRefreshCookieName, refreshCookieOptions } from "@/lib/auth/cookies";
2
+ import { fail, ok } from "@/lib/api/response";
3
3
 
4
4
  const authBaseUrl =
5
5
  process.env.BETTER_AUTH_URL ??
@@ -1,5 +1,5 @@
1
- import { fail, ok } from "@/lib/api-response";
2
- import { getSessionUser } from "@/lib/rbac";
1
+ import { fail, ok } from "@/lib/api/response";
2
+ import { getSessionUser } from "@/lib/auth/rbac";
3
3
 
4
4
  export async function GET(request: Request) {
5
5
  const user = await getSessionUser(request.headers);
@@ -1,5 +1,5 @@
1
- import { getRefreshCookieName } from "@/lib/auth-cookies";
2
- import { fail, ok } from "@/lib/api-response";
1
+ import { getRefreshCookieName } from "@/lib/auth/cookies";
2
+ import { fail, ok } from "@/lib/api/response";
3
3
 
4
4
  const authBaseUrl =
5
5
  process.env.BETTER_AUTH_URL ??
@@ -4,14 +4,14 @@ import {
4
4
  getUserByIdForActor,
5
5
  updateUserRecord,
6
6
  } from "@/features/users/services";
7
- import { fail, ok } from "@/lib/api-response";
7
+ import { fail, ok } from "@/lib/api/response";
8
8
  import {
9
9
  canActOnUser,
10
10
  canAssignRole,
11
11
  getSessionUser,
12
12
  isSuperAdmin,
13
- } from "@/lib/rbac";
14
- import prisma from "@/lib/prisma";
13
+ } from "@/lib/auth/rbac";
14
+ import prisma from "@/lib/db/prisma";
15
15
 
16
16
  type RouteContext = {
17
17
  params: Promise<{ id: string }>;
@@ -3,9 +3,9 @@ import {
3
3
  createUserRecord,
4
4
  listUsersForActor,
5
5
  } from "@/features/users/services";
6
- import { fail, ok } from "@/lib/api-response";
7
- import { canAssignRole, getSessionUser } from "@/lib/rbac";
8
- import prisma from "@/lib/prisma";
6
+ import { fail, ok } from "@/lib/api/response";
7
+ import { canAssignRole, getSessionUser } from "@/lib/auth/rbac";
8
+ import prisma from "@/lib/db/prisma";
9
9
 
10
10
  export async function GET(request: Request) {
11
11
  const actor = await getSessionUser(request.headers);
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { useEffect, useState } from "react";
4
4
  import { useTranslations } from "next-intl";
5
- import { protectedApi } from "@/lib/axios-instance";
5
+ import { protectedApi } from "@/lib/api/axios";
6
6
  import type { ApiSuccess } from "@/types";
7
7
  import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
8
8
 
@@ -5,7 +5,7 @@ import type { FormEvent } from "react";
5
5
  import { useRouter } from "next/navigation";
6
6
  import { useTranslations } from "next-intl";
7
7
  import { toast } from "sonner";
8
- import { protectedApi } from "@/lib/axios-instance";
8
+ import { protectedApi } from "@/lib/api/axios";
9
9
  import { changePasswordSchema } from "@/features/auth/validations";
10
10
  import { Button } from "@/components/ui/button";
11
11
  import { Card, CardContent } from "@/components/ui/card";
@@ -5,8 +5,8 @@ import type { FormEvent } from "react";
5
5
  import { useRouter } from "next/navigation";
6
6
  import { useTranslations } from "next-intl";
7
7
  import { toast } from "sonner";
8
- import { publicApi } from "@/lib/axios-instance";
9
- import { setAccessToken } from "@/lib/token-store";
8
+ import { publicApi } from "@/lib/api/axios";
9
+ import { setAccessToken } from "@/lib/api/token-store";
10
10
  import { signInSchema } from "@/features/auth/validations";
11
11
  import type { ApiSuccess } from "@/types";
12
12
  import { Button } from "@/components/ui/button";
@@ -1,7 +1,7 @@
1
1
  import type { Role, User } from "@prisma/client";
2
2
  import { INTERNAL_EMAIL_DOMAIN, SUPER_ADMIN_USERNAME } from "@/lib/constants";
3
3
  import { auth } from "@/lib/auth";
4
- import prisma from "@/lib/prisma";
4
+ import prisma from "@/lib/db/prisma";
5
5
  import type { CreateUserInput, UpdateUserInput } from "@/features/users/validations";
6
6
 
7
7
  export type UserWithRole = User & { role: Role | null };
@@ -4,7 +4,7 @@ export async function register() {
4
4
  }
5
5
 
6
6
  try {
7
- const { runBootstrap } = await import("@/lib/bootstrap");
7
+ const { runBootstrap } = await import("@/lib/auth/bootstrap");
8
8
  await runBootstrap();
9
9
  } catch (error) {
10
10
  const message =
@@ -2,13 +2,12 @@ import type { PrismaClient } from "@prisma/client";
2
2
  import {
3
3
  ADMIN_ROLE_LEVEL,
4
4
  ADMIN_ROLE_NAME,
5
+ DEFAULT_ADMIN_PASSWORD,
5
6
  INTERNAL_EMAIL_DOMAIN,
6
7
  SUPER_ADMIN_USERNAME,
7
8
  } from "@/lib/constants";
8
9
  import { auth } from "@/lib/auth";
9
- import prisma from "@/lib/prisma";
10
-
11
- const DEFAULT_ADMIN_PASSWORD = "admin";
10
+ import prisma from "@/lib/db/prisma";
12
11
 
13
12
  function hasRoleModel(client: PrismaClient): boolean {
14
13
  const delegate = (
@@ -0,0 +1 @@
1
+ export { auth } from "./server";
@@ -1,7 +1,7 @@
1
1
  import { headers } from "next/headers";
2
2
  import type { Role, User } from "@prisma/client";
3
3
  import { auth } from "@/lib/auth";
4
- import prisma from "@/lib/prisma";
4
+ import prisma from "@/lib/db/prisma";
5
5
  import { SUPER_ADMIN_USERNAME } from "@/lib/constants";
6
6
 
7
7
  export type SessionUser = User & { role: Role | null };
@@ -27,10 +27,7 @@ export function isSuperAdmin(user: Pick<User, "username">): boolean {
27
27
  return user.username === SUPER_ADMIN_USERNAME;
28
28
  }
29
29
 
30
- export function canActOnUser(
31
- actor: SessionUser,
32
- target: SessionUser,
33
- ): boolean {
30
+ export function canActOnUser(actor: SessionUser, target: SessionUser): boolean {
34
31
  if (isSuperAdmin(target)) {
35
32
  return false;
36
33
  }
@@ -1,7 +1,7 @@
1
1
  import { betterAuth } from "better-auth/minimal";
2
2
  import { prismaAdapter } from "better-auth/adapters/prisma";
3
3
  import { jwt, username } from "better-auth/plugins";
4
- import prisma from "@/lib/prisma";
4
+ import prisma from "@/lib/db/prisma";
5
5
 
6
6
  const socialProviders = {
7
7
  // AUTO_GENERATED_AUTH_PROVIDERS_START
@@ -15,6 +15,7 @@ export const auth = betterAuth({
15
15
  plugins: [jwt(), username()],
16
16
  emailAndPassword: {
17
17
  enabled: true,
18
+ minPasswordLength: 8,
18
19
  },
19
20
  socialProviders,
20
21
  });
@@ -1,6 +1,9 @@
1
1
  /** Built-in super-admin account username — protected from RBAC mutations. */
2
2
  export const SUPER_ADMIN_USERNAME = "admin";
3
3
 
4
+ /** Dev-only bootstrap password (≥8 chars for Better Auth). Change on first login. */
5
+ export const DEFAULT_ADMIN_PASSWORD = "admin1234";
6
+
4
7
  export const ADMIN_ROLE_NAME = "admin";
5
8
  export const ADMIN_ROLE_LEVEL = 100;
6
9
 
@@ -1,4 +1,4 @@
1
- import { fail, ok } from "@/lib/api-response";
1
+ import { fail, ok } from "@/lib/api/response";
2
2
 
3
3
  export async function GET(request: Request) {
4
4
  const { searchParams } = new URL(request.url);
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { useQuery } from "@tanstack/react-query";
4
- import { api } from "@/lib/axios-instance";
4
+ import { api } from "@/lib/api/axios";
5
5
  import type { ApiSuccess } from "@/types";
6
6
 
7
7
  export function useChatHistory(conversationId: string) {
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { useMutation, useQueryClient } from "@tanstack/react-query";
4
- import { api } from "@/lib/axios-instance";
4
+ import { api } from "@/lib/api/axios";
5
5
  import type { ApiSuccess } from "@/types";
6
6
 
7
7
  type SendMessageInput = {
@@ -1,7 +1,7 @@
1
1
  import type { ReactNode } from "react";
2
2
  import { redirect } from "next/navigation";
3
3
  import { DashboardLayout } from "@/components/layout/private/dashboard-layout";
4
- import { getSessionUser } from "@/lib/rbac";
4
+ import { getSessionUser } from "@/lib/auth/rbac";
5
5
 
6
6
  export default async function DashboardRouteLayout({
7
7
  children,
@@ -0,0 +1,5 @@
1
+ import { redirect } from "next/navigation";
2
+
3
+ export default function HomePage() {
4
+ redirect("/dashboard");
5
+ }
@@ -3,7 +3,7 @@
3
3
  import { LogOut, Monitor, Moon, Sun } from "lucide-react";
4
4
  import { useTheme } from "next-themes";
5
5
  import { useTranslations } from "next-intl";
6
- import { authClient } from "@/lib/auth-client";
6
+ import { authClient } from "@/lib/auth/client";
7
7
  import { Avatar, AvatarFallback } from "@/components/ui/avatar";
8
8
  import { Button } from "@/components/ui/button";
9
9
  import { ScrollArea } from "@/components/ui/scroll-area";
@@ -31,18 +31,6 @@ model User {
31
31
  sessions Session[]
32
32
  accounts Account[]
33
33
  }
34
-
35
- // Starter demo table for template onboarding only.
36
- // Rename/remove this `Example` model before production migration.
37
- // Keep this block under review to avoid creating unwanted prod tables.
38
- model Example {
39
- id String @id @default(cuid())
40
- name String
41
- description String?
42
- createdAt DateTime @default(now())
43
- updatedAt DateTime @updatedAt
44
- }
45
-
46
34
  model Session {
47
35
  id String @id @default(cuid())
48
36
  token String @unique
@@ -2,7 +2,7 @@ import "dotenv/config";
2
2
  import { defineConfig, env } from "prisma/config";
3
3
 
4
4
  type Env = {
5
- DATABASE_URL: string;
5
+ DIRECT_URL: string;
6
6
  };
7
7
 
8
8
  export default defineConfig({
@@ -11,6 +11,6 @@ export default defineConfig({
11
11
  path: "prisma/migrations",
12
12
  },
13
13
  datasource: {
14
- url: env<Env>("DATABASE_URL"),
14
+ url: env<Env>("DIRECT_URL"),
15
15
  },
16
16
  });
@@ -0,0 +1,23 @@
1
+ import { PrismaPg } from "@prisma/adapter-pg";
2
+ import { PrismaClient } from "@prisma/client";
3
+ import { Pool } from "pg";
4
+
5
+ declare global {
6
+ var prisma: PrismaClient | undefined;
7
+ }
8
+
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 });
18
+
19
+ if (process.env.NODE_ENV !== "production") {
20
+ global.prisma = prisma;
21
+ }
22
+
23
+ export default prisma;
@@ -1,7 +1,7 @@
1
1
  import { createExampleSchema } from "@/example/validations";
2
2
  import { listExamples } from "@/example/services";
3
- import { fail, ok } from "@/lib/api-response";
4
- import prisma from "@/lib/prisma";
3
+ import { fail, ok } from "@/lib/api/response";
4
+ import prisma from "@/lib/db/prisma";
5
5
 
6
6
  export async function GET() {
7
7
  try {
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { useQuery } from "@tanstack/react-query";
4
- import { api } from "@/lib/axios-instance";
4
+ import { api } from "@/lib/api/axios";
5
5
  import type { ApiSuccess } from "@/types";
6
6
 
7
7
  type ExampleItem = {
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { useMutation, useQueryClient } from "@tanstack/react-query";
4
- import { api } from "@/lib/axios-instance";
4
+ import { api } from "@/lib/api/axios";
5
5
  import type { CreateExampleInput } from "@/example/validations";
6
6
  import type { ApiSuccess } from "@/types";
7
7
 
@@ -1,4 +1,4 @@
1
- import prisma from "@/lib/prisma";
1
+ import prisma from "@/lib/db/prisma";
2
2
 
3
3
  export async function listExamples() {
4
4
  return prisma.example.findMany({
@@ -0,0 +1,17 @@
1
+ import type { NextConfig } from "next";
2
+ import createNextIntlPlugin from "next-intl/plugin";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const rootDir = path.dirname(fileURLToPath(import.meta.url));
7
+
8
+ const nextConfig: NextConfig = {
9
+ reactStrictMode: true,
10
+ turbopack: {
11
+ root: rootDir,
12
+ },
13
+ };
14
+
15
+ const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts");
16
+
17
+ export default withNextIntl(nextConfig);
@@ -0,0 +1,42 @@
1
+ import type { Metadata } from "next";
2
+ import { Be_Vietnam_Pro } from "next/font/google";
3
+ import { getLocale, getMessages } from "next-intl/server";
4
+ import { NextIntlClientProvider } from "next-intl";
5
+ import { Toaster } from "sonner";
6
+ import type { ReactNode } from "react";
7
+ import { QueryProvider } from "@/components/providers/query-provider";
8
+ import { ThemeProvider } from "@/components/providers/theme-provider";
9
+ import { branding } from "@/config/branding";
10
+ import "@/app/globals.css";
11
+
12
+ const beVietnamPro = Be_Vietnam_Pro({
13
+ subsets: ["latin", "vietnamese"],
14
+ weight: ["400", "500", "600", "700"],
15
+ });
16
+
17
+ export const metadata: Metadata = {
18
+ title: branding.projectName,
19
+ description: branding.description,
20
+ };
21
+
22
+ export default async function RootLayout({
23
+ children,
24
+ }: Readonly<{
25
+ children: ReactNode;
26
+ }>) {
27
+ const locale = await getLocale();
28
+ const messages = await getMessages();
29
+
30
+ return (
31
+ <html lang={locale} suppressHydrationWarning>
32
+ <body className={beVietnamPro.className}>
33
+ <NextIntlClientProvider locale={locale} messages={messages}>
34
+ <ThemeProvider attribute="class" defaultTheme="light" enableSystem>
35
+ <QueryProvider>{children}</QueryProvider>
36
+ <Toaster richColors position="top-right" />
37
+ </ThemeProvider>
38
+ </NextIntlClientProvider>
39
+ </body>
40
+ </html>
41
+ );
42
+ }
@@ -0,0 +1,28 @@
1
+ import { removeResource } from "@/lib/supabase/storage";
2
+ import {
3
+ isSupabaseManagedImageUrl,
4
+ syncRemovedRichTextImages,
5
+ type RemoveManagedImageFn,
6
+ } from "@/lib/rich-text";
7
+ import type { SerializedEditorState } from "lexical";
8
+
9
+ export const removeSupabaseManagedImage: RemoveManagedImageFn = async (
10
+ _url,
11
+ ref,
12
+ ) => {
13
+ await removeResource(ref.path, ref.bucket);
14
+ };
15
+
16
+ /**
17
+ * Deletes Supabase storage objects for images removed from rich text content.
18
+ * Only URLs belonging to this project's configured Supabase bucket are removed.
19
+ */
20
+ export async function syncRemovedSupabaseRichTextImages(
21
+ previous: SerializedEditorState | null | undefined,
22
+ next: SerializedEditorState | null | undefined,
23
+ ) {
24
+ return syncRemovedRichTextImages(previous, next, {
25
+ isManagedUrl: isSupabaseManagedImageUrl,
26
+ removeManagedImage: removeSupabaseManagedImage,
27
+ });
28
+ }
@@ -1,16 +1,2 @@
1
- # --- Database (Supabase) ---
2
- DATABASE_URL="postgresql://postgres.your-project-ref:your-supabase-db-password@aws-0-us-east-1.pooler.supabase.com:6543/postgres?pgbouncer=true"
3
- DIRECT_URL="postgresql://postgres.your-project-ref:your-supabase-db-password@aws-0-us-east-1.pooler.supabase.com:5432/postgres"
4
- NEXT_PUBLIC_SUPABASE_URL=""
5
- # Public/browser-safe anon key from Supabase Project Settings -> API -> anon public
6
- NEXT_PUBLIC_SUPABASE_ANON_KEY=""
7
- # Storage bucket name used by the app (default scaffold bucket name: public)
8
- NEXT_PUBLIC_SUPABASE_STORAGE_BUCKET="public"
9
-
10
- # --- Auth ---
11
- BETTER_AUTH_SECRET="__BETTER_AUTH_SECRET__"
12
- BETTER_AUTH_URL="http://localhost:3000"
13
-
14
1
  # --- App ---
15
2
  NEXT_PUBLIC_APP_URL="http://localhost:3000"
16
- NEXT_PUBLIC_DEFAULT_LOCALE="vi"
@@ -1,16 +1,2 @@
1
- # --- Database (Supabase) ---
2
- DATABASE_URL="postgresql://postgres.your-project-ref:your-supabase-db-password@aws-0-us-east-1.pooler.supabase.com:6543/postgres?pgbouncer=true"
3
- DIRECT_URL="postgresql://postgres.your-project-ref:your-supabase-db-password@aws-0-us-east-1.pooler.supabase.com:5432/postgres"
4
- NEXT_PUBLIC_SUPABASE_URL=""
5
- # Public/browser-safe anon key from Supabase Project Settings -> API -> anon public
6
- NEXT_PUBLIC_SUPABASE_ANON_KEY=""
7
- # Storage bucket name used by the app (default scaffold bucket name: public)
8
- NEXT_PUBLIC_SUPABASE_STORAGE_BUCKET="public"
9
-
10
- # --- Auth ---
11
- BETTER_AUTH_SECRET="__BETTER_AUTH_SECRET__"
12
- BETTER_AUTH_URL="http://localhost:3000"
13
-
14
1
  # --- App ---
15
2
  NEXT_PUBLIC_APP_URL="http://localhost:3000"
16
- NEXT_PUBLIC_DEFAULT_LOCALE="vi"
@@ -1,16 +1,2 @@
1
- # --- Database (Supabase) ---
2
- DATABASE_URL="postgresql://postgres.your-project-ref:your-supabase-db-password@aws-0-us-east-1.pooler.supabase.com:6543/postgres?pgbouncer=true"
3
- DIRECT_URL="postgresql://postgres.your-project-ref:your-supabase-db-password@aws-0-us-east-1.pooler.supabase.com:5432/postgres"
4
- NEXT_PUBLIC_SUPABASE_URL=""
5
- # Public/browser-safe anon key from Supabase Project Settings -> API -> anon public
6
- NEXT_PUBLIC_SUPABASE_ANON_KEY=""
7
- # Storage bucket name used by the app (default scaffold bucket name: public)
8
- NEXT_PUBLIC_SUPABASE_STORAGE_BUCKET="public"
9
-
10
- # --- Auth ---
11
- BETTER_AUTH_SECRET="your-32-plus-char-random-secret"
12
- BETTER_AUTH_URL="http://localhost:3000"
13
-
14
1
  # --- App ---
15
2
  NEXT_PUBLIC_APP_URL="http://localhost:3000"
16
- NEXT_PUBLIC_DEFAULT_LOCALE="vi"