I'm in a rush to release so I am adding features that are needed to make it usable.

This commit is contained in:
Cody Cook 2025-02-17 22:03:33 -08:00
commit 4c2857b445
25 changed files with 2475 additions and 3475 deletions

View file

@ -139,7 +139,19 @@ class DJ
private function loadDJMixes(): void
{
// Determine if the current user is an admin.
$isAdmin = false;
if (isset($_SESSION['user']) && isset($_SESSION['user']['role']) && $_SESSION['user']['role'] === 'admin') {
$isAdmin = true;
}
if ($isAdmin) {
$stmt = $this->db->prepare("SELECT * FROM mix WHERE dj1 = ? OR dj2 = ? OR dj3 = ?");
} else {
// Only return mixes that are approved (pending = 0) for non-admin users.
$stmt = $this->db->prepare("SELECT * FROM mix WHERE (dj1 = ? OR dj2 = ? OR dj3 = ?) AND pending = 0");
}
$stmt->bind_param("iii", $this->id, $this->id, $this->id);
$stmt->execute();
$result = $stmt->get_result();
@ -149,10 +161,9 @@ class DJ
}
$stmt->close();
$this->mixes = $mixes;
}
private function loadBySlug(): bool
{
$socials = [];

View file

@ -57,7 +57,15 @@ class Mix
private function get_mix_by_id()
{
// Check if current user is admin
$isAdmin = (isset($_SESSION['user']) && isset($_SESSION['user']['role']) && $_SESSION['user']['role'] === 'admin');
if ($isAdmin) {
$stmt = $this->db->prepare("SELECT * FROM mix WHERE id = ?");
} else {
// Only return approved mixes (pending = 0) for non-admins
$stmt = $this->db->prepare("SELECT * FROM mix WHERE id = ? AND pending = 0");
}
$stmt->bind_param("i", $this->id);
$stmt->execute();
$result = $stmt->get_result();
@ -65,7 +73,6 @@ class Mix
$stmt->close();
return $mix;
}
/**
* @param $mix
* @return true
@ -213,7 +220,14 @@ class Mix
private function get_mix_by_slug()
{
// Check if current user is admin
$isAdmin = (isset($_SESSION['user']) && isset($_SESSION['user']['role']) && $_SESSION['user']['role'] === 'admin');
if ($isAdmin) {
$stmt = $this->db->prepare("SELECT * FROM mix WHERE slug = ?");
} else {
$stmt = $this->db->prepare("SELECT * FROM mix WHERE slug = ? AND pending = 0");
}
$stmt->bind_param("s", $this->slug);
$stmt->execute();
$result = $stmt->get_result();

View file

@ -61,8 +61,116 @@ Class User{
return $user;
}
public function login(mixed $username, mixed $password)
public function login($email, $password)
{
// Retrieve user record by email
$stmt = $this->db->prepare("SELECT * FROM users WHERE email = ?");
$stmt->bind_param("s", $email);
$stmt->execute();
$result = $stmt->get_result();
$user_data = $result->fetch_assoc();
$stmt->close();
// Check login_attempts table for lockout status
$stmt = $this->db->prepare("SELECT * FROM login_attempts WHERE email = ?");
$stmt->bind_param("s", $email);
$stmt->execute();
$attempt_data = $stmt->get_result()->fetch_assoc();
$stmt->close();
$current_time = new \DateTime();
if ($attempt_data && !empty($attempt_data['lockout_until'])) {
$lockout_until = new \DateTime($attempt_data['lockout_until']);
if ($current_time < $lockout_until) {
return "Account locked until " . $lockout_until->format('Y-m-d H:i:s') . ". Please try again later.";
}
}
// If no user record found, still update login_attempts to mitigate enumeration issues
if (!$user_data) {
$this->updateFailedAttempt($email);
return "Invalid email or password.";
}
// Verify the password using password_verify
if (password_verify($password, $user_data['password'])) {
// Successful login clear login attempts and set session variables
$this->resetLoginAttempts($email);
$_SESSION['user'] = [
'id' => $user_data['id'],
'email' => $user_data['email'],
'username' => $user_data['username'],
'role' => $user_data['isAdmin'] ? 'admin' : 'user'
];
return true;
} else {
$attempts = $this->updateFailedAttempt($email);
return "Invalid email or password. Attempt $attempts of 3.";
}
}
/**
* Update (or create) a record in the login_attempts table for a failed attempt.
* If attempts reach 3, set a lockout that doubles each time.
* Returns the current number of attempts.
*/
private function updateFailedAttempt($email)
{
// Check for an existing record
$stmt = $this->db->prepare("SELECT * FROM login_attempts WHERE email = ?");
$stmt->bind_param("s", $email);
$stmt->execute();
$record = $stmt->get_result()->fetch_assoc();
$stmt->close();
$current_time = new \DateTime();
if ($record) {
$attempts = $record['attempts'] + 1;
$lockouts = $record['lockouts'];
if ($attempts >= 3) {
// Increment lockouts and calculate the new lockout duration:
// Duration in minutes = 30 * 2^(lockouts)
$lockouts++;
$duration = 30 * pow(2, $lockouts - 1);
$lockout_until = clone $current_time;
$lockout_until->modify("+{$duration} minutes");
// Reset attempts to 0 on lockout
$attempts = 0;
$stmt = $this->db->prepare("UPDATE login_attempts SET attempts = ?, lockouts = ?, last_attempt = NOW(), lockout_until = ? WHERE email = ?");
$lockout_until_str = $lockout_until->format('Y-m-d H:i:s');
$stmt->bind_param("iiss", $attempts, $lockouts, $lockout_until_str, $email);
$stmt->execute();
$stmt->close();
} else {
$stmt = $this->db->prepare("UPDATE login_attempts SET attempts = ?, last_attempt = NOW() WHERE email = ?");
$stmt->bind_param("is", $attempts, $email);
$stmt->execute();
$stmt->close();
}
return $attempts;
} else {
// Create a new record for this email
$attempts = 1;
$lockouts = 0;
$stmt = $this->db->prepare("INSERT INTO login_attempts (email, attempts, lockouts, last_attempt) VALUES (?, ?, ?, NOW())");
$stmt->bind_param("sii", $email, $attempts, $lockouts);
$stmt->execute();
$stmt->close();
return $attempts;
}
}
/**
* Reset the login_attempts record for the given email.
*/
private function resetLoginAttempts($email)
{
$stmt = $this->db->prepare("DELETE FROM login_attempts WHERE email = ?");
$stmt->bind_param("s", $email);
$stmt->execute();
$stmt->close();
}

View file

@ -15,7 +15,8 @@
"yosymfony/toml": "*",
"ext-mysqli": "*",
"ext-curl": "*",
"phpunit/phpunit": ">8.5.1.0"
"phpunit/phpunit": ">8.5.1.0",
"aws/aws-sdk-php": "*"
},
"autoload": {
"psr-4": {

14
dj.php
View file

@ -233,15 +233,11 @@ require_once 'includes/header.php';
$utm_params = '?utm_source=website&utm_medium=share_modal&utm_campaign=sharing';
$share_url = urlencode($url . $utm_params);
?>
<a href="#" id="copyLinkBtn" class="btn btn-secondary w-100 mb-2">Copy URL</a>
<a href="#" id="copyLinkBtn" class="btn btn-secondary w-100 mb-2"><?php echo $locale['copyurl'];?></a>
<a href="https://www.facebook.com/sharer/sharer.php?u=<?php echo $share_url; ?>"
target="_blank" class="btn btn-primary w-100 mb-2" onclick="hideModal()">Share to
Facebook</a>
target="_blank" class="btn btn-primary w-100 mb-2" onclick="hideModal()"><?php echo $locale['sharetofb'];?></a>
<a href="https://twitter.com/intent/tweet?url=<?php echo $share_url; ?>"
target="_blank" class="btn btn-info w-100 mb-2" onclick="hideModal()">Share to
Twitter</a>
<a href="https://www.instagram.com/" target="_blank" class="btn btn-danger w-100"
onclick="hideModal()">Share to Instagram</a>
target="_blank" class="btn btn-info w-100 mb-2" onclick="hideModal()"><?php echo $locale['sharetotwitter'];?></a>
</div>
</div>
</div>
@ -260,10 +256,10 @@ require_once 'includes/header.php';
copyLinkBtn.onclick = function () {
navigator.clipboard.writeText(urlToCopy).then(function () {
alert('URL copied to clipboard');
alert('<?php echo $locale['urlcopiedtoclipboard'];?>');
shareModal.hide();
}, function (err) {
alert('Failed to copy URL: ' + err);
alert('<?php echo $locale['failedtocopyurl'];?>: ' + err);
});
}

121
forgot-password.php Normal file
View file

@ -0,0 +1,121 @@
<?php
session_start();
require_once 'includes/globals.php';
require_once 'vendor/autoload.php';
use DJMixHosting\Database;
use Aws\Ses\SesClient;
use Aws\Exception\AwsException;
if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_POST['email'])) {
$email = trim($_POST['email']);
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$_SESSION['error'] = "Invalid email format.";
header("Location: forgot-password.php");
exit;
}
$db = new Database($config);
// Check if email exists in the system
$stmt = $db->prepare("SELECT id, username FROM users WHERE email = ?");
$stmt->bind_param("s", $email);
$stmt->execute();
$result = $stmt->get_result();
$userData = $result->fetch_assoc();
$stmt->close();
// Always show a success message (to avoid disclosing which emails are registered)
$_SESSION['success'] = "If the email exists in our system, a password reset link has been sent.";
if ($userData) {
$user_id = $userData['id'];
// Generate a password reset verification code valid for 15 minutes
$verification_code = bin2hex(random_bytes(16));
$expires_at = date("Y-m-d H:i:s", strtotime("+15 minutes"));
// Insert a record with purpose 'password_reset'
$stmt = $db->prepare("REPLACE INTO email_verifications (user_id, email, verification_code, expires_at, purpose) VALUES (?, ?, ?, ?, 'password_reset')");
$stmt->bind_param("isss", $user_id, $email, $verification_code, $expires_at);
$stmt->execute();
$stmt->close();
// Send password reset email via AWS SES
$sesClient = new SesClient([
'version' => 'latest',
'region' => $config['aws']['ses']['region'],
'credentials' => [
'key' => $config['aws']['ses']['access_key'],
'secret' => $config['aws']['ses']['secret_key'],
]
]);
$sender_email = $config['aws']['ses']['sender_email'];
$recipient_email = $email;
$subject = "Password Reset Request";
$reset_link = $config['app']['url'] . "/password-reset.php?code={$verification_code}";
$body_text = "You have requested to reset your password. Please click the link below to reset your password:\n\n";
$body_text .= "{$reset_link}\n\nIf you did not request this, please ignore this email. This link will expire in 15 minutes.";
try {
$result = $sesClient->sendEmail([
'Destination' => [
'ToAddresses' => [$recipient_email],
],
'ReplyToAddresses' => [$sender_email],
'Source' => $sender_email,
'Message' => [
'Body' => [
'Text' => [
'Charset' => 'UTF-8',
'Data' => $body_text,
],
],
'Subject' => [
'Charset' => 'UTF-8',
'Data' => $subject,
],
],
]);
} catch (AwsException $e) {
// Optionally log the error without disclosing details to the user.
}
}
header("Location: forgot-password.php");
exit;
}
require_once 'includes/header.php';
?>
<section class="forgot-password-section py-5">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-5">
<?php
if(isset($_SESSION['error'])) {
echo '<div class="alert alert-danger alert-dismissible fade show mb-4" role="alert">' . htmlspecialchars($_SESSION['error']) . '<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>';
unset($_SESSION['error']);
}
if(isset($_SESSION['success'])) {
echo '<div class="alert alert-success alert-dismissible fade show mb-4" role="alert">' . htmlspecialchars($_SESSION['success']) . '<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>';
unset($_SESSION['success']);
}
?>
<div class="card shadow-sm border-0">
<div class="card-body p-4">
<h3 class="text-center mb-4">Forgot Password</h3>
<form action="forgot-password.php" method="post" class="needs-validation" novalidate>
<div class="mb-3">
<label for="email" class="form-label">Enter your email address</label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<button type="submit" class="btn btn-primary w-100">Submit</button>
</form>
</div>
</div>
</div>
</div>
</div>
</section>
<?php require_once 'includes/footer.php'; ?>

View file

@ -156,15 +156,11 @@ require_once 'includes/header.php';
$utm_params = '?utm_source=website&utm_medium=share_modal&utm_campaign=sharing';
$share_url = urlencode($url . $utm_params);
?>
<a href="#" id="copyLinkBtn" class="btn btn-secondary w-100 mb-2">Copy URL</a>
<a href="#" id="copyLinkBtn" class="btn btn-secondary w-100 mb-2"><?php echo $locale['copyurl'];?></a>
<a href="https://www.facebook.com/sharer/sharer.php?u=<?php echo $share_url; ?>"
target="_blank" class="btn btn-primary w-100 mb-2" onclick="hideModal()">Share to
Facebook</a>
target="_blank" class="btn btn-primary w-100 mb-2" onclick="hideModal()"><?php echo $locale['sharetofb'];?></a>
<a href="https://twitter.com/intent/tweet?url=<?php echo $share_url; ?>"
target="_blank" class="btn btn-info w-100 mb-2" onclick="hideModal()">Share to
Twitter</a>
<a href="https://www.instagram.com/" target="_blank" class="btn btn-danger w-100"
onclick="hideModal()">Share to Instagram</a>
target="_blank" class="btn btn-dark w-100 mb-2" onclick="hideModal()"><?php echo $locale['sharetotwitter'];?></a>
</div>
</div>
</div>
@ -183,10 +179,10 @@ require_once 'includes/header.php';
copyLinkBtn.onclick = function () {
navigator.clipboard.writeText(urlToCopy).then(function () {
alert('URL copied to clipboard');
alert('<?php echo $locale['urlcopiedtoclipboard'];?>');
shareModal.hide();
}, function (err) {
alert('Failed to copy URL: ' + err);
alert('<?php echo $locale['failedtocopyurl'];?>: ' + err);
});
}

4
includes/.htaccess Normal file
View file

@ -0,0 +1,4 @@
<Files "config.toml">
Order allow,deny
Deny from all
</Files>

View file

@ -11,3 +11,8 @@ if (isset($_GET['lang'])) {
}
$locale = loadLocale($lang);
function langPrintString($string, $lang)
{
echo $lang[$string];
}

View file

@ -65,7 +65,12 @@ $current_lang = $_SESSION['lang'] ?? $config['app']['locale'];
<select class="form-select" id="languageSelect"
onchange="location = this.value;">
<?php
$currentUrl = strtok($_SERVER["REQUEST_URI"], '?');
if (isset($_SERVER["REQUIEST_URI"])) {
$currentUrl = strtok($_SERVER["REQUEST_URI"], '?') ?? '/';
} else {
$currentUrl = '/';
}
$queryParams = $_GET;
foreach ($languages as $key => $value) {
$queryParams['lang'] = $key;

View file

@ -67,5 +67,8 @@ return [
"sharetofb" => "Share to Facebook",
"sharetoig" => "Share to Instagram",
"copyurl" => "Copy URL",
"urlcopiedtoclipboard" => "URL copied to clipboard",
"failedtocopyurl" => "Failed to copy URL",
"name" => "Name",
"username" => "Username",
];

153
login.php
View file

@ -1,66 +1,149 @@
<?php
require_once 'includes/globals.php';
if (isset($_SESSION['user'])) {
header("Location: /profile");
exit;
}
require_once 'vendor/autoload.php';
use DJMixHosting\Database;
use DJMixHosting\User;
// Generate a CSRF token if one is not set
if (!isset($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
$title = $locale['home'];
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
if (isset($_POST['email'], $_POST['password'], $_POST['csrf_token'])) {
if (isset($_POST['username']) && isset($_POST['password'])) {
$username = $_POST['username'];
// Check the CSRF token
if ($_POST['csrf_token'] !== $_SESSION['csrf_token']) {
$_SESSION['error'] = $locale['message'] . ": Invalid form submission. Please try again.";
} else {
$email = $_POST['email'];
$password = $_POST['password'];
$db = new Database($config);
$user = new User($db);
$user->login($username, $password);
$result = $user->login($email, $password);
if ($result === true) {
// Successful login, redirect to profile page
header("Location: profile.php");
exit;
} else {
// Set error message from login method (includes lockout messages)
$_SESSION['error'] = $result;
}
}
require_once 'includes/header.php'; ?>
<section>
<div class="container py-5">
<div class="row">
<div class="col">
<nav aria-label="breadcrumb" class="bg-body-tertiary rounded-3 p-3 mb-4">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="/"><?php echo $locale['home']; ?></a></li>
<li class="breadcrumb-item active"><a href="/login.php"><?php echo $locale['login']; ?></a>
</li>
</ol>
</nav>
</div>
</div>
</div>
</section>
<section>
<div class="container py-5">
<div class="row">
<div class="col-lg-6 offset-lg-3">
<div class="card">
<div class="card-body">
<h5 class="card-title"><?php echo $locale['login']; ?></h5>
<form action="login.php" method="post">
}
}
require_once 'includes/header.php';
?>
<section class="login-section py-5">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-5">
<?php
if (isset($_SESSION['error'])) {
echo '<div class="alert alert-danger alert-dismissible fade show mb-4" role="alert">
' . htmlspecialchars($_SESSION['error']) . '
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>';
unset($_SESSION['error']);
}
?>
<div class="card shadow-sm border-0">
<div class="card-body p-4">
<h3 class="text-center mb-4">Login</h3>
<form action="login.php" method="post" class="needs-validation" novalidate>
<div class="mb-3">
<label for="username" class="form-label"><?php echo $locale['username']; ?></label>
<input type="text" class="form-control" id="username" name="username" required>
<label for="email" class="form-label">Email</label>
<input type="email" class="form-control form-control-lg" id="email" name="email" required>
</div>
<div class="mb-3">
<label for="password" class="form-label"><?php echo $locale['password']; ?></label>
<input type="password" class="form-control" id="password" name="password" required>
<label for="password" class="form-label">Password</label>
<div class="input-group">
<input type="password" class="form-control form-control-lg" id="password" name="password" required>
<button class="btn btn-outline-secondary px-3" type="button" id="togglePassword"
style="border-left: none;">
<i class="bi bi-eye-fill fs-5"></i>
</button>
</div>
</div>
<div class="mb-4 form-check">
<input type="checkbox" class="form-check-input" id="rememberMe" name="remember_me">
<label class="form-check-label" for="rememberMe">Remember me</label>
</div>
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($_SESSION['csrf_token']); ?>">
<button type="submit" class="btn btn-primary w-100 btn-lg mb-3">Login</button>
<div class="text-center">
<a href="forgot-password.php" class="text-decoration-none">Forgot password?</a>
</div>
<button type="submit" class="btn btn-primary"><?php echo $locale['login']; ?></button>
</form>
</div>
</div>
<div class="text-center mt-4">
<p class="mb-0">Don't have an account? <a href="register.php" class="text-decoration-none">Sign up</a></p>
</div>
</div>
</div>
</div>
</section>
<style>
.login-section {
min-height: calc(100vh - 200px); /* Adjust based on your header/footer height */
}
.form-control:focus {
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.15);
}
#togglePassword:hover {
background-color: #f8f9fa;
}
#togglePassword:focus {
box-shadow: none;
}
.input-group .form-control:focus {
border-right: 1px solid #86b7fe;
}
.input-group .btn {
border-left: 0;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Form validation
const form = document.querySelector('.needs-validation');
form.addEventListener('submit', function(event) {
if (!form.checkValidity()) {
event.preventDefault();
event.stopPropagation();
}
form.classList.add('was-validated');
}, false);
// Password visibility toggle with improved UX
const togglePassword = document.querySelector('#togglePassword');
const password = document.querySelector('#password');
togglePassword.addEventListener('click', function() {
const type = password.getAttribute('type') === 'password' ? 'text' : 'password';
password.setAttribute('type', type);
this.querySelector('i').classList.toggle('bi-eye-fill');
this.querySelector('i').classList.toggle('bi-eye-slash-fill');
});
});
</script>
<?php require_once 'includes/footer.php'; ?>

28
mix.php
View file

@ -39,7 +39,7 @@ require_once 'includes/header.php'; ?>
<nav aria-label="breadcrumb" class="bg-body-tertiary rounded-3 p-3 mb-4">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="/"><?php echo $locale['home']; ?></a></li>
<li class="breadcrumb-item"><a href="/mix"><?php echo $locale['mixes']; ?></a></li>
<li class="breadcrumb-item"><?php echo $locale['mixes']; ?></li>
<li class="breadcrumb-item active"
aria-current="page"><?php
if (isset($mix) && $mix->get_name() != "") {
@ -76,7 +76,7 @@ require_once 'includes/header.php'; ?>
<div class="card-body bg-body-secondary text-center">
<?php
if ($mix->is_download_only()) {
echo "<a href='/mix/" . $mix->get_slug() . "/download" . "' class='btn btn-primary'>" . $locale['download'] . "</a>";
echo "<a href='/mix/" . $mix->get_slug() . "/download" . "' class='btn btn-primary w-100 mb-2'>" . $locale['download'] . "</a>";
} else {
?>
<div id="audio-player">
@ -91,12 +91,7 @@ require_once 'includes/header.php'; ?>
<span id="current-time">0:00</span> / <span id="duration">0:00</span>
</div>
</div>
<?php
}
?>
<?php } ?>
</div>
</div>
<div class="card mb-4">
@ -398,14 +393,14 @@ require_once 'includes/header.php'; ?>
$utm_params = '?utm_source=website&utm_medium=share_modal&utm_campaign=sharing';
$share_url = urlencode($url . $utm_params);
?>
<a href="#" id="copyLinkBtn" class="btn btn-secondary w-100 mb-2">Copy URL</a>
<a href="#" id="copyLinkBtn"
class="btn btn-secondary w-100 mb-2"><?php echo $locale['copyurl']; ?></a>
<a href="https://www.facebook.com/sharer/sharer.php?u=<?php echo $share_url; ?>"
target="_blank" class="btn btn-primary w-100 mb-2" onclick="hideModal()">Share to
Facebook</a>
target="_blank" class="btn btn-primary w-100 mb-2"
onclick="hideModal()"><?php echo $locale['sharetofb']; ?></a>
<a href="https://twitter.com/intent/tweet?url=<?php echo $share_url; ?>"
target="_blank" class="btn btn-dark w-100 mb-2" onclick="hideModal()">Share to X (formerly Twitter)</a>
<a href="https://www.instagram.com/" target="_blank" class="btn btn-danger w-100"
onclick="hideModal()">Share to Instagram</a>
target="_blank" class="btn btn-dark w-100 mb-2"
onclick="hideModal()"><?php echo $locale['sharetotwitter']; ?></a>
</div>
</div>
</div>
@ -426,10 +421,10 @@ require_once 'includes/header.php'; ?>
copyLinkBtn.onclick = function () {
navigator.clipboard.writeText(urlToCopy).then(function () {
alert('URL copied to clipboard');
alert('<?php echo $locale['urlcopiedtoclipboard'];?>');
shareModal.hide();
}, function (err) {
alert('Failed to copy URL: ' + err);
alert('<?php echo $locale['failedtocopyurl'];?>: ' + err);
});
}
@ -572,7 +567,6 @@ require_once 'includes/header.php'; ?>
</script>
<?php else: ?>
<div class="row">
<div class="col">

View file

@ -177,14 +177,11 @@ require_once 'includes/header.php';
$utm_params = '?utm_source=website&utm_medium=share_modal&utm_campaign=sharing';
$share_url = urlencode($url . $utm_params);
?>
<a href="#" id="copyLinkBtn" class="btn btn-secondary w-100 mb-2">Copy URL</a>
<a href="#" id="copyLinkBtn" class="btn btn-secondary w-100 mb-2"><?php echo $locale['copyurl'];?></a>
<a href="https://www.facebook.com/sharer/sharer.php?u=<?php echo $share_url; ?>"
target="_blank" class="btn btn-primary w-100 mb-2" onclick="hideModal()">Share to
Facebook</a>
target="_blank" class="btn btn-primary w-100 mb-2" onclick="hideModal()"><?php echo $locale['sharetofb'];?></a>
<a href="https://twitter.com/intent/tweet?url=<?php echo $share_url; ?>"
target="_blank" class="btn btn-info w-100 mb-2" onclick="hideModal()">Share to Twitter</a>
<a href="https://www.instagram.com/" target="_blank" class="btn btn-danger w-100"
onclick="hideModal()">Share to Instagram</a>
target="_blank" class="btn btn-dark w-100 mb-2" onclick="hideModal()"><?php echo $locale['sharetotwitter'];?></a>
</div>
</div>
</div>
@ -205,10 +202,10 @@ require_once 'includes/header.php';
copyLinkBtn.onclick = function () {
navigator.clipboard.writeText(urlToCopy).then(function () {
alert('URL copied to clipboard');
alert('<?php echo $locale['urlcopiedtoclipboard'];?>');
shareModal.hide();
}, function (err) {
alert('Failed to copy URL: ' + err);
alert('<?php echo $locale['failedtocopyurl'];?>: ' + err);
});
}

124
password-reset.php Normal file
View file

@ -0,0 +1,124 @@
<?php
session_start();
require_once 'includes/globals.php';
require_once 'vendor/autoload.php';
use DJMixHosting\Database;
$db = new Database($config);
$verification_code = "";
if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['code'])) {
$verification_code = trim($_GET['code']);
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$verification_code = trim($_POST['verification_code'] ?? '');
$username = trim($_POST['username'] ?? '');
$new_password = $_POST['new_password'] ?? '';
$confirm_password = $_POST['confirm_password'] ?? '';
if (empty($verification_code) || empty($username) || empty($new_password) || empty($confirm_password)) {
$_SESSION['error'] = "All fields are required.";
header("Location: password-reset.php?code=" . urlencode($verification_code));
exit;
}
if ($new_password !== $confirm_password) {
$_SESSION['error'] = "Passwords do not match.";
header("Location: password-reset.php?code=" . urlencode($verification_code));
exit;
}
// Look up the password reset record (purpose 'password_reset')
$stmt = $db->prepare("SELECT * FROM email_verifications WHERE verification_code = ? AND purpose = 'password_reset'");
$stmt->bind_param("s", $verification_code);
$stmt->execute();
$result = $stmt->get_result();
$record = $result->fetch_assoc();
$stmt->close();
if (!$record) {
$_SESSION['error'] = "Invalid or expired password reset code.";
header("Location: password-reset.php?code=" . urlencode($verification_code));
exit;
}
// Check expiration
$current_time = new DateTime();
$expires_at = new DateTime($record['expires_at']);
if ($current_time > $expires_at) {
$_SESSION['error'] = "Password reset code has expired.";
header("Location: password-reset.php?code=" . urlencode($verification_code));
exit;
}
// Verify the username matches the record
$stmt = $db->prepare("SELECT id, username FROM users WHERE id = ? AND username = ?");
$stmt->bind_param("is", $record['user_id'], $username);
$stmt->execute();
$userData = $stmt->get_result()->fetch_assoc();
$stmt->close();
if (!$userData) {
$_SESSION['error'] = "Username does not match our records.";
header("Location: password-reset.php?code=" . urlencode($verification_code));
exit;
}
// Update the user's password
$hashed_password = password_hash($new_password, PASSWORD_DEFAULT);
$stmt = $db->prepare("UPDATE users SET password = ? WHERE id = ?");
$stmt->bind_param("si", $hashed_password, $userData['id']);
$stmt->execute();
$stmt->close();
// Remove the password reset record
$stmt = $db->prepare("DELETE FROM email_verifications WHERE verification_code = ? AND purpose = 'password_reset'");
$stmt->bind_param("s", $verification_code);
$stmt->execute();
$stmt->close();
$_SESSION['success'] = "Your password has been reset successfully. Please log in with your new password.";
header("Location: login.php");
exit;
}
require_once 'includes/header.php';
?>
<section class="password-reset-section py-5">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-5">
<?php
if(isset($_SESSION['error'])) {
echo '<div class="alert alert-danger alert-dismissible fade show mb-4" role="alert">' . htmlspecialchars($_SESSION['error']) . '<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>';
unset($_SESSION['error']);
}
?>
<div class="card shadow-sm border-0">
<div class="card-body p-4">
<h3 class="text-center mb-4">Reset Password</h3>
<form action="password-reset.php" method="post" class="needs-validation" novalidate>
<input type="hidden" name="verification_code" value="<?php echo htmlspecialchars($verification_code); ?>">
<div class="mb-3">
<label for="username" class="form-label">Enter your username</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="new_password" class="form-label">New Password</label>
<input type="password" class="form-control" id="new_password" name="new_password" required>
</div>
<div class="mb-3">
<label for="confirm_password" class="form-label">Confirm New Password</label>
<input type="password" class="form-control" id="confirm_password" name="confirm_password" required>
</div>
<button type="submit" class="btn btn-primary w-100">Reset Password</button>
</form>
</div>
</div>
</div>
</div>
</div>
</section>
<?php require_once 'includes/footer.php'; ?>

File diff suppressed because one or more lines are too long

278
profile.php Normal file
View file

@ -0,0 +1,278 @@
<?php
// profile.php
require_once 'includes/globals.php';
require_once 'vendor/autoload.php';
// Make sure the user is authenticated; otherwise redirect to login.
if (!isset($_SESSION['user'])) {
header("Location: login.php");
exit;
}
use DJMixHosting\Database;
$db = new Database($config);
// Retrieve the full user record from the database
$userId = $_SESSION['user']['id'];
$stmt = $db->prepare("SELECT * FROM users WHERE id = ?");
$stmt->bind_param("i", $userId);
$stmt->execute();
$result = $stmt->get_result();
$userData = $result->fetch_assoc();
$stmt->close();
// Determine if editing should be disabled (if email not verified)
$editingDisabled = (int)$userData['emailVerified'] !== 1; // Assuming 1 means verified
// Optionally, set a flag for showing an alert to verify email.
$alertMessage = "";
if ($editingDisabled) {
$alertMessage = "Please verify your email to enable profile editing.";
}
require_once 'includes/header.php';
?>
<section class="container py-5">
<?php if (!empty($alertMessage)): ?>
<div class="alert alert-warning" role="alert">
<?php echo $alertMessage; ?>
</div>
<?php endif; ?>
<div class="row">
<!-- Left Sidebar: Profile Picture and Controls -->
<div class="col-lg-4">
<div class="card mb-4">
<div class="card-body bg-body-secondary text-center">
<img src="<?php echo htmlspecialchars($userData['img'] ?: 'default_profile.png'); ?>"
alt="avatar"
class="rounded-circle img-fluid" style="width: 150px;">
<!-- Remove username from here -->
<button type="button" class="btn btn-sm btn-secondary mb-2"
<?php echo ($editingDisabled) ? 'disabled' : ''; ?>
data-bs-toggle="modal" data-bs-target="#profilePictureModal">
Change Picture
</button>
</div>
</div>
<!-- List group for username, email, name, and password -->
<div class="list-group mb-4">
<div class="list-group-item d-flex justify-content-between align-items-center">
<span><?php echo $locale['username']; ?>: <?php echo htmlspecialchars($userData['username']); ?></span>
<button type="button" class="btn btn-sm btn-secondary"
<?php echo ($editingDisabled) ? 'disabled' : ''; ?>
data-bs-toggle="modal" data-bs-target="#usernameModal">
Change
</button>
</div>
<div class="list-group-item d-flex justify-content-between align-items-center">
<span><?php echo $locale['email']; ?>: <?php echo htmlspecialchars($userData['email']); ?></span>
<div>
<button type="button" class="btn btn-sm btn-secondary me-1"
<?php echo ($editingDisabled) ? 'disabled' : ''; ?>
data-bs-toggle="modal" data-bs-target="#emailModal">
Change
</button>
<?php if (!$userData['emailVerified']): ?>
<button type="button" class="btn btn-sm btn-primary"
data-bs-toggle="modal" data-bs-target="#verifyEmailModal">
Verify
</button>
<?php endif; ?>
</div>
</div>
<div class="list-group-item d-flex justify-content-between align-items-center">
<span><?php echo $locale['name']; ?>: <?php echo htmlspecialchars($userData['firstName'] . ' ' . $userData['lastName']); ?></span>
<button type="button" class="btn btn-sm btn-secondary"
<?php echo ($editingDisabled) ? 'disabled' : ''; ?>
data-bs-toggle="modal" data-bs-target="#nameModal">
Change Name
</button>
</div>
<div class="list-group-item d-flex justify-content-between align-items-center">
<span>Password</span>
<button type="button" class="btn btn-sm btn-secondary"
<?php echo ($editingDisabled) ? 'disabled' : ''; ?>
data-bs-toggle="modal" data-bs-target="#passwordModal">
Change
</button>
</div>
</div>
</div>
<!-- Right Content: Additional Options -->
<div class="col-lg-8">
<div class="card mb-4">
<div class="card-body bg-body-secondary">
<h5>Additional Features</h5>
<p>Followed DJs and recent ratings will appear here once implemented.</p>
</div>
</div>
</div>
</div>
</section>
<!-- Modals -->
<!-- 1. Profile Picture Modal -->
<div class="modal fade" id="profilePictureModal" tabindex="-1" aria-labelledby="profilePictureModalLabel" aria-hidden="true">
<div class="modal-dialog">
<form action="update_profile_picture.php" method="post" enctype="multipart/form-data">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="profilePictureModalLabel">Change Profile Picture</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<!-- CSRF token can be added here as hidden input if needed -->
<div class="mb-3">
<label for="profilePicture" class="form-label">Select new profile picture</label>
<input type="file" class="form-control" id="profilePicture" name="profile_picture" accept="image/*" required>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Update Picture</button>
</div>
</div>
</form>
</div>
</div>
<!-- 2. Username Modal -->
<div class="modal fade" id="usernameModal" tabindex="-1" aria-labelledby="usernameModalLabel" aria-hidden="true">
<div class="modal-dialog">
<form action="update_username.php" method="post">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="usernameModalLabel">Change Username</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="newUsername" class="form-label">New Username</label>
<input type="text" class="form-control" id="newUsername" name="new_username" required>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Update Username</button>
</div>
</div>
</form>
</div>
</div>
<!-- 3. Email Modal -->
<div class="modal fade" id="emailModal" tabindex="-1" aria-labelledby="emailModalLabel" aria-hidden="true">
<div class="modal-dialog">
<form action="update_email.php" method="post">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="emailModalLabel">Change Email</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="newEmail" class="form-label">New Email Address</label>
<input type="email" class="form-control" id="newEmail" name="new_email" required>
</div>
<p class="text-muted">Note: Changing your email will require you to verify the new address.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Update Email</button>
</div>
</div>
</form>
</div>
</div>
<!-- 4. Verify Email Modal -->
<div class="modal fade" id="verifyEmailModal" tabindex="-1" aria-labelledby="verifyEmailModalLabel" aria-hidden="true">
<div class="modal-dialog">
<form action="verify_email.php" method="post">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="verifyEmailModalLabel">Verify Your Email</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>An email with a verification code has been sent to your address. Please enter the code below. (Or click the link in the email to auto-verify.)</p>
<div class="mb-3">
<label for="verificationCode" class="form-label">Verification Code</label>
<input type="text" class="form-control" id="verificationCode" name="verification_code" required>
</div>
<p class="small text-muted">You can only request a new code once every 15 minutes.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Verify Email</button>
</div>
</div>
</form>
</div>
</div>
<!-- 5. Name Modal (First & Last Name) -->
<div class="modal fade" id="nameModal" tabindex="-1" aria-labelledby="nameModalLabel" aria-hidden="true">
<div class="modal-dialog">
<form action="update_name.php" method="post">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="nameModalLabel">Change Your Name</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="firstName" class="form-label">First Name</label>
<input type="text" class="form-control" id="firstName" name="first_name" value="<?php echo htmlspecialchars($userData['firstName']); ?>" required>
</div>
<div class="mb-3">
<label for="lastName" class="form-label">Last Name</label>
<input type="text" class="form-control" id="lastName" name="last_name" value="<?php echo htmlspecialchars($userData['lastName']); ?>" required>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Update Name</button>
</div>
</div>
</form>
</div>
</div>
<!-- 6. Password Modal -->
<div class="modal fade" id="passwordModal" tabindex="-1" aria-labelledby="passwordModalLabel" aria-hidden="true">
<div class="modal-dialog">
<form action="update_password.php" method="post">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="passwordModalLabel">Change Password</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="currentPassword" class="form-label">Current Password</label>
<input type="password" class="form-control" id="currentPassword" name="current_password" required>
</div>
<div class="mb-3">
<label for="newPassword" class="form-label">New Password</label>
<input type="password" class="form-control" id="newPassword" name="new_password" required>
</div>
<div class="mb-3">
<label for="confirmPassword" class="form-label">Confirm New Password</label>
<input type="password" class="form-control" id="confirmPassword" name="confirm_password" required>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Change Password</button>
</div>
</div>
</form>
</div>
</div>
<?php require_once 'includes/footer.php'; ?>

187
register.php Normal file
View file

@ -0,0 +1,187 @@
<?php
session_start();
require_once 'includes/globals.php';
require_once 'vendor/autoload.php';
use DJMixHosting\Database;
use Aws\Ses\SesClient;
use Aws\Exception\AwsException;
// If the user is already logged in, redirect them
if(isset($_SESSION['user'])) {
header("Location: profile.php");
exit;
}
if($_SERVER['REQUEST_METHOD'] == 'POST') {
// Gather form fields
$username = trim($_POST['username'] ?? '');
$email = trim($_POST['email'] ?? '');
$password = $_POST['password'] ?? '';
$confirm_password = $_POST['confirm_password'] ?? '';
$first_name = trim($_POST['first_name'] ?? '');
$last_name = trim($_POST['last_name'] ?? '');
// Basic validation
$errors = [];
if(empty($username) || empty($email) || empty($password) || empty($confirm_password) || empty($first_name) || empty($last_name)) {
$errors[] = "All fields are required.";
}
if($password !== $confirm_password) {
$errors[] = "Passwords do not match.";
}
if(!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors[] = "Invalid email format.";
}
if(!preg_match('/^[a-zA-Z0-9_]{3,25}$/', $username)) {
$errors[] = "Invalid username format.";
}
if(!empty($errors)) {
$_SESSION['error'] = implode(" ", $errors);
header("Location: register.php");
exit;
}
$db = new Database($config);
// Check if username or email already exists
$stmt = $db->prepare("SELECT id FROM users WHERE username = ? OR email = ?");
$stmt->bind_param("ss", $username, $email);
$stmt->execute();
$result = $stmt->get_result();
if($result->num_rows > 0) {
$_SESSION['error'] = "Username or email already exists.";
header("Location: register.php");
exit;
}
$stmt->close();
// Insert the new user record. (Assuming columns firstName and lastName exist.)
$hashed_password = password_hash($password, PASSWORD_DEFAULT);
$stmt = $db->prepare("INSERT INTO users (username, password, email, firstName, lastName, img, emailVerified) VALUES (?, ?, ?, ?, ?, '', 0)");
$stmt->bind_param("sssss", $username, $hashed_password, $email, $first_name, $last_name);
if(!$stmt->execute()){
$_SESSION['error'] = "Registration failed. Please try again.";
header("Location: register.php");
exit;
}
$user_id = $stmt->insert_id;
$stmt->close();
// Log the user in
$_SESSION['user'] = [
'id' => $user_id,
'username' => $username,
'email' => $email
];
// Trigger email verification: generate a verification code valid for 15 minutes
$verification_code = bin2hex(random_bytes(16));
$expires_at = date("Y-m-d H:i:s", strtotime("+15 minutes"));
// Insert record with purpose 'email_verification'
$stmt = $db->prepare("REPLACE INTO email_verifications (user_id, email, verification_code, expires_at, purpose) VALUES (?, ?, ?, ?, 'email_verification')");
$stmt->bind_param("isss", $user_id, $email, $verification_code, $expires_at);
$stmt->execute();
$stmt->close();
// Send verification email via AWS SES using config settings
$sesClient = new SesClient([
'version' => 'latest',
'region' => $config['aws']['ses']['region'],
'credentials' => [
'key' => $config['aws']['ses']['access_key'],
'secret' => $config['aws']['ses']['secret_key']
]
]);
$sender_email = $config['aws']['ses']['sender_email'];
$recipient_email = $email;
$subject = "Verify Your Email Address";
$verification_link = $config['app']['url'] . "/verify_email.php?code={$verification_code}";
$body_text = "Thank you for registering at " . $config['app']['name'] . ".\n\n";
$body_text .= "Please verify your email address by clicking the link below or by entering the verification code in your profile:\n\n";
$body_text .= "{$verification_link}\n\nYour verification code is: {$verification_code}\nThis code will expire in 15 minutes.";
try {
$result = $sesClient->sendEmail([
'Destination' => [
'ToAddresses' => [$recipient_email],
],
'ReplyToAddresses' => [$sender_email],
'Source' => $sender_email,
'Message' => [
'Body' => [
'Text' => [
'Charset' => 'UTF-8',
'Data' => $body_text,
],
],
'Subject' => [
'Charset' => 'UTF-8',
'Data' => $subject,
],
],
]);
$_SESSION['success'] = "Registration successful! A verification email has been sent to your email address.";
} catch (AwsException $e) {
$_SESSION['error'] = "Registration successful, but failed to send verification email: " . $e->getAwsErrorMessage();
}
header("Location: profile.php");
exit;
}
require_once 'includes/header.php';
?>
<section class="register-section py-5">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-6">
<?php
if(isset($_SESSION['error'])) {
echo '<div class="alert alert-danger alert-dismissible fade show mb-4" role="alert">' . htmlspecialchars($_SESSION['error']) . '<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>';
unset($_SESSION['error']);
}
?>
<div class="card shadow-sm border-0">
<div class="card-body p-4">
<h3 class="text-center mb-4">Register</h3>
<form action="register.php" method="post" class="needs-validation" novalidate>
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<div class="mb-3">
<label for="firstName" class="form-label">First Name</label>
<input type="text" class="form-control" id="firstName" name="first_name" required>
</div>
<div class="mb-3">
<label for="lastName" class="form-label">Last Name</label>
<input type="text" class="form-control" id="lastName" name="last_name" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<div class="mb-3">
<label for="confirm_password" class="form-label">Confirm Password</label>
<input type="password" class="form-control" id="confirm_password" name="confirm_password" required>
</div>
<button type="submit" class="btn btn-primary w-100">Register</button>
</form>
</div>
</div>
<div class="text-center mt-4">
<p class="mb-0">Already have an account? <a href="login.php" class="text-decoration-none">Login</a></p>
</div>
</div>
</div>
</div>
</section>
<?php require_once 'includes/footer.php'; ?>

97
update_email.php Normal file
View file

@ -0,0 +1,97 @@
<?php
session_start();
require_once 'includes/globals.php';
require_once 'vendor/autoload.php';
use DJMixHosting\Database;
use Aws\Ses\SesClient;
use Aws\Exception\AwsException;
if (!isset($_SESSION['user'])) {
header("Location: login.php");
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header("Location: profile.php");
exit;
}
if (!isset($_POST['new_email']) || empty($_POST['new_email'])) {
$_SESSION['error'] = "New email address is required.";
header("Location: profile.php");
exit;
}
$new_email = filter_var($_POST['new_email'], FILTER_VALIDATE_EMAIL);
if (!$new_email) {
$_SESSION['error'] = "Invalid email format.";
header("Location: profile.php");
exit;
}
$db = new Database($config);
$userId = $_SESSION['user']['id'];
// Update the user's email and mark it as unverified
$stmt = $db->prepare("UPDATE users SET email = ?, emailVerified = 0 WHERE id = ?");
$stmt->bind_param("si", $new_email, $userId);
$stmt->execute();
$stmt->close();
// Generate verification code and expiry (15 minutes from now)
$verification_code = bin2hex(random_bytes(16));
$expires_at = date("Y-m-d H:i:s", strtotime("+15 minutes"));
// Store the verification record (using REPLACE to update any existing record for this user and email)
$stmt = $db->prepare("REPLACE INTO email_verifications (user_id, email, verification_code, expires_at) VALUES (?, ?, ?, ?)");
$stmt->bind_param("isss", $userId, $new_email, $verification_code, $expires_at);
$stmt->execute();
$stmt->close();
// Send email using AWS SES with config settings
$sesClient = new SesClient([
'version' => 'latest',
'region' => $config['aws']['ses']['region'],
'credentials' => [
'key' => $config['aws']['ses']['access_key'],
'secret' => $config['aws']['ses']['secret_key'],
]
]);
$sender_email = $config['aws']['ses']['sender_email'];
$recipient_email = $new_email;
$subject = "Verify Your Email Address";
// Construct a verification link. Users can click this link to auto-submit the code.
$verification_link = $config['app']['url'] . "/verify_email.php?code={$verification_code}";
$body_text = "Please verify your email address by clicking the link below or by entering the code in your profile:\n\n";
$body_text .= "{$verification_link}\n\nYour verification code is: {$verification_code}\nThis code will expire in 15 minutes.";
try {
$result = $sesClient->sendEmail([
'Destination' => [
'ToAddresses' => [$recipient_email],
],
'ReplyToAddresses' => [$sender_email],
'Source' => $sender_email,
'Message' => [
'Body' => [
'Text' => [
'Charset' => 'UTF-8',
'Data' => $body_text,
],
],
'Subject' => [
'Charset' => 'UTF-8',
'Data' => $subject,
],
],
]);
$_SESSION['success'] = "Email updated. A verification email has been sent to your new address.";
} catch (AwsException $e) {
$_SESSION['error'] = "Failed to send verification email: " . $e->getAwsErrorMessage();
}
header("Location: profile.php");
exit;

43
update_name.php Normal file
View file

@ -0,0 +1,43 @@
<?php
session_start();
require_once 'includes/globals.php';
require_once 'vendor/autoload.php';
use DJMixHosting\Database;
// Ensure the user is logged in
if (!isset($_SESSION['user'])) {
header("Location: login.php");
exit;
}
// Check that both first and last names were provided
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || empty($_POST['first_name']) || empty($_POST['last_name'])) {
$_SESSION['error'] = "Both first name and last name are required.";
header("Location: profile.php");
exit;
}
$firstName = trim($_POST['first_name']);
$lastName = trim($_POST['last_name']);
$db = new Database($config);
$userId = $_SESSION['user']['id'];
// Update the user's first and last name in the database
$stmt = $db->prepare("UPDATE users SET firstName = ?, lastName = ? WHERE id = ?");
$stmt->bind_param("ssi", $firstName, $lastName, $userId);
if (!$stmt->execute()) {
$_SESSION['error'] = "Failed to update name. Please try again.";
header("Location: profile.php");
exit;
}
$stmt->close();
// Optionally update session data if you store these fields there
$_SESSION['user']['firstName'] = $firstName;
$_SESSION['user']['lastName'] = $lastName;
$_SESSION['success'] = "Name updated successfully.";
header("Location: profile.php");
exit;

84
update_password.php Normal file
View file

@ -0,0 +1,84 @@
<?php
session_start();
require_once 'includes/globals.php';
require_once 'vendor/autoload.php';
use DJMixHosting\Database;
// Ensure the user is authenticated.
if (!isset($_SESSION['user'])) {
header("Location: login.php");
exit;
}
// Check for required POST fields.
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || empty($_POST['current_password']) || empty($_POST['new_password']) || empty($_POST['confirm_password'])) {
$_SESSION['error'] = "All password fields are required.";
header("Location: profile.php");
exit;
}
$current_password = $_POST['current_password'];
$new_password = $_POST['new_password'];
$confirm_password = $_POST['confirm_password'];
// Validate that the new password meets the requirements:
// - 8 to 32 characters
// - at least one uppercase letter
// - at least one lowercase letter
// - at least one number
// - at least one symbol
$pattern = '/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{8,32}$/';
if (!preg_match($pattern, $new_password)) {
$_SESSION['error'] = "New password must be 8-32 characters and include at least one uppercase letter, one lowercase letter, one number, and one symbol.";
header("Location: profile.php");
exit;
}
// Verify that new password and confirmation match.
if ($new_password !== $confirm_password) {
$_SESSION['error'] = "New password and confirmation do not match.";
header("Location: profile.php");
exit;
}
$db = new Database($config);
$userId = $_SESSION['user']['id'];
// Retrieve the current password hash from the database.
$stmt = $db->prepare("SELECT password FROM users WHERE id = ?");
$stmt->bind_param("i", $userId);
$stmt->execute();
$result = $stmt->get_result();
$userData = $result->fetch_assoc();
$stmt->close();
if (!$userData) {
$_SESSION['error'] = "User not found.";
header("Location: profile.php");
exit;
}
// Verify that the current password is correct.
if (!password_verify($current_password, $userData['password'])) {
$_SESSION['error'] = "Current password is incorrect.";
header("Location: profile.php");
exit;
}
// Hash the new password.
$hashed_new_password = password_hash($new_password, PASSWORD_DEFAULT);
// Update the user's password in the database.
$stmt = $db->prepare("UPDATE users SET password = ? WHERE id = ?");
$stmt->bind_param("si", $hashed_new_password, $userId);
if (!$stmt->execute()) {
$_SESSION['error'] = "Failed to update password. Please try again.";
header("Location: profile.php");
exit;
}
$stmt->close();
$_SESSION['success'] = "Password updated successfully.";
header("Location: profile.php");
exit;

53
update_username.php Normal file
View file

@ -0,0 +1,53 @@
<?php
session_start();
require_once 'includes/globals.php';
require_once 'vendor/autoload.php';
use DJMixHosting\Database;
if (!isset($_SESSION['user'])) {
header("Location: login.php");
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_POST['new_username']) || empty($_POST['new_username'])) {
$_SESSION['error'] = "New username is required.";
header("Location: profile.php");
exit;
}
$new_username = trim($_POST['new_username']);
// Validate username (for example, only alphanumeric and underscores, 3-25 characters)
if (!preg_match('/^[a-zA-Z0-9_]{3,25}$/', $new_username)) {
$_SESSION['error'] = "Invalid username format.";
header("Location: profile.php");
exit;
}
$db = new Database($config);
$userId = $_SESSION['user']['id'];
// Check if the new username already exists (excluding the current user)
$stmt = $db->prepare("SELECT id FROM users WHERE username = ? AND id != ?");
$stmt->bind_param("si", $new_username, $userId);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows > 0) {
$_SESSION['error'] = "Username already taken.";
header("Location: profile.php");
exit;
}
$stmt->close();
// Update the username in the database
$stmt = $db->prepare("UPDATE users SET username = ? WHERE id = ?");
$stmt->bind_param("si", $new_username, $userId);
$stmt->execute();
$stmt->close();
// Update session data
$_SESSION['user']['username'] = $new_username;
$_SESSION['success'] = "Username updated successfully.";
header("Location: profile.php");
exit;

149
upload.php Normal file
View file

@ -0,0 +1,149 @@
<?php
// upload.php - Step 1: File upload and immediate processing
require_once 'includes/globals.php';
// Ensure user is authenticated
if (!isset($_SESSION['user'])) {
header("Location: login.php");
exit;
}
// Process the form submission
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['mix_file'])) {
// Get upload config from $config
$tmpPath = rtrim($config['uploads']['tmp_path'], '/') . '/';
$maxFileSize = $config['uploads']['max_file_size'];
$allowedMimeTypes = $config['uploads']['allowed_mime_type'];
$allowedFileTypes = $config['uploads']['allowed_file_types'];
$file = $_FILES['mix_file'];
// Basic file validations
if ($file['error'] !== UPLOAD_ERR_OK) {
$_SESSION['error'] = "File upload error.";
header("Location: upload.php");
exit;
}
if ($file['size'] > $maxFileSize) {
$_SESSION['error'] = "File is too large.";
header("Location: upload.php");
exit;
}
// Get file extension and mime type
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($ext, $allowedFileTypes)) {
$_SESSION['error'] = "File type not allowed.";
header("Location: upload.php");
exit;
}
if (!in_array($file['type'], $allowedMimeTypes)) {
$_SESSION['error'] = "MIME type not allowed.";
header("Location: upload.php");
exit;
}
// Create a unique temporary filename
$uniqueName = uniqid() . "." . $ext;
$destination = $tmpPath . $uniqueName;
if (!move_uploaded_file($file['tmp_name'], $destination)) {
$_SESSION['error'] = "Failed to save uploaded file.";
header("Location: upload.php");
exit;
}
// Process the file: if mp3, extract metadata; if zip, extract each mp3's metadata.
$uploadTask = [];
$uploadTask['original_name'] = $file['name'];
$uploadTask['local_path'] = $destination;
$uploadTask['size'] = $file['size'];
$uploadTask['ext'] = $ext;
if ($ext === "mp3") {
// Process MP3 file using shell_exec calls (ensure you sanitize arguments)
$escapedFile = escapeshellarg($destination);
$artist = trim(shell_exec("eyed3 --query='%a%' $escapedFile"));
$title = trim(shell_exec("eyed3 --query='%t%' $escapedFile"));
$duration = trim(shell_exec("mp3info -p \"%S\" $escapedFile"));
// You can extract additional info as needed
$uploadTask['file_type'] = 'mp3';
$uploadTask['metadata'] = [
'artist' => $artist,
'title' => $title,
'duration' => $duration, // in seconds
];
$uploadTask['mediaplayer'] = 1;
} elseif ($ext === "zip") {
// Process ZIP file using ZipArchive
$zip = new ZipArchive;
if ($zip->open($destination) === true) {
$totalDuration = 0;
$tracklist = [];
for ($i = 0; $i < $zip->numFiles; $i++) {
$entryName = $zip->getNameIndex($i);
$entryExt = strtolower(pathinfo($entryName, PATHINFO_EXTENSION));
if ($entryExt === "mp3") {
// Extract the MP3 temporarily to process metadata
$tempDir = $tmpPath . uniqid('zip_');
mkdir($tempDir);
$tempFilePath = $tempDir . '/' . basename($entryName);
$zip->extractTo($tempDir, $entryName);
$escapedFile = escapeshellarg($tempFilePath);
$title = trim(shell_exec("eyed3 --query='%t%' $escapedFile"));
$duration = trim(shell_exec("mp3info -p \"%S\" $escapedFile"));
$tracklist[] = $title ?: basename($entryName);
$totalDuration += (int)$duration;
unlink($tempFilePath);
rmdir($tempDir);
}
}
$zip->close();
$uploadTask['file_type'] = 'zip';
$uploadTask['metadata'] = [
'tracklist' => $tracklist,
'total_duration' => $totalDuration,
];
// Mark ZIPs as download only (no mediaplayer)
$uploadTask['mediaplayer'] = 0;
} else {
$_SESSION['error'] = "Failed to open ZIP file.";
header("Location: upload.php");
exit;
}
} else {
$_SESSION['error'] = "Unsupported file type.";
header("Location: upload.php");
exit;
}
// Save the upload task details in session so step 2 can use them.
$_SESSION['upload_task'] = $uploadTask;
header("Location: upload_details.php");
exit;
}
require_once 'includes/header.php';
?>
<section class="upload-section py-5">
<div class="container">
<h2 class="mb-4">Upload a New Mix</h2>
<?php
if (isset($_SESSION['error'])) {
echo '<div class="alert alert-danger">' . htmlspecialchars($_SESSION['error']) . '</div>';
unset($_SESSION['error']);
}
?>
<form action="upload.php" method="post" enctype="multipart/form-data">
<div class="mb-3">
<label for="mix_file" class="form-label">Select Mix File (MP3 or ZIP)</label>
<input type="file" class="form-control" id="mix_file" name="mix_file" accept=".mp3,.zip" required>
</div>
<!-- Optionally, add album art upload here later -->
<button type="submit" class="btn btn-primary">Upload File</button>
</form>
</div>
</section>
<?php require_once 'includes/footer.php'; ?>

245
upload_details.php Normal file
View file

@ -0,0 +1,245 @@
<?php
// upload_details.php - Step 2: Enter mix details and finalize upload
require_once 'includes/globals.php';
require_once 'vendor/autoload.php';
use DJMixHosting\Database;
use DJMixHosting\Genres;
use DJMixHosting\DJs;
use Aws\Credentials\Credentials;
// Ensure user is authenticated and an upload task exists
if (!isset($_SESSION['user']) || !isset($_SESSION['upload_task'])) {
header("Location: upload.php");
exit;
}
$uploadTask = $_SESSION['upload_task'];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Get form fields
$title = trim($_POST['title'] ?? '');
$description = trim($_POST['description'] ?? '');
$recorded = trim($_POST['recorded'] ?? '');
$selectedGenres = $_POST['genres'] ?? []; // an array of genre IDs or names
$dj1 = $_POST['dj1'] ?? 0;
$dj2 = $_POST['dj2'] ?? 0;
$dj3 = $_POST['dj3'] ?? 0;
// Basic validation
if (empty($title)) {
$_SESSION['error'] = "Mix title is required.";
header("Location: upload_details.php");
exit;
}
// Generate a slug from the title
function slugify($text) {
// Replace non-letter or digits by -
$text = preg_replace('~[^\pL\d]+~u', '-', $text);
// Transliterate
$text = iconv('utf-8', 'us-ascii//TRANSLIT', $text);
// Remove unwanted characters
$text = preg_replace('~[^-\w]+~', '', $text);
// Trim
$text = trim($text, '-');
// Remove duplicate -
$text = preg_replace('~-+~', '-', $text);
// Lowercase
$text = strtolower($text);
return empty($text) ? 'n-a' : $text;
}
$slug = slugify($title);
$credentials = new Credentials(
$config['aws']['s3']['access_key'],
$config['aws']['s3']['secret_key']
);
$s3Client = new Aws\S3\S3Client([
'version' => 'latest',
'region' => $config['aws']['s3']['region'],
'endpoint' => $config['aws']['s3']['host'],
'credentials' => $credentials,
'use_path_style_endpoint' => true,
'profile' => null, // disable reading ~/.aws/config
'use_aws_shared_config_files' => false,
]);
// Determine remote file path for now, store in a temporary folder on Spaces
$remotePath = "temp/mixes/" . uniqid() . "_" . basename($uploadTask['local_path']);
// Determine MIME type from file extension
$mimeType = ($uploadTask['ext'] === 'mp3') ? 'audio/mpeg' : 'application/zip';
try {
$s3Client->putObject([
'Bucket' => $config['aws']['s3']['bucket'],
'Key' => $remotePath,
'SourceFile' => $uploadTask['local_path'],
'ACL' => 'private', // file remains hidden until approved
'ContentType' => $mimeType,
]);
} catch (Exception $e) {
$_SESSION['error'] = "Error uploading file to CDN: " . $e->getMessage();
header("Location: upload_details.php");
exit;
}
// Now insert a new mix record into the database with pending status.
$db = new Database($config);
$stmt = $db->prepare("INSERT INTO mix (title, slug, description, cover, url, seconds, mediaplayer, dj1, dj2, dj3, pending, recorded) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?)");
// Determine the duration and mediaplayer flag from the upload task:
// For an MP3 file, set mediaplayer to 0; for a ZIP file, set it to 1.
$seconds = 0;
if ($uploadTask['file_type'] === 'mp3') {
$seconds = (int)$uploadTask['metadata']['duration'];
$mediaplayer = 0; // MP3 gets a player
} elseif ($uploadTask['file_type'] === 'zip') {
$seconds = (int)$uploadTask['metadata']['total_duration'];
$mediaplayer = 1; // ZIPs are download only, so flag them as 1
}
// The URL can be constructed as per your CDN structure; for now, we store the remote path.
$url = $remotePath;
// For DJs, cast to integer (0 if none selected)
$dj1 = (int)$dj1;
$dj2 = (int)$dj2;
$dj3 = (int)$dj3;
$cover = "";
$stmt->bind_param("sssssiiiiss", $title, $slug, $description, $cover, $url, $seconds, $mediaplayer, $dj1, $dj2, $dj3, $recorded);
if (!$stmt->execute()) {
$_SESSION['error'] = "Error saving mix to database.";
header("Location: upload_details.php");
exit;
}
$mixId = $stmt->insert_id;
$stmt->close();
// Insert mix_meta entries for genres and tracklist
// For genres, assume $selectedGenres is an array of genre IDs (or names, if new genres need to be added)
foreach ($selectedGenres as $genreId) {
$stmt = $db->prepare("INSERT INTO mix_meta (mix_id, attribute, value) VALUES (?, 'genre', ?)");
$stmt->bind_param("is", $mixId, $genreId);
$stmt->execute();
$stmt->close();
}
// If the file was mp3 and metadata includes a track title or tracklist, insert that as well:
if ($uploadTask['file_type'] === 'mp3' && !empty($uploadTask['metadata']['title'])) {
$tracklist = $uploadTask['metadata']['title'];
$stmt = $db->prepare("INSERT INTO mix_meta (mix_id, attribute, value) VALUES (?, 'tracklist', ?)");
$stmt->bind_param("is", $mixId, $tracklist);
$stmt->execute();
$stmt->close();
} elseif ($uploadTask['file_type'] === 'zip' && !empty($uploadTask['metadata']['tracklist'])) {
// If multiple tracks, you might store them as a newline-separated string
$tracklist = implode("\n", $uploadTask['metadata']['tracklist']);
$stmt = $db->prepare("INSERT INTO mix_meta (mix_id, attribute, value) VALUES (?, 'tracklist', ?)");
$stmt->bind_param("is", $mixId, $tracklist);
$stmt->execute();
$stmt->close();
}
// Cleanup: delete the local temporary file and clear session task
unlink($uploadTask['local_path']);
unset($_SESSION['upload_task']);
$_SESSION['success'] = "Mix uploaded successfully and is pending approval.";
header("Location: profile.php");
exit;
}
require_once 'includes/header.php';
// Load available genres and DJs for the form
$db = new Database($config);
$genresObj = new DJMixHosting\Genres($db);
$allGenres = $genresObj->get_all_genres();
$djsObj = new DJMixHosting\DJs($db);
$allDJs = $djsObj->get_all_djs();
?>
<section class="upload-details-section py-5">
<div class="container">
<h2 class="mb-4">Enter Mix Details</h2>
<?php
if(isset($_SESSION['error'])) {
echo '<div class="alert alert-danger">' . htmlspecialchars($_SESSION['error']) . '</div>';
unset($_SESSION['error']);
}
?>
<div class="card mb-4">
<div class="card-body">
<h5>File Summary</h5>
<p><strong>Original Name:</strong> <?php echo htmlspecialchars($uploadTask['original_name']); ?></p>
<p><strong>File Type:</strong> <?php echo htmlspecialchars(strtoupper($uploadTask['file_type'])); ?></p>
<p><strong>Size:</strong> <?php echo round($uploadTask['size'] / 1024, 2); ?> KB</p>
<?php if ($uploadTask['file_type'] === 'mp3'): ?>
<p><strong>Duration:</strong> <?php echo htmlspecialchars($uploadTask['metadata']['duration']); ?> seconds</p>
<?php elseif ($uploadTask['file_type'] === 'zip'): ?>
<p><strong>Total Duration:</strong> <?php echo htmlspecialchars($uploadTask['metadata']['total_duration']); ?> seconds</p>
<?php endif; ?>
</div>
</div>
<form action="upload_details.php" method="post" class="needs-validation" novalidate>
<div class="mb-3">
<label for="title" class="form-label">Mix Title</label>
<input type="text" class="form-control" id="title" name="title" required>
</div>
<div class="mb-3">
<label for="description" class="form-label">Mix Description</label>
<textarea class="form-control" id="description" name="description" rows="3"></textarea>
</div>
<div class="mb-3">
<label for="recorded" class="form-label">Recorded Date</label>
<input type="date" class="form-control" id="recorded" name="recorded" required>
</div>
<div class="mb-3">
<label for="genres" class="form-label">Select Genres (type to search)</label>
<select class="form-select" id="genres" name="genres[]" multiple required>
<?php foreach ($allGenres as $genre): ?>
<option value="<?php echo htmlspecialchars($genre['id']); ?>"><?php echo htmlspecialchars($genre['name']); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label class="form-label">Select DJs (Maximum 3)</label>
<div class="row">
<div class="col">
<select class="form-select" name="dj1" required>
<option value="">Select DJ 1</option>
<?php foreach ($allDJs as $dj): ?>
<option value="<?php echo htmlspecialchars($dj['id']); ?>"><?php echo htmlspecialchars($dj['name']); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col">
<select class="form-select" name="dj2">
<option value="">Select DJ 2 (Optional)</option>
<?php foreach ($allDJs as $dj): ?>
<option value="<?php echo htmlspecialchars($dj['id']); ?>"><?php echo htmlspecialchars($dj['name']); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col">
<select class="form-select" name="dj3">
<option value="">Select DJ 3 (Optional)</option>
<?php foreach ($allDJs as $dj): ?>
<option value="<?php echo htmlspecialchars($dj['id']); ?>"><?php echo htmlspecialchars($dj['name']); ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary">Submit Mix</button>
</form>
</div>
</section>
<?php require_once 'includes/footer.php'; ?>

65
verify_email.php Normal file
View file

@ -0,0 +1,65 @@
<?php
session_start();
require_once 'includes/globals.php';
require_once 'vendor/autoload.php';
use DJMixHosting\Database;
if (!isset($_SESSION['user'])) {
header("Location: login.php");
exit;
}
$db = new Database($config);
$userId = $_SESSION['user']['id'];
// Retrieve the verification code from GET or POST
$verification_code = "";
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['verification_code'])) {
$verification_code = trim($_POST['verification_code']);
} elseif (isset($_GET['code'])) {
$verification_code = trim($_GET['code']);
} else {
$_SESSION['error'] = "Verification code is required.";
header("Location: profile.php");
exit;
}
// Look up the email verification record for this user and code
$stmt = $db->prepare("SELECT * FROM email_verifications WHERE user_id = ? AND verification_code = ?");
$stmt->bind_param("is", $userId, $verification_code);
$stmt->execute();
$result = $stmt->get_result();
$record = $result->fetch_assoc();
$stmt->close();
if (!$record) {
$_SESSION['error'] = "Invalid verification code.";
header("Location: profile.php");
exit;
}
// Check if the verification code has expired
$current_time = new DateTime();
$expires_at = new DateTime($record['expires_at']);
if ($current_time > $expires_at) {
$_SESSION['error'] = "Verification code has expired. Please request a new one.";
header("Location: profile.php");
exit;
}
// Verification successful: update the user's record
$stmt = $db->prepare("UPDATE users SET emailVerified = 1 WHERE id = ?");
$stmt->bind_param("i", $userId);
$stmt->execute();
$stmt->close();
// Remove the verification record for cleanup
$stmt = $db->prepare("DELETE FROM email_verifications WHERE user_id = ?");
$stmt->bind_param("i", $userId);
$stmt->execute();
$stmt->close();
$_SESSION['success'] = "Email verified successfully.";
header("Location: profile.php");
exit;