create-wabck 1.0.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.
@@ -0,0 +1,294 @@
1
+ // ─────────────────────────────────────────────────────────────────────────────
2
+ // Middleware Templates
3
+ // ─────────────────────────────────────────────────────────────────────────────
4
+
5
+ /** Generate src/middlewares/error.middleware.js */
6
+ export function errorMiddleware(options) {
7
+ const { language } = options;
8
+ const isTS = language === 'typescript';
9
+
10
+ const typeImports = isTS
11
+ ? `import { Request, Response, NextFunction } from 'express';\n`
12
+ : '';
13
+ const fnSig = isTS
14
+ ? '(err: any, _req: Request, res: Response, _next: NextFunction): void'
15
+ : '(err, _req, res, _next)';
16
+
17
+ return `${typeImports}import { ApiError } from '../utils/apiError.js';
18
+
19
+ /**
20
+ * Global Error Handling Middleware
21
+ *
22
+ * Catches all errors passed to next(err) and returns a consistent JSON
23
+ * response. Must be the LAST middleware registered in app.js.
24
+ */
25
+ export const errorMiddleware = ${fnSig} => {
26
+ let statusCode = err.statusCode || 500;
27
+ let message = err.message || 'Internal Server Error';
28
+ let errors = err.errors || [];
29
+
30
+ // ── Handle specific error types ──────────────────────────────────────────
31
+
32
+ // Mongoose CastError (invalid ObjectId)
33
+ if (err.name === 'CastError') {
34
+ statusCode = 400;
35
+ message = \`Invalid \${err.path}: \${err.value}\`;
36
+ }
37
+
38
+ // Mongoose duplicate key error
39
+ if (err.code === 11000) {
40
+ statusCode = 409;
41
+ const field = Object.keys(err.keyValue || {})[0] || 'field';
42
+ message = \`Duplicate value for \${field}\`;
43
+ }
44
+
45
+ // Mongoose validation error
46
+ if (err.name === 'ValidationError') {
47
+ statusCode = 422;
48
+ errors = Object.values(err.errors || {}).map((e${isTS ? ': any' : ''}) => e.message);
49
+ message = 'Validation failed';
50
+ }
51
+
52
+ // JWT errors
53
+ if (err.name === 'JsonWebTokenError') {
54
+ statusCode = 401;
55
+ message = 'Invalid token';
56
+ }
57
+ if (err.name === 'TokenExpiredError') {
58
+ statusCode = 401;
59
+ message = 'Token has expired';
60
+ }
61
+
62
+ // Zod validation error
63
+ if (err.name === 'ZodError') {
64
+ statusCode = 422;
65
+ errors = err.errors?.map((e${isTS ? ': any' : ''}) => \`\${e.path.join('.')}: \${e.message}\`) || [];
66
+ message = 'Validation failed';
67
+ }
68
+
69
+ res.status(statusCode).json({
70
+ success: false,
71
+ statusCode,
72
+ message,
73
+ errors,
74
+ ...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
75
+ });
76
+ };
77
+ `;
78
+ }
79
+
80
+ /** Generate src/middlewares/notFound.middleware.js */
81
+ export function notFoundMiddleware(options) {
82
+ const { language } = options;
83
+ const isTS = language === 'typescript';
84
+ const typeImports = isTS ? `import { Request, Response } from 'express';\n` : '';
85
+ const fnSig = isTS ? '(req: Request, res: Response): void' : '(req, res)';
86
+
87
+ return `${typeImports}
88
+ /**
89
+ * 404 Not Found Middleware
90
+ *
91
+ * Catches any request that doesn't match a registered route and returns
92
+ * a structured 404 response.
93
+ */
94
+ export const notFoundMiddleware = ${fnSig} => {
95
+ res.status(404).json({
96
+ success: false,
97
+ statusCode: 404,
98
+ message: \`Route \${req.method} \${req.originalUrl} not found\`,
99
+ });
100
+ };
101
+ `;
102
+ }
103
+
104
+ /** Generate src/middlewares/rateLimiter.middleware.js */
105
+ export function rateLimiterMiddleware(options) {
106
+ const { language } = options;
107
+ const isTS = language === 'typescript';
108
+
109
+ return `import rateLimit from 'express-rate-limit';
110
+
111
+ /**
112
+ * Global Rate Limiter
113
+ *
114
+ * Limits repeated requests from the same IP to prevent abuse.
115
+ * Configured to allow 100 requests per 15-minute window.
116
+ */
117
+ export const rateLimiter = rateLimit({
118
+ windowMs: 15 * 60 * 1000, // 15 minutes
119
+ max: 100, // max requests per window per IP
120
+ standardHeaders: true, // Return rate limit info in RateLimit-* headers
121
+ legacyHeaders: false, // Disable the X-RateLimit-* headers
122
+ message: {
123
+ success: false,
124
+ statusCode: 429,
125
+ message: 'Too many requests from this IP, please try again after 15 minutes.',
126
+ },
127
+ });
128
+
129
+ /**
130
+ * Stricter rate limiter for sensitive auth endpoints.
131
+ * Allows only 10 requests per 15 minutes.
132
+ */
133
+ export const authRateLimiter = rateLimit({
134
+ windowMs: 15 * 60 * 1000,
135
+ max: 10,
136
+ standardHeaders: true,
137
+ legacyHeaders: false,
138
+ message: {
139
+ success: false,
140
+ statusCode: 429,
141
+ message: 'Too many authentication attempts, please try again after 15 minutes.',
142
+ },
143
+ });
144
+ `;
145
+ }
146
+
147
+ /** Generate src/middlewares/auth.middleware.js */
148
+ export function authMiddleware(options) {
149
+ const { auth, language } = options;
150
+ const isTS = language === 'typescript';
151
+
152
+ const typeImports = isTS
153
+ ? `import { Request, Response, NextFunction } from 'express';\n`
154
+ : '';
155
+
156
+ if (auth === 'jwt') {
157
+ return `${typeImports}import jwt from 'jsonwebtoken';
158
+ import { ApiError } from '../utils/apiError.js';
159
+ import { asyncHandler } from '../utils/asyncHandler.js';
160
+
161
+ ${isTS ? `
162
+ // Extend Express Request type to include the authenticated user
163
+ declare global {
164
+ namespace Express {
165
+ interface Request {
166
+ user?: {
167
+ id: string;
168
+ email: string;
169
+ role: string;
170
+ };
171
+ }
172
+ }
173
+ }
174
+ ` : ''}
175
+ /**
176
+ * JWT Authentication Middleware
177
+ *
178
+ * Verifies the Bearer token from the Authorization header and attaches
179
+ * the decoded user payload to req.user for use in downstream handlers.
180
+ */
181
+ export const authenticate = asyncHandler(async (req${isTS ? ': Request' : ''}, _res${isTS ? ': Response' : ''}, next${isTS ? ': NextFunction' : ''}) => {
182
+ const authHeader = req.headers.authorization;
183
+
184
+ if (!authHeader?.startsWith('Bearer ')) {
185
+ throw new ApiError(401, 'No token provided. Authorization header must be: Bearer <token>');
186
+ }
187
+
188
+ const token = authHeader.split(' ')[1];
189
+
190
+ const secret = process.env.JWT_SECRET;
191
+ if (!secret) throw new ApiError(500, 'JWT_SECRET is not configured');
192
+
193
+ const decoded = jwt.verify(token, secret)${isTS ? ' as { id: string; email: string; role: string }' : ''};
194
+ req.user = {
195
+ id: decoded.id,
196
+ email: decoded.email,
197
+ role: decoded.role,
198
+ };
199
+
200
+ next();
201
+ });
202
+
203
+ /**
204
+ * Role-Based Authorization Middleware Factory
205
+ *
206
+ * @param {...string} roles - Allowed roles
207
+ * @example
208
+ * router.delete('/users/:id', authenticate, authorize('admin'), deleteUser);
209
+ */
210
+ export const authorize = (...roles${isTS ? ': string[]' : ''}) =>
211
+ asyncHandler(async (req${isTS ? ': Request' : ''}, _res${isTS ? ': Response' : ''}, next${isTS ? ': NextFunction' : ''}) => {
212
+ if (!req.user) {
213
+ throw new ApiError(401, 'Not authenticated');
214
+ }
215
+ if (!roles.includes(req.user.role)) {
216
+ throw new ApiError(403, \`Role '\${req.user.role}' is not allowed to access this resource\`);
217
+ }
218
+ next();
219
+ });
220
+ `;
221
+ }
222
+
223
+ if (auth === 'clerk') {
224
+ return `${typeImports}import { clerkMiddleware, getAuth } from '@clerk/express';
225
+ import { ApiError } from '../utils/apiError.js';
226
+ import { asyncHandler } from '../utils/asyncHandler.js';
227
+
228
+ /**
229
+ * Clerk Authentication Middleware
230
+ *
231
+ * Mount clerkMiddleware() globally in app.js, then use requireAuth
232
+ * on protected routes.
233
+ */
234
+ export { clerkMiddleware };
235
+
236
+ /**
237
+ * Require authentication via Clerk.
238
+ * Throws 401 if the request is not authenticated.
239
+ */
240
+ export const requireAuth = asyncHandler(async (req${isTS ? ': Request' : ''}, _res${isTS ? ': Response' : ''}, next${isTS ? ': NextFunction' : ''}) => {
241
+ const { userId } = getAuth(req);
242
+ if (!userId) {
243
+ throw new ApiError(401, 'Authentication required. Please sign in.');
244
+ }
245
+ next();
246
+ });
247
+ `;
248
+ }
249
+
250
+ if (auth === 'firebase') {
251
+ return `${typeImports}import admin from 'firebase-admin';
252
+ import { ApiError } from '../utils/apiError.js';
253
+ import { asyncHandler } from '../utils/asyncHandler.js';
254
+
255
+ // Initialize Firebase Admin SDK once
256
+ if (!admin.apps.length) {
257
+ admin.initializeApp({
258
+ credential: admin.credential.cert({
259
+ projectId: process.env.FIREBASE_PROJECT_ID,
260
+ privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\\\n/g, '\\n'),
261
+ clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
262
+ }),
263
+ });
264
+ }
265
+
266
+ /**
267
+ * Firebase Authentication Middleware
268
+ *
269
+ * Verifies the Firebase ID token from the Authorization header and
270
+ * attaches the decoded token to req.user.
271
+ */
272
+ export const authenticate = asyncHandler(async (req${isTS ? ': Request' : ''}, _res${isTS ? ': Response' : ''}, next${isTS ? ': NextFunction' : ''}) => {
273
+ const authHeader = req.headers.authorization;
274
+
275
+ if (!authHeader?.startsWith('Bearer ')) {
276
+ throw new ApiError(401, 'No Firebase token provided');
277
+ }
278
+
279
+ const idToken = authHeader.split(' ')[1];
280
+
281
+ const decoded = await admin.auth().verifyIdToken(idToken);
282
+ (req${isTS ? ' as any' : ''}).user = {
283
+ id: decoded.uid,
284
+ email: decoded.email || '',
285
+ role: (decoded.role${isTS ? ' as string' : ''}) || 'user',
286
+ };
287
+
288
+ next();
289
+ });
290
+ `;
291
+ }
292
+
293
+ return `// No authentication selected.\n`;
294
+ }
@@ -0,0 +1,318 @@
1
+ // ─────────────────────────────────────────────────────────────────────────────
2
+ // Model Templates — user.model.js for each database
3
+ // ─────────────────────────────────────────────────────────────────────────────
4
+
5
+ /** Generate src/models/user.model.js */
6
+ export function userModel(options) {
7
+ const { database, language } = options;
8
+ const isTS = language === 'typescript';
9
+
10
+ switch (database) {
11
+ // ── MongoDB / Mongoose ──────────────────────────────────────────────────
12
+ case 'mongodb':
13
+ return `import mongoose from 'mongoose';
14
+ import bcrypt from 'bcryptjs';
15
+
16
+ ${isTS ? `
17
+ export interface IUser extends mongoose.Document {
18
+ name: string;
19
+ email: string;
20
+ password: string;
21
+ role: 'user' | 'admin';
22
+ isActive: boolean;
23
+ createdAt: Date;
24
+ updatedAt: Date;
25
+ comparePassword(candidatePassword: string): Promise<boolean>;
26
+ }
27
+ ` : ''}
28
+ const userSchema = new mongoose.Schema${isTS ? '<IUser>' : ''}(
29
+ {
30
+ name: {
31
+ type: String,
32
+ required: [true, 'Name is required'],
33
+ trim: true,
34
+ minlength: [2, 'Name must be at least 2 characters'],
35
+ maxlength: [50, 'Name cannot exceed 50 characters'],
36
+ },
37
+ email: {
38
+ type: String,
39
+ required: [true, 'Email is required'],
40
+ unique: true,
41
+ lowercase: true,
42
+ trim: true,
43
+ match: [/^\\S+@\\S+\\.\\S+$/, 'Please provide a valid email'],
44
+ },
45
+ password: {
46
+ type: String,
47
+ required: [true, 'Password is required'],
48
+ minlength: [8, 'Password must be at least 8 characters'],
49
+ select: false, // Never return password in queries by default
50
+ },
51
+ role: {
52
+ type: String,
53
+ enum: ['user', 'admin'],
54
+ default: 'user',
55
+ },
56
+ isActive: {
57
+ type: Boolean,
58
+ default: true,
59
+ },
60
+ },
61
+ {
62
+ timestamps: true, // Adds createdAt and updatedAt
63
+ toJSON: {
64
+ transform(_doc, ret) {
65
+ delete ret.password;
66
+ delete ret.__v;
67
+ return ret;
68
+ },
69
+ },
70
+ }
71
+ );
72
+
73
+ // ── Pre-save hook — hash password before saving ────────────────────────────
74
+ userSchema.pre('save', async function (next) {
75
+ // Only hash if the password field was modified
76
+ if (!this.isModified('password')) return next();
77
+ const salt = await bcrypt.genSalt(12);
78
+ this.password = await bcrypt.hash(this.password, salt);
79
+ next();
80
+ });
81
+
82
+ // ── Instance method — compare plain text password with hash ───────────────
83
+ userSchema.methods.comparePassword = async function (
84
+ candidatePassword${isTS ? ': string' : ''}
85
+ )${isTS ? ': Promise<boolean>' : ''} {
86
+ return bcrypt.compare(candidatePassword, this.password);
87
+ };
88
+
89
+ export const User = mongoose.model${isTS ? '<IUser>' : ''}('User', userSchema);
90
+ `;
91
+
92
+ // ── PostgreSQL / Prisma ─────────────────────────────────────────────────
93
+ case 'postgresql':
94
+ // Prisma uses a schema.prisma file — this is the User service stub
95
+ return `/**
96
+ * User Model — PostgreSQL (Prisma)
97
+ *
98
+ * The actual schema is defined in prisma/schema.prisma.
99
+ * This file re-exports the Prisma client and provides a typed User type.
100
+ */
101
+ export { prisma } from '../config/db.js';
102
+
103
+ ${isTS ? `
104
+ import type { User } from '@prisma/client';
105
+ export type { User };
106
+ export type CreateUserInput = Pick<User, 'name' | 'email'> & { password: string };
107
+ export type UpdateUserInput = Partial<Pick<User, 'name' | 'email' | 'role' | 'isActive'>>;
108
+ ` : ''}
109
+ `;
110
+
111
+ // ── MySQL / mysql2 ──────────────────────────────────────────────────────
112
+ case 'mysql':
113
+ return `import { pool } from '../config/db.js';
114
+ import bcrypt from 'bcryptjs';
115
+
116
+ ${isTS ? `
117
+ export interface User {
118
+ id: number;
119
+ name: string;
120
+ email: string;
121
+ password?: string;
122
+ role: string;
123
+ is_active: boolean;
124
+ created_at: Date;
125
+ updated_at: Date;
126
+ }
127
+ ` : ''}
128
+ /**
129
+ * User Model (MySQL)
130
+ *
131
+ * Provides CRUD operations wrapping raw mysql2 queries.
132
+ * Creates the users table if it does not exist.
133
+ */
134
+ export const UserModel = {
135
+ /** Ensure the users table exists */
136
+ async initialize()${isTS ? ': Promise<void>' : ''} {
137
+ await pool.query(\`
138
+ CREATE TABLE IF NOT EXISTS users (
139
+ id INT AUTO_INCREMENT PRIMARY KEY,
140
+ name VARCHAR(100) NOT NULL,
141
+ email VARCHAR(255) NOT NULL UNIQUE,
142
+ password VARCHAR(255) NOT NULL,
143
+ role ENUM('user','admin') DEFAULT 'user',
144
+ is_active BOOLEAN DEFAULT TRUE,
145
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
146
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
147
+ )
148
+ \`);
149
+ },
150
+
151
+ async findAll()${isTS ? ': Promise<User[]>' : ''} {
152
+ const [rows] = await pool.query(
153
+ 'SELECT id, name, email, role, is_active, created_at, updated_at FROM users WHERE is_active = 1'
154
+ );
155
+ return rows${isTS ? ' as User[]' : ''};
156
+ },
157
+
158
+ async findById(id${isTS ? ': number' : ''})${isTS ? ': Promise<User | null>' : ''} {
159
+ const [rows] = await pool.query(
160
+ 'SELECT id, name, email, role, is_active, created_at, updated_at FROM users WHERE id = ?',
161
+ [id]
162
+ );
163
+ const users = rows${isTS ? ' as User[]' : ''};
164
+ return users[0] || null;
165
+ },
166
+
167
+ async findByEmail(email${isTS ? ': string' : ''})${isTS ? ': Promise<User | null>' : ''} {
168
+ const [rows] = await pool.query('SELECT * FROM users WHERE email = ?', [email]);
169
+ const users = rows${isTS ? ' as User[]' : ''};
170
+ return users[0] || null;
171
+ },
172
+
173
+ async create(data${isTS ? ': { name: string; email: string; password: string }' : ''})${isTS ? ': Promise<User>' : ''} {
174
+ const hashed = await bcrypt.hash(data.password, 12);
175
+ const [result] = await pool.query(
176
+ 'INSERT INTO users (name, email, password) VALUES (?, ?, ?)',
177
+ [data.name, data.email, hashed]
178
+ );
179
+ const insertResult = result${isTS ? ' as any' : ''};
180
+ return this.findById(insertResult.insertId)${isTS ? ' as Promise<User>' : ''};
181
+ },
182
+
183
+ async update(id${isTS ? ': number' : ''}, data${isTS ? ': Partial<User>' : ''})${isTS ? ': Promise<User | null>' : ''} {
184
+ const fields = Object.keys(data).map((k) => \`\${k} = ?\`).join(', ');
185
+ await pool.query(\`UPDATE users SET \${fields} WHERE id = ?\`, [...Object.values(data), id]);
186
+ return this.findById(id);
187
+ },
188
+
189
+ async delete(id${isTS ? ': number' : ''})${isTS ? ': Promise<void>' : ''} {
190
+ await pool.query('DELETE FROM users WHERE id = ?', [id]);
191
+ },
192
+
193
+ async comparePassword(plain${isTS ? ': string' : ''}, hashed${isTS ? ': string' : ''})${isTS ? ': Promise<boolean>' : ''} {
194
+ return bcrypt.compare(plain, hashed);
195
+ },
196
+ };
197
+ `;
198
+
199
+ // ── SQLite / better-sqlite3 ─────────────────────────────────────────────
200
+ case 'sqlite':
201
+ return `import { db } from '../config/db.js';
202
+ import bcrypt from 'bcryptjs';
203
+
204
+ ${isTS ? `
205
+ export interface User {
206
+ id: number;
207
+ name: string;
208
+ email: string;
209
+ password?: string;
210
+ role: string;
211
+ is_active: number; // SQLite uses 0/1 for booleans
212
+ created_at: string;
213
+ updated_at: string;
214
+ }
215
+ ` : ''}
216
+ /**
217
+ * User Model (SQLite)
218
+ *
219
+ * better-sqlite3 is fully synchronous — no async/await for queries.
220
+ * Initialize() should be called once at startup.
221
+ */
222
+ export const UserModel = {
223
+ initialize()${isTS ? ': void' : ''} {
224
+ db.exec(\`
225
+ CREATE TABLE IF NOT EXISTS users (
226
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
227
+ name TEXT NOT NULL,
228
+ email TEXT NOT NULL UNIQUE,
229
+ password TEXT NOT NULL,
230
+ role TEXT DEFAULT 'user',
231
+ is_active INTEGER DEFAULT 1,
232
+ created_at TEXT DEFAULT (datetime('now')),
233
+ updated_at TEXT DEFAULT (datetime('now'))
234
+ )
235
+ \`);
236
+ },
237
+
238
+ findAll()${isTS ? ': User[]' : ''} {
239
+ return db.prepare(
240
+ 'SELECT id, name, email, role, is_active, created_at, updated_at FROM users WHERE is_active = 1'
241
+ ).all()${isTS ? ' as User[]' : ''};
242
+ },
243
+
244
+ findById(id${isTS ? ': number' : ''})${isTS ? ': User | undefined' : ''} {
245
+ return db.prepare(
246
+ 'SELECT id, name, email, role, is_active, created_at, updated_at FROM users WHERE id = ?'
247
+ ).get(id)${isTS ? ' as User | undefined' : ''};
248
+ },
249
+
250
+ findByEmail(email${isTS ? ': string' : ''})${isTS ? ': User | undefined' : ''} {
251
+ return db.prepare('SELECT * FROM users WHERE email = ?').get(email)${isTS ? ' as User | undefined' : ''};
252
+ },
253
+
254
+ async create(data${isTS ? ': { name: string; email: string; password: string }' : ''}) {
255
+ const hashed = await bcrypt.hash(data.password, 12);
256
+ const stmt = db.prepare('INSERT INTO users (name, email, password) VALUES (?, ?, ?)');
257
+ const info = stmt.run(data.name, data.email, hashed);
258
+ return this.findById(info.lastInsertRowid${isTS ? ' as number' : ''});
259
+ },
260
+
261
+ update(id${isTS ? ': number' : ''}, data${isTS ? ': Partial<User>' : ''}) {
262
+ const fields = Object.keys(data).map((k) => \`\${k} = ?\`).join(', ');
263
+ db.prepare(\`UPDATE users SET \${fields}, updated_at = datetime('now') WHERE id = ?\`).run(
264
+ ...Object.values(data), id
265
+ );
266
+ return this.findById(id);
267
+ },
268
+
269
+ delete(id${isTS ? ': number' : ''})${isTS ? ': void' : ''} {
270
+ db.prepare('DELETE FROM users WHERE id = ?').run(id);
271
+ },
272
+
273
+ async comparePassword(plain${isTS ? ': string' : ''}, hashed${isTS ? ': string' : ''})${isTS ? ': Promise<boolean>' : ''} {
274
+ return bcrypt.compare(plain, hashed);
275
+ },
276
+ };
277
+ `;
278
+
279
+ default:
280
+ return `// No database selected — no user model generated.\n`;
281
+ }
282
+ }
283
+
284
+ /** Generate prisma/schema.prisma */
285
+ export function prismaSchema(options) {
286
+ const { projectName } = options;
287
+ return `// Prisma Schema for ${projectName}
288
+ // Run: npx prisma migrate dev --name init
289
+
290
+ generator client {
291
+ provider = "prisma-client-js"
292
+ }
293
+
294
+ datasource db {
295
+ provider = "postgresql"
296
+ url = env("DATABASE_URL")
297
+ }
298
+
299
+ model User {
300
+ id String @id @default(cuid())
301
+ name String
302
+ email String @unique
303
+ password String
304
+ role Role @default(USER)
305
+ isActive Boolean @default(true)
306
+ createdAt DateTime @default(now())
307
+ updatedAt DateTime @updatedAt
308
+
309
+ @@index([email])
310
+ @@map("users")
311
+ }
312
+
313
+ enum Role {
314
+ USER
315
+ ADMIN
316
+ }
317
+ `;
318
+ }