migrated out more modules and refactored db system

This commit is contained in:
2026-01-01 21:56:21 +11:00
parent 03095448d6
commit 8f6b4b0026
81 changed files with 462 additions and 5016 deletions

View File

@@ -2,6 +2,7 @@ package middleware
import (
"context"
"database/sql"
"net/http"
"sync/atomic"
"time"
@@ -11,25 +12,24 @@ import (
"projectreshoot/pkg/config"
"projectreshoot/pkg/contexts"
"projectreshoot/pkg/cookies"
"projectreshoot/pkg/db"
"projectreshoot/pkg/jwt"
"git.haelnorr.com/h/golib/hlog"
"git.haelnorr.com/h/golib/jwt"
"github.com/pkg/errors"
"github.com/rs/zerolog"
)
// Attempt to use a valid refresh token to generate a new token pair
func refreshAuthTokens(
config *config.Config,
ctx context.Context,
tx *db.SafeWTX,
tokenGen *jwt.TokenGenerator,
tx *sql.Tx,
w http.ResponseWriter,
req *http.Request,
ref *jwt.RefreshToken,
) (*models.User, error) {
user, err := ref.GetUser(ctx, tx)
user, err := models.GetUserFromID(tx, ref.SUB)
if err != nil {
return nil, errors.Wrap(err, "ref.GetUser")
return nil, errors.Wrap(err, "models.GetUser")
}
rememberMe := map[string]bool{
@@ -38,14 +38,14 @@ func refreshAuthTokens(
}[ref.TTL]
// Set fresh to true because new tokens coming from refresh request
err = cookies.SetTokenCookies(w, req, config, user, false, rememberMe)
err = cookies.SetTokenCookies(w, req, config, tokenGen, user, false, rememberMe)
if err != nil {
return nil, errors.Wrap(err, "cookies.SetTokenCookies")
}
// New tokens sent, revoke the used refresh token
err = jwt.RevokeToken(ctx, tx, ref)
err = ref.Revoke(tx)
if err != nil {
return nil, errors.Wrap(err, "jwt.RevokeToken")
return nil, errors.Wrap(err, "ref.Revoke")
}
// Return the authorized user
return user, nil
@@ -54,23 +54,26 @@ func refreshAuthTokens(
// Check the cookies for token strings and attempt to authenticate them
func getAuthenticatedUser(
config *config.Config,
ctx context.Context,
tx *db.SafeWTX,
tokenGen *jwt.TokenGenerator,
tx *sql.Tx,
w http.ResponseWriter,
r *http.Request,
) (*contexts.AuthenticatedUser, error) {
// Get token strings from cookies
atStr, rtStr := cookies.GetTokenStrings(r)
if atStr == "" && rtStr == "" {
return nil, errors.New("No token strings provided")
}
// Attempt to parse the access token
aT, err := jwt.ParseAccessToken(config, ctx, tx, atStr)
aT, err := tokenGen.ValidateAccess(tx, atStr)
if err != nil {
// Access token invalid, attempt to parse refresh token
rT, err := jwt.ParseRefreshToken(config, ctx, tx, rtStr)
rT, err := tokenGen.ValidateRefresh(tx, rtStr)
if err != nil {
return nil, errors.Wrap(err, "jwt.ParseRefreshToken")
return nil, errors.Wrap(err, "tokenGen.ValidateRefresh")
}
// Refresh token valid, attempt to get a new token pair
user, err := refreshAuthTokens(config, ctx, tx, w, r, rT)
user, err := refreshAuthTokens(config, tokenGen, tx, w, r, rT)
if err != nil {
return nil, errors.Wrap(err, "refreshAuthTokens")
}
@@ -82,9 +85,9 @@ func getAuthenticatedUser(
return &authUser, nil
}
// Access token valid
user, err := aT.GetUser(ctx, tx)
user, err := models.GetUserFromID(tx, aT.SUB)
if err != nil {
return nil, errors.Wrap(err, "aT.GetUser")
return nil, errors.Wrap(err, "models.GetUser")
}
authUser := contexts.AuthenticatedUser{
User: user,
@@ -96,9 +99,10 @@ func getAuthenticatedUser(
// Attempt to authenticate the user and add their account details
// to the request context
func Authentication(
logger *zerolog.Logger,
logger *hlog.Logger,
config *config.Config,
conn *db.SafeConn,
conn *sql.DB,
tokenGen *jwt.TokenGenerator,
next http.Handler,
maint *uint32,
) http.Handler {
@@ -115,7 +119,7 @@ func Authentication(
}
// Start the transaction
tx, err := conn.Begin(ctx)
tx, err := conn.BeginTx(ctx, nil)
if err != nil {
// Failed to start transaction, skip auth
logger.Warn().Err(err).
@@ -123,7 +127,7 @@ func Authentication(
handler.ErrorPage(http.StatusServiceUnavailable, w, r)
return
}
user, err := getAuthenticatedUser(config, ctx, tx, w, r)
user, err := getAuthenticatedUser(config, tokenGen, tx, w, r)
if err != nil {
tx.Rollback()
// User auth failed, delete the cookies to avoid repeat requests

View File

@@ -1,148 +0,0 @@
package middleware
import (
"io"
"net/http"
"net/http/httptest"
"strconv"
"sync/atomic"
"testing"
"projectreshoot/pkg/contexts"
"projectreshoot/pkg/db"
"projectreshoot/pkg/tests"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAuthenticationMiddleware(t *testing.T) {
cfg, err := tests.TestConfig()
require.NoError(t, err)
logger := tests.NilLogger()
ver, err := strconv.ParseInt(cfg.DBName, 10, 0)
require.NoError(t, err)
wconn, rconn, err := tests.SetupTestDB(ver)
require.NoError(t, err)
sconn := db.MakeSafe(wconn, rconn, logger)
defer sconn.Close()
// Handler to check outcome of Authentication middleware
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user := contexts.GetUser(r.Context())
if user == nil {
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(strconv.Itoa(0)))
return
} else {
w.WriteHeader(http.StatusOK)
w.Write([]byte(strconv.Itoa(user.ID)))
}
})
var maint uint32
atomic.StoreUint32(&maint, 0)
// Add the middleware and create the server
authHandler := Authentication(logger, cfg, sconn, testHandler, &maint)
require.NoError(t, err)
server := httptest.NewServer(authHandler)
defer server.Close()
tokens := getTokens()
tests := []struct {
name string
id int
accessToken string
refreshToken string
expectedCode int
}{
{
name: "Valid Access Token (Fresh)",
id: 1,
accessToken: tokens["accessFresh"],
refreshToken: "",
expectedCode: http.StatusOK,
},
{
name: "Valid Access Token (Unfresh)",
id: 1,
accessToken: tokens["accessUnfresh"],
refreshToken: tokens["refreshExpired"],
expectedCode: http.StatusOK,
},
{
name: "Valid Refresh Token (Triggers Refresh)",
id: 1,
accessToken: tokens["accessExpired"],
refreshToken: tokens["refreshValid"],
expectedCode: http.StatusOK,
},
{
name: "Both tokens expired",
accessToken: tokens["accessExpired"],
refreshToken: tokens["refreshExpired"],
expectedCode: http.StatusUnauthorized,
},
{
name: "Access token revoked",
accessToken: tokens["accessRevoked"],
refreshToken: "",
expectedCode: http.StatusUnauthorized,
},
{
name: "Refresh token revoked",
accessToken: "",
refreshToken: tokens["refreshRevoked"],
expectedCode: http.StatusUnauthorized,
},
{
name: "Invalid Tokens",
accessToken: tokens["invalid"],
refreshToken: tokens["invalid"],
expectedCode: http.StatusUnauthorized,
},
{
name: "No Tokens",
accessToken: "",
refreshToken: "",
expectedCode: http.StatusUnauthorized,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := &http.Client{}
req, _ := http.NewRequest(http.MethodGet, server.URL, nil)
// Add cookies if provided
if tt.accessToken != "" {
req.AddCookie(&http.Cookie{Name: "access", Value: tt.accessToken})
}
if tt.refreshToken != "" {
req.AddCookie(&http.Cookie{Name: "refresh", Value: tt.refreshToken})
}
resp, err := client.Do(req)
assert.NoError(t, err)
assert.Equal(t, tt.expectedCode, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
assert.Equal(t, strconv.Itoa(tt.id), string(body))
})
}
}
// get the tokens to test with
func getTokens() map[string]string {
tokens := map[string]string{
"accessFresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjQ4OTU2NzIyMTAsImZyZXNoIjo0ODk1NjcyMjEwLCJpYXQiOjE3Mzk2NzIyMTAsImlzcyI6IjEyNy4wLjAuMSIsImp0aSI6ImE4Njk2YWM4LTg3OWMtNDdkNC1iZWM2LTRlY2Y4MTRiZThiZiIsInNjb3BlIjoiYWNjZXNzIiwic3ViIjoxLCJ0dGwiOiJzZXNzaW9uIn0.6nAquDY0JBLPdaJ9q_sMpKj1ISG4Vt2U05J57aoPue8",
"accessUnfresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjMzMjk5Njc1NjcxLCJmcmVzaCI6MTczOTY3NTY3MSwiaWF0IjoxNzM5Njc1NjcxLCJpc3MiOiIxMjcuMC4wLjEiLCJqdGkiOiJjOGNhZmFjNy0yODkzLTQzNzMtOTI4ZS03MGUwODJkYmM2MGIiLCJzY29wZSI6ImFjY2VzcyIsInN1YiI6MSwidHRsIjoic2Vzc2lvbiJ9.plWQVFwHlhXUYI5utS7ny1JfXjJSFrigkq-PnTHD5VY",
"accessExpired": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3Mzk2NzIyNDgsImZyZXNoIjoxNzM5NjcyMjQ4LCJpYXQiOjE3Mzk2NzIyNDgsImlzcyI6IjEyNy4wLjAuMSIsImp0aSI6IjgxYzA1YzBjLTJhOGItNGQ2MC04Yzc4LWY2ZTQxODYxZDFmNCIsInNjb3BlIjoiYWNjZXNzIiwic3ViIjoxLCJ0dGwiOiJzZXNzaW9uIn0.iI1f17kKTuFDEMEYltJRIwRYgYQ-_nF9Wsn0KR6x77Q",
"refreshValid": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjQ4OTU2NzE5MjIsImlhdCI6MTczOTY3MTkyMiwiaXNzIjoiMTI3LjAuMC4xIiwianRpIjoiZTUxMTY3ZWEtNDA3OS00ZTczLTkzZDQtNTgwZDMzODRjZDU4Iiwic2NvcGUiOiJyZWZyZXNoIiwic3ViIjoxLCJ0dGwiOiJzZXNzaW9uIn0.tvtqQ8Z4WrYWHHb0MaEPdsU2FT2KLRE1zHOv3ipoFyc",
"refreshExpired": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3Mzk2NzIyNDgsImlhdCI6MTczOTY3MjI0OCwiaXNzIjoiMTI3LjAuMC4xIiwianRpIjoiZTg5YTc5MTYtZGEzYi00YmJhLWI3ZDMtOWI1N2ViNjRhMmU0Iiwic2NvcGUiOiJyZWZyZXNoIiwic3ViIjoxLCJ0dGwiOiJzZXNzaW9uIn0.rH_fytC7Duxo598xacu820pQKF9ELbG8674h_bK_c4I",
"accessRevoked": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjQ4OTU2NzE5MjIsImZyZXNoIjoxNzM5NjcxOTIyLCJpYXQiOjE3Mzk2NzE5MjIsImlzcyI6IjEyNy4wLjAuMSIsImp0aSI6IjBhNmIzMzhlLTkzMGEtNDNmZS04ZjcwLTFhNmRhZWQyNTZmYSIsInNjb3BlIjoiYWNjZXNzIiwic3ViIjoxLCJ0dGwiOiJzZXNzaW9uIn0.mZLuCp9amcm2_CqYvbHPlk86nfiuy_Or8TlntUCw4Qs",
"refreshRevoked": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjMzMjk5Njc1NjcxLCJpYXQiOjE3Mzk2NzU2NzEsImlzcyI6IjEyNy4wLjAuMSIsImp0aSI6ImI3ZmE1MWRjLTg1MzItNDJlMS04NzU2LTVkMjViZmIyMDAzYSIsInNjb3BlIjoicmVmcmVzaCIsInN1YiI6MSwidHRsIjoic2Vzc2lvbiJ9.5Q9yDZN5FubfCWHclUUZEkJPOUHcOEpVpgcUK-ameHo",
"invalid": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0ODUxNDA5ODQsImlhdCI6MTQ4NTEzNzM4NCwiaXNzIjoiYWNtZS5jb20iLCJzdWIiOiIyOWFjMGMxOC0wYjRhLTQyY2YtODJmYy0wM2Q1NzAzMThhMWQiLCJhcHBsaWNhdGlvbklkIjoiNzkxMDM3MzQtOTdhYi00ZDFhLWFmMzctZTAwNmQwNWQyOTUyIiwicm9sZXMiOltdfQ.Mp0Pcwsz5VECK11Kf2ZZNF_SMKu5CgBeLN9ZOP04kZo",
}
return tokens
}

View File

@@ -6,7 +6,7 @@ import (
"projectreshoot/pkg/contexts"
"time"
"github.com/rs/zerolog"
"git.haelnorr.com/h/golib/hlog"
)
// Wraps the http.ResponseWriter, adding a statusCode field
@@ -22,7 +22,7 @@ func (w *wrappedWriter) WriteHeader(statusCode int) {
}
// Middleware to add logs to console with details of the request
func Logging(logger *zerolog.Logger, next http.Handler) http.Handler {
func Logging(logger *hlog.Logger, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/static/css/output.css" ||
r.URL.Path == "/static/favicon.ico" {

View File

@@ -1,87 +0,0 @@
package middleware
import (
"net/http"
"net/http/httptest"
"strconv"
"sync/atomic"
"testing"
"projectreshoot/pkg/db"
"projectreshoot/pkg/tests"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPageLoginRequired(t *testing.T) {
cfg, err := tests.TestConfig()
require.NoError(t, err)
logger := tests.NilLogger()
ver, err := strconv.ParseInt(cfg.DBName, 10, 0)
require.NoError(t, err)
wconn, rconn, err := tests.SetupTestDB(ver)
require.NoError(t, err)
sconn := db.MakeSafe(wconn, rconn, logger)
defer sconn.Close()
// Handler to check outcome of Authentication middleware
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
var maint uint32
atomic.StoreUint32(&maint, 0)
// Add the middleware and create the server
loginRequiredHandler := LoginReq(testHandler)
authHandler := Authentication(logger, cfg, sconn, loginRequiredHandler, &maint)
server := httptest.NewServer(authHandler)
defer server.Close()
tokens := getTokens()
tests := []struct {
name string
accessToken string
refreshToken string
expectedCode int
}{
{
name: "Valid Login",
accessToken: tokens["accessFresh"],
refreshToken: "",
expectedCode: http.StatusOK,
},
{
name: "Expired login",
accessToken: tokens["accessExpired"],
refreshToken: tokens["refreshExpired"],
expectedCode: http.StatusUnauthorized,
},
{
name: "No login",
accessToken: "",
refreshToken: "",
expectedCode: http.StatusUnauthorized,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := &http.Client{}
req, _ := http.NewRequest(http.MethodGet, server.URL, nil)
// Add cookies if provided
if tt.accessToken != "" {
req.AddCookie(&http.Cookie{Name: "access", Value: tt.accessToken})
}
if tt.refreshToken != "" {
req.AddCookie(&http.Cookie{Name: "refresh", Value: tt.refreshToken})
}
resp, err := client.Do(req)
assert.NoError(t, err)
assert.Equal(t, tt.expectedCode, resp.StatusCode)
})
}
}

View File

@@ -1,94 +0,0 @@
package middleware
import (
"net/http"
"net/http/httptest"
"strconv"
"sync/atomic"
"testing"
"projectreshoot/pkg/db"
"projectreshoot/pkg/tests"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestReauthRequired(t *testing.T) {
cfg, err := tests.TestConfig()
require.NoError(t, err)
logger := tests.NilLogger()
ver, err := strconv.ParseInt(cfg.DBName, 10, 0)
require.NoError(t, err)
wconn, rconn, err := tests.SetupTestDB(ver)
require.NoError(t, err)
sconn := db.MakeSafe(wconn, rconn, logger)
defer sconn.Close()
// Handler to check outcome of Authentication middleware
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
var maint uint32
atomic.StoreUint32(&maint, 0)
// Add the middleware and create the server
reauthRequiredHandler := FreshReq(testHandler)
loginRequiredHandler := LoginReq(reauthRequiredHandler)
authHandler := Authentication(logger, cfg, sconn, loginRequiredHandler, &maint)
server := httptest.NewServer(authHandler)
defer server.Close()
tokens := getTokens()
tests := []struct {
name string
accessToken string
refreshToken string
expectedCode int
}{
{
name: "Fresh Login",
accessToken: tokens["accessFresh"],
refreshToken: "",
expectedCode: http.StatusOK,
},
{
name: "Unfresh Login",
accessToken: tokens["accessUnfresh"],
refreshToken: "",
expectedCode: 444,
},
{
name: "Expired login",
accessToken: tokens["accessExpired"],
refreshToken: tokens["refreshExpired"],
expectedCode: http.StatusUnauthorized,
},
{
name: "No login",
accessToken: "",
refreshToken: "",
expectedCode: http.StatusUnauthorized,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := &http.Client{}
req, _ := http.NewRequest(http.MethodGet, server.URL, nil)
// Add cookies if provided
if tt.accessToken != "" {
req.AddCookie(&http.Cookie{Name: "access", Value: tt.accessToken})
}
if tt.refreshToken != "" {
req.AddCookie(&http.Cookie{Name: "refresh", Value: tt.refreshToken})
}
resp, err := client.Do(req)
assert.NoError(t, err)
assert.Equal(t, tt.expectedCode, resp.StatusCode)
})
}
}