195 lines
6.8 KiB
Go
195 lines
6.8 KiB
Go
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)
|
|
protected.GET("/users", app.handleAccountUsers)
|
|
protected.POST("/users", app.handleAccountUsersPost)
|
|
protected.POST("/users/bulk/disable", app.handleAccountUsersBulkDisable)
|
|
protected.POST("/users/bulk/enable", app.handleAccountUsersBulkEnable)
|
|
protected.POST("/users/bulk/revoke-sessions", app.handleAccountUsersBulkRevokeSessions)
|
|
protected.POST("/users/:id/invite/resend", app.handleAccountUsersResendInvite)
|
|
protected.GET("/users/:id", app.handleAccountUserEdit)
|
|
protected.POST("/users/:id", app.handleAccountUserEditPost)
|
|
protected.POST("/users/:id/disable", app.handleAccountUserDisable)
|
|
protected.POST("/users/:id/enable", app.handleAccountUserEnable)
|
|
protected.POST("/users/:id/password/reset", app.handleAccountUserPasswordReset)
|
|
protected.POST("/users/:id/sessions/revoke", app.handleAccountUserRevokeSessions)
|
|
}
|
|
|
|
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
|
|
}
|