create-charcole 2.0.4 → 2.1.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 (75) hide show
  1. package/CHANGELOG.md +200 -14
  2. package/README.md +137 -332
  3. package/bin/index.js +236 -55
  4. package/bin/lib/pkgManager.js +8 -25
  5. package/bin/lib/templateHandler.js +5 -42
  6. package/package.json +2 -2
  7. package/plans/V2_1_PLAN.md +20 -0
  8. package/template/js/.env.example +8 -0
  9. package/template/js/basePackage.json +11 -13
  10. package/template/js/src/app.js +4 -1
  11. package/template/js/src/modules/auth/auth.constants.js +3 -0
  12. package/template/js/src/modules/auth/auth.controller.js +29 -0
  13. package/template/js/src/modules/auth/auth.middlewares.js +19 -0
  14. package/template/js/src/modules/auth/auth.routes.js +9 -0
  15. package/template/js/src/modules/auth/auth.schemas.js +60 -0
  16. package/template/js/src/modules/auth/auth.service.js +67 -0
  17. package/template/js/src/modules/auth/package.json +6 -0
  18. package/template/js/src/repositories/user.repo.js +19 -0
  19. package/template/js/src/routes/index.js +25 -0
  20. package/template/js/src/routes/protected.js +13 -0
  21. package/template/ts/.env.example +8 -0
  22. package/template/ts/basePackage.json +19 -15
  23. package/template/ts/build.js +46 -0
  24. package/template/ts/src/app.ts +5 -4
  25. package/template/ts/src/middlewares/errorHandler.ts +15 -23
  26. package/template/ts/src/middlewares/requestLogger.ts +1 -1
  27. package/template/ts/src/middlewares/validateRequest.ts +1 -1
  28. package/template/ts/src/modules/auth/auth.constants.ts +6 -0
  29. package/template/ts/src/modules/auth/auth.controller.ts +32 -0
  30. package/template/ts/src/modules/auth/auth.middlewares.ts +46 -0
  31. package/template/ts/src/modules/auth/auth.routes.ts +9 -0
  32. package/template/ts/src/modules/auth/auth.schemas.ts +73 -0
  33. package/template/ts/src/modules/auth/auth.service.ts +106 -0
  34. package/template/ts/src/modules/auth/package.json +10 -0
  35. package/template/ts/src/modules/health/controller.ts +3 -3
  36. package/template/ts/src/repositories/user.repo.ts +33 -0
  37. package/template/ts/src/routes/index.ts +24 -0
  38. package/template/ts/src/routes/protected.ts +13 -0
  39. package/template/ts/src/server.ts +0 -1
  40. package/template/ts/src/utils/logger.ts +1 -1
  41. package/template/ts/tsconfig.json +14 -7
  42. package/template/js/ARCHITECTURE_DIAGRAMS.md +0 -283
  43. package/template/js/CHECKLIST.md +0 -279
  44. package/template/js/COMPLETE.md +0 -405
  45. package/template/js/ERROR_HANDLING.md +0 -393
  46. package/template/js/IMPLEMENTATION.md +0 -368
  47. package/template/js/IMPLEMENTATION_COMPLETE.md +0 -363
  48. package/template/js/INDEX.md +0 -290
  49. package/template/js/QUICK_REFERENCE.md +0 -270
  50. package/template/js/package.json +0 -28
  51. package/template/js/src/routes.js +0 -17
  52. package/template/js/test-api.js +0 -100
  53. package/template/ts/ARCHITECTURE_DIAGRAMS.md +0 -283
  54. package/template/ts/CHECKLIST.md +0 -279
  55. package/template/ts/COMPLETE.md +0 -405
  56. package/template/ts/ERROR_HANDLING.md +0 -393
  57. package/template/ts/IMPLEMENTATION.md +0 -368
  58. package/template/ts/IMPLEMENTATION_COMPLETE.md +0 -363
  59. package/template/ts/INDEX.md +0 -290
  60. package/template/ts/QUICK_REFERENCE.md +0 -270
  61. package/template/ts/package.json +0 -32
  62. package/template/ts/src/app.js +0 -75
  63. package/template/ts/src/config/constants.js +0 -20
  64. package/template/ts/src/config/env.js +0 -26
  65. package/template/ts/src/middlewares/errorHandler.js +0 -180
  66. package/template/ts/src/middlewares/requestLogger.js +0 -33
  67. package/template/ts/src/middlewares/validateRequest.js +0 -42
  68. package/template/ts/src/modules/health/controller.js +0 -50
  69. package/template/ts/src/routes.js +0 -17
  70. package/template/ts/src/routes.ts +0 -16
  71. package/template/ts/src/server.js +0 -38
  72. package/template/ts/src/utils/AppError.js +0 -182
  73. package/template/ts/src/utils/logger.js +0 -73
  74. package/template/ts/src/utils/response.js +0 -51
  75. package/template/ts/test-api.js +0 -100
@@ -0,0 +1,60 @@
1
+ import { z } from "zod";
2
+ import { USER_ROLES, AUTH_PROVIDERS } from "./auth.constants.js";
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
+ export const validate = (schema, data) => {
59
+ return schema.parse(data);
60
+ };
@@ -0,0 +1,67 @@
1
+ import bcrypt from "bcryptjs";
2
+ import jwt from "jsonwebtoken";
3
+ import { registerSchema, loginSchema } from "./auth.schemas.js";
4
+
5
+ const SALT_ROUNDS = 10;
6
+ const JWT_EXPIRES_IN = "7d";
7
+
8
+ export const AuthService = {
9
+ async hashPassword(password) {
10
+ return bcrypt.hash(password, SALT_ROUNDS);
11
+ },
12
+
13
+ async comparePassword(password, hash) {
14
+ return bcrypt.compare(password, hash);
15
+ },
16
+
17
+ signToken(payload) {
18
+ return jwt.sign(payload, process.env.JWT_SECRET, {
19
+ expiresIn: JWT_EXPIRES_IN,
20
+ });
21
+ },
22
+
23
+ async register(data, userRepo) {
24
+ const input = registerSchema.parse(data);
25
+
26
+ const existingUser = await userRepo.findByEmail(input.email);
27
+ if (existingUser) {
28
+ throw new Error("Email already in use");
29
+ }
30
+
31
+ const passwordHash = await this.hashPassword(input.password);
32
+
33
+ const user = await userRepo.create({
34
+ email: input.email,
35
+ name: input.name,
36
+ passwordHash,
37
+ });
38
+
39
+ return user;
40
+ },
41
+
42
+ async login(data, userRepo) {
43
+ const input = loginSchema.parse(data);
44
+
45
+ const user = await userRepo.findByEmail(input.email);
46
+ if (!user || !user.passwordHash) {
47
+ throw new Error("Invalid credentials");
48
+ }
49
+
50
+ const isValid = await this.comparePassword(
51
+ input.password,
52
+ user.passwordHash,
53
+ );
54
+
55
+ if (!isValid) {
56
+ throw new Error("Invalid credentials");
57
+ }
58
+
59
+ const token = this.signToken({
60
+ sub: user.id,
61
+ email: user.email,
62
+ role: user.role,
63
+ });
64
+
65
+ return { user, token };
66
+ },
67
+ };
@@ -0,0 +1,6 @@
1
+ {
2
+ "dependencies": {
3
+ "bcryptjs": "^3.0.3",
4
+ "jsonwebtoken": "^9.0.3"
5
+ }
6
+ }
@@ -0,0 +1,19 @@
1
+ import { randomUUID } from "crypto";
2
+
3
+ const users = [];
4
+
5
+ export const userRepo = {
6
+ async findByEmail(email) {
7
+ return users.find((u) => u.email === email);
8
+ },
9
+
10
+ async create(data) {
11
+ const user = {
12
+ id: randomUUID(),
13
+ role: "user",
14
+ ...data,
15
+ };
16
+ users.push(user);
17
+ return user;
18
+ },
19
+ };
@@ -0,0 +1,25 @@
1
+ import { Router } from "express";
2
+ import {
3
+ getHealth,
4
+ createItem,
5
+ createItemSchema,
6
+ } from "../modules/health/controller.js";
7
+ import { validateRequest } from "../middlewares/validateRequest.js";
8
+ import { requireAuth } from "../modules/auth/auth.middlewares.js";
9
+ import protectedRoutes from "./protected.js";
10
+ import authRoutes from "../modules/auth/auth.routes.js";
11
+ const router = Router();
12
+
13
+ // Health check
14
+ router.get("/health", getHealth);
15
+
16
+ // Example: Create item with validation
17
+ router.post("/items", validateRequest(createItemSchema), createItem);
18
+
19
+ // 🔐 Auth routes
20
+ router.use("/auth", authRoutes);
21
+
22
+ // 🔐 Protected routes (REQUIRED BEARER TOKEN FOR THEM)
23
+ router.use("/protected", protectedRoutes);
24
+
25
+ export default router;
@@ -0,0 +1,13 @@
1
+ import { Router } from "express";
2
+ import { requireAuth } from "../modules/auth/auth.middlewares.js";
3
+
4
+ const router = Router();
5
+
6
+ router.get("/me", requireAuth, (req, res) => {
7
+ res.json({
8
+ message: "You are authenticated",
9
+ user: req.user,
10
+ });
11
+ });
12
+
13
+ export default router;
@@ -1,8 +1,16 @@
1
+ # Server Configuration
1
2
  NODE_ENV=development
2
3
  PORT=3000
3
4
 
5
+ # Logging
4
6
  LOG_LEVEL=info
5
7
 
8
+ # CORS
6
9
  CORS_ORIGIN=*
7
10
 
11
+ # Request
8
12
  REQUEST_TIMEOUT=30000
13
+
14
+
15
+ # Authentication
16
+ JWT_SECRET=your-secret-key-here
@@ -1,32 +1,36 @@
1
1
  {
2
2
  "name": "charcole",
3
- "version": "2.0.4",
3
+ "version": "2.1.0",
4
4
  "description": "Production-grade Node.js Express API",
5
- "main": "src/server.js",
5
+ "main": "dist/server.js",
6
+ "type": "module",
6
7
  "scripts": {
7
- "start": "node src/server.js",
8
- "dev": "nodemon src/server.js",
9
- "lint": "echo 'Add linting here'",
10
- "test": "echo 'Add tests here'"
8
+ "start": "node dist/server.js",
9
+ "dev": "tsx watch src/server.ts",
10
+ "build": "node build.js",
11
+ "clean": "rimraf dist",
12
+ "prebuild": "npm run clean",
13
+ "lint": "echo \"Add linting here\"",
14
+ "test": "echo \"Add tests here\""
11
15
  },
12
16
  "engines": {
13
17
  "node": ">=18.0.0"
14
18
  },
15
- "keywords": [],
16
- "author": "",
17
- "license": "ISC",
18
- "type": "module",
19
19
  "dependencies": {
20
+ "express": "^4.18.2",
20
21
  "cors": "^2.8.5",
21
22
  "dotenv": "^16.3.1",
22
- "express": "^4.18.2",
23
- "zod": "^3.22.4"
23
+ "zod": "^3.25.76"
24
24
  },
25
25
  "devDependencies": {
26
+ "@types/cors": "^2.8.19",
26
27
  "@types/express": "^4.17.21",
27
28
  "@types/node": "^25.0.10",
28
- "nodemon": "^3.0.2",
29
- "ts-node-dev": "^2.0.0",
29
+ "esbuild": "^0.24.2",
30
+ "rimraf": "^6.0.1",
31
+ "tsc-alias": "^1.8.8",
32
+ "tsx": "^4.19.2",
30
33
  "typescript": "^5.9.3"
31
- }
34
+ },
35
+ "license": "ISC"
32
36
  }
@@ -0,0 +1,46 @@
1
+ import { build } from "esbuild";
2
+ import { glob } from "glob";
3
+ import fs from "fs";
4
+ import path from "path";
5
+
6
+ const entryPoints = await glob("src/**/*.ts");
7
+
8
+ // Custom plugin to rewrite .ts imports to .js
9
+ const rewriteImportsPlugin = {
10
+ name: "rewrite-imports",
11
+ setup(build) {
12
+ build.onLoad({ filter: /\.ts$/ }, async (args) => {
13
+ const contents = await fs.promises.readFile(args.path, "utf8");
14
+
15
+ // Replace .ts extensions with .js in imports
16
+ const modifiedContents = contents
17
+ .replace(/from\s+['"]([^'"]+)\.ts['"]/g, 'from "$1.js"')
18
+ .replace(/import\s+['"]([^'"]+)\.ts['"]/g, 'import "$1.js"');
19
+
20
+ return {
21
+ contents: modifiedContents,
22
+ loader: "ts",
23
+ };
24
+ });
25
+ },
26
+ };
27
+
28
+ await build({
29
+ entryPoints,
30
+ outdir: "dist",
31
+ bundle: false,
32
+ platform: "node",
33
+ format: "esm",
34
+ target: "es2020",
35
+ sourcemap: true,
36
+ outExtension: { ".js": ".js" },
37
+ packages: "external",
38
+ plugins: [rewriteImportsPlugin],
39
+ })
40
+ .then(() => {
41
+ console.log("✅ Build completed successfully!");
42
+ })
43
+ .catch((error) => {
44
+ console.error("❌ Build failed:", error);
45
+ process.exit(1);
46
+ });
@@ -1,8 +1,8 @@
1
1
  import express, { Request, Response, NextFunction } from "express";
2
2
  import cors from "cors";
3
-
4
- import { env } from "./config/env.js";
5
- import { requestLogger } from "./middlewares/requestLogger.js";
3
+ import { userRepo } from "./repositories/user.repo.ts";
4
+ import { env } from "./config/env.ts";
5
+ import { requestLogger } from "./middlewares/requestLogger.ts";
6
6
  import {
7
7
  errorHandler,
8
8
  asyncHandler,
@@ -10,7 +10,7 @@ import {
10
10
  } from "./middlewares/errorHandler.js";
11
11
  import { sendSuccess } from "./utils/response.js";
12
12
  import { logger } from "./utils/logger.js";
13
- import routes from "./routes.js";
13
+ import routes from "./routes/index.js";
14
14
 
15
15
  export const app = express();
16
16
 
@@ -64,3 +64,4 @@ app.use((req: Request) => {
64
64
  app.use(errorHandler);
65
65
 
66
66
  logger.info("Express app configured successfully");
67
+ app.locals.userRepo = userRepo;
@@ -1,6 +1,6 @@
1
1
  import { ZodError } from "zod";
2
- import { HTTP_STATUS, ERROR_MESSAGES } from "../config/constants.js";
3
- import { logger } from "../utils/logger.js";
2
+ import { HTTP_STATUS, ERROR_MESSAGES } from "../config/constants.ts";
3
+ import { logger } from "../utils/logger.ts";
4
4
  import {
5
5
  AppError,
6
6
  ValidationError,
@@ -10,9 +10,16 @@ import {
10
10
  NotFoundError,
11
11
  ConflictError,
12
12
  BadRequestError,
13
- } from "../utils/AppError.js";
14
- import { env } from "../config/env.js";
15
- import { Request, Response, NextFunction, RequestHandler } from "express";
13
+ } from "../utils/AppError.ts";
14
+ import { env } from "../config/env.ts";
15
+ import { Request, Response, NextFunction } from "express";
16
+
17
+ // Custom type for async route handlers
18
+ type AsyncRequestHandler = (
19
+ req: Request,
20
+ res: Response,
21
+ next: NextFunction,
22
+ ) => Promise<any> | any;
16
23
 
17
24
  const normalizeError = (err: unknown): AppError => {
18
25
  if (err instanceof AppError) {
@@ -176,24 +183,9 @@ export const errorHandler = (
176
183
  * Example:
177
184
  * router.get('/users/:id', asyncHandler(getUserHandler))
178
185
  */
179
- export const asyncHandler = (fn: RequestHandler): RequestHandler => {
180
- return (req: Request, res: Response, next: NextFunction): any => {
181
- try {
182
- const result = fn(req, res, next);
183
-
184
- if (
185
- result &&
186
- typeof result === "object" &&
187
- "then" in result &&
188
- typeof result.then === "function"
189
- ) {
190
- return (result as Promise<any>).catch(next);
191
- }
192
-
193
- return result;
194
- } catch (err) {
195
- return next(err);
196
- }
186
+ export const asyncHandler = (fn: AsyncRequestHandler) => {
187
+ return (req: Request, res: Response, next: NextFunction): void => {
188
+ Promise.resolve(fn(req, res, next)).catch(next);
197
189
  };
198
190
  };
199
191
 
@@ -1,5 +1,5 @@
1
1
  import { Request, Response, NextFunction } from "express";
2
- import { logger } from "../utils/logger.js";
2
+ import { logger } from "../utils/logger.ts";
3
3
 
4
4
  /**
5
5
  * Request logging middleware
@@ -1,6 +1,6 @@
1
1
  import { Request, Response, NextFunction } from "express";
2
2
  import { z, ZodError } from "zod";
3
- import { ValidationError } from "../utils/AppError.js";
3
+ import { ValidationError } from "../utils/AppError.ts";
4
4
 
5
5
  /**
6
6
  * Request validation middleware
@@ -0,0 +1,6 @@
1
+ export const USER_ROLES = ["user", "admin"] as const;
2
+
3
+ export const AUTH_PROVIDERS = ["credentials"] as const;
4
+
5
+ export type UserRole = (typeof USER_ROLES)[number];
6
+ export type AuthProvider = (typeof AUTH_PROVIDERS)[number];
@@ -0,0 +1,32 @@
1
+ import { AuthService } from "./auth.service.ts";
2
+ import { Request, Response } from "express";
3
+
4
+ export const AuthController = {
5
+ async register(req: Request, res: Response): Promise<void> {
6
+ try {
7
+ const user = await AuthService.register(
8
+ req.body,
9
+ req.app.locals.userRepo,
10
+ );
11
+
12
+ res.status(201).json({
13
+ message: "User registered successfully",
14
+ user,
15
+ });
16
+ } catch (err) {
17
+ const message = err instanceof Error ? err.message : "An error occurred";
18
+ res.status(400).json({ message });
19
+ }
20
+ },
21
+
22
+ async login(req: Request, res: Response): Promise<void> {
23
+ try {
24
+ const result = await AuthService.login(req.body, req.app.locals.userRepo);
25
+
26
+ res.json(result);
27
+ } catch (err) {
28
+ const message = err instanceof Error ? err.message : "An error occurred";
29
+ res.status(401).json({ message });
30
+ }
31
+ },
32
+ };
@@ -0,0 +1,46 @@
1
+ import jwt from "jsonwebtoken";
2
+ import { Request, Response, NextFunction } from "express";
3
+
4
+ type JwtPayload = {
5
+ sub: string;
6
+ email: string;
7
+ role: string;
8
+ };
9
+
10
+ // Extend Express Request type to include user property
11
+ declare global {
12
+ namespace Express {
13
+ interface Request {
14
+ user?: JwtPayload;
15
+ }
16
+ }
17
+ }
18
+
19
+ export const requireAuth = (
20
+ req: Request,
21
+ res: Response,
22
+ next: NextFunction,
23
+ ): void => {
24
+ const authHeader = req.headers.authorization;
25
+
26
+ if (!authHeader?.startsWith("Bearer ")) {
27
+ res.status(401).json({ message: "Unauthorized" });
28
+ return;
29
+ }
30
+
31
+ const token = authHeader.split(" ")[1];
32
+
33
+ try {
34
+ const secret = process.env.JWT_SECRET;
35
+
36
+ if (!secret) {
37
+ throw new Error("JWT_SECRET environment variable is not defined");
38
+ }
39
+
40
+ const payload = jwt.verify(token, secret) as JwtPayload;
41
+ req.user = payload;
42
+ next();
43
+ } catch {
44
+ res.status(401).json({ message: "Invalid token" });
45
+ }
46
+ };
@@ -0,0 +1,9 @@
1
+ import { Router } from "express";
2
+ import { AuthController } from "./auth.controller.ts";
3
+
4
+ const router = Router();
5
+
6
+ router.post("/register", AuthController.register);
7
+ router.post("/login", AuthController.login);
8
+
9
+ 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
+ };