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("密码修改成功"))
|
||||
}
|
||||
|
||||
@@ -4,10 +4,11 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/linux-do/credit/internal/common/response"
|
||||
"github.com/linux-do/credit/internal/util"
|
||||
)
|
||||
|
||||
// DecodeJSONBody decodes JSON reader to target
|
||||
@@ -32,7 +33,7 @@ func IDParam(c *gin.Context) (uint, bool) {
|
||||
func IDParamByName(c *gin.Context, name string) (uint, bool) {
|
||||
id, err := strconv.ParseUint(c.Param(name), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
response.RespondBadRequest(c, "")
|
||||
c.JSON(http.StatusBadRequest, util.Err("参数错误"))
|
||||
return 0, false
|
||||
}
|
||||
return uint(id), true
|
||||
@@ -41,7 +42,7 @@ func IDParamByName(c *gin.Context, name string) (uint, bool) {
|
||||
// JSON binds JSON body of context request to target
|
||||
func JSON(c *gin.Context, target any) bool {
|
||||
if err := DecodeJSONBody(c.Request.Body, target); err != nil {
|
||||
response.RespondBadRequest(c, "")
|
||||
c.JSON(http.StatusBadRequest, util.Err("参数错误"))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
package response
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const invalidParamsMessage = "参数错误"
|
||||
|
||||
// RespondSuccess sends a successful response with data
|
||||
func RespondSuccess(c *gin.Context, data any) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": data,
|
||||
})
|
||||
}
|
||||
|
||||
// RespondSuccessWithExtras sends a successful response with data and extra fields
|
||||
func RespondSuccessWithExtras(c *gin.Context, data any, extras gin.H) {
|
||||
payload := gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": data,
|
||||
}
|
||||
for key, value := range extras {
|
||||
payload[key] = value
|
||||
}
|
||||
c.JSON(http.StatusOK, payload)
|
||||
}
|
||||
|
||||
// RespondSuccessMessage sends a successful response with a custom message
|
||||
func RespondSuccessMessage(c *gin.Context, message string) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": message,
|
||||
})
|
||||
}
|
||||
|
||||
// RespondFailure sends a failed response with http.StatusOK and a failure message
|
||||
func RespondFailure(c *gin.Context, message string) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": message,
|
||||
})
|
||||
}
|
||||
|
||||
// RespondBadRequest sends a bad request response (400)
|
||||
func RespondBadRequest(c *gin.Context, message string) {
|
||||
if message == "" {
|
||||
message = invalidParamsMessage
|
||||
}
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"success": false,
|
||||
"message": message,
|
||||
})
|
||||
}
|
||||
|
||||
// RespondUnauthorized sends an unauthorized response (401)
|
||||
func RespondUnauthorized(c *gin.Context, message string) {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"success": false,
|
||||
"message": message,
|
||||
})
|
||||
}
|
||||
|
||||
// RespondForbidden sends a forbidden response (403)
|
||||
func RespondForbidden(c *gin.Context, message string) {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"success": false,
|
||||
"message": message,
|
||||
})
|
||||
}
|
||||
|
||||
// RespondErrorWithStatus sends a response with target HTTP status code and a message
|
||||
func RespondErrorWithStatus(c *gin.Context, code int, message string) {
|
||||
c.JSON(code, gin.H{
|
||||
"success": false,
|
||||
"message": message,
|
||||
})
|
||||
}
|
||||
@@ -19,11 +19,13 @@ package migrator
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/linux-do/credit/internal/model"
|
||||
|
||||
"github.com/linux-do/credit/internal/config"
|
||||
"github.com/linux-do/credit/internal/db"
|
||||
"github.com/linux-do/credit/internal/db/idgen"
|
||||
)
|
||||
|
||||
func Migrate() {
|
||||
@@ -46,6 +48,8 @@ func Migrate() {
|
||||
|
||||
// 初始化系统配置数据
|
||||
initSystemConfigs()
|
||||
// 初始化默认管理员用户
|
||||
initDefaultAdmin()
|
||||
}
|
||||
|
||||
// initSystemConfigs 初始化系统配置数据
|
||||
@@ -113,3 +117,35 @@ func initSystemConfigs() {
|
||||
log.Printf("[PostgreSQL] initialized %d default system configs\n", len(defaultConfigs))
|
||||
}
|
||||
}
|
||||
|
||||
// initDefaultAdmin 初始化默认管理员用户
|
||||
func initDefaultAdmin() {
|
||||
tx := db.DB(context.Background())
|
||||
|
||||
var count int64
|
||||
if err := tx.Model(&model.User{}).Where("username = ?", "admin").Count(&count).Error; err != nil {
|
||||
log.Printf("[PostgreSQL] failed to check default admin user: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
return
|
||||
}
|
||||
|
||||
adminUser := model.User{
|
||||
ID: idgen.NextUint64ID(),
|
||||
Username: "admin",
|
||||
Password: "12345678", // 密码使用明文存储
|
||||
Nickname: "Administrator",
|
||||
AvatarUrl: "",
|
||||
IsActive: true,
|
||||
IsAdmin: true,
|
||||
LastLoginAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := tx.Create(&adminUser).Error; err != nil {
|
||||
log.Printf("[PostgreSQL] failed to create default admin user: %v\n", err)
|
||||
} else {
|
||||
log.Printf("[PostgreSQL] default admin user created successfully (username: admin, password: 12345678)\n")
|
||||
}
|
||||
}
|
||||
|
||||
+30
-124
@@ -17,41 +17,26 @@ limitations under the License.
|
||||
package model
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/linux-do/credit/internal/common"
|
||||
"github.com/linux-do/credit/internal/util"
|
||||
"github.com/shopspring/decimal"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type TrustLevel uint8
|
||||
|
||||
const (
|
||||
TrustLevelNewUser TrustLevel = iota
|
||||
TrustLevelBasicUser
|
||||
TrustLevelUser
|
||||
TrustLevelActiveUser
|
||||
TrustLevelLeader
|
||||
)
|
||||
|
||||
// OAuthUserInfo 用户信息结构(同时支持 OIDC ID Token claims 和 UserEndpoint 响应)
|
||||
type OAuthUserInfo struct {
|
||||
Id uint64 `json:"id"`
|
||||
Sub string `json:"sub"`
|
||||
Username string `json:"username"`
|
||||
PreferredUsername string `json:"preferred_username"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
Active bool `json:"active"`
|
||||
AvatarUrl string `json:"avatar_url"`
|
||||
TrustLevel TrustLevel `json:"trust_level"`
|
||||
Id uint64 `json:"id"`
|
||||
Sub string `json:"sub"`
|
||||
Username string `json:"username"`
|
||||
PreferredUsername string `json:"preferred_username"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
Active bool `json:"active"`
|
||||
AvatarUrl string `json:"avatar_url"`
|
||||
}
|
||||
|
||||
// GetID 获取用户 ID
|
||||
@@ -68,49 +53,25 @@ func (u *OAuthUserInfo) GetID() uint64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// UserGamificationScoreResponse API响应
|
||||
type UserGamificationScoreResponse struct {
|
||||
User struct {
|
||||
GamificationScore int64 `json:"gamification_score"`
|
||||
} `json:"user"`
|
||||
}
|
||||
|
||||
// LeaderboardResponse 排行榜 API 响应
|
||||
type LeaderboardResponse struct {
|
||||
Users []LeaderboardUser `json:"users"`
|
||||
}
|
||||
|
||||
// LeaderboardUser 排行榜用户信息
|
||||
type LeaderboardUser struct {
|
||||
ID uint64 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
TotalScore int64 `json:"total_score"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID uint64 `json:"id" gorm:"primaryKey;index:idx_users_active_bal_id,priority:3"`
|
||||
Username string `json:"username" gorm:"size:64;uniqueIndex"`
|
||||
Password string `json:"password,omitempty" gorm:"size:255"`
|
||||
Nickname string `json:"nickname" gorm:"size:255"`
|
||||
AvatarUrl string `json:"avatar_url" gorm:"size:255"`
|
||||
TrustLevel TrustLevel `json:"trust_level" gorm:"index"`
|
||||
PayScore int64 `json:"pay_score" gorm:"default:0;index"`
|
||||
SignKey string `json:"sign_key" gorm:"size:64;uniqueIndex;not null"`
|
||||
TotalReceive decimal.Decimal `json:"total_receive" gorm:"type:numeric(20,2);default:0"`
|
||||
TotalPayment decimal.Decimal `json:"total_payment" gorm:"type:numeric(20,2);default:0"`
|
||||
TotalTransfer decimal.Decimal `json:"total_transfer" gorm:"type:numeric(20,2);default:0"`
|
||||
TotalCommunity decimal.Decimal `json:"total_community" gorm:"type:numeric(20,2);default:0"`
|
||||
CommunityBalance decimal.Decimal `json:"community_balance" gorm:"type:numeric(20,2);default:0"`
|
||||
AvailableBalance decimal.Decimal `json:"available_balance" gorm:"type:numeric(20,2);default:0;index:idx_users_active_bal_id,priority:2"`
|
||||
PendingBalance decimal.Decimal `json:"pending_balance" gorm:"type:numeric(20,2);default:0"`
|
||||
IsActive bool `json:"is_active" gorm:"default:true;index:idx_users_active_bal_id,priority:1"`
|
||||
IsAdmin bool `json:"is_admin" gorm:"default:false"`
|
||||
LastLoginAt time.Time `json:"last_login_at" gorm:"index"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;index"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime;index"`
|
||||
ID uint64 `json:"id" gorm:"primaryKey"`
|
||||
Username string `json:"username" gorm:"size:64;uniqueIndex"`
|
||||
Password string `json:"password,omitempty" gorm:"size:255"`
|
||||
Nickname string `json:"nickname" gorm:"size:255"`
|
||||
AvatarUrl string `json:"avatar_url" gorm:"size:255"`
|
||||
IsActive bool `json:"is_active" gorm:"default:true;index"`
|
||||
IsAdmin bool `json:"is_admin" gorm:"default:false"`
|
||||
LastLoginAt time.Time `json:"last_login_at" gorm:"index"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;index"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime;index"`
|
||||
}
|
||||
|
||||
func (u *User) SetPassword(password string) error {
|
||||
u.Password = password
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) SetEncryptedPassword(password string) error {
|
||||
if password == "" {
|
||||
u.Password = ""
|
||||
return nil
|
||||
@@ -127,7 +88,11 @@ func (u *User) CheckPassword(password string) bool {
|
||||
if u.Password == "" || password == "" {
|
||||
return false
|
||||
}
|
||||
return util.CheckPasswordHash(u.Password, password)
|
||||
isBcrypt := strings.HasPrefix(u.Password, "$2a$") || strings.HasPrefix(u.Password, "$2b$") || strings.HasPrefix(u.Password, "$2y$")
|
||||
if isBcrypt {
|
||||
return util.CheckPasswordHash(u.Password, password)
|
||||
}
|
||||
return u.Password == password
|
||||
}
|
||||
|
||||
func (u *User) GetByID(tx *gorm.DB, id uint64) error {
|
||||
@@ -137,70 +102,13 @@ func (u *User) GetByID(tx *gorm.DB, id uint64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByIDs 批量查询用户
|
||||
func GetByIDs(tx *gorm.DB, ids []uint64) ([]User, error) {
|
||||
var users []User
|
||||
if err := tx.Where("id IN ?", ids).Find(&users).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (u *User) GetUserGamificationScore(ctx context.Context) (*UserGamificationScoreResponse, error) {
|
||||
if u.Username == "dev_user" {
|
||||
var response UserGamificationScoreResponse
|
||||
response.User.GamificationScore = 12345
|
||||
return &response, nil
|
||||
}
|
||||
url := fmt.Sprintf("https://linux.do/u/%s.json", u.Username)
|
||||
resp, err := util.Request(ctx, http.MethodGet, url, nil, nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("获取用户积分失败,状态码: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var response UserGamificationScoreResponse
|
||||
if err = json.NewDecoder(resp.Body).Decode(&response); err != nil {
|
||||
return nil, fmt.Errorf("解析用户积分响应失败: %w", err)
|
||||
}
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
// GetLeaderboard 获取排行榜数据
|
||||
func GetLeaderboard(ctx context.Context, page int) (*LeaderboardResponse, error) {
|
||||
url := fmt.Sprintf("https://linux.do/leaderboard/1.json?period=all_time&page=%d", page)
|
||||
resp, err := util.Request(ctx, http.MethodGet, url, nil, nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("获取排行榜失败,状态码: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var response LeaderboardResponse
|
||||
if err = json.NewDecoder(resp.Body).Decode(&response); err != nil {
|
||||
return nil, fmt.Errorf("解析排行榜响应失败: %w", err)
|
||||
}
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
// UpdateFromOAuthInfo 根据 OAuth 信息更新用户数据
|
||||
func (u *User) UpdateFromOAuthInfo(oauthInfo *OAuthUserInfo) {
|
||||
u.Username = oauthInfo.Username
|
||||
u.Nickname = oauthInfo.Name
|
||||
u.AvatarUrl = oauthInfo.AvatarUrl
|
||||
u.IsActive = oauthInfo.Active
|
||||
u.TrustLevel = oauthInfo.TrustLevel
|
||||
u.LastLoginAt = time.Now()
|
||||
if oauthInfo.Username == "dev_user" {
|
||||
u.IsAdmin = true
|
||||
}
|
||||
}
|
||||
|
||||
// CheckActive 检查用户账户是否激活,未激活则返回错误
|
||||
@@ -220,10 +128,8 @@ func (u *User) CreateUser(tx *gorm.DB, oauthInfo *OAuthUserInfo) error {
|
||||
Nickname: oauthInfo.Name,
|
||||
AvatarUrl: oauthInfo.AvatarUrl,
|
||||
IsActive: oauthInfo.Active,
|
||||
TrustLevel: oauthInfo.TrustLevel,
|
||||
SignKey: util.GenerateUniqueIDSimple(),
|
||||
LastLoginAt: now,
|
||||
IsAdmin: oauthInfo.Username == "dev_user",
|
||||
IsAdmin: false,
|
||||
}
|
||||
if err := tx.Create(&newUser).Error; err != nil {
|
||||
return err
|
||||
|
||||
@@ -117,6 +117,7 @@ func Serve() {
|
||||
apiV1Router.GET("/oauth/logout", oauth.Logout)
|
||||
apiV1Router.POST("/oauth/callback", oauth.Callback)
|
||||
apiV1Router.GET("/oauth/user-info", oauth.LoginRequired(), oauth.UserInfo)
|
||||
apiV1Router.GET("/user-info", oauth.LoginRequired(), oauth.UserInfo)
|
||||
apiV1Router.GET("/oauth/external-accounts", oauth.LoginRequired(), oauth.ListExternalAccounts)
|
||||
apiV1Router.POST("/oauth/external-accounts/:id/delete", oauth.LoginRequired(), oauth.DeleteExternalAccount)
|
||||
|
||||
@@ -127,6 +128,7 @@ func Serve() {
|
||||
userRouter.POST("/register", user.Register)
|
||||
userRouter.GET("/logout", user.Logout)
|
||||
userRouter.GET("/self", oauth.LoginRequired(), oauth.UserInfo)
|
||||
userRouter.POST("/change-password", oauth.LoginRequired(), user.ChangePassword)
|
||||
|
||||
// Access Token
|
||||
tokenRouter := userRouter.Group("/access-tokens")
|
||||
@@ -171,6 +173,7 @@ func Serve() {
|
||||
|
||||
// Users
|
||||
adminRouter.GET("/users", admin_user.ListUsers)
|
||||
adminRouter.POST("/users", admin_user.CreateUser)
|
||||
adminRouter.PUT("/users/:id/status", admin_user.UpdateUserStatus)
|
||||
|
||||
// System Config
|
||||
|
||||
Reference in New Issue
Block a user