feat(admin): implement full admin dashboard structure

This commit is contained in:
2026-05-01 00:29:06 +03:00
parent 5f3f63b710
commit 3844473eb3
11 changed files with 1908 additions and 2 deletions

102
lib/server/admin.go Normal file
View File

@@ -0,0 +1,102 @@
package server
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
"warpbox/lib/config"
)
const adminSessionCookie = "warpbox_admin_session"
const adminSessionMarker = "1"
func (app *App) adminLoginEnabled() bool {
return app.config.AdminLoginEnabled(app.config.AdminPassword != "")
}
func (app *App) adminAuthMiddleware(ctx *gin.Context) {
if !app.adminLoginEnabled() {
ctx.Redirect(http.StatusSeeOther, "/")
ctx.Abort()
return
}
token, err := ctx.Cookie(adminSessionCookie)
if err != nil || token != app.adminSessionToken() {
ctx.Redirect(http.StatusSeeOther, "/admin/login")
ctx.Abort()
return
}
ctx.Next()
}
func (app *App) adminSessionToken() string {
// A simple deterministic token derived from the admin credentials.
// This will improve when proper user/session storage is added.
return app.config.AdminUsername + ":" + app.config.AdminPassword
}
func (app *App) handleAdminLogin(ctx *gin.Context) {
if !app.adminLoginEnabled() {
ctx.Redirect(http.StatusSeeOther, "/")
return
}
// Already logged in.
if token, err := ctx.Cookie(adminSessionCookie); err == nil && token == app.adminSessionToken() {
ctx.Redirect(http.StatusSeeOther, "/admin/dashboard")
return
}
ctx.HTML(http.StatusOK, "admin/login.html", gin.H{})
}
func (app *App) handleAdminLoginPost(ctx *gin.Context) {
if !app.adminLoginEnabled() {
ctx.Redirect(http.StatusSeeOther, "/")
return
}
username := strings.TrimSpace(ctx.PostForm("username"))
password := ctx.PostForm("password")
if username != app.config.AdminUsername || password != app.config.AdminPassword {
ctx.HTML(http.StatusUnauthorized, "admin/login.html", gin.H{
"ErrorMessage": "Invalid username or password.",
})
return
}
secure := app.config.AdminCookieSecure
maxAge := int(app.config.SessionTTLSeconds)
ctx.SetCookie(adminSessionCookie, app.adminSessionToken(), maxAge, "/admin", "", secure, true)
ctx.Redirect(http.StatusSeeOther, "/admin/dashboard")
}
func (app *App) handleAdminLogout(ctx *gin.Context) {
secure := app.config.AdminCookieSecure
ctx.SetCookie(adminSessionCookie, "", -1, "/admin", "", secure, true)
ctx.Redirect(http.StatusSeeOther, "/admin/login")
}
func (app *App) handleAdminDashboard(ctx *gin.Context) {
if !app.adminLoginEnabled() {
ctx.Redirect(http.StatusSeeOther, "/")
return
}
dashboardEnabled := config.AdminEnabledTrue
if cfgVal := app.config.AdminEnabled; cfgVal == config.AdminEnabledAuto || cfgVal == config.AdminEnabledTrue {
dashboardEnabled = cfgVal
}
ctx.HTML(http.StatusOK, "admin/dashboard.html", gin.H{
"AdminUsername": app.config.AdminUsername,
"AdminEmail": app.config.AdminEmail,
"DashboardEnabled": string(dashboardEnabled),
})
}

View File

@@ -1,6 +1,7 @@
package server
import (
"html/template"
"time"
"github.com/gin-contrib/gzip"
@@ -29,7 +30,11 @@ func Run(addr string) error {
app := &App{config: cfg}
router := gin.Default()
router.LoadHTMLGlob("templates/*.html")
htmlTemplates, err := loadHTMLTemplates()
if err != nil {
return err
}
router.SetHTMLTemplate(htmlTemplates)
routing.Register(router, routing.Handlers{
Index: app.handleIndex,
@@ -45,6 +50,12 @@ func Run(addr string) error {
FileStatusUpdate: app.handleFileStatusUpdate,
DirectBoxUpload: app.handleDirectBoxUpload,
LegacyUpload: app.handleLegacyUpload,
AdminLogin: app.handleAdminLogin,
AdminLoginPost: app.handleAdminLoginPost,
AdminLogout: app.handleAdminLogout,
AdminDashboard: app.handleAdminDashboard,
AdminAuth: app.adminAuthMiddleware,
})
compressed := router.Group("/", gzip.Gzip(gzip.DefaultCompression))
@@ -55,6 +66,22 @@ func Run(addr string) error {
return router.Run(addr)
}
func loadHTMLTemplates() (*template.Template, error) {
tmpl := template.New("")
for _, pattern := range []string{
"templates/*.html",
"templates/admin/*.html",
"templates/admin/partials/*.html",
} {
var err error
tmpl, err = tmpl.ParseGlob(pattern)
if err != nil {
return nil, err
}
}
return tmpl, nil
}
func applyBoxstoreRuntimeConfig(cfg *config.Config) {
boxstore.SetUploadRoot(cfg.UploadsDir)
boxstore.SetOneTimeDownloadExpiry(cfg.OneTimeDownloadExpirySeconds)