Skip to content

Commit

Permalink
Allow user account deletion (from OddBird). (#2031)
Browse files Browse the repository at this point in the history
  • Loading branch information
jgerigmeyer authored Jul 19, 2022
1 parent d9612be commit 6ca1de3
Show file tree
Hide file tree
Showing 22 changed files with 426 additions and 29 deletions.
20 changes: 20 additions & 0 deletions docs/api/schema.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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?",
Expand All @@ -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 Org</2> and <6>enable Dev Hub</6>.",
"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 Hub</2>.",
"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.",
Expand Down
5 changes: 5 additions & 0 deletions locales_dev/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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?",
Expand All @@ -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 Org</2> and <6>enable Dev Hub</6>.",
"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 Hub</2>.",
"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.",
Expand Down
25 changes: 25 additions & 0 deletions metecho/api/migrations/0110_alter_scratchorg_owner.py
Original file line number Diff line number Diff line change
@@ -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,
),
),
]
15 changes: 13 additions & 2 deletions metecho/api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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))

Expand Down
13 changes: 13 additions & 0 deletions metecho/api/tests/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
6 changes: 6 additions & 0 deletions metecho/api/tests/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
6 changes: 6 additions & 0 deletions metecho/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
2 changes: 1 addition & 1 deletion metecho/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
),
Expand Down
51 changes: 51 additions & 0 deletions src/js/components/user/delete.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div>
<div className="slds-text-heading_large slds-m-bottom_small">
{t('Delete Account')}
</div>
<div className="slds-m-bottom_medium slds-text-body_regular">
<Trans i18nKey="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.
</Trans>
</div>
<Button
label={t('Delete Account')}
variant="brand"
onClick={openDeleteModal}
/>
<DeleteModal
model={user as User}
modelType={OBJECT_TYPES.USER}
isOpen={deleteModalOpen}
redirect={routes.login()}
handleClose={closeDeleteModal}
/>
</div>
);
};

export default DeleteAccount;
35 changes: 26 additions & 9 deletions src/js/components/user/info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { useDispatch, useSelector } from 'react-redux';

import ConnectModal from '@/js/components/user/connect';
import Logout from '@/js/components/user/logout';
import { ManageAccountButton } from '@/js/components/user/manage';
import {
ExternalLink,
SpinnerWrapper,
Expand All @@ -23,20 +24,23 @@ import { selectUserState } from '@/js/store/user/selectors';

const ConnectToSalesforce = ({
toggleModal,
closeDropdown,
}: {
toggleModal: React.Dispatch<React.SetStateAction<boolean>>;
closeDropdown: () => void;
}) => {
const { t } = useTranslation();

const openConnectModal = () => {
toggleModal(true);
closeDropdown();
};

return (
<>
<Button
label={t('Connect to Salesforce')}
className="slds-text-body_regular slds-p-right_xx-small"
className="slds-text-heading_small slds-p-right_xx-small"
variant="link"
onClick={openConnectModal}
/>
Expand Down Expand Up @@ -239,6 +243,13 @@ const UserDropdown = () => {
const { t } = useTranslation();
const user = useSelector(selectUserState);
const [modalOpen, setModalOpen] = useState(false);
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const toggleDropdown = () => {
setIsDropdownOpen(!isDropdownOpen);
};
const closeDropdown = () => {
setIsDropdownOpen(false);
};

if (!user) {
return null;
Expand All @@ -247,6 +258,9 @@ const UserDropdown = () => {
return (
<>
<Popover
isOpen={isDropdownOpen}
onClick={toggleDropdown}
onRequestClose={closeDropdown}
align="bottom right"
body={
<>
Expand Down Expand Up @@ -278,14 +292,14 @@ const UserDropdown = () => {
size="small"
/>
)}
<div className="slds-p-left_x-large">
<h2
id="user-info-heading"
className="slds-text-heading_small"
>
{user.username}
<div className="slds-p-left_x-large slds-text-heading_small">
<h2 id="user-info-heading">{user.username}</h2>
<h2 className="slds-p-top_small slds-m-top_xx-small">
<ManageAccountButton onClick={toggleDropdown} />
</h2>
<h2 className="slds-p-top_small">
<Logout />
</h2>
<Logout className="slds-m-top_xx-small" />
</div>
</div>
</header>
Expand All @@ -294,7 +308,10 @@ const UserDropdown = () => {
{user.valid_token_for || user.devhub_username ? (
<ConnectionInfo user={user} />
) : (
<ConnectToSalesforce toggleModal={setModalOpen} />
<ConnectToSalesforce
toggleModal={setModalOpen}
closeDropdown={closeDropdown}
/>
)}
</div>
)}
Expand Down
Loading

0 comments on commit 6ca1de3

Please sign in to comment.