Compare commits
7 Commits
f879d25bc7
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 9fd7fbd2e5 | |||
|
|
26bacd8928 | ||
| fe610b094d | |||
|
|
efc1fc3906 | ||
| 457170a1ae | |||
| 03fb096317 | |||
| 8fc8d737e5 |
41
add_profile.php
Normal file
41
add_profile.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
require 'db.php';
|
||||
require 'auth.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
require_login();
|
||||
|
||||
$user_id = current_user_id();
|
||||
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$name = trim($data['name'] ?? '');
|
||||
|
||||
if ($name === '') {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'error' => 'Profile name is required']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Prevent duplicate profile names per user
|
||||
$stmt = $pdo->prepare("SELECT id FROM profiles WHERE user_id = ? AND name = ?");
|
||||
$stmt->execute([$user_id, $name]);
|
||||
|
||||
if ($stmt->fetchColumn()) {
|
||||
http_response_code(409);
|
||||
echo json_encode(['success' => false, 'error' => 'Profile already exists']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Insert new profile
|
||||
$stmt = $pdo->prepare("INSERT INTO profiles (user_id, name, is_default) VALUES (?, ?, 0)");
|
||||
$stmt->execute([$user_id, $name]);
|
||||
|
||||
$newProfileId = $pdo->lastInsertId();
|
||||
|
||||
// Make it active immediately
|
||||
$_SESSION['active_profile_id'] = (int)$newProfileId;
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'profile_id' => $newProfileId
|
||||
]);
|
||||
@@ -6,6 +6,13 @@ header('Content-Type: application/json');
|
||||
require_login();
|
||||
|
||||
$user_id = current_user_id();
|
||||
$profile_id = $_SESSION['active_profile_id'] ?? null;
|
||||
|
||||
if (!$profile_id) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'error' => 'No active profile']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$name = trim($data['name'] ?? '');
|
||||
@@ -16,7 +23,7 @@ if ($name === '') {
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("INSERT INTO projects (user_id, name) VALUES (?, ?)");
|
||||
$stmt->execute([$user_id, $name]);
|
||||
$stmt = $pdo->prepare("INSERT INTO projects (user_id, profile_id, name) VALUES (?, ?, ?)");
|
||||
$stmt->execute([$user_id, $profile_id, $name]);
|
||||
|
||||
echo json_encode(['success' => true, 'id' => $pdo->lastInsertId()]);
|
||||
|
||||
11
get_data.php
11
get_data.php
@@ -6,9 +6,16 @@ header('Content-Type: application/json');
|
||||
require_login();
|
||||
|
||||
$user_id = current_user_id();
|
||||
$profile_id = $_SESSION['active_profile_id'] ?? null;
|
||||
|
||||
$stmt = $pdo->prepare("SELECT * FROM projects WHERE user_id = ? ORDER BY sort_order ASC");
|
||||
$stmt->execute([$user_id]);
|
||||
if (!$profile_id) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'error' => 'No active profile']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("SELECT * FROM projects WHERE user_id = ? AND profile_id = ? ORDER BY sort_order ASC");
|
||||
$stmt->execute([$user_id, $profile_id]);
|
||||
$projects = $stmt->fetchAll();
|
||||
|
||||
foreach ($projects as &$project) {
|
||||
|
||||
18
get_profiles.php
Normal file
18
get_profiles.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
require 'db.php';
|
||||
require 'auth.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
require_login();
|
||||
|
||||
$user_id = current_user_id();
|
||||
|
||||
$stmt = $pdo->prepare("SELECT id, name, is_default FROM profiles WHERE user_id = ? ORDER BY is_default DESC, name ASC");
|
||||
$stmt->execute([$user_id]);
|
||||
$profiles = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'active_profile_id' => $_SESSION['active_profile_id'] ?? null,
|
||||
'profiles' => $profiles
|
||||
]);
|
||||
337
index.php
337
index.php
@@ -144,8 +144,19 @@
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.profile-select {
|
||||
height: 32px; /* match your header-action-btn buttons */
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700; /* or 700 for stronger bold */
|
||||
padding-right: 2.5rem; /* key: reserve space for the arrow */
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.add-project-btn {
|
||||
font-size: 1.4rem;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600; /* or 700 for stronger bold */
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
@@ -259,37 +270,36 @@
|
||||
</button>
|
||||
</span>
|
||||
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
|
||||
<!-- Add Project -->
|
||||
<button class="btn btn-success btn-lg header-action-btn"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#addProjectModal">
|
||||
<i class="bi bi-plus-lg"></i>
|
||||
</button>
|
||||
|
||||
<!-- User Dropdown -->
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-outline-secondary btn-lg header-action-btn"
|
||||
type="button"
|
||||
id="menuButton"
|
||||
data-bs-toggle="dropdown">
|
||||
<i class="bi bi-person-circle"></i>
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<select id="profileSelect" class="form-select btn-lg profile-select" style="width: 140px;"></select>
|
||||
<!-- Add Project -->
|
||||
<button class="btn btn-success btn-lg header-action-btn"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#addProjectModal">
|
||||
<i class="bi bi-plus-lg"></i>
|
||||
</button>
|
||||
|
||||
<ul class="dropdown-menu dropdown-menu-end custom-dropdown" aria-labelledby="menuButton">
|
||||
<li class="dropdown-item-text fw-semibold fs-5" id="dropdownUserEmail"></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<button class="dropdown-item fs-5" type="button" id="logoutBtn">
|
||||
<i class="bi bi-box-arrow-right"></i>
|
||||
<span>Logout</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- User Dropdown -->
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-outline-secondary btn-lg header-action-btn"
|
||||
type="button"
|
||||
id="menuButton"
|
||||
data-bs-toggle="dropdown">
|
||||
<i class="bi bi-person-circle"></i>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
<ul class="dropdown-menu dropdown-menu-end custom-dropdown" aria-labelledby="menuButton">
|
||||
<li class="dropdown-item-text fw-semibold fs-5" id="dropdownUserEmail"></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<button class="dropdown-item fs-5" type="button" id="logoutBtn">
|
||||
<i class="bi bi-box-arrow-right"></i>
|
||||
<span>Logout</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</h1>
|
||||
|
||||
|
||||
@@ -430,8 +440,8 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Email</label>
|
||||
<input type="email" class="form-control" id="loginEmail" required>
|
||||
<label class="form-label">Email or Username</label>
|
||||
<input type="text" class="form-control" id="loginEmail" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Password</label>
|
||||
@@ -460,6 +470,10 @@
|
||||
<label class="form-label">Email</label>
|
||||
<input type="email" class="form-control" id="registerEmail" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Username (optional)</label>
|
||||
<input type="text" class="form-control" id="registerUsername">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Password (8+ chars)</label>
|
||||
<input type="password" class="form-control" id="registerPassword" required minlength="8">
|
||||
@@ -538,131 +552,149 @@
|
||||
favicon.type = "image/svg+xml";
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function loadProfiles() {
|
||||
fetch('get_profiles.php')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (!data.success) return;
|
||||
|
||||
const sel = document.getElementById('profileSelect');
|
||||
sel.innerHTML = '';
|
||||
|
||||
data.profiles.forEach(p => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = p.id;
|
||||
opt.textContent = p.name;
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
|
||||
if (data.active_profile_id) {
|
||||
sel.value = String(data.active_profile_id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function fetchProjects() {
|
||||
fetch('get_data.php')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
function fetchProjects() {
|
||||
fetch('get_data.php')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
|
||||
if (!Array.isArray(data)) {
|
||||
if (data && data.error === 'Not authenticated') {
|
||||
new bootstrap.Modal(document.getElementById('loginModal')).show();
|
||||
return;
|
||||
}
|
||||
console.error('get_data.php returned non-array:', data);
|
||||
if (!Array.isArray(data)) {
|
||||
if (data && data.error === 'Not authenticated') {
|
||||
new bootstrap.Modal(document.getElementById('loginModal')).show();
|
||||
return;
|
||||
}
|
||||
console.error('get_data.php returned non-array:', data);
|
||||
return;
|
||||
}
|
||||
|
||||
const container = document.getElementById('projectGrid');
|
||||
container.innerHTML = '';
|
||||
const container = document.getElementById('projectGrid');
|
||||
container.innerHTML = '';
|
||||
|
||||
data.forEach((project, idx) => {
|
||||
data.forEach((project, idx) => {
|
||||
|
||||
const projectId = `project-${project.id}`;
|
||||
const colorClass = `project-color-${idx % 10}`;
|
||||
const projectId = `project-${project.id}`;
|
||||
const colorClass = `project-color-${idx % 10}`;
|
||||
|
||||
const col = document.createElement('div');
|
||||
const col = document.createElement('div');
|
||||
|
||||
col.className = `col-12 col-sm-6 col-md-4 col-lg-2 col-xl-2 col-xxl-1 g-2 m-0 project-column`;
|
||||
col.dataset.projectId = project.id;
|
||||
col.classList.add("draggable-project");
|
||||
col.innerHTML = `
|
||||
<div class="project-card ${colorClass}">
|
||||
<div class="project-header">
|
||||
<span class="project-title">${project.name}</span>
|
||||
<div class="d-flex align-items-center">
|
||||
<button class="btn-icon add-task-btn" data-project-id="${project.id}" title="Add Task">
|
||||
<i class="bi bi-plus"></i>
|
||||
</button>
|
||||
<button class="btn-icon text-danger delete-project-btn" data-project-id="${project.id}" title="Delete Project">
|
||||
<i class="bi bi-dash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="project-body">
|
||||
${project.tasks.map(task => `
|
||||
<div class="d-flex justify-content-between align-items-center small-text mb-1 task-text">
|
||||
<span class="${task.highlighted == 1 ? 'highlighted' : ''} ${task.highlighted == 2 ? 'completed' : ''}".trim()
|
||||
data-task-id="${task.id}">${task.name}</span>
|
||||
<div class="d-flex">
|
||||
<button class="btn-icon add-subtask-btn" data-task-id="${task.id}" title="Add Subtask">
|
||||
<i class="bi bi-plus"></i>
|
||||
</button>
|
||||
<button class="btn-icon text-danger delete-task-btn" data-task-id="${task.id}" title="Delete Task">
|
||||
<i class="bi bi-dash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ms-3">
|
||||
${task.subtasks.map(sub => `
|
||||
<div class="d-flex justify-content-between align-items-center small-text mb-1">
|
||||
<span class="${sub.highlighted == 1 ? 'highlighted' : ''} ${sub.highlighted == 2 ? 'completed' : ''}".trim()
|
||||
data-subtask-id="${sub.id}">${sub.name}</span>
|
||||
<button class="btn-icon text-danger delete-subtask-btn" data-subtask-id="${sub.id}" title="Delete Subtask">
|
||||
<i class="bi bi-dash"></i>
|
||||
</button>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
col.className = `col-12 col-sm-6 col-md-4 col-lg-2 col-xl-2 col-xxl-1 g-2 m-0 project-column`;
|
||||
col.dataset.projectId = project.id;
|
||||
col.classList.add("draggable-project");
|
||||
col.innerHTML = `
|
||||
<div class="project-card ${colorClass}">
|
||||
<div class="project-header">
|
||||
<span class="project-title">${project.name}</span>
|
||||
<div class="d-flex align-items-center">
|
||||
<button class="btn-icon add-task-btn" data-project-id="${project.id}" title="Add Task">
|
||||
<i class="bi bi-plus"></i>
|
||||
</button>
|
||||
<button class="btn-icon text-danger delete-project-btn" data-project-id="${project.id}" title="Delete Project">
|
||||
<i class="bi bi-dash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="project-body">
|
||||
${project.tasks.map(task => `
|
||||
<div class="d-flex justify-content-between align-items-center small-text mb-1 task-text">
|
||||
<span class="${task.highlighted == 1 ? 'highlighted' : ''} ${task.highlighted == 2 ? 'completed' : ''}".trim()
|
||||
data-task-id="${task.id}">${task.name}</span>
|
||||
<div class="d-flex">
|
||||
<button class="btn-icon add-subtask-btn" data-task-id="${task.id}" title="Add Subtask">
|
||||
<i class="bi bi-plus"></i>
|
||||
</button>
|
||||
<button class="btn-icon text-danger delete-task-btn" data-task-id="${task.id}" title="Delete Task">
|
||||
<i class="bi bi-dash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ms-3">
|
||||
${task.subtasks.map(sub => `
|
||||
<div class="d-flex justify-content-between align-items-center small-text mb-1">
|
||||
<span class="${sub.highlighted == 1 ? 'highlighted' : ''} ${sub.highlighted == 2 ? 'completed' : ''}".trim()
|
||||
data-subtask-id="${sub.id}">${sub.name}</span>
|
||||
<button class="btn-icon text-danger delete-subtask-btn" data-subtask-id="${sub.id}" title="Delete Subtask">
|
||||
<i class="bi bi-dash"></i>
|
||||
</button>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.appendChild(col);
|
||||
});
|
||||
container.appendChild(col);
|
||||
});
|
||||
|
||||
|
||||
// Create the drop indicator element
|
||||
const placeholder = document.createElement('div');
|
||||
placeholder.className = 'project-column';
|
||||
placeholder.innerHTML = `<div class="project-placeholder"></div>`;
|
||||
// Create the drop indicator element
|
||||
const placeholder = document.createElement('div');
|
||||
placeholder.className = 'project-column';
|
||||
placeholder.innerHTML = `<div class="project-placeholder"></div>`;
|
||||
|
||||
|
||||
// Setup drag events for project reordering
|
||||
document.querySelectorAll('.project-column').forEach(col => {
|
||||
col.setAttribute('draggable', 'true');
|
||||
// Setup drag events for project reordering
|
||||
document.querySelectorAll('.project-column').forEach(col => {
|
||||
col.setAttribute('draggable', 'true');
|
||||
|
||||
col.addEventListener('dragstart', (e) => {
|
||||
e.dataTransfer.setData('text/plain', col.dataset.projectId);
|
||||
col.classList.add('dragging');
|
||||
});
|
||||
col.addEventListener('dragstart', (e) => {
|
||||
e.dataTransfer.setData('text/plain', col.dataset.projectId);
|
||||
col.classList.add('dragging');
|
||||
});
|
||||
|
||||
col.addEventListener('dragend', () => {
|
||||
col.classList.remove('dragging');
|
||||
placeholder.remove();
|
||||
});
|
||||
col.addEventListener('dragend', () => {
|
||||
col.classList.remove('dragging');
|
||||
placeholder.remove();
|
||||
});
|
||||
|
||||
col.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
const dragging = document.querySelector('.dragging');
|
||||
if (!dragging || dragging === col) return;
|
||||
col.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
const dragging = document.querySelector('.dragging');
|
||||
if (!dragging || dragging === col) return;
|
||||
|
||||
const bounding = col.getBoundingClientRect();
|
||||
const offset = e.clientY - bounding.top;
|
||||
const bounding = col.getBoundingClientRect();
|
||||
const offset = e.clientY - bounding.top;
|
||||
|
||||
if (offset < bounding.height / 2) {
|
||||
col.parentNode.insertBefore(placeholder, col);
|
||||
} else {
|
||||
col.parentNode.insertBefore(placeholder, col.nextSibling);
|
||||
}
|
||||
});
|
||||
if (offset < bounding.height / 2) {
|
||||
col.parentNode.insertBefore(placeholder, col);
|
||||
} else {
|
||||
col.parentNode.insertBefore(placeholder, col.nextSibling);
|
||||
}
|
||||
});
|
||||
|
||||
col.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
const dragging = document.querySelector('.dragging');
|
||||
if (placeholder && placeholder.parentNode) {
|
||||
placeholder.parentNode.insertBefore(dragging, placeholder);
|
||||
placeholder.remove();
|
||||
updateProjectOrder(); // This already exists in your code
|
||||
}
|
||||
});
|
||||
});
|
||||
col.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
const dragging = document.querySelector('.dragging');
|
||||
if (placeholder && placeholder.parentNode) {
|
||||
placeholder.parentNode.insertBefore(dragging, placeholder);
|
||||
placeholder.remove();
|
||||
updateProjectOrder(); // This already exists in your code
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// Enable drag-and-drop sorting
|
||||
@@ -914,10 +946,22 @@ document.querySelectorAll('.project-column').forEach(col => {
|
||||
.then(data => {
|
||||
bootstrap.Modal.getInstance(document.getElementById('loginModal')).hide();
|
||||
|
||||
// Show/hide settings button according to permissions
|
||||
if (!data.user.can_manage_settings) {
|
||||
document.querySelectorAll('.settings-btn').forEach(b => b.style.display = 'none');
|
||||
} else {
|
||||
document.querySelectorAll('.settings-btn').forEach(b => b.style.display = '');
|
||||
}
|
||||
|
||||
// Update user email or username if exists in the dropdown and refresh profiles + projects
|
||||
if (data.user) {
|
||||
const userEl = document.getElementById('dropdownUserEmail');
|
||||
if (userEl) userEl.textContent = (data.user.username && data.user.username.trim())
|
||||
? data.user.username
|
||||
: data.user.email;
|
||||
}
|
||||
|
||||
loadProfiles();
|
||||
fetchProjects();
|
||||
})
|
||||
.catch(ex => {
|
||||
@@ -930,6 +974,7 @@ document.querySelectorAll('.project-column').forEach(col => {
|
||||
document.getElementById('registerForm').addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
const email = document.getElementById('registerEmail').value.trim();
|
||||
const username = document.getElementById('registerUsername').value.trim();
|
||||
const password = document.getElementById('registerPassword').value;
|
||||
|
||||
const err = document.getElementById('registerError');
|
||||
@@ -940,7 +985,7 @@ document.querySelectorAll('.project-column').forEach(col => {
|
||||
fetch('register.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password })
|
||||
body: JSON.stringify({ email, username: username || null, password })
|
||||
})
|
||||
.then(async r => {
|
||||
const data = await r.json();
|
||||
@@ -957,6 +1002,22 @@ document.querySelectorAll('.project-column').forEach(col => {
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('profileSelect').addEventListener('change', () => {
|
||||
const profileId = parseInt(document.getElementById('profileSelect').value, 10);
|
||||
|
||||
fetch('set_profile.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ profile_id: profileId })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (!data.success) return;
|
||||
fetchProjects();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadSettings();
|
||||
|
||||
@@ -971,7 +1032,8 @@ document.querySelectorAll('.project-column').forEach(col => {
|
||||
|
||||
// Add user name to user menu
|
||||
if (me.logged_in) {
|
||||
document.getElementById('dropdownUserEmail').textContent = me.user.email;
|
||||
document.getElementById('dropdownUserEmail').textContent =
|
||||
(me.user.username && me.user.username.trim()) ? me.user.username : me.user.email;
|
||||
}
|
||||
|
||||
// Hide settings button if user can't manage settings
|
||||
@@ -979,6 +1041,7 @@ document.querySelectorAll('.project-column').forEach(col => {
|
||||
document.querySelectorAll('.settings-btn').forEach(b => b.style.display = 'none');
|
||||
}
|
||||
|
||||
loadProfiles();
|
||||
fetchProjects();
|
||||
});
|
||||
});
|
||||
@@ -996,6 +1059,12 @@ document.querySelectorAll('.project-column').forEach(col => {
|
||||
// Clear UI
|
||||
document.getElementById('projectGrid').innerHTML = '';
|
||||
|
||||
// Clear Profile List
|
||||
document.getElementById('profileSelect').innerHTML = '';
|
||||
|
||||
// Clear User Email
|
||||
document.getElementById('dropdownUserEmail').textContent = '';
|
||||
|
||||
// Show login modal
|
||||
new bootstrap.Modal(document.getElementById('loginModal')).show();
|
||||
})
|
||||
|
||||
53
login.php
53
login.php
@@ -5,19 +5,32 @@ require 'auth.php';
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$email = strtolower(trim($data['email'] ?? ''));
|
||||
|
||||
// Keep front-end field name "email" for compatibility; it can now be email OR username
|
||||
$identifierRaw = trim($data['email'] ?? '');
|
||||
$identifier = strtolower($identifierRaw);
|
||||
$password = strval($data['password'] ?? '');
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT u.id, u.email, u.password_hash,
|
||||
if ($identifier === '') {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'error' => 'Email or username is required']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$isEmail = filter_var($identifier, FILTER_VALIDATE_EMAIL) !== false;
|
||||
|
||||
$sql = "
|
||||
SELECT u.id, u.email, u.username, u.password_hash,
|
||||
r.name AS role_name,
|
||||
r.can_manage_settings
|
||||
FROM users u
|
||||
JOIN roles r ON r.id = u.role_id
|
||||
WHERE u.email = ?
|
||||
WHERE " . ($isEmail ? "u.email = ?" : "u.username = ?") . "
|
||||
LIMIT 1
|
||||
");
|
||||
$stmt->execute([$email]);
|
||||
";
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute([$identifier]);
|
||||
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$user || !password_verify($password, $user['password_hash'])) {
|
||||
@@ -27,18 +40,40 @@ if (!$user || !password_verify($password, $user['password_hash'])) {
|
||||
}
|
||||
|
||||
$_SESSION['user'] = [
|
||||
'id' => intval($user['id']),
|
||||
'id' => (int)$user['id'],
|
||||
'email' => $user['email'],
|
||||
'username' => $user['username'], // NEW
|
||||
'role' => $user['role_name'],
|
||||
'can_manage_settings' => intval($user['can_manage_settings']),
|
||||
'can_manage_settings' => (int)$user['can_manage_settings'],
|
||||
];
|
||||
|
||||
// Set active profile for this session (default profile if available)
|
||||
$stmt = $pdo->prepare("SELECT id FROM profiles WHERE user_id = ? AND is_default = 1 LIMIT 1");
|
||||
$stmt->execute([$_SESSION['user']['id']]);
|
||||
$profileId = $stmt->fetchColumn();
|
||||
|
||||
if (!$profileId) {
|
||||
$stmt = $pdo->prepare("SELECT id FROM profiles WHERE user_id = ? ORDER BY id ASC LIMIT 1");
|
||||
$stmt->execute([$_SESSION['user']['id']]);
|
||||
$profileId = $stmt->fetchColumn();
|
||||
}
|
||||
|
||||
if (!$profileId) {
|
||||
$stmt = $pdo->prepare("INSERT INTO profiles (user_id, name, is_default) VALUES (?, 'Default', 1)");
|
||||
$stmt->execute([$_SESSION['user']['id']]);
|
||||
$profileId = $pdo->lastInsertId();
|
||||
}
|
||||
|
||||
$_SESSION['active_profile_id'] = (int)$profileId;
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'user' => [
|
||||
'id' => $_SESSION['user']['id'],
|
||||
'email' => $_SESSION['user']['email'],
|
||||
'username' => $_SESSION['user']['username'], // NEW
|
||||
'role' => $_SESSION['user']['role'],
|
||||
'can_manage_settings' => $_SESSION['user']['can_manage_settings'],
|
||||
]
|
||||
],
|
||||
'active_profile_id' => $_SESSION['active_profile_id']
|
||||
]);
|
||||
|
||||
62
register.php
62
register.php
@@ -6,6 +6,8 @@ header('Content-Type: application/json');
|
||||
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
$email = strtolower(trim($data['email'] ?? ''));
|
||||
$usernameRaw = trim($data['username'] ?? '');
|
||||
$username = $usernameRaw === '' ? null : strtolower($usernameRaw);
|
||||
$password = strval($data['password'] ?? '');
|
||||
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
@@ -19,12 +21,31 @@ if (strlen($password) < 8) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Optional username validation
|
||||
if ($username !== null) {
|
||||
if (strlen($username) < 3 || strlen($username) > 50) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'error' => 'Username must be between 3 and 50 characters']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// letters/numbers + . _ - ; must start/end with letter/number
|
||||
if (!preg_match('/^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$/', $username)) {
|
||||
http_response_code(400);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => 'Username may contain letters, numbers, underscore, hyphen, dot, and must start/end with a letter or number'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
$hash = password_hash($password, PASSWORD_DEFAULT);
|
||||
|
||||
// Standard role id from roles table
|
||||
$stmt = $pdo->prepare("SELECT id FROM roles WHERE name = 'standard' LIMIT 1");
|
||||
$stmt->execute();
|
||||
$role_id = intval($stmt->fetchColumn());
|
||||
$role_id = (int)$stmt->fetchColumn();
|
||||
|
||||
if ($role_id <= 0) {
|
||||
http_response_code(500);
|
||||
@@ -33,10 +54,45 @@ if ($role_id <= 0) {
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $pdo->prepare("INSERT INTO users (email, password_hash, role_id) VALUES (?, ?, ?)");
|
||||
$stmt->execute([$email, $hash, $role_id]);
|
||||
$pdo->beginTransaction();
|
||||
|
||||
// Friendly conflict checks (DB unique constraints should still exist)
|
||||
$stmt = $pdo->prepare('SELECT 1 FROM users WHERE email = ? LIMIT 1');
|
||||
$stmt->execute([$email]);
|
||||
if ($stmt->fetchColumn()) {
|
||||
$pdo->rollBack();
|
||||
http_response_code(409);
|
||||
echo json_encode(['success' => false, 'error' => 'Email already in use']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($username !== null) {
|
||||
$stmt = $pdo->prepare('SELECT 1 FROM users WHERE username = ? LIMIT 1');
|
||||
$stmt->execute([$username]);
|
||||
if ($stmt->fetchColumn()) {
|
||||
$pdo->rollBack();
|
||||
http_response_code(409);
|
||||
echo json_encode(['success' => false, 'error' => 'Username already in use']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Create user (username is nullable)
|
||||
$stmt = $pdo->prepare("INSERT INTO users (email, username, password_hash, role_id) VALUES (?, ?, ?, ?)");
|
||||
$stmt->execute([$email, $username, $hash, $role_id]);
|
||||
$userId = (int)$pdo->lastInsertId();
|
||||
|
||||
// Create default profile for this user
|
||||
$stmt = $pdo->prepare("INSERT INTO profiles (user_id, name, is_default) VALUES (?, 'Default', 1)");
|
||||
$stmt->execute([$userId]);
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
|
||||
} catch (Throwable $e) {
|
||||
if ($pdo->inTransaction()) $pdo->rollBack();
|
||||
|
||||
http_response_code(409);
|
||||
echo json_encode(['success' => false, 'error' => 'Account already exists']);
|
||||
}
|
||||
|
||||
30
set_profile.php
Normal file
30
set_profile.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
require 'db.php';
|
||||
require 'auth.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
require_login();
|
||||
|
||||
$user_id = current_user_id();
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$profile_id = (int)($input['profile_id'] ?? 0);
|
||||
|
||||
if ($profile_id <= 0) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'error' => 'Invalid profile_id']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("SELECT id FROM profiles WHERE id = ? AND user_id = ?");
|
||||
$stmt->execute([$profile_id, $user_id]);
|
||||
|
||||
if (!$stmt->fetchColumn()) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['success' => false, 'error' => 'Profile not allowed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$_SESSION['active_profile_id'] = $profile_id;
|
||||
|
||||
echo json_encode(['success' => true, 'active_profile_id' => $profile_id]);
|
||||
Reference in New Issue
Block a user