Show admin links when logged in

This commit is contained in:
Jan-Lukas Else 2021-02-20 23:35:16 +01:00
parent bb73d4831c
commit e74afac829
18 changed files with 121 additions and 69 deletions

View File

@ -2,6 +2,7 @@ package main
import ( import (
"bytes" "bytes"
"context"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"io" "io"
@ -25,20 +26,20 @@ func jwtKey() []byte {
func authMiddleware(next http.Handler) http.Handler { func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Check if already logged in
if loggedIn, ok := r.Context().Value(loggedInKey).(bool); ok && loggedIn {
next.ServeHTTP(w, r)
return
}
// 1. Check BasicAuth // 1. Check BasicAuth
if username, password, ok := r.BasicAuth(); ok && checkCredentials(username, password) { if username, password, ok := r.BasicAuth(); ok && checkCredentials(username, password) {
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
return return
} }
// 2. Check JWT // 2. Check JWT
if tokenCookie, err := r.Cookie("token"); err == nil { if checkAuthToken(r) {
claims := &authClaims{} next.ServeHTTP(w, r)
if tkn, err := jwt.ParseWithClaims(tokenCookie.Value, claims, func(t *jwt.Token) (interface{}, error) { return
return jwtKey(), nil
}); err == nil && tkn.Valid && claims.TokenType == "login" && checkUsername(claims.Username) {
next.ServeHTTP(w, r)
return
}
} }
// 3. Show login form // 3. Show login form
w.WriteHeader(http.StatusUnauthorized) w.WriteHeader(http.StatusUnauthorized)
@ -50,7 +51,7 @@ func authMiddleware(next http.Handler) http.Handler {
_ = r.ParseForm() _ = r.ParseForm()
b = []byte(r.PostForm.Encode()) b = []byte(r.PostForm.Encode())
} }
render(w, templateLogin, &renderData{ render(w, r, templateLogin, &renderData{
Data: map[string]string{ Data: map[string]string{
"loginmethod": r.Method, "loginmethod": r.Method,
"loginheaders": base64.StdEncoding.EncodeToString(h), "loginheaders": base64.StdEncoding.EncodeToString(h),
@ -60,6 +61,32 @@ func authMiddleware(next http.Handler) http.Handler {
}) })
} }
func checkAuthToken(r *http.Request) bool {
if tokenCookie, err := r.Cookie("token"); err == nil {
claims := &authClaims{}
if tkn, err := jwt.ParseWithClaims(tokenCookie.Value, claims, func(t *jwt.Token) (interface{}, error) {
return jwtKey(), nil
}); err == nil && tkn.Valid &&
claims.TokenType == "login" &&
checkUsername(claims.Username) {
return true
}
}
return false
}
const loggedInKey requestContextKey = "loggedIn"
func checkLoggedIn(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if checkAuthToken(r) {
next.ServeHTTP(rw, r.WithContext(context.WithValue(r.Context(), loggedInKey, true)))
return
}
next.ServeHTTP(rw, r)
})
}
func checkIsLogin(next http.Handler) http.Handler { func checkIsLogin(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if !checkLogin(rw, r) { if !checkLogin(rw, r) {

View File

@ -4,7 +4,7 @@ import (
"net/http" "net/http"
) )
func serveBlogStats(blog, statsPath string) func(w http.ResponseWriter, r *http.Request) { func serveBlogStats(blog, statsPath string) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
// Build query // Build query
query, params := buildPostsQuery(&postsRequestConfig{ query, params := buildPostsQuery(&postsRequestConfig{
@ -36,7 +36,7 @@ func serveBlogStats(blog, statsPath string) func(w http.ResponseWriter, r *http.
counts = append(counts, count) counts = append(counts, count)
} }
} }
render(w, templateBlogStats, &renderData{ render(w, r, templateBlogStats, &renderData{
BlogString: blog, BlogString: blog,
Canonical: statsPath, Canonical: statsPath,
Data: map[string]interface{}{ Data: map[string]interface{}{

View File

@ -31,43 +31,52 @@ func initCache() (err error) {
func cacheMiddleware(next http.Handler) http.Handler { func cacheMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if appConfig.Cache.Enable && // Do checks
// check method if !appConfig.Cache.Enable {
(r.Method == http.MethodGet || r.Method == http.MethodHead) && next.ServeHTTP(w, r)
// check bypass query return
!(r.URL.Query().Get("cache") == "0" || r.URL.Query().Get("cache") == "false") { }
key := cacheKey(r) if !(r.Method == http.MethodGet || r.Method == http.MethodHead) {
// Get cache or render it next.ServeHTTP(w, r)
cacheInterface, _, _ := cacheGroup.Do(key, func() (interface{}, error) { return
return getCache(key, next, r), nil }
}) if r.URL.Query().Get("cache") == "0" || r.URL.Query().Get("cache") == "false" {
cache := cacheInterface.(*cacheItem) next.ServeHTTP(w, r)
// copy cached headers return
for k, v := range cache.header { }
w.Header()[k] = v if loggedIn, ok := r.Context().Value(loggedInKey).(bool); ok && loggedIn {
} next.ServeHTTP(w, r)
setCacheHeaders(w, cache) return
// check conditional request }
if ifNoneMatchHeader := r.Header.Get("If-None-Match"); ifNoneMatchHeader != "" && ifNoneMatchHeader == cache.eTag { // Search and serve cache
key := cacheKey(r)
// Get cache or render it
cacheInterface, _, _ := cacheGroup.Do(key, func() (interface{}, error) {
return getCache(key, next, r), nil
})
cache := cacheInterface.(*cacheItem)
// copy cached headers
for k, v := range cache.header {
w.Header()[k] = v
}
setCacheHeaders(w, cache)
// check conditional request
if ifNoneMatchHeader := r.Header.Get("If-None-Match"); ifNoneMatchHeader != "" && ifNoneMatchHeader == cache.eTag {
// send 304
w.WriteHeader(http.StatusNotModified)
return
}
if ifModifiedSinceHeader := r.Header.Get("If-Modified-Since"); ifModifiedSinceHeader != "" {
if t, err := dateparse.ParseAny(ifModifiedSinceHeader); err == nil && t.After(cache.creationTime) {
// send 304 // send 304
w.WriteHeader(http.StatusNotModified) w.WriteHeader(http.StatusNotModified)
return return
} }
if ifModifiedSinceHeader := r.Header.Get("If-Modified-Since"); ifModifiedSinceHeader != "" {
t, err := dateparse.ParseAny(ifModifiedSinceHeader)
if err == nil && t.After(cache.creationTime) {
// send 304
w.WriteHeader(http.StatusNotModified)
return
}
}
// set status code
w.WriteHeader(cache.code)
// write cached body
_, _ = w.Write(cache.body)
return
} }
next.ServeHTTP(w, r) // set status code
w.WriteHeader(cache.code)
// write cached body
_, _ = w.Write(cache.body)
}) })
} }

View File

@ -34,7 +34,7 @@ func captchaMiddleware(next http.Handler) http.Handler {
_ = r.ParseForm() _ = r.ParseForm()
b = []byte(r.PostForm.Encode()) b = []byte(r.PostForm.Encode())
} }
render(w, templateCaptcha, &renderData{ render(w, r, templateCaptcha, &renderData{
Data: map[string]string{ Data: map[string]string{
"captchamethod": r.Method, "captchamethod": r.Method,
"captchaheaders": base64.StdEncoding.EncodeToString(h), "captchaheaders": base64.StdEncoding.EncodeToString(h),

View File

@ -41,7 +41,7 @@ func serveComment(blog string) func(http.ResponseWriter, *http.Request) {
return return
} }
w.Header().Set("X-Robots-Tag", "noindex") w.Header().Set("X-Robots-Tag", "noindex")
render(w, templateComment, &renderData{ render(w, r, templateComment, &renderData{
BlogString: blog, BlogString: blog,
Canonical: appConfig.Server.PublicAddress + appConfig.Blogs[blog].getRelativePath(fmt.Sprintf("/comment/%d", id)), Canonical: appConfig.Server.PublicAddress + appConfig.Blogs[blog].getRelativePath(fmt.Sprintf("/comment/%d", id)),
Data: comment, Data: comment,

View File

@ -33,7 +33,7 @@ func (p *commentsPaginationAdapter) Slice(offset, length int, data interface{})
return err return err
} }
func commentsAdmin(blog, commentPath string) func(w http.ResponseWriter, r *http.Request) { func commentsAdmin(blog, commentPath string) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
// Adapter // Adapter
pageNoString := chi.URLParam(r, "page") pageNoString := chi.URLParam(r, "page")
@ -69,7 +69,7 @@ func commentsAdmin(blog, commentPath string) func(w http.ResponseWriter, r *http
} }
nextPath = fmt.Sprintf("%s/page/%d", commentPath, nextPage) nextPath = fmt.Sprintf("%s/page/%d", commentPath, nextPage)
// Render // Render
render(w, templateCommentsAdmin, &renderData{ render(w, r, templateCommentsAdmin, &renderData{
BlogString: blog, BlogString: blog,
Data: map[string]interface{}{ Data: map[string]interface{}{
"Comments": comments, "Comments": comments,

View File

@ -2,8 +2,8 @@ package main
import "net/http" import "net/http"
func serveCustomPage(blog *configBlog, page *customPage) func(w http.ResponseWriter, r *http.Request) { func serveCustomPage(blog *configBlog, page *customPage) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, _ *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
if appConfig.Cache != nil && appConfig.Cache.Enable && page.Cache { if appConfig.Cache != nil && appConfig.Cache.Enable && page.Cache {
if page.CacheExpiration != 0 { if page.CacheExpiration != 0 {
setInternalCacheExpirationHeader(w, page.CacheExpiration) setInternalCacheExpirationHeader(w, page.CacheExpiration)
@ -11,7 +11,7 @@ func serveCustomPage(blog *configBlog, page *customPage) func(w http.ResponseWri
setInternalCacheExpirationHeader(w, int(appConfig.Cache.Expiration)) setInternalCacheExpirationHeader(w, int(appConfig.Cache.Expiration))
} }
} }
render(w, page.Template, &renderData{ render(w, r, page.Template, &renderData{
Blog: blog, Blog: blog,
Canonical: appConfig.Server.PublicAddress + page.Path, Canonical: appConfig.Server.PublicAddress + page.Path,
Data: page.Data, Data: page.Data,

View File

@ -11,9 +11,9 @@ import (
const editorPath = "/editor" const editorPath = "/editor"
func serveEditor(blog string) func(w http.ResponseWriter, _ *http.Request) { func serveEditor(blog string) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, _ *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
render(w, templateEditor, &renderData{ render(w, r, templateEditor, &renderData{
BlogString: blog, BlogString: blog,
Data: map[string]interface{}{ Data: map[string]interface{}{
"Drafts": loadDrafts(blog), "Drafts": loadDrafts(blog),
@ -38,7 +38,7 @@ func serveEditorPost(blog string) func(w http.ResponseWriter, r *http.Request) {
return return
} }
mf := post.toMfItem() mf := post.toMfItem()
render(w, templateEditor, &renderData{ render(w, r, templateEditor, &renderData{
BlogString: blog, BlogString: blog,
Data: map[string]interface{}{ Data: map[string]interface{}{
"UpdatePostURL": parsedURL.String(), "UpdatePostURL": parsedURL.String(),

View File

@ -24,7 +24,7 @@ func serveError(w http.ResponseWriter, r *http.Request, message string, status i
if message == "" { if message == "" {
message = http.StatusText(status) message = http.StatusText(status)
} }
render(w, templateError, &renderData{ render(w, r, templateError, &renderData{
Data: &errorData{ Data: &errorData{
Title: title, Title: title,
Message: message, Message: message,

View File

@ -98,6 +98,7 @@ func buildHandler() (http.Handler, error) {
} }
r.Use(checkIsLogin) r.Use(checkIsLogin)
r.Use(checkIsCaptcha) r.Use(checkIsCaptcha)
r.Use(checkLoggedIn)
// Profiler // Profiler
if appConfig.Server.Debug { if appConfig.Server.Debug {

View File

@ -53,7 +53,7 @@ func indieAuthRequest(w http.ResponseWriter, r *http.Request) {
serveError(w, r, "state must not be empty", http.StatusBadRequest) serveError(w, r, "state must not be empty", http.StatusBadRequest)
return return
} }
render(w, "indieauth", &renderData{ render(w, r, "indieauth", &renderData{
Data: data, Data: data,
}) })
} }

View File

@ -144,7 +144,7 @@ func notificationsAdmin(notificationPath string) func(http.ResponseWriter, *http
} }
nextPath = fmt.Sprintf("%s/page/%d", notificationPath, nextPage) nextPath = fmt.Sprintf("%s/page/%d", notificationPath, nextPage)
// Render // Render
render(w, templateNotificationsAdmin, &renderData{ render(w, r, templateNotificationsAdmin, &renderData{
Data: map[string]interface{}{ Data: map[string]interface{}{
"Notifications": notifications, "Notifications": notifications,
"HasPrev": hasPrev, "HasPrev": hasPrev,

View File

@ -66,7 +66,7 @@ func servePost(w http.ResponseWriter, r *http.Request) {
template = templateStaticHome template = templateStaticHome
} }
w.Header().Add("Link", fmt.Sprintf("<%s>; rel=shortlink", p.shortURL())) w.Header().Add("Link", fmt.Sprintf("<%s>; rel=shortlink", p.shortURL()))
render(w, template, &renderData{ render(w, r, template, &renderData{
BlogString: p.Blog, BlogString: p.Blog,
Canonical: canonical, Canonical: canonical,
Data: p, Data: p,
@ -285,7 +285,7 @@ func serveIndex(ic *indexConfig) func(w http.ResponseWriter, r *http.Request) {
if summaryTemplate == "" { if summaryTemplate == "" {
summaryTemplate = templateSummary summaryTemplate = templateSummary
} }
render(w, templateIndex, &renderData{ render(w, r, templateIndex, &renderData{
BlogString: ic.blog, BlogString: ic.blog,
Canonical: appConfig.Server.PublicAddress + path, Canonical: appConfig.Server.PublicAddress + path,
Data: map[string]interface{}{ Data: map[string]interface{}{

View File

@ -39,6 +39,7 @@ const (
templateCaptcha = "captcha" templateCaptcha = "captcha"
templateCommentsAdmin = "commentsadmin" templateCommentsAdmin = "commentsadmin"
templateNotificationsAdmin = "notificationsadmin" templateNotificationsAdmin = "notificationsadmin"
templateWebmentionAdmin = "webmentionadmin"
) )
var templates map[string]*template.Template var templates map[string]*template.Template
@ -238,9 +239,10 @@ type renderData struct {
Canonical string Canonical string
Blog *configBlog Blog *configBlog
Data interface{} Data interface{}
LoggedIn bool
} }
func render(w http.ResponseWriter, template string, data *renderData) { func render(w http.ResponseWriter, r *http.Request, template string, data *renderData) {
// Check render data // Check render data
if data.Blog == nil { if data.Blog == nil {
if len(data.BlogString) == 0 { if len(data.BlogString) == 0 {
@ -259,15 +261,20 @@ func render(w http.ResponseWriter, template string, data *renderData) {
if data.Data == nil { if data.Data == nil {
data.Data = map[string]interface{}{} data.Data = map[string]interface{}{}
} }
// We need to use a buffer here to enable minification // Check login
var buffer bytes.Buffer if loggedIn, ok := r.Context().Value(loggedInKey).(bool); ok && loggedIn {
err := templates[template].ExecuteTemplate(&buffer, template, data) data.LoggedIn = true
}
// Minify and write response
mw := minifier.Writer(contentTypeHTML, w)
defer func() {
_ = mw.Close()
}()
err := templates[template].ExecuteTemplate(mw, template, data)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
// Set content type // Set content type
w.Header().Set(contentType, contentTypeHTMLUTF8) w.Header().Set(contentType, contentTypeHTMLUTF8)
// Write buffered response
_, _ = writeMinified(w, contentTypeHTML, buffer.Bytes())
} }

View File

@ -21,7 +21,7 @@ func serveSearch(blog string, servePath string) func(w http.ResponseWriter, r *h
http.Redirect(w, r, path.Join(servePath, searchEncode(q)), http.StatusFound) http.Redirect(w, r, path.Join(servePath, searchEncode(q)), http.StatusFound)
return return
} }
render(w, templateSearch, &renderData{ render(w, r, templateSearch, &renderData{
BlogString: blog, BlogString: blog,
Canonical: appConfig.Server.PublicAddress + servePath, Canonical: appConfig.Server.PublicAddress + servePath,
}) })

View File

@ -9,7 +9,7 @@ func serveTaxonomy(blog string, tax *taxonomy) func(w http.ResponseWriter, r *ht
serveError(w, r, err.Error(), http.StatusInternalServerError) serveError(w, r, err.Error(), http.StatusInternalServerError)
return return
} }
render(w, templateTaxonomy, &renderData{ render(w, r, templateTaxonomy, &renderData{
BlogString: blog, BlogString: blog,
Canonical: appConfig.Server.PublicAddress + r.URL.Path, Canonical: appConfig.Server.PublicAddress + r.URL.Path,
Data: map[string]interface{}{ Data: map[string]interface{}{

View File

@ -4,8 +4,16 @@
{{ with .Blog.Description }}<p><i>{{ . }}</i></p>{{ end }} {{ with .Blog.Description }}<p><i>{{ . }}</i></p>{{ end }}
<nav> <nav>
{{ with menu .Blog "main" }} {{ with menu .Blog "main" }}
{{ range $i, $item := .Items }}{{ if ne $i 0 }} &bull; {{ end }}<a href="{{ $item.Link }}">{{ $item.Title }}</a>{{ end }} {{ range $i, $item := .Items }}{{ if ne $i 0 }} &bull; {{ end }}<a href="{{ $item.Link }}">{{ $item.Title }}</a>{{ end }}
{{ end }} {{ end }}
</nav> </nav>
{{ if .LoggedIn }}
<nav>
<a href="{{ blogrelative .Blog "/editor" }}">{{ string .Blog.Lang "editor" }}</a>
&bull; <a href="/notifications">{{ string .Blog.Lang "notifications" }}</a>
&bull; <a href="/webmention">{{ string .Blog.Lang "webmentions" }}</a>
&bull; <a href="{{ blogrelative .Blog "/comment" }}">{{ string .Blog.Lang "comments" }}</a>
</nav>
{{ end }}
</header> </header>
{{ end }} {{ end }}

View File

@ -67,7 +67,7 @@ func webmentionAdmin(w http.ResponseWriter, r *http.Request) {
} }
nextPath = fmt.Sprintf("%s/page/%d", webmentionPath, nextPage) nextPath = fmt.Sprintf("%s/page/%d", webmentionPath, nextPage)
// Render // Render
render(w, "webmentionadmin", &renderData{ render(w, r, templateWebmentionAdmin, &renderData{
Data: map[string]interface{}{ Data: map[string]interface{}{
"Mentions": mentions, "Mentions": mentions,
"HasPrev": hasPrev, "HasPrev": hasPrev,