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 (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"io"
@ -25,20 +26,20 @@ func jwtKey() []byte {
func authMiddleware(next http.Handler) http.Handler {
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
if username, password, ok := r.BasicAuth(); ok && checkCredentials(username, password) {
next.ServeHTTP(w, r)
return
}
// 2. Check JWT
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) {
next.ServeHTTP(w, r)
return
}
if checkAuthToken(r) {
next.ServeHTTP(w, r)
return
}
// 3. Show login form
w.WriteHeader(http.StatusUnauthorized)
@ -50,7 +51,7 @@ func authMiddleware(next http.Handler) http.Handler {
_ = r.ParseForm()
b = []byte(r.PostForm.Encode())
}
render(w, templateLogin, &renderData{
render(w, r, templateLogin, &renderData{
Data: map[string]string{
"loginmethod": r.Method,
"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 {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if !checkLogin(rw, r) {

View File

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

View File

@ -31,43 +31,52 @@ func initCache() (err error) {
func cacheMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if appConfig.Cache.Enable &&
// check method
(r.Method == http.MethodGet || r.Method == http.MethodHead) &&
// check bypass query
!(r.URL.Query().Get("cache") == "0" || r.URL.Query().Get("cache") == "false") {
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 {
// Do checks
if !appConfig.Cache.Enable {
next.ServeHTTP(w, r)
return
}
if !(r.Method == http.MethodGet || r.Method == http.MethodHead) {
next.ServeHTTP(w, r)
return
}
if r.URL.Query().Get("cache") == "0" || r.URL.Query().Get("cache") == "false" {
next.ServeHTTP(w, r)
return
}
if loggedIn, ok := r.Context().Value(loggedInKey).(bool); ok && loggedIn {
next.ServeHTTP(w, r)
return
}
// 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
w.WriteHeader(http.StatusNotModified)
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()
b = []byte(r.PostForm.Encode())
}
render(w, templateCaptcha, &renderData{
render(w, r, templateCaptcha, &renderData{
Data: map[string]string{
"captchamethod": r.Method,
"captchaheaders": base64.StdEncoding.EncodeToString(h),

View File

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

View File

@ -33,7 +33,7 @@ func (p *commentsPaginationAdapter) Slice(offset, length int, data interface{})
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) {
// Adapter
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)
// Render
render(w, templateCommentsAdmin, &renderData{
render(w, r, templateCommentsAdmin, &renderData{
BlogString: blog,
Data: map[string]interface{}{
"Comments": comments,

View File

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

View File

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

View File

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

View File

@ -98,6 +98,7 @@ func buildHandler() (http.Handler, error) {
}
r.Use(checkIsLogin)
r.Use(checkIsCaptcha)
r.Use(checkLoggedIn)
// Profiler
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)
return
}
render(w, "indieauth", &renderData{
render(w, r, "indieauth", &renderData{
Data: data,
})
}

View File

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

View File

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

View File

@ -39,6 +39,7 @@ const (
templateCaptcha = "captcha"
templateCommentsAdmin = "commentsadmin"
templateNotificationsAdmin = "notificationsadmin"
templateWebmentionAdmin = "webmentionadmin"
)
var templates map[string]*template.Template
@ -238,9 +239,10 @@ type renderData struct {
Canonical string
Blog *configBlog
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
if data.Blog == nil {
if len(data.BlogString) == 0 {
@ -259,15 +261,20 @@ func render(w http.ResponseWriter, template string, data *renderData) {
if data.Data == nil {
data.Data = map[string]interface{}{}
}
// We need to use a buffer here to enable minification
var buffer bytes.Buffer
err := templates[template].ExecuteTemplate(&buffer, template, data)
// Check login
if loggedIn, ok := r.Context().Value(loggedInKey).(bool); ok && loggedIn {
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 {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Set content type
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)
return
}
render(w, templateSearch, &renderData{
render(w, r, templateSearch, &renderData{
BlogString: blog,
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)
return
}
render(w, templateTaxonomy, &renderData{
render(w, r, templateTaxonomy, &renderData{
BlogString: blog,
Canonical: appConfig.Server.PublicAddress + r.URL.Path,
Data: map[string]interface{}{

View File

@ -4,8 +4,16 @@
{{ with .Blog.Description }}<p><i>{{ . }}</i></p>{{ end }}
<nav>
{{ 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 }}
</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>
{{ end }}

View File

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