fix: move implementation to "internal" package
Some checks failed
Build Docker Image / Build-Docker-Image (push) Has been cancelled
Some checks failed
Build Docker Image / Build-Docker-Image (push) Has been cancelled
This commit is contained in:
80
internal/handler/middleware/authenticate.go
Normal file
80
internal/handler/middleware/authenticate.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"spend-sparrow/internal/service"
|
||||
"spend-sparrow/internal/types"
|
||||
)
|
||||
|
||||
type ContextKey string
|
||||
|
||||
var SessionKey ContextKey = "session"
|
||||
var UserKey ContextKey = "user"
|
||||
|
||||
func Authenticate(service service.Auth) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
sessionId := getSessionID(r)
|
||||
session, user, _ := service.SignInSession(sessionId)
|
||||
|
||||
var err error
|
||||
// Always sign in anonymous
|
||||
// This way, we can always generate csrf tokens
|
||||
if session == nil {
|
||||
session, err = service.SignInAnonymous()
|
||||
if err != nil {
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
cookie := CreateSessionCookie(session.Id)
|
||||
http.SetCookie(w, &cookie)
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
ctx = context.WithValue(ctx, UserKey, user)
|
||||
ctx = context.WithValue(ctx, SessionKey, session)
|
||||
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func GetUser(r *http.Request) *types.User {
|
||||
obj := r.Context().Value(UserKey)
|
||||
if obj == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
user, ok := obj.(*types.User)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
func GetSession(r *http.Request) *types.Session {
|
||||
obj := r.Context().Value(SessionKey)
|
||||
if obj == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
session, ok := obj.(*types.Session)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
func getSessionID(r *http.Request) string {
|
||||
cookie, err := r.Cookie("id")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return cookie.Value
|
||||
}
|
||||
18
internal/handler/middleware/cache_control.go
Normal file
18
internal/handler/middleware/cache_control.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func CacheControl(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
shouldCache := strings.HasPrefix(r.URL.Path, "/static")
|
||||
|
||||
if !shouldCache {
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
63
internal/handler/middleware/cross_site_request_forgery.go
Normal file
63
internal/handler/middleware/cross_site_request_forgery.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"spend-sparrow/internal/log"
|
||||
"spend-sparrow/internal/service"
|
||||
"spend-sparrow/internal/types"
|
||||
"spend-sparrow/internal/utils"
|
||||
)
|
||||
|
||||
type csrfResponseWriter struct {
|
||||
http.ResponseWriter
|
||||
auth service.Auth
|
||||
session *types.Session
|
||||
}
|
||||
|
||||
func newCsrfResponseWriter(w http.ResponseWriter, auth service.Auth, session *types.Session) *csrfResponseWriter {
|
||||
return &csrfResponseWriter{
|
||||
ResponseWriter: w,
|
||||
auth: auth,
|
||||
session: session,
|
||||
}
|
||||
}
|
||||
|
||||
func (rr *csrfResponseWriter) Write(data []byte) (int, error) {
|
||||
dataStr := string(data)
|
||||
csrfToken, err := rr.auth.GetCsrfToken(rr.session)
|
||||
if err == nil {
|
||||
dataStr = strings.ReplaceAll(dataStr, "CSRF_TOKEN", csrfToken)
|
||||
}
|
||||
|
||||
return rr.ResponseWriter.Write([]byte(dataStr))
|
||||
}
|
||||
|
||||
func CrossSiteRequestForgery(auth service.Auth) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
session := GetSession(r)
|
||||
|
||||
if r.Method == http.MethodPost ||
|
||||
r.Method == http.MethodPut ||
|
||||
r.Method == http.MethodDelete ||
|
||||
r.Method == http.MethodPatch {
|
||||
csrfToken := r.Header.Get("Csrf-Token")
|
||||
|
||||
if session == nil || csrfToken == "" || !auth.IsCsrfTokenValid(csrfToken, session.Id) {
|
||||
log.Info("CSRF-Token \"%s\" not correct", csrfToken)
|
||||
if r.Header.Get("Hx-Request") == "true" {
|
||||
utils.TriggerToastWithStatus(w, r, "error", "CSRF-Token not correct", http.StatusBadRequest)
|
||||
} else {
|
||||
http.Error(w, "CSRF-Token not correct", http.StatusBadRequest)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
responseWriter := newCsrfResponseWriter(w, auth, session)
|
||||
next.ServeHTTP(responseWriter, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
15
internal/handler/middleware/default.go
Normal file
15
internal/handler/middleware/default.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package middleware
|
||||
|
||||
import "net/http"
|
||||
|
||||
func CreateSessionCookie(sessionId string) http.Cookie {
|
||||
return http.Cookie{
|
||||
Name: "id",
|
||||
Value: sessionId,
|
||||
MaxAge: 60 * 60 * 8, // 8 hours
|
||||
Secure: true,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
Path: "/",
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"spend-sparrow/internal/service"
|
||||
)
|
||||
|
||||
func GenerateRecurringTransactions(transactionRecurring service.TransactionRecurring) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
user := GetUser(r)
|
||||
if user == nil || r.Method != http.MethodGet {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
_ = transactionRecurring.GenerateTransactions(user)
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
40
internal/handler/middleware/gzip.go
Normal file
40
internal/handler/middleware/gzip.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"spend-sparrow/internal/log"
|
||||
)
|
||||
|
||||
type gzipResponseWriter struct {
|
||||
io.Writer
|
||||
http.ResponseWriter
|
||||
}
|
||||
|
||||
func (w gzipResponseWriter) Write(b []byte) (int, error) {
|
||||
return w.Writer.Write(b)
|
||||
}
|
||||
|
||||
func Gzip(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Encoding", "gzip")
|
||||
gz := gzip.NewWriter(w)
|
||||
wrapper := gzipResponseWriter{Writer: gz, ResponseWriter: w}
|
||||
|
||||
next.ServeHTTP(wrapper, r)
|
||||
|
||||
err := gz.Close()
|
||||
if err != nil && !errors.Is(err, http.ErrBodyNotAllowed) {
|
||||
log.Error("Gzip: could not close Writer: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
47
internal/handler/middleware/logger.go
Normal file
47
internal/handler/middleware/logger.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"spend-sparrow/internal/log"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
|
||||
var (
|
||||
metrics = promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "mefit_request_total",
|
||||
Help: "The total number of requests processed",
|
||||
},
|
||||
[]string{"path", "method", "status"},
|
||||
)
|
||||
)
|
||||
|
||||
type WrappedWriter struct {
|
||||
http.ResponseWriter
|
||||
StatusCode int
|
||||
}
|
||||
|
||||
func (w *WrappedWriter) WriteHeader(code int) {
|
||||
w.StatusCode = code
|
||||
w.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
|
||||
func Log(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
|
||||
wrapped := &WrappedWriter{
|
||||
ResponseWriter: w,
|
||||
StatusCode: http.StatusOK,
|
||||
}
|
||||
next.ServeHTTP(wrapped, r)
|
||||
|
||||
log.Info(r.RemoteAddr + " " + strconv.Itoa(wrapped.StatusCode) + " " + r.Method + " " + r.URL.Path + " " + time.Since(start).String())
|
||||
metrics.WithLabelValues(r.URL.Path, r.Method, http.StatusText(wrapped.StatusCode)).Inc()
|
||||
})
|
||||
}
|
||||
40
internal/handler/middleware/security_headers.go
Normal file
40
internal/handler/middleware/security_headers.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"spend-sparrow/internal/types"
|
||||
)
|
||||
|
||||
func SecurityHeaders(serverSettings *types.Settings) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.Header().Set("Access-Control-Allow-Origin", serverSettings.BaseUrl)
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, DELETE")
|
||||
w.Header().Set("Content-Security-Policy",
|
||||
"default-src 'none'; "+
|
||||
"script-src 'self'; "+
|
||||
"font-src 'self'; "+
|
||||
"connect-src 'self'; "+
|
||||
"img-src 'self'; "+
|
||||
"style-src 'self'; "+
|
||||
"form-action 'self'; "+
|
||||
"frame-ancestors 'none'; ",
|
||||
)
|
||||
w.Header().Set("Cross-Origin-Resource-Policy", "same-origin")
|
||||
w.Header().Set("Cross-Origin-Opener-Policy", "same-origin")
|
||||
w.Header().Set("Cross-Origin-Embedder-Policy", "require-corp")
|
||||
w.Header().Set("Permissions-Policy", "geolocation=(), camera=(), microphone=(), interest-cohort=()")
|
||||
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
|
||||
w.Header().Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains; preload")
|
||||
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
14
internal/handler/middleware/wrapper.go
Normal file
14
internal/handler/middleware/wrapper.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package middleware
|
||||
|
||||
import "net/http"
|
||||
|
||||
// Chain list of handlers together.
|
||||
func Wrapper(next http.Handler, handlers ...func(http.Handler) http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
lastHandler := next
|
||||
for _, handler := range handlers {
|
||||
lastHandler = handler(lastHandler)
|
||||
}
|
||||
lastHandler.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user