package server import ( "net/http" "strings" "time" "github.com/gin-gonic/gin" "warpbox/lib/metastore" ) const accountSessionCookie = "warpbox_account_session" func (app *App) registerAccountRoutes(router *gin.Engine) { account := router.Group("/account") account.Use(noStoreAdminHeaders) account.GET("/login", app.handleAccountLogin) account.POST("/login", app.handleAccountLoginPost) protected := account.Group("") protected.Use(app.requireAccountSession) protected.GET("", app.handleAccountDashboard) protected.GET("/", app.handleAccountDashboard) protected.POST("/logout", app.handleAccountLogout) protected.GET("/settings", app.handleAccountSettings) protected.POST("/settings", app.handleAccountSettingsPost) protected.POST("/settings/reset", app.handleAccountSettingsReset) protected.GET("/settings/export.json", app.handleAccountSettingsExport) protected.POST("/settings/import.json", app.handleAccountSettingsImport) protected.GET("/alerts", app.handleAccountAlerts) protected.GET("/alerts/export.json", app.handleAccountAlertsExport) protected.POST("/alerts/bulk/acknowledge", app.handleAccountAlertBulkAcknowledge) protected.POST("/alerts/bulk/close", app.handleAccountAlertBulkClose) protected.POST("/alerts/:id/acknowledge", app.handleAccountAlertAcknowledge) protected.POST("/alerts/:id/close", app.handleAccountAlertClose) protected.GET("/boxes", app.handleAccountBoxes) protected.GET("/boxes/export.csv", app.handleAccountBoxesExport) protected.POST("/boxes/bulk/expire", app.handleAccountBoxesBulkExpire) protected.POST("/boxes/bulk/delete", app.handleAccountBoxesBulkDelete) protected.POST("/boxes/bulk/bump-expiry", app.handleAccountBoxesBulkBumpExpiry) protected.POST("/boxes/delete-largest", app.handleAccountBoxesDeleteLargest) protected.GET("/boxes/:id", app.handleAccountBoxManager) protected.POST("/boxes/:id", app.handleAccountBoxUpdate) protected.POST("/boxes/:id/extend", app.handleAccountBoxExtend) protected.POST("/boxes/:id/expire", app.handleAccountBoxExpire) protected.POST("/boxes/:id/delete", app.handleAccountBoxDelete) protected.POST("/boxes/:id/password", app.handleAccountBoxPassword) protected.POST("/boxes/:id/password/remove", app.handleAccountBoxPasswordRemove) protected.POST("/boxes/:id/files/delete", app.handleAccountBoxFilesDelete) } func (app *App) handleAccountLogin(ctx *gin.Context) { if app.isAccountSessionValid(ctx) { ctx.Redirect(http.StatusSeeOther, "/account") return } app.renderAccountLogin(ctx, "") } func (app *App) handleAccountLoginPost(ctx *gin.Context) { if !app.adminLoginEnabled { app.renderAccountLogin(ctx, "Account login is disabled.") return } username := strings.TrimSpace(ctx.PostForm("username")) password := ctx.PostForm("password") user, ok, err := app.store.GetUserByUsername(username) if err != nil { ctx.String(http.StatusInternalServerError, "Could not load user") return } if !ok || user.Disabled || !metastore.VerifyPassword(user.PasswordHash, password) { app.renderAccountLogin(ctx, "The username or password was not accepted.") return } if _, err := app.permissionsForUser(user); err != nil { ctx.String(http.StatusInternalServerError, "Could not load permissions") return } session, err := app.store.CreateSession(user.ID, time.Duration(app.config.SessionTTLSeconds)*time.Second) if err != nil { ctx.String(http.StatusInternalServerError, "Could not create session") return } ctx.SetSameSite(http.SameSiteLaxMode) ctx.SetCookie(accountSessionCookie, session.Token, int(app.config.SessionTTLSeconds), "/account", "", app.config.AdminCookieSecure, true) ctx.Redirect(http.StatusSeeOther, "/account") } func (app *App) handleAccountLogout(ctx *gin.Context) { if token, err := ctx.Cookie(accountSessionCookie); err == nil { _ = app.store.DeleteSession(token) } ctx.SetSameSite(http.SameSiteLaxMode) ctx.SetCookie(accountSessionCookie, "", -1, "/account", "", app.config.AdminCookieSecure, true) ctx.Redirect(http.StatusSeeOther, "/account/login") } func (app *App) requireAccountSession(ctx *gin.Context) { token, err := ctx.Cookie(accountSessionCookie) if err != nil { ctx.Redirect(http.StatusSeeOther, "/account/login") ctx.Abort() return } session, ok, err := app.store.GetSession(token) if err != nil || !ok { ctx.Redirect(http.StatusSeeOther, "/account/login") ctx.Abort() return } if !validAdminCSRF(ctx, session) { ctx.String(http.StatusForbidden, "Permission denied") ctx.Abort() return } user, ok, err := app.store.GetUser(session.UserID) if err != nil || !ok || user.Disabled { ctx.Redirect(http.StatusSeeOther, "/account/login") ctx.Abort() return } perms, err := app.permissionsForUser(user) if err != nil { ctx.Redirect(http.StatusSeeOther, "/account/login") ctx.Abort() return } ctx.Set("accountUser", user) ctx.Set("adminUser", user) ctx.Set("accountPerms", perms) ctx.Set("adminPerms", perms) ctx.Set("accountSession", session) ctx.Set("accountCSRFToken", session.CSRFToken) ctx.Set("adminCSRFToken", session.CSRFToken) ctx.Next() } func (app *App) isAccountSessionValid(ctx *gin.Context) bool { token, err := ctx.Cookie(accountSessionCookie) if err != nil { return false } session, ok, err := app.store.GetSession(token) if err != nil || !ok { return false } user, ok, err := app.store.GetUser(session.UserID) if err != nil || !ok || user.Disabled { return false } _, err = app.permissionsForUser(user) return err == nil } func (app *App) renderAccountLogin(ctx *gin.Context, errorMessage string) { ctx.HTML(http.StatusOK, "account_login.html", gin.H{ "PageTitle": "WarpBox Account Login", "AdminLoginEnabled": app.adminLoginEnabled, "AccountLoginEnabled": app.adminLoginEnabled, "Error": errorMessage, }) } func currentAccountUser(ctx *gin.Context) (metastore.User, bool) { if current, ok := ctx.Get("accountUser"); ok { if user, ok := current.(metastore.User); ok { return user, true } } if current, ok := ctx.Get("adminUser"); ok { if user, ok := current.(metastore.User); ok { return user, true } } return metastore.User{}, false }