diff --git a/Fix_HTMLi-AddMember.md b/Fix_HTMLi-AddMember.md new file mode 100644 index 00000000..211f0812 --- /dev/null +++ b/Fix_HTMLi-AddMember.md @@ -0,0 +1,27 @@ +Hello + +##Issue +This pull request serves as a placeholder for Issue ==> https://github.com/AIxBlock-2023/awesome-ai-dev-platform-opensource/issues/50 + +HTML injection is a type of injection vulnerability that occurs when a user is able to control an input point and is able to inject arbitrary HTML code into a vulnerable web application. + +AIxBlock application service allows the execution of arbitrary HTML Injection payload on AIxBlock mail service through the "invited new members" email notification template. + +##See below for screenshot. +https://drive.google.com/drive/folders/1IJAtg8wJoQKJ2MzKimP938f6CFeMOnZg?usp=sharing + +##To Reproduce +Steps to reproduce the behavior: + +- Go to "https://app.aixblock.io/user/organization' +- Go to Account Settings --> Organizations --> Add Member +- Scroll down to 'User Name' field and enter the payload (<\h1\>test.com<\/h1\> (please remove \) +- Enter the Victim's email address and forward + + +##Expected behavior +A clear and concise description of what you expected to happen. + +- Form field should should be sanitized against meta characters +- Ensure the application logic also prevent the executions of injected payloads + diff --git a/Fix_IDOR-on-Organization.md b/Fix_IDOR-on-Organization.md new file mode 100644 index 00000000..0a05a5b5 --- /dev/null +++ b/Fix_IDOR-on-Organization.md @@ -0,0 +1,27 @@ +Hello + +##Issue +This pull request serves as a placeholder for Issue ==> https://github.com/AIxBlock-2023/awesome-ai-dev-platform-opensource/issues/47 + +The API endpoint "/api/organizations/{Orgaizatio-ID}/memberships" is vulnerable to Insecure Direct Object Reference (IDOR), allowing authenticated users to view any user's data by manipulating the "{Orgaizatio-ID}" value. + +##To Reproduce +Steps to reproduce the behavior: + + 1- Go to 'https://app.aixblock.io/api/organizations/{Orgaizatio-ID}/memberships?page=1&page_size=9&project_id=&search=' + 2- Click on 'Enter any 4 digit integer e.g 6754' + 3- Check the 'Server Response' + +##Business Risk + + 1- Exposure of Any Organization information + 2- Exposure of Any Organization Admin PII + 3- Exposure of User Email Address, Phone number, Username, Full name, UUID & UID. + +##Expected behavior + +A clear and concise description of what you expected to happen. + +Ensure SessionID and UserID matches the Organization ID before returning a 200 OK and Organization information. + +Otherwise (i.e if the SessionID & UserID does not match the OrganizationID) Return 401 Unauthorized value. diff --git a/Fix_PrivilegeEscal.md b/Fix_PrivilegeEscal.md new file mode 100644 index 00000000..0adffb42 --- /dev/null +++ b/Fix_PrivilegeEscal.md @@ -0,0 +1,13 @@ +Hello + +This pull request serves as a placeholder for Issue #58. + +The file was added to meet the project’s submission requirements. + +Remediation guide: + +- DO not rely on front-end response before granting user access to sensitive functionalities. + +- Ensure Super_Admin functionalities are not accessible to base_users and admins, by checking the permission level tied to the user session cookie or JWToken + +- SUPER_ADMIN RIGHT should be tied to access token and session cookie upon account creation, and it should also be checked when request is sent to any SUPER_ADMIN endpoint. diff --git a/bugfix-0xygyn/fix-infodisclosure b/bugfix-0xygyn/fix-infodisclosure new file mode 100644 index 00000000..e475fffa --- /dev/null +++ b/bugfix-0xygyn/fix-infodisclosure @@ -0,0 +1,10 @@ +Recommended fix for security issue 203: + +Since the "MarketPlace Model" functionality does not require any API calls to another user's profile, + +Commenting out that API call to /api/user/(the 6 IDs called) in the code base is what is required to fix this vulnerability. + +###COMMENT OUT THE API CALL TO THAT ENDPOINT. + +Also, disabling the API endpoint /api/user/ID is required from the API Collection since /api/users/ID exist. + diff --git a/bugfixes_0xygyn/bugfixes/0XYGYN_SECURITY_FIXES.md b/bugfixes_0xygyn/bugfixes/0XYGYN_SECURITY_FIXES.md new file mode 100644 index 00000000..3ad80a55 --- /dev/null +++ b/bugfixes_0xygyn/bugfixes/0XYGYN_SECURITY_FIXES.md @@ -0,0 +1,88 @@ +# Security Fixes + +This document summarizes the patched vulnerabilities and provides file locations for the reported vulnerabilities listed below: + +--- + +### #111 +- **Affected sub-domain:** https://app.aixblock.io/ +- **Affected Endpoint:** POST /api/invite/csv +- **Affected Parameter:** `organizationId` + +### #108 +- **Affected sub-domain:** https://app.aixblock.io/ +- **Affected Endpoint:** POST /api/invite/csv +- **Affected Parameter:** `organizationId` +- **Vulnerability Type:** Broken Access Control (IDOR) + +### #98 +- **Affected sub-domain:** https://app.aixblock.io/ +- **Affected Endpoint:** POST /api/storages/link-global/1818/823/s3 +- **Vulnerability Type:** Broken Access Control (IDOR) + +### #97 +- **Affected sub-domain:** https://app.aixblock.io/ +- **Affected Endpoint:** GET /api/storages/s3-server/1 +- **Vulnerability Type:** Broken Access Control (IDOR) + +### #95 +- **Affected sub-domain:** https://app.aixblock.io/ +- **Affected Endpoint:** + - GET /api/storages/azure/5 +- **Vulnerability Type:** Broken Access Control (IDOR) + +### #94 +- **Affected sub-domain:** https://app.aixblock.io/ +- **Affected Endpoint:** + - GET /api/storages/s3/ID +- **Vulnerability Type:** Broken Access Control (IDOR) + +### #93 +- **Affected sub-domain:** https://app.aixblock.io/ +- **Affected Endpoint:** + - GET /api/storages/s3/18 +- **Vulnerability Type:** Broken Access Control (IDOR) + +### #59 +- **Affected sub-domain:** https://app.aixblock.io/ +- **Affected Endpoint:** + - GET https://app.aixblock.io/api/current-user/whoami +- **Vulnerability Type:** Privilege Escalation + +### #47 +- **Affected sub-domain:** https://app.aixblock.io/ +- **Affected Endpoint:** + - GET /api/organizations/{Organization-ID}/memberships +- **Vulnerability Type:** Broken Access Control (IDOR) + +--- + +## Fixed issues + +- **IDOR vulnerabilities on storage detail endpoints** (#93, #94, #95, #97) + - **File:** `aixblock_core/io_storages/api.py` + - Added project permission checks in `ImportStorageDetailAPI.get_object` and `ExportStorageDetailAPI.get_object`. + +- **IDOR on organization member listing** (#47) + - **File:** `aixblock_core/organizations/api.py` + - `OrganizationMemberListAPI.get_queryset` now verifies the requesting user belongs to the organization. + +- **Unrestricted organization invitations** (#111, #108) + - **File:** `aixblock_core/organizations/api.py` + - `OrganizationInviteAPIByCSV.post` validates that the requester is an admin of the specified organization. + +- **Privilege Escalation and Excessive data exposure in `whoami` endpoint** (#59) + - **File:** `aixblock_core/users/api.py` + - `UserWhoAmIAPI` uses `UserCompactSerializer` instead of `UserSerializer`. + +--- + +## Paths and line references + +| File | Lines | +|-----------------------------|------------------------| +| `io_storages/api.py` | see lines around 103 and 279 | +| `organizations/api.py` | lines around 158-170 and 426-437 | +| `users/api.py` | lines around 362-371 | +| `core/permissions.py` | lines around 112-121 | + diff --git a/bugfixes_0xygyn/bugfixes/core/permissions.py b/bugfixes_0xygyn/bugfixes/core/permissions.py new file mode 100644 index 00000000..8ce03b14 --- /dev/null +++ b/bugfixes_0xygyn/bugfixes/core/permissions.py @@ -0,0 +1,121 @@ +"""This file and its contents are licensed under the Apache License 2.0. Please see the included NOTICE for copyright information and LICENSE for a copy of the license. +""" +import logging +import rules + +from pydantic import BaseModel +from typing import Optional + +from rest_framework.permissions import BasePermission + +logger = logging.getLogger(__name__) + + +class AllPermissions(BaseModel): + organizations_create = 'organizations.create' + organizations_view = 'organizations.view' + organizations_change = 'organizations.change' + organizations_delete = 'organizations.delete' + organizations_invite = 'organizations.invite' + projects_create = 'projects.create' + projects_view = 'projects.view' + projects_change = 'projects.change' + projects_delete = 'projects.delete' + tasks_create = 'tasks.create' + tasks_view = 'tasks.view' + tasks_change = 'tasks.change' + tasks_delete = 'tasks.delete' + annotations_create = 'annotations.create' + annotations_view = 'annotations.view' + annotations_change = 'annotations.change' + annotations_delete = 'annotations.delete' + actions_perform = 'actions.perform' + predictions_any = 'predictions.any' + avatar_any = 'avatar.any' + labels_create = 'labels.create' + labels_view = 'labels.view' + labels_change = 'labels.change' + labels_delete = 'labels.delete' + model_marketplace_create = 'model_marketplace.create' + model_marketplace_view = 'model_marketplace.view' + model_marketplace_change = 'model_marketplace.change' + model_marketplace_delete = 'model_marketplace.delete' + orders_delete = 'orders.delete' + orders_view = 'orders.view' + orders_change = 'orders.change' + orders_create = 'orders.create' + reward_point_view = "reward_point.view" + reward_point_change = "reward_point.change" + reward_point_delete = "reward_point.delete" + reward_point_create = "reward_point.create" + installation_service_view = "installation_service.view" + installation_service_change = "installation_service.change" + installation_service_delete = "installation_service.delete" + installation_service_create = "installation_service.create" + + +all_permissions = AllPermissions() + + +class ViewClassPermission(BaseModel): + GET: Optional[str] = None + PATCH: Optional[str] = None + PUT: Optional[str] = None + DELETE: Optional[str] = None + POST: Optional[str] = None + + +def make_perm(name, pred, overwrite=False): + if rules.perm_exists(name): + if overwrite: + rules.remove_perm(name) + else: + return + rules.add_perm(name, pred) + + +for _, permission_name in all_permissions: + make_perm(permission_name, rules.is_authenticated) + + +class IsSuperAdmin(BasePermission): + """ + Allows access only to admin users. + """ + + def has_permission(self, request, view): + return bool(request.user and request.user.is_superuser) + +class IsSuperAdminOrg(BasePermission): + """ + Allows access only to admin org users. + """ + + def has_permission(self, request, view): + try: + from organizations.models import OrganizationMember + from projects.models import Project + + if 'project_id' in request.data: + project = Project.objects.get(id=request.data.get('project_id')) + else: + project = Project.objects.get(id=request.data.get('project')) + check_org_admin = OrganizationMember.objects.filter(organization_id = project.organization_id, user_id=request.user.id, is_admin=True).exists() + if not check_org_admin: + if not project.organization.created_by_id == request.user.id and not request.user.is_superuser: + return False + return True + except: + return True + +def permission_org(user, project): + try: + from organizations.models import OrganizationMember + + check_org_admin = OrganizationMember.objects.filter(organization_id = project.organization_id, user_id=user.id, is_admin=True).exists() + if not check_org_admin: + if not project.organization.created_by_id == user.id and not user.is_superuser: + return False + return True + except Exception: + return True \ No newline at end of file diff --git a/bugfixes_0xygyn/bugfixes/io_storages/api.py b/bugfixes_0xygyn/bugfixes/io_storages/api.py new file mode 100644 index 00000000..438aa514 --- /dev/null +++ b/bugfixes_0xygyn/bugfixes/io_storages/api.py @@ -0,0 +1,473 @@ +"""This file and its contents are licensed under the Apache License 2.0. Please see the included NOTICE for copyright information and LICENSE for a copy of the license. +""" +import logging +import inspect +import os + +from rest_framework import generics +from rest_framework.parsers import FormParser, JSONParser, MultiPartParser +from rest_framework.response import Response +from rest_framework.exceptions import NotFound, PermissionDenied +from drf_yasg import openapi as openapi +from django.conf import settings +from drf_yasg.utils import swagger_auto_schema + +from core.permissions import all_permissions, permission_org +from core.utils.common import get_object_with_check_and_log +from core.utils.io import read_yaml +from io_storages.serializers import ImportStorageSerializer, ExportStorageSerializer +from projects.models import Project +from core.services.minio_client import MinioClient +from core.permissions import IsSuperAdminOrg + +from organizations.models import OrganizationMember, Organization +from rest_framework.exceptions import ValidationError +from .s3.models import S3ImportStorage +# from .localfiles.models import LocalFilesImportStorage +from .gdriver.models import GDriverImportStorage +from .gcs.models import GCSImportStorage +from .redis.models import RedisImportStorage +from .azure_blob.models import AzureBlobImportStorage + +from .s3.models import S3ExportStorage +# from .localfiles.models import LocalFilesExportStorage +from .gdriver.models import GDriverExportStorage +from .gcs.models import GCSExportStorage +from .redis.models import RedisExportStorage +from .azure_blob.models import AzureBlobExportStorage +from projects.models import Project + +from io_storages.s3.storage_server import S3Server + +logger = logging.getLogger(__name__) + + +class ImportStorageListAPI(generics.ListCreateAPIView): + parser_classes = (JSONParser, FormParser, MultiPartParser) + permission_required = all_permissions.projects_change + serializer_class = ImportStorageSerializer + + def get_queryset(self): + project_pk = self.request.query_params.get('project') + project = get_object_with_check_and_log(self.request, Project, pk=project_pk) + self.check_object_permissions(self.request, project) + ImportStorageClass = self.serializer_class.Meta.model + return ImportStorageClass.objects.filter(project=project) + + def post(self, request, *args, **kwargs): + project = Project.objects.get(id=self.request.data.get('project')) + check_org_admin = OrganizationMember.objects.filter(organization_id=project.organization_id, + user_id=self.request.user.id, is_admin=True).exists() + + if not check_org_admin and not self.request.user.is_superuser: + raise ValidationError( + "You do not have permission to setup this project", code=403 + ) + + serializer = self.get_serializer(data=self.request.data) + serializer.is_valid(raise_exception=True) + + # Save the storage object + storage = serializer.save() + + # Sync logic for another storage + def sync_another_storage(storage): + try: + storage.create_folder(f"raw_files_{project.pk}") + # from io_storages.s3.models import S3ImportStorage + + # if storage != S3ImportStorage.objects.filter(project_id=storage.project_id).first(): + # s3_prj = S3ImportStorage.objects.filter(project_id=storage.project_id).first() + # storage.alias_storage(s3_prj) + # storage.copy_storage(s3_prj) + + except Exception as e: + print(e) + + import threading + + storage_thread = threading.Thread(target=sync_another_storage, args=(storage,)) + storage_thread.start() + + return Response(self.serializer_class(storage).data) + # return super(ImportStorageListAPI, self).post(request, *args, **kwargs) + +class ImportStorageDetailAPI(generics.RetrieveUpdateDestroyAPIView): + """RUD storage by pk specified in URL""" + + parser_classes = (JSONParser, FormParser, MultiPartParser) + serializer_class = ImportStorageSerializer + permission_required = all_permissions.projects_change + permission_classes = [IsSuperAdminOrg] + + def get_object(self): + obj = super().get_object() + # Vulnerable: no explicit organization permission check + # if not permission_org(self.request.user, obj.project): + # raise PermissionDenied + # Fix: ensure user has permission on the related project + if not permission_org(self.request.user, obj.project): # issues #97, #95, #94, #93 + raise PermissionDenied("User lacks organization permissions") + return obj + + @swagger_auto_schema(auto_schema=None) + def put(self, request, *args, **kwargs): + return super(ImportStorageDetailAPI, self).put(request, *args, **kwargs) + + # def patch(self, request, *args, **kwargs): + # # try: + # project = Project.objects.get(id=self.request.data.get('project')) + # check_org_admin = OrganizationMember.objects.filter(organization_id = project.organization_id, user_id=self.request.user.id, is_admin=True).exists() + # if not check_org_admin: + # if not project.organization.created_by_id == self.request.user.id and not self.request.user.is_superuser: + # raise ValidationError("You do not have permission to setup this storage", code=403) + # # except Exception as e: + # # print(e) + # return super(ImportStorageDetailAPI, self).patch(request, *args, **kwargs) + + # def delete(self, request, *args, **kwargs): + # project = Project.objects.get(id=self.request.data.get('project')) + # check_org_admin = OrganizationMember.objects.filter(organization_id = project.organization_id, user_id=self.request.user.id, is_admin=True).exists() + # if not check_org_admin: + # if not project.organization.created_by_id == self.request.user.id and not self.request.user.is_superuser: + # return Response({"message": "You do not have permission to delete this storage"}, status=403) + # return super(ImportStorageDetailAPI, self).delete(request, *args, **kwargs) + +class ExportStorageListAPI(generics.ListCreateAPIView): + parser_classes = (JSONParser, FormParser, MultiPartParser) + permission_required = all_permissions.projects_change + serializer_class = ExportStorageSerializer + + def get_queryset(self): + project_pk = self.request.query_params.get('project') + project = get_object_with_check_and_log(self.request, Project, pk=project_pk) + self.check_object_permissions(self.request, project) + ImportStorageClass = self.serializer_class.Meta.model + return ImportStorageClass.objects.filter(project_id=project.id) + + def perform_create(self, serializer): + if self.request.data.get('is_global') == 1 or self.request.data.get('project') == None: + print(self.request.data) + # del self.request.data["null"] + kwargs = self.request.data + if kwargs.get('storage_type') == "s3": + + # export_storage = S3ExportStorage.objects.create( + # # project=kwargs.get('pk'), + # title=kwargs.get('title'), + # bucket=kwargs.get('path'), + # prefix=kwargs.get('prefix'), + # regex_filter=kwargs.get('regex'), + # use_blob_urls=True, + # region_name=kwargs.get('region_name'), + # ) + + storage = S3ImportStorage(title=kwargs.get('title'), region_name=kwargs.get('region_name'),bucket=kwargs.get('bucket'), + aws_access_key_id = kwargs.get('aws_access_key_id'), + aws_secret_access_key = kwargs.get('aws_secret_access_key'), + s3_endpoint = kwargs.get('s3_endpoint').strip(), + aws_session_token = kwargs.get('aws_session_token'), + use_blob_urls=True, + regex_filter=kwargs.get('regex_filter'), + is_global=True, + user_id=self.request.user.id, + project_id=kwargs.get('pk'), + org_id=self.request.user.active_organization_id) + storage.validate_connection() + storage.save() + storage.refresh_from_db() + return Response(status=200) + elif kwargs.get('storage_type', "") == "azure": + + storage = AzureBlobImportStorage( + title=kwargs.get('title'), + # export_storage=export_storage.id, + # project=kwargs.get('pk'), + container=kwargs.get('container'), + prefix=kwargs.get('prefix'), + regex_filter=kwargs.get('regex_filter'), + account_name = kwargs.get('account_name'), + account_key = kwargs.get('account_key'), + use_blob_urls=True, + is_global=True, + user_id=self.request.user.id,org_id=self.request.user.active_organization_id,project_id=kwargs.get('pk') + ) + storage.validate_connection() + storage.save() + storage.refresh_from_db() + return Response(status=200) + elif kwargs.get('storage_type', "") == "redis": + + storage = RedisImportStorage( + title=kwargs.get('title'), + # export_storage=export_storage.id, + # project=kwargs.get('pk'), + path=kwargs.get('path'), + host=kwargs.get('host'), + port=kwargs.get('port'), + password=kwargs.get('password'),is_global=True,user_id=self.request.user.id,org_id=self.request.user.active_organization_id,project_id=kwargs.get('pk') + ) + storage.validate_connection() + storage.save() + storage.refresh_from_db() + return Response(status=200) + + elif kwargs.get('storage_type', "") == "gdriver": + + storage = GDriverImportStorage( + title=kwargs.get('title'), + # export_storage=export_storage.id, + # project=kwargs.get('pk'), + bucket=kwargs.get('bucket'), + prefix=kwargs.get('prefix'), + regex_filter=kwargs.get('regex'), + google_application_credentials=kwargs.get('google_application_credentials'), + use_blob_urls=True,is_global=True,user_id=self.request.user.id,org_id=self.request.user.active_organization_id + ) + storage.validate_connection() + storage.save() + storage.refresh_from_db() + return Response(status=200) + elif kwargs.get('storage_type', "") == "gcs": + + storage = GCSImportStorage( + title=kwargs.get('title'), + # export_storage=export_storage.id, + project=kwargs.get('pk'), + bucket=kwargs.get('bucket'), + prefix=kwargs.get('prefix'), + regex_filter=kwargs.get('regex_filter'), + google_application_credentials=kwargs.get('google_application_credentials'), + use_blob_urls=True,is_global=True,user_id=self.request.user.id,org_id=self.request.user.active_organization_id + ) + storage.validate_connection() + storage.save() + storage.refresh_from_db() + return Response(status=200) + + else: + project = Project.objects.get(id=self.request.data.get('project')) + check_org_admin = OrganizationMember.objects.filter(organization_id = project.organization_id, user_id=self.request.user.id, is_admin=True).exists() + if not check_org_admin: + if not project.organization.created_by_id == self.request.user.id and not self.request.user.is_superuser: + raise ValidationError("You do not have permission to setup this project", code=403) + + import_id = self.request.data.get("import_id") + serializer = self.get_serializer(data=self.request.data) + serializer.is_valid(raise_exception=True) + + # Lưu đối tượng và lấy dữ liệu đã lưu + storage = serializer.save() + + if import_id: + storage.create_connect_to_import_storage(import_id) + + if settings.SYNC_ON_TARGET_STORAGE_CREATION: + storage.sync() + + +class ExportStorageDetailAPI(generics.RetrieveUpdateDestroyAPIView): + """RUD storage by pk specified in URL""" + + parser_classes = (JSONParser, FormParser, MultiPartParser) + serializer_class = ExportStorageSerializer + permission_required = all_permissions.projects_change + permission_classes = [IsSuperAdminOrg] + + def get_object(self): + obj = super().get_object() + # Vulnerable: missing permission check allowed IDOR access + # Fix: verify organization permissions before returning object + if not permission_org(self.request.user, obj.project): # issues #97, #95, #94, #93 + raise PermissionDenied("User lacks organization permissions") + return obj + + @swagger_auto_schema(auto_schema=None) + def put(self, request, *args, **kwargs): + return super(ExportStorageDetailAPI, self).put(request, *args, **kwargs) + + # def patch(self, request, *args, **kwargs): + # project = Project.objects.get(id=self.request.data.get('project')) + # check_org_admin = OrganizationMember.objects.filter(organization_id = project.organization_id, user_id=self.request.user.id, is_admin=True).exists() + # if not check_org_admin: + # if not project.organization.created_by_id == self.request.user.id and not self.request.user.is_superuser: + # raise ValidationError("You do not have permission to setup this storage", code=403) + # return super(ExportStorageDetailAPI, self).patch(request, *args, **kwargs) + + # def delete(self, request, *args, **kwargs): + # project = Project.objects.get(id=self.request.data.get('project')) + # check_org_admin = OrganizationMember.objects.filter(organization_id = project.organization_id, user_id=self.request.user.id, is_admin=True).exists() + # if not check_org_admin: + # if not project.organization.created_by_id == self.request.user.id and not self.request.user.is_superuser: + # return Response({"message": "You do not have permission to delete this storage"}, status=403) + # return super(ExportStorageDetailAPI, self).delete(request, *args, **kwargs) + +class ImportStorageSyncAPI(generics.GenericAPIView): + + parser_classes = (JSONParser, FormParser, MultiPartParser) + permission_required = all_permissions.projects_change + serializer_class = ImportStorageSerializer + + def get_queryset(self): + ImportStorageClass = self.serializer_class.Meta.model + return ImportStorageClass.objects.all() + + def post(self, request, *args, **kwargs): + storage = self.get_object() + + # Get the first project (or handle multiple projects if needed) + if request.data and len(request.data) > 0: + if "regex_filter" in request.data: + storage.regex_filter = request.data["regex_filter"] + if "use_blob_urls" in request.data: + storage.use_blob_urls = request.data["use_blob_urls"] + if "presign_ttl" in request.data: + storage.presign_ttl = request.data["presign_ttl"] + storage.save() + + if ( + "type_import" in request.data + and request.data["type_import"] == "dataset" + ): + try: + from io_storages.functions import get_all_project_storage_with_name + storages = get_all_project_storage_with_name(storage.project.id) + # cloud_client = MinioClient( + # access_key=storage.project.s3_access_key, # Use `project` here instead of `storage.project` + # secret_key=storage.project.s3_secret_key, + # ) + + # objects = cloud_client.get_list_project( + # bucket_name=f"project_{storage.project.id}", object_name="dataset/" + # ) + + # for object in objects: + # from model_marketplace.models import DatasetModelMarketplace + + # user = request.user + # if not DatasetModelMarketplace.objects.filter( + # name=object + # ).exists(): + # DatasetModelMarketplace.objects.create( + # name=object, + # owner_id=user.id, + # author_id=user.id, + # project_id=storage.project.id, # Use the first project + # catalog_id=storage.project.template.catalog_model_id, + # model_id=0, + # order=0, + # config=0, + # dataset_storage_id=storage.id, + # version=object, + # ) + except Exception as e: + print(e) + + # check connectivity & access, raise an exception if not satisfied + storage.validate_connection() + storage.sync() + storage.sync_export_storage() + storage.refresh_from_db() + return Response(self.serializer_class(storage).data) + + +class ExportStorageSyncAPI(generics.GenericAPIView): + + parser_classes = (JSONParser, FormParser, MultiPartParser) + permission_required = all_permissions.projects_change + serializer_class = ExportStorageSerializer + + def get_queryset(self): + ExportStorageClass = self.serializer_class.Meta.model + return ExportStorageClass.objects.all() + + def post(self, request, *args, **kwargs): + storage = self.get_object() + # check connectivity & access, raise an exception if not satisfied + storage.validate_connection() + storage.sync() + storage.refresh_from_db() + return Response(self.serializer_class(storage).data) + + +class StorageValidateAPI(generics.CreateAPIView): + parser_classes = (JSONParser, FormParser, MultiPartParser) + permission_required = all_permissions.projects_change + + def create(self, request, *args, **kwargs): + instance = None + storage_id = request.data.get('id') + if storage_id: + instance = generics.get_object_or_404(self.serializer_class.Meta.model.objects.all(), pk=storage_id) + if not instance.has_permission(request.user): + raise PermissionDenied() + serializer = self.get_serializer(instance=instance, data=request.data) + serializer.is_valid(raise_exception=True) + return Response() + + +class StorageFormLayoutAPI(generics.RetrieveAPIView): + + parser_classes = (JSONParser, FormParser, MultiPartParser) + permission_required = all_permissions.projects_change + swagger_schema = None + storage_type = None + + @swagger_auto_schema(auto_schema=None) + def get(self, request, *args, **kwargs): + project_id = request.query_params.get('project_id') + + form_layout_file = os.path.join(os.path.dirname(inspect.getfile(self.__class__)), 'form_layout.yml') + + if not os.path.exists(form_layout_file): + raise NotFound(f'"form_layout.yml" is not found for {self.__class__.__name__}') + + form_layout = read_yaml(form_layout_file) + + try: + project = Project.objects.filter(id=project_id).first() + + data_type = project.data_types + if 'image' in data_type: + filter_s3 = f"raw_files_{project.pk}/.+\.(jpg|png|jpeg|gif|webp|svg)" + elif 'audio' in data_type: + filter_s3 = f"raw_files_{project.pk}/.+\.(mp3|ogg|wav|m4a|m4b|aiff|au|flac)" + elif 'video' in data_type: + filter_s3 = f"raw_files_{project.pk}/.+\.(mp4|h264|webm|webm)" + else: + filter_s3 = f"raw_files_{project.pk}/.+\.(txt|csv|json|tsv)" + + field = next( + (field for section in form_layout.get('ImportStorage', []) + for field in section.get('fields', []) + if field and field.get('label') == 'File Filter Regex'), + None + ) + + if field: + field['value'] = filter_s3 + + except Exception as e: + print(e) + + form_layout = self.post_process_form(form_layout) + return Response(form_layout[self.storage_type]) + + def post_process_form(self, form_layout): + return form_layout + + +class ImportStorageValidateAPI(StorageValidateAPI): + serializer_class = ImportStorageSerializer + + +class ExportStorageValidateAPI(StorageValidateAPI): + serializer_class = ExportStorageSerializer + + +class ImportStorageFormLayoutAPI(StorageFormLayoutAPI): + storage_type = 'ImportStorage' + + +class ExportStorageFormLayoutAPI(StorageFormLayoutAPI): + storage_type = 'ExportStorage' diff --git a/bugfixes_0xygyn/bugfixes/organizations/api.py b/bugfixes_0xygyn/bugfixes/organizations/api.py new file mode 100644 index 00000000..ca9c6ba8 --- /dev/null +++ b/bugfixes_0xygyn/bugfixes/organizations/api.py @@ -0,0 +1,1324 @@ +"""This file and its contents are licensed under the Apache License 2.0. Please see the included NOTICE for copyright information and LICENSE for a copy of the license. +""" + +import copy +import csv +import json +import logging +import os +import re +import time +import threading +from datetime import datetime +import random +import drf_yasg.openapi as openapi + +from django.http import HttpResponse, Http404 +from django.urls import reverse +from django.conf import settings +from rest_framework.parsers import ( + FormParser, + JSONParser, + MultiPartParser, + FileUploadParser, +) +from rest_framework import generics +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework.pagination import PageNumberPagination +from rest_framework.exceptions import ValidationError, PermissionDenied + +from drf_yasg.utils import swagger_auto_schema +from drf_yasg import openapi +from core.utils.common import create_hash +from django.utils.decorators import method_decorator + +from aixblock_core.core.permissions import all_permissions, ViewClassPermission +from aixblock_core.core.utils.common import get_object_with_check_and_log +from aixblock_core.core.utils.params import bool_from_request +from aixblock_core.projects.serializers import GetFieldsSerializer, ProjectSerializer + + +from organizations.models import Organization, OrganizationMember, Organization_Project_Permission +from organizations.serializers import ( + OrganizationReporRequest, + OrganizationSerializer, + OrganizationIdSerializer, + OrganizationMemberUserSerializer, + OrganizationInviteSerializer, + OrganizationsParamsSerializer, OrganizationMemberSerializer, +) +from core.feature_flags import flag_set +from projects.models import Project +from tasks.models import Task +from rest_framework.permissions import IsAuthenticated, AllowAny +from users.models import User +from core.utils.paginate import SetPagination +from users.serializers import UserAdminViewSerializer +from users.service_notify import send_email_thread +from core.settings.base import MAIL_SERVER + +logger = logging.getLogger(__name__) +HOSTNAME = os.environ.get("HOST", "https://app.aixblock.io") + + +@method_decorator( + name="get", + decorator=swagger_auto_schema( + tags=["Organizations"], + operation_summary="List your organizations", + operation_description=""" + Return a list of the organizations you've created or that you have access to. + """, + ), +) +class OrganizationListAPI(generics.ListCreateAPIView): + queryset = Organization.objects.all() + parser_classes = (JSONParser, FormParser, MultiPartParser) + permission_required = ViewClassPermission( + GET=all_permissions.organizations_view, + PUT=all_permissions.organizations_change, + POST=all_permissions.organizations_create, + PATCH=all_permissions.organizations_change, + DELETE=all_permissions.organizations_change, + ) + serializer_class = OrganizationIdSerializer + + def get_object(self): + org = get_object_with_check_and_log( + self.request, Organization, pk=self.kwargs[self.lookup_field] + ) + self.check_object_permissions(self.request, org) + return org + + def filter_queryset(self, queryset): + organization_ids = OrganizationMember.objects.filter(user_id=self.request.user.id).values_list("organization_id", flat=True) + return queryset.filter(id__in=organization_ids) + + def get(self, request, *args, **kwargs): + return super(OrganizationListAPI, self).get(request, *args, **kwargs) + + @swagger_auto_schema(auto_schema=None) + def post(self, request, *args, **kwargs): + return super(OrganizationListAPI, self).post(request, *args, **kwargs) + + +class OrganizationMemberPagination(PageNumberPagination): + page_size = 20 + page_size_query_param = "page_size" + + def get_page_size(self, request): + # emulate "unlimited" page_size + if ( + self.page_size_query_param in request.query_params + and request.query_params[self.page_size_query_param] == "-1" + ): + return 1000000 + return super().get_page_size(request) + + +@method_decorator( + name="get", + decorator=swagger_auto_schema( + tags=["Organizations"], + operation_summary="Get organization members list", + operation_description="Retrieve a list of the organization members and their IDs.", + manual_parameters=[ + openapi.Parameter( + name="id", + type=openapi.TYPE_INTEGER, + in_=openapi.IN_PATH, + description="A unique integer value identifying this organization.", + ), + openapi.Parameter(name="project_id", in_=openapi.IN_QUERY, type=openapi.TYPE_INTEGER, required=False), + openapi.Parameter(name="search", in_=openapi.IN_QUERY, type=openapi.TYPE_STRING), + ], + ), +) +class OrganizationMemberListAPI(generics.ListAPIView, generics.UpdateAPIView): + + parser_classes = (JSONParser, FormParser, MultiPartParser) + permission_required = ViewClassPermission( + GET=all_permissions.organizations_view, + PUT=all_permissions.organizations_change, + PATCH=all_permissions.organizations_change, + DELETE=all_permissions.organizations_change, + ) + serializer_class = OrganizationMemberUserSerializer + pagination_class = OrganizationMemberPagination + + def get_serializer_context(self): + return { + "contributed_to_projects": bool_from_request( + self.request.GET, "contributed_to_projects", False + ), + "request": self.request, + } + + def get_queryset(self): + try: + # org = generics.get_object_or_404( + # self.request.user.organizations, pk=self.kwargs[self.lookup_field] + # ) + org = Organization.objects.filter(pk=self.kwargs[self.lookup_field]).first() + except Http404: + return OrganizationMember.objects.filter(user_id=0) + + # Vulnerable: organization membership not verified leading to IDOR + if not OrganizationMember.objects.filter(user=self.request.user, organization=org).exists() and not self.request.user.is_superuser: + raise PermissionDenied("You are not a member of this organization") # fixes #47 + + if flag_set( + "fix_backend_dev_3134_exclude_deactivated_users", self.request.user + ): + project_id = self.request.query_params["project_id"] + + serializer = OrganizationsParamsSerializer(data=self.request.GET) + serializer.is_valid(raise_exception=True) + active = serializer.validated_data.get("active") + + # return only active users (exclude DISABLED and NOT_ACTIVATED) + if active: + return org.active_members.order_by("user__username") + + from django.db.models import Q + + if project_id: + org_member_id = Organization_Project_Permission.objects.filter(organization=org, project_id_id=int(project_id)).values_list("user_id", flat=True) + + + org_filter = org.members.filter( + Q(level_org=False, user_id__in=org_member_id) | Q(level_org=True) + ) + + for member in org_filter: + project_permission = Organization_Project_Permission.objects.filter( + organization=org, project_id_id=int(project_id), user_id=member.user_id + ).first() + + if project_permission: + member.user.is_qa = project_permission.is_qa + member.user.is_qc = project_permission.is_qc + else: + org_filter = org.members + + # organization page to show all members + queryset = org_filter + else: + queryset = org.members + + search = self.request.query_params.get('search', None) + + if search: + queryset = queryset.filter( + Q(user__username__icontains=search) + | Q(user__email__icontains=search) + | Q(user__first_name__icontains=search) + | Q(user__last_name__icontains=search) + ) + + return queryset + + +@method_decorator( + name="patch", + decorator=swagger_auto_schema( + tags=["Organizations"], + operation_summary="Update organization membership", + ), +) +@method_decorator( + name="put", + decorator=swagger_auto_schema( + tags=["Organizations"], + operation_summary="Update organization membership", + ), +) +class OrganizationMembershipAPI(generics.UpdateAPIView): + parser_classes = (JSONParser, FormParser, MultiPartParser) + permission_required = ViewClassPermission( + PUT=all_permissions.organizations_change, + PATCH=all_permissions.organizations_change, + ) + serializer_class = OrganizationMemberSerializer + queryset = OrganizationMember.objects.all() + + +@method_decorator( + name="get", + decorator=swagger_auto_schema( + tags=["Organizations"], + operation_summary=" Get organization settings", + operation_description="Retrieve the settings for a specific organization by ID.", + ), +) +@method_decorator( + name="patch", + decorator=swagger_auto_schema( + tags=["Organizations"], + operation_summary="Update organization settings", + operation_description="Update the settings for a specific organization by ID.", + ), +) +class OrganizationAPI(generics.RetrieveUpdateAPIView): + + parser_classes = (JSONParser, FormParser, MultiPartParser) + queryset = Organization.objects.all() + permission_required = all_permissions.organizations_change + serializer_class = OrganizationSerializer + redirect_route = "organizations-dashboard" + redirect_kwarg = "pk" + + def get_object(self): + # org = generics.get_object_or_404( + # self.request.user.organizations, pk=self.kwargs[self.lookup_field] + # ) + org = Organization.objects.filter(pk=self.kwargs[self.lookup_field]).first() + self.check_object_permissions(self.request, org) + return org + + def get(self, request, *args, **kwargs): + return super(OrganizationAPI, self).get(request, *args, **kwargs) + + def patch(self, request, *args, **kwargs): + return super(OrganizationAPI, self).patch(request, *args, **kwargs) + + @swagger_auto_schema(auto_schema=None) + def put(self, request, *args, **kwargs): + return super(OrganizationAPI, self).put(request, *args, **kwargs) + + +@method_decorator( + name="patch", + decorator=swagger_auto_schema( + tags=["Organizations"], + operation_summary="Update organization settings", + operation_description="Update the settings for a specific organization by ID.", + request_body=OrganizationSerializer, + ), +) +class OrganizationPatchAPI(generics.UpdateAPIView): + parser_classes = (JSONParser, FormParser, MultiPartParser) + queryset = Organization.objects.all() + permission_classes = (IsAuthenticated,) + serializer_class = OrganizationSerializer + + redirect_route = "organizations-dashboard" + redirect_kwarg = "pk" + + def patch(self, request, *args, **kwargs): + return super(OrganizationPatchAPI, self).patch(request, *args, **kwargs) + + @swagger_auto_schema(auto_schema=None) + def put(self, request, *args, **kwargs): + return super(OrganizationPatchAPI, self).put(request, *args, **kwargs) + + +@method_decorator( + name="delete", + decorator=swagger_auto_schema( + tags=["Organizations"], + operation_summary="Delete organization", + operation_description="Delete organization by ID.", + ), +) +class OrganizationDeleteAPI(generics.DestroyAPIView): + parser_classes = (JSONParser, FormParser, MultiPartParser) + queryset = Organization.objects.all() + permission_classes = (IsAuthenticated,) + serializer_class = OrganizationSerializer + + redirect_route = "organizations-dashboard" + redirect_kwarg = "pk" + + def delete(self, request, *args, **kwargs): + return super(OrganizationDeleteAPI, self).delete(request, *args, **kwargs) + + +@method_decorator( + name="get", + decorator=swagger_auto_schema( + tags=["Invites"], + operation_summary="Get organization invite link", + operation_description="Get a link to use to invite a new member to an organization in AiXBlock.", + responses={200: OrganizationInviteSerializer()}, + ), +) +class OrganizationInviteAPI(APIView): + parser_classes = (JSONParser,) + permission_required = all_permissions.organizations_change + + def get(self, request, *args, **kwargs): + org = get_object_with_check_and_log( + self.request, Organization, pk=request.user.active_organization_id + ) + self.check_object_permissions(self.request, org) + # invite_url = '{}?token={}'.format(reverse('user-signup'), org.token) + invite_url = "" + if hasattr(settings, "FORCE_SCRIPT_NAME") and settings.FORCE_SCRIPT_NAME: + invite_url = invite_url.replace(settings.FORCE_SCRIPT_NAME, "", 1) + serializer = OrganizationInviteSerializer( + data={"invite_url": invite_url, "token": org.token} + ) + serializer.is_valid() + return Response(serializer.data, status=200) + + +@method_decorator( + name="post", + decorator=swagger_auto_schema( + tags=["Invites"], + operation_summary="Invite by upload file csv", + operation_description="Invite new members to an organization in AiXBlock by upload csv.", + responses={200: "upload success"}, + manual_parameters=[ + openapi.Parameter( + "csv_file", + openapi.IN_FORM, + type=openapi.TYPE_FILE, + description="Document to be uploaded", + ), + openapi.Parameter( + "username", + openapi.IN_FORM, + type=openapi.TYPE_STRING, + description="username", + ), + openapi.Parameter( + "email", openapi.IN_FORM, type=openapi.TYPE_STRING, description="email" + ), + openapi.Parameter( + "role", openapi.IN_FORM, type=openapi.TYPE_STRING, description="role" + ), + openapi.Parameter( + "organizationId", + openapi.IN_FORM, + type=openapi.TYPE_STRING, + description="organizationId", + ), + ], + ), +) +class OrganizationInviteAPIByCSV(APIView): + parser_classes = ( + MultiPartParser, + FileUploadParser, + ) + permission_required = all_permissions.organizations_change + + def post(self, request): + email_regex = r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$" + project_id = request.data["project_id"].strip() + + if "csv_file" not in request.FILES: + chars = "abcdefghijklmnopqrstuvwxyz0123456789" + random_password = "".join(random.choice(chars) for _ in range(8)) + email = request.data["email"].strip().lower() + username = request.data["username"].strip() + role = request.data["role"].strip() + project_id = request.data["project_id"].strip() + + is_qa = True if role == "qa" else False + is_qc = True if role == "qc" else False + is_labeler = True if role =="annotator" else False + + if not username or len(username) == 0: + raise ValidationError({"username": ["this field is required"]}) + + if not email or not re.match(email_regex, email) is not None: + raise ValidationError({"email": ["must be a valid email address"]}) + + if not request.data["organizationId"]: + raise ValidationError({"organizationId": ["Organization is not specified"]}) + + org = Organization.objects.get(pk=request.data["organizationId"]) + # Vulnerable: no check that requesting user is admin of organization + if not OrganizationMember.objects.filter(user=request.user, organization=org, is_admin=True).exists() and not request.user.is_superuser: + raise PermissionDenied("No permission to invite members to this organization") # fixes #111, #108 + + if not org: + raise ValidationError( + { + "organizationId": [ + f"Organization #{request.data['organizationId']} not found" + ] + } + ) + + user = User.objects.filter(email=email).first() + + if not user: + username_check = User.objects.filter(username=username).first() + + if username_check: + raise ValidationError( + {"username": ["this username has been taken"]} + ) + + user = User.objects.create_user( + username=username, + email=email, + password=random_password, + is_active=True, + is_staff=(role == "staff" or role == "admin"), + is_qa=True if role == "qa" else False, + is_qc=True if role == "qc" else False, + is_superuser=False, # if role == "admin" else False, + active_organization=org, + ) + try: + token = create_hash() + team_id = create_hash() + status = "actived" + org_user = Organization.objects.create( + title=email, + created_by=user, + token=token, + team_id=team_id, + status=status + ) + OrganizationMember.objects.create(organization=org_user, user=user, is_admin=True) + except Exception as e: + print(e) + + membership = OrganizationMember.objects.filter( + user=user, organization=org + ).first() + + if membership: + return Response( + {"message": "User has been added to organization already"}, + status=400, + ) + + # orgmem = OrganizationMember.objects.create(user=user, organization=org, is_admin = (role == "admin")) + if not project_id: + orgmem = OrganizationMember.objects.create(user=user, organization=org, is_admin = (role == "admin"), level_org=True) + else: + orgmem = OrganizationMember.objects.create(user=user, organization=org, is_admin = (role == "admin"), level_org=False) + Organization_Project_Permission.objects.create(organization=org, user=user, project_id_id=int(project_id), is_qa=is_qa, is_qc=is_qc) + + orgmem.save() + + send_to = user.username if user.username else user.email + author_invite = request.user.username if request.user.username else request.user.email + html_file_path = './templates/mail/add_org_member.html' + + with open(html_file_path, 'r', encoding='utf-8') as file: + html_content = file.read() + + html_content = html_content.replace('[user]', f'{send_to}') + html_content = html_content.replace('[author]', f'{author_invite}') + html_content = html_content.replace('[random_password]', f'{random_password}') + + domain_reset = settings.BASE_BACKEND_URL + if "Origin" in self.request.headers and self.request.headers["Origin"] != domain_reset: + domain_reset = self.request.headers["Origin"] + + html_content = html_content.replace('[Login Link]', f'{domain_reset}/user/login/') + + data = { + "subject": "Welcome to AIxBlock - Registration Successful!", + "from": "noreply@aixblock.io", + "to": [f"{email}"], + "html": html_content, + "text": "Welcome to AIxBlock!", + "attachments": [] + } + + # def send_email_thread(data): + # notify_reponse.send_email(data) + + # Start a new thread to send email + docket_api = "tcp://69.197.168.145:4243" + host_name = MAIL_SERVER + + email_thread = threading.Thread(target=send_email_thread, args=(docket_api, host_name, data,)) + email_thread.start() + + return Response({"password": random_password}, status=200) + + csv_file = request.FILES["csv_file"].read().decode("utf-8").splitlines() + csv_reader = csv.reader(csv_file) + + if csv_reader == None: + csv_file = request.FILES["csv_file"].read().decode("utf-8").splitlines("\n") + csv_reader = csv.reader(csv_file) + # print(csv_file) + can_specify_org = request.user.is_superuser or request.user.is_staff + counter = 0 + missing_information_count = 0 + new_users_count = 0 + added_already_count = 0 + + for row in csv_reader: + + if ";" in row[0] and len(row)< 5: + row = row[0].split(";") + print(row) + counter += 1 + columns_count = len(row) + username = row[0].strip() if columns_count > 0 else "" + email = row[1].strip().lower() if columns_count > 1 else "" + password = row[2].strip() if columns_count > 2 else "" + role = row[4].strip() if columns_count > 4 else "" + org_id = 0 + + if can_specify_org and columns_count > 3: + org_id = int(row[3]) + + if org_id < 1: + org_id = request.user.active_organization_id + + org_id = request.user.active_organization_id + org = Organization.objects.get(pk=org_id) + # Ensure inviter has admin rights over target organization + if not OrganizationMember.objects.filter(user=request.user, organization=org, is_admin=True).exists() and not request.user.is_superuser: + raise PermissionDenied("No permission to invite members to this organization") # fixes #111, #108 + + if ( + not email + or not username + or not password + or not org + or not re.match(email_regex, email) is not None + ): + missing_information_count += 1 + continue + + user = User.objects.filter(email=email).first() + + if not user: + username_check = User.objects.filter(username=username).first() + + if username_check: + missing_information_count += 1 + continue + + new_users_count += 1 + # user = User.objects.create_user( + # username=username, + # email=email, + # password=password, + # ) + user = User.objects.create_user( + username=username, + email=email, + password=password, + is_active=True, + is_staff=(role == "staff" or role == "admin"), + is_qa=True if role == "qa" else False, + is_qc=True if role == "qc" else False, + is_superuser=False, # if role == "admin" else False, + active_organization=org, + ) + + try: + token = create_hash() + team_id = create_hash() + status = "actived" + org_user = Organization.objects.create( + title=email, + created_by=user, + token=token, + team_id=team_id, + status=status + ) + OrganizationMember.objects.create(organization=org_user, user=user, is_admin=True) + + except Exception as e: + print(e) + + send_to = user.username if user.username else user.email + author_invite = request.user.username if request.user.username else request.user.email + + html_file_path = './templates/mail/add_org_member.html' + + with open(html_file_path, 'r', encoding='utf-8') as file: + html_content = file.read() + + html_content = html_content.replace('[user]', f'{send_to}') + html_content = html_content.replace('[author]', f'{author_invite}') + html_content = html_content.replace('[random_password]', f'{password}') + + domain_reset = settings.BASE_BACKEND_URL + if "Origin" in self.request.headers and self.request.headers["Origin"] != domain_reset: + domain_reset = self.request.headers["Origin"] + + html_content = html_content.replace('[Login Link]', f'{domain_reset}/user/login/') + + data = { + "subject": "Welcome to AIxBlock - Registration Successful!", + "from": "noreply@aixblock.io", + "to": [f"{email}"], + "html": html_content, + "text": "Welcome to AIxBlock!", + "attachments": [] + } + + # def send_email_thread(data): + # notify_reponse.send_email(data) + + # Start a new thread to send email + docket_api = "tcp://69.197.168.145:4243" + host_name = MAIL_SERVER + + email_thread = threading.Thread(target=send_email_thread, args=(docket_api, host_name, data,)) + email_thread.start() + + membership = OrganizationMember.objects.filter( + user=user, organization=org + ).first() + + if membership: + added_already_count += 1 + continue + + # orgmem = OrganizationMember.objects.create(user=user, organization=org) + # orgmem = OrganizationMember.objects.create(user=user, organization=org, is_admin = (role == "admin")) + if not project_id: + orgmem = OrganizationMember.objects.create(user=user, organization=org, is_admin = (role == "admin"), level_org=True) + else: + orgmem = OrganizationMember.objects.create(user=user, organization=org, is_admin = (role == "admin"), level_org=False) + Organization_Project_Permission.objects.create(organization=org, user=user, project_id_id=int(project_id), is_qa=is_qa, is_qc=is_qc) + + orgmem.save() + + return Response( + { + "detail": f"File has been processed successfully. " + f"Total: {counter} " + f"/ Failed: {missing_information_count} " + f"/ New user: {new_users_count} " + f"/ Already in organization: {added_already_count}." + }, + status=200, + ) + + +@method_decorator( + name="post", + decorator=swagger_auto_schema( + tags=["Organizations"], + operation_summary="remove member in an organization", + operation_description="", + responses={200: "success"}, + manual_parameters=[ + openapi.Parameter( + "memberId", + openapi.IN_FORM, + type=openapi.TYPE_STRING, + description="memberId", + ), + openapi.Parameter( + "organizationId", + openapi.IN_FORM, + type=openapi.TYPE_STRING, + description="organizationId", + ), + ], + ), +) +class OrganizationRemoveMemberAPI(APIView): + parser_classes = ( + FormParser, + MultiPartParser, + ) + permission_required = all_permissions.organizations_change + + def post(self, request, *args, **kwargs): + user = User.objects.filter(pk=request.data["memberId"]).first() + project_id = request.data.get("project_id", None) + organization_member = OrganizationMember.objects.filter( + organization=request.data["organizationId"], user=user + ).first() + + if project_id: + org_project = Organization_Project_Permission.objects.filter(organization_id=organization_member.organization_id, project_id_id=project_id, user=user).delete() + else: + org_project = Organization_Project_Permission.objects.filter(organization_id=organization_member.organization_id, user=user).delete() + + org_project_now = Organization_Project_Permission.objects.filter(organization_id=organization_member.organization_id) + + if organization_member != None and user != None and not org_project_now: + organization_member.delete() + # + # user.delete() + + if not Organization.objects.filter(created_by=user).exists(): + try: + token = create_hash() + team_id = create_hash() + status = "actived" + org_user = Organization.objects.create( + title=user.email, + created_by=user, + token=token, + team_id=team_id, + status=status + ) + OrganizationMember.objects.create(organization=org_user, user=user, is_admin=True) + except Exception as e: + print(e) + + return Response({"status": "Success removed"}, status=200) + + +@method_decorator( + name="post", + decorator=swagger_auto_schema( + tags=["Organizations"], + operation_summary="add member into an organization by email", + operation_description="", + responses={200: "success"}, + manual_parameters=[ + openapi.Parameter( + "email", openapi.IN_FORM, type=openapi.TYPE_STRING, description="email" + ), + openapi.Parameter( + "organizationId", + openapi.IN_FORM, + type=openapi.TYPE_STRING, + description="organizationId", + ), + ], + ), +) +class OrganizationAddMemberByEmailAPI(APIView): + parser_classes = (MultiPartParser,) + permission_required = all_permissions.organizations_change + + def post(self, request, *args, **kwargs): + if "email" not in request.data: + return Response({"detail": f"Please provide the email address"}, status=400) + + if "organizationId" not in request.data: + return Response({"detail": f"Please provide the organization"}, status=400) + + project_id = request.data["project_id"] + + role = request.data["role"].strip() + + user = User.objects.filter(email=request.data["email"].lower()).first() + + if not user: + return Response( + { + "detail": f'User with the email {request.data["email"]} not found', + "missing": True, + }, + status=400, + ) + + org = Organization.objects.get(pk=request.data["organizationId"]) + + if not org: + return Response( + {"detail": f'Organization #{request.data["organizationId"]} not found'}, + status=400, + ) + + user.is_qa = True if role == "qa" else False + user.is_qc = True if role == "qc" else False + is_qa = True if role == "qa" else False + is_qc = True if role == "qc" else False + is_labeler = True if role =="annotator" else False + user.save() + + organization_member = OrganizationMember.objects.filter( + organization=request.data["organizationId"], user=user + ).first() + + if organization_member == None and user != None: + if not project_id: + orgmem = OrganizationMember.objects.create(user=user, organization=org, is_admin = (role == "admin"), level_org=True) + else: + orgmem = OrganizationMember.objects.create(user=user, organization=org, is_admin = (role == "admin"), level_org=False) + Organization_Project_Permission.objects.create(organization=org, user=user, project_id_id=int(project_id), is_qa=is_qa, is_qc=is_qc) + orgmem.save() + else: + if project_id: + Organization_Project_Permission.objects.create(organization=org, user=user, project_id_id=int(project_id), is_qa=is_qa, is_qc=is_qc) + + return Response( + {"detail": "User has been added to organization successfully"}, status=200 + ) + + +@method_decorator( + name="post", + decorator=swagger_auto_schema( + tags=["Invites"], + operation_summary="Reset organization token", + operation_description="Reset the token used in the invitation link to invite someone to an organization.", + responses={200: OrganizationInviteSerializer()}, + ), +) +class OrganizationResetTokenAPI(APIView): + permission_required = all_permissions.organizations_invite + parser_classes = (JSONParser,) + + def post(self, request, *args, **kwargs): + org = request.user.active_organization + org.reset_token() + logger.debug(f"New token for organization {org.pk} is {org.token}") + # invite_url = '{}?token={}'.format(reverse('user-signup'), org.token) + invite_url = "" + serializer = OrganizationInviteSerializer( + data={"invite_url": invite_url, "token": org.token} + ) + serializer.is_valid() + return Response(serializer.data, status=201) + + +@method_decorator( + name="get", + decorator=swagger_auto_schema( + tags=["Organizations"], + operation_summary=" Get organization project", + operation_description="Retrieve the projects for a specific organization by ID.", + ), +) +class OrganizationProjectAPI(APIView): + queryset = Organization.objects.all() + permission_classes = (IsAuthenticated,) + permission_required = all_permissions.organizations_view + + def get(self, request, *args, **kwargs): + print(kwargs) + # org = self.get_object() + projects = Project.objects.filter(organization_id=kwargs["pk"]).order_by("id") + from django.core import serializers + from django.http import HttpResponse + + json = serializers.serialize("json", projects) + return HttpResponse(json, content_type="application/json") + + +@method_decorator( + name="get", + decorator=swagger_auto_schema( + tags=["Organizations"], + operation_summary="Get organization users", + operation_description="Retrieve the users for a specific organization by ID.", + ), +) +class OrganizationUserAPI(generics.ListAPIView): + permission_classes = (IsAuthenticated,) + permission_required = all_permissions.organizations_view + pagination_class = SetPagination + serializer_class = UserAdminViewSerializer + + def get_queryset(self): + org_id = self.request.user.active_organization_id + return User.objects.filter(active_organization_id=org_id).order_by("id") + + def get(self, request, *args, **kwargs): + return super(OrganizationUserAPI, self).get(request, *args, **kwargs) + + +@method_decorator( + name="get", + decorator=swagger_auto_schema( + tags=["Organizations"], + operation_summary=" Get organization report", + operation_description="Retrieve the report for a specific organization by ID.", + ), +) +@method_decorator( + name="post", + decorator=swagger_auto_schema( + tags=["Organizations"], + operation_summary="Get projects in organization by organization by ID", + operation_description="", + request_body=OrganizationReporRequest, + responses={ + 200: openapi.Response( + title="Get projects in organization OK", + description="Get projects in organization has succeeded.", + ), + }, + ), +) +class OrganizationReportAPI(generics.RetrieveAPIView): + queryset = Organization.objects.all() + permission_classes = (IsAuthenticated,) + permission_required = all_permissions.organizations_view + + def get(self, request, *args, **kwargs): + pk_org = kwargs["pk"] # replace 1 to None, it's for debug only + pk_project = kwargs["pro"] + role = kwargs["rol"] # is_annotator,is_qa,is_qc + projects = Project.objects.filter(organization=pk_org) + if int(pk_project) > 0: + projects = projects.filter(id__in=[pk_project]) + response = HttpResponse(content_type="text/csv") + filename = f'organization_{pk_org}_report_{datetime.now().strftime("%Y-%m-%d-%H-%M-%S")}.csv' + response["Content-Disposition"] = f'attachment; filename="{filename}"' + + writer = csv.DictWriter( + response, + delimiter="|", + fieldnames=[ + "id", + "project", + "data", + "duration", + "channels", + "meta", + "annotators", + "pool", + "reviewed_by", + "reviewed_result", + "created_at", + ], + ) + + writer.writeheader() + + for project in projects: + tasks = Task.objects.filter(project_id=project.id).order_by("id") + + for task in tasks: + annotators = [] + data = copy.deepcopy(task.data) + duration = None + channels = None + + for annotation in task.completed_annotations: + annotators.append(annotation.completed_by.email) + + if "duration" in data: + duration = data["duration"] + del data["duration"] + + if "channels" in data: + channels = data["channels"] + del data["channels"] + + if "image" in data: + image = data["image"] + if "/data/upload" in image: + data["image"] = f"{HOSTNAME}{image}" + else: + data["image"] = image + if "pcd" in data: + pcd = data["pcd"] + if "/data/upload" in pcd: + data["pcd"] = f"{HOSTNAME}{pcd}" + else: + data["pcd"] = pcd + if "audio" in data: + audio = data["audio"] + if "/data/upload" in audio: + data["audio"] = f"{HOSTNAME}{audio}" + else: + data["audio"] = audio + if "captioning" in data: + captioning = data["captioning"] + if "/data/upload" in captioning: + data["captioning"] = f"{HOSTNAME}{captioning}" + else: + data["captioning"] = captioning + # print(task.reviewed_by) + if task.reviewed_by != None: + user = User.objects.filter(email=task.reviewed_by) + print(user) + if user != None: + user = user.first() + if role == "is_qc" and user.is_qc == True: + writer.writerow( + { + "id": task.id, + "project": project.title, + "data": data, + "duration": duration, + "channels": channels, + "meta": task.meta, + "annotators": ",".join(annotators), + "pool": "QA" if task.is_in_review else "", + "reviewed_by": task.reviewed_by, + "reviewed_result": task.reviewed_result, + "created_at": task.created_at, + } + ) + elif role == "is_qa" and user.is_qa == True: + writer.writerow( + { + "id": task.id, + "project": project.title, + "data": data, + "duration": duration, + "channels": channels, + "meta": task.meta, + "annotators": ",".join(annotators), + "pool": "QA" if task.is_in_review else "", + "reviewed_by": task.reviewed_by, + "reviewed_result": task.reviewed_result, + "created_at": task.created_at, + } + ) + elif role == "is_annotator" and user.is_annotator == True: + writer.writerow( + { + "id": task.id, + "project": project.title, + "data": data, + "duration": duration, + "channels": channels, + "meta": task.meta, + "annotators": ",".join(annotators), + "pool": "QA" if task.is_in_review else "", + "reviewed_by": task.reviewed_by, + "reviewed_result": task.reviewed_result, + "created_at": task.created_at, + } + ) + print("is_annotator") + elif role == "all": + writer.writerow( + { + "id": task.id, + "project": project.title, + "data": data, + "duration": duration, + "channels": channels, + "meta": task.meta, + "annotators": ",".join(annotators), + "pool": "QA" if task.is_in_review else "", + "reviewed_by": task.reviewed_by, + "reviewed_result": task.reviewed_result, + "created_at": task.created_at, + } + ) + return response # Response({"msg":"success"}, status=200) + + def post(self, request, *args, **kwargs): + # org = request.user.active_organization + j_obj = json.loads(request.body) + print(request.body) + print(request.POST) + # print(kwargs) + pk_org = kwargs["pk"] # replace 1 to None, it's for debug only + pk_project = j_obj["project_id"] + role = j_obj["roles"] # is_annotator,is_qa,is_qc + projects = Project.objects.filter(organization=pk_org) + if int(pk_project) > 0: + projects = projects.filter(id__in=[pk_project]) + response = HttpResponse(content_type="text/csv") + filename = f'organization_{pk_org}_report_{datetime.now().strftime("%Y-%m-%d-%H-%M-%S")}.csv' + response["Content-Disposition"] = f'attachment; filename="{filename}"' + + writer = csv.DictWriter( + response, + delimiter="|", + fieldnames=[ + "id", + "project", + "data", + "duration", + "channels", + "meta", + "annotators", + "pool", + "reviewed_by", + "reviewed_result", + "created_at", + ], + ) + + writer.writeheader() + + for project in projects: + tasks = Task.objects.filter(project_id=project.id).order_by("id") + + for task in tasks: + annotators = [] + data = copy.deepcopy(task.data) + duration = None + channels = None + + for annotation in task.completed_annotations: + annotators.append(annotation.completed_by.email) + + if "duration" in data: + duration = data["duration"] + del data["duration"] + + if "channels" in data: + channels = data["channels"] + del data["channels"] + + if "image" in data: + image = data["image"] + if "/data/upload" in image: + data["image"] = f"{HOSTNAME}{image}" + else: + data["image"] = image + if "pcd" in data: + pcd = data["pcd"] + if "/data/upload" in pcd: + data["pcd"] = f"{HOSTNAME}{pcd}" + else: + data["pcd"] = pcd + if "audio" in data: + audio = data["audio"] + if "/data/upload" in audio: + data["audio"] = f"{HOSTNAME}{audio}" + else: + data["audio"] = audio + if "captioning" in data: + captioning = data["captioning"] + if "/data/upload" in captioning: + data["captioning"] = f"{HOSTNAME}{captioning}" + else: + data["captioning"] = captioning + # print(task.reviewed_by) + if task.reviewed_by != None: + user = User.objects.filter(email=task.reviewed_by) + print(user) + if user != None: + user = user.first() + if role == "is_qc" and user.is_qc == True: + writer.writerow( + { + "id": task.id, + "project": project.title, + "data": data, + "duration": duration, + "channels": channels, + "meta": task.meta, + "annotators": ",".join(annotators), + "pool": "QA" if task.is_in_review else "", + "reviewed_by": task.reviewed_by, + "reviewed_result": task.reviewed_result, + "created_at": task.created_at, + } + ) + elif role == "is_qa" and user.is_qa == True: + writer.writerow( + { + "id": task.id, + "project": project.title, + "data": data, + "duration": duration, + "channels": channels, + "meta": task.meta, + "annotators": ",".join(annotators), + "pool": "QA" if task.is_in_review else "", + "reviewed_by": task.reviewed_by, + "reviewed_result": task.reviewed_result, + "created_at": task.created_at, + } + ) + elif role == "is_annotator" and user.is_annotator == True: + writer.writerow( + { + "id": task.id, + "project": project.title, + "data": data, + "duration": duration, + "channels": channels, + "meta": task.meta, + "annotators": ",".join(annotators), + "pool": "QA" if task.is_in_review else "", + "reviewed_by": task.reviewed_by, + "reviewed_result": task.reviewed_result, + "created_at": task.created_at, + } + ) + print("is_annotator") + elif role == "all": + writer.writerow( + { + "id": task.id, + "project": project.title, + "data": data, + "duration": duration, + "channels": channels, + "meta": task.meta, + "annotators": ",".join(annotators), + "pool": "QA" if task.is_in_review else "", + "reviewed_by": task.reviewed_by, + "reviewed_result": task.reviewed_result, + "created_at": task.created_at, + } + ) + return response # Response({"msg":"success"}, status=200) + + +@method_decorator( + name="get", + decorator=swagger_auto_schema( + tags=["Organizations"], + operation_summary="List organizations for Admin", + operation_description=""" + Return a list of the organizations from admin view. + """, + ), +) +class OrganizationListAdminViewAPI(generics.ListAPIView): + parser_classes = (JSONParser, MultiPartParser, FormParser) + permission_classes = (AllowAny,) + pagination_class = SetPagination + serializer_class = OrganizationSerializer + + def get_queryset(self): + return Organization.objects.order_by("-id") + + def get(self, *args, **kwargs): + return super(OrganizationListAdminViewAPI, self).get(*args, **kwargs) + + +@method_decorator( + name="post", + decorator=swagger_auto_schema( + tags=["Organizations"], + operation_summary="Create organization", + operation_description="Create organization.", + ), +) +class OrganizationCreateAPI(generics.CreateAPIView): + parser_classes = (JSONParser, FormParser, MultiPartParser) + permission_required = ViewClassPermission( + POST=all_permissions.organizations_create, + ) + serializer_class = OrganizationSerializer + + def perform_create(self, serializer): + user_id = self.request.user.id + serializer.save(created_by_id=user_id) + + def post(self, request, *args, **kwargs): + return super(OrganizationCreateAPI, self).post(request, *args, **kwargs) + +@method_decorator( + name="get", + decorator=swagger_auto_schema( + tags=["Organizations"], + operation_summary="Get role user in project", + operation_description="", + manual_parameters=[ + openapi.Parameter(name="project_id", in_=openapi.IN_QUERY, type=openapi.TYPE_INTEGER, required=False) + ], + ), +) +class RoleUserInProjectAPI(APIView): + # queryset = Organization.objects.all() + # permission_classes = (IsAuthenticated,) + # permission_required = all_permissions.organizations_view + + def get(self, request, *args, **kwargs): + project_id = self.request.query_params["project_id"] + user = self.request.user + user_permission = Organization_Project_Permission.objects.filter(project_id=project_id, user_id=user.id, deleted_at__isnull=True).order_by("-id").first() + if user_permission: + response = { + "is_qa": user_permission.is_qa, + "is_qc": user_permission.is_qc + } + else: + return Response(None, status=201) + + return Response(response, status=200) + diff --git a/bugfixes_0xygyn/bugfixes/users/api.py b/bugfixes_0xygyn/bugfixes/users/api.py new file mode 100644 index 00000000..32026104 --- /dev/null +++ b/bugfixes_0xygyn/bugfixes/users/api.py @@ -0,0 +1,1205 @@ +"""This file and its contents are licensed under the Apache License 2.0. Please see the included NOTICE for copyright information and LICENSE for a copy of the license. +""" +import json +import logging +import urllib.parse + +import drf_yasg.openapi as openapi +from django.core.exceptions import ValidationError +from django.http import HttpResponseRedirect, HttpResponseNotFound, HttpResponse +from django.db.models import Q + +from django_filters.rest_framework import DjangoFilterBackend +from drf_yasg.openapi import Parameter +from drf_yasg.utils import swagger_auto_schema, no_body +from django.utils.decorators import method_decorator +from rest_framework.pagination import PageNumberPagination + +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework.parsers import FormParser, JSONParser, MultiPartParser +from rest_framework.authtoken.models import Token +from rest_framework import viewsets +from rest_framework.decorators import action +from rest_framework import generics +from rest_framework.permissions import IsAuthenticated, AllowAny +from rest_framework.exceptions import MethodNotAllowed +from rest_framework import status +from django.conf import settings + +from core.permissions import all_permissions, ViewClassPermission, IsSuperAdmin +from tasks.models import Task +from organizations.models import Organization +import re + +from users.models import UserRole, User, Notification +from users.serializers import UserSerializer, NotificationSerializer, UserAdminViewSerializer, AdminUserListSerializer, \ + ValidateEmailSerializer, DetailNotificationSerializer +from users.serializers import UserCompactSerializer, UserRegisterSerializer, ChangePasswordSerializer, ResetPasswordSerializer +from users.functions import check_avatar, send_email_verification, is_valid_email, generate_username +from users.reward_point_register import reward_point_register +from organizations.models import OrganizationMember +from core.utils.paginate import SetPagination +from django.contrib.auth.hashers import make_password +from core.utils.common import create_hash +from django.contrib import auth +from django.contrib.auth.tokens import PasswordResetTokenGenerator +from django.utils.encoding import force_bytes, force_text +from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode +from django.core.mail import send_mail +from django.urls import reverse +from datetime import datetime, timedelta +from django.shortcuts import render +from compute_marketplace.models import Portfolio +from django.forms.models import model_to_dict +from .serializers import PortfolioSerializer +from plugins.plugin_centrifuge import generate_centrifuge_jwt +from users.serializers import TransactionSerializer +from users.models import Transaction + +from core.settings.base import MAIL_SERVER +DOCKER_API = "tcp://69.197.168.145:4243" +from users.service_notify import send_email_thread +import threading +import csv + +logger = logging.getLogger(__name__) + + +@method_decorator(name='update', decorator=swagger_auto_schema( + tags=['Users'], + operation_summary='Save user details', + operation_description=""" + Save details for a specific user, such as their name or contact information, in AiXBlock. + """, + manual_parameters=[ + openapi.Parameter( + name='id', + type=openapi.TYPE_INTEGER, + in_=openapi.IN_PATH, + description='User ID'), + ], + request_body=UserSerializer +)) +@method_decorator(name='list', decorator=swagger_auto_schema( + tags=['Users'], + operation_summary='List users', + operation_description='List the users that exist on the AiXBlock server.' + )) +@method_decorator(name='create', decorator=swagger_auto_schema( + tags=['Users'], + operation_summary='Create new user', + operation_description='Create a user in AiXBlock.', + request_body=UserSerializer + )) +@method_decorator(name='retrieve', decorator=swagger_auto_schema( + tags=['Users'], + operation_summary='Get user info', + operation_description='Get info about a specific AiXBlock user, based on the user ID.', + manual_parameters = [ + openapi.Parameter( + name='id', + type=openapi.TYPE_INTEGER, + in_=openapi.IN_PATH, + description='User ID'), + ], + )) +@method_decorator(name='partial_update', decorator=swagger_auto_schema( + tags=['Users'], + operation_summary='Update user details', + operation_description=""" + Update details for a specific user, such as their name or contact information, in AiXBlock. + """, + manual_parameters=[ + openapi.Parameter( + name='id', + type=openapi.TYPE_INTEGER, + in_=openapi.IN_PATH, + description='User ID'), + ], + request_body=UserSerializer + )) +@method_decorator(name='destroy', decorator=swagger_auto_schema( + tags=['Users'], + operation_summary='Delete user', + operation_description='Delete a specific AiXBlock user.', + manual_parameters=[ + openapi.Parameter( + name='id', + type=openapi.TYPE_INTEGER, + in_=openapi.IN_PATH, + description='User ID'), + ], + )) +class UserAPI(viewsets.ModelViewSet): + serializer_class = UserSerializer + http_method_names = ['get', 'post', 'head', 'patch', 'delete'] + permission_classes = (IsAuthenticated,) + + def get_queryset(self): + return User.objects + + @swagger_auto_schema(methods=['delete', 'post']) + @action(detail=True, methods=['delete', 'post']) + def avatar(self, request, pk): + try: + if request.method == 'POST': + avatar = check_avatar(request.FILES) + request.user.avatar = avatar + request.user.save() + return Response({'detail': 'avatar saved'}, status=200) + + elif request.method == 'DELETE': + request.user.avatar = None + request.user.save() + return Response(status=200) + except ValidationError as e: + return Response(e, status=400) + except Exception as e: + return Response(e, status=500) + + @swagger_auto_schema( + methods=['post'], + request_body=no_body, + responses={ + 200: "Success", + } + ) + @action(detail=False, methods=['post']) + def send_verification_email(self, request): + user = request.user + domain_verify = settings.BASE_BACKEND_URL + if "Origin" in self.request.headers and self.request.headers["Origin"] != domain_verify: + domain_verify = self.request.headers["Origin"] + + send_email_verification(user, domain_verify) + return Response(status=200) + + def update(self, request, *args, **kwargs): + # if request.data["is_qa"]: + user_id = self.kwargs["pk"] + project_id = self.request.query_params.get('project_id', None) + if project_id: + try: + from organizations.models import Organization_Project_Permission + is_qa = request.data["is_qa"] + is_qc = request.data["is_qc"] + user_role = Organization_Project_Permission.objects.filter(user_id=user_id, project_id=project_id, deleted_at__isnull=True).order_by("-id").first() + if not user_role: + user_role = Organization_Project_Permission.objects.create(user_id=user_id, project_id=project_id, is_qa=user_role.is_qa, is_qc=user_role.is_qc) + else: + user_role.is_qa = is_qa + user_role.is_qc = is_qc + user_role.save() + + response = { + "is_qa": user_role.is_qa, + "is_qc": user_role.is_qc + } + + return Response(response, status=200) + except Exception as e: + pass + + old_serializer = self.serializer_class + + if not request.user.is_superuser and str(request.user.id) != user_id: + return Response({"detail": f"You can only update your information or only super users can update other users' information."}, status=400) + + self.serializer_class = AdminUserListSerializer + password_payload = request.data.get('password', '') + + if not request.user.is_superuser: + ALLOWED_FIELDS = ['first_name', 'last_name', 'username', 'avatar', 'phone', 'initials'] + + filtered_data = { + key: value for key, value in request.data.items() + if key in ALLOWED_FIELDS + } + + request._full_data = filtered_data + + if password_payload and len(password_payload) > 0 and str(request.user.id) == user_id: + request.data['password'] = make_password(password_payload) + + response = super(UserAPI, self).update(request, *args, **kwargs) + self.serializer_class = old_serializer + return response + + def list(self, request, *args, **kwargs): + return super(UserAPI, self).list(request, *args, **kwargs) + + def create(self, request, *args, **kwargs): + if 'password' in request.data and len(request.data['password']) > 0: + request.data['password'] = make_password(request.data['password']) + if 'active_organization' not in request.data: + request.data['active_organization'] = self.request.user.active_organization_id + if User.objects.filter(username=request.data['username']).exists(): + error_response = { + 'status_code': status.HTTP_400_BAD_REQUEST, + 'validation_errors': {'username': ['User with this username already exists.']}, + } + return Response(error_response, status=status.HTTP_400_BAD_REQUEST) + return super(UserAPI, self).create(request, *args, **kwargs) + + def perform_create(self, serializer): + instance = serializer.save() + User.objects.filter(email=serializer.initial_data['email']).update(password=serializer.initial_data['password']) + self.request.user.active_organization.add_user(instance) + + def retrieve(self, request, *args, **kwargs): + return super(UserAPI, self).retrieve(request, *args, **kwargs) + + def partial_update(self, request, *args, **kwargs): + result = super(UserAPI, self).partial_update(request, *args, **kwargs) + + # newsletters + if 'allow_newsletters' in request.data: + user = User.objects.get(id=request.user.id) # we need an updated user + request.user.advanced_json = { # request.user instance will be unchanged in request all the time + 'email': user.email, 'allow_newsletters': user.allow_newsletters, + 'update-notifications': 1, 'new-user': 0 + } + return result + + def destroy(self, request, *args, **kwargs): + return super(UserAPI, self).destroy(request, *args, **kwargs) + + +@method_decorator(name='get', decorator=swagger_auto_schema( + tags=['Users'], + operation_summary='List users for Admin', + operation_description='List the users that exist on the AiXBlock server from admin view.', + manual_parameters=[ + Parameter(name="search", in_=openapi.IN_QUERY, type=openapi.TYPE_STRING), + ])) +class UserListAdminViewAPI(generics.ListAPIView): + parser_classes = (JSONParser,) + permission_classes = (AllowAny, ) + pagination_class = SetPagination + serializer_class = UserSerializer + + def get_queryset(self): + search = self.request.query_params.get('search', None) + queryset = User.objects.order_by('-id') + + if search: + queryset = queryset.filter( + Q(username__icontains=search) + | Q(email__icontains=search) + | Q(first_name__icontains=search) + | Q(last_name__icontains=search) + ) + + return queryset + + def get(self, *args, **kwargs): + return super(UserListAdminViewAPI, self).get(*args, **kwargs) + +@method_decorator(name='post', decorator=swagger_auto_schema( + tags=['Users'], + operation_summary='Reset user token', + operation_description='Reset the user token for the current user.', + responses={ + 201: openapi.Response( + description='User token response', + schema=openapi.Schema( + description='User token', + type=openapi.TYPE_OBJECT, + properties={ + 'token': openapi.Schema( + description='Token', + type=openapi.TYPE_STRING + ) + } + )) + })) +class UserResetTokenAPI(APIView): + parser_classes = (JSONParser, FormParser, MultiPartParser) + queryset = User.objects.all() + permission_classes = (IsAuthenticated,) + + def post(self, request, *args, **kwargs): + user = request.user + token = user.reset_token() + logger.debug(f'New token for user {user.pk} is {token.key}') + return Response({'token': token.key}, status=201) + + +@method_decorator(name='get', decorator=swagger_auto_schema( + tags=['Users'], + operation_summary='Get user token', + operation_description='Get a user token to authenticate to the API as the current user.', + responses={ + 200: openapi.Response( + description='User token response', + type=openapi.TYPE_OBJECT, + properties={ + 'detail': openapi.Schema( + description='Token', + type=openapi.TYPE_STRING + ) + } + ) + })) +class UserGetTokenAPI(APIView): + parser_classes = (JSONParser,) + permission_classes = (IsAuthenticated,) + + def get(self, request, *args, **kwargs): + user = request.user + token = Token.objects.filter(user=user.id).first() + if not token: + return Response({'token': ''}, status=200) + return Response({'token': token.key}, status=200) + + +@method_decorator(name='get', decorator=swagger_auto_schema( + tags=['Users'], + operation_summary='Retrieve my user', + operation_description='Retrieve details of the account that you are using to access the API.' + )) +class UserWhoAmIAPI(generics.RetrieveAPIView): + parser_classes = (JSONParser, FormParser, MultiPartParser) + queryset = User.objects.all() + permission_classes = (IsAuthenticated, ) + # Vulnerable: exposed full UserSerializer including sensitive fields + # serializer_class = UserSerializer + # Fix: limit fields returned to prevent privilege escalation + serializer_class = UserCompactSerializer # fixes #59 + + def get_object(self): + import os + if os.getenv('RUNNING_CRONJOB') != '1': + from compute_marketplace.schedule_func import start_thread_cron + + user = self.request.user + centrifuge_token = generate_centrifuge_jwt(user.id) + setattr(user, "centrifuge_token", centrifuge_token) + + if not user.username or len(user.username) == 0: + user.username = generate_username() + user.save() + + try: + if user.id not in user.active_organization.active_members.values_list("user_id", flat=True): + user.active_organization = None + # user is organization member + if user.active_organization is None: + org_pk = Organization.find_by_user(user).pk + user.active_organization_id = org_pk + user.save(update_fields=['active_organization']) + except Exception as e: + print(e) + + return user + + def get(self, request, *args, **kwargs): + return super(UserWhoAmIAPI, self).get(request, *args, **kwargs) + + +class UserSwitchOrganizationAPI(APIView): + parser_classes = (JSONParser, FormParser, MultiPartParser) + queryset = User.objects.all() + permission_classes = (IsAuthenticated,) + serializer_class = UserSerializer + + @staticmethod + def post(request, *args, **kwargs): + user = request.user + organization_id = request.data.get("organization_id", None) + organization_member = OrganizationMember.objects.filter(organization=organization_id, user=user).first() + + if organization_member is None: + return Response({"message": "No member found for organization #" + organization_id.__str__()}, status=400) + else: + user.active_organization = organization_member.organization + user.save() + + return Response({}, status=200) + + +class NotificationPagination(PageNumberPagination): + page_size = 10 + page_size_query_param = 'page_size' + max_page_size = 10000 + + +@method_decorator(name='get', decorator=swagger_auto_schema( + tags=['Users'], + operation_summary='Get notifications', + operation_description='Get notification of current user.' + )) +class NotificationAPI(generics.ListAPIView): + parser_classes = (JSONParser, FormParser, MultiPartParser) + permission_classes = (IsAuthenticated, ) + serializer_class = NotificationSerializer + pagination_class = NotificationPagination + + def get_queryset(self): + return Notification.objects.filter(user=self.request.user).all() + + def get(self, request, *args, **kwargs): + return super(NotificationAPI, self).get(request, *args, **kwargs) + + +class NotificationActionAPI(APIView): + parser_classes = (JSONParser,) + queryset = User.objects.all() + permission_classes = (IsAuthenticated,) + serializer_class = UserSerializer + + @staticmethod + def post(request, *args, **kwargs): + user = request.user + action = request.data.get("action", None) + + if action == "mark_all_read": + Task.objects.filter(user=user).filter(is_read=False).update(is_read=True) + return Response({}, status=200) + elif action == "mark_read": + notification_id = int(request.data.get("id", None)) + notification = Notification.objects.filter(user=user).filter(id=notification_id) + + if notification: + notification.update(is_read=True) + return Response({}, status=200) + else: + return Response({}, status=400) + + return Response({}, status=400) + +@method_decorator(name='get', decorator=swagger_auto_schema( + tags=['Users'], + operation_summary='Get notifications', + operation_description='Get notification of current user.' + )) +class DetailNotificationComputeAPI(generics.ListAPIView): + parser_classes = (JSONParser, FormParser, MultiPartParser) + permission_classes = (IsAuthenticated, ) + serializer_class = DetailNotificationSerializer + pagination_class = NotificationPagination + + def get_queryset(self): + history_id = self.kwargs.get('history_id') + return Notification.objects.filter(history_id=history_id).all() + + def get(self, request, *args, **kwargs): + return super(DetailNotificationComputeAPI, self).get(request, *args, **kwargs) + +# @method_decorator(name='get', decorator=swagger_auto_schema( +# tags=['Users'], +# operation_summary='Get deltail compute notifications', +# operation_description='Get notification deltail of compute.' +# )) +# class DetailNotificationComputeAPI(generics.ListAPIView): +# parser_classes = (JSONParser, FormParser, MultiPartParser) +# permission_classes = (IsAuthenticated, ) +# serializer_class = DetailNotificationSerializer +# pagination_class = NotificationPagination + +# def get_queryset(self): +# history_id = self.kwargs.get('history_id') +# return Notification.objects.filter(history_id=history_id).all() + +# def get(self, request, *args, **kwargs): +# return super(DetailNotificationComputeAPI, self).get(request, *args, **kwargs) + +@method_decorator(name='get', decorator=swagger_auto_schema( + tags=['Users'], + operation_summary='Get simple user information', + operation_description='Get simple information about a user.', + manual_parameters=[ + openapi.Parameter( + name='id', + type=openapi.TYPE_INTEGER, + in_=openapi.IN_PATH, + description='User ID'), + ], + )) +class UserGetSimpleAPI(generics.RetrieveAPIView): + permission_classes = (IsAuthenticated,) + queryset = User.objects.all() + serializer_class = UserCompactSerializer + + def retrieve(self, request, *args, **kwargs): + return super(UserGetSimpleAPI, self).retrieve(request, *args, **kwargs) + +@method_decorator(name='post', decorator=swagger_auto_schema( + tags=['Users'], + operation_summary='SignUp An User', +)) + +class UserRegisterApi(generics.CreateAPIView): + authentication_classes = [] #disables authentication + permission_classes = [] #disables permission + queryset = User.objects.all() + serializer_class = UserRegisterSerializer + def post(self, request, *args, **kwargs): + serializer = UserRegisterSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + email = self.request.data.get('email') + email = email.strip().lower() + check = User.objects.filter(email=email) + + # import dns.resolver + # _, domain = email.split("@") + # try: + # records = dns.resolver.query(domain, 'MX') + # mxRecord = records[0].exchange + # mxRecord = str(mxRecord) + # except: + # return Response({'error': 'Your domain email is wrong'}, status=400) + + # return Response({'error': 'Your email is taken'}, status=400) + if check: + return Response({'error': 'Your email is taken'}, status=400) + + email_validate = is_valid_email(email) + email_verified = False + + # In case the validate API not working, user still can signup and verify their account. + if not email_validate['has_error']: + if email_validate['is_valid']: + # If the email address is valid, they won't be asked for the email verify + email_verified = True + else: + return Response({'error': 'Your email address is not valid'}, status=400) + + serializer.validated_data['password'] = make_password(serializer.validated_data['password']) + role = serializer.validated_data.pop('role') + user = User.objects.create( + **serializer.validated_data, + username=serializer.validated_data["email"], + is_verified=email_verified, + ) + if role == UserRole.compute_supplier: + user.is_compute_supplier = True + elif role == UserRole.model_seller: + user.is_model_seller = True + elif role == UserRole.labeler: + user.is_labeler = True + + token = create_hash() + team_id = create_hash() + status = "actived" + + organization = Organization.objects.create( + title=email, + created_by=user, + token=token, + team_id=team_id, + status=status + ) + + OrganizationMember.objects.create(organization=organization, user=user, is_admin=True,) + + html_file_path = './aixblock_labeltool/templates/mail/register.html' + with open(html_file_path, 'r', encoding='utf-8') as file: + html_content = file.read() + + # notify_reponse = ServiceSendEmail(DOCKER_API) + send_to = user.username if user.username else user.email + html_content = html_content.replace('[user]', f'{send_to}') + + data = { + "subject": "Welcome to AIxBlock - Registration Successful!", + "from": "noreply@aixblock.io", + "to": [f"{user.email}"], + "html": html_content, + # "text": "Welcome to AIxBlock!", + } + + docket_api = "tcp://69.197.168.145:4243" + host_name = MAIL_SERVER + _send_mail = True + if "Host" in self.request.headers: + if "app.aixblock.io" not in self.request.headers["Host"] and "stag.aixblock.io" not in self.request.headers["Host"]: + _send_mail = False + + if _send_mail: + email_thread = threading.Thread(target=send_email_thread, args=(docket_api, host_name, data,)) + email_thread.start() + + # if response != 202: + # user.delete() + # organization.delete() + # return Response({'error': 'Your email is taken'}, status=400) + + user.active_organization = organization + user.save() + + if settings.REGISTER_TOPUP > 0: + reward_point_register(user, topup_amount=settings.REGISTER_TOPUP) + + auth.login(request, user, backend='django.contrib.auth.backends.ModelBackend') + return Response({'message': 'Register successful'}, status=201) + +@method_decorator(name='post', decorator=swagger_auto_schema( + tags=['Users'], + operation_summary='User Info', +)) +class UserInfoAPI(APIView): + parser_classes = (JSONParser, FormParser, MultiPartParser) + queryset = User.objects.all() + permission_classes = (IsAuthenticated,) + serializer_class = UserSerializer + + def post(self, request, *args, **kwargs): + user = request.user + centrifuge_token = generate_centrifuge_jwt(user.id) + return Response( + { + "message": "User info", + "data": { + "id": user.id, + "username": user.username, + "email": user.email, + "centrifuge_token": centrifuge_token, + }, + }, + status=200, + ) + +@method_decorator(name='post', decorator=swagger_auto_schema( + tags=['Users'], + operation_summary='User Info From Oauths', +)) +class Oauth2UserInfoAPI(APIView): + parser_classes = (JSONParser, FormParser, MultiPartParser) + queryset = User.objects.all() + serializer_class = UserSerializer + authentication_classes = [] #disables authentication + permission_classes = [] #disables permission + + def post(self, request, *args, **kwargs): + from oauth2_provider.models import AccessToken + + auth_header = request.headers.get('Authorization') + + if not auth_header or not auth_header.startswith('Token '): + return Response({"detail": "Authorization header failed."}, status=status.HTTP_400_BAD_REQUEST) + + token_value = auth_header.split(' ')[1] + + try: + access_token = AccessToken.objects.get(token=token_value) + user = access_token.user + + return Response( + { + "id": user.id, + "username": user.username, + "email": user.email, + "is_superuser": user.is_superuser, + }, + status=200, + ) + + except AccessToken.DoesNotExist: + return Response({"detail": "Invalid or expired token."}, status=status.HTTP_401_UNAUTHORIZED) + +@method_decorator(name='post', decorator=swagger_auto_schema( + tags=['Users'], + operation_summary='Change Password', + request_body=ChangePasswordSerializer, +)) + +class UserChangePassword(generics.ListAPIView): + serializer = ChangePasswordSerializer + def post(self, request, *args, **kwargs): + email = self.request.data.get("email").lower() + user = User.objects.filter(email = email).first() + old_password = self.request.data.get("old_password") + new_password = self.request.data.get("new_password") + + # Check if the old password is correct + if not user.check_password(old_password): + return Response({"Wrong password."}, status=status.HTTP_400_BAD_REQUEST) + + if old_password == new_password: + return Response({"New password must be different from old password."}, status=status.HTTP_400_BAD_REQUEST) + + user.set_password(new_password) + user.save() + return Response({"message": "Password changed successfully."}, status=status.HTTP_200_OK) + + # return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + +@method_decorator(name='post', decorator=swagger_auto_schema( + tags=['Users'], + operation_summary='Reset Password', + request_body=ResetPasswordSerializer, +)) +class PasswordResetView(generics.ListAPIView): + serializer = ResetPasswordSerializer + authentication_classes = [] #disables authentication + permission_classes = [] #disables permission + def post(self, request): + domain_reset = settings.BASE_BACKEND_URL + if "Origin" in self.request.headers and self.request.headers["Origin"] != domain_reset: + domain_reset = self.request.headers["Origin"] + email = self.request.data.get("email").lower() + user = User.objects.filter(email=email).first() + if user: + token_generator = PasswordResetTokenGenerator() + uidb64 = urlsafe_base64_encode(force_bytes(user.pk)) + token = token_generator._make_token_with_timestamp(user, int((datetime.now() + timedelta(minutes=60*24)).timestamp())) + reset_link = f'{domain_reset}/user/reset-password/?uidb64={uidb64}&token={token}' + reset_link = re.sub(r'(?

Hi {send_to},

You recently requested to reset your password for your AIxBlock account.

Please click the link below to set up a new password

{reset_link}

If you did not request a password reset, please ignore this email or contact support if you have concerns.

Best Regards,

The AIxBlock Team

Note: This link will expire in 24 hours.

""" + data = { + "subject": "Reset Your Password AIxBlock", + "from": "noreply@aixblock.io", + "to": [f"{user.email}"], + "html": html_content, + "text": 'Reset password', + } + # notify_reponse.send_email(data) + docket_api = "tcp://69.197.168.145:4243" + host_name = MAIL_SERVER + + email_thread = threading.Thread(target=send_email_thread, args=(docket_api, host_name, data,)) + email_thread.start() + + return Response({'detail': 'Password reset email has been sent.'}, status=status.HTTP_200_OK) + else: + return Response({'detail': 'No user found with this email address.'}, status=status.HTTP_404_NOT_FOUND) + +@method_decorator(name='get', decorator=swagger_auto_schema( + tags=['Users'], + operation_summary='Password reset request done', +)) +class PasswordResetDoneView(APIView): + def get(self, request): + return Response({'detail': 'Password reset request done.'}, status=status.HTTP_200_OK) + +@method_decorator(name='post', decorator=swagger_auto_schema( + tags=['Users'], + operation_summary='Confirm password reset', + request_body=openapi.Schema( + type=openapi.TYPE_OBJECT, + required=['new_password'], + properties={ + 'new_password': openapi.Schema(type=openapi.TYPE_STRING, description='New password'), + }, + ), + manual_parameters=[ + openapi.Parameter( + name='uidb64', + in_=openapi.IN_PATH, + type=openapi.TYPE_STRING, + required=True, + description='User ID encoded in base64', + ), + openapi.Parameter( + name='token', + in_=openapi.IN_PATH, + type=openapi.TYPE_STRING, + required=True, + description='Token for password reset', + ), + ], + responses={ + 200: openapi.Response(description='Password has been reset successfully.'), + 400: openapi.Response(description='Invalid user or token.'), + } +)) +class PasswordResetConfirmView(generics.ListAPIView): + serializer = ResetPasswordSerializer + authentication_classes = [] #disables authentication + permission_classes = [] #disables permission + + def get(self, request, uidb64, token): + try: + uidb64 = self.kwargs.get('uidb64') + token = self.kwargs.get('token') + uid = force_text(urlsafe_base64_decode(uidb64)) + user = User.objects.get(pk=uid) + if PasswordResetTokenGenerator().check_token(user, token): + # return render(request, 'users/user_login.html') + return Response({'detail': 'Successfull'}, status=status.HTTP_200_OK) + else: + return Response({'detail': 'Invalid token.'}, status=status.HTTP_400_BAD_REQUEST) + except (TypeError, ValueError, OverflowError, User.DoesNotExist): + return Response({'detail': 'Invalid user.'}, status=status.HTTP_400_BAD_REQUEST) + + def post(self, request, **kwargs): + try: + uidb64 = self.kwargs.get('uidb64') + token = self.kwargs.get('token') + uid = force_text(urlsafe_base64_decode(uidb64)) + user = User.objects.get(pk=uid) + if PasswordResetTokenGenerator().check_token(user, token): + new_password = self.request.data.get('new_password') + user.set_password(new_password) + user.save() + return Response({'detail': 'Password has been reset successfully.'}, status=status.HTTP_200_OK) + else: + return Response({'detail': 'Invalid token.'}, status=status.HTTP_400_BAD_REQUEST) + except (TypeError, ValueError, OverflowError, User.DoesNotExist): + return Response({'detail': 'Invalid user.'}, status=status.HTTP_400_BAD_REQUEST) + +@method_decorator(name='get', decorator=swagger_auto_schema( + tags=['Users'], + operation_summary='Password reset complete', +)) +class PasswordResetCompleteView(APIView): + authentication_classes = [] #disables authentication + permission_classes = [] #disables permission + def get(self, request): + return Response({'detail': 'Password reset complete.'}, status=status.HTTP_200_OK) + + +@method_decorator( + name="get", + decorator=swagger_auto_schema( + tags=["Users"], + operation_summary="Get Portfolio", + manual_parameters=[ + openapi.Parameter( + "token_symbol", + openapi.IN_QUERY, + description="Token Symbol", + type=openapi.TYPE_STRING, + required=True, + ), + ], + responses={200: PortfolioSerializer(many=False)}, + ), +) +class GetPortfolio(generics.RetrieveAPIView): + def get_object(self): + user_id = self.request.user.id + token_symbol = self.request.query_params.get("token_symbol") + + if user_id and token_symbol: + queryset = Portfolio.objects.filter( + account_id=user_id, token_symbol=token_symbol + ) + if queryset.exists(): + return queryset.first() + return None + + def get(self, request, *args, **kwargs): + instance = self.get_object() + if not instance: + return Response( + {"detail": "Portfolio not found."}, status=status.HTTP_404_NOT_FOUND + ) + instance_dict = model_to_dict(instance) + return Response(instance_dict, status=status.HTTP_200_OK) + + +@method_decorator( + name="patch", + decorator=swagger_auto_schema( + tags=["Users"], + operation_summary="Deposit Portfolio", + manual_parameters=[ + openapi.Parameter( + "token_symbol", + openapi.IN_QUERY, + description="Token Symbol", + type=openapi.TYPE_STRING, + required=True, + ), + openapi.Parameter( + "amount_deposit", + openapi.IN_QUERY, + description="deposit amount", + type=openapi.TYPE_STRING, + required=True, + ), + ], + responses={200: PortfolioSerializer(many=False)}, + ), +) +class DepositPortfolio(generics.UpdateAPIView): + def get_object(self): + user_id = self.request.user.id + token_symbol = self.request.query_params.get("token_symbol") + + if user_id and token_symbol: + queryset = Portfolio.objects.filter( + account_id=user_id, token_symbol=token_symbol + ) + if queryset.exists(): + return queryset.first() + return None + + def patch(self, request, *args, **kwargs): + return Response( + {"detail": "This API is deprecated"}, status=status.HTTP_200_OK + ) + + +class EmailVerifyView(generics.GenericAPIView): + + def get(self, request, pk, token, *args, **kwargs): + user = User.objects.filter(id=pk).first() + + if not user: + return HttpResponseNotFound() + + if PasswordResetTokenGenerator().check_token(user, token): + user.is_verified = True + user.save() + return HttpResponseRedirect(redirect_to="/") + + return HttpResponseNotFound() + + +@method_decorator(name='post', decorator=swagger_auto_schema( + tags=['Users'], + operation_summary='Validate email address', + request_body=ValidateEmailSerializer, +)) +class ValidateEmailAPI(generics.ListAPIView): + serializer = ValidateEmailSerializer + authentication_classes = [] # disables authentication + permission_classes = [] # disables permission + + def post(self, request): + serializer = ValidateEmailSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + return Response(is_valid_email(serializer.validated_data["email"]), status=status.HTTP_200_OK) + +@method_decorator(name='post', decorator=swagger_auto_schema( + tags=['Users'], + operation_summary='Update Token Jupyter Admin', + request_body=openapi.Schema( + type=openapi.TYPE_OBJECT, + required=['token'], + properties={ + 'token': openapi.Schema(type=openapi.TYPE_STRING, description='token'), + }, + ), +)) +class UpdateTokenJupyterAdmin(APIView): + parser_classes = (JSONParser, FormParser, MultiPartParser) + queryset = User.objects.all() + serializer_class = UserSerializer + + def post(self, request, *args, **kwargs): + from .functions import update_token_admin + token_value = request.data.get('token') + update_token_admin(token_value) + + return Response({"detail": "Done"}, status=200) + + +@method_decorator(name='get', decorator=swagger_auto_schema( + tags=['Users'], + operation_summary='Download all users', +)) +class AdminDownloadUsers(generics.RetrieveAPIView): + queryset = User.objects.all().order_by("pk") + permission_classes = [IsSuperAdmin] + + def get(self, request): + response = HttpResponse( + content_type="text/csv", + headers={"Content-Disposition": 'attachment; filename="platform-users.csv"'}, + ) + writer = csv.writer(response, delimiter=",", dialect="excel") + writer.writerow([ + "ID", + "Email", + "Username", + "First name", + "Last name", + "Active?", + "Verified?", + "Superadmin?", + "QA?", + "QC?", + "AI Builder?", + "Compute Supplier?", + "Model Seller?", + "Labeler?", + "Date joined", + ]) + + for user in self.get_queryset(): + writer.writerow([ + user.id, + user.email, + user.username, + user.first_name, + user.last_name, + user.is_active, + user.is_verified, + user.is_superuser, + user.is_qa, + user.is_qc, + not user.is_compute_supplier and not user.is_model_seller and not user.is_labeler and not user.is_freelancer, + user.is_compute_supplier, + user.is_model_seller, + user.is_labeler or user.is_freelancer, + user.date_joined, + ]) + + return response + +class TransactionPagination(PageNumberPagination): + page_size = 10 # Số mục mặc định mỗi trang + page_size_query_param = 'page_size' # Cho phép client tùy chỉnh số mục trên mỗi trang + max_page_size = 100 # Số mục tối đa mỗi trang + +@method_decorator(name='get', decorator=swagger_auto_schema( + tags=['User'], + operation_summary='Get transaction', + manual_parameters=[ + openapi.Parameter( + 'page', openapi.IN_QUERY, description="Page number", + type=openapi.TYPE_INTEGER, default=1 + ), + openapi.Parameter( + 'page_size', openapi.IN_QUERY, description="page size", + type=openapi.TYPE_INTEGER, default=10 + ), + ] +)) + +@method_decorator(name='post', decorator=swagger_auto_schema( + tags=['User'], + operation_summary='Create transaction', + request_body=TransactionSerializer +)) + +class TransactionAPI(generics.GenericAPIView): + serializer_class = TransactionSerializer + pagination_class = TransactionPagination + queryset = Transaction.objects.all() + + def get_queryset(self): + user_id = self.request.user.id + return Transaction.objects.filter(user_id=user_id, status=Transaction.Status.SUCCESS).order_by("-id") + + def get(self, request, *args, **kwargs): + try: + queryset = self.get_queryset() + page = self.paginate_queryset(queryset) + if page is not None: + serializer = self.get_serializer(page, many=True) + return self.get_paginated_response(serializer.data) + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + except Exception as e: + return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) + + def post(self, request, *args, **kwargs): + try: + # Lấy các trường cần thiết từ request, sử dụng request.data.get() + order_id = request.data.get('order_id', None) + amount = request.data.get('amount', None) + description = request.data.get('description', None) + wallet_address = request.data.get('wallet_address', None) + unit = request.data.get('unit', None) + network = request.data.get('network', None) + type_value = request.data.get('type', None) + # Lưu ý: Bạn có thể bỏ qua user_id nếu muốn gán tự động từ request.user.id, + # ngay cả khi trong request body có truyền user_id, bạn sẽ ghi đè lại. + + # Tạo đối tượng Transaction mới + transaction = Transaction.objects.create( + user_id=request.user.id, # Gán user_id từ thông tin user đăng nhập + order_id=order_id, + amount=amount, + description=description, + wallet_address=wallet_address, + unit=unit, + network=network, + type=type_value + ) + + # Sử dụng serializer để trả về dữ liệu mới tạo + serializer = self.get_serializer(transaction) + return Response(serializer.data, status=status.HTTP_201_CREATED) + except Exception as e: + return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) + +@method_decorator(name='patch', decorator=swagger_auto_schema( + tags=['User'], + operation_summary='Update transaction', + request_body=TransactionSerializer, + # manual_parameters=[ + # openapi.Parameter( + # 'pk', openapi.IN_PATH, + # description="ID transaction", + # type=openapi.TYPE_INTEGER + # ) + # ] +)) +class UpdateTransactionAPI(generics.UpdateAPIView): + serializer_class = TransactionSerializer + queryset = Transaction.objects.all() + + def patch(self, request, *args, **kwargs): + try: + transaction_id = request.data.get('id') or kwargs.get('pk') + if not transaction_id: + return Response( + {"error": "Transaction ID is required for update."}, + status=status.HTTP_400_BAD_REQUEST + ) + transaction = Transaction.objects.get(id=transaction_id, user_id=request.user.id) + serializer = self.get_serializer(transaction, data=request.data, partial=True) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + except Transaction.DoesNotExist: + return Response( + {"error": "Transaction not found."}, + status=status.HTTP_404_NOT_FOUND + ) + except Exception as e: + return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) + +@method_decorator(name='delete', decorator=swagger_auto_schema( + tags=['User'], + operation_summary='Delete transaction', + # manual_parameters=[ + # openapi.Parameter( + # 'pk', openapi.IN_PATH, + # description="ID transaction", + # type=openapi.TYPE_INTEGER + # ) + # ] +)) +class DeleteTransactionAPI(generics.DestroyAPIView): + serializer_class = TransactionSerializer + queryset = Transaction.objects.all() + + def delete(self, request, *args, **kwargs): + try: + transaction_id = kwargs.get('pk') + if not transaction_id: + return Response( + {"error": "Transaction ID is required for delete."}, + status=status.HTTP_400_BAD_REQUEST + ) + transaction = Transaction.objects.get(id=transaction_id, user_id=request.user.id) + transaction.delete() + return Response( + {"message": "Transaction deleted successfully."}, + status=status.HTTP_200_OK + ) + except Transaction.DoesNotExist: + return Response( + {"error": "Transaction not found."}, + status=status.HTTP_404_NOT_FOUND + ) + except Exception as e: + return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) \ No newline at end of file