diff options
12 files changed, 144 insertions, 4 deletions
diff --git a/client/src/app/+my-account/my-account-settings/my-account-danger-zone/index.ts b/client/src/app/+my-account/my-account-settings/my-account-danger-zone/index.ts new file mode 100644 index 000000000..88a39bb97 --- /dev/null +++ b/client/src/app/+my-account/my-account-settings/my-account-danger-zone/index.ts | |||
@@ -0,0 +1 @@ | |||
export * from './my-account-danger-zone.component' | |||
diff --git a/client/src/app/+my-account/my-account-settings/my-account-danger-zone/my-account-danger-zone.component.html b/client/src/app/+my-account/my-account-settings/my-account-danger-zone/my-account-danger-zone.component.html new file mode 100644 index 000000000..8caff972f --- /dev/null +++ b/client/src/app/+my-account/my-account-settings/my-account-danger-zone/my-account-danger-zone.component.html | |||
@@ -0,0 +1,5 @@ | |||
1 | <div class="delete-me"> | ||
2 | <p>Once you delete your account, there is no going back. Please be certain.</p> | ||
3 | |||
4 | <button (click)="deleteMe()">Delete your account</button> | ||
5 | </div> \ No newline at end of file | ||
diff --git a/client/src/app/+my-account/my-account-settings/my-account-danger-zone/my-account-danger-zone.component.scss b/client/src/app/+my-account/my-account-settings/my-account-danger-zone/my-account-danger-zone.component.scss new file mode 100644 index 000000000..0ca310468 --- /dev/null +++ b/client/src/app/+my-account/my-account-settings/my-account-danger-zone/my-account-danger-zone.component.scss | |||
@@ -0,0 +1,11 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | .delete-me { | ||
5 | font-size: 15px; | ||
6 | |||
7 | button { | ||
8 | @include peertube-button; | ||
9 | @include grey-button; | ||
10 | } | ||
11 | } \ No newline at end of file | ||
diff --git a/client/src/app/+my-account/my-account-settings/my-account-danger-zone/my-account-danger-zone.component.ts b/client/src/app/+my-account/my-account-settings/my-account-danger-zone/my-account-danger-zone.component.ts new file mode 100644 index 000000000..63a121f64 --- /dev/null +++ b/client/src/app/+my-account/my-account-settings/my-account-danger-zone/my-account-danger-zone.component.ts | |||
@@ -0,0 +1,46 @@ | |||
1 | import { Component, Input } from '@angular/core' | ||
2 | import { NotificationsService } from 'angular2-notifications' | ||
3 | import { AuthService, ConfirmService, RedirectService } from '../../../core' | ||
4 | import { UserService } from '../../../shared' | ||
5 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
6 | import { User } from '@app/shared' | ||
7 | |||
8 | @Component({ | ||
9 | selector: 'my-account-danger-zone', | ||
10 | templateUrl: './my-account-danger-zone.component.html', | ||
11 | styleUrls: [ './my-account-danger-zone.component.scss' ] | ||
12 | }) | ||
13 | export class MyAccountDangerZoneComponent { | ||
14 | @Input() user: User = null | ||
15 | |||
16 | constructor ( | ||
17 | private authService: AuthService, | ||
18 | private notificationsService: NotificationsService, | ||
19 | private userService: UserService, | ||
20 | private confirmService: ConfirmService, | ||
21 | private redirectService: RedirectService, | ||
22 | private i18n: I18n | ||
23 | ) { } | ||
24 | |||
25 | async deleteMe () { | ||
26 | const res = await this.confirmService.confirmWithInput( | ||
27 | this.i18n('Are you sure you want to delete your account? This will delete all you data, including channels, videos etc.'), | ||
28 | this.i18n('Type your username to confirm'), | ||
29 | this.user.username, | ||
30 | this.i18n('Delete your account'), | ||
31 | this.i18n('Delete my account') | ||
32 | ) | ||
33 | if (res === false) return | ||
34 | |||
35 | this.userService.deleteMe().subscribe( | ||
36 | () => { | ||
37 | this.notificationsService.success(this.i18n('Success'), this.i18n('Your account is deleted.')) | ||
38 | |||
39 | this.authService.logout() | ||
40 | this.redirectService.redirectToHomepage() | ||
41 | }, | ||
42 | |||
43 | err => this.notificationsService.error(this.i18n('Error'), err.message) | ||
44 | ) | ||
45 | } | ||
46 | } | ||
diff --git a/client/src/app/+my-account/my-account-settings/my-account-settings.component.html b/client/src/app/+my-account/my-account-settings/my-account-settings.component.html index ff08cb777..c7e23cd1f 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-settings.component.html +++ b/client/src/app/+my-account/my-account-settings/my-account-settings.component.html | |||
@@ -14,3 +14,6 @@ | |||
14 | 14 | ||
15 | <div i18n class="account-title">Video settings</div> | 15 | <div i18n class="account-title">Video settings</div> |
16 | <my-account-video-settings [user]="user" [userInformationLoaded]="userInformationLoaded"></my-account-video-settings> | 16 | <my-account-video-settings [user]="user" [userInformationLoaded]="userInformationLoaded"></my-account-video-settings> |
17 | |||
18 | <div i18n class="account-title">Danger zone</div> | ||
19 | <my-account-danger-zone [user]="user"></my-account-danger-zone> \ No newline at end of file | ||
diff --git a/client/src/app/+my-account/my-account.module.ts b/client/src/app/+my-account/my-account.module.ts index 5403ab649..29b49e8d9 100644 --- a/client/src/app/+my-account/my-account.module.ts +++ b/client/src/app/+my-account/my-account.module.ts | |||
@@ -13,6 +13,7 @@ import { MyAccountVideoChannelCreateComponent } from '@app/+my-account/my-accoun | |||
13 | import { MyAccountVideoChannelUpdateComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channel-update.component' | 13 | import { MyAccountVideoChannelUpdateComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channel-update.component' |
14 | import { ActorAvatarInfoComponent } from '@app/+my-account/shared/actor-avatar-info.component' | 14 | import { ActorAvatarInfoComponent } from '@app/+my-account/shared/actor-avatar-info.component' |
15 | import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-video-imports/my-account-video-imports.component' | 15 | import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-video-imports/my-account-video-imports.component' |
16 | import { MyAccountDangerZoneComponent } from '@app/+my-account/my-account-settings/my-account-danger-zone' | ||
16 | 17 | ||
17 | @NgModule({ | 18 | @NgModule({ |
18 | imports: [ | 19 | imports: [ |
@@ -32,7 +33,8 @@ import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-vide | |||
32 | MyAccountVideoChannelCreateComponent, | 33 | MyAccountVideoChannelCreateComponent, |
33 | MyAccountVideoChannelUpdateComponent, | 34 | MyAccountVideoChannelUpdateComponent, |
34 | ActorAvatarInfoComponent, | 35 | ActorAvatarInfoComponent, |
35 | MyAccountVideoImportsComponent | 36 | MyAccountVideoImportsComponent, |
37 | MyAccountDangerZoneComponent | ||
36 | ], | 38 | ], |
37 | 39 | ||
38 | exports: [ | 40 | exports: [ |
diff --git a/client/src/app/shared/users/user.service.ts b/client/src/app/shared/users/user.service.ts index 365a21df7..e6dc3dbf8 100644 --- a/client/src/app/shared/users/user.service.ts +++ b/client/src/app/shared/users/user.service.ts | |||
@@ -39,6 +39,16 @@ export class UserService { | |||
39 | ) | 39 | ) |
40 | } | 40 | } |
41 | 41 | ||
42 | deleteMe () { | ||
43 | const url = UserService.BASE_USERS_URL + 'me' | ||
44 | |||
45 | return this.authHttp.delete(url) | ||
46 | .pipe( | ||
47 | map(this.restExtractor.extractDataBool), | ||
48 | catchError(err => this.restExtractor.handleError(err)) | ||
49 | ) | ||
50 | } | ||
51 | |||
42 | changeAvatar (avatarForm: FormData) { | 52 | changeAvatar (avatarForm: FormData) { |
43 | const url = UserService.BASE_USERS_URL + 'me/avatar/pick' | 53 | const url = UserService.BASE_USERS_URL + 'me/avatar/pick' |
44 | 54 | ||
diff --git a/server/controllers/api/users.ts b/server/controllers/api/users.ts index 36bf0e0fe..3d2586c3a 100644 --- a/server/controllers/api/users.ts +++ b/server/controllers/api/users.ts | |||
@@ -30,6 +30,7 @@ import { | |||
30 | usersVideoRatingValidator | 30 | usersVideoRatingValidator |
31 | } from '../../middlewares' | 31 | } from '../../middlewares' |
32 | import { | 32 | import { |
33 | deleteMeValidator, | ||
33 | usersAskResetPasswordValidator, | 34 | usersAskResetPasswordValidator, |
34 | usersResetPasswordValidator, | 35 | usersResetPasswordValidator, |
35 | videoImportsSortValidator, | 36 | videoImportsSortValidator, |
@@ -62,6 +63,11 @@ usersRouter.get('/me', | |||
62 | authenticate, | 63 | authenticate, |
63 | asyncMiddleware(getUserInformation) | 64 | asyncMiddleware(getUserInformation) |
64 | ) | 65 | ) |
66 | usersRouter.delete('/me', | ||
67 | authenticate, | ||
68 | asyncMiddleware(deleteMeValidator), | ||
69 | asyncMiddleware(deleteMe) | ||
70 | ) | ||
65 | 71 | ||
66 | usersRouter.get('/me/video-quota-used', | 72 | usersRouter.get('/me/video-quota-used', |
67 | authenticate, | 73 | authenticate, |
@@ -296,8 +302,18 @@ async function listUsers (req: express.Request, res: express.Response, next: exp | |||
296 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | 302 | return res.json(getFormattedObjects(resultList.data, resultList.total)) |
297 | } | 303 | } |
298 | 304 | ||
305 | async function deleteMe (req: express.Request, res: express.Response) { | ||
306 | const user: UserModel = res.locals.oauth.token.User | ||
307 | |||
308 | await user.destroy() | ||
309 | |||
310 | auditLogger.delete(res.locals.oauth.token.User.Account.Actor.getIdentifier(), new UserAuditView(user.toFormattedJSON())) | ||
311 | |||
312 | return res.sendStatus(204) | ||
313 | } | ||
314 | |||
299 | async function removeUser (req: express.Request, res: express.Response, next: express.NextFunction) { | 315 | async function removeUser (req: express.Request, res: express.Response, next: express.NextFunction) { |
300 | const user = await UserModel.loadById(req.params.id) | 316 | const user: UserModel = res.locals.user |
301 | 317 | ||
302 | await user.destroy() | 318 | await user.destroy() |
303 | 319 | ||
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts index 8ca9763a1..3c207c81f 100644 --- a/server/middlewares/validators/users.ts +++ b/server/middlewares/validators/users.ts | |||
@@ -74,6 +74,19 @@ const usersRemoveValidator = [ | |||
74 | } | 74 | } |
75 | ] | 75 | ] |
76 | 76 | ||
77 | const deleteMeValidator = [ | ||
78 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
79 | const user: UserModel = res.locals.oauth.token.User | ||
80 | if (user.username === 'root') { | ||
81 | return res.status(400) | ||
82 | .send({ error: 'You cannot delete your root account.' }) | ||
83 | .end() | ||
84 | } | ||
85 | |||
86 | return next() | ||
87 | } | ||
88 | ] | ||
89 | |||
77 | const usersUpdateValidator = [ | 90 | const usersUpdateValidator = [ |
78 | param('id').isInt().not().isEmpty().withMessage('Should have a valid id'), | 91 | param('id').isInt().not().isEmpty().withMessage('Should have a valid id'), |
79 | body('email').optional().isEmail().withMessage('Should have a valid email attribute'), | 92 | body('email').optional().isEmail().withMessage('Should have a valid email attribute'), |
@@ -215,6 +228,7 @@ const usersResetPasswordValidator = [ | |||
215 | 228 | ||
216 | export { | 229 | export { |
217 | usersAddValidator, | 230 | usersAddValidator, |
231 | deleteMeValidator, | ||
218 | usersRegisterValidator, | 232 | usersRegisterValidator, |
219 | usersRemoveValidator, | 233 | usersRemoveValidator, |
220 | usersUpdateValidator, | 234 | usersUpdateValidator, |
diff --git a/server/tests/api/check-params/users.ts b/server/tests/api/check-params/users.ts index 62faabc54..60165ae22 100644 --- a/server/tests/api/check-params/users.ts +++ b/server/tests/api/check-params/users.ts | |||
@@ -8,7 +8,7 @@ import { UserRole, VideoImport, VideoImportState } from '../../../../shared' | |||
8 | import { | 8 | import { |
9 | createUser, flushTests, getMyUserInformation, getMyUserVideoRating, getUsersList, immutableAssign, killallServers, makeGetRequest, | 9 | createUser, flushTests, getMyUserInformation, getMyUserVideoRating, getUsersList, immutableAssign, killallServers, makeGetRequest, |
10 | makePostBodyRequest, makeUploadRequest, makePutBodyRequest, registerUser, removeUser, runServer, ServerInfo, setAccessTokensToServers, | 10 | makePostBodyRequest, makeUploadRequest, makePutBodyRequest, registerUser, removeUser, runServer, ServerInfo, setAccessTokensToServers, |
11 | updateUser, uploadVideo, userLogin | 11 | updateUser, uploadVideo, userLogin, deleteMe |
12 | } from '../../utils' | 12 | } from '../../utils' |
13 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' | 13 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' |
14 | import { getMagnetURI, getMyVideoImports, getYoutubeVideoUrl, importVideo } from '../../utils/videos/video-imports' | 14 | import { getMagnetURI, getMyVideoImports, getYoutubeVideoUrl, importVideo } from '../../utils/videos/video-imports' |
@@ -469,6 +469,12 @@ describe('Test users API validators', function () { | |||
469 | }) | 469 | }) |
470 | }) | 470 | }) |
471 | 471 | ||
472 | describe('When deleting our account', function () { | ||
473 | it('Should fail with with the root account', async function () { | ||
474 | await deleteMe(server.url, server.accessToken, 400) | ||
475 | }) | ||
476 | }) | ||
477 | |||
472 | describe('When register a new user', function () { | 478 | describe('When register a new user', function () { |
473 | const registrationPath = path + '/register' | 479 | const registrationPath = path + '/register' |
474 | const baseCorrectParams = { | 480 | const baseCorrectParams = { |
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts index 1ea599859..c9e8eb6f9 100644 --- a/server/tests/api/users/users.ts +++ b/server/tests/api/users/users.ts | |||
@@ -6,7 +6,8 @@ import { UserRole } from '../../../../shared/index' | |||
6 | import { | 6 | import { |
7 | createUser, flushTests, getBlacklistedVideosList, getMyUserInformation, getMyUserVideoQuotaUsed, getMyUserVideoRating, | 7 | createUser, flushTests, getBlacklistedVideosList, getMyUserInformation, getMyUserVideoQuotaUsed, getMyUserVideoRating, |
8 | getUserInformation, getUsersList, getUsersListPaginationAndSort, getVideosList, killallServers, login, makePutBodyRequest, rateVideo, | 8 | getUserInformation, getUsersList, getUsersListPaginationAndSort, getVideosList, killallServers, login, makePutBodyRequest, rateVideo, |
9 | registerUser, removeUser, removeVideo, runServer, ServerInfo, testImage, updateMyAvatar, updateMyUser, updateUser, uploadVideo, userLogin | 9 | registerUser, removeUser, removeVideo, runServer, ServerInfo, testImage, updateMyAvatar, updateMyUser, updateUser, uploadVideo, userLogin, |
10 | deleteMe | ||
10 | } from '../../utils/index' | 11 | } from '../../utils/index' |
11 | import { follow } from '../../utils/server/follows' | 12 | import { follow } from '../../utils/server/follows' |
12 | import { setAccessTokensToServers } from '../../utils/users/login' | 13 | import { setAccessTokensToServers } from '../../utils/users/login' |
@@ -478,6 +479,20 @@ describe('Test users', function () { | |||
478 | expect(user.videoQuota).to.equal(5 * 1024 * 1024) | 479 | expect(user.videoQuota).to.equal(5 * 1024 * 1024) |
479 | }) | 480 | }) |
480 | 481 | ||
482 | it('Should remove me', async function () { | ||
483 | { | ||
484 | const res = await getUsersList(server.url, server.accessToken) | ||
485 | expect(res.body.data.find(u => u.username === 'user_15')).to.not.be.undefined | ||
486 | } | ||
487 | |||
488 | await deleteMe(server.url, accessToken) | ||
489 | |||
490 | { | ||
491 | const res = await getUsersList(server.url, server.accessToken) | ||
492 | expect(res.body.data.find(u => u.username === 'user_15')).to.be.undefined | ||
493 | } | ||
494 | }) | ||
495 | |||
481 | after(async function () { | 496 | after(async function () { |
482 | killallServers([ server ]) | 497 | killallServers([ server ]) |
483 | 498 | ||
diff --git a/server/tests/utils/users/users.ts b/server/tests/utils/users/users.ts index 37b15f64a..e24e721bd 100644 --- a/server/tests/utils/users/users.ts +++ b/server/tests/utils/users/users.ts | |||
@@ -56,6 +56,16 @@ function getMyUserInformation (url: string, accessToken: string, specialStatus = | |||
56 | .expect('Content-Type', /json/) | 56 | .expect('Content-Type', /json/) |
57 | } | 57 | } |
58 | 58 | ||
59 | function deleteMe (url: string, accessToken: string, specialStatus = 204) { | ||
60 | const path = '/api/v1/users/me' | ||
61 | |||
62 | return request(url) | ||
63 | .delete(path) | ||
64 | .set('Accept', 'application/json') | ||
65 | .set('Authorization', 'Bearer ' + accessToken) | ||
66 | .expect(specialStatus) | ||
67 | } | ||
68 | |||
59 | function getMyUserVideoQuotaUsed (url: string, accessToken: string, specialStatus = 200) { | 69 | function getMyUserVideoQuotaUsed (url: string, accessToken: string, specialStatus = 200) { |
60 | const path = '/api/v1/users/me/video-quota-used' | 70 | const path = '/api/v1/users/me/video-quota-used' |
61 | 71 | ||
@@ -216,6 +226,7 @@ export { | |||
216 | registerUser, | 226 | registerUser, |
217 | getMyUserInformation, | 227 | getMyUserInformation, |
218 | getMyUserVideoRating, | 228 | getMyUserVideoRating, |
229 | deleteMe, | ||
219 | getMyUserVideoQuotaUsed, | 230 | getMyUserVideoQuotaUsed, |
220 | getUsersList, | 231 | getUsersList, |
221 | getUsersListPaginationAndSort, | 232 | getUsersListPaginationAndSort, |