create-nara 1.0.23 → 1.0.25

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.

Potentially problematic release.


This version of create-nara might be problematic. Click here for more details.

Files changed (26) hide show
  1. package/package.json +1 -1
  2. package/templates/base/app/config/constants.ts +251 -0
  3. package/templates/base/app/config/env.ts +110 -0
  4. package/templates/base/app/config/index.ts +8 -0
  5. package/templates/base/app/services/Logger.ts +161 -0
  6. package/templates/features/auth/app/middlewares/csrf.ts +197 -0
  7. package/templates/features/auth/app/middlewares/rateLimit.ts +220 -0
  8. package/templates/features/auth/app/middlewares/requestLogger.ts +200 -0
  9. package/templates/features/auth/app/middlewares/securityHeaders.ts +291 -0
  10. package/templates/features/auth/app/validators/index.ts +49 -0
  11. package/templates/features/auth/app/validators/schemas.ts +455 -0
  12. package/templates/features/auth/app/validators/validate.ts +103 -0
  13. package/templates/features/auth/routes/auth.ts +27 -26
  14. package/templates/features/db/app/models/EmailVerificationToken.ts +77 -0
  15. package/templates/features/db/app/models/PasswordResetToken.ts +83 -0
  16. package/templates/features/db/migrations/20240101000002_create_password_reset_tokens.ts +15 -0
  17. package/templates/features/db/migrations/20240101000003_create_email_verification_tokens.ts +18 -0
  18. package/templates/features/db/seeds/users.ts +36 -0
  19. package/templates/features/uploads/app/services/Storage.ts +266 -0
  20. package/templates/features/uploads/routes/uploads.ts +5 -4
  21. package/templates/minimal/app/utils/route-helper.ts +26 -0
  22. package/templates/minimal/server.ts +7 -1
  23. package/templates/svelte/app/utils/route-helper.ts +26 -0
  24. package/templates/svelte/server.ts +7 -1
  25. package/templates/vue/app/utils/route-helper.ts +26 -0
  26. package/templates/vue/server.ts +7 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-nara",
3
- "version": "1.0.23",
3
+ "version": "1.0.25",
4
4
  "description": "CLI to scaffold NARA projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,251 @@
1
+ /**
2
+ * Application Constants
3
+ *
4
+ * Centralized configuration values to avoid magic numbers/strings
5
+ * scattered throughout the codebase.
6
+ */
7
+
8
+ // ============================================
9
+ // Server Configuration
10
+ // ============================================
11
+
12
+ export const SERVER = {
13
+ /** Maximum request body size in bytes (10MB) */
14
+ MAX_BODY_SIZE: 10 * 1024 * 1024,
15
+
16
+ /** Default server port */
17
+ DEFAULT_PORT: 3000,
18
+
19
+ /** Default Vite dev server port */
20
+ DEFAULT_VITE_PORT: 5173,
21
+ } as const;
22
+
23
+ // ============================================
24
+ // Authentication & Security
25
+ // ============================================
26
+
27
+ export const AUTH = {
28
+ /** JWT expiry in seconds (7 days) */
29
+ JWT_EXPIRY_SECONDS: 7 * 24 * 60 * 60,
30
+
31
+ /** Session cookie expiry in milliseconds (30 days) */
32
+ SESSION_EXPIRY_MS: 30 * 24 * 60 * 60 * 1000,
33
+
34
+ /** Error cookie expiry in milliseconds (5 minutes) */
35
+ ERROR_COOKIE_EXPIRY_MS: 5 * 60 * 1000,
36
+
37
+ /** Minimum password length */
38
+ MIN_PASSWORD_LENGTH: 8,
39
+
40
+ /** Maximum password length */
41
+ MAX_PASSWORD_LENGTH: 100,
42
+
43
+ /** Bcrypt salt rounds */
44
+ BCRYPT_SALT_ROUNDS: 10,
45
+ } as const;
46
+
47
+ // ============================================
48
+ // Pagination
49
+ // ============================================
50
+
51
+ export const PAGINATION = {
52
+ /** Default page size */
53
+ DEFAULT_PAGE_SIZE: 10,
54
+
55
+ /** Maximum page size allowed */
56
+ MAX_PAGE_SIZE: 100,
57
+
58
+ /** Default page number */
59
+ DEFAULT_PAGE: 1,
60
+ } as const;
61
+
62
+ // ============================================
63
+ // User & Profile
64
+ // ============================================
65
+
66
+ export const USER = {
67
+ /** Minimum name length */
68
+ MIN_NAME_LENGTH: 2,
69
+
70
+ /** Maximum name length */
71
+ MAX_NAME_LENGTH: 100,
72
+
73
+ /** Minimum phone length */
74
+ MIN_PHONE_LENGTH: 10,
75
+
76
+ /** Maximum phone length */
77
+ MAX_PHONE_LENGTH: 20,
78
+ } as const;
79
+
80
+ // ============================================
81
+ // File Upload
82
+ // ============================================
83
+
84
+ export const UPLOAD = {
85
+ /** Maximum file size in bytes (5MB) */
86
+ MAX_FILE_SIZE: 5 * 1024 * 1024,
87
+
88
+ /** Allowed image extensions */
89
+ ALLOWED_IMAGE_EXTENSIONS: ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'],
90
+
91
+ /** Allowed document extensions */
92
+ ALLOWED_DOC_EXTENSIONS: ['pdf', 'doc', 'docx', 'xls', 'xlsx'],
93
+
94
+ /** Avatar upload directory */
95
+ AVATAR_DIR: 'avatars',
96
+
97
+ /** General assets directory */
98
+ ASSETS_DIR: 'assets',
99
+ } as const;
100
+
101
+ // ============================================
102
+ // Cache & Performance
103
+ // ============================================
104
+
105
+ export const CACHE = {
106
+ /** Static asset cache duration in seconds (1 year) */
107
+ STATIC_ASSET_MAX_AGE: 365 * 24 * 60 * 60,
108
+
109
+ /** API response cache duration in seconds (5 minutes) */
110
+ API_CACHE_DURATION: 5 * 60,
111
+
112
+ /** Session cache duration in seconds (1 hour) */
113
+ SESSION_CACHE_DURATION: 60 * 60,
114
+ } as const;
115
+
116
+ // ============================================
117
+ // Rate Limiting
118
+ // ============================================
119
+
120
+ export const RATE_LIMIT = {
121
+ /** Maximum requests per window (general) */
122
+ MAX_REQUESTS: 100,
123
+
124
+ /** Window duration in milliseconds (15 minutes) */
125
+ WINDOW_MS: 15 * 60 * 1000,
126
+
127
+ /** Login attempts before lockout */
128
+ MAX_LOGIN_ATTEMPTS: 5,
129
+
130
+ /** Login lockout duration in milliseconds (15 minutes) */
131
+ LOGIN_LOCKOUT_MS: 15 * 60 * 1000,
132
+
133
+ /** API rate limit (requests per minute) */
134
+ API_MAX_REQUESTS: 60,
135
+
136
+ /** API rate limit window (1 minute) */
137
+ API_WINDOW_MS: 60 * 1000,
138
+
139
+ /** Strict rate limit for sensitive endpoints */
140
+ STRICT_MAX_REQUESTS: 10,
141
+
142
+ /** Strict rate limit window (1 minute) */
143
+ STRICT_WINDOW_MS: 60 * 1000,
144
+ } as const;
145
+
146
+ // ============================================
147
+ // Security Headers
148
+ // ============================================
149
+
150
+ export const SECURITY = {
151
+ /** HSTS max-age in seconds (1 year) */
152
+ HSTS_MAX_AGE: 365 * 24 * 60 * 60,
153
+
154
+ /** CSRF token length in bytes */
155
+ CSRF_TOKEN_LENGTH: 32,
156
+
157
+ /** CSRF cookie max age in seconds (24 hours) */
158
+ CSRF_COOKIE_MAX_AGE: 24 * 60 * 60,
159
+
160
+ /** CSRF cookie name */
161
+ CSRF_COOKIE_NAME: 'csrf_token',
162
+
163
+ /** CSRF header name */
164
+ CSRF_HEADER_NAME: 'X-CSRF-Token',
165
+ } as const;
166
+
167
+ // ============================================
168
+ // Database
169
+ // ============================================
170
+
171
+ export const DATABASE = {
172
+ /** Default connection type */
173
+ DEFAULT_CONNECTION: 'development',
174
+
175
+ /** Connection pool min */
176
+ POOL_MIN: 2,
177
+
178
+ /** Connection pool max */
179
+ POOL_MAX: 10,
180
+ } as const;
181
+
182
+ // ============================================
183
+ // Logging
184
+ // ============================================
185
+
186
+ export const LOGGING = {
187
+ /** Available log levels */
188
+ LEVELS: ['trace', 'debug', 'info', 'warn', 'error', 'fatal'] as const,
189
+
190
+ /** Default log level */
191
+ DEFAULT_LEVEL: 'info',
192
+
193
+ /** Log file directory */
194
+ LOG_DIR: 'logs',
195
+
196
+ /** Max log file size in bytes (10MB) */
197
+ MAX_LOG_SIZE: 10 * 1024 * 1024,
198
+
199
+ /** Number of log files to keep */
200
+ MAX_LOG_FILES: 10,
201
+ } as const;
202
+
203
+ // ============================================
204
+ // HTTP Status Codes (commonly used)
205
+ // ============================================
206
+
207
+ export const HTTP_STATUS = {
208
+ OK: 200,
209
+ CREATED: 201,
210
+ NO_CONTENT: 204,
211
+ BAD_REQUEST: 400,
212
+ UNAUTHORIZED: 401,
213
+ FORBIDDEN: 403,
214
+ NOT_FOUND: 404,
215
+ CONFLICT: 409,
216
+ UNPROCESSABLE_ENTITY: 422,
217
+ TOO_MANY_REQUESTS: 429,
218
+ INTERNAL_SERVER_ERROR: 500,
219
+ SERVICE_UNAVAILABLE: 503,
220
+ } as const;
221
+
222
+ // ============================================
223
+ // Error Messages
224
+ // ============================================
225
+
226
+ export const ERROR_MESSAGES = {
227
+ UNAUTHORIZED: 'Unauthorized',
228
+ FORBIDDEN: 'Access denied',
229
+ NOT_FOUND: 'Not found',
230
+ VALIDATION_FAILED: 'Validation failed',
231
+ INTERNAL_ERROR: 'Internal server error',
232
+ INVALID_CREDENTIALS: 'Invalid email or password',
233
+ EMAIL_EXISTS: 'Email already in use',
234
+ TOKEN_EXPIRED: 'Invalid or expired token',
235
+ SESSION_EXPIRED: 'Session expired, please login again',
236
+ } as const;
237
+
238
+ // ============================================
239
+ // Success Messages
240
+ // ============================================
241
+
242
+ export const SUCCESS_MESSAGES = {
243
+ USER_CREATED: 'User created successfully',
244
+ USER_UPDATED: 'User updated successfully',
245
+ USER_DELETED: 'User deleted successfully',
246
+ PROFILE_UPDATED: 'Profile updated successfully',
247
+ PASSWORD_CHANGED: 'Password changed successfully',
248
+ PASSWORD_RESET_SENT: 'Password reset link sent',
249
+ LOGIN_SUCCESS: 'Login successful',
250
+ LOGOUT_SUCCESS: 'Logout successful',
251
+ } as const;
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Environment Configuration & Validation
3
+ *
4
+ * Validates required environment variables on application startup.
5
+ * Provides typed access to environment configuration.
6
+ */
7
+ import { SERVER, LOGGING } from './constants.js';
8
+
9
+ // ============================================
10
+ // Type Definitions
11
+ // ============================================
12
+
13
+ export interface Env {
14
+ // Server
15
+ NODE_ENV: 'development' | 'production' | 'test';
16
+ PORT: number;
17
+ APP_URL: string;
18
+
19
+ // Database
20
+ DB_CONNECTION: string;
21
+
22
+ // Auth
23
+ JWT_SECRET: string;
24
+
25
+ // Logging
26
+ LOG_LEVEL: typeof LOGGING.LEVELS[number];
27
+ }
28
+
29
+ // ============================================
30
+ // Validation Function
31
+ // ============================================
32
+
33
+ /**
34
+ * Validate environment variables
35
+ * @returns Validated and typed environment object
36
+ * @throws Error if validation fails
37
+ */
38
+ export function validateEnv(): Env {
39
+ const errors: string[] = [];
40
+ const env = process.env;
41
+
42
+ // Validate NODE_ENV
43
+ const validNodeEnvs = ['development', 'production', 'test'] as const;
44
+ const nodeEnv = env.NODE_ENV || 'development';
45
+ if (!validNodeEnvs.includes(nodeEnv as any)) {
46
+ errors.push(` - NODE_ENV: Must be one of ${validNodeEnvs.join(', ')}`);
47
+ }
48
+
49
+ // Validate PORT
50
+ const port = parseInt(env.PORT || String(SERVER.DEFAULT_PORT), 10);
51
+ if (isNaN(port) || port <= 0) {
52
+ errors.push(' - PORT: Must be a positive number');
53
+ }
54
+
55
+ // Validate LOG_LEVEL
56
+ const logLevel = env.LOG_LEVEL || 'info';
57
+ if (!LOGGING.LEVELS.includes(logLevel as any)) {
58
+ errors.push(` - LOG_LEVEL: Must be one of ${LOGGING.LEVELS.join(', ')}`);
59
+ }
60
+
61
+ // Warning for missing JWT_SECRET in production
62
+ if (nodeEnv === 'production' && !env.JWT_SECRET) {
63
+ errors.push(' - JWT_SECRET: Required in production');
64
+ }
65
+
66
+ if (errors.length > 0) {
67
+ console.error('\n❌ Environment validation failed:\n');
68
+ console.error(errors.join('\n'));
69
+ console.error('\nPlease check your .env file and ensure all required variables are set.\n');
70
+ process.exit(1);
71
+ }
72
+
73
+ return {
74
+ NODE_ENV: nodeEnv as 'development' | 'production' | 'test',
75
+ PORT: port,
76
+ APP_URL: env.APP_URL || `http://localhost:${port}`,
77
+ DB_CONNECTION: env.DB_CONNECTION || 'development',
78
+ JWT_SECRET: env.JWT_SECRET || 'your-secret-key',
79
+ LOG_LEVEL: logLevel as typeof LOGGING.LEVELS[number],
80
+ };
81
+ }
82
+
83
+ /**
84
+ * Get environment summary for logging
85
+ */
86
+ export function getEnvSummary(env: Env) {
87
+ return {
88
+ nodeEnv: env.NODE_ENV,
89
+ port: env.PORT,
90
+ appUrl: env.APP_URL,
91
+ dbConnection: env.DB_CONNECTION,
92
+ logLevel: env.LOG_LEVEL,
93
+ };
94
+ }
95
+
96
+ // ============================================
97
+ // Singleton Instance
98
+ // ============================================
99
+
100
+ let _env: Env | null = null;
101
+
102
+ /**
103
+ * Get validated environment (lazy initialization)
104
+ */
105
+ export function getEnv(): Env {
106
+ if (!_env) {
107
+ _env = validateEnv();
108
+ }
109
+ return _env;
110
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Config Module
3
+ *
4
+ * Re-exports all configuration modules.
5
+ */
6
+
7
+ export * from './constants.js';
8
+ export * from './env.js';
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Logger Service
3
+ *
4
+ * Simple structured logging without external dependencies.
5
+ * Supports multiple log levels and structured data.
6
+ */
7
+
8
+ type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
9
+
10
+ const LOG_LEVELS: Record<LogLevel, number> = {
11
+ trace: 10,
12
+ debug: 20,
13
+ info: 30,
14
+ warn: 40,
15
+ error: 50,
16
+ fatal: 60,
17
+ };
18
+
19
+ /**
20
+ * Get current log level from environment
21
+ */
22
+ function getLogLevel(): LogLevel {
23
+ const level = (process.env.LOG_LEVEL || 'info').toLowerCase() as LogLevel;
24
+ return LOG_LEVELS[level] !== undefined ? level : 'info';
25
+ }
26
+
27
+ /**
28
+ * Check if a log level should be logged
29
+ */
30
+ function shouldLog(level: LogLevel): boolean {
31
+ const currentLevel = getLogLevel();
32
+ return LOG_LEVELS[level] >= LOG_LEVELS[currentLevel];
33
+ }
34
+
35
+ /**
36
+ * Format log message with timestamp and metadata
37
+ */
38
+ function formatLog(level: LogLevel, message: string, data?: Record<string, any>): string {
39
+ const timestamp = new Date().toISOString();
40
+ const levelUpper = level.toUpperCase().padEnd(5);
41
+
42
+ if (data && Object.keys(data).length > 0) {
43
+ return `[${timestamp}] ${levelUpper} ${message} ${JSON.stringify(data)}`;
44
+ }
45
+ return `[${timestamp}] ${levelUpper} ${message}`;
46
+ }
47
+
48
+ /**
49
+ * Logger class with utility methods
50
+ */
51
+ class Logger {
52
+ /**
53
+ * Log trace level message (very detailed debugging)
54
+ */
55
+ trace(message: string, data?: Record<string, any>): void {
56
+ if (shouldLog('trace')) {
57
+ console.log(formatLog('trace', message, data));
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Log debug level message
63
+ */
64
+ debug(message: string, data?: Record<string, any>): void {
65
+ if (shouldLog('debug')) {
66
+ console.log(formatLog('debug', message, data));
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Log info level message
72
+ */
73
+ info(message: string, data?: Record<string, any>): void {
74
+ if (shouldLog('info')) {
75
+ console.log(formatLog('info', message, data));
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Log warning level message
81
+ */
82
+ warn(message: string, data?: Record<string, any>): void {
83
+ if (shouldLog('warn')) {
84
+ console.warn(formatLog('warn', message, data));
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Log error level message
90
+ */
91
+ error(message: string, error?: Error | Record<string, any>): void {
92
+ if (shouldLog('error')) {
93
+ if (error instanceof Error) {
94
+ console.error(formatLog('error', message, {
95
+ error: error.message,
96
+ stack: error.stack,
97
+ }));
98
+ } else if (error) {
99
+ console.error(formatLog('error', message, error));
100
+ } else {
101
+ console.error(formatLog('error', message));
102
+ }
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Log fatal level message (application crash)
108
+ */
109
+ fatal(message: string, error?: Error | Record<string, any>): void {
110
+ if (shouldLog('fatal')) {
111
+ if (error instanceof Error) {
112
+ console.error(formatLog('fatal', message, {
113
+ error: error.message,
114
+ stack: error.stack,
115
+ }));
116
+ } else if (error) {
117
+ console.error(formatLog('fatal', message, error));
118
+ } else {
119
+ console.error(formatLog('fatal', message));
120
+ }
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Log HTTP request
126
+ */
127
+ logRequest(data: {
128
+ method: string;
129
+ url: string;
130
+ statusCode?: number;
131
+ responseTime?: number;
132
+ userId?: string;
133
+ ip?: string;
134
+ }): void {
135
+ this.info('HTTP Request', data);
136
+ }
137
+
138
+ /**
139
+ * Log database query (use sparingly in production)
140
+ */
141
+ logQuery(query: string, duration?: number): void {
142
+ this.debug('Database Query', { query, duration });
143
+ }
144
+
145
+ /**
146
+ * Log authentication event
147
+ */
148
+ logAuth(event: string, data: Record<string, any>): void {
149
+ this.info(`Auth: ${event}`, data);
150
+ }
151
+
152
+ /**
153
+ * Log security event
154
+ */
155
+ logSecurity(event: string, data: Record<string, any>): void {
156
+ this.warn(`Security: ${event}`, data);
157
+ }
158
+ }
159
+
160
+ // Export singleton instance
161
+ export default new Logger();