Skip to content

Commit

Permalink
updated google auth workflow; added forgot password functionality and…
Browse files Browse the repository at this point in the history
… page
  • Loading branch information
chrisrobison committed Nov 19, 2024
1 parent c058adb commit fc77a5f
Show file tree
Hide file tree
Showing 4 changed files with 328 additions and 21 deletions.
26 changes: 21 additions & 5 deletions auth.php
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
<?php

$req = json_encode($_REQUEST);
file_put_contents("google-auth.log", $req, FILE_APPEND);
$req = json_encode($_REQUEST, JSON_PRETTY_PRINT);
file_put_contents("google-auth.log", $req."\n", FILE_APPEND);

require __DIR__ . '/lib/Auth.php';
require __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/lib/db.php';
require_once __DIR__ . '/lib/User.php';

use League\OAuth2\Client\Provider\Google;

Expand Down Expand Up @@ -56,10 +59,23 @@
// We got an access token, let's now get the owner details
$ownerDetails = $provider->getResourceOwner($token);

$out = ["firstName"=>$ownerDetails->getFirstName(), "lastName"=>$ownerDetails->getLastName(), "email"=>$ownerDetails->getEmail(), "language"=>"en", "password"=>$token->getToken(), "googleToken"=>$token->getToken(), "verified"=>true ];

$out = ["firstName"=>$ownerDetails->getFirstName(), "lastName"=>$ownerDetails->getLastName(), "email"=>$ownerDetails->getEmail(), "avatar"=>$ownerDetails->getAvatar(), "language"=>"en", "password"=>$token->getToken(), "googleToken"=>$token->getToken(), "verified"=>true ];

$_SESSION['googleProfile'] = $out;
$_SESSION['token'] = $token->getToken();

$db = new User();
$id = $db->findId(["email"=>$out['email']]);

$user = $db->get($id);
$_SESSION['user'] = $user;

if ($token->getExpires() < time()) {
$token->getRefreshToken();
}

// Use these details to create a new profile
header("Location: /index.html");
//header("Location: /index.html");

} catch (Exception $e) {

Expand Down
162 changes: 162 additions & 0 deletions forgot-password.html
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>
159 changes: 144 additions & 15 deletions lib/Auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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];
}
}


2 changes: 1 addition & 1 deletion lib/Mailer.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@ public function sendVerification($email, $code) {

file_put_contents(__DIR__."/x.log", $cmd."\n---\n".$msg."\n---\n", FILE_APPEND);

print $send;
// print $send;
}
}

0 comments on commit fc77a5f

Please sign in to comment.