@thinhnguyencth1204/nextcli 0.8.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.
- package/README.md +27 -24
- package/dist/cli.js +168 -107
- package/package.json +1 -1
- package/templates/features/api/src/lib/api/axios.ts +1 -90
- package/templates/features/auth/messages/vi/auth.json +2 -1
- package/templates/features/auth/src/app/(auth)/change-password/page.tsx +5 -4
- package/templates/features/auth/src/app/(auth)/layout.tsx +2 -5
- package/templates/features/auth/src/app/(auth)/sign-in/page.tsx +5 -4
- package/templates/features/auth/src/app/api/v1/auth/login/route.ts +24 -29
- package/templates/features/auth/src/app/api/v1/auth/logout/route.ts +0 -5
- package/templates/features/auth/src/components/layout/auth/auth-shell.tsx +24 -0
- package/templates/features/auth/src/features/auth/components/account-panel.tsx +15 -3
- package/templates/features/auth/src/features/auth/components/change-password-form.tsx +27 -30
- package/templates/features/auth/src/features/auth/components/sign-in-form.tsx +33 -42
- package/templates/features/auth/src/lib/auth/client.ts +2 -2
- package/templates/features/auth/src/lib/auth/server.ts +2 -2
- package/templates/features/dashboard/src/app/(dashboard)/account/page.tsx +9 -7
- package/templates/features/dashboard/src/app/(dashboard)/dashboard/page.tsx +24 -10
- package/templates/features/dashboard/src/components/layout/private/app-sidebar.tsx +1 -13
- package/templates/features/dashboard/src/components/layout/private/dashboard-layout.tsx +31 -22
- package/templates/features/dashboard/src/components/layout/private/page-shell.tsx +40 -0
- package/templates/features/database/prisma/schema.prisma +1 -0
- package/templates/features/example/messages/vi/example.json +11 -1
- package/templates/features/example/src/app/(dashboard)/example/page.tsx +92 -3
- package/templates/features/example/src/example/components/example-table.tsx +15 -2
- 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/SETUP.md +62 -10
- package/templates/next-base/bun.lock +407 -0
- package/templates/next-base/messages/vi/auth.json +43 -0
- package/templates/next-base/messages/vi/common.json +53 -0
- package/templates/next-base/messages/vi/example.json +20 -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 +24 -5
- package/templates/next-base/prisma/schema.prisma +85 -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 +15 -0
- package/templates/next-base/src/app/(auth)/layout.tsx +6 -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 +15 -0
- package/templates/next-base/src/app/(dashboard)/account/page.tsx +20 -0
- package/templates/next-base/src/app/(dashboard)/dashboard/page.tsx +31 -0
- package/templates/next-base/src/app/(dashboard)/example/page.tsx +102 -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 +65 -0
- package/templates/next-base/src/app/api/v1/auth/logout/route.ts +23 -0
- package/templates/next-base/src/app/api/v1/auth/me/route.ts +24 -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/layout.tsx +14 -6
- package/templates/next-base/src/app/page.tsx +2 -25
- package/templates/next-base/src/components/branding/logo.tsx +27 -4
- package/templates/next-base/src/components/layout/auth/auth-shell.tsx +24 -0
- package/templates/next-base/src/components/layout/private/app-sidebar.tsx +32 -0
- package/templates/next-base/src/components/layout/private/dashboard-layout.tsx +63 -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/layout/private/page-shell.tsx +40 -0
- package/templates/next-base/src/components/providers/query-provider.tsx +17 -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 +64 -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 +92 -0
- package/templates/next-base/src/features/auth/components/change-password-form.tsx +79 -0
- package/templates/next-base/src/features/auth/components/sign-in-form.tsx +86 -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/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 +56 -0
- package/templates/next-base/src/lib/api/response.ts +45 -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/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/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/features/api/src/lib/api/token-store.ts +0 -13
- package/templates/features/auth/src/app/api/v1/auth/refresh/route.ts +0 -32
- package/templates/features/auth/src/lib/auth/cookies.ts +0 -15
|
@@ -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.";
|
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
"submit": "Đăng nhập",
|
|
12
12
|
"submitting": "Đang đăng nhập...",
|
|
13
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
14
|
"success": "Đăng nhập thành công.",
|
|
16
15
|
"failed": "Đăng nhập thất bại."
|
|
17
16
|
},
|
|
@@ -30,6 +29,8 @@
|
|
|
30
29
|
},
|
|
31
30
|
"account": {
|
|
32
31
|
"title": "Tài khoản",
|
|
32
|
+
"description": "Xem thông tin phiên đăng nhập hiện tại.",
|
|
33
|
+
"panelTitle": "Thông tin tài khoản",
|
|
33
34
|
"loading": "Đang tải thông tin tài khoản...",
|
|
34
35
|
"noSession": "Chưa có phiên đăng nhập hoạt động.",
|
|
35
36
|
"userId": "Mã người dùng",
|
|
@@ -5,10 +5,11 @@ export default function ChangePasswordPage() {
|
|
|
5
5
|
const t = useTranslations("auth.changePasswordPage");
|
|
6
6
|
|
|
7
7
|
return (
|
|
8
|
-
<
|
|
9
|
-
<
|
|
10
|
-
|
|
8
|
+
<div className="space-y-6">
|
|
9
|
+
<div className="text-center sm:text-left">
|
|
10
|
+
<h1 className="text-2xl font-semibold tracking-tight">{t("title")}</h1>
|
|
11
|
+
</div>
|
|
11
12
|
<ChangePasswordForm />
|
|
12
|
-
</
|
|
13
|
+
</div>
|
|
13
14
|
);
|
|
14
15
|
}
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
+
import { AuthShell } from "@/components/layout/auth/auth-shell";
|
|
2
3
|
|
|
3
4
|
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
|
-
);
|
|
5
|
+
return <AuthShell>{children}</AuthShell>;
|
|
9
6
|
}
|
|
@@ -5,10 +5,11 @@ export default function SignInPage() {
|
|
|
5
5
|
const t = useTranslations("auth.signInPage");
|
|
6
6
|
|
|
7
7
|
return (
|
|
8
|
-
<
|
|
9
|
-
<
|
|
10
|
-
|
|
8
|
+
<div className="space-y-6">
|
|
9
|
+
<div className="text-center sm:text-left">
|
|
10
|
+
<h1 className="text-2xl font-semibold tracking-tight">{t("title")}</h1>
|
|
11
|
+
</div>
|
|
11
12
|
<SignInForm />
|
|
12
|
-
</
|
|
13
|
+
</div>
|
|
13
14
|
);
|
|
14
15
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { getRefreshCookieName, refreshCookieOptions } from "@/lib/auth/cookies";
|
|
2
1
|
import { fail, ok } from "@/lib/api/response";
|
|
3
2
|
import prisma from "@/lib/db/prisma";
|
|
4
3
|
|
|
@@ -19,16 +18,19 @@ export async function POST(request: Request) {
|
|
|
19
18
|
});
|
|
20
19
|
}
|
|
21
20
|
|
|
22
|
-
const signInResponse = await fetch(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
const signInResponse = await fetch(
|
|
22
|
+
`${authBaseUrl}/api/auth/sign-in/username`,
|
|
23
|
+
{
|
|
24
|
+
method: "POST",
|
|
25
|
+
headers: {
|
|
26
|
+
"Content-Type": "application/json",
|
|
27
|
+
},
|
|
28
|
+
body: JSON.stringify({
|
|
29
|
+
username: payload.username,
|
|
30
|
+
password: payload.password,
|
|
31
|
+
}),
|
|
26
32
|
},
|
|
27
|
-
|
|
28
|
-
username: payload.username,
|
|
29
|
-
password: payload.password,
|
|
30
|
-
}),
|
|
31
|
-
});
|
|
33
|
+
);
|
|
32
34
|
|
|
33
35
|
if (!signInResponse.ok) {
|
|
34
36
|
return fail("UNAUTHORIZED", "Invalid credentials.", {
|
|
@@ -36,20 +38,12 @@ export async function POST(request: Request) {
|
|
|
36
38
|
});
|
|
37
39
|
}
|
|
38
40
|
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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();
|
|
41
|
+
const upstreamSetCookies =
|
|
42
|
+
typeof signInResponse.headers.getSetCookie === "function"
|
|
43
|
+
? signInResponse.headers.getSetCookie()
|
|
44
|
+
: [];
|
|
45
|
+
const cookieHeader =
|
|
46
|
+
upstreamSetCookies[0] ?? signInResponse.headers.get("set-cookie");
|
|
53
47
|
|
|
54
48
|
const dbUser = await prisma.user.findUnique({
|
|
55
49
|
where: { username: payload.username },
|
|
@@ -57,14 +51,15 @@ export async function POST(request: Request) {
|
|
|
57
51
|
});
|
|
58
52
|
|
|
59
53
|
const response = ok({
|
|
60
|
-
accessToken: tokenPayload.token,
|
|
61
54
|
requirePasswordChange: dbUser?.requirePasswordChange ?? false,
|
|
62
55
|
});
|
|
63
56
|
|
|
64
|
-
if (
|
|
65
|
-
|
|
57
|
+
if (upstreamSetCookies.length > 0) {
|
|
58
|
+
for (const setCookie of upstreamSetCookies) {
|
|
59
|
+
response.headers.append("set-cookie", setCookie);
|
|
60
|
+
}
|
|
61
|
+
} else if (cookieHeader) {
|
|
62
|
+
response.headers.append("set-cookie", cookieHeader);
|
|
66
63
|
}
|
|
67
|
-
response.cookies.set(getRefreshCookieName(), crypto.randomUUID(), refreshCookieOptions());
|
|
68
|
-
|
|
69
64
|
return response;
|
|
70
65
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { getRefreshCookieName, refreshCookieOptions } from "@/lib/auth/cookies";
|
|
2
1
|
import { fail, ok } from "@/lib/api/response";
|
|
3
2
|
|
|
4
3
|
const authBaseUrl =
|
|
@@ -20,9 +19,5 @@ export async function POST(request: Request) {
|
|
|
20
19
|
: fail("UPSTREAM_ERROR", "Failed to sign out.", {
|
|
21
20
|
status: signOutResponse.status,
|
|
22
21
|
});
|
|
23
|
-
response.cookies.set(getRefreshCookieName(), "", {
|
|
24
|
-
...refreshCookieOptions(),
|
|
25
|
-
maxAge: 0,
|
|
26
|
-
});
|
|
27
22
|
return response;
|
|
28
23
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { Logo } from "@/components/branding/logo";
|
|
3
|
+
|
|
4
|
+
type AuthShellProps = {
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function AuthShell({ children }: AuthShellProps) {
|
|
9
|
+
return (
|
|
10
|
+
<div className="flex min-h-screen flex-col items-center justify-center bg-muted/40 py-12 sm:px-6 lg:px-8">
|
|
11
|
+
<div className="sm:mx-auto sm:w-full sm:max-w-md">
|
|
12
|
+
<div className="flex justify-center">
|
|
13
|
+
<Logo variant="auth" />
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
|
18
|
+
<div className="rounded-lg border bg-card px-4 py-8 shadow-sm sm:px-10">
|
|
19
|
+
{children}
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -49,17 +49,29 @@ export function AccountPanel() {
|
|
|
49
49
|
}, []);
|
|
50
50
|
|
|
51
51
|
if (loading) {
|
|
52
|
-
return
|
|
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
|
|
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("
|
|
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
|
-
<
|
|
53
|
-
<
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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(
|
|
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
|
-
<
|
|
63
|
-
<
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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,7 +1,7 @@
|
|
|
1
1
|
import { createAuthClient } from "better-auth/react";
|
|
2
|
-
import {
|
|
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: [
|
|
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 {
|
|
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: [
|
|
15
|
+
plugins: [username()],
|
|
16
16
|
emailAndPassword: {
|
|
17
17
|
enabled: true,
|
|
18
18
|
minPasswordLength: 8,
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import Link from "next/link";
|
|
2
2
|
import { useTranslations } from "next-intl";
|
|
3
3
|
import { AccountPanel } from "@/features/auth/components/account-panel";
|
|
4
|
+
import { PageShell } from "@/components/layout/private/page-shell";
|
|
4
5
|
import { Button } from "@/components/ui/button";
|
|
5
6
|
|
|
6
7
|
export default function AccountPage() {
|
|
7
8
|
const t = useTranslations("auth.account");
|
|
8
9
|
|
|
9
10
|
return (
|
|
10
|
-
<
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
<PageShell title={t("title")} description={t("description")}>
|
|
12
|
+
<div className="space-y-4">
|
|
13
|
+
<AccountPanel />
|
|
14
|
+
<Button asChild variant="outline">
|
|
15
|
+
<Link href="/sign-in">{t("goToSignIn")}</Link>
|
|
16
|
+
</Button>
|
|
17
|
+
</div>
|
|
18
|
+
</PageShell>
|
|
17
19
|
);
|
|
18
20
|
}
|
|
@@ -1,17 +1,31 @@
|
|
|
1
1
|
import { useTranslations } from "next-intl";
|
|
2
|
-
import {
|
|
2
|
+
import { PageShell } from "@/components/layout/private/page-shell";
|
|
3
3
|
|
|
4
4
|
export default function DashboardPage() {
|
|
5
|
-
const t = useTranslations("common.
|
|
5
|
+
const t = useTranslations("common.dashboardPage");
|
|
6
|
+
|
|
7
|
+
const stats = [
|
|
8
|
+
{ title: t("stats.users.title"), value: t("stats.users.value") },
|
|
9
|
+
{ title: t("stats.records.title"), value: t("stats.records.value") },
|
|
10
|
+
{ title: t("stats.activity.title"), value: t("stats.activity.value") },
|
|
11
|
+
];
|
|
6
12
|
|
|
7
13
|
return (
|
|
8
|
-
<
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
<PageShell title={t("title")} description={t("description")}>
|
|
15
|
+
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
|
16
|
+
{stats.map((stat) => (
|
|
17
|
+
<div
|
|
18
|
+
key={stat.title}
|
|
19
|
+
className="overflow-hidden rounded-lg border bg-card p-6 shadow-sm"
|
|
20
|
+
>
|
|
21
|
+
<h3 className="text-base font-semibold leading-6">{stat.title}</h3>
|
|
22
|
+
<p className="mt-2 text-3xl font-bold tracking-tight text-primary">
|
|
23
|
+
{stat.value}
|
|
24
|
+
</p>
|
|
25
|
+
</div>
|
|
26
|
+
))}
|
|
27
|
+
</div>
|
|
28
|
+
<p className="mt-8 text-sm text-muted-foreground">{t("placeholder")}</p>
|
|
29
|
+
</PageShell>
|
|
16
30
|
);
|
|
17
31
|
}
|
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
useSidebar,
|
|
7
|
-
Sidebar,
|
|
8
|
-
SidebarHeader,
|
|
9
|
-
SidebarTrigger,
|
|
10
|
-
} from "@/components/ui/sidebar";
|
|
11
|
-
import { Logo } from "@/components/branding/logo";
|
|
4
|
+
import { useSidebar, Sidebar, SidebarTrigger } from "@/components/ui/sidebar";
|
|
12
5
|
import { NavSidebar } from "@/components/layout/private/nav-sidebar";
|
|
13
6
|
import { cn } from "@/utils/cn";
|
|
14
7
|
|
|
@@ -33,11 +26,6 @@ export function AppSidebar() {
|
|
|
33
26
|
</SidebarTrigger>
|
|
34
27
|
</div>
|
|
35
28
|
)}
|
|
36
|
-
<SidebarHeader className="p-3">
|
|
37
|
-
<Link href="/dashboard">
|
|
38
|
-
<Logo showLabel />
|
|
39
|
-
</Link>
|
|
40
|
-
</SidebarHeader>
|
|
41
29
|
<NavSidebar />
|
|
42
30
|
</Sidebar>
|
|
43
31
|
);
|