2024-09-07 21:22:41 +03:00
|
|
|
|
package web
|
2022-05-18 21:03:36 +03:00
|
|
|
|
|
|
|
|
|
import (
|
2022-07-11 21:59:55 +05:00
|
|
|
|
"fmt"
|
2024-09-07 23:55:39 +03:00
|
|
|
|
"log/slog"
|
2022-07-11 21:59:55 +05:00
|
|
|
|
"mime"
|
|
|
|
|
"net/http"
|
|
|
|
|
"os"
|
|
|
|
|
"sort"
|
2022-05-18 21:03:36 +03:00
|
|
|
|
|
2024-09-07 21:22:41 +03:00
|
|
|
|
"github.com/bouncepaw/mycorrhiza/internal/cfg"
|
|
|
|
|
"github.com/bouncepaw/mycorrhiza/internal/user"
|
2022-07-11 21:59:55 +05:00
|
|
|
|
"github.com/bouncepaw/mycorrhiza/util"
|
2024-09-07 21:22:41 +03:00
|
|
|
|
"github.com/bouncepaw/mycorrhiza/web/viewutil"
|
|
|
|
|
"github.com/gorilla/mux"
|
2022-05-18 21:03:36 +03:00
|
|
|
|
)
|
|
|
|
|
|
2024-09-07 21:22:41 +03:00
|
|
|
|
const adminTranslationRu = `
|
|
|
|
|
{{define "panel title"}}Панель админстратора{{end}}
|
|
|
|
|
{{define "panel safe section title"}}Безопасная секция{{end}}
|
|
|
|
|
{{define "panel link about"}}Об этой вики{{end}}
|
|
|
|
|
{{define "panel update header"}}Обновить ссылки в верхней панели{{end}}
|
|
|
|
|
{{define "panel link user list"}}Список пользователей{{end}}
|
|
|
|
|
{{define "panel users"}}Управление пользователями{{end}}
|
|
|
|
|
{{define "panel unsafe section title"}}Опасная секция{{end}}
|
|
|
|
|
{{define "panel shutdown"}}Выключить вики{{end}}
|
|
|
|
|
{{define "panel reindex hyphae"}}Переиндексировать гифы{{end}}
|
|
|
|
|
{{define "panel interwiki"}}Интервики{{end}}
|
|
|
|
|
|
|
|
|
|
{{define "manage users"}}Управление пользователями{{end}}
|
|
|
|
|
{{define "create user"}}Создать пользователя{{end}}
|
|
|
|
|
{{define "reindex users"}}Переиндексировать пользователей{{end}}
|
|
|
|
|
{{define "name"}}Имя{{end}}
|
|
|
|
|
{{define "group"}}Группа{{end}}
|
|
|
|
|
{{define "registered at"}}Зарегистрирован{{end}}
|
|
|
|
|
{{define "actions"}}Действия{{end}}
|
|
|
|
|
{{define "edit"}}Изменить{{end}}
|
|
|
|
|
|
|
|
|
|
{{define "new user"}}Новый пользователь{{end}}
|
|
|
|
|
{{define "password"}}Пароль{{end}}
|
|
|
|
|
{{define "confirm password"}}Подтвердить пароль{{end}}
|
|
|
|
|
{{define "change password"}}Изменить пароль{{end}}
|
|
|
|
|
{{define "non local password change"}}Поменять пароль можно только у локальных пользователей.{{end}}
|
|
|
|
|
{{define "create"}}Создать{{end}}
|
|
|
|
|
|
|
|
|
|
{{define "change group"}}Изменить группу{{end}}
|
|
|
|
|
{{define "user x"}}Пользователь {{.}}{{end}}
|
|
|
|
|
{{define "update"}}Обновить{{end}}
|
|
|
|
|
{{define "delete user"}}Удалить пользователя{{end}}
|
|
|
|
|
{{define "delete user tip"}}Удаляет пользователя из базы данных. Правки пользователя будут сохранены. Имя пользователя освободится для повторной регистрации.{{end}}
|
|
|
|
|
|
|
|
|
|
{{define "delete user?"}}Удалить пользователя {{.}}?{{end}}
|
|
|
|
|
{{define "delete user warning"}}Вы уверены, что хотите удалить этого пользователя из базы данных? Это действие нельзя отменить.{{end}}
|
|
|
|
|
`
|
|
|
|
|
|
|
|
|
|
func viewPanel(meta viewutil.Meta) {
|
|
|
|
|
viewutil.ExecutePage(meta, panelChain, &viewutil.BaseData{})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type listData struct {
|
|
|
|
|
*viewutil.BaseData
|
|
|
|
|
UserHypha string
|
|
|
|
|
Users []*user.User
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func viewList(meta viewutil.Meta, users []*user.User) {
|
|
|
|
|
viewutil.ExecutePage(meta, listChain, listData{
|
|
|
|
|
BaseData: &viewutil.BaseData{},
|
|
|
|
|
UserHypha: cfg.UserHypha,
|
|
|
|
|
Users: users,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type newUserData struct {
|
|
|
|
|
*viewutil.BaseData
|
|
|
|
|
Form util.FormData
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func viewNewUser(meta viewutil.Meta, form util.FormData) {
|
|
|
|
|
viewutil.ExecutePage(meta, newUserChain, newUserData{
|
|
|
|
|
BaseData: &viewutil.BaseData{},
|
|
|
|
|
Form: form,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type editDeleteUserData struct {
|
|
|
|
|
*viewutil.BaseData
|
|
|
|
|
Form util.FormData
|
|
|
|
|
U *user.User
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func viewEditUser(meta viewutil.Meta, form util.FormData, u *user.User) {
|
|
|
|
|
viewutil.ExecutePage(meta, editUserChain, editDeleteUserData{
|
|
|
|
|
BaseData: &viewutil.BaseData{},
|
|
|
|
|
Form: form,
|
|
|
|
|
U: u,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func viewDeleteUser(meta viewutil.Meta, form util.FormData, u *user.User) {
|
|
|
|
|
viewutil.ExecutePage(meta, deleteUserChain, editDeleteUserData{
|
|
|
|
|
BaseData: &viewutil.BaseData{},
|
|
|
|
|
Form: form,
|
|
|
|
|
U: u,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-11 21:59:55 +05:00
|
|
|
|
// handlerAdmin provides the admin panel.
|
|
|
|
|
func handlerAdmin(w http.ResponseWriter, rq *http.Request) {
|
|
|
|
|
w.Header().Set("Content-Type", "text/html;charset=utf-8")
|
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
|
viewPanel(viewutil.MetaFrom(w, rq))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// handlerAdminShutdown kills the wiki.
|
|
|
|
|
func handlerAdminShutdown(w http.ResponseWriter, rq *http.Request) {
|
|
|
|
|
if user.CanProceed(rq, "admin/shutdown") {
|
2024-09-07 23:55:39 +03:00
|
|
|
|
slog.Info("An admin commanded the wiki to shutdown")
|
2022-07-11 21:59:55 +05:00
|
|
|
|
os.Exit(0)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// handlerAdminReindexUsers reinitialises the user system.
|
|
|
|
|
func handlerAdminReindexUsers(w http.ResponseWriter, rq *http.Request) {
|
|
|
|
|
user.ReadUsersFromFilesystem()
|
|
|
|
|
redirectTo := rq.Referer()
|
|
|
|
|
if redirectTo == "" {
|
|
|
|
|
redirectTo = "/hypha/" + cfg.UserHypha
|
|
|
|
|
}
|
|
|
|
|
http.Redirect(w, rq, redirectTo, http.StatusSeeOther)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func handlerAdminUsers(w http.ResponseWriter, rq *http.Request) {
|
|
|
|
|
// Get a sorted list of users
|
|
|
|
|
var users []*user.User
|
|
|
|
|
for u := range user.YieldUsers() {
|
|
|
|
|
users = append(users, u)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sort.Slice(users, func(i, j int) bool {
|
|
|
|
|
less := users[i].RegisteredAt.Before(users[j].RegisteredAt)
|
|
|
|
|
return less
|
|
|
|
|
})
|
|
|
|
|
viewList(viewutil.MetaFrom(w, rq), users)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func handlerAdminUserEdit(w http.ResponseWriter, rq *http.Request) {
|
|
|
|
|
vars := mux.Vars(rq)
|
|
|
|
|
u := user.ByName(vars["username"])
|
|
|
|
|
if u == nil {
|
|
|
|
|
util.HTTP404Page(w, "404 page not found")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
f := util.FormDataFromRequest(rq, []string{"group"})
|
|
|
|
|
|
|
|
|
|
if rq.Method == http.MethodPost {
|
|
|
|
|
oldGroup := u.Group
|
|
|
|
|
newGroup := f.Get("group")
|
|
|
|
|
|
|
|
|
|
if user.ValidGroup(newGroup) {
|
|
|
|
|
u.Group = newGroup
|
|
|
|
|
if err := user.SaveUserDatabase(); err != nil {
|
|
|
|
|
u.Group = oldGroup
|
2024-09-07 23:55:39 +03:00
|
|
|
|
slog.Info("Failed to save user database", "err", err)
|
2022-07-11 21:59:55 +05:00
|
|
|
|
f = f.WithError(err)
|
|
|
|
|
} else {
|
|
|
|
|
http.Redirect(w, rq, "/admin/users/", http.StatusSeeOther)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
f = f.WithError(fmt.Errorf("invalid group ‘%s’", newGroup))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
f.Put("group", u.Group)
|
|
|
|
|
|
|
|
|
|
if f.HasError() {
|
|
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
|
|
|
}
|
|
|
|
|
w.Header().Set("Content-Type", mime.TypeByExtension(".html"))
|
2022-08-06 22:33:37 +05:00
|
|
|
|
|
|
|
|
|
viewEditUser(viewutil.MetaFrom(w, rq), f, u)
|
2022-05-18 21:03:36 +03:00
|
|
|
|
}
|
|
|
|
|
|
2023-11-27 14:55:45 +01:00
|
|
|
|
func handlerAdminUserChangePassword(w http.ResponseWriter, rq *http.Request) {
|
|
|
|
|
vars := mux.Vars(rq)
|
|
|
|
|
u := user.ByName(vars["username"])
|
|
|
|
|
if u == nil {
|
|
|
|
|
util.HTTP404Page(w, "404 page not found")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
f := util.FormDataFromRequest(rq, []string{"password", "password_confirm"})
|
|
|
|
|
|
|
|
|
|
password := f.Get("password")
|
|
|
|
|
passwordConfirm := f.Get("password_confirm")
|
|
|
|
|
// server side validation
|
|
|
|
|
if password == "" {
|
|
|
|
|
err := fmt.Errorf("passwords should not be empty")
|
|
|
|
|
f = f.WithError(err)
|
|
|
|
|
}
|
|
|
|
|
if password == passwordConfirm {
|
|
|
|
|
previousPassword := u.Password // for rollback
|
|
|
|
|
if err := u.ChangePassword(password); err != nil {
|
|
|
|
|
f = f.WithError(err)
|
|
|
|
|
} else {
|
|
|
|
|
if err := user.SaveUserDatabase(); err != nil {
|
|
|
|
|
u.Password = previousPassword
|
|
|
|
|
f = f.WithError(err)
|
|
|
|
|
} else {
|
|
|
|
|
http.Redirect(w, rq, "/admin/users/", http.StatusSeeOther)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
err := fmt.Errorf("passwords do not match")
|
|
|
|
|
f = f.WithError(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if f.HasError() {
|
|
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
|
|
|
}
|
|
|
|
|
w.Header().Set("Content-Type", mime.TypeByExtension(".html"))
|
|
|
|
|
|
|
|
|
|
viewEditUser(viewutil.MetaFrom(w, rq), f, u)
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-11 21:59:55 +05:00
|
|
|
|
func handlerAdminUserDelete(w http.ResponseWriter, rq *http.Request) {
|
|
|
|
|
vars := mux.Vars(rq)
|
|
|
|
|
u := user.ByName(vars["username"])
|
|
|
|
|
if u == nil {
|
|
|
|
|
util.HTTP404Page(w, "404 page not found")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
f := util.NewFormData()
|
|
|
|
|
|
|
|
|
|
if rq.Method == http.MethodPost {
|
|
|
|
|
f = f.WithError(user.DeleteUser(u.Name))
|
|
|
|
|
if !f.HasError() {
|
|
|
|
|
http.Redirect(w, rq, "/admin/users/", http.StatusSeeOther)
|
|
|
|
|
} else {
|
2024-09-07 23:55:39 +03:00
|
|
|
|
slog.Info("Failed to delete user", "err", f.Error())
|
2022-07-11 21:59:55 +05:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if f.HasError() {
|
|
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
|
|
|
}
|
|
|
|
|
w.Header().Set("Content-Type", mime.TypeByExtension(".html"))
|
2022-08-06 22:33:37 +05:00
|
|
|
|
viewDeleteUser(viewutil.MetaFrom(w, rq), f, u)
|
2022-07-11 21:59:55 +05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func handlerAdminUserNew(w http.ResponseWriter, rq *http.Request) {
|
|
|
|
|
if rq.Method == http.MethodGet {
|
|
|
|
|
w.Header().Set("Content-Type", mime.TypeByExtension(".html"))
|
2022-08-06 22:33:37 +05:00
|
|
|
|
viewNewUser(viewutil.MetaFrom(w, rq), util.NewFormData())
|
2022-07-11 21:59:55 +05:00
|
|
|
|
} else if rq.Method == http.MethodPost {
|
|
|
|
|
// Create a user
|
|
|
|
|
f := util.FormDataFromRequest(rq, []string{"name", "password", "group"})
|
|
|
|
|
|
|
|
|
|
err := user.Register(f.Get("name"), f.Get("password"), f.Get("group"), "local", true)
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
|
|
|
w.Header().Set("Content-Type", mime.TypeByExtension(".html"))
|
2022-08-06 22:33:37 +05:00
|
|
|
|
viewNewUser(viewutil.MetaFrom(w, rq), f.WithError(err))
|
2022-07-11 21:59:55 +05:00
|
|
|
|
} else {
|
|
|
|
|
http.Redirect(w, rq, "/admin/users/", http.StatusSeeOther)
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-05-18 21:03:36 +03:00
|
|
|
|
}
|