@thinhnguyencth1204/nextcli 0.9.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/cli.js +3 -3
  2. package/package.json +1 -1
  3. package/templates/features/api/src/lib/api/axios.ts +1 -90
  4. package/templates/features/auth/messages/vi/auth.json +2 -1
  5. package/templates/features/auth/src/app/(auth)/change-password/page.tsx +5 -4
  6. package/templates/features/auth/src/app/(auth)/layout.tsx +2 -5
  7. package/templates/features/auth/src/app/(auth)/sign-in/page.tsx +5 -4
  8. package/templates/features/auth/src/app/api/v1/auth/login/route.ts +24 -29
  9. package/templates/features/auth/src/app/api/v1/auth/logout/route.ts +0 -5
  10. package/templates/features/auth/src/components/layout/auth/auth-shell.tsx +24 -0
  11. package/templates/features/auth/src/features/auth/components/account-panel.tsx +15 -3
  12. package/templates/features/auth/src/features/auth/components/change-password-form.tsx +27 -30
  13. package/templates/features/auth/src/features/auth/components/sign-in-form.tsx +33 -42
  14. package/templates/features/auth/src/lib/auth/client.ts +2 -2
  15. package/templates/features/auth/src/lib/auth/server.ts +2 -2
  16. package/templates/features/dashboard/src/app/(dashboard)/account/page.tsx +9 -7
  17. package/templates/features/dashboard/src/app/(dashboard)/dashboard/page.tsx +24 -10
  18. package/templates/features/dashboard/src/components/layout/private/app-sidebar.tsx +1 -13
  19. package/templates/features/dashboard/src/components/layout/private/dashboard-layout.tsx +31 -22
  20. package/templates/features/dashboard/src/components/layout/private/page-shell.tsx +40 -0
  21. package/templates/features/database/prisma/schema.prisma +1 -0
  22. package/templates/features/example/messages/vi/example.json +11 -1
  23. package/templates/features/example/src/app/(dashboard)/example/page.tsx +92 -3
  24. package/templates/features/example/src/example/components/example-table.tsx +15 -2
  25. package/templates/next-base/bun.lock +407 -0
  26. package/templates/next-base/messages/vi/auth.json +2 -1
  27. package/templates/next-base/messages/vi/common.json +19 -0
  28. package/templates/next-base/messages/vi/example.json +11 -1
  29. package/templates/next-base/next-env.d.ts +1 -1
  30. package/templates/next-base/prisma/schema.prisma +1 -0
  31. package/templates/next-base/src/app/(auth)/change-password/page.tsx +5 -4
  32. package/templates/next-base/src/app/(auth)/layout.tsx +2 -5
  33. package/templates/next-base/src/app/(auth)/sign-in/page.tsx +5 -4
  34. package/templates/next-base/src/app/(dashboard)/account/page.tsx +9 -7
  35. package/templates/next-base/src/app/(dashboard)/dashboard/page.tsx +24 -10
  36. package/templates/next-base/src/app/(dashboard)/example/page.tsx +92 -3
  37. package/templates/next-base/src/app/api/v1/auth/login/route.ts +24 -29
  38. package/templates/next-base/src/app/api/v1/auth/logout/route.ts +0 -5
  39. package/templates/next-base/src/components/branding/logo.tsx +27 -4
  40. package/templates/next-base/src/components/layout/auth/auth-shell.tsx +24 -0
  41. package/templates/next-base/src/components/layout/private/app-sidebar.tsx +1 -13
  42. package/templates/next-base/src/components/layout/private/dashboard-layout.tsx +31 -22
  43. package/templates/next-base/src/components/layout/private/page-shell.tsx +40 -0
  44. package/templates/next-base/src/example/components/example-table.tsx +15 -2
  45. package/templates/next-base/src/features/auth/components/account-panel.tsx +15 -3
  46. package/templates/next-base/src/features/auth/components/change-password-form.tsx +27 -30
  47. package/templates/next-base/src/features/auth/components/sign-in-form.tsx +33 -42
  48. package/templates/next-base/src/lib/api/axios.ts +1 -90
  49. package/templates/next-base/src/lib/auth/client.ts +2 -2
  50. package/templates/next-base/src/lib/auth/server.ts +2 -2
  51. package/templates/features/api/src/lib/api/token-store.ts +0 -13
  52. package/templates/features/auth/src/app/api/v1/auth/refresh/route.ts +0 -32
  53. package/templates/features/auth/src/lib/auth/cookies.ts +0 -15
  54. package/templates/next-base/src/app/api/v1/auth/refresh/route.ts +0 -32
  55. package/templates/next-base/src/lib/api/token-store.ts +0 -13
  56. package/templates/next-base/src/lib/auth/cookies.ts +0 -15
@@ -49,17 +49,29 @@ export function AccountPanel() {
49
49
  }, []);
50
50
 
51
51
  if (loading) {
52
- return <p>{t("loading")}</p>;
52
+ return (
53
+ <Card className="max-w-xl">
54
+ <CardContent className="py-8 text-sm text-muted-foreground">
55
+ {t("loading")}
56
+ </CardContent>
57
+ </Card>
58
+ );
53
59
  }
54
60
 
55
61
  if (!session?.user) {
56
- return <p>{t("noSession")}</p>;
62
+ return (
63
+ <Card className="max-w-xl">
64
+ <CardContent className="py-8 text-sm text-muted-foreground">
65
+ {t("noSession")}
66
+ </CardContent>
67
+ </Card>
68
+ );
57
69
  }
58
70
 
59
71
  return (
60
72
  <Card className="max-w-xl">
61
73
  <CardHeader>
62
- <CardTitle>{t("title")}</CardTitle>
74
+ <CardTitle>{t("panelTitle")}</CardTitle>
63
75
  </CardHeader>
64
76
  <CardContent className="space-y-2 text-sm">
65
77
  <p>
@@ -8,7 +8,6 @@ import { toast } from "sonner";
8
8
  import { protectedApi } from "@/lib/api/axios";
9
9
  import { changePasswordSchema } from "@/features/auth/validations";
10
10
  import { Button } from "@/components/ui/button";
11
- import { Card, CardContent } from "@/components/ui/card";
12
11
  import { Input } from "@/components/ui/input";
13
12
  import { Label } from "@/components/ui/label";
14
13
 
@@ -49,34 +48,32 @@ export function ChangePasswordForm() {
49
48
  };
50
49
 
51
50
  return (
52
- <Card>
53
- <CardContent className="pt-6">
54
- <form onSubmit={onSubmit} className="grid gap-4">
55
- <div className="grid gap-2">
56
- <Label htmlFor="currentPassword">{t("currentPassword")}</Label>
57
- <Input
58
- id="currentPassword"
59
- type="password"
60
- value={currentPassword}
61
- onChange={(event) => setCurrentPassword(event.target.value)}
62
- required
63
- />
64
- </div>
65
- <div className="grid gap-2">
66
- <Label htmlFor="newPassword">{t("newPassword")}</Label>
67
- <Input
68
- id="newPassword"
69
- type="password"
70
- value={newPassword}
71
- onChange={(event) => setNewPassword(event.target.value)}
72
- required
73
- />
74
- </div>
75
- <Button type="submit" disabled={isSubmitting}>
76
- {isSubmitting ? t("submitting") : t("submit")}
77
- </Button>
78
- </form>
79
- </CardContent>
80
- </Card>
51
+ <form onSubmit={onSubmit} className="space-y-6">
52
+ <div className="space-y-2">
53
+ <Label htmlFor="currentPassword">{t("currentPassword")}</Label>
54
+ <Input
55
+ id="currentPassword"
56
+ type="password"
57
+ value={currentPassword}
58
+ onChange={(event) => setCurrentPassword(event.target.value)}
59
+ className="h-12"
60
+ required
61
+ />
62
+ </div>
63
+ <div className="space-y-2">
64
+ <Label htmlFor="newPassword">{t("newPassword")}</Label>
65
+ <Input
66
+ id="newPassword"
67
+ type="password"
68
+ value={newPassword}
69
+ onChange={(event) => setNewPassword(event.target.value)}
70
+ className="h-12"
71
+ required
72
+ />
73
+ </div>
74
+ <Button type="submit" className="h-12 w-full" disabled={isSubmitting}>
75
+ {isSubmitting ? t("submitting") : t("submit")}
76
+ </Button>
77
+ </form>
81
78
  );
82
79
  }
@@ -6,16 +6,13 @@ import { useRouter } from "next/navigation";
6
6
  import { useTranslations } from "next-intl";
7
7
  import { toast } from "sonner";
8
8
  import { publicApi } from "@/lib/api/axios";
9
- import { setAccessToken } from "@/lib/api/token-store";
10
9
  import { signInSchema } from "@/features/auth/validations";
11
10
  import type { ApiSuccess } from "@/types";
12
11
  import { Button } from "@/components/ui/button";
13
- import { Card, CardContent } from "@/components/ui/card";
14
12
  import { Input } from "@/components/ui/input";
15
13
  import { Label } from "@/components/ui/label";
16
14
 
17
15
  type LoginResponse = {
18
- accessToken: string;
19
16
  requirePasswordChange: boolean;
20
17
  };
21
18
 
@@ -41,14 +38,10 @@ export function SignInForm() {
41
38
  withCredentials: true,
42
39
  });
43
40
  const payload = (response.data as ApiSuccess<LoginResponse>).data;
44
- if (!payload?.accessToken) {
45
- toast.error(t("missingAccessToken"));
46
- return;
47
- }
48
-
49
- setAccessToken(payload.accessToken);
50
41
  toast.success(t("success"));
51
- router.push(payload.requirePasswordChange ? "/change-password" : "/dashboard");
42
+ router.push(
43
+ payload.requirePasswordChange ? "/change-password" : "/dashboard",
44
+ );
52
45
  router.refresh();
53
46
  } catch (error) {
54
47
  const message = error instanceof Error ? error.message : t("failed");
@@ -59,37 +52,35 @@ export function SignInForm() {
59
52
  };
60
53
 
61
54
  return (
62
- <Card>
63
- <CardContent className="pt-6">
64
- <form onSubmit={onSubmit} className="grid gap-4">
65
- <div className="grid gap-2">
66
- <Label htmlFor="username">{t("username")}</Label>
67
- <Input
68
- id="username"
69
- value={username}
70
- onChange={(event) => setUsername(event.target.value)}
71
- placeholder={t("usernamePlaceholder")}
72
- autoComplete="username"
73
- required
74
- />
75
- </div>
76
- <div className="grid gap-2">
77
- <Label htmlFor="password">{t("password")}</Label>
78
- <Input
79
- id="password"
80
- type="password"
81
- value={password}
82
- onChange={(event) => setPassword(event.target.value)}
83
- placeholder={t("passwordPlaceholder")}
84
- autoComplete="current-password"
85
- required
86
- />
87
- </div>
88
- <Button type="submit" disabled={isSubmitting}>
89
- {isSubmitting ? t("submitting") : t("submit")}
90
- </Button>
91
- </form>
92
- </CardContent>
93
- </Card>
55
+ <form onSubmit={onSubmit} className="space-y-6">
56
+ <div className="space-y-2">
57
+ <Label htmlFor="username">{t("username")}</Label>
58
+ <Input
59
+ id="username"
60
+ value={username}
61
+ onChange={(event) => setUsername(event.target.value)}
62
+ placeholder={t("usernamePlaceholder")}
63
+ autoComplete="username"
64
+ className="h-12"
65
+ required
66
+ />
67
+ </div>
68
+ <div className="space-y-2">
69
+ <Label htmlFor="password">{t("password")}</Label>
70
+ <Input
71
+ id="password"
72
+ type="password"
73
+ value={password}
74
+ onChange={(event) => setPassword(event.target.value)}
75
+ placeholder={t("passwordPlaceholder")}
76
+ autoComplete="current-password"
77
+ className="h-12"
78
+ required
79
+ />
80
+ </div>
81
+ <Button type="submit" className="h-12 w-full" disabled={isSubmitting}>
82
+ {isSubmitting ? t("submitting") : t("submit")}
83
+ </Button>
84
+ </form>
94
85
  );
95
86
  }
@@ -1,17 +1,8 @@
1
1
  import axios from "axios";
2
- import {
3
- clearAccessToken,
4
- getAccessToken,
5
- setAccessToken,
6
- } from "@/lib/api/token-store";
7
- import type { ApiErrorResponse, ApiSuccess } from "@/types";
2
+ import type { ApiErrorResponse } from "@/types";
8
3
 
9
4
  const baseURL = process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000";
10
5
 
11
- type RetryableRequestConfig = {
12
- _retry?: boolean;
13
- };
14
-
15
6
  export const publicApi = axios.create({
16
7
  baseURL,
17
8
  withCredentials: true,
@@ -28,86 +19,6 @@ export const protectedApi = axios.create({
28
19
  },
29
20
  });
30
21
 
31
- protectedApi.interceptors.request.use((config) => {
32
- const token = getAccessToken();
33
- if (token) {
34
- config.headers = config.headers ?? {};
35
- config.headers.Authorization = `Bearer ${token}`;
36
- }
37
- return config;
38
- });
39
-
40
- let isRefreshing = false;
41
- let waitingQueue: Array<(token: string | null) => void> = [];
42
-
43
- function flushQueue(token: string | null): void {
44
- for (const resolve of waitingQueue) {
45
- resolve(token);
46
- }
47
- waitingQueue = [];
48
- }
49
-
50
- async function refreshAccessToken(): Promise<string | null> {
51
- try {
52
- const response = await publicApi.post("/api/v1/auth/refresh", null, {
53
- withCredentials: true,
54
- });
55
- const token = (response.data as ApiSuccess<{ accessToken: string }>).data
56
- ?.accessToken;
57
- if (!token) {
58
- clearAccessToken();
59
- return null;
60
- }
61
- setAccessToken(token);
62
- return token;
63
- } catch {
64
- clearAccessToken();
65
- return null;
66
- }
67
- }
68
-
69
- protectedApi.interceptors.response.use(
70
- (response) => response,
71
- async (error) => {
72
- const config = error.config as typeof error.config & RetryableRequestConfig;
73
- if (!config || config._retry || error.response?.status !== 401) {
74
- return Promise.reject(error);
75
- }
76
-
77
- if (isRefreshing) {
78
- return new Promise((resolve, reject) => {
79
- waitingQueue.push((token) => {
80
- if (!token) {
81
- reject(error);
82
- return;
83
- }
84
-
85
- config.headers = config.headers ?? {};
86
- config.headers.Authorization = `Bearer ${token}`;
87
- resolve(protectedApi(config));
88
- });
89
- });
90
- }
91
-
92
- config._retry = true;
93
- isRefreshing = true;
94
-
95
- try {
96
- const token = await refreshAccessToken();
97
- flushQueue(token);
98
- if (!token) {
99
- return Promise.reject(error);
100
- }
101
-
102
- config.headers = config.headers ?? {};
103
- config.headers.Authorization = `Bearer ${token}`;
104
- return protectedApi(config);
105
- } finally {
106
- isRefreshing = false;
107
- }
108
- },
109
- );
110
-
111
22
  function extractApiErrorMessage(error: unknown): string {
112
23
  if (!axios.isAxiosError(error)) {
113
24
  return "Unexpected error occurred.";
@@ -1,7 +1,7 @@
1
1
  import { createAuthClient } from "better-auth/react";
2
- import { jwtClient, usernameClient } from "better-auth/client/plugins";
2
+ import { 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(), usernameClient()],
6
+ plugins: [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, username } from "better-auth/plugins";
3
+ import { username } from "better-auth/plugins";
4
4
  import prisma from "@/lib/db/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(), username()],
15
+ plugins: [username()],
16
16
  emailAndPassword: {
17
17
  enabled: true,
18
18
  minPasswordLength: 8,
@@ -1,13 +0,0 @@
1
- let accessToken: string | null = null;
2
-
3
- export function getAccessToken(): string | null {
4
- return accessToken;
5
- }
6
-
7
- export function setAccessToken(token: string | null): void {
8
- accessToken = token;
9
- }
10
-
11
- export function clearAccessToken(): void {
12
- accessToken = null;
13
- }
@@ -1,32 +0,0 @@
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
- }
@@ -1,15 +0,0 @@
1
- const refreshCookieName = "nextcli_refresh_token";
2
-
3
- export function refreshCookieOptions() {
4
- return {
5
- httpOnly: true,
6
- secure: process.env.NODE_ENV === "production",
7
- sameSite: "lax",
8
- path: "/",
9
- maxAge: 60 * 60 * 24 * 30,
10
- } as const;
11
- }
12
-
13
- export function getRefreshCookieName(): string {
14
- return refreshCookieName;
15
- }
@@ -1,32 +0,0 @@
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
- }
@@ -1,13 +0,0 @@
1
- let accessToken: string | null = null;
2
-
3
- export function getAccessToken(): string | null {
4
- return accessToken;
5
- }
6
-
7
- export function setAccessToken(token: string | null): void {
8
- accessToken = token;
9
- }
10
-
11
- export function clearAccessToken(): void {
12
- accessToken = null;
13
- }
@@ -1,15 +0,0 @@
1
- const refreshCookieName = "nextcli_refresh_token";
2
-
3
- export function refreshCookieOptions() {
4
- return {
5
- httpOnly: true,
6
- secure: process.env.NODE_ENV === "production",
7
- sameSite: "lax",
8
- path: "/",
9
- maxAge: 60 * 60 * 24 * 30,
10
- } as const;
11
- }
12
-
13
- export function getRefreshCookieName(): string {
14
- return refreshCookieName;
15
- }