-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
updated google auth workflow; added forgot password functionality and…
… page
- Loading branch information
1 parent
c058adb
commit fc77a5f
Showing
4 changed files
with
328 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
<!DOCTYPE html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="UTF-8"> | ||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
<title>Forgot Password - The Give Hub</title> | ||
<link rel="stylesheet" href="login.css"> | ||
</head> | ||
<body> | ||
<div class="login-container"> | ||
<div class="login-header"> | ||
<img src="/img/black-logo.svg" class="logo"> | ||
<div class="login-header-text"> | ||
<h1><span style="font-size:0.7em;font-weight:400;">Reset your password on</span><br> The Give Hub</h1> | ||
<p>Enter your email to receive reset instructions</p> | ||
</div> | ||
</div> | ||
|
||
<form class="login-form" id="forgotPasswordForm"> | ||
<div id="requestStep" class="registration-step active"> | ||
<div class="form-group"> | ||
<label for="email">Email address</label> | ||
<input type="email" id="email" name="email" required autocomplete="email"> | ||
<div class="form-error" id="emailError"></div> | ||
</div> | ||
|
||
<button type="submit" class="login-button" id="submitButton"> | ||
<div class="spinner"></div> | ||
<span>Send Reset Link</span> | ||
</button> | ||
|
||
<div class="form-error" id="submitError"></div> | ||
</div> | ||
|
||
<!-- Success message (hidden by default) --> | ||
<div id="successStep" class="registration-step"> | ||
<div class="success-message"> | ||
<svg class="success-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path> | ||
<polyline points="22 4 12 14.01 9 11.01"></polyline> | ||
</svg> | ||
<h2>Check your email</h2> | ||
<p>We've sent password reset instructions to your email address.</p> | ||
<button type="button" class="login-button" onclick="window.location.href='login.html'"> | ||
Return to Login | ||
</button> | ||
</div> | ||
</div> | ||
|
||
<div class="register-prompt"> | ||
Remember your password? <a href="/login.html">Sign in</a> | ||
</div> | ||
</form> | ||
</div> | ||
|
||
<script> | ||
const app = { | ||
elements: { | ||
form: document.getElementById('forgotPasswordForm'), | ||
email: document.getElementById('email'), | ||
submitButton: document.getElementById('submitButton'), | ||
emailError: document.getElementById('emailError'), | ||
submitError: document.getElementById('submitError'), | ||
requestStep: document.getElementById('requestStep'), | ||
successStep: document.getElementById('successStep') | ||
}, | ||
|
||
init() { | ||
this.setupEventListeners(); | ||
}, | ||
|
||
setupEventListeners() { | ||
this.elements.form.addEventListener('submit', (e) => this.handleSubmit(e)); | ||
|
||
// Clear errors on input | ||
this.elements.email.addEventListener('input', () => { | ||
this.elements.emailError.textContent = ''; | ||
this.elements.emailError.classList.remove('active'); | ||
}); | ||
}, | ||
|
||
async handleSubmit(e) { | ||
e.preventDefault(); | ||
|
||
// Reset errors | ||
this.clearErrors(); | ||
|
||
// Validate | ||
if (!this.validateForm()) { | ||
return; | ||
} | ||
|
||
// Show loading state | ||
this.setLoading(true); | ||
|
||
try { | ||
const response = await fetch('/api/auth/forgot-password', { | ||
method: 'POST', | ||
headers: { | ||
'Content-Type': 'application/json' | ||
}, | ||
body: JSON.stringify({ | ||
email: this.elements.email.value | ||
}) | ||
}); | ||
|
||
const data = await response.json(); | ||
|
||
if (data.success) { | ||
// Show success message | ||
this.elements.requestStep.classList.remove('active'); | ||
this.elements.successStep.classList.add('active'); | ||
} else { | ||
this.showError('submitError', data.error || 'Failed to send reset link'); | ||
} | ||
} catch (error) { | ||
this.showError('submitError', 'An error occurred. Please try again.'); | ||
} finally { | ||
this.setLoading(false); | ||
} | ||
}, | ||
|
||
validateForm() { | ||
let isValid = true; | ||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; | ||
|
||
if (!this.elements.email.value) { | ||
this.showError('emailError', 'Email is required'); | ||
isValid = false; | ||
} else if (!emailRegex.test(this.elements.email.value)) { | ||
this.showError('emailError', 'Please enter a valid email address'); | ||
isValid = false; | ||
} | ||
|
||
return isValid; | ||
}, | ||
|
||
showError(elementId, message) { | ||
const element = this.elements[elementId]; | ||
element.textContent = message; | ||
element.classList.add('active'); | ||
}, | ||
|
||
clearErrors() { | ||
['emailError', 'submitError'].forEach(id => { | ||
const element = this.elements[id]; | ||
element.textContent = ''; | ||
element.classList.remove('active'); | ||
}); | ||
}, | ||
|
||
setLoading(loading) { | ||
this.elements.submitButton.classList.toggle('loading', loading); | ||
this.elements.submitButton.disabled = loading; | ||
} | ||
}; | ||
|
||
// Initialize the app | ||
document.addEventListener('DOMContentLoaded', () => app.init()); | ||
</script> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -124,23 +124,50 @@ public function register($data) { | |
function sendVerification($data) { | ||
$verificationCode = random_int(100000, 999999); | ||
$verificationExpires = new MongoDB\BSON\UTCDateTime((time() + $this->config['verification_expire']) * 1000); | ||
|
||
$user = [ | ||
'email' => $data['email'], | ||
'personalInfo' => [ | ||
'email' => $data['email'] | ||
], | ||
'auth' => [ | ||
'verificationCode' => $verificationCode, | ||
'verificationExpires' => $verificationExpires, | ||
'verified' => false, | ||
'twoFactorEnabled' => false | ||
] | ||
]; | ||
|
||
$result = $this->db->users->insertOne($user); | ||
|
||
$all = $this->db->users->find(["email"=>$data['email']])->toArray(); | ||
if (isset($all) && count($all)) { | ||
$id = $all[0]->_id; | ||
} | ||
if ($id) { | ||
$user = [ | ||
'email' => $data['email'], | ||
'personalInfo' => [ | ||
'email' => $data['email'] | ||
], | ||
'auth' => [ | ||
'verificationCode' => $verificationCode, | ||
'verificationExpires' => $verificationExpires, | ||
'verified' => false, | ||
'twoFactorEnabled' => false | ||
] | ||
]; | ||
|
||
$result = $this->db->users->updateOne(['_id' => new MongoDB\BSON\ObjectId($id)], ['$set' => $user]); | ||
|
||
} else { | ||
$user = [ | ||
'email' => $data['email'], | ||
'personalInfo' => [ | ||
'email' => $data['email'] | ||
], | ||
'auth' => [ | ||
'verificationCode' => $verificationCode, | ||
'verificationExpires' => $verificationExpires, | ||
'verified' => false, | ||
'twoFactorEnabled' => false | ||
] | ||
]; | ||
|
||
$result = $this->db->users->insertOne($user); | ||
} | ||
$this->mail->sendVerification($data['email'], $verificationCode); | ||
return [ | ||
'success' => true, | ||
'message' => 'Registration successful. Please check your email for verification code.' | ||
]; | ||
|
||
|
||
} | ||
|
||
public function requestVerification($data) { | ||
|
@@ -341,6 +368,108 @@ private function generateTokens($userId) { | |
'expires' => $expire | ||
]; | ||
} | ||
/** | ||
* Generates a password reset token and sends reset email | ||
*/ | ||
public function handleForgotPassword($email) { | ||
global $db; | ||
|
||
// Validate email | ||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { | ||
return ['success' => false, 'error' => 'Invalid email format']; | ||
} | ||
|
||
// Check if user exists | ||
$stmt = $db->prepare("SELECT id, firstName FROM users WHERE email = ?"); | ||
$stmt->execute([$email]); | ||
$user = $stmt->fetch(PDO::FETCH_ASSOC); | ||
|
||
if (!$user) { | ||
// Return success even if user doesn't exist for security | ||
return ['success' => true]; | ||
} | ||
|
||
// Generate secure random token | ||
$token = bin2hex(random_bytes(32)); | ||
$expires = date('Y-m-d H:i:s', strtotime('+1 hour')); | ||
|
||
// Store reset token in database | ||
$stmt = $db->prepare("INSERT INTO password_resets (user_id, token, expires) VALUES (?, ?, ?)"); | ||
$stmt->execute([$user['id'], password_hash($token, PASSWORD_DEFAULT), $expires]); | ||
|
||
// Send reset email | ||
$resetLink = "https://app.thegivehub.com/reset-password.html?token=" . urlencode($token); | ||
|
||
$to = $email; | ||
$subject = "Reset Your Give Hub Password"; | ||
|
||
$message = " | ||
<html> | ||
<head> | ||
<title>Reset Your Password</title> | ||
</head> | ||
<body> | ||
<p>Hi {$user['firstName']},</p> | ||
<p>We received a request to reset your password for your Give Hub account.</p> | ||
<p>To reset your password, click the link below (or copy and paste it into your browser):</p> | ||
<p><a href=\"{$resetLink}\">{$resetLink}</a></p> | ||
<p>This link will expire in 1 hour.</p> | ||
<p>If you didn't request this password reset, you can safely ignore this email.</p> | ||
<p>Best regards,<br>The Give Hub Team</p> | ||
</body> | ||
</html> | ||
"; | ||
|
||
$headers = [ | ||
'MIME-Version: 1.0', | ||
'Content-type: text/html; charset=utf-8', | ||
'From: The Give Hub <[email protected]>', | ||
'Reply-To: [email protected]', | ||
'X-Mailer: PHP/' . phpversion() | ||
]; | ||
|
||
mail($to, $subject, $message, implode("\r\n", $headers)); | ||
|
||
return ['success' => true]; | ||
} | ||
|
||
/** | ||
* Validates reset token and updates password | ||
*/ | ||
public function resetPassword($token, $newPassword) { | ||
global $db; | ||
|
||
if (strlen($newPassword) < 8) { | ||
return ['success' => false, 'error' => 'Password must be at least 8 characters']; | ||
} | ||
|
||
// Find valid reset token | ||
$stmt = $db->prepare(" | ||
SELECT pr.user_id, pr.token, u.email | ||
FROM password_resets pr | ||
JOIN users u ON u.id = pr.user_id | ||
WHERE pr.expires > NOW() | ||
ORDER BY pr.created_at DESC | ||
LIMIT 1 | ||
"); | ||
$stmt->execute(); | ||
$reset = $stmt->fetch(PDO::FETCH_ASSOC); | ||
|
||
if (!$reset || !password_verify($token, $reset['token'])) { | ||
return ['success' => false, 'error' => 'Invalid or expired reset token']; | ||
} | ||
|
||
// Update password | ||
$hashedPassword = password_hash($newPassword, PASSWORD_DEFAULT); | ||
$stmt = $db->prepare("UPDATE users SET password = ? WHERE id = ?"); | ||
$stmt->execute([$hashedPassword, $reset['user_id']]); | ||
|
||
// Delete used reset token | ||
$stmt = $db->prepare("DELETE FROM password_resets WHERE user_id = ?"); | ||
$stmt->execute([$reset['user_id']]); | ||
|
||
return ['success' => true]; | ||
} | ||
} | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters