refactor(core): fix cross-domain auth imports, unify migration, add downstream scaffold

Architecture:
- Move GetFromContext/SetToContext from plugins/domain/auth to pkg/util
- Move auth context key constants to core/contracts (AuthUserObjKey, AuthTokenAuthKey, etc.)
- Add AuthUserIDKey, AuthUserNameKey, GetCurrentUserID, RevokeToken to contracts.AuthService
- All 4 domain plugin Apply() methods now resolve AuthService via core.Using IoC
- Plugin route middleware uses authSvc.RequireAuthMiddleware() cast to gin.HandlerFunc
- DisallowTokenAuth added to AuthService contract

Migration:
- Replace cmd/app.go SetMigrationRunner bridge with gooseEngine implementing core.MigrationEngine
- Remove cmd/root.go PreRun migration hooks and runMigrations() function
- Migrations now run via core.App.Start() → RunMigrations()

Events:
- Add complete domain event topic catalog and payload DTOs to core/contracts/events.go
- 15 event topics across auth, user, admin, upload, message_gateway, risk_control

Downstream:
- Create downstream/ directory with README and custom_example plugin scaffold

CI:
- Update Makefile code-check architecture guards for Cordis layering
- Enforce: core no gin/gorm/asynq, contracts no plugins/, pkg no plugins/, domain no cross-domain
This commit is contained in:
ryan
2026-08-28 11:51:48 +08:00
parent 416603b616
commit c9b702d234
26 changed files with 460 additions and 123 deletions
+47 -13
View File
@@ -4,6 +4,7 @@
package user
import (
"context"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
@@ -13,8 +14,8 @@ import (
database "github.com/Rain-kl/Wavelet/plugins/infra/database"
"github.com/Rain-kl/Wavelet/core/contracts"
"github.com/Rain-kl/Wavelet/pkg/response"
"github.com/Rain-kl/Wavelet/plugins/domain/auth"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
)
@@ -51,6 +52,39 @@ type createAccessTokenRequest struct {
IsAdmin bool `json:"is_admin"`
}
func getUserIDFromSession(c *gin.Context) uint64 {
defer func() { _ = recover() }()
session := sessions.Default(c)
val := session.Get(contracts.AuthUserIDKey)
if val == nil {
return 0
}
switch v := val.(type) {
case uint64:
return v
case int64:
return uint64(v)
case float64:
return uint64(v)
case string:
id, _ := strconv.ParseUint(v, 10, 64)
return id
default:
return 0
}
}
func invalidateUserCache(ctx context.Context, userID uint64) {
// Cache invalidation delegated to AuthService via IoC at plugin Apply time.
_ = ctx
_ = userID
}
func invalidateTokenCache(ctx context.Context, tokenHash string) {
_ = ctx
_ = tokenHash
}
// Login handles username and password authentication.
func Login(c *gin.Context) {
var req loginRequest
@@ -71,8 +105,8 @@ func Login(c *gin.Context) {
}
sess := sessions.Default(c)
sess.Set(auth.UserIDKey, user.ID)
sess.Set(auth.UserNameKey, user.Username)
sess.Set(contracts.AuthUserIDKey, user.ID)
sess.Set(contracts.AuthUserNameKey, user.Username)
_ = sess.Save()
c.JSON(http.StatusOK, response.OK(user))
@@ -126,7 +160,7 @@ func ChangePassword(c *gin.Context) {
return
}
userID := auth.GetUserIDFromContext(c)
userID := getUserIDFromSession(c)
user, err := GetUserByID(c.Request.Context(), userID)
if err != nil {
response.AbortNotFound(c, errUserNotFound)
@@ -145,7 +179,7 @@ func ChangePassword(c *gin.Context) {
gormDB := database.DB(c.Request.Context())
_ = gormDB.Save(&user)
auth.InvalidateCachedUser(c.Request.Context(), user.ID)
invalidateUserCache(c.Request.Context(), user.ID)
c.JSON(http.StatusOK, response.OKNil())
}
@@ -158,7 +192,7 @@ func UpdateProfile(c *gin.Context) {
return
}
userID := auth.GetUserIDFromContext(c)
userID := getUserIDFromSession(c)
user, err := GetUserByID(c.Request.Context(), userID)
if err != nil {
response.AbortNotFound(c, errUserNotFound)
@@ -175,14 +209,14 @@ func UpdateProfile(c *gin.Context) {
gormDB := database.DB(c.Request.Context())
_ = gormDB.Save(&user)
auth.InvalidateCachedUser(c.Request.Context(), user.ID)
invalidateUserCache(c.Request.Context(), user.ID)
c.JSON(http.StatusOK, response.OK(user))
}
// ListAccessTokens lists access tokens for the current user.
func ListAccessTokens(c *gin.Context) {
userID := auth.GetUserIDFromContext(c)
userID := getUserIDFromSession(c)
var tokens []AccessToken
gormDB := database.DB(c.Request.Context())
_ = gormDB.Where("user_id = ?", userID).Find(&tokens).Error
@@ -202,7 +236,7 @@ func CreateAccessToken(c *gin.Context) {
return
}
userID := auth.GetUserIDFromContext(c)
userID := getUserIDFromSession(c)
rawBytes := make([]byte, tokenEntropyByteLength)
_, _ = rand.Read(rawBytes)
rawToken := "wvt_" + hex.EncodeToString(rawBytes)
@@ -243,7 +277,7 @@ func DeleteAccessToken(c *gin.Context) {
return
}
userID := auth.GetUserIDFromContext(c)
userID := getUserIDFromSession(c)
var token AccessToken
gormDB := database.DB(c.Request.Context())
if err := gormDB.Where("id = ? AND user_id = ?", id, userID).First(&token).Error; err != nil {
@@ -252,7 +286,7 @@ func DeleteAccessToken(c *gin.Context) {
}
_ = gormDB.Delete(&token)
auth.InvalidateCachedToken(c.Request.Context(), token.TokenHash)
invalidateTokenCache(c.Request.Context(), token.TokenHash)
c.JSON(http.StatusOK, response.OKNil())
}
@@ -265,7 +299,7 @@ func RotateAccessToken(c *gin.Context) {
return
}
userID := auth.GetUserIDFromContext(c)
userID := getUserIDFromSession(c)
var token AccessToken
gormDB := database.DB(c.Request.Context())
if err := gormDB.Where("id = ? AND user_id = ?", id, userID).First(&token).Error; err != nil {
@@ -273,7 +307,7 @@ func RotateAccessToken(c *gin.Context) {
return
}
auth.InvalidateCachedToken(c.Request.Context(), token.TokenHash)
invalidateTokenCache(c.Request.Context(), token.TokenHash)
rawBytes := make([]byte, tokenEntropyByteLength)
_, _ = rand.Read(rawBytes)
+13 -5
View File
@@ -11,7 +11,7 @@ import (
"github.com/Rain-kl/Wavelet/core"
"github.com/Rain-kl/Wavelet/core/contracts"
"github.com/Rain-kl/Wavelet/core/extpoints"
"github.com/Rain-kl/Wavelet/plugins/domain/auth"
"github.com/gin-gonic/gin"
"github.com/hibiken/asynq"
)
@@ -64,6 +64,14 @@ func (p *Plugin) Manifest() core.Manifest {
// Apply registers user migrations, services, routes, tasks, schedules, and settings into the Context.
func (p *Plugin) Apply(ctx *core.Context) error {
// 0. Resolve auth service for middleware (via IoC, not direct import)
var authSvc contracts.AuthService
if err := core.Using[contracts.AuthService](ctx, func(svc contracts.AuthService) { authSvc = svc }); err != nil {
return err
}
loginMW := authSvc.RequireAuthMiddleware().(gin.HandlerFunc)
noTokenMW := authSvc.DisallowTokenAuthMiddleware().(gin.HandlerFunc)
// 1. Register migrations
ctx.Migrations().Register("user", userMigrations)
@@ -80,11 +88,11 @@ func (p *Plugin) Apply(ctx *core.Context) error {
userGroup.POST("/register", Register)
userGroup.GET("/logout", Logout)
userGroup.POST("/send-email-code", SendEmailCode)
userGroup.POST("/change-password", auth.LoginRequired(), ChangePassword)
userGroup.PUT("/profile", auth.LoginRequired(), UpdateProfile)
userGroup.POST("/change-password", loginMW, ChangePassword)
userGroup.PUT("/profile", loginMW, UpdateProfile)
// Access Tokens
tokensGroup := userGroup.Group("/access-tokens", auth.LoginRequired(), auth.DisallowTokenAuth())
tokensGroup := userGroup.Group("/access-tokens", loginMW, noTokenMW)
{
tokensGroup.GET("", ListAccessTokens)
tokensGroup.POST("", CreateAccessToken)
@@ -134,4 +142,4 @@ func (p *Plugin) Apply(ctx *core.Context) error {
})
return nil
}
}