From d86ddd0e9d3d735dc4c68e267d8c9cda75530b4a Mon Sep 17 00:00:00 2001 From: zhenorzz Date: Fri, 12 Jan 2024 15:35:23 +0800 Subject: [PATCH] update password when first login & add password period --- cmd/server/api/user/handler.go | 48 +++++++-- config/toml.go | 1 + database/1.16.2.sql | 2 + database/goploy.sql | 1 + goploy.example.toml | 2 + internal/cache/memory/user.go | 29 ++---- internal/cache/user.go | 11 +- internal/model/user.go | 31 +++--- internal/server/response/json.go | 1 + internal/server/router.go | 3 +- web/src/api/user.ts | 1 + web/src/lang/en.json | 1 + web/src/lang/zh-cn.json | 1 + web/src/store/modules/user/index.ts | 3 +- web/src/views/login/index.vue | 156 ++++++++++++++++++++++------ web/src/views/user/profile.vue | 2 - 16 files changed, 210 insertions(+), 83 deletions(-) create mode 100644 database/1.16.2.sql diff --git a/cmd/server/api/user/handler.go b/cmd/server/api/user/handler.go index 2fdf4b4..0f3d8b2 100644 --- a/cmd/server/api/user/handler.go +++ b/cmd/server/api/user/handler.go @@ -20,6 +20,7 @@ import ( "github.com/zhenorzz/goploy/internal/model" "github.com/zhenorzz/goploy/internal/server" "github.com/zhenorzz/goploy/internal/server/response" + "github.com/zhenorzz/goploy/internal/validator" "net/http" "strconv" "strings" @@ -56,9 +57,10 @@ func (u User) Handler() []server.Route { // @Router /user/login [post] func (User) Login(gp *server.Goploy) server.Response { type ReqData struct { - Account string `json:"account" validate:"required,min=1,max=25"` - Password string `json:"password" validate:"required,password"` - CaptchaKey string `json:"captchaKey" validate:"omitempty"` + Account string `json:"account" validate:"required,min=1,max=25"` + Password string `json:"password" validate:"required,password"` + NewPassword string `json:"newPassword"` + CaptchaKey string `json:"captchaKey" validate:"omitempty"` } var reqData ReqData if err := gp.Decode(&reqData); err != nil { @@ -138,23 +140,23 @@ func (User) Login(gp *server.Goploy) server.Response { } else { if userData.ID == 0 { - return response.JSON{Code: response.Error, Message: "We couldn't verify your identity. Please confirm if your username and password are correct."} + return response.JSON{Code: response.Error, Message: "We couldn't verify your identity. Please confirm if your username and password are correct"} } if err := userData.Validate(reqData.Password); err != nil { - errorTimes := userCache.IncErrorTimes(reqData.Account, cache.UserCacheExpireTime, cache.UserCacheShowCaptchaTime) // error times over 5 times, then lock the account 15 minutes - if errorTimes >= cache.UserCacheMaxErrorTimes { + if userCache.IncrErrorTimes(reqData.Account, cache.UserCacheExpireTime) >= cache.UserCacheMaxErrorTimes { userCache.LockAccount(reqData.Account, cache.UserCacheLockTime) } return response.JSON{Code: response.Deny, Message: err.Error()} } } - userCache.DeleteShowCaptcha(reqData.Account) + userCache.DeleteErrorTimes(reqData.Account) if userData.State == model.Disable { return response.JSON{Code: response.AccountDisabled, Message: "Account is disabled"} } + namespaceList, err := model.Namespace{UserID: userData.ID}.GetAllByUserID() if err != nil { return response.JSON{Code: response.Error, Message: err.Error()} @@ -162,6 +164,29 @@ func (User) Login(gp *server.Goploy) server.Response { return response.JSON{Code: response.Error, Message: "No space assigned, please contact the administrator"} } + if reqData.NewPassword != "" { + if err := validator.Validate.Var(reqData.NewPassword, "password"); err != nil { + return response.JSON{Code: response.PasswordExpired, Message: err.Error()} + } + if reqData.Password == reqData.NewPassword { + return response.JSON{Code: response.PasswordExpired, Message: "The password cannot be the same as the previous one"} + } + if err := (model.User{ID: userData.ID, Password: reqData.NewPassword, PasswordUpdateTime: sql.NullString{ + String: time.Now().Format("20060102150405"), + Valid: true, + }}).UpdatePassword(); err != nil { + return response.JSON{Code: response.Error, Message: err.Error()} + } + } else if !userData.PasswordUpdateTime.Valid { + return response.JSON{Code: response.PasswordExpired, Message: "You need to change your password upon first login"} + } else if config.Toml.APP.PasswordPeriod > 0 { + passwordUpdateTime, _ := time.Parse(time.DateTime, userData.PasswordUpdateTime.String) + passwordUpdateTime = passwordUpdateTime.AddDate(0, 0, config.Toml.APP.PasswordPeriod) + if passwordUpdateTime.Before(time.Now()) { + return response.JSON{Code: response.PasswordExpired, Message: "Password expired, please change"} + } + } + token, err := userData.CreateToken() if err != nil { return response.JSON{Code: response.Error, Message: err.Error()} @@ -482,7 +507,14 @@ func (User) ChangePassword(gp *server.Goploy) server.Response { return response.JSON{Code: response.Error, Message: err.Error()} } - if err := (model.User{ID: gp.UserInfo.ID, Password: reqData.NewPassword}).UpdatePassword(); err != nil { + if reqData.OldPassword == reqData.NewPassword { + return response.JSON{Code: response.Error, Message: "The password cannot be the same as the previous one."} + } + + if err := (model.User{ID: gp.UserInfo.ID, Password: reqData.NewPassword, PasswordUpdateTime: sql.NullString{ + String: time.Now().Format("20060102150405"), + Valid: true, + }}).UpdatePassword(); err != nil { return response.JSON{Code: response.Error, Message: err.Error()} } return response.JSON{} diff --git a/config/toml.go b/config/toml.go index 923ea4b..e568d08 100644 --- a/config/toml.go +++ b/config/toml.go @@ -37,6 +37,7 @@ type APPConfig struct { DeployLimit int32 `toml:"deployLimit"` ShutdownTimeout time.Duration `toml:"shutdownTimeout"` RepositoryPath string `toml:"repositoryPath"` + PasswordPeriod int `toml:"passwordPeriod"` } type CORSConfig struct { diff --git a/database/1.16.2.sql b/database/1.16.2.sql new file mode 100644 index 0000000..88dd03d --- /dev/null +++ b/database/1.16.2.sql @@ -0,0 +1,2 @@ +alter table user + add password_update_time datetime null after password; \ No newline at end of file diff --git a/database/goploy.sql b/database/goploy.sql index edb45be..3dfb89b 100644 --- a/database/goploy.sql +++ b/database/goploy.sql @@ -247,6 +247,7 @@ CREATE TABLE IF NOT EXISTS `user` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `account` varchar(30) NOT NULL DEFAULT '', `password` varchar(60) NOT NULL DEFAULT '', + `password_update_time` datetime DEFAULT NULL, `name` varchar(30) NOT NULL DEFAULT '', `contact` varchar(255) NOT NULL DEFAULT '', `state` tinyint(1) NOT NULL DEFAULT '1' COMMENT '0.disable 1.enable', diff --git a/goploy.example.toml b/goploy.example.toml index f37ac8f..b23f32d 100644 --- a/goploy.example.toml +++ b/goploy.example.toml @@ -3,6 +3,8 @@ env = 'production' deployLimit = 8 shutdownTimeout = 10 repositoryPath = '' +# How many days before the password needs to be changed? +passwordPeriod = 0 [cors] enabled = false diff --git a/internal/cache/memory/user.go b/internal/cache/memory/user.go index fab7420..45adfd4 100644 --- a/internal/cache/memory/user.go +++ b/internal/cache/memory/user.go @@ -6,9 +6,8 @@ import ( ) const ( - UserCacheKey = "login_error_times_" - UserCacheLockKey = "login_lock_" - UserCacheShowCaptchaKey = "login_show_captcha_" + UserCacheKey = "login_error_times_" + UserCacheLockKey = "login_lock_" ) type UserCache struct { @@ -18,6 +17,7 @@ type UserCache struct { type user struct { times int + data any expireIn time.Time } @@ -25,7 +25,7 @@ var userCache = &UserCache{ data: make(map[string]user), } -func (uc *UserCache) IncErrorTimes(account string, expireTime time.Duration, showCaptchaTime time.Duration) int { +func (uc *UserCache) IncrErrorTimes(account string, expireTime time.Duration) int { uc.Lock() defer uc.Unlock() @@ -47,16 +47,6 @@ func (uc *UserCache) IncErrorTimes(account string, expireTime time.Duration, sho delete(uc.data, cacheKey) }) - // show captcha - showCaptchaKey := getShowCaptchaKey(account) - uc.data[showCaptchaKey] = user{ - times: 1, - expireIn: time.Now().Add(showCaptchaTime), - } - time.AfterFunc(showCaptchaTime, func() { - delete(uc.data, showCaptchaKey) - }) - return times } @@ -97,17 +87,16 @@ func (uc *UserCache) IsShowCaptcha(account string) bool { uc.RLock() defer uc.RUnlock() - showCaptchaKey := getShowCaptchaKey(account) - v, ok := uc.data[showCaptchaKey] + v, ok := uc.data[getCacheKey(account)] return ok && !v.expireIn.IsZero() && v.expireIn.After(time.Now()) && v.times > 0 } -func (uc *UserCache) DeleteShowCaptcha(account string) { +func (uc *UserCache) DeleteErrorTimes(account string) { uc.Lock() defer uc.Unlock() - delete(uc.data, getShowCaptchaKey(account)) + delete(uc.data, getCacheKey(account)) } func getCacheKey(account string) string { @@ -118,10 +107,6 @@ func getLockKey(account string) string { return UserCacheLockKey + account } -func getShowCaptchaKey(account string) string { - return UserCacheShowCaptchaKey + account -} - func GetUserCache() *UserCache { return userCache } diff --git a/internal/cache/user.go b/internal/cache/user.go index 2d6f88f..f723f33 100644 --- a/internal/cache/user.go +++ b/internal/cache/user.go @@ -3,16 +3,15 @@ package cache import "time" type User interface { - IncErrorTimes(account string, expireTime time.Duration, showCaptchaTime time.Duration) int + IncrErrorTimes(account string, expireTime time.Duration) int LockAccount(account string, lockTime time.Duration) IsLock(account string) bool IsShowCaptcha(account string) bool - DeleteShowCaptcha(account string) + DeleteErrorTimes(account string) } const ( - UserCacheMaxErrorTimes = 5 - UserCacheExpireTime = 5 * time.Minute - UserCacheLockTime = 15 * time.Minute - UserCacheShowCaptchaTime = 15 * time.Minute + UserCacheMaxErrorTimes = 5 + UserCacheExpireTime = 5 * time.Minute + UserCacheLockTime = 15 * time.Minute ) diff --git a/internal/model/user.go b/internal/model/user.go index 11be2b5..b8e739b 100644 --- a/internal/model/user.go +++ b/internal/model/user.go @@ -6,6 +6,7 @@ package model import ( "crypto/rand" + "database/sql" "errors" "github.com/zhenorzz/goploy/config" "time" @@ -24,17 +25,18 @@ const ( ) type User struct { - ID int64 `json:"id"` - Account string `json:"account"` - Password string `json:"password"` - Name string `json:"name"` - Contact string `json:"contact"` - SuperManager int64 `json:"superManager"` - State uint8 `json:"state"` - InsertTime string `json:"insertTime"` - UpdateTime string `json:"updateTime"` - LastLoginTime string `json:"lastLoginTime"` - ApiKey string `json:"apiKey"` + ID int64 `json:"id"` + Account string `json:"account"` + Password string `json:"password"` + PasswordUpdateTime sql.NullString `json:"passwordUpdateTime"` + Name string `json:"name"` + Contact string `json:"contact"` + SuperManager int64 `json:"superManager"` + State uint8 `json:"state"` + InsertTime string `json:"insertTime"` + UpdateTime string `json:"updateTime"` + LastLoginTime string `json:"lastLoginTime"` + ApiKey string `json:"apiKey"` } type Users []User @@ -57,12 +59,12 @@ func (u User) GetData() (User, error) { func (u User) GetDataByAccount() (User, error) { var user User err := sq. - Select("id, account, password, name, contact, super_manager, state, insert_time, update_time"). + Select("id, account, password, password_update_time, name, contact, super_manager, state, insert_time, update_time"). From(userTable). Where(sq.Eq{"account": u.Account}). RunWith(DB). QueryRow(). - Scan(&user.ID, &user.Account, &user.Password, &user.Name, &user.Contact, &user.SuperManager, &user.State, &user.InsertTime, &user.UpdateTime) + Scan(&user.ID, &user.Account, &user.Password, &user.PasswordUpdateTime, &user.Name, &user.Contact, &user.SuperManager, &user.State, &user.InsertTime, &user.UpdateTime) if err != nil { return user, err } @@ -221,7 +223,8 @@ func (u User) UpdatePassword() error { _, err = sq. Update(userTable). SetMap(sq.Eq{ - "password": string(hashedPassword), + "password": string(hashedPassword), + "password_update_time": u.PasswordUpdateTime, }). Where(sq.Eq{"id": u.ID}). RunWith(DB). diff --git a/internal/server/response/json.go b/internal/server/response/json.go index 04d140d..91598c6 100644 --- a/internal/server/response/json.go +++ b/internal/server/response/json.go @@ -31,6 +31,7 @@ const ( IllegalRequest = 10001 NamespaceInvalid = 10002 IllegalParam = 10003 + PasswordExpired = 10004 LoginExpired = 10086 ) diff --git a/internal/server/router.go b/internal/server/router.go index f88cb8c..952ef5b 100644 --- a/internal/server/router.go +++ b/internal/server/router.go @@ -6,6 +6,7 @@ package server import ( "database/sql" + "errors" "fmt" "github.com/golang-jwt/jwt" log "github.com/sirupsen/logrus" @@ -157,7 +158,7 @@ func (rt *Router) doRequest(w http.ResponseWriter, r *http.Request) (*Goploy, Re UserID: gp.UserInfo.ID, }.GetDataByUserNamespace() if err != nil { - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return gp, response.JSON{Code: response.NamespaceInvalid, Message: "No available namespace"} } else { return gp, response.JSON{Code: response.Deny, Message: err.Error()} diff --git a/web/src/api/user.ts b/web/src/api/user.ts index b8bfac5..f1a9ef0 100644 --- a/web/src/api/user.ts +++ b/web/src/api/user.ts @@ -19,6 +19,7 @@ export class Login extends Request { public param: { account: string password: string + newPassword: string captchaKey: string } public declare datagram: { diff --git a/web/src/lang/en.json b/web/src/lang/en.json index a88b157..a39ae23 100644 --- a/web/src/lang/en.json +++ b/web/src/lang/en.json @@ -1,6 +1,7 @@ { "default": "Default", "name": "Name", + "signIn": "Sign in", "tag": "Tag", "script": "Script", "owner": "Owner", diff --git a/web/src/lang/zh-cn.json b/web/src/lang/zh-cn.json index fa9e42b..db5ebdc 100644 --- a/web/src/lang/zh-cn.json +++ b/web/src/lang/zh-cn.json @@ -1,6 +1,7 @@ { "default": "默认", "name": "名称", + "signIn": "登录", "tag": "标签", "script": "脚本", "owner": "拥有者", diff --git a/web/src/store/modules/user/index.ts b/web/src/store/modules/user/index.ts index a9235c3..c0c35ac 100644 --- a/web/src/store/modules/user/index.ts +++ b/web/src/store/modules/user/index.ts @@ -31,11 +31,12 @@ const mutations: MutationTree = { const actions: ActionTree = { // user login login(_, userInfo) { - const { account, password, captchaKey } = userInfo + const { account, password, newPassword, captchaKey } = userInfo return new Promise((resolve, reject) => { new Login({ account: account.trim(), password: password, + newPassword: newPassword, captchaKey: captchaKey, }) .request() diff --git a/web/src/views/login/index.vue b/web/src/views/login/index.vue index f8c9298..0d67c84 100644 --- a/web/src/views/login/index.vue +++ b/web/src/views/login/index.vue @@ -5,7 +5,7 @@