@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.
Files changed (2) hide show
  1. package/dist/index.js +697 -48
  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 le}from"commander";import te from"fs-extra";import T from"path";import{fileURLToPath as me}from"url";import s from"fs-extra";import r from"path";import n from"chalk";import ne from"ora";import _ from"inquirer";import{execSync as oe}from"child_process";import ye from"fs-extra";var i=(e,o=process.cwd())=>{try{return oe(e,{stdio:"inherit",cwd:o}),!0}catch{return!1}},f=()=>{let e=process.env.npm_config_user_agent||"";return e.includes("pnpm")?"pnpm":e.includes("yarn")?"yarn":"npm"};var k=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
- `,j=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';
@@ -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
- `,v=`import dotenv from 'dotenv';
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
- `,R=`import "dotenv/config";
80
- import { PrismaClient } from "@prisma/client";
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
- `,S=`import { betterAuth } from "better-auth";
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.jwt_secret,
103
- baseURL: "http://localhost:8000",
104
- trustedOrigins: ["http://localhost:3000"],
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
- `,E=`import { Router } from 'express';
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
- `,A=`import { ErrorRequestHandler } from 'express';
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
- `,C=`import { Request, Response, NextFunction } from 'express';
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
- `,D=`import { NextFunction, Request, RequestHandler, Response } from 'express';
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
- `,F=`class ApiError extends Error {
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
- `,O=`import { JSDOM } from 'jsdom';
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
- `,P=`import { Request, Response, NextFunction } from 'express';
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
- `,I=`import { Response } from 'express';
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
- `,$=`generator client {
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
- `,N=`model User {
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
- `,q=`import "dotenv/config";
251
- import { defineConfig } from "prisma/config";
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
- `,H=`{
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
- `,M=`node_modules/
530
+ `,$=`node_modules/
375
531
  .env
376
532
  dist/
377
533
  *.log
378
534
  .DS_Store
379
- `,U=e=>`# \u{1F680} ${e}
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
- `;var x=async e=>{let o=e;o||(o=(await _.prompt([{type:"input",name:"projectName",message:"What is your project name?",default:"shakil-stack-app"}])).projectName),o||(console.log(n.red("\u274C Error: Project name is required.")),process.exit(1));let{packageManager:a,useShadcn:g,installDeps:c}=await _.prompt([{type:"list",name:"packageManager",message:"Which package manager do you want to use?",choices:["pnpm","npm","yarn"],default:f()},{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}]),t=r.join(process.cwd(),o);s.existsSync(t)&&(console.log(n.red(`\u274C Error: Directory ${o} already exists.`)),process.exit(1)),console.log(n.cyan(`
410
- \u{1F680} Initializing ${n.bold(o)}...
411
- `));let d=ne("\u{1F6E0}\uFE0F Creating project structure...").start();try{await s.ensureDir(t),await s.ensureDir(r.join(t,"backend","src","app","config")),await s.ensureDir(r.join(t,"backend","src","app","lib")),await s.ensureDir(r.join(t,"backend","src","app","module")),await s.ensureDir(r.join(t,"backend","src","app","routes")),await s.ensureDir(r.join(t,"backend","src","app","middleware")),await s.ensureDir(r.join(t,"backend","src","app","utils")),await s.ensureDir(r.join(t,"backend","src","app","errorHelpers")),await s.ensureDir(r.join(t,"backend","prisma","schema")),console.log(n.cyan(`
412
- \u{1F5BC}\uFE0F Scaffolding Next.js frontend...`)),i(`npx create-next-app@latest frontend --ts --tailwind --eslint --app --src-dir --import-alias "@/*" --use-${a}`,t);let l=["config","hooks","lib","services","types"];for(let y of l)await s.ensureDir(r.join(t,"frontend","src",y));if(g){console.log(n.cyan(`
413
- \u{1F3A8} Setting up shadcn/ui...`));try{i("npx shadcn@latest init -d",r.join(t,"frontend")),console.log(n.cyan("\u{1F4E6} Adding common shadcn/ui components...")),i(`npx shadcn@latest add ${["button","card","input","label","textarea","dialog","dropdown-menu","table","tabs","checkbox"].join(" ")} -y`,r.join(t,"frontend")),console.log(n.green("\u2705 shadcn/ui and common components initialized successfully!\u2728"))}catch{console.log(n.yellow(`
414
- \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(t,"backend","src","server.ts"),k(o)),await s.outputFile(r.join(t,"backend","src","app.ts"),j(o)),await s.outputFile(r.join(t,"backend","src","app","config","index.ts"),v),await s.outputFile(r.join(t,"backend","src","app","lib","prisma.ts"),R),await s.outputFile(r.join(t,"backend","src","app","lib","auth.ts"),S),await s.outputFile(r.join(t,"backend","src","app","routes","index.ts"),E),await s.outputFile(r.join(t,"backend","src","app","middleware","globalErrorHandler.ts"),A),await s.outputFile(r.join(t,"backend","src","app","middleware","notFound.ts"),C),await s.outputFile(r.join(t,"backend","src","app","middleware","sanitizeRequest.ts"),P),await s.outputFile(r.join(t,"backend","src","app","utils","catchAsync.ts"),D),await s.outputFile(r.join(t,"backend","src","app","utils","sendResponse.ts"),I),await s.outputFile(r.join(t,"backend","src","app","utils","sanitizer.ts"),O),await s.outputFile(r.join(t,"backend","src","app","errorHelpers","ApiError.ts"),F),await s.outputFile(r.join(t,"backend","prisma","schema","schema.prisma"),$),await s.outputFile(r.join(t,"backend","prisma","schema","auth.prisma"),N),await s.outputFile(r.join(t,"backend","prisma.config.ts"),q),await s.outputFile(r.join(t,"backend","tsconfig.json"),H),await s.outputFile(r.join(t,".gitignore"),M),await s.outputFile(r.join(t,"LICENSE"),L),await s.outputFile(r.join(t,"README.md"),U(o)),await s.outputFile(r.join(t,"CODE_OF_CONDUCT.md"),z),await s.outputFile(r.join(t,".env"),`DATABASE_URL="postgresql://postgres:postgres@localhost:5432/${o}"
415
- 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}"
416
1051
  NODE_ENV="development"
417
- PORT=8000`);let b={name:`${o}-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(t,"backend","package.json"),b,{spaces:2}),d.succeed(n.green("\u2705 Project structure created! \u2728")),c&&(console.log(n.yellow(`
418
- \u{1F4E6} Finalizing dependencies with ${a}...
419
- `)),i(`${a} install`,r.join(t,"backend"))),console.log(n.cyan("To get started:")),console.log(n.white(` cd ${o}`)),console.log(n.white(` cd backend && ${a} dev
420
- `)),console.log(n.white(` cd frontend && ${a} dev
421
- `))}catch(l){d.fail(n.red("\u274C Failed to initialize project.")),console.error(l),process.exit(1)}};import m from"fs-extra";import h from"path";import p from"chalk";import ie from"ora";var B=(e,o)=>`import { Request, Response } from 'express';
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 './${o}.service.js';
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
- `,W=(e,o)=>`import { ${e} } from '@prisma/client';
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
- `,G=(e,o)=>`import { Router } from 'express';
452
- import { ${e}Controller } from './${o}.controller.js';
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-${o}', ${e}Controller.create${e});
1099
+ router.post('/create-${r}', ${e}Controller.create${e});
457
1100
 
458
1101
  export const ${e}Routes = router;
459
- `,V=e=>`export type I${e} = {
1102
+ `,Se=e=>`export type I${e} = {
460
1103
  // Define interface
461
1104
  };
462
- `,J=e=>`import { z } from 'zod';
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
- `,Y=e=>`export const ${e}SearchableFields = [];
474
- `,K=e=>`model ${e} {
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 X=async e=>{e||(console.log(p.red("\u274C Error: Module name is required.")),process.exit(1));let o=e.charAt(0).toUpperCase()+e.slice(1),a=e.toLowerCase(),g=m.existsSync("backend")?"backend":".",c=h.join(g,"src","app","module",o);m.existsSync(h.join(g,"src","app","module"))||(console.log(p.red("\u274C Error: This command must be run inside your shakil-stack project root or backend directory.")),process.exit(1)),m.existsSync(c)&&(console.log(p.red(`\u274C Error: Module ${o} already exists.`)),process.exit(1));let t=ie(`\u{1F6E0}\uFE0F Generating module: ${p.cyan(o)}...`).start();try{await m.ensureDir(c);let d={"controller.ts":B(o,a),"service.ts":W(o,a),"route.ts":G(o,a),"interface.ts":V(o),"validation.ts":J(o),"constant.ts":Y(o)};await m.outputFile(h.join(g,"prisma","schema",`${a}.prisma`),K(o));for(let[l,b]of Object.entries(d))await m.outputFile(h.join(c,`${a}.${l}`),b);t.succeed(p.green(`\u2705 Module ${o} generated successfully! \u2728`)),console.log(p.gray(`Created at: ${c}`))}catch(d){t.fail(p.red("\u274C Failed to generate module.")),console.error(d)}};import pe from"fs-extra";import ce from"chalk";var Q=async()=>{let e=f(),o=pe.existsSync("backend")?"backend":".";console.log(ce.cyan(`\u{1F3D7}\uFE0F Building backend with ${e}...`)),i(`${e} run build`,o)};import de from"fs-extra";import w from"chalk";var Z=async e=>{let o=de.existsSync("backend")?"backend":".";e==="generate"?(console.log(w.cyan("\u{1F504} Generating Prisma client...")),i("npx prisma generate",o)):e==="migrate"?(console.log(w.cyan("\u{1F680} Running Prisma migrations...")),i("npx prisma migrate dev",o)):console.log(w.red(`\u274C Error: Unknown prisma subcommand: ${e}`))};var ue=me(import.meta.url),ee=T.dirname(ue),ge=T.resolve(ee,te.existsSync(T.resolve(ee,"../../package.json"))?"../../package.json":"../package.json"),fe=te.readJsonSync(ge),u=new le;u.name("shakil-stack").description("Full-stack EchoNet-style project generator CLI").version(fe.version);u.command("init").description("Initialize a new full-stack project").argument("[projectName]","Name of the project").action(e=>{x(e)});u.command("generate").alias("g").description("Generate a new module").argument("<type>","Type of generation (module)").argument("<name>","Name of the module").action((e,o)=>{e==="module"?X(o):console.log(`\u274C Error: Unknown generation type: ${e}`)});u.command("build").description("Build the backend for production").action(()=>{Q()});u.command("prisma").description("Prisma utilities").argument("<subcommand>","generate | migrate").action(e=>{Z(e)});process.argv.slice(2).length?u.parse(process.argv):x();
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.6",
3
+ "version": "2.2.8",
4
4
  "description": "Full-stack EchoNet-style project generator CLI",
5
5
  "keywords": [
6
6
  "shakil-stack",