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Версия JavaIP сервераАктивенДействия
+
+
+ + +
+

Загрузка нового модпака

+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+ +

Загрузка файлов для модпака

+
+
+ + + +
+
+ + +

Поддерживаются архивы .zip ( будут распакованы) и отдельные файлы.

+
+ + +
+
+
+
+ + +{{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 = - 'Главная' + - 'Профиль' + - '' + - 'Выйти'; - 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 + + '' + + 'Выйти'; + + 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 = + 'Главная' + + 'Профиль' + + '' + + 'Выйти'; + 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 {