create-nara 1.0.21 → 1.0.23

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.

package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-nara",
3
- "version": "1.0.21",
3
+ "version": "1.0.23",
4
4
  "description": "CLI to scaffold NARA projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,21 +1,71 @@
1
- import { BaseController, jsonSuccess, ValidationError } from '@nara-web/core';
1
+ import { BaseController, jsonSuccess, jsonError, ValidationError } from '@nara-web/core';
2
2
  import type { NaraRequest, NaraResponse } from '@nara-web/core';
3
+ import { UserModel } from '../models/User.js';
4
+ import { db } from '../config/database.js';
3
5
  import bcrypt from 'bcrypt';
4
6
 
5
7
  export class UserController extends BaseController {
6
8
  async index(req: NaraRequest, res: NaraResponse) {
7
- // TODO: Implement pagination and filtering
8
- // const { page = 1, limit = 10, search, filter } = req.query;
9
- // const users = await UserModel.paginate({ page, limit, search, filter });
9
+ // Get query parameters
10
+ const page = parseInt(req.query?.page as string) || 1;
11
+ const limit = parseInt(req.query?.limit as string) || 10;
12
+ const search = (req.query?.search as string) || '';
13
+ const filter = (req.query?.filter as string) || 'all';
14
+ const offset = (page - 1) * limit;
15
+
16
+ // Build query
17
+ let query = db('users').select(
18
+ 'id', 'name', 'email', 'phone', 'avatar', 'role',
19
+ 'email_verified_at', 'created_at', 'updated_at'
20
+ );
21
+
22
+ // Apply search filter
23
+ if (search) {
24
+ query = query.where(function() {
25
+ this.where('name', 'like', `%${search}%`)
26
+ .orWhere('email', 'like', `%${search}%`);
27
+ });
28
+ }
29
+
30
+ // Apply role filter
31
+ if (filter === 'admin') {
32
+ query = query.where('role', 'admin');
33
+ } else if (filter === 'user') {
34
+ query = query.where('role', 'user');
35
+ } else if (filter === 'verified') {
36
+ query = query.whereNotNull('email_verified_at');
37
+ } else if (filter === 'unverified') {
38
+ query = query.whereNull('email_verified_at');
39
+ }
40
+
41
+ // Get total count for pagination
42
+ const countQuery = query.clone();
43
+ const [{ count: totalCount }] = await countQuery.count('* as count');
44
+ const total = Number(totalCount);
45
+
46
+ // Apply pagination and ordering
47
+ const users = await query
48
+ .orderBy('created_at', 'desc')
49
+ .limit(limit)
50
+ .offset(offset);
51
+
52
+ // Transform users to include is_admin and is_verified flags
53
+ const transformedUsers = users.map(user => ({
54
+ ...user,
55
+ is_admin: user.role === 'admin',
56
+ is_verified: !!user.email_verified_at
57
+ }));
58
+
59
+ const totalPages = Math.ceil(total / limit);
10
60
 
11
61
  return jsonSuccess(res, {
12
- users: [],
13
- total: 0,
14
- page: 1,
15
- limit: 10,
16
- totalPages: 0,
17
- hasNext: false,
18
- hasPrev: false,
62
+ users: transformedUsers,
63
+ total,
64
+ page,
65
+ limit,
66
+ totalPages,
67
+ hasNext: page < totalPages,
68
+ hasPrev: page > 1,
19
69
  });
20
70
  }
21
71
 
@@ -29,12 +79,40 @@ export class UserController extends BaseController {
29
79
  });
30
80
  }
31
81
 
32
- // TODO: Create user in database
33
- // const hashedPassword = password ? await bcrypt.hash(password, 10) : null;
34
- // const user = await UserModel.create({ name, email, password: hashedPassword, phone, is_admin, is_verified });
82
+ // Check if email already exists
83
+ const existing = await UserModel.findByEmail(email);
84
+ if (existing) {
85
+ throw new ValidationError({ email: ['Email already registered'] });
86
+ }
87
+
88
+ // Hash password if provided, otherwise generate random
89
+ const hashedPassword = password
90
+ ? await bcrypt.hash(password, 10)
91
+ : await bcrypt.hash(Math.random().toString(36).slice(-8), 10);
92
+
93
+ // Create user in database
94
+ const [userId] = await UserModel.create({
95
+ name,
96
+ email,
97
+ password: hashedPassword,
98
+ phone: phone || null,
99
+ role: is_admin ? 'admin' : 'user',
100
+ email_verified_at: is_verified ? new Date().toISOString() : null
101
+ });
102
+
103
+ // Fetch created user
104
+ const user = await UserModel.findById(userId);
35
105
 
36
106
  return jsonSuccess(res, {
37
- user: { id: '1', name, email, phone, is_admin, is_verified }
107
+ user: {
108
+ id: user?.id,
109
+ name: user?.name,
110
+ email: user?.email,
111
+ phone: user?.phone,
112
+ avatar: user?.avatar,
113
+ is_admin: user?.role === 'admin',
114
+ is_verified: !!user?.email_verified_at
115
+ }
38
116
  }, 'User created successfully');
39
117
  }
40
118
 
@@ -49,15 +127,49 @@ export class UserController extends BaseController {
49
127
  });
50
128
  }
51
129
 
52
- // TODO: Update user in database
53
- // const updateData: any = { name, email, phone, is_admin, is_verified };
54
- // if (password) {
55
- // updateData.password = await bcrypt.hash(password, 10);
56
- // }
57
- // await UserModel.update(id, updateData);
130
+ // Check if user exists
131
+ const existingUser = await UserModel.findById(Number(id));
132
+ if (!existingUser) {
133
+ return jsonError(res, 'User not found', 404);
134
+ }
135
+
136
+ // Check if email is taken by another user
137
+ const emailUser = await UserModel.findByEmail(email);
138
+ if (emailUser && emailUser.id !== Number(id)) {
139
+ throw new ValidationError({ email: ['Email already registered'] });
140
+ }
141
+
142
+ // Build update data
143
+ const updateData: Record<string, any> = {
144
+ name,
145
+ email,
146
+ phone: phone || null,
147
+ role: is_admin ? 'admin' : 'user',
148
+ email_verified_at: is_verified ? (existingUser.email_verified_at || new Date().toISOString()) : null,
149
+ updated_at: new Date().toISOString()
150
+ };
151
+
152
+ // Hash new password if provided
153
+ if (password) {
154
+ updateData.password = await bcrypt.hash(password, 10);
155
+ }
156
+
157
+ // Update user in database
158
+ await UserModel.update(Number(id), updateData);
159
+
160
+ // Fetch updated user
161
+ const user = await UserModel.findById(Number(id));
58
162
 
59
163
  return jsonSuccess(res, {
60
- user: { id, name, email, phone, is_admin, is_verified }
164
+ user: {
165
+ id: user?.id,
166
+ name: user?.name,
167
+ email: user?.email,
168
+ phone: user?.phone,
169
+ avatar: user?.avatar,
170
+ is_admin: user?.role === 'admin',
171
+ is_verified: !!user?.email_verified_at
172
+ }
61
173
  }, 'User updated successfully');
62
174
  }
63
175
 
@@ -70,9 +182,15 @@ export class UserController extends BaseController {
70
182
  });
71
183
  }
72
184
 
73
- // TODO: Delete users from database
74
- // await UserModel.deleteMany(ids);
185
+ // Prevent deleting current user
186
+ const currentUserId = req.user?.id;
187
+ if (currentUserId && ids.includes(currentUserId)) {
188
+ return jsonError(res, 'Cannot delete your own account', 400);
189
+ }
190
+
191
+ // Delete users from database
192
+ const deleted = await db('users').whereIn('id', ids).delete();
75
193
 
76
- return jsonSuccess(res, {}, `${ids.length} user(s) deleted successfully`);
194
+ return jsonSuccess(res, { deleted }, `${deleted} user(s) deleted successfully`);
77
195
  }
78
196
  }
@@ -144,6 +144,73 @@ function formatValidationErrors(errors: Record<string, string[]> | undefined): s
144
144
  return messages.join('; ');
145
145
  }
146
146
 
147
+ /**
148
+ * Map HTTP status codes to human-readable messages
149
+ */
150
+ function getHttpErrorMessage(status: number, statusText?: string): string {
151
+ const messages: Record<number, string> = {
152
+ 400: 'Invalid request. Please check your input.',
153
+ 401: 'Please log in to continue.',
154
+ 403: 'You do not have permission to perform this action.',
155
+ 404: 'The requested resource was not found.',
156
+ 405: 'This action is not allowed.',
157
+ 408: 'Request timed out. Please try again.',
158
+ 409: 'This action conflicts with existing data.',
159
+ 413: 'File too large. Please upload a smaller file.',
160
+ 422: 'Invalid data provided. Please check your input.',
161
+ 429: 'Too many requests. Please wait a moment.',
162
+ 500: 'Server error. Please try again later.',
163
+ 502: 'Server is temporarily unavailable.',
164
+ 503: 'Service unavailable. Please try again later.',
165
+ 504: 'Server took too long to respond.',
166
+ };
167
+ return messages[status] || statusText || `Error ${status}`;
168
+ }
169
+
170
+ /**
171
+ * Parse error from various response types
172
+ */
173
+ function parseErrorResponse(error: unknown): { message: string; code?: string; errors?: Record<string, string[]> } {
174
+ const axiosError = error as {
175
+ response?: { status?: number; statusText?: string; data?: ApiResponse | string };
176
+ message?: string;
177
+ code?: string;
178
+ };
179
+
180
+ // Network error (no response)
181
+ if (!axiosError.response) {
182
+ if (axiosError.code === 'ERR_NETWORK') {
183
+ return { message: 'Unable to connect to server. Please check your connection.' };
184
+ }
185
+ if (axiosError.code === 'ECONNABORTED') {
186
+ return { message: 'Request timed out. Please try again.' };
187
+ }
188
+ return { message: axiosError.message || 'Network error. Please try again.' };
189
+ }
190
+
191
+ const { status, statusText, data } = axiosError.response;
192
+
193
+ // If response is a string (HTML error page), use status code message
194
+ if (typeof data === 'string') {
195
+ return { message: getHttpErrorMessage(status || 500, statusText) };
196
+ }
197
+
198
+ // If response is JSON with our API format
199
+ if (data && typeof data === 'object') {
200
+ const apiResponse = data as ApiResponse;
201
+ if (apiResponse.message) {
202
+ return {
203
+ message: apiResponse.message,
204
+ code: apiResponse.code,
205
+ errors: apiResponse.errors
206
+ };
207
+ }
208
+ }
209
+
210
+ // Fallback to HTTP status message
211
+ return { message: getHttpErrorMessage(status || 500, statusText) };
212
+ }
213
+
147
214
  /**
148
215
  * Standardized API call wrapper that handles JSON responses from backend
149
216
  */
@@ -152,11 +219,11 @@ export async function api<T = unknown>(
152
219
  options: ApiOptions = {}
153
220
  ): Promise<ApiResponse<T>> {
154
221
  const { showSuccessToast = true, showErrorToast = true } = options;
155
-
222
+
156
223
  try {
157
224
  const response = await axiosCall();
158
225
  const result = response.data;
159
-
226
+
160
227
  if (result.success) {
161
228
  if (showSuccessToast && result.message) {
162
229
  Toast(result.message, 'success');
@@ -164,27 +231,24 @@ export async function api<T = unknown>(
164
231
  return { success: true, message: result.message, data: result.data };
165
232
  } else {
166
233
  if (showErrorToast) {
167
- const errorMsg = result.errors
168
- ? formatValidationErrors(result.errors) || result.message
234
+ const errorMsg = result.errors
235
+ ? formatValidationErrors(result.errors) || result.message
169
236
  : result.message;
170
237
  if (errorMsg) Toast(errorMsg, 'error');
171
238
  }
172
239
  return { success: false, message: result.message, code: result.code, errors: result.errors };
173
240
  }
174
241
  } catch (error: unknown) {
175
- const axiosError = error as { response?: { data?: ApiResponse } };
176
- const message = axiosError?.response?.data?.message || 'An error occurred. Please try again.';
177
- const code = axiosError?.response?.data?.code;
178
- const errors = axiosError?.response?.data?.errors;
179
-
242
+ const parsed = parseErrorResponse(error);
243
+
180
244
  if (showErrorToast) {
181
- const errorMsg = errors
182
- ? formatValidationErrors(errors) || message
183
- : message;
245
+ const errorMsg = parsed.errors
246
+ ? formatValidationErrors(parsed.errors) || parsed.message
247
+ : parsed.message;
184
248
  Toast(errorMsg, 'error');
185
249
  }
186
-
187
- return { success: false, message, code, errors };
250
+
251
+ return { success: false, message: parsed.message, code: parsed.code, errors: parsed.errors };
188
252
  }
189
253
  }
190
254
 
@@ -28,24 +28,44 @@
28
28
  headers: { 'Content-Type': 'application/json' },
29
29
  body: JSON.stringify({ email: form.email })
30
30
  });
31
+
32
+ // Handle non-JSON responses
33
+ const contentType = response.headers.get('content-type');
34
+ if (!contentType || !contentType.includes('application/json')) {
35
+ const statusMessages: Record<number, string> = {
36
+ 400: 'Please enter a valid email address.',
37
+ 404: 'Password reset service not available.',
38
+ 429: 'Too many requests. Please wait a moment.',
39
+ 500: 'Server error. Please try again later.',
40
+ };
41
+ Toast(statusMessages[response.status] || `Server error (${response.status})`, 'error');
42
+ return;
43
+ }
44
+
31
45
  const result = await response.json();
32
46
 
33
47
  if (result.success) {
34
48
  success = true;
35
49
  form.email = "";
36
- Toast(result.message || 'Reset link sent!', 'success');
50
+ Toast(result.message || 'Reset link sent! Check your email.', 'success');
37
51
  } else {
38
52
  // Handle validation errors
39
53
  if (result.errors) {
40
54
  const errorMessages = Object.values(result.errors).flat() as string[];
41
- Toast(errorMessages[0] || result.message || 'Failed to send reset link', 'error');
55
+ Toast(errorMessages[0] || result.message || 'Please enter a valid email address.', 'error');
42
56
  } else {
43
- Toast(result.message || 'Failed to send reset link', 'error');
57
+ Toast(result.message || 'Failed to send reset link. Please try again.', 'error');
44
58
  }
45
59
  }
46
60
  } catch (err: unknown) {
47
- const errorMessage = err instanceof Error ? err.message : 'Network error. Please check your connection.';
48
- Toast(errorMessage, 'error');
61
+ if (err instanceof SyntaxError) {
62
+ Toast('Server returned an invalid response. Please try again.', 'error');
63
+ } else if (err instanceof TypeError) {
64
+ Toast('Unable to connect to server. Please check your connection.', 'error');
65
+ } else {
66
+ const errorMessage = err instanceof Error ? err.message : 'An unexpected error occurred.';
67
+ Toast(errorMessage, 'error');
68
+ }
49
69
  } finally {
50
70
  loading = false;
51
71
  }
@@ -34,6 +34,22 @@
34
34
  headers: { 'Content-Type': 'application/json' },
35
35
  body: JSON.stringify({ email: form.email, password: form.password })
36
36
  });
37
+
38
+ // Handle non-JSON responses (server errors returning HTML)
39
+ const contentType = response.headers.get('content-type');
40
+ if (!contentType || !contentType.includes('application/json')) {
41
+ const statusMessages: Record<number, string> = {
42
+ 401: 'Invalid email or password.',
43
+ 403: 'Access denied.',
44
+ 404: 'Login service not available.',
45
+ 500: 'Server error. Please try again later.',
46
+ 502: 'Server is temporarily unavailable.',
47
+ 503: 'Service unavailable. Please try again later.',
48
+ };
49
+ Toast(statusMessages[response.status] || `Server error (${response.status})`, 'error');
50
+ return;
51
+ }
52
+
37
53
  const result = await response.json();
38
54
 
39
55
  if (result.success) {
@@ -43,14 +59,21 @@
43
59
  // Handle validation errors
44
60
  if (result.errors) {
45
61
  const errorMessages = Object.values(result.errors).flat() as string[];
46
- Toast(errorMessages[0] || result.message || 'Login failed', 'error');
62
+ Toast(errorMessages[0] || result.message || 'Invalid email or password.', 'error');
47
63
  } else {
48
- Toast(result.message || 'Login failed', 'error');
64
+ Toast(result.message || 'Invalid email or password.', 'error');
49
65
  }
50
66
  }
51
67
  } catch (err: unknown) {
52
- const errorMessage = err instanceof Error ? err.message : 'Network error. Please check your connection.';
53
- Toast(errorMessage, 'error');
68
+ // Network or parsing error
69
+ if (err instanceof SyntaxError) {
70
+ Toast('Server returned an invalid response. Please try again.', 'error');
71
+ } else if (err instanceof TypeError && (err.message.includes('fetch') || err.message.includes('network'))) {
72
+ Toast('Unable to connect to server. Please check your connection.', 'error');
73
+ } else {
74
+ const errorMessage = err instanceof Error ? err.message : 'An unexpected error occurred.';
75
+ Toast(errorMessage, 'error');
76
+ }
54
77
  } finally {
55
78
  loading = false;
56
79
  }
@@ -42,6 +42,23 @@
42
42
  headers: { 'Content-Type': 'application/json' },
43
43
  body: JSON.stringify({ name: form.name, email: form.email, password: form.password })
44
44
  });
45
+
46
+ // Handle non-JSON responses (server errors returning HTML)
47
+ const contentType = response.headers.get('content-type');
48
+ if (!contentType || !contentType.includes('application/json')) {
49
+ const statusMessages: Record<number, string> = {
50
+ 400: 'Invalid registration data.',
51
+ 403: 'Registration is not allowed.',
52
+ 404: 'Registration service not available.',
53
+ 409: 'This email is already registered.',
54
+ 500: 'Server error. Please try again later.',
55
+ 502: 'Server is temporarily unavailable.',
56
+ 503: 'Service unavailable. Please try again later.',
57
+ };
58
+ Toast(statusMessages[response.status] || `Server error (${response.status})`, 'error');
59
+ return;
60
+ }
61
+
45
62
  const result = await response.json();
46
63
 
47
64
  if (result.success) {
@@ -51,14 +68,21 @@
51
68
  // Handle validation errors
52
69
  if (result.errors) {
53
70
  const errorMessages = Object.values(result.errors).flat() as string[];
54
- Toast(errorMessages[0] || result.message || 'Registration failed', 'error');
71
+ Toast(errorMessages[0] || result.message || 'Registration failed. Please check your input.', 'error');
55
72
  } else {
56
- Toast(result.message || 'Registration failed', 'error');
73
+ Toast(result.message || 'Registration failed. Please check your input.', 'error');
57
74
  }
58
75
  }
59
76
  } catch (err: unknown) {
60
- const errorMessage = err instanceof Error ? err.message : 'Network error. Please check your connection.';
61
- Toast(errorMessage, 'error');
77
+ // Network or parsing error
78
+ if (err instanceof SyntaxError) {
79
+ Toast('Server returned an invalid response. Please try again.', 'error');
80
+ } else if (err instanceof TypeError && (err.message.includes('fetch') || err.message.includes('network'))) {
81
+ Toast('Unable to connect to server. Please check your connection.', 'error');
82
+ } else {
83
+ const errorMessage = err instanceof Error ? err.message : 'An unexpected error occurred.';
84
+ Toast(errorMessage, 'error');
85
+ }
62
86
  } finally {
63
87
  loading = false;
64
88
  }
@@ -42,23 +42,43 @@
42
42
  headers: { 'Content-Type': 'application/json' },
43
43
  body: JSON.stringify({ password: form.password, token: form.token })
44
44
  });
45
+
46
+ // Handle non-JSON responses
47
+ const contentType = response.headers.get('content-type');
48
+ if (!contentType || !contentType.includes('application/json')) {
49
+ const statusMessages: Record<number, string> = {
50
+ 400: 'Invalid reset request. Please try again.',
51
+ 401: 'Reset link has expired. Please request a new one.',
52
+ 404: 'Password reset service not available.',
53
+ 500: 'Server error. Please try again later.',
54
+ };
55
+ Toast(statusMessages[response.status] || `Server error (${response.status})`, 'error');
56
+ return;
57
+ }
58
+
45
59
  const result = await response.json();
46
60
 
47
61
  if (result.success) {
48
- Toast(result.message || 'Password reset successful', 'success');
62
+ Toast(result.message || 'Password reset successful! Please log in.', 'success');
49
63
  router.visit('/login');
50
64
  } else {
51
65
  // Handle validation errors
52
66
  if (result.errors) {
53
67
  const errorMessages = Object.values(result.errors).flat() as string[];
54
- Toast(errorMessages[0] || result.message || 'Password reset failed', 'error');
68
+ Toast(errorMessages[0] || result.message || 'Password reset failed. Please try again.', 'error');
55
69
  } else {
56
- Toast(result.message || 'Password reset failed', 'error');
70
+ Toast(result.message || 'Password reset failed. Please try again.', 'error');
57
71
  }
58
72
  }
59
73
  } catch (err: unknown) {
60
- const errorMessage = err instanceof Error ? err.message : 'Network error. Please check your connection.';
61
- Toast(errorMessage, 'error');
74
+ if (err instanceof SyntaxError) {
75
+ Toast('Server returned an invalid response. Please try again.', 'error');
76
+ } else if (err instanceof TypeError) {
77
+ Toast('Unable to connect to server. Please check your connection.', 'error');
78
+ } else {
79
+ const errorMessage = err instanceof Error ? err.message : 'An unexpected error occurred.';
80
+ Toast(errorMessage, 'error');
81
+ }
62
82
  } finally {
63
83
  loading = false;
64
84
  }