Skip to content

Commit ef920ba

Browse files
committed
account deletion
1 parent be5deb9 commit ef920ba

6 files changed

Lines changed: 178 additions & 24 deletions

File tree

api/src/controllers/user.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1003,3 +1003,36 @@ export const disableTotp = async (req, res) => {
10031003
res.status(400).send('Request must include token')
10041004
}
10051005
}
1006+
1007+
export const deleteAccount = async (req, res) => {
1008+
if (req.body.password) {
1009+
try {
1010+
const user = await User.findOne({ _id: req.userId }).lean()
1011+
1012+
if (!user) {
1013+
res.status(404).send('User does not exist')
1014+
return
1015+
}
1016+
1017+
if (user.username === 'admin') {
1018+
res.status(403).send('Primary admin account cannot be deleted')
1019+
return
1020+
}
1021+
1022+
const matches = await bcrypt.compare(req.body.password, user.password)
1023+
1024+
if (!matches) {
1025+
res.status(401).send('Incorrect password')
1026+
return
1027+
}
1028+
1029+
await User.deleteOne({ _id: req.userId })
1030+
1031+
res.sendStatus(200)
1032+
} catch (e) {
1033+
res.status(500).send(e.message)
1034+
}
1035+
} else {
1036+
res.status(400).send('Request must include password')
1037+
}
1038+
}

api/src/routes/account.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
generateTotpSecret,
1111
enableTotp,
1212
disableTotp,
13+
deleteAccount,
1314
} from '../controllers/user'
1415

1516
const router = express.Router()
@@ -25,5 +26,6 @@ export default (mail) => {
2526
router.get('/totp/generate', generateTotpSecret)
2627
router.post('/totp/enable', enableTotp)
2728
router.post('/totp/disable', disableTotp)
29+
router.post('/delete', deleteAccount)
2830
return router
2931
}

client/components/Button.js

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const StyledButton = styled.button(
99
css({
1010
appearance: 'none',
1111
bg: 'primary',
12-
color: getLuminance(theme.colors.primary) >= 0.5 ? '#000' : '#fff',
12+
color: getLuminance(theme.colors.primary) >= 0.5 ? '#202224' : '#f8f8f8',
1313
border: '2px solid',
1414
borderColor: 'primary',
1515
borderRadius: 1,
@@ -99,6 +99,35 @@ const StyledButton = styled.button(
9999
: lighten(0.1, theme.colors.sidebar),
100100
},
101101
},
102+
danger: {
103+
bg: 'error',
104+
color: '#f8f8f8',
105+
borderColor: 'error',
106+
'&:hover': {
107+
borderColor:
108+
theme.name === 'light'
109+
? darken(0.1, theme.colors.error)
110+
: lighten(0.1, theme.colors.error),
111+
},
112+
'&:focus, &:active': {
113+
bg:
114+
theme.name === 'light'
115+
? darken(0.1, theme.colors.error)
116+
: lighten(0.1, theme.colors.error),
117+
borderColor:
118+
theme.name === 'light'
119+
? darken(0.1, theme.colors.error)
120+
: lighten(0.1, theme.colors.error),
121+
},
122+
'&[disabled]': {
123+
'&:hover': {
124+
borderColor: 'sidebar',
125+
},
126+
'&:focus, &:active': {
127+
bg: 'sidebar',
128+
},
129+
},
130+
},
102131
},
103132
}),
104133
layout,

client/components/Comment.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,12 @@ const Comment = ({ comment }) => {
100100
)}
101101
</Text>
102102
) : (
103-
<Text>Comment by deleted user</Text>
103+
<Text>
104+
Comment by{' '}
105+
<Text as="span" color="grey">
106+
deleted user
107+
</Text>
108+
</Text>
104109
)}
105110
<Text color="grey" textAlign="right">
106111
Posted {moment(comment.created).format('HH:mm Do MMM YYYY')}

client/pages/account.js

Lines changed: 106 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import React, { useState, useContext } from 'react'
22
import getConfig from 'next/config'
3+
import { useRouter } from 'next/router'
34
import moment from 'moment'
45
import copy from 'copy-to-clipboard'
56
import jwt from 'jsonwebtoken'
67
import pluralize from 'pluralize'
8+
import { ThemeContext } from 'styled-components'
9+
import { transparentize } from 'polished'
710
import { Copy } from '@styled-icons/boxicons-regular/Copy'
811
import { Check } from '@styled-icons/boxicons-regular/Check'
912
import { X } from '@styled-icons/boxicons-regular/X'
@@ -74,10 +77,13 @@ const Account = ({ token, invites = [], user, userRole }) => {
7477
const [totpEnabled, setTotpEnabled] = useState(user.totp.enabled)
7578
const [totpQrData, setTotpQrData] = useState()
7679
const [totpBackupCodes, setTotpBackupCodes] = useState()
80+
const [showDeleteAccountModal, setShowDeleteAccountModal] = useState(false)
7781

7882
const { addNotification } = useContext(NotificationContext)
7983
const { setLoading } = useContext(LoadingContext)
8084

85+
const theme = useContext(ThemeContext)
86+
8187
const {
8288
publicRuntimeConfig: {
8389
SQ_API_URL,
@@ -88,6 +94,8 @@ const Account = ({ token, invites = [], user, userRole }) => {
8894
},
8995
} = getConfig()
9096

97+
const router = useRouter()
98+
9199
const handleGenerateInvite = async (e) => {
92100
e.preventDefault()
93101
setLoading(true)
@@ -283,6 +291,34 @@ const Account = ({ token, invites = [], user, userRole }) => {
283291
}
284292
}
285293

294+
const handleDeleteAccount = async (e) => {
295+
e.preventDefault()
296+
const form = new FormData(e.target)
297+
298+
try {
299+
const deleteAccountRes = await fetch(`${SQ_API_URL}/account/delete`, {
300+
method: 'POST',
301+
headers: {
302+
'Content-Type': 'application/json',
303+
Authorization: `Bearer ${token}`,
304+
},
305+
body: JSON.stringify({
306+
password: form.get('password'),
307+
}),
308+
})
309+
310+
if (deleteAccountRes.status !== 200) {
311+
const reason = await deleteAccountRes.text()
312+
throw new Error(reason)
313+
}
314+
315+
await router.push('/logout')
316+
} catch (e) {
317+
addNotification('error', `Could not delete account: ${e.message}`)
318+
console.error(e)
319+
}
320+
}
321+
286322
return (
287323
<>
288324
<SEO title="My account" />
@@ -527,27 +563,46 @@ const Account = ({ token, invites = [], user, userRole }) => {
527563
)}
528564
</form>
529565
</Box>
530-
<Text as="h2" mb={4}>
531-
Change password
532-
</Text>
533-
<form onSubmit={handleChangePassword}>
534-
<Input
535-
name="password"
536-
type="password"
537-
label="Current password"
538-
mb={4}
539-
required
540-
/>
541-
<Input
542-
name="newPassword"
543-
type="password"
544-
label="New password"
545-
autoComplete="new-password"
546-
mb={4}
547-
required
548-
/>
549-
<Button>Change password</Button>
550-
</form>
566+
<Box mb={5}>
567+
<Text as="h2" mb={4}>
568+
Change password
569+
</Text>
570+
<form onSubmit={handleChangePassword}>
571+
<Input
572+
name="password"
573+
type="password"
574+
label="Current password"
575+
mb={4}
576+
required
577+
/>
578+
<Input
579+
name="newPassword"
580+
type="password"
581+
label="New password"
582+
autoComplete="new-password"
583+
mb={4}
584+
required
585+
/>
586+
<Button>Change password</Button>
587+
</form>
588+
</Box>
589+
{user.username !== 'admin' && (
590+
<Box
591+
bg={transparentize(0.7, theme.colors.error)}
592+
borderRadius={1}
593+
p={4}
594+
>
595+
<Text as="h2" mb={4}>
596+
Danger zone
597+
</Text>
598+
<Button
599+
variant="danger"
600+
onClick={() => setShowDeleteAccountModal(true)}
601+
>
602+
Delete my account
603+
</Button>
604+
</Box>
605+
)}
551606
{showInviteModal && (
552607
<Modal close={() => setShowInviteModal(false)}>
553608
<Text mb={5}>
@@ -566,6 +621,7 @@ const Account = ({ token, invites = [], user, userRole }) => {
566621
<Box display="flex" justifyContent="flex-end">
567622
<Button
568623
onClick={() => setShowInviteModal(false)}
624+
type="button"
569625
variant="secondary"
570626
mr={3}
571627
>
@@ -576,6 +632,35 @@ const Account = ({ token, invites = [], user, userRole }) => {
576632
</form>
577633
</Modal>
578634
)}
635+
{showDeleteAccountModal && (
636+
<Modal close={() => setShowDeleteAccountModal(false)}>
637+
<Text mb={5}>
638+
Are you sure you want to delete your account? This action cannot be
639+
undone, and you may not be able to register again. Your personal
640+
information will be deleted but your uploaded torrents will remain.
641+
</Text>
642+
<form onSubmit={handleDeleteAccount}>
643+
<Input
644+
name="password"
645+
type="password"
646+
label="Password"
647+
mb={4}
648+
required
649+
/>
650+
<Box display="flex" justifyContent="flex-end">
651+
<Button
652+
onClick={() => setShowDeleteAccountModal(false)}
653+
type="button"
654+
variant="secondary"
655+
mr={3}
656+
>
657+
Cancel
658+
</Button>
659+
<Button variant="danger">Yes, delete my account</Button>
660+
</Box>
661+
</form>
662+
</Modal>
663+
)}
579664
</>
580665
)
581666
}

client/pages/requests/[index].js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ const Request = ({ request, token, user }) => {
207207
<Box display="flex" alignItems="center">
208208
<Text as="h1">{request.title}</Text>
209209
</Box>
210-
{user === request.createdBy._id && (
210+
{user === request.createdBy?._id && (
211211
<Button onClick={handleDelete} variant="secondary">
212212
Delete
213213
</Button>

0 commit comments

Comments
 (0)