]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Add ability to delete our account
authorChocobozzz <me@florianbigard.com>
Wed, 8 Aug 2018 08:55:27 +0000 (10:55 +0200)
committerChocobozzz <me@florianbigard.com>
Wed, 8 Aug 2018 08:55:27 +0000 (10:55 +0200)
12 files changed:
client/src/app/+my-account/my-account-settings/my-account-danger-zone/index.ts [new file with mode: 0644]
client/src/app/+my-account/my-account-settings/my-account-danger-zone/my-account-danger-zone.component.html [new file with mode: 0644]
client/src/app/+my-account/my-account-settings/my-account-danger-zone/my-account-danger-zone.component.scss [new file with mode: 0644]
client/src/app/+my-account/my-account-settings/my-account-danger-zone/my-account-danger-zone.component.ts [new file with mode: 0644]
client/src/app/+my-account/my-account-settings/my-account-settings.component.html
client/src/app/+my-account/my-account.module.ts
client/src/app/shared/users/user.service.ts
server/controllers/api/users.ts
server/middlewares/validators/users.ts
server/tests/api/check-params/users.ts
server/tests/api/users/users.ts
server/tests/utils/users/users.ts

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 (file)
index 0000000..88a39bb
--- /dev/null
@@ -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 (file)
index 0000000..8caff97
--- /dev/null
@@ -0,0 +1,5 @@
+<div class="delete-me">
+  <p>Once you delete your account, there is no going back. Please be certain.</p>
+
+  <button (click)="deleteMe()">Delete your account</button>
+</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 (file)
index 0000000..0ca3104
--- /dev/null
@@ -0,0 +1,11 @@
+@import '_variables';
+@import '_mixins';
+
+.delete-me {
+  font-size: 15px;
+
+  button {
+    @include peertube-button;
+    @include grey-button;
+  }
+}
\ 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 (file)
index 0000000..63a121f
--- /dev/null
@@ -0,0 +1,46 @@
+import { Component, Input } from '@angular/core'
+import { NotificationsService } from 'angular2-notifications'
+import { AuthService, ConfirmService, RedirectService } from '../../../core'
+import { UserService } from '../../../shared'
+import { I18n } from '@ngx-translate/i18n-polyfill'
+import { User } from '@app/shared'
+
+@Component({
+  selector: 'my-account-danger-zone',
+  templateUrl: './my-account-danger-zone.component.html',
+  styleUrls: [ './my-account-danger-zone.component.scss' ]
+})
+export class MyAccountDangerZoneComponent {
+  @Input() user: User = null
+
+  constructor (
+    private authService: AuthService,
+    private notificationsService: NotificationsService,
+    private userService: UserService,
+    private confirmService: ConfirmService,
+    private redirectService: RedirectService,
+    private i18n: I18n
+  ) { }
+
+  async deleteMe () {
+    const res = await this.confirmService.confirmWithInput(
+      this.i18n('Are you sure you want to delete your account? This will delete all you data, including channels, videos etc.'),
+      this.i18n('Type your username to confirm'),
+      this.user.username,
+      this.i18n('Delete your account'),
+      this.i18n('Delete my account')
+    )
+    if (res === false) return
+
+    this.userService.deleteMe().subscribe(
+      () => {
+        this.notificationsService.success(this.i18n('Success'), this.i18n('Your account is deleted.'))
+
+        this.authService.logout()
+        this.redirectService.redirectToHomepage()
+      },
+
+      err => this.notificationsService.error(this.i18n('Error'), err.message)
+    )
+  }
+}
index ff08cb7771fc33d505671ca477add55042be8b7f..c7e23cd1ff79453b42a1bebb97540cba2fbf0f82 100644 (file)
@@ -14,3 +14,6 @@
 
 <div i18n class="account-title">Video settings</div>
 <my-account-video-settings [user]="user" [userInformationLoaded]="userInformationLoaded"></my-account-video-settings>
+
+<div i18n class="account-title">Danger zone</div>
+<my-account-danger-zone [user]="user"></my-account-danger-zone>
\ No newline at end of file
index 5403ab64945c0aa21d5a15fab002fa5087ac7608..29b49e8d965d2cab8efe44d7b225c91aa92bff31 100644 (file)
@@ -13,6 +13,7 @@ import { MyAccountVideoChannelCreateComponent } from '@app/+my-account/my-accoun
 import { MyAccountVideoChannelUpdateComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channel-update.component'
 import { ActorAvatarInfoComponent } from '@app/+my-account/shared/actor-avatar-info.component'
 import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-video-imports/my-account-video-imports.component'
+import { MyAccountDangerZoneComponent } from '@app/+my-account/my-account-settings/my-account-danger-zone'
 
 @NgModule({
   imports: [
@@ -32,7 +33,8 @@ import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-vide
     MyAccountVideoChannelCreateComponent,
     MyAccountVideoChannelUpdateComponent,
     ActorAvatarInfoComponent,
-    MyAccountVideoImportsComponent
+    MyAccountVideoImportsComponent,
+    MyAccountDangerZoneComponent
   ],
 
   exports: [
index 365a21df74675c64d5eac7fb2f9db1e6a62e0701..e6dc3dbf8e51d4a600fc8b3e816804877066f65c 100644 (file)
@@ -39,6 +39,16 @@ export class UserService {
                )
   }
 
+  deleteMe () {
+    const url = UserService.BASE_USERS_URL + 'me'
+
+    return this.authHttp.delete(url)
+               .pipe(
+                 map(this.restExtractor.extractDataBool),
+                 catchError(err => this.restExtractor.handleError(err))
+               )
+  }
+
   changeAvatar (avatarForm: FormData) {
     const url = UserService.BASE_USERS_URL + 'me/avatar/pick'
 
index 36bf0e0fe49713249f95ee5a399f6f80e0d7734f..3d2586c3a1f31bc8a21026f786f93a4256665d91 100644 (file)
@@ -30,6 +30,7 @@ import {
   usersVideoRatingValidator
 } from '../../middlewares'
 import {
+  deleteMeValidator,
   usersAskResetPasswordValidator,
   usersResetPasswordValidator,
   videoImportsSortValidator,
@@ -62,6 +63,11 @@ usersRouter.get('/me',
   authenticate,
   asyncMiddleware(getUserInformation)
 )
+usersRouter.delete('/me',
+  authenticate,
+  asyncMiddleware(deleteMeValidator),
+  asyncMiddleware(deleteMe)
+)
 
 usersRouter.get('/me/video-quota-used',
   authenticate,
@@ -296,8 +302,18 @@ async function listUsers (req: express.Request, res: express.Response, next: exp
   return res.json(getFormattedObjects(resultList.data, resultList.total))
 }
 
+async function deleteMe (req: express.Request, res: express.Response) {
+  const user: UserModel = res.locals.oauth.token.User
+
+  await user.destroy()
+
+  auditLogger.delete(res.locals.oauth.token.User.Account.Actor.getIdentifier(), new UserAuditView(user.toFormattedJSON()))
+
+  return res.sendStatus(204)
+}
+
 async function removeUser (req: express.Request, res: express.Response, next: express.NextFunction) {
-  const user = await UserModel.loadById(req.params.id)
+  const user: UserModel = res.locals.user
 
   await user.destroy()
 
index 8ca9763a1ddf6b164aae734bc0f567728a0c6367..3c207c81fa9b04e98beaa7c2aece996cc50c3bdb 100644 (file)
@@ -74,6 +74,19 @@ const usersRemoveValidator = [
   }
 ]
 
+const deleteMeValidator = [
+  async (req: express.Request, res: express.Response, next: express.NextFunction) => {
+    const user: UserModel = res.locals.oauth.token.User
+    if (user.username === 'root') {
+      return res.status(400)
+                .send({ error: 'You cannot delete your root account.' })
+                .end()
+    }
+
+    return next()
+  }
+]
+
 const usersUpdateValidator = [
   param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
   body('email').optional().isEmail().withMessage('Should have a valid email attribute'),
@@ -215,6 +228,7 @@ const usersResetPasswordValidator = [
 
 export {
   usersAddValidator,
+  deleteMeValidator,
   usersRegisterValidator,
   usersRemoveValidator,
   usersUpdateValidator,
index 62faabc542923433a8d0add42025872efe97d371..60165ae228421de5e2e9b17f3ab2e8757a335346 100644 (file)
@@ -8,7 +8,7 @@ import { UserRole, VideoImport, VideoImportState } from '../../../../shared'
 import {
   createUser, flushTests, getMyUserInformation, getMyUserVideoRating, getUsersList, immutableAssign, killallServers, makeGetRequest,
   makePostBodyRequest, makeUploadRequest, makePutBodyRequest, registerUser, removeUser, runServer, ServerInfo, setAccessTokensToServers,
-  updateUser, uploadVideo, userLogin
+  updateUser, uploadVideo, userLogin, deleteMe
 } from '../../utils'
 import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params'
 import { getMagnetURI, getMyVideoImports, getYoutubeVideoUrl, importVideo } from '../../utils/videos/video-imports'
@@ -469,6 +469,12 @@ describe('Test users API validators', function () {
     })
   })
 
+  describe('When deleting our account', function () {
+    it('Should fail with with the root account', async function () {
+      await deleteMe(server.url, server.accessToken, 400)
+    })
+  })
+
   describe('When register a new user', function () {
     const registrationPath = path + '/register'
     const baseCorrectParams = {
index 1ea599859313a7310a3f71e4b5fffb61db8f1ab1..c9e8eb6f9310bd77ace07daa6c0bfea1e030475c 100644 (file)
@@ -6,7 +6,8 @@ import { UserRole } from '../../../../shared/index'
 import {
   createUser, flushTests, getBlacklistedVideosList, getMyUserInformation, getMyUserVideoQuotaUsed, getMyUserVideoRating,
   getUserInformation, getUsersList, getUsersListPaginationAndSort, getVideosList, killallServers, login, makePutBodyRequest, rateVideo,
-  registerUser, removeUser, removeVideo, runServer, ServerInfo, testImage, updateMyAvatar, updateMyUser, updateUser, uploadVideo, userLogin
+  registerUser, removeUser, removeVideo, runServer, ServerInfo, testImage, updateMyAvatar, updateMyUser, updateUser, uploadVideo, userLogin,
+  deleteMe
 } from '../../utils/index'
 import { follow } from '../../utils/server/follows'
 import { setAccessTokensToServers } from '../../utils/users/login'
@@ -478,6 +479,20 @@ describe('Test users', function () {
     expect(user.videoQuota).to.equal(5 * 1024 * 1024)
   })
 
+  it('Should remove me', async function () {
+    {
+      const res = await getUsersList(server.url, server.accessToken)
+      expect(res.body.data.find(u => u.username === 'user_15')).to.not.be.undefined
+    }
+
+    await deleteMe(server.url, accessToken)
+
+    {
+      const res = await getUsersList(server.url, server.accessToken)
+      expect(res.body.data.find(u => u.username === 'user_15')).to.be.undefined
+    }
+  })
+
   after(async function () {
     killallServers([ server ])
 
index 37b15f64a4e39753c2c7794b803d66158f60b618..e24e721bdba5604f240c3abef2b10f62fb22b115 100644 (file)
@@ -56,6 +56,16 @@ function getMyUserInformation (url: string, accessToken: string, specialStatus =
           .expect('Content-Type', /json/)
 }
 
+function deleteMe (url: string, accessToken: string, specialStatus = 204) {
+  const path = '/api/v1/users/me'
+
+  return request(url)
+    .delete(path)
+    .set('Accept', 'application/json')
+    .set('Authorization', 'Bearer ' + accessToken)
+    .expect(specialStatus)
+}
+
 function getMyUserVideoQuotaUsed (url: string, accessToken: string, specialStatus = 200) {
   const path = '/api/v1/users/me/video-quota-used'
 
@@ -216,6 +226,7 @@ export {
   registerUser,
   getMyUserInformation,
   getMyUserVideoRating,
+  deleteMe,
   getMyUserVideoQuotaUsed,
   getUsersList,
   getUsersListPaginationAndSort,