Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions client/modules/User/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { showToast, setToastText } from '../IDE/actions/toast';
import type {
CreateApiKeyRequestBody,
CreateUserRequestBody,
DeleteAccountRequestBody,
Error,
PublicUser,
PublicUserOrError,
Expand Down Expand Up @@ -506,3 +507,33 @@ export function setUserCookieConsent(
});
};
}

/**
* - Method: `DELETE`
* - Endpoint: `/account`
* - Authenticated: `true`
* - Id: `UserController.deleteAccount`
*
* Description:
* - Permanently delete the authenticated user's account and all their data.
* - Returns the error message from the server on failure, or redirects to `/` on success.
*/
export function deleteAccount(formValues: DeleteAccountRequestBody) {
return (dispatch: Dispatch) =>
new Promise<void | string>((resolve) => {
apiClient
.delete('/account', { data: formValues })
.then(() => {
dispatch({ type: ActionTypes.UNAUTH_USER });
dispatch({ type: ActionTypes.RESET_PROJECT });
dispatch({ type: ActionTypes.CLEAR_CONSOLE });
browserHistory.push('/');
resolve();
})
.catch((error) => {
const message =
error.response?.data?.message || 'Error deleting account.';
resolve(message);
});
});
}
113 changes: 113 additions & 0 deletions client/modules/User/components/DangerZone.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import type { ThunkDispatch } from 'redux-thunk';
import type { AnyAction } from 'redux';
import { Button, ButtonKinds, ButtonTypes } from '../../../common/Button';
import { deleteAccount } from '../actions';
import { RootState } from '../../../reducers';

export function DangerZone() {
const { t } = useTranslation();
const dispatch = useDispatch<ThunkDispatch<RootState, unknown, AnyAction>>();
const hasPassword = useSelector(
(state: RootState) =>
state.user.github === undefined && state.user.google === undefined
);

const [isConfirming, setIsConfirming] = useState(false);
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);

const handleDelete = async (event: React.FormEvent) => {
event.preventDefault();
setError('');
setIsSubmitting(true);
const result = await dispatch(
deleteAccount(hasPassword ? { password } : {})
);
setIsSubmitting(false);
if (result) {
setError(result as string);
}
};

const header = (
<React.Fragment>
<h2 className="form-container__divider">{t('DangerZone.Title')}</h2>
<p className="account__social-text">
{t('DangerZone.DeleteAccountDescription')}
</p>
</React.Fragment>
);

if (!isConfirming) {
return (
<div className="account__danger-zone">
{header}
<div className="account__social-stack">
<Button
kind={ButtonKinds.PRIMARY}
onClick={() => setIsConfirming(true)}
>
{t('DangerZone.DeleteAccount')}
</Button>
</div>
</div>
);
}

return (
<div className="account__danger-zone">
{header}
<form className="form" onSubmit={handleDelete}>
{hasPassword && (
<p className="form__field">
<label
htmlFor="danger-zone-password"
className="account__inline-label"
>
{t('DangerZone.PasswordLabel')}
</label>
<input
className="form__input"
aria-label={t('DangerZone.PasswordARIA')}
id="danger-zone-password"
type="password"
autoComplete="current-password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</p>
)}
{error && (
<p className="form-error" aria-live="polite">
{error}
</p>
)}
<div className="account__action-stack">
<Button
kind={ButtonKinds.SECONDARY}
type={ButtonTypes.BUTTON}
onClick={() => {
setIsConfirming(false);
setPassword('');
setError('');
}}
disabled={isSubmitting}
>
{t('DangerZone.Cancel')}
</Button>
<Button
kind={ButtonKinds.PRIMARY}
type={ButtonTypes.SUBMIT}
disabled={isSubmitting || (hasPassword && password === '')}
>
{t('DangerZone.Confirm')}
</Button>
</div>
</form>
</div>
);
}
Loading
Loading