diff --git a/internal/api/api.go b/internal/api/api.go
index 887913a..3721020 100644
--- a/internal/api/api.go
+++ b/internal/api/api.go
@@ -50,6 +50,7 @@ func (h *Handler) RegisterRoutes(mux *http.ServeMux) {
// Website endpoints.
mux.HandleFunc("POST /api/web/register", h.register)
mux.HandleFunc("POST /api/web/login", h.webLogin)
+ mux.HandleFunc("GET /api/web/me", h.webMe)
mux.HandleFunc("POST /api/web/profile/skin", h.uploadSkin)
mux.HandleFunc("POST /api/web/profile/cape", h.uploadCape)
mux.HandleFunc("DELETE /api/web/profile/skin", h.deleteSkin)
@@ -429,6 +430,29 @@ func (h *Handler) authenticateRequest(w http.ResponseWriter, r *http.Request) in
return userID
}
+// webMe returns the current authenticated user's info (for checking admin status in UI).
+func (h *Handler) webMe(w http.ResponseWriter, r *http.Request) {
+ userID := h.authenticateRequest(w, r)
+ if userID == 0 {
+ return
+ }
+
+ var username, uuid, role string
+ err := h.db.Pool().QueryRow(r.Context(),
+ `SELECT username, uuid, role FROM users WHERE id = $1`, userID,
+ ).Scan(&username, &uuid, &role)
+ if err != nil {
+ utils.WriteError(w, http.StatusInternalServerError, "Database error")
+ return
+ }
+
+ utils.WriteJSON(w, http.StatusOK, map[string]string{
+ "username": username,
+ "uuid": uuid,
+ "role": role,
+ })
+}
+
func (h *Handler) launcherLatest(w http.ResponseWriter, r *http.Request) {
osParam := r.URL.Query().Get("os")
archParam := r.URL.Query().Get("arch")
diff --git a/internal/templates/html/admin.html b/internal/templates/html/admin.html
new file mode 100644
index 0000000..81ebb35
--- /dev/null
+++ b/internal/templates/html/admin.html
@@ -0,0 +1,264 @@
+{{define "content"}}
+
+
Админ-панель: Управление модпаками
+
+
+
+
+
+
+
+
Список модпаков
+
+
+
+
+ | Slug |
+ Название |
+ Версия Minecraft |
+ Версия Java |
+ IP сервера |
+ Активен |
+ Действия |
+
+
+
+
+
+
+
+
+
+
+
+
Загрузка нового модпака
+
+
+
+
+
Загрузка файлов для модпака
+
+
+
+
+
+
+{{end}}
\ No newline at end of file
diff --git a/internal/templates/html/base.html b/internal/templates/html/base.html
index 62cb67e..7e6840e 100644
--- a/internal/templates/html/base.html
+++ b/internal/templates/html/base.html
@@ -357,24 +357,63 @@
const username = localStorage.getItem('username');
const nav = document.getElementById('mainNav');
if (token && username) {
- nav.innerHTML =
- 'Главная' +
- 'Профиль' +
- '' + escapeHtml(username) + '
' +
- 'Выйти';
- document.getElementById('nav-logout').addEventListener('click', function(e) {
- e.preventDefault();
- localStorage.removeItem('token');
- localStorage.removeItem('uuid');
- localStorage.removeItem('username');
- window.location.href = '/';
+ // Fetch user info to check role
+ fetch('/api/web/me', {
+ headers: {'Authorization': 'Bearer ' + token}
+ })
+ .then(response => {
+ if (!response.ok) throw new Error('Not authenticated');
+ return response.json();
+ })
+ .then(data => {
+ let adminLink = '';
+ if (data.role === 'admin') {
+ adminLink = 'Админ-панель';
+ }
+
+ nav.innerHTML =
+ 'Главная' +
+ 'Профиль' +
+ adminLink +
+ '' + escapeHtml(username) + '
' +
+ 'Выйти';
+
+ document.getElementById('nav-logout').addEventListener('click', function(e) {
+ e.preventDefault();
+ localStorage.removeItem('token');
+ localStorage.removeItem('uuid');
+ localStorage.removeItem('username');
+ window.location.href = '/';
+ });
+
+ // Highlight active nav link
+ const path = window.location.pathname;
+ document.querySelectorAll('header nav a').forEach(function(a) {
+ if (a.getAttribute('href') === path) a.classList.add('active');
+ });
+ })
+ .catch(err => {
+ // If we can't fetch user info, still show basic nav
+ nav.innerHTML =
+ 'Главная' +
+ 'Профиль' +
+ '' + escapeHtml(username) + '
' +
+ 'Выйти';
+ document.getElementById('nav-logout').addEventListener('click', function(e) {
+ e.preventDefault();
+ localStorage.removeItem('token');
+ localStorage.removeItem('uuid');
+ localStorage.removeItem('username');
+ window.location.href = '/';
+ });
+
+ // Highlight active nav link
+ const path = window.location.pathname;
+ document.querySelectorAll('header nav a').forEach(function(a) {
+ if (a.getAttribute('href') === path) a.classList.add('active');
+ });
});
}
- // Highlight active nav link
- const path = window.location.pathname;
- document.querySelectorAll('header nav a').forEach(function(a) {
- if (a.getAttribute('href') === path) a.classList.add('active');
- });
})();
function escapeHtml(s) {
const d = document.createElement('div');
diff --git a/internal/templates/templates.go b/internal/templates/templates.go
index 3cae8cc..a474d57 100644
--- a/internal/templates/templates.go
+++ b/internal/templates/templates.go
@@ -41,6 +41,7 @@ func (h *Handler) RegisterRoutes(mux *http.ServeMux) {
mux.HandleFunc("GET /login", h.loginPage)
mux.HandleFunc("GET /register", h.registerPage)
mux.HandleFunc("GET /profile", h.profilePage)
+ mux.HandleFunc("GET /admin", h.adminPage)
}
// ── Page handlers ──────────────────────────────────────────────
@@ -65,6 +66,13 @@ func (h *Handler) profilePage(w http.ResponseWriter, r *http.Request) {
h.render(w, "profile.html", pageData{Title: "Профиль"})
}
+func (h *Handler) adminPage(w http.ResponseWriter, r *http.Request) {
+ // Check if user is logged in via token in cookie or localStorage?
+ // For simplicity, we rely on the API endpoints to check auth.
+ // We'll just render the admin page; the JS will check for token and redirect to login if needed.
+ h.render(w, "admin.html", pageData{Title: "Админ-панель"})
+}
+
// render executes the "base.html" template which {{template "content" .}}
// pulls in the per-page content block.
func (h *Handler) render(w http.ResponseWriter, page string, data pageData) {
@@ -88,7 +96,7 @@ func (h *Handler) render(w http.ResponseWriter, page string, data pageData) {
// multiple {{define "content"}} blocks in wildcard-parsed files overwrite
// each other (last alphabetically wins).
func (h *Handler) parseTemplates() {
- pages := []string{"index.html", "login.html", "register.html", "profile.html"}
+ pages := []string{"index.html", "login.html", "register.html", "profile.html", "admin.html"}
for _, page := range pages {
tmpl, err := template.New("base.html").Funcs(template.FuncMap{}).ParseFS(templateFS, "html/base.html", "html/"+page)
if err != nil {