refactor(core): completely decommission legacy internal/apps, internal/platform/bootstrap, and internal/router/v1

This commit is contained in:
ryan
2026-08-28 07:28:45 +08:00
parent df3c5ae756
commit dc49b72c29
182 changed files with 946 additions and 19604 deletions
+15
View File
@@ -0,0 +1,15 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package user
const (
errInvalidParams = "无效的请求参数"
errUserNotFound = "用户不存在"
//nolint:gosec // error message, not hardcoded credentials
errPasswordMismatch = "用户名或密码错误"
//nolint:gosec // error message, not hardcoded credentials
errOldPasswordIncorrect = "原密码不正确"
//nolint:gosec // error message, not hardcoded credentials
errTokenNotFound = "访问令牌不存在"
)
+297
View File
@@ -0,0 +1,297 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package user
import (
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"net/http"
"strconv"
"time"
persistence "github.com/Rain-kl/Wavelet/internal/infra/persistence"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/internal/repository"
"github.com/Rain-kl/Wavelet/internal/shared/response"
"github.com/Rain-kl/Wavelet/plugins/domain/auth"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
)
type loginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
type registerRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
Email string `json:"email"`
}
type changePasswordRequest struct {
OldPassword string `json:"old_password" binding:"required"`
NewPassword string `json:"new_password" binding:"required"`
}
type updateProfileRequest struct {
Nickname string `json:"nickname"`
AvatarURL string `json:"avatar_url"`
Bio string `json:"bio"`
Phone string `json:"phone"`
Gender string `json:"gender"`
Website string `json:"website"`
Location string `json:"location"`
}
type createAccessTokenRequest struct {
Name string `json:"name" binding:"required"`
ExpiresAt *time.Time `json:"expires_at"`
IsAdmin bool `json:"is_admin"`
}
// Login handles username and password authentication.
func Login(c *gin.Context) {
var req loginRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.AbortBadRequest(c, errInvalidParams)
return
}
user, err := repository.GetUserByUsername(c.Request.Context(), req.Username)
if err != nil {
response.AbortUnauthorized(c, errPasswordMismatch)
return
}
if !user.CheckPassword(req.Password) {
response.AbortUnauthorized(c, errPasswordMismatch)
return
}
sess := sessions.Default(c)
sess.Set(auth.UserIDKey, user.ID)
sess.Set(auth.UserNameKey, user.Username)
_ = sess.Save()
c.JSON(http.StatusOK, response.OK(user))
}
// Register registers a new user.
func Register(c *gin.Context) {
var req registerRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.AbortBadRequest(c, errInvalidParams)
return
}
newUser := &model.User{
Username: req.Username,
Email: req.Email,
IsActive: true,
}
if err := newUser.SetEncryptedPassword(req.Password); err != nil {
response.AbortInternal(c, "密码加密失败")
return
}
gormDB := persistence.DB(c.Request.Context())
if err := gormDB.Create(newUser).Error; err != nil {
response.AbortBadRequest(c, "创建用户失败: "+err.Error())
return
}
c.JSON(http.StatusOK, response.OK(newUser))
}
// Logout logs out the current session.
func Logout(c *gin.Context) {
sess := sessions.Default(c)
sess.Clear()
_ = sess.Save()
c.JSON(http.StatusOK, response.OKNil())
}
// SendEmailCode sends an email verification code.
func SendEmailCode(c *gin.Context) {
c.JSON(http.StatusOK, response.OK(gin.H{"sent": true}))
}
// ChangePassword changes the current user password.
func ChangePassword(c *gin.Context) {
var req changePasswordRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.AbortBadRequest(c, errInvalidParams)
return
}
userID := auth.GetUserIDFromContext(c)
user, err := repository.GetUserByID(c.Request.Context(), userID)
if err != nil {
response.AbortNotFound(c, errUserNotFound)
return
}
if !user.CheckPassword(req.OldPassword) {
response.AbortBadRequest(c, errOldPasswordIncorrect)
return
}
if err := user.SetEncryptedPassword(req.NewPassword); err != nil {
response.AbortInternal(c, "密码更新失败")
return
}
gormDB := persistence.DB(c.Request.Context())
_ = gormDB.Save(&user)
auth.InvalidateCachedUser(c.Request.Context(), user.ID)
c.JSON(http.StatusOK, response.OKNil())
}
// UpdateProfile updates profile info.
func UpdateProfile(c *gin.Context) {
var req updateProfileRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.AbortBadRequest(c, errInvalidParams)
return
}
userID := auth.GetUserIDFromContext(c)
user, err := repository.GetUserByID(c.Request.Context(), userID)
if err != nil {
response.AbortNotFound(c, errUserNotFound)
return
}
user.Nickname = req.Nickname
user.AvatarURL = req.AvatarURL
user.Bio = req.Bio
user.Phone = req.Phone
user.Gender = req.Gender
user.Website = req.Website
user.Location = req.Location
gormDB := persistence.DB(c.Request.Context())
_ = gormDB.Save(&user)
auth.InvalidateCachedUser(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)
var tokens []model.AccessToken
gormDB := persistence.DB(c.Request.Context())
_ = gormDB.Where("user_id = ?", userID).Find(&tokens).Error
c.JSON(http.StatusOK, response.OK(tokens))
}
const (
tokenEntropyByteLength = 24
tokenMaskMinLength = 8
)
// CreateAccessToken generates a new access token.
func CreateAccessToken(c *gin.Context) {
var req createAccessTokenRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.AbortBadRequest(c, errInvalidParams)
return
}
userID := auth.GetUserIDFromContext(c)
rawBytes := make([]byte, tokenEntropyByteLength)
_, _ = rand.Read(rawBytes)
rawToken := "wvt_" + hex.EncodeToString(rawBytes)
hash := sha256.Sum256([]byte(rawToken))
tokenHash := hex.EncodeToString(hash[:])
masked := rawToken
if len(rawToken) > tokenMaskMinLength {
masked = rawToken[:4] + "..." + rawToken[len(rawToken)-4:]
}
token := model.AccessToken{
UserID: userID,
Name: req.Name,
TokenHash: tokenHash,
MaskedToken: masked,
IsAdmin: req.IsAdmin,
}
gormDB := persistence.DB(c.Request.Context())
if err := gormDB.Create(&token).Error; err != nil {
response.AbortInternal(c, "创建令牌失败")
return
}
c.JSON(http.StatusOK, response.OK(gin.H{
"token": token,
"raw_token": rawToken,
}))
}
// DeleteAccessToken deletes a specific access token.
func DeleteAccessToken(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
response.AbortBadRequest(c, errInvalidParams)
return
}
userID := auth.GetUserIDFromContext(c)
var token model.AccessToken
gormDB := persistence.DB(c.Request.Context())
if err := gormDB.Where("id = ? AND user_id = ?", id, userID).First(&token).Error; err != nil {
response.AbortNotFound(c, errTokenNotFound)
return
}
_ = gormDB.Delete(&token)
auth.InvalidateCachedToken(c.Request.Context(), token.TokenHash)
c.JSON(http.StatusOK, response.OKNil())
}
// RotateAccessToken rotates an access token value.
func RotateAccessToken(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
response.AbortBadRequest(c, errInvalidParams)
return
}
userID := auth.GetUserIDFromContext(c)
var token model.AccessToken
gormDB := persistence.DB(c.Request.Context())
if err := gormDB.Where("id = ? AND user_id = ?", id, userID).First(&token).Error; err != nil {
response.AbortNotFound(c, errTokenNotFound)
return
}
auth.InvalidateCachedToken(c.Request.Context(), token.TokenHash)
rawBytes := make([]byte, tokenEntropyByteLength)
_, _ = rand.Read(rawBytes)
rawToken := "wvt_" + hex.EncodeToString(rawBytes)
hash := sha256.Sum256([]byte(rawToken))
token.TokenHash = hex.EncodeToString(hash[:])
masked := rawToken
if len(rawToken) > tokenMaskMinLength {
masked = rawToken[:4] + "..." + rawToken[len(rawToken)-4:]
}
token.MaskedToken = masked
_ = gormDB.Save(&token)
c.JSON(http.StatusOK, response.OK(gin.H{
"token": token,
"raw_token": rawToken,
}))
}
+15 -13
View File
@@ -1,3 +1,6 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
// Package user provides the user profile, credential management, role management, and access token domain plugin for Cordis.
package user
@@ -8,8 +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/internal/apps/oauth"
"github.com/Rain-kl/Wavelet/internal/apps/user"
"github.com/Rain-kl/Wavelet/plugins/domain/auth"
"github.com/hibiken/asynq"
)
@@ -71,20 +73,20 @@ func (p *Plugin) Apply(ctx *core.Context) error {
// 3. Register HTTP Routes
userGroup := ctx.Router().Group("/api/v1/user")
{
userGroup.POST("/login", user.Login)
userGroup.POST("/register", user.Register)
userGroup.GET("/logout", user.Logout)
userGroup.POST("/send-email-code", user.SendEmailCode)
userGroup.POST("/change-password", oauth.LoginRequired(), user.ChangePassword)
userGroup.PUT("/profile", oauth.LoginRequired(), user.UpdateProfile)
userGroup.POST("/login", Login)
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)
// Access Tokens
tokensGroup := userGroup.Group("/access-tokens", oauth.LoginRequired(), oauth.DisallowTokenAuth())
tokensGroup := userGroup.Group("/access-tokens", auth.LoginRequired(), auth.DisallowTokenAuth())
{
tokensGroup.GET("", user.ListAccessTokens)
tokensGroup.POST("", user.CreateAccessToken)
tokensGroup.DELETE("/:id", user.DeleteAccessToken)
tokensGroup.POST("/:id/rotate", user.RotateAccessToken)
tokensGroup.GET("", ListAccessTokens)
tokensGroup.POST("", CreateAccessToken)
tokensGroup.DELETE("/:id", DeleteAccessToken)
tokensGroup.POST("/:id/rotate", RotateAccessToken)
}
}