From 4b0859f25a9106d9b7017abe06789d6681e669ae Mon Sep 17 00:00:00 2001 From: Khai Tran Date: Wed, 27 Mar 2024 18:08:11 +0700 Subject: [PATCH 1/2] chore: Admin filters list users --- locker_server/api/v1_0/users/views.py | 3 +- .../api_orm/repositories/user_repository.py | 106 ++++++++++++------ .../shared/constants/transactions.py | 5 + 3 files changed, 81 insertions(+), 33 deletions(-) diff --git a/locker_server/api/v1_0/users/views.py b/locker_server/api/v1_0/users/views.py index 8b38b51..cf4bb07 100644 --- a/locker_server/api/v1_0/users/views.py +++ b/locker_server/api/v1_0/users/views.py @@ -101,7 +101,8 @@ def get_queryset(self): users = self.user_service.list_users_by_admin(**{ "register_from": self.check_int_param(self.request.query_params.get("register_from")), "register_to": self.check_int_param(self.request.query_params.get("register_to")), - "plan": self.request.query_params.get("plan"), + # "plan": self.request.query_params.get("plan"), + "can_use_plan": self.request.query_params.get("plan") or self.request.query_params.get("can_use_plan"), "user_ids": self.request.query_params.get("user_ids"), "utm_source": self.request.query_params.get("utm_source"), "q": self.request.query_params.get("q"), diff --git a/locker_server/api_orm/repositories/user_repository.py b/locker_server/api_orm/repositories/user_repository.py index 3d2b604..fe29e96 100644 --- a/locker_server/api_orm/repositories/user_repository.py +++ b/locker_server/api_orm/repositories/user_repository.py @@ -1,17 +1,15 @@ import json -from datetime import timedelta, datetime -from typing import Union, Dict, Optional, Tuple, List - import requests import stripe -from django.conf import settings +from datetime import timedelta, datetime +from typing import Dict, Optional, Tuple, List + from django.core.exceptions import ObjectDoesNotExist -from django.db.models import Subquery, OuterRef, Count, Case, When, IntegerField, Value, Q, Sum, CharField, F, Min +from django.db.models import Subquery, OuterRef, Count, Case, When, IntegerField, Value, Q, Sum, CharField, F from django.db.models.expressions import RawSQL -from django.forms import FloatField from locker_server.api_orm.model_parsers.wrapper import get_model_parser -from locker_server.api_orm.models import UserScoreORM +from locker_server.api_orm.models import UserScoreORM, PMUserPlanFamilyORM from locker_server.api_orm.models.wrapper import get_user_model, get_enterprise_member_model, get_enterprise_model, \ get_event_model, get_cipher_model, get_device_access_token_model, get_team_model, get_team_member_model, \ get_device_model @@ -19,21 +17,18 @@ from locker_server.core.entities.user.user import User from locker_server.core.repositories.user_repository import UserRepository from locker_server.shared.caching.sync_cache import delete_sync_cache_data -from locker_server.shared.constants.account import ACCOUNT_TYPE_ENTERPRISE, ACCOUNT_TYPE_PERSONAL, \ - LOGIN_METHOD_PASSWORD, LOGIN_METHOD_PASSWORDLESS -from locker_server.shared.constants.backup_credential import CREDENTIAL_TYPE_HMAC +from locker_server.shared.constants.account import * from locker_server.shared.constants.ciphers import * -from locker_server.shared.constants.enterprise_members import E_MEMBER_STATUS_CONFIRMED, E_MEMBER_ROLE_PRIMARY_ADMIN, \ - E_MEMBER_ROLE_ADMIN +from locker_server.shared.constants.device_type import * +from locker_server.shared.constants.enterprise_members import * from locker_server.shared.constants.event import EVENT_USER_BLOCK_LOGIN from locker_server.shared.constants.members import MEMBER_ROLE_OWNER from locker_server.shared.constants.policy import POLICY_TYPE_PASSWORDLESS, POLICY_TYPE_2FA -from locker_server.shared.constants.transactions import PAYMENT_STATUS_PAID, PLAN_TYPE_PM_FREE -from locker_server.shared.external_services.locker_background.background_factory import BackgroundFactory -from locker_server.shared.external_services.locker_background.constants import BG_NOTIFY +from locker_server.shared.constants.transactions import * from locker_server.shared.external_services.requester.retry_requester import requester from locker_server.shared.log.cylog import CyLog -from locker_server.shared.utils.app import now, start_end_month_current, datetime_from_ts +from locker_server.shared.utils.app import now, start_end_month_current + DeviceORM = get_device_model() UserORM = get_user_model() @@ -84,6 +79,8 @@ def _generate_duration_init_data(start, end, duration="monthly"): def search_from_cystack_id(cls, **filter_params): q_param = filter_params.get("q") utm_source_param = filter_params.get("utm_source") + is_locker_param = filter_params.get("is_locker") + # TODO: Update: dont use get. use POST headers = {'Authorization': settings.MICRO_SERVICE_USER_AUTH} url = "{}/micro_services/users?".format(settings.GATEWAY_API) if q_param: @@ -110,6 +107,8 @@ def list_users_orm(cls, **filters) -> List[UserORM]: register_from_param = filters.get("register_from") register_to_param = filters.get("register_to") plan_param = filters.get("plan") + can_use_plan_param = filters.get("can_use_plan") + status_param = filters.get("status") activated_param = filters.get("activated") user_ids_param = filters.get("user_ids") utm_source_param = filters.get("utm_source") @@ -128,45 +127,88 @@ def list_users_orm(cls, **filters) -> List[UserORM]: users_orm = users_orm.filter(Q(pm_user_plan__isnull=True) | Q(pm_user_plan__pm_plan__alias=plan_param)) else: users_orm = users_orm.filter(pm_user_plan__pm_plan__alias=plan_param) - if activated_param: - if activated_param == "0": + if can_use_plan_param: + if plan_param == PLAN_TYPE_PM_ENTERPRISE: + enterprise_user_ids = list( + EnterpriseMemberORM.objects.filter(enterprise__locked=False).exclude( + user__isnull=True + ).values_list('user_id', flat=True) + ) + users_orm = users_orm.filter(user_id__in=enterprise_user_ids) + elif plan_param in LIST_FAMILY_PLAN: + family_member_user_ids = list(PMUserPlanFamilyORM.objects.filter().exclude( + user__isnull=True + ).values_list('user_id', flat=True)) + users_orm = users_orm.filter( + Q(pm_user_plan__pm_plan__alias=plan_param) | Q(user_id__in=family_member_user_ids) + ).distinct() + elif plan_param == PLAN_TYPE_PM_FREE: + users_orm = users_orm.filter(Q(pm_user_plan__isnull=True) | Q(pm_user_plan__pm_plan__alias=plan_param)) + else: + users_orm = users_orm.filter(pm_user_plan__pm_plan__alias=plan_param) + + if activated_param is not None: + if activated_param == "0" or activated_param is False: users_orm = users_orm.filter(activated=False) - elif activated_param == "1": + elif activated_param == "1" or activated_param is True: users_orm = users_orm.filter(activated=True) if device_type_param: device_users_orm = users_orm.annotate( web_device_count=Count( - Case(When(user_devices__client_id='web', then=1), output_field=IntegerField()) + Case(When(user_devices__client_id=CLIENT_ID_WEB, then=1), output_field=IntegerField()) ), mobile_device_count=Count( - Case(When(user_devices__client_id='mobile', then=1), output_field=IntegerField()) + Case(When(user_devices__client_id=CLIENT_ID_MOBILE, then=1), output_field=IntegerField()) ), ios_device_count=Count( - Case(When(user_devices__device_type=1, then=1), output_field=IntegerField()) + Case(When(user_devices__device_type=DEVICE_TYPE_IOS, then=1), output_field=IntegerField()) ), android_device_count=Count( - Case(When(user_devices__device_type=0, then=1), output_field=IntegerField()) + Case(When(user_devices__device_type=DEVICE_TYPE_ANDROID, then=1), output_field=IntegerField()) ), extension_device_count=Count( - Case(When(user_devices__client_id='browser', then=1), output_field=IntegerField()) + Case(When(user_devices__client_id=CLIENT_ID_BROWSER, then=1), output_field=IntegerField()) ), desktop_device_count=Count( - Case(When(user_devices__client_id="desktop", then=1), output_field=IntegerField()) - ) + Case(When(user_devices__client_id=CLIENT_ID_DESKTOP, then=1), output_field=IntegerField()) + ), + desktop_windows_count=Count( + Case(When( + Q(user_devices__client_id=CLIENT_ID_DESKTOP, user_devices__device_type=DEVICE_TYPE_WINDOWS), + then=1 + ), output_field=IntegerField()) + ), + desktop_linux_count=Count( + Case(When( + Q(user_devices__client_id=CLIENT_ID_DESKTOP, user_devices__device_type=DEVICE_TYPE_LINUX), + then=1 + ), output_field=IntegerField()) + ), + desktop_mac_count=Count( + Case(When( + Q(user_devices__client_id=CLIENT_ID_DESKTOP, user_devices__device_type=DEVICE_TYPE_MAC), + then=1 + ), output_field=IntegerField()) + ), ) - if device_type_param == "mobile": device_users_orm = device_users_orm.filter(mobile_device_count__gt=0) - if device_type_param == "android": + elif device_type_param == "android": device_users_orm = device_users_orm.filter(android_device_count__gt=0) - if device_type_param == "ios": + elif device_type_param == "ios": device_users_orm = device_users_orm.filter(ios_device_count__gt=0) - if device_type_param == "web": + elif device_type_param == "web": device_users_orm = device_users_orm.filter(web_device_count__gt=0) - if device_type_param == "browser": + elif device_type_param == "browser": device_users_orm = device_users_orm.filter(extension_device_count__gt=0) - if device_type_param == "desktop": + elif device_type_param == "desktop": device_users_orm = device_users_orm.filter(desktop_device_count__gt=0) + elif device_type_param == "desktop_windows": + device_users_orm = device_users_orm.filter(desktop_windows_count__gt=0) + elif device_type_param == "desktop_linux": + device_users_orm = device_users_orm.filter(desktop_linux_count__gt=0) + elif device_type_param == "desktop_mac": + device_users_orm = device_users_orm.filter(desktop_mac_count__gt=0) users_orm = users_orm.filter(user_id__in=device_users_orm.values_list('user_id', flat=True)) if user_ids_param: users_orm = users_orm.filter(user_id__in=user_ids_param.split(",")) diff --git a/locker_server/shared/constants/transactions.py b/locker_server/shared/constants/transactions.py index 5705f5f..d16f80a 100644 --- a/locker_server/shared/constants/transactions.py +++ b/locker_server/shared/constants/transactions.py @@ -71,9 +71,14 @@ PLAN_TYPE_PM_LIFETIME = "pm_lifetime_premium" PLAN_TYPE_PM_LIFETIME_FAMILY = "pm_lifetime_family" + +LIST_FAMILY_PLAN = [PLAN_TYPE_PM_FAMILY, PLAN_TYPE_PM_LIFETIME_FAMILY] + LIST_LIFETIME_PLAN = [PLAN_TYPE_PM_LIFETIME, PLAN_TYPE_PM_LIFETIME_FAMILY] + LIST_PLAN_TYPE = [PLAN_TYPE_PM_FREE, PLAN_TYPE_PM_PREMIUM, PLAN_TYPE_PM_FAMILY, PLAN_TYPE_PM_ENTERPRISE, PLAN_TYPE_PM_LIFETIME, PLAN_TYPE_PM_LIFETIME_FAMILY] + FAMILY_MAX_MEMBER = 6 # ------------- Banking code ----------------------------- # From ac7ece627041db7d48ce9cb7d03f2415db67a15e Mon Sep 17 00:00:00 2001 From: Khai Tran Date: Thu, 28 Mar 2024 13:38:26 +0700 Subject: [PATCH 2/2] chore: Update list users --- locker_server/api/v1_0/users/serializers.py | 6 +-- locker_server/api/v1_0/users/views.py | 27 ++++++++--- .../repositories/user_plan_repository.py | 22 ++++++++- .../api_orm/repositories/user_repository.py | 45 +++++++++++++------ .../core/repositories/user_plan_repository.py | 6 ++- locker_server/core/services/user_service.py | 5 ++- 6 files changed, 86 insertions(+), 25 deletions(-) diff --git a/locker_server/api/v1_0/users/serializers.py b/locker_server/api/v1_0/users/serializers.py index a9da678..fc7610e 100644 --- a/locker_server/api/v1_0/users/serializers.py +++ b/locker_server/api/v1_0/users/serializers.py @@ -240,11 +240,11 @@ def to_representation(self, instance): data = { "id": instance.user_id, "internal_id": instance.internal_id, - "creation_data": instance.creation_date, - "revision_data": instance.revision_date, + "creation_date": instance.creation_date, + "revision_date": instance.revision_date, "first_login": instance.first_login, "activated": instance.activated, - "activated_data": instance.activated_date, + "activated_date": instance.activated_date, "account_revision_date": instance.account_revision_date, "master_password_score": instance.master_password_score, "timeout": instance.timeout, diff --git a/locker_server/api/v1_0/users/views.py b/locker_server/api/v1_0/users/views.py index cf4bb07..d9885b4 100644 --- a/locker_server/api/v1_0/users/views.py +++ b/locker_server/api/v1_0/users/views.py @@ -107,6 +107,7 @@ def get_queryset(self): "utm_source": self.request.query_params.get("utm_source"), "q": self.request.query_params.get("q"), "activated": self.request.query_params.get("activated"), + "status": self.request.query_params.get("status"), "device_type": self.request.query_params.get("device_type") }) return users @@ -967,10 +968,9 @@ def retrieve(self, request, *args, **kwargs): user_id=user.user_id ) data["items"] = ciphers_count - current_plan = self.user_service.get_current_plan( - user=user - ) - data["current_plan"] = current_plan.pm_plan.alias if current_plan else None + usable_plan_alias, db_plan_alias = self.get_usable_plan(user_id=user.user_id) + data["current_plan"] = db_plan_alias + data["usable_plan"] = usable_plan_alias return Response(status=status.HTTP_200_OK, data=data) @@ -1000,9 +1000,24 @@ def list_users(self, request, *args, **kwargs): page = self.paginate_queryset(queryset) if page is not None: serializer = self.get_serializer(page, many=True) - return self.get_paginated_response(serializer.data) + data = self.normalize_users_data(users_data=serializer.data) + return self.get_paginated_response(data) serializer = self.get_serializer(queryset, many=True) - return Response(serializer.data) + data = self.normalize_users_data(users_data=serializer.data) + return Response(data) + + def get_usable_plan(self, user_id): + return self.user_service.get_usable_plan_alias(user_id=user_id) + + def normalize_users_data(self, users_data): + for user_data in users_data: + user_id = user_data.get("id") + usable_plan_alias, db_plan_alias = self.get_usable_plan(user_id=user_id) + user_data.update({ + "current_plan": db_plan_alias, + "usable_plan": usable_plan_alias, + }) + return users_data def get_sso_token_id(self): decoded_token = self.auth_service.decode_token(self.request.auth.access_token, secret=settings.SECRET_KEY) diff --git a/locker_server/api_orm/repositories/user_plan_repository.py b/locker_server/api_orm/repositories/user_plan_repository.py index 3348512..a805e04 100644 --- a/locker_server/api_orm/repositories/user_plan_repository.py +++ b/locker_server/api_orm/repositories/user_plan_repository.py @@ -1,6 +1,6 @@ import ast import math -from typing import Dict, Optional, List +from typing import Dict, Optional, List, Tuple from django.conf import settings from django.core.exceptions import MultipleObjectsReturned @@ -175,6 +175,26 @@ def get_user_plan(self, user_id: int) -> Optional[PMUserPlan]: user_plan_orm = self._get_current_plan_orm(user_id=user_id) return ModelParser.user_plan_parser().parse_user_plan(user_plan_orm=user_plan_orm) + def get_user_usable_plan_alias(self, user_id: int) -> Tuple: + """ + Get the current plan in the database and the real plan can be used + :param user_id: + :return: (tuple) the_usable_alias, db_plan_alias + """ + user_plan_orm = self._get_current_plan_orm(user_id=user_id) + if not user_plan_orm: + return None, None + db_plan_alias = user_plan_orm.pm_plan.alias + if db_plan_alias == PLAN_TYPE_PM_FREE: + # Check user is enterprise member or family member + family_member = user_plan_orm.user.pm_plan_family.first() + if family_member: + return family_member.root_user_plan.pm_plan.alias, db_plan_alias + else: + is_enterprise_member = user_plan_orm.user.enterprise_members.exist() + return PLAN_TYPE_PM_ENTERPRISE, db_plan_alias if is_enterprise_member else db_plan_alias, db_plan_alias + return db_plan_alias, db_plan_alias + def get_mobile_user_plan(self, pm_mobile_subscription: str) -> Optional[PMUserPlan]: try: user_plan_orm = PMUserPlanORM.objects.get(pm_mobile_subscription=pm_mobile_subscription) diff --git a/locker_server/api_orm/repositories/user_repository.py b/locker_server/api_orm/repositories/user_repository.py index fe29e96..955b18d 100644 --- a/locker_server/api_orm/repositories/user_repository.py +++ b/locker_server/api_orm/repositories/user_repository.py @@ -80,25 +80,32 @@ def search_from_cystack_id(cls, **filter_params): q_param = filter_params.get("q") utm_source_param = filter_params.get("utm_source") is_locker_param = filter_params.get("is_locker") - # TODO: Update: dont use get. use POST + status_param = filter_params.get("status") headers = {'Authorization': settings.MICRO_SERVICE_USER_AUTH} url = "{}/micro_services/users?".format(settings.GATEWAY_API) - if q_param: - url += "&q={}".format(q_param) - if utm_source_param: - url += "&utm_source={}".format(utm_source_param) + data_send = {} + if q_param is not None: + data_send.update({"q": q_param}) + if utm_source_param is not None: + data_send.update({"utm_source": utm_source_param}), + if is_locker_param is not None: + data_send.update({"is_locker": is_locker_param}) + if status_param is not None: + data_send.update({"status": status_param}) try: - res = requester(method="GET", url=url, headers=headers) + res = requester(method="POST", url=url, headers=headers, data_send=data_send) if res.status_code == 200: try: return res.json() except json.JSONDecodeError: - CyLog.error( - **{"message": f"[!] User.search_from_cystack_id JSON Decode error: {res.url} {res.text}"}) - return {} - return {} + CyLog.error(**{ + "message": f"[!] User.search_from_cystack_id JSON Decode error: {res.url} {res.text}" + }) + return [] + return [] except (requests.RequestException, requests.ConnectTimeout): - return {} + CyLog.error(**{"message": f"[!] User.search_from_cystack_id Request Connect error"}) + return [] @classmethod def list_users_orm(cls, **filters) -> List[UserORM]: @@ -114,8 +121,11 @@ def list_users_orm(cls, **filters) -> List[UserORM]: utm_source_param = filters.get("utm_source") device_type_param = filters.get("device_type") - if q_param or utm_source_param: - user_ids = cls.search_from_cystack_id(**{"q": q_param, "utm_source": utm_source_param}).get("ids", []) + if q_param or utm_source_param or status_param in ["unverified", "deleted"]: + users_data = cls.search_from_cystack_id(**{ + "q": q_param, "utm_source": utm_source_param, "status_param": status_param, "is_locker": True + }) + user_ids = [u.get("id") for u in users_data] users_orm = users_orm.filter(user_id__in=user_ids) if register_from_param: @@ -152,6 +162,15 @@ def list_users_orm(cls, **filters) -> List[UserORM]: users_orm = users_orm.filter(activated=False) elif activated_param == "1" or activated_param is True: users_orm = users_orm.filter(activated=True) + + if status_param is not None: + if status_param == "created_master_pwd": + users_orm = users_orm.filter(activated=True) + elif status_param == "verified": + users_data = cls.search_from_cystack_id(**{"is_activated": True, "is_locker": True}) + verified_user_ids = [u.get("id") for u in users_data] + users_orm = users_orm.filter(user_id__in=verified_user_ids).exclude(activated=False) + if device_type_param: device_users_orm = users_orm.annotate( web_device_count=Count( diff --git a/locker_server/core/repositories/user_plan_repository.py b/locker_server/core/repositories/user_plan_repository.py index 029de84..edcc312 100644 --- a/locker_server/core/repositories/user_plan_repository.py +++ b/locker_server/core/repositories/user_plan_repository.py @@ -1,4 +1,4 @@ -from typing import Union, Dict, Optional, List +from typing import Union, Dict, Optional, List, Tuple from abc import ABC, abstractmethod from locker_server.core.entities.enterprise.enterprise import Enterprise @@ -28,6 +28,10 @@ def list_expiring_enterprise_plans(self) -> List[PMUserPlan]: def get_user_plan(self, user_id: int) -> Optional[PMUserPlan]: pass + @abstractmethod + def get_user_usable_plan_alias(self, user_id: int) -> Tuple: + pass + @abstractmethod def get_mobile_user_plan(self, pm_mobile_subscription: str) -> Optional[PMUserPlan]: pass diff --git a/locker_server/core/services/user_service.py b/locker_server/core/services/user_service.py index f1bb302..0748272 100644 --- a/locker_server/core/services/user_service.py +++ b/locker_server/core/services/user_service.py @@ -1,5 +1,5 @@ from datetime import datetime -from typing import Optional, List, Dict, NoReturn, Union +from typing import Optional, List, Dict, NoReturn, Union, Tuple import jwt from django.conf import settings @@ -88,6 +88,9 @@ def __init__(self, user_repository: UserRepository, def get_current_plan(self, user: User) -> PMUserPlan: return self.user_plan_repository.get_user_plan(user_id=user.user_id) + def get_usable_plan_alias(self, user_id: int) -> Tuple[str, str]: + return self.user_plan_repository.get_user_usable_plan_alias(user_id=user_id) + def update_plan(self, user_id: int, plan_type_alias: str, duration: str = DURATION_MONTHLY, scope: str = None, **kwargs): return self.user_plan_repository.update_plan(