2021-02-09 11:53:33 +00:00
|
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
|
2021-02-04 10:36:02 +00:00
|
|
|
package handlers
|
|
|
|
|
|
|
|
import (
|
2021-02-15 16:21:06 +00:00
|
|
|
"errors"
|
2021-02-04 10:36:02 +00:00
|
|
|
"fmt"
|
|
|
|
"net/http"
|
2021-02-16 11:18:01 +00:00
|
|
|
"time"
|
2021-02-04 10:36:02 +00:00
|
|
|
|
2021-02-16 10:20:38 +00:00
|
|
|
"github.com/gorilla/csrf"
|
2021-02-08 11:57:14 +00:00
|
|
|
"github.com/gorilla/sessions"
|
|
|
|
"go.mindeco.de/http/auth"
|
2021-02-04 10:36:02 +00:00
|
|
|
"go.mindeco.de/http/render"
|
2021-02-11 15:23:22 +00:00
|
|
|
"go.mindeco.de/logging"
|
2021-02-04 10:36:02 +00:00
|
|
|
|
2021-02-09 16:38:51 +00:00
|
|
|
"github.com/ssb-ngi-pointer/go-ssb-room/admindb"
|
|
|
|
"github.com/ssb-ngi-pointer/go-ssb-room/internal/repo"
|
2021-02-10 14:50:36 +00:00
|
|
|
"github.com/ssb-ngi-pointer/go-ssb-room/roomstate"
|
2021-02-09 16:38:51 +00:00
|
|
|
"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/handlers/news"
|
|
|
|
"github.com/ssb-ngi-pointer/go-ssb-room/web/i18n"
|
|
|
|
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
|
2021-02-04 10:36:02 +00:00
|
|
|
)
|
|
|
|
|
2021-02-22 16:55:12 +00:00
|
|
|
var HTMLTempaltes = []string{
|
|
|
|
"landing/index.tmpl",
|
|
|
|
"landing/about.tmpl",
|
2021-02-23 19:23:50 +00:00
|
|
|
"notice/list.tmpl",
|
|
|
|
"notice/show.tmpl",
|
2021-02-22 16:55:12 +00:00
|
|
|
"error.tmpl",
|
|
|
|
}
|
|
|
|
|
2021-02-04 16:21:21 +00:00
|
|
|
// New initializes the whole web stack for rooms, with all the sub-modules and routing.
|
2021-02-08 11:57:14 +00:00
|
|
|
func New(
|
2021-02-11 15:23:22 +00:00
|
|
|
logger logging.Interface,
|
2021-02-08 11:57:14 +00:00
|
|
|
repo repo.Interface,
|
2021-02-10 14:50:36 +00:00
|
|
|
roomState *roomstate.Manager,
|
2021-02-08 16:47:42 +00:00
|
|
|
as admindb.AuthWithSSBService,
|
|
|
|
fs admindb.AuthFallbackService,
|
2021-02-11 15:43:19 +00:00
|
|
|
al admindb.AllowListService,
|
2021-02-22 16:55:12 +00:00
|
|
|
ns admindb.NoticesService,
|
2021-02-23 19:23:50 +00:00
|
|
|
ps admindb.PinnedNoticesService,
|
2021-02-08 11:57:14 +00:00
|
|
|
) (http.Handler, error) {
|
2021-02-11 15:23:22 +00:00
|
|
|
m := router.CompleteApp()
|
2021-02-04 10:36:02 +00:00
|
|
|
|
2021-02-04 15:52:55 +00:00
|
|
|
locHelper, err := i18n.New(repo)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2021-02-09 11:39:57 +00:00
|
|
|
var a *auth.Handler
|
|
|
|
|
2021-02-22 14:26:51 +00:00
|
|
|
r, err := render.New(web.Templates,
|
2021-02-11 15:23:22 +00:00
|
|
|
render.SetLogger(logger),
|
2021-02-23 18:22:21 +00:00
|
|
|
render.BaseTemplates("base.tmpl", "menu.tmpl"),
|
2021-02-08 11:57:14 +00:00
|
|
|
render.AddTemplates(concatTemplates(
|
2021-02-22 16:55:12 +00:00
|
|
|
HTMLTempaltes,
|
2021-02-08 11:57:14 +00:00
|
|
|
news.HTMLTemplates,
|
2021-02-08 16:47:42 +00:00
|
|
|
roomsAuth.HTMLTemplates,
|
2021-02-08 11:57:14 +00:00
|
|
|
admin.HTMLTemplates,
|
|
|
|
)...),
|
2021-02-22 15:20:26 +00:00
|
|
|
render.ErrorTemplate("error.tmpl"),
|
2021-02-04 11:00:12 +00:00
|
|
|
render.FuncMap(web.TemplateFuncs(m)),
|
2021-02-04 16:21:21 +00:00
|
|
|
// TODO: move these to the i18n helper pkg
|
2021-02-10 14:50:36 +00:00
|
|
|
render.InjectTemplateFunc("i18npl", func(r *http.Request) interface{} {
|
2021-02-15 16:21:06 +00:00
|
|
|
loc := i18n.LocalizerFromRequest(locHelper, r)
|
2021-02-10 14:50:36 +00:00
|
|
|
return loc.LocalizePlurals
|
|
|
|
}),
|
2021-02-04 15:52:55 +00:00
|
|
|
render.InjectTemplateFunc("i18n", func(r *http.Request) interface{} {
|
2021-02-15 16:21:06 +00:00
|
|
|
loc := i18n.LocalizerFromRequest(locHelper, r)
|
2021-02-04 15:52:55 +00:00
|
|
|
return loc.LocalizeSimple
|
|
|
|
}),
|
2021-02-23 16:33:49 +00:00
|
|
|
render.InjectTemplateFunc("current_page_is", func(r *http.Request) interface{} {
|
|
|
|
return func(routeName string) bool {
|
|
|
|
url, err := router.CompleteApp().Get(routeName).URLPath()
|
|
|
|
if err != nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return r.RequestURI == url.Path
|
|
|
|
}
|
|
|
|
}),
|
2021-02-09 11:39:57 +00:00
|
|
|
render.InjectTemplateFunc("is_logged_in", func(r *http.Request) interface{} {
|
|
|
|
no := func() *admindb.User { return nil }
|
|
|
|
|
|
|
|
v, err := a.AuthenticateRequest(r)
|
|
|
|
if err != nil {
|
|
|
|
return no
|
|
|
|
}
|
2021-02-11 14:15:56 +00:00
|
|
|
|
2021-02-09 11:39:57 +00:00
|
|
|
uid, ok := v.(int64)
|
|
|
|
if !ok {
|
2021-02-22 15:29:05 +00:00
|
|
|
panic(fmt.Sprintf("warning: not the expected ID type from authenticated session: %T\n", v))
|
2021-02-09 11:39:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
user, err := fs.GetByID(r.Context(), uid)
|
|
|
|
if err != nil {
|
|
|
|
return no
|
|
|
|
}
|
|
|
|
|
|
|
|
yes := func() *admindb.User { return user }
|
|
|
|
return yes
|
|
|
|
}),
|
2021-02-04 10:36:02 +00:00
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("web Handler: failed to create renderer: %w", err)
|
|
|
|
}
|
|
|
|
|
2021-02-08 16:47:42 +00:00
|
|
|
cookieCodec, err := web.LoadOrCreateCookieSecrets(repo)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2021-02-08 11:57:14 +00:00
|
|
|
store := &sessions.CookieStore{
|
2021-02-08 16:47:42 +00:00
|
|
|
Codecs: cookieCodec,
|
2021-02-08 11:57:14 +00:00
|
|
|
Options: &sessions.Options{
|
|
|
|
Path: "/",
|
2021-02-16 11:18:01 +00:00
|
|
|
MaxAge: 2 * 60 * 60, // two hours in seconds // TODO: configure
|
2021-02-08 11:57:14 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2021-02-15 16:21:06 +00:00
|
|
|
// TODO: this is just the error handler for http/auth, not render
|
2021-02-09 12:40:57 +00:00
|
|
|
authErrH := func(rw http.ResponseWriter, req *http.Request, err error, code int) {
|
2021-02-15 16:21:06 +00:00
|
|
|
var ih = i18n.LocalizerFromRequest(locHelper, req)
|
|
|
|
|
|
|
|
// default, unlocalized message
|
2021-02-09 12:40:57 +00:00
|
|
|
msg := err.Error()
|
|
|
|
|
|
|
|
// localize some specific error messages
|
2021-02-15 16:21:06 +00:00
|
|
|
var (
|
|
|
|
aa admindb.ErrAlreadyAdded
|
|
|
|
)
|
|
|
|
switch {
|
|
|
|
case err == auth.ErrBadLogin:
|
|
|
|
msg = ih.LocalizeSimple("AuthErrorBadLogin")
|
|
|
|
|
|
|
|
case errors.Is(err, admindb.ErrNotFound):
|
|
|
|
msg = ih.LocalizeSimple("ErrorNotFound")
|
|
|
|
|
|
|
|
case errors.As(err, &aa):
|
|
|
|
msg = ih.LocalizeSimple("ErrorAlreadyAdded")
|
2021-02-09 12:40:57 +00:00
|
|
|
}
|
|
|
|
|
2021-02-22 14:26:51 +00:00
|
|
|
r.HTML("error.tmpl", func(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
|
2021-02-09 12:40:57 +00:00
|
|
|
return errorTemplateData{
|
|
|
|
Err: msg,
|
|
|
|
// TODO: localize?
|
|
|
|
Status: http.StatusText(code),
|
|
|
|
StatusCode: code,
|
|
|
|
}, nil
|
|
|
|
}).ServeHTTP(rw, req)
|
|
|
|
}
|
|
|
|
|
2021-02-22 14:26:51 +00:00
|
|
|
notAuthorizedH := r.HTML("error.tmpl", func(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
|
2021-02-08 11:57:14 +00:00
|
|
|
statusCode := http.StatusUnauthorized
|
|
|
|
rw.WriteHeader(statusCode)
|
|
|
|
return errorTemplateData{
|
|
|
|
statusCode,
|
|
|
|
"Unauthorized",
|
|
|
|
"you are not authorized to access the requested site",
|
|
|
|
}, nil
|
|
|
|
})
|
|
|
|
|
2021-02-09 11:39:57 +00:00
|
|
|
a, err = auth.NewHandler(fs,
|
2021-02-08 11:57:14 +00:00
|
|
|
auth.SetStore(store),
|
2021-02-09 12:40:57 +00:00
|
|
|
auth.SetErrorHandler(authErrH),
|
2021-02-08 11:57:14 +00:00
|
|
|
auth.SetNotAuthorizedHandler(notAuthorizedH),
|
2021-02-16 11:18:01 +00:00
|
|
|
auth.SetLifetime(2*time.Hour), // TODO: configure
|
2021-02-08 11:57:14 +00:00
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("web Handler: failed to init fallback auth system: %w", err)
|
|
|
|
}
|
|
|
|
|
2021-02-16 10:20:38 +00:00
|
|
|
// 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)
|
|
|
|
})),
|
|
|
|
)
|
|
|
|
|
2021-02-11 15:43:19 +00:00
|
|
|
// this router is a bit of a qurik
|
|
|
|
// TODO: explain problem between gorilla/mux named routers and authentication
|
|
|
|
mainMux := &http.ServeMux{}
|
|
|
|
|
2021-02-04 10:36:02 +00:00
|
|
|
// hookup handlers to the router
|
2021-02-11 14:15:56 +00:00
|
|
|
news.Handler(m, r)
|
2021-02-08 11:57:14 +00:00
|
|
|
roomsAuth.Handler(m, r, a)
|
|
|
|
|
2021-02-25 10:13:48 +00:00
|
|
|
adminHandler := a.Authenticate(admin.Handler(r, roomState, al, ns, ps))
|
2021-02-11 15:43:19 +00:00
|
|
|
mainMux.Handle("/admin/", adminHandler)
|
2021-02-04 10:36:02 +00:00
|
|
|
|
2021-02-22 14:26:51 +00:00
|
|
|
m.Get(router.CompleteIndex).Handler(r.StaticHTML("landing/index.tmpl"))
|
|
|
|
m.Get(router.CompleteAbout).Handler(r.StaticHTML("landing/about.tmpl"))
|
2021-02-04 10:36:02 +00:00
|
|
|
|
2021-02-23 19:23:50 +00:00
|
|
|
var nr noticeHandler
|
2021-02-22 16:55:12 +00:00
|
|
|
nr.notices = ns
|
2021-02-23 19:23:50 +00:00
|
|
|
nr.pinned = ps
|
|
|
|
m.Get(router.CompleteNoticeList).Handler(r.HTML("notice/list.tmpl", nr.list))
|
|
|
|
m.Get(router.CompleteNoticeShow).Handler(r.HTML("notice/show.tmpl", nr.show))
|
2021-02-22 16:55:12 +00:00
|
|
|
|
2021-02-22 14:26:51 +00:00
|
|
|
m.PathPrefix("/assets/").Handler(http.StripPrefix("/assets/", http.FileServer(web.Assets)))
|
2021-02-04 16:46:51 +00:00
|
|
|
|
2021-02-22 14:26:51 +00:00
|
|
|
m.NotFoundHandler = r.HTML("error.tmpl", func(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
|
2021-02-08 11:57:14 +00:00
|
|
|
rw.WriteHeader(http.StatusNotFound)
|
2021-02-15 16:21:06 +00:00
|
|
|
msg := i18n.LocalizerFromRequest(locHelper, req).LocalizeSimple("PageNotFound")
|
2021-02-15 11:08:33 +00:00
|
|
|
return errorTemplateData{http.StatusNotFound, "Not Found", msg}, nil
|
2021-02-04 10:36:02 +00:00
|
|
|
})
|
2021-02-04 15:52:55 +00:00
|
|
|
|
2021-02-11 15:43:19 +00:00
|
|
|
mainMux.Handle("/", m)
|
|
|
|
|
2021-02-16 10:20:38 +00:00
|
|
|
// apply middleware
|
2021-02-11 15:43:19 +00:00
|
|
|
var finalHandler http.Handler = mainMux
|
|
|
|
finalHandler = logging.InjectHandler(logger)(finalHandler)
|
2021-02-16 10:20:38 +00:00
|
|
|
finalHandler = CSRF(finalHandler)
|
2021-02-11 15:23:22 +00:00
|
|
|
|
2021-02-08 11:57:14 +00:00
|
|
|
if web.Production {
|
2021-02-11 15:23:22 +00:00
|
|
|
return finalHandler, nil
|
2021-02-08 11:57:14 +00:00
|
|
|
}
|
|
|
|
|
2021-02-11 15:23:22 +00:00
|
|
|
return r.GetReloader()(finalHandler), nil
|
2021-02-04 10:36:02 +00:00
|
|
|
}
|
2021-02-08 11:57:14 +00:00
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|