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,4 +1,5 @@
|
|
|
1
1
|
import status from "http-status";
|
|
2
|
+
import { Types } from "mongoose";
|
|
2
3
|
import { getMongoDb, mongoose } from "../../database/mongoose";
|
|
3
4
|
import { AppError } from "../../shared/errors/app-error";
|
|
4
5
|
|
|
@@ -27,6 +28,14 @@ export type AuthSessionDocument = {
|
|
|
27
28
|
expiresAt?: Date;
|
|
28
29
|
};
|
|
29
30
|
|
|
31
|
+
export type AuthVerificationDocument = {
|
|
32
|
+
identifier: string;
|
|
33
|
+
value: string;
|
|
34
|
+
expiresAt?: Date;
|
|
35
|
+
createdAt?: Date;
|
|
36
|
+
updatedAt?: Date;
|
|
37
|
+
};
|
|
38
|
+
|
|
30
39
|
export const getAuthCollections = async () => {
|
|
31
40
|
await mongoose();
|
|
32
41
|
|
|
@@ -36,6 +45,7 @@ export const getAuthCollections = async () => {
|
|
|
36
45
|
return {
|
|
37
46
|
users: db.collection<AuthUserDocument>("user"),
|
|
38
47
|
sessions: db.collection<AuthSessionDocument>("session"),
|
|
48
|
+
verifications: db.collection<AuthVerificationDocument>("verification"),
|
|
39
49
|
};
|
|
40
50
|
} catch {
|
|
41
51
|
throw new AppError(
|
|
@@ -47,5 +57,5 @@ export const getAuthCollections = async () => {
|
|
|
47
57
|
|
|
48
58
|
export const deleteAuthUserById = async (id: string) => {
|
|
49
59
|
const { users } = await getAuthCollections();
|
|
50
|
-
await users.deleteOne({ id });
|
|
60
|
+
await users.deleteOne({ _id: new Types.ObjectId(id) });
|
|
51
61
|
};
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
+
{{#if database == "prisma"}}
|
|
1
2
|
import { Role } from "@prisma/client";
|
|
3
|
+
{{/if}}
|
|
4
|
+
{{#if database == "mongoose"}}
|
|
5
|
+
import { Role } from "../modules/auth/auth.constants";
|
|
6
|
+
type AuthRole = (typeof Role)[keyof typeof Role];
|
|
7
|
+
{{/if}}
|
|
2
8
|
|
|
3
9
|
declare global {
|
|
4
10
|
namespace Express {
|
|
@@ -7,7 +13,12 @@ declare global {
|
|
|
7
13
|
id: string;
|
|
8
14
|
name: string;
|
|
9
15
|
email: string;
|
|
16
|
+
{{#if database == "prisma"}}
|
|
10
17
|
role: Role;
|
|
18
|
+
{{/if}}
|
|
19
|
+
{{#if database == "mongoose"}}
|
|
20
|
+
role: AuthRole | string;
|
|
21
|
+
{{/if}}
|
|
11
22
|
};
|
|
12
23
|
}
|
|
13
24
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
|
|
3
|
+
const SUPPORTED_PROVIDERS = [
|
|
4
|
+
"google",
|
|
5
|
+
"github",
|
|
6
|
+
"facebook",
|
|
7
|
+
"twitter",
|
|
8
|
+
"discord",
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
export async function GET(
|
|
12
|
+
request: NextRequest,
|
|
13
|
+
{ params }: { params: Promise<{ provider: string }> },
|
|
14
|
+
) {
|
|
15
|
+
const { provider } = await params;
|
|
16
|
+
|
|
17
|
+
if (!SUPPORTED_PROVIDERS.includes(provider)) {
|
|
18
|
+
return NextResponse.redirect(
|
|
19
|
+
new URL("/login?error=unsupported_provider", request.url),
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const { searchParams } = request.nextUrl;
|
|
24
|
+
|
|
25
|
+
const accessToken = searchParams.get("accessToken");
|
|
26
|
+
const refreshToken = searchParams.get("refreshToken");
|
|
27
|
+
const sessionToken = searchParams.get("token");
|
|
28
|
+
const redirectPath = searchParams.get("redirect") || "/dashboard";
|
|
29
|
+
|
|
30
|
+
// Prevent open-redirect attacks
|
|
31
|
+
const isValidRedirectPath =
|
|
32
|
+
redirectPath.startsWith("/") && !redirectPath.startsWith("//");
|
|
33
|
+
const finalRedirect = isValidRedirectPath ? redirectPath : "/dashboard";
|
|
34
|
+
|
|
35
|
+
if (!accessToken || !refreshToken) {
|
|
36
|
+
return NextResponse.redirect(
|
|
37
|
+
new URL("/login?error=oauth_failed", request.url),
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
42
|
+
const ONE_DAY_SECONDS = 60 * 60 * 24;
|
|
43
|
+
const SEVEN_DAYS_SECONDS = ONE_DAY_SECONDS * 7;
|
|
44
|
+
|
|
45
|
+
const response = NextResponse.redirect(new URL(finalRedirect, request.url));
|
|
46
|
+
|
|
47
|
+
response.cookies.set("accessToken", accessToken, {
|
|
48
|
+
httpOnly: true,
|
|
49
|
+
secure: isProduction,
|
|
50
|
+
sameSite: "lax",
|
|
51
|
+
path: "/",
|
|
52
|
+
maxAge: ONE_DAY_SECONDS,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
response.cookies.set("refreshToken", refreshToken, {
|
|
56
|
+
httpOnly: true,
|
|
57
|
+
secure: isProduction,
|
|
58
|
+
sameSite: "lax",
|
|
59
|
+
path: "/",
|
|
60
|
+
maxAge: SEVEN_DAYS_SECONDS,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (sessionToken) {
|
|
64
|
+
response.cookies.set("better-auth.session_token", sessionToken, {
|
|
65
|
+
httpOnly: true,
|
|
66
|
+
secure: isProduction,
|
|
67
|
+
sameSite: "lax",
|
|
68
|
+
path: "/",
|
|
69
|
+
maxAge: ONE_DAY_SECONDS,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return response;
|
|
74
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export const dynamic = "force-dynamic";
|
|
2
|
+
|
|
3
|
+
import DashboardHeader from "@/components/dashboard/dashboard-header";
|
|
4
|
+
import DashboardSidebar from "@/components/dashboard/dashboard-sidebar";
|
|
5
|
+
import { SidebarProvider } from "@/components/ui/sidebar";
|
|
6
|
+
import {
|
|
7
|
+
getSession,
|
|
8
|
+
} from "@/features/auth/services/auth.service";
|
|
9
|
+
import { sidebar } from "@/lib/constant/dashboard";
|
|
10
|
+
import { redirect } from "next/navigation";
|
|
11
|
+
|
|
12
|
+
type Role = keyof typeof sidebar;
|
|
13
|
+
|
|
14
|
+
export default async function layout({
|
|
15
|
+
children,
|
|
16
|
+
}: {
|
|
17
|
+
children: React.ReactNode;
|
|
18
|
+
}) {
|
|
19
|
+
const user = await getSession();
|
|
20
|
+
|
|
21
|
+
if (!user) {
|
|
22
|
+
redirect("/login");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const role = user?.role as Role | undefined;
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<SidebarProvider>
|
|
29
|
+
<div className="flex min-h-screen w-full">
|
|
30
|
+
{/* Sidebar */}
|
|
31
|
+
{role && <DashboardSidebar menu={sidebar[role]} user={user} />}
|
|
32
|
+
|
|
33
|
+
{/* Main area */}
|
|
34
|
+
<div className="flex flex-col flex-1">
|
|
35
|
+
{/* Header/Navbar */}
|
|
36
|
+
<DashboardHeader role={role} />
|
|
37
|
+
|
|
38
|
+
{/* Main content */}
|
|
39
|
+
<main className="flex-1">
|
|
40
|
+
<div className="@container/main min-h-screen w-full px-4 py-4 lg:px-6">
|
|
41
|
+
{children}
|
|
42
|
+
</div>
|
|
43
|
+
</main>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</SidebarProvider>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use server";
|
|
2
|
+
|
|
3
|
+
import { envVars } from "@/lib/env";
|
|
4
|
+
import { setTokenInCookies } from "@/lib/utils/token";
|
|
5
|
+
import { cookies } from "next/headers";
|
|
6
|
+
|
|
7
|
+
export async function getNewTokensWithRefreshToken(
|
|
8
|
+
refreshToken: string,
|
|
9
|
+
): Promise<boolean> {
|
|
10
|
+
try {
|
|
11
|
+
const res = await fetch(`${envVars.API_URL}/auth/refresh-token`, {
|
|
12
|
+
method: "POST",
|
|
13
|
+
headers: {
|
|
14
|
+
"Content-Type": "application/json",
|
|
15
|
+
Cookie: `refreshToken=${refreshToken}`,
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
if (!res.ok) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const { data } = await res.json();
|
|
24
|
+
|
|
25
|
+
const { accessToken, refreshToken: newRefreshToken, token } = data;
|
|
26
|
+
|
|
27
|
+
if (accessToken) {
|
|
28
|
+
await setTokenInCookies("accessToken", accessToken);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (newRefreshToken) {
|
|
32
|
+
await setTokenInCookies("refreshToken", newRefreshToken);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (token) {
|
|
36
|
+
await setTokenInCookies("better-auth.session_token", token, 24 * 60 * 60); // 1 day in seconds
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return true;
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.error("Error refreshing token:", error);
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function setTokens(tokens: {
|
|
47
|
+
accessToken?: string;
|
|
48
|
+
refreshToken?: string;
|
|
49
|
+
token?: string;
|
|
50
|
+
}) {
|
|
51
|
+
if (!tokens) return false;
|
|
52
|
+
|
|
53
|
+
if (tokens.accessToken) {
|
|
54
|
+
await setTokenInCookies("accessToken", tokens.accessToken);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (tokens.refreshToken) {
|
|
58
|
+
await setTokenInCookies("refreshToken", tokens.refreshToken);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (tokens.token) {
|
|
62
|
+
await setTokenInCookies(
|
|
63
|
+
"better-auth.session_token",
|
|
64
|
+
tokens.token,
|
|
65
|
+
24 * 60 * 60,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function getSession() {
|
|
73
|
+
try {
|
|
74
|
+
const cookieStore = await cookies();
|
|
75
|
+
const accessToken = cookieStore.get("accessToken")?.value;
|
|
76
|
+
const sessionToken = cookieStore.get("better-auth.session_token")?.value;
|
|
77
|
+
|
|
78
|
+
if (!accessToken) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const res = await fetch(`${envVars.API_URL}/v1/auth/me`, {
|
|
83
|
+
method: "GET",
|
|
84
|
+
headers: {
|
|
85
|
+
"Content-Type": "application/json",
|
|
86
|
+
Cookie: `accessToken=${accessToken}; better-auth.session_token=${sessionToken}`,
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (!res.ok) {
|
|
91
|
+
console.error("Failed to fetch user info:", res.status, res.statusText);
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const { data } = await res.json();
|
|
96
|
+
|
|
97
|
+
return data;
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error("Error fetching user info:", error);
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import Footer from "@/components/footer";
|
|
2
|
+
import Navbar from "@/components/navbar";
|
|
3
|
+
import React from "react";
|
|
4
|
+
|
|
5
|
+
export default function Layout({ children }: { children: React.ReactNode }) {
|
|
6
|
+
return (
|
|
7
|
+
<div className="min-h-screen flex flex-col">
|
|
8
|
+
<Navbar />
|
|
9
|
+
<main className="max-w-7xl mx-auto">{children}</main>
|
|
10
|
+
<Footer />
|
|
11
|
+
</div>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { getNewTokensWithRefreshToken } from "@/features/auth/services/auth.service";
|
|
2
|
+
import { ApiResponse } from "@/types/api.types";
|
|
3
|
+
import axios from "axios";
|
|
4
|
+
import { cookies, headers } from "next/headers";
|
|
5
|
+
import { envVars } from "../env";
|
|
6
|
+
import { isTokenExpiringSoon } from "../utils/token";
|
|
7
|
+
|
|
8
|
+
if (!envVars.API_URL) {
|
|
9
|
+
throw new Error("API_BASE_URL is not defined in environment variables");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function tryRefreshToken(
|
|
13
|
+
accessToken: string,
|
|
14
|
+
refreshToken: string,
|
|
15
|
+
): Promise<void> {
|
|
16
|
+
if (!(await isTokenExpiringSoon(accessToken))) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const requestHeader = await headers();
|
|
21
|
+
|
|
22
|
+
if (requestHeader.get("x-token-refreshed") === "1") {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
await getNewTokensWithRefreshToken(refreshToken);
|
|
28
|
+
} catch (error: unknown) {
|
|
29
|
+
console.error("Error refreshing token in http client:", error);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const axiosInstance = async () => {
|
|
34
|
+
const cookieStore = await cookies();
|
|
35
|
+
const accessToken = cookieStore.get("accessToken")?.value;
|
|
36
|
+
const refreshToken = cookieStore.get("refreshToken")?.value;
|
|
37
|
+
|
|
38
|
+
if (accessToken && refreshToken) {
|
|
39
|
+
await tryRefreshToken(accessToken, refreshToken);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const cookieHeader = cookieStore
|
|
43
|
+
.getAll()
|
|
44
|
+
.map((cookie) => `${cookie.name}=${cookie.value}`)
|
|
45
|
+
.join("; ");
|
|
46
|
+
|
|
47
|
+
const instance = axios.create({
|
|
48
|
+
baseURL: envVars.API_URL,
|
|
49
|
+
timeout: 30000,
|
|
50
|
+
headers: {
|
|
51
|
+
"Content-Type": "application/json",
|
|
52
|
+
Cookie: cookieHeader,
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return instance;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export interface ApiRequestOptions {
|
|
60
|
+
params?: Record<string, unknown>;
|
|
61
|
+
headers?: Record<string, string>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const httpGet = async <TData>(
|
|
65
|
+
endpoint: string,
|
|
66
|
+
options?: ApiRequestOptions,
|
|
67
|
+
): Promise<ApiResponse<TData>> => {
|
|
68
|
+
try {
|
|
69
|
+
const instance = await axiosInstance();
|
|
70
|
+
const response = await instance.get<ApiResponse<TData>>(endpoint, {
|
|
71
|
+
params: options?.params,
|
|
72
|
+
headers: options?.headers,
|
|
73
|
+
});
|
|
74
|
+
return response.data;
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error(`GET request to ${endpoint} failed:`, error);
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const httpPost = async <TData>(
|
|
82
|
+
endpoint: string,
|
|
83
|
+
data: unknown,
|
|
84
|
+
options?: ApiRequestOptions,
|
|
85
|
+
): Promise<ApiResponse<TData>> => {
|
|
86
|
+
try {
|
|
87
|
+
const instance = await axiosInstance();
|
|
88
|
+
const response = await instance.post<ApiResponse<TData>>(endpoint, data, {
|
|
89
|
+
params: options?.params,
|
|
90
|
+
headers: options?.headers,
|
|
91
|
+
});
|
|
92
|
+
return response.data;
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.error(`POST request to ${endpoint} failed:`, error);
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const httpPut = async <TData>(
|
|
100
|
+
endpoint: string,
|
|
101
|
+
data: unknown,
|
|
102
|
+
options?: ApiRequestOptions,
|
|
103
|
+
): Promise<ApiResponse<TData>> => {
|
|
104
|
+
try {
|
|
105
|
+
const instance = await axiosInstance();
|
|
106
|
+
const response = await instance.put<ApiResponse<TData>>(endpoint, data, {
|
|
107
|
+
params: options?.params,
|
|
108
|
+
headers: options?.headers,
|
|
109
|
+
});
|
|
110
|
+
return response.data;
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.error(`PUT request to ${endpoint} failed:`, error);
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const httpPatch = async <TData>(
|
|
118
|
+
endpoint: string,
|
|
119
|
+
data: unknown,
|
|
120
|
+
options?: ApiRequestOptions,
|
|
121
|
+
): Promise<ApiResponse<TData>> => {
|
|
122
|
+
try {
|
|
123
|
+
const instance = await axiosInstance();
|
|
124
|
+
const response = await instance.patch<ApiResponse<TData>>(endpoint, data, {
|
|
125
|
+
params: options?.params,
|
|
126
|
+
headers: options?.headers,
|
|
127
|
+
});
|
|
128
|
+
return response.data;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.error(`PATCH request to ${endpoint} failed:`, error);
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const httpDelete = async <TData>(
|
|
136
|
+
endpoint: string,
|
|
137
|
+
options?: ApiRequestOptions,
|
|
138
|
+
): Promise<ApiResponse<TData>> => {
|
|
139
|
+
try {
|
|
140
|
+
const instance = await axiosInstance();
|
|
141
|
+
const response = await instance.delete<ApiResponse<TData>>(endpoint, {
|
|
142
|
+
params: options?.params,
|
|
143
|
+
headers: options?.headers,
|
|
144
|
+
});
|
|
145
|
+
return response.data;
|
|
146
|
+
} catch (error) {
|
|
147
|
+
console.error(`DELETE request to ${endpoint} failed:`, error);
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export const api = {
|
|
153
|
+
get: httpGet,
|
|
154
|
+
post: httpPost,
|
|
155
|
+
put: httpPut,
|
|
156
|
+
patch: httpPatch,
|
|
157
|
+
delete: httpDelete,
|
|
158
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
interface EnvVars {
|
|
2
|
+
APP_NAME: string;
|
|
3
|
+
APP_URL: string;
|
|
4
|
+
API_URL: string;
|
|
5
|
+
BETTER_AUTH_URL: string;
|
|
6
|
+
JWT_ACCESS_SECRET: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const loadEnvVars = (): EnvVars => {
|
|
10
|
+
const requiredVars: (keyof EnvVars)[] = [
|
|
11
|
+
"APP_NAME",
|
|
12
|
+
"APP_URL",
|
|
13
|
+
"API_URL",
|
|
14
|
+
"BETTER_AUTH_URL",
|
|
15
|
+
"JWT_ACCESS_SECRET",
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
for (const varName of requiredVars) {
|
|
19
|
+
if (!process.env[`NEXT_PUBLIC_${varName}`]) {
|
|
20
|
+
console.warn(
|
|
21
|
+
`Environment variable NEXT_PUBLIC_${varName} is not set. Using default value.`,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
APP_NAME: process.env.NEXT_PUBLIC_APP_NAME || "App Name",
|
|
28
|
+
APP_URL: process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000",
|
|
29
|
+
API_URL: process.env.NEXT_PUBLIC_API_URL || "http://localhost:5000/api",
|
|
30
|
+
BETTER_AUTH_URL: process.env.NEXT_PUBLIC_BETTER_AUTH_URL || "http://localhost:5000",
|
|
31
|
+
JWT_ACCESS_SECRET: process.env.JWT_ACCESS_SECRET || "",
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const envVars = loadEnvVars();
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export type UserRole = "ADMIN" | "USER";
|
|
2
|
+
|
|
3
|
+
export const authRoutes = [
|
|
4
|
+
"/login",
|
|
5
|
+
"/register",
|
|
6
|
+
"/forgot-password",
|
|
7
|
+
"/reset-password",
|
|
8
|
+
"/verify-email",
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
export const isAuthRoute = (pathname: string) => {
|
|
12
|
+
return authRoutes.some((router: string) => router === pathname);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type RouteConfig = {
|
|
16
|
+
exact: string[];
|
|
17
|
+
pattern: RegExp[];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const commonProtectedRoutes: RouteConfig = {
|
|
21
|
+
exact: ["/dashboard/my-profile"],
|
|
22
|
+
pattern: [/^\/dashboard(?!\/admin)(\/.*)?$/],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const adminProtectedRoutes: RouteConfig = {
|
|
26
|
+
pattern: [/^\/dashboard\/admin/],
|
|
27
|
+
exact: [],
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const isRouteMatches = (pathname: string, routes: RouteConfig) => {
|
|
31
|
+
if (routes.exact.includes(pathname)) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
return routes.pattern.some((pattern: RegExp) => pattern.test(pathname));
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const getRouteOwner = (pathname: string): "ADMIN" | "COMMON" | null => {
|
|
38
|
+
if (isRouteMatches(pathname, adminProtectedRoutes)) {
|
|
39
|
+
return "ADMIN";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (isRouteMatches(pathname, commonProtectedRoutes)) {
|
|
43
|
+
return "COMMON";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return null;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const getDefaultDashboardRoute = (role: UserRole) => {
|
|
50
|
+
if (role === "ADMIN") {
|
|
51
|
+
return "/dashboard/admin";
|
|
52
|
+
}
|
|
53
|
+
if (role === "USER") {
|
|
54
|
+
return "/dashboard";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return "/";
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const isValidRedirectForRole = (
|
|
61
|
+
redirectPath: string,
|
|
62
|
+
role: UserRole,
|
|
63
|
+
) => {
|
|
64
|
+
const routeOwner = getRouteOwner(redirectPath);
|
|
65
|
+
|
|
66
|
+
if (routeOwner === null || routeOwner === "COMMON") {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (routeOwner === role) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return false;
|
|
75
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use server";
|
|
2
|
+
|
|
3
|
+
import { cookies } from "next/headers";
|
|
4
|
+
|
|
5
|
+
export const setCookie = async (
|
|
6
|
+
name: string,
|
|
7
|
+
value: string,
|
|
8
|
+
maxAgeInSeconds: number,
|
|
9
|
+
) => {
|
|
10
|
+
const cookieStore = await cookies();
|
|
11
|
+
|
|
12
|
+
cookieStore.set(name, value, {
|
|
13
|
+
httpOnly: true,
|
|
14
|
+
secure: true,
|
|
15
|
+
sameSite: "strict",
|
|
16
|
+
path: "/",
|
|
17
|
+
maxAge: maxAgeInSeconds,
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const getCookie = async (name: string) => {
|
|
22
|
+
const cookieStore = await cookies();
|
|
23
|
+
return cookieStore.get(name)?.value;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const deleteCookie = async (name: string) => {
|
|
27
|
+
const cookieStore = await cookies();
|
|
28
|
+
cookieStore.delete(name);
|
|
29
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import jwt, { JwtPayload } from "jsonwebtoken";
|
|
2
|
+
|
|
3
|
+
const verifyToken = (token: string, secret: string) => {
|
|
4
|
+
try {
|
|
5
|
+
const decoded = jwt.verify(token, secret) as JwtPayload;
|
|
6
|
+
return {
|
|
7
|
+
success: true,
|
|
8
|
+
data: decoded,
|
|
9
|
+
};
|
|
10
|
+
} catch (error: unknown) {
|
|
11
|
+
return {
|
|
12
|
+
success: false,
|
|
13
|
+
message:
|
|
14
|
+
error instanceof Error ? error.message : "Token verification failed",
|
|
15
|
+
error,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const decodedToken = (token: string) => {
|
|
21
|
+
const decoded = jwt.decode(token) as JwtPayload;
|
|
22
|
+
return decoded;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const jwtUtils = {
|
|
26
|
+
verifyToken,
|
|
27
|
+
decodedToken,
|
|
28
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use server";
|
|
2
|
+
|
|
3
|
+
import jwt, { JwtPayload } from "jsonwebtoken";
|
|
4
|
+
import { setCookie } from "./cookie";
|
|
5
|
+
|
|
6
|
+
const getTokenSecondsRemaining = (token: string): number => {
|
|
7
|
+
if (!token) return 0;
|
|
8
|
+
try {
|
|
9
|
+
const tokenPayload = jwt.decode(token) as JwtPayload;
|
|
10
|
+
|
|
11
|
+
if (tokenPayload && !tokenPayload.exp) {
|
|
12
|
+
return 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const remainingSeconds =
|
|
16
|
+
(tokenPayload.exp as number) - Math.floor(Date.now() / 1000);
|
|
17
|
+
|
|
18
|
+
return remainingSeconds > 0 ? remainingSeconds : 0;
|
|
19
|
+
} catch {
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const setTokenInCookies = async (
|
|
25
|
+
name: string,
|
|
26
|
+
token: string,
|
|
27
|
+
fallbackMaxAgeInSeconds = 60 * 60 * 24, // 1 days
|
|
28
|
+
) => {
|
|
29
|
+
let maxAgeInSeconds;
|
|
30
|
+
|
|
31
|
+
if (name !== "better-auth.session_token") {
|
|
32
|
+
maxAgeInSeconds = getTokenSecondsRemaining(token);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
await setCookie(name, token, maxAgeInSeconds || fallbackMaxAgeInSeconds);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export async function isTokenExpiringSoon(
|
|
39
|
+
token: string,
|
|
40
|
+
thresholdInSeconds = 300,
|
|
41
|
+
): Promise<boolean> {
|
|
42
|
+
const remainingSeconds = getTokenSecondsRemaining(token);
|
|
43
|
+
return remainingSeconds > 0 && remainingSeconds <= thresholdInSeconds;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function isTokenExpired(token: string): Promise<boolean> {
|
|
47
|
+
const remainingSeconds = getTokenSecondsRemaining(token);
|
|
48
|
+
return remainingSeconds === 0;
|
|
49
|
+
}
|