mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-06 15:46:37 +08:00
user
This commit is contained in:
@@ -68,7 +68,7 @@ func TestListAuthSources(t *testing.T) {
|
||||
}
|
||||
dbConn.Create(&source)
|
||||
|
||||
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true, SignKey: "admin_key"}
|
||||
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true}
|
||||
router := setupTestRouter(adminUser)
|
||||
|
||||
req, _ := http.NewRequest("GET", "/api/v1/admin/auth-sources", nil)
|
||||
@@ -105,7 +105,7 @@ func TestCreateAuthSource(t *testing.T) {
|
||||
dbConn, _, cleanup := testhelper.SetupTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true, SignKey: "admin_key"}
|
||||
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true}
|
||||
router := setupTestRouter(adminUser)
|
||||
|
||||
t.Run("create successfully", func(t *testing.T) {
|
||||
@@ -175,7 +175,7 @@ func TestUpdateAuthSource(t *testing.T) {
|
||||
}
|
||||
dbConn.Create(&source)
|
||||
|
||||
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true, SignKey: "admin_key"}
|
||||
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true}
|
||||
router := setupTestRouter(adminUser)
|
||||
|
||||
t.Run("update keep client secret", func(t *testing.T) {
|
||||
@@ -252,7 +252,7 @@ func TestToggleAuthSource(t *testing.T) {
|
||||
}
|
||||
dbConn.Create(&source)
|
||||
|
||||
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true, SignKey: "admin_key"}
|
||||
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true}
|
||||
router := setupTestRouter(adminUser)
|
||||
|
||||
t.Run("cannot activate without credentials", func(t *testing.T) {
|
||||
@@ -318,7 +318,7 @@ func TestDeleteAuthSource(t *testing.T) {
|
||||
}
|
||||
dbConn.Create(&externalAccount)
|
||||
|
||||
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true, SignKey: "admin_key"}
|
||||
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true}
|
||||
router := setupTestRouter(adminUser)
|
||||
|
||||
req, _ := http.NewRequest("DELETE", "/api/v1/admin/auth-sources/1", nil)
|
||||
|
||||
@@ -62,7 +62,7 @@ func TestCreateSystemConfig(t *testing.T) {
|
||||
dbConn, _, cleanup := testhelper.SetupTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true, SignKey: "admin_key"}
|
||||
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true}
|
||||
router := setupTestRouter(adminUser)
|
||||
|
||||
t.Run("create successfully", func(t *testing.T) {
|
||||
@@ -124,7 +124,7 @@ func TestListSystemConfigs(t *testing.T) {
|
||||
_, _, cleanup := testhelper.SetupTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true, SignKey: "admin_key"}
|
||||
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true}
|
||||
router := setupTestRouter(adminUser)
|
||||
|
||||
t.Run("list all seeded configurations", func(t *testing.T) {
|
||||
@@ -171,7 +171,7 @@ func TestGetSystemConfig(t *testing.T) {
|
||||
_, _, cleanup := testhelper.SetupTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true, SignKey: "admin_key"}
|
||||
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true}
|
||||
router := setupTestRouter(adminUser)
|
||||
|
||||
t.Run("get existing configuration", func(t *testing.T) {
|
||||
@@ -210,7 +210,7 @@ func TestUpdateSystemConfig(t *testing.T) {
|
||||
dbConn, _, cleanup := testhelper.SetupTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true, SignKey: "admin_key"}
|
||||
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true}
|
||||
router := setupTestRouter(adminUser)
|
||||
|
||||
t.Run("update successfully", func(t *testing.T) {
|
||||
@@ -263,7 +263,7 @@ func TestDeleteSystemConfig(t *testing.T) {
|
||||
dbConn, _, cleanup := testhelper.SetupTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true, SignKey: "admin_key"}
|
||||
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true}
|
||||
router := setupTestRouter(adminUser)
|
||||
|
||||
t.Run("delete successfully", func(t *testing.T) {
|
||||
|
||||
@@ -61,7 +61,7 @@ func TestListTaskTypes(t *testing.T) {
|
||||
_, _, cleanup := testhelper.SetupTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true, SignKey: "admin_key"}
|
||||
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true}
|
||||
router := setupTestRouter(adminUser)
|
||||
|
||||
req, _ := http.NewRequest("GET", "/api/v1/admin/tasks/types", nil)
|
||||
@@ -99,7 +99,7 @@ func TestDispatchTask(t *testing.T) {
|
||||
_, _, cleanup := testhelper.SetupTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true, SignKey: "admin_key"}
|
||||
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true}
|
||||
router := setupTestRouter(adminUser)
|
||||
|
||||
t.Run("dispatch valid task successfully", func(t *testing.T) {
|
||||
@@ -156,7 +156,7 @@ func TestListTaskExecutions(t *testing.T) {
|
||||
_, _, cleanup := testhelper.SetupTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true, SignKey: "admin_key"}
|
||||
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true}
|
||||
router := setupTestRouter(adminUser)
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -245,7 +245,7 @@ func TestGetTaskExecution(t *testing.T) {
|
||||
_, _, cleanup := testhelper.SetupTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true, SignKey: "admin_key"}
|
||||
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true}
|
||||
router := setupTestRouter(adminUser)
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -308,7 +308,7 @@ func TestRetryTask(t *testing.T) {
|
||||
_, _, cleanup := testhelper.SetupTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true, SignKey: "admin_key"}
|
||||
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true}
|
||||
router := setupTestRouter(adminUser)
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -415,7 +415,7 @@ func TestRetryTaskMaxRetryExceeded(t *testing.T) {
|
||||
_, _, cleanup := testhelper.SetupTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true, SignKey: "admin_key"}
|
||||
adminUser := &model.User{ID: 1001, Username: "admin", IsAdmin: true}
|
||||
router := setupTestRouter(adminUser)
|
||||
ctx := context.Background()
|
||||
|
||||
|
||||
@@ -20,4 +20,8 @@ const (
|
||||
userNotFound = "用户不存在"
|
||||
cannotDisable = "不能禁用管理员用户"
|
||||
updateUserFailed = "更新用户状态失败"
|
||||
usernameExists = "用户名已存在"
|
||||
usernameRequired = "用户名不能为空"
|
||||
passwordTooShort = "密码长度不能少于 8 位"
|
||||
createUserFailed = "创建用户失败"
|
||||
)
|
||||
|
||||
@@ -23,9 +23,9 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/linux-do/credit/internal/db"
|
||||
"github.com/linux-do/credit/internal/db/idgen"
|
||||
"github.com/linux-do/credit/internal/model"
|
||||
"github.com/linux-do/credit/internal/util"
|
||||
"github.com/shopspring/decimal"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -38,23 +38,15 @@ type listUsersRequest struct {
|
||||
}
|
||||
|
||||
type user struct {
|
||||
ID uint64 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Nickname string `json:"nickname"`
|
||||
AvatarUrl string `json:"avatar_url"`
|
||||
TrustLevel model.TrustLevel `json:"trust_level"`
|
||||
PayScore int64 `json:"pay_score"`
|
||||
TotalReceive decimal.Decimal `json:"total_receive"`
|
||||
TotalPayment decimal.Decimal `json:"total_payment"`
|
||||
TotalTransfer decimal.Decimal `json:"total_transfer"`
|
||||
TotalCommunity decimal.Decimal `json:"total_community"`
|
||||
CommunityBalance decimal.Decimal `json:"community_balance"`
|
||||
AvailableBalance decimal.Decimal `json:"available_balance"`
|
||||
IsActive bool `json:"is_active"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
LastLoginAt time.Time `json:"last_login_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID uint64 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Nickname string `json:"nickname"`
|
||||
AvatarUrl string `json:"avatar_url"`
|
||||
IsActive bool `json:"is_active"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
LastLoginAt time.Time `json:"last_login_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// listUsersResponse 用户列表响应
|
||||
@@ -76,6 +68,7 @@ type listUsersResponse struct {
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||
// @Failure 500 {object} util.ResponseAny "内部错误"
|
||||
// @Router /api/v1/admin/users [get]
|
||||
// ListUsers 获取用户列表
|
||||
func ListUsers(c *gin.Context) {
|
||||
var req listUsersRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
@@ -105,9 +98,7 @@ func ListUsers(c *gin.Context) {
|
||||
|
||||
offset := (req.Page - 1) * req.PageSize
|
||||
if err := query.
|
||||
Select("id, username, nickname, avatar_url, trust_level, pay_score, " +
|
||||
"total_receive, total_payment, total_transfer, total_community, " +
|
||||
"community_balance, available_balance, is_active, is_admin, " +
|
||||
Select("id, username, nickname, avatar_url, is_active, is_admin, " +
|
||||
"last_login_at, created_at, updated_at").
|
||||
Order("id DESC").
|
||||
Offset(offset).
|
||||
@@ -185,3 +176,94 @@ func UpdateUserStatus(c *gin.Context) {
|
||||
|
||||
c.JSON(http.StatusOK, util.OKNil())
|
||||
}
|
||||
|
||||
// createUserRequest 创建用户请求
|
||||
type createUserRequest struct {
|
||||
Username string `json:"username" binding:"required,min=3,max=64"`
|
||||
Password string `json:"password" binding:"required,min=8,max=64"`
|
||||
Nickname string `json:"nickname" binding:"omitempty,max=64"`
|
||||
IsActive bool `json:"is_active"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
}
|
||||
|
||||
// CreateUser 创建用户
|
||||
// @Summary 创建用户
|
||||
// @Description 创建一个本地密码登录的新用户,需要管理员权限
|
||||
// @Tags admin
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security SessionCookie
|
||||
// @Param request body user.createUserRequest true "创建用户参数"
|
||||
// @Success 200 {object} util.ResponseAny{data=user.user} "创建成功"
|
||||
// @Failure 400 {object} util.ResponseAny "参数错误或用户名已存在"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Failure 403 {object} util.ResponseAny "无管理员权限"
|
||||
// @Failure 500 {object} util.ResponseAny "内部错误"
|
||||
// @Router /api/v1/admin/users [post]
|
||||
func CreateUser(c *gin.Context) {
|
||||
var req createUserRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, util.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
req.Username = strings.TrimSpace(req.Username)
|
||||
req.Nickname = strings.TrimSpace(req.Nickname)
|
||||
req.Password = strings.TrimSpace(req.Password)
|
||||
|
||||
if req.Username == "" {
|
||||
c.JSON(http.StatusBadRequest, util.Err(usernameRequired))
|
||||
return
|
||||
}
|
||||
if len(req.Password) < 8 {
|
||||
c.JSON(http.StatusBadRequest, util.Err(passwordTooShort))
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
var count int64
|
||||
if err := db.DB(ctx).Table("users").Where("username = ?", req.Username).Count(&count).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
if count > 0 {
|
||||
c.JSON(http.StatusBadRequest, util.Err(usernameExists))
|
||||
return
|
||||
}
|
||||
|
||||
newUser := model.User{
|
||||
ID: idgen.NextUint64ID(),
|
||||
Username: req.Username,
|
||||
Nickname: req.Nickname,
|
||||
IsActive: req.IsActive,
|
||||
IsAdmin: req.IsAdmin,
|
||||
LastLoginAt: time.Time{},
|
||||
}
|
||||
if newUser.Nickname == "" {
|
||||
newUser.Nickname = req.Username
|
||||
}
|
||||
|
||||
if err := newUser.SetEncryptedPassword(req.Password); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.DB(ctx).Create(&newUser).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
res := user{
|
||||
ID: newUser.ID,
|
||||
Username: newUser.Username,
|
||||
Nickname: newUser.Nickname,
|
||||
AvatarUrl: newUser.AvatarUrl,
|
||||
IsActive: newUser.IsActive,
|
||||
IsAdmin: newUser.IsAdmin,
|
||||
LastLoginAt: newUser.LastLoginAt,
|
||||
CreatedAt: newUser.CreatedAt,
|
||||
UpdatedAt: newUser.UpdatedAt,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, util.OK(res))
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ import (
|
||||
"github.com/linux-do/credit/internal/model"
|
||||
"github.com/linux-do/credit/internal/testhelper"
|
||||
"github.com/linux-do/credit/internal/util"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
func setupTestRouter(authUser *model.User) *gin.Engine {
|
||||
@@ -46,6 +45,7 @@ func setupTestRouter(authUser *model.User) *gin.Engine {
|
||||
})
|
||||
|
||||
adminGroup.GET("/users", ListUsers)
|
||||
adminGroup.POST("/users", CreateUser)
|
||||
adminGroup.PUT("/users/:id/status", UpdateUserStatus)
|
||||
return r
|
||||
}
|
||||
@@ -57,34 +57,28 @@ func TestListUsers(t *testing.T) {
|
||||
// Seed users
|
||||
users := []model.User{
|
||||
{
|
||||
ID: 1001,
|
||||
Username: "alice",
|
||||
Nickname: "Alice Nickname",
|
||||
IsActive: true,
|
||||
IsAdmin: false,
|
||||
AvailableBalance: decimal.NewFromFloat(100.0),
|
||||
LastLoginAt: time.Now(),
|
||||
SignKey: "alice_sign_key",
|
||||
ID: 1001,
|
||||
Username: "alice",
|
||||
Nickname: "Alice Nickname",
|
||||
IsActive: true,
|
||||
IsAdmin: false,
|
||||
LastLoginAt: time.Now(),
|
||||
},
|
||||
{
|
||||
ID: 1002,
|
||||
Username: "bob",
|
||||
Nickname: "Bob Nickname",
|
||||
IsActive: true,
|
||||
IsAdmin: false,
|
||||
AvailableBalance: decimal.NewFromFloat(50.0),
|
||||
LastLoginAt: time.Now(),
|
||||
SignKey: "bob_sign_key",
|
||||
ID: 1002,
|
||||
Username: "bob",
|
||||
Nickname: "Bob Nickname",
|
||||
IsActive: true,
|
||||
IsAdmin: false,
|
||||
LastLoginAt: time.Now(),
|
||||
},
|
||||
{
|
||||
ID: 1003,
|
||||
Username: "charlie",
|
||||
Nickname: "Charlie Nickname",
|
||||
IsActive: false,
|
||||
IsAdmin: true,
|
||||
AvailableBalance: decimal.NewFromFloat(9999.0),
|
||||
LastLoginAt: time.Now(),
|
||||
SignKey: "charlie_sign_key",
|
||||
ID: 1003,
|
||||
Username: "charlie",
|
||||
Nickname: "Charlie Nickname",
|
||||
IsActive: false,
|
||||
IsAdmin: true,
|
||||
LastLoginAt: time.Now(),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -94,7 +88,7 @@ func TestListUsers(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
adminUser := &model.User{ID: 1003, Username: "charlie", IsAdmin: true, SignKey: "charlie_sign_key"}
|
||||
adminUser := &model.User{ID: 1003, Username: "charlie", IsAdmin: true}
|
||||
router := setupTestRouter(adminUser)
|
||||
|
||||
t.Run("basic pagination list", func(t *testing.T) {
|
||||
@@ -185,14 +179,12 @@ func TestUpdateUserStatus(t *testing.T) {
|
||||
Username: "alice",
|
||||
IsActive: true,
|
||||
IsAdmin: false,
|
||||
SignKey: "alice_sign_key",
|
||||
}
|
||||
adminUser := model.User{
|
||||
ID: 1002,
|
||||
Username: "bob",
|
||||
IsActive: true,
|
||||
IsAdmin: true,
|
||||
SignKey: "bob_sign_key",
|
||||
}
|
||||
|
||||
dbConn.Create(®ularUser)
|
||||
@@ -252,3 +244,107 @@ func TestUpdateUserStatus(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateUser(t *testing.T) {
|
||||
dbConn, _, cleanup := testhelper.SetupTestEnvironment(t)
|
||||
defer cleanup()
|
||||
|
||||
adminUser := &model.User{ID: 1003, Username: "charlie", IsAdmin: true}
|
||||
router := setupTestRouter(adminUser)
|
||||
|
||||
t.Run("create user successfully", func(t *testing.T) {
|
||||
payload := createUserRequest{
|
||||
Username: "newuser",
|
||||
Password: "newpassword123",
|
||||
Nickname: "New Nickname",
|
||||
IsActive: true,
|
||||
IsAdmin: false,
|
||||
}
|
||||
body, _ := json.Marshal(payload)
|
||||
req, _ := http.NewRequest("POST", "/api/v1/admin/users", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected 200 OK, got %d. Body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var resp util.ResponseAny
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("failed to parse response: %v", err)
|
||||
}
|
||||
|
||||
if resp.ErrorMsg != "" {
|
||||
t.Errorf("expected empty error message, got '%s'", resp.ErrorMsg)
|
||||
}
|
||||
|
||||
dataBytes, _ := json.Marshal(resp.Data)
|
||||
var resUser user
|
||||
if err := json.Unmarshal(dataBytes, &resUser); err != nil {
|
||||
t.Fatalf("failed to parse response data: %v", err)
|
||||
}
|
||||
|
||||
if resUser.Username != "newuser" || resUser.Nickname != "New Nickname" || !resUser.IsActive || resUser.IsAdmin {
|
||||
t.Errorf("unexpected user values: %+v", resUser)
|
||||
}
|
||||
|
||||
// Verify in DB
|
||||
var dbUser model.User
|
||||
if err := dbConn.Where("username = ?", "newuser").First(&dbUser).Error; err != nil {
|
||||
t.Fatalf("failed to find user in db: %v", err)
|
||||
}
|
||||
if !dbUser.CheckPassword("newpassword123") {
|
||||
t.Error("password was not hashed correctly")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("create user with duplicate username", func(t *testing.T) {
|
||||
// Create the first user
|
||||
existing := model.User{
|
||||
ID: 2001,
|
||||
Username: "dupuser",
|
||||
Nickname: "Dup User",
|
||||
}
|
||||
dbConn.Create(&existing)
|
||||
|
||||
payload := createUserRequest{
|
||||
Username: "dupuser",
|
||||
Password: "password123",
|
||||
Nickname: "Another Nick",
|
||||
IsActive: true,
|
||||
}
|
||||
body, _ := json.Marshal(payload)
|
||||
req, _ := http.NewRequest("POST", "/api/v1/admin/users", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400 Bad Request, got %d. Body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var resp util.ResponseAny
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
if resp.ErrorMsg != usernameExists {
|
||||
t.Errorf("expected error '%s', got '%s'", usernameExists, resp.ErrorMsg)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("validation error - password too short", func(t *testing.T) {
|
||||
payload := createUserRequest{
|
||||
Username: "shortpass",
|
||||
Password: "123",
|
||||
IsActive: true,
|
||||
}
|
||||
body, _ := json.Marshal(payload)
|
||||
req, _ := http.NewRequest("POST", "/api/v1/admin/users", bytes.NewBuffer(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400 Bad Request, got %d. Body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -712,7 +712,6 @@ func TestCallbackBind(t *testing.T) {
|
||||
ID: 777,
|
||||
Username: "existing_member",
|
||||
Nickname: "Existing Member",
|
||||
SignKey: "key",
|
||||
IsActive: true,
|
||||
LastLoginAt: time.Now(),
|
||||
}
|
||||
@@ -841,7 +840,6 @@ func TestCallbackBind(t *testing.T) {
|
||||
ID: 888,
|
||||
Username: "another_member",
|
||||
Nickname: "Another Member",
|
||||
SignKey: "key2",
|
||||
IsActive: true,
|
||||
LastLoginAt: time.Now(),
|
||||
}
|
||||
@@ -929,7 +927,6 @@ func TestExternalAccountsListAndDelete(t *testing.T) {
|
||||
ID: 555,
|
||||
Username: "account_holder",
|
||||
IsActive: true,
|
||||
SignKey: "key5",
|
||||
})
|
||||
|
||||
dbConn.Create(&model.AuthSource{
|
||||
@@ -1000,7 +997,6 @@ func TestLoginRequiredAndRiskChecks(t *testing.T) {
|
||||
ID: 1122,
|
||||
Username: "risk_tester",
|
||||
IsActive: true,
|
||||
SignKey: "keysign",
|
||||
})
|
||||
|
||||
// Enable risk checks
|
||||
|
||||
@@ -23,48 +23,25 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/linux-do/credit/internal/model"
|
||||
"github.com/linux-do/credit/internal/util"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
type BasicUserInfo struct {
|
||||
ID uint64 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Nickname string `json:"nickname"`
|
||||
TrustLevel model.TrustLevel `json:"trust_level"`
|
||||
AvatarUrl string `json:"avatar_url"`
|
||||
TotalReceive decimal.Decimal `json:"total_receive"`
|
||||
TotalPayment decimal.Decimal `json:"total_payment"`
|
||||
TotalTransfer decimal.Decimal `json:"total_transfer"`
|
||||
TotalCommunity decimal.Decimal `json:"total_community"`
|
||||
CommunityBalance decimal.Decimal `json:"community_balance"`
|
||||
AvailableBalance decimal.Decimal `json:"available_balance"`
|
||||
PendingBalance decimal.Decimal `json:"pending_balance"`
|
||||
PayScore int64 `json:"pay_score"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
RemainQuota decimal.Decimal `json:"remain_quota"`
|
||||
PayLevel string `json:"pay_level"`
|
||||
DailyLimit *int64 `json:"daily_limit"`
|
||||
ID uint64 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Nickname string `json:"nickname"`
|
||||
AvatarUrl string `json:"avatar_url"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
NeedChangePassword bool `json:"need_change_password"`
|
||||
}
|
||||
|
||||
func BuildBasicUserInfo(user *model.User) BasicUserInfo {
|
||||
func BuildBasicUserInfo(user *model.User, needChange bool) BasicUserInfo {
|
||||
return BasicUserInfo{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Nickname: user.Nickname,
|
||||
TrustLevel: user.TrustLevel,
|
||||
AvatarUrl: user.AvatarUrl,
|
||||
TotalReceive: user.TotalReceive,
|
||||
TotalPayment: user.TotalPayment,
|
||||
TotalTransfer: user.TotalTransfer,
|
||||
TotalCommunity: user.TotalCommunity,
|
||||
CommunityBalance: user.CommunityBalance,
|
||||
AvailableBalance: user.AvailableBalance,
|
||||
PendingBalance: user.PendingBalance,
|
||||
PayScore: user.PayScore,
|
||||
IsAdmin: user.IsAdmin,
|
||||
RemainQuota: decimal.NewFromInt(-1),
|
||||
PayLevel: "Free",
|
||||
DailyLimit: nil,
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Nickname: user.Nickname,
|
||||
AvatarUrl: user.AvatarUrl,
|
||||
IsAdmin: user.IsAdmin,
|
||||
NeedChangePassword: needChange,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,10 +56,12 @@ func BuildBasicUserInfo(user *model.User) BasicUserInfo {
|
||||
// @Router /api/v1/oauth/user-info [get]
|
||||
func UserInfo(c *gin.Context) {
|
||||
user, _ := util.GetFromContext[*model.User](c, UserObjKey)
|
||||
session := sessions.Default(c)
|
||||
needChange := session.Get("need_change_password") == true
|
||||
|
||||
c.JSON(
|
||||
http.StatusOK,
|
||||
util.OK(BuildBasicUserInfo(user)),
|
||||
util.OK(BuildBasicUserInfo(user, needChange)),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -255,7 +255,7 @@ func normalizeOAuthUserInfo(userInfo *model.OAuthUserInfo) error {
|
||||
func buildCallbackResult(user *model.User, status string) OAuthCallbackResult {
|
||||
result := OAuthCallbackResult{Status: status}
|
||||
if user != nil {
|
||||
info := BuildBasicUserInfo(user)
|
||||
info := BuildBasicUserInfo(user, false)
|
||||
result.User = &info
|
||||
}
|
||||
return result
|
||||
|
||||
@@ -35,7 +35,6 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/linux-do/credit/internal/apps/oauth"
|
||||
"github.com/linux-do/credit/internal/common/response"
|
||||
"github.com/linux-do/credit/internal/config"
|
||||
"github.com/linux-do/credit/internal/db"
|
||||
"github.com/linux-do/credit/internal/db/idgen"
|
||||
@@ -73,20 +72,20 @@ func UploadFile(c *gin.Context) {
|
||||
|
||||
header, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
response.RespondFailure(c, ErrNoFileSelected)
|
||||
c.JSON(http.StatusOK, util.Err(ErrNoFileSelected))
|
||||
return
|
||||
}
|
||||
|
||||
file, err := header.Open()
|
||||
if err != nil {
|
||||
response.RespondFailure(c, ErrOpenFileFailed)
|
||||
c.JSON(http.StatusOK, util.Err(ErrOpenFileFailed))
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// 校验大小
|
||||
if header.Size > maxUploadSize {
|
||||
response.RespondFailure(c, "文件大小不能超过 32MB")
|
||||
c.JSON(http.StatusOK, util.Err("文件大小不能超过 32MB"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -109,7 +108,7 @@ func UploadFile(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
if !allowed {
|
||||
response.RespondFailure(c, ErrUnsupportedFormat)
|
||||
c.JSON(http.StatusOK, util.Err(ErrUnsupportedFormat))
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -119,7 +118,7 @@ func UploadFile(c *gin.Context) {
|
||||
var buf bytes.Buffer
|
||||
size, err := io.Copy(&buf, io.TeeReader(file, hashWriter))
|
||||
if err != nil {
|
||||
response.RespondFailure(c, ErrProcessFileFailed)
|
||||
c.JSON(http.StatusOK, util.Err(ErrProcessFileFailed))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -152,15 +151,15 @@ func UploadFile(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := db.DB(ctx).Create(&newUpload).Error; err != nil {
|
||||
response.RespondFailure(c, ErrSaveUploadRecordFailed)
|
||||
c.JSON(http.StatusOK, util.Err(ErrSaveUploadRecordFailed))
|
||||
return
|
||||
}
|
||||
|
||||
logger.InfoF(ctx, "文件触发秒传成功! ID: %d, Path: %s", id, existing.FilePath)
|
||||
response.RespondSuccess(c, newUpload)
|
||||
c.JSON(http.StatusOK, util.OK(newUpload))
|
||||
return
|
||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
response.RespondFailure(c, "文件校验失败")
|
||||
c.JSON(http.StatusOK, util.Err("文件校验失败"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -169,7 +168,7 @@ func UploadFile(c *gin.Context) {
|
||||
var meta model.UploadMetadata
|
||||
if metadataStr != "" {
|
||||
if err := json.Unmarshal([]byte(metadataStr), &meta); err != nil {
|
||||
response.RespondFailure(c, "元数据 JSON 格式不合法")
|
||||
c.JSON(http.StatusOK, util.Err("元数据 JSON 格式不合法"))
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -192,7 +191,7 @@ func UploadFile(c *gin.Context) {
|
||||
err = storage.PutObject(ctx, fullKey, bytes.NewReader(buf.Bytes()), size, mimeType)
|
||||
if err != nil {
|
||||
logger.ErrorF(ctx, "S3 存储上传失败: %v", err)
|
||||
response.RespondFailure(c, ErrSaveFileFailed)
|
||||
c.JSON(http.StatusOK, util.Err(ErrSaveFileFailed))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
@@ -200,14 +199,14 @@ func UploadFile(c *gin.Context) {
|
||||
localDir := filepath.Join("uploads", time.Now().Format("2006/01/02"))
|
||||
if err := os.MkdirAll(localDir, 0755); err != nil {
|
||||
logger.ErrorF(ctx, "创建本地上传目录失败: %v", err)
|
||||
response.RespondFailure(c, ErrSaveFileFailed)
|
||||
c.JSON(http.StatusOK, util.Err(ErrSaveFileFailed))
|
||||
return
|
||||
}
|
||||
|
||||
localPath := filepath.Join(localDir, fmt.Sprintf("%d.%s", id, ext))
|
||||
if err := os.WriteFile(localPath, buf.Bytes(), 0644); err != nil {
|
||||
logger.ErrorF(ctx, "本地磁盘写入文件失败: %v", err)
|
||||
response.RespondFailure(c, ErrSaveFileFailed)
|
||||
c.JSON(http.StatusOK, util.Err(ErrSaveFileFailed))
|
||||
return
|
||||
}
|
||||
// 统一使用相对路径,方便将来环境移植或备份
|
||||
@@ -235,11 +234,11 @@ func UploadFile(c *gin.Context) {
|
||||
if storageDriver == "local" {
|
||||
_ = os.Remove(subPath)
|
||||
}
|
||||
response.RespondFailure(c, ErrSaveUploadRecordFailed)
|
||||
c.JSON(http.StatusOK, util.Err(ErrSaveUploadRecordFailed))
|
||||
return
|
||||
}
|
||||
|
||||
response.RespondSuccess(c, newUpload)
|
||||
c.JSON(http.StatusOK, util.OK(newUpload))
|
||||
}
|
||||
|
||||
// DownloadFile 通用单文件下载接口
|
||||
@@ -259,7 +258,7 @@ func DownloadFile(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
uploadID, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
response.RespondFailure(c, "无效的文件 ID")
|
||||
c.JSON(http.StatusOK, util.Err("无效的文件 ID"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -269,7 +268,7 @@ func DownloadFile(c *gin.Context) {
|
||||
c.AbortWithStatus(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
response.RespondFailure(c, "查询文件记录失败")
|
||||
c.JSON(http.StatusOK, util.Err("查询文件记录失败"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -317,7 +316,7 @@ func BatchDownloadFiles(c *gin.Context) {
|
||||
|
||||
var req batchDownloadRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.RespondFailure(c, "参数绑定失败,请传入有效的文件 ID 数组")
|
||||
c.JSON(http.StatusOK, util.Err("参数绑定失败,请传入有效的文件 ID 数组"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -326,7 +325,7 @@ func BatchDownloadFiles(c *gin.Context) {
|
||||
for _, idStr := range req.IDs {
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
response.RespondFailure(c, fmt.Sprintf("无效的 ID 值: %s", idStr))
|
||||
c.JSON(http.StatusOK, util.Err(fmt.Sprintf("无效的 ID 值: %s", idStr)))
|
||||
return
|
||||
}
|
||||
ids = append(ids, id)
|
||||
@@ -335,12 +334,12 @@ func BatchDownloadFiles(c *gin.Context) {
|
||||
// 查库获取所有匹配且正常的文件记录
|
||||
var uploads []model.Upload
|
||||
if err := db.DB(ctx).Where("id IN ? AND status IN (?, ?)", ids, model.UploadStatusPending, model.UploadStatusUsed).Find(&uploads).Error; err != nil {
|
||||
response.RespondFailure(c, "检索文件记录失败")
|
||||
c.JSON(http.StatusOK, util.Err("检索文件记录失败"))
|
||||
return
|
||||
}
|
||||
|
||||
if len(uploads) == 0 {
|
||||
response.RespondFailure(c, "没有找到任何有效的文件记录进行打包")
|
||||
c.JSON(http.StatusOK, util.Err("没有找到任何有效的文件记录进行打包"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -436,7 +435,7 @@ func ListMyFiles(c *gin.Context) {
|
||||
|
||||
var req listMyFilesRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
response.RespondFailure(c, "参数错误")
|
||||
c.JSON(http.StatusOK, util.Err("参数错误"))
|
||||
return
|
||||
}
|
||||
if req.Page <= 0 {
|
||||
@@ -461,23 +460,23 @@ func ListMyFiles(c *gin.Context) {
|
||||
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
response.RespondFailure(c, "查询文件数量失败")
|
||||
c.JSON(http.StatusOK, util.Err("查询文件数量失败"))
|
||||
return
|
||||
}
|
||||
|
||||
var items []model.Upload
|
||||
offset := (req.Page - 1) * req.PageSize
|
||||
if err := query.Order("created_at DESC").Offset(offset).Limit(req.PageSize).Find(&items).Error; err != nil {
|
||||
response.RespondFailure(c, "查询文件列表失败")
|
||||
c.JSON(http.StatusOK, util.Err("查询文件列表失败"))
|
||||
return
|
||||
}
|
||||
|
||||
response.RespondSuccess(c, listMyFilesResponse{
|
||||
c.JSON(http.StatusOK, util.OK(listMyFilesResponse{
|
||||
Total: total,
|
||||
Page: req.Page,
|
||||
PageSize: req.PageSize,
|
||||
Items: items,
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
// DeleteFile 软删除文件记录
|
||||
@@ -498,7 +497,7 @@ func DeleteFile(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
uploadID, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
response.RespondFailure(c, "无效的文件 ID")
|
||||
c.JSON(http.StatusOK, util.Err("无效的文件 ID"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -508,7 +507,7 @@ func DeleteFile(c *gin.Context) {
|
||||
c.AbortWithStatus(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
response.RespondFailure(c, "查询文件记录失败")
|
||||
c.JSON(http.StatusOK, util.Err("查询文件记录失败"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -519,11 +518,11 @@ func DeleteFile(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := db.DB(ctx).Model(&upload).Update("status", model.UploadStatusDeleted).Error; err != nil {
|
||||
response.RespondFailure(c, "删除文件失败")
|
||||
c.JSON(http.StatusOK, util.Err("删除文件失败"))
|
||||
return
|
||||
}
|
||||
|
||||
response.RespondSuccess(c, nil)
|
||||
c.JSON(http.StatusOK, util.OKNil())
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
|
||||
@@ -39,9 +39,8 @@ import (
|
||||
)
|
||||
|
||||
type testResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
ErrorMsg string `json:"error_msg"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
|
||||
func setupTestRouter(authUser *model.User) *gin.Engine {
|
||||
@@ -161,8 +160,8 @@ func TestUploadFile(t *testing.T) {
|
||||
t.Fatalf("failed to unmarshal response: %v", err)
|
||||
}
|
||||
|
||||
if !resp.Success {
|
||||
t.Fatalf("expected success response, got failure: %s", resp.Message)
|
||||
if resp.ErrorMsg != "" {
|
||||
t.Fatalf("expected success response, got failure: %s", resp.ErrorMsg)
|
||||
}
|
||||
|
||||
// Verify database record
|
||||
@@ -212,7 +211,7 @@ func TestUploadFile(t *testing.T) {
|
||||
|
||||
var resp testResponse
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
if resp.Success || !strings.Contains(resp.Message, ErrUnsupportedFormat) {
|
||||
if resp.ErrorMsg == "" || !strings.Contains(resp.ErrorMsg, ErrUnsupportedFormat) {
|
||||
t.Errorf("expected unsupported format error, got: %v", resp)
|
||||
}
|
||||
})
|
||||
@@ -249,8 +248,8 @@ func TestUploadFile(t *testing.T) {
|
||||
var resp2 testResponse
|
||||
json.Unmarshal(w2.Body.Bytes(), &resp2)
|
||||
|
||||
if !resp2.Success {
|
||||
t.Fatalf("second upload was unsuccessful: %s", resp2.Message)
|
||||
if resp2.ErrorMsg != "" {
|
||||
t.Fatalf("second upload was unsuccessful: %s", resp2.ErrorMsg)
|
||||
}
|
||||
|
||||
var uploadRecord2 model.Upload
|
||||
@@ -306,8 +305,8 @@ func TestUploadFile(t *testing.T) {
|
||||
var resp testResponse
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
|
||||
if !resp.Success {
|
||||
t.Fatalf("local upload failed: %s", resp.Message)
|
||||
if resp.ErrorMsg != "" {
|
||||
t.Fatalf("local upload failed: %s", resp.ErrorMsg)
|
||||
}
|
||||
|
||||
var localRecord model.Upload
|
||||
|
||||
@@ -17,12 +17,12 @@ limitations under the License.
|
||||
package user
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/linux-do/credit/internal/apps/oauth"
|
||||
"github.com/linux-do/credit/internal/common/response"
|
||||
"github.com/linux-do/credit/internal/db"
|
||||
"github.com/linux-do/credit/internal/model"
|
||||
"github.com/linux-do/credit/internal/util"
|
||||
@@ -46,17 +46,18 @@ type tokenResponse struct {
|
||||
// @Success 200 {object} util.ResponseAny{data=[]model.AccessToken} "令牌列表"
|
||||
// @Failure 401 {object} util.ResponseAny "未登录"
|
||||
// @Router /api/v1/user/access-tokens [get]
|
||||
// ListAccessTokens 获取当前用户的 AccessToken 列表
|
||||
func ListAccessTokens(c *gin.Context) {
|
||||
currUser, _ := util.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||||
ctx := c.Request.Context()
|
||||
|
||||
var tokens []model.AccessToken
|
||||
if err := db.DB(ctx).Where("user_id = ?", currUser.ID).Order("created_at desc").Find(&tokens).Error; err != nil {
|
||||
response.RespondFailure(c, err.Error())
|
||||
c.JSON(http.StatusOK, util.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
response.RespondSuccess(c, tokens)
|
||||
c.JSON(http.StatusOK, util.OK(tokens))
|
||||
}
|
||||
|
||||
// CreateAccessToken 创建一个新的 AccessToken
|
||||
@@ -76,13 +77,13 @@ func CreateAccessToken(c *gin.Context) {
|
||||
|
||||
var req createTokenRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.RespondFailure(c, "参数绑定失败")
|
||||
c.JSON(http.StatusOK, util.Err("参数绑定失败"))
|
||||
return
|
||||
}
|
||||
|
||||
req.Name = strings.TrimSpace(req.Name)
|
||||
if req.Name == "" {
|
||||
response.RespondFailure(c, "令牌名称不能为空")
|
||||
c.JSON(http.StatusOK, util.Err("令牌名称不能为空"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -94,19 +95,19 @@ func CreateAccessToken(c *gin.Context) {
|
||||
|
||||
var count int64
|
||||
if err := db.DB(ctx).Model(&model.AccessToken{}).Where("user_id = ?", currUser.ID).Count(&count).Error; err != nil {
|
||||
response.RespondFailure(c, err.Error())
|
||||
c.JSON(http.StatusOK, util.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if int(count) >= maxLimit {
|
||||
response.RespondFailure(c, "已达到访问令牌最大创建数量限制")
|
||||
c.JSON(http.StatusOK, util.Err("已达到访问令牌最大创建数量限制"))
|
||||
return
|
||||
}
|
||||
|
||||
// 生成 Token
|
||||
tokenStr, err := model.GenerateTokenString()
|
||||
if err != nil {
|
||||
response.RespondFailure(c, "生成令牌失败")
|
||||
c.JSON(http.StatusOK, util.Err("生成令牌失败"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -121,14 +122,14 @@ func CreateAccessToken(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := db.DB(ctx).Create(&tokenRecord).Error; err != nil {
|
||||
response.RespondFailure(c, err.Error())
|
||||
c.JSON(http.StatusOK, util.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
response.RespondSuccess(c, tokenResponse{
|
||||
c.JSON(http.StatusOK, util.OK(tokenResponse{
|
||||
Token: tokenStr,
|
||||
Record: tokenRecord,
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
// DeleteAccessToken 删除一个 AccessToken
|
||||
@@ -148,22 +149,22 @@ func DeleteAccessToken(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
response.RespondFailure(c, "无效的令牌ID")
|
||||
c.JSON(http.StatusOK, util.Err("无效的令牌ID"))
|
||||
return
|
||||
}
|
||||
|
||||
tx := db.DB(ctx).Where("id = ? AND user_id = ?", id, currUser.ID).Delete(&model.AccessToken{})
|
||||
if tx.Error != nil {
|
||||
response.RespondFailure(c, tx.Error.Error())
|
||||
c.JSON(http.StatusOK, util.Err(tx.Error.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if tx.RowsAffected == 0 {
|
||||
response.RespondFailure(c, "令牌不存在或无权操作")
|
||||
c.JSON(http.StatusOK, util.Err("令牌不存在或无权操作"))
|
||||
return
|
||||
}
|
||||
|
||||
response.RespondSuccess(c, "删除成功")
|
||||
c.JSON(http.StatusOK, util.OK("删除成功"))
|
||||
}
|
||||
|
||||
// RotateAccessToken 轮换一个 AccessToken
|
||||
@@ -183,20 +184,20 @@ func RotateAccessToken(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
response.RespondFailure(c, "无效的令牌ID")
|
||||
c.JSON(http.StatusOK, util.Err("无效的令牌ID"))
|
||||
return
|
||||
}
|
||||
|
||||
var tokenRecord model.AccessToken
|
||||
if err := db.DB(ctx).Where("id = ? AND user_id = ?", id, currUser.ID).First(&tokenRecord).Error; err != nil {
|
||||
response.RespondFailure(c, "令牌不存在或无权操作")
|
||||
c.JSON(http.StatusOK, util.Err("令牌不存在或无权操作"))
|
||||
return
|
||||
}
|
||||
|
||||
// 生成新的 Token
|
||||
newTokenStr, err := model.GenerateTokenString()
|
||||
if err != nil {
|
||||
response.RespondFailure(c, "生成令牌失败")
|
||||
c.JSON(http.StatusOK, util.Err("生成令牌失败"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -208,12 +209,12 @@ func RotateAccessToken(c *gin.Context) {
|
||||
tokenRecord.LastUsedAt = nil // 轮换后重置使用时间
|
||||
|
||||
if err := db.DB(ctx).Save(&tokenRecord).Error; err != nil {
|
||||
response.RespondFailure(c, err.Error())
|
||||
c.JSON(http.StatusOK, util.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
response.RespondSuccess(c, tokenResponse{
|
||||
c.JSON(http.StatusOK, util.OK(tokenResponse{
|
||||
Token: newTokenStr,
|
||||
Record: tokenRecord,
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -10,7 +11,6 @@ import (
|
||||
"github.com/linux-do/credit/internal/apps/oauth"
|
||||
"github.com/linux-do/credit/internal/common"
|
||||
"github.com/linux-do/credit/internal/common/bind"
|
||||
"github.com/linux-do/credit/internal/common/response"
|
||||
"github.com/linux-do/credit/internal/db"
|
||||
"github.com/linux-do/credit/internal/model"
|
||||
"github.com/linux-do/credit/internal/util"
|
||||
@@ -75,7 +75,7 @@ func setLoginSession(c *gin.Context, user *model.User) error {
|
||||
// @Router /api/v1/user/login [post]
|
||||
func Login(c *gin.Context) {
|
||||
if !isPasswordLoginEnabled() {
|
||||
response.RespondFailure(c, "管理员关闭了密码登录")
|
||||
c.JSON(http.StatusOK, util.Err("管理员关闭了密码登录"))
|
||||
return
|
||||
}
|
||||
var req loginRequest
|
||||
@@ -84,36 +84,56 @@ func Login(c *gin.Context) {
|
||||
}
|
||||
req.Username = strings.TrimSpace(req.Username)
|
||||
if req.Username == "" || req.Password == "" {
|
||||
response.RespondFailure(c, "无效的参数")
|
||||
c.JSON(http.StatusOK, util.Err("无效的参数"))
|
||||
return
|
||||
}
|
||||
|
||||
var user model.User
|
||||
ctx := c.Request.Context()
|
||||
if err := db.DB(ctx).Where("username = ?", req.Username).First(&user).Error; err != nil {
|
||||
response.RespondFailure(c, "用户名或密码错误")
|
||||
c.JSON(http.StatusOK, util.Err("用户名或密码错误"))
|
||||
return
|
||||
}
|
||||
if !user.IsActive {
|
||||
response.RespondFailure(c, common.BannedAccount)
|
||||
c.JSON(http.StatusOK, util.Err(common.BannedAccount))
|
||||
return
|
||||
}
|
||||
|
||||
// 判定是否是明文密码存储
|
||||
isPlaintext := !(strings.HasPrefix(user.Password, "$2a$") || strings.HasPrefix(user.Password, "$2b$") || strings.HasPrefix(user.Password, "$2y$"))
|
||||
|
||||
if !user.CheckPassword(req.Password) {
|
||||
response.RespondFailure(c, "用户名或密码错误")
|
||||
c.JSON(http.StatusOK, util.Err("用户名或密码错误"))
|
||||
return
|
||||
}
|
||||
|
||||
session := sessions.Default(c)
|
||||
needChangePassword := false
|
||||
|
||||
// 如果是以明文密码登录,在数据库中置换为加密密码
|
||||
if isPlaintext {
|
||||
if err := user.SetEncryptedPassword(req.Password); err == nil {
|
||||
if err := db.DB(ctx).Model(&user).Update("password", user.Password).Error; err != nil {
|
||||
c.JSON(http.StatusOK, util.Err("升级密码安全算法失败,请重试"))
|
||||
return
|
||||
}
|
||||
needChangePassword = true
|
||||
session.Set("need_change_password", true)
|
||||
_ = session.Save()
|
||||
}
|
||||
}
|
||||
|
||||
user.LastLoginAt = time.Now()
|
||||
if err := db.DB(ctx).Model(&user).Update("last_login_at", user.LastLoginAt).Error; err != nil {
|
||||
response.RespondFailure(c, err.Error())
|
||||
c.JSON(http.StatusOK, util.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
if err := setLoginSession(c, &user); err != nil {
|
||||
response.RespondFailure(c, "无法保存会话信息,请重试")
|
||||
c.JSON(http.StatusOK, util.Err("无法保存会话信息,请重试"))
|
||||
return
|
||||
}
|
||||
|
||||
response.RespondSuccess(c, oauth.BuildBasicUserInfo(&user))
|
||||
c.JSON(http.StatusOK, util.OK(oauth.BuildBasicUserInfo(&user, needChangePassword)))
|
||||
}
|
||||
|
||||
// Register 用户注册
|
||||
@@ -129,7 +149,7 @@ func Login(c *gin.Context) {
|
||||
// @Router /api/v1/user/register [post]
|
||||
func Register(c *gin.Context) {
|
||||
if !isRegistrationEnabled() || !isPasswordRegisterEnabled() {
|
||||
response.RespondFailure(c, "管理员关闭了注册")
|
||||
c.JSON(http.StatusOK, util.Err("管理员关闭了注册"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -144,22 +164,22 @@ func Register(c *gin.Context) {
|
||||
req.DisplayName = strings.TrimSpace(req.DisplayName)
|
||||
|
||||
if req.Username == "" || req.Password == "" {
|
||||
response.RespondFailure(c, "无效的参数")
|
||||
c.JSON(http.StatusOK, util.Err("无效的参数"))
|
||||
return
|
||||
}
|
||||
if len(req.Password) < 8 {
|
||||
response.RespondFailure(c, "密码长度不能少于 8 位")
|
||||
c.JSON(http.StatusOK, util.Err("密码长度不能少于 8 位"))
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
var count int64
|
||||
if err := db.DB(ctx).Model(&model.User{}).Where("username = ?", req.Username).Count(&count).Error; err != nil {
|
||||
response.RespondFailure(c, err.Error())
|
||||
c.JSON(http.StatusOK, util.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
if count > 0 {
|
||||
response.RespondFailure(c, "用户名已存在")
|
||||
c.JSON(http.StatusOK, util.Err("用户名已存在"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -167,9 +187,6 @@ func Register(c *gin.Context) {
|
||||
Username: req.Username,
|
||||
Nickname: req.Nickname,
|
||||
AvatarUrl: "",
|
||||
TrustLevel: model.TrustLevelNewUser,
|
||||
PayScore: 0,
|
||||
SignKey: util.GenerateUniqueIDSimple(),
|
||||
IsActive: true,
|
||||
IsAdmin: false,
|
||||
LastLoginAt: time.Now(),
|
||||
@@ -181,21 +198,21 @@ func Register(c *gin.Context) {
|
||||
user.Nickname = req.Username
|
||||
}
|
||||
if err := user.SetPassword(req.Password); err != nil {
|
||||
response.RespondFailure(c, err.Error())
|
||||
c.JSON(http.StatusOK, util.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.DB(ctx).Create(&user).Error; err != nil {
|
||||
response.RespondFailure(c, err.Error())
|
||||
c.JSON(http.StatusOK, util.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if err := setLoginSession(c, &user); err != nil {
|
||||
response.RespondFailure(c, "无法保存会话信息,请重试")
|
||||
c.JSON(http.StatusOK, util.Err("无法保存会话信息,请重试"))
|
||||
return
|
||||
}
|
||||
|
||||
response.RespondSuccess(c, oauth.BuildBasicUserInfo(&user))
|
||||
c.JSON(http.StatusOK, util.OK(oauth.BuildBasicUserInfo(&user, false)))
|
||||
}
|
||||
|
||||
// Logout 用户退出登录
|
||||
@@ -212,8 +229,80 @@ func Logout(c *gin.Context) {
|
||||
session.Options(util.GetSessionOptions(-1))
|
||||
session.Clear()
|
||||
if err := session.Save(); err != nil {
|
||||
response.RespondFailure(c, err.Error())
|
||||
c.JSON(http.StatusOK, util.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
response.RespondSuccessMessage(c, "")
|
||||
c.JSON(http.StatusOK, util.OK(""))
|
||||
}
|
||||
|
||||
type changePasswordRequest struct {
|
||||
OldPassword string `json:"old_password"`
|
||||
NewPassword string `json:"new_password"`
|
||||
}
|
||||
|
||||
// ChangePassword 修改用户密码
|
||||
// @Summary 修改用户密码
|
||||
// @Description 修改当前登录用户的密码。修改成功后,如果是首次明文登录的升级提示,则清除修改密码的提示状态。
|
||||
// @Tags user
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body user.changePasswordRequest true "修改密码请求参数"
|
||||
// @Success 200 {object} util.ResponseAny{data=string} "修改密码成功"
|
||||
// @Failure 400 {object} util.ResponseAny "原密码错误或新密码不符合要求"
|
||||
// @Failure 401 {object} util.ResponseAny "请先登录"
|
||||
// @Router /api/v1/user/change-password [post]
|
||||
func ChangePassword(c *gin.Context) {
|
||||
var req changePasswordRequest
|
||||
if !bind.JSON(c, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
req.OldPassword = strings.TrimSpace(req.OldPassword)
|
||||
req.NewPassword = strings.TrimSpace(req.NewPassword)
|
||||
|
||||
if req.OldPassword == "" || req.NewPassword == "" {
|
||||
c.JSON(http.StatusOK, util.Err("无效的参数"))
|
||||
return
|
||||
}
|
||||
if len(req.NewPassword) < 8 {
|
||||
c.JSON(http.StatusOK, util.Err("新密码长度不能少于 8 位"))
|
||||
return
|
||||
}
|
||||
|
||||
userObj, _ := util.GetFromContext[*model.User](c, oauth.UserObjKey)
|
||||
if userObj == nil {
|
||||
c.JSON(http.StatusUnauthorized, util.Err("请先登录"))
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
var dbUser model.User
|
||||
if err := db.DB(ctx).Where("id = ?", userObj.ID).First(&dbUser).Error; err != nil {
|
||||
c.JSON(http.StatusOK, util.Err("未找到该用户"))
|
||||
return
|
||||
}
|
||||
|
||||
// 校验旧密码
|
||||
if !dbUser.CheckPassword(req.OldPassword) {
|
||||
c.JSON(http.StatusOK, util.Err("原密码不正确"))
|
||||
return
|
||||
}
|
||||
|
||||
// 加密并更新为新密码
|
||||
if err := dbUser.SetEncryptedPassword(req.NewPassword); err != nil {
|
||||
c.JSON(http.StatusOK, util.Err("密码加密失败,请重试"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.DB(ctx).Model(&dbUser).Update("password", dbUser.Password).Error; err != nil {
|
||||
c.JSON(http.StatusOK, util.Err(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
// 清除 Session 中修改密码提示状态
|
||||
session := sessions.Default(c)
|
||||
session.Delete("need_change_password")
|
||||
_ = session.Save()
|
||||
|
||||
c.JSON(http.StatusOK, util.OK("密码修改成功"))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user