Merge branch 'release/2.2.0' into develop
authorChocobozzz <me@florianbigard.com>
Wed, 20 May 2020 11:53:51 +0000 (13:53 +0200)
committerChocobozzz <me@florianbigard.com>
Wed, 20 May 2020 11:53:51 +0000 (13:53 +0200)
24 files changed:
client/src/app/+admin/follows/followers-list/followers-list.component.html
client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.html
client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.html
client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html
client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html
client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html
client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.html
client/src/app/+my-account/my-account-settings/my-account-settings.component.html
client/src/app/header/header.component.scss
client/src/app/shared/forms/markdown-textarea.component.scss
client/src/app/shared/misc/peertube-web-storage.ts
client/src/app/videos/+video-watch/comment/video-comments.component.html
client/src/app/videos/+video-watch/comment/video-comments.component.scss
client/src/sass/application.scss
client/src/sass/bootstrap.scss
client/src/sass/include/_mixins.scss
client/src/sass/include/_variables.scss
client/src/sass/primeng-custom.scss
server/lib/oauth-model.ts
server/middlewares/validators/users.ts
server/tests/api/check-params/users.ts
server/tests/api/videos/video-imports.ts
server/tests/plugins/external-auth.ts
shared/extra-utils/users/users.ts

index 93378a533abd367f54a57584917bc918adc00a2f..298871fce3f99ae22a4248399c8dc5a9d303361c 100644 (file)
@@ -23,7 +23,7 @@
       <th style="width: 100px;" i18n pSortableColumn="state">State <p-sortIcon field="state"></p-sortIcon></th>
       <th style="width: 100px;" i18n pSortableColumn="score">Score <p-sortIcon field="score"></p-sortIcon></th>
       <th style="width: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
-      <th style="width: 100px;"></th>
+      <th style="width: 150px;"></th>
     </tr>
   </ng-template>
 
index 28d57f83c224e707ef5df534f5deb30ac4f15428..c08154bcd1c86751fb64415b8971543532b1ce0b 100644 (file)
@@ -22,7 +22,7 @@
       <th style="width: 160px;" i18n *ngIf="isDisplayingRemoteVideos()">Strategy</th>
       <th i18n pSortableColumn="name">Video <p-sortIcon field="name"></p-sortIcon></th >
       <th style="width: 100px;" i18n *ngIf="isDisplayingRemoteVideos()">Total size</th>
-      <th style="width: 80px;"></th>
+      <th style="width: 150px;"></th>
     </tr>
   </ng-template>
 
index a4ab2a58c923b8ae6c9350a2b29901ad0abb2ee6..b7d40be6063ea7ead5b2c187ead1a09428eca94d 100644 (file)
@@ -21,7 +21,7 @@
     <tr>
       <th style="width: 100%;" i18n>Account</th>
       <th style="width: 150px;" i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th>
-      <th style="width: 100px;"></th> <!-- column for action buttons --> 
+      <th style="width: 150px;"></th> <!-- column for action buttons -->
     </tr>
   </ng-template>
 
index dab068dd64f599ba2451c900812b26d161b1a840..589a11b7b317226649cfdbc1efa70090d364f944 100644 (file)
@@ -25,7 +25,7 @@
     <tr>
       <th style="width: 100%;" i18n>Instance</th>
       <th style="width: 150px;" i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th>
-      <th style="width: 100px;"></th> <!-- column for action buttons -->
+      <th style="width: 150px;"></th> <!-- column for action buttons -->
     </tr>
   </ng-template>
 
index 1c9530152410c5afcbf920332212dd4f4eddd4c0..d30475794d2b583ff6e0e7f474c467e224663031 100644 (file)
@@ -41,7 +41,7 @@
       <th i18n>Video</th>
       <th style="width: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
       <th i18n pSortableColumn="state" style="width: 80px;">State <p-sortIcon field="state"></p-sortIcon></th>
-      <th style="width: 120px;"></th>
+      <th style="width: 150px;"></th>
     </tr>
   </ng-template>
 
index c4c4e765acb8ccdda5a33022b82da885740452eb..cfa04514fef156787959c047ab1895b66ba96816 100644 (file)
@@ -25,7 +25,7 @@
       <th style="width: 100px;" i18n>Sensitive</th>
       <th style="width: 120px;" i18n>Unfederated</th>
       <th style="width: 150px;" i18n pSortableColumn="createdAt">Date <p-sortIcon field="createdAt"></p-sortIcon></th>
-      <th style="width: 120px;"></th>
+      <th style="width: 150px;"></th>
     </tr>
   </ng-template>
 
index f39f66696c96e7caf274178554613c0dd42b34fa..ce176d68294376f3baca3acd742eeee7649f848e 100644 (file)
@@ -9,7 +9,7 @@
   <span class="email">{{ user.pendingEmail }}</span> is awaiting email verification
 </div>
 
-<form role="form" class="change-email" (ngSubmit)="changeEmail()" [formGroup]="form">
+<form role="form" class="change-email" (ngSubmit)="changeEmail()" [formGroup]="form" *ngIf="user.pluginAuth === null">
 
   <div class="form-group">
     <label i18n for="new-email">New email</label>
@@ -23,6 +23,7 @@
   </div>
 
   <div class="form-group">
+    <label i18n for="new-email">Your current password</label>
     <input
       type="password" id="password" i18n-placeholder placeholder="Your password" autocomplete="off"
       formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }" class="form-control"
index f1c4665456d82163876321fca5151871cc394700..b4e4d29f0698d709870c54e4e8f14d2ebf0c5568 100644 (file)
@@ -58,7 +58,7 @@
   </div>
 </div>
 
-<div class="form-row mt-5"> <!-- password grid -->
+<div class="form-row mt-5" *ngIf="user.pluginAuth === null"> <!-- password grid -->
   <div class="form-group col-12 col-lg-4 col-xl-3">
     <div i18n class="account-title">PASSWORD</div>
   </div>
index 91b39077367ba2b638ea0c2de687a21a824afdff..1e4ce2c56ebcb27efbe96258cff5f0d57ec11f01 100644 (file)
@@ -10,7 +10,6 @@ my-search-typeahead {
   @include orange-button;
   @include button-with-icon(22px, 3px, -1px);
 
-  color: var(--mainBackgroundColor) !important;
   margin-right: 25px;
 
   @media screen and (max-width: 600px) {
index 8e5739e452dcee16aaa24c9ffe5fa8f05008f999..16f319587805a384da2d0abfee038be419838837 100644 (file)
@@ -14,7 +14,8 @@ $input-border-radius: 3px;
     textarea {
       @include peertube-textarea(100%, 150px);
 
-      background-color: var(--textareaBackgroundColor);
+      background-color: var(--markdownTextareaBackgroundColor);
+
       font-family: monospace;
       font-size: 13px;
       border-bottom: none;
index fff2096784a8c75fe8974305b9ff072b97bf4274..6a152dd988bb3c77cf02c5b3eb70e82538775b5a 100644 (file)
@@ -47,26 +47,32 @@ try {
   peertubeLocalStorage = localStorage
   peertubeSessionStorage = sessionStorage
 } catch (err) {
-  const instance = new MemoryStorage()
+  const instanceLocalStorage = new MemoryStorage()
+  const instanceSessionStorage = new MemoryStorage()
 
-  peertubeLocalStorage = sessionStorage = new Proxy(instance, {
-    set: function (obj, prop: string | number, value) {
-      if (MemoryStorage.prototype.hasOwnProperty(prop)) {
-        instance[prop] = value
-      } else {
-        instance.setItem(prop, value)
+  function proxify (instance: MemoryStorage) {
+    return new Proxy(instance, {
+      set: function (obj, prop: string | number, value) {
+        if (MemoryStorage.prototype.hasOwnProperty(prop)) {
+          instance[prop] = value
+        } else {
+          instance.setItem(prop, value)
+        }
+        return true
+      },
+      get: function (target, name: string | number) {
+        if (MemoryStorage.prototype.hasOwnProperty(name)) {
+          return instance[name]
+        }
+        if (valuesMap.has(name)) {
+          return instance.getItem(name)
+        }
       }
-      return true
-    },
-    get: function (target, name: string | number) {
-      if (MemoryStorage.prototype.hasOwnProperty(name)) {
-        return instance[name]
-      }
-      if (valuesMap.has(name)) {
-        return instance.getItem(name)
-      }
-    }
-  })
+    })
+  }
+
+  peertubeLocalStorage = proxify(instanceLocalStorage)
+  peertubeSessionStorage = proxify(instanceSessionStorage)
 }
 
 export {
index a21042f094c45e4b1c8746be1da6c8bf410d7bbc..affbd4793fb40093cf001d2a8979c0273e8b03c8 100644 (file)
     <my-feed [syndicationItems]="syndicationItems"></my-feed>
 
     <div ngbDropdown class="d-inline-block ml-4">
-      <button class="btn btn-sm btn-outline-secondary" id="dropdownSortComments" ngbDropdownToggle i18n>
+      <button class="btn btn-sm btn-outline-secondary" id="dropdown-sort-comments" ngbDropdownToggle i18n>
         SORT BY
       </button>
-      <div ngbDropdownMenu aria-labelledby="dropdownSortComments">
+      <div ngbDropdownMenu aria-labelledby="dropdown-sort-comments">
         <button (click)="handleSortChange('-createdAt')" ngbDropdownItem i18n>Most recent first (default)</button>
         <button (click)="handleSortChange('-totalReplies')" ngbDropdownItem i18n>Most replies first</button>
       </div>
@@ -72,7 +72,7 @@
         >
           <div *ngIf="comment.totalReplies !== 0 && !threadComments[comment.id]" (click)="viewReplies(comment.id)" class="view-replies mb-2">
             <span class="glyphicon glyphicon-menu-down"></span>
-  
+
             <ng-container *ngIf="comment.totalRepliesFromVideoAuthor > 0; then hasAuthorComments; else noAuthorComments"></ng-container>
             <ng-template #hasAuthorComments>
               <ng-container *ngIf="comment.totalReplies !== comment.totalRepliesFromVideoAuthor; else onlyAuthorComments" i18n>
@@ -83,7 +83,7 @@
               </ng-template>
             </ng-template>
             <ng-template i18n #noAuthorComments>View {{ comment.totalReplies }} replies</ng-template>
-  
+
             <my-small-loader class="comment-thread-loading ml-1" [loading]="threadLoading[comment.id]"></my-small-loader>
           </div>
         </my-video-comment>
index 5ed1ac6295a9ca051267e659d1704945ea1fd1b9..df42fae7310b284274b98b9e0a53fdfbd0ef439a 100644 (file)
@@ -21,7 +21,7 @@
   .title-page {
     margin-right: 0;
   }
-  
+
   my-feed {
     display: inline-block;
     margin-left: 5px;
@@ -33,7 +33,7 @@
   }
 }
 
-#dropdownSortComments {
+#dropdown-sort-comments {
   font-weight: 600;
   text-transform: uppercase;
   border: none;
index d637c94d98274d988527ab17de7e41237a6f29c8..17ed5c8f82a3d3ce3b02896bcb264f81ef28a0c5 100644 (file)
@@ -35,10 +35,13 @@ body {
   --menuForegroundColor: #{$menu-color};
   --submenuColor: #{$sub-menu-color};
 
+  --inputForegroundColor: #{$input-foreground-color};
   --inputBackgroundColor: #{$input-background-color};
   --inputPlaceholderColor: #{$input-placeholder-color};
 
+  --textareaForegroundColor: #{$textarea-foreground-color};
   --textareaBackgroundColor: #{$textarea-background-color};
+  --markdownTextareaBackgroundColor: #{$markdown-textarea-background-color};
 
   --actionButtonColor: #{$grey-foreground-color};
   --supportButtonBackgroundColor: #{transparent};
index cb266cc68a3f80ad00ffd3a70967f12863b43c5c..7985472ed0cef7e20eb1305605d4e23b2daf77a5 100644 (file)
@@ -37,6 +37,8 @@ $icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/';
 }
 
 .dropdown-menu {
+  z-index: z(dropdown) + 1 !important;
+
   border-radius: 3px;
   box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2);
   font-size: 15px;
index f157ded5ec70deef7ff0f35997277127babd60f0..99ca25f9cfdf89ce6418a40a6fe894c19fa10bab 100644 (file)
@@ -90,7 +90,8 @@
   display: inline-block;
   height: $button-height;
   width: $width;
-  background: var(--inputBackgroundColor);
+  color: var(--inputForegroundColor);
+  background-color: var(--inputBackgroundColor);
   border: 1px solid #C6C6C6;
   border-radius: 3px;
   padding-left: 15px;
 @mixin peertube-textarea ($width, $height) {
   @include peertube-input-text($width);
 
+  color: var(--textareaForegroundColor);
+  background-color: var(--textareaBackgroundColor);
   height: $height;
   padding: 5px 15px;
   font-size: 15px;
   margin: 0;
   width: $width;
   border-radius: 3px;
+  color: var(--inputForegroundColor);
   background: var(--inputBackgroundColor);
   position: relative;
   font-size: 15px;
index 46f1e99f7f8bc46ecc03ac409e7fa1baecf1740a..cdac8ae6f76bd41fdd3e898aac5e0bb2fd0ea9b9 100644 (file)
@@ -63,10 +63,13 @@ $video-thumbnail-ratio: $video-thumbnail-width / $video-thumbnail-height;
 
 $theater-bottom-space: 115px;
 
+$input-foreground-color: $fg-color;
 $input-background-color: $bg-color;
 $input-placeholder-color: #898989;
 
-$textarea-background-color: $grey-background-hover-color;
+$textarea-foreground-color: $fg-color;
+$textarea-background-color: $bg-color;
+$markdown-textarea-background-color: $grey-background-hover-color;
 
 $sub-menu-margin-bottom: 30px;
 $sub-menu-margin-bottom-small-view: 10px;
@@ -92,10 +95,13 @@ $variables: (
   --menuForegroundColor: var(--menuForegroundColor),
   --submenuColor: var(--submenuColor),
 
+  --inputForegroundColor: var(--inputForegroundColor),
   --inputBackgroundColor: var(--inputBackgroundColor),
   --inputPlaceholderColor: var(--inputPlaceholderColor),
 
+  --textareaForegroundColor: var(--textareaForegroundColor),
   --textareaBackgroundColor: var(--textareaBackgroundColor),
+  --markdownTextareaBackgroundColor: var(--markdownTextareaBackgroundColor),
 
   --actionButtonColor: var(--actionButtonColor),
   --supportButtonColor: var(--supportButtonColor),
index d48f2dfc45d5db64b20617f1559bd15f911f7845..33483533e3c66a67a3eecfb39c0ea65206c6d14b 100644 (file)
@@ -140,13 +140,13 @@ p-table {
         font-size: 11px !important;
         top: 0 !important;
 
-        &.pi-sort-up {
+        &.pi-sort-amount-up-alt {
           @extend .glyphicon-triangle-top;
 
           color: var(--mainForegroundColor) !important;
         }
 
-        &.pi-sort-down {
+        &.pi-sort-amount-down {
           @extend .glyphicon-triangle-bottom;
 
           color: var(--mainForegroundColor) !important;
@@ -302,12 +302,12 @@ p-table {
     @if $mobile-paginator {
       p-paginator .ui-paginator-bottom {
         display: block;
-  
+
         .ui-paginator-current {
           position: relative;
           display: block;
         }
-  
+
         a, .ui-paginator-pages {
           vertical-align: middle;
         }
@@ -345,7 +345,7 @@ p-multiselect {
     }
   }
 
-  .pi.pi-chevron-down{
+  .pi.pi-chevron-down {
     margin-left: 0 !important;
 
     &::after {
index e5ea4636ee8b2e83dfc3c18afc74a29da4e9a3c8..db546efb16c10deaa043ab8de977e2a66d861c59 100644 (file)
@@ -14,6 +14,7 @@ import { UserAdminFlag } from '@shared/models/users/user-flag.model'
 import { createUserAccountAndChannelAndPlaylist } from './user'
 import { UserRole } from '@shared/models/users/user-role'
 import { PluginManager } from '@server/lib/plugins/plugin-manager'
+import { ActorModel } from '@server/models/activitypub/actor'
 
 type TokenInfo = { accessToken: string, refreshToken: string, accessTokenExpiresAt: Date, refreshTokenExpiresAt: Date }
 
@@ -109,6 +110,9 @@ async function getUser (usernameOrEmail?: string, password?: string) {
     let user = await UserModel.loadByEmail(obj.user.email)
     if (!user) user = await createUserFromExternal(obj.pluginName, obj.user)
 
+    // Cannot create a user
+    if (!user) throw new AccessDeniedError('Cannot create such user: an actor with that name already exists.')
+
     // If the user does not belongs to a plugin, it was created before its installation
     // Then we just go through a regular login process
     if (user.pluginAuth !== null) {
@@ -208,6 +212,10 @@ async function createUserFromExternal (pluginAuth: string, options: {
   role: UserRole
   displayName: string
 }) {
+  // Check an actor does not already exists with that name (removed user)
+  const actor = await ActorModel.loadLocalByName(options.username)
+  if (actor) return null
+
   const userToCreate = new UserModel({
     username: options.username,
     password: null,
index 840b9fc744379e2a13039efafeb5d31b98824e30..3bdbcdf6a349cdcad74ced6392be1aebf6b2ddf5 100644 (file)
@@ -234,14 +234,19 @@ const usersUpdateMeValidator = [
   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
     logger.debug('Checking usersUpdateMe parameters', { parameters: omit(req.body, 'password') })
 
+    const user = res.locals.oauth.token.User
+
     if (req.body.password || req.body.email) {
+      if (user.pluginAuth !== null) {
+        return res.status(400)
+                  .json({ error: 'You cannot update your email or password that is associated with an external auth system.' })
+      }
+
       if (!req.body.currentPassword) {
         return res.status(400)
                   .json({ error: 'currentPassword parameter is missing.' })
-                  .end()
       }
 
-      const user = res.locals.oauth.token.User
       if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
         return res.status(401)
                   .json({ error: 'currentPassword is invalid.' })
index 4d597f0a3d53a2502af95c98a96256fc427309cc..6e737af15dcb583137b779163c3f9afe72547ed6 100644 (file)
@@ -1044,7 +1044,7 @@ describe('Test users API validators', function () {
       }
       await importVideo(server.url, server.accessToken, immutableAssign(baseAttributes, { targetUrl: getYoutubeVideoUrl() }))
       await importVideo(server.url, server.accessToken, immutableAssign(baseAttributes, { magnetUri: getMagnetURI() }))
-      await importVideo(server.url, server.accessToken, immutableAssign(baseAttributes, { torrentfile: 'video-720p.torrent' }))
+      await importVideo(server.url, server.accessToken, immutableAssign(baseAttributes, { torrentfile: 'video-720p.torrent' as any }))
 
       await waitJobs([ server ])
 
index 4d5989f43995e25a377e877f56e2528f4ae20510..d211859e401ca191aee42f8d1c9d7078182c22bd 100644 (file)
@@ -175,7 +175,7 @@ Ajouter un sous-titre est vraiment facile`)
 
     {
       const attributes = immutableAssign(baseAttributes, {
-        torrentfile: 'video-720p.torrent',
+        torrentfile: 'video-720p.torrent' as any,
         description: 'this is a super torrent description',
         tags: [ 'tag_torrent1', 'tag_torrent2' ]
       })
index a85672782892a0936460436ac6ef3f41d464136a..57361be05a84763b70c71a84b4b03b05b4129fe5 100644 (file)
@@ -255,6 +255,16 @@ describe('Test external auth plugins', function () {
     expect(body.role).to.equal(UserRole.USER)
   })
 
+  it('Should not update an external auth email', async function () {
+    await updateMyUser({
+      url: server.url,
+      accessToken: cyanAccessToken,
+      email: 'toto@example.com',
+      currentPassword: 'toto',
+      statusCodeExpected: 400
+    })
+  })
+
   it('Should reject token of Kefka by the plugin hook', async function () {
     this.timeout(10000)
 
index 54b506bce04810bcc0fd55e52605fe1ce42c25dd..08b7743a6b48f0fd0cbe518b49563a17f7e49ba8 100644 (file)
@@ -216,7 +216,7 @@ function unblockUser (url: string, userId: number | string, accessToken: string,
     .expect(expectedStatus)
 }
 
-function updateMyUser (options: { url: string, accessToken: string } & UserUpdateMe) {
+function updateMyUser (options: { url: string, accessToken: string, statusCodeExpected?: number } & UserUpdateMe) {
   const path = '/api/v1/users/me'
 
   const toSend: UserUpdateMe = omit(options, 'url', 'accessToken')
@@ -226,7 +226,7 @@ function updateMyUser (options: { url: string, accessToken: string } & UserUpdat
     path,
     token: options.accessToken,
     fields: toSend,
-    statusCodeExpected: 204
+    statusCodeExpected: options.statusCodeExpected || 204
   })
 }