diff options
-rw-r--r-- | server/helpers/custom-validators/video-captions.ts | 9 | ||||
-rw-r--r-- | server/helpers/custom-validators/video-imports.ts | 9 | ||||
-rw-r--r-- | server/initializers/checker-after-init.ts | 3 | ||||
-rw-r--r-- | server/initializers/installer.ts | 6 | ||||
-rw-r--r-- | server/lib/auth/external-auth.ts | 18 | ||||
-rw-r--r-- | server/lib/auth/oauth-model.ts | 53 | ||||
-rw-r--r-- | server/models/video/video-file.ts | 2 | ||||
-rw-r--r-- | server/tests/fixtures/peertube-plugin-test-external-auth-one/main.js | 9 | ||||
-rw-r--r-- | server/tests/fixtures/peertube-plugin-test-id-pass-auth-two/main.js | 13 | ||||
-rw-r--r-- | server/tests/plugins/external-auth.ts | 34 | ||||
-rw-r--r-- | server/tests/plugins/id-and-pass-auth.ts | 34 | ||||
-rw-r--r-- | server/types/express.d.ts | 1 | ||||
-rw-r--r-- | server/types/lib.d.ts | 12 | ||||
-rw-r--r-- | server/types/plugins/register-server-auth.model.ts | 14 | ||||
-rw-r--r-- | support/doc/plugins/guide.md | 22 |
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 | ||
11 | const videoCaptionTypesRegex = Object.keys(MIMETYPES.VIDEO_CAPTIONS.MIMETYPE_EXT) | 11 | // MacOS sends application/octet-stream |
12 | .concat([ 'application/octet-stream' ]) // MacOS sends application/octet-stream | 12 | const videoCaptionTypesRegex = [ ...Object.keys(MIMETYPES.VIDEO_CAPTIONS.MIMETYPE_EXT), 'application/octet-stream' ] |
13 | .map(m => `(${m})`) | 13 | .map(m => `(${m})`) |
14 | .join('|') | 14 | .join('|') |
15 | |||
15 | function isVideoCaptionFile (files: UploadFilesForCheck, field: string) { | 16 | function 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 | ||
25 | const videoTorrentImportRegex = Object.keys(MIMETYPES.TORRENT.MIMETYPE_EXT) | 25 | // MacOS sends application/octet-stream |
26 | .concat([ 'application/octet-stream' ]) // MacOS sends application/octet-stream | 26 | const videoTorrentImportRegex = [ ...Object.keys(MIMETYPES.TORRENT.MIMETYPE_EXT), 'application/octet-stream' ] |
27 | .map(m => `(${m})`) | 27 | .map(m => `(${m})`) |
28 | .join('|') | 28 | .join('|') |
29 | |||
29 | function isVideoImportTorrentFile (files: UploadFilesForCheck) { | 30 | function 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 () { | |||
174 | function checkStorageConfig () { | 174 | function 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' |
21 | import { UserAdminFlag, UserRole } from '@shared/models' | 21 | import { UserAdminFlag, UserRole } from '@shared/models' |
22 | import { BypassLogin } from './oauth-model' | ||
22 | 23 | ||
23 | export type ExternalUser = | 24 | export 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 = | |||
28 | const authBypassTokens = new Map<string, { | 29 | const 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 | ||
88 | async function getBypassFromPasswordGrant (username: string, password: string) { | 91 | async 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 | ||
153 | function getBypassFromExternalAuth (username: string, externalAuthToken: string) { | 157 | function 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 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import { AccessDeniedError } from '@node-oauth/oauth2-server' | 2 | import { AccessDeniedError } from '@node-oauth/oauth2-server' |
3 | import { PluginManager } from '@server/lib/plugins/plugin-manager' | 3 | import { PluginManager } from '@server/lib/plugins/plugin-manager' |
4 | import { AccountModel } from '@server/models/account/account' | ||
5 | import { AuthenticatedResultUpdaterFieldName, RegisterServerAuthenticatedResult } from '@server/types' | ||
4 | import { MOAuthClient } from '@server/types/models' | 6 | import { MOAuthClient } from '@server/types/models' |
5 | import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' | 7 | import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' |
6 | import { MUser } from '@server/types/models/user/user' | 8 | import { MUser, MUserDefault } from '@server/types/models/user/user' |
7 | import { pick } from '@shared/core-utils' | 9 | import { pick } from '@shared/core-utils' |
10 | import { AttributesOnly } from '@shared/typescript-utils' | ||
8 | import { logger } from '../../helpers/logger' | 11 | import { logger } from '../../helpers/logger' |
9 | import { CONFIG } from '../../initializers/config' | 12 | import { CONFIG } from '../../initializers/config' |
10 | import { OAuthClientModel } from '../../models/oauth/oauth-client' | 13 | import { 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 | ||
32 | async function getAccessToken (bearerToken: string) { | 36 | async 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 | ||
243 | async 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 | |||
237 | function checkUserValidityOrThrow (user: MUser) { | 288 | function 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 | |||
2 | import { OutgoingHttpHeaders } from 'http' | 1 | import { OutgoingHttpHeaders } from 'http' |
3 | import { RegisterServerAuthExternalOptions } from '@server/types' | 2 | import { RegisterServerAuthExternalOptions } from '@server/types' |
4 | import { | 3 | import { |
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 @@ | |||
1 | type 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 | |||
10 | interface 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 | ||
5 | export type RegisterServerAuthOptions = RegisterServerAuthPassOptions | RegisterServerAuthExternalOptions | 5 | export type RegisterServerAuthOptions = RegisterServerAuthPassOptions | RegisterServerAuthExternalOptions |
6 | 6 | ||
7 | export type AuthenticatedResultUpdaterFieldName = 'displayName' | 'role' | 'adminFlags' | 'videoQuota' | 'videoQuotaDaily' | ||
8 | |||
7 | export interface RegisterServerAuthenticatedResult { | 9 | export 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 | ||