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.
- 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 -52
- 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} +12 -7
- 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/constants.ts → express/mongo-modules/auth.constants.ts} +0 -1
- 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 +157 -22
- 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 +10 -29
- package/modules/auth/better-auth/files/shared/lib/constant/dashboard.ts +90 -0
- package/modules/auth/better-auth/files/shared/prisma/enums.prisma +0 -1
- 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 +20 -18
- package/modules/auth/better-auth/generator.json +174 -53
- 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 -537
- 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 -41
- package/modules/auth/better-auth/files/nextjs/templates/email-otp.tsx +0 -74
- package/templates/express/node_modules/.bin/acorn +0 -17
- package/templates/express/node_modules/.bin/eslint +0 -17
- package/templates/express/node_modules/.bin/tsc +0 -17
- package/templates/express/node_modules/.bin/tsserver +0 -17
- package/templates/express/node_modules/.bin/tsx +0 -17
- package/templates/nextjs/lib/api/http.ts +0 -40
- package/templates/nextjs/next-env.d.ts +0 -6
- package/templates/react/dist/assets/index-D4AHT4dU.js +0 -193
- package/templates/react/dist/assets/index-rpwj5ZOX.css +0 -1
- package/templates/react/dist/index.html +0 -14
- package/templates/react/dist/vite.svg +0 -1
- 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/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
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import dotenv from "dotenv";
|
|
2
|
+
import status from "http-status";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { AppError } from "../shared/errors/app-error";
|
|
5
|
+
|
|
6
|
+
dotenv.config({ path: path.join(process.cwd(), ".env") });
|
|
7
|
+
|
|
8
|
+
interface EnvConfig {
|
|
9
|
+
NODE_ENV: string;
|
|
10
|
+
PORT: string;
|
|
11
|
+
APP_NAME?: string;
|
|
12
|
+
APP_URL: string;
|
|
13
|
+
{{#if database}}
|
|
14
|
+
DATABASE_URL: string;
|
|
15
|
+
{{/if}}
|
|
16
|
+
CLOUDINARY: {
|
|
17
|
+
CLOUDINARY_CLOUD_NAME: string;
|
|
18
|
+
CLOUDINARY_API_KEY: string;
|
|
19
|
+
CLOUDINARY_API_SECRET: string;
|
|
20
|
+
CLOUDINARY_UPLOAD_PRESET?: string;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const loadEnvVars = (): EnvConfig => {
|
|
25
|
+
const requiredEnvVars = [
|
|
26
|
+
"NODE_ENV",
|
|
27
|
+
"PORT",
|
|
28
|
+
"APP_NAME",
|
|
29
|
+
"APP_URL",
|
|
30
|
+
{{#if database}}
|
|
31
|
+
"DATABASE_URL",
|
|
32
|
+
{{/if}}
|
|
33
|
+
"CLOUDINARY_CLOUD_NAME",
|
|
34
|
+
"CLOUDINARY_API_KEY",
|
|
35
|
+
"CLOUDINARY_API_SECRET",
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
requiredEnvVars.forEach((varName) => {
|
|
39
|
+
if (!process.env[varName]) {
|
|
40
|
+
throw new AppError(
|
|
41
|
+
status.INTERNAL_SERVER_ERROR,
|
|
42
|
+
`Environment variable ${varName} is required but not set in .env file.`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
NODE_ENV: process.env.NODE_ENV as string,
|
|
49
|
+
PORT: process.env.PORT as string,
|
|
50
|
+
APP_NAME: process.env.APP_NAME ?? "Your App",
|
|
51
|
+
APP_URL: process.env.APP_URL as string,
|
|
52
|
+
{{#if database}}
|
|
53
|
+
DATABASE_URL: process.env.DATABASE_URL as string,
|
|
54
|
+
{{/if}}
|
|
55
|
+
CLOUDINARY: {
|
|
56
|
+
CLOUDINARY_CLOUD_NAME: process.env.CLOUDINARY_CLOUD_NAME as string,
|
|
57
|
+
CLOUDINARY_API_KEY: process.env.CLOUDINARY_API_KEY as string,
|
|
58
|
+
CLOUDINARY_API_SECRET: process.env.CLOUDINARY_API_SECRET as string,
|
|
59
|
+
CLOUDINARY_UPLOAD_PRESET:
|
|
60
|
+
process.env.CLOUDINARY_UPLOAD_PRESET as string,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const envVars = loadEnvVars();
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { v2 as cloudinary, UploadApiResponse } from "cloudinary";
|
|
2
|
+
import status from "http-status";
|
|
3
|
+
import { AppError } from "../shared/errors/app-error";
|
|
4
|
+
import { envVars } from "./env";
|
|
5
|
+
|
|
6
|
+
cloudinary.config({
|
|
7
|
+
cloud_name: envVars.CLOUDINARY.CLOUDINARY_CLOUD_NAME,
|
|
8
|
+
api_key: envVars.CLOUDINARY.CLOUDINARY_API_KEY,
|
|
9
|
+
api_secret: envVars.CLOUDINARY.CLOUDINARY_API_SECRET,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const uploadFileToCloudinary = async (
|
|
13
|
+
buffer: Buffer,
|
|
14
|
+
fileName: string,
|
|
15
|
+
): Promise<UploadApiResponse> => {
|
|
16
|
+
if (!buffer || !fileName) {
|
|
17
|
+
throw new AppError(
|
|
18
|
+
status.BAD_REQUEST,
|
|
19
|
+
"File buffer and file name are required for upload",
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const extension = fileName.split(".").pop()?.toLocaleLowerCase();
|
|
24
|
+
|
|
25
|
+
const fileNameWithoutExtension = fileName
|
|
26
|
+
.split(".")
|
|
27
|
+
.slice(0, -1)
|
|
28
|
+
.join(".")
|
|
29
|
+
.toLowerCase()
|
|
30
|
+
.replace(/\s+/g, "-")
|
|
31
|
+
// eslint-disable-next-line no-useless-escape
|
|
32
|
+
.replace(/[^a-z0-9\-]/g, "");
|
|
33
|
+
|
|
34
|
+
const uniqueName =
|
|
35
|
+
Math.random().toString(36).substring(2) +
|
|
36
|
+
"-" +
|
|
37
|
+
Date.now() +
|
|
38
|
+
"-" +
|
|
39
|
+
fileNameWithoutExtension;
|
|
40
|
+
|
|
41
|
+
const folder =
|
|
42
|
+
extension === "pdf" ? "pdfs" : extension === "mp4" ? "videos" : "images";
|
|
43
|
+
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
cloudinary.uploader
|
|
46
|
+
.upload_stream(
|
|
47
|
+
{
|
|
48
|
+
resource_type: "auto",
|
|
49
|
+
public_id: `app/${folder}/${uniqueName}`,
|
|
50
|
+
folder: `app/${folder}`,
|
|
51
|
+
},
|
|
52
|
+
(error, result) => {
|
|
53
|
+
if (error) {
|
|
54
|
+
return reject(
|
|
55
|
+
new AppError(
|
|
56
|
+
status.INTERNAL_SERVER_ERROR,
|
|
57
|
+
"Failed to upload file to Cloudinary",
|
|
58
|
+
),
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
resolve(result as UploadApiResponse);
|
|
62
|
+
},
|
|
63
|
+
)
|
|
64
|
+
.end(buffer);
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const deleteFileFromCloudinary = async (url: string) => {
|
|
69
|
+
try {
|
|
70
|
+
const regex = /\/v\d+\/(.+?)(?:\.[a-zA-Z0-9]+)+$/;
|
|
71
|
+
|
|
72
|
+
const match = url.match(regex);
|
|
73
|
+
|
|
74
|
+
if (match && match[1]) {
|
|
75
|
+
const publicId = match[1];
|
|
76
|
+
|
|
77
|
+
// try several resource types to ensure deletion for image/video/raw
|
|
78
|
+
const tryTypes: Array<"image" | "video" | "raw"> = [
|
|
79
|
+
"image",
|
|
80
|
+
"video",
|
|
81
|
+
"raw",
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
for (const t of tryTypes) {
|
|
85
|
+
try {
|
|
86
|
+
await cloudinary.uploader.destroy(publicId, { resource_type: t });
|
|
87
|
+
} catch {
|
|
88
|
+
// continue trying other types
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
console.log(`File ${publicId} deleted from cloudinary`);
|
|
93
|
+
}
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error("Error deleting file from Cloudinary:", error);
|
|
96
|
+
throw new AppError(
|
|
97
|
+
status.INTERNAL_SERVER_ERROR,
|
|
98
|
+
"Failed to delete file from Cloudinary",
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export const cloudinaryUpload = cloudinary;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Request, Response } from "express";
|
|
2
|
+
import status from "http-status";
|
|
3
|
+
import { catchAsync } from "../../shared/utils/catch-async";
|
|
4
|
+
import { sendResponse } from "../../shared/utils/send-response";
|
|
5
|
+
import { mediaService } from "./media.service";
|
|
6
|
+
import {
|
|
7
|
+
MediaSignInput,
|
|
8
|
+
mediaSignSchema,
|
|
9
|
+
mediaUploadDeleteSchema,
|
|
10
|
+
mediaUploadPresignSchema,
|
|
11
|
+
} from "./media.type";
|
|
12
|
+
|
|
13
|
+
const signMedia = catchAsync(async (req: Request, res: Response) => {
|
|
14
|
+
const publicId = req.params.publicId as string;
|
|
15
|
+
const transformation = (req.query.transformation as string) || undefined;
|
|
16
|
+
|
|
17
|
+
const payload = mediaSignSchema.parse({
|
|
18
|
+
publicId,
|
|
19
|
+
transformation,
|
|
20
|
+
}) as MediaSignInput;
|
|
21
|
+
const result = await mediaService.signMedia(payload);
|
|
22
|
+
|
|
23
|
+
sendResponse(res, {
|
|
24
|
+
status: status.OK,
|
|
25
|
+
success: true,
|
|
26
|
+
message: "Cloudinary URL generated",
|
|
27
|
+
data: result,
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const createPresign = catchAsync(async (req: Request, res: Response) => {
|
|
32
|
+
const payload = mediaUploadPresignSchema.parse(req.body);
|
|
33
|
+
const result = await mediaService.createMediaPresign(payload);
|
|
34
|
+
|
|
35
|
+
sendResponse(res, {
|
|
36
|
+
status: status.OK,
|
|
37
|
+
success: true,
|
|
38
|
+
message: "Presigned upload created",
|
|
39
|
+
data: result,
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const deleteUploads = catchAsync(async (req: Request, res: Response) => {
|
|
44
|
+
const payload = mediaUploadDeleteSchema.parse(req.body);
|
|
45
|
+
const result = await mediaService.deleteMediaUploads(payload);
|
|
46
|
+
|
|
47
|
+
sendResponse(res, {
|
|
48
|
+
status: status.OK,
|
|
49
|
+
success: true,
|
|
50
|
+
message: "Uploads deleted",
|
|
51
|
+
data: result,
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
export const mediaController = {
|
|
56
|
+
signMedia,
|
|
57
|
+
createPresign,
|
|
58
|
+
deleteUploads,
|
|
59
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{{#if database == "prisma"}}
|
|
2
|
+
import { Role } from "@prisma/client";
|
|
3
|
+
{{/if}}
|
|
4
|
+
{{#if database == "mongoose"}}
|
|
5
|
+
import { Role } from "../../modules/auth/auth.constants";
|
|
6
|
+
{{/if}}
|
|
7
|
+
import { Router } from "express";
|
|
8
|
+
import { authorize } from "../../shared/middlewares/authorize.middleware";
|
|
9
|
+
import { mediaController } from "./media.controller";
|
|
10
|
+
|
|
11
|
+
const router = Router();
|
|
12
|
+
|
|
13
|
+
router.get(
|
|
14
|
+
"/:publicId/transform",
|
|
15
|
+
authorize(Role.ADMIN, Role.USER),
|
|
16
|
+
mediaController.signMedia,
|
|
17
|
+
);
|
|
18
|
+
router.post(
|
|
19
|
+
"/upload/presign",
|
|
20
|
+
authorize(Role.ADMIN, Role.USER),
|
|
21
|
+
mediaController.createPresign,
|
|
22
|
+
);
|
|
23
|
+
router.post(
|
|
24
|
+
"/upload/delete",
|
|
25
|
+
authorize(Role.ADMIN, Role.USER),
|
|
26
|
+
mediaController.deleteUploads,
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
export const mediaRoutes = router;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { envVars } from "../../config/env";
|
|
2
|
+
import { cloudinaryUpload, deleteFileFromCloudinary } from "../../config/media";
|
|
3
|
+
import {
|
|
4
|
+
MediaSignInput,
|
|
5
|
+
MediaUploadDeleteInput,
|
|
6
|
+
MediaUploadPresignInput,
|
|
7
|
+
} from "./media.type";
|
|
8
|
+
|
|
9
|
+
const signMedia = async (payload: MediaSignInput) => {
|
|
10
|
+
const { publicId, transformation } = payload;
|
|
11
|
+
|
|
12
|
+
const options = transformation ? { transformation } : undefined;
|
|
13
|
+
|
|
14
|
+
const url = cloudinaryUpload.url(publicId, options);
|
|
15
|
+
|
|
16
|
+
return { url };
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const createMediaPresign = async (payload: MediaUploadPresignInput) => {
|
|
20
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
21
|
+
const paramsToSign: Record<string, string | number> = { timestamp };
|
|
22
|
+
|
|
23
|
+
if (payload.folder) paramsToSign.folder = payload.folder;
|
|
24
|
+
if (payload.publicId) paramsToSign.public_id = payload.publicId;
|
|
25
|
+
if (payload.resourceType && payload.resourceType !== "auto")
|
|
26
|
+
paramsToSign.resource_type = payload.resourceType;
|
|
27
|
+
|
|
28
|
+
const resourceType = payload.resourceType || "auto";
|
|
29
|
+
|
|
30
|
+
// Unsigned flow: return upload_preset if configured
|
|
31
|
+
if (payload.unsigned) {
|
|
32
|
+
const upload_preset =
|
|
33
|
+
envVars.CLOUDINARY.CLOUDINARY_UPLOAD_PRESET ?? undefined;
|
|
34
|
+
const uploadUrl = `https://api.cloudinary.com/v1_1/${envVars.CLOUDINARY.CLOUDINARY_CLOUD_NAME}/${resourceType}/upload`;
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
uploadUrl,
|
|
38
|
+
upload_preset,
|
|
39
|
+
unsigned: true,
|
|
40
|
+
folder: payload.folder,
|
|
41
|
+
publicId: payload.publicId,
|
|
42
|
+
resourceType: payload.resourceType,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const signature = cloudinaryUpload.utils.api_sign_request(
|
|
47
|
+
paramsToSign,
|
|
48
|
+
envVars.CLOUDINARY.CLOUDINARY_API_SECRET,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const uploadUrl = `https://api.cloudinary.com/v1_1/${envVars.CLOUDINARY.CLOUDINARY_CLOUD_NAME}/${resourceType}/upload`;
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
uploadUrl,
|
|
55
|
+
api_key: envVars.CLOUDINARY.CLOUDINARY_API_KEY,
|
|
56
|
+
timestamp,
|
|
57
|
+
signature,
|
|
58
|
+
folder: payload.folder,
|
|
59
|
+
publicId: payload.publicId,
|
|
60
|
+
resourceType: payload.resourceType,
|
|
61
|
+
unsigned: false,
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const deleteMediaUploads = async (payload: MediaUploadDeleteInput) => {
|
|
66
|
+
const keys = payload.keys ?? [];
|
|
67
|
+
const urls = payload.urls ?? [];
|
|
68
|
+
|
|
69
|
+
const deleted: string[] = [];
|
|
70
|
+
const failed: string[] = [];
|
|
71
|
+
|
|
72
|
+
for (const key of keys) {
|
|
73
|
+
try {
|
|
74
|
+
// try destroy across possible types
|
|
75
|
+
const tryTypes: Array<"image" | "video" | "raw"> = [
|
|
76
|
+
"image",
|
|
77
|
+
"video",
|
|
78
|
+
"raw",
|
|
79
|
+
];
|
|
80
|
+
let ok = false;
|
|
81
|
+
for (const t of tryTypes) {
|
|
82
|
+
try {
|
|
83
|
+
await cloudinaryUpload.uploader.destroy(key, { resource_type: t });
|
|
84
|
+
ok = true;
|
|
85
|
+
break;
|
|
86
|
+
} catch {
|
|
87
|
+
// try next
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (ok) deleted.push(key);
|
|
91
|
+
else failed.push(key);
|
|
92
|
+
} catch {
|
|
93
|
+
failed.push(key);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
for (const url of urls) {
|
|
98
|
+
try {
|
|
99
|
+
await deleteFileFromCloudinary(url);
|
|
100
|
+
deleted.push(url);
|
|
101
|
+
} catch {
|
|
102
|
+
failed.push(url);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return { deleted, failed };
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const mediaService = {
|
|
110
|
+
signMedia,
|
|
111
|
+
createMediaPresign,
|
|
112
|
+
deleteMediaUploads,
|
|
113
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const mediaSignSchema = z.object({
|
|
4
|
+
publicId: z.string().min(1),
|
|
5
|
+
transformation: z.string().min(1).optional(),
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export const mediaUploadPresignSchema = z.object({
|
|
9
|
+
folder: z.string().min(1).optional(),
|
|
10
|
+
publicId: z.string().min(1).optional(),
|
|
11
|
+
resourceType: z
|
|
12
|
+
.enum(["image", "video", "raw", "auto"] as const)
|
|
13
|
+
.optional()
|
|
14
|
+
.default("auto"),
|
|
15
|
+
unsigned: z.boolean().optional().default(false),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const mediaUploadDeleteSchema = z
|
|
19
|
+
.object({
|
|
20
|
+
keys: z.array(z.string().min(1)).optional(),
|
|
21
|
+
urls: z.array(z.string().min(1)).optional(),
|
|
22
|
+
})
|
|
23
|
+
.refine(
|
|
24
|
+
(value) => (value.keys?.length ?? 0) > 0 || (value.urls?.length ?? 0) > 0,
|
|
25
|
+
{
|
|
26
|
+
message: "keys or urls required",
|
|
27
|
+
},
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
export type MediaSignInput = z.infer<typeof mediaSignSchema>;
|
|
31
|
+
export type MediaUploadPresignInput = z.infer<typeof mediaUploadPresignSchema>;
|
|
32
|
+
export type MediaUploadDeleteInput = z.infer<typeof mediaUploadDeleteSchema>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cloudinary",
|
|
3
|
+
"type": "storage",
|
|
4
|
+
"priority": 5,
|
|
5
|
+
"operations": [
|
|
6
|
+
{
|
|
7
|
+
"type": "create-file",
|
|
8
|
+
"source": "express/config/*",
|
|
9
|
+
"destination": "src/config/*",
|
|
10
|
+
"condition": { "framework": "express" }
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"type": "create-file",
|
|
14
|
+
"source": "express/modules/media/*",
|
|
15
|
+
"destination": "src/modules/media/*",
|
|
16
|
+
"condition": { "framework": "express" }
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"type": "add-dependency",
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"cloudinary": "^1.39.0"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"type": "add-env",
|
|
26
|
+
"envVars": {
|
|
27
|
+
"CLOUDINARY_CLOUD_NAME": "your_cloud_name",
|
|
28
|
+
"CLOUDINARY_API_KEY": "your_api_key",
|
|
29
|
+
"CLOUDINARY_API_SECRET": "your_api_secret",
|
|
30
|
+
"CLOUDINARY_UPLOAD_PRESET": "your_upload_preset"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cloudinary",
|
|
3
|
+
"displayName": "Cloudinary Storage",
|
|
4
|
+
"category": "storage",
|
|
5
|
+
"description": "Cloudinary provider for uploads",
|
|
6
|
+
"supportedFrameworks": ["express"],
|
|
7
|
+
"compatibility": {
|
|
8
|
+
"frameworks": ["express"],
|
|
9
|
+
"languages": ["typescript", "javascript"]
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "shadcn",
|
|
3
|
+
"type": "ui",
|
|
4
|
+
"priority": 5,
|
|
5
|
+
"operations": [
|
|
6
|
+
{
|
|
7
|
+
"type": "run-command",
|
|
8
|
+
"command": "{{packageManager=='pnpm' ? 'pnpm dlx shadcn@latest init -t next -d -y' : packageManager=='yarn' ? 'yarn dlx shadcn@latest init -t next -d -y' : packageManager=='bun' ? 'bunx shadcn@latest init -t next -d -y' : 'npx shadcn@latest init -t next -d -y'}}",
|
|
9
|
+
"condition": { "framework": "nextjs" }
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"type": "run-command",
|
|
13
|
+
"command": "{{packageManager=='pnpm' ? 'pnpm dlx shadcn@latest init -t vite -d -y' : packageManager=='yarn' ? 'yarn dlx shadcn@latest init -t vite -d -y' : packageManager=='bun' ? 'bunx shadcn@latest init -t vite -d -y' : 'npx shadcn@latest init -t vite -d -y'}}",
|
|
14
|
+
"condition": { "framework": "react" }
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"type": "run-command",
|
|
18
|
+
"command": "{{packageManager=='pnpm' ? 'pnpm dlx shadcn@latest add button -y' : packageManager=='yarn' ? 'yarn dlx shadcn@latest add button -y' : packageManager=='bun' ? 'bun dlx shadcn@latest add button -y' : 'npx shadcn@latest add button -y'}}"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "shadcn",
|
|
3
|
+
"displayName": "Shadcn UI",
|
|
4
|
+
"category": "ui",
|
|
5
|
+
"description": "Shadcn-style component set + tailwind helpers",
|
|
6
|
+
"supportedFrameworks": ["react", "nextjs"],
|
|
7
|
+
"compatibility": {
|
|
8
|
+
"frameworks": ["react", "nextjs"],
|
|
9
|
+
"languages": ["typescript", "javascript"]
|
|
10
|
+
}
|
|
11
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stackkit",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"description": "Production-ready CLI to create and extend JavaScript or TypeScript apps with modular stacks.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
},
|
|
25
25
|
"scripts": {
|
|
26
26
|
"dev": "tsc --watch",
|
|
27
|
-
"
|
|
27
|
+
"bundle": "node ./scripts/bundle.js",
|
|
28
|
+
"build": "npm run clean && tsc && npm run copy-assets && npm run bundle",
|
|
28
29
|
"copy-assets": "node ./scripts/copy-assets.js",
|
|
29
30
|
"clean": "node ./scripts/clean.js",
|
|
30
31
|
"typecheck": "tsc --noEmit",
|
|
@@ -52,32 +53,29 @@
|
|
|
52
53
|
],
|
|
53
54
|
"author": "Tariqul Islam",
|
|
54
55
|
"license": "MIT",
|
|
55
|
-
"engines": {
|
|
56
|
-
"node": ">=18"
|
|
57
|
-
},
|
|
58
|
-
"dependencies": {
|
|
59
|
-
"chalk": "^4.1.2",
|
|
60
|
-
"commander": "^12.0.0",
|
|
61
|
-
"detect-package-manager": "^3.0.1",
|
|
62
|
-
"execa": "^5.1.1",
|
|
63
|
-
"fs-extra": "^11.2.0",
|
|
64
|
-
"inquirer": "^9.3.8",
|
|
65
|
-
"ora": "^5.4.1",
|
|
66
|
-
"validate-npm-package-name": "^5.0.1",
|
|
67
|
-
"@babel/core": "^7.28.5",
|
|
68
|
-
"@babel/preset-env": "^7.28.5",
|
|
69
|
-
"@babel/preset-typescript": "^7.28.5",
|
|
70
|
-
"@babel/preset-react": "^7.28.5",
|
|
71
|
-
"recast": "^0.20.5",
|
|
72
|
-
"@babel/parser": "^7.28.5",
|
|
73
|
-
"@babel/plugin-transform-typescript": "^7.28.5",
|
|
74
|
-
"@babel/plugin-transform-react-jsx": "^7.27.1"
|
|
75
|
-
},
|
|
76
56
|
"devDependencies": {
|
|
57
|
+
"@babel/core": "^7.29.0",
|
|
58
|
+
"@babel/parser": "^7.29.2",
|
|
59
|
+
"@babel/plugin-transform-typescript": "^7.28.6",
|
|
60
|
+
"@babel/preset-env": "^7.29.2",
|
|
61
|
+
"@babel/preset-typescript": "^7.28.5",
|
|
77
62
|
"@types/fs-extra": "^11.0.4",
|
|
78
|
-
"@types/
|
|
79
|
-
"@types/
|
|
63
|
+
"@types/node": "^25.5.0",
|
|
64
|
+
"@types/prompts": "^2.4.9",
|
|
80
65
|
"@types/validate-npm-package-name": "^4.0.2",
|
|
81
|
-
"
|
|
66
|
+
"chalk": "^5.6.2",
|
|
67
|
+
"commander": "^14.0.3",
|
|
68
|
+
"detect-package-manager": "^3.0.2",
|
|
69
|
+
"esbuild": "^0.27.4",
|
|
70
|
+
"execa": "^9.6.1",
|
|
71
|
+
"fs-extra": "^11.3.4",
|
|
72
|
+
"ora": "^9.3.0",
|
|
73
|
+
"prompts": "^2.4.2",
|
|
74
|
+
"recast": "^0.23.11",
|
|
75
|
+
"typescript": "^6.0.2",
|
|
76
|
+
"validate-npm-package-name": "^7.0.2"
|
|
77
|
+
},
|
|
78
|
+
"engines": {
|
|
79
|
+
"node": ">=18"
|
|
82
80
|
}
|
|
83
81
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# Express
|
|
1
|
+
# Express API
|
|
2
2
|
|
|
3
|
-
Production-ready Express (TypeScript) starter for building REST APIs
|
|
3
|
+
Production-ready Express (TypeScript) starter for building REST APIs, generated by [StackKit](https://github.com/tariqul420/stackkit).
|
|
4
4
|
|
|
5
5
|
## Requirements
|
|
6
6
|
|
|
@@ -23,13 +23,15 @@ npm run dev
|
|
|
23
23
|
|
|
24
24
|
## Scripts
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
| Command | Description |
|
|
27
|
+
| ------------ | ------------------------ |
|
|
28
|
+
| `pnpm dev` | Start development server |
|
|
29
|
+
| `pnpm build` | Compile TypeScript |
|
|
30
|
+
| `pnpm start` | Start production server |
|
|
29
31
|
|
|
30
32
|
## Environment
|
|
31
33
|
|
|
32
|
-
|
|
34
|
+
Copy `.env.example` to `.env` and fill in your credentials. Do not commit secrets.
|
|
33
35
|
|
|
34
36
|
## Recommended Folder & File Structure
|
|
35
37
|
|
|
@@ -47,8 +49,8 @@ express-api/
|
|
|
47
49
|
│
|
|
48
50
|
│ ├── database/
|
|
49
51
|
│ │ └── prisma.ts # PrismaClient singleton
|
|
50
|
-
│
|
|
51
|
-
│ ├── lib/
|
|
52
|
+
│
|
|
53
|
+
│ ├── lib/
|
|
52
54
|
│ └── auth.ts # Auth server config
|
|
53
55
|
│
|
|
54
56
|
│ ├── shared/
|
|
@@ -114,11 +116,4 @@ express-api/
|
|
|
114
116
|
|
|
115
117
|
---
|
|
116
118
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
This project was scaffolded using **StackKit** — a CLI toolkit for building production-ready applications.
|
|
120
|
-
|
|
121
|
-
- Generated via: `npx stackkit@latest create`
|
|
122
|
-
|
|
123
|
-
Learn more about StackKit:
|
|
124
|
-
https://github.com/tariqul420/stackkit
|
|
119
|
+
Generated with [StackKit](https://github.com/tariqul420/stackkit) — `npx stackkit@latest create`
|
|
@@ -8,12 +8,13 @@ dotenv.config({ path: path.join(process.cwd(), ".env") });
|
|
|
8
8
|
interface EnvConfig {
|
|
9
9
|
NODE_ENV: string;
|
|
10
10
|
PORT: string;
|
|
11
|
+
APP_NAME: string;
|
|
11
12
|
APP_URL: string;
|
|
12
13
|
FRONTEND_URL: string;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
const loadEnvVars = (): EnvConfig => {
|
|
16
|
-
const requiredEnvVars = ["NODE_ENV", "PORT", "APP_URL", "FRONTEND_URL"];
|
|
17
|
+
const requiredEnvVars = ["NODE_ENV", "PORT", "APP_NAME", "APP_URL", "FRONTEND_URL"];
|
|
17
18
|
|
|
18
19
|
requiredEnvVars.forEach((varName) => {
|
|
19
20
|
if (!process.env[varName]) {
|
|
@@ -25,10 +26,11 @@ const loadEnvVars = (): EnvConfig => {
|
|
|
25
26
|
});
|
|
26
27
|
|
|
27
28
|
return {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
APP_NAME: process.env.APP_NAME || "App Name",
|
|
30
|
+
NODE_ENV: process.env.NODE_ENV || "development",
|
|
31
|
+
PORT: process.env.PORT || "3000",
|
|
32
|
+
APP_URL: process.env.APP_URL || "http://localhost:5000",
|
|
33
|
+
FRONTEND_URL: process.env.FRONTEND_URL || "http://localhost:3000",
|
|
32
34
|
};
|
|
33
35
|
};
|
|
34
36
|
|