mirror of
https://github.com/osmarks/mycorrhiza.git
synced 2025-03-10 13:38:20 +00:00

Didn't have the chance to migrate //all// templates just yet. We'll get there. * Implement yet another template system * Move orphans to the new system and fix a bug in it * Link orphans in the admin panel * Move the backlink handlers to the web package * Move auth routing to web * Move /user-list to the new system * Move change password and translate it * Move stuff * Move admin-related stuff to the web * Move a lot of files into internal dir Outside of it are web and stuff that needs further refactoring * Fix static not loading and de-qtpl tree * Move tree to internal * Keep the globe on the same line #230 * Revert "Keep the globe on the same line #230" This reverts commit ae78e5e459b1e980ba89bf29e61f75c0625ed2c7. * Migrate templates from hypview: delete, edit, start empty and existing WIP The delete media view was removed, I didn't even know it still existed as a GET. A rudiment. * Make views multi-file and break compilation * Megarefactoring of hypha views * Auth-related stuffs * Fix some of those weird imports * Migrate cat views * Fix cat js * Lower standards * Internalize trauma
332 lines
10 KiB
Go
332 lines
10 KiB
Go
// Package web contains web handlers and initialization stuff.
|
||
package web
|
||
|
||
import (
|
||
"errors"
|
||
"fmt"
|
||
"github.com/bouncepaw/mycorrhiza/internal/cfg"
|
||
"github.com/bouncepaw/mycorrhiza/internal/user"
|
||
"github.com/bouncepaw/mycorrhiza/l18n"
|
||
"github.com/bouncepaw/mycorrhiza/web/viewutil"
|
||
"io"
|
||
"log"
|
||
"log/slog"
|
||
"mime"
|
||
"net/http"
|
||
"net/url"
|
||
"strings"
|
||
|
||
"github.com/bouncepaw/mycorrhiza/help"
|
||
"github.com/bouncepaw/mycorrhiza/history/histweb"
|
||
"github.com/bouncepaw/mycorrhiza/hypview"
|
||
"github.com/bouncepaw/mycorrhiza/interwiki"
|
||
"github.com/bouncepaw/mycorrhiza/misc"
|
||
"github.com/gorilla/mux"
|
||
|
||
"github.com/bouncepaw/mycorrhiza/util"
|
||
)
|
||
|
||
// Handler initializes and returns the HTTP router based on the configuration.
|
||
func Handler() http.Handler {
|
||
router := mux.NewRouter()
|
||
router.Use(func(next http.Handler) http.Handler {
|
||
return http.HandlerFunc(func(w http.ResponseWriter, rq *http.Request) {
|
||
util.PrepareRq(rq)
|
||
w.Header().Add("Content-Security-Policy",
|
||
"default-src 'self' telegram.org *.telegram.org; "+
|
||
"img-src * data:; media-src *; style-src *; font-src * data:")
|
||
next.ServeHTTP(w, rq)
|
||
})
|
||
})
|
||
router.StrictSlash(true)
|
||
|
||
// Public routes. They're always accessible regardless of the user status.
|
||
misc.InitAssetHandlers(router)
|
||
|
||
// Auth
|
||
router.HandleFunc("/user-list", handlerUserList)
|
||
router.HandleFunc("/lock", handlerLock)
|
||
// The check below saves a lot of extra checks and lines of codes in other places in this file.
|
||
if cfg.UseAuth {
|
||
if cfg.AllowRegistration {
|
||
router.HandleFunc("/register", handlerRegister).Methods(http.MethodPost, http.MethodGet)
|
||
}
|
||
if cfg.TelegramEnabled {
|
||
router.HandleFunc("/telegram-login", handlerTelegramLogin)
|
||
}
|
||
router.HandleFunc("/login", handlerLogin)
|
||
router.HandleFunc("/logout", handlerLogout)
|
||
}
|
||
|
||
// Wiki routes. They may be locked or restricted.
|
||
r := router.PathPrefix("").Subrouter()
|
||
r.Use(func(next http.Handler) http.Handler {
|
||
return http.HandlerFunc(func(w http.ResponseWriter, rq *http.Request) {
|
||
user := user.FromRequest(rq)
|
||
if !user.ShowLockMaybe(w, rq) {
|
||
next.ServeHTTP(w, rq)
|
||
}
|
||
})
|
||
})
|
||
|
||
initReaders(r)
|
||
initMutators(r)
|
||
help.InitHandlers(r)
|
||
misc.InitHandlers(r)
|
||
hypview.Init()
|
||
histweb.InitHandlers(r)
|
||
interwiki.InitHandlers(r)
|
||
|
||
r.PathPrefix("/add-to-category").HandlerFunc(handlerAddToCategory).Methods("POST")
|
||
r.PathPrefix("/remove-from-category").HandlerFunc(handlerRemoveFromCategory).Methods("POST")
|
||
r.PathPrefix("/category/").HandlerFunc(handlerCategory).Methods("GET")
|
||
r.PathPrefix("/edit-category/").HandlerFunc(handlerEditCategory).Methods("GET")
|
||
r.PathPrefix("/category").HandlerFunc(handlerListCategory).Methods("GET")
|
||
|
||
// Admin routes
|
||
if cfg.UseAuth {
|
||
adminRouter := r.PathPrefix("/admin").Subrouter()
|
||
adminRouter.Use(groupMiddleware("admin"))
|
||
|
||
adminRouter.HandleFunc("/shutdown", handlerAdminShutdown).Methods(http.MethodPost)
|
||
adminRouter.HandleFunc("/reindex-users", handlerAdminReindexUsers).Methods(http.MethodPost)
|
||
|
||
adminRouter.HandleFunc("/new-user", handlerAdminUserNew).Methods(http.MethodGet, http.MethodPost)
|
||
adminRouter.HandleFunc("/users/{username}/edit", handlerAdminUserEdit).Methods(http.MethodGet, http.MethodPost)
|
||
adminRouter.HandleFunc("/users/{username}/change-password", handlerAdminUserChangePassword).Methods(http.MethodPost)
|
||
adminRouter.HandleFunc("/users/{username}/delete", handlerAdminUserDelete).Methods(http.MethodGet, http.MethodPost)
|
||
adminRouter.HandleFunc("/users", handlerAdminUsers)
|
||
|
||
adminRouter.HandleFunc("/", handlerAdmin)
|
||
|
||
settingsRouter := r.PathPrefix("/settings").Subrouter()
|
||
// TODO: check if necessary?
|
||
//settingsRouter.Use(groupMiddleware("settings"))
|
||
settingsRouter.HandleFunc("/change-password", handlerUserChangePassword).Methods(http.MethodGet, http.MethodPost)
|
||
}
|
||
|
||
// Index page
|
||
r.HandleFunc("/", func(w http.ResponseWriter, rq *http.Request) {
|
||
// Let's pray it never fails
|
||
addr, _ := url.Parse("/hypha/" + cfg.HomeHypha)
|
||
rq.URL = addr
|
||
handlerHypha(w, rq)
|
||
})
|
||
|
||
initPages()
|
||
|
||
return router
|
||
}
|
||
|
||
func groupMiddleware(group string) func(http.Handler) http.Handler {
|
||
return func(next http.Handler) http.Handler {
|
||
return http.HandlerFunc(func(w http.ResponseWriter, rq *http.Request) {
|
||
if cfg.UseAuth && user.CanProceed(rq, group) {
|
||
next.ServeHTTP(w, rq)
|
||
return
|
||
}
|
||
|
||
// TODO: handle this better. Merge this code with all other
|
||
// authorization code in this project.
|
||
|
||
w.WriteHeader(http.StatusForbidden)
|
||
io.WriteString(w, "403 forbidden")
|
||
})
|
||
}
|
||
}
|
||
|
||
// Auth
|
||
func handlerUserList(w http.ResponseWriter, rq *http.Request) {
|
||
admins, moderators, editors, readers := user.UsersInGroups()
|
||
_ = pageUserList.RenderTo(viewutil.MetaFrom(w, rq),
|
||
map[string]any{
|
||
"Admins": admins,
|
||
"Moderators": moderators,
|
||
"Editors": editors,
|
||
"Readers": readers,
|
||
})
|
||
}
|
||
|
||
func handlerLock(w http.ResponseWriter, rq *http.Request) {
|
||
_ = pageAuthLock.RenderTo(viewutil.MetaFrom(w, rq), map[string]any{})
|
||
}
|
||
|
||
// handlerRegister displays the register form (GET) or registers the user (POST).
|
||
func handlerRegister(w http.ResponseWriter, rq *http.Request) {
|
||
util.PrepareRq(rq)
|
||
if rq.Method == http.MethodGet {
|
||
slog.Info("Showing registration form")
|
||
_ = pageAuthRegister.RenderTo(viewutil.MetaFrom(w, rq), map[string]any{
|
||
"UseAuth": cfg.UseAuth,
|
||
"AllowRegistration": cfg.AllowRegistration,
|
||
"RawQuery": rq.URL.RawQuery,
|
||
"WikiName": cfg.WikiName,
|
||
})
|
||
return
|
||
}
|
||
|
||
var (
|
||
username = rq.PostFormValue("username")
|
||
password = rq.PostFormValue("password")
|
||
err = user.Register(username, password, "editor", "local", false)
|
||
)
|
||
if err != nil {
|
||
slog.Info("Failed to register", "username", username, "err", err.Error())
|
||
w.Header().Set("Content-Type", mime.TypeByExtension(".html"))
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
_ = pageAuthRegister.RenderTo(viewutil.MetaFrom(w, rq), map[string]any{
|
||
"UseAuth": cfg.UseAuth,
|
||
"AllowRegistration": cfg.AllowRegistration,
|
||
"RawQuery": rq.URL.RawQuery,
|
||
"WikiName": cfg.WikiName,
|
||
|
||
"Err": err,
|
||
"Username": username,
|
||
"Password": password,
|
||
})
|
||
return
|
||
}
|
||
|
||
slog.Info("Registered user", "username", username)
|
||
if err := user.LoginDataHTTP(w, username, password); err != nil {
|
||
return
|
||
}
|
||
http.Redirect(w, rq, "/"+rq.URL.RawQuery, http.StatusSeeOther)
|
||
}
|
||
|
||
// handlerLogout shows the logout form (GET) or logs the user out (POST).
|
||
func handlerLogout(w http.ResponseWriter, rq *http.Request) {
|
||
if rq.Method == http.MethodPost {
|
||
slog.Info("Somebody logged out")
|
||
user.LogoutFromRequest(w, rq)
|
||
http.Redirect(w, rq, "/", http.StatusSeeOther)
|
||
return
|
||
}
|
||
|
||
var (
|
||
u = user.FromRequest(rq)
|
||
can = u != nil
|
||
)
|
||
w.Header().Set("Content-Type", "text/html;charset=utf-8")
|
||
if can {
|
||
slog.Info("Logging out", "username", u.Name)
|
||
w.WriteHeader(http.StatusOK)
|
||
} else {
|
||
slog.Info("Unknown user logging out")
|
||
w.WriteHeader(http.StatusForbidden)
|
||
}
|
||
_ = pageAuthLogout.RenderTo(viewutil.MetaFrom(w, rq), map[string]any{
|
||
"CanLogout": can,
|
||
})
|
||
}
|
||
|
||
// handlerLogin shows the login form (GET) or logs the user in (POST).
|
||
func handlerLogin(w http.ResponseWriter, rq *http.Request) {
|
||
if rq.Method == http.MethodGet {
|
||
w.WriteHeader(http.StatusOK)
|
||
_ = pageAuthLogin.RenderTo(viewutil.MetaFrom(w, rq), map[string]any{
|
||
"UseAuth": cfg.UseAuth,
|
||
"ErrUnknownUsername": false,
|
||
"ErrWrongPassword": false,
|
||
"ErrTelegram": false,
|
||
"Err": nil,
|
||
"WikiName": cfg.WikiName,
|
||
})
|
||
slog.Info("Somebody logging in")
|
||
return
|
||
}
|
||
|
||
var (
|
||
username = util.CanonicalName(rq.PostFormValue("username"))
|
||
password = rq.PostFormValue("password")
|
||
err = user.LoginDataHTTP(w, username, password)
|
||
)
|
||
if err != nil {
|
||
w.WriteHeader(http.StatusInternalServerError)
|
||
_ = pageAuthLogin.RenderTo(viewutil.MetaFrom(w, rq), map[string]any{
|
||
"UseAuth": cfg.UseAuth,
|
||
"ErrUnknownUsername": errors.Is(err, user.ErrUnknownUsername),
|
||
"ErrWrongPassword": errors.Is(err, user.ErrWrongPassword),
|
||
"ErrTelegram": false, // TODO: ?
|
||
"Err": err.Error(),
|
||
"WikiName": cfg.WikiName,
|
||
"Username": username,
|
||
})
|
||
slog.Info("Failed to log in", "username", username, "err", err.Error())
|
||
return
|
||
}
|
||
http.Redirect(w, rq, "/", http.StatusSeeOther)
|
||
slog.Info("Logged in", "username", username)
|
||
}
|
||
|
||
func handlerTelegramLogin(w http.ResponseWriter, rq *http.Request) {
|
||
// Note there is no lock here.
|
||
lc := l18n.FromRequest(rq)
|
||
w.Header().Set("Content-Type", "text/html;charset=utf-8")
|
||
_ = rq.ParseForm()
|
||
var (
|
||
values = rq.URL.Query()
|
||
username = strings.ToLower(values.Get("username"))
|
||
seemsValid = user.TelegramAuthParamsAreValid(values)
|
||
err = user.Register(
|
||
username,
|
||
"", // Password matters not
|
||
"editor",
|
||
"telegram",
|
||
false,
|
||
)
|
||
)
|
||
// If registering a user via Telegram failed, because a Telegram user with this name
|
||
// has already registered, then everything is actually ok!
|
||
if user.HasUsername(username) && user.ByName(username).Source == "telegram" {
|
||
err = nil
|
||
}
|
||
|
||
if !seemsValid {
|
||
err = errors.New("Wrong parameters")
|
||
}
|
||
|
||
if err != nil {
|
||
slog.Info("Failed to register", "username", username, "err", err.Error(), "method", "telegram")
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
_, _ = io.WriteString(
|
||
w,
|
||
viewutil.Base(
|
||
viewutil.MetaFrom(w, rq),
|
||
lc.Get("ui.error"),
|
||
fmt.Sprintf(
|
||
`<main class="main-width"><p>%s</p><p>%s</p><p><a href="/login">%s<a></p></main>`,
|
||
lc.Get("auth.error_telegram"),
|
||
err.Error(),
|
||
lc.Get("auth.go_login"),
|
||
),
|
||
map[string]string{},
|
||
),
|
||
)
|
||
return
|
||
}
|
||
|
||
errmsg := user.LoginDataHTTP(w, username, "")
|
||
if errmsg != nil {
|
||
log.Printf("Failed to login ‘%s’ using Telegram: %s", username, err.Error())
|
||
w.WriteHeader(http.StatusBadRequest)
|
||
_, _ = io.WriteString(
|
||
w,
|
||
viewutil.Base(
|
||
viewutil.MetaFrom(w, rq),
|
||
"Error",
|
||
fmt.Sprintf(
|
||
`<main class="main-width"><p>%s</p><p>%s</p><p><a href="/login">%s<a></p></main>`,
|
||
lc.Get("auth.error_telegram"),
|
||
err.Error(),
|
||
lc.Get("auth.go_login"),
|
||
),
|
||
map[string]string{},
|
||
),
|
||
)
|
||
return
|
||
}
|
||
http.Redirect(w, rq, "/", http.StatusSeeOther)
|
||
slog.Info("Logged in", "username", username, "method", "telegram")
|
||
}
|