diff --git a/controllers/user.go b/controllers/user.go
index 693eb6d5..46a4343e 100644
--- a/controllers/user.go
+++ b/controllers/user.go
@@ -561,8 +561,9 @@ func (c *ApiController) SetPassword() {
targetUser.Password = newPassword
targetUser.UpdateUserPassword(organization)
targetUser.NeedUpdatePassword = false
+ targetUser.LastChangePasswordTime = util.GetCurrentTime()
- _, err = object.UpdateUser(userId, targetUser, []string{"password", "need_update_password", "password_type"}, false)
+ _, err = object.UpdateUser(userId, targetUser, []string{"password", "need_update_password", "password_type", "last_change_password_time"}, false)
if err != nil {
c.ResponseError(err.Error())
return
diff --git a/object/check.go b/object/check.go
index ac3425d5..6bf4ebd8 100644
--- a/object/check.go
+++ b/object/check.go
@@ -381,7 +381,13 @@ func CheckUserPassword(organization string, username string, password string, la
if err != nil {
return nil, err
}
+
+ err = checkPasswordExpired(user, lang)
+ if err != nil {
+ return nil, err
+ }
}
+
return user, nil
}
diff --git a/object/check_password_expired.go b/object/check_password_expired.go
new file mode 100644
index 00000000..3d5d2f98
--- /dev/null
+++ b/object/check_password_expired.go
@@ -0,0 +1,53 @@
+// Copyright 2024 The Casdoor Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package object
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/casdoor/casdoor/i18n"
+ "github.com/casdoor/casdoor/util"
+)
+
+func checkPasswordExpired(user *User, lang string) error {
+ organization, err := GetOrganizationByUser(user)
+ if err != nil {
+ return err
+ }
+ if organization == nil {
+ return fmt.Errorf(i18n.Translate(lang, "check:Organization does not exist"))
+ }
+
+ passwordExpireDays := organization.PasswordExpireDays
+ if passwordExpireDays <= 0 {
+ return nil
+ }
+
+ lastChangePasswordTime := user.LastChangePasswordTime
+ if lastChangePasswordTime == "" {
+ if user.CreatedTime == "" {
+ return fmt.Errorf(i18n.Translate(lang, "check:Your password has expired. Please reset your password by clicking \"Forgot password\""))
+ }
+ lastChangePasswordTime = user.CreatedTime
+ }
+
+ lastTime := util.String2Time(lastChangePasswordTime)
+ expireTime := lastTime.AddDate(0, 0, passwordExpireDays)
+ if time.Now().After(expireTime) {
+ return fmt.Errorf(i18n.Translate(lang, "check:Your password has expired. Please reset your password by clicking \"Forgot password\""))
+ }
+ return nil
+}
diff --git a/object/user.go b/object/user.go
index 3f8c52bf..2d44b3ce 100644
--- a/object/user.go
+++ b/object/user.go
@@ -200,8 +200,9 @@ type User struct {
Permissions []*Permission `json:"permissions"`
Groups []string `xorm:"groups varchar(1000)" json:"groups"`
- LastSigninWrongTime string `xorm:"varchar(100)" json:"lastSigninWrongTime"`
- SigninWrongTimes int `json:"signinWrongTimes"`
+ LastChangePasswordTime string `xorm:"varchar(100)" json:"lastChangePasswordTime"`
+ LastSigninWrongTime string `xorm:"varchar(100)" json:"lastSigninWrongTime"`
+ SigninWrongTimes int `json:"signinWrongTimes"`
ManagedAccounts []ManagedAccount `xorm:"managedAccounts blob" json:"managedAccounts"`
MfaAccounts []MfaAccount `xorm:"mfaAccounts blob" json:"mfaAccounts"`
@@ -690,7 +691,7 @@ func UpdateUser(id string, user *User, columns []string, isAdmin bool) (bool, er
"owner", "display_name", "avatar", "first_name", "last_name",
"location", "address", "country_code", "region", "language", "affiliation", "title", "id_card_type", "id_card", "homepage", "bio", "tag", "language", "gender", "birthday", "education", "score", "karma", "ranking", "signup_application",
"is_admin", "is_forbidden", "is_deleted", "hash", "is_default_avatar", "properties", "webauthnCredentials", "managedAccounts", "face_ids", "mfaAccounts",
- "signin_wrong_times", "last_signin_wrong_time", "groups", "access_key", "access_secret", "mfa_phone_enabled", "mfa_email_enabled",
+ "signin_wrong_times", "last_change_password_time", "last_signin_wrong_time", "groups", "access_key", "access_secret", "mfa_phone_enabled", "mfa_email_enabled",
"github", "google", "qq", "wechat", "facebook", "dingtalk", "weibo", "gitee", "linkedin", "wecom", "lark", "gitlab", "adfs",
"baidu", "alipay", "casdoor", "infoflow", "apple", "azuread", "azureadb2c", "slack", "steam", "bilibili", "okta", "douyin", "line", "amazon",
"auth0", "battlenet", "bitbucket", "box", "cloudfoundry", "dailymotion", "deezer", "digitalocean", "discord", "dropbox",
diff --git a/web/src/UserEditPage.js b/web/src/UserEditPage.js
index 47e855e3..420891c0 100644
--- a/web/src/UserEditPage.js
+++ b/web/src/UserEditPage.js
@@ -1009,6 +1009,19 @@ class UserEditPage extends React.Component {
);
+ } else if (accountItem.name === "Last change password time") {
+ return (
+
+
+ {Setting.getLabel(i18next.t("user:Last change password time"), i18next.t("user:Last change password time"))} :
+
+
+ {
+ this.updateUserField("lastChangePasswordTime", e.target.value);
+ }} />
+
+
+ );
} else if (accountItem.name === "Managed accounts") {
return (