@shakil-dev/shakil-stack 2.2.7 → 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.
Files changed (2) hide show
  1. package/dist/index.js +691 -54
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,20 +1,48 @@
1
1
  #!/usr/bin/env node
2
- import{Command as Te}from"commander";import pe from"fs-extra";import N from"path";import{fileURLToPath as we}from"url";import s from"fs-extra";import r from"path";import n from"chalk";import ue from"ora";import K from"inquirer";import{execSync as me}from"child_process";import Se from"fs-extra";var m=(e,t=process.cwd())=>{try{return me(e,{stdio:"inherit",cwd:t}),!0}catch{return!1}},w=()=>{let e=process.env.npm_config_user_agent||"";return e.includes("pnpm")?"pnpm":e.includes("yarn")?"yarn":"npm"};var $=e=>`import { Server } from 'http';
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
- const server: Server = app.listen(config.port, () => {
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
- `,_=e=>`import cors from 'cors';
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';
@@ -66,7 +94,7 @@ app.use(globalErrorHandler);
66
94
  app.use(notFound);
67
95
 
68
96
  export default app;
69
- `,j=`import dotenv from 'dotenv';
97
+ `,F=`import dotenv from 'dotenv';
70
98
  import path from 'path';
71
99
 
72
100
  dotenv.config({ path: path.join(process.cwd(), "..", ".env") });
@@ -76,11 +104,14 @@ export default {
76
104
  port: process.env.PORT || 8000,
77
105
  database_url: process.env.DATABASE_URL,
78
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',
79
110
  better_auth_base_url: process.env.BETTER_AUTH_BASE_URL,
80
111
  client_url: process.env.CLIENT_URL,
81
112
  };
82
- `,v=`import "dotenv/config";
83
- import { PrismaClient } from "@prisma/client";
113
+ `,P=`import "dotenv/config";
114
+ import { PrismaClient } from "../../../generated/prisma/index.js";
84
115
  import pkg from 'pg';
85
116
  import { PrismaPg } from '@prisma/adapter-pg';
86
117
  import config from '../config/index.js';
@@ -93,7 +124,7 @@ const prisma = new PrismaClient({ adapter });
93
124
 
94
125
  export default prisma;
95
126
  export { prisma };
96
- `,k=`import { betterAuth } from "better-auth";
127
+ `,I=`import { betterAuth } from "better-auth";
97
128
  import { prismaAdapter } from "better-auth/adapters/prisma";
98
129
  import config from "../config/index.js";
99
130
  import { prisma } from "./prisma.js";
@@ -102,17 +133,29 @@ export const auth = betterAuth({
102
133
  database: prismaAdapter(prisma, {
103
134
  provider: "postgresql",
104
135
  }),
105
- secret: config.jwt_secret,
136
+ secret: config.better_auth_secret,
106
137
  baseURL: config.better_auth_base_url,
107
138
  trustedOrigins: [config.client_url as string],
108
139
  emailAndPassword: {
109
140
  enabled: true,
110
141
  },
111
142
  });
112
- `,q=`import { Router } from 'express';
143
+ `,ce=`import { Router } from "express";
144
+ import { AuthRoutes } from "../module/auth/auth.route.js";
145
+
113
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
+
114
157
  export default router;
115
- `,L=`import { ErrorRequestHandler } from 'express';
158
+ `,pe=`import { ErrorRequestHandler } from 'express';
116
159
  import config from '../config/index.js';
117
160
 
118
161
  const globalErrorHandler: ErrorRequestHandler = (error, req, res, next) => {
@@ -124,7 +167,7 @@ const globalErrorHandler: ErrorRequestHandler = (error, req, res, next) => {
124
167
  };
125
168
 
126
169
  export default globalErrorHandler;
127
- `,U=`import { Request, Response, NextFunction } from 'express';
170
+ `,le=`import { Request, Response, NextFunction } from 'express';
128
171
  import httpStatus from 'http-status';
129
172
 
130
173
  const notFound = (req: Request, res: Response, next: NextFunction) => {
@@ -135,7 +178,7 @@ const notFound = (req: Request, res: Response, next: NextFunction) => {
135
178
  };
136
179
 
137
180
  export default notFound;
138
- `,H=`import { NextFunction, Request, RequestHandler, Response } from 'express';
181
+ `,ue=`import { NextFunction, Request, RequestHandler, Response } from 'express';
139
182
 
140
183
  const catchAsync = (fn: RequestHandler) => {
141
184
  return async (req: Request, res: Response, next: NextFunction) => {
@@ -148,7 +191,7 @@ const catchAsync = (fn: RequestHandler) => {
148
191
  };
149
192
 
150
193
  export default catchAsync;
151
- `,M=`class ApiError extends Error {
194
+ `,de=`class ApiError extends Error {
152
195
  statusCode: number;
153
196
  constructor(statusCode: number, message: string | undefined, stack = '') {
154
197
  super(message);
@@ -158,7 +201,7 @@ export default catchAsync;
158
201
  }
159
202
  }
160
203
  export default ApiError;
161
- `,z=`import { JSDOM } from 'jsdom';
204
+ `,me=`import { JSDOM } from 'jsdom';
162
205
  import createDOMPurify from 'dompurify';
163
206
 
164
207
  const window = new JSDOM('').window;
@@ -173,7 +216,7 @@ export const sanitize = (data: any): any => {
173
216
  }
174
217
  return data;
175
218
  };
176
- `,B=`import { Request, Response, NextFunction } from 'express';
219
+ `,ge=`import { Request, Response, NextFunction } from 'express';
177
220
  import { sanitize } from '../utils/sanitizer.js';
178
221
 
179
222
  export const sanitizeRequest = (req: Request, res: Response, next: NextFunction) => {
@@ -182,7 +225,7 @@ export const sanitizeRequest = (req: Request, res: Response, next: NextFunction)
182
225
  if (req.params) sanitize(req.params);
183
226
  next();
184
227
  };
185
- `,W=`import { Response } from 'express';
228
+ `,fe=`import { Response } from 'express';
186
229
 
187
230
  type IResponse<T> = {
188
231
  statusCode: number;
@@ -206,16 +249,15 @@ const sendResponse = <T>(res: Response, data: IResponse<T>) => {
206
249
  };
207
250
 
208
251
  export default sendResponse;
209
- `,E=`generator client {
252
+ `,U=`generator client {
210
253
  provider = "prisma-client-js"
211
254
  output = "../../generated/prisma"
212
255
  }
213
256
 
214
257
  datasource db {
215
258
  provider = "postgresql"
216
- url = env("DATABASE_URL")
217
259
  }
218
- `,G=`model User {
260
+ `,O=`model User {
219
261
  id String @id @default(uuid())
220
262
  email String @unique
221
263
  name String
@@ -251,9 +293,12 @@ model Account {
251
293
  updatedAt DateTime @updatedAt
252
294
  user User @relation(fields: [userId], references: [id], onDelete: Cascade)
253
295
  }
254
- `,R=`import "dotenv/config";
255
- import { defineConfig } from "prisma/config";
296
+ `,_=`import dotenv from "dotenv";
297
+ import path from "path";
256
298
  import process from "process";
299
+ import { defineConfig } from "prisma/config";
300
+
301
+ dotenv.config({ path: path.join(process.cwd(), "..", ".env") });
257
302
 
258
303
  export default defineConfig({
259
304
  schema: "prisma/schema",
@@ -261,7 +306,94 @@ export default defineConfig({
261
306
  url: process.env.DATABASE_URL,
262
307
  },
263
308
  });
264
- `,V=`{
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=`{
265
397
  "compilerOptions": {
266
398
  "target": "ES2022",
267
399
  "module": "NodeNext",
@@ -280,7 +412,27 @@ export default defineConfig({
280
412
  "include": ["src/**/*"],
281
413
  "exclude": ["node_modules", "dist"]
282
414
  }
283
- `;var S=`MIT License
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;
435
+ `;var L=`MIT License
284
436
 
285
437
  Copyright (c) 2026 Shakil Ahmed Billal
286
438
 
@@ -301,7 +453,7 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
301
453
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
302
454
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
303
455
  SOFTWARE.
304
- `,A=`# Contributor Covenant Code of Conduct
456
+ `,z=`# Contributor Covenant Code of Conduct
305
457
 
306
458
  ## Our Pledge
307
459
 
@@ -375,12 +527,12 @@ version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
375
527
 
376
528
  [homepage]: http://contributor-covenant.org
377
529
  [version]: http://contributor-covenant.org/version/1/4
378
- `,C=`node_modules/
530
+ `,$=`node_modules/
379
531
  .env
380
532
  dist/
381
533
  *.log
382
534
  .DS_Store
383
- `,F=e=>`# \u{1F680} ${e}
535
+ `,H=e=>`# \u{1F680} ${e}
384
536
 
385
537
  This project was generated using [Shakil-Stack](https://github.com/shakil-ahmed-billal/shakil-stack-cli).
386
538
 
@@ -410,25 +562,510 @@ ${e}/
410
562
 
411
563
  ---
412
564
  Built with \u26A1 by **Shakil Ahmed Billal**
413
- `;var O=async e=>{let t=e;t||(t=(await K.prompt([{type:"input",name:"projectName",message:"What is your project name?",default:"shakil-stack-app"}])).projectName),t||(console.log(n.red("\u274C Error: Project name is required.")),process.exit(1));let{packageManager:p,useShadcn:f,installDeps:l}=await K.prompt([{type:"list",name:"packageManager",message:"Which package manager do you want to use?",choices:["pnpm","npm","yarn"],default:w()},{type:"confirm",name:"useShadcn",message:"Would you like to use shadcn/ui?",default:!0},{type:"confirm",name:"installDeps",message:"Do you want to install dependencies automatically?",default:!0}]),o=r.join(process.cwd(),t);s.existsSync(o)&&(console.log(n.red(`\u274C Error: Directory ${t} already exists.`)),process.exit(1)),console.log(n.cyan(`
414
- \u{1F680} Initializing ${n.bold(t)}...
415
- `));let d=ue("\u{1F6E0}\uFE0F Creating project structure...").start();try{await s.ensureDir(o),await s.ensureDir(r.join(o,"backend","src","app","config")),await s.ensureDir(r.join(o,"backend","src","app","lib")),await s.ensureDir(r.join(o,"backend","src","app","module")),await s.ensureDir(r.join(o,"backend","src","app","routes")),await s.ensureDir(r.join(o,"backend","src","app","middleware")),await s.ensureDir(r.join(o,"backend","src","app","utils")),await s.ensureDir(r.join(o,"backend","src","app","errorHelpers")),await s.ensureDir(r.join(o,"backend","prisma","schema")),console.log(n.cyan(`
416
- \u{1F5BC}\uFE0F Scaffolding Next.js frontend...`)),m(`npx create-next-app@latest frontend --ts --tailwind --eslint --app --src-dir --import-alias "@/*" --use-${p}`,o);let g=["config","hooks","lib","services","types"];for(let T of g)await s.ensureDir(r.join(o,"frontend","src",T));if(f){console.log(n.cyan(`
417
- \u{1F3A8} Setting up shadcn/ui...`));try{m("npx shadcn@latest init -d",r.join(o,"frontend")),console.log(n.cyan("\u{1F4E6} Adding common shadcn/ui components...")),m(`npx shadcn@latest add ${["button","card","input","label","textarea","dialog","dropdown-menu","table","tabs","checkbox"].join(" ")} -y`,r.join(o,"frontend")),console.log(n.green("\u2705 shadcn/ui and common components initialized successfully!\u2728"))}catch{console.log(n.yellow(`
418
- \u26A0\uFE0F Warning: Failed to automate shadcn/ui init. You can run "npx shadcn@latest init" in the frontend folder.`))}}await s.outputFile(r.join(o,"backend","src","server.ts"),$(t)),await s.outputFile(r.join(o,"backend","src","app.ts"),_(t)),await s.outputFile(r.join(o,"backend","src","app","config","index.ts"),j),await s.outputFile(r.join(o,"backend","src","app","lib","prisma.ts"),v),await s.outputFile(r.join(o,"backend","src","app","lib","auth.ts"),k),await s.outputFile(r.join(o,"backend","src","app","routes","index.ts"),q),await s.outputFile(r.join(o,"backend","src","app","middleware","globalErrorHandler.ts"),L),await s.outputFile(r.join(o,"backend","src","app","middleware","notFound.ts"),U),await s.outputFile(r.join(o,"backend","src","app","middleware","sanitizeRequest.ts"),B),await s.outputFile(r.join(o,"backend","src","app","utils","catchAsync.ts"),H),await s.outputFile(r.join(o,"backend","src","app","utils","sendResponse.ts"),W),await s.outputFile(r.join(o,"backend","src","app","utils","sanitizer.ts"),z),await s.outputFile(r.join(o,"backend","src","app","errorHelpers","ApiError.ts"),M),await s.outputFile(r.join(o,"backend","prisma","schema","schema.prisma"),E),await s.outputFile(r.join(o,"backend","prisma","schema","auth.prisma"),G),await s.outputFile(r.join(o,"backend","prisma.config.ts"),R),await s.outputFile(r.join(o,"backend","tsconfig.json"),V),await s.outputFile(r.join(o,".gitignore"),C),await s.outputFile(r.join(o,"LICENSE"),S),await s.outputFile(r.join(o,"README.md"),F(t)),await s.outputFile(r.join(o,"CODE_OF_CONDUCT.md"),A),await s.outputFile(r.join(o,".env"),`DATABASE_URL="postgresql://postgres:postgres@localhost:5432/${t}"
419
- JWT_SECRET="your-secret-key"
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}"
420
1051
  NODE_ENV="development"
421
1052
  PORT=8000
422
1053
  BETTER_AUTH_BASE_URL="http://localhost:8000"
423
- CLIENT_URL="http://localhost:3000"`);let h={name:`${t}-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:{"@prisma/adapter-pg":"^7.5.0","@prisma/client":"^7.5.0","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",pg:"^8.20.0",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","@types/pg":"^8.20.0","@types/morgan":"^1.9.10","@types/jsdom":"^21.1.7",prisma:"^7.5.0",tsx:"^4.21.0",nodemon:"^3.1.14",tsup:"^8.5.1",typescript:"^5.9.3",eslint:"^9.21.0",prettier:"^3.5.2"}};await s.writeJson(r.join(o,"backend","package.json"),h,{spaces:2}),d.succeed(n.green("\u2705 Project structure created! \u2728")),l&&(console.log(n.yellow(`
424
- \u{1F4E6} Finalizing dependencies with ${p}...
425
- `)),m(`${p} install`,r.join(o,"backend"))),console.log(n.cyan("To get started:")),console.log(n.white(` cd ${t}`)),console.log(n.white(` cd backend && ${p} dev
426
- `)),console.log(n.white(` cd frontend && ${p} dev
427
- `))}catch(g){d.fail(n.red("\u274C Failed to initialize project.")),console.error(g),process.exit(1)}};import x from"fs-extra";import P from"path";import b from"chalk";import ge from"ora";var X=(e,t)=>`import { Request, Response } from 'express';
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';
428
1065
  import httpStatus from 'http-status';
429
1066
  import catchAsync from '../../utils/catchAsync.js';
430
1067
  import sendResponse from '../../utils/sendResponse.js';
431
- import { ${e}Service } from './${t}.service.js';
1068
+ import { ${e}Service } from './${r}.service.js';
432
1069
 
433
1070
  const create${e} = catchAsync(async (req: Request, res: Response) => {
434
1071
  const result = await ${e}Service.create${e}IntoDB(req.body);
@@ -443,7 +1080,7 @@ const create${e} = catchAsync(async (req: Request, res: Response) => {
443
1080
  export const ${e}Controller = {
444
1081
  create${e},
445
1082
  };
446
- `,Q=(e,t)=>`import { ${e} } from '@prisma/client';
1083
+ `,Ce=(e,r)=>`import { ${e} } from '@prisma/client';
447
1084
  import prisma from '../../lib/prisma.js';
448
1085
 
449
1086
  const create${e}IntoDB = async (payload: any) => {
@@ -454,18 +1091,18 @@ const create${e}IntoDB = async (payload: any) => {
454
1091
  export const ${e}Service = {
455
1092
  create${e}IntoDB,
456
1093
  };
457
- `,Z=(e,t)=>`import { Router } from 'express';
458
- import { ${e}Controller } from './${t}.controller.js';
1094
+ `,Re=(e,r)=>`import { Router } from 'express';
1095
+ import { ${e}Controller } from './${r}.controller.js';
459
1096
 
460
1097
  const router = Router();
461
1098
 
462
- router.post('/create-${t}', ${e}Controller.create${e});
1099
+ router.post('/create-${r}', ${e}Controller.create${e});
463
1100
 
464
1101
  export const ${e}Routes = router;
465
- `,ee=e=>`export type I${e} = {
1102
+ `,Se=e=>`export type I${e} = {
466
1103
  // Define interface
467
1104
  };
468
- `,te=e=>`import { z } from 'zod';
1105
+ `,Ae=e=>`import { z } from 'zod';
469
1106
 
470
1107
  const create${e}ValidationSchema = z.object({
471
1108
  body: z.object({
@@ -476,17 +1113,17 @@ const create${e}ValidationSchema = z.object({
476
1113
  export const ${e}Validations = {
477
1114
  create${e}ValidationSchema,
478
1115
  };
479
- `,oe=e=>`export const ${e}SearchableFields = [];
480
- `,re=e=>`model ${e} {
1116
+ `,Ee=e=>`export const ${e}SearchableFields = [];
1117
+ `,Fe=e=>`model ${e} {
481
1118
  id String @id @default(uuid())
482
1119
  name String
483
1120
  createdAt DateTime @default(now())
484
1121
  updatedAt DateTime @updatedAt
485
1122
  }
486
- `;var se=async e=>{e||(console.log(b.red("\u274C Error: Module name is required.")),process.exit(1));let t=e.charAt(0).toUpperCase()+e.slice(1),p=e.toLowerCase(),f=x.existsSync("backend")?"backend":".",l=P.join(f,"src","app","module",t);x.existsSync(P.join(f,"src","app","module"))||(console.log(b.red("\u274C Error: This command must be run inside your shakil-stack project root or backend directory.")),process.exit(1)),x.existsSync(l)&&(console.log(b.red(`\u274C Error: Module ${t} already exists.`)),process.exit(1));let o=ge(`\u{1F6E0}\uFE0F Generating module: ${b.cyan(t)}...`).start();try{await x.ensureDir(l);let d={"controller.ts":X(t,p),"service.ts":Q(t,p),"route.ts":Z(t,p),"interface.ts":ee(t),"validation.ts":te(t),"constant.ts":oe(t)};await x.outputFile(P.join(f,"prisma","schema",`${p}.prisma`),re(t));for(let[g,h]of Object.entries(d))await x.outputFile(P.join(l,`${p}.${g}`),h);o.succeed(b.green(`\u2705 Module ${t} generated successfully! \u2728`)),console.log(b.gray(`Created at: ${l}`))}catch(d){o.fail(b.red("\u274C Failed to generate module.")),console.error(d)}};import he from"fs-extra";import be from"chalk";var ne=async()=>{let e=w(),t=he.existsSync("backend")?"backend":".";console.log(be.cyan(`\u{1F3D7}\uFE0F Building backend with ${e}...`)),m(`${e} run build`,t)};import ye from"fs-extra";import I from"chalk";var ae=async e=>{let t=ye.existsSync("backend")?"backend":".";e==="generate"?(console.log(I.cyan("\u{1F504} Generating Prisma client...")),m("npx prisma generate --schema prisma/schema",t)):e==="migrate"?(console.log(I.cyan("\u{1F680} Running Prisma migrations...")),m("npx prisma migrate dev --schema prisma/schema",t)):console.log(I.red(`\u274C Error: Unknown prisma subcommand: ${e}`))};import i from"fs-extra";import c from"path";import u from"chalk";import xe from"ora";var ie=async()=>{let e=process.cwd(),t=c.join(e,"backend"),p=c.basename(e);i.existsSync(t)||(console.log(u.red("\u274C Error: Not in a shakil-stack project root (backend folder not found).")),process.exit(1));let f=xe("\u{1F504} Updating project structure...").start();try{let l=c.join(t,".env"),o=c.join(e,".env"),d="";i.existsSync(l)?(d=await i.readFile(l,"utf-8"),i.existsSync(o)?(console.log(u.yellow(`
487
- \u26A0\uFE0F Both backend/.env and root .env exist. Merging...`)),await i.remove(l)):(await i.move(l,o),console.log(u.yellow(`
488
- \u{1F4DD} Moved .env from backend/ to root.`)))):i.existsSync(o)&&(d=await i.readFile(o,"utf-8"));let g=['BETTER_AUTH_BASE_URL="http://localhost:8000"','CLIENT_URL="http://localhost:3000"'],h=d;for(let a of g){let de=a.split("=")[0];h.includes(de)||(h+=`
489
- ${a}`)}h!==d&&(await i.outputFile(o,h.trim()+`
490
- `),console.log(u.green("\u2705 Updated root .env with missing variables.")));let T=[{path:c.join(t,"src","app","config","index.ts"),content:j},{path:c.join(t,"src","app","lib","prisma.ts"),content:v},{path:c.join(t,"src","app","lib","auth.ts"),content:k},{path:c.join(t,"prisma","schema","schema.prisma"),content:E},{path:c.join(t,"prisma.config.ts"),content:R}];for(let a of T)await i.outputFile(a.path,a.content);let le=[{path:c.join(e,".gitignore"),content:C},{path:c.join(e,"LICENSE"),content:S},{path:c.join(e,"README.md"),content:F(p)},{path:c.join(e,"CODE_OF_CONDUCT.md"),content:A}];for(let a of le)i.existsSync(a.path)||(await i.outputFile(a.path,a.content),console.log(u.cyan(`\u{1F4C4} Added ${c.basename(a.path)} to root.`)));let D=c.join(t,".gitignore");if(i.existsSync(D)){let a=await i.readFile(D,"utf-8");a.includes(".env")&&(a=a.replace(/\.env\n?/,"").trim(),await i.outputFile(D,a+`
491
- `))}f.succeed(u.green("\u2705 Project updated successfully! \u2728")),console.log(u.cyan(`
492
- Next Steps:`)),console.log(u.white("1. Check your root .env file and update credentials if needed.")),console.log(u.white("2. Run 'shakil-stack prisma generate' to update the Prisma client."))}catch(l){f.fail(u.red("\u274C Failed to update project.")),console.error(l),process.exit(1)}};var je=we(import.meta.url),ce=N.dirname(je),ve=N.resolve(ce,pe.existsSync(N.resolve(ce,"../../package.json"))?"../../package.json":"../package.json"),ke=pe.readJsonSync(ve),y=new Te;y.name("shakil-stack").description("Full-stack EchoNet-style project generator CLI").version(ke.version);y.command("init").description("Initialize a new full-stack project").argument("[projectName]","Name of the project").action(e=>{O(e)});y.command("update").description("Update an existing project to the latest shakil-stack version").action(()=>{ie()});y.command("generate").alias("g").description("Generate a new module").argument("<type>","Type of generation (module)").argument("<name>","Name of the module").action((e,t)=>{e==="module"?se(t):console.log(`\u274C Error: Unknown generation type: ${e}`)});y.command("build").description("Build the backend for production").action(()=>{ne()});y.command("prisma").description("Prisma utilities").argument("<subcommand>","generate | migrate").action(e=>{ae(e)});process.argv.slice(2).length?y.parse(process.argv):O();
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shakil-dev/shakil-stack",
3
- "version": "2.2.7",
3
+ "version": "2.2.8",
4
4
  "description": "Full-stack EchoNet-style project generator CLI",
5
5
  "keywords": [
6
6
  "shakil-stack",