fix: move implementation to "internal" package
Some checks failed
Build Docker Image / Build-Docker-Image (push) Has been cancelled

This commit is contained in:
2025-05-29 13:23:13 +02:00
parent 9bb0cc475d
commit 0c40680d76
72 changed files with 245 additions and 230 deletions

View 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
}

View 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)
})
}

View 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)
})
}
}

View 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: "/",
}
}

View File

@@ -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)
})
}
}

View 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)
}
})
}

View 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()
})
}

View 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)
})
}
}

View 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)
})
}