2026-05-25 16:52:57 +03:00
|
|
|
package handlers
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"crypto/sha256"
|
|
|
|
|
"encoding/hex"
|
|
|
|
|
"net/http"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"warpbox.dev/backend/libs/services"
|
|
|
|
|
"warpbox.dev/backend/libs/web"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const adminCookieName = "warpbox_admin"
|
|
|
|
|
|
|
|
|
|
type adminPageData struct {
|
|
|
|
|
Stats services.AdminStats
|
|
|
|
|
Boxes []adminBoxView
|
|
|
|
|
Error string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type adminBoxView struct {
|
|
|
|
|
ID string
|
|
|
|
|
CreatedAt string
|
|
|
|
|
ExpiresAt string
|
|
|
|
|
FileCount int
|
|
|
|
|
TotalSizeLabel string
|
|
|
|
|
DownloadCount int
|
|
|
|
|
MaxDownloads int
|
|
|
|
|
Protected bool
|
|
|
|
|
Expired bool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *App) AdminLogin(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if a.isAdmin(r) {
|
|
|
|
|
http.Redirect(w, r, "/admin", http.StatusSeeOther)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
a.renderAdminLogin(w, http.StatusOK, "")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *App) AdminLoginPost(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if err := r.ParseForm(); err != nil {
|
|
|
|
|
a.renderAdminLogin(w, http.StatusBadRequest, "Unable to read login form.")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if a.cfg.AdminToken == "" || r.FormValue("token") != a.cfg.AdminToken {
|
|
|
|
|
a.logger.Warn("admin login failed", "source", "admin", "severity", "warn", "code", 4301)
|
|
|
|
|
a.renderAdminLogin(w, http.StatusUnauthorized, "Invalid admin token.")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
http.SetCookie(w, &http.Cookie{
|
|
|
|
|
Name: adminCookieName,
|
|
|
|
|
Value: adminCookieValue(a.cfg.AdminToken),
|
|
|
|
|
Path: "/admin",
|
|
|
|
|
HttpOnly: true,
|
|
|
|
|
SameSite: http.SameSiteLaxMode,
|
|
|
|
|
Secure: r.TLS != nil,
|
|
|
|
|
Expires: time.Now().Add(12 * time.Hour),
|
|
|
|
|
})
|
|
|
|
|
a.logger.Info("admin login", "source", "admin", "severity", "user_activity", "code", 2301)
|
|
|
|
|
http.Redirect(w, r, "/admin", http.StatusSeeOther)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *App) AdminLogout(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
http.SetCookie(w, &http.Cookie{
|
|
|
|
|
Name: adminCookieName,
|
|
|
|
|
Value: "",
|
|
|
|
|
Path: "/admin",
|
|
|
|
|
HttpOnly: true,
|
|
|
|
|
SameSite: http.SameSiteLaxMode,
|
|
|
|
|
MaxAge: -1,
|
|
|
|
|
})
|
|
|
|
|
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *App) AdminDashboard(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if !a.requireAdmin(w, r) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stats, err := a.uploadService.AdminStats()
|
|
|
|
|
if err != nil {
|
|
|
|
|
http.Error(w, "unable to load admin stats", http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
boxes, err := a.adminBoxes(8)
|
|
|
|
|
if err != nil {
|
|
|
|
|
http.Error(w, "unable to load recent boxes", http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
a.renderer.Render(w, http.StatusOK, "admin.html", web.PageData{
|
|
|
|
|
Title: "Admin overview",
|
|
|
|
|
Description: "Warpbox admin overview.",
|
|
|
|
|
Data: adminPageData{
|
|
|
|
|
Stats: stats,
|
|
|
|
|
Boxes: boxes,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *App) AdminFiles(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if !a.requireAdmin(w, r) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stats, err := a.uploadService.AdminStats()
|
|
|
|
|
if err != nil {
|
|
|
|
|
http.Error(w, "unable to load admin stats", http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
boxes, err := a.adminBoxes(100)
|
|
|
|
|
if err != nil {
|
|
|
|
|
http.Error(w, "unable to load boxes", http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
a.renderer.Render(w, http.StatusOK, "admin.html", web.PageData{
|
|
|
|
|
Title: "Admin files",
|
|
|
|
|
Description: "Manage Warpbox uploads.",
|
|
|
|
|
Data: adminPageData{
|
|
|
|
|
Stats: stats,
|
|
|
|
|
Boxes: boxes,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *App) AdminDeleteBox(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if !a.requireAdmin(w, r) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
boxID := r.PathValue("boxID")
|
|
|
|
|
if err := a.uploadService.DeleteBox(boxID); err != nil {
|
|
|
|
|
a.logger.Warn("admin delete failed", "source", "admin", "severity", "warn", "code", 4302, "box_id", boxID, "error", err.Error())
|
|
|
|
|
http.Error(w, "unable to delete box", http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
http.Redirect(w, r, "/admin/files", http.StatusSeeOther)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 17:05:59 +03:00
|
|
|
func (a *App) AdminViewBox(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if !a.requireAdmin(w, r) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
box, err := a.uploadService.GetBox(r.PathValue("boxID"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
http.NotFound(w, r)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if a.uploadService.IsProtected(box) {
|
|
|
|
|
http.SetCookie(w, &http.Cookie{
|
|
|
|
|
Name: unlockCookieName(box.ID),
|
|
|
|
|
Value: a.uploadService.UnlockToken(box),
|
|
|
|
|
Path: "/d/" + box.ID,
|
|
|
|
|
HttpOnly: true,
|
|
|
|
|
SameSite: http.SameSiteLaxMode,
|
|
|
|
|
Secure: r.TLS != nil,
|
|
|
|
|
Expires: box.ExpiresAt,
|
|
|
|
|
})
|
|
|
|
|
a.logger.Info("admin bypassed box password", "source", "admin", "severity", "user_activity", "code", 2302, "box_id", box.ID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
http.Redirect(w, r, "/d/"+box.ID, http.StatusSeeOther)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 16:52:57 +03:00
|
|
|
func (a *App) renderAdminLogin(w http.ResponseWriter, status int, message string) {
|
|
|
|
|
a.renderer.Render(w, status, "admin_login.html", web.PageData{
|
|
|
|
|
Title: "Admin login",
|
|
|
|
|
Description: "Sign in to the Warpbox admin console.",
|
|
|
|
|
Data: adminPageData{
|
|
|
|
|
Error: message,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *App) adminBoxes(limit int) ([]adminBoxView, error) {
|
|
|
|
|
boxes, err := a.uploadService.AdminBoxes(limit)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rows := make([]adminBoxView, 0, len(boxes))
|
|
|
|
|
for _, box := range boxes {
|
|
|
|
|
rows = append(rows, adminBoxView{
|
|
|
|
|
ID: box.ID,
|
|
|
|
|
CreatedAt: box.CreatedAt.Format("Jan 2 15:04"),
|
|
|
|
|
ExpiresAt: box.ExpiresAt.Format("Jan 2 15:04"),
|
|
|
|
|
FileCount: box.FileCount,
|
|
|
|
|
TotalSizeLabel: box.TotalSizeLabel,
|
|
|
|
|
DownloadCount: box.DownloadCount,
|
|
|
|
|
MaxDownloads: box.MaxDownloads,
|
|
|
|
|
Protected: box.Protected,
|
|
|
|
|
Expired: box.Expired,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
return rows, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *App) requireAdmin(w http.ResponseWriter, r *http.Request) bool {
|
|
|
|
|
if a.isAdmin(r) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *App) isAdmin(r *http.Request) bool {
|
|
|
|
|
if a.cfg.AdminToken == "" {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
cookie, err := r.Cookie(adminCookieName)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return cookie.Value == adminCookieValue(a.cfg.AdminToken)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func adminCookieValue(token string) string {
|
|
|
|
|
sum := sha256.Sum256([]byte("warpbox-admin:" + token))
|
|
|
|
|
return hex.EncodeToString(sum[:])
|
|
|
|
|
}
|