diff --git a/docs/api/schema.yml b/docs/api/schema.yml index 37d8597df..6fc8b5fc4 100644 --- a/docs/api/schema.yml +++ b/docs/api/schema.yml @@ -1323,6 +1323,26 @@ paths: schema: $ref: '#/components/schemas/FullUser' description: '' + /api/user/{id}/delete/: + delete: + operationId: user_delete_destroy + description: Actions related to the current user. + parameters: + - in: path + name: id + schema: + type: string + format: HashID + description: A unique integer value identifying this user. + required: true + tags: + - user + security: + - tokenAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body /api/user/agree_to_tos/: put: operationId: user_agree_to_tos_update diff --git a/locales/en/translation.json b/locales/en/translation.json index 0fde49818..e7c94d323 100644 --- a/locales/en/translation.json +++ b/locales/en/translation.json @@ -57,6 +57,7 @@ "Completed item": "Completed item", "Confirm": "Confirm", "Confirm Changing Developer and Deleting Dev Org": "Confirm Changing Developer and Deleting Dev Org", + "Confirm Deleting Account": "Confirm Deleting Account", "Confirm Deleting Epic": "Confirm Deleting Epic", "Confirm Deleting Org With Unretrieved Changes": "Confirm Deleting Org With Unretrieved Changes", "Confirm Deleting Task": "Confirm Deleting Task", @@ -107,6 +108,7 @@ "Currently Assigned": "Currently Assigned", "Custom Domain": "Custom Domain", "Delete": "Delete", + "Delete Account": "Delete Account", "Delete Epic": "Delete Epic", "Delete Org": "Delete Org", "Delete Task": "Delete Task", @@ -186,6 +188,7 @@ "Make a Scratch Org to view the Project and play.": "Make a Scratch Org to view the Project and play.", "Make changes and retrieve them into a repository on GitHub.": "Make changes and retrieve them into a repository on GitHub.", "Make changes in Dev Org": "Make changes in Dev Org", + "Manage Account": "Manage Account", "Markdown Guide": "Markdown Guide", "Merge pull request on GitHub": "Merge pull request on GitHub", "Merged": "Merged", @@ -438,6 +441,7 @@ "check again": "check again", "confirmDeleteEpic": "Are you sure you want to delete Epic “<1>{{name}}1>”? This will also delete any Tasks and Orgs in this Epic.", "confirmDeleteTask": "Are you sure you want to delete Task “<1>{{name}}1>”? This will also delete any Orgs in this Task.", + "confirmDeleteUser": "Your Dev Orgs will be deleted, and any unretrieved changes will be lost. This action cannot be undone. Are you sure you want to delete your Metecho account?", "confirmRemoveCollaboratorsHeading": "Confirm Removing Collaborator", "confirmRemoveCollaboratorsHeading_plural": "Confirm Removing Collaborators", "confirmRemoveCollaboratorsMessage": "The following user is being removed from this Epic, but is already assigned to at least one Task. Removing this user will not remove them from any assigned Tasks. Are you sure you want to remove this user from the Epic?", @@ -450,6 +454,7 @@ "createProjectScratchOrgHelp": "<0>Visit an Epic or Task to create a Scratch Org from a work in progress.0>", "createScratchOrgContributeWarning": "<0><0>You will not be able to retrieve any changes made in this Scratch Org.0>0>", "createScratchOrgHelp": "<0>You are creating a Scratch Org for <1>{{type}}1> “<3>{{name}}3>.”0><1>Your new Org will expire in 30 days.<1>1>You will be able to access your Org from this <4>{{type}}4> page.1>", + "deleteAccountChanges": "Your Dev Orgs will be deleted, and any unretrieved changes will be lost. This action cannot be undone. Deleting this account will not remove you as a Project collaborator on GitHub.", "devHubInfo": "Connection to a Salesforce Org with Dev Hub enabled is required to create a Dev, Test, or Scratch Org. Learn how to <2>create a Developer Edition Org2> and <6>enable Dev Hub6>.", "devHubNotEnabled": "This Salesforce Org does not have Dev Hub enabled or your user does not have permission to create Dev, Test, or Scratch Orgs. Learn how to <2>enable Dev Hub2>.", "epicCollaborators": "Only users who have access to the GitHub repository for this Epic will appear in the list below. Visit GitHub to invite additional Collaborators.", diff --git a/locales_dev/en/translation.json b/locales_dev/en/translation.json index dd6b493ad..22fa5f089 100644 --- a/locales_dev/en/translation.json +++ b/locales_dev/en/translation.json @@ -57,6 +57,7 @@ "Completed item": "Completed item", "Confirm": "Confirm", "Confirm Changing Developer and Deleting Dev Org": "Confirm Changing Developer and Deleting Dev Org", + "Confirm Deleting Account": "Confirm Deleting Account", "Confirm Deleting Epic": "Confirm Deleting Epic", "Confirm Deleting Org With Unretrieved Changes": "Confirm Deleting Org With Unretrieved Changes", "Confirm Deleting Task": "Confirm Deleting Task", @@ -107,6 +108,7 @@ "Currently Assigned": "Currently Assigned", "Custom Domain": "Custom Domain", "Delete": "Delete", + "Delete Account": "Delete Account", "Delete Epic": "Delete Epic", "Delete Org": "Delete Org", "Delete Task": "Delete Task", @@ -186,6 +188,7 @@ "Make a Scratch Org to view the Project and play.": "Make a Scratch Org to view the Project and play.", "Make changes and retrieve them into a repository on GitHub.": "Make changes and retrieve them into a repository on GitHub.", "Make changes in Dev Org": "Make changes in Dev Org", + "Manage Account": "Manage Account", "Markdown Guide": "Markdown Guide", "Merge pull request on GitHub": "Merge pull request on GitHub", "Merged": "Merged", @@ -438,6 +441,7 @@ "check again": "check again", "confirmDeleteEpic": "Are you sure you want to delete Epic “<1>{{name}}1>”? This will also delete any Tasks and Orgs in this Epic.", "confirmDeleteTask": "Are you sure you want to delete Task “<1>{{name}}1>”? This will also delete any Orgs in this Task.", + "confirmDeleteUser": "Your Dev Orgs will be deleted, and any unretrieved changes will be lost. This action cannot be undone. Are you sure you want to delete your Metecho account?", "confirmRemoveCollaboratorsHeading": "Confirm Removing Collaborator", "confirmRemoveCollaboratorsHeading_plural": "Confirm Removing Collaborator", "confirmRemoveCollaboratorsMessage": "The following user is being removed from this Epic, but is already assigned to at least one Task. Removing this user will not remove them from any assigned Tasks. Are you sure you want to remove this user from the Epic?", @@ -450,6 +454,7 @@ "createProjectScratchOrgHelp": "<0>Visit an Epic or Task to create a Scratch Org from a work in progress.0>", "createScratchOrgContributeWarning": "<0><0>You will not be able to retrieve any changes made in this Scratch Org.0>0>", "createScratchOrgHelp": "<0>You are creating a Scratch Org for <1>{{type}}1> “<3>{{name}}3>.”0><1>Your new Org will expire in 30 days.<1>1>You will be able to access your Org from this <4>{{type}}4> page.1>", + "deleteAccountChanges": "Your Dev Orgs will be deleted, and any unretrieved changes will be lost. This action cannot be undone. Deleting this account will not remove you as a Project collaborator on GitHub.", "devHubInfo": "Connection to a Salesforce Org with Dev Hub enabled is required to create a Dev, Test, or Scratch Org. Learn how to <2>create a Developer Edition Org2> and <6>enable Dev Hub6>.", "devHubNotEnabled": "This Salesforce Org does not have Dev Hub enabled or your user does not have permission to create Dev, Test, or Scratch Orgs. Learn how to <2>enable Dev Hub2>.", "epicCollaborators": "Only users who have access to the GitHub repository for this Epic will appear in the list below. Visit GitHub to invite additional Collaborators.", diff --git a/metecho/api/migrations/0110_alter_scratchorg_owner.py b/metecho/api/migrations/0110_alter_scratchorg_owner.py new file mode 100644 index 000000000..8f7fc5a7e --- /dev/null +++ b/metecho/api/migrations/0110_alter_scratchorg_owner.py @@ -0,0 +1,25 @@ +# Generated by Django 4.0.4 on 2022-06-07 13:40 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("api", "0109_support_project_creation"), + ] + + operations = [ + migrations.AlterField( + model_name="scratchorg", + name="owner", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/metecho/api/models.py b/metecho/api/models.py index 8f9e7afc1..7a3af8cab 100644 --- a/metecho/api/models.py +++ b/metecho/api/models.py @@ -216,6 +216,17 @@ def _get_org_property(self, key): except (AttributeError, KeyError, TypeError): return None + def delete(self, *args, **kwargs): + for scratch_org_to_delete in self.scratchorg_set.all(): + scratch_org_to_delete.owner = None + scratch_org_to_delete.owner_sf_username = "" + scratch_org_to_delete.owner_gh_username = "" + scratch_org_to_delete.owner_gh_id = "" + scratch_org_to_delete.save() + scratch_org_to_delete.queue_delete(originating_user_id=self.id) + + super().delete(*args, **kwargs) + @property def github_id(self) -> Optional[str]: try: @@ -1198,7 +1209,7 @@ class ScratchOrg( description = MarkdownField(blank=True, property_suffix="_markdown") org_type = StringField(choices=ScratchOrgType.choices) org_config_name = StringField() - owner = models.ForeignKey(User, on_delete=models.PROTECT) + owner = models.ForeignKey(User, on_delete=models.PROTECT, blank=True, null=True) last_modified_at = models.DateTimeField(null=True, blank=True) expires_at = models.DateTimeField(null=True, blank=True) latest_commit = StringField(blank=True) @@ -1265,7 +1276,7 @@ def save(self, *args, **kwargs): self.clean_config() ret = super().save(*args, **kwargs) - if is_new: + if is_new and self.owner: self.queue_provision(originating_user_id=str(self.owner.id)) self.notify_org_provisioning(originating_user_id=str(self.owner.id)) diff --git a/metecho/api/tests/models.py b/metecho/api/tests/models.py index f658e6397..1062de75a 100644 --- a/metecho/api/tests/models.py +++ b/metecho/api/tests/models.py @@ -777,6 +777,19 @@ def test_is_devhub_enabled__sf_error(self, user_factory, social_account_factory) get_devhub_api.return_value = client assert not user.is_devhub_enabled + def test_user_delete_triggers_scratch_org_delete( + self, mocker, user_factory, scratch_org_factory + ): + mocker.patch("metecho.api.admin.gh") + scratch_org_delete_job = mocker.patch("metecho.api.jobs.delete_scratch_org_job") + + user = user_factory() + scratch_org = scratch_org_factory(last_modified_at=now(), owner=user) + assert user.scratchorg_set.first() == scratch_org + + user.delete() + assert scratch_org_delete_job.delay.called + @pytest.mark.django_db class TestScratchOrg: diff --git a/metecho/api/tests/views.py b/metecho/api/tests/views.py index e00fa1d24..e0891b6f8 100644 --- a/metecho/api/tests/views.py +++ b/metecho/api/tests/views.py @@ -246,6 +246,12 @@ def test_check_app_installation__no_permissions( len(data["messages"]) == 3 ), "Expected three error messages when permission checks fail" + def test_delete(self, client, mocker): + response = client.delete(reverse("current-user-detail")) + assert response.status_code == 204 + with pytest.raises(client.user.DoesNotExist): + client.user.refresh_from_db() + @pytest.mark.django_db class TestGitHubIssueViewset: diff --git a/metecho/api/views.py b/metecho/api/views.py index 90dd573c9..d804497f2 100644 --- a/metecho/api/views.py +++ b/metecho/api/views.py @@ -206,6 +206,12 @@ def refresh_orgs(self, request): request.user.queue_refresh_organizations() return Response(status=status.HTTP_202_ACCEPTED) + @extend_schema(request=None) + @action(methods=["delete"], detail=True) + def delete(self, request, *args, **kwargs): + request.user.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + class UserViewSet(mixins.RetrieveModelMixin, mixins.ListModelMixin, GenericViewSet): """Read-only information about all users.""" diff --git a/metecho/urls.py b/metecho/urls.py index 772e62aac..ccea1f15f 100644 --- a/metecho/urls.py +++ b/metecho/urls.py @@ -40,7 +40,7 @@ # Routes to pass through to the front end JS route-handler # Ensure the CSRF token is always present via a cookie to be read by JS re_path( - r"^($|login\/?$|terms\/?$|projects(\/|$)|accounts(\/|$))", + r"^($|login\/?$|manage\/?$|terms\/?$|projects(\/|$)|accounts(\/|$))", ensure_csrf_cookie(TemplateView.as_view(template_name="index.html")), name="frontend", ), diff --git a/src/js/components/user/delete.tsx b/src/js/components/user/delete.tsx new file mode 100644 index 000000000..33a9de004 --- /dev/null +++ b/src/js/components/user/delete.tsx @@ -0,0 +1,51 @@ +import Button from '@salesforce/design-system-react/components/button'; +import React, { useState } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import { useSelector } from 'react-redux'; + +import { DeleteModal } from '@/js/components/utils'; +import { User } from '@/js/store/user/reducer'; +import { selectUserState } from '@/js/store/user/selectors'; +import { OBJECT_TYPES } from '@/js/utils/constants'; +import routes from '@/js/utils/routes'; + +const DeleteAccount = () => { + const [deleteModalOpen, setDeleteModalOpen] = useState(false); + const user = useSelector(selectUserState); + const closeDeleteModal = () => { + setDeleteModalOpen(false); + }; + const openDeleteModal = () => { + setDeleteModalOpen(true); + }; + const { t } = useTranslation(); + + return ( +