Feat: user setting
This commit is contained in:
parent
33a917cc75
commit
4420a75c04
16 changed files with 577 additions and 20 deletions
1
go.mod
1
go.mod
|
@ -15,6 +15,7 @@ require (
|
|||
github.com/gin-gonic/gin v1.4.0
|
||||
github.com/go-ini/ini v1.50.0
|
||||
github.com/go-mail/mail v2.3.1+incompatible
|
||||
github.com/gofrs/uuid v3.2.0+incompatible
|
||||
github.com/gomodule/redigo v2.0.0+incompatible
|
||||
github.com/google/go-querystring v1.0.0
|
||||
github.com/gorilla/websocket v1.4.1
|
||||
|
|
2
go.sum
2
go.sum
|
@ -59,6 +59,8 @@ github.com/go-mail/mail v2.3.1+incompatible/go.mod h1:VPWjmmNyRsWXQZHVHT3g0YbIIN
|
|||
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
|
||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
|
|
|
@ -148,7 +148,7 @@ Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; verti
|
|||
{Name: "hot_share_num", Value: `10`, Type: "share"},
|
||||
{Name: "allow_buy_group", Value: `1`, Type: "group_sell"},
|
||||
{Name: "group_sell_data", Value: `[]`, Type: "group_sell"},
|
||||
{Name: "gravatar_server", Value: `https://v2ex.assets.uxengine.net/gravatar/`, Type: "avatar"},
|
||||
{Name: "gravatar_server", Value: `https://gravatar.loli.net/`, Type: "avatar"},
|
||||
{Name: "defaultTheme", Value: `#3f51b5`, Type: "basic"},
|
||||
{Name: "themes", Value: `{"#3f51b5":{"palette":{"primary":{"light":"#7986cb","main":"#3f51b5","dark":"#303f9f","contrastText":"#fff"},"secondary":{"light":"#ff4081","main":"#f50057","dark":"#c51162","contrastText":"#fff"},"error":{"light":"#e57373","main":"#f44336","dark":"#d32f2f","contrastText":"#fff"},"explorer":{"filename":"#474849","icon":"#8f8f8f","bgSelected":"#D5DAF0","emptyIcon":"#e8e8e8"}}}}`, Type: "basic"},
|
||||
{Name: "aria2_token", Value: `your token`, Type: "aria2"},
|
||||
|
@ -160,6 +160,11 @@ Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; verti
|
|||
{Name: "max_parallel_transfer", Value: `4`, Type: "task"},
|
||||
{Name: "secret_key", Value: util.RandStringRunes(256), Type: "auth"},
|
||||
{Name: "temp_path", Value: "temp", Type: "path"},
|
||||
{Name: "avatar_path", Value: "avatar", Type: "path"},
|
||||
{Name: "avatar_size", Value: "2097152", Type: "avatar"},
|
||||
{Name: "avatar_size_l", Value: "200", Type: "avatar"},
|
||||
{Name: "avatar_size_m", Value: "130", Type: "avatar"},
|
||||
{Name: "avatar_size_s", Value: "50", Type: "avatar"},
|
||||
{Name: "score_enabled", Value: "1", Type: "score"},
|
||||
{Name: "share_score_rate", Value: "80", Type: "score"},
|
||||
{Name: "score_price", Value: "1", Type: "score"},
|
||||
|
|
|
@ -45,7 +45,7 @@ type User struct {
|
|||
NotifyDate *time.Time // 通知超出配额时的日期
|
||||
|
||||
// 关联模型
|
||||
Group Group `gorm:"association_autoupdate:false"`
|
||||
Group Group `gorm:"save_associations:false:false"`
|
||||
Policy Policy `gorm:"PRELOAD:false,association_autoupdate:false"`
|
||||
|
||||
// 数据库忽略字段
|
||||
|
@ -73,12 +73,12 @@ func (user *User) DeductionStorage(size uint64) bool {
|
|||
}
|
||||
if size <= user.Storage {
|
||||
user.Storage -= size
|
||||
DB.Model(user).UpdateColumn("storage", gorm.Expr("storage - ?", size))
|
||||
DB.Model(user).Update("storage", gorm.Expr("storage - ?", size))
|
||||
return true
|
||||
}
|
||||
// 如果要减少的容量超出已用容量,则设为零
|
||||
user.Storage = 0
|
||||
DB.Model(user).UpdateColumn("storage", 0)
|
||||
DB.Model(user).Update("storage", 0)
|
||||
|
||||
return false
|
||||
}
|
||||
|
@ -90,7 +90,7 @@ func (user *User) IncreaseStorage(size uint64) bool {
|
|||
}
|
||||
if size <= user.GetRemainingCapacity() {
|
||||
user.Storage += size
|
||||
DB.Model(user).UpdateColumn("storage", gorm.Expr("storage + ?", size))
|
||||
DB.Model(user).Update("storage", gorm.Expr("storage + ?", size))
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
@ -103,7 +103,7 @@ func (user *User) PayScore(score int) bool {
|
|||
}
|
||||
if score <= user.Score {
|
||||
user.Score -= score
|
||||
DB.Model(user).UpdateColumn("score", gorm.Expr("score - ?", score))
|
||||
DB.Model(user).Update("score", gorm.Expr("score - ?", score))
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
@ -112,7 +112,7 @@ func (user *User) PayScore(score int) bool {
|
|||
// AddScore 增加积分
|
||||
func (user *User) AddScore(score int) {
|
||||
user.Score += score
|
||||
DB.Model(user).UpdateColumn("score", gorm.Expr("score + ?", score))
|
||||
DB.Model(user).Update("score", gorm.Expr("score + ?", score))
|
||||
}
|
||||
|
||||
// IncreaseStorageWithoutCheck 忽略可用容量,增加用户已用容量
|
||||
|
@ -121,7 +121,7 @@ func (user *User) IncreaseStorageWithoutCheck(size uint64) {
|
|||
return
|
||||
}
|
||||
user.Storage += size
|
||||
DB.Model(user).UpdateColumn("storage", gorm.Expr("storage + ?", size))
|
||||
DB.Model(user).Update("storage", gorm.Expr("storage + ?", size))
|
||||
|
||||
}
|
||||
|
||||
|
@ -175,7 +175,7 @@ func GetActiveUserByID(ID interface{}) (User, error) {
|
|||
// GetUserByEmail 用Email获取用户
|
||||
func GetUserByEmail(email string) (User, error) {
|
||||
var user User
|
||||
result := DB.Set("gorm:auto_preload", true).Where("email = ?", email).First(&user)
|
||||
result := DB.Set("gorm:auto_preload", true).Where("status = ? and email = ?", Active, email).First(&user)
|
||||
return user, result.Error
|
||||
}
|
||||
|
||||
|
@ -296,6 +296,11 @@ func (user *User) SetStatus(status int) {
|
|||
DB.Model(&user).Update("status", status)
|
||||
}
|
||||
|
||||
// Update 更新用户
|
||||
func (user *User) Update(val map[string]interface{}) error {
|
||||
return DB.Model(user).Updates(val).Error
|
||||
}
|
||||
|
||||
// GetGroupExpiredUsers 获取用户组过期的用户
|
||||
func GetGroupExpiredUsers() []User {
|
||||
var users []User
|
||||
|
|
|
@ -49,5 +49,5 @@ func (user *User) RegisterAuthn(credential *webauthn.Credential) {
|
|||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
DB.Model(user).UpdateColumn("authn", string(res))
|
||||
DB.Model(user).Update("authn", string(res))
|
||||
}
|
||||
|
|
|
@ -151,7 +151,6 @@ func TestNewUser(t *testing.T) {
|
|||
newUser := NewUser()
|
||||
asserts.IsType(User{}, newUser)
|
||||
asserts.NotEmpty(newUser.Avatar)
|
||||
asserts.NotEmpty(newUser.OptionsSerialized)
|
||||
}
|
||||
|
||||
func TestUser_AfterFind(t *testing.T) {
|
||||
|
@ -304,7 +303,7 @@ func TestUser_DeductionStorage(t *testing.T) {
|
|||
Storage: 10,
|
||||
}
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectExec("UPDATE(.+)").WithArgs(5, 1).WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
mock.ExpectExec("UPDATE(.+)").WithArgs(5, sqlmock.AnyArg(), 1).WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
mock.ExpectCommit()
|
||||
|
||||
asserts.True(user.DeductionStorage(5))
|
||||
|
@ -319,7 +318,7 @@ func TestUser_DeductionStorage(t *testing.T) {
|
|||
Storage: 10,
|
||||
}
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectExec("UPDATE(.+)").WithArgs(0, 1).WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
mock.ExpectExec("UPDATE(.+)").WithArgs(0, sqlmock.AnyArg(), 1).WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
mock.ExpectCommit()
|
||||
|
||||
asserts.False(user.DeductionStorage(20))
|
||||
|
@ -355,11 +354,11 @@ func TestUser_IncreaseStorageWithoutCheck(t *testing.T) {
|
|||
func TestGetUserByEmail(t *testing.T) {
|
||||
asserts := assert.New(t)
|
||||
|
||||
mock.ExpectQuery("SELECT(.+)").WithArgs("abslant@foxmail.com").WillReturnRows(sqlmock.NewRows([]string{"id", "email"}))
|
||||
mock.ExpectQuery("SELECT(.+)").WithArgs(Active, "abslant@foxmail.com").WillReturnRows(sqlmock.NewRows([]string{"id", "email"}))
|
||||
_, err := GetUserByEmail("abslant@foxmail.com")
|
||||
|
||||
asserts.NoError(mock.ExpectationsWereMet())
|
||||
asserts.Error(err)
|
||||
asserts.NoError(mock.ExpectationsWereMet())
|
||||
}
|
||||
|
||||
func TestUser_AfterCreate(t *testing.T) {
|
||||
|
|
159
pkg/qq/connect.go
Normal file
159
pkg/qq/connect.go
Normal file
|
@ -0,0 +1,159 @@
|
|||
package qq
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
model "github.com/HFO4/cloudreve/models"
|
||||
"github.com/HFO4/cloudreve/pkg/request"
|
||||
"github.com/HFO4/cloudreve/pkg/serializer"
|
||||
"github.com/gofrs/uuid"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// LoginPage 登陆页面描述
|
||||
type LoginPage struct {
|
||||
URL string
|
||||
SecretKey string
|
||||
}
|
||||
|
||||
// UserCredentials 登陆成功后的凭证
|
||||
type UserCredentials struct {
|
||||
OpenID string
|
||||
AccessToken string
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrNotEnabled 未开启登录功能
|
||||
ErrNotEnabled = serializer.NewError(serializer.CodeNoPermissionErr, "QQ登录功能未开启", nil)
|
||||
// ErrObtainAccessToken 无法获取AccessToken
|
||||
ErrObtainAccessToken = serializer.NewError(serializer.CodeParamErr, "无法获取AccessToken", nil)
|
||||
// ErrObtainOpenID 无法获取OpenID
|
||||
ErrObtainOpenID = serializer.NewError(serializer.CodeParamErr, "无法获取OpenID", nil)
|
||||
//ErrDecodeResponse 无法解析服务端响应
|
||||
ErrDecodeResponse = serializer.NewError(serializer.CodeInternalSetting, "无法解析服务端响应", nil)
|
||||
)
|
||||
|
||||
// NewLoginRequest 新建登录会话
|
||||
func NewLoginRequest() (*LoginPage, error) {
|
||||
// 获取相关设定
|
||||
options := model.GetSettingByNames("qq_login", "qq_login_id")
|
||||
if options["qq_login"] == "0" {
|
||||
return nil, ErrNotEnabled
|
||||
}
|
||||
|
||||
// 生成唯一ID
|
||||
u2, err := uuid.NewV4()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
secret := fmt.Sprintf("%x", md5.Sum(u2.Bytes()))
|
||||
|
||||
// 生成登录地址
|
||||
loginURL, _ := url.Parse("https://graph.qq.com/oauth2.0/authorize?response_type=code")
|
||||
queries := loginURL.Query()
|
||||
queries.Add("client_id", options["qq_login_id"])
|
||||
queries.Add("redirect_uri", getCallbackURL())
|
||||
queries.Add("state", secret)
|
||||
loginURL.RawQuery = queries.Encode()
|
||||
|
||||
return &LoginPage{
|
||||
URL: loginURL.String(),
|
||||
SecretKey: secret,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getCallbackURL() string {
|
||||
return "https://drive.aoaoao.me/Callback/QQ"
|
||||
// 生成回调地址
|
||||
gateway, _ := url.Parse("/#/login/qq")
|
||||
callback := model.GetSiteURL().ResolveReference(gateway).String()
|
||||
|
||||
return callback
|
||||
}
|
||||
|
||||
func getAccessTokenURL(code string) string {
|
||||
// 获取相关设定
|
||||
options := model.GetSettingByNames("qq_login_id", "qq_login_key")
|
||||
|
||||
api, _ := url.Parse("https://graph.qq.com/oauth2.0/token?grant_type=authorization_code")
|
||||
queries := api.Query()
|
||||
queries.Add("client_id", options["qq_login_id"])
|
||||
queries.Add("redirect_uri", getCallbackURL())
|
||||
queries.Add("client_secret", options["qq_login_key"])
|
||||
queries.Add("code", code)
|
||||
api.RawQuery = queries.Encode()
|
||||
|
||||
return api.String()
|
||||
}
|
||||
|
||||
func getResponse(body string) (map[string]interface{}, error) {
|
||||
var res map[string]interface{}
|
||||
|
||||
if !strings.Contains(body, "callback") {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
body = strings.TrimPrefix(body, "callback(")
|
||||
body = strings.TrimSuffix(body, ");\n")
|
||||
|
||||
err := json.Unmarshal([]byte(body), &res)
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
// Callback 处理回调,返回openid和access key
|
||||
func Callback(code string) (*UserCredentials, error) {
|
||||
// 获取相关设定
|
||||
options := model.GetSettingByNames("qq_login")
|
||||
if options["qq_login"] == "0" {
|
||||
return nil, ErrNotEnabled
|
||||
}
|
||||
|
||||
api := getAccessTokenURL(code)
|
||||
|
||||
// 获取AccessToken
|
||||
client := request.HTTPClient{}
|
||||
res := client.Request("GET", api, nil)
|
||||
resp, err := res.GetResponse()
|
||||
if err != nil {
|
||||
return nil, ErrObtainAccessToken.WithError(err)
|
||||
}
|
||||
|
||||
// 如果服务端返回错误
|
||||
errResp, err := getResponse(resp)
|
||||
if msg, ok := errResp["error_description"]; err == nil && ok {
|
||||
return nil, ErrObtainAccessToken.WithError(errors.New(msg.(string)))
|
||||
}
|
||||
|
||||
// 获取AccessToken
|
||||
vals, err := url.ParseQuery(resp)
|
||||
if err != nil {
|
||||
return nil, ErrDecodeResponse.WithError(err)
|
||||
}
|
||||
accessToken := vals.Get("access_token")
|
||||
|
||||
// 用 AccessToken 换取OpenID
|
||||
res = client.Request("GET", "https://graph.qq.com/oauth2.0/me?access_token="+accessToken, nil)
|
||||
resp, err = res.GetResponse()
|
||||
if err != nil {
|
||||
return nil, ErrObtainOpenID.WithError(err)
|
||||
}
|
||||
|
||||
// 解析服务端响应
|
||||
errResp, err = getResponse(resp)
|
||||
if msg, ok := errResp["error_description"]; err == nil && ok {
|
||||
return nil, ErrObtainOpenID.WithError(errors.New(msg.(string)))
|
||||
}
|
||||
|
||||
if openid, ok := errResp["openid"]; ok {
|
||||
return &UserCredentials{
|
||||
OpenID: openid.(string),
|
||||
AccessToken: accessToken,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, ErrDecodeResponse
|
||||
}
|
|
@ -46,6 +46,7 @@ type group struct {
|
|||
ShareFreeEnabled bool `json:"shareFree"`
|
||||
ShareDownload bool `json:"shareDownload"`
|
||||
CompressEnabled bool `json:"compress"`
|
||||
WebDAVEnabled bool `json:"webdav"`
|
||||
}
|
||||
|
||||
type tag struct {
|
||||
|
@ -91,6 +92,7 @@ func BuildUser(user model.User) User {
|
|||
ShareFreeEnabled: user.Group.OptionsSerialized.ShareFree,
|
||||
ShareDownload: user.Group.OptionsSerialized.ShareDownload,
|
||||
CompressEnabled: user.Group.OptionsSerialized.ArchiveTask,
|
||||
WebDAVEnabled: user.Group.WebDAVEnabled,
|
||||
},
|
||||
Tags: buildTagRes(tags),
|
||||
}
|
||||
|
|
|
@ -2,12 +2,14 @@ package thumb
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
model "github.com/HFO4/cloudreve/models"
|
||||
"github.com/HFO4/cloudreve/pkg/util"
|
||||
"image"
|
||||
"image/gif"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
|
@ -66,14 +68,36 @@ func (image *Thumb) GetSize() (int, int) {
|
|||
|
||||
// Save 保存图像到给定路径
|
||||
func (image *Thumb) Save(path string) (err error) {
|
||||
out, err := os.Create(path)
|
||||
defer out.Close()
|
||||
out, err := util.CreatNestedFile(path)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
err = png.Encode(out, image.src)
|
||||
return err
|
||||
|
||||
}
|
||||
|
||||
// CreateAvatar 创建头像
|
||||
func (image *Thumb) CreateAvatar(uid uint) error {
|
||||
// 读取头像相关设定
|
||||
savePath := model.GetSettingByName("avatar_path")
|
||||
s := model.GetIntSetting("avatar_size_s", 50)
|
||||
m := model.GetIntSetting("avatar_size_m", 130)
|
||||
l := model.GetIntSetting("avatar_size_l", 200)
|
||||
|
||||
// 生成头像缩略图
|
||||
src := image.src
|
||||
for k, size := range []int{s, m, l} {
|
||||
image.src = resize.Resize(uint(size), uint(size), src, resize.Lanczos3)
|
||||
err := image.Save(filepath.Join(savePath, fmt.Sprintf("avatar_%d_%d.png", uid, k)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@ import (
|
|||
"github.com/HFO4/cloudreve/pkg/filesystem/driver/local"
|
||||
"github.com/HFO4/cloudreve/pkg/filesystem/fsctx"
|
||||
"github.com/HFO4/cloudreve/pkg/serializer"
|
||||
"github.com/HFO4/cloudreve/pkg/util"
|
||||
"github.com/HFO4/cloudreve/service/explorer"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
|
@ -294,7 +293,7 @@ func FileUploadStream(c *gin.Context) {
|
|||
File: c.Request.Body,
|
||||
Size: fileSize,
|
||||
Name: fileName,
|
||||
VirtualPath: util.DotPathToStandardPath(filePath),
|
||||
VirtualPath: filePath,
|
||||
}
|
||||
|
||||
// 创建文件系统
|
||||
|
|
|
@ -17,6 +17,7 @@ func ParamErrorMsg(filed string, tag string) string {
|
|||
"Path": "路径",
|
||||
"SourceID": "原始资源",
|
||||
"URL": "链接",
|
||||
"Nick": "昵称",
|
||||
}
|
||||
// 未通过的规则与中文对应
|
||||
tagMap := map[string]string{
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
model "github.com/HFO4/cloudreve/models"
|
||||
"github.com/HFO4/cloudreve/pkg/authn"
|
||||
"github.com/HFO4/cloudreve/pkg/serializer"
|
||||
"github.com/HFO4/cloudreve/pkg/thumb"
|
||||
"github.com/HFO4/cloudreve/pkg/util"
|
||||
"github.com/HFO4/cloudreve/service/user"
|
||||
"github.com/duo-labs/webauthn/webauthn"
|
||||
|
@ -157,3 +158,117 @@ func UserTasks(c *gin.Context) {
|
|||
c.JSON(200, ErrorResponse(err))
|
||||
}
|
||||
}
|
||||
|
||||
// UserSetting 获取用户设定
|
||||
func UserSetting(c *gin.Context) {
|
||||
var service user.SettingService
|
||||
if err := c.ShouldBindUri(&service); err == nil {
|
||||
res := service.Settings(c, CurrentUser(c))
|
||||
c.JSON(200, res)
|
||||
} else {
|
||||
c.JSON(200, ErrorResponse(err))
|
||||
}
|
||||
}
|
||||
|
||||
// UseGravatar 设定头像使用全球通用
|
||||
func UseGravatar(c *gin.Context) {
|
||||
u := CurrentUser(c)
|
||||
if err := u.Update(map[string]interface{}{"avatar": "gravatar"}); err != nil {
|
||||
c.JSON(200, serializer.Err(serializer.CodeDBError, "无法更新头像", err))
|
||||
return
|
||||
}
|
||||
c.JSON(200, serializer.Response{})
|
||||
}
|
||||
|
||||
// UploadAvatar 从文件上传头像
|
||||
func UploadAvatar(c *gin.Context) {
|
||||
// 取得头像上传大小限制
|
||||
maxSize := model.GetIntSetting("avatar_size", 2097152)
|
||||
if c.Request.ContentLength == -1 || c.Request.ContentLength > int64(maxSize) {
|
||||
c.JSON(200, serializer.Err(serializer.CodeUploadFailed, "头像尺寸太大", nil))
|
||||
return
|
||||
}
|
||||
|
||||
// 取得上传的文件
|
||||
file, err := c.FormFile("avatar")
|
||||
if err != nil {
|
||||
c.JSON(200, serializer.Err(serializer.CodeIOFailed, "无法读取头像数据", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 初始化头像
|
||||
r, err := file.Open()
|
||||
if err != nil {
|
||||
c.JSON(200, serializer.Err(serializer.CodeIOFailed, "无法读取头像数据", err))
|
||||
return
|
||||
}
|
||||
avatar, err := thumb.NewThumbFromFile(r, file.Filename)
|
||||
if err != nil {
|
||||
c.JSON(200, serializer.Err(serializer.CodeIOFailed, "无法解析图像数据", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 创建头像
|
||||
u := CurrentUser(c)
|
||||
err = avatar.CreateAvatar(u.ID)
|
||||
if err != nil {
|
||||
c.JSON(200, serializer.Err(serializer.CodeIOFailed, "无法创建头像", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 保存头像标记
|
||||
if err := u.Update(map[string]interface{}{
|
||||
"avatar": "file",
|
||||
}); err != nil {
|
||||
c.JSON(200, serializer.Err(serializer.CodeDBError, "无法更新头像", err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, serializer.Response{})
|
||||
}
|
||||
|
||||
// GetUserAvatar 获取用户头像
|
||||
func GetUserAvatar(c *gin.Context) {
|
||||
var service user.AvatarService
|
||||
if err := c.ShouldBindUri(&service); err == nil {
|
||||
res := service.Get(c)
|
||||
if res.Code == -301 {
|
||||
// 重定向到gravatar
|
||||
c.Redirect(301, res.Data.(string))
|
||||
}
|
||||
} else {
|
||||
c.JSON(200, ErrorResponse(err))
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateOption 更改用户设定
|
||||
func UpdateOption(c *gin.Context) {
|
||||
var service user.SettingUpdateService
|
||||
if err := c.ShouldBindUri(&service); err == nil {
|
||||
var (
|
||||
subService user.OptionsChangeHandler
|
||||
subErr error
|
||||
)
|
||||
|
||||
switch service.Option {
|
||||
case "nick":
|
||||
subService = &user.ChangerNick{}
|
||||
case "vip":
|
||||
subService = &user.VIPUnsubscribe{}
|
||||
case "qq":
|
||||
subService = &user.QQBind{}
|
||||
}
|
||||
|
||||
subErr = c.ShouldBindJSON(subService)
|
||||
if subErr != nil {
|
||||
c.JSON(200, ErrorResponse(subErr))
|
||||
return
|
||||
}
|
||||
|
||||
res := subService.Update(c, CurrentUser(c))
|
||||
c.JSON(200, res)
|
||||
|
||||
} else {
|
||||
c.JSON(200, ErrorResponse(err))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -131,3 +131,14 @@ func PayJSCallback(c *gin.Context) {
|
|||
payNotify.SendResponseMsg()
|
||||
|
||||
}
|
||||
|
||||
// QQCallback QQ互联回调
|
||||
func QQCallback(c *gin.Context) {
|
||||
var service vas.QQCallbackService
|
||||
if err := c.ShouldBindJSON(&service); err == nil {
|
||||
res := service.Callback(c, CurrentUser(c))
|
||||
c.JSON(200, res)
|
||||
} else {
|
||||
c.JSON(200, ErrorResponse(err))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -112,6 +112,11 @@ func InitMasterRouter() *gin.Engine {
|
|||
middleware.HashID(hashid.UserID),
|
||||
controllers.GetUserShare,
|
||||
)
|
||||
// 获取用户头像
|
||||
user.GET("avatar/:id/:size",
|
||||
middleware.HashID(hashid.UserID),
|
||||
controllers.GetUserAvatar,
|
||||
)
|
||||
}
|
||||
|
||||
// 需要携带签名验证的
|
||||
|
@ -132,6 +137,11 @@ func InitMasterRouter() *gin.Engine {
|
|||
// 回调接口
|
||||
callback := v3.Group("callback")
|
||||
{
|
||||
// QQ互联回调
|
||||
callback.POST(
|
||||
"qq",
|
||||
controllers.QQCallback,
|
||||
)
|
||||
// PAYJS回调
|
||||
callback.POST(
|
||||
"payjs",
|
||||
|
@ -266,6 +276,14 @@ func InitMasterRouter() *gin.Engine {
|
|||
setting.GET("policies", controllers.UserAvailablePolicies)
|
||||
// 任务队列
|
||||
setting.GET("tasks", controllers.UserTasks)
|
||||
// 获取当前用户设定
|
||||
setting.GET("", controllers.UserSetting)
|
||||
// 从文件上传头像
|
||||
setting.POST("avatar", controllers.UploadAvatar)
|
||||
// 设定为Gravatar头像
|
||||
setting.PUT("avatar", controllers.UseGravatar)
|
||||
// 更改用户设定
|
||||
setting.PATCH(":option", controllers.UpdateOption)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,18 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
model "github.com/HFO4/cloudreve/models"
|
||||
"github.com/HFO4/cloudreve/pkg/qq"
|
||||
"github.com/HFO4/cloudreve/pkg/serializer"
|
||||
"github.com/HFO4/cloudreve/pkg/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SettingService 通用设置服务
|
||||
|
@ -15,6 +24,143 @@ type SettingListService struct {
|
|||
Page int `form:"page" binding:"required,min=1"`
|
||||
}
|
||||
|
||||
// AvatarService 头像服务
|
||||
type AvatarService struct {
|
||||
Size string `uri:"size" binding:"required,eq=l|eq=m|eq=s"`
|
||||
}
|
||||
|
||||
// SettingUpdateService 设定更改服务
|
||||
type SettingUpdateService struct {
|
||||
Option string `uri:"option" binding:"required,eq=nick|eq=theme|eq=homepage|eq=vip|eq=qq"`
|
||||
}
|
||||
|
||||
// OptionsChangeHandler 属性更改接口
|
||||
type OptionsChangeHandler interface {
|
||||
Update(*gin.Context, *model.User) serializer.Response
|
||||
}
|
||||
|
||||
// ChangerNick 昵称更改服务
|
||||
type ChangerNick struct {
|
||||
Nick string `json:"nick" binding:"required,min=1,max=255"`
|
||||
}
|
||||
|
||||
// VIPUnsubscribe 用户组解约服务
|
||||
type VIPUnsubscribe struct {
|
||||
}
|
||||
|
||||
// QQBind QQ互联服务
|
||||
type QQBind struct {
|
||||
}
|
||||
|
||||
// Update 绑定或解绑QQ
|
||||
func (service *QQBind) Update(c *gin.Context, user *model.User) serializer.Response {
|
||||
// 解除绑定
|
||||
if user.OpenID != "" {
|
||||
if err := user.Update(map[string]interface{}{"open_id": ""}); err != nil {
|
||||
return serializer.DBErr("接触绑定失败", err)
|
||||
}
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
// 新建绑定
|
||||
res, err := qq.NewLoginRequest()
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeNotSet, "无法使用QQ登录", err)
|
||||
}
|
||||
|
||||
// 设定QQ登录会话Secret
|
||||
util.SetSession(c, map[string]interface{}{"qq_login_secret": res.SecretKey})
|
||||
|
||||
return serializer.Response{
|
||||
Data: res.URL,
|
||||
}
|
||||
}
|
||||
|
||||
// Update 用户组解约
|
||||
func (service *VIPUnsubscribe) Update(c *gin.Context, user *model.User) serializer.Response {
|
||||
if user.GroupExpires != nil {
|
||||
timeNow := time.Now()
|
||||
if time.Now().Before(*user.GroupExpires) {
|
||||
if err := user.Update(map[string]interface{}{"group_expires": &timeNow}); err != nil {
|
||||
return serializer.DBErr("解约失败", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
// Update 更改昵称
|
||||
func (service *ChangerNick) Update(c *gin.Context, user *model.User) serializer.Response {
|
||||
if err := user.Update(map[string]interface{}{"nick": service.Nick}); err != nil {
|
||||
return serializer.DBErr("无法更新昵称", err)
|
||||
}
|
||||
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
// Get 获取用户头像
|
||||
func (service *AvatarService) Get(c *gin.Context) serializer.Response {
|
||||
// 查找目标用户
|
||||
uid, _ := c.Get("object_id")
|
||||
user, err := model.GetActiveUserByID(uid.(uint))
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeNotFound, "用户不存在", err)
|
||||
}
|
||||
|
||||
// 未设定头像时,返回404错误
|
||||
if user.Avatar == "" {
|
||||
c.Status(404)
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
// 获取头像设置
|
||||
sizes := map[string]string{
|
||||
"s": model.GetSettingByName("avatar_size_s"),
|
||||
"m": model.GetSettingByName("avatar_size_m"),
|
||||
"l": model.GetSettingByName("avatar_size_l"),
|
||||
}
|
||||
|
||||
// Gravatar 头像重定向
|
||||
if user.Avatar == "gravatar" {
|
||||
server := model.GetSettingByName("gravatar_server")
|
||||
gravatarRoot, err := url.Parse(server)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeInternalSetting, "无法解析 Gravatar 服务器地址", err)
|
||||
}
|
||||
|
||||
has := md5.Sum([]byte(user.Email))
|
||||
avatar, _ := url.Parse(fmt.Sprintf("/avatar/%x?d=mm&s=%s", has, sizes[service.Size]))
|
||||
|
||||
return serializer.Response{
|
||||
Code: -301,
|
||||
Data: gravatarRoot.ResolveReference(avatar).String(),
|
||||
}
|
||||
}
|
||||
|
||||
// 本地文件头像
|
||||
if user.Avatar == "file" {
|
||||
avatarRoot := model.GetSettingByName("avatar_path")
|
||||
sizeToInt := map[string]string{
|
||||
"s": "0",
|
||||
"m": "1",
|
||||
"l": "2",
|
||||
}
|
||||
|
||||
avatar, err := os.Open(filepath.Join(avatarRoot, fmt.Sprintf("avatar_%d_%s.png", user.ID, sizeToInt[service.Size])))
|
||||
if err != nil {
|
||||
c.Status(404)
|
||||
return serializer.Response{}
|
||||
}
|
||||
defer avatar.Close()
|
||||
|
||||
http.ServeContent(c.Writer, c.Request, "avatar.png", user.UpdatedAt, avatar)
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
c.Status(404)
|
||||
return serializer.Response{}
|
||||
}
|
||||
|
||||
// ListTasks 列出任务
|
||||
func (service *SettingListService) ListTasks(c *gin.Context, user *model.User) serializer.Response {
|
||||
tasks, total := model.ListTasks(user.ID, service.Page, 10, "updated_at desc")
|
||||
|
@ -36,3 +182,30 @@ func (service *SettingService) Policy(c *gin.Context, user *model.User) serializ
|
|||
|
||||
return serializer.BuildPolicySettingRes(available, ¤t)
|
||||
}
|
||||
|
||||
// Settings 获取用户设定
|
||||
func (service *SettingService) Settings(c *gin.Context, user *model.User) serializer.Response {
|
||||
// 取得存储策略设定
|
||||
policy := service.Policy(c, user)
|
||||
|
||||
// 用户组有效期
|
||||
var groupExpires int64
|
||||
if user.GroupExpires != nil {
|
||||
if expires := user.GroupExpires.Unix() - time.Now().Unix(); expires > 0 {
|
||||
groupExpires = user.GroupExpires.Unix()
|
||||
}
|
||||
}
|
||||
|
||||
return serializer.Response{
|
||||
Data: map[string]interface{}{
|
||||
"policy": policy.Data.(map[string]interface{}),
|
||||
"uid": user.ID,
|
||||
"qq": user.OpenID != "",
|
||||
"homepage": !user.OptionsSerialized.ProfileOff,
|
||||
"two_factor": user.TwoFactor != "",
|
||||
"prefer_theme": user.OptionsSerialized.PreferredTheme,
|
||||
"themes": model.GetSettingByNames("themes"),
|
||||
"group_expires": groupExpires,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
43
service/vas/qq.go
Normal file
43
service/vas/qq.go
Normal file
|
@ -0,0 +1,43 @@
|
|||
package vas
|
||||
|
||||
import (
|
||||
model "github.com/HFO4/cloudreve/models"
|
||||
"github.com/HFO4/cloudreve/pkg/qq"
|
||||
"github.com/HFO4/cloudreve/pkg/serializer"
|
||||
"github.com/HFO4/cloudreve/pkg/util"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// QQCallbackService QQ互联回调处理服务
|
||||
type QQCallbackService struct {
|
||||
Code string `json:"code" binding:"required"`
|
||||
State string `json:"state" binding:"required"`
|
||||
}
|
||||
|
||||
// Callback 处理QQ互联回调
|
||||
func (service *QQCallbackService) Callback(c *gin.Context, user *model.User) serializer.Response {
|
||||
state := util.GetSession(c, "qq_login_secret")
|
||||
if stateStr, ok := state.(string); !ok || stateStr != service.State {
|
||||
return serializer.Err(serializer.CodeSignExpired, "请求过期,请重试", nil)
|
||||
}
|
||||
|
||||
credential, err := qq.Callback(service.Code)
|
||||
if err != nil {
|
||||
return serializer.Err(serializer.CodeNotSet, "无法获取登录状态", err)
|
||||
}
|
||||
|
||||
// 如果已登录,则绑定已有用户
|
||||
if user != nil {
|
||||
if user.OpenID != "" {
|
||||
return serializer.Err(serializer.CodeCallbackError, "您已绑定了QQ账号,请先解除绑定", nil)
|
||||
}
|
||||
if err := user.Update(map[string]interface{}{"open_id": credential.OpenID}); err != nil {
|
||||
return serializer.DBErr("绑定失败", err)
|
||||
}
|
||||
return serializer.Response{
|
||||
Data: "/setting",
|
||||
}
|
||||
}
|
||||
|
||||
return serializer.Response{}
|
||||
}
|
Loading…
Add table
Reference in a new issue