@shakil-dev/shakil-stack 2.2.6 → 2.2.8
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/dist/index.js +697 -48
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,20 +1,48 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{Command as
|
|
2
|
+
import{Command as Ve}from"commander";import qe from"fs-extra";import ne from"path";import{fileURLToPath as We}from"url";import s from"fs-extra";import o from"path";import we from"crypto";import a from"chalk";import De from"ora";import ve from"inquirer";import{execSync as Ne}from"child_process";import Xe from"fs-extra";var l=(e,r=process.cwd())=>{try{return Ne(e,{stdio:"inherit",cwd:r}),!0}catch{return!1}},E=()=>{let e=process.env.npm_config_user_agent||"";return e.includes("pnpm")?"pnpm":e.includes("yarn")?"yarn":"npm"};var ae=e=>`import { Server } from 'http';
|
|
3
3
|
import app from './app.js';
|
|
4
4
|
import config from './app/config/index.js';
|
|
5
5
|
|
|
6
|
+
let server: Server;
|
|
7
|
+
|
|
6
8
|
async function bootstrap() {
|
|
7
9
|
try {
|
|
8
|
-
|
|
10
|
+
server = app.listen(config.port, () => {
|
|
9
11
|
console.log(\`\u{1F680} Server is running on http://localhost:\${config.port}\`);
|
|
10
12
|
});
|
|
13
|
+
|
|
14
|
+
const exitHandler = () => {
|
|
15
|
+
if (server) {
|
|
16
|
+
server.close(() => {
|
|
17
|
+
console.log('\u{1F6D1} Server closed');
|
|
18
|
+
process.exit(1);
|
|
19
|
+
});
|
|
20
|
+
} else {
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const unexpectedErrorHandler = (error: unknown) => {
|
|
26
|
+
console.error('\u274C Unexpected error:', error);
|
|
27
|
+
exitHandler();
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
process.on('uncaughtException', unexpectedErrorHandler);
|
|
31
|
+
process.on('unhandledRejection', unexpectedErrorHandler);
|
|
32
|
+
|
|
33
|
+
process.on('SIGTERM', () => {
|
|
34
|
+
console.log('SIGTERM received');
|
|
35
|
+
if (server) {
|
|
36
|
+
server.close();
|
|
37
|
+
}
|
|
38
|
+
});
|
|
11
39
|
} catch (error) {
|
|
12
40
|
console.error('Failed to start server:', error);
|
|
13
41
|
}
|
|
14
42
|
}
|
|
15
43
|
|
|
16
44
|
bootstrap();
|
|
17
|
-
`,
|
|
45
|
+
`,ie=e=>`import cors from 'cors';
|
|
18
46
|
import express, { Application, Request, Response } from 'express';
|
|
19
47
|
import httpStatus from 'http-status';
|
|
20
48
|
import globalErrorHandler from './app/middleware/globalErrorHandler.js';
|
|
@@ -25,12 +53,13 @@ import morgan from 'morgan';
|
|
|
25
53
|
import helmet from 'helmet';
|
|
26
54
|
import { rateLimit } from 'express-rate-limit';
|
|
27
55
|
import { sanitizeRequest } from './app/middleware/sanitizeRequest.js';
|
|
56
|
+
import config from './app/config/index.js';
|
|
28
57
|
|
|
29
58
|
const app: Application = express();
|
|
30
59
|
|
|
31
60
|
app.use(helmet());
|
|
32
61
|
app.use(cors({
|
|
33
|
-
origin: ["http://localhost:3000", "http://127.0.0.1:3000"],
|
|
62
|
+
origin: [config.client_url as string, "http://localhost:3000", "http://127.0.0.1:3000"],
|
|
34
63
|
credentials: true,
|
|
35
64
|
methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
|
|
36
65
|
allowedHeaders: ["Content-Type", "Authorization", "Cookie"]
|
|
@@ -65,7 +94,7 @@ app.use(globalErrorHandler);
|
|
|
65
94
|
app.use(notFound);
|
|
66
95
|
|
|
67
96
|
export default app;
|
|
68
|
-
`,
|
|
97
|
+
`,F=`import dotenv from 'dotenv';
|
|
69
98
|
import path from 'path';
|
|
70
99
|
|
|
71
100
|
dotenv.config({ path: path.join(process.cwd(), "..", ".env") });
|
|
@@ -75,9 +104,14 @@ export default {
|
|
|
75
104
|
port: process.env.PORT || 8000,
|
|
76
105
|
database_url: process.env.DATABASE_URL,
|
|
77
106
|
jwt_secret: process.env.JWT_SECRET,
|
|
107
|
+
better_auth_secret: process.env.BETTER_AUTH_SECRET,
|
|
108
|
+
jwt_access_expires_in: process.env.JWT_ACCESS_EXPIRES_IN || '1h',
|
|
109
|
+
jwt_refresh_expires_in: process.env.JWT_REFRESH_EXPIRES_IN || '7d',
|
|
110
|
+
better_auth_base_url: process.env.BETTER_AUTH_BASE_URL,
|
|
111
|
+
client_url: process.env.CLIENT_URL,
|
|
78
112
|
};
|
|
79
|
-
`,
|
|
80
|
-
import { PrismaClient } from "
|
|
113
|
+
`,P=`import "dotenv/config";
|
|
114
|
+
import { PrismaClient } from "../../../generated/prisma/index.js";
|
|
81
115
|
import pkg from 'pg';
|
|
82
116
|
import { PrismaPg } from '@prisma/adapter-pg';
|
|
83
117
|
import config from '../config/index.js';
|
|
@@ -90,7 +124,7 @@ const prisma = new PrismaClient({ adapter });
|
|
|
90
124
|
|
|
91
125
|
export default prisma;
|
|
92
126
|
export { prisma };
|
|
93
|
-
`,
|
|
127
|
+
`,I=`import { betterAuth } from "better-auth";
|
|
94
128
|
import { prismaAdapter } from "better-auth/adapters/prisma";
|
|
95
129
|
import config from "../config/index.js";
|
|
96
130
|
import { prisma } from "./prisma.js";
|
|
@@ -99,17 +133,29 @@ export const auth = betterAuth({
|
|
|
99
133
|
database: prismaAdapter(prisma, {
|
|
100
134
|
provider: "postgresql",
|
|
101
135
|
}),
|
|
102
|
-
secret: config.
|
|
103
|
-
baseURL:
|
|
104
|
-
trustedOrigins: [
|
|
136
|
+
secret: config.better_auth_secret,
|
|
137
|
+
baseURL: config.better_auth_base_url,
|
|
138
|
+
trustedOrigins: [config.client_url as string],
|
|
105
139
|
emailAndPassword: {
|
|
106
140
|
enabled: true,
|
|
107
141
|
},
|
|
108
142
|
});
|
|
109
|
-
`,
|
|
143
|
+
`,ce=`import { Router } from "express";
|
|
144
|
+
import { AuthRoutes } from "../module/auth/auth.route.js";
|
|
145
|
+
|
|
110
146
|
const router = Router();
|
|
147
|
+
|
|
148
|
+
const moduleRoutes = [
|
|
149
|
+
{
|
|
150
|
+
path: "/auth",
|
|
151
|
+
route: AuthRoutes,
|
|
152
|
+
},
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
moduleRoutes.forEach((route) => router.use(route.path, route.route));
|
|
156
|
+
|
|
111
157
|
export default router;
|
|
112
|
-
`,
|
|
158
|
+
`,pe=`import { ErrorRequestHandler } from 'express';
|
|
113
159
|
import config from '../config/index.js';
|
|
114
160
|
|
|
115
161
|
const globalErrorHandler: ErrorRequestHandler = (error, req, res, next) => {
|
|
@@ -121,7 +167,7 @@ const globalErrorHandler: ErrorRequestHandler = (error, req, res, next) => {
|
|
|
121
167
|
};
|
|
122
168
|
|
|
123
169
|
export default globalErrorHandler;
|
|
124
|
-
`,
|
|
170
|
+
`,le=`import { Request, Response, NextFunction } from 'express';
|
|
125
171
|
import httpStatus from 'http-status';
|
|
126
172
|
|
|
127
173
|
const notFound = (req: Request, res: Response, next: NextFunction) => {
|
|
@@ -132,7 +178,7 @@ const notFound = (req: Request, res: Response, next: NextFunction) => {
|
|
|
132
178
|
};
|
|
133
179
|
|
|
134
180
|
export default notFound;
|
|
135
|
-
`,
|
|
181
|
+
`,ue=`import { NextFunction, Request, RequestHandler, Response } from 'express';
|
|
136
182
|
|
|
137
183
|
const catchAsync = (fn: RequestHandler) => {
|
|
138
184
|
return async (req: Request, res: Response, next: NextFunction) => {
|
|
@@ -145,7 +191,7 @@ const catchAsync = (fn: RequestHandler) => {
|
|
|
145
191
|
};
|
|
146
192
|
|
|
147
193
|
export default catchAsync;
|
|
148
|
-
`,
|
|
194
|
+
`,de=`class ApiError extends Error {
|
|
149
195
|
statusCode: number;
|
|
150
196
|
constructor(statusCode: number, message: string | undefined, stack = '') {
|
|
151
197
|
super(message);
|
|
@@ -155,7 +201,7 @@ export default catchAsync;
|
|
|
155
201
|
}
|
|
156
202
|
}
|
|
157
203
|
export default ApiError;
|
|
158
|
-
`,
|
|
204
|
+
`,me=`import { JSDOM } from 'jsdom';
|
|
159
205
|
import createDOMPurify from 'dompurify';
|
|
160
206
|
|
|
161
207
|
const window = new JSDOM('').window;
|
|
@@ -170,7 +216,7 @@ export const sanitize = (data: any): any => {
|
|
|
170
216
|
}
|
|
171
217
|
return data;
|
|
172
218
|
};
|
|
173
|
-
`,
|
|
219
|
+
`,ge=`import { Request, Response, NextFunction } from 'express';
|
|
174
220
|
import { sanitize } from '../utils/sanitizer.js';
|
|
175
221
|
|
|
176
222
|
export const sanitizeRequest = (req: Request, res: Response, next: NextFunction) => {
|
|
@@ -179,7 +225,7 @@ export const sanitizeRequest = (req: Request, res: Response, next: NextFunction)
|
|
|
179
225
|
if (req.params) sanitize(req.params);
|
|
180
226
|
next();
|
|
181
227
|
};
|
|
182
|
-
`,
|
|
228
|
+
`,fe=`import { Response } from 'express';
|
|
183
229
|
|
|
184
230
|
type IResponse<T> = {
|
|
185
231
|
statusCode: number;
|
|
@@ -203,7 +249,7 @@ const sendResponse = <T>(res: Response, data: IResponse<T>) => {
|
|
|
203
249
|
};
|
|
204
250
|
|
|
205
251
|
export default sendResponse;
|
|
206
|
-
|
|
252
|
+
`,U=`generator client {
|
|
207
253
|
provider = "prisma-client-js"
|
|
208
254
|
output = "../../generated/prisma"
|
|
209
255
|
}
|
|
@@ -211,7 +257,7 @@ export default sendResponse;
|
|
|
211
257
|
datasource db {
|
|
212
258
|
provider = "postgresql"
|
|
213
259
|
}
|
|
214
|
-
`,
|
|
260
|
+
`,O=`model User {
|
|
215
261
|
id String @id @default(uuid())
|
|
216
262
|
email String @unique
|
|
217
263
|
name String
|
|
@@ -247,9 +293,12 @@ model Account {
|
|
|
247
293
|
updatedAt DateTime @updatedAt
|
|
248
294
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
249
295
|
}
|
|
250
|
-
`,
|
|
251
|
-
import
|
|
296
|
+
`,_=`import dotenv from "dotenv";
|
|
297
|
+
import path from "path";
|
|
252
298
|
import process from "process";
|
|
299
|
+
import { defineConfig } from "prisma/config";
|
|
300
|
+
|
|
301
|
+
dotenv.config({ path: path.join(process.cwd(), "..", ".env") });
|
|
253
302
|
|
|
254
303
|
export default defineConfig({
|
|
255
304
|
schema: "prisma/schema",
|
|
@@ -257,7 +306,94 @@ export default defineConfig({
|
|
|
257
306
|
url: process.env.DATABASE_URL,
|
|
258
307
|
},
|
|
259
308
|
});
|
|
260
|
-
`,
|
|
309
|
+
`,q=`import jwt, { JwtPayload, SignOptions } from 'jsonwebtoken';
|
|
310
|
+
|
|
311
|
+
const createToken = (payload: Record<string, unknown>, secret: string, options: SignOptions) => {
|
|
312
|
+
return jwt.sign(payload, secret, options);
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const verifyToken = (token: string, secret: string) => {
|
|
316
|
+
try {
|
|
317
|
+
const decoded = jwt.verify(token, secret);
|
|
318
|
+
return { success: true, data: decoded as JwtPayload };
|
|
319
|
+
} catch (error) {
|
|
320
|
+
return { success: false, error };
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
export const jwtUtils = { createToken, verifyToken };
|
|
325
|
+
`,N=`import { CookieOptions, Request, Response } from "express";
|
|
326
|
+
|
|
327
|
+
const setCookie = (res: Response, key: string, value: string, options: CookieOptions) => {
|
|
328
|
+
res.cookie(key, value, options);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const getCookie = (req: Request, key: string) => {
|
|
332
|
+
return req.cookies?.[key];
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const clearCookie = (res: Response, key: string, options: CookieOptions) => {
|
|
336
|
+
res.clearCookie(key, options);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export const CookieUtils = {
|
|
340
|
+
setCookie,
|
|
341
|
+
getCookie,
|
|
342
|
+
clearCookie,
|
|
343
|
+
};
|
|
344
|
+
`,D=`import { Response } from "express";
|
|
345
|
+
import { JwtPayload, SignOptions } from "jsonwebtoken";
|
|
346
|
+
import config from "../config/index.js";
|
|
347
|
+
import { jwtUtils } from "./jwt.js";
|
|
348
|
+
|
|
349
|
+
const getAccessToken = (payload: JwtPayload) => {
|
|
350
|
+
return jwtUtils.createToken(
|
|
351
|
+
payload,
|
|
352
|
+
config.jwt_secret as string,
|
|
353
|
+
{ expiresIn: config.jwt_access_expires_in } as SignOptions
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const getRefreshToken = (payload: JwtPayload) => {
|
|
358
|
+
return jwtUtils.createToken(
|
|
359
|
+
payload,
|
|
360
|
+
config.jwt_secret as string,
|
|
361
|
+
{ expiresIn: config.jwt_refresh_expires_in } as SignOptions
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export const setAccessTokenCookie = (res: Response, token: string) => {
|
|
366
|
+
res.cookie('accessToken', token, {
|
|
367
|
+
httpOnly: true,
|
|
368
|
+
secure: config.env === 'production',
|
|
369
|
+
sameSite: config.env === 'production' ? 'none' : 'lax',
|
|
370
|
+
maxAge: 3600000, // 1 hour
|
|
371
|
+
path: '/',
|
|
372
|
+
});
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
export const setRefreshTokenCookie = (res: Response, token: string) => {
|
|
376
|
+
res.cookie('refreshToken', token, {
|
|
377
|
+
httpOnly: true,
|
|
378
|
+
secure: config.env === 'production',
|
|
379
|
+
sameSite: config.env === 'production' ? 'none' : 'lax',
|
|
380
|
+
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
|
|
381
|
+
path: '/',
|
|
382
|
+
});
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
export const setBetterAuthSessionCookie = (res: Response, token: string) => {
|
|
386
|
+
res.cookie('better-auth.session_token', token, {
|
|
387
|
+
httpOnly: true,
|
|
388
|
+
secure: config.env === 'production',
|
|
389
|
+
sameSite: config.env === 'production' ? 'none' : 'lax',
|
|
390
|
+
maxAge: 7 * 24 * 60 * 60 * 1000,
|
|
391
|
+
path: '/',
|
|
392
|
+
});
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
export const tokenUtils = { getAccessToken, getRefreshToken, setAccessTokenCookie, setRefreshTokenCookie, setBetterAuthSessionCookie };
|
|
396
|
+
`,he=`{
|
|
261
397
|
"compilerOptions": {
|
|
262
398
|
"target": "ES2022",
|
|
263
399
|
"module": "NodeNext",
|
|
@@ -276,6 +412,26 @@ export default defineConfig({
|
|
|
276
412
|
"include": ["src/**/*"],
|
|
277
413
|
"exclude": ["node_modules", "dist"]
|
|
278
414
|
}
|
|
415
|
+
`,ke=`import { NextFunction, Request, Response } from 'express';
|
|
416
|
+
import type { AnyZodObject } from 'zod';
|
|
417
|
+
|
|
418
|
+
const validateRequest = (schema: AnyZodObject) => {
|
|
419
|
+
return async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
|
420
|
+
try {
|
|
421
|
+
await schema.parseAsync({
|
|
422
|
+
body: req.body,
|
|
423
|
+
query: req.query,
|
|
424
|
+
params: req.params,
|
|
425
|
+
cookies: req.cookies,
|
|
426
|
+
});
|
|
427
|
+
return next();
|
|
428
|
+
} catch (error) {
|
|
429
|
+
next(error);
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
export default validateRequest;
|
|
279
435
|
`;var L=`MIT License
|
|
280
436
|
|
|
281
437
|
Copyright (c) 2026 Shakil Ahmed Billal
|
|
@@ -371,12 +527,12 @@ version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
|
|
|
371
527
|
|
|
372
528
|
[homepage]: http://contributor-covenant.org
|
|
373
529
|
[version]: http://contributor-covenant.org/version/1/4
|
|
374
|
-
|
|
530
|
+
`,$=`node_modules/
|
|
375
531
|
.env
|
|
376
532
|
dist/
|
|
377
533
|
*.log
|
|
378
534
|
.DS_Store
|
|
379
|
-
`,
|
|
535
|
+
`,H=e=>`# \u{1F680} ${e}
|
|
380
536
|
|
|
381
537
|
This project was generated using [Shakil-Stack](https://github.com/shakil-ahmed-billal/shakil-stack-cli).
|
|
382
538
|
|
|
@@ -406,23 +562,510 @@ ${e}/
|
|
|
406
562
|
|
|
407
563
|
---
|
|
408
564
|
Built with \u26A1 by **Shakil Ahmed Billal**
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
565
|
+
`,B=e=>`{
|
|
566
|
+
"name": "${e}",
|
|
567
|
+
"version": "1.0.0",
|
|
568
|
+
"private": true,
|
|
569
|
+
"scripts": {
|
|
570
|
+
"dev:backend": "cd backend && pnpm dev",
|
|
571
|
+
"dev:frontend": "cd frontend && pnpm dev",
|
|
572
|
+
"build:backend": "cd backend && pnpm build",
|
|
573
|
+
"build:frontend": "cd frontend && pnpm build",
|
|
574
|
+
"version:patch": "npm version patch --no-git-tag-version",
|
|
575
|
+
"push": "npm version patch && git add . && git commit -m 'chore: bump version' && git push"
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
`;var M=`import { Request, Response } from "express";
|
|
579
|
+
import httpStatus from "http-status";
|
|
580
|
+
import { AuthService } from "./auth.service.js";
|
|
581
|
+
import catchAsync from "../../utils/catchAsync.js";
|
|
582
|
+
import sendResponse from "../../utils/sendResponse.js";
|
|
583
|
+
import { tokenUtils } from "../../utils/token.js";
|
|
584
|
+
import config from "../../config/index.js";
|
|
585
|
+
|
|
586
|
+
const registerUser = catchAsync(async (req: Request, res: Response) => {
|
|
587
|
+
const result = await AuthService.registerUser(req.body);
|
|
588
|
+
const { accessToken, refreshToken, token, ...rest } = result as Record<string, any>;
|
|
589
|
+
|
|
590
|
+
tokenUtils.setAccessTokenCookie(res, accessToken);
|
|
591
|
+
tokenUtils.setRefreshTokenCookie(res, refreshToken);
|
|
592
|
+
tokenUtils.setBetterAuthSessionCookie(res, token as string);
|
|
593
|
+
|
|
594
|
+
sendResponse(res, {
|
|
595
|
+
statusCode: httpStatus.CREATED,
|
|
596
|
+
success: true,
|
|
597
|
+
message: "User registered successfully",
|
|
598
|
+
data: { token, accessToken, refreshToken, ...rest }
|
|
599
|
+
});
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
const loginUser = catchAsync(async (req: Request, res: Response) => {
|
|
603
|
+
const result = await AuthService.loginUser(req.body);
|
|
604
|
+
const { accessToken, refreshToken, token, ...rest } = result as Record<string, any>;
|
|
605
|
+
|
|
606
|
+
tokenUtils.setAccessTokenCookie(res, accessToken);
|
|
607
|
+
tokenUtils.setRefreshTokenCookie(res, refreshToken);
|
|
608
|
+
tokenUtils.setBetterAuthSessionCookie(res, token as string);
|
|
609
|
+
|
|
610
|
+
sendResponse(res, {
|
|
611
|
+
statusCode: httpStatus.OK,
|
|
612
|
+
success: true,
|
|
613
|
+
message: "User logged in successfully",
|
|
614
|
+
data: { token, accessToken, refreshToken, ...rest }
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
const logoutUser = catchAsync(async (req: Request, res: Response) => {
|
|
619
|
+
const betterAuthSessionToken = req.cookies?.["better-auth.session_token"] || "";
|
|
620
|
+
await AuthService.logoutUser(betterAuthSessionToken);
|
|
621
|
+
|
|
622
|
+
const isProd = config.env === 'production';
|
|
623
|
+
const cookieOptions = {
|
|
624
|
+
httpOnly: true,
|
|
625
|
+
secure: isProd,
|
|
626
|
+
sameSite: isProd ? "none" : "lax" as any,
|
|
627
|
+
path: '/'
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
res.clearCookie('accessToken', cookieOptions);
|
|
631
|
+
res.clearCookie('refreshToken', cookieOptions);
|
|
632
|
+
res.clearCookie('better-auth.session_token', cookieOptions);
|
|
633
|
+
|
|
634
|
+
sendResponse(res, {
|
|
635
|
+
statusCode: httpStatus.OK,
|
|
636
|
+
success: true,
|
|
637
|
+
message: "User logged out successfully",
|
|
638
|
+
data: null
|
|
639
|
+
});
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
const getMe = catchAsync(async (req: Request, res: Response) => {
|
|
643
|
+
const user = (req as any).user;
|
|
644
|
+
|
|
645
|
+
sendResponse(res, {
|
|
646
|
+
statusCode: httpStatus.OK,
|
|
647
|
+
success: true,
|
|
648
|
+
message: "User session retrieved successfully",
|
|
649
|
+
data: { user }
|
|
650
|
+
});
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
export const AuthController = { registerUser, loginUser, logoutUser, getMe };
|
|
654
|
+
`,V=`import status from "http-status";
|
|
655
|
+
import AppError from "../../errorHelpers/ApiError.js";
|
|
656
|
+
import { auth } from "../../lib/auth.js";
|
|
657
|
+
import { tokenUtils } from "../../utils/token.js";
|
|
658
|
+
import { ILoginUserPayload, IRegisterUserPayload } from "./auth.interface.js";
|
|
659
|
+
|
|
660
|
+
const registerUser = async (payload: IRegisterUserPayload) => {
|
|
661
|
+
const { name, email, password } = payload;
|
|
662
|
+
|
|
663
|
+
const data = await auth.api.signUpEmail({
|
|
664
|
+
body: { name, email, password: password || "" }
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
if (!data.user) {
|
|
668
|
+
throw new AppError(status.BAD_REQUEST, "Failed to register user");
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const accessToken = tokenUtils.getAccessToken({
|
|
672
|
+
userId: data.user.id,
|
|
673
|
+
name: data.user.name,
|
|
674
|
+
email: data.user.email,
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
const refreshToken = tokenUtils.getRefreshToken({
|
|
678
|
+
userId: data.user.id,
|
|
679
|
+
name: data.user.name,
|
|
680
|
+
email: data.user.email,
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
return { ...data, accessToken, refreshToken };
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const loginUser = async (payload: ILoginUserPayload) => {
|
|
687
|
+
const { email, password } = payload;
|
|
688
|
+
|
|
689
|
+
const data = await auth.api.signInEmail({
|
|
690
|
+
body: { email, password: password || "" }
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
if (!data.user) {
|
|
694
|
+
throw new AppError(status.UNAUTHORIZED, "Invalid credentials");
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
const accessToken = tokenUtils.getAccessToken({
|
|
698
|
+
userId: data.user.id,
|
|
699
|
+
name: data.user.name,
|
|
700
|
+
email: data.user.email,
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
const refreshToken = tokenUtils.getRefreshToken({
|
|
704
|
+
userId: data.user.id,
|
|
705
|
+
name: data.user.name,
|
|
706
|
+
email: data.user.email,
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
return { ...data, accessToken, refreshToken };
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const logoutUser = async (sessionToken: string) => {
|
|
713
|
+
const result = await auth.api.signOut({
|
|
714
|
+
headers: new Headers({ Authorization: \`Bearer \${sessionToken}\` })
|
|
715
|
+
});
|
|
716
|
+
return result;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
export const AuthService = { registerUser, loginUser, logoutUser };
|
|
720
|
+
`,W=`import { Router } from "express";
|
|
721
|
+
import { AuthController } from "./auth.controller.js";
|
|
722
|
+
import validateRequest from "../../middleware/validateRequest.js";
|
|
723
|
+
import { AuthValidation } from "./auth.validation.js";
|
|
724
|
+
|
|
725
|
+
const router = Router();
|
|
726
|
+
|
|
727
|
+
router.post(
|
|
728
|
+
"/register",
|
|
729
|
+
validateRequest(AuthValidation.registerValidationSchema),
|
|
730
|
+
AuthController.registerUser
|
|
731
|
+
);
|
|
732
|
+
|
|
733
|
+
router.post(
|
|
734
|
+
"/login",
|
|
735
|
+
validateRequest(AuthValidation.loginValidationSchema),
|
|
736
|
+
AuthController.loginUser
|
|
737
|
+
);
|
|
738
|
+
|
|
739
|
+
router.post("/logout", AuthController.logoutUser);
|
|
740
|
+
|
|
741
|
+
export const AuthRoutes = router;
|
|
742
|
+
`,G=`import { z } from "zod";
|
|
743
|
+
import { AuthValidation } from "./auth.validation.js";
|
|
744
|
+
|
|
745
|
+
export type IRegisterUserPayload = z.infer<typeof AuthValidation.registerValidationSchema>;
|
|
746
|
+
export type ILoginUserPayload = z.infer<typeof AuthValidation.loginValidationSchema>;
|
|
747
|
+
`,J=`import { z } from "zod";
|
|
748
|
+
|
|
749
|
+
const registerValidationSchema = z.object({
|
|
750
|
+
body: z.object({
|
|
751
|
+
name: z.string().min(1, "Name is required"),
|
|
752
|
+
email: z.string().email("Invalid email address"),
|
|
753
|
+
password: z.string().min(6, "Password must be at least 6 characters"),
|
|
754
|
+
}),
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
const loginValidationSchema = z.object({
|
|
758
|
+
body: z.object({
|
|
759
|
+
email: z.string().email("Invalid email address"),
|
|
760
|
+
password: z.string().min(1, "Password is required"),
|
|
761
|
+
}),
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
export const AuthValidation = {
|
|
765
|
+
registerValidationSchema,
|
|
766
|
+
loginValidationSchema,
|
|
767
|
+
};
|
|
768
|
+
`;var K=`import axios from "axios";
|
|
769
|
+
|
|
770
|
+
export const httpClient = axios.create({
|
|
771
|
+
baseURL: process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000/api/v1",
|
|
772
|
+
withCredentials: true,
|
|
773
|
+
headers: {
|
|
774
|
+
"Content-Type": "application/json",
|
|
775
|
+
},
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
httpClient.interceptors.response.use(
|
|
779
|
+
(response) => response.data,
|
|
780
|
+
(error) => {
|
|
781
|
+
return Promise.reject(error);
|
|
782
|
+
}
|
|
783
|
+
);
|
|
784
|
+
`,Q=`"use server";
|
|
785
|
+
import { cookies } from "next/headers";
|
|
786
|
+
|
|
787
|
+
export async function setTokenInCookies(name: string, token: string, maxAge?: number) {
|
|
788
|
+
const cookieStore = await cookies();
|
|
789
|
+
cookieStore.set(name, token, {
|
|
790
|
+
httpOnly: true,
|
|
791
|
+
secure: process.env.NODE_ENV === "production",
|
|
792
|
+
sameSite: process.env.NODE_ENV === "production" ? "none" : "lax",
|
|
793
|
+
path: "/",
|
|
794
|
+
maxAge: maxAge || 3600, // Default 1 hour
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
`,Y=`"use server";
|
|
798
|
+
import { cookies } from "next/headers";
|
|
799
|
+
|
|
800
|
+
export async function deleteCookie(name: string) {
|
|
801
|
+
const cookieStore = await cookies();
|
|
802
|
+
cookieStore.delete(name);
|
|
803
|
+
}
|
|
804
|
+
`,X=`"use server";
|
|
805
|
+
|
|
806
|
+
import { httpClient } from '@/lib/axios/httpClient';
|
|
807
|
+
import { setTokenInCookies } from '@/lib/tokenUtils';
|
|
808
|
+
import { deleteCookie } from '@/lib/cookieUtils';
|
|
809
|
+
import { cookies } from 'next/headers';
|
|
810
|
+
|
|
811
|
+
export const loginAction = async (payload: any) => {
|
|
812
|
+
try {
|
|
813
|
+
const response: any = await httpClient.post("/auth/login", payload);
|
|
814
|
+
|
|
815
|
+
if (response?.success && response?.data) {
|
|
816
|
+
const { accessToken, refreshToken, token } = response.data;
|
|
817
|
+
|
|
818
|
+
if (accessToken) await setTokenInCookies("accessToken", accessToken);
|
|
819
|
+
if (refreshToken) await setTokenInCookies("refreshToken", refreshToken);
|
|
820
|
+
if (token) await setTokenInCookies("better-auth.session_token", token, 24 * 60 * 60);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
return response;
|
|
824
|
+
} catch (error: any) {
|
|
825
|
+
return {
|
|
826
|
+
success: false,
|
|
827
|
+
message: error?.response?.data?.message || error.message || "Login failed"
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
export const registerAction = async (payload: any) => {
|
|
833
|
+
try {
|
|
834
|
+
const response: any = await httpClient.post("/auth/register", payload);
|
|
835
|
+
|
|
836
|
+
if (response?.success && response?.data) {
|
|
837
|
+
const { accessToken, refreshToken, token } = response.data;
|
|
838
|
+
|
|
839
|
+
if (accessToken) await setTokenInCookies("accessToken", accessToken);
|
|
840
|
+
if (refreshToken) await setTokenInCookies("refreshToken", refreshToken);
|
|
841
|
+
if (token) await setTokenInCookies("better-auth.session_token", token, 24 * 60 * 60);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
return response;
|
|
845
|
+
} catch (error: any) {
|
|
846
|
+
return {
|
|
847
|
+
success: false,
|
|
848
|
+
message: error?.response?.data?.message || error.message || "Registration failed"
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
export const logoutAction = async () => {
|
|
854
|
+
try {
|
|
855
|
+
await httpClient.post("/auth/logout", {});
|
|
856
|
+
await deleteCookie("accessToken");
|
|
857
|
+
await deleteCookie("refreshToken");
|
|
858
|
+
await deleteCookie("better-auth.session_token");
|
|
859
|
+
return { success: true };
|
|
860
|
+
} catch (error: any) {
|
|
861
|
+
await deleteCookie("accessToken");
|
|
862
|
+
await deleteCookie("refreshToken");
|
|
863
|
+
await deleteCookie("better-auth.session_token");
|
|
864
|
+
return { success: false, message: "Logged out locally." };
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
`,Z=`
|
|
868
|
+
"use client"
|
|
869
|
+
|
|
870
|
+
import { useForm } from "react-hook-form";
|
|
871
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
872
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
873
|
+
import * as z from "zod";
|
|
874
|
+
import { loginAction } from "@/services/auth.actions";
|
|
875
|
+
import { useRouter } from "next/navigation";
|
|
876
|
+
import { useState } from "react";
|
|
877
|
+
import { Button } from "@/components/ui/button";
|
|
878
|
+
import { Input } from "@/components/ui/input";
|
|
879
|
+
import { Label } from "@/components/ui/label";
|
|
880
|
+
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
|
881
|
+
|
|
882
|
+
const loginSchema = z.object({
|
|
883
|
+
email: z.string().email("Invalid email address"),
|
|
884
|
+
password: z.string().min(1, "Password is required"),
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
type LoginFormValues = z.infer<typeof loginSchema>;
|
|
888
|
+
|
|
889
|
+
export function LoginForm() {
|
|
890
|
+
const router = useRouter();
|
|
891
|
+
const queryClient = useQueryClient();
|
|
892
|
+
const [error, setError] = useState<string | null>(null);
|
|
893
|
+
const [loading, setLoading] = useState(false);
|
|
894
|
+
|
|
895
|
+
const { register, handleSubmit, formState: { errors } } = useForm<LoginFormValues>({
|
|
896
|
+
resolver: zodResolver(loginSchema),
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
const onSubmit = async (data: LoginFormValues) => {
|
|
900
|
+
setLoading(true);
|
|
901
|
+
setError(null);
|
|
902
|
+
try {
|
|
903
|
+
const response = await loginAction({ body: data });
|
|
904
|
+
if (response.success) {
|
|
905
|
+
queryClient.invalidateQueries({ queryKey: ["user"] });
|
|
906
|
+
router.push("/dashboard");
|
|
907
|
+
} else {
|
|
908
|
+
setError(response.message);
|
|
909
|
+
}
|
|
910
|
+
} catch (err: any) {
|
|
911
|
+
setError("An unexpected error occurred");
|
|
912
|
+
} finally {
|
|
913
|
+
setLoading(false);
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
|
|
917
|
+
return (
|
|
918
|
+
<Card>
|
|
919
|
+
<CardHeader>
|
|
920
|
+
<CardTitle>Login</CardTitle>
|
|
921
|
+
<CardDescription>Enter your credentials to access your account</CardDescription>
|
|
922
|
+
</CardHeader>
|
|
923
|
+
<form onSubmit={handleSubmit(onSubmit)}>
|
|
924
|
+
<CardContent className="space-y-4">
|
|
925
|
+
<div className="space-y-2">
|
|
926
|
+
<Label htmlFor="email">Email</Label>
|
|
927
|
+
<Input id="email" type="email" {...register("email")} placeholder="m@example.com" />
|
|
928
|
+
{errors.email && <p className="text-sm text-red-500">{errors.email.message}</p>}
|
|
929
|
+
</div>
|
|
930
|
+
<div className="space-y-2">
|
|
931
|
+
<Label htmlFor="password">Password</Label>
|
|
932
|
+
<Input id="password" type="password" {...register("password")} />
|
|
933
|
+
{errors.password && <p className="text-sm text-red-500">{errors.password.message}</p>}
|
|
934
|
+
</div>
|
|
935
|
+
{error && <p className="text-sm text-red-500">{error}</p>}
|
|
936
|
+
</CardContent>
|
|
937
|
+
<CardFooter>
|
|
938
|
+
<Button type="submit" className="w-full" disabled={loading}>
|
|
939
|
+
{loading ? "Logging in..." : "Login"}
|
|
940
|
+
</Button>
|
|
941
|
+
</CardFooter>
|
|
942
|
+
</form>
|
|
943
|
+
</Card>
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
`,ee=`
|
|
947
|
+
"use client"
|
|
948
|
+
|
|
949
|
+
import { useForm } from "react-hook-form";
|
|
950
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
951
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
952
|
+
import * as z from "zod";
|
|
953
|
+
import { registerAction } from "@/services/auth.actions";
|
|
954
|
+
import { useRouter } from "next/navigation";
|
|
955
|
+
import { useState } from "react";
|
|
956
|
+
import { Button } from "@/components/ui/button";
|
|
957
|
+
import { Input } from "@/components/ui/input";
|
|
958
|
+
import { Label } from "@/components/ui/label";
|
|
959
|
+
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
|
960
|
+
|
|
961
|
+
const registerSchema = z.object({
|
|
962
|
+
name: z.string().min(1, "Name is required"),
|
|
963
|
+
email: z.string().email("Invalid email address"),
|
|
964
|
+
password: z.string().min(6, "Password must be at least 6 characters"),
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
type RegisterFormValues = z.infer<typeof registerSchema>;
|
|
968
|
+
|
|
969
|
+
export function RegisterForm() {
|
|
970
|
+
const router = useRouter();
|
|
971
|
+
const queryClient = useQueryClient();
|
|
972
|
+
const [error, setError] = useState<string | null>(null);
|
|
973
|
+
const [loading, setLoading] = useState(false);
|
|
974
|
+
|
|
975
|
+
const { register, handleSubmit, formState: { errors } } = useForm<RegisterFormValues>({
|
|
976
|
+
resolver: zodResolver(registerSchema),
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
const onSubmit = async (data: RegisterFormValues) => {
|
|
980
|
+
setLoading(true);
|
|
981
|
+
setError(null);
|
|
982
|
+
try {
|
|
983
|
+
const response = await registerAction({ body: data });
|
|
984
|
+
if (response.success) {
|
|
985
|
+
queryClient.invalidateQueries({ queryKey: ["user"] });
|
|
986
|
+
router.push("/dashboard");
|
|
987
|
+
} else {
|
|
988
|
+
setError(response.message);
|
|
989
|
+
}
|
|
990
|
+
} catch (err: any) {
|
|
991
|
+
setError("An unexpected error occurred");
|
|
992
|
+
} finally {
|
|
993
|
+
setLoading(false);
|
|
994
|
+
}
|
|
995
|
+
};
|
|
996
|
+
|
|
997
|
+
return (
|
|
998
|
+
<Card>
|
|
999
|
+
<CardHeader>
|
|
1000
|
+
<CardTitle>Register</CardTitle>
|
|
1001
|
+
<CardDescription>Create a new account to get started</CardDescription>
|
|
1002
|
+
</CardHeader>
|
|
1003
|
+
<form onSubmit={handleSubmit(onSubmit)}>
|
|
1004
|
+
<CardContent className="space-y-4">
|
|
1005
|
+
<div className="space-y-2">
|
|
1006
|
+
<Label htmlFor="name">Name</Label>
|
|
1007
|
+
<Input id="name" {...register("name")} placeholder="John Doe" />
|
|
1008
|
+
{errors.name && <p className="text-sm text-red-500">{errors.name.message}</p>}
|
|
1009
|
+
</div>
|
|
1010
|
+
<div className="space-y-2">
|
|
1011
|
+
<Label htmlFor="email">Email</Label>
|
|
1012
|
+
<Input id="email" type="email" {...register("email")} placeholder="m@example.com" />
|
|
1013
|
+
{errors.email && <p className="text-sm text-red-500">{errors.email.message}</p>}
|
|
1014
|
+
</div>
|
|
1015
|
+
<div className="space-y-2">
|
|
1016
|
+
<Label htmlFor="password">Password</Label>
|
|
1017
|
+
<Input id="password" type="password" {...register("password")} />
|
|
1018
|
+
{errors.password && <p className="text-sm text-red-500">{errors.password.message}</p>}
|
|
1019
|
+
</div>
|
|
1020
|
+
{error && <p className="text-sm text-red-500">{error}</p>}
|
|
1021
|
+
</CardContent>
|
|
1022
|
+
<CardFooter>
|
|
1023
|
+
<Button type="submit" className="w-full" disabled={loading}>
|
|
1024
|
+
{loading ? "Creating account..." : "Register"}
|
|
1025
|
+
</Button>
|
|
1026
|
+
</CardFooter>
|
|
1027
|
+
</form>
|
|
1028
|
+
</Card>
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
`,C=e=>`
|
|
1032
|
+
import { ${e==="login"?"LoginForm":"RegisterForm"} } from "@/components/auth/${e}-form";
|
|
1033
|
+
|
|
1034
|
+
export default function ${e==="login"?"LoginPage":"RegisterPage"}() {
|
|
1035
|
+
return (
|
|
1036
|
+
<div className="container flex items-center justify-center min-h-screen py-12">
|
|
1037
|
+
<div className="w-full max-w-md">
|
|
1038
|
+
<${e==="login"?"LoginForm":"RegisterForm"} />
|
|
1039
|
+
</div>
|
|
1040
|
+
</div>
|
|
1041
|
+
);
|
|
1042
|
+
}
|
|
1043
|
+
`;var se=async e=>{let r=e;r||(r=(await ve.prompt([{type:"input",name:"projectName",message:"What is your project name?",default:"shakil-stack-app"}])).projectName),r||(console.log(a.red("\u274C Error: Project name is required.")),process.exit(1));let{packageManager:d,useShadcn:x,installDeps:m,setupPrisma:u,setupBetterAuth:g,setupApi:T,setupLogin:h,setupGit:oe}=await ve.prompt([{type:"list",name:"packageManager",message:"Which package manager do you want to use?",choices:["pnpm","npm","yarn"],default:E()},{type:"confirm",name:"useShadcn",message:"Would you like to use shadcn/ui?",default:!0},{type:"confirm",name:"setupPrisma",message:"Would you like to setup Prisma?",default:!0},{type:"confirm",name:"setupBetterAuth",message:"Would you like to setup Better-Auth?",default:!0},{type:"confirm",name:"setupApi",message:"Would you like to setup API modules (AuthController, etc.)?",default:!0},{type:"confirm",name:"setupLogin",message:"Would you like to setup Frontend Login/Register pages?",default:!0},{type:"confirm",name:"installDeps",message:"Do you want to install dependencies automatically?",default:!0},{type:"confirm",name:"setupGit",message:"Would you like to setup Git with automatic version control?",default:!0}]),k=r==="."||r==="./",t=k?process.cwd():o.join(process.cwd(),r),p=k?o.basename(process.cwd()):r;!k&&s.existsSync(t)&&(console.log(a.red(`\u274C Error: Directory ${r} already exists.`)),process.exit(1)),console.log(a.cyan(`
|
|
1044
|
+
\u{1F680} Initializing ${a.bold(p)} in ${k?"current directory":r}...
|
|
1045
|
+
`));let S=De("\u{1F6E0}\uFE0F Creating project structure...").start();try{await s.ensureDir(t),await s.ensureDir(o.join(t,"backend","src","app","config")),await s.ensureDir(o.join(t,"backend","src","app","lib")),await s.ensureDir(o.join(t,"backend","src","app","module")),await s.ensureDir(o.join(t,"backend","src","app","routes")),await s.ensureDir(o.join(t,"backend","src","app","middleware")),await s.ensureDir(o.join(t,"backend","src","app","utils")),await s.ensureDir(o.join(t,"backend","src","app","errorHelpers")),u&&await s.ensureDir(o.join(t,"backend","prisma","schema")),T&&await s.ensureDir(o.join(t,"backend","src","app","module","auth")),console.log(a.cyan(`
|
|
1046
|
+
\u{1F5BC}\uFE0F Scaffolding Next.js frontend...`)),l(`npx create-next-app@latest frontend --ts --tailwind --eslint --app --src-dir --import-alias "@/*" --use-${d}`,t);let b=["config","hooks","lib","services","types"];T&&b.push("lib/axios"),h&&b.push("components/auth","app/(auth)/login","app/(auth)/register");for(let y of b)await s.ensureDir(o.join(t,"frontend","src",y));if(x){console.log(a.cyan(`
|
|
1047
|
+
\u{1F3A8} Setting up shadcn/ui...`));try{l("npx shadcn@latest init -d",o.join(t,"frontend")),console.log(a.cyan("\u{1F4E6} Adding common shadcn/ui components...")),l(`npx shadcn@latest add ${["button","card","input","label","textarea","dialog","dropdown-menu","table","tabs","checkbox"].join(" ")} -y`,o.join(t,"frontend")),console.log(a.green("\u2705 shadcn/ui and common components initialized successfully!\u2728"))}catch{console.log(a.yellow(`
|
|
1048
|
+
\u26A0\uFE0F Warning: Failed to automate shadcn/ui init. You can run "npx shadcn@latest init" in the frontend folder.`))}}await s.outputFile(o.join(t,"backend","src","server.ts"),ae(p)),await s.outputFile(o.join(t,"backend","src","app.ts"),ie(p)),await s.outputFile(o.join(t,"backend","src","app","config","index.ts"),F),u&&await s.outputFile(o.join(t,"backend","src","app","lib","prisma.ts"),P),g&&await s.outputFile(o.join(t,"backend","src","app","lib","auth.ts"),I),await s.outputFile(o.join(t,"backend","src","app","routes","index.ts"),ce),await s.outputFile(o.join(t,"backend","src","app","middleware","globalErrorHandler.ts"),pe),await s.outputFile(o.join(t,"backend","src","app","middleware","notFound.ts"),le),await s.outputFile(o.join(t,"backend","src","app","middleware","sanitizeRequest.ts"),ge),await s.outputFile(o.join(t,"backend","src","app","middleware","validateRequest.ts"),ke),await s.outputFile(o.join(t,"backend","src","app","utils","catchAsync.ts"),ue),await s.outputFile(o.join(t,"backend","src","app","utils","sendResponse.ts"),fe),await s.outputFile(o.join(t,"backend","src","app","utils","sanitizer.ts"),me),await s.outputFile(o.join(t,"backend","src","app","errorHelpers","ApiError.ts"),de),u&&(await s.outputFile(o.join(t,"backend","prisma","schema","schema.prisma"),U),g&&await s.outputFile(o.join(t,"backend","prisma","schema","auth.prisma"),O),await s.outputFile(o.join(t,"backend","prisma.config.ts"),_)),T&&(await s.outputFile(o.join(t,"backend","src","app","module","auth","auth.controller.ts"),M),await s.outputFile(o.join(t,"backend","src","app","module","auth","auth.service.ts"),V),await s.outputFile(o.join(t,"backend","src","app","module","auth","auth.route.ts"),W),await s.outputFile(o.join(t,"backend","src","app","module","auth","auth.interface.ts"),G),await s.outputFile(o.join(t,"backend","src","app","module","auth","auth.validation.ts"),J),await s.outputFile(o.join(t,"backend","src","app","utils","jwt.ts"),q),await s.outputFile(o.join(t,"backend","src","app","utils","cookie.ts"),N),await s.outputFile(o.join(t,"backend","src","app","utils","token.ts"),D)),T&&(await s.outputFile(o.join(t,"frontend","src","lib","axios","httpClient.ts"),K),await s.outputFile(o.join(t,"frontend","src","lib","tokenUtils.ts"),Q),await s.outputFile(o.join(t,"frontend","src","lib","cookieUtils.ts"),Y),await s.outputFile(o.join(t,"frontend","src","services","auth.actions.ts"),X)),h&&(await s.outputFile(o.join(t,"frontend","src","components","auth","login-form.tsx"),Z),await s.outputFile(o.join(t,"frontend","src","components","auth","register-form.tsx"),ee),await s.outputFile(o.join(t,"frontend","src","app","(auth)","login","page.tsx"),C("login")),await s.outputFile(o.join(t,"frontend","src","app","(auth)","register","page.tsx"),C("register"))),await s.outputFile(o.join(t,"backend","tsconfig.json"),he),await s.outputFile(o.join(t,".gitignore"),$),await s.outputFile(o.join(t,"LICENSE"),L),await s.outputFile(o.join(t,"README.md"),H(p)),await s.outputFile(o.join(t,"CODE_OF_CONDUCT.md"),z);let A=we.randomBytes(32).toString("hex"),i=we.randomBytes(32).toString("hex");await s.outputFile(o.join(t,".env"),`DATABASE_URL="postgresql://postgres:postgres@localhost:5432/${p}"
|
|
1049
|
+
JWT_SECRET="${A}"
|
|
1050
|
+
BETTER_AUTH_SECRET="${i}"
|
|
416
1051
|
NODE_ENV="development"
|
|
417
|
-
PORT=8000
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
`)
|
|
421
|
-
|
|
1052
|
+
PORT=8000
|
|
1053
|
+
BETTER_AUTH_BASE_URL="http://localhost:8000"
|
|
1054
|
+
CLIENT_URL="http://localhost:3000"
|
|
1055
|
+
NEXT_PUBLIC_API_URL="http://localhost:8000/api/v1"`),await s.outputFile(o.join(t,"package.json"),B(p));let w={name:`${p}-backend`,version:"1.0.0",type:"module",scripts:{test:'echo "Error: no test specified" && exit 1',dev:"nodemon --exec tsx src/server.ts",build:"prisma generate && tsup src/server.ts --format esm --platform node --target node20 --outDir dist --external pg-native",postinstall:"prisma generate",start:"node dist/server.js","prisma:generate":"prisma generate","prisma:migrate":"prisma migrate dev","prisma:studio":"prisma studio",seed:"tsx prisma/seed.ts",setup:"pnpm install && pnpm add @prisma/adapter-pg pg && pnpm add -D @types/pg && pnpm prisma:generate",predev:"pnpm run prisma:generate",init:"pnpm run prisma:generate && pnpm run prisma:migrate --name init",lint:"eslint src/**/*.ts","lint:fix":"eslint src/**/*.ts --fix",format:"prettier --write .",push:"prisma db push",pull:"prisma db pull"},dependencies:{...u?{"@prisma/adapter-pg":"^7.5.0","@prisma/client":"^7.5.0",pg:"^8.20.0"}:{},...g?{"better-auth":"^1.5.6"}:{},"cookie-parser":"^1.4.7",cors:"^2.8.6",dompurify:"^3.3.3",dotenv:"^17.3.1",express:"^5.2.1","express-rate-limit":"^8.3.1",helmet:"^8.1.0","http-status":"^2.1.0",jsdom:"^29.0.1",jsonwebtoken:"^9.0.3",morgan:"^1.10.1",winston:"^3.19.0",zod:"^4.3.6"},devDependencies:{"@types/cookie-parser":"^1.4.10","@types/cors":"^2.8.19","@types/express":"^5.0.6","@types/node":"^20.19.37",...u?{"@types/pg":"^8.20.0",prisma:"^7.5.0"}:{},"@types/morgan":"^1.9.10","@types/jsdom":"^21.1.7",tsx:"^4.21.0",nodemon:"^3.1.14",tsup:"^8.5.1",typescript:"^5.9.3",eslint:"^9.21.0",prettier:"^3.5.2"}};if(await s.writeJson(o.join(t,"backend","package.json"),w,{spaces:2}),S.succeed(a.green("\u2705 Project structure created! \u2728")),m&&(console.log(a.yellow(`
|
|
1056
|
+
\u{1F4E6} Finalizing dependencies with ${d}...
|
|
1057
|
+
`)),l(`${d} install`,t),l(`${d} install`,o.join(t,"backend")),T||h)){console.log(a.yellow(`
|
|
1058
|
+
\u{1F4E6} Adding frontend auth dependencies...
|
|
1059
|
+
`));let y=["axios"];h&&y.push("react-hook-form","zod","@hookform/resolvers/zod","@tanstack/react-query"),l(`${d} add ${y.join(" ")}`,o.join(t,"frontend"))}if(oe){console.log(a.cyan(`
|
|
1060
|
+
Git Initializing...`));try{let y=o.join(t,"frontend",".git");s.existsSync(y)&&await s.remove(y),l("git init",t),l("git add .",t),l('git commit -m "Initial commit from shakil-stack"',t),console.log(a.green(" Git initialized successfully!"))}catch{console.log(a.yellow("\u26A0\uFE0F Warning: Failed to initialize Git."))}}console.log(a.cyan(`
|
|
1061
|
+
\u{1F525} Your project is ready! Follow these steps to start:
|
|
1062
|
+
`)),r!=="./"&&r!=="."&&console.log(a.white(` 1. ${a.bold(`cd ${r}`)}`)),console.log(a.white(` 2. Ensure your ${a.bold("PostgreSQL")} database is running.`)),console.log(a.white(` 3. Update ${a.bold(".env")} credentials if needed.`)),console.log(a.white(` 4. ${a.bold("pnpm dev:backend")} (Starts server & Prisma Studio)`)),console.log(a.white(` 5. ${a.bold("pnpm dev:frontend")} (Starts Next.js application)`)),console.log(a.green(`
|
|
1063
|
+
Happy coding! \u{1F680}
|
|
1064
|
+
`))}catch(b){S.fail(a.red("\u274C Failed to initialize project.")),console.error(b),process.exit(1)}};import R from"fs-extra";import te from"path";import v from"chalk";import ze from"ora";var je=(e,r)=>`import { Request, Response } from 'express';
|
|
422
1065
|
import httpStatus from 'http-status';
|
|
423
1066
|
import catchAsync from '../../utils/catchAsync.js';
|
|
424
1067
|
import sendResponse from '../../utils/sendResponse.js';
|
|
425
|
-
import { ${e}Service } from './${
|
|
1068
|
+
import { ${e}Service } from './${r}.service.js';
|
|
426
1069
|
|
|
427
1070
|
const create${e} = catchAsync(async (req: Request, res: Response) => {
|
|
428
1071
|
const result = await ${e}Service.create${e}IntoDB(req.body);
|
|
@@ -437,7 +1080,7 @@ const create${e} = catchAsync(async (req: Request, res: Response) => {
|
|
|
437
1080
|
export const ${e}Controller = {
|
|
438
1081
|
create${e},
|
|
439
1082
|
};
|
|
440
|
-
`,
|
|
1083
|
+
`,Ce=(e,r)=>`import { ${e} } from '@prisma/client';
|
|
441
1084
|
import prisma from '../../lib/prisma.js';
|
|
442
1085
|
|
|
443
1086
|
const create${e}IntoDB = async (payload: any) => {
|
|
@@ -448,18 +1091,18 @@ const create${e}IntoDB = async (payload: any) => {
|
|
|
448
1091
|
export const ${e}Service = {
|
|
449
1092
|
create${e}IntoDB,
|
|
450
1093
|
};
|
|
451
|
-
`,
|
|
452
|
-
import { ${e}Controller } from './${
|
|
1094
|
+
`,Re=(e,r)=>`import { Router } from 'express';
|
|
1095
|
+
import { ${e}Controller } from './${r}.controller.js';
|
|
453
1096
|
|
|
454
1097
|
const router = Router();
|
|
455
1098
|
|
|
456
|
-
router.post('/create-${
|
|
1099
|
+
router.post('/create-${r}', ${e}Controller.create${e});
|
|
457
1100
|
|
|
458
1101
|
export const ${e}Routes = router;
|
|
459
|
-
`,
|
|
1102
|
+
`,Se=e=>`export type I${e} = {
|
|
460
1103
|
// Define interface
|
|
461
1104
|
};
|
|
462
|
-
`,
|
|
1105
|
+
`,Ae=e=>`import { z } from 'zod';
|
|
463
1106
|
|
|
464
1107
|
const create${e}ValidationSchema = z.object({
|
|
465
1108
|
body: z.object({
|
|
@@ -470,11 +1113,17 @@ const create${e}ValidationSchema = z.object({
|
|
|
470
1113
|
export const ${e}Validations = {
|
|
471
1114
|
create${e}ValidationSchema,
|
|
472
1115
|
};
|
|
473
|
-
`,
|
|
474
|
-
`,
|
|
1116
|
+
`,Ee=e=>`export const ${e}SearchableFields = [];
|
|
1117
|
+
`,Fe=e=>`model ${e} {
|
|
475
1118
|
id String @id @default(uuid())
|
|
476
1119
|
name String
|
|
477
1120
|
createdAt DateTime @default(now())
|
|
478
1121
|
updatedAt DateTime @updatedAt
|
|
479
1122
|
}
|
|
480
|
-
`;var
|
|
1123
|
+
`;var Pe=async e=>{e||(console.log(v.red("\u274C Error: Module name is required.")),process.exit(1));let r=e.charAt(0).toUpperCase()+e.slice(1),d=e.toLowerCase(),x=R.existsSync("backend")?"backend":".",m=te.join(x,"src","app","module",r);R.existsSync(te.join(x,"src","app","module"))||(console.log(v.red("\u274C Error: This command must be run inside your shakil-stack project root or backend directory.")),process.exit(1)),R.existsSync(m)&&(console.log(v.red(`\u274C Error: Module ${r} already exists.`)),process.exit(1));let u=ze(`\u{1F6E0}\uFE0F Generating module: ${v.cyan(r)}...`).start();try{await R.ensureDir(m);let g={"controller.ts":je(r,d),"service.ts":Ce(r,d),"route.ts":Re(r,d),"interface.ts":Se(r),"validation.ts":Ae(r),"constant.ts":Ee(r)};await R.outputFile(te.join(x,"prisma","schema",`${d}.prisma`),Fe(r));for(let[T,h]of Object.entries(g))await R.outputFile(te.join(m,`${d}.${T}`),h);u.succeed(v.green(`\u2705 Module ${r} generated successfully! \u2728`)),console.log(v.gray(`Created at: ${m}`))}catch(g){u.fail(v.red("\u274C Failed to generate module.")),console.error(g)}};import $e from"fs-extra";import He from"chalk";var Ie=async()=>{let e=E(),r=$e.existsSync("backend")?"backend":".";console.log(He.cyan(`\u{1F3D7}\uFE0F Building backend with ${e}...`)),l(`${e} run build`,r)};import Be from"fs-extra";import re from"chalk";var Ue=async e=>{let r=Be.existsSync("backend")?"backend":".";e==="generate"?(console.log(re.cyan("\u{1F504} Generating Prisma client...")),l("npx prisma generate",r)):e==="migrate"?(console.log(re.cyan("\u{1F680} Running Prisma migrations...")),l("npx prisma migrate dev",r)):console.log(re.red(`\u274C Error: Unknown prisma subcommand: ${e}`))};import c from"fs-extra";import n from"path";import f from"chalk";import Me from"ora";var Oe=async()=>{let e=process.cwd(),r=n.join(e,"backend"),d=n.basename(e);c.existsSync(r)||(console.log(f.red("\u274C Error: Not in a shakil-stack project root (backend folder not found).")),process.exit(1));let x=Me("\u{1F504} Updating project structure...").start();try{let m=n.join(r,".env"),u=n.join(e,".env"),g="";c.existsSync(m)?(g=await c.readFile(m,"utf-8"),c.existsSync(u)?(console.log(f.yellow(`
|
|
1124
|
+
\u26A0\uFE0F Both backend/.env and root .env exist. Merging...`)),await c.remove(m)):(await c.move(m,u),console.log(f.yellow(`
|
|
1125
|
+
\u{1F4DD} Moved .env from backend/ to root.`)))):c.existsSync(u)&&(g=await c.readFile(u,"utf-8"));let T=['BETTER_AUTH_BASE_URL="http://localhost:8000"','CLIENT_URL="http://localhost:3000"','JWT_ACCESS_EXPIRES_IN="1h"','JWT_REFRESH_EXPIRES_IN="7d"'],h=g;for(let i of T){let w=i.split("=")[0];h.includes(w)||(h+=`
|
|
1126
|
+
${i}`)}h!==g&&(await c.outputFile(u,h.trim()+`
|
|
1127
|
+
`),console.log(f.green("\u2705 Updated root .env with missing variables.")));let oe=[{path:n.join(r,"src","app","config","index.ts"),content:F},{path:n.join(r,"src","app","lib","prisma.ts"),content:P},{path:n.join(r,"src","app","lib","auth.ts"),content:I},{path:n.join(r,"prisma","schema","schema.prisma"),content:U},{path:n.join(r,"prisma.config.ts"),content:_}];for(let i of oe)await c.outputFile(i.path,i.content);let k=n.join(r,"src","app","module","auth");await c.ensureDir(k);let t=[{path:n.join(k,"auth.controller.ts"),content:M},{path:n.join(k,"auth.service.ts"),content:V},{path:n.join(k,"auth.route.ts"),content:W},{path:n.join(k,"auth.interface.ts"),content:G},{path:n.join(k,"auth.validation.ts"),content:J},{path:n.join(r,"src","app","utils","jwt.ts"),content:q},{path:n.join(r,"src","app","utils","cookie.ts"),content:N},{path:n.join(r,"src","app","utils","token.ts"),content:D},{path:n.join(r,"prisma","schema","auth.prisma"),content:O}];for(let i of t)c.existsSync(i.path)||(await c.outputFile(i.path,i.content),console.log(f.cyan(`\u{1F195} Added ${n.basename(i.path)} to auth module.`)));let p=n.join(e,"frontend");if(c.existsSync(p)){let i=[{path:n.join(p,"src","lib","axios","httpClient.ts"),content:K},{path:n.join(p,"src","lib","tokenUtils.ts"),content:Q},{path:n.join(p,"src","lib","cookieUtils.ts"),content:Y},{path:n.join(p,"src","services","auth.actions.ts"),content:X},{path:n.join(p,"src","components","auth","login-form.tsx"),content:Z},{path:n.join(p,"src","components","auth","register-form.tsx"),content:ee},{path:n.join(p,"src","app","(auth)","login","page.tsx"),content:C("login")},{path:n.join(p,"src","app","(auth)","register","page.tsx"),content:C("register")}];for(let w of i)c.existsSync(w.path)||(await c.outputFile(w.path,w.content),console.log(f.cyan(`\u{1F195} Added ${n.basename(w.path)} to frontend.`)))}let S=n.basename(e),b=[{path:n.join(e,".gitignore"),content:$},{path:n.join(e,"LICENSE"),content:L},{path:n.join(e,"README.md"),content:H(S)},{path:n.join(e,"CODE_OF_CONDUCT.md"),content:z},{path:n.join(e,"package.json"),content:B(S)}];for(let i of b)c.existsSync(i.path)||(await c.outputFile(i.path,i.content),console.log(f.cyan(`\u{1F4C4} Added ${n.basename(i.path)} to root.`)));let A=n.join(r,".gitignore");if(c.existsSync(A)){let i=await c.readFile(A,"utf-8");i.includes(".env")&&(i=i.replace(/\.env\n?/,"").trim(),await c.outputFile(A,i+`
|
|
1128
|
+
`))}x.succeed(f.green("\u2705 Project updated successfully! \u2728")),console.log(f.cyan(`
|
|
1129
|
+
Next Steps:`)),console.log(f.white("1. Check your root .env file and update credentials if needed.")),console.log(f.white("2. Run 'shakil-stack prisma generate' to update the Prisma client."))}catch(m){x.fail(f.red("\u274C Failed to update project.")),console.error(m),process.exit(1)}};var Ge=We(import.meta.url),_e=ne.dirname(Ge),Je=ne.resolve(_e,qe.existsSync(ne.resolve(_e,"../../package.json"))?"../../package.json":"../package.json"),Ke=qe.readJsonSync(Je),j=new Ve;j.name("shakil-stack").description("Full-stack EchoNet-style project generator CLI").version(Ke.version);j.command("init").description("Initialize a new full-stack project").argument("[projectName]","Name of the project").action(e=>{se(e)});j.command("update").description("Update an existing project to the latest shakil-stack version").action(()=>{Oe()});j.command("generate").alias("g").description("Generate a new module").argument("<type>","Type of generation (module)").argument("<name>","Name of the module").action((e,r)=>{e==="module"?Pe(r):console.log(`\u274C Error: Unknown generation type: ${e}`)});j.command("build").description("Build the backend for production").action(()=>{Ie()});j.command("prisma").description("Prisma utilities").argument("<subcommand>","generate | migrate").action(e=>{Ue(e)});process.argv.slice(2).length?j.parse(process.argv):se();
|