feat(auth): replace hand-rolled auth with Ory Kratos — phase 2 backend
Delegates all credential management (login, register, password reset, email verification, social sign-in) to Ory Kratos. The Go API now acts as a resource server: the new KratosAuth middleware validates sessions against the Kratos whoami endpoint, writes the local User mirror into Echo context, and all existing domain handlers continue working unchanged. Hand-rolled token auth, AuthToken model, apple_auth/ google_auth services, and the auth refresh flow are removed. Tests are updated to use the fake-token middleware pattern so existing integration assertions require no rewrite. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,12 +6,15 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/treytartt/honeydue-api/internal/apperrors"
|
||||
"github.com/treytartt/honeydue-api/internal/config"
|
||||
@@ -22,7 +25,6 @@ import (
|
||||
"github.com/treytartt/honeydue-api/internal/services"
|
||||
"github.com/treytartt/honeydue-api/internal/testutil"
|
||||
"github.com/treytartt/honeydue-api/internal/validator"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// SubscriptionTestApp holds components for subscription integration testing
|
||||
@@ -31,11 +33,51 @@ type SubscriptionTestApp struct {
|
||||
Router *echo.Echo
|
||||
SubscriptionService *services.SubscriptionService
|
||||
SubscriptionRepo *repositories.SubscriptionRepository
|
||||
tokenStore map[string]*models.User
|
||||
tokenStoreMu sync.RWMutex
|
||||
}
|
||||
|
||||
// fakeAuthMiddleware returns an Echo middleware that authenticates requests using
|
||||
// the in-process tokenStore instead of calling the real Kratos session endpoint.
|
||||
func (app *SubscriptionTestApp) fakeAuthMiddleware() echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
ah := c.Request().Header.Get("Authorization")
|
||||
if ah == "" {
|
||||
return apperrors.Unauthorized("error.not_authenticated")
|
||||
}
|
||||
tok := ah
|
||||
if len(ah) > 6 && ah[:6] == "Token " {
|
||||
tok = ah[6:]
|
||||
} else if len(ah) > 7 && ah[:7] == "Bearer " {
|
||||
tok = ah[7:]
|
||||
}
|
||||
app.tokenStoreMu.RLock()
|
||||
user, ok := app.tokenStore[tok]
|
||||
app.tokenStoreMu.RUnlock()
|
||||
if !ok {
|
||||
return apperrors.Unauthorized("error.not_authenticated")
|
||||
}
|
||||
c.Set("auth_user", user)
|
||||
c.Set("auth_token", tok)
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// registerAndLogin creates a user directly in the DB and returns a fake token
|
||||
// and user ID. No HTTP register/login calls are made.
|
||||
func (app *SubscriptionTestApp) registerAndLogin(t *testing.T, username, email, _ string) (string, uint) {
|
||||
t.Helper()
|
||||
user := testutil.CreateTestUser(t, app.DB, username, email, "")
|
||||
tok := uuid.NewString()
|
||||
app.tokenStoreMu.Lock()
|
||||
app.tokenStore[tok] = user
|
||||
app.tokenStoreMu.Unlock()
|
||||
return tok, user.ID
|
||||
}
|
||||
|
||||
func setupSubscriptionTest(t *testing.T) *SubscriptionTestApp {
|
||||
// Echo does not need test mode
|
||||
|
||||
db := testutil.SetupTestDB(t)
|
||||
testutil.SeedLookupData(t, db)
|
||||
|
||||
@@ -67,22 +109,23 @@ func setupSubscriptionTest(t *testing.T) *SubscriptionTestApp {
|
||||
residenceHandler := handlers.NewResidenceHandler(residenceService, nil, nil, true)
|
||||
subscriptionHandler := handlers.NewSubscriptionHandler(subscriptionService, nil)
|
||||
|
||||
// Create router
|
||||
app := &SubscriptionTestApp{
|
||||
DB: db,
|
||||
SubscriptionService: subscriptionService,
|
||||
SubscriptionRepo: subscriptionRepo,
|
||||
tokenStore: make(map[string]*models.User),
|
||||
}
|
||||
|
||||
// Create router with fake auth middleware
|
||||
e := echo.New()
|
||||
e.Validator = validator.NewCustomValidator()
|
||||
e.HTTPErrorHandler = apperrors.HTTPErrorHandler
|
||||
|
||||
// Public routes
|
||||
auth := e.Group("/api/auth")
|
||||
{
|
||||
auth.POST("/register", authHandler.Register)
|
||||
auth.POST("/login", authHandler.Login)
|
||||
}
|
||||
e.Use(middleware.TimezoneMiddleware())
|
||||
|
||||
// Protected routes
|
||||
authMiddleware := middleware.NewAuthMiddleware(db, nil)
|
||||
api := e.Group("/api")
|
||||
api.Use(authMiddleware.TokenAuth())
|
||||
api.Use(app.fakeAuthMiddleware())
|
||||
{
|
||||
api.GET("/auth/me", authHandler.CurrentUser)
|
||||
|
||||
@@ -98,12 +141,8 @@ func setupSubscriptionTest(t *testing.T) *SubscriptionTestApp {
|
||||
}
|
||||
}
|
||||
|
||||
return &SubscriptionTestApp{
|
||||
DB: db,
|
||||
Router: e,
|
||||
SubscriptionService: subscriptionService,
|
||||
SubscriptionRepo: subscriptionRepo,
|
||||
}
|
||||
app.Router = e
|
||||
return app
|
||||
}
|
||||
|
||||
// Helper to make authenticated requests
|
||||
@@ -129,36 +168,6 @@ func (app *SubscriptionTestApp) makeAuthenticatedRequest(t *testing.T, method, p
|
||||
return w
|
||||
}
|
||||
|
||||
// Helper to register and login a user, returns token and user ID
|
||||
func (app *SubscriptionTestApp) registerAndLogin(t *testing.T, username, email, password string) (string, uint) {
|
||||
// Register
|
||||
registerBody := map[string]string{
|
||||
"username": username,
|
||||
"email": email,
|
||||
"password": password,
|
||||
}
|
||||
w := app.makeAuthenticatedRequest(t, "POST", "/api/auth/register", registerBody, "")
|
||||
require.Equal(t, http.StatusCreated, w.Code)
|
||||
|
||||
// Login
|
||||
loginBody := map[string]string{
|
||||
"username": username,
|
||||
"password": password,
|
||||
}
|
||||
w = app.makeAuthenticatedRequest(t, "POST", "/api/auth/login", loginBody, "")
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var loginResp map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &loginResp)
|
||||
require.NoError(t, err)
|
||||
|
||||
token := loginResp["token"].(string)
|
||||
userMap := loginResp["user"].(map[string]interface{})
|
||||
userID := uint(userMap["id"].(float64))
|
||||
|
||||
return token, userID
|
||||
}
|
||||
|
||||
// TestIntegration_IsFreeBypassesLimitations tests that users with IsFree=true
|
||||
// see limitations_enabled=false regardless of global settings
|
||||
func TestIntegration_IsFreeBypassesLimitations(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user