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 +1 -1
- package/templates/features/auth/app/controllers/UserController.ts +143 -25
- package/templates/svelte/resources/js/components/helper.ts +78 -14
- package/templates/svelte/resources/js/pages/auth/forgot-password.svelte +25 -5
- package/templates/svelte/resources/js/pages/auth/login.svelte +27 -4
- package/templates/svelte/resources/js/pages/auth/register.svelte +28 -4
- package/templates/svelte/resources/js/pages/auth/reset-password.svelte +25 -5
package/package.json
CHANGED
|
@@ -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
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
14
|
-
page
|
|
15
|
-
limit
|
|
16
|
-
totalPages
|
|
17
|
-
hasNext:
|
|
18
|
-
hasPrev:
|
|
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
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
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: {
|
|
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
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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: {
|
|
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
|
-
//
|
|
74
|
-
|
|
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, {}, `${
|
|
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
|
|
176
|
-
|
|
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 || '
|
|
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
|
-
|
|
48
|
-
|
|
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 || '
|
|
62
|
+
Toast(errorMessages[0] || result.message || 'Invalid email or password.', 'error');
|
|
47
63
|
} else {
|
|
48
|
-
Toast(result.message || '
|
|
64
|
+
Toast(result.message || 'Invalid email or password.', 'error');
|
|
49
65
|
}
|
|
50
66
|
}
|
|
51
67
|
} catch (err: unknown) {
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
61
|
-
|
|
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
|
-
|
|
61
|
-
|
|
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
|
}
|