stackkit 0.3.4 → 0.3.6

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 (203) hide show
  1. package/README.md +50 -42
  2. package/dist/cli/add.js +122 -56
  3. package/dist/cli/create.d.ts +2 -0
  4. package/dist/cli/create.js +271 -95
  5. package/dist/cli/doctor.js +1 -0
  6. package/dist/cli/list.d.ts +1 -1
  7. package/dist/cli/list.js +6 -4
  8. package/dist/index.js +234 -191
  9. package/dist/lib/constants.d.ts +4 -0
  10. package/dist/lib/constants.js +4 -0
  11. package/dist/lib/discovery/module-discovery.d.ts +4 -0
  12. package/dist/lib/discovery/module-discovery.js +56 -0
  13. package/dist/lib/generation/code-generator.d.ts +11 -2
  14. package/dist/lib/generation/code-generator.js +42 -3
  15. package/dist/lib/generation/generator-utils.js +3 -1
  16. package/dist/lib/pm/package-manager.js +16 -13
  17. package/dist/lib/ui/logger.js +3 -2
  18. package/dist/lib/utils/path-resolver.d.ts +2 -0
  19. package/dist/lib/utils/path-resolver.js +8 -0
  20. package/dist/meta.json +8312 -0
  21. package/modules/auth/better-auth/files/{shared → express}/config/env.ts +48 -52
  22. package/modules/auth/better-auth/files/express/middlewares/authorize.ts +20 -1
  23. package/modules/auth/better-auth/files/express/modules/auth.controller.ts +349 -0
  24. package/modules/auth/better-auth/files/express/modules/{auth/auth.route.ts → auth.route.ts} +12 -7
  25. package/modules/auth/better-auth/files/express/modules/auth.service.ts +664 -0
  26. package/modules/auth/better-auth/files/express/modules/{auth/auth.type.ts → auth.type.ts} +22 -9
  27. package/modules/auth/better-auth/files/{shared/mongoose/auth/constants.ts → express/mongo-modules/auth.constants.ts} +0 -1
  28. package/modules/auth/better-auth/files/{shared/mongoose/auth/helper.ts → express/mongo-modules/auth.helper.ts} +11 -1
  29. package/modules/auth/better-auth/files/express/types/express.d.ts +11 -0
  30. package/modules/auth/better-auth/files/nextjs/api-route.ts +74 -0
  31. package/modules/auth/better-auth/files/nextjs/dashboard/pages/(user)/page.tsx +6 -0
  32. package/modules/auth/better-auth/files/nextjs/dashboard/pages/admin/page.tsx +6 -0
  33. package/modules/auth/better-auth/files/nextjs/dashboard/pages/layout.tsx +48 -0
  34. package/modules/auth/better-auth/files/nextjs/dashboard/pages/my-profile/page.tsx +5 -0
  35. package/modules/auth/better-auth/files/nextjs/features/services/auth.service.ts +102 -0
  36. package/modules/auth/better-auth/files/nextjs/layout/layout.tsx +13 -0
  37. package/modules/auth/better-auth/files/nextjs/lib/axios/http.ts +158 -0
  38. package/modules/auth/better-auth/files/nextjs/lib/env.ts +35 -0
  39. package/modules/auth/better-auth/files/nextjs/lib/utils/auth.ts +75 -0
  40. package/modules/auth/better-auth/files/nextjs/lib/utils/cookie.ts +29 -0
  41. package/modules/auth/better-auth/files/nextjs/lib/utils/jwt.ts +28 -0
  42. package/modules/auth/better-auth/files/nextjs/lib/utils/token.ts +49 -0
  43. package/modules/auth/better-auth/files/nextjs/pages/forgot-password/page.tsx +5 -0
  44. package/modules/auth/better-auth/files/nextjs/pages/layout.tsx +11 -0
  45. package/modules/auth/better-auth/files/nextjs/pages/login/page.tsx +9 -0
  46. package/modules/auth/better-auth/files/nextjs/pages/register/page.tsx +5 -0
  47. package/modules/auth/better-auth/files/nextjs/pages/reset-password/page.tsx +10 -0
  48. package/modules/auth/better-auth/files/nextjs/pages/verify-email/page.tsx +10 -0
  49. package/modules/auth/better-auth/files/nextjs/proxy.ts +157 -22
  50. package/modules/auth/better-auth/files/nextjs/theme/providers/theme-provider.tsx +11 -0
  51. package/modules/auth/better-auth/files/nextjs/types/api.types.ts +18 -0
  52. package/modules/auth/better-auth/files/react/components/protected-route.tsx +39 -0
  53. package/modules/auth/better-auth/files/react/components/route-guards.tsx +13 -0
  54. package/modules/auth/better-auth/files/react/dashboard/admin/pages/overview.tsx +3 -0
  55. package/modules/auth/better-auth/files/react/dashboard/pages/overview.tsx +3 -0
  56. package/modules/auth/better-auth/files/react/features/pages/forgot-password.tsx +5 -0
  57. package/modules/auth/better-auth/files/react/features/pages/login.tsx +5 -0
  58. package/modules/auth/better-auth/files/react/features/pages/my-profile.tsx +5 -0
  59. package/modules/auth/better-auth/files/react/features/pages/oauth-callback.tsx +59 -0
  60. package/modules/auth/better-auth/files/react/features/pages/register.tsx +5 -0
  61. package/modules/auth/better-auth/files/react/features/pages/reset-password.tsx +10 -0
  62. package/modules/auth/better-auth/files/react/features/pages/verify-email.tsx +10 -0
  63. package/modules/auth/better-auth/files/react/layout/dashboard-layout.tsx +54 -0
  64. package/modules/auth/better-auth/files/react/lib/axios/http.ts +68 -0
  65. package/modules/auth/better-auth/files/react/lib/env.ts +25 -0
  66. package/modules/auth/better-auth/files/react/router.tsx +73 -0
  67. package/modules/auth/better-auth/files/react/theme/components/providers/theme-provider-context.ts +13 -0
  68. package/modules/auth/better-auth/files/react/theme/components/providers/theme-provider.tsx +51 -0
  69. package/modules/auth/better-auth/files/react/theme/hooks/use-theme.ts +8 -0
  70. package/modules/auth/better-auth/files/shared/features/components/change-password-dialog.tsx +113 -0
  71. package/modules/auth/better-auth/files/shared/features/components/forgot-password-form.tsx +84 -0
  72. package/modules/auth/better-auth/files/shared/features/components/login-form.tsx +134 -0
  73. package/modules/auth/better-auth/files/shared/features/components/my-profile.tsx +147 -0
  74. package/modules/auth/better-auth/files/shared/features/components/profile-form.tsx +205 -0
  75. package/modules/auth/better-auth/files/shared/features/components/register-form.tsx +100 -0
  76. package/modules/auth/better-auth/files/shared/features/components/reset-password-form.tsx +111 -0
  77. package/modules/auth/better-auth/files/shared/features/components/social-login-buttons.tsx +47 -0
  78. package/modules/auth/better-auth/files/shared/features/components/user-profile-menu.tsx +106 -0
  79. package/modules/auth/better-auth/files/shared/features/components/verify-email-form.tsx +110 -0
  80. package/modules/auth/better-auth/files/shared/features/queries/auth.mutations.tsx +312 -0
  81. package/modules/auth/better-auth/files/shared/features/queries/auth.querie.ts +19 -0
  82. package/modules/auth/better-auth/files/shared/features/services/auth.api.ts +81 -0
  83. package/modules/auth/better-auth/files/shared/features/types/auth.type.ts +47 -0
  84. package/modules/auth/better-auth/files/shared/features/validators/change-password.validator.ts +18 -0
  85. package/modules/auth/better-auth/files/shared/features/validators/forgot.validator.ts +7 -0
  86. package/modules/auth/better-auth/files/shared/features/validators/login.validator.ts +14 -0
  87. package/modules/auth/better-auth/files/shared/features/validators/profile.validator.ts +8 -0
  88. package/modules/auth/better-auth/files/shared/features/validators/register.validator.ts +9 -0
  89. package/modules/auth/better-auth/files/shared/features/validators/reset.validator.ts +9 -0
  90. package/modules/auth/better-auth/files/shared/features/validators/verify.validator.ts +8 -0
  91. package/modules/auth/better-auth/files/shared/lib/auth-client.ts +2 -1
  92. package/modules/auth/better-auth/files/shared/lib/auth.ts +10 -29
  93. package/modules/auth/better-auth/files/shared/lib/constant/dashboard.ts +90 -0
  94. package/modules/auth/better-auth/files/shared/prisma/enums.prisma +0 -1
  95. package/modules/auth/better-auth/files/shared/theme/mode-toggle.tsx +30 -0
  96. package/modules/auth/better-auth/files/shared/ui/shadcn/components/dashboard/dashboard-header.tsx +94 -0
  97. package/modules/auth/better-auth/files/shared/ui/shadcn/components/dashboard/dashboard-sidebar.tsx +255 -0
  98. package/modules/auth/better-auth/files/shared/ui/shadcn/components/footer.tsx +35 -0
  99. package/modules/auth/better-auth/files/shared/ui/shadcn/components/navbar.tsx +145 -0
  100. package/modules/auth/better-auth/files/shared/ui/shadcn/form-field/input-field.tsx +440 -0
  101. package/modules/auth/better-auth/files/shared/utils/email.ts +20 -18
  102. package/modules/auth/better-auth/generator.json +174 -53
  103. package/modules/auth/better-auth/module.json +2 -2
  104. package/modules/components/files/shared/hooks/use-file-upload.ts +412 -0
  105. package/modules/components/files/shared/lib/utils/url-helpers.ts +110 -0
  106. package/modules/components/files/shared/shadcn/dashboard/data-table-column-selector.tsx +52 -0
  107. package/modules/components/files/shared/shadcn/dashboard/data-table-footer.tsx +156 -0
  108. package/modules/components/files/shared/shadcn/dashboard/data-table.tsx +405 -0
  109. package/modules/components/files/shared/shadcn/global/form-field/input-field.tsx +440 -0
  110. package/modules/components/files/shared/shadcn/global/form-field/media-uploader-field.tsx +745 -0
  111. package/modules/components/files/shared/shadcn/global/form-field/multi-select-field.tsx +207 -0
  112. package/modules/components/files/shared/shadcn/global/form-field/select-field.tsx +247 -0
  113. package/modules/components/files/shared/shadcn/global/form-field/textarea-field.tsx +277 -0
  114. package/modules/components/files/shared/shadcn/global/form-field/tiptap-editor-field.tsx +35 -0
  115. package/modules/components/files/shared/shadcn/global/no-results.tsx +41 -0
  116. package/modules/components/files/shared/shadcn/tiptap-editor/editor-menu-bar.tsx +217 -0
  117. package/modules/components/files/shared/shadcn/tiptap-editor/tiptap-editor.tsx +104 -0
  118. package/modules/components/files/shared/url/load-more.tsx +93 -0
  119. package/modules/components/files/shared/url/search-bar.tsx +131 -0
  120. package/modules/components/files/shared/url/sort-select.tsx +118 -0
  121. package/modules/components/files/shared/url/url-tabs.tsx +77 -0
  122. package/modules/components/generator.json +109 -0
  123. package/modules/components/module.json +11 -0
  124. package/modules/database/mongoose/generator.json +3 -14
  125. package/modules/database/mongoose/module.json +2 -2
  126. package/modules/database/prisma/generator.json +6 -12
  127. package/modules/database/prisma/module.json +2 -2
  128. package/modules/storage/cloudinary/files/express/config/env.ts +65 -0
  129. package/modules/storage/cloudinary/files/express/config/media.ts +103 -0
  130. package/modules/storage/cloudinary/files/express/modules/media/media.controller.ts +59 -0
  131. package/modules/storage/cloudinary/files/express/modules/media/media.route.ts +29 -0
  132. package/modules/storage/cloudinary/files/express/modules/media/media.service.ts +113 -0
  133. package/modules/storage/cloudinary/files/express/modules/media/media.type.ts +32 -0
  134. package/modules/storage/cloudinary/generator.json +34 -0
  135. package/modules/storage/cloudinary/module.json +11 -0
  136. package/modules/ui/shadcn/generator.json +21 -0
  137. package/modules/ui/shadcn/module.json +11 -0
  138. package/package.json +24 -26
  139. package/templates/express/README.md +11 -16
  140. package/templates/express/src/config/env.ts +7 -5
  141. package/templates/nextjs/README.md +13 -18
  142. package/templates/nextjs/app/favicon.ico +0 -0
  143. package/templates/nextjs/app/layout.tsx +6 -4
  144. package/templates/nextjs/components/providers/query-provider.tsx +3 -0
  145. package/templates/nextjs/env.example +3 -1
  146. package/templates/nextjs/lib/axios/http.ts +23 -0
  147. package/templates/nextjs/lib/env.ts +7 -5
  148. package/templates/nextjs/package.json +2 -1
  149. package/templates/nextjs/template.json +1 -2
  150. package/templates/react/README.md +9 -14
  151. package/templates/react/index.html +1 -1
  152. package/templates/react/package.json +1 -1
  153. package/templates/react/src/assets/favicon.ico +0 -0
  154. package/templates/react/src/components/providers/query-provider.tsx +38 -0
  155. package/templates/react/src/{shared/components → components}/seo.tsx +4 -8
  156. package/templates/react/src/lib/axios/http.ts +24 -0
  157. package/templates/react/src/main.tsx +8 -11
  158. package/templates/react/src/{features/about/pages → pages}/about.tsx +1 -1
  159. package/templates/react/src/{features/home/pages → pages}/home.tsx +1 -1
  160. package/templates/react/src/router.tsx +6 -6
  161. package/templates/react/src/vite-env.d.ts +2 -1
  162. package/templates/react/template.json +0 -1
  163. package/templates/react/tsconfig.app.json +6 -0
  164. package/templates/react/tsconfig.json +7 -1
  165. package/templates/react/vite.config.ts +12 -0
  166. package/modules/auth/authjs/files/nextjs/api/auth/[...nextauth]/route.ts +0 -3
  167. package/modules/auth/authjs/files/nextjs/proxy.ts +0 -1
  168. package/modules/auth/authjs/files/shared/lib/auth.ts +0 -119
  169. package/modules/auth/authjs/files/shared/prisma/schema.prisma +0 -61
  170. package/modules/auth/authjs/generator.json +0 -64
  171. package/modules/auth/authjs/module.json +0 -13
  172. package/modules/auth/better-auth/files/express/modules/auth/auth.controller.ts +0 -264
  173. package/modules/auth/better-auth/files/express/modules/auth/auth.service.ts +0 -537
  174. package/modules/auth/better-auth/files/express/templates/google-redirect.ejs +0 -24
  175. package/modules/auth/better-auth/files/nextjs/api/auth/[...all]/route.ts +0 -4
  176. package/modules/auth/better-auth/files/nextjs/lib/auth/auth-guards.ts +0 -41
  177. package/modules/auth/better-auth/files/nextjs/templates/email-otp.tsx +0 -74
  178. package/templates/express/node_modules/.bin/acorn +0 -17
  179. package/templates/express/node_modules/.bin/eslint +0 -17
  180. package/templates/express/node_modules/.bin/tsc +0 -17
  181. package/templates/express/node_modules/.bin/tsserver +0 -17
  182. package/templates/express/node_modules/.bin/tsx +0 -17
  183. package/templates/nextjs/lib/api/http.ts +0 -40
  184. package/templates/nextjs/next-env.d.ts +0 -6
  185. package/templates/react/dist/assets/index-D4AHT4dU.js +0 -193
  186. package/templates/react/dist/assets/index-rpwj5ZOX.css +0 -1
  187. package/templates/react/dist/index.html +0 -14
  188. package/templates/react/dist/vite.svg +0 -1
  189. package/templates/react/public/vite.svg +0 -1
  190. package/templates/react/src/app/layouts/dashboard-layout.tsx +0 -8
  191. package/templates/react/src/app/layouts/public-layout.tsx +0 -5
  192. package/templates/react/src/app/providers.tsx +0 -20
  193. package/templates/react/src/app/router.tsx +0 -21
  194. package/templates/react/src/assets/react.svg +0 -1
  195. package/templates/react/src/shared/api/http.ts +0 -39
  196. package/templates/react/src/shared/components/loading.tsx +0 -8
  197. package/templates/react/src/shared/lib/query-client.ts +0 -12
  198. package/templates/react/src/utils/storage.ts +0 -35
  199. package/templates/react/src/utils/utils.ts +0 -3
  200. /package/templates/nextjs/app/{page.tsx → (public)/(root)/page.tsx} +0 -0
  201. /package/templates/react/src/{shared/components → components}/error-boundary.tsx +0 -0
  202. /package/templates/react/src/{shared/components → components}/layout.tsx +0 -0
  203. /package/templates/react/src/{shared/pages → pages}/not-found.tsx +0 -0
@@ -0,0 +1,5 @@
1
+ import ForgotPasswordForm from "@/features/auth/components/forgot-password-form";
2
+
3
+ export default function ForgotPassword() {
4
+ return <ForgotPasswordForm />;
5
+ }
@@ -0,0 +1,11 @@
1
+ export default function AuthLayout({
2
+ children,
3
+ }: {
4
+ children: React.ReactNode;
5
+ }) {
6
+ return (
7
+ <main className="flex min-h-screen w-full items-center justify-center">
8
+ {children}
9
+ </main>
10
+ );
11
+ }
@@ -0,0 +1,9 @@
1
+ import LoginForm from "@/features/auth/components/login-form";
2
+
3
+ export default function Login({
4
+ searchParams,
5
+ }: {
6
+ searchParams?: { redirect?: string };
7
+ }) {
8
+ return <LoginForm searchParams={searchParams} />;
9
+ }
@@ -0,0 +1,5 @@
1
+ import RegisterForm from "@/features/auth/components/register-form";
2
+
3
+ export default function Register() {
4
+ return <RegisterForm />;
5
+ }
@@ -0,0 +1,10 @@
1
+ import ResetPasswordForm from "@/features/auth/components/reset-password-form";
2
+ import { Suspense } from "react";
3
+
4
+ export default function ResetPassword() {
5
+ return (
6
+ <Suspense>
7
+ <ResetPasswordForm />
8
+ </Suspense>
9
+ );
10
+ }
@@ -0,0 +1,10 @@
1
+ import VerifyEmailForm from "@/features/auth/components/verify-email-form";
2
+ import { Suspense } from "react";
3
+
4
+ export default function VerifyEmail() {
5
+ return (
6
+ <Suspense>
7
+ <VerifyEmailForm />
8
+ </Suspense>
9
+ );
10
+ }
@@ -1,34 +1,169 @@
1
- import { NextResponse, type NextRequest } from "next/server";
1
+ import {
2
+ getNewTokensWithRefreshToken,
3
+ getSession,
4
+ } from "@/features/auth/services/auth.service";
5
+ import { envVars } from "@/lib/env";
6
+ import {
7
+ getDefaultDashboardRoute,
8
+ getRouteOwner,
9
+ isAuthRoute,
10
+ UserRole,
11
+ } from "@/lib/utils/auth";
12
+ import { jwtUtils } from "@/lib/utils/jwt";
13
+ import { isTokenExpiringSoon } from "@/lib/utils/token";
14
+ import { NextRequest, NextResponse } from "next/server";
2
15
 
3
- function isAuthenticated(req: NextRequest) {
4
- const token = req.cookies.get("better-auth.session_token")?.value;
5
-
6
- return Boolean(token);
16
+ async function refreshTokenMiddleware(refreshToken: string): Promise<boolean> {
17
+ try {
18
+ const refresh = await getNewTokensWithRefreshToken(refreshToken);
19
+ if (!refresh) {
20
+ return false;
21
+ }
22
+ return true;
23
+ } catch (error) {
24
+ console.error("Error refreshing token in middleware:", error);
25
+ return false;
26
+ }
7
27
  }
8
28
 
9
- export function proxy(req: NextRequest) {
10
- const { pathname, search } = req.nextUrl;
11
- const authed = isAuthenticated(req);
29
+ export async function proxy(request: NextRequest) {
30
+ try {
31
+ const { pathname } = request.nextUrl;
32
+ const accessToken = request.cookies.get("accessToken")?.value;
33
+ const refreshToken = request.cookies.get("refreshToken")?.value;
12
34
 
13
- if (pathname === "/login" || pathname === "/signup") {
14
- if (authed) return NextResponse.redirect(new URL("/dashboard", req.url));
15
- return NextResponse.next();
16
- }
35
+ const decodedAccessToken =
36
+ accessToken &&
37
+ jwtUtils.verifyToken(accessToken, envVars.JWT_ACCESS_SECRET as string)
38
+ .data;
17
39
 
18
- if (pathname.startsWith("/dashboard") && !authed) {
19
- const next = encodeURIComponent(pathname + (search || ""));
20
- return NextResponse.redirect(new URL(`/login?next=${next}`, req.url));
21
- }
40
+ const isValidAccessToken =
41
+ accessToken &&
42
+ jwtUtils.verifyToken(accessToken, envVars.JWT_ACCESS_SECRET as string)
43
+ .success;
44
+
45
+ let userRole: UserRole | null = null;
46
+
47
+ if (decodedAccessToken) {
48
+ userRole = (decodedAccessToken.role as UserRole) || null;
49
+ }
50
+
51
+ const normalizedUserRole = userRole === "ADMIN" ? "ADMIN" : "USER";
52
+ const routerOwner = getRouteOwner(pathname);
53
+ const isAuth = isAuthRoute(pathname);
54
+
55
+ if (
56
+ isValidAccessToken &&
57
+ refreshToken &&
58
+ (await isTokenExpiringSoon(accessToken))
59
+ ) {
60
+ const requestHeaders = new Headers(request.headers);
61
+
62
+ const response = NextResponse.next({
63
+ request: {
64
+ headers: requestHeaders,
65
+ },
66
+ });
67
+
68
+ try {
69
+ const refreshed = await refreshTokenMiddleware(refreshToken);
70
+
71
+ if (refreshed) {
72
+ requestHeaders.set("x-token-refreshed", "1");
73
+ }
74
+
75
+ return NextResponse.next({
76
+ request: {
77
+ headers: requestHeaders,
78
+ },
79
+ headers: response.headers,
80
+ });
81
+ } catch (error) {
82
+ console.error("Error refreshing token:", error);
83
+ }
84
+
85
+ return response;
86
+ }
22
87
 
23
- return NextResponse.next();
88
+ if (isAuth && isValidAccessToken) {
89
+ return NextResponse.redirect(
90
+ new URL(getDefaultDashboardRoute(userRole as UserRole), request.url),
91
+ );
92
+ }
93
+
94
+ if (pathname === "/reset-password") {
95
+ const email = request.nextUrl.searchParams.get("email");
96
+
97
+ if (accessToken && email) {
98
+ const userInfo = await getSession();
99
+
100
+ if (userInfo.needPasswordChange) {
101
+ return NextResponse.next();
102
+ } else {
103
+ return NextResponse.redirect(
104
+ new URL(
105
+ getDefaultDashboardRoute(userRole as UserRole),
106
+ request.url,
107
+ ),
108
+ );
109
+ }
110
+ }
111
+
112
+ if (email) {
113
+ return NextResponse.next();
114
+ }
115
+
116
+ const loginUrl = new URL("/login", request.url);
117
+ loginUrl.searchParams.set("redirect", pathname);
118
+ return NextResponse.redirect(loginUrl);
119
+ }
120
+
121
+ if (routerOwner === null || routerOwner === "COMMON") {
122
+ return NextResponse.next();
123
+ }
124
+
125
+ if (!accessToken || !isValidAccessToken) {
126
+ const loginUrl = new URL("/login", request.url);
127
+ loginUrl.searchParams.set("redirect", pathname);
128
+ return NextResponse.redirect(loginUrl);
129
+ }
130
+
131
+ if (accessToken) {
132
+ const userInfo = await getSession();
133
+ if (userInfo) {
134
+ if (userInfo.emailVerified === false && pathname !== "/verify-email") {
135
+ const verifyEmailUrl = new URL("/verify-email", request.url);
136
+ verifyEmailUrl.searchParams.set("email", userInfo.email);
137
+ return NextResponse.redirect(verifyEmailUrl);
138
+ }
139
+
140
+ if (userInfo.needPasswordChange && pathname !== "/reset-password") {
141
+ const resetPasswordUrl = new URL("/reset-password", request.url);
142
+ resetPasswordUrl.searchParams.set("email", userInfo.email);
143
+ return NextResponse.redirect(resetPasswordUrl);
144
+ }
145
+ }
146
+ }
147
+
148
+ const routeOwnerNormalized = routerOwner === "ADMIN" ? "ADMIN" : "USER";
149
+
150
+ if (routeOwnerNormalized === "ADMIN" && normalizedUserRole !== "ADMIN") {
151
+ return NextResponse.redirect(
152
+ new URL(
153
+ getDefaultDashboardRoute(normalizedUserRole as UserRole),
154
+ request.url,
155
+ ),
156
+ );
157
+ }
158
+
159
+ return NextResponse.next();
160
+ } catch (error) {
161
+ console.error("Error in proxy middleware:", error);
162
+ }
24
163
  }
25
164
 
26
165
  export const config = {
27
166
  matcher: [
28
- "/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)",
29
- "/(api|trpc)(.*)",
30
- "/login",
31
- "/signup",
32
- "/dashboard/:path*",
167
+ "/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|.well-known).*)",
33
168
  ],
34
169
  };
@@ -0,0 +1,11 @@
1
+ "use client"
2
+
3
+ import { ThemeProvider as NextThemesProvider } from "next-themes"
4
+ import * as React from "react"
5
+
6
+ export function ThemeProvider({
7
+ children,
8
+ ...props
9
+ }: React.ComponentProps<typeof NextThemesProvider>) {
10
+ return <NextThemesProvider {...props}>{children}</NextThemesProvider>
11
+ }
@@ -0,0 +1,18 @@
1
+ export interface ApiResponse<TData = unknown> {
2
+ success: true;
3
+ message: string;
4
+ data: TData;
5
+ meta?: PaginationMeta;
6
+ }
7
+
8
+ export interface PaginationMeta {
9
+ page: number;
10
+ limit: number;
11
+ total: number;
12
+ totalPages: number;
13
+ }
14
+
15
+ export interface ApiErrorResponse {
16
+ success: false;
17
+ message: string;
18
+ }
@@ -0,0 +1,39 @@
1
+ import { useMeQuery } from "@/features/auth/queries/auth.querie";
2
+ import { Navigate, Outlet, useLocation } from "react-router";
3
+
4
+ interface ProtectedRouteProps {
5
+ requiredRole?: string;
6
+ redirectTo?: string;
7
+ redirectRoleTo?: { role: string; to: string };
8
+ }
9
+
10
+ export default function ProtectedRoute({
11
+ requiredRole,
12
+ redirectTo = "/dashboard",
13
+ redirectRoleTo,
14
+ }: ProtectedRouteProps) {
15
+ const { data: user, isLoading } = useMeQuery();
16
+ const location = useLocation();
17
+
18
+ if (isLoading) {
19
+ return (
20
+ <div className="min-h-screen flex items-center justify-center">
21
+ <div className="size-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
22
+ </div>
23
+ );
24
+ }
25
+
26
+ if (!user) {
27
+ return <Navigate to={`/login?redirect=${encodeURIComponent(location.pathname)}`} replace />;
28
+ }
29
+
30
+ if (redirectRoleTo && user.role === redirectRoleTo.role) {
31
+ return <Navigate to={redirectRoleTo.to} replace />;
32
+ }
33
+
34
+ if (requiredRole && user.role !== requiredRole) {
35
+ return <Navigate to={redirectTo} replace />;
36
+ }
37
+
38
+ return <Outlet />;
39
+ }
@@ -0,0 +1,13 @@
1
+ import ProtectedRoute from "./protected-route";
2
+
3
+ export function AuthenticatedRoute() {
4
+ return <ProtectedRoute />;
5
+ }
6
+
7
+ export function UserRoute() {
8
+ return <ProtectedRoute redirectRoleTo={{ role: "ADMIN", to: "/dashboard/admin" }} />;
9
+ }
10
+
11
+ export function AdminRoute() {
12
+ return <ProtectedRoute requiredRole="ADMIN" redirectTo="/dashboard" />;
13
+ }
@@ -0,0 +1,3 @@
1
+ export default function Overview() {
2
+ return <div>This is the admin overview page.</div>;
3
+ }
@@ -0,0 +1,3 @@
1
+ export default function Overview() {
2
+ return <div>This is the user overview page.</div>;
3
+ }
@@ -0,0 +1,5 @@
1
+ import ForgotPasswordForm from "../components/forgot-password-form";
2
+
3
+ export default function ForgotPasswordPage() {
4
+ return <ForgotPasswordForm />;
5
+ }
@@ -0,0 +1,5 @@
1
+ import LoginForm from "../components/login-form";
2
+
3
+ export default function LoginPage() {
4
+ return <LoginForm />;
5
+ }
@@ -0,0 +1,5 @@
1
+ import MyProfile from "../components/my-profile";
2
+
3
+ export default function MyProfilePage() {
4
+ return <MyProfile />;
5
+ }
@@ -0,0 +1,59 @@
1
+ import { AUTH_QUERY_KEYS } from "@/features/auth/queries/auth.querie";
2
+ import { useQueryClient } from "@tanstack/react-query";
3
+ import { useEffect } from "react";
4
+ import { useNavigate, useSearchParams } from "react-router";
5
+
6
+ interface JwtPayload {
7
+ role?: string;
8
+ exp?: number;
9
+ }
10
+
11
+ function decodeJwt(token: string): JwtPayload | null {
12
+ try {
13
+ return JSON.parse(atob(token.split(".")[1])) as JwtPayload;
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
18
+
19
+ function setCookie(name: string, value: string, exp?: number) {
20
+ const expires = exp ? new Date(exp * 1000).toUTCString() : "";
21
+ document.cookie = `${name}=${value}; path=/; SameSite=Lax${expires ? `; expires=${expires}` : ""}`;
22
+ }
23
+
24
+ export default function OAuthCallbackPage() {
25
+ const navigate = useNavigate();
26
+ const queryClient = useQueryClient();
27
+ const [searchParams] = useSearchParams();
28
+
29
+ useEffect(() => {
30
+ const accessToken = searchParams.get("accessToken");
31
+ const refreshToken = searchParams.get("refreshToken");
32
+ const redirectPath = searchParams.get("redirect");
33
+
34
+ if (accessToken) {
35
+ const payload = decodeJwt(accessToken);
36
+ setCookie("accessToken", accessToken, payload?.exp);
37
+
38
+ if (refreshToken) {
39
+ const refreshPayload = decodeJwt(refreshToken);
40
+ setCookie("refreshToken", refreshToken, refreshPayload?.exp);
41
+ }
42
+
43
+ const defaultRoute = payload?.role === "ADMIN" ? "/dashboard/admin" : "/dashboard";
44
+ const destination = redirectPath || defaultRoute;
45
+
46
+ queryClient.invalidateQueries({ queryKey: AUTH_QUERY_KEYS.me }).then(() => {
47
+ navigate(destination, { replace: true });
48
+ });
49
+ } else {
50
+ navigate("/login", { replace: true });
51
+ }
52
+ }, [navigate, queryClient, searchParams]);
53
+
54
+ return (
55
+ <div className="min-h-screen flex items-center justify-center">
56
+ <div className="size-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
57
+ </div>
58
+ );
59
+ }
@@ -0,0 +1,5 @@
1
+ import RegisterForm from "../components/register-form";
2
+
3
+ export default function RegisterPage() {
4
+ return <RegisterForm />;
5
+ }
@@ -0,0 +1,10 @@
1
+ import { Suspense } from "react";
2
+ import ResetPasswordForm from "../components/reset-password-form";
3
+
4
+ export default function ResetPasswordPage() {
5
+ return (
6
+ <Suspense>
7
+ <ResetPasswordForm />
8
+ </Suspense>
9
+ );
10
+ }
@@ -0,0 +1,10 @@
1
+ import { Suspense } from "react";
2
+ import VerifyEmailForm from "../components/verify-email-form";
3
+
4
+ export default function VerifyEmailPage() {
5
+ return (
6
+ <Suspense>
7
+ <VerifyEmailForm />
8
+ </Suspense>
9
+ );
10
+ }
@@ -0,0 +1,54 @@
1
+ import DashboardHeader from "@/components/dashboard/dashboard-header";
2
+ import DashboardSidebar from "@/components/dashboard/dashboard-sidebar";
3
+ import { SidebarProvider } from "@/components/ui/sidebar";
4
+ import { useMeQuery } from "@/features/auth/queries/auth.querie";
5
+ import { sidebar } from "@/lib/constant/dashboard";
6
+ import { Navigate, Outlet, useLocation } from "react-router";
7
+
8
+ const AUTH_PATHS = ["/login", "/register", "/forgot-password", "/reset-password", "/verify-email"];
9
+
10
+ export default function DashboardLayout() {
11
+ const { data: user, isLoading } = useMeQuery();
12
+ const { pathname } = useLocation();
13
+
14
+ if (isLoading) {
15
+ return (
16
+ <div className="min-h-screen flex items-center justify-center">
17
+ <div className="size-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
18
+ </div>
19
+ );
20
+ }
21
+
22
+ if (user && AUTH_PATHS.includes(pathname)) {
23
+ const dest = user.role === "ADMIN" ? "/dashboard/admin" : "/dashboard";
24
+ return <Navigate to={dest} replace />;
25
+ }
26
+
27
+ if (!user) {
28
+ return (
29
+ <div className="min-h-screen flex items-center justify-center bg-background p-4">
30
+ <Outlet />
31
+ </div>
32
+ );
33
+ }
34
+
35
+ const role = user.role in sidebar ? (user.role as keyof typeof sidebar) : "USER";
36
+
37
+ return (
38
+ <SidebarProvider>
39
+ <div className="flex min-h-screen w-full">
40
+ <DashboardSidebar menu={sidebar[role]} user={user} />
41
+
42
+ <div className="flex flex-col flex-1">
43
+ <DashboardHeader role={role as "USER" | "ADMIN"} />
44
+
45
+ <main className="flex-1">
46
+ <div className="@container/main min-h-screen w-full px-4 py-4 lg:px-6">
47
+ <Outlet />
48
+ </div>
49
+ </main>
50
+ </div>
51
+ </div>
52
+ </SidebarProvider>
53
+ );
54
+ }
@@ -0,0 +1,68 @@
1
+ import { envVars } from "@/lib/env";
2
+ import axios, { type AxiosError, type AxiosResponse, type InternalAxiosRequestConfig } from "axios";
3
+
4
+ const axiosInstance = axios.create({
5
+ baseURL: envVars.API_URL,
6
+ withCredentials: true,
7
+ timeout: 30000,
8
+ headers: { "Content-Type": "application/json" },
9
+ });
10
+
11
+ let isRefreshing = false;
12
+ let refreshQueue: Array<{
13
+ resolve: (value: unknown) => void;
14
+ reject: (reason?: unknown) => void;
15
+ }> = [];
16
+
17
+ function processRefreshQueue(error: unknown) {
18
+ refreshQueue.forEach(({ resolve, reject }) => {
19
+ if (error) reject(error);
20
+ else resolve(null);
21
+ });
22
+ refreshQueue = [];
23
+ }
24
+
25
+ axiosInstance.interceptors.response.use(
26
+ (response: AxiosResponse) => response,
27
+ async (error: AxiosError) => {
28
+ const originalRequest = error.config as InternalAxiosRequestConfig & {
29
+ _retry?: boolean;
30
+ };
31
+
32
+ if (error.response?.status === 401 && !originalRequest._retry &&
33
+ !originalRequest.url?.includes("/v1/auth/refresh-token")) {
34
+ if (isRefreshing) {
35
+ return new Promise((resolve, reject) => {
36
+ refreshQueue.push({ resolve, reject });
37
+ }).then(() => axiosInstance(originalRequest));
38
+ }
39
+
40
+ originalRequest._retry = true;
41
+ isRefreshing = true;
42
+
43
+ try {
44
+ await axiosInstance.post("/v1/auth/refresh-token", {});
45
+ processRefreshQueue(null);
46
+ return axiosInstance(originalRequest);
47
+ } catch (refreshError) {
48
+ processRefreshQueue(refreshError);
49
+ if (typeof window !== "undefined" && !window.location.pathname.startsWith("/login")) {
50
+ window.location.href = "/login";
51
+ }
52
+ return Promise.reject(refreshError);
53
+ } finally {
54
+ isRefreshing = false;
55
+ }
56
+ }
57
+
58
+ const data = error.response?.data as {
59
+ message?: string;
60
+ error?: string;
61
+ } | null;
62
+ const message = data?.message ?? data?.error ?? error.message ?? "An error occurred";
63
+ return Promise.reject(new Error(message));
64
+ },
65
+ );
66
+
67
+ export { axiosInstance as api };
68
+ export default axiosInstance;
@@ -0,0 +1,25 @@
1
+ interface EnvVars {
2
+ APP_NAME: string;
3
+ APP_URL: string;
4
+ API_URL: string;
5
+ BETTER_AUTH_URL: string;
6
+ }
7
+
8
+ const loadEnvVars = (): EnvVars => {
9
+ const requiredVars: (keyof EnvVars)[] = ["APP_NAME", "APP_URL", "API_URL", "BETTER_AUTH_URL"];
10
+
11
+ for (const varName of requiredVars) {
12
+ if (!import.meta.env[`VITE_${varName}`]) {
13
+ console.warn(`Environment variable VITE_${varName} is not set. Using default value.`);
14
+ }
15
+ }
16
+
17
+ return {
18
+ APP_NAME: import.meta.env.VITE_APP_NAME || "StackKit",
19
+ APP_URL: import.meta.env.VITE_APP_URL || "http://localhost:3000",
20
+ API_URL: import.meta.env.VITE_API_URL || "http://localhost:5000/api",
21
+ BETTER_AUTH_URL: import.meta.env.VITE_BETTER_AUTH_URL || "http://localhost:5000",
22
+ };
23
+ };
24
+
25
+ export const envVars = loadEnvVars();