togethere.cloud/public_html/administration/users/setting/index.php

592 lines
23 KiB
PHP

<?php
require_once __DIR__ . '/../../includes/auth.php';
require_once __DIR__ . '/../../includes/config.php';
require_once __DIR__ . '/../../includes/header.php';
require_once __DIR__ . '/../../includes/sidebar.php';
try {
$pdo->exec(
"CREATE TABLE IF NOT EXISTS blocked_usernames (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(20) NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_by INT NULL,
UNIQUE KEY unique_blocked_username (name),
KEY idx_blocked_created_by (created_by)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
);
} catch (Throwable $e) {
}
$blockedRows = [];
try {
$stmt = $pdo->prepare(
"SELECT b.id, b.name, b.created_at, b.created_by, u.username AS created_by_username
FROM blocked_usernames b
LEFT JOIN users u ON u.id = b.created_by
ORDER BY b.created_at DESC, b.id DESC
LIMIT 100"
);
$stmt->execute();
$blockedRows = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
} catch (Throwable $e) {
$blockedRows = [];
}
$suspendedRows = [];
try {
$stmt2 = $pdo->prepare(
"SELECT u.id, u.username, u.email,
COALESCE(u.suspension_reason, '') AS suspension_reason,
u.suspended_until,
a.username AS suspended_by_username
FROM users u
LEFT JOIN users a ON a.id = u.suspended_by
WHERE u.account_suspended = 1
ORDER BY u.id DESC
LIMIT 100"
);
$stmt2->execute();
$suspendedRows = $stmt2->fetchAll(PDO::FETCH_ASSOC) ?: [];
} catch (Throwable $e) {
// Columns may not exist yet
try {
$stmt2b = $pdo->prepare(
"SELECT id, username, email, '' AS suspension_reason, NULL AS suspended_until, NULL AS suspended_by_username
FROM users WHERE account_suspended = 1 ORDER BY id DESC LIMIT 100"
);
$stmt2b->execute();
$suspendedRows = $stmt2b->fetchAll(PDO::FETCH_ASSOC) ?: [];
} catch (Throwable $e2) {
$suspendedRows = [];
}
}
?>
<div class="admin-content">
<style>
.admin-page-title {
font-size: 24px;
font-weight: 600;
color: #23282d;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #0073aa;
}
.admin-content-box {
background: #fff;
border: 1px solid #ddd;
border-radius: 4px;
padding: 20px;
margin-top: 20px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.blocked-form {
display: grid;
grid-template-columns: 1fr auto;
gap: 10px;
margin-bottom: 14px;
align-items: end;
}
.blocked-form label {
display: block;
margin-bottom: 6px;
font-size: 13px;
font-weight: 600;
color: #444;
}
.blocked-form input[type="text"] {
width: 100%;
min-height: 38px;
border: 1px solid #d0d7de;
border-radius: 6px;
padding: 8px 12px;
font-size: 14px;
outline: none;
}
.blocked-form input[type="text"]:focus {
border-color: #0073aa;
box-shadow: 0 0 0 3px rgba(0,115,170,0.12);
}
.blocked-btn {
min-height: 38px;
border: 0;
border-radius: 6px;
padding: 9px 14px;
background: #0073aa;
color: #fff;
font-size: 13px;
font-weight: 700;
cursor: pointer;
}
.blocked-btn:hover {
background: #005f8d;
}
.blocked-status {
min-height: 20px;
font-size: 13px;
margin-bottom: 12px;
color: #666;
}
.blocked-status.success {
color: #1e7e34;
font-weight: 600;
}
.blocked-status.error {
color: #b32d2e;
font-weight: 600;
}
.blocked-help {
font-size: 12px;
color: #666;
margin-bottom: 14px;
line-height: 1.45;
}
.blocked-table-wrap {
overflow-x: auto;
border: 1px solid #e6e6e6;
border-radius: 8px;
}
.blocked-table {
width: 100%;
min-width: 560px;
border-collapse: collapse;
background: #fff;
}
.blocked-table th,
.blocked-table td {
padding: 10px 12px;
border-bottom: 1px solid #eee;
text-align: left;
font-size: 13px;
}
.blocked-table th {
background: #f8f9fa;
color: #23282d;
font-weight: 700;
white-space: nowrap;
}
.blocked-table tbody tr:hover {
background: #fafcff;
}
.blocked-empty {
padding: 16px;
text-align: center;
font-size: 13px;
color: #666;
}
@media (max-width: 720px) {
.blocked-form {
grid-template-columns: 1fr;
}
}
/* Custom Modal */
.su-modal-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.55);
z-index: 10000;
align-items: center;
justify-content: center;
animation: suFadeIn 0.2s ease-out;
}
.su-modal-overlay.active { display: flex; }
.su-modal-box {
background: #fff;
border-radius: 8px;
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
max-width: 520px;
width: 90%;
animation: suSlideUp 0.25s ease-out;
}
.su-modal-head {
padding: 18px 22px;
border-radius: 8px 8px 0 0;
display: flex;
align-items: center;
gap: 10px;
color: #fff;
}
.su-modal-head.green { background: linear-gradient(135deg,#28a745,#1e7e34); }
.su-modal-head.blue { background: linear-gradient(135deg,#0073aa,#005a87); }
.su-modal-head-icon { font-size: 24px; }
.su-modal-head-title { font-size: 17px; font-weight: 700; margin: 0; }
.su-modal-body {
padding: 22px;
font-size: 14px;
color: #444;
line-height: 1.6;
}
.su-modal-body textarea {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 13px;
resize: vertical;
box-sizing: border-box;
margin-top: 8px;
}
.su-modal-body textarea:focus {
outline: none;
border-color: #0073aa;
box-shadow: 0 0 0 3px rgba(0,115,170,0.1);
}
.su-modal-foot {
padding: 14px 22px;
border-top: 1px solid #eee;
display: flex;
gap: 10px;
justify-content: flex-end;
}
.su-modal-btn {
padding: 9px 20px;
border: none;
border-radius: 5px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.su-modal-btn-primary { background: #28a745; color: #fff; }
.su-modal-btn-primary:hover { background: #1e7e34; transform: translateY(-1px); }
.su-modal-btn-secondary { background: #f1f1f1; color: #444; border: 1px solid #ddd; }
.su-modal-btn-secondary:hover { background: #e2e2e2; }
@keyframes suFadeIn { from { opacity:0 } to { opacity:1 } }
@keyframes suSlideUp { from { transform:translateY(40px);opacity:0 } to { transform:translateY(0);opacity:1 } }
</style>
<h1 class="admin-page-title">⚙️ Użytkownicy - Ustawienia</h1>
<div class="admin-content-box">
<h3 style="margin:0 0 12px 0; color:#23282d;">Blokowane nazwy użytkowników</h3>
<div class="blocked-help">
Endpoint: <strong>/admin/user/settings/blocked-names</strong> • metody: <strong>POST, DELETE</strong> • format nazwy: <strong>[A-Za-z0-9_&!]{1,20}</strong>
</div>
<form id="blockedNameForm" class="blocked-form">
<div>
<label for="blockedNameInput">Nazwa użytkownika do zablokowania</label>
<input type="text" id="blockedNameInput" maxlength="20" placeholder="np. admin" required>
</div>
<button class="blocked-btn" type="submit">Zablokuj nazwę</button>
</form>
<div id="blockedStatus" class="blocked-status"></div>
<div class="blocked-table-wrap">
<table class="blocked-table" aria-label="Zablokowane nazwy użytkowników">
<thead>
<tr>
<th style="width:70px;">ID</th>
<th>Nazwa</th>
<th style="width:190px;">Data dodania</th>
<th style="width:220px;">Dodane przez</th>
<th style="width:120px;">Akcja</th>
</tr>
</thead>
<tbody id="blockedTableBody">
<?php if (empty($blockedRows)): ?>
<tr>
<td colspan="5" class="blocked-empty">Brak zablokowanych nazw</td>
</tr>
<?php else: ?>
<?php foreach ($blockedRows as $row): ?>
<tr id="blocked-row-<?php echo (int)($row['id'] ?? 0); ?>">
<td><?php echo (int)($row['id'] ?? 0); ?></td>
<td><strong><?php echo htmlspecialchars((string)($row['name'] ?? ''), ENT_QUOTES, 'UTF-8'); ?></strong></td>
<td><?php echo htmlspecialchars((string)($row['created_at'] ?? ''), ENT_QUOTES, 'UTF-8'); ?></td>
<td>
<?php
$createdByUsername = (string)($row['created_by_username'] ?? '');
$createdById = (int)($row['created_by'] ?? 0);
if ($createdByUsername !== '') {
echo htmlspecialchars($createdByUsername, ENT_QUOTES, 'UTF-8') . ' (#' . $createdById . ')';
} elseif ($createdById > 0) {
echo '#' . $createdById;
} else {
echo '-';
}
?>
</td>
<td>
<button
type="button"
class="blocked-btn"
style="background:#dc3545;font-size:12px;padding:6px 10px;"
onclick="deleteBlockedName(<?php echo (int)($row['id'] ?? 0); ?>, '<?php echo htmlspecialchars((string)($row['name'] ?? ''), ENT_QUOTES, 'UTF-8'); ?>')"
>
Usuń
</button>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<div class="admin-content-box" style="margin-top:20px;">
<h3 style="margin:0 0 12px 0; color:#23282d;">🚫 Zawieszeni gracze</h3>
<div id="suspendStatus" class="blocked-status"></div>
<div class="blocked-table-wrap">
<table class="blocked-table" aria-label="Zawieszeni gracze">
<thead>
<tr>
<th style="width:60px;">ID</th>
<th>Username</th>
<th>Email</th>
<th>Powód zawieszenia</th>
<th style="width:160px;">Zawieszony do</th>
<th style="width:100px;">Akcja</th>
</tr>
</thead>
<tbody>
<?php if (empty($suspendedRows)): ?>
<tr>
<td colspan="6" class="blocked-empty">Brak zawieszonych graczy</td>
</tr>
<?php else: ?>
<?php foreach ($suspendedRows as $sr): ?>
<tr id="suspend-row-<?php echo (int)$sr['id']; ?>">
<td><?php echo (int)$sr['id']; ?></td>
<td><strong><?php echo htmlspecialchars((string)($sr['username'] ?? ''), ENT_QUOTES, 'UTF-8'); ?></strong></td>
<td style="font-size:12px;"><?php echo htmlspecialchars((string)($sr['email'] ?? ''), ENT_QUOTES, 'UTF-8'); ?></td>
<td style="font-size:12px;"><?php echo htmlspecialchars((string)($sr['suspension_reason'] ?? '-'), ENT_QUOTES, 'UTF-8'); ?></td>
<td style="font-size:12px;"><?php echo $sr['suspended_until'] ? htmlspecialchars((string)$sr['suspended_until'], ENT_QUOTES, 'UTF-8') : 'bezterminowo'; ?></td>
<td>
<button class="blocked-btn" style="background:#28a745;font-size:12px;padding:6px 10px;"
onclick="unsuspendUser(<?php echo (int)$sr['id']; ?>)">
Odwieś
</button>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<!-- Custom Modal -->
<div id="suModal" class="su-modal-overlay">
<div class="su-modal-box">
<div id="suModalHead" class="su-modal-head green">
<span id="suModalIcon" class="su-modal-head-icon"></span>
<h3 id="suModalTitle" class="su-modal-head-title"></h3>
</div>
<div id="suModalBody" class="su-modal-body"></div>
<div id="suModalFoot" class="su-modal-foot"></div>
</div>
</div>
<script>
function suShowPrompt(title, message, defaultVal, icon, colorClass) {
return new Promise((resolve) => {
const modal = document.getElementById('suModal');
document.getElementById('suModalHead').className = 'su-modal-head ' + (colorClass || 'blue');
document.getElementById('suModalIcon').textContent = icon || '✏️';
document.getElementById('suModalTitle').textContent = title;
document.getElementById('suModalBody').innerHTML =
'<p style="margin:0 0 6px;">' + message + '</p>' +
'<textarea id="suPromptVal" rows="3">' + (defaultVal || '') + '</textarea>';
document.getElementById('suModalFoot').innerHTML =
'<button class="su-modal-btn su-modal-btn-secondary" id="suBtnCancel">Anuluj</button>' +
'<button class="su-modal-btn su-modal-btn-primary" id="suBtnOk">✅ Potwierdź</button>';
modal.classList.add('active');
setTimeout(() => { const t = document.getElementById('suPromptVal'); if (t) { t.focus(); t.select(); } }, 50);
document.getElementById('suBtnOk').onclick = () => {
const v = document.getElementById('suPromptVal').value;
modal.classList.remove('active');
resolve(v);
};
document.getElementById('suBtnCancel').onclick = () => {
modal.classList.remove('active');
resolve(null);
};
modal.onclick = (e) => { if (e.target === modal) { modal.classList.remove('active'); resolve(null); } };
});
}
async function unsuspendUser(userId) {
const reasonInput = await suShowPrompt(
'✅ Odwieszenie konta #' + userId,
'Podaj powód odwieszenia (opcjonalnie):',
'Decyzja administracyjna po weryfikacji sytuacji.',
'✅', 'green'
);
if (reasonInput === null) return;
const reason = (reasonInput || '').trim();
const statusEl = document.getElementById('suspendStatus');
statusEl.textContent = 'Przetwarzanie...';
statusEl.className = 'blocked-status';
try {
const resp = await fetch('../../../api/suspendUser.php', {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'unsuspend', user_id: userId, reason })
});
const text = await resp.text();
let json = null;
try {
json = JSON.parse(text);
} catch (_) {
throw new Error('API zwróciło nieprawidłową odpowiedź (oczekiwano JSON).');
}
if (!resp.ok || !json.success) {
throw new Error(json.error || 'Błąd odwieszenia');
}
statusEl.textContent = json.message || 'Konto zostało odwieszone.';
statusEl.className = 'blocked-status success';
const row = document.getElementById('suspend-row-' + userId);
if (row) row.remove();
} catch (err) {
statusEl.textContent = err.message || 'Błąd podczas odwieszania.';
statusEl.className = 'blocked-status error';
}
}
</script>
<script>
(function () {
const form = document.getElementById('blockedNameForm');
const input = document.getElementById('blockedNameInput');
const statusEl = document.getElementById('blockedStatus');
const blockedTableBody = document.getElementById('blockedTableBody');
const endpoint = '/admin/user/settings/blocked-names/index.php';
function setStatus(message, type) {
statusEl.textContent = message || '';
statusEl.classList.remove('success', 'error');
if (type) statusEl.classList.add(type);
}
async function parseApiResponse(response) {
const rawText = await response.text();
if (!rawText) {
throw new Error('API zwróciło pustą odpowiedź.');
}
try {
return JSON.parse(rawText);
} catch (_) {
throw new Error('API zwróciło nieprawidłową odpowiedź (oczekiwano JSON).');
}
}
function renderEmptyBlockedTable() {
if (!blockedTableBody) return;
const stillRows = blockedTableBody.querySelectorAll('tr[id^="blocked-row-"]');
if (stillRows.length > 0) return;
blockedTableBody.innerHTML = '<tr><td colspan="5" class="blocked-empty">Brak zablokowanych nazw</td></tr>';
}
window.deleteBlockedName = async function (blockedId, blockedName) {
if (!blockedId) {
setStatus('Nieprawidłowy identyfikator blokady.', 'error');
return;
}
const accepted = window.confirm('Czy na pewno chcesz usunąć blokadę nazwy "' + blockedName + '"?');
if (!accepted) return;
setStatus('Usuwanie blokady...', null);
try {
const response = await fetch(endpoint, {
method: 'DELETE',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ id: blockedId })
});
const json = await parseApiResponse(response);
if (!response.ok) {
throw new Error((json && json.message) ? json.message : 'Nie udało się usunąć blokady nazwy.');
}
const row = document.getElementById('blocked-row-' + blockedId);
if (row) row.remove();
renderEmptyBlockedTable();
setStatus(json.message || 'Nazwa została usunięta z listy zablokowanych.', 'success');
} catch (error) {
setStatus(error && error.message ? error.message : 'Błąd podczas usuwania blokady.', 'error');
}
};
form.addEventListener('submit', async function (e) {
e.preventDefault();
const value = (input.value || '').trim();
if (value === '') {
setStatus('Nazwa użytkownika nie może być pusta.', 'error');
return;
}
setStatus('Zapisywanie...', null);
try {
const response = await fetch(endpoint, {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ name: value })
});
const json = await parseApiResponse(response);
if (!response.ok) {
throw new Error((json && json.message) ? json.message : 'Nie udało się zablokować nazwy.');
}
setStatus(json.message || 'Nazwa użytkownika została zablokowana pomyślnie.', 'success');
input.value = '';
setTimeout(function () {
window.location.reload();
}, 500);
} catch (error) {
setStatus(error && error.message ? error.message : 'Błąd podczas zapisu.', 'error');
}
});
})();
</script>
</div>
<?php
require_once __DIR__ . '/../../includes/footer.php';
?>