From a072cfe1cfb345de997003bc5897980b02db495e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=BA=90=E6=96=87=E9=9B=A8?= <41315874+fumiama@users.noreply.github.com> Date: Sat, 18 Mar 2023 00:25:19 +0800 Subject: [PATCH] finish upload avatar --- backend/{file/provider.go => api/file.go} | 15 +-- backend/api/login.go | 5 +- backend/api/main.go | 17 +++- backend/api/upload.go | 98 +++++++++++++++++++ backend/api/user.go | 34 ++++++- backend/global/base.go | 2 +- backend/global/user.go | 49 ++++++++-- backend/utils/method.go | 2 +- frontend/vben/mock/sys/user.ts | 8 +- frontend/vben/src/api/sys/model/userModel.ts | 6 ++ .../components/Cropper/src/CopperModal.vue | 2 +- .../src/views/page/settings/BaseSetting.vue | 37 ++++--- .../src/views/page/settings/SecureSetting.vue | 20 +++- frontend/vben/src/views/page/settings/data.ts | 29 +----- frontend/vben/types/store.d.ts | 3 + go.mod | 1 + go.sum | 2 + main.go | 10 +- 18 files changed, 268 insertions(+), 72 deletions(-) rename backend/{file/provider.go => api/file.go} (58%) create mode 100644 backend/api/upload.go diff --git a/backend/file/provider.go b/backend/api/file.go similarity index 58% rename from backend/file/provider.go rename to backend/api/file.go index d7c51fc..4b58d3c 100644 --- a/backend/file/provider.go +++ b/backend/api/file.go @@ -1,26 +1,27 @@ -package file +package api import ( "net/http" - "strings" "github.com/fumiama/paper-manager/backend/global" "github.com/fumiama/paper-manager/backend/utils" "github.com/sirupsen/logrus" ) -// Handler serves contents in global.FileFolder -func Handler(w http.ResponseWriter, r *http.Request) { +// FileHandler serves contents in global.FileFolder +func FileHandler(w http.ResponseWriter, r *http.Request) { if !utils.IsMethod("GET", w, r) { return } - i := strings.LastIndex(r.URL.Path, "/") - fn := r.URL.Path[i+1:] + if r.URL.Path[0] != '/' { + r.URL.Path = "/" + r.URL.Path + } + fn := r.URL.Path[6:] if fn == "" { http.Error(w, "400 Bad Request: empty path", http.StatusBadRequest) return } name := global.FileFolder + fn - logrus.Infoln("[file.Handler]\t serve", name) + logrus.Infoln("[file.FileHandler] serve", name) http.ServeFile(w, r, name) } diff --git a/backend/api/login.go b/backend/api/login.go index 60cb6a6..5a74aec 100644 --- a/backend/api/login.go +++ b/backend/api/login.go @@ -52,9 +52,12 @@ func getLoginSalt(username string) (*saltinfo, error) { return nil, errNoSuchUser } s, _ := loginstatus.Load(username) - if s != loginStatusNo { + if s == loginStatusYes { return nil, errInvalidLoginStatus } + if s >= loginStatusFailLast { + return nil, errTooManyFailedLogins + } salt := loginsalts.Get(username) if salt.count != nil { if atomic.AddUintptr(salt.count, 1) >= maxSaltCount { diff --git a/backend/api/main.go b/backend/api/main.go index d63ad57..3333c1b 100644 --- a/backend/api/main.go +++ b/backend/api/main.go @@ -9,13 +9,16 @@ import ( // Handler serves all backend /api call func Handler(w http.ResponseWriter, r *http.Request) { + if r.URL.Path[0] != '/' { + r.URL.Path = "/" + r.URL.Path + } if r.URL.Path == "/api/getLoginSalt" { if !utils.IsMethod("GET", w, r) { return } username := r.URL.Query().Get("username") if username == "" { - http.Error(w, "400 Bad Request: empty username", http.StatusBadRequest) + writeresult(w, codeError, nil, "empty username", typeError) return } salt, err := getLoginSalt(username) @@ -58,6 +61,18 @@ func Handler(w http.ResponseWriter, r *http.Request) { writeresult(w, codeSuccess, r, messageOk, typeSuccess) return } + if r.URL.Path == "/api/logout" { + if !utils.IsMethod("GET", w, r) { + return + } + err := logout(r.Header.Get("Authorization")) + if err != nil { + writeresult(w, codeError, nil, err.Error(), typeError) + return + } + writeresult(w, codeSuccess, nil, messageOk, typeSuccess) + return + } if !utils.IsMethod("GET", w, r) { return } diff --git a/backend/api/upload.go b/backend/api/upload.go new file mode 100644 index 0000000..f7df0ec --- /dev/null +++ b/backend/api/upload.go @@ -0,0 +1,98 @@ +package api + +import ( + "bytes" + "io" + "net/http" + "os" + "strconv" + "strings" + + "github.com/fumiama/imgsz" + "github.com/sirupsen/logrus" + + "github.com/fumiama/paper-manager/backend/global" + "github.com/fumiama/paper-manager/backend/utils" +) + +type upload struct { + Message string `json:"message"` + Code int `json:"code"` + URL string `json:"url"` +} + +// UploadHandler receives uploaded files +func UploadHandler(w http.ResponseWriter, r *http.Request) { + if !utils.IsMethod("POST", w, r) { + return + } + token := r.Header.Get("Authorization") + user := usertokens.Get(token) + if user == nil { + writeresult(w, codeError, nil, errInvalidToken.Error(), typeError) + return + } + ff, h, err := r.FormFile("avatar") + if err == nil { + defer ff.Close() + ct := h.Header.Get("Content-Type") + un := h.Filename + logrus.Infoln("[file.UploadHandler] receive avatar, username:", un, "& mime:", ct) + if !strings.HasPrefix(ct, "image/") { + writeresult(w, codeError, nil, "invalid mimetype", typeError) + return + } + if un != user.Name { + writeresult(w, codeError, nil, "username mismatch", typeError) + return + } + err = os.MkdirAll(global.FileFolder+strconv.Itoa(*user.ID), 0755) + if err != nil { + writeresult(w, codeError, nil, err.Error(), typeError) + return + } + buf := bytes.NewBuffer(make([]byte, 0, h.Size)) + _, err := io.Copy(buf, ff) + if err != nil { + writeresult(w, codeError, nil, err.Error(), typeError) + return + } + data := buf.Bytes() + _, format, err := imgsz.DecodeSize(buf) + if err != nil { + writeresult(w, codeError, nil, err.Error(), typeError) + return + } + userf := global.FileFolder + strconv.Itoa(*user.ID) + "/" + err = os.MkdirAll(userf, 0755) + if err != nil { + writeresult(w, codeError, nil, err.Error(), typeError) + return + } + avf := userf + "avatar." + format + err = os.WriteFile(avf, data, 0644) + if err != nil { + writeresult(w, codeError, nil, err.Error(), typeError) + return + } + err = global.UserDB.UpdateUserInfo(*user.ID, "", avf[6:], "") + if err != nil { + writeresult(w, codeError, nil, err.Error(), typeError) + return + } + writeresult(w, codeSuccess, &upload{ + Message: messageOk, + Code: codeSuccess, + URL: avf[6:], + }, messageOk, typeSuccess) + user.Avtr = avf[6:] + usertokens.Set(token, user) + logrus.Infoln("[file.UploadHandler] save avatar to", avf[6:]) + return + } + if err != http.ErrMissingFile { + writeresult(w, codeError, nil, err.Error(), typeError) + return + } + +} diff --git a/backend/api/user.go b/backend/api/user.go index 29edbe1..1b798e9 100644 --- a/backend/api/user.go +++ b/backend/api/user.go @@ -2,10 +2,16 @@ package api import ( "errors" + "strings" + "time" "github.com/fumiama/paper-manager/backend/global" ) +const ( + chineseDateLayout = "2006年01月02日15时04分05秒" +) + var ( errInvalidToken = errors.New("invalid token") ) @@ -18,6 +24,9 @@ type getUserInfoResult struct { Desc string `json:"desc"` HomePath string `json:"homePath"` Roles []role `json:"roles"` + Date string `json:"date"` + Last string `json:"last"` + Contact string `json:"contact"` } func getUserInfo(token string) (*getUserInfoResult, error) { @@ -25,6 +34,16 @@ func getUserInfo(token string) (*getUserInfoResult, error) { if user == nil { return nil, errInvalidToken } + cont := user.Cont + if len(cont) > 7 { + sb := strings.Builder{} + sb.WriteString(cont[:3]) + for i := 0; i < len(cont)-7; i++ { + sb.WriteByte('*') + } + sb.WriteString(cont[len(cont)-4:]) + cont = sb.String() + } return &getUserInfoResult{ UserID: *user.ID, Username: user.Name, @@ -37,6 +56,19 @@ func getUserInfo(token string) (*getUserInfoResult, error) { } return "/dashboard/workbench" }(), - Roles: []role{{RoleName: user.Role.Nick(), Value: user.Role.String()}}, + Roles: []role{{RoleName: user.Role.Nick(), Value: user.Role.String()}}, + Date: time.Unix(user.Date, 0).Format(chineseDateLayout), + Last: time.Unix(user.Last, 0).Format(chineseDateLayout), + Contact: cont, }, nil } + +func logout(token string) error { + user := usertokens.Get(token) + if user == nil { + return errInvalidToken + } + loginstatus.Delete(user.Name) + usertokens.Delete(token) + return nil +} diff --git a/backend/global/base.go b/backend/global/base.go index 8ca9d22..df6fba8 100644 --- a/backend/global/base.go +++ b/backend/global/base.go @@ -22,7 +22,7 @@ func init() { func initdir(folder string) { err := os.MkdirAll(folder, 0755) if err != nil { - logrus.Errorln("[os.MkdirAll]\t", err) + logrus.Errorln("[os.MkdirAll]", err) os.Exit(line()) } } diff --git a/backend/global/user.go b/backend/global/user.go index be4e66f..5a64ac7 100644 --- a/backend/global/user.go +++ b/backend/global/user.go @@ -56,6 +56,7 @@ var ( ErrEmptyUserID = errors.New("empty user ID") ErrEmptyContect = errors.New("empty contact") ErrUsernameExists = errors.New("username exists") + ErrInvalidName = errors.New("invalid name") ) func init() { @@ -88,7 +89,7 @@ func init() { Cont: "028-61830156", Desc: "日は山の端にかかりぬ", }, "系统") - logrus.Warn("[global.user] 初次启动, 创建初始账户 fumiama 密码 123456") + logrus.Warn("[user] 初次启动, 创建初始账户 fumiama 密码 123456") } } @@ -121,6 +122,11 @@ func (u *UserDatabase) AddUser(user *User, opname string) error { if u.IsNameExists(user.Name) { return ErrUsernameExists } + for _, c := range user.Name { + if !(c >= '0' && c <= '9') && !(c >= 'A' && c <= 'Z') && !(c >= 'a' && c <= 'z') { + return ErrInvalidName + } + } user.Date = time.Now().Unix() user.Last = user.Date _ = u.notifyUserAdded(opname, user.Name) @@ -146,7 +152,7 @@ func (u *UserDatabase) UpdateUserInfo(id int, nick, avtr, desc string) error { } u.mu.Lock() defer u.mu.Unlock() - return u.db.Insert(UserTableUser, user) + return u.db.Insert(UserTableUser, &user) } // UpdateUserRole ... @@ -161,7 +167,7 @@ func (u *UserDatabase) UpdateUserRole(id int, nr UserRole) error { user.Role = nr u.mu.Lock() defer u.mu.Unlock() - return u.db.Insert(UserTableUser, user) + return u.db.Insert(UserTableUser, &user) } // UpdateUserPassword ... @@ -178,7 +184,7 @@ func (u *UserDatabase) UpdateUserPassword(id int, npwd string) error { _ = u.notifyPasswordChange(user.Name, npwd) u.mu.Lock() defer u.mu.Unlock() - return u.db.Insert(UserTableUser, user) + return u.db.Insert(UserTableUser, &user) } // UpdateUserContact ... @@ -194,12 +200,18 @@ func (u *UserDatabase) UpdateUserContact(id int, ncont string) error { _ = u.notifyContactChange(user.Name, ncont) u.mu.Lock() defer u.mu.Unlock() - return u.db.Insert(UserTableUser, user) + return u.db.Insert(UserTableUser, &user) } // GetUserByName avoids sql injection by removing ; ' " = func (u *UserDatabase) GetUserByName(username string) (user User, err error) { username = strings.NewReplacer(";", "", "'", "", `"`, "", "=", "").Replace(username) + for _, c := range username { + if !(c >= '0' && c <= '9') && !(c >= 'A' && c <= 'Z') && !(c >= 'a' && c <= 'z') { + err = ErrInvalidName + return + } + } u.mu.RLock() err = u.db.Find(UserTableUser, &user, "WHERE Name='"+username+"'") u.mu.RUnlock() @@ -209,6 +221,11 @@ func (u *UserDatabase) GetUserByName(username string) (user User, err error) { // IsNameExists avoids sql injection by removing ; ' " = func (u *UserDatabase) IsNameExists(username string) bool { username = strings.NewReplacer(";", "", "'", "", `"`, "", "=", "").Replace(username) + for _, c := range username { + if !(c >= '0' && c <= '9') && !(c >= 'A' && c <= 'Z') && !(c >= 'a' && c <= 'z') { + return false + } + } u.mu.RLock() defer u.mu.RUnlock() return u.db.CanFind(UserTableUser, "WHERE Name='"+username+"'") @@ -265,6 +282,21 @@ func (u *UserDatabase) GetSuperIDs() (ids []int, err error) { return } +// IsUser checks if token is valid for a user +func (user *User) IsUser() bool { + return user.Role == RoleUser || user.Role == RoleFileManager || user.Role == RoleSuper +} + +// IsFileManager checks if token is valid for a filemgr +func (user *User) IsFileManager() bool { + return user.Role == RoleFileManager || user.Role == RoleSuper +} + +// IsSuper checks if token is valid for a super +func (user *User) IsSuper() bool { + return user.Role == RoleSuper +} + // Message is shown in the workbench type Message struct { ID *int @@ -282,7 +314,7 @@ func (u *UserDatabase) SendMessage(m *Message) error { m.Date = time.Now().Unix() u.mu.Lock() defer u.mu.Unlock() - return u.db.InsertUnique(UserTableMessage, &m) + return u.db.InsertUnique(UserTableMessage, m) } // NotifyRegister will send register notification to all supers @@ -290,6 +322,11 @@ func (u *UserDatabase) NotifyRegister(name, cont, pswd string) error { if name == "" { return ErrEmptyName } + for _, c := range name { + if !(c >= '0' && c <= '9') && !(c >= 'A' && c <= 'Z') && !(c >= 'a' && c <= 'z') { + return ErrInvalidName + } + } if pswd == "" { return ErrEmptyPassword } diff --git a/backend/utils/method.go b/backend/utils/method.go index f25248a..fdf78c0 100644 --- a/backend/utils/method.go +++ b/backend/utils/method.go @@ -18,7 +18,7 @@ func IP(r *http.Request) string { // IsMethod check if the method meets the requirement // and response 405 Method Not Allowed if not matched func IsMethod(m string, w http.ResponseWriter, r *http.Request) bool { - logrus.Infoln("[utils.IsMethod]\t accept", IP(r), r.Method, r.URL) + logrus.Infoln("[utils.IsMethod] accept", IP(r), r.Method, r.URL) if r.Method != m { http.Error(w, "405 Method Not Allowed", http.StatusMethodNotAllowed) return false diff --git a/frontend/vben/mock/sys/user.ts b/frontend/vben/mock/sys/user.ts index d6b0f9b..951aea8 100644 --- a/frontend/vben/mock/sys/user.ts +++ b/frontend/vben/mock/sys/user.ts @@ -143,7 +143,7 @@ export default [ return resultSuccess(codeList) }, }, - { + /*{ url: '/api/logout', timeout: 200, method: 'get', @@ -156,13 +156,13 @@ export default [ } return resultSuccess(undefined, { message: 'Token has been destroyed' }) }, - }, - { + },*/ + /*{ url: '/api/testRetry', statusCode: 405, method: 'get', response: () => { return resultError('Error!') }, - }, + },*/ ] as MockMethod[] diff --git a/frontend/vben/src/api/sys/model/userModel.ts b/frontend/vben/src/api/sys/model/userModel.ts index 32f0096..cecb453 100644 --- a/frontend/vben/src/api/sys/model/userModel.ts +++ b/frontend/vben/src/api/sys/model/userModel.ts @@ -66,6 +66,12 @@ export interface GetUserInfoModel { avatar: string // 介绍 desc?: string + // 创建日期 + date: string + // 上次修改密码日期 + last: string + // 电话 + contact: string } export interface GetLoginSaltModel { diff --git a/frontend/vben/src/components/Cropper/src/CopperModal.vue b/frontend/vben/src/components/Cropper/src/CopperModal.vue index 9adb347..860388c 100644 --- a/frontend/vben/src/components/Cropper/src/CopperModal.vue +++ b/frontend/vben/src/components/Cropper/src/CopperModal.vue @@ -187,7 +187,7 @@ try { setModalProps({ confirmLoading: true }) const result = await uploadApi({ name: 'file', file: blob, filename }) - emit('uploadSuccess', { source: previewSource.value, data: result.url }) + emit('uploadSuccess', { source: previewSource.value, data: result.data.result.url }) closeModal() } finally { setModalProps({ confirmLoading: false }) diff --git a/frontend/vben/src/views/page/settings/BaseSetting.vue b/frontend/vben/src/views/page/settings/BaseSetting.vue index d49c6ee..818e528 100644 --- a/frontend/vben/src/views/page/settings/BaseSetting.vue +++ b/frontend/vben/src/views/page/settings/BaseSetting.vue @@ -8,8 +8,8 @@
头像