diff --git a/.idea/httpRequests/http-requests-log.http b/.idea/httpRequests/http-requests-log.http deleted file mode 100644 index 2568f76..0000000 --- a/.idea/httpRequests/http-requests-log.http +++ /dev/null @@ -1,18 +0,0 @@ -POST http://127.0.0.1:5000/Api/V3/User/Login -Accept: */* -Cache-Control: no-cache - -{} - -<> 2019-11-05T074354.200.json - -### - -POST http://127.0.0.1:5000/Api/V3/User/Login -Accept: */* -Cache-Control: no-cache - -<> 2019-11-05T074329.200.json - -### - diff --git a/go.mod b/go.mod index 91e02c9..4a13281 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,13 @@ module Cloudreve go 1.12 require ( + github.com/DATA-DOG/go-sqlmock v1.3.3 github.com/gin-gonic/gin v1.4.0 github.com/go-playground/locales v0.13.0 // indirect github.com/go-playground/universal-translator v0.16.0 // indirect github.com/jinzhu/gorm v1.9.11 github.com/leodido/go-urn v1.2.0 // indirect + github.com/pkg/errors v0.8.0 github.com/stretchr/testify v1.4.0 gopkg.in/go-playground/validator.v8 v8.18.2 gopkg.in/go-playground/validator.v9 v9.30.0 diff --git a/main.go b/main.go index ab7dae3..2c83d77 100644 --- a/main.go +++ b/main.go @@ -1,9 +1,14 @@ package main import ( + "Cloudreve/models" "Cloudreve/routers" ) +func init() { + model.Init() +} + func main() { api := routers.InitRouter() diff --git a/models/init.go b/models/init.go index dbc6ba4..dccf0e5 100644 --- a/models/init.go +++ b/models/init.go @@ -13,8 +13,10 @@ import ( var DB *gorm.DB // Database 在中间件中初始化mysql链接 -func Database(connString string) { - db, err := gorm.Open("mysql", connString) +func Init() { + //TODO 从配置文件中读取 包括DEBUG模式 + util.Log().Info("初始化数据库连接\n") + db, err := gorm.Open("mysql", "root:root@(localhost)/v3?charset=utf8&parseTime=True&loc=Local") db.LogMode(true) // Error if err != nil { diff --git a/models/migration.go b/models/migration.go index c706a24..4561c1b 100644 --- a/models/migration.go +++ b/models/migration.go @@ -1,8 +1,32 @@ package model -//执行数据迁移 +import ( + "Cloudreve/pkg/util" + "github.com/jinzhu/gorm" +) +//执行数据迁移 func migration() { // 自动迁移模式 - DB.AutoMigrate() + DB.Set("gorm:table_options", "ENGINE=InnoDB").AutoMigrate(&User{}) + + // 添加初始用户 + _, err := GetUser(1) + if gorm.IsRecordNotFoundError(err) { + defaultUser := NewUser() + //TODO 动态生成密码 + defaultUser.Email = "admin@cloudreve.org" + defaultUser.Nick = "admin" + defaultUser.Status = Active + defaultUser.Group = 1 + defaultUser.PrimaryGroup = 1 + err := defaultUser.SetPassword("admin") + if err != nil { + util.Log().Panic("无法创建密码, ", err) + } + if err := DB.Create(&defaultUser).Error; err != nil { + util.Log().Panic("无法创建初始用户, ", err) + } + } + } diff --git a/models/user.go b/models/user.go new file mode 100644 index 0000000..c9962c4 --- /dev/null +++ b/models/user.go @@ -0,0 +1,100 @@ +package model + +import ( + "Cloudreve/pkg/serializer" + "Cloudreve/pkg/util" + "crypto/sha1" + "encoding/hex" + "encoding/json" + "github.com/jinzhu/gorm" + "github.com/pkg/errors" + "strings" + "time" +) + +const ( + // Active 账户正常状态 + Active = iota + // NotActivicated 未激活 + NotActivicated + // Baned 被封禁 + Baned +) + +// User 用户模型 +type User struct { + gorm.Model + Email string `gorm:"type:varchar(100);unique_index"` + Nick string `gorm:"size:50"` + Password string + Status int + Group int + PrimaryGroup int + ActivationKey string + Storage int64 + LastNotify *time.Time + OpenID string + TwoFactor string + Delay int + Avatar string + Options string `gorm:"size:4096"` +} + +// GetUser 用ID获取用户 +func GetUser(ID interface{}) (User, error) { + var user User + result := DB.First(&user, ID) + return user, result.Error +} + +// NewUser 返回一个新的空 User +func NewUser() User { + options := serializer.UserOption{ + ProfileOn: 1, + } + optionsValue, _ := json.Marshal(&options) + return User{ + Avatar: "default", + Options: string(optionsValue), + } +} + +// CheckPassword 根据明文校验密码 +func (user *User) CheckPassword(password string) (bool, error) { + + // 根据存储密码拆分为 Salt 和 Digest + passwordStore := strings.Split(user.Password, ":") + if len(passwordStore) != 2 { + return false, errors.New("Unknown password type") + } + + // todo 兼容V2/V1密码 + //计算 Salt 和密码组合的SHA1摘要 + hash := sha1.New() + _, err := hash.Write([]byte(password + passwordStore[0])) + bs := hex.EncodeToString(hash.Sum(nil)) + if err != nil { + return false, err + } + + return bs == passwordStore[1], nil +} + +// SetPassword 根据给定明文设定 User 的 Password 字段 +func (user *User) SetPassword(password string) error { + //生成16位 Salt + salt := util.RandStringRunes(16) + + //计算 Salt 和密码组合的SHA1摘要 + hash := sha1.New() + _, err := hash.Write([]byte(password + salt)) + bs := hex.EncodeToString(hash.Sum(nil)) + + if err != nil { + return err + } + + //存储 Salt 值和摘要, ":"分割 + user.Password = salt + ":" + string(bs) + return nil +} diff --git a/models/user_test.go b/models/user_test.go new file mode 100644 index 0000000..b710377 --- /dev/null +++ b/models/user_test.go @@ -0,0 +1,83 @@ +package model + +import ( + "github.com/DATA-DOG/go-sqlmock" + "github.com/jinzhu/gorm" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestGetUser(t *testing.T) { + asserts := assert.New(t) + + // 准备数据库 Mock + db, mock, err := sqlmock.New() + if err != nil { + t.Errorf("An error '%s' was not expected when opening a stub database connection", err) + } + DB, _ = gorm.Open("mysql", db) + defer db.Close() + + //找到用户时 + rows := sqlmock.NewRows([]string{"id", "deleted_at", "email"}). + AddRow(1, nil, "admin@cloudreve.org") + + mock.ExpectQuery("^SELECT (.+)").WillReturnRows(rows) + + user, err := GetUser(1) + asserts.NoError(err) + asserts.Equal(User{ + Model: gorm.Model{ + ID: 1, + DeletedAt: nil, + }, + Email: "admin@cloudreve.org", + }, user) + + //未找到用户时 + mock.ExpectQuery("^SELECT (.+)").WillReturnError(errors.New("not found")) + user, err = GetUser(1) + asserts.Error(err) + asserts.Equal(User{}, user) +} + +func TestUser_SetPassword(t *testing.T) { + asserts := assert.New(t) + user := User{} + err := user.SetPassword("Cause Sega does what nintendon't") + asserts.NoError(err) + asserts.NotEmpty(user.Password) +} + +func TestUser_CheckPassword(t *testing.T) { + asserts := assert.New(t) + user := User{} + err := user.SetPassword("Cause Sega does what nintendon't") + asserts.NoError(err) + + //密码正确 + res, err := user.CheckPassword("Cause Sega does what nintendon't") + asserts.NoError(err) + asserts.True(res) + + //密码错误 + res, err = user.CheckPassword("Cause Sega does what Nintendon't") + asserts.NoError(err) + asserts.False(res) + + //密码字段为空 + user = User{} + res, err = user.CheckPassword("Cause Sega does what nintendon't") + asserts.Error(err) + asserts.False(res) + +} + +func TestNewUser(t *testing.T) { + asserts := assert.New(t) + newUser := NewUser() + asserts.IsType(User{}, newUser) + asserts.NotEmpty(newUser.Avatar) + asserts.NotEmpty(newUser.Options) +} diff --git a/pkg/serializer/user.go b/pkg/serializer/user.go new file mode 100644 index 0000000..df0c7ac --- /dev/null +++ b/pkg/serializer/user.go @@ -0,0 +1,7 @@ +package serializer + +// UserOption 用户个性化配置字段 +type UserOption struct { + ProfileOn int `json:"profile_on"` + WebDAVKey string `json:"webdav_key"` +} diff --git a/pkg/util/common.go b/pkg/util/common.go new file mode 100644 index 0000000..5124156 --- /dev/null +++ b/pkg/util/common.go @@ -0,0 +1,18 @@ +package util + +import ( + "math/rand" + "time" +) + +// RandStringRunes 返回随机字符串 +func RandStringRunes(n int) string { + var letterRunes = []rune("1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + + rand.Seed(time.Now().UnixNano()) + b := make([]rune, n) + for i := range b { + b[i] = letterRunes[rand.Intn(len(letterRunes))] + } + return string(b) +} diff --git a/routers/controllers/main_test.go b/routers/controllers/main_test.go index 47eac22..6111d5c 100644 --- a/routers/controllers/main_test.go +++ b/routers/controllers/main_test.go @@ -9,6 +9,7 @@ import ( "testing" ) +// 测试 ErrorResponse func TestErrorResponse(t *testing.T) { asserts := assert.New(t)