Merge branch 'release/2.2.0' into develop

pull/2780/head
Chocobozzz 2020-05-20 13:53:51 +02:00
commit 745437e3ab
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
24 changed files with 92 additions and 47 deletions

View 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="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: 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: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
<th style="width: 100px;"></th> <th style="width: 150px;"></th>
</tr> </tr>
</ng-template> </ng-template>

View File

@ -22,7 +22,7 @@
<th style="width: 160px;" i18n *ngIf="isDisplayingRemoteVideos()">Strategy</th> <th style="width: 160px;" i18n *ngIf="isDisplayingRemoteVideos()">Strategy</th>
<th i18n pSortableColumn="name">Video <p-sortIcon field="name"></p-sortIcon></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: 100px;" i18n *ngIf="isDisplayingRemoteVideos()">Total size</th>
<th style="width: 80px;"></th> <th style="width: 150px;"></th>
</tr> </tr>
</ng-template> </ng-template>

View File

@ -21,7 +21,7 @@
<tr> <tr>
<th style="width: 100%;" i18n>Account</th> <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: 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> </tr>
</ng-template> </ng-template>

View File

@ -25,7 +25,7 @@
<tr> <tr>
<th style="width: 100%;" i18n>Instance</th> <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: 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> </tr>
</ng-template> </ng-template>

View File

@ -41,7 +41,7 @@
<th i18n>Video</th> <th i18n>Video</th>
<th style="width: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></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 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> </tr>
</ng-template> </ng-template>

View File

@ -25,7 +25,7 @@
<th style="width: 100px;" i18n>Sensitive</th> <th style="width: 100px;" i18n>Sensitive</th>
<th style="width: 120px;" i18n>Unfederated</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: 150px;" i18n pSortableColumn="createdAt">Date <p-sortIcon field="createdAt"></p-sortIcon></th>
<th style="width: 120px;"></th> <th style="width: 150px;"></th>
</tr> </tr>
</ng-template> </ng-template>

View File

@ -9,7 +9,7 @@
<span class="email">{{ user.pendingEmail }}</span> is awaiting email verification <span class="email">{{ user.pendingEmail }}</span> is awaiting email verification
</div> </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"> <div class="form-group">
<label i18n for="new-email">New email</label> <label i18n for="new-email">New email</label>
@ -23,6 +23,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label i18n for="new-email">Your current password</label>
<input <input
type="password" id="password" i18n-placeholder placeholder="Your password" autocomplete="off" type="password" id="password" i18n-placeholder placeholder="Your password" autocomplete="off"
formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }" class="form-control" formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }" class="form-control"

View File

@ -58,7 +58,7 @@
</div> </div>
</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 class="form-group col-12 col-lg-4 col-xl-3">
<div i18n class="account-title">PASSWORD</div> <div i18n class="account-title">PASSWORD</div>
</div> </div>

View File

@ -10,7 +10,6 @@ my-search-typeahead {
@include orange-button; @include orange-button;
@include button-with-icon(22px, 3px, -1px); @include button-with-icon(22px, 3px, -1px);
color: var(--mainBackgroundColor) !important;
margin-right: 25px; margin-right: 25px;
@media screen and (max-width: 600px) { @media screen and (max-width: 600px) {

View File

@ -14,7 +14,8 @@ $input-border-radius: 3px;
textarea { textarea {
@include peertube-textarea(100%, 150px); @include peertube-textarea(100%, 150px);
background-color: var(--textareaBackgroundColor); background-color: var(--markdownTextareaBackgroundColor);
font-family: monospace; font-family: monospace;
font-size: 13px; font-size: 13px;
border-bottom: none; border-bottom: none;

View File

@ -47,26 +47,32 @@ try {
peertubeLocalStorage = localStorage peertubeLocalStorage = localStorage
peertubeSessionStorage = sessionStorage peertubeSessionStorage = sessionStorage
} catch (err) { } catch (err) {
const instance = new MemoryStorage() const instanceLocalStorage = new MemoryStorage()
const instanceSessionStorage = new MemoryStorage()
peertubeLocalStorage = sessionStorage = new Proxy(instance, { function proxify (instance: MemoryStorage) {
set: function (obj, prop: string | number, value) { return new Proxy(instance, {
if (MemoryStorage.prototype.hasOwnProperty(prop)) { set: function (obj, prop: string | number, value) {
instance[prop] = value if (MemoryStorage.prototype.hasOwnProperty(prop)) {
} else { instance[prop] = value
instance.setItem(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)) { peertubeLocalStorage = proxify(instanceLocalStorage)
return instance[name] peertubeSessionStorage = proxify(instanceSessionStorage)
}
if (valuesMap.has(name)) {
return instance.getItem(name)
}
}
})
} }
export { export {

View File

@ -12,10 +12,10 @@
<my-feed [syndicationItems]="syndicationItems"></my-feed> <my-feed [syndicationItems]="syndicationItems"></my-feed>
<div ngbDropdown class="d-inline-block ml-4"> <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 SORT BY
</button> </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('-createdAt')" ngbDropdownItem i18n>Most recent first (default)</button>
<button (click)="handleSortChange('-totalReplies')" ngbDropdownItem i18n>Most replies first</button> <button (click)="handleSortChange('-totalReplies')" ngbDropdownItem i18n>Most replies first</button>
</div> </div>
@ -72,7 +72,7 @@
> >
<div *ngIf="comment.totalReplies !== 0 && !threadComments[comment.id]" (click)="viewReplies(comment.id)" class="view-replies mb-2"> <div *ngIf="comment.totalReplies !== 0 && !threadComments[comment.id]" (click)="viewReplies(comment.id)" class="view-replies mb-2">
<span class="glyphicon glyphicon-menu-down"></span> <span class="glyphicon glyphicon-menu-down"></span>
<ng-container *ngIf="comment.totalRepliesFromVideoAuthor > 0; then hasAuthorComments; else noAuthorComments"></ng-container> <ng-container *ngIf="comment.totalRepliesFromVideoAuthor > 0; then hasAuthorComments; else noAuthorComments"></ng-container>
<ng-template #hasAuthorComments> <ng-template #hasAuthorComments>
<ng-container *ngIf="comment.totalReplies !== comment.totalRepliesFromVideoAuthor; else onlyAuthorComments" i18n> <ng-container *ngIf="comment.totalReplies !== comment.totalRepliesFromVideoAuthor; else onlyAuthorComments" i18n>
@ -83,7 +83,7 @@
</ng-template> </ng-template>
</ng-template> </ng-template>
<ng-template i18n #noAuthorComments>View {{ comment.totalReplies }} replies</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> <my-small-loader class="comment-thread-loading ml-1" [loading]="threadLoading[comment.id]"></my-small-loader>
</div> </div>
</my-video-comment> </my-video-comment>

View File

@ -21,7 +21,7 @@
.title-page { .title-page {
margin-right: 0; margin-right: 0;
} }
my-feed { my-feed {
display: inline-block; display: inline-block;
margin-left: 5px; margin-left: 5px;
@ -33,7 +33,7 @@
} }
} }
#dropdownSortComments { #dropdown-sort-comments {
font-weight: 600; font-weight: 600;
text-transform: uppercase; text-transform: uppercase;
border: none; border: none;

View File

@ -35,10 +35,13 @@ body {
--menuForegroundColor: #{$menu-color}; --menuForegroundColor: #{$menu-color};
--submenuColor: #{$sub-menu-color}; --submenuColor: #{$sub-menu-color};
--inputForegroundColor: #{$input-foreground-color};
--inputBackgroundColor: #{$input-background-color}; --inputBackgroundColor: #{$input-background-color};
--inputPlaceholderColor: #{$input-placeholder-color}; --inputPlaceholderColor: #{$input-placeholder-color};
--textareaForegroundColor: #{$textarea-foreground-color};
--textareaBackgroundColor: #{$textarea-background-color}; --textareaBackgroundColor: #{$textarea-background-color};
--markdownTextareaBackgroundColor: #{$markdown-textarea-background-color};
--actionButtonColor: #{$grey-foreground-color}; --actionButtonColor: #{$grey-foreground-color};
--supportButtonBackgroundColor: #{transparent}; --supportButtonBackgroundColor: #{transparent};

View File

@ -37,6 +37,8 @@ $icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/';
} }
.dropdown-menu { .dropdown-menu {
z-index: z(dropdown) + 1 !important;
border-radius: 3px; 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); 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; font-size: 15px;

View File

@ -90,7 +90,8 @@
display: inline-block; display: inline-block;
height: $button-height; height: $button-height;
width: $width; width: $width;
background: var(--inputBackgroundColor); color: var(--inputForegroundColor);
background-color: var(--inputBackgroundColor);
border: 1px solid #C6C6C6; border: 1px solid #C6C6C6;
border-radius: 3px; border-radius: 3px;
padding-left: 15px; padding-left: 15px;
@ -121,6 +122,8 @@
@mixin peertube-textarea ($width, $height) { @mixin peertube-textarea ($width, $height) {
@include peertube-input-text($width); @include peertube-input-text($width);
color: var(--textareaForegroundColor);
background-color: var(--textareaBackgroundColor);
height: $height; height: $height;
padding: 5px 15px; padding: 5px 15px;
font-size: 15px; font-size: 15px;
@ -280,6 +283,7 @@
margin: 0; margin: 0;
width: $width; width: $width;
border-radius: 3px; border-radius: 3px;
color: var(--inputForegroundColor);
background: var(--inputBackgroundColor); background: var(--inputBackgroundColor);
position: relative; position: relative;
font-size: 15px; font-size: 15px;

View File

@ -63,10 +63,13 @@ $video-thumbnail-ratio: $video-thumbnail-width / $video-thumbnail-height;
$theater-bottom-space: 115px; $theater-bottom-space: 115px;
$input-foreground-color: $fg-color;
$input-background-color: $bg-color; $input-background-color: $bg-color;
$input-placeholder-color: #898989; $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: 30px;
$sub-menu-margin-bottom-small-view: 10px; $sub-menu-margin-bottom-small-view: 10px;
@ -92,10 +95,13 @@ $variables: (
--menuForegroundColor: var(--menuForegroundColor), --menuForegroundColor: var(--menuForegroundColor),
--submenuColor: var(--submenuColor), --submenuColor: var(--submenuColor),
--inputForegroundColor: var(--inputForegroundColor),
--inputBackgroundColor: var(--inputBackgroundColor), --inputBackgroundColor: var(--inputBackgroundColor),
--inputPlaceholderColor: var(--inputPlaceholderColor), --inputPlaceholderColor: var(--inputPlaceholderColor),
--textareaForegroundColor: var(--textareaForegroundColor),
--textareaBackgroundColor: var(--textareaBackgroundColor), --textareaBackgroundColor: var(--textareaBackgroundColor),
--markdownTextareaBackgroundColor: var(--markdownTextareaBackgroundColor),
--actionButtonColor: var(--actionButtonColor), --actionButtonColor: var(--actionButtonColor),
--supportButtonColor: var(--supportButtonColor), --supportButtonColor: var(--supportButtonColor),

View File

@ -140,13 +140,13 @@ p-table {
font-size: 11px !important; font-size: 11px !important;
top: 0 !important; top: 0 !important;
&.pi-sort-up { &.pi-sort-amount-up-alt {
@extend .glyphicon-triangle-top; @extend .glyphicon-triangle-top;
color: var(--mainForegroundColor) !important; color: var(--mainForegroundColor) !important;
} }
&.pi-sort-down { &.pi-sort-amount-down {
@extend .glyphicon-triangle-bottom; @extend .glyphicon-triangle-bottom;
color: var(--mainForegroundColor) !important; color: var(--mainForegroundColor) !important;
@ -302,12 +302,12 @@ p-table {
@if $mobile-paginator { @if $mobile-paginator {
p-paginator .ui-paginator-bottom { p-paginator .ui-paginator-bottom {
display: block; display: block;
.ui-paginator-current { .ui-paginator-current {
position: relative; position: relative;
display: block; display: block;
} }
a, .ui-paginator-pages { a, .ui-paginator-pages {
vertical-align: middle; vertical-align: middle;
} }
@ -345,7 +345,7 @@ p-multiselect {
} }
} }
.pi.pi-chevron-down{ .pi.pi-chevron-down {
margin-left: 0 !important; margin-left: 0 !important;
&::after { &::after {

View File

@ -14,6 +14,7 @@ import { UserAdminFlag } from '@shared/models/users/user-flag.model'
import { createUserAccountAndChannelAndPlaylist } from './user' import { createUserAccountAndChannelAndPlaylist } from './user'
import { UserRole } from '@shared/models/users/user-role' import { UserRole } from '@shared/models/users/user-role'
import { PluginManager } from '@server/lib/plugins/plugin-manager' 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 } 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) let user = await UserModel.loadByEmail(obj.user.email)
if (!user) user = await createUserFromExternal(obj.pluginName, obj.user) 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 // If the user does not belongs to a plugin, it was created before its installation
// Then we just go through a regular login process // Then we just go through a regular login process
if (user.pluginAuth !== null) { if (user.pluginAuth !== null) {
@ -208,6 +212,10 @@ async function createUserFromExternal (pluginAuth: string, options: {
role: UserRole role: UserRole
displayName: string 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({ const userToCreate = new UserModel({
username: options.username, username: options.username,
password: null, password: null,

View File

@ -234,14 +234,19 @@ const usersUpdateMeValidator = [
async (req: express.Request, res: express.Response, next: express.NextFunction) => { async (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking usersUpdateMe parameters', { parameters: omit(req.body, 'password') }) 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 (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) { if (!req.body.currentPassword) {
return res.status(400) return res.status(400)
.json({ error: 'currentPassword parameter is missing.' }) .json({ error: 'currentPassword parameter is missing.' })
.end()
} }
const user = res.locals.oauth.token.User
if (await user.isPasswordMatch(req.body.currentPassword) !== true) { if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
return res.status(401) return res.status(401)
.json({ error: 'currentPassword is invalid.' }) .json({ error: 'currentPassword is invalid.' })

View 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, { targetUrl: getYoutubeVideoUrl() }))
await importVideo(server.url, server.accessToken, immutableAssign(baseAttributes, { magnetUri: getMagnetURI() })) 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 ]) await waitJobs([ server ])

View File

@ -175,7 +175,7 @@ Ajouter un sous-titre est vraiment facile`)
{ {
const attributes = immutableAssign(baseAttributes, { const attributes = immutableAssign(baseAttributes, {
torrentfile: 'video-720p.torrent', torrentfile: 'video-720p.torrent' as any,
description: 'this is a super torrent description', description: 'this is a super torrent description',
tags: [ 'tag_torrent1', 'tag_torrent2' ] tags: [ 'tag_torrent1', 'tag_torrent2' ]
}) })

View File

@ -255,6 +255,16 @@ describe('Test external auth plugins', function () {
expect(body.role).to.equal(UserRole.USER) 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 () { it('Should reject token of Kefka by the plugin hook', async function () {
this.timeout(10000) this.timeout(10000)

View File

@ -216,7 +216,7 @@ function unblockUser (url: string, userId: number | string, accessToken: string,
.expect(expectedStatus) .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 path = '/api/v1/users/me'
const toSend: UserUpdateMe = omit(options, 'url', 'accessToken') const toSend: UserUpdateMe = omit(options, 'url', 'accessToken')
@ -226,7 +226,7 @@ function updateMyUser (options: { url: string, accessToken: string } & UserUpdat
path, path,
token: options.accessToken, token: options.accessToken,
fields: toSend, fields: toSend,
statusCodeExpected: 204 statusCodeExpected: options.statusCodeExpected || 204
}) })
} }