Repository: TencentBlueKing/bk-user

Issue:
【BUG】用户在个人中心进行重置密码，页面跳转后，提示接口异常
**用文字描述你遇到的问题**

用户进行登陆后，在个人中心，进行密码重置，跳转到密码重置后，显示接口调用异常

**重现方法**

<img width="960" alt="企业微信截图_1686189293222" src="https://github.com/TencentBlueKing/bk-user/assets/49228807/f973b23f-b7f2-4d78-85e8-0f11acb4f654">
<img width="910" alt="企业微信截图_16861893591893" src="https://github.com/TencentBlueKing/bk-user/assets/49228807/cfc6e9aa-7456-4ab3-baa7-dcae74c7ba7e">


**预期行为**

无异常，且对密码，做强度校验

**版本**
- 提供用户管理的具体版本号
- 2.5.2
- 是否是企业版问题？
- 是

**如果是 SaaS 页面问题，请提供使用的操作系统和浏览器信息**
 - OS: [e.g. iOS]
 - Browser [e.g. chrome, safari]
 - Version [e.g. 22]

**额外信息**

任何你觉得有助于问题解决的内容



Discussion:
经排查接口`api/v1/web/passwords/settings/by_token/?token={}`，是提供给忘记密码进行密码重置功能进行密码强度检测使用的。从个人中心进行重置密码，无token（该token为profile_token）提供，因此调用失败。

1. token(profile_token)如何生成 
登录界面，忘记密码，进行密码重置功能（密码变更逻辑通过profile_token修改密码）
![image](https://github.com/TencentBlueKing/bk-user/assets/49228807/7bfcbf0b-ca60-46c1-a542-0f04c3b7cec0)


使用任意一种进行触发
![image](https://github.com/TencentBlueKing/bk-user/assets/49228807/03db2cc3-1585-4864-b3cd-3d4ea45940d4)

  1.1 手机短信
  发送验证码到用户手机，校验验证码通过后，获取profile_token，携带profile_token跳转到密码重置页面
  1.2 邮箱
  直接生成带有profile_token的链接发送到用户邮箱，该链接跳转到密码重置页面
![image](https://github.com/TencentBlueKing/bk-user/assets/49228807/7587fba3-e6c8-47a6-9e98-1dfba1fa51ca)


  1.3. 通过profile_token，获取用户目录密码配置`api/v1/web/passwords/settings/by_token/?token={profile_token}`
  操作人在使用忘记密码进行密码重置功能，是非登录态，所以需要通过profile_token，获取用户目录密码配置。前端对操作人输入的密码做强度校验

2. 异常点
  用户通过个人中心（登录态），点击重置密码, 跳转进行密码重置。密码变更逻辑是：通过校验旧密码[old_password]，变更为[new_password]）。
  并不会生成profile_token，导致 获取用户目录密码配置`api/v1/web/passwords/settings/by_token/?token={profile_token}`接口调用失败
![image](https://github.com/TencentBlueKing/bk-user/assets/49228807/e459d731-6810-4406-9f33-b1ade06dae07)





修复方案：
1. 前端通过解析cookie，获取用户名的domain
2. 通过domian，获取对应目录的密码设置
（因从个人中心进行密码修改，已经是登录态，后端解析请求用户，返回对应用户目录的密码设置给前端）
增加一个接口
`api/v1/web/passwords/settings/by_domain/?domain={}`
1. 与前端确认 [无登录态下忘记密码后重置密码] /reset_password 和 [登录态下修改密码]/change_password 这两个页面是否共用了部分逻辑，比如拉取密码配置(密码强度要求、加密传输要求等) api/v1/web/passwords/settings/by_token/

2. 确定共用，则调整 api/v1/web/passwords/settings/by_token/ 后台API，支持token 和 登录态两种方式获取密码相关配置
- 优先Token，无Token则使用登录的用户（无Token需要与前端确认好，而不是现在的传递undefined）
- 登录态如何获取可以参考 passwords/modify/ 这个后台API的处理
2.5.4

Resolving patch:
diff --git a/src/api/bkuser_core/api/web/password/serializers.py b/src/api/bkuser_core/api/web/password/serializers.py
--- a/src/api/bkuser_core/api/web/password/serializers.py
+++ b/src/api/bkuser_core/api/web/password/serializers.py
@@ -53,6 +53,14 @@ class PasswordModifyInputSLZ(serializers.Serializer):
     old_password = Base64OrPlainField(required=True, max_length=254)
     new_password = Base64OrPlainField(required=True, max_length=254)
 
+    def validate(self, attrs):
+        # 根据context，获取操作用户的目录id
+        category_id = self.context["category_id"]
+        # 解析密码是否加密
+        attrs["old_password"] = get_raw_password(category_id, attrs["old_password"])
+        attrs["new_password"] = get_raw_password(category_id, attrs["new_password"])
+        return attrs
+
 
 class PasswordListSettingsByTokenInputSLZ(serializers.Serializer):
-    token = serializers.CharField(required=True, max_length=254)
+    token = serializers.CharField(required=False, max_length=254)
diff --git a/src/api/bkuser_core/api/web/password/views.py b/src/api/bkuser_core/api/web/password/views.py
--- a/src/api/bkuser_core/api/web/password/views.py
+++ b/src/api/bkuser_core/api/web/password/views.py
@@ -129,22 +129,21 @@ def post(self, request, *args, **kwargs):
 
 class PasswordModifyApi(generics.CreateAPIView):
     def post(self, request, *args, **kwargs):
-        slz = PasswordModifyInputSLZ(data=request.data)
-        slz.is_valid(raise_exception=True)
-        data = slz.validated_data
-        old_password = data["old_password"]
-        new_password = data["new_password"]
-
         # SaaS 修改密码页面需要登录态, 登录用户即operator
         username = get_operator(request)
-
         # 注意, 这里的username是带域的
         username, domain = parse_username_domain(username)
         if not domain:
             domain = ProfileCategory.objects.get(default=True).domain
-
         instance = Profile.objects.get(username=username, domain=domain)
 
+        # 通过context传入操作用户的目录id
+        slz = PasswordModifyInputSLZ(data=request.data, context={"category_id": instance.category_id})
+        slz.is_valid(raise_exception=True)
+        data = slz.validated_data
+        old_password = data["old_password"]
+        new_password = data["new_password"]
+
         # 1. check old match
         if not instance.check_password(old_password):
             raise error_codes.PASSWORD_ERROR
@@ -178,10 +177,22 @@ def get(self, request, *args, **kwargs):
         slz.is_valid(raise_exception=True)
 
         data = slz.validated_data
-        token = data["token"]
-
-        token_holder = get_token_handler(token)
-        profile = token_holder.profile
+        token = data.get("token")
+
+        if token:
+            # 根据profile_token 换取目录密码设置
+            token_holder = get_token_handler(token)
+            profile = token_holder.profile
+        else:
+            # 兼容登录态的change_password页面获取目录密码配置
+            username = get_operator(request)
+            username, domain = parse_username_domain(username)
+            if not domain:
+                domain = ProfileCategory.objects.get(default=True).domain
+            try:
+                profile = Profile.objects.get(username=username, domain=domain)
+            except Profile.DoesNotExist:
+                raise error_codes.USER_DOES_NOT_EXIST
 
         category = get_category(profile.category_id)
         namespace = SettingsEnableNamespaces.PASSWORD.value


Test patch: