aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--server/helpers/custom-validators/video-captions.ts9
-rw-r--r--server/helpers/custom-validators/video-imports.ts9
-rw-r--r--server/initializers/checker-after-init.ts3
-rw-r--r--server/initializers/installer.ts6
-rw-r--r--server/lib/auth/external-auth.ts18
-rw-r--r--server/lib/auth/oauth-model.ts53
-rw-r--r--server/models/video/video-file.ts2
-rw-r--r--server/tests/fixtures/peertube-plugin-test-external-auth-one/main.js9
-rw-r--r--server/tests/fixtures/peertube-plugin-test-id-pass-auth-two/main.js13
-rw-r--r--server/tests/plugins/external-auth.ts34
-rw-r--r--server/tests/plugins/id-and-pass-auth.ts34
-rw-r--r--server/types/express.d.ts1
-rw-r--r--server/types/lib.d.ts12
-rw-r--r--server/types/plugins/register-server-auth.model.ts14
-rw-r--r--support/doc/plugins/guide.md22
15 files changed, 214 insertions, 25 deletions
diff --git a/server/helpers/custom-validators/video-captions.ts b/server/helpers/custom-validators/video-captions.ts
index 59ba005fe..d5b09ea03 100644
--- a/server/helpers/custom-validators/video-captions.ts
+++ b/server/helpers/custom-validators/video-captions.ts
@@ -8,10 +8,11 @@ function isVideoCaptionLanguageValid (value: any) {
8 return exists(value) && VIDEO_LANGUAGES[value] !== undefined 8 return exists(value) && VIDEO_LANGUAGES[value] !== undefined
9} 9}
10 10
11const videoCaptionTypesRegex = Object.keys(MIMETYPES.VIDEO_CAPTIONS.MIMETYPE_EXT) 11// MacOS sends application/octet-stream
12 .concat([ 'application/octet-stream' ]) // MacOS sends application/octet-stream 12const videoCaptionTypesRegex = [ ...Object.keys(MIMETYPES.VIDEO_CAPTIONS.MIMETYPE_EXT), 'application/octet-stream' ]
13 .map(m => `(${m})`) 13 .map(m => `(${m})`)
14 .join('|') 14 .join('|')
15
15function isVideoCaptionFile (files: UploadFilesForCheck, field: string) { 16function isVideoCaptionFile (files: UploadFilesForCheck, field: string) {
16 return isFileValid({ 17 return isFileValid({
17 files, 18 files,
diff --git a/server/helpers/custom-validators/video-imports.ts b/server/helpers/custom-validators/video-imports.ts
index af93aea56..da8962cb6 100644
--- a/server/helpers/custom-validators/video-imports.ts
+++ b/server/helpers/custom-validators/video-imports.ts
@@ -22,10 +22,11 @@ function isVideoImportStateValid (value: any) {
22 return exists(value) && VIDEO_IMPORT_STATES[value] !== undefined 22 return exists(value) && VIDEO_IMPORT_STATES[value] !== undefined
23} 23}
24 24
25const videoTorrentImportRegex = Object.keys(MIMETYPES.TORRENT.MIMETYPE_EXT) 25// MacOS sends application/octet-stream
26 .concat([ 'application/octet-stream' ]) // MacOS sends application/octet-stream 26const videoTorrentImportRegex = [ ...Object.keys(MIMETYPES.TORRENT.MIMETYPE_EXT), 'application/octet-stream' ]
27 .map(m => `(${m})`) 27 .map(m => `(${m})`)
28 .join('|') 28 .join('|')
29
29function isVideoImportTorrentFile (files: UploadFilesForCheck) { 30function isVideoImportTorrentFile (files: UploadFilesForCheck) {
30 return isFileValid({ 31 return isFileValid({
31 files, 32 files,
diff --git a/server/initializers/checker-after-init.ts b/server/initializers/checker-after-init.ts
index 09e878eee..e6432641b 100644
--- a/server/initializers/checker-after-init.ts
+++ b/server/initializers/checker-after-init.ts
@@ -174,7 +174,8 @@ function checkRemoteRedundancyConfig () {
174function checkStorageConfig () { 174function checkStorageConfig () {
175 // Check storage directory locations 175 // Check storage directory locations
176 if (isProdInstance()) { 176 if (isProdInstance()) {
177 const configStorage = config.get('storage') 177 const configStorage = config.get<{ [ name: string ]: string }>('storage')
178
178 for (const key of Object.keys(configStorage)) { 179 for (const key of Object.keys(configStorage)) {
179 if (configStorage[key].startsWith('storage/')) { 180 if (configStorage[key].startsWith('storage/')) {
180 logger.warn( 181 logger.warn(
diff --git a/server/initializers/installer.ts b/server/initializers/installer.ts
index f5d8eedf1..f48f348a7 100644
--- a/server/initializers/installer.ts
+++ b/server/initializers/installer.ts
@@ -51,8 +51,7 @@ function removeCacheAndTmpDirectories () {
51 const tasks: Promise<any>[] = [] 51 const tasks: Promise<any>[] = []
52 52
53 // Cache directories 53 // Cache directories
54 for (const key of Object.keys(cacheDirectories)) { 54 for (const dir of cacheDirectories) {
55 const dir = cacheDirectories[key]
56 tasks.push(removeDirectoryOrContent(dir)) 55 tasks.push(removeDirectoryOrContent(dir))
57 } 56 }
58 57
@@ -87,8 +86,7 @@ function createDirectoriesIfNotExist () {
87 } 86 }
88 87
89 // Cache directories 88 // Cache directories
90 for (const key of Object.keys(cacheDirectories)) { 89 for (const dir of cacheDirectories) {
91 const dir = cacheDirectories[key]
92 tasks.push(ensureDir(dir)) 90 tasks.push(ensureDir(dir))
93 } 91 }
94 92
diff --git a/server/lib/auth/external-auth.ts b/server/lib/auth/external-auth.ts
index 155ec03d8..bc5b74257 100644
--- a/server/lib/auth/external-auth.ts
+++ b/server/lib/auth/external-auth.ts
@@ -19,6 +19,7 @@ import {
19 RegisterServerExternalAuthenticatedResult 19 RegisterServerExternalAuthenticatedResult
20} from '@server/types/plugins/register-server-auth.model' 20} from '@server/types/plugins/register-server-auth.model'
21import { UserAdminFlag, UserRole } from '@shared/models' 21import { UserAdminFlag, UserRole } from '@shared/models'
22import { BypassLogin } from './oauth-model'
22 23
23export type ExternalUser = 24export type ExternalUser =
24 Pick<MUser, 'username' | 'email' | 'role' | 'adminFlags' | 'videoQuotaDaily' | 'videoQuota'> & 25 Pick<MUser, 'username' | 'email' | 'role' | 'adminFlags' | 'videoQuotaDaily' | 'videoQuota'> &
@@ -28,6 +29,7 @@ export type ExternalUser =
28const authBypassTokens = new Map<string, { 29const authBypassTokens = new Map<string, {
29 expires: Date 30 expires: Date
30 user: ExternalUser 31 user: ExternalUser
32 userUpdater: RegisterServerAuthenticatedResult['userUpdater']
31 authName: string 33 authName: string
32 npmName: string 34 npmName: string
33}>() 35}>()
@@ -63,7 +65,8 @@ async function onExternalUserAuthenticated (options: {
63 expires, 65 expires,
64 user, 66 user,
65 npmName, 67 npmName,
66 authName 68 authName,
69 userUpdater: authResult.userUpdater
67 }) 70 })
68 71
69 // Cleanup expired tokens 72 // Cleanup expired tokens
@@ -85,7 +88,7 @@ async function getAuthNameFromRefreshGrant (refreshToken?: string) {
85 return tokenModel?.authName 88 return tokenModel?.authName
86} 89}
87 90
88async function getBypassFromPasswordGrant (username: string, password: string) { 91async function getBypassFromPasswordGrant (username: string, password: string): Promise<BypassLogin> {
89 const plugins = PluginManager.Instance.getIdAndPassAuths() 92 const plugins = PluginManager.Instance.getIdAndPassAuths()
90 const pluginAuths: { npmName?: string, registerAuthOptions: RegisterServerAuthPassOptions }[] = [] 93 const pluginAuths: { npmName?: string, registerAuthOptions: RegisterServerAuthPassOptions }[] = []
91 94
@@ -140,7 +143,8 @@ async function getBypassFromPasswordGrant (username: string, password: string) {
140 bypass: true, 143 bypass: true,
141 pluginName: pluginAuth.npmName, 144 pluginName: pluginAuth.npmName,
142 authName: authOptions.authName, 145 authName: authOptions.authName,
143 user: buildUserResult(loginResult) 146 user: buildUserResult(loginResult),
147 userUpdater: loginResult.userUpdater
144 } 148 }
145 } catch (err) { 149 } catch (err) {
146 logger.error('Error in auth method %s of plugin %s', authOptions.authName, pluginAuth.npmName, { err }) 150 logger.error('Error in auth method %s of plugin %s', authOptions.authName, pluginAuth.npmName, { err })
@@ -150,7 +154,7 @@ async function getBypassFromPasswordGrant (username: string, password: string) {
150 return undefined 154 return undefined
151} 155}
152 156
153function getBypassFromExternalAuth (username: string, externalAuthToken: string) { 157function getBypassFromExternalAuth (username: string, externalAuthToken: string): BypassLogin {
154 const obj = authBypassTokens.get(externalAuthToken) 158 const obj = authBypassTokens.get(externalAuthToken)
155 if (!obj) throw new Error('Cannot authenticate user with unknown bypass token') 159 if (!obj) throw new Error('Cannot authenticate user with unknown bypass token')
156 160
@@ -174,6 +178,7 @@ function getBypassFromExternalAuth (username: string, externalAuthToken: string)
174 bypass: true, 178 bypass: true,
175 pluginName: npmName, 179 pluginName: npmName,
176 authName, 180 authName,
181 userUpdater: obj.userUpdater,
177 user 182 user
178 } 183 }
179} 184}
@@ -194,6 +199,11 @@ function isAuthResultValid (npmName: string, authName: string, result: RegisterS
194 if (result.videoQuota && !isUserVideoQuotaValid(result.videoQuota + '')) return returnError('videoQuota') 199 if (result.videoQuota && !isUserVideoQuotaValid(result.videoQuota + '')) return returnError('videoQuota')
195 if (result.videoQuotaDaily && !isUserVideoQuotaDailyValid(result.videoQuotaDaily + '')) return returnError('videoQuotaDaily') 200 if (result.videoQuotaDaily && !isUserVideoQuotaDailyValid(result.videoQuotaDaily + '')) return returnError('videoQuotaDaily')
196 201
202 if (result.userUpdater && typeof result.userUpdater !== 'function') {
203 logger.error('Auth method %s of plugin %s did not provide a valid user updater function.', authName, npmName)
204 return false
205 }
206
197 return true 207 return true
198} 208}
199 209
diff --git a/server/lib/auth/oauth-model.ts b/server/lib/auth/oauth-model.ts
index 603cc0f5f..43909284f 100644
--- a/server/lib/auth/oauth-model.ts
+++ b/server/lib/auth/oauth-model.ts
@@ -1,10 +1,13 @@
1import express from 'express' 1import express from 'express'
2import { AccessDeniedError } from '@node-oauth/oauth2-server' 2import { AccessDeniedError } from '@node-oauth/oauth2-server'
3import { PluginManager } from '@server/lib/plugins/plugin-manager' 3import { PluginManager } from '@server/lib/plugins/plugin-manager'
4import { AccountModel } from '@server/models/account/account'
5import { AuthenticatedResultUpdaterFieldName, RegisterServerAuthenticatedResult } from '@server/types'
4import { MOAuthClient } from '@server/types/models' 6import { MOAuthClient } from '@server/types/models'
5import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' 7import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token'
6import { MUser } from '@server/types/models/user/user' 8import { MUser, MUserDefault } from '@server/types/models/user/user'
7import { pick } from '@shared/core-utils' 9import { pick } from '@shared/core-utils'
10import { AttributesOnly } from '@shared/typescript-utils'
8import { logger } from '../../helpers/logger' 11import { logger } from '../../helpers/logger'
9import { CONFIG } from '../../initializers/config' 12import { CONFIG } from '../../initializers/config'
10import { OAuthClientModel } from '../../models/oauth/oauth-client' 13import { OAuthClientModel } from '../../models/oauth/oauth-client'
@@ -27,6 +30,7 @@ export type BypassLogin = {
27 pluginName: string 30 pluginName: string
28 authName?: string 31 authName?: string
29 user: ExternalUser 32 user: ExternalUser
33 userUpdater: RegisterServerAuthenticatedResult['userUpdater']
30} 34}
31 35
32async function getAccessToken (bearerToken: string) { 36async function getAccessToken (bearerToken: string) {
@@ -84,7 +88,9 @@ async function getUser (usernameOrEmail?: string, password?: string, bypassLogin
84 logger.info('Bypassing oauth login by plugin %s.', bypassLogin.pluginName) 88 logger.info('Bypassing oauth login by plugin %s.', bypassLogin.pluginName)
85 89
86 let user = await UserModel.loadByEmail(bypassLogin.user.email) 90 let user = await UserModel.loadByEmail(bypassLogin.user.email)
91
87 if (!user) user = await createUserFromExternal(bypassLogin.pluginName, bypassLogin.user) 92 if (!user) user = await createUserFromExternal(bypassLogin.pluginName, bypassLogin.user)
93 else user = await updateUserFromExternal(user, bypassLogin.user, bypassLogin.userUpdater)
88 94
89 // Cannot create a user 95 // Cannot create a user
90 if (!user) throw new AccessDeniedError('Cannot create such user: an actor with that name already exists.') 96 if (!user) throw new AccessDeniedError('Cannot create such user: an actor with that name already exists.')
@@ -234,6 +240,51 @@ async function createUserFromExternal (pluginAuth: string, userOptions: External
234 return user 240 return user
235} 241}
236 242
243async function updateUserFromExternal (
244 user: MUserDefault,
245 userOptions: ExternalUser,
246 userUpdater: RegisterServerAuthenticatedResult['userUpdater']
247) {
248 if (!userUpdater) return user
249
250 {
251 type UserAttributeKeys = keyof AttributesOnly<UserModel>
252 const mappingKeys: { [ id in UserAttributeKeys ]?: AuthenticatedResultUpdaterFieldName } = {
253 role: 'role',
254 adminFlags: 'adminFlags',
255 videoQuota: 'videoQuota',
256 videoQuotaDaily: 'videoQuotaDaily'
257 }
258
259 for (const modelKey of Object.keys(mappingKeys)) {
260 const pluginOptionKey = mappingKeys[modelKey]
261
262 const newValue = userUpdater({ fieldName: pluginOptionKey, currentValue: user[modelKey], newValue: userOptions[pluginOptionKey] })
263 user.set(modelKey, newValue)
264 }
265 }
266
267 {
268 type AccountAttributeKeys = keyof Partial<AttributesOnly<AccountModel>>
269 const mappingKeys: { [ id in AccountAttributeKeys ]?: AuthenticatedResultUpdaterFieldName } = {
270 name: 'displayName'
271 }
272
273 for (const modelKey of Object.keys(mappingKeys)) {
274 const optionKey = mappingKeys[modelKey]
275
276 const newValue = userUpdater({ fieldName: optionKey, currentValue: user.Account[modelKey], newValue: userOptions[optionKey] })
277 user.Account.set(modelKey, newValue)
278 }
279 }
280
281 logger.debug('Updated user %s with plugin userUpdated function.', user.email, { user, userOptions })
282
283 user.Account = await user.Account.save()
284
285 return user.save()
286}
287
237function checkUserValidityOrThrow (user: MUser) { 288function checkUserValidityOrThrow (user: MUser) {
238 if (user.blocked) throw new AccessDeniedError('User is blocked.') 289 if (user.blocked) throw new AccessDeniedError('User is blocked.')
239} 290}
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts
index 9c4e6d078..9b42955ef 100644
--- a/server/models/video/video-file.ts
+++ b/server/models/video/video-file.ts
@@ -439,7 +439,7 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel>
439 if (!element) return videoFile.save({ transaction }) 439 if (!element) return videoFile.save({ transaction })
440 440
441 for (const k of Object.keys(videoFile.toJSON())) { 441 for (const k of Object.keys(videoFile.toJSON())) {
442 element[k] = videoFile[k] 442 element.set(k, videoFile[k])
443 } 443 }
444 444
445 return element.save({ transaction }) 445 return element.save({ transaction })
diff --git a/server/tests/fixtures/peertube-plugin-test-external-auth-one/main.js b/server/tests/fixtures/peertube-plugin-test-external-auth-one/main.js
index cdbaf11ac..58bc27661 100644
--- a/server/tests/fixtures/peertube-plugin-test-external-auth-one/main.js
+++ b/server/tests/fixtures/peertube-plugin-test-external-auth-one/main.js
@@ -36,7 +36,14 @@ async function register ({
36 displayName: 'Kefka Palazzo', 36 displayName: 'Kefka Palazzo',
37 adminFlags: 1, 37 adminFlags: 1,
38 videoQuota: 42000, 38 videoQuota: 42000,
39 videoQuotaDaily: 42100 39 videoQuotaDaily: 42100,
40
41 // Always use new value except for videoQuotaDaily field
42 userUpdater: ({ fieldName, currentValue, newValue }) => {
43 if (fieldName === 'videoQuotaDaily') return currentValue
44
45 return newValue
46 }
40 }) 47 })
41 }, 48 },
42 hookTokenValidity: (options) => { 49 hookTokenValidity: (options) => {
diff --git a/server/tests/fixtures/peertube-plugin-test-id-pass-auth-two/main.js b/server/tests/fixtures/peertube-plugin-test-id-pass-auth-two/main.js
index ceab7b60d..fad5abf60 100644
--- a/server/tests/fixtures/peertube-plugin-test-id-pass-auth-two/main.js
+++ b/server/tests/fixtures/peertube-plugin-test-id-pass-auth-two/main.js
@@ -33,7 +33,18 @@ async function register ({
33 if (body.id === 'laguna' && body.password === 'laguna password') { 33 if (body.id === 'laguna' && body.password === 'laguna password') {
34 return Promise.resolve({ 34 return Promise.resolve({
35 username: 'laguna', 35 username: 'laguna',
36 email: 'laguna@example.com' 36 email: 'laguna@example.com',
37 displayName: 'Laguna Loire',
38 adminFlags: 1,
39 videoQuota: 42000,
40 videoQuotaDaily: 42100,
41
42 // Always use new value except for videoQuotaDaily field
43 userUpdater: ({ fieldName, currentValue, newValue }) => {
44 if (fieldName === 'videoQuotaDaily') return currentValue
45
46 return newValue
47 }
37 }) 48 })
38 } 49 }
39 50
diff --git a/server/tests/plugins/external-auth.ts b/server/tests/plugins/external-auth.ts
index ee78ae5aa..e600f958f 100644
--- a/server/tests/plugins/external-auth.ts
+++ b/server/tests/plugins/external-auth.ts
@@ -51,6 +51,7 @@ describe('Test external auth plugins', function () {
51 51
52 let kefkaAccessToken: string 52 let kefkaAccessToken: string
53 let kefkaRefreshToken: string 53 let kefkaRefreshToken: string
54 let kefkaId: number
54 55
55 let externalAuthToken: string 56 let externalAuthToken: string
56 57
@@ -184,6 +185,8 @@ describe('Test external auth plugins', function () {
184 expect(body.adminFlags).to.equal(UserAdminFlag.BYPASS_VIDEO_AUTO_BLACKLIST) 185 expect(body.adminFlags).to.equal(UserAdminFlag.BYPASS_VIDEO_AUTO_BLACKLIST)
185 expect(body.videoQuota).to.equal(42000) 186 expect(body.videoQuota).to.equal(42000)
186 expect(body.videoQuotaDaily).to.equal(42100) 187 expect(body.videoQuotaDaily).to.equal(42100)
188
189 kefkaId = body.id
187 } 190 }
188 }) 191 })
189 192
@@ -246,6 +249,37 @@ describe('Test external auth plugins', function () {
246 expect(body.role.id).to.equal(UserRole.USER) 249 expect(body.role.id).to.equal(UserRole.USER)
247 }) 250 })
248 251
252 it('Should login Kefka and update the profile', async function () {
253 {
254 await server.users.update({ userId: kefkaId, videoQuota: 43000, videoQuotaDaily: 43100 })
255 await server.users.updateMe({ token: kefkaAccessToken, displayName: 'kefka updated' })
256
257 const body = await server.users.getMyInfo({ token: kefkaAccessToken })
258 expect(body.username).to.equal('kefka')
259 expect(body.account.displayName).to.equal('kefka updated')
260 expect(body.videoQuota).to.equal(43000)
261 expect(body.videoQuotaDaily).to.equal(43100)
262 }
263
264 {
265 const res = await loginExternal({
266 server,
267 npmName: 'test-external-auth-one',
268 authName: 'external-auth-2',
269 username: 'kefka'
270 })
271
272 kefkaAccessToken = res.access_token
273 kefkaRefreshToken = res.refresh_token
274
275 const body = await server.users.getMyInfo({ token: kefkaAccessToken })
276 expect(body.username).to.equal('kefka')
277 expect(body.account.displayName).to.equal('Kefka Palazzo')
278 expect(body.videoQuota).to.equal(42000)
279 expect(body.videoQuotaDaily).to.equal(43100)
280 }
281 })
282
249 it('Should not update an external auth email', async function () { 283 it('Should not update an external auth email', async function () {
250 await server.users.updateMe({ 284 await server.users.updateMe({
251 token: cyanAccessToken, 285 token: cyanAccessToken,
diff --git a/server/tests/plugins/id-and-pass-auth.ts b/server/tests/plugins/id-and-pass-auth.ts
index fc24a5656..10155c28b 100644
--- a/server/tests/plugins/id-and-pass-auth.ts
+++ b/server/tests/plugins/id-and-pass-auth.ts
@@ -13,6 +13,7 @@ describe('Test id and pass auth plugins', function () {
13 13
14 let lagunaAccessToken: string 14 let lagunaAccessToken: string
15 let lagunaRefreshToken: string 15 let lagunaRefreshToken: string
16 let lagunaId: number
16 17
17 before(async function () { 18 before(async function () {
18 this.timeout(30000) 19 this.timeout(30000)
@@ -78,8 +79,10 @@ describe('Test id and pass auth plugins', function () {
78 const body = await server.users.getMyInfo({ token: lagunaAccessToken }) 79 const body = await server.users.getMyInfo({ token: lagunaAccessToken })
79 80
80 expect(body.username).to.equal('laguna') 81 expect(body.username).to.equal('laguna')
81 expect(body.account.displayName).to.equal('laguna') 82 expect(body.account.displayName).to.equal('Laguna Loire')
82 expect(body.role.id).to.equal(UserRole.USER) 83 expect(body.role.id).to.equal(UserRole.USER)
84
85 lagunaId = body.id
83 } 86 }
84 }) 87 })
85 88
@@ -132,6 +135,33 @@ describe('Test id and pass auth plugins', function () {
132 expect(body.role.id).to.equal(UserRole.MODERATOR) 135 expect(body.role.id).to.equal(UserRole.MODERATOR)
133 }) 136 })
134 137
138 it('Should login Laguna and update the profile', async function () {
139 {
140 await server.users.update({ userId: lagunaId, videoQuota: 43000, videoQuotaDaily: 43100 })
141 await server.users.updateMe({ token: lagunaAccessToken, displayName: 'laguna updated' })
142
143 const body = await server.users.getMyInfo({ token: lagunaAccessToken })
144 expect(body.username).to.equal('laguna')
145 expect(body.account.displayName).to.equal('laguna updated')
146 expect(body.videoQuota).to.equal(43000)
147 expect(body.videoQuotaDaily).to.equal(43100)
148 }
149
150 {
151 const body = await server.login.login({ user: { username: 'laguna', password: 'laguna password' } })
152 lagunaAccessToken = body.access_token
153 lagunaRefreshToken = body.refresh_token
154 }
155
156 {
157 const body = await server.users.getMyInfo({ token: lagunaAccessToken })
158 expect(body.username).to.equal('laguna')
159 expect(body.account.displayName).to.equal('Laguna Loire')
160 expect(body.videoQuota).to.equal(42000)
161 expect(body.videoQuotaDaily).to.equal(43100)
162 }
163 })
164
135 it('Should reject token of laguna by the plugin hook', async function () { 165 it('Should reject token of laguna by the plugin hook', async function () {
136 this.timeout(10000) 166 this.timeout(10000)
137 167
@@ -147,7 +177,7 @@ describe('Test id and pass auth plugins', function () {
147 await server.servers.waitUntilLog('valid username') 177 await server.servers.waitUntilLog('valid username')
148 178
149 await command.login({ user: { username: 'kiros', password: 'kiros password' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) 179 await command.login({ user: { username: 'kiros', password: 'kiros password' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
150 await server.servers.waitUntilLog('valid display name') 180 await server.servers.waitUntilLog('valid displayName')
151 181
152 await command.login({ user: { username: 'raine', password: 'raine password' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) 182 await command.login({ user: { username: 'raine', password: 'raine password' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
153 await server.servers.waitUntilLog('valid role') 183 await server.servers.waitUntilLog('valid role')
diff --git a/server/types/express.d.ts b/server/types/express.d.ts
index 99244d2a0..6fea4dac2 100644
--- a/server/types/express.d.ts
+++ b/server/types/express.d.ts
@@ -1,4 +1,3 @@
1
2import { OutgoingHttpHeaders } from 'http' 1import { OutgoingHttpHeaders } from 'http'
3import { RegisterServerAuthExternalOptions } from '@server/types' 2import { RegisterServerAuthExternalOptions } from '@server/types'
4import { 3import {
diff --git a/server/types/lib.d.ts b/server/types/lib.d.ts
new file mode 100644
index 000000000..c901e2032
--- /dev/null
+++ b/server/types/lib.d.ts
@@ -0,0 +1,12 @@
1type ObjectKeys<T> =
2 T extends object
3 ? `${Exclude<keyof T, symbol>}`[]
4 : T extends number
5 ? []
6 : T extends any | string
7 ? string[]
8 : never
9
10interface ObjectConstructor {
11 keys<T> (o: T): ObjectKeys<T>
12}
diff --git a/server/types/plugins/register-server-auth.model.ts b/server/types/plugins/register-server-auth.model.ts
index a17fc4b0f..e10968c20 100644
--- a/server/types/plugins/register-server-auth.model.ts
+++ b/server/types/plugins/register-server-auth.model.ts
@@ -4,15 +4,29 @@ import { MOAuthToken, MUser } from '../models'
4 4
5export type RegisterServerAuthOptions = RegisterServerAuthPassOptions | RegisterServerAuthExternalOptions 5export type RegisterServerAuthOptions = RegisterServerAuthPassOptions | RegisterServerAuthExternalOptions
6 6
7export type AuthenticatedResultUpdaterFieldName = 'displayName' | 'role' | 'adminFlags' | 'videoQuota' | 'videoQuotaDaily'
8
7export interface RegisterServerAuthenticatedResult { 9export interface RegisterServerAuthenticatedResult {
10 // Update the user profile if it already exists
11 // Default behaviour is no update
12 // Introduced in PeerTube >= 5.1
13 userUpdater?: <T> (options: {
14 fieldName: AuthenticatedResultUpdaterFieldName
15 currentValue: T
16 newValue: T
17 }) => T
18
8 username: string 19 username: string
9 email: string 20 email: string
10 role?: UserRole 21 role?: UserRole
11 displayName?: string 22 displayName?: string
12 23
24 // PeerTube >= 5.1
13 adminFlags?: UserAdminFlag 25 adminFlags?: UserAdminFlag
14 26
27 // PeerTube >= 5.1
15 videoQuota?: number 28 videoQuota?: number
29 // PeerTube >= 5.1
16 videoQuotaDaily?: number 30 videoQuotaDaily?: number
17} 31}
18 32
diff --git a/support/doc/plugins/guide.md b/support/doc/plugins/guide.md
index a1131ced5..9ddab3ece 100644
--- a/support/doc/plugins/guide.md
+++ b/support/doc/plugins/guide.md
@@ -433,7 +433,27 @@ function register (...) {
433 username: 'user' 433 username: 'user'
434 email: 'user@example.com' 434 email: 'user@example.com'
435 role: 2 435 role: 2
436 displayName: 'User display name' 436 displayName: 'User display name',
437
438 // Custom admin flags (bypass video auto moderation etc.)
439 // https://github.com/Chocobozzz/PeerTube/blob/develop/shared/models/users/user-flag.model.ts
440 // PeerTube >= 5.1
441 adminFlags: 0,
442 // Quota in bytes
443 // PeerTube >= 5.1
444 videoQuota: 1024 * 1024 * 1024, // 1GB
445 // PeerTube >= 5.1
446 videoQuotaDaily: -1, // Unlimited
447
448 // Update the user profile if it already exists
449 // Default behaviour is no update
450 // Introduced in PeerTube >= 5.1
451 userUpdater: ({ fieldName, currentValue, newValue }) => {
452 // Always use new value except for videoQuotaDaily field
453 if (fieldName === 'videoQuotaDaily') return currentValue
454
455 return newValue
456 }
437 }) 457 })
438 }) 458 })
439 459