Merge branch 'feature/SO035' into develop
This commit is contained in:
commit
0a8a79552c
|
@ -1,3 +1,4 @@
|
||||||
|
import { environment } from 'src/environments/environment'
|
||||||
import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core'
|
import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core'
|
||||||
import { ActivatedRoute, Router } from '@angular/router'
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
import { AuthService, Notifier, RedirectService, SessionStorageService, UserService } from '@app/core'
|
import { AuthService, Notifier, RedirectService, SessionStorageService, UserService } from '@app/core'
|
||||||
|
@ -7,7 +8,7 @@ import { USER_OTP_TOKEN_VALIDATOR } from '@app/shared/form-validators/user-valid
|
||||||
import { FormReactive, FormReactiveService, InputTextComponent } from '@app/shared/shared-forms'
|
import { FormReactive, FormReactiveService, InputTextComponent } from '@app/shared/shared-forms'
|
||||||
import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance'
|
import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance'
|
||||||
import { NgbAccordion, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbAccordion, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { PluginsManager } from '@root-helpers/plugins-manager'
|
import { getExternalAuthHref } from '@shared/core-utils'
|
||||||
import { RegisteredExternalAuthConfig, ServerConfig } from '@shared/models'
|
import { RegisteredExternalAuthConfig, ServerConfig } from '@shared/models'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -119,7 +120,7 @@ export class LoginComponent extends FormReactive implements OnInit, AfterViewIni
|
||||||
}
|
}
|
||||||
|
|
||||||
getAuthHref (auth: RegisteredExternalAuthConfig) {
|
getAuthHref (auth: RegisteredExternalAuthConfig) {
|
||||||
return PluginsManager.getExternalAuthHref(auth)
|
return getExternalAuthHref(environment.apiUrl, auth)
|
||||||
}
|
}
|
||||||
|
|
||||||
login () {
|
login () {
|
||||||
|
|
|
@ -5,10 +5,11 @@ import { HttpClient, HttpErrorResponse, HttpHeaders, HttpParams } from '@angular
|
||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { Router } from '@angular/router'
|
import { Router } from '@angular/router'
|
||||||
import { Notifier } from '@app/core/notification/notifier.service'
|
import { Notifier } from '@app/core/notification/notifier.service'
|
||||||
import { logger, OAuthUserTokens, objectToUrlEncoded, peertubeLocalStorage } from '@root-helpers/index'
|
import { logger, OAuthUserTokens, objectToUrlEncoded, peertubeLocalStorage, PluginsManager } from '@root-helpers/index'
|
||||||
import { HttpStatusCode, MyUser as UserServerModel, OAuthClientLocal, User, UserLogin, UserRefreshToken } from '@shared/models'
|
import { HttpStatusCode, MyUser as UserServerModel, OAuthClientLocal, User, UserLogin, UserRefreshToken } from '@shared/models'
|
||||||
import { environment } from '../../../environments/environment'
|
import { environment } from '../../../environments/environment'
|
||||||
import { RestExtractor } from '../rest/rest-extractor.service'
|
import { RestExtractor } from '../rest/rest-extractor.service'
|
||||||
|
import { ServerService } from '../server'
|
||||||
import { AuthStatus } from './auth-status.model'
|
import { AuthStatus } from './auth-status.model'
|
||||||
import { AuthUser } from './auth-user.model'
|
import { AuthUser } from './auth-user.model'
|
||||||
|
|
||||||
|
@ -44,6 +45,7 @@ export class AuthService {
|
||||||
private refreshingTokenObservable: Observable<any>
|
private refreshingTokenObservable: Observable<any>
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
|
private serverService: ServerService,
|
||||||
private http: HttpClient,
|
private http: HttpClient,
|
||||||
private notifier: Notifier,
|
private notifier: Notifier,
|
||||||
private hotkeysService: HotkeysService,
|
private hotkeysService: HotkeysService,
|
||||||
|
@ -213,25 +215,28 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
|
||||||
const headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded')
|
const headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded')
|
||||||
|
|
||||||
this.refreshingTokenObservable = this.http.post<UserRefreshToken>(AuthService.BASE_TOKEN_URL, body, { headers })
|
this.refreshingTokenObservable = this.http.post<UserRefreshToken>(AuthService.BASE_TOKEN_URL, body, { headers })
|
||||||
.pipe(
|
.pipe(
|
||||||
map(res => this.handleRefreshToken(res)),
|
map(res => this.handleRefreshToken(res)),
|
||||||
tap(() => {
|
tap(() => {
|
||||||
this.refreshingTokenObservable = null
|
this.refreshingTokenObservable = null
|
||||||
}),
|
}),
|
||||||
catchError(err => {
|
catchError(err => {
|
||||||
this.refreshingTokenObservable = null
|
this.refreshingTokenObservable = null
|
||||||
|
|
||||||
logger.error(err)
|
logger.error(err)
|
||||||
logger.info('Cannot refresh token -> logout...')
|
logger.info('Cannot refresh token -> logout...')
|
||||||
this.logout()
|
this.logout()
|
||||||
this.router.navigate([ '/login' ])
|
|
||||||
|
|
||||||
return observableThrowError(() => ({
|
const externalLoginUrl = PluginsManager.getDefaultLoginHref(environment.apiUrl, this.serverService.getHTMLConfig())
|
||||||
error: $localize`You need to reconnect.`
|
if (externalLoginUrl) window.location.href = externalLoginUrl
|
||||||
}))
|
else this.router.navigate([ '/login' ])
|
||||||
}),
|
|
||||||
share()
|
return observableThrowError(() => ({
|
||||||
)
|
error: $localize`You need to reconnect.`
|
||||||
|
}))
|
||||||
|
}),
|
||||||
|
share()
|
||||||
|
)
|
||||||
|
|
||||||
return this.refreshingTokenObservable
|
return this.refreshingTokenObservable
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { HotkeysService } from 'angular2-hotkeys'
|
import { HotkeysService } from 'angular2-hotkeys'
|
||||||
import * as debug from 'debug'
|
import * as debug from 'debug'
|
||||||
import { switchMap } from 'rxjs/operators'
|
import { switchMap } from 'rxjs/operators'
|
||||||
|
import { environment } from 'src/environments/environment'
|
||||||
import { ViewportScroller } from '@angular/common'
|
import { ViewportScroller } from '@angular/common'
|
||||||
import { Component, OnInit, ViewChild } from '@angular/core'
|
import { Component, OnInit, ViewChild } from '@angular/core'
|
||||||
import { Router } from '@angular/router'
|
import { Router } from '@angular/router'
|
||||||
|
@ -131,12 +132,7 @@ export class MenuComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
getExternalLoginHref () {
|
getExternalLoginHref () {
|
||||||
if (!this.serverConfig || this.serverConfig.client.menu.login.redirectOnSingleExternalAuth !== true) return undefined
|
return PluginsManager.getDefaultLoginHref(environment.apiUrl, this.serverConfig)
|
||||||
|
|
||||||
const externalAuths = this.serverConfig.plugin.registeredExternalAuths
|
|
||||||
if (externalAuths.length !== 1) return undefined
|
|
||||||
|
|
||||||
return PluginsManager.getExternalAuthHref(externalAuths[0])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isRegistrationAllowed () {
|
isRegistrationAllowed () {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import * as debug from 'debug'
|
||||||
import { firstValueFrom, ReplaySubject } from 'rxjs'
|
import { firstValueFrom, ReplaySubject } from 'rxjs'
|
||||||
import { first, shareReplay } from 'rxjs/operators'
|
import { first, shareReplay } from 'rxjs/operators'
|
||||||
import { RegisterClientHelpers } from 'src/types/register-client-option.model'
|
import { RegisterClientHelpers } from 'src/types/register-client-option.model'
|
||||||
import { getHookType, internalRunHook } from '@shared/core-utils/plugins/hooks'
|
import { getExternalAuthHref, getHookType, internalRunHook } from '@shared/core-utils/plugins/hooks'
|
||||||
import {
|
import {
|
||||||
ClientHookName,
|
ClientHookName,
|
||||||
clientHookObject,
|
clientHookObject,
|
||||||
|
@ -16,7 +16,6 @@ import {
|
||||||
RegisterClientRouteOptions,
|
RegisterClientRouteOptions,
|
||||||
RegisterClientSettingsScriptOptions,
|
RegisterClientSettingsScriptOptions,
|
||||||
RegisterClientVideoFieldOptions,
|
RegisterClientVideoFieldOptions,
|
||||||
RegisteredExternalAuthConfig,
|
|
||||||
ServerConfigPlugin
|
ServerConfigPlugin
|
||||||
} from '@shared/models'
|
} from '@shared/models'
|
||||||
import { environment } from '../environments/environment'
|
import { environment } from '../environments/environment'
|
||||||
|
@ -94,9 +93,13 @@ class PluginsManager {
|
||||||
return isTheme ? '/themes' : '/plugins'
|
return isTheme ? '/themes' : '/plugins'
|
||||||
}
|
}
|
||||||
|
|
||||||
static getExternalAuthHref (auth: RegisteredExternalAuthConfig) {
|
static getDefaultLoginHref (apiUrl: string, serverConfig: HTMLServerConfig) {
|
||||||
return environment.apiUrl + `/plugins/${auth.name}/${auth.version}/auth/${auth.authName}`
|
if (!serverConfig || serverConfig.client.menu.login.redirectOnSingleExternalAuth !== true) return undefined
|
||||||
|
|
||||||
|
const externalAuths = serverConfig.plugin.registeredExternalAuths
|
||||||
|
if (externalAuths.length !== 1) return undefined
|
||||||
|
|
||||||
|
return getExternalAuthHref(apiUrl, externalAuths[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
loadPluginsList (config: HTMLServerConfig) {
|
loadPluginsList (config: HTMLServerConfig) {
|
||||||
|
|
|
@ -37,6 +37,11 @@ rates_limit:
|
||||||
window: 10 minutes
|
window: 10 minutes
|
||||||
max: 10
|
max: 10
|
||||||
|
|
||||||
|
oauth2:
|
||||||
|
token_lifetime:
|
||||||
|
access_token: '1 day'
|
||||||
|
refresh_token: '2 weeks'
|
||||||
|
|
||||||
# Proxies to trust to get real client IP
|
# Proxies to trust to get real client IP
|
||||||
# If you run PeerTube just behind a local proxy (nginx), keep 'loopback'
|
# If you run PeerTube just behind a local proxy (nginx), keep 'loopback'
|
||||||
# If you run PeerTube behind a remote proxy, add the proxy IP address (or subnet)
|
# If you run PeerTube behind a remote proxy, add the proxy IP address (or subnet)
|
||||||
|
|
|
@ -35,6 +35,11 @@ rates_limit:
|
||||||
window: 10 minutes
|
window: 10 minutes
|
||||||
max: 10
|
max: 10
|
||||||
|
|
||||||
|
oauth2:
|
||||||
|
token_lifetime:
|
||||||
|
access_token: '1 day'
|
||||||
|
refresh_token: '2 weeks'
|
||||||
|
|
||||||
# Proxies to trust to get real client IP
|
# Proxies to trust to get real client IP
|
||||||
# If you run PeerTube just behind a local proxy (nginx), keep 'loopback'
|
# If you run PeerTube just behind a local proxy (nginx), keep 'loopback'
|
||||||
# If you run PeerTube behind a remote proxy, add the proxy IP address (or subnet)
|
# If you run PeerTube behind a remote proxy, add the proxy IP address (or subnet)
|
||||||
|
|
|
@ -8,10 +8,11 @@ function isVideoCaptionLanguageValid (value: any) {
|
||||||
return exists(value) && VIDEO_LANGUAGES[value] !== undefined
|
return exists(value) && VIDEO_LANGUAGES[value] !== undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const videoCaptionTypesRegex = Object.keys(MIMETYPES.VIDEO_CAPTIONS.MIMETYPE_EXT)
|
// MacOS sends application/octet-stream
|
||||||
.concat([ 'application/octet-stream' ]) // MacOS sends application/octet-stream
|
const videoCaptionTypesRegex = [ ...Object.keys(MIMETYPES.VIDEO_CAPTIONS.MIMETYPE_EXT), 'application/octet-stream' ]
|
||||||
.map(m => `(${m})`)
|
.map(m => `(${m})`)
|
||||||
.join('|')
|
.join('|')
|
||||||
|
|
||||||
function isVideoCaptionFile (files: UploadFilesForCheck, field: string) {
|
function isVideoCaptionFile (files: UploadFilesForCheck, field: string) {
|
||||||
return isFileValid({
|
return isFileValid({
|
||||||
files,
|
files,
|
||||||
|
|
|
@ -22,10 +22,11 @@ function isVideoImportStateValid (value: any) {
|
||||||
return exists(value) && VIDEO_IMPORT_STATES[value] !== undefined
|
return exists(value) && VIDEO_IMPORT_STATES[value] !== undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const videoTorrentImportRegex = Object.keys(MIMETYPES.TORRENT.MIMETYPE_EXT)
|
// MacOS sends application/octet-stream
|
||||||
.concat([ 'application/octet-stream' ]) // MacOS sends application/octet-stream
|
const videoTorrentImportRegex = [ ...Object.keys(MIMETYPES.TORRENT.MIMETYPE_EXT), 'application/octet-stream' ]
|
||||||
.map(m => `(${m})`)
|
.map(m => `(${m})`)
|
||||||
.join('|')
|
.join('|')
|
||||||
|
|
||||||
function isVideoImportTorrentFile (files: UploadFilesForCheck) {
|
function isVideoImportTorrentFile (files: UploadFilesForCheck) {
|
||||||
return isFileValid({
|
return isFileValid({
|
||||||
files,
|
files,
|
||||||
|
|
|
@ -174,7 +174,8 @@ function checkRemoteRedundancyConfig () {
|
||||||
function checkStorageConfig () {
|
function checkStorageConfig () {
|
||||||
// Check storage directory locations
|
// Check storage directory locations
|
||||||
if (isProdInstance()) {
|
if (isProdInstance()) {
|
||||||
const configStorage = config.get('storage')
|
const configStorage = config.get<{ [ name: string ]: string }>('storage')
|
||||||
|
|
||||||
for (const key of Object.keys(configStorage)) {
|
for (const key of Object.keys(configStorage)) {
|
||||||
if (configStorage[key].startsWith('storage/')) {
|
if (configStorage[key].startsWith('storage/')) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
|
|
|
@ -13,6 +13,7 @@ function checkMissedConfig () {
|
||||||
'webserver.https', 'webserver.hostname', 'webserver.port',
|
'webserver.https', 'webserver.hostname', 'webserver.port',
|
||||||
'secrets.peertube',
|
'secrets.peertube',
|
||||||
'trust_proxy',
|
'trust_proxy',
|
||||||
|
'oauth2.token_lifetime.access_token', 'oauth2.token_lifetime.refresh_token',
|
||||||
'database.hostname', 'database.port', 'database.username', 'database.password', 'database.pool.max',
|
'database.hostname', 'database.port', 'database.username', 'database.password', 'database.pool.max',
|
||||||
'smtp.hostname', 'smtp.port', 'smtp.username', 'smtp.password', 'smtp.tls', 'smtp.from_address',
|
'smtp.hostname', 'smtp.port', 'smtp.username', 'smtp.password', 'smtp.tls', 'smtp.from_address',
|
||||||
'email.body.signature', 'email.subject.prefix',
|
'email.body.signature', 'email.subject.prefix',
|
||||||
|
|
|
@ -149,6 +149,12 @@ const CONFIG = {
|
||||||
HOSTNAME: config.get<string>('webserver.hostname'),
|
HOSTNAME: config.get<string>('webserver.hostname'),
|
||||||
PORT: config.get<number>('webserver.port')
|
PORT: config.get<number>('webserver.port')
|
||||||
},
|
},
|
||||||
|
OAUTH2: {
|
||||||
|
TOKEN_LIFETIME: {
|
||||||
|
ACCESS_TOKEN: parseDurationToMs(config.get<string>('oauth2.token_lifetime.access_token')),
|
||||||
|
REFRESH_TOKEN: parseDurationToMs(config.get<string>('oauth2.token_lifetime.refresh_token'))
|
||||||
|
}
|
||||||
|
},
|
||||||
RATES_LIMIT: {
|
RATES_LIMIT: {
|
||||||
API: {
|
API: {
|
||||||
WINDOW_MS: parseDurationToMs(config.get<string>('rates_limit.api.window')),
|
WINDOW_MS: parseDurationToMs(config.get<string>('rates_limit.api.window')),
|
||||||
|
|
|
@ -101,11 +101,6 @@ const SORTABLE_COLUMNS = {
|
||||||
VIDEO_REDUNDANCIES: [ 'name' ]
|
VIDEO_REDUNDANCIES: [ 'name' ]
|
||||||
}
|
}
|
||||||
|
|
||||||
const OAUTH_LIFETIME = {
|
|
||||||
ACCESS_TOKEN: 3600 * 24, // 1 day, for upload
|
|
||||||
REFRESH_TOKEN: 1209600 // 2 weeks
|
|
||||||
}
|
|
||||||
|
|
||||||
const ROUTE_CACHE_LIFETIME = {
|
const ROUTE_CACHE_LIFETIME = {
|
||||||
FEEDS: '15 minutes',
|
FEEDS: '15 minutes',
|
||||||
ROBOTS: '2 hours',
|
ROBOTS: '2 hours',
|
||||||
|
@ -1033,7 +1028,6 @@ export {
|
||||||
JOB_ATTEMPTS,
|
JOB_ATTEMPTS,
|
||||||
AP_CLEANER,
|
AP_CLEANER,
|
||||||
LAST_MIGRATION_VERSION,
|
LAST_MIGRATION_VERSION,
|
||||||
OAUTH_LIFETIME,
|
|
||||||
CUSTOM_HTML_TAG_COMMENTS,
|
CUSTOM_HTML_TAG_COMMENTS,
|
||||||
STATS_TIMESERIE,
|
STATS_TIMESERIE,
|
||||||
BROADCAST_CONCURRENCY,
|
BROADCAST_CONCURRENCY,
|
||||||
|
|
|
@ -51,8 +51,7 @@ function removeCacheAndTmpDirectories () {
|
||||||
const tasks: Promise<any>[] = []
|
const tasks: Promise<any>[] = []
|
||||||
|
|
||||||
// Cache directories
|
// Cache directories
|
||||||
for (const key of Object.keys(cacheDirectories)) {
|
for (const dir of cacheDirectories) {
|
||||||
const dir = cacheDirectories[key]
|
|
||||||
tasks.push(removeDirectoryOrContent(dir))
|
tasks.push(removeDirectoryOrContent(dir))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,8 +86,7 @@ function createDirectoriesIfNotExist () {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache directories
|
// Cache directories
|
||||||
for (const key of Object.keys(cacheDirectories)) {
|
for (const dir of cacheDirectories) {
|
||||||
const dir = cacheDirectories[key]
|
|
||||||
tasks.push(ensureDir(dir))
|
tasks.push(ensureDir(dir))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,26 +1,35 @@
|
||||||
|
|
||||||
import { isUserDisplayNameValid, isUserRoleValid, isUserUsernameValid } from '@server/helpers/custom-validators/users'
|
import {
|
||||||
|
isUserAdminFlagsValid,
|
||||||
|
isUserDisplayNameValid,
|
||||||
|
isUserRoleValid,
|
||||||
|
isUserUsernameValid,
|
||||||
|
isUserVideoQuotaDailyValid,
|
||||||
|
isUserVideoQuotaValid
|
||||||
|
} from '@server/helpers/custom-validators/users'
|
||||||
import { logger } from '@server/helpers/logger'
|
import { logger } from '@server/helpers/logger'
|
||||||
import { generateRandomString } from '@server/helpers/utils'
|
import { generateRandomString } from '@server/helpers/utils'
|
||||||
import { PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME } from '@server/initializers/constants'
|
import { PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME } from '@server/initializers/constants'
|
||||||
import { PluginManager } from '@server/lib/plugins/plugin-manager'
|
import { PluginManager } from '@server/lib/plugins/plugin-manager'
|
||||||
import { OAuthTokenModel } from '@server/models/oauth/oauth-token'
|
import { OAuthTokenModel } from '@server/models/oauth/oauth-token'
|
||||||
|
import { MUser } from '@server/types/models'
|
||||||
import {
|
import {
|
||||||
RegisterServerAuthenticatedResult,
|
RegisterServerAuthenticatedResult,
|
||||||
RegisterServerAuthPassOptions,
|
RegisterServerAuthPassOptions,
|
||||||
RegisterServerExternalAuthenticatedResult
|
RegisterServerExternalAuthenticatedResult
|
||||||
} from '@server/types/plugins/register-server-auth.model'
|
} from '@server/types/plugins/register-server-auth.model'
|
||||||
import { UserRole } from '@shared/models'
|
import { UserAdminFlag, UserRole } from '@shared/models'
|
||||||
|
import { BypassLogin } from './oauth-model'
|
||||||
|
|
||||||
|
export type ExternalUser =
|
||||||
|
Pick<MUser, 'username' | 'email' | 'role' | 'adminFlags' | 'videoQuotaDaily' | 'videoQuota'> &
|
||||||
|
{ displayName: string }
|
||||||
|
|
||||||
// Token is the key, expiration date is the value
|
// Token is the key, expiration date is the value
|
||||||
const authBypassTokens = new Map<string, {
|
const authBypassTokens = new Map<string, {
|
||||||
expires: Date
|
expires: Date
|
||||||
user: {
|
user: ExternalUser
|
||||||
username: string
|
userUpdater: RegisterServerAuthenticatedResult['userUpdater']
|
||||||
email: string
|
|
||||||
displayName: string
|
|
||||||
role: UserRole
|
|
||||||
}
|
|
||||||
authName: string
|
authName: string
|
||||||
npmName: string
|
npmName: string
|
||||||
}>()
|
}>()
|
||||||
|
@ -56,7 +65,8 @@ async function onExternalUserAuthenticated (options: {
|
||||||
expires,
|
expires,
|
||||||
user,
|
user,
|
||||||
npmName,
|
npmName,
|
||||||
authName
|
authName,
|
||||||
|
userUpdater: authResult.userUpdater
|
||||||
})
|
})
|
||||||
|
|
||||||
// Cleanup expired tokens
|
// Cleanup expired tokens
|
||||||
|
@ -78,7 +88,7 @@ async function getAuthNameFromRefreshGrant (refreshToken?: string) {
|
||||||
return tokenModel?.authName
|
return tokenModel?.authName
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getBypassFromPasswordGrant (username: string, password: string) {
|
async function getBypassFromPasswordGrant (username: string, password: string): Promise<BypassLogin> {
|
||||||
const plugins = PluginManager.Instance.getIdAndPassAuths()
|
const plugins = PluginManager.Instance.getIdAndPassAuths()
|
||||||
const pluginAuths: { npmName?: string, registerAuthOptions: RegisterServerAuthPassOptions }[] = []
|
const pluginAuths: { npmName?: string, registerAuthOptions: RegisterServerAuthPassOptions }[] = []
|
||||||
|
|
||||||
|
@ -133,7 +143,8 @@ async function getBypassFromPasswordGrant (username: string, password: string) {
|
||||||
bypass: true,
|
bypass: true,
|
||||||
pluginName: pluginAuth.npmName,
|
pluginName: pluginAuth.npmName,
|
||||||
authName: authOptions.authName,
|
authName: authOptions.authName,
|
||||||
user: buildUserResult(loginResult)
|
user: buildUserResult(loginResult),
|
||||||
|
userUpdater: loginResult.userUpdater
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('Error in auth method %s of plugin %s', authOptions.authName, pluginAuth.npmName, { err })
|
logger.error('Error in auth method %s of plugin %s', authOptions.authName, pluginAuth.npmName, { err })
|
||||||
|
@ -143,7 +154,7 @@ async function getBypassFromPasswordGrant (username: string, password: string) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBypassFromExternalAuth (username: string, externalAuthToken: string) {
|
function getBypassFromExternalAuth (username: string, externalAuthToken: string): BypassLogin {
|
||||||
const obj = authBypassTokens.get(externalAuthToken)
|
const obj = authBypassTokens.get(externalAuthToken)
|
||||||
if (!obj) throw new Error('Cannot authenticate user with unknown bypass token')
|
if (!obj) throw new Error('Cannot authenticate user with unknown bypass token')
|
||||||
|
|
||||||
|
@ -167,33 +178,29 @@ function getBypassFromExternalAuth (username: string, externalAuthToken: string)
|
||||||
bypass: true,
|
bypass: true,
|
||||||
pluginName: npmName,
|
pluginName: npmName,
|
||||||
authName,
|
authName,
|
||||||
|
userUpdater: obj.userUpdater,
|
||||||
user
|
user
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isAuthResultValid (npmName: string, authName: string, result: RegisterServerAuthenticatedResult) {
|
function isAuthResultValid (npmName: string, authName: string, result: RegisterServerAuthenticatedResult) {
|
||||||
if (!isUserUsernameValid(result.username)) {
|
const returnError = (field: string) => {
|
||||||
logger.error('Auth method %s of plugin %s did not provide a valid username.', authName, npmName, { username: result.username })
|
logger.error('Auth method %s of plugin %s did not provide a valid %s.', authName, npmName, field, { [field]: result[field] })
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!result.email) {
|
if (!isUserUsernameValid(result.username)) return returnError('username')
|
||||||
logger.error('Auth method %s of plugin %s did not provide a valid email.', authName, npmName, { email: result.email })
|
if (!result.email) return returnError('email')
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// role is optional
|
// Following fields are optional
|
||||||
if (result.role && !isUserRoleValid(result.role)) {
|
if (result.role && !isUserRoleValid(result.role)) return returnError('role')
|
||||||
logger.error('Auth method %s of plugin %s did not provide a valid role.', authName, npmName, { role: result.role })
|
if (result.displayName && !isUserDisplayNameValid(result.displayName)) return returnError('displayName')
|
||||||
return false
|
if (result.adminFlags && !isUserAdminFlagsValid(result.adminFlags)) return returnError('adminFlags')
|
||||||
}
|
if (result.videoQuota && !isUserVideoQuotaValid(result.videoQuota + '')) return returnError('videoQuota')
|
||||||
|
if (result.videoQuotaDaily && !isUserVideoQuotaDailyValid(result.videoQuotaDaily + '')) return returnError('videoQuotaDaily')
|
||||||
|
|
||||||
// display name is optional
|
if (result.userUpdater && typeof result.userUpdater !== 'function') {
|
||||||
if (result.displayName && !isUserDisplayNameValid(result.displayName)) {
|
logger.error('Auth method %s of plugin %s did not provide a valid user updater function.', authName, npmName)
|
||||||
logger.error(
|
|
||||||
'Auth method %s of plugin %s did not provide a valid display name.',
|
|
||||||
authName, npmName, { displayName: result.displayName }
|
|
||||||
)
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -205,7 +212,12 @@ function buildUserResult (pluginResult: RegisterServerAuthenticatedResult) {
|
||||||
username: pluginResult.username,
|
username: pluginResult.username,
|
||||||
email: pluginResult.email,
|
email: pluginResult.email,
|
||||||
role: pluginResult.role ?? UserRole.USER,
|
role: pluginResult.role ?? UserRole.USER,
|
||||||
displayName: pluginResult.displayName || pluginResult.username
|
displayName: pluginResult.displayName || pluginResult.username,
|
||||||
|
|
||||||
|
adminFlags: pluginResult.adminFlags ?? UserAdminFlag.NONE,
|
||||||
|
|
||||||
|
videoQuota: pluginResult.videoQuota,
|
||||||
|
videoQuotaDaily: pluginResult.videoQuotaDaily
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { AccessDeniedError } from '@node-oauth/oauth2-server'
|
import { AccessDeniedError } from '@node-oauth/oauth2-server'
|
||||||
import { PluginManager } from '@server/lib/plugins/plugin-manager'
|
import { PluginManager } from '@server/lib/plugins/plugin-manager'
|
||||||
|
import { AccountModel } from '@server/models/account/account'
|
||||||
|
import { AuthenticatedResultUpdaterFieldName, RegisterServerAuthenticatedResult } from '@server/types'
|
||||||
import { MOAuthClient } from '@server/types/models'
|
import { MOAuthClient } from '@server/types/models'
|
||||||
import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token'
|
import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token'
|
||||||
import { MUser } from '@server/types/models/user/user'
|
import { MUser, MUserDefault } from '@server/types/models/user/user'
|
||||||
import { pick } from '@shared/core-utils'
|
import { pick } from '@shared/core-utils'
|
||||||
import { UserRole } from '@shared/models/users/user-role'
|
import { AttributesOnly } from '@shared/typescript-utils'
|
||||||
import { logger } from '../../helpers/logger'
|
import { logger } from '../../helpers/logger'
|
||||||
import { CONFIG } from '../../initializers/config'
|
import { CONFIG } from '../../initializers/config'
|
||||||
import { OAuthClientModel } from '../../models/oauth/oauth-client'
|
import { OAuthClientModel } from '../../models/oauth/oauth-client'
|
||||||
|
@ -13,6 +15,7 @@ import { OAuthTokenModel } from '../../models/oauth/oauth-token'
|
||||||
import { UserModel } from '../../models/user/user'
|
import { UserModel } from '../../models/user/user'
|
||||||
import { findAvailableLocalActorName } from '../local-actor'
|
import { findAvailableLocalActorName } from '../local-actor'
|
||||||
import { buildUser, createUserAccountAndChannelAndPlaylist } from '../user'
|
import { buildUser, createUserAccountAndChannelAndPlaylist } from '../user'
|
||||||
|
import { ExternalUser } from './external-auth'
|
||||||
import { TokensCache } from './tokens-cache'
|
import { TokensCache } from './tokens-cache'
|
||||||
|
|
||||||
type TokenInfo = {
|
type TokenInfo = {
|
||||||
|
@ -26,12 +29,8 @@ export type BypassLogin = {
|
||||||
bypass: boolean
|
bypass: boolean
|
||||||
pluginName: string
|
pluginName: string
|
||||||
authName?: string
|
authName?: string
|
||||||
user: {
|
user: ExternalUser
|
||||||
username: string
|
userUpdater: RegisterServerAuthenticatedResult['userUpdater']
|
||||||
email: string
|
|
||||||
displayName: string
|
|
||||||
role: UserRole
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAccessToken (bearerToken: string) {
|
async function getAccessToken (bearerToken: string) {
|
||||||
|
@ -89,7 +88,9 @@ async function getUser (usernameOrEmail?: string, password?: string, bypassLogin
|
||||||
logger.info('Bypassing oauth login by plugin %s.', bypassLogin.pluginName)
|
logger.info('Bypassing oauth login by plugin %s.', bypassLogin.pluginName)
|
||||||
|
|
||||||
let user = await UserModel.loadByEmail(bypassLogin.user.email)
|
let user = await UserModel.loadByEmail(bypassLogin.user.email)
|
||||||
|
|
||||||
if (!user) user = await createUserFromExternal(bypassLogin.pluginName, bypassLogin.user)
|
if (!user) user = await createUserFromExternal(bypassLogin.pluginName, bypassLogin.user)
|
||||||
|
else user = await updateUserFromExternal(user, bypassLogin.user, bypassLogin.userUpdater)
|
||||||
|
|
||||||
// Cannot create a user
|
// Cannot create a user
|
||||||
if (!user) throw new AccessDeniedError('Cannot create such user: an actor with that name already exists.')
|
if (!user) throw new AccessDeniedError('Cannot create such user: an actor with that name already exists.')
|
||||||
|
@ -219,16 +220,11 @@ export {
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async function createUserFromExternal (pluginAuth: string, options: {
|
async function createUserFromExternal (pluginAuth: string, userOptions: ExternalUser) {
|
||||||
username: string
|
const username = await findAvailableLocalActorName(userOptions.username)
|
||||||
email: string
|
|
||||||
role: UserRole
|
|
||||||
displayName: string
|
|
||||||
}) {
|
|
||||||
const username = await findAvailableLocalActorName(options.username)
|
|
||||||
|
|
||||||
const userToCreate = buildUser({
|
const userToCreate = buildUser({
|
||||||
...pick(options, [ 'email', 'role' ]),
|
...pick(userOptions, [ 'email', 'role', 'adminFlags', 'videoQuota', 'videoQuotaDaily' ]),
|
||||||
|
|
||||||
username,
|
username,
|
||||||
emailVerified: null,
|
emailVerified: null,
|
||||||
|
@ -238,12 +234,57 @@ async function createUserFromExternal (pluginAuth: string, options: {
|
||||||
|
|
||||||
const { user } = await createUserAccountAndChannelAndPlaylist({
|
const { user } = await createUserAccountAndChannelAndPlaylist({
|
||||||
userToCreate,
|
userToCreate,
|
||||||
userDisplayName: options.displayName
|
userDisplayName: userOptions.displayName
|
||||||
})
|
})
|
||||||
|
|
||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function updateUserFromExternal (
|
||||||
|
user: MUserDefault,
|
||||||
|
userOptions: ExternalUser,
|
||||||
|
userUpdater: RegisterServerAuthenticatedResult['userUpdater']
|
||||||
|
) {
|
||||||
|
if (!userUpdater) return user
|
||||||
|
|
||||||
|
{
|
||||||
|
type UserAttributeKeys = keyof AttributesOnly<UserModel>
|
||||||
|
const mappingKeys: { [ id in UserAttributeKeys ]?: AuthenticatedResultUpdaterFieldName } = {
|
||||||
|
role: 'role',
|
||||||
|
adminFlags: 'adminFlags',
|
||||||
|
videoQuota: 'videoQuota',
|
||||||
|
videoQuotaDaily: 'videoQuotaDaily'
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const modelKey of Object.keys(mappingKeys)) {
|
||||||
|
const pluginOptionKey = mappingKeys[modelKey]
|
||||||
|
|
||||||
|
const newValue = userUpdater({ fieldName: pluginOptionKey, currentValue: user[modelKey], newValue: userOptions[pluginOptionKey] })
|
||||||
|
user.set(modelKey, newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
type AccountAttributeKeys = keyof Partial<AttributesOnly<AccountModel>>
|
||||||
|
const mappingKeys: { [ id in AccountAttributeKeys ]?: AuthenticatedResultUpdaterFieldName } = {
|
||||||
|
name: 'displayName'
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const modelKey of Object.keys(mappingKeys)) {
|
||||||
|
const optionKey = mappingKeys[modelKey]
|
||||||
|
|
||||||
|
const newValue = userUpdater({ fieldName: optionKey, currentValue: user.Account[modelKey], newValue: userOptions[optionKey] })
|
||||||
|
user.Account.set(modelKey, newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug('Updated user %s with plugin userUpdated function.', user.email, { user, userOptions })
|
||||||
|
|
||||||
|
user.Account = await user.Account.save()
|
||||||
|
|
||||||
|
return user.save()
|
||||||
|
}
|
||||||
|
|
||||||
function checkUserValidityOrThrow (user: MUser) {
|
function checkUserValidityOrThrow (user: MUser) {
|
||||||
if (user.blocked) throw new AccessDeniedError('User is blocked.')
|
if (user.blocked) throw new AccessDeniedError('User is blocked.')
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,10 +10,11 @@ import OAuth2Server, {
|
||||||
} from '@node-oauth/oauth2-server'
|
} from '@node-oauth/oauth2-server'
|
||||||
import { randomBytesPromise } from '@server/helpers/core-utils'
|
import { randomBytesPromise } from '@server/helpers/core-utils'
|
||||||
import { isOTPValid } from '@server/helpers/otp'
|
import { isOTPValid } from '@server/helpers/otp'
|
||||||
|
import { CONFIG } from '@server/initializers/config'
|
||||||
import { MOAuthClient } from '@server/types/models'
|
import { MOAuthClient } from '@server/types/models'
|
||||||
import { sha1 } from '@shared/extra-utils'
|
import { sha1 } from '@shared/extra-utils'
|
||||||
import { HttpStatusCode } from '@shared/models'
|
import { HttpStatusCode } from '@shared/models'
|
||||||
import { OAUTH_LIFETIME, OTP } from '../../initializers/constants'
|
import { OTP } from '../../initializers/constants'
|
||||||
import { BypassLogin, getClient, getRefreshToken, getUser, revokeToken, saveToken } from './oauth-model'
|
import { BypassLogin, getClient, getRefreshToken, getUser, revokeToken, saveToken } from './oauth-model'
|
||||||
|
|
||||||
class MissingTwoFactorError extends Error {
|
class MissingTwoFactorError extends Error {
|
||||||
|
@ -32,8 +33,9 @@ class InvalidTwoFactorError extends Error {
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
const oAuthServer = new OAuth2Server({
|
const oAuthServer = new OAuth2Server({
|
||||||
accessTokenLifetime: OAUTH_LIFETIME.ACCESS_TOKEN,
|
// Wants seconds
|
||||||
refreshTokenLifetime: OAUTH_LIFETIME.REFRESH_TOKEN,
|
accessTokenLifetime: CONFIG.OAUTH2.TOKEN_LIFETIME.ACCESS_TOKEN / 1000,
|
||||||
|
refreshTokenLifetime: CONFIG.OAUTH2.TOKEN_LIFETIME.REFRESH_TOKEN / 1000,
|
||||||
|
|
||||||
// See https://github.com/oauthjs/node-oauth2-server/wiki/Model-specification for the model specifications
|
// See https://github.com/oauthjs/node-oauth2-server/wiki/Model-specification for the model specifications
|
||||||
model: require('./oauth-model')
|
model: require('./oauth-model')
|
||||||
|
@ -182,10 +184,10 @@ function generateRandomToken () {
|
||||||
|
|
||||||
function getTokenExpiresAt (type: 'access' | 'refresh') {
|
function getTokenExpiresAt (type: 'access' | 'refresh') {
|
||||||
const lifetime = type === 'access'
|
const lifetime = type === 'access'
|
||||||
? OAUTH_LIFETIME.ACCESS_TOKEN
|
? CONFIG.OAUTH2.TOKEN_LIFETIME.ACCESS_TOKEN
|
||||||
: OAUTH_LIFETIME.REFRESH_TOKEN
|
: CONFIG.OAUTH2.TOKEN_LIFETIME.REFRESH_TOKEN
|
||||||
|
|
||||||
return new Date(Date.now() + lifetime * 1000)
|
return new Date(Date.now() + lifetime)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildToken () {
|
async function buildToken () {
|
||||||
|
|
|
@ -36,8 +36,8 @@ export class TokensCache {
|
||||||
const token = this.userHavingToken.get(userId)
|
const token = this.userHavingToken.get(userId)
|
||||||
|
|
||||||
if (token !== undefined) {
|
if (token !== undefined) {
|
||||||
this.accessTokenCache.del(token)
|
this.accessTokenCache.delete(token)
|
||||||
this.userHavingToken.del(userId)
|
this.userHavingToken.delete(userId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,8 +45,8 @@ export class TokensCache {
|
||||||
const tokenModel = this.accessTokenCache.get(token)
|
const tokenModel = this.accessTokenCache.get(token)
|
||||||
|
|
||||||
if (tokenModel !== undefined) {
|
if (tokenModel !== undefined) {
|
||||||
this.userHavingToken.del(tokenModel.userId)
|
this.userHavingToken.delete(tokenModel.userId)
|
||||||
this.accessTokenCache.del(token)
|
this.accessTokenCache.delete(token)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -439,7 +439,7 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel>
|
||||||
if (!element) return videoFile.save({ transaction })
|
if (!element) return videoFile.save({ transaction })
|
||||||
|
|
||||||
for (const k of Object.keys(videoFile.toJSON())) {
|
for (const k of Object.keys(videoFile.toJSON())) {
|
||||||
element[k] = videoFile[k]
|
element.set(k, videoFile[k])
|
||||||
}
|
}
|
||||||
|
|
||||||
return element.save({ transaction })
|
return element.save({ transaction })
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import './oauth'
|
||||||
import './two-factor'
|
import './two-factor'
|
||||||
import './user-subscriptions'
|
import './user-subscriptions'
|
||||||
import './user-videos'
|
import './user-videos'
|
||||||
|
|
|
@ -0,0 +1,192 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||||
|
|
||||||
|
import { expect } from 'chai'
|
||||||
|
import { wait } from '@shared/core-utils'
|
||||||
|
import { HttpStatusCode, OAuth2ErrorCode, PeerTubeProblemDocument } from '@shared/models'
|
||||||
|
import { cleanupTests, createSingleServer, killallServers, PeerTubeServer, setAccessTokensToServers } from '@shared/server-commands'
|
||||||
|
|
||||||
|
describe('Test oauth', function () {
|
||||||
|
let server: PeerTubeServer
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
this.timeout(30000)
|
||||||
|
|
||||||
|
server = await createSingleServer(1, {
|
||||||
|
rates_limit: {
|
||||||
|
login: {
|
||||||
|
max: 30
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await setAccessTokensToServers([ server ])
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('OAuth client', function () {
|
||||||
|
|
||||||
|
function expectInvalidClient (body: PeerTubeProblemDocument) {
|
||||||
|
expect(body.code).to.equal(OAuth2ErrorCode.INVALID_CLIENT)
|
||||||
|
expect(body.error).to.contain('client is invalid')
|
||||||
|
expect(body.type.startsWith('https://')).to.be.true
|
||||||
|
expect(body.type).to.contain(OAuth2ErrorCode.INVALID_CLIENT)
|
||||||
|
}
|
||||||
|
|
||||||
|
it('Should create a new client')
|
||||||
|
|
||||||
|
it('Should return the first client')
|
||||||
|
|
||||||
|
it('Should remove the last client')
|
||||||
|
|
||||||
|
it('Should not login with an invalid client id', async function () {
|
||||||
|
const client = { id: 'client', secret: server.store.client.secret }
|
||||||
|
const body = await server.login.login({ client, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||||
|
|
||||||
|
expectInvalidClient(body)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should not login with an invalid client secret', async function () {
|
||||||
|
const client = { id: server.store.client.id, secret: 'coucou' }
|
||||||
|
const body = await server.login.login({ client, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||||
|
|
||||||
|
expectInvalidClient(body)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Login', function () {
|
||||||
|
|
||||||
|
function expectInvalidCredentials (body: PeerTubeProblemDocument) {
|
||||||
|
expect(body.code).to.equal(OAuth2ErrorCode.INVALID_GRANT)
|
||||||
|
expect(body.error).to.contain('credentials are invalid')
|
||||||
|
expect(body.type.startsWith('https://')).to.be.true
|
||||||
|
expect(body.type).to.contain(OAuth2ErrorCode.INVALID_GRANT)
|
||||||
|
}
|
||||||
|
|
||||||
|
it('Should not login with an invalid username', async function () {
|
||||||
|
const user = { username: 'captain crochet', password: server.store.user.password }
|
||||||
|
const body = await server.login.login({ user, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||||
|
|
||||||
|
expectInvalidCredentials(body)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should not login with an invalid password', async function () {
|
||||||
|
const user = { username: server.store.user.username, password: 'mew_three' }
|
||||||
|
const body = await server.login.login({ user, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||||
|
|
||||||
|
expectInvalidCredentials(body)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should be able to login', async function () {
|
||||||
|
await server.login.login({ expectedStatus: HttpStatusCode.OK_200 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should be able to login with an insensitive username', async function () {
|
||||||
|
const user = { username: 'RoOt', password: server.store.user.password }
|
||||||
|
await server.login.login({ user, expectedStatus: HttpStatusCode.OK_200 })
|
||||||
|
|
||||||
|
const user2 = { username: 'rOoT', password: server.store.user.password }
|
||||||
|
await server.login.login({ user: user2, expectedStatus: HttpStatusCode.OK_200 })
|
||||||
|
|
||||||
|
const user3 = { username: 'ROOt', password: server.store.user.password }
|
||||||
|
await server.login.login({ user: user3, expectedStatus: HttpStatusCode.OK_200 })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Logout', function () {
|
||||||
|
|
||||||
|
it('Should logout (revoke token)', async function () {
|
||||||
|
await server.login.logout({ token: server.accessToken })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should not be able to get the user information', async function () {
|
||||||
|
await server.users.getMyInfo({ expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should not be able to upload a video', async function () {
|
||||||
|
await server.videos.upload({ attributes: { name: 'video' }, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should be able to login again', async function () {
|
||||||
|
const body = await server.login.login()
|
||||||
|
server.accessToken = body.access_token
|
||||||
|
server.refreshToken = body.refresh_token
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should be able to get my user information again', async function () {
|
||||||
|
await server.users.getMyInfo()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should have an expired access token', async function () {
|
||||||
|
this.timeout(60000)
|
||||||
|
|
||||||
|
await server.sql.setTokenField(server.accessToken, 'accessTokenExpiresAt', new Date().toISOString())
|
||||||
|
await server.sql.setTokenField(server.accessToken, 'refreshTokenExpiresAt', new Date().toISOString())
|
||||||
|
|
||||||
|
await killallServers([ server ])
|
||||||
|
await server.run()
|
||||||
|
|
||||||
|
await server.users.getMyInfo({ expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should not be able to refresh an access token with an expired refresh token', async function () {
|
||||||
|
await server.login.refreshToken({ refreshToken: server.refreshToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should refresh the token', async function () {
|
||||||
|
this.timeout(50000)
|
||||||
|
|
||||||
|
const futureDate = new Date(new Date().getTime() + 1000 * 60).toISOString()
|
||||||
|
await server.sql.setTokenField(server.accessToken, 'refreshTokenExpiresAt', futureDate)
|
||||||
|
|
||||||
|
await killallServers([ server ])
|
||||||
|
await server.run()
|
||||||
|
|
||||||
|
const res = await server.login.refreshToken({ refreshToken: server.refreshToken })
|
||||||
|
server.accessToken = res.body.access_token
|
||||||
|
server.refreshToken = res.body.refresh_token
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should be able to get my user information again', async function () {
|
||||||
|
await server.users.getMyInfo()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Custom token lifetime', function () {
|
||||||
|
before(async function () {
|
||||||
|
this.timeout(120_000)
|
||||||
|
|
||||||
|
await server.kill()
|
||||||
|
await server.run({
|
||||||
|
oauth2: {
|
||||||
|
token_lifetime: {
|
||||||
|
access_token: '2 seconds',
|
||||||
|
refresh_token: '2 seconds'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should have a very short access token lifetime', async function () {
|
||||||
|
this.timeout(50000)
|
||||||
|
|
||||||
|
const { access_token: accessToken } = await server.login.login()
|
||||||
|
await server.users.getMyInfo({ token: accessToken })
|
||||||
|
|
||||||
|
await wait(3000)
|
||||||
|
await server.users.getMyInfo({ token: accessToken, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should have a very short refresh token lifetime', async function () {
|
||||||
|
this.timeout(50000)
|
||||||
|
|
||||||
|
const { refresh_token: refreshToken } = await server.login.login()
|
||||||
|
await server.login.refreshToken({ refreshToken })
|
||||||
|
|
||||||
|
await wait(3000)
|
||||||
|
await server.login.refreshToken({ refreshToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
after(async function () {
|
||||||
|
await cleanupTests([ server ])
|
||||||
|
})
|
||||||
|
})
|
|
@ -2,15 +2,8 @@
|
||||||
|
|
||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import { testImage } from '@server/tests/shared'
|
import { testImage } from '@server/tests/shared'
|
||||||
import { AbuseState, HttpStatusCode, OAuth2ErrorCode, UserAdminFlag, UserRole, VideoPlaylistType } from '@shared/models'
|
import { AbuseState, HttpStatusCode, UserAdminFlag, UserRole, VideoPlaylistType } from '@shared/models'
|
||||||
import {
|
import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@shared/server-commands'
|
||||||
cleanupTests,
|
|
||||||
createSingleServer,
|
|
||||||
killallServers,
|
|
||||||
makePutBodyRequest,
|
|
||||||
PeerTubeServer,
|
|
||||||
setAccessTokensToServers
|
|
||||||
} from '@shared/server-commands'
|
|
||||||
|
|
||||||
describe('Test users', function () {
|
describe('Test users', function () {
|
||||||
let server: PeerTubeServer
|
let server: PeerTubeServer
|
||||||
|
@ -39,166 +32,6 @@ describe('Test users', function () {
|
||||||
await server.plugins.install({ npmName: 'peertube-theme-background-red' })
|
await server.plugins.install({ npmName: 'peertube-theme-background-red' })
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('OAuth client', function () {
|
|
||||||
it('Should create a new client')
|
|
||||||
|
|
||||||
it('Should return the first client')
|
|
||||||
|
|
||||||
it('Should remove the last client')
|
|
||||||
|
|
||||||
it('Should not login with an invalid client id', async function () {
|
|
||||||
const client = { id: 'client', secret: server.store.client.secret }
|
|
||||||
const body = await server.login.login({ client, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
|
||||||
|
|
||||||
expect(body.code).to.equal(OAuth2ErrorCode.INVALID_CLIENT)
|
|
||||||
expect(body.error).to.contain('client is invalid')
|
|
||||||
expect(body.type.startsWith('https://')).to.be.true
|
|
||||||
expect(body.type).to.contain(OAuth2ErrorCode.INVALID_CLIENT)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should not login with an invalid client secret', async function () {
|
|
||||||
const client = { id: server.store.client.id, secret: 'coucou' }
|
|
||||||
const body = await server.login.login({ client, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
|
||||||
|
|
||||||
expect(body.code).to.equal(OAuth2ErrorCode.INVALID_CLIENT)
|
|
||||||
expect(body.error).to.contain('client is invalid')
|
|
||||||
expect(body.type.startsWith('https://')).to.be.true
|
|
||||||
expect(body.type).to.contain(OAuth2ErrorCode.INVALID_CLIENT)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Login', function () {
|
|
||||||
|
|
||||||
it('Should not login with an invalid username', async function () {
|
|
||||||
const user = { username: 'captain crochet', password: server.store.user.password }
|
|
||||||
const body = await server.login.login({ user, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
|
||||||
|
|
||||||
expect(body.code).to.equal(OAuth2ErrorCode.INVALID_GRANT)
|
|
||||||
expect(body.error).to.contain('credentials are invalid')
|
|
||||||
expect(body.type.startsWith('https://')).to.be.true
|
|
||||||
expect(body.type).to.contain(OAuth2ErrorCode.INVALID_GRANT)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should not login with an invalid password', async function () {
|
|
||||||
const user = { username: server.store.user.username, password: 'mew_three' }
|
|
||||||
const body = await server.login.login({ user, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
|
||||||
|
|
||||||
expect(body.code).to.equal(OAuth2ErrorCode.INVALID_GRANT)
|
|
||||||
expect(body.error).to.contain('credentials are invalid')
|
|
||||||
expect(body.type.startsWith('https://')).to.be.true
|
|
||||||
expect(body.type).to.contain(OAuth2ErrorCode.INVALID_GRANT)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should not be able to upload a video', async function () {
|
|
||||||
token = 'my_super_token'
|
|
||||||
|
|
||||||
await server.videos.upload({ token, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should not be able to follow', async function () {
|
|
||||||
token = 'my_super_token'
|
|
||||||
|
|
||||||
await server.follows.follow({
|
|
||||||
hosts: [ 'http://example.com' ],
|
|
||||||
token,
|
|
||||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should not be able to unfollow')
|
|
||||||
|
|
||||||
it('Should be able to login', async function () {
|
|
||||||
const body = await server.login.login({ expectedStatus: HttpStatusCode.OK_200 })
|
|
||||||
|
|
||||||
token = body.access_token
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should be able to login with an insensitive username', async function () {
|
|
||||||
const user = { username: 'RoOt', password: server.store.user.password }
|
|
||||||
await server.login.login({ user, expectedStatus: HttpStatusCode.OK_200 })
|
|
||||||
|
|
||||||
const user2 = { username: 'rOoT', password: server.store.user.password }
|
|
||||||
await server.login.login({ user: user2, expectedStatus: HttpStatusCode.OK_200 })
|
|
||||||
|
|
||||||
const user3 = { username: 'ROOt', password: server.store.user.password }
|
|
||||||
await server.login.login({ user: user3, expectedStatus: HttpStatusCode.OK_200 })
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Logout', function () {
|
|
||||||
it('Should logout (revoke token)', async function () {
|
|
||||||
await server.login.logout({ token: server.accessToken })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should not be able to get the user information', async function () {
|
|
||||||
await server.users.getMyInfo({ expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should not be able to upload a video', async function () {
|
|
||||||
await server.videos.upload({ attributes: { name: 'video' }, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should not be able to rate a video', async function () {
|
|
||||||
const path = '/api/v1/videos/'
|
|
||||||
const data = {
|
|
||||||
rating: 'likes'
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
url: server.url,
|
|
||||||
path: path + videoId,
|
|
||||||
token: 'wrong token',
|
|
||||||
fields: data,
|
|
||||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
|
||||||
}
|
|
||||||
await makePutBodyRequest(options)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should be able to login again', async function () {
|
|
||||||
const body = await server.login.login()
|
|
||||||
server.accessToken = body.access_token
|
|
||||||
server.refreshToken = body.refresh_token
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should be able to get my user information again', async function () {
|
|
||||||
await server.users.getMyInfo()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should have an expired access token', async function () {
|
|
||||||
this.timeout(60000)
|
|
||||||
|
|
||||||
await server.sql.setTokenField(server.accessToken, 'accessTokenExpiresAt', new Date().toISOString())
|
|
||||||
await server.sql.setTokenField(server.accessToken, 'refreshTokenExpiresAt', new Date().toISOString())
|
|
||||||
|
|
||||||
await killallServers([ server ])
|
|
||||||
await server.run()
|
|
||||||
|
|
||||||
await server.users.getMyInfo({ expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should not be able to refresh an access token with an expired refresh token', async function () {
|
|
||||||
await server.login.refreshToken({ refreshToken: server.refreshToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should refresh the token', async function () {
|
|
||||||
this.timeout(50000)
|
|
||||||
|
|
||||||
const futureDate = new Date(new Date().getTime() + 1000 * 60).toISOString()
|
|
||||||
await server.sql.setTokenField(server.accessToken, 'refreshTokenExpiresAt', futureDate)
|
|
||||||
|
|
||||||
await killallServers([ server ])
|
|
||||||
await server.run()
|
|
||||||
|
|
||||||
const res = await server.login.refreshToken({ refreshToken: server.refreshToken })
|
|
||||||
server.accessToken = res.body.access_token
|
|
||||||
server.refreshToken = res.body.refresh_token
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Should be able to get my user information again', async function () {
|
|
||||||
await server.users.getMyInfo()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Creating a user', function () {
|
describe('Creating a user', function () {
|
||||||
|
|
||||||
it('Should be able to create a new user', async function () {
|
it('Should be able to create a new user', async function () {
|
||||||
|
@ -512,6 +345,7 @@ describe('Test users', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Updating another user', function () {
|
describe('Updating another user', function () {
|
||||||
|
|
||||||
it('Should be able to update another user', async function () {
|
it('Should be able to update another user', async function () {
|
||||||
await server.users.update({
|
await server.users.update({
|
||||||
userId,
|
userId,
|
||||||
|
@ -562,13 +396,6 @@ describe('Test users', function () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Video blacklists', function () {
|
|
||||||
|
|
||||||
it('Should be able to list my video blacklist', async function () {
|
|
||||||
await server.blacklist.list({ token: userToken })
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Remove a user', function () {
|
describe('Remove a user', function () {
|
||||||
|
|
||||||
before(async function () {
|
before(async function () {
|
||||||
|
@ -653,8 +480,9 @@ describe('Test users', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('User blocking', function () {
|
describe('User blocking', function () {
|
||||||
let user16Id
|
let user16Id: number
|
||||||
let user16AccessToken
|
let user16AccessToken: string
|
||||||
|
|
||||||
const user16 = {
|
const user16 = {
|
||||||
username: 'user_16',
|
username: 'user_16',
|
||||||
password: 'my super password'
|
password: 'my super password'
|
||||||
|
|
|
@ -33,7 +33,17 @@ async function register ({
|
||||||
username: 'kefka',
|
username: 'kefka',
|
||||||
email: 'kefka@example.com',
|
email: 'kefka@example.com',
|
||||||
role: 0,
|
role: 0,
|
||||||
displayName: 'Kefka Palazzo'
|
displayName: 'Kefka Palazzo',
|
||||||
|
adminFlags: 1,
|
||||||
|
videoQuota: 42000,
|
||||||
|
videoQuotaDaily: 42100,
|
||||||
|
|
||||||
|
// Always use new value except for videoQuotaDaily field
|
||||||
|
userUpdater: ({ fieldName, currentValue, newValue }) => {
|
||||||
|
if (fieldName === 'videoQuotaDaily') return currentValue
|
||||||
|
|
||||||
|
return newValue
|
||||||
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
hookTokenValidity: (options) => {
|
hookTokenValidity: (options) => {
|
||||||
|
|
|
@ -33,7 +33,18 @@ async function register ({
|
||||||
if (body.id === 'laguna' && body.password === 'laguna password') {
|
if (body.id === 'laguna' && body.password === 'laguna password') {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
username: 'laguna',
|
username: 'laguna',
|
||||||
email: 'laguna@example.com'
|
email: 'laguna@example.com',
|
||||||
|
displayName: 'Laguna Loire',
|
||||||
|
adminFlags: 1,
|
||||||
|
videoQuota: 42000,
|
||||||
|
videoQuotaDaily: 42100,
|
||||||
|
|
||||||
|
// Always use new value except for videoQuotaDaily field
|
||||||
|
userUpdater: ({ fieldName, currentValue, newValue }) => {
|
||||||
|
if (fieldName === 'videoQuotaDaily') return currentValue
|
||||||
|
|
||||||
|
return newValue
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import { wait } from '@shared/core-utils'
|
import { wait } from '@shared/core-utils'
|
||||||
import { HttpStatusCode, UserRole } from '@shared/models'
|
import { HttpStatusCode, UserAdminFlag, UserRole } from '@shared/models'
|
||||||
import {
|
import {
|
||||||
cleanupTests,
|
cleanupTests,
|
||||||
createSingleServer,
|
createSingleServer,
|
||||||
|
@ -51,6 +51,7 @@ describe('Test external auth plugins', function () {
|
||||||
|
|
||||||
let kefkaAccessToken: string
|
let kefkaAccessToken: string
|
||||||
let kefkaRefreshToken: string
|
let kefkaRefreshToken: string
|
||||||
|
let kefkaId: number
|
||||||
|
|
||||||
let externalAuthToken: string
|
let externalAuthToken: string
|
||||||
|
|
||||||
|
@ -156,6 +157,9 @@ describe('Test external auth plugins', function () {
|
||||||
expect(body.account.displayName).to.equal('cyan')
|
expect(body.account.displayName).to.equal('cyan')
|
||||||
expect(body.email).to.equal('cyan@example.com')
|
expect(body.email).to.equal('cyan@example.com')
|
||||||
expect(body.role.id).to.equal(UserRole.USER)
|
expect(body.role.id).to.equal(UserRole.USER)
|
||||||
|
expect(body.adminFlags).to.equal(UserAdminFlag.NONE)
|
||||||
|
expect(body.videoQuota).to.equal(5242880)
|
||||||
|
expect(body.videoQuotaDaily).to.equal(-1)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -178,6 +182,11 @@ describe('Test external auth plugins', function () {
|
||||||
expect(body.account.displayName).to.equal('Kefka Palazzo')
|
expect(body.account.displayName).to.equal('Kefka Palazzo')
|
||||||
expect(body.email).to.equal('kefka@example.com')
|
expect(body.email).to.equal('kefka@example.com')
|
||||||
expect(body.role.id).to.equal(UserRole.ADMINISTRATOR)
|
expect(body.role.id).to.equal(UserRole.ADMINISTRATOR)
|
||||||
|
expect(body.adminFlags).to.equal(UserAdminFlag.BYPASS_VIDEO_AUTO_BLACKLIST)
|
||||||
|
expect(body.videoQuota).to.equal(42000)
|
||||||
|
expect(body.videoQuotaDaily).to.equal(42100)
|
||||||
|
|
||||||
|
kefkaId = body.id
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -240,6 +249,37 @@ describe('Test external auth plugins', function () {
|
||||||
expect(body.role.id).to.equal(UserRole.USER)
|
expect(body.role.id).to.equal(UserRole.USER)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('Should login Kefka and update the profile', async function () {
|
||||||
|
{
|
||||||
|
await server.users.update({ userId: kefkaId, videoQuota: 43000, videoQuotaDaily: 43100 })
|
||||||
|
await server.users.updateMe({ token: kefkaAccessToken, displayName: 'kefka updated' })
|
||||||
|
|
||||||
|
const body = await server.users.getMyInfo({ token: kefkaAccessToken })
|
||||||
|
expect(body.username).to.equal('kefka')
|
||||||
|
expect(body.account.displayName).to.equal('kefka updated')
|
||||||
|
expect(body.videoQuota).to.equal(43000)
|
||||||
|
expect(body.videoQuotaDaily).to.equal(43100)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const res = await loginExternal({
|
||||||
|
server,
|
||||||
|
npmName: 'test-external-auth-one',
|
||||||
|
authName: 'external-auth-2',
|
||||||
|
username: 'kefka'
|
||||||
|
})
|
||||||
|
|
||||||
|
kefkaAccessToken = res.access_token
|
||||||
|
kefkaRefreshToken = res.refresh_token
|
||||||
|
|
||||||
|
const body = await server.users.getMyInfo({ token: kefkaAccessToken })
|
||||||
|
expect(body.username).to.equal('kefka')
|
||||||
|
expect(body.account.displayName).to.equal('Kefka Palazzo')
|
||||||
|
expect(body.videoQuota).to.equal(42000)
|
||||||
|
expect(body.videoQuotaDaily).to.equal(43100)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
it('Should not update an external auth email', async function () {
|
it('Should not update an external auth email', async function () {
|
||||||
await server.users.updateMe({
|
await server.users.updateMe({
|
||||||
token: cyanAccessToken,
|
token: cyanAccessToken,
|
||||||
|
|
|
@ -13,6 +13,7 @@ describe('Test id and pass auth plugins', function () {
|
||||||
|
|
||||||
let lagunaAccessToken: string
|
let lagunaAccessToken: string
|
||||||
let lagunaRefreshToken: string
|
let lagunaRefreshToken: string
|
||||||
|
let lagunaId: number
|
||||||
|
|
||||||
before(async function () {
|
before(async function () {
|
||||||
this.timeout(30000)
|
this.timeout(30000)
|
||||||
|
@ -78,8 +79,10 @@ describe('Test id and pass auth plugins', function () {
|
||||||
const body = await server.users.getMyInfo({ token: lagunaAccessToken })
|
const body = await server.users.getMyInfo({ token: lagunaAccessToken })
|
||||||
|
|
||||||
expect(body.username).to.equal('laguna')
|
expect(body.username).to.equal('laguna')
|
||||||
expect(body.account.displayName).to.equal('laguna')
|
expect(body.account.displayName).to.equal('Laguna Loire')
|
||||||
expect(body.role.id).to.equal(UserRole.USER)
|
expect(body.role.id).to.equal(UserRole.USER)
|
||||||
|
|
||||||
|
lagunaId = body.id
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -132,6 +135,33 @@ describe('Test id and pass auth plugins', function () {
|
||||||
expect(body.role.id).to.equal(UserRole.MODERATOR)
|
expect(body.role.id).to.equal(UserRole.MODERATOR)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('Should login Laguna and update the profile', async function () {
|
||||||
|
{
|
||||||
|
await server.users.update({ userId: lagunaId, videoQuota: 43000, videoQuotaDaily: 43100 })
|
||||||
|
await server.users.updateMe({ token: lagunaAccessToken, displayName: 'laguna updated' })
|
||||||
|
|
||||||
|
const body = await server.users.getMyInfo({ token: lagunaAccessToken })
|
||||||
|
expect(body.username).to.equal('laguna')
|
||||||
|
expect(body.account.displayName).to.equal('laguna updated')
|
||||||
|
expect(body.videoQuota).to.equal(43000)
|
||||||
|
expect(body.videoQuotaDaily).to.equal(43100)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const body = await server.login.login({ user: { username: 'laguna', password: 'laguna password' } })
|
||||||
|
lagunaAccessToken = body.access_token
|
||||||
|
lagunaRefreshToken = body.refresh_token
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const body = await server.users.getMyInfo({ token: lagunaAccessToken })
|
||||||
|
expect(body.username).to.equal('laguna')
|
||||||
|
expect(body.account.displayName).to.equal('Laguna Loire')
|
||||||
|
expect(body.videoQuota).to.equal(42000)
|
||||||
|
expect(body.videoQuotaDaily).to.equal(43100)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
it('Should reject token of laguna by the plugin hook', async function () {
|
it('Should reject token of laguna by the plugin hook', async function () {
|
||||||
this.timeout(10000)
|
this.timeout(10000)
|
||||||
|
|
||||||
|
@ -147,7 +177,7 @@ describe('Test id and pass auth plugins', function () {
|
||||||
await server.servers.waitUntilLog('valid username')
|
await server.servers.waitUntilLog('valid username')
|
||||||
|
|
||||||
await command.login({ user: { username: 'kiros', password: 'kiros password' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
await command.login({ user: { username: 'kiros', password: 'kiros password' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||||
await server.servers.waitUntilLog('valid display name')
|
await server.servers.waitUntilLog('valid displayName')
|
||||||
|
|
||||||
await command.login({ user: { username: 'raine', password: 'raine password' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
await command.login({ user: { username: 'raine', password: 'raine password' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||||
await server.servers.waitUntilLog('valid role')
|
await server.servers.waitUntilLog('valid role')
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
import { OutgoingHttpHeaders } from 'http'
|
import { OutgoingHttpHeaders } from 'http'
|
||||||
import { RegisterServerAuthExternalOptions } from '@server/types'
|
import { RegisterServerAuthExternalOptions } from '@server/types'
|
||||||
import {
|
import {
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
type ObjectKeys<T> =
|
||||||
|
T extends object
|
||||||
|
? `${Exclude<keyof T, symbol>}`[]
|
||||||
|
: T extends number
|
||||||
|
? []
|
||||||
|
: T extends any | string
|
||||||
|
? string[]
|
||||||
|
: never
|
||||||
|
|
||||||
|
interface ObjectConstructor {
|
||||||
|
keys<T> (o: T): ObjectKeys<T>
|
||||||
|
}
|
|
@ -1,14 +1,33 @@
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { UserRole } from '@shared/models'
|
import { UserAdminFlag, UserRole } from '@shared/models'
|
||||||
import { MOAuthToken, MUser } from '../models'
|
import { MOAuthToken, MUser } from '../models'
|
||||||
|
|
||||||
export type RegisterServerAuthOptions = RegisterServerAuthPassOptions | RegisterServerAuthExternalOptions
|
export type RegisterServerAuthOptions = RegisterServerAuthPassOptions | RegisterServerAuthExternalOptions
|
||||||
|
|
||||||
|
export type AuthenticatedResultUpdaterFieldName = 'displayName' | 'role' | 'adminFlags' | 'videoQuota' | 'videoQuotaDaily'
|
||||||
|
|
||||||
export interface RegisterServerAuthenticatedResult {
|
export interface RegisterServerAuthenticatedResult {
|
||||||
|
// Update the user profile if it already exists
|
||||||
|
// Default behaviour is no update
|
||||||
|
// Introduced in PeerTube >= 5.1
|
||||||
|
userUpdater?: <T> (options: {
|
||||||
|
fieldName: AuthenticatedResultUpdaterFieldName
|
||||||
|
currentValue: T
|
||||||
|
newValue: T
|
||||||
|
}) => T
|
||||||
|
|
||||||
username: string
|
username: string
|
||||||
email: string
|
email: string
|
||||||
role?: UserRole
|
role?: UserRole
|
||||||
displayName?: string
|
displayName?: string
|
||||||
|
|
||||||
|
// PeerTube >= 5.1
|
||||||
|
adminFlags?: UserAdminFlag
|
||||||
|
|
||||||
|
// PeerTube >= 5.1
|
||||||
|
videoQuota?: number
|
||||||
|
// PeerTube >= 5.1
|
||||||
|
videoQuotaDaily?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RegisterServerExternalAuthenticatedResult extends RegisterServerAuthenticatedResult {
|
export interface RegisterServerExternalAuthenticatedResult extends RegisterServerAuthenticatedResult {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { RegisteredExternalAuthConfig } from '@shared/models'
|
||||||
import { HookType } from '../../models/plugins/hook-type.enum'
|
import { HookType } from '../../models/plugins/hook-type.enum'
|
||||||
import { isCatchable, isPromise } from '../common/promises'
|
import { isCatchable, isPromise } from '../common/promises'
|
||||||
|
|
||||||
|
@ -49,7 +50,12 @@ async function internalRunHook <T> (options: {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getExternalAuthHref (apiUrl: string, auth: RegisteredExternalAuthConfig) {
|
||||||
|
return apiUrl + `/plugins/${auth.name}/${auth.version}/auth/${auth.authName}`
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getHookType,
|
getHookType,
|
||||||
internalRunHook
|
internalRunHook,
|
||||||
|
getExternalAuthHref
|
||||||
}
|
}
|
||||||
|
|
|
@ -199,7 +199,7 @@ function buildRequest (req: request.Test, options: CommonRequestParams) {
|
||||||
return req.expect((res) => {
|
return req.expect((res) => {
|
||||||
if (options.expectedStatus && res.status !== options.expectedStatus) {
|
if (options.expectedStatus && res.status !== options.expectedStatus) {
|
||||||
throw new Error(`Expected status ${options.expectedStatus}, got ${res.status}. ` +
|
throw new Error(`Expected status ${options.expectedStatus}, got ${res.status}. ` +
|
||||||
`\nThe server responded this error: "${res.body?.error ?? res.text}".\n` +
|
`\nThe server responded: "${res.body?.error ?? res.text}".\n` +
|
||||||
'You may take a closer look at the logs. To see how to do so, check out this page: ' +
|
'You may take a closer look at the logs. To see how to do so, check out this page: ' +
|
||||||
'https://github.com/Chocobozzz/PeerTube/blob/develop/support/doc/development/tests.md#debug-server-logs')
|
'https://github.com/Chocobozzz/PeerTube/blob/develop/support/doc/development/tests.md#debug-server-logs')
|
||||||
}
|
}
|
||||||
|
|
|
@ -433,7 +433,27 @@ function register (...) {
|
||||||
username: 'user'
|
username: 'user'
|
||||||
email: 'user@example.com'
|
email: 'user@example.com'
|
||||||
role: 2
|
role: 2
|
||||||
displayName: 'User display name'
|
displayName: 'User display name',
|
||||||
|
|
||||||
|
// Custom admin flags (bypass video auto moderation etc.)
|
||||||
|
// https://github.com/Chocobozzz/PeerTube/blob/develop/shared/models/users/user-flag.model.ts
|
||||||
|
// PeerTube >= 5.1
|
||||||
|
adminFlags: 0,
|
||||||
|
// Quota in bytes
|
||||||
|
// PeerTube >= 5.1
|
||||||
|
videoQuota: 1024 * 1024 * 1024, // 1GB
|
||||||
|
// PeerTube >= 5.1
|
||||||
|
videoQuotaDaily: -1, // Unlimited
|
||||||
|
|
||||||
|
// Update the user profile if it already exists
|
||||||
|
// Default behaviour is no update
|
||||||
|
// Introduced in PeerTube >= 5.1
|
||||||
|
userUpdater: ({ fieldName, currentValue, newValue }) => {
|
||||||
|
// Always use new value except for videoQuotaDaily field
|
||||||
|
if (fieldName === 'videoQuotaDaily') return currentValue
|
||||||
|
|
||||||
|
return newValue
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue