stackkit 0.3.5 → 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.
- package/README.md +50 -42
- package/dist/cli/add.js +122 -56
- package/dist/cli/create.d.ts +2 -0
- package/dist/cli/create.js +271 -95
- package/dist/cli/doctor.js +1 -0
- package/dist/cli/list.d.ts +1 -1
- package/dist/cli/list.js +6 -4
- package/dist/index.js +234 -191
- package/dist/lib/constants.d.ts +4 -0
- package/dist/lib/constants.js +4 -0
- package/dist/lib/discovery/module-discovery.d.ts +4 -0
- package/dist/lib/discovery/module-discovery.js +56 -0
- package/dist/lib/generation/code-generator.d.ts +11 -2
- package/dist/lib/generation/code-generator.js +42 -3
- package/dist/lib/generation/generator-utils.js +3 -1
- package/dist/lib/pm/package-manager.js +16 -13
- package/dist/lib/ui/logger.js +3 -2
- package/dist/lib/utils/path-resolver.d.ts +2 -0
- package/dist/lib/utils/path-resolver.js +8 -0
- package/dist/meta.json +8312 -0
- package/modules/auth/better-auth/files/{shared → express}/config/env.ts +48 -50
- package/modules/auth/better-auth/files/express/middlewares/authorize.ts +20 -1
- package/modules/auth/better-auth/files/express/modules/auth.controller.ts +349 -0
- package/modules/auth/better-auth/files/express/modules/{auth/auth.route.ts → auth.route.ts} +9 -4
- package/modules/auth/better-auth/files/express/modules/auth.service.ts +664 -0
- package/modules/auth/better-auth/files/express/modules/{auth/auth.type.ts → auth.type.ts} +22 -9
- package/modules/auth/better-auth/files/{shared/mongoose/auth/helper.ts → express/mongo-modules/auth.helper.ts} +11 -1
- package/modules/auth/better-auth/files/express/types/express.d.ts +11 -0
- package/modules/auth/better-auth/files/nextjs/api-route.ts +74 -0
- package/modules/auth/better-auth/files/nextjs/dashboard/pages/(user)/page.tsx +6 -0
- package/modules/auth/better-auth/files/nextjs/dashboard/pages/admin/page.tsx +6 -0
- package/modules/auth/better-auth/files/nextjs/dashboard/pages/layout.tsx +48 -0
- package/modules/auth/better-auth/files/nextjs/dashboard/pages/my-profile/page.tsx +5 -0
- package/modules/auth/better-auth/files/nextjs/features/services/auth.service.ts +102 -0
- package/modules/auth/better-auth/files/nextjs/layout/layout.tsx +13 -0
- package/modules/auth/better-auth/files/nextjs/lib/axios/http.ts +158 -0
- package/modules/auth/better-auth/files/nextjs/lib/env.ts +35 -0
- package/modules/auth/better-auth/files/nextjs/lib/utils/auth.ts +75 -0
- package/modules/auth/better-auth/files/nextjs/lib/utils/cookie.ts +29 -0
- package/modules/auth/better-auth/files/nextjs/lib/utils/jwt.ts +28 -0
- package/modules/auth/better-auth/files/nextjs/lib/utils/token.ts +49 -0
- package/modules/auth/better-auth/files/nextjs/pages/forgot-password/page.tsx +5 -0
- package/modules/auth/better-auth/files/nextjs/pages/layout.tsx +11 -0
- package/modules/auth/better-auth/files/nextjs/pages/login/page.tsx +9 -0
- package/modules/auth/better-auth/files/nextjs/pages/register/page.tsx +5 -0
- package/modules/auth/better-auth/files/nextjs/pages/reset-password/page.tsx +10 -0
- package/modules/auth/better-auth/files/nextjs/pages/verify-email/page.tsx +10 -0
- package/modules/auth/better-auth/files/nextjs/proxy.ts +154 -42
- package/modules/auth/better-auth/files/nextjs/theme/providers/theme-provider.tsx +11 -0
- package/modules/auth/better-auth/files/nextjs/types/api.types.ts +18 -0
- package/modules/auth/better-auth/files/react/components/protected-route.tsx +39 -0
- package/modules/auth/better-auth/files/react/components/route-guards.tsx +13 -0
- package/modules/auth/better-auth/files/react/dashboard/admin/pages/overview.tsx +3 -0
- package/modules/auth/better-auth/files/react/dashboard/pages/overview.tsx +3 -0
- package/modules/auth/better-auth/files/react/features/pages/forgot-password.tsx +5 -0
- package/modules/auth/better-auth/files/react/features/pages/login.tsx +5 -0
- package/modules/auth/better-auth/files/react/features/pages/my-profile.tsx +5 -0
- package/modules/auth/better-auth/files/react/features/pages/oauth-callback.tsx +59 -0
- package/modules/auth/better-auth/files/react/features/pages/register.tsx +5 -0
- package/modules/auth/better-auth/files/react/features/pages/reset-password.tsx +10 -0
- package/modules/auth/better-auth/files/react/features/pages/verify-email.tsx +10 -0
- package/modules/auth/better-auth/files/react/layout/dashboard-layout.tsx +54 -0
- package/modules/auth/better-auth/files/react/lib/axios/http.ts +68 -0
- package/modules/auth/better-auth/files/react/lib/env.ts +25 -0
- package/modules/auth/better-auth/files/react/router.tsx +73 -0
- package/modules/auth/better-auth/files/react/theme/components/providers/theme-provider-context.ts +13 -0
- package/modules/auth/better-auth/files/react/theme/components/providers/theme-provider.tsx +51 -0
- package/modules/auth/better-auth/files/react/theme/hooks/use-theme.ts +8 -0
- package/modules/auth/better-auth/files/shared/features/components/change-password-dialog.tsx +113 -0
- package/modules/auth/better-auth/files/shared/features/components/forgot-password-form.tsx +84 -0
- package/modules/auth/better-auth/files/shared/features/components/login-form.tsx +134 -0
- package/modules/auth/better-auth/files/shared/features/components/my-profile.tsx +147 -0
- package/modules/auth/better-auth/files/shared/features/components/profile-form.tsx +205 -0
- package/modules/auth/better-auth/files/shared/features/components/register-form.tsx +100 -0
- package/modules/auth/better-auth/files/shared/features/components/reset-password-form.tsx +111 -0
- package/modules/auth/better-auth/files/shared/features/components/social-login-buttons.tsx +47 -0
- package/modules/auth/better-auth/files/shared/features/components/user-profile-menu.tsx +106 -0
- package/modules/auth/better-auth/files/shared/features/components/verify-email-form.tsx +110 -0
- package/modules/auth/better-auth/files/shared/features/queries/auth.mutations.tsx +312 -0
- package/modules/auth/better-auth/files/shared/features/queries/auth.querie.ts +19 -0
- package/modules/auth/better-auth/files/shared/features/services/auth.api.ts +81 -0
- package/modules/auth/better-auth/files/shared/features/types/auth.type.ts +47 -0
- package/modules/auth/better-auth/files/shared/features/validators/change-password.validator.ts +18 -0
- package/modules/auth/better-auth/files/shared/features/validators/forgot.validator.ts +7 -0
- package/modules/auth/better-auth/files/shared/features/validators/login.validator.ts +14 -0
- package/modules/auth/better-auth/files/shared/features/validators/profile.validator.ts +8 -0
- package/modules/auth/better-auth/files/shared/features/validators/register.validator.ts +9 -0
- package/modules/auth/better-auth/files/shared/features/validators/reset.validator.ts +9 -0
- package/modules/auth/better-auth/files/shared/features/validators/verify.validator.ts +8 -0
- package/modules/auth/better-auth/files/shared/lib/auth-client.ts +2 -1
- package/modules/auth/better-auth/files/shared/lib/auth.ts +5 -19
- package/modules/auth/better-auth/files/shared/lib/constant/dashboard.ts +90 -0
- package/modules/auth/better-auth/files/shared/theme/mode-toggle.tsx +30 -0
- package/modules/auth/better-auth/files/shared/ui/shadcn/components/dashboard/dashboard-header.tsx +94 -0
- package/modules/auth/better-auth/files/shared/ui/shadcn/components/dashboard/dashboard-sidebar.tsx +255 -0
- package/modules/auth/better-auth/files/shared/ui/shadcn/components/footer.tsx +35 -0
- package/modules/auth/better-auth/files/shared/ui/shadcn/components/navbar.tsx +145 -0
- package/modules/auth/better-auth/files/shared/ui/shadcn/form-field/input-field.tsx +440 -0
- package/modules/auth/better-auth/files/shared/utils/email.ts +2 -17
- package/modules/auth/better-auth/generator.json +172 -51
- package/modules/auth/better-auth/module.json +2 -2
- package/modules/components/files/shared/hooks/use-file-upload.ts +412 -0
- package/modules/components/files/shared/lib/utils/url-helpers.ts +110 -0
- package/modules/components/files/shared/shadcn/dashboard/data-table-column-selector.tsx +52 -0
- package/modules/components/files/shared/shadcn/dashboard/data-table-footer.tsx +156 -0
- package/modules/components/files/shared/shadcn/dashboard/data-table.tsx +405 -0
- package/modules/components/files/shared/shadcn/global/form-field/input-field.tsx +440 -0
- package/modules/components/files/shared/shadcn/global/form-field/media-uploader-field.tsx +745 -0
- package/modules/components/files/shared/shadcn/global/form-field/multi-select-field.tsx +207 -0
- package/modules/components/files/shared/shadcn/global/form-field/select-field.tsx +247 -0
- package/modules/components/files/shared/shadcn/global/form-field/textarea-field.tsx +277 -0
- package/modules/components/files/shared/shadcn/global/form-field/tiptap-editor-field.tsx +35 -0
- package/modules/components/files/shared/shadcn/global/no-results.tsx +41 -0
- package/modules/components/files/shared/shadcn/tiptap-editor/editor-menu-bar.tsx +217 -0
- package/modules/components/files/shared/shadcn/tiptap-editor/tiptap-editor.tsx +104 -0
- package/modules/components/files/shared/url/load-more.tsx +93 -0
- package/modules/components/files/shared/url/search-bar.tsx +131 -0
- package/modules/components/files/shared/url/sort-select.tsx +118 -0
- package/modules/components/files/shared/url/url-tabs.tsx +77 -0
- package/modules/components/generator.json +109 -0
- package/modules/components/module.json +11 -0
- package/modules/database/mongoose/generator.json +3 -14
- package/modules/database/mongoose/module.json +2 -2
- package/modules/database/prisma/generator.json +6 -12
- package/modules/database/prisma/module.json +2 -2
- package/modules/storage/cloudinary/files/express/config/env.ts +65 -0
- package/modules/storage/cloudinary/files/express/config/media.ts +103 -0
- package/modules/storage/cloudinary/files/express/modules/media/media.controller.ts +59 -0
- package/modules/storage/cloudinary/files/express/modules/media/media.route.ts +29 -0
- package/modules/storage/cloudinary/files/express/modules/media/media.service.ts +113 -0
- package/modules/storage/cloudinary/files/express/modules/media/media.type.ts +32 -0
- package/modules/storage/cloudinary/generator.json +34 -0
- package/modules/storage/cloudinary/module.json +11 -0
- package/modules/ui/shadcn/generator.json +21 -0
- package/modules/ui/shadcn/module.json +11 -0
- package/package.json +24 -26
- package/templates/express/README.md +11 -16
- package/templates/express/src/config/env.ts +7 -5
- package/templates/nextjs/README.md +13 -18
- package/templates/nextjs/app/favicon.ico +0 -0
- package/templates/nextjs/app/layout.tsx +6 -4
- package/templates/nextjs/components/providers/query-provider.tsx +3 -0
- package/templates/nextjs/env.example +3 -1
- package/templates/nextjs/lib/axios/http.ts +23 -0
- package/templates/nextjs/lib/env.ts +7 -5
- package/templates/nextjs/package.json +2 -1
- package/templates/nextjs/template.json +1 -2
- package/templates/react/README.md +9 -14
- package/templates/react/index.html +1 -1
- package/templates/react/package.json +1 -1
- package/templates/react/src/assets/favicon.ico +0 -0
- package/templates/react/src/components/providers/query-provider.tsx +38 -0
- package/templates/react/src/{shared/components → components}/seo.tsx +4 -8
- package/templates/react/src/lib/axios/http.ts +24 -0
- package/templates/react/src/main.tsx +8 -11
- package/templates/react/src/{features/about/pages → pages}/about.tsx +1 -1
- package/templates/react/src/{features/home/pages → pages}/home.tsx +1 -1
- package/templates/react/src/router.tsx +6 -6
- package/templates/react/src/vite-env.d.ts +2 -1
- package/templates/react/template.json +0 -1
- package/templates/react/tsconfig.app.json +6 -0
- package/templates/react/tsconfig.json +7 -1
- package/templates/react/vite.config.ts +12 -0
- package/modules/auth/authjs/files/nextjs/api/auth/[...nextauth]/route.ts +0 -3
- package/modules/auth/authjs/files/nextjs/proxy.ts +0 -1
- package/modules/auth/authjs/files/shared/lib/auth.ts +0 -119
- package/modules/auth/authjs/files/shared/prisma/schema.prisma +0 -61
- package/modules/auth/authjs/generator.json +0 -64
- package/modules/auth/authjs/module.json +0 -13
- package/modules/auth/better-auth/files/express/modules/auth/auth.controller.ts +0 -264
- package/modules/auth/better-auth/files/express/modules/auth/auth.service.ts +0 -549
- package/modules/auth/better-auth/files/express/templates/google-redirect.ejs +0 -24
- package/modules/auth/better-auth/files/nextjs/api/auth/[...all]/route.ts +0 -4
- package/modules/auth/better-auth/files/nextjs/lib/auth/auth-guards.ts +0 -31
- package/modules/auth/better-auth/files/nextjs/templates/email-otp.tsx +0 -74
- package/templates/nextjs/lib/api/http.ts +0 -40
- package/templates/react/public/vite.svg +0 -1
- package/templates/react/src/app/layouts/dashboard-layout.tsx +0 -8
- package/templates/react/src/app/layouts/public-layout.tsx +0 -5
- package/templates/react/src/app/providers.tsx +0 -20
- package/templates/react/src/app/router.tsx +0 -21
- package/templates/react/src/assets/react.svg +0 -1
- package/templates/react/src/shared/api/http.ts +0 -39
- package/templates/react/src/shared/components/loading.tsx +0 -8
- package/templates/react/src/shared/lib/query-client.ts +0 -12
- package/templates/react/src/utils/storage.ts +0 -35
- package/templates/react/src/utils/utils.ts +0 -3
- /package/modules/auth/better-auth/files/{shared/mongoose/auth/constants.ts → express/mongo-modules/auth.constants.ts} +0 -0
- /package/templates/nextjs/app/{page.tsx → (public)/(root)/page.tsx} +0 -0
- /package/templates/react/src/{shared/components → components}/error-boundary.tsx +0 -0
- /package/templates/react/src/{shared/components → components}/layout.tsx +0 -0
- /package/templates/react/src/{shared/pages → pages}/not-found.tsx +0 -0
|
@@ -1,57 +1,169 @@
|
|
|
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";
|
|
1
14
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
15
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
role: "ADMIN",
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
if (data) {
|
|
17
|
-
isAuthenticated = true;
|
|
18
|
-
const role = data?.role;
|
|
19
|
-
isAdmin = role === "ADMIN";
|
|
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;
|
|
20
26
|
}
|
|
27
|
+
}
|
|
28
|
+
|
|
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;
|
|
34
|
+
|
|
35
|
+
const decodedAccessToken =
|
|
36
|
+
accessToken &&
|
|
37
|
+
jwtUtils.verifyToken(accessToken, envVars.JWT_ACCESS_SECRET as string)
|
|
38
|
+
.data;
|
|
39
|
+
|
|
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
|
+
}
|
|
21
50
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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);
|
|
30
83
|
}
|
|
31
84
|
|
|
32
|
-
return
|
|
85
|
+
return response;
|
|
33
86
|
}
|
|
34
|
-
return NextResponse.next();
|
|
35
|
-
}
|
|
36
87
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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");
|
|
44
96
|
|
|
45
|
-
|
|
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
|
+
}
|
|
46
163
|
}
|
|
47
164
|
|
|
48
165
|
export const config = {
|
|
49
166
|
matcher: [
|
|
50
|
-
"/((?!_next|
|
|
51
|
-
"/(api|trpc)(.*)",
|
|
52
|
-
"/login",
|
|
53
|
-
"/register",
|
|
54
|
-
"/signup",
|
|
55
|
-
"/dashboard/:path*",
|
|
167
|
+
"/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|.well-known).*)",
|
|
56
168
|
],
|
|
57
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,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,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();
|