@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.
- package/dist/index.js +732 -57
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,20 +1,48 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{Command as
|
|
2
|
+
import{Command as 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
|
-
|
|
10
|
+
server = app.listen(config.port, () => {
|
|
9
11
|
console.log(\`\u{1F680} Server is running on http://localhost:\${config.port}\`);
|
|
10
12
|
});
|
|
13
|
+
|
|
14
|
+
const exitHandler = () => {
|
|
15
|
+
if (server) {
|
|
16
|
+
server.close(() => {
|
|
17
|
+
console.log('\u{1F6D1} Server closed');
|
|
18
|
+
process.exit(1);
|
|
19
|
+
});
|
|
20
|
+
} else {
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const unexpectedErrorHandler = (error: unknown) => {
|
|
26
|
+
console.error('\u274C Unexpected error:', error);
|
|
27
|
+
exitHandler();
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
process.on('uncaughtException', unexpectedErrorHandler);
|
|
31
|
+
process.on('unhandledRejection', unexpectedErrorHandler);
|
|
32
|
+
|
|
33
|
+
process.on('SIGTERM', () => {
|
|
34
|
+
console.log('SIGTERM received');
|
|
35
|
+
if (server) {
|
|
36
|
+
server.close();
|
|
37
|
+
}
|
|
38
|
+
});
|
|
11
39
|
} catch (error) {
|
|
12
40
|
console.error('Failed to start server:', error);
|
|
13
41
|
}
|
|
14
42
|
}
|
|
15
43
|
|
|
16
44
|
bootstrap();
|
|
17
|
-
`,
|
|
45
|
+
`,ie=e=>`import cors from 'cors';
|
|
18
46
|
import express, { Application, Request, Response } from 'express';
|
|
19
47
|
import httpStatus from 'http-status';
|
|
20
48
|
import globalErrorHandler from './app/middleware/globalErrorHandler.js';
|
|
@@ -66,7 +94,7 @@ app.use(globalErrorHandler);
|
|
|
66
94
|
app.use(notFound);
|
|
67
95
|
|
|
68
96
|
export default app;
|
|
69
|
-
`,
|
|
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
|
-
`,
|
|
83
|
-
import { PrismaClient } from "
|
|
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
|
-
`,
|
|
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.
|
|
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
|
-
`,
|
|
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
|
-
`,
|
|
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
|
-
`,
|
|
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
|
-
`,
|
|
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
|
-
`,
|
|
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
|
-
`,
|
|
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
|
-
`,
|
|
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
|
-
`,
|
|
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
|
-
`,
|
|
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
|
-
`,
|
|
219
|
-
id String @id @default(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
255
|
-
|
|
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
|
-
`,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
`,
|
|
538
|
+
`,z=`node_modules/
|
|
379
539
|
.env
|
|
380
540
|
dist/
|
|
381
541
|
*.log
|
|
382
542
|
.DS_Store
|
|
383
|
-
`,
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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"
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
`)
|
|
427
|
-
|
|
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 './${
|
|
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
|
-
`,
|
|
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
|
-
`,
|
|
458
|
-
import { ${e}Controller } from './${
|
|
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-${
|
|
1137
|
+
router.post('/create-${r}', ${e}Controller.create${e});
|
|
463
1138
|
|
|
464
1139
|
export const ${e}Routes = router;
|
|
465
|
-
`,
|
|
1140
|
+
`,Ee=e=>`export type I${e} = {
|
|
466
1141
|
// Define interface
|
|
467
1142
|
};
|
|
468
|
-
`,
|
|
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
|
-
`,
|
|
480
|
-
`,
|
|
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
|
|
487
|
-
\u26A0\uFE0F Both backend/.env and root .env exist. Merging...`)),await
|
|
488
|
-
\u{1F4DD} Moved .env from backend/ to root.`)))):
|
|
489
|
-
${
|
|
490
|
-
`),console.log(
|
|
491
|
-
`))}
|
|
492
|
-
Next Steps:`)),console.log(
|
|
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();
|