Skip to content

Commit 9b10748

Browse files
committed
Add password reset functionality with email confirmation flow
1 parent 5e8151d commit 9b10748

File tree

6 files changed

+262
-53
lines changed

6 files changed

+262
-53
lines changed

public/i18n/en.json

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
"settings": "Settings",
1212
"logout": "Logout",
1313
"theme": "Theme",
14-
"language": "Language"
14+
"language": "Language",
15+
"back_to_login": "Back to Login"
1516
},
1617
"errors": {
1718
"page_not_found": "404 - Page Not Found",
@@ -22,7 +23,12 @@
2223
"invalid_credentials": "Invalid credentials. Please check your email and password.",
2324
"auth_server_error": "Authentication server error. Please try again later.",
2425
"registration_failed": "Registration failed: ",
25-
"passwords_not_match": "Passwords do not match"
26+
"passwords_not_match": "Passwords do not match",
27+
"password_reset_failed": "Password reset failed",
28+
"reset_email_failed": "Failed to send password reset email",
29+
"unknown_error": "An unknown error occurred",
30+
"must_be_logged_in": "You must be logged in to reset your password",
31+
"invalid_reset_token": "Invalid or expired password reset token"
2632
},
2733
"auth": {
2834
"email": "Email",
@@ -43,6 +49,14 @@
4349
"to_get_started": "to get started.",
4450
"forgot_password": "Forgot your password?",
4551
"reset_password": "Reset Password",
52+
"reset_password_description": "Enter your new password below.",
53+
"reset_password_email_description": "Enter your email address below and we'll send you a link to reset your password.",
54+
"password_requirements": "Password must be at least 8 characters",
55+
"password_reset_success": "Password reset successfully! Please log in with your new password.",
56+
"reset_email_sent": "Password reset link has been sent to your email. Please check your inbox.",
57+
"send_reset_link": "Send Reset Link",
58+
"resetting": "Resetting...",
59+
"sending": "Sending...",
4660
"first_time_login": "If this is your first time logging in after the system update, use the default password:",
4761
"password_change_prompt": "You'll be prompted to change your password after login.",
4862
"check_auth_status": "Check Auth Status",

public/js/main.js

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,42 @@ function initRouter() {
134134
}
135135
},
136136
'/reset-password': {
137-
view: () => loadPage('/views/reset-password.html')
137+
view: () => loadPage('/views/reset-password.html'),
138+
afterRender: () => {
139+
// Apply translations after rendering
140+
const storedLang = localStorage.getItem('profullstack-language');
141+
if (storedLang && window.app && window.app.localizer) {
142+
console.log(`Post-render reset-password: Applying stored language: ${storedLang}`);
143+
window.app.localizer.setLanguage(storedLang);
144+
145+
// Force translation application
146+
if (window.app.translatePage) {
147+
window.app.translatePage();
148+
}
149+
}
150+
151+
// Dispatch the spa-transition-end event to ensure translations are applied
152+
document.dispatchEvent(new CustomEvent('spa-transition-end'));
153+
}
154+
},
155+
'/reset-password-confirm': {
156+
view: () => loadPage('/views/reset-password-confirm.html'),
157+
afterRender: () => {
158+
// Apply translations after rendering
159+
const storedLang = localStorage.getItem('profullstack-language');
160+
if (storedLang && window.app && window.app.localizer) {
161+
console.log(`Post-render reset-password-confirm: Applying stored language: ${storedLang}`);
162+
window.app.localizer.setLanguage(storedLang);
163+
164+
// Force translation application
165+
if (window.app.translatePage) {
166+
window.app.translatePage();
167+
}
168+
}
169+
170+
// Dispatch the spa-transition-end event to ensure translations are applied
171+
document.dispatchEvent(new CustomEvent('spa-transition-end'));
172+
}
138173
},
139174
'/state-demo': {
140175
view: () => loadPage('/views/state-demo.html')
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<div class="login-container">
2+
<h2 data-i18n="auth.reset_password">Reset Password</h2>
3+
<p data-i18n="auth.reset_password_description">Enter your new password below.</p>
4+
5+
<form id="reset-password-form">
6+
<div class="form-group">
7+
<label for="password" data-i18n="auth.new_password">New Password</label>
8+
<input type="password" id="password" name="password" required minlength="8">
9+
<div class="form-help" data-i18n="auth.password_requirements">Password must be at least 8 characters</div>
10+
</div>
11+
12+
<div class="form-group">
13+
<label for="confirm-password" data-i18n="auth.confirm_password">Confirm Password</label>
14+
<input type="password" id="confirm-password" name="confirm-password" required minlength="8">
15+
</div>
16+
17+
<button type="submit" class="login-button" data-i18n="auth.reset_password">Reset Password</button>
18+
</form>
19+
20+
<p class="login-link"><a href="/login" data-i18n="navigation.back_to_login">Back to Login</a></p>
21+
</div>
22+
23+
<script>
24+
document.addEventListener('DOMContentLoaded', () => {
25+
const form = document.getElementById('reset-password-form');
26+
if (!form) return;
27+
28+
form.addEventListener('submit', async (e) => {
29+
e.preventDefault();
30+
31+
const password = document.getElementById('password').value;
32+
const confirmPassword = document.getElementById('confirm-password').value;
33+
34+
if (password !== confirmPassword) {
35+
alert(window.i18n.t('errors.passwords_not_match'));
36+
return;
37+
}
38+
39+
// Show loading state
40+
const submitButton = form.querySelector('button[type="submit"]');
41+
const originalButtonText = submitButton.textContent;
42+
submitButton.textContent = window.i18n.t('auth.resetting');
43+
submitButton.disabled = true;
44+
45+
try {
46+
// Get the reset token from the URL
47+
const urlParams = new URLSearchParams(window.location.search);
48+
const resetToken = urlParams.get('token');
49+
50+
if (!resetToken) {
51+
throw new Error(window.i18n.t('errors.invalid_reset_token'));
52+
}
53+
54+
// Send password reset confirmation to the server API
55+
const response = await fetch('/api/1/auth/reset-password-confirm', {
56+
method: 'POST',
57+
headers: {
58+
'Content-Type': 'application/json'
59+
},
60+
body: JSON.stringify({
61+
token: resetToken,
62+
password: password
63+
})
64+
});
65+
66+
if (!response.ok) {
67+
const errorData = await response.json();
68+
throw new Error(errorData.error || response.statusText);
69+
}
70+
71+
// Show success message
72+
alert(window.i18n.t('auth.password_reset_success'));
73+
74+
// Redirect to login page
75+
window.router.navigate('/login');
76+
} catch (error) {
77+
console.error('Password reset error:', error);
78+
submitButton.textContent = originalButtonText;
79+
submitButton.disabled = false;
80+
81+
// Show error message
82+
alert(window.i18n.t('errors.password_reset_failed') + ': ' + (error.message || window.i18n.t('errors.unknown_error')));
83+
}
84+
});
85+
});
86+
</script>

public/views/reset-password.html

Lines changed: 30 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,67 @@
1-
<div class="auth-container">
2-
<h1>Reset Password</h1>
3-
<p class="auth-description">Enter your new password below.</p>
1+
<div class="login-container">
2+
<h2 data-i18n="auth.forgot_password">Forgot Password</h2>
3+
<p data-i18n="auth.reset_password_email_description">Enter your email address below and we'll send you a link to reset your password.</p>
44

5-
<form id="reset-password-form" class="auth-form">
5+
<form id="reset-password-email-form">
66
<div class="form-group">
7-
<label for="password">New Password</label>
8-
<input type="password" id="password" name="password" required minlength="8" placeholder="Enter your new password">
9-
<small>Password must be at least 8 characters</small>
7+
<label for="email" data-i18n="auth.email">Email</label>
8+
<input type="email" id="email" name="email" required>
109
</div>
1110

12-
<div class="form-group">
13-
<label for="confirm-password">Confirm Password</label>
14-
<input type="password" id="confirm-password" name="confirm-password" required minlength="8" placeholder="Confirm your new password">
15-
</div>
16-
17-
<button type="submit" class="btn btn-primary">Reset Password</button>
11+
<button type="submit" class="login-button" data-i18n="auth.send_reset_link">Send Reset Link</button>
1812
</form>
1913

20-
<div class="auth-links">
21-
<a href="/login">Back to Login</a>
22-
</div>
14+
<p class="login-link"><a href="/login" data-i18n="navigation.back_to_login">Back to Login</a></p>
2315
</div>
2416

2517
<script>
2618
document.addEventListener('DOMContentLoaded', () => {
27-
const form = document.getElementById('reset-password-form');
19+
const form = document.getElementById('reset-password-email-form');
2820
if (!form) return;
2921

3022
form.addEventListener('submit', async (e) => {
3123
e.preventDefault();
3224

33-
const password = document.getElementById('password').value;
34-
const confirmPassword = document.getElementById('confirm-password').value;
35-
36-
if (password !== confirmPassword) {
37-
alert('Passwords do not match');
38-
return;
39-
}
25+
const email = document.getElementById('email').value;
4026

4127
// Show loading state
4228
const submitButton = form.querySelector('button[type="submit"]');
4329
const originalButtonText = submitButton.textContent;
44-
submitButton.textContent = 'Resetting...';
30+
submitButton.textContent = window.i18n.t('auth.sending');
4531
submitButton.disabled = true;
4632

4733
try {
48-
// Import Supabase client for JWT authentication
49-
const { createClient } = await import('https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2/+esm');
50-
const supabaseUrl = 'https://arokhsfbkdnfuklmqajh.supabase.co'; // Should match server config
51-
const supabaseAnonKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImFyb2toc2Zia2RuZnVrbG1xYWpoIiwicm9sZSI6ImFub24iLCJpYXQiOjE2ODI0NTI4MDAsImV4cCI6MTk5ODAyODgwMH0.KxwHdxWXLLrJtFzLAYI-fwzgz8m5xsHD4XGdNw_xJm8'; // Public anon key
52-
53-
console.log('Creating Supabase client for password reset');
54-
const supabase = createClient(supabaseUrl, supabaseAnonKey);
34+
// Import the API client
35+
const { ApiClient } = await import('./api-client.js');
5536

56-
// Get the JWT token from localStorage
57-
const jwtToken = localStorage.getItem('jwt_token');
58-
59-
if (!jwtToken) {
60-
throw new Error('You must be logged in to reset your password');
61-
}
62-
63-
// Update the user's password
64-
const { error } = await supabase.auth.updateUser({
65-
password: password
37+
// Send password reset request to the server API
38+
const response = await fetch('/api/1/auth/reset-password', {
39+
method: 'POST',
40+
headers: {
41+
'Content-Type': 'application/json'
42+
},
43+
body: JSON.stringify({ email })
6644
});
6745

68-
if (error) {
69-
throw error;
46+
if (!response.ok) {
47+
const errorData = await response.json();
48+
throw new Error(errorData.error || response.statusText);
7049
}
7150

7251
// Show success message
73-
alert('Password reset successfully! Please log in with your new password.');
52+
alert(window.i18n.t('auth.reset_email_sent'));
7453

75-
// Redirect to login page
76-
window.router.navigate('/login');
54+
// Clear the form
55+
form.reset();
56+
submitButton.textContent = originalButtonText;
57+
submitButton.disabled = false;
7758
} catch (error) {
78-
console.error('Password reset error:', error);
59+
console.error('Password reset email error:', error);
7960
submitButton.textContent = originalButtonText;
8061
submitButton.disabled = false;
8162

8263
// Show error message
83-
alert('Password reset failed: ' + (error.message || 'An unknown error occurred'));
64+
alert(window.i18n.t('errors.reset_email_failed') + ': ' + (error.message || window.i18n.t('errors.unknown_error')));
8465
}
8566
});
8667
});

src/routes/auth.js

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,80 @@ export async function refreshTokenHandler(c) {
129129
}
130130
}
131131

132+
/**
133+
* Route handler for requesting a password reset
134+
* @param {Object} c - Hono context
135+
* @returns {Response} - JSON response with status
136+
*/
137+
export async function resetPasswordHandler(c) {
138+
try {
139+
// Get email from request body
140+
const { email } = await c.req.json();
141+
142+
if (!email) {
143+
return c.json({ error: 'Email is required' }, 400);
144+
}
145+
146+
console.log(`Requesting password reset for ${email}`);
147+
148+
// Request password reset from Supabase
149+
const { error } = await supabase.auth.resetPasswordForEmail(email, {
150+
redirectTo: `${c.req.header('origin') || 'http://localhost:3000'}/reset-password-confirm`
151+
});
152+
153+
if (error) {
154+
console.error('Error requesting password reset:', error);
155+
return c.json({ error: 'Failed to request password reset: ' + error.message }, 500);
156+
}
157+
158+
return c.json({
159+
success: true,
160+
message: 'Password reset email sent successfully'
161+
});
162+
} catch (error) {
163+
console.error('Error in reset password handler:', error);
164+
return errorUtils.handleError(error, c);
165+
}
166+
}
167+
168+
/**
169+
* Route handler for confirming a password reset
170+
* @param {Object} c - Hono context
171+
* @returns {Response} - JSON response with status
172+
*/
173+
export async function resetPasswordConfirmHandler(c) {
174+
try {
175+
// Get token and new password from request body
176+
const { token, password } = await c.req.json();
177+
178+
if (!token || !password) {
179+
return c.json({ error: 'Token and password are required' }, 400);
180+
}
181+
182+
console.log('Confirming password reset');
183+
184+
// Update user's password using the token
185+
const { error } = await supabase.auth.verifyOtp({
186+
token_hash: token,
187+
type: 'recovery',
188+
new_password: password
189+
});
190+
191+
if (error) {
192+
console.error('Error confirming password reset:', error);
193+
return c.json({ error: 'Failed to reset password: ' + error.message }, 500);
194+
}
195+
196+
return c.json({
197+
success: true,
198+
message: 'Password reset successfully'
199+
});
200+
} catch (error) {
201+
console.error('Error in reset password confirm handler:', error);
202+
return errorUtils.handleError(error, c);
203+
}
204+
}
205+
132206
/**
133207
* Route configuration for auth endpoints
134208
*/
@@ -143,3 +217,15 @@ export const registerRoute = {
143217
path: '/api/1/auth/register',
144218
handler: registerHandler
145219
};
220+
221+
export const resetPasswordRoute = {
222+
method: 'POST',
223+
path: '/api/1/auth/reset-password',
224+
handler: resetPasswordHandler
225+
};
226+
227+
export const resetPasswordConfirmRoute = {
228+
method: 'POST',
229+
path: '/api/1/auth/reset-password-confirm',
230+
handler: resetPasswordConfirmHandler
231+
};

0 commit comments

Comments
 (0)