create-charcole 2.0.4 → 2.2.0

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 (100) hide show
  1. package/CHANGELOG.md +290 -14
  2. package/README.md +258 -312
  3. package/bin/index.js +392 -55
  4. package/bin/lib/pkgManager.js +8 -25
  5. package/bin/lib/templateHandler.js +5 -42
  6. package/create-charcole-2.1.0.tgz +0 -0
  7. package/package.json +2 -2
  8. package/packages/swagger/BACKWARD_COMPATIBILITY.md +145 -0
  9. package/packages/swagger/CHANGELOG.md +404 -0
  10. package/packages/swagger/README.md +578 -0
  11. package/packages/swagger/charcole-swagger-1.0.0.tgz +0 -0
  12. package/packages/swagger/package-lock.json +1715 -0
  13. package/packages/swagger/package.json +44 -0
  14. package/packages/swagger/src/helpers.js +427 -0
  15. package/packages/swagger/src/index.d.ts +126 -0
  16. package/packages/swagger/src/index.js +12 -0
  17. package/packages/swagger/src/setup.js +100 -0
  18. package/template/js/.env.example +8 -0
  19. package/template/js/README.md +128 -5
  20. package/template/js/basePackage.json +11 -13
  21. package/template/js/src/app.js +8 -2
  22. package/template/js/src/config/swagger.config.js +15 -0
  23. package/template/js/src/lib/swagger/SWAGGER_GUIDE.md +561 -0
  24. package/template/js/src/modules/auth/auth.constants.js +3 -0
  25. package/template/js/src/modules/auth/auth.controller.js +29 -0
  26. package/template/js/src/modules/auth/auth.middlewares.js +19 -0
  27. package/template/js/src/modules/auth/auth.routes.js +131 -0
  28. package/template/js/src/modules/auth/auth.schemas.js +60 -0
  29. package/template/js/src/modules/auth/auth.service.js +67 -0
  30. package/template/js/src/modules/auth/package.json +6 -0
  31. package/template/js/src/modules/health/controller.js +104 -3
  32. package/template/js/src/modules/swagger/charcole-swagger-1.0.0.tgz +0 -0
  33. package/template/js/src/modules/swagger/package.json +5 -0
  34. package/template/js/src/repositories/user.repo.js +19 -0
  35. package/template/js/src/routes/index.js +25 -0
  36. package/template/js/src/routes/protected.js +57 -0
  37. package/template/ts/.env.example +8 -0
  38. package/template/ts/README.md +128 -5
  39. package/template/ts/basePackage.json +19 -15
  40. package/template/ts/build.js +46 -0
  41. package/template/ts/src/app.ts +12 -7
  42. package/template/ts/src/config/swagger.config.ts +30 -0
  43. package/template/ts/src/lib/swagger/SWAGGER_GUIDE.md +561 -0
  44. package/template/ts/src/middlewares/errorHandler.ts +15 -23
  45. package/template/ts/src/middlewares/requestLogger.ts +1 -1
  46. package/template/ts/src/middlewares/validateRequest.ts +1 -1
  47. package/template/ts/src/modules/auth/auth.constants.ts +6 -0
  48. package/template/ts/src/modules/auth/auth.controller.ts +32 -0
  49. package/template/ts/src/modules/auth/auth.middlewares.ts +46 -0
  50. package/template/ts/src/modules/auth/auth.routes.ts +52 -0
  51. package/template/ts/src/modules/auth/auth.schemas.ts +73 -0
  52. package/template/ts/src/modules/auth/auth.service.ts +106 -0
  53. package/template/ts/src/modules/auth/package.json +10 -0
  54. package/template/ts/src/modules/health/controller.ts +61 -45
  55. package/template/ts/src/modules/swagger/charcole-swagger-1.0.0.tgz +0 -0
  56. package/template/ts/src/modules/swagger/package.json +5 -0
  57. package/template/ts/src/repositories/user.repo.ts +33 -0
  58. package/template/ts/src/routes/index.ts +24 -0
  59. package/template/ts/src/routes/protected.ts +46 -0
  60. package/template/ts/src/server.ts +3 -4
  61. package/template/ts/src/utils/logger.ts +1 -1
  62. package/template/ts/tsconfig.json +14 -7
  63. package/tmpclaude-1049-cwd +1 -0
  64. package/tmpclaude-3e37-cwd +1 -0
  65. package/tmpclaude-4d73-cwd +1 -0
  66. package/tmpclaude-8a8e-cwd +1 -0
  67. package/template/js/ARCHITECTURE_DIAGRAMS.md +0 -283
  68. package/template/js/CHECKLIST.md +0 -279
  69. package/template/js/COMPLETE.md +0 -405
  70. package/template/js/ERROR_HANDLING.md +0 -393
  71. package/template/js/IMPLEMENTATION.md +0 -368
  72. package/template/js/IMPLEMENTATION_COMPLETE.md +0 -363
  73. package/template/js/INDEX.md +0 -290
  74. package/template/js/QUICK_REFERENCE.md +0 -270
  75. package/template/js/package.json +0 -28
  76. package/template/js/src/routes.js +0 -17
  77. package/template/js/test-api.js +0 -100
  78. package/template/ts/ARCHITECTURE_DIAGRAMS.md +0 -283
  79. package/template/ts/CHECKLIST.md +0 -279
  80. package/template/ts/COMPLETE.md +0 -405
  81. package/template/ts/ERROR_HANDLING.md +0 -393
  82. package/template/ts/IMPLEMENTATION.md +0 -368
  83. package/template/ts/IMPLEMENTATION_COMPLETE.md +0 -363
  84. package/template/ts/INDEX.md +0 -290
  85. package/template/ts/QUICK_REFERENCE.md +0 -270
  86. package/template/ts/package.json +0 -32
  87. package/template/ts/src/app.js +0 -75
  88. package/template/ts/src/config/constants.js +0 -20
  89. package/template/ts/src/config/env.js +0 -26
  90. package/template/ts/src/middlewares/errorHandler.js +0 -180
  91. package/template/ts/src/middlewares/requestLogger.js +0 -33
  92. package/template/ts/src/middlewares/validateRequest.js +0 -42
  93. package/template/ts/src/modules/health/controller.js +0 -50
  94. package/template/ts/src/routes.js +0 -17
  95. package/template/ts/src/routes.ts +0 -16
  96. package/template/ts/src/server.js +0 -38
  97. package/template/ts/src/utils/AppError.js +0 -182
  98. package/template/ts/src/utils/logger.js +0 -73
  99. package/template/ts/src/utils/response.js +0 -51
  100. package/template/ts/test-api.js +0 -100
@@ -0,0 +1,52 @@
1
+ import { Router } from "express";
2
+ import { AuthController } from "./auth.controller.ts";
3
+
4
+ const router = Router();
5
+
6
+ /**
7
+ * @swagger
8
+ * /api/auth/register:
9
+ * post:
10
+ * summary: Register a new user
11
+ * description: Create a new user account with email and password
12
+ * tags:
13
+ * - Authentication
14
+ * requestBody:
15
+ * required: true
16
+ * content:
17
+ * application/json:
18
+ * schema:
19
+ * $ref: '#/components/schemas/registerSchema'
20
+ * responses:
21
+ * 201:
22
+ * $ref: '#/components/responses/Success'
23
+ * 400:
24
+ * $ref: '#/components/responses/ValidationError'
25
+ */
26
+ router.post("/register", AuthController.register);
27
+
28
+ /**
29
+ * @swagger
30
+ * /api/auth/login:
31
+ * post:
32
+ * summary: Login user
33
+ * description: Authenticate user with email and password
34
+ * tags:
35
+ * - Authentication
36
+ * requestBody:
37
+ * required: true
38
+ * content:
39
+ * application/json:
40
+ * schema:
41
+ * $ref: '#/components/schemas/loginSchema'
42
+ * responses:
43
+ * 200:
44
+ * $ref: '#/components/responses/Success'
45
+ * 401:
46
+ * $ref: '#/components/responses/Unauthorized'
47
+ * 400:
48
+ * $ref: '#/components/responses/ValidationError'
49
+ */
50
+ router.post("/login", AuthController.login);
51
+
52
+ export default router;
@@ -0,0 +1,73 @@
1
+ import { z } from "zod";
2
+ import { USER_ROLES, AUTH_PROVIDERS } from "./auth.constants.ts";
3
+
4
+ export const emailSchema = z
5
+ .string()
6
+ .email("Invalid email address")
7
+ .toLowerCase();
8
+
9
+ export const passwordSchema = z
10
+ .string()
11
+ .min(8, "Password must be at least 8 characters")
12
+ .max(72, "Password too long");
13
+
14
+ export const userSchema = z.object({
15
+ id: z.string().uuid(),
16
+ email: emailSchema,
17
+ name: z.string().min(1).max(100),
18
+ role: z.enum(USER_ROLES).default("user"),
19
+ provider: z.enum(AUTH_PROVIDERS).default("credentials"),
20
+
21
+ passwordHash: z.string().optional(), // credentials only
22
+ isEmailVerified: z.boolean().default(false),
23
+
24
+ createdAt: z.date(),
25
+ updatedAt: z.date(),
26
+ });
27
+
28
+ export const registerSchema = z.object({
29
+ name: z.string().min(1, "Name is required"),
30
+ email: emailSchema,
31
+ password: passwordSchema,
32
+ });
33
+
34
+ export const loginSchema = z.object({
35
+ email: emailSchema,
36
+ password: z.string().min(1, "Password is required"),
37
+ });
38
+
39
+ export const jwtPayloadSchema = z.object({
40
+ sub: z.string().uuid(), // user id
41
+ email: emailSchema,
42
+ role: z.enum(USER_ROLES),
43
+ });
44
+
45
+ export const forgotPasswordSchema = z.object({
46
+ email: emailSchema,
47
+ });
48
+
49
+ export const resetPasswordSchema = z.object({
50
+ token: z.string().min(1),
51
+ newPassword: passwordSchema,
52
+ });
53
+
54
+ export const publicUserSchema = userSchema.omit({
55
+ passwordHash: true,
56
+ });
57
+
58
+ // Generic validate function with proper typing
59
+ export const validate = <T extends z.ZodTypeAny>(
60
+ schema: T,
61
+ data: unknown,
62
+ ): z.infer<T> => {
63
+ return schema.parse(data);
64
+ };
65
+
66
+ // Export inferred types for use in other files
67
+ export type User = z.infer<typeof userSchema>;
68
+ export type RegisterInput = z.infer<typeof registerSchema>;
69
+ export type LoginInput = z.infer<typeof loginSchema>;
70
+ export type JwtPayload = z.infer<typeof jwtPayloadSchema>;
71
+ export type ForgotPasswordInput = z.infer<typeof forgotPasswordSchema>;
72
+ export type ResetPasswordInput = z.infer<typeof resetPasswordSchema>;
73
+ export type PublicUser = z.infer<typeof publicUserSchema>;
@@ -0,0 +1,106 @@
1
+ import bcrypt from "bcryptjs";
2
+ import jwt from "jsonwebtoken";
3
+ import { registerSchema, loginSchema } from "./auth.schemas.ts";
4
+ import type { z } from "zod";
5
+
6
+ const SALT_ROUNDS = 10;
7
+ const JWT_EXPIRES_IN = "7d";
8
+
9
+ // Type definitions
10
+ type RegisterInput = z.infer<typeof registerSchema>;
11
+ type LoginInput = z.infer<typeof loginSchema>;
12
+
13
+ type User = {
14
+ id: string;
15
+ email: string;
16
+ name: string;
17
+ passwordHash: string;
18
+ role: string;
19
+ };
20
+
21
+ type UserRepo = {
22
+ findByEmail(email: string): Promise<User | null>;
23
+ create(data: {
24
+ email: string;
25
+ name: string;
26
+ passwordHash: string;
27
+ }): Promise<User>;
28
+ };
29
+
30
+ type JwtPayload = {
31
+ sub: string;
32
+ email: string;
33
+ role: string;
34
+ };
35
+
36
+ type LoginResult = {
37
+ user: User;
38
+ token: string;
39
+ };
40
+
41
+ export const AuthService = {
42
+ async hashPassword(password: string): Promise<string> {
43
+ return bcrypt.hash(password, SALT_ROUNDS);
44
+ },
45
+
46
+ async comparePassword(password: string, hash: string): Promise<boolean> {
47
+ return bcrypt.compare(password, hash);
48
+ },
49
+
50
+ signToken(payload: JwtPayload): string {
51
+ const secret = process.env.JWT_SECRET;
52
+
53
+ if (!secret) {
54
+ throw new Error("JWT_SECRET environment variable is not defined");
55
+ }
56
+
57
+ return jwt.sign(payload, secret, {
58
+ expiresIn: JWT_EXPIRES_IN,
59
+ });
60
+ },
61
+
62
+ async register(data: unknown, userRepo: UserRepo): Promise<User> {
63
+ const input: RegisterInput = registerSchema.parse(data);
64
+
65
+ const existingUser = await userRepo.findByEmail(input.email);
66
+ if (existingUser) {
67
+ throw new Error("Email already in use");
68
+ }
69
+
70
+ const passwordHash = await this.hashPassword(input.password);
71
+
72
+ const user = await userRepo.create({
73
+ email: input.email,
74
+ name: input.name,
75
+ passwordHash,
76
+ });
77
+
78
+ return user;
79
+ },
80
+
81
+ async login(data: unknown, userRepo: UserRepo): Promise<LoginResult> {
82
+ const input: LoginInput = loginSchema.parse(data);
83
+
84
+ const user = await userRepo.findByEmail(input.email);
85
+ if (!user || !user.passwordHash) {
86
+ throw new Error("Invalid credentials");
87
+ }
88
+
89
+ const isValid = await this.comparePassword(
90
+ input.password,
91
+ user.passwordHash,
92
+ );
93
+
94
+ if (!isValid) {
95
+ throw new Error("Invalid credentials");
96
+ }
97
+
98
+ const token = this.signToken({
99
+ sub: user.id,
100
+ email: user.email,
101
+ role: user.role,
102
+ });
103
+
104
+ return { user, token };
105
+ },
106
+ };
@@ -0,0 +1,10 @@
1
+ {
2
+ "dependencies": {
3
+ "bcryptjs": "^3.0.3",
4
+ "jsonwebtoken": "^9.0.3"
5
+ },
6
+ "devDependencies": {
7
+ "@types/bcryptjs": "^2.4.6",
8
+ "@types/jsonwebtoken": "^9.0.10"
9
+ }
10
+ }
@@ -1,64 +1,80 @@
1
1
  import { Request, Response } from "express";
2
2
  import { z } from "zod";
3
- import { sendSuccess } from "../../utils/response.js";
4
- import { asyncHandler } from "../../middlewares/errorHandler.js";
5
- import { validateRequest } from "../../middlewares/validateRequest.js";
6
-
7
- const healthCheckSchema = z.object({
8
- query: z.object({}),
9
- params: z.object({}),
10
- body: z.object({}),
11
- });
12
-
13
- const createItemSchema = z.object({
14
- body: z.object({
15
- name: z.string().min(1, "Name is required").max(100),
16
- description: z.string().optional(),
17
- }),
18
- query: z.object({}),
19
- params: z.object({}),
20
- });
21
-
22
- type CreateItemBody = z.infer<typeof createItemSchema>["body"];
3
+ import { sendSuccess } from "../../utils/response.ts";
4
+ import { asyncHandler } from "../../middlewares/errorHandler.ts";
23
5
 
24
6
  /**
25
- * Health check endpoint
26
- * Always returns healthy status (ping endpoint)
7
+ * @swagger
8
+ * /api/health:
9
+ * get:
10
+ * summary: Health check endpoint
11
+ * description: Returns the health status of the API
12
+ * tags:
13
+ * - Health
14
+ * responses:
15
+ * 200:
16
+ * $ref: '#/components/responses/Success'
27
17
  */
28
- export const getHealth = [
29
- validateRequest(healthCheckSchema),
30
- asyncHandler(async (req: Request, res: Response) => {
31
- const response = {
18
+ export const getHealth = asyncHandler(async (req: Request, res: Response) => {
19
+ sendSuccess(
20
+ res,
21
+ {
32
22
  status: "healthy" as const,
33
23
  uptime: process.uptime(),
34
24
  timestamp: new Date().toISOString(),
35
- };
36
-
37
- sendSuccess(res, response, 200, "Service is healthy");
38
- }),
39
- ];
25
+ },
26
+ 200,
27
+ "Service is healthy",
28
+ );
29
+ });
40
30
 
41
31
  /**
42
32
  * Example POST endpoint with validation
43
33
  * Demonstrates proper error handling with Zod validation
44
34
  */
45
- export const createItem = [
46
- validateRequest(createItemSchema),
47
- asyncHandler(async (req: Request, res: Response) => {
48
- const validatedData = req.validatedData as { body: CreateItemBody };
49
- const { name, description } = validatedData.body;
35
+ export const createItemSchema = z.object({
36
+ body: z.object({
37
+ name: z.string().min(1, "Name is required").max(100),
38
+ description: z.string().optional(),
39
+ }),
40
+ });
41
+
42
+ /**
43
+ * @swagger
44
+ * /api/items:
45
+ * post:
46
+ * summary: Create a new item
47
+ * description: Example endpoint demonstrating validation with Zod
48
+ * tags:
49
+ * - Items
50
+ * requestBody:
51
+ * required: true
52
+ * content:
53
+ * application/json:
54
+ * schema:
55
+ * $ref: '#/components/schemas/createItemSchema'
56
+ * responses:
57
+ * 201:
58
+ * $ref: '#/components/responses/Success'
59
+ * 400:
60
+ * $ref: '#/components/responses/ValidationError'
61
+ */
62
+ export const createItem = asyncHandler(async (req: Request, res: Response) => {
63
+ const parsed = createItemSchema.parse({ body: req.body });
64
+ const { name, description } = parsed.body;
50
65
 
51
- await new Promise((resolve) => setTimeout(resolve, 10));
66
+ // Simulate some async work
67
+ await new Promise((resolve) => setTimeout(resolve, 10));
52
68
 
53
- const response = {
69
+ sendSuccess(
70
+ res,
71
+ {
54
72
  id: Math.random().toString(36).substr(2, 9),
55
73
  name,
56
74
  description: description || null,
57
75
  createdAt: new Date().toISOString(),
58
- };
59
-
60
- sendSuccess(res, response, 201, "Item created successfully");
61
- }),
62
- ];
63
-
64
- export { createItemSchema };
76
+ },
77
+ 201,
78
+ "Item created successfully",
79
+ );
80
+ });
@@ -0,0 +1,5 @@
1
+ {
2
+ "dependencies": {
3
+ "@charcoles/swagger": "file:./charcole-swagger-1.0.0.tgz"
4
+ }
5
+ }
@@ -0,0 +1,33 @@
1
+ import { randomUUID } from "crypto";
2
+ import type { User } from "../modules/auth/auth.schemas.ts";
3
+
4
+ const users: User[] = [];
5
+
6
+ type CreateUserData = {
7
+ email: string;
8
+ name: string;
9
+ passwordHash: string;
10
+ };
11
+
12
+ export const userRepo = {
13
+ async findByEmail(email: string): Promise<User | undefined> {
14
+ return users.find((u) => u.email === email);
15
+ },
16
+
17
+ async create(data: CreateUserData): Promise<User> {
18
+ const user: User = {
19
+ id: randomUUID(),
20
+ email: data.email,
21
+ name: data.name,
22
+ passwordHash: data.passwordHash,
23
+ role: "user",
24
+ provider: "credentials",
25
+ isEmailVerified: false,
26
+ createdAt: new Date(),
27
+ updatedAt: new Date(),
28
+ };
29
+
30
+ users.push(user);
31
+ return user;
32
+ },
33
+ };
@@ -0,0 +1,24 @@
1
+ import { Router } from "express";
2
+ import {
3
+ getHealth,
4
+ createItem,
5
+ createItemSchema,
6
+ } from "../modules/health/controller.ts";
7
+ import { validateRequest } from "../middlewares/validateRequest.ts";
8
+ import protectedRoutes from "./protected.ts";
9
+ import authRoutes from "../modules/auth/auth.routes.ts";
10
+ const router = Router();
11
+
12
+ // Health check
13
+ router.get("/health", getHealth);
14
+
15
+ // Example: Create item with validation
16
+ router.post("/items", validateRequest(createItemSchema), createItem);
17
+
18
+ // 🔐 Auth routes
19
+ router.use("/auth", authRoutes);
20
+
21
+ // 🔐 Protected routes (REQUIRED BEARER TOKEN FOR THEM)
22
+ router.use("/protected", protectedRoutes);
23
+
24
+ export default router;
@@ -0,0 +1,46 @@
1
+ import { Router } from "express";
2
+ import { requireAuth } from "../modules/auth/auth.middlewares.ts";
3
+
4
+ const router = Router();
5
+
6
+ /**
7
+ * @swagger
8
+ * /api/protected/me:
9
+ * get:
10
+ * summary: Get current user profile
11
+ * description: Returns the authenticated user's information
12
+ * tags:
13
+ * - Protected
14
+ * security:
15
+ * - bearerAuth: []
16
+ * responses:
17
+ * 200:
18
+ * description: User profile retrieved successfully
19
+ * content:
20
+ * application/json:
21
+ * schema:
22
+ * type: object
23
+ * properties:
24
+ * message:
25
+ * type: string
26
+ * example: You are authenticated
27
+ * user:
28
+ * type: object
29
+ * properties:
30
+ * id:
31
+ * type: string
32
+ * example: user_123abc
33
+ * email:
34
+ * type: string
35
+ * example: user@example.com
36
+ * 401:
37
+ * description: Unauthorized - Invalid or missing token
38
+ */
39
+ router.get("/me", requireAuth, (req, res) => {
40
+ res.json({
41
+ message: "You are authenticated",
42
+ user: req.user,
43
+ });
44
+ });
45
+
46
+ export default router;
@@ -1,8 +1,7 @@
1
1
  import "dotenv/config";
2
-
3
- import { app } from "./app.js";
4
- import { env } from "./config/env.js";
5
- import { logger } from "./utils/logger.js";
2
+ import { app } from "./app.ts";
3
+ import { env } from "./config/env.ts";
4
+ import { logger } from "./utils/logger.ts";
6
5
 
7
6
  const PORT = env.PORT;
8
7
 
@@ -1,4 +1,4 @@
1
- import { env } from "../config/env.js";
1
+ import { env } from "../config/env.ts";
2
2
 
3
3
  type LogLevel = "debug" | "info" | "warn" | "error" | "fatal";
4
4
 
@@ -2,18 +2,25 @@
2
2
  "compilerOptions": {
3
3
  "target": "ES2020",
4
4
  "module": "ESNext",
5
- "rootDir": "src",
6
- "outDir": "dist",
5
+ "moduleResolution": "bundler",
6
+ "rootDir": "./src",
7
+ "outDir": "./dist",
7
8
  "strict": true,
8
9
  "esModuleInterop": true,
9
10
  "forceConsistentCasingInFileNames": true,
10
11
  "skipLibCheck": true,
11
- "moduleResolution": "node",
12
12
  "resolveJsonModule": true,
13
13
  "allowJs": false,
14
- "noEmitOnError": true,
15
- "sourceMap": true
14
+ "noEmitOnError": false,
15
+ "sourceMap": true,
16
+ "declaration": false,
17
+ "allowImportingTsExtensions": true,
18
+ "noEmit": true
16
19
  },
17
- "include": ["src/**/*.ts"],
18
- "exclude": ["node_modules", "dist"]
20
+ "include": ["src/**/*"],
21
+ "exclude": ["node_modules", "dist"],
22
+ "ts-node": {
23
+ "esm": true,
24
+ "experimentalSpecifierResolution": "node"
25
+ }
19
26
  }
@@ -0,0 +1 @@
1
+ /e/SAFEZONE/charcole
@@ -0,0 +1 @@
1
+ /e/SAFEZONE/charcole
@@ -0,0 +1 @@
1
+ /e/SAFEZONE/charcole
@@ -0,0 +1 @@
1
+ /e/SAFEZONE/charcole