@shakil-dev/shakil-stack 2.2.7 → 2.2.9

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 +732 -57
  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 Ke}from"commander";import De from"fs-extra";import ne from"path";import{fileURLToPath as Ye}from"url";import s from"fs-extra";import o from"path";import je from"crypto";import a from"chalk";import Be from"ora";import Ce from"inquirer";import{execSync as He}from"child_process";import st from"fs-extra";var d=(e,r=process.cwd())=>{try{return He(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":"pnpm"};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
+ `,de=`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
+ `,ue=`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
+ `,fe=`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
+ `,ge=`import { Response } from 'express';
186
229
 
187
230
  type IResponse<T> = {
188
231
  statusCode: number;
@@ -206,19 +249,20 @@ 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 {
219
- id String @id @default(uuid())
260
+ `,N=`model User {
261
+ id String @id @default(cuid())
220
262
  email String @unique
221
263
  name String
264
+ emailVerified Boolean @default(false)
265
+ image String?
222
266
  createdAt DateTime @default(now())
223
267
  updatedAt DateTime @updatedAt
224
268
  accounts Account[]
@@ -226,17 +270,19 @@ datasource db {
226
270
  }
227
271
 
228
272
  model Session {
229
- id String @id @default(uuid())
273
+ id String @id @default(cuid())
230
274
  userId String
231
275
  token String @unique
232
276
  expiresAt DateTime
277
+ ipAddress String?
278
+ userAgent String?
233
279
  createdAt DateTime @default(now())
234
280
  updatedAt DateTime @updatedAt
235
281
  user User @relation(fields: [userId], references: [id], onDelete: Cascade)
236
282
  }
237
283
 
238
284
  model Account {
239
- id String @id @default(uuid())
285
+ id String @id @default(cuid())
240
286
  userId String
241
287
  accountId String
242
288
  providerId String
@@ -251,9 +297,21 @@ model Account {
251
297
  updatedAt DateTime @updatedAt
252
298
  user User @relation(fields: [userId], references: [id], onDelete: Cascade)
253
299
  }
254
- `,R=`import "dotenv/config";
255
- import { defineConfig } from "prisma/config";
300
+
301
+ model Verification {
302
+ id String @id @default(cuid())
303
+ identifier String
304
+ value String
305
+ expiresAt DateTime
306
+ createdAt DateTime @default(now())
307
+ updatedAt DateTime @updatedAt
308
+ }
309
+ `,O=`import dotenv from "dotenv";
310
+ import path from "path";
256
311
  import process from "process";
312
+ import { defineConfig } from "prisma/config";
313
+
314
+ dotenv.config({ path: path.join(process.cwd(), "..", ".env") });
257
315
 
258
316
  export default defineConfig({
259
317
  schema: "prisma/schema",
@@ -261,7 +319,94 @@ export default defineConfig({
261
319
  url: process.env.DATABASE_URL,
262
320
  },
263
321
  });
264
- `,V=`{
322
+ `,_=`import jwt, { JwtPayload, SignOptions } from 'jsonwebtoken';
323
+
324
+ const createToken = (payload: Record<string, unknown>, secret: string, options: SignOptions) => {
325
+ return jwt.sign(payload, secret, options);
326
+ };
327
+
328
+ const verifyToken = (token: string, secret: string) => {
329
+ try {
330
+ const decoded = jwt.verify(token, secret);
331
+ return { success: true, data: decoded as JwtPayload };
332
+ } catch (error) {
333
+ return { success: false, error };
334
+ }
335
+ };
336
+
337
+ export const jwtUtils = { createToken, verifyToken };
338
+ `,q=`import { CookieOptions, Request, Response } from "express";
339
+
340
+ const setCookie = (res: Response, key: string, value: string, options: CookieOptions) => {
341
+ res.cookie(key, value, options);
342
+ }
343
+
344
+ const getCookie = (req: Request, key: string) => {
345
+ return req.cookies?.[key];
346
+ }
347
+
348
+ const clearCookie = (res: Response, key: string, options: CookieOptions) => {
349
+ res.clearCookie(key, options);
350
+ }
351
+
352
+ export const CookieUtils = {
353
+ setCookie,
354
+ getCookie,
355
+ clearCookie,
356
+ };
357
+ `,D=`import { Response } from "express";
358
+ import { JwtPayload, SignOptions } from "jsonwebtoken";
359
+ import config from "../config/index.js";
360
+ import { jwtUtils } from "./jwt.js";
361
+
362
+ const getAccessToken = (payload: JwtPayload) => {
363
+ return jwtUtils.createToken(
364
+ payload,
365
+ config.jwt_secret as string,
366
+ { expiresIn: config.jwt_access_expires_in } as SignOptions
367
+ );
368
+ }
369
+
370
+ const getRefreshToken = (payload: JwtPayload) => {
371
+ return jwtUtils.createToken(
372
+ payload,
373
+ config.jwt_secret as string,
374
+ { expiresIn: config.jwt_refresh_expires_in } as SignOptions
375
+ );
376
+ }
377
+
378
+ export const setAccessTokenCookie = (res: Response, token: string) => {
379
+ res.cookie('accessToken', token, {
380
+ httpOnly: true,
381
+ secure: config.env === 'production',
382
+ sameSite: config.env === 'production' ? 'none' : 'lax',
383
+ maxAge: 3600000, // 1 hour
384
+ path: '/',
385
+ });
386
+ };
387
+
388
+ export const setRefreshTokenCookie = (res: Response, token: string) => {
389
+ res.cookie('refreshToken', token, {
390
+ httpOnly: true,
391
+ secure: config.env === 'production',
392
+ sameSite: config.env === 'production' ? 'none' : 'lax',
393
+ maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
394
+ path: '/',
395
+ });
396
+ };
397
+
398
+ export const setBetterAuthSessionCookie = (res: Response, token: string) => {
399
+ res.cookie('better-auth.session_token', token, {
400
+ httpOnly: true,
401
+ secure: config.env === 'production',
402
+ sameSite: config.env === 'production' ? 'none' : 'lax',
403
+ maxAge: 7 * 24 * 60 * 60 * 1000,
404
+ path: '/',
405
+ });
406
+ };
407
+
408
+ export const tokenUtils = { getAccessToken, getRefreshToken, setAccessTokenCookie, setRefreshTokenCookie, setBetterAuthSessionCookie };
409
+ `,he=`{
265
410
  "compilerOptions": {
266
411
  "target": "ES2022",
267
412
  "module": "NodeNext",
@@ -280,7 +425,22 @@ export default defineConfig({
280
425
  "include": ["src/**/*"],
281
426
  "exclude": ["node_modules", "dist"]
282
427
  }
283
- `;var S=`MIT License
428
+ `,ke=`import { NextFunction, Request, Response } from 'express';
429
+ import type { AnyZodObject } from 'zod';
430
+
431
+ const validateRequest = (schema: AnyZodObject) => {
432
+ return async (req: Request, res: Response, next: NextFunction): Promise<void> => {
433
+ try {
434
+ await schema.parseAsync(req.body);
435
+ return next();
436
+ } catch (error) {
437
+ next(error);
438
+ }
439
+ };
440
+ };
441
+
442
+ export default validateRequest;
443
+ `;var L=`MIT License
284
444
 
285
445
  Copyright (c) 2026 Shakil Ahmed Billal
286
446
 
@@ -301,7 +461,7 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
301
461
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
302
462
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
303
463
  SOFTWARE.
304
- `,A=`# Contributor Covenant Code of Conduct
464
+ `,$=`# Contributor Covenant Code of Conduct
305
465
 
306
466
  ## Our Pledge
307
467
 
@@ -375,12 +535,12 @@ version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
375
535
 
376
536
  [homepage]: http://contributor-covenant.org
377
537
  [version]: http://contributor-covenant.org/version/1/4
378
- `,C=`node_modules/
538
+ `,z=`node_modules/
379
539
  .env
380
540
  dist/
381
541
  *.log
382
542
  .DS_Store
383
- `,F=e=>`# \u{1F680} ${e}
543
+ `,H=e=>`# \u{1F680} ${e}
384
544
 
385
545
  This project was generated using [Shakil-Stack](https://github.com/shakil-ahmed-billal/shakil-stack-cli).
386
546
 
@@ -410,25 +570,540 @@ ${e}/
410
570
 
411
571
  ---
412
572
  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"
573
+ `,B=e=>`{
574
+ "name": "${e}",
575
+ "version": "1.0.0",
576
+ "private": true,
577
+ "scripts": {
578
+ "dev:backend": "cd backend && pnpm dev",
579
+ "dev:frontend": "cd frontend && pnpm dev",
580
+ "build:backend": "cd backend && pnpm build",
581
+ "build:frontend": "cd frontend && pnpm build",
582
+ "version:patch": "npm version patch --no-git-tag-version",
583
+ "push": "npm version patch && git add . && git commit -m 'chore: bump version' && git push"
584
+ }
585
+ }
586
+ `;var M=`import { Request, Response } from "express";
587
+ import httpStatus from "http-status";
588
+ import { AuthService } from "./auth.service.js";
589
+ import catchAsync from "../../utils/catchAsync.js";
590
+ import sendResponse from "../../utils/sendResponse.js";
591
+ import { tokenUtils } from "../../utils/token.js";
592
+ import config from "../../config/index.js";
593
+
594
+ const registerUser = catchAsync(async (req: Request, res: Response) => {
595
+ const result = await AuthService.registerUser(req.body);
596
+ const { accessToken, refreshToken, token, ...rest } = result as Record<string, any>;
597
+
598
+ tokenUtils.setAccessTokenCookie(res, accessToken);
599
+ tokenUtils.setRefreshTokenCookie(res, refreshToken);
600
+ tokenUtils.setBetterAuthSessionCookie(res, token as string);
601
+
602
+ sendResponse(res, {
603
+ statusCode: httpStatus.CREATED,
604
+ success: true,
605
+ message: "User registered successfully",
606
+ data: { token, accessToken, refreshToken, ...rest }
607
+ });
608
+ });
609
+
610
+ const loginUser = catchAsync(async (req: Request, res: Response) => {
611
+ const result = await AuthService.loginUser(req.body);
612
+ const { accessToken, refreshToken, token, ...rest } = result as Record<string, any>;
613
+
614
+ tokenUtils.setAccessTokenCookie(res, accessToken);
615
+ tokenUtils.setRefreshTokenCookie(res, refreshToken);
616
+ tokenUtils.setBetterAuthSessionCookie(res, token as string);
617
+
618
+ sendResponse(res, {
619
+ statusCode: httpStatus.OK,
620
+ success: true,
621
+ message: "User logged in successfully",
622
+ data: { token, accessToken, refreshToken, ...rest }
623
+ });
624
+ });
625
+
626
+ const logoutUser = catchAsync(async (req: Request, res: Response) => {
627
+ const betterAuthSessionToken = req.cookies?.["better-auth.session_token"] || "";
628
+ await AuthService.logoutUser(betterAuthSessionToken);
629
+
630
+ const isProd = config.env === 'production';
631
+ const cookieOptions = {
632
+ httpOnly: true,
633
+ secure: isProd,
634
+ sameSite: isProd ? "none" : "lax" as any,
635
+ path: '/'
636
+ };
637
+
638
+ res.clearCookie('accessToken', cookieOptions);
639
+ res.clearCookie('refreshToken', cookieOptions);
640
+ res.clearCookie('better-auth.session_token', cookieOptions);
641
+
642
+ sendResponse(res, {
643
+ statusCode: httpStatus.OK,
644
+ success: true,
645
+ message: "User logged out successfully",
646
+ data: null
647
+ });
648
+ });
649
+
650
+ const getMe = catchAsync(async (req: Request, res: Response) => {
651
+ const user = (req as any).user;
652
+
653
+ sendResponse(res, {
654
+ statusCode: httpStatus.OK,
655
+ success: true,
656
+ message: "User session retrieved successfully",
657
+ data: { user }
658
+ });
659
+ });
660
+
661
+ export const AuthController = { registerUser, loginUser, logoutUser, getMe };
662
+ `,V=`import status from "http-status";
663
+ import AppError from "../../errorHelpers/ApiError.js";
664
+ import { auth } from "../../lib/auth.js";
665
+ import { tokenUtils } from "../../utils/token.js";
666
+ import { ILoginUserPayload, IRegisterUserPayload } from "./auth.interface.js";
667
+
668
+ const registerUser = async (payload: IRegisterUserPayload) => {
669
+ const { name, email, password } = payload;
670
+
671
+ const data = await auth.api.signUpEmail({
672
+ body: { name, email, password: password || "" }
673
+ });
674
+
675
+ if (!data.user) {
676
+ throw new AppError(status.BAD_REQUEST, "Failed to register user");
677
+ }
678
+
679
+ const accessToken = tokenUtils.getAccessToken({
680
+ userId: data.user.id,
681
+ name: data.user.name,
682
+ email: data.user.email,
683
+ });
684
+
685
+ const refreshToken = tokenUtils.getRefreshToken({
686
+ userId: data.user.id,
687
+ name: data.user.name,
688
+ email: data.user.email,
689
+ });
690
+
691
+ return { ...data, accessToken, refreshToken };
692
+ }
693
+
694
+ const loginUser = async (payload: ILoginUserPayload) => {
695
+ const { email, password } = payload;
696
+
697
+ const data = await auth.api.signInEmail({
698
+ body: { email, password: password || "" }
699
+ });
700
+
701
+ if (!data.user) {
702
+ throw new AppError(status.UNAUTHORIZED, "Invalid credentials");
703
+ }
704
+
705
+ const accessToken = tokenUtils.getAccessToken({
706
+ userId: data.user.id,
707
+ name: data.user.name,
708
+ email: data.user.email,
709
+ });
710
+
711
+ const refreshToken = tokenUtils.getRefreshToken({
712
+ userId: data.user.id,
713
+ name: data.user.name,
714
+ email: data.user.email,
715
+ });
716
+
717
+ return { ...data, accessToken, refreshToken };
718
+ }
719
+
720
+ const logoutUser = async (sessionToken: string) => {
721
+ const result = await auth.api.signOut({
722
+ headers: new Headers({ Authorization: \`Bearer \${sessionToken}\` })
723
+ });
724
+ return result;
725
+ }
726
+
727
+ export const AuthService = { registerUser, loginUser, logoutUser };
728
+ `,W=`import { Router } from "express";
729
+ import { AuthController } from "./auth.controller.js";
730
+ import validateRequest from "../../middleware/validateRequest.js";
731
+ import { AuthValidation } from "./auth.validation.js";
732
+
733
+ const router = Router();
734
+
735
+ router.post(
736
+ "/register",
737
+ validateRequest(AuthValidation.registerValidationSchema),
738
+ AuthController.registerUser
739
+ );
740
+
741
+ router.post(
742
+ "/login",
743
+ validateRequest(AuthValidation.loginValidationSchema),
744
+ AuthController.loginUser
745
+ );
746
+
747
+ router.post("/logout", AuthController.logoutUser);
748
+
749
+ export const AuthRoutes = router;
750
+ `,G=`import { z } from "zod";
751
+ import { AuthValidation } from "./auth.validation.js";
752
+
753
+ export type IRegisterUserPayload = z.infer<typeof AuthValidation.registerValidationSchema>;
754
+ export type ILoginUserPayload = z.infer<typeof AuthValidation.loginValidationSchema>;
755
+ `,J=`import { z } from "zod";
756
+
757
+ const registerValidationSchema = z.object({
758
+ name: z.string().min(1, "Name is required"),
759
+ email: z.string().email("Invalid email address"),
760
+ password: z.string().min(6, "Password must be at least 6 characters"),
761
+ });
762
+
763
+ const loginValidationSchema = z.object({
764
+ email: z.string().email("Invalid email address"),
765
+ password: z.string().min(1, "Password is required"),
766
+ });
767
+
768
+ export const AuthValidation = {
769
+ registerValidationSchema,
770
+ loginValidationSchema,
771
+ };
772
+ `;var Q=`import axios from "axios";
773
+
774
+ export const httpClient = axios.create({
775
+ baseURL: process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000/api/v1",
776
+ withCredentials: true,
777
+ headers: {
778
+ "Content-Type": "application/json",
779
+ },
780
+ });
781
+
782
+ httpClient.interceptors.response.use(
783
+ (response) => response.data,
784
+ (error) => {
785
+ return Promise.reject(error);
786
+ }
787
+ );
788
+ `,K=`"use server";
789
+ import { cookies } from "next/headers";
790
+
791
+ export async function setTokenInCookies(name: string, token: string, maxAge?: number) {
792
+ const cookieStore = await cookies();
793
+ cookieStore.set(name, token, {
794
+ httpOnly: true,
795
+ secure: process.env.NODE_ENV === "production",
796
+ sameSite: process.env.NODE_ENV === "production" ? "none" : "lax",
797
+ path: "/",
798
+ maxAge: maxAge || 3600, // Default 1 hour
799
+ });
800
+ }
801
+ `,Y=`"use server";
802
+ import { cookies } from "next/headers";
803
+
804
+ export async function deleteCookie(name: string) {
805
+ const cookieStore = await cookies();
806
+ cookieStore.delete(name);
807
+ }
808
+ `,X=`"use server";
809
+
810
+ import { httpClient } from '@/lib/axios/httpClient';
811
+ import { setTokenInCookies } from '@/lib/tokenUtils';
812
+ import { deleteCookie } from '@/lib/cookieUtils';
813
+ import { cookies } from 'next/headers';
814
+
815
+ export const loginAction = async (payload: any) => {
816
+ try {
817
+ const response: any = await httpClient.post("/auth/login", payload);
818
+
819
+ if (response?.success && response?.data) {
820
+ const { accessToken, refreshToken, token } = response.data;
821
+
822
+ if (accessToken) await setTokenInCookies("accessToken", accessToken);
823
+ if (refreshToken) await setTokenInCookies("refreshToken", refreshToken);
824
+ if (token) await setTokenInCookies("better-auth.session_token", token, 24 * 60 * 60);
825
+ }
826
+
827
+ return response;
828
+ } catch (error: any) {
829
+ return {
830
+ success: false,
831
+ message: error?.response?.data?.message || error.message || "Login failed"
832
+ };
833
+ }
834
+ }
835
+
836
+ export const registerAction = async (payload: any) => {
837
+ try {
838
+ const response: any = await httpClient.post("/auth/register", payload);
839
+
840
+ if (response?.success && response?.data) {
841
+ const { accessToken, refreshToken, token } = response.data;
842
+
843
+ if (accessToken) await setTokenInCookies("accessToken", accessToken);
844
+ if (refreshToken) await setTokenInCookies("refreshToken", refreshToken);
845
+ if (token) await setTokenInCookies("better-auth.session_token", token, 24 * 60 * 60);
846
+ }
847
+
848
+ return response;
849
+ } catch (error: any) {
850
+ return {
851
+ success: false,
852
+ message: error?.response?.data?.message || error.message || "Registration failed"
853
+ };
854
+ }
855
+ }
856
+
857
+ export const logoutAction = async () => {
858
+ try {
859
+ await httpClient.post("/auth/logout", {});
860
+ await deleteCookie("accessToken");
861
+ await deleteCookie("refreshToken");
862
+ await deleteCookie("better-auth.session_token");
863
+ return { success: true };
864
+ } catch (error: any) {
865
+ await deleteCookie("accessToken");
866
+ await deleteCookie("refreshToken");
867
+ await deleteCookie("better-auth.session_token");
868
+ return { success: false, message: "Logged out locally." };
869
+ }
870
+ }
871
+ `,Z=`
872
+ "use client"
873
+
874
+ import { useForm } from "react-hook-form";
875
+ import { useQueryClient } from "@tanstack/react-query";
876
+ import { zodResolver } from "@hookform/resolvers/zod";
877
+ import * as z from "zod";
878
+ import { loginAction } from "@/services/auth.actions";
879
+ import { useRouter } from "next/navigation";
880
+ import { useState } from "react";
881
+ import { Button } from "@/components/ui/button";
882
+ import { Input } from "@/components/ui/input";
883
+ import { Label } from "@/components/ui/label";
884
+ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
885
+
886
+ const loginSchema = z.object({
887
+ email: z.string().email("Invalid email address"),
888
+ password: z.string().min(1, "Password is required"),
889
+ });
890
+
891
+ type LoginFormValues = z.infer<typeof loginSchema>;
892
+
893
+ export function LoginForm() {
894
+ const router = useRouter();
895
+ const queryClient = useQueryClient();
896
+ const [error, setError] = useState<string | null>(null);
897
+ const [loading, setLoading] = useState(false);
898
+
899
+ const { register, handleSubmit, formState: { errors } } = useForm<LoginFormValues>({
900
+ resolver: zodResolver(loginSchema),
901
+ });
902
+
903
+ const onSubmit = async (data: LoginFormValues) => {
904
+ setLoading(true);
905
+ setError(null);
906
+ try {
907
+ const response = await loginAction(data);
908
+ if (response.success) {
909
+ queryClient.invalidateQueries({ queryKey: ["user"] });
910
+ router.push("/dashboard");
911
+ } else {
912
+ setError(response.message);
913
+ }
914
+ } catch (err: any) {
915
+ setError("An unexpected error occurred");
916
+ } finally {
917
+ setLoading(false);
918
+ }
919
+ };
920
+
921
+ return (
922
+ <Card>
923
+ <CardHeader>
924
+ <CardTitle>Login</CardTitle>
925
+ <CardDescription>Enter your credentials to access your account</CardDescription>
926
+ </CardHeader>
927
+ <form onSubmit={handleSubmit(onSubmit)}>
928
+ <CardContent className="space-y-4">
929
+ <div className="space-y-2">
930
+ <Label htmlFor="email">Email</Label>
931
+ <Input id="email" type="email" {...register("email")} placeholder="m@example.com" />
932
+ {errors.email && <p className="text-sm text-red-500">{errors.email.message}</p>}
933
+ </div>
934
+ <div className="space-y-2">
935
+ <Label htmlFor="password">Password</Label>
936
+ <Input id="password" type="password" {...register("password")} />
937
+ {errors.password && <p className="text-sm text-red-500">{errors.password.message}</p>}
938
+ </div>
939
+ {error && <p className="text-sm text-red-500">{error}</p>}
940
+ </CardContent>
941
+ <CardFooter>
942
+ <Button type="submit" className="w-full" disabled={loading}>
943
+ {loading ? "Logging in..." : "Login"}
944
+ </Button>
945
+ </CardFooter>
946
+ </form>
947
+ </Card>
948
+ );
949
+ }
950
+ `,ee=`
951
+ "use client"
952
+
953
+ import { useForm } from "react-hook-form";
954
+ import { useQueryClient } from "@tanstack/react-query";
955
+ import { zodResolver } from "@hookform/resolvers/zod";
956
+ import * as z from "zod";
957
+ import { registerAction } from "@/services/auth.actions";
958
+ import { useRouter } from "next/navigation";
959
+ import { useState } from "react";
960
+ import { Button } from "@/components/ui/button";
961
+ import { Input } from "@/components/ui/input";
962
+ import { Label } from "@/components/ui/label";
963
+ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
964
+
965
+ const registerSchema = z.object({
966
+ name: z.string().min(1, "Name is required"),
967
+ email: z.string().email("Invalid email address"),
968
+ password: z.string().min(6, "Password must be at least 6 characters"),
969
+ });
970
+
971
+ type RegisterFormValues = z.infer<typeof registerSchema>;
972
+
973
+ export function RegisterForm() {
974
+ const router = useRouter();
975
+ const queryClient = useQueryClient();
976
+ const [error, setError] = useState<string | null>(null);
977
+ const [loading, setLoading] = useState(false);
978
+
979
+ const { register, handleSubmit, formState: { errors } } = useForm<RegisterFormValues>({
980
+ resolver: zodResolver(registerSchema),
981
+ });
982
+
983
+ const onSubmit = async (data: RegisterFormValues) => {
984
+ setLoading(true);
985
+ setError(null);
986
+ try {
987
+ const response = await registerAction(data);
988
+ if (response.success) {
989
+ queryClient.invalidateQueries({ queryKey: ["user"] });
990
+ router.push("/dashboard");
991
+ } else {
992
+ setError(response.message);
993
+ }
994
+ } catch (err: any) {
995
+ setError("An unexpected error occurred");
996
+ } finally {
997
+ setLoading(false);
998
+ }
999
+ };
1000
+
1001
+ return (
1002
+ <Card>
1003
+ <CardHeader>
1004
+ <CardTitle>Register</CardTitle>
1005
+ <CardDescription>Create a new account to get started</CardDescription>
1006
+ </CardHeader>
1007
+ <form onSubmit={handleSubmit(onSubmit)}>
1008
+ <CardContent className="space-y-4">
1009
+ <div className="space-y-2">
1010
+ <Label htmlFor="name">Name</Label>
1011
+ <Input id="name" {...register("name")} placeholder="John Doe" />
1012
+ {errors.name && <p className="text-sm text-red-500">{errors.name.message}</p>}
1013
+ </div>
1014
+ <div className="space-y-2">
1015
+ <Label htmlFor="email">Email</Label>
1016
+ <Input id="email" type="email" {...register("email")} placeholder="m@example.com" />
1017
+ {errors.email && <p className="text-sm text-red-500">{errors.email.message}</p>}
1018
+ </div>
1019
+ <div className="space-y-2">
1020
+ <Label htmlFor="password">Password</Label>
1021
+ <Input id="password" type="password" {...register("password")} />
1022
+ {errors.password && <p className="text-sm text-red-500">{errors.password.message}</p>}
1023
+ </div>
1024
+ {error && <p className="text-sm text-red-500">{error}</p>}
1025
+ </CardContent>
1026
+ <CardFooter>
1027
+ <Button type="submit" className="w-full" disabled={loading}>
1028
+ {loading ? "Creating account..." : "Register"}
1029
+ </Button>
1030
+ </CardFooter>
1031
+ </form>
1032
+ </Card>
1033
+ );
1034
+ }
1035
+ `,C=e=>`
1036
+ import { ${e==="login"?"LoginForm":"RegisterForm"} } from "@/components/auth/${e}-form";
1037
+
1038
+ export default function ${e==="login"?"LoginPage":"RegisterPage"}() {
1039
+ return (
1040
+ <div className="container flex items-center justify-center min-h-screen py-12">
1041
+ <div className="w-full max-w-md">
1042
+ <${e==="login"?"LoginForm":"RegisterForm"} />
1043
+ </div>
1044
+ </div>
1045
+ );
1046
+ }
1047
+ `,be=`
1048
+ "use client";
1049
+
1050
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
1051
+ import { ReactNode, useState } from "react";
1052
+
1053
+ export function Providers({ children }: { children: ReactNode }) {
1054
+ const [queryClient] = useState(() => new QueryClient());
1055
+
1056
+ return (
1057
+ <QueryClientProvider client={queryClient}>
1058
+ {children}
1059
+ </QueryClientProvider>
1060
+ );
1061
+ }
1062
+ `,we=`
1063
+ import { Providers } from "@/components/Providers";
1064
+ import "./globals.css";
1065
+
1066
+ export default function RootLayout({
1067
+ children,
1068
+ }: {
1069
+ children: React.ReactNode;
1070
+ }) {
1071
+ return (
1072
+ <html lang="en">
1073
+ <body>
1074
+ <Providers>{children}</Providers>
1075
+ </body>
1076
+ </html>
1077
+ );
1078
+ }
1079
+ `;var se=async e=>{let r=e;r||(r=(await Ce.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:l,useShadcn:x,installDeps:m,setupPrisma:u,setupBetterAuth:f,setupApi:T,setupLogin:h,setupGit:oe}=await Ce.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(`
1080
+ \u{1F680} Initializing ${a.bold(p)} in ${k?"current directory":r}...
1081
+ `));let S=Be("\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(`
1082
+ \u{1F5BC}\uFE0F Scaffolding Next.js frontend...`)),d(`npx create-next-app@latest frontend --ts --tailwind --eslint --app --src-dir --import-alias "@/*" --use-${l}`,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(`
1083
+ \u{1F3A8} Setting up shadcn/ui...`));try{d("npx shadcn@latest init -d",o.join(t,"frontend")),console.log(a.cyan("\u{1F4E6} Adding common shadcn/ui components...")),d(`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(`
1084
+ \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),f&&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"),fe),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"),de),await s.outputFile(o.join(t,"backend","src","app","utils","sendResponse.ts"),ge),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"),ue),u&&(await s.outputFile(o.join(t,"backend","prisma","schema","schema.prisma"),U),f&&await s.outputFile(o.join(t,"backend","prisma","schema","auth.prisma"),N),await s.outputFile(o.join(t,"backend","prisma.config.ts"),O)),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"),_),await s.outputFile(o.join(t,"backend","src","app","utils","cookie.ts"),q),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"),Q),await s.outputFile(o.join(t,"frontend","src","lib","tokenUtils.ts"),K),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,"frontend","src","components","Providers.tsx"),be),await s.outputFile(o.join(t,"frontend","src","app","layout.tsx"),we)),await s.outputFile(o.join(t,"backend","tsconfig.json"),he),await s.outputFile(o.join(t,".gitignore"),z),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"),$);let A=je.randomBytes(32).toString("hex"),i=je.randomBytes(32).toString("hex"),w=`DATABASE_URL="postgresql://postgres:postgres@localhost:5432/${p}"
1085
+ JWT_SECRET="${A}"
1086
+ BETTER_AUTH_SECRET="${i}"
420
1087
  NODE_ENV="development"
421
1088
  PORT=8000
422
1089
  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';
1090
+ CLIENT_URL="http://localhost:3000"`,Le="NEXT_PUBLIC_API_URL=http://localhost:8000/api/v1";await s.outputFile(o.join(t,".env"),w),await s.outputFile(o.join(t,"frontend",".env.local"),Le),await s.outputFile(o.join(t,"package.json"),B(p)),l==="pnpm"&&await s.outputFile(o.join(t,"pnpm-workspace.yaml"),`packages:
1091
+ - 'backend'
1092
+ - 'frontend'
1093
+ `);let $e={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"}:{},...f?{"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"),$e,{spaces:2}),S.succeed(a.green("\u2705 Project structure created! \u2728")),m&&(console.log(a.yellow(`
1094
+ \u{1F4E6} Finalizing dependencies with ${l}...
1095
+ `)),d(`${l} install`,t),d(`${l} install`,o.join(t,"backend")),T||h)){console.log(a.yellow(`
1096
+ \u{1F4E6} Adding frontend auth dependencies...
1097
+ `));let y=["axios"];h&&y.push("react-hook-form","zod","@hookform/resolvers","@tanstack/react-query");let ze=l==="pnpm"?`${l} add ${y.join(" ")} --filter ./frontend`:`${l} add ${y.join(" ")}`;d(ze,t)}if(oe){console.log(a.cyan(`
1098
+ Git Initializing...`));try{let y=o.join(t,"frontend",".git");s.existsSync(y)&&await s.remove(y),d("git init",t),d("git add .",t),d('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(`
1099
+ \u{1F525} Your project is ready! Follow these steps to start:
1100
+ `)),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(`
1101
+ Happy coding! \u{1F680}
1102
+ `))}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 Ve from"ora";var Re=(e,r)=>`import { Request, Response } from 'express';
428
1103
  import httpStatus from 'http-status';
429
1104
  import catchAsync from '../../utils/catchAsync.js';
430
1105
  import sendResponse from '../../utils/sendResponse.js';
431
- import { ${e}Service } from './${t}.service.js';
1106
+ import { ${e}Service } from './${r}.service.js';
432
1107
 
433
1108
  const create${e} = catchAsync(async (req: Request, res: Response) => {
434
1109
  const result = await ${e}Service.create${e}IntoDB(req.body);
@@ -443,7 +1118,7 @@ const create${e} = catchAsync(async (req: Request, res: Response) => {
443
1118
  export const ${e}Controller = {
444
1119
  create${e},
445
1120
  };
446
- `,Q=(e,t)=>`import { ${e} } from '@prisma/client';
1121
+ `,Se=(e,r)=>`import { ${e} } from '@prisma/client';
447
1122
  import prisma from '../../lib/prisma.js';
448
1123
 
449
1124
  const create${e}IntoDB = async (payload: any) => {
@@ -454,18 +1129,18 @@ const create${e}IntoDB = async (payload: any) => {
454
1129
  export const ${e}Service = {
455
1130
  create${e}IntoDB,
456
1131
  };
457
- `,Z=(e,t)=>`import { Router } from 'express';
458
- import { ${e}Controller } from './${t}.controller.js';
1132
+ `,Ae=(e,r)=>`import { Router } from 'express';
1133
+ import { ${e}Controller } from './${r}.controller.js';
459
1134
 
460
1135
  const router = Router();
461
1136
 
462
- router.post('/create-${t}', ${e}Controller.create${e});
1137
+ router.post('/create-${r}', ${e}Controller.create${e});
463
1138
 
464
1139
  export const ${e}Routes = router;
465
- `,ee=e=>`export type I${e} = {
1140
+ `,Ee=e=>`export type I${e} = {
466
1141
  // Define interface
467
1142
  };
468
- `,te=e=>`import { z } from 'zod';
1143
+ `,Fe=e=>`import { z } from 'zod';
469
1144
 
470
1145
  const create${e}ValidationSchema = z.object({
471
1146
  body: z.object({
@@ -476,17 +1151,17 @@ const create${e}ValidationSchema = z.object({
476
1151
  export const ${e}Validations = {
477
1152
  create${e}ValidationSchema,
478
1153
  };
479
- `,oe=e=>`export const ${e}SearchableFields = [];
480
- `,re=e=>`model ${e} {
1154
+ `,Pe=e=>`export const ${e}SearchableFields = [];
1155
+ `,Ie=e=>`model ${e} {
481
1156
  id String @id @default(uuid())
482
1157
  name String
483
1158
  createdAt DateTime @default(now())
484
1159
  updatedAt DateTime @updatedAt
485
1160
  }
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();
1161
+ `;var Ue=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),l=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=Ve(`\u{1F6E0}\uFE0F Generating module: ${v.cyan(r)}...`).start();try{await R.ensureDir(m);let f={"controller.ts":Re(r,l),"service.ts":Se(r,l),"route.ts":Ae(r,l),"interface.ts":Ee(r),"validation.ts":Fe(r),"constant.ts":Pe(r)};await R.outputFile(te.join(x,"prisma","schema",`${l}.prisma`),Ie(r));for(let[T,h]of Object.entries(f))await R.outputFile(te.join(m,`${l}.${T}`),h);u.succeed(v.green(`\u2705 Module ${r} generated successfully! \u2728`)),console.log(v.gray(`Created at: ${m}`))}catch(f){u.fail(v.red("\u274C Failed to generate module.")),console.error(f)}};import We from"fs-extra";import Ge from"chalk";var Ne=async()=>{let e=E(),r=We.existsSync("backend")?"backend":".";console.log(Ge.cyan(`\u{1F3D7}\uFE0F Building backend with ${e}...`)),d(`${e} run build`,r)};import Je from"fs-extra";import re from"chalk";var Oe=async e=>{let r=Je.existsSync("backend")?"backend":".";e==="generate"?(console.log(re.cyan("\u{1F504} Generating Prisma client...")),d("npx prisma generate",r)):e==="migrate"?(console.log(re.cyan("\u{1F680} Running Prisma migrations...")),d("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 g from"chalk";import Qe from"ora";var _e=async()=>{let e=process.cwd(),r=n.join(e,"backend"),l=n.basename(e);c.existsSync(r)||(console.log(g.red("\u274C Error: Not in a shakil-stack project root (backend folder not found).")),process.exit(1));let x=Qe("\u{1F504} Updating project structure...").start();try{let m=n.join(r,".env"),u=n.join(e,".env"),f="";c.existsSync(m)?(f=await c.readFile(m,"utf-8"),c.existsSync(u)?(console.log(g.yellow(`
1162
+ \u26A0\uFE0F Both backend/.env and root .env exist. Merging...`)),await c.remove(m)):(await c.move(m,u),console.log(g.yellow(`
1163
+ \u{1F4DD} Moved .env from backend/ to root.`)))):c.existsSync(u)&&(f=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=f;for(let i of T){let w=i.split("=")[0];h.includes(w)||(h+=`
1164
+ ${i}`)}h!==f&&(await c.outputFile(u,h.trim()+`
1165
+ `),console.log(g.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:O}];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:_},{path:n.join(r,"src","app","utils","cookie.ts"),content:q},{path:n.join(r,"src","app","utils","token.ts"),content:D},{path:n.join(r,"prisma","schema","auth.prisma"),content:N}];for(let i of t)c.existsSync(i.path)||(await c.outputFile(i.path,i.content),console.log(g.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:Q},{path:n.join(p,"src","lib","tokenUtils.ts"),content:K},{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(g.cyan(`\u{1F195} Added ${n.basename(w.path)} to frontend.`)))}let S=n.basename(e),b=[{path:n.join(e,".gitignore"),content:z},{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:$},{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(g.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+`
1166
+ `))}x.succeed(g.green("\u2705 Project updated successfully! \u2728")),console.log(g.cyan(`
1167
+ Next Steps:`)),console.log(g.white("1. Check your root .env file and update credentials if needed.")),console.log(g.white("2. Run 'shakil-stack prisma generate' to update the Prisma client."))}catch(m){x.fail(g.red("\u274C Failed to update project.")),console.error(m),process.exit(1)}};var Xe=Ye(import.meta.url),qe=ne.dirname(Xe),Ze=ne.resolve(qe,De.existsSync(ne.resolve(qe,"../../package.json"))?"../../package.json":"../package.json"),et=De.readJsonSync(Ze),j=new Ke;j.name("shakil-stack").description("Full-stack EchoNet-style project generator CLI").version(et.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(()=>{_e()});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"?Ue(r):console.log(`\u274C Error: Unknown generation type: ${e}`)});j.command("build").description("Build the backend for production").action(()=>{Ne()});j.command("prisma").description("Prisma utilities").argument("<subcommand>","generate | migrate").action(e=>{Oe(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.9",
4
4
  "description": "Full-stack EchoNet-style project generator CLI",
5
5
  "keywords": [
6
6
  "shakil-stack",