352 lines
9.5 KiB
Go
352 lines
9.5 KiB
Go
// SPDX-License-Identifier: MIT
|
|
|
|
package handlers
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"html/template"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/gorilla/csrf"
|
|
"github.com/gorilla/sessions"
|
|
"github.com/russross/blackfriday/v2"
|
|
"go.mindeco.de/http/auth"
|
|
"go.mindeco.de/http/render"
|
|
"go.mindeco.de/logging"
|
|
|
|
"github.com/ssb-ngi-pointer/go-ssb-room/internal/network"
|
|
"github.com/ssb-ngi-pointer/go-ssb-room/internal/repo"
|
|
"github.com/ssb-ngi-pointer/go-ssb-room/internal/signinwithssb"
|
|
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
|
|
"github.com/ssb-ngi-pointer/go-ssb-room/roomstate"
|
|
"github.com/ssb-ngi-pointer/go-ssb-room/web"
|
|
"github.com/ssb-ngi-pointer/go-ssb-room/web/handlers/admin"
|
|
roomsAuth "github.com/ssb-ngi-pointer/go-ssb-room/web/handlers/auth"
|
|
"github.com/ssb-ngi-pointer/go-ssb-room/web/i18n"
|
|
"github.com/ssb-ngi-pointer/go-ssb-room/web/members"
|
|
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
|
|
refs "go.mindeco.de/ssb-refs"
|
|
)
|
|
|
|
var HTMLTemplates = []string{
|
|
"landing/index.tmpl",
|
|
"landing/about.tmpl",
|
|
"aliases-resolved.html",
|
|
"invite/accept.tmpl",
|
|
"invite/consumed.tmpl",
|
|
"notice/list.tmpl",
|
|
"notice/show.tmpl",
|
|
"error.tmpl",
|
|
}
|
|
|
|
// Databases is an options stuct for the required databases of the web handlers
|
|
type Databases struct {
|
|
Aliases roomdb.AliasesService
|
|
AuthFallback roomdb.AuthFallbackService
|
|
AuthWithSSB roomdb.AuthWithSSBService
|
|
DeniedKeys roomdb.DeniedKeysService
|
|
Invites roomdb.InvitesService
|
|
Notices roomdb.NoticesService
|
|
Members roomdb.MembersService
|
|
PinnedNotices roomdb.PinnedNoticesService
|
|
}
|
|
|
|
// NetworkInfo encapsulates the domain name of the room, it's ssb/secret-handshake public key and the HTTP and MUXRPC TCP ports.
|
|
type NetworkInfo struct {
|
|
PortMUXRPC uint
|
|
PortHTTPS uint // 0 assumes default (443)
|
|
|
|
RoomID refs.FeedRef
|
|
|
|
Domain string
|
|
}
|
|
|
|
// New initializes the whole web stack for rooms, with all the sub-modules and routing.
|
|
func New(
|
|
logger logging.Interface,
|
|
repo repo.Interface,
|
|
netInfo NetworkInfo,
|
|
roomState *roomstate.Manager,
|
|
roomEndpoints network.Endpoints,
|
|
bridge *signinwithssb.SignalBridge,
|
|
dbs Databases,
|
|
) (http.Handler, error) {
|
|
m := router.CompleteApp()
|
|
|
|
locHelper, err := i18n.New(repo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
r, err := render.New(web.Templates,
|
|
render.SetLogger(logger),
|
|
render.BaseTemplates("base.tmpl", "menu.tmpl"),
|
|
render.AddTemplates(concatTemplates(
|
|
HTMLTemplates,
|
|
roomsAuth.HTMLTemplates,
|
|
admin.HTMLTemplates,
|
|
)...),
|
|
render.ErrorTemplate("error.tmpl"),
|
|
render.FuncMap(web.TemplateFuncs(m)),
|
|
// TODO: move these to the i18n helper pkg
|
|
render.InjectTemplateFunc("i18npl", func(r *http.Request) interface{} {
|
|
loc := i18n.LocalizerFromRequest(locHelper, r)
|
|
return loc.LocalizePlurals
|
|
}),
|
|
render.InjectTemplateFunc("i18n", func(r *http.Request) interface{} {
|
|
loc := i18n.LocalizerFromRequest(locHelper, r)
|
|
return loc.LocalizeSimple
|
|
}),
|
|
render.InjectTemplateFunc("current_page_is", func(r *http.Request) interface{} {
|
|
return func(routeName string) bool {
|
|
route := router.CompleteApp().Get(routeName)
|
|
if route == nil {
|
|
return false
|
|
}
|
|
url, err := route.URLPath()
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return r.RequestURI == url.Path
|
|
}
|
|
}),
|
|
render.InjectTemplateFunc("urlToNotice", func(r *http.Request) interface{} {
|
|
return func(name string) *url.URL {
|
|
noticeName := roomdb.PinnedNoticeName(name)
|
|
if !noticeName.Valid() {
|
|
return nil
|
|
}
|
|
notice, err := dbs.PinnedNotices.Get(r.Context(), noticeName, "en-GB")
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
route := router.CompleteApp().GetRoute(router.CompleteNoticeShow)
|
|
if route == nil {
|
|
return nil
|
|
}
|
|
u, err := route.URLPath()
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
noticeID := strconv.FormatInt(notice.ID, 10)
|
|
q := u.Query()
|
|
q.Add("id", noticeID)
|
|
u.RawQuery = q.Encode()
|
|
return u
|
|
}
|
|
}),
|
|
render.InjectTemplateFunc("is_logged_in", members.TemplateHelper()),
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("web Handler: failed to create renderer: %w", err)
|
|
}
|
|
|
|
cookieCodec, err := web.LoadOrCreateCookieSecrets(repo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cookieStore := &sessions.CookieStore{
|
|
Codecs: cookieCodec,
|
|
Options: &sessions.Options{
|
|
Path: "/",
|
|
MaxAge: 2 * 60 * 60, // two hours in seconds // TODO: configure
|
|
},
|
|
}
|
|
|
|
// TODO: this is just the error handler for http/auth, not render
|
|
authErrH := func(rw http.ResponseWriter, req *http.Request, err error, code int) {
|
|
var ih = i18n.LocalizerFromRequest(locHelper, req)
|
|
|
|
// default, unlocalized message
|
|
msg := err.Error()
|
|
|
|
// localize some specific error messages
|
|
var (
|
|
aa roomdb.ErrAlreadyAdded
|
|
)
|
|
switch {
|
|
case err == auth.ErrBadLogin:
|
|
msg = ih.LocalizeSimple("AuthErrorBadLogin")
|
|
|
|
case errors.Is(err, roomdb.ErrNotFound):
|
|
msg = ih.LocalizeSimple("ErrorNotFound")
|
|
|
|
case errors.As(err, &aa):
|
|
msg = ih.LocalizeSimple("ErrorAlreadyAdded")
|
|
}
|
|
|
|
r.HTML("error.tmpl", func(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
return errorTemplateData{
|
|
Err: msg,
|
|
// TODO: localize?
|
|
Status: http.StatusText(code),
|
|
StatusCode: code,
|
|
}, nil
|
|
}).ServeHTTP(rw, req)
|
|
}
|
|
|
|
notAuthorizedH := r.HTML("error.tmpl", func(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
statusCode := http.StatusUnauthorized
|
|
rw.WriteHeader(statusCode)
|
|
return errorTemplateData{
|
|
statusCode,
|
|
"Unauthorized",
|
|
"you are not authorized to access the requested site",
|
|
}, nil
|
|
})
|
|
|
|
authWithPassword, err := auth.NewHandler(dbs.AuthFallback,
|
|
auth.SetStore(cookieStore),
|
|
auth.SetErrorHandler(authErrH),
|
|
auth.SetNotAuthorizedHandler(notAuthorizedH),
|
|
auth.SetLifetime(2*time.Hour), // TODO: configure
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("web Handler: failed to init fallback auth system: %w", err)
|
|
}
|
|
|
|
// Cross Site Request Forgery prevention middleware
|
|
csrfKey, err := web.LoadOrCreateCSRFSecret(repo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
CSRF := csrf.Protect(csrfKey,
|
|
csrf.ErrorHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
|
err := csrf.FailureReason(req)
|
|
// TODO: localize error?
|
|
r.Error(w, req, http.StatusForbidden, err)
|
|
})),
|
|
)
|
|
|
|
// this router is a bit of a qurik
|
|
// TODO: explain problem between gorilla/mux named routers and authentication
|
|
mainMux := &http.ServeMux{}
|
|
|
|
// start hooking up handlers to the router
|
|
|
|
authWithSSB := roomsAuth.NewWithSSBHandler(
|
|
m,
|
|
r,
|
|
netInfo.RoomID,
|
|
roomEndpoints,
|
|
dbs.Aliases,
|
|
dbs.Members,
|
|
dbs.AuthWithSSB,
|
|
cookieStore,
|
|
bridge,
|
|
)
|
|
|
|
// just hooks up the router to the handler
|
|
roomsAuth.NewFallbackPasswordHandler(m, r, authWithPassword)
|
|
|
|
adminHandler := admin.Handler(
|
|
netInfo.Domain,
|
|
r,
|
|
roomState,
|
|
admin.Databases{
|
|
Aliases: dbs.Aliases,
|
|
DeniedKeys: dbs.DeniedKeys,
|
|
Invites: dbs.Invites,
|
|
Notices: dbs.Notices,
|
|
Members: dbs.Members,
|
|
PinnedNotices: dbs.PinnedNotices,
|
|
},
|
|
)
|
|
mainMux.Handle("/admin/", members.AuthenticateFromContext(r)(adminHandler))
|
|
|
|
m.Get(router.CompleteIndex).Handler(r.HTML("landing/index.tmpl", func(w http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
notice, err := dbs.PinnedNotices.Get(req.Context(), roomdb.NoticeDescription, "en-GB")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to find description: %w", err)
|
|
}
|
|
markdown := blackfriday.Run([]byte(notice.Content), blackfriday.WithNoExtensions())
|
|
return noticeShowData{
|
|
ID: notice.ID,
|
|
Title: notice.Title,
|
|
Content: template.HTML(markdown),
|
|
Language: notice.Language,
|
|
}, nil
|
|
}))
|
|
m.Get(router.CompleteAbout).Handler(r.StaticHTML("landing/about.tmpl"))
|
|
|
|
var nh = noticeHandler{
|
|
notices: dbs.Notices,
|
|
pinned: dbs.PinnedNotices,
|
|
}
|
|
m.Get(router.CompleteNoticeList).Handler(r.HTML("notice/list.tmpl", nh.list))
|
|
m.Get(router.CompleteNoticeShow).Handler(r.HTML("notice/show.tmpl", nh.show))
|
|
|
|
var ah = aliasHandler{
|
|
r: r,
|
|
|
|
db: dbs.Aliases,
|
|
|
|
roomID: netInfo.RoomID,
|
|
muxrpcHostAndPort: fmt.Sprintf("%s:%d", netInfo.Domain, netInfo.PortMUXRPC),
|
|
}
|
|
m.Get(router.CompleteAliasResolve).HandlerFunc(ah.resolve)
|
|
|
|
var ih = inviteHandler{
|
|
invites: dbs.Invites,
|
|
|
|
roomPubKey: netInfo.RoomID.PubKey(),
|
|
muxrpcHostAndPort: fmt.Sprintf("%s:%d", netInfo.Domain, netInfo.PortMUXRPC),
|
|
}
|
|
m.Get(router.CompleteInviteAccept).Handler(r.HTML("invite/accept.tmpl", ih.acceptForm))
|
|
m.Get(router.CompleteInviteConsume).Handler(r.HTML("invite/consumed.tmpl", ih.consume))
|
|
|
|
m.PathPrefix("/assets/").Handler(http.StripPrefix("/assets/", http.FileServer(web.Assets)))
|
|
|
|
m.NotFoundHandler = r.HTML("error.tmpl", func(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
rw.WriteHeader(http.StatusNotFound)
|
|
msg := i18n.LocalizerFromRequest(locHelper, req).LocalizeSimple("PageNotFound")
|
|
return errorTemplateData{http.StatusNotFound, "Not Found", msg}, nil
|
|
})
|
|
|
|
mainMux.Handle("/", m)
|
|
|
|
// apply HTTP middleware
|
|
middlewares := []func(http.Handler) http.Handler{
|
|
logging.InjectHandler(logger),
|
|
members.ContextInjecter(dbs.Members, authWithPassword, authWithSSB),
|
|
CSRF,
|
|
}
|
|
|
|
if !web.Production {
|
|
middlewares = append(middlewares, r.GetReloader())
|
|
}
|
|
|
|
var finalHandler http.Handler = mainMux
|
|
for _, applyMiddleware := range middlewares {
|
|
finalHandler = applyMiddleware(finalHandler)
|
|
}
|
|
|
|
return finalHandler, nil
|
|
}
|
|
|
|
// utils
|
|
|
|
type errorTemplateData struct {
|
|
StatusCode int
|
|
Status string
|
|
Err string
|
|
}
|
|
|
|
func concatTemplates(lst ...[]string) []string {
|
|
var catted []string
|
|
|
|
for _, tpls := range lst {
|
|
for _, t := range tpls {
|
|
catted = append(catted, t)
|
|
}
|
|
|
|
}
|
|
return catted
|
|
}
|