Add confirm when admin use custom js/css
authorChocobozzz <me@florianbigard.com>
Thu, 22 Feb 2018 14:29:32 +0000 (15:29 +0100)
committerChocobozzz <me@florianbigard.com>
Thu, 22 Feb 2018 14:29:32 +0000 (15:29 +0100)
17 files changed:
client/.angular-cli.json
client/src/app/+admin/config/config.routes.ts
client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
client/src/app/+admin/follows/following-add/following-add.component.ts
client/src/app/+admin/follows/following-list/following-list.component.ts
client/src/app/+admin/users/user-list/user-list.component.ts
client/src/app/+admin/video-blacklist/video-blacklist-list/video-blacklist-list.component.ts
client/src/app/account/account-videos/account-videos.component.ts
client/src/app/core/confirm/confirm.component.html
client/src/app/core/confirm/confirm.component.scss [new file with mode: 0644]
client/src/app/core/confirm/confirm.component.ts
client/src/app/core/confirm/confirm.service.ts
client/src/app/core/core.module.ts
client/src/app/shared/guards/can-deactivate-guard.service.ts
client/src/app/videos/+video-watch/comment/video-comments.component.ts
client/src/app/videos/+video-watch/video-watch.component.ts
server/controllers/client.ts

index 643e1b319884e7db1ef06c7a08c4daff30d4a3ea..cb387db0a85f2fb084268acce48a3ac90b2e9f08 100644 (file)
@@ -8,12 +8,7 @@
       "root": "src",
       "outDir": "dist",
       "assets": [
-        {
-          "glob": "**/*",
-          "input": "./assets/images",
-          "output": "./client/assets/images",
-          "allowOutsideOutDir": false
-        },
+        "./assets/images",
         "./manifest.json"
       ],
       "deployUrl": "client/",
index a46b0ddfd8a3455b2c6c6f8ce327bf463f168f70..2ca2f8fdeadd2f94608a10c68e0170db83d09c96 100644 (file)
@@ -23,7 +23,7 @@ export const ConfigRoutes: Routes = [
         component: EditCustomConfigComponent,
         data: {
           meta: {
-            title: 'Following list'
+            title: 'Edit custom configuration'
           }
         }
       }
index 02726853616d7c765e7418e8695c62b696bdd9c7..ccec89a8ed9851ac4d89a57748fdd37fdef7eb7a 100644 (file)
@@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core'
 import { FormBuilder, FormGroup } from '@angular/forms'
 import { Router } from '@angular/router'
 import { ConfigService } from '@app/+admin/config/shared/config.service'
+import { ConfirmService } from '@app/core'
 import { ServerService } from '@app/core/server/server.service'
 import { FormReactive, USER_VIDEO_QUOTA } from '@app/shared'
 import {
@@ -61,12 +62,16 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
     userVideoQuota: USER_VIDEO_QUOTA.MESSAGES
   }
 
+  private oldCustomJavascript: string
+  private oldCustomCSS: string
+
   constructor (
     private formBuilder: FormBuilder,
     private router: Router,
     private notificationsService: NotificationsService,
     private configService: ConfigService,
-    private serverService: ServerService
+    private serverService: ServerService,
+    private confirmService: ConfirmService
   ) {
     super()
   }
@@ -109,6 +114,9 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
         res => {
           this.customConfig = res
 
+          this.oldCustomCSS = this.customConfig.instance.customizations.css
+          this.oldCustomJavascript = this.customConfig.instance.customizations.javascript
+
           this.updateForm()
         },
 
@@ -124,7 +132,27 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
     return this.form.value['signupEnabled'] === true
   }
 
-  formValidated () {
+  async formValidated () {
+    const newCustomizationJavascript = this.form.value['customizationJavascript']
+    const newCustomizationCSS = this.form.value['customizationCSS']
+
+    const customizations = []
+    if (newCustomizationJavascript && newCustomizationJavascript !== this.oldCustomJavascript) customizations.push('JavaScript')
+    if (newCustomizationCSS && newCustomizationCSS !== this.oldCustomCSS) customizations.push('CSS')
+
+    if (customizations.length !== 0) {
+      const customizationsText = customizations.join('/')
+
+      const message = `You set custom ${customizationsText}. ` +
+        'This could lead to security issues or bugs if you do not understand it. ' +
+        'Are you sure you want to update the configuration?'
+      const label = `Please type "I understand the ${customizationsText} I set" to confirm.`
+      const expectedInputValue = `I understand the ${customizationsText} I set`
+
+      const confirmRes = await this.confirmService.confirmWithInput(message, label, expectedInputValue)
+      if (confirmRes === false) return
+    }
+
     const data = {
       instance: {
         name: this.form.value['instanceName'],
index bf842129d8b91c5aee8979ae56027a50e90f0bc3..c296c8852cbf15230952106dc3380c159cdbca51 100644 (file)
@@ -43,7 +43,7 @@ export class FollowingAddComponent {
     }
   }
 
-  addFollowing () {
+  async addFollowing () {
     this.error = ''
 
     const hosts = this.getNotEmptyHosts()
@@ -57,20 +57,17 @@ export class FollowingAddComponent {
     }
 
     const confirmMessage = 'If you confirm, you will send a follow request to:<br /> - ' + hosts.join('<br /> - ')
-    this.confirmService.confirm(confirmMessage, 'Follow new server(s)').subscribe(
-      res => {
-        if (res === false) return
+    const res = await this.confirmService.confirm(confirmMessage, 'Follow new server(s)')
+    if (res === false) return
 
-        this.followService.follow(hosts).subscribe(
-          status => {
-            this.notificationsService.success('Success', 'Follow request(s) sent!')
+    this.followService.follow(hosts).subscribe(
+      () => {
+        this.notificationsService.success('Success', 'Follow request(s) sent!')
 
-            setTimeout(() => this.router.navigate([ '/admin/follows/following-list' ]), 500)
-          },
+        setTimeout(() => this.router.navigate([ '/admin/follows/following-list' ]), 500)
+      },
 
-          err => this.notificationsService.error('Error', err.message)
-        )
-      }
+      err => this.notificationsService.error('Error', err.message)
     )
   }
 
index d4f8d0309ea4414036fd90c5c1a43fb663917790..ad1bd453674862d6352b98350b897eb07405ad92 100644 (file)
@@ -25,20 +25,17 @@ export class FollowingListComponent extends RestTable {
     super()
   }
 
-  removeFollowing (follow: AccountFollow) {
-    this.confirmService.confirm(`Do you really want to unfollow ${follow.following.host}?`, 'Unfollow').subscribe(
-      res => {
-        if (res === false) return
+  async removeFollowing (follow: AccountFollow) {
+    const res = await this.confirmService.confirm(`Do you really want to unfollow ${follow.following.host}?`, 'Unfollow')
+    if (res === false) return
 
-        this.followService.unfollow(follow).subscribe(
-          () => {
-            this.notificationsService.success('Success', `You are not following ${follow.following.host} anymore.`)
-            this.loadData()
-          },
+    this.followService.unfollow(follow).subscribe(
+      () => {
+        this.notificationsService.success('Success', `You are not following ${follow.following.host} anymore.`)
+        this.loadData()
+      },
 
-          err => this.notificationsService.error('Error', err.message)
-        )
-      }
+      err => this.notificationsService.error('Error', err.message)
     )
   }
 
index 1e8e1af49ea88545d56b041ec25638242c7c09f8..5121528080412933c85b1e0b3313817b38854c01 100644 (file)
@@ -1,10 +1,10 @@
 import { Component } from '@angular/core'
-import { SortMeta } from 'primeng/components/common/sortmeta'
 
 import { NotificationsService } from 'angular2-notifications'
+import { SortMeta } from 'primeng/components/common/sortmeta'
 
 import { ConfirmService } from '../../../core'
-import { RestTable, RestPagination, User } from '../../../shared'
+import { RestPagination, RestTable, User } from '../../../shared'
 import { UserService } from '../shared'
 
 @Component({
@@ -27,25 +27,22 @@ export class UserListComponent extends RestTable {
     super()
   }
 
-  removeUser (user: User) {
+  async removeUser (user: User) {
     if (user.username === 'root') {
       this.notificationsService.error('Error', 'You cannot delete root.')
       return
     }
 
-    this.confirmService.confirm('Do you really want to delete this user?', 'Delete').subscribe(
-      res => {
-        if (res === false) return
+    const res = await this.confirmService.confirm('Do you really want to delete this user?', 'Delete')
+    if (res === false) return
 
-        this.userService.removeUser(user).subscribe(
-          () => {
-            this.notificationsService.success('Success', `User ${user.username} deleted.`)
-            this.loadData()
-          },
+    this.userService.removeUser(user).subscribe(
+      () => {
+        this.notificationsService.success('Success', `User ${user.username} deleted.`)
+        this.loadData()
+      },
 
-          err => this.notificationsService.error('Error', err.message)
-        )
-      }
+      err => this.notificationsService.error('Error', err.message)
     )
   }
 
index 56024b247c27e4a71de4736a4e08109d2a3c77ab..f4cf2125917bc2abb90fe15c176fff3a4a4bd66a 100644 (file)
@@ -31,22 +31,19 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit {
     this.loadData()
   }
 
-  removeVideoFromBlacklist (entry: BlacklistedVideo) {
+  async removeVideoFromBlacklist (entry: BlacklistedVideo) {
     const confirmMessage = 'Do you really want to remove this video from the blacklist ? It will be available again in the video list.'
 
-    this.confirmService.confirm(confirmMessage, 'Remove').subscribe(
-      res => {
-        if (res === false) return
+    const res = await this.confirmService.confirm(confirmMessage, 'Remove')
+    if (res === false) return
 
-        this.videoBlacklistService.removeVideoFromBlacklist(entry.videoId).subscribe(
-          status => {
-            this.notificationsService.success('Success', `Video ${entry.name} removed from the blacklist.`)
-            this.loadData()
-          },
+    this.videoBlacklistService.removeVideoFromBlacklist(entry.videoId).subscribe(
+      () => {
+        this.notificationsService.success('Success', `Video ${entry.name} removed from the blacklist.`)
+        this.loadData()
+      },
 
-          err => this.notificationsService.error('Error', err.message)
-        )
-      }
+      err => this.notificationsService.error('Error', err.message)
     )
   }
 
index e9d044dbf43d27c797dab02b7853ccf8b93704f8..a286bad1c7432ca7cdf31a0339fcf96103dc3214 100644 (file)
@@ -56,55 +56,49 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit
     return this.videoService.getMyVideos(newPagination, this.sort)
   }
 
-  deleteSelectedVideos () {
+  async deleteSelectedVideos () {
     const toDeleteVideosIds = Object.keys(this.checkedVideos)
       .filter(k => this.checkedVideos[k] === true)
       .map(k => parseInt(k, 10))
 
-    this.confirmService.confirm(`Do you really want to delete ${toDeleteVideosIds.length} videos?`, 'Delete').subscribe(
-      res => {
-        if (res === false) return
-
-        const observables: Observable<any>[] = []
-        for (const videoId of toDeleteVideosIds) {
-          const o = this.videoService
-            .removeVideo(videoId)
-            .do(() => this.spliceVideosById(videoId))
-
-          observables.push(o)
-        }
-
-        Observable.from(observables)
-          .concatAll()
-          .subscribe(
-            res => {
-              this.notificationsService.success('Success', `${toDeleteVideosIds.length} videos deleted.`)
-              this.buildVideoPages()
-            },
-
-            err => this.notificationsService.error('Error', err.message)
-          )
-      }
-    )
+    const res = await this.confirmService.confirm(`Do you really want to delete ${toDeleteVideosIds.length} videos?`, 'Delete')
+    if (res === false) return
+
+    const observables: Observable<any>[] = []
+    for (const videoId of toDeleteVideosIds) {
+      const o = this.videoService
+        .removeVideo(videoId)
+        .do(() => this.spliceVideosById(videoId))
+
+      observables.push(o)
+    }
+
+    Observable.from(observables)
+      .concatAll()
+      .subscribe(
+        res => {
+          this.notificationsService.success('Success', `${toDeleteVideosIds.length} videos deleted.`)
+          this.buildVideoPages()
+        },
+
+        err => this.notificationsService.error('Error', err.message)
+      )
   }
 
-  deleteVideo (video: Video) {
-    this.confirmService.confirm(`Do you really want to delete ${video.name}?`, 'Delete').subscribe(
-      res => {
-        if (res === false) return
-
-        this.videoService.removeVideo(video.id)
-          .subscribe(
-            status => {
-              this.notificationsService.success('Success', `Video ${video.name} deleted.`)
-              this.spliceVideosById(video.id)
-              this.buildVideoPages()
-            },
-
-            error => this.notificationsService.error('Error', error.message)
-          )
-      }
-    )
+  async deleteVideo (video: Video) {
+    const res = await this.confirmService.confirm(`Do you really want to delete ${video.name}?`, 'Delete')
+    if (res === false) return
+
+    this.videoService.removeVideo(video.id)
+      .subscribe(
+        status => {
+          this.notificationsService.success('Success', `Video ${video.name} deleted.`)
+          this.spliceVideosById(video.id)
+          this.buildVideoPages()
+        },
+
+        error => this.notificationsService.error('Error', error.message)
+      )
   }
 
   private spliceVideosById (id: number) {
index cc2c28de27c15b0f6825b7c29eec6e4c184d0410..90274b24847fd452adb2c0ea390c6669c76a7298 100644 (file)
       <div class="modal-body" >
         <div [innerHtml]="message"></div>
 
+        <div *ngIf="inputLabel && expectedInputValue" class="form-group">
+          <label for="confirmInput">{{ inputLabel }}</label>
+          <input type="text" id="confirmInput" name="confirmInput" [(ngModel)]="inputValue" />
+        </div>
+
         <div class="form-group inputs">
           <span class="action-button action-button-cancel" (click)="cancel()">
             Cancel
           </span>
 
           <input
-            type="submit" value="Confirm" class="action-button-submit"
+            type="submit" value="Confirm" class="action-button-submit" [disabled]="isConfirmationDisabled()"
             (click)="confirm()"
           >
         </div>
diff --git a/client/src/app/core/confirm/confirm.component.scss b/client/src/app/core/confirm/confirm.component.scss
new file mode 100644 (file)
index 0000000..93dd792
--- /dev/null
@@ -0,0 +1,17 @@
+@import '_variables';
+@import '_mixins';
+
+.button {
+  padding: 0 13px;
+}
+
+input[type=text] {
+  @include peertube-input-text(100%);
+  display: block;
+}
+
+.form-group {
+  margin: 20px 0;
+}
+
+
index 0515d969aa7575d4021ccc306a32d7bb4ef32714..8f81b7a988a74fbec57da6bd22c6603ca2edd3ea 100644 (file)
@@ -4,21 +4,20 @@ import { ModalDirective } from 'ngx-bootstrap/modal'
 
 import { ConfirmService } from './confirm.service'
 
-export interface ConfigChangedEvent {
-  columns: { [id: string]: { isDisplayed: boolean } }
-  config: { resultsPerPage: number }
-}
-
 @Component({
   selector: 'my-confirm',
   templateUrl: './confirm.component.html',
-  styles: [ '.button { padding: 0 13px; }' ]
+  styleUrls: [ './confirm.component.scss' ]
 })
 export class ConfirmComponent implements OnInit {
   @ViewChild('confirmModal') confirmModal: ModalDirective
 
   title = ''
   message = ''
+  expectedInputValue = ''
+  inputLabel = ''
+
+  inputValue = ''
 
   constructor (private confirmService: ConfirmService) {
     // Empty
@@ -31,10 +30,13 @@ export class ConfirmComponent implements OnInit {
     }
 
     this.confirmService.showConfirm.subscribe(
-      ({ title, message }) => {
+      ({ title, message, expectedInputValue, inputLabel }) => {
         this.title = title
         this.message = message
 
+        this.inputLabel = inputLabel
+        this.expectedInputValue = expectedInputValue
+
         this.showModal()
       }
     )
@@ -52,6 +54,13 @@ export class ConfirmComponent implements OnInit {
     this.hideModal()
   }
 
+  isConfirmationDisabled () {
+    // No input validation
+    if (!this.inputLabel || !this.expectedInputValue) return false
+
+    return this.expectedInputValue !== this.inputValue
+  }
+
   showModal () {
     this.confirmModal.show()
   }
index f12ff184800e7ca2b0eec46f8dc59bcdead51860..f30feb9d0b2e9c3dc991e226e1fd32c1acaa878b 100644 (file)
@@ -1,15 +1,22 @@
 import { Injectable } from '@angular/core'
 import { Subject } from 'rxjs/Subject'
 import 'rxjs/add/operator/first'
+import 'rxjs/add/operator/toPromise'
 
 @Injectable()
 export class ConfirmService {
-  showConfirm = new Subject<{ title, message }>()
+  showConfirm = new Subject<{ title: string, message: string, inputLabel?: string, expectedInputValue?: string }>()
   confirmResponse = new Subject<boolean>()
 
-  confirm (message = '', title = '') {
+  confirm (message: string, title = '') {
     this.showConfirm.next({ title, message })
 
-    return this.confirmResponse.asObservable().first()
+    return this.confirmResponse.asObservable().first().toPromise()
+  }
+
+  confirmWithInput (message: string, inputLabel: string, expectedInputValue: string, title = '') {
+    this.showConfirm.next({ title, message, inputLabel, expectedInputValue })
+
+    return this.confirmResponse.asObservable().first().toPromise()
   }
 }
index eea6f340b125a61fd815a1f97e04fae68b0c9ccd..36dbe8b5ccd516fd4b12be66a5313a4021b390c6 100644 (file)
@@ -1,5 +1,6 @@
 import { CommonModule } from '@angular/common'
 import { NgModule, Optional, SkipSelf } from '@angular/core'
+import { FormsModule } from '@angular/forms'
 import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
 import { RouterModule } from '@angular/router'
 import { LoadingBarModule } from '@ngx-loading-bar/core'
@@ -18,6 +19,7 @@ import { ServerService } from './server'
   imports: [
     CommonModule,
     RouterModule,
+    FormsModule,
     BrowserAnimationsModule,
 
     ModalModule,
index 15618f6990a41d1836cc28738476880d1f300516..c3b5f37f84057d32920afd5c807c8bb1f604353d 100644 (file)
@@ -15,7 +15,7 @@ export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate>
     currentRoute: ActivatedRouteSnapshot,
     currentState: RouterStateSnapshot,
     nextState: RouterStateSnapshot
-  ): Observable<boolean> | boolean {
+  ) {
     const result = component.canDeactivate()
     const text = result.text || 'All unsaved data will be lost, are you sure you want to leave this page?'
 
index 16f1a0643f0d5d12b2e0422ede7e96b24cfeb783..711a01ba0acfa70979cfedecca765bd86df98237 100644 (file)
@@ -109,38 +109,35 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
     this.viewReplies(commentTree.comment.id)
   }
 
-  onWantedToDelete (commentToDelete: VideoComment) {
+  async onWantedToDelete (commentToDelete: VideoComment) {
     let message = 'Do you really want to delete this comment?'
     if (commentToDelete.totalReplies !== 0) message += `${commentToDelete.totalReplies} would be deleted too.`
 
-    this.confirmService.confirm(message, 'Delete').subscribe(
-      res => {
-        if (res === false) return
-
-        this.videoCommentService.deleteVideoComment(commentToDelete.videoId, commentToDelete.id)
-          .subscribe(
-            () => {
-              // Delete the comment in the tree
-              if (commentToDelete.inReplyToCommentId) {
-                const thread = this.threadComments[commentToDelete.threadId]
-                if (!thread) {
-                  console.error(`Cannot find thread ${commentToDelete.threadId} of the comment to delete ${commentToDelete.id}`)
-                  return
-                }
-
-                this.deleteLocalCommentThread(thread, commentToDelete)
-                return
-              }
-
-              // Delete the thread
-              this.comments = this.comments.filter(c => c.id !== commentToDelete.id)
-              this.componentPagination.totalItems--
-            },
-
-            err => this.notificationsService.error('Error', err.message)
-          )
-      }
-    )
+    const res = await this.confirmService.confirm(message, 'Delete')
+    if (res === false) return
+
+    this.videoCommentService.deleteVideoComment(commentToDelete.videoId, commentToDelete.id)
+      .subscribe(
+        () => {
+          // Delete the comment in the tree
+          if (commentToDelete.inReplyToCommentId) {
+            const thread = this.threadComments[commentToDelete.threadId]
+            if (!thread) {
+              console.error(`Cannot find thread ${commentToDelete.threadId} of the comment to delete ${commentToDelete.id}`)
+              return
+            }
+
+            this.deleteLocalCommentThread(thread, commentToDelete)
+            return
+          }
+
+          // Delete the thread
+          this.comments = this.comments.filter(c => c.id !== commentToDelete.id)
+          this.componentPagination.totalItems--
+        },
+
+        err => this.notificationsService.error('Error', err.message)
+      )
   }
 
   isUserLoggedIn () {
index 6b118b1de2bfddc2c6ffa725447d14b71b109233..d04d503109a4cd83d7406d4d17e18e0b1369b48b 100644 (file)
@@ -130,24 +130,21 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
     }
   }
 
-  blacklistVideo (event: Event) {
+  async blacklistVideo (event: Event) {
     event.preventDefault()
 
-    this.confirmService.confirm('Do you really want to blacklist this video?', 'Blacklist').subscribe(
-      res => {
-        if (res === false) return
+    const res = await this.confirmService.confirm('Do you really want to blacklist this video?', 'Blacklist')
+    if (res === false) return
 
-        this.videoBlacklistService.blacklistVideo(this.video.id)
-                                  .subscribe(
-                                    status => {
-                                      this.notificationsService.success('Success', `Video ${this.video.name} had been blacklisted.`)
-                                      this.router.navigate(['/videos/list'])
-                                    },
+    this.videoBlacklistService.blacklistVideo(this.video.id)
+                              .subscribe(
+                                status => {
+                                  this.notificationsService.success('Success', `Video ${this.video.name} had been blacklisted.`)
+                                  this.router.navigate(['/videos/list'])
+                                },
 
-                                    error => this.notificationsService.error('Error', error.message)
-                                  )
-      }
-    )
+                                error => this.notificationsService.error('Error', error.message)
+                              )
   }
 
   showMoreDescription () {
@@ -236,26 +233,22 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
     return this.video.isRemovableBy(this.authService.getUser())
   }
 
-  removeVideo (event: Event) {
+  async removeVideo (event: Event) {
     event.preventDefault()
 
-    this.confirmService.confirm('Do you really want to delete this video?', 'Delete')
-      .subscribe(
-        res => {
-          if (res === false) return
+    const res = await this.confirmService.confirm('Do you really want to delete this video?', 'Delete')
+    if (res === false) return
 
-          this.videoService.removeVideo(this.video.id)
-            .subscribe(
-              status => {
-                this.notificationsService.success('Success', `Video ${this.video.name} deleted.`)
+    this.videoService.removeVideo(this.video.id)
+      .subscribe(
+        status => {
+          this.notificationsService.success('Success', `Video ${this.video.name} deleted.`)
 
-                // Go back to the video-list.
-                this.router.navigate([ '/videos/list' ])
-              },
+          // Go back to the video-list.
+          this.router.navigate([ '/videos/list' ])
+        },
 
-              error => this.notificationsService.error('Error', error.message)
-            )
-        }
+        error => this.notificationsService.error('Error', error.message)
       )
   }
 
index 2fcca6f761227ffd4d8774eb19ec88c928aa2c1b..df2eee9c987278711ffc3f1f1ce85668441a390b 100644 (file)
@@ -10,7 +10,7 @@ import { VideoModel } from '../models/video/video'
 const clientsRouter = express.Router()
 
 const distPath = join(root(), 'client', 'dist')
-const assetsImagesPath = join(root(), 'client', 'dist', 'client', 'assets', 'images')
+const assetsImagesPath = join(root(), 'client', 'dist', 'assets', 'images')
 const embedPath = join(distPath, 'standalone', 'videos', 'embed.html')
 const indexPath = join(distPath, 'index.html')