838 lines
25 KiB
Go
838 lines
25 KiB
Go
// Copyright (c) 2020, Andreas Schneider
|
|
// All rights reserved.
|
|
//
|
|
// Redistribution and use in source and binary forms, with or without
|
|
// modification, are permitted provided that the following conditions are met:
|
|
// * Redistributions of source code must retain the above copyright
|
|
// notice, this list of conditions and the following disclaimer.
|
|
// * Redistributions in binary form must reproduce the above copyright
|
|
// notice, this list of conditions and the following disclaimer in the
|
|
// documentation and/or other materials provided with the distribution.
|
|
// * Neither the name of the <organization> nor the
|
|
// names of its contributors may be used to endorse or promote products
|
|
// derived from this software without specific prior written permission.
|
|
//
|
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
// DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
|
|
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"html/template"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi"
|
|
passwordvalidator "github.com/lane-c-wagner/go-password-validator"
|
|
uuid "github.com/satori/go.uuid"
|
|
"github.com/tidwall/buntdb"
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
type webAdminHandler struct {
|
|
router chi.Router
|
|
shareStore ShareStore
|
|
tplError *template.Template
|
|
tplConfirm *template.Template
|
|
tplLogin *template.Template
|
|
tplIndex *template.Template
|
|
tplUsers *template.Template
|
|
tplShares *template.Template
|
|
tplMyShares *template.Template
|
|
tplShareAddUser *template.Template
|
|
tplShareAddLogin *template.Template
|
|
tplCreateShare *template.Template
|
|
tplCreateUser *template.Template
|
|
tplChangePassword *template.Template
|
|
}
|
|
|
|
type sessionContext struct {
|
|
h *webAdminHandler
|
|
w http.ResponseWriter
|
|
r *http.Request
|
|
user User
|
|
baseModel map[string]interface{}
|
|
}
|
|
|
|
const sessionCookieName = "sharedavsession"
|
|
const sessionLifetime = 1 * time.Hour
|
|
|
|
const confirmRequested = 0
|
|
const confirmAccepted = 1
|
|
const confirmDenied = 2
|
|
|
|
const minPasswordEntropy = 60
|
|
|
|
func newWebAdminHandler(app *app) *webAdminHandler {
|
|
sessionStore, err := buntdb.Open(":memory:")
|
|
if err != nil {
|
|
panic("cannot initialize session store: " + err.Error())
|
|
}
|
|
|
|
loadTemplate := func(filename string) *template.Template {
|
|
t, err := template.ParseFiles(
|
|
path.Join("templates", "base.html"),
|
|
path.Join("templates", filename+".html"),
|
|
)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("cannot load template %q: %v", filename, err))
|
|
}
|
|
return t
|
|
}
|
|
loadSingleTemplate := func(filename string) *template.Template {
|
|
t, err := template.ParseFiles(path.Join("templates", filename+".html"))
|
|
if err != nil {
|
|
panic(fmt.Sprintf("cannot load template %q: %v", filename, err))
|
|
}
|
|
return t
|
|
}
|
|
|
|
h := &webAdminHandler{
|
|
shareStore: app.shareStore,
|
|
tplError: loadTemplate("error"),
|
|
tplConfirm: loadTemplate("confirm"),
|
|
tplLogin: loadSingleTemplate("login"),
|
|
tplIndex: loadTemplate("index"),
|
|
tplUsers: loadTemplate("users"),
|
|
tplShares: loadTemplate("shares"),
|
|
tplMyShares: loadTemplate("my-shares"),
|
|
tplShareAddUser: loadTemplate("share-add-user"),
|
|
tplShareAddLogin: loadTemplate("share-add-login"),
|
|
tplCreateShare: loadTemplate("create-share"),
|
|
tplCreateUser: loadTemplate("create-user"),
|
|
tplChangePassword: loadTemplate("change-password"),
|
|
}
|
|
|
|
r := chi.NewRouter()
|
|
r.Use(disableCaching)
|
|
|
|
r.Route("/login", func(r chi.Router) {
|
|
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
|
sessionContext := h.buildSessionContext(w, r)
|
|
sessionContext.RenderPage(h.tplLogin, nil)
|
|
})
|
|
r.Post("/", func(w http.ResponseWriter, r *http.Request) {
|
|
sessionContext := h.buildSessionContext(w, r)
|
|
|
|
username := r.FormValue("username")
|
|
if strings.ContainsAny(username, "*:") {
|
|
sessionContext.RenderError("Username or password wrong.", "login")
|
|
return
|
|
}
|
|
|
|
user, err := app.userStore.GetUser(username)
|
|
if err != nil {
|
|
sessionContext.RenderError("Username or password wrong.", "login")
|
|
return
|
|
}
|
|
|
|
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(r.FormValue("password"))); err != nil {
|
|
sessionContext.RenderError("Username or password wrong.", "login")
|
|
return
|
|
}
|
|
|
|
sessionId := uuid.NewV4()
|
|
if err := sessionStore.Update(func(tx *buntdb.Tx) error {
|
|
_, _, err := tx.Set(sessionId.String(), user.Username, &buntdb.SetOptions{
|
|
Expires: true,
|
|
TTL: sessionLifetime,
|
|
})
|
|
return err
|
|
}); err != nil {
|
|
log.Printf("error setting session: %v\n", err)
|
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
cookie := &http.Cookie{
|
|
Name: sessionCookieName,
|
|
Value: sessionId.String(),
|
|
Expires: time.Now().Add(sessionLifetime),
|
|
HttpOnly: true,
|
|
}
|
|
http.SetCookie(w, cookie)
|
|
|
|
sessionContext.Redirect("./")
|
|
})
|
|
})
|
|
|
|
ar := r.With(authenticated(sessionStore, app.userStore))
|
|
|
|
ar.Get("/logout", func(w http.ResponseWriter, r *http.Request) {
|
|
sessionContext := h.buildSessionContext(w, r)
|
|
sessionCookie, err := r.Cookie(sessionCookieName)
|
|
if err != nil {
|
|
http.Redirect(w, r, "login", http.StatusFound)
|
|
return
|
|
}
|
|
if err := sessionStore.Update(func(tx *buntdb.Tx) error {
|
|
_, err := tx.Delete(sessionCookie.Value)
|
|
return err
|
|
}); err != nil {
|
|
log.Printf("error unsetting session: %v\n", err)
|
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
sessionContext.Redirect("./")
|
|
})
|
|
ar.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
|
sessionContext := h.buildSessionContext(w, r)
|
|
sessionContext.RenderPage(h.tplIndex, nil)
|
|
})
|
|
ar.Get("/users", func(w http.ResponseWriter, r *http.Request) {
|
|
sessionContext := h.buildSessionContext(w, r)
|
|
|
|
if sessionContext.user.Role != GlobalRoleAdmin {
|
|
sessionContext.Unauthorized()
|
|
return
|
|
}
|
|
|
|
users, err := app.userStore.GetUsers()
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
sessionContext.RenderPage(h.tplUsers, map[string]interface{}{
|
|
"Users": users,
|
|
})
|
|
})
|
|
ar.Get("/shares", func(w http.ResponseWriter, r *http.Request) {
|
|
sessionContext := h.buildSessionContext(w, r)
|
|
|
|
if sessionContext.user.Role != GlobalRoleAdmin {
|
|
sessionContext.Unauthorized()
|
|
return
|
|
}
|
|
|
|
shares, err := app.shareStore.GetShares()
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
type shareInfo struct {
|
|
Share
|
|
Users []ShareUser
|
|
}
|
|
|
|
shareInfos := make([]shareInfo, len(shares))
|
|
for i := range shares {
|
|
shareInfos[i].Share = shares[i]
|
|
users, err := app.shareStore.GetShareUsers(shares[i])
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
shareInfos[i].Users = users
|
|
}
|
|
|
|
sessionContext.RenderPage(h.tplShares, map[string]interface{}{
|
|
"ShareInfos": shareInfos,
|
|
})
|
|
})
|
|
ar.Get("/my-shares", func(w http.ResponseWriter, r *http.Request) {
|
|
sessionContext := h.buildSessionContext(w, r)
|
|
|
|
shares, err := app.shareStore.FindSharesByUser(sessionContext.user.Username)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
type shareInfo struct {
|
|
Share
|
|
IsAdmin bool
|
|
Logins []Login
|
|
Users []ShareUser
|
|
}
|
|
|
|
shareInfos := make([]shareInfo, len(shares))
|
|
for i := range shares {
|
|
shareInfos[i].Share = shares[i].Share
|
|
|
|
shareRole, err := app.shareStore.GetShareAccess(shares[i].Share, sessionContext.user.Username)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
shareInfos[i].IsAdmin = sessionContext.user.Role == GlobalRoleAdmin ||
|
|
shareRole == ShareRoleAdmin
|
|
|
|
if shareInfos[i].IsAdmin {
|
|
users, err := app.shareStore.GetShareUsers(shares[i].Share)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
shareInfos[i].Users = users
|
|
}
|
|
|
|
logins, err := app.shareStore.GetShareLogins(shares[i].Share, sessionContext.user.Username)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
shareInfos[i].Logins = logins
|
|
}
|
|
sessionContext.RenderPage(h.tplMyShares, map[string]interface{}{
|
|
"ShareInfos": shareInfos,
|
|
})
|
|
})
|
|
ar.Route("/share-add-user", func(r chi.Router) {
|
|
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
|
sessionContext := h.buildSessionContext(w, r)
|
|
|
|
share, err := app.shareStore.GetShare(r.URL.Query().Get("share"))
|
|
if err != nil {
|
|
sessionContext.RenderError(template.HTML("Internal error: "+err.Error()), "")
|
|
return
|
|
}
|
|
|
|
if !sessionContext.IsAdmin(share) {
|
|
sessionContext.Unauthorized()
|
|
return
|
|
}
|
|
|
|
users, err := app.userStore.GetUsers()
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
sessionContext.RenderPage(h.tplShareAddUser, map[string]interface{}{
|
|
"ShareId": share.UUID,
|
|
"Users": users,
|
|
})
|
|
})
|
|
r.Post("/", func(w http.ResponseWriter, r *http.Request) {
|
|
sessionContext := h.buildSessionContext(w, r)
|
|
|
|
returnURL := "share-add-user?share=" + r.FormValue("share")
|
|
|
|
share, err := app.shareStore.GetShare(r.FormValue("share"))
|
|
if err != nil {
|
|
sessionContext.RenderError(template.HTML("Internal error: "+err.Error()), "")
|
|
return
|
|
}
|
|
|
|
if !sessionContext.IsAdmin(share) {
|
|
sessionContext.Unauthorized()
|
|
return
|
|
}
|
|
|
|
user, err := app.userStore.GetUser(r.FormValue("user"))
|
|
if err == ErrUserNotFound {
|
|
sessionContext.RenderError("User not found.", returnURL)
|
|
return
|
|
} else if err != nil {
|
|
sessionContext.RenderError(template.HTML("Internal error: "+err.Error()), "")
|
|
return
|
|
}
|
|
|
|
err = app.shareStore.AddUserToShare(share, user.Username, ShareRole(r.FormValue("role")))
|
|
if err != nil {
|
|
sessionContext.RenderError(template.HTML("Cannot add user to share: "+err.Error()), returnURL)
|
|
return
|
|
}
|
|
|
|
sessionContext.Redirect("shares")
|
|
})
|
|
})
|
|
ar.Post("/share-delete-user", func(w http.ResponseWriter, r *http.Request) {
|
|
sessionContext := h.buildSessionContext(w, r)
|
|
|
|
returnURL := r.FormValue("source")
|
|
|
|
share, err := app.shareStore.GetShare(r.FormValue("share"))
|
|
if err != nil {
|
|
sessionContext.RenderError(template.HTML("Internal error: "+err.Error()), "")
|
|
return
|
|
}
|
|
|
|
if !sessionContext.IsAdmin(share) {
|
|
sessionContext.Unauthorized()
|
|
return
|
|
}
|
|
|
|
username := r.FormValue("user")
|
|
|
|
message := fmt.Sprintf(`You are about to delete the user "%s" from the share %s.<br/>
|
|
This will irrevocably also remove all logins of that user.<br/><br/>
|
|
Are you sure you want to continue?`, username, share.Name)
|
|
if confirmStatus := sessionContext.RequestConfirmation(template.HTML(message), "share-delete-user"); confirmStatus == confirmRequested {
|
|
// We have already rendered. Nothing to do.
|
|
return
|
|
} else if confirmStatus == confirmAccepted {
|
|
err = app.shareStore.RemoveUserFromShare(share, username)
|
|
if err != nil {
|
|
sessionContext.RenderError(template.HTML("Cannot remove user from share: "+err.Error()), returnURL)
|
|
return
|
|
}
|
|
}
|
|
|
|
http.Redirect(w, r, returnURL, http.StatusFound)
|
|
})
|
|
ar.Route("/share-add-login", func(r chi.Router) {
|
|
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
|
sessionContext := h.buildSessionContext(w, r)
|
|
|
|
share, err := app.shareStore.GetShare(r.URL.Query().Get("share"))
|
|
if err != nil {
|
|
sessionContext.RenderError(template.HTML("Internal error: "+err.Error()), "")
|
|
return
|
|
}
|
|
|
|
if !sessionContext.IsAdmin(share) {
|
|
sessionContext.Unauthorized()
|
|
return
|
|
}
|
|
|
|
sessionContext.RenderPage(h.tplShareAddLogin, map[string]interface{}{
|
|
"ShareId": share.UUID,
|
|
})
|
|
})
|
|
r.Post("/", func(w http.ResponseWriter, r *http.Request) {
|
|
sessionContext := h.buildSessionContext(w, r)
|
|
|
|
returnURL := "share-add-login?share=" + r.FormValue("share")
|
|
|
|
share, err := app.shareStore.GetShare(r.FormValue("share"))
|
|
if err != nil {
|
|
sessionContext.RenderError(template.HTML("Internal error: "+err.Error()), returnURL)
|
|
return
|
|
}
|
|
|
|
if !sessionContext.IsAdmin(share) {
|
|
sessionContext.Unauthorized()
|
|
return
|
|
}
|
|
|
|
pwHash, err := bcrypt.GenerateFromPassword([]byte(r.FormValue("password")), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
sessionContext.RenderError(template.HTML("Internal error: "+err.Error()), returnURL)
|
|
return
|
|
}
|
|
|
|
err = app.shareStore.AddLogin(share, sessionContext.user.Username, Login{
|
|
LoginName: r.FormValue("login"),
|
|
Password: string(pwHash),
|
|
ReadOnly: r.FormValue("readonly") == "on",
|
|
})
|
|
if err != nil {
|
|
sessionContext.RenderError(template.HTML("Cannot add login: "+err.Error()), returnURL)
|
|
return
|
|
}
|
|
|
|
sessionContext.Redirect("my-shares")
|
|
})
|
|
})
|
|
ar.Post("/share-delete-login", func(w http.ResponseWriter, r *http.Request) {
|
|
sessionContext := h.buildSessionContext(w, r)
|
|
|
|
returnURL := "shares"
|
|
|
|
share, err := app.shareStore.GetShare(r.FormValue("share"))
|
|
if err != nil {
|
|
sessionContext.RenderError(template.HTML("Internal error: "+err.Error()), "")
|
|
return
|
|
}
|
|
|
|
if !sessionContext.IsAdmin(share) {
|
|
sessionContext.Unauthorized()
|
|
return
|
|
}
|
|
|
|
loginName := r.FormValue("login")
|
|
|
|
message := fmt.Sprintf(`You are about to delete the login "%s".<br/><br/>
|
|
Are you sure you want to continue?`, loginName)
|
|
if confirmStatus := sessionContext.RequestConfirmation(template.HTML(message), "share-delete-login"); confirmStatus == confirmRequested {
|
|
// We have already rendered. Nothing to do.
|
|
return
|
|
} else if confirmStatus == confirmAccepted {
|
|
err = app.shareStore.RemoveLogin(share, sessionContext.user.Username, loginName)
|
|
if err != nil {
|
|
sessionContext.RenderError(template.HTML("Cannot remove login from share: "+err.Error()), returnURL)
|
|
return
|
|
}
|
|
}
|
|
|
|
http.Redirect(w, r, "my-shares", http.StatusFound)
|
|
})
|
|
ar.Route("/create-share", func(r chi.Router) {
|
|
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
|
sessionContext := h.buildSessionContext(w, r)
|
|
|
|
owned := r.FormValue("owned") == "true"
|
|
if !owned && sessionContext.user.Role != GlobalRoleAdmin {
|
|
sessionContext.Unauthorized()
|
|
return
|
|
}
|
|
|
|
sessionContext.RenderPage(h.tplCreateShare, nil)
|
|
})
|
|
r.Post("/", func(w http.ResponseWriter, r *http.Request) {
|
|
sessionContext := h.buildSessionContext(w, r)
|
|
|
|
owned := r.FormValue("owned") == "true"
|
|
if !owned && sessionContext.user.Role != GlobalRoleAdmin {
|
|
sessionContext.Unauthorized()
|
|
return
|
|
}
|
|
|
|
share, err := app.shareStore.CreateShare()
|
|
if err != nil {
|
|
sessionContext.RenderError(template.HTML("Cannot create share: "+err.Error()), "")
|
|
return
|
|
}
|
|
|
|
share.Name = r.FormValue("name")
|
|
share.Description = r.FormValue("description")
|
|
|
|
if err := app.shareStore.UpdateShareAttributes(share); err != nil {
|
|
sessionContext.RenderError(template.HTML("Cannot update share: "+err.Error()), "")
|
|
return
|
|
}
|
|
|
|
if owned {
|
|
if err := app.shareStore.AddUserToShare(share, sessionContext.user.Username, ShareRoleAdmin); err != nil {
|
|
sessionContext.RenderError(template.HTML("Cannot add self to share: "+err.Error()), "")
|
|
return
|
|
}
|
|
sessionContext.Redirect("my-shares#share-" + share.UUID.String())
|
|
} else {
|
|
sessionContext.Redirect("shares#share-" + share.UUID.String())
|
|
}
|
|
})
|
|
})
|
|
ar.Post("/delete-share", func(w http.ResponseWriter, r *http.Request) {
|
|
sessionContext := h.buildSessionContext(w, r)
|
|
|
|
share, err := app.shareStore.GetShare(r.FormValue("share"))
|
|
if err != nil {
|
|
sessionContext.RenderError(template.HTML("Internal error: "+err.Error()), "")
|
|
return
|
|
}
|
|
|
|
if !sessionContext.IsAdmin(share) {
|
|
sessionContext.Unauthorized()
|
|
return
|
|
}
|
|
|
|
message := fmt.Sprintf(`You are about to delete the share %s (%s).<br/>
|
|
This will delete all data permanently.<br/><br/>
|
|
Are you sure you want to continue?`, share.UUID, share.Name)
|
|
if confirmStatus := sessionContext.RequestConfirmation(template.HTML(message), "delete-share"); confirmStatus == confirmRequested {
|
|
// We have already rendered. Nothing to do.
|
|
return
|
|
} else if confirmStatus == confirmAccepted {
|
|
if err := app.shareStore.RemoveShare(share.UUID); err != nil {
|
|
sessionContext.RenderError(template.HTML("Share cannot be removed: "+err.Error()), "shares")
|
|
return
|
|
}
|
|
}
|
|
sessionContext.Redirect("shares")
|
|
})
|
|
ar.Route("/create-user", func(r chi.Router) {
|
|
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
|
sessionContext := h.buildSessionContext(w, r)
|
|
|
|
if sessionContext.user.Role != GlobalRoleAdmin {
|
|
sessionContext.Unauthorized()
|
|
return
|
|
}
|
|
|
|
sessionContext.RenderPage(h.tplCreateUser, nil)
|
|
})
|
|
r.Post("/", func(w http.ResponseWriter, r *http.Request) {
|
|
sessionContext := h.buildSessionContext(w, r)
|
|
|
|
if sessionContext.user.Role != GlobalRoleAdmin {
|
|
sessionContext.Unauthorized()
|
|
return
|
|
}
|
|
|
|
returnURL := "users"
|
|
|
|
pwHash, err := bcrypt.GenerateFromPassword([]byte(r.FormValue("password")), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
sessionContext.RenderError(template.HTML("Internal error: "+err.Error()), returnURL)
|
|
return
|
|
}
|
|
|
|
user := User{
|
|
Username: r.FormValue("username"),
|
|
Password: string(pwHash),
|
|
Role: GlobalRole(r.FormValue("role")),
|
|
}
|
|
|
|
switch user.Role {
|
|
case GlobalRoleUser:
|
|
case GlobalRoleAdmin:
|
|
default:
|
|
sessionContext.RenderError(template.HTML("Invalid role."), "")
|
|
return
|
|
}
|
|
|
|
err = app.userStore.AddUser(user)
|
|
if err != nil {
|
|
sessionContext.RenderError(template.HTML("Cannot create user: "+err.Error()), "")
|
|
return
|
|
}
|
|
|
|
sessionContext.Redirect("users#user-" + user.Username)
|
|
})
|
|
})
|
|
ar.Post("/delete-user", func(w http.ResponseWriter, r *http.Request) {
|
|
sessionContext := h.buildSessionContext(w, r)
|
|
|
|
if sessionContext.user.Role != GlobalRoleAdmin {
|
|
sessionContext.Unauthorized()
|
|
return
|
|
}
|
|
|
|
user, err := app.userStore.GetUser(r.FormValue("user"))
|
|
if err != nil {
|
|
sessionContext.RenderError(template.HTML("Cannot delete user: "+err.Error()), "users")
|
|
return
|
|
}
|
|
|
|
message := fmt.Sprintf(`You are about to delete the user %s (<i>%s</i>).<br/>
|
|
This will delete all share associations and logins.<br/><br/>
|
|
Are you sure you want to continue?`, user.Username, user.Role)
|
|
if confirmStatus := sessionContext.RequestConfirmation(template.HTML(message), "delete-user"); confirmStatus == confirmRequested {
|
|
// We have already rendered. Nothing to do.
|
|
return
|
|
} else if confirmStatus == confirmAccepted {
|
|
cmd := CmdUserDelete{Username: user.Username}
|
|
if err := cmd.Run(app); err != nil {
|
|
sessionContext.RenderError(template.HTML("Cannot delete user: "+err.Error()), "users")
|
|
return
|
|
}
|
|
}
|
|
|
|
invalidateSession(sessionStore, user.Username)
|
|
|
|
sessionContext.Redirect("users")
|
|
})
|
|
ar.Route("/change-password", func(r chi.Router) {
|
|
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
|
sessionContext := h.buildSessionContext(w, r)
|
|
sessionContext.RenderPage(h.tplChangePassword, nil)
|
|
})
|
|
r.Post("/", func(w http.ResponseWriter, r *http.Request) {
|
|
sessionContext := h.buildSessionContext(w, r)
|
|
|
|
currentPassword := r.FormValue("password")
|
|
newPassword := r.FormValue("password-new")
|
|
repeatPassword := r.FormValue("password-repeat")
|
|
|
|
if err := bcrypt.CompareHashAndPassword([]byte(sessionContext.user.Password), []byte(currentPassword)); err != nil {
|
|
sessionContext.RenderError(template.HTML("The current password is wrong. Please check."), "")
|
|
return
|
|
}
|
|
|
|
if newPassword != repeatPassword {
|
|
sessionContext.RenderError(template.HTML("The new password doesn't match the repeated password."), "")
|
|
return
|
|
}
|
|
|
|
if err := passwordvalidator.Validate(newPassword, minPasswordEntropy); err != nil {
|
|
sessionContext.RenderError(template.HTML("The new password is not strong enough."), "")
|
|
return
|
|
}
|
|
|
|
pwHash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
sessionContext.RenderError(template.HTML("Internal error: "+err.Error()), "")
|
|
return
|
|
}
|
|
|
|
if err := app.userStore.UpdateUser(User{
|
|
Username: sessionContext.user.Username,
|
|
Password: string(pwHash),
|
|
}); err != nil {
|
|
sessionContext.RenderError(template.HTML("Internal error: "+err.Error()), "")
|
|
return
|
|
}
|
|
|
|
invalidateSession(sessionStore, sessionContext.user.Username)
|
|
|
|
sessionContext.Redirect("./")
|
|
})
|
|
})
|
|
h.router = r
|
|
|
|
return h
|
|
}
|
|
|
|
func (h *webAdminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
h.router.ServeHTTP(w, r)
|
|
}
|
|
|
|
func (h *webAdminHandler) buildSessionContext(w http.ResponseWriter, r *http.Request) *sessionContext {
|
|
sessionContext := &sessionContext{
|
|
h: h,
|
|
w: w,
|
|
r: r,
|
|
baseModel: map[string]interface{}{},
|
|
}
|
|
sessionUser := userFromContext(r)
|
|
if sessionUser != nil {
|
|
sessionContext.user = *sessionUser
|
|
sessionContext.baseModel["SessionUser"] = sessionUser
|
|
}
|
|
return sessionContext
|
|
}
|
|
|
|
func (s *sessionContext) Redirect(target string) {
|
|
http.Redirect(s.w, s.r, target, http.StatusFound)
|
|
}
|
|
|
|
func (s *sessionContext) RenderPage(tmpl *template.Template, model map[string]interface{}) {
|
|
effectiveModel := map[string]interface{}{}
|
|
for k, v := range s.baseModel {
|
|
effectiveModel[k] = v
|
|
}
|
|
for k, v := range model {
|
|
effectiveModel[k] = v
|
|
}
|
|
if err := tmpl.Execute(s.w, effectiveModel); err != nil {
|
|
http.Error(s.w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
func (s *sessionContext) RenderError(msg template.HTML, returnURL string) {
|
|
model := map[string]interface{}{
|
|
"ErrorMessage": msg,
|
|
"ReturnURL": returnURL,
|
|
}
|
|
s.RenderPage(s.h.tplError, model)
|
|
}
|
|
|
|
func (s *sessionContext) IsAdmin(share Share) bool {
|
|
if s.user.Role != GlobalRoleAdmin {
|
|
shareRole, err := s.h.shareStore.GetShareAccess(share, s.user.Username)
|
|
if err != nil || shareRole != ShareRoleAdmin {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (s *sessionContext) Unauthorized() {
|
|
http.Error(s.w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
|
|
}
|
|
|
|
func (s *sessionContext) RequestConfirmation(msg template.HTML, returnURL string) int {
|
|
if s.r.FormValue("_yes") != "" {
|
|
return confirmAccepted
|
|
} else if s.r.FormValue("_no") != "" {
|
|
return confirmDenied
|
|
} else {
|
|
fields := map[string]string{}
|
|
for k, v := range s.r.Form {
|
|
fields[k] = v[0]
|
|
}
|
|
model := map[string]interface{}{
|
|
"URL": returnURL,
|
|
"Message": msg,
|
|
"Fields": fields,
|
|
}
|
|
|
|
s.RenderPage(s.h.tplConfirm, model)
|
|
return confirmRequested
|
|
}
|
|
}
|
|
|
|
func disableCaching(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
|
w.Header().Set("Pragma", "no-cache")
|
|
w.Header().Set("Expires", "0")
|
|
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
func authenticated(sessionStore *buntdb.DB, userStore UserStore) func(http.Handler) http.Handler {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
sessionCookie, err := r.Cookie(sessionCookieName)
|
|
if err != nil {
|
|
http.Redirect(w, r, "login", http.StatusFound)
|
|
return
|
|
}
|
|
var username string
|
|
if err := sessionStore.View(func(tx *buntdb.Tx) error {
|
|
val, err := tx.Get(sessionCookie.Value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
username = val
|
|
return nil
|
|
}); err != nil {
|
|
http.Redirect(w, r, "login", http.StatusFound)
|
|
return
|
|
}
|
|
|
|
user, err := userStore.GetUser(username)
|
|
if err != nil {
|
|
http.Redirect(w, r, "login", http.StatusFound)
|
|
return
|
|
}
|
|
|
|
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), "user", &user)))
|
|
})
|
|
}
|
|
}
|
|
|
|
func userFromContext(r *http.Request) *User {
|
|
if user, ok := r.Context().Value("user").(*User); ok {
|
|
return user
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func invalidateSession(store *buntdb.DB, username string) {
|
|
err := store.Update(func(tx *buntdb.Tx) error {
|
|
var sessionIds []string
|
|
if err := tx.AscendKeys("*", func(key, value string) bool {
|
|
if value == username {
|
|
sessionIds = append(sessionIds, key)
|
|
}
|
|
return true
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, sessionId := range sessionIds {
|
|
if _, err := tx.Delete(sessionId); err != nil {
|
|
fmt.Fprintf(os.Stderr, "cannot remove session: %v\n", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "cannot invalidate session: %v\n", err)
|
|
}
|
|
}
|