Files
MrixsCraft-server/internal/templates/html/admin.html
Vladimir Zagainov 75ea7c70c2
Some checks failed
CI / lint (push) Failing after 15s
CI / test (push) Has been skipped
CI / build (push) Has been skipped
CI / docker (push) Has been skipped
auth: implement cookie-based auth for HTML endpoints and Bearer token auth for API endpoints
Details:

  • HTML endpoints (/, /profile, /admin, /login, /register):

    - Authenticate via HTTP-only cookie named 'token'

    - Handlers in internal/templates/templates.go check cookie validity

    - /admin endpoint additionally checks for role='admin'

    - Unauthenticated users redirected to /login

    - Non-admin users accessing /admin get HTTP 403 Forbidden

  • API endpoints (/api/*):

    - Authenticate via Bearer token in Authorization header only

    - Handlers in internal/api/api.go use authenticateRequest() function

    - Function extracts token from 'Authorization: Bearer <token>' header

    - Validates token against yggdrasil_sessions table

    - No cookie checking for API endpoints (launcher compatibility)

  • Web login (/api/web/login):

    - Sets HTTP-only cookie 'token' for browser storage

    - Returns JSON with token, UUID, username for JS localStorage

    - Maintains backward compatibility with existing JavaScript

  • JavaScript in HTML pages:

    - Gets token from localStorage (set by login response)

    - Sets Authorization: Bearer <token> header for API fetch calls

    - Updated admin.html and profile.js to include token in headers

This separation ensures:

  • HTML endpoints work automatically with browser cookies

  • API endpoints work with browsers (via JS) and launchers (Bearer tokens)

  • Security sensitive actions require proper role validation

  • Clean separation of concerns between document and API interfaces
2026-06-07 23:11:51 +03:00

264 lines
9.5 KiB
HTML

{{define "content"}}
<div class="container">
<h1>Админ-панель: Управление модпаками</h1>
<!-- Alerts -->
<div id="alert-error" class="alert alert-error"></div>
<div id="alert-success" class="alert alert-success"></div>
<!-- Modpacks List -->
<div class="card">
<h2>Список модпаков</h2>
<div id="modpacks-table">
<table>
<thead>
<tr>
<th>Slug</th>
<th>Название</th>
<th>Версия Minecraft</th>
<th>Версия Java</th>
<th>IP сервера</th>
<th>Активен</th>
<th>Действия</th>
</tr>
</thead>
<tbody id="modpacks-tbody">
<!-- Will be filled by JavaScript -->
</tbody>
</table>
</div>
</div>
<!-- Upload Form -->
<div class="card">
<h2>Загрузка нового модпака</h2>
<form id="modpack-form">
<div class="grid-2">
<div>
<label for="slug">Slug (уникальный идентификатор)</label>
<input type="text" id="slug" name="slug" required>
</div>
<div>
<label for="name">Название</label>
<input type="text" id="name" name="name" required>
</div>
<div>
<label for="minecraft_version">Версия Minecraft</label>
<input type="text" id="minecraft_version" name="minecraft_version" required>
</div>
<div>
<label for="java_version">Версия Java</label>
<input type="number" id="java_version" name="java_version" min="8" required>
</div>
<div>
<label for="server_ip">IP сервера</label>
<input type="text" id="server_ip" name="server_ip" placeholder="Например: play.mrixs.me">
</div>
</div>
<button type="submit" class="btn">Создать модпак</button>
</form>
<hr>
<h3>Загрузка файлов для модпака</h3>
<form id="upload-form" enctype="multipart/form-data">
<div>
<label for="upload-slug">Slug модпака</label>
<input type="text" id="upload-slug" name="slug" list="modpack-slugs" required>
<datalist id="modpack-slugs"></datalist>
</div>
<div>
<label for="files">Файлы (перетащите или выберите)</label>
<input type="file" id="files" name="files" multiple>
<p>Поддерживаются архивы .zip ( будут распакованы) и отдельные файлы.</p>
</div>
<div id="upload-progress" style="display: none; margin-top: 1rem;">
<div>Прогресс: <span id="upload-percent">0%</span></div>
<progress id="upload-bar" value="0" max="100"></progress>
</div>
<button type="submit" class="btn" id="upload-btn">Загрузить файлы</button>
</form>
<div id="upload-result" style="margin-top: 1rem;"></div>
</div>
</div>
<script>
// Helper functions
function showAlert(error, success) {
const errorAlert = document.getElementById('alert-error');
const successAlert = document.getElementById('alert-success');
if (error) {
errorAlert.textContent = error;
errorAlert.classList.add('show');
successAlert.classList.remove('show');
} else if (success) {
successAlert.textContent = success;
successAlert.classList.add('show');
errorAlert.classList.remove('show');
} else {
errorAlert.classList.remove('show');
successAlert.classList.remove('show');
}
}
function getToken() {
return localStorage.getItem('token');
}
// Fetch and display modpacks
async function loadModpacks() {
const token = getToken();
if (!token) {
window.location.href = '/login';
return;
}
try {
const response = await fetch('/api/admin/modpacks', {
headers: {
'Authorization': 'Bearer ' + token
}
});
if (!response.ok) {
throw new Error('Failed to fetch modpacks');
}
const modpacks = await response.json();
const tbody = document.getElementById('modpacks-tbody');
const datalist = document.getElementById('modpack-slugs');
// Clear existing
tbody.innerHTML = '';
datalist.innerHTML = '';
modpacks.forEach(mp => {
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${mp.slug}</td>
<td>${mp.name}</td>
<td>${mp.minecraft_version}</td>
<td>${mp.java_version}</td>
<td>${mp.server_ip || '-'}</td>
<td>${mp.is_active ? 'Да' : 'Нет'}</td>
<td>
<!-- We don't have edit/delete endpoints in API yet, but we can add later -->
<button class="btn btn-sm btn-outline" onclick="loadModpackForEdit('${mp.slug}')">Редактировать</button>
</td>
`;
tbody.appendChild(tr);
// Add to datalist for upload form
const option = document.createElement('option');
option.value = mp.slug;
datalist.appendChild(option);
});
} catch (err) {
showAlert('Ошибка загрузки модпаков: ' + err.message, null);
console.error(err);
}
}
// Handle modpack creation
document.getElementById('modpack-form').addEventListener('submit', async (e) => {
e.preventDefault();
const token = getToken();
if (!token) return;
const formData = new FormData(e.target);
const data = {
slug: formData.get('slug'),
name: formData.get('name'),
minecraft_version: formData.get('minecraft_version'),
java_version: parseInt(formData.get('java_version')),
server_ip: formData.get('server_ip')
};
try {
const response = await fetch('/api/admin/modpacks', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
},
body: JSON.stringify(data)
});
if (response.ok) {
showAlert(null, 'Модпак успешно создан');
e.target.reset();
loadModpacks(); // Refresh list
} else {
const error = await response.json();
showAlert(error.errorMessage || 'Ошибка создания модпака', null);
}
} catch (err) {
showAlert('Ошибка сети: ' + err.message, null);
}
});
// Handle file upload
document.getElementById('upload-form').addEventListener('submit', async (e) => {
e.preventDefault();
const token = getToken();
if (!token) return;
const formData = new FormData(e.target);
const slug = formData.get('slug');
const files = document.getElementById('files').files;
if (files.length === 0) {
showAlert('Пожалуйста, выберите файлы для загрузки', null);
return;
}
// Show progress
const progressDiv = document.getElementById('upload-progress');
const percentSpan = document.getElementById('upload-percent');
const progressBar = document.getElementById('upload-bar');
progressDiv.style.display = 'block';
percentSpan.textContent = '0%';
progressBar.value = 0;
try {
// We'll use the existing upload endpoint: POST /api/admin/modpacks/{slug}/upload
const response = await fetch(`/api/admin/modpacks/${slug}/upload`, {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + token
},
body: formData
});
if (response.ok) {
const result = await response.json();
showAlert(null, `Загружено файлов: ${result.count}`);
document.getElementById('upload-form').reset();
progressDiv.style.display = 'none';
} else {
const error = await response.json();
showAlert(error.errorMessage || 'Ошибка загрузки файлов', null);
}
} catch (err) {
showAlert('Ошибка сети: ' + err.message, null);
} finally {
progressDiv.style.display = 'none';
}
});
// Simulate upload progress (since we don't have progress events from fetch with FormData easily)
// For a real progress bar, we'd need to use XMLHttpSocket or a custom backend endpoint.
// For now, we'll just show indeterminate or fake progress.
// We'll update this if we decide to implement proper progress later.
// Placeholder for edit function (to be implemented if we add edit endpoint)
function loadModpackForEdit(slug) {
alert('Редактирование пока не реализовано. Используйте API напрямую.');
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
loadModpacks();
});
</script>
{{end}}