diff options
Diffstat (limited to 'server/lib')
26 files changed, 474 insertions, 151 deletions
diff --git a/server/lib/auth/external-auth.ts b/server/lib/auth/external-auth.ts index 053112801..bc5b74257 100644 --- a/server/lib/auth/external-auth.ts +++ b/server/lib/auth/external-auth.ts | |||
@@ -1,26 +1,35 @@ | |||
1 | 1 | ||
2 | import { isUserDisplayNameValid, isUserRoleValid, isUserUsernameValid } from '@server/helpers/custom-validators/users' | 2 | import { |
3 | isUserAdminFlagsValid, | ||
4 | isUserDisplayNameValid, | ||
5 | isUserRoleValid, | ||
6 | isUserUsernameValid, | ||
7 | isUserVideoQuotaDailyValid, | ||
8 | isUserVideoQuotaValid | ||
9 | } from '@server/helpers/custom-validators/users' | ||
3 | import { logger } from '@server/helpers/logger' | 10 | import { logger } from '@server/helpers/logger' |
4 | import { generateRandomString } from '@server/helpers/utils' | 11 | import { generateRandomString } from '@server/helpers/utils' |
5 | import { PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME } from '@server/initializers/constants' | 12 | import { PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME } from '@server/initializers/constants' |
6 | import { PluginManager } from '@server/lib/plugins/plugin-manager' | 13 | import { PluginManager } from '@server/lib/plugins/plugin-manager' |
7 | import { OAuthTokenModel } from '@server/models/oauth/oauth-token' | 14 | import { OAuthTokenModel } from '@server/models/oauth/oauth-token' |
15 | import { MUser } from '@server/types/models' | ||
8 | import { | 16 | import { |
9 | RegisterServerAuthenticatedResult, | 17 | RegisterServerAuthenticatedResult, |
10 | RegisterServerAuthPassOptions, | 18 | RegisterServerAuthPassOptions, |
11 | RegisterServerExternalAuthenticatedResult | 19 | RegisterServerExternalAuthenticatedResult |
12 | } from '@server/types/plugins/register-server-auth.model' | 20 | } from '@server/types/plugins/register-server-auth.model' |
13 | import { UserRole } from '@shared/models' | 21 | import { UserAdminFlag, UserRole } from '@shared/models' |
22 | import { BypassLogin } from './oauth-model' | ||
23 | |||
24 | export type ExternalUser = | ||
25 | Pick<MUser, 'username' | 'email' | 'role' | 'adminFlags' | 'videoQuotaDaily' | 'videoQuota'> & | ||
26 | { displayName: string } | ||
14 | 27 | ||
15 | // Token is the key, expiration date is the value | 28 | // Token is the key, expiration date is the value |
16 | const authBypassTokens = new Map<string, { | 29 | const authBypassTokens = new Map<string, { |
17 | expires: Date | 30 | expires: Date |
18 | user: { | 31 | user: ExternalUser |
19 | username: string | 32 | userUpdater: RegisterServerAuthenticatedResult['userUpdater'] |
20 | email: string | ||
21 | displayName: string | ||
22 | role: UserRole | ||
23 | } | ||
24 | authName: string | 33 | authName: string |
25 | npmName: string | 34 | npmName: string |
26 | }>() | 35 | }>() |
@@ -56,7 +65,8 @@ async function onExternalUserAuthenticated (options: { | |||
56 | expires, | 65 | expires, |
57 | user, | 66 | user, |
58 | npmName, | 67 | npmName, |
59 | authName | 68 | authName, |
69 | userUpdater: authResult.userUpdater | ||
60 | }) | 70 | }) |
61 | 71 | ||
62 | // Cleanup expired tokens | 72 | // Cleanup expired tokens |
@@ -78,7 +88,7 @@ async function getAuthNameFromRefreshGrant (refreshToken?: string) { | |||
78 | return tokenModel?.authName | 88 | return tokenModel?.authName |
79 | } | 89 | } |
80 | 90 | ||
81 | async function getBypassFromPasswordGrant (username: string, password: string) { | 91 | async function getBypassFromPasswordGrant (username: string, password: string): Promise<BypassLogin> { |
82 | const plugins = PluginManager.Instance.getIdAndPassAuths() | 92 | const plugins = PluginManager.Instance.getIdAndPassAuths() |
83 | const pluginAuths: { npmName?: string, registerAuthOptions: RegisterServerAuthPassOptions }[] = [] | 93 | const pluginAuths: { npmName?: string, registerAuthOptions: RegisterServerAuthPassOptions }[] = [] |
84 | 94 | ||
@@ -133,7 +143,8 @@ async function getBypassFromPasswordGrant (username: string, password: string) { | |||
133 | bypass: true, | 143 | bypass: true, |
134 | pluginName: pluginAuth.npmName, | 144 | pluginName: pluginAuth.npmName, |
135 | authName: authOptions.authName, | 145 | authName: authOptions.authName, |
136 | user: buildUserResult(loginResult) | 146 | user: buildUserResult(loginResult), |
147 | userUpdater: loginResult.userUpdater | ||
137 | } | 148 | } |
138 | } catch (err) { | 149 | } catch (err) { |
139 | 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 }) |
@@ -143,7 +154,7 @@ async function getBypassFromPasswordGrant (username: string, password: string) { | |||
143 | return undefined | 154 | return undefined |
144 | } | 155 | } |
145 | 156 | ||
146 | function getBypassFromExternalAuth (username: string, externalAuthToken: string) { | 157 | function getBypassFromExternalAuth (username: string, externalAuthToken: string): BypassLogin { |
147 | const obj = authBypassTokens.get(externalAuthToken) | 158 | const obj = authBypassTokens.get(externalAuthToken) |
148 | 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') |
149 | 160 | ||
@@ -167,33 +178,29 @@ function getBypassFromExternalAuth (username: string, externalAuthToken: string) | |||
167 | bypass: true, | 178 | bypass: true, |
168 | pluginName: npmName, | 179 | pluginName: npmName, |
169 | authName, | 180 | authName, |
181 | userUpdater: obj.userUpdater, | ||
170 | user | 182 | user |
171 | } | 183 | } |
172 | } | 184 | } |
173 | 185 | ||
174 | function isAuthResultValid (npmName: string, authName: string, result: RegisterServerAuthenticatedResult) { | 186 | function isAuthResultValid (npmName: string, authName: string, result: RegisterServerAuthenticatedResult) { |
175 | if (!isUserUsernameValid(result.username)) { | 187 | const returnError = (field: string) => { |
176 | logger.error('Auth method %s of plugin %s did not provide a valid username.', authName, npmName, { username: result.username }) | 188 | logger.error('Auth method %s of plugin %s did not provide a valid %s.', authName, npmName, field, { [field]: result[field] }) |
177 | return false | 189 | return false |
178 | } | 190 | } |
179 | 191 | ||
180 | if (!result.email) { | 192 | if (!isUserUsernameValid(result.username)) return returnError('username') |
181 | logger.error('Auth method %s of plugin %s did not provide a valid email.', authName, npmName, { email: result.email }) | 193 | if (!result.email) return returnError('email') |
182 | return false | ||
183 | } | ||
184 | 194 | ||
185 | // role is optional | 195 | // Following fields are optional |
186 | if (result.role && !isUserRoleValid(result.role)) { | 196 | if (result.role && !isUserRoleValid(result.role)) return returnError('role') |
187 | logger.error('Auth method %s of plugin %s did not provide a valid role.', authName, npmName, { role: result.role }) | 197 | if (result.displayName && !isUserDisplayNameValid(result.displayName)) return returnError('displayName') |
188 | return false | 198 | if (result.adminFlags && !isUserAdminFlagsValid(result.adminFlags)) return returnError('adminFlags') |
189 | } | 199 | if (result.videoQuota && !isUserVideoQuotaValid(result.videoQuota + '')) return returnError('videoQuota') |
200 | if (result.videoQuotaDaily && !isUserVideoQuotaDailyValid(result.videoQuotaDaily + '')) return returnError('videoQuotaDaily') | ||
190 | 201 | ||
191 | // display name is optional | 202 | if (result.userUpdater && typeof result.userUpdater !== 'function') { |
192 | if (result.displayName && !isUserDisplayNameValid(result.displayName)) { | 203 | logger.error('Auth method %s of plugin %s did not provide a valid user updater function.', authName, npmName) |
193 | logger.error( | ||
194 | 'Auth method %s of plugin %s did not provide a valid display name.', | ||
195 | authName, npmName, { displayName: result.displayName } | ||
196 | ) | ||
197 | return false | 204 | return false |
198 | } | 205 | } |
199 | 206 | ||
@@ -205,7 +212,12 @@ function buildUserResult (pluginResult: RegisterServerAuthenticatedResult) { | |||
205 | username: pluginResult.username, | 212 | username: pluginResult.username, |
206 | email: pluginResult.email, | 213 | email: pluginResult.email, |
207 | role: pluginResult.role ?? UserRole.USER, | 214 | role: pluginResult.role ?? UserRole.USER, |
208 | displayName: pluginResult.displayName || pluginResult.username | 215 | displayName: pluginResult.displayName || pluginResult.username, |
216 | |||
217 | adminFlags: pluginResult.adminFlags ?? UserAdminFlag.NONE, | ||
218 | |||
219 | videoQuota: pluginResult.videoQuota, | ||
220 | videoQuotaDaily: pluginResult.videoQuotaDaily | ||
209 | } | 221 | } |
210 | } | 222 | } |
211 | 223 | ||
diff --git a/server/lib/auth/oauth-model.ts b/server/lib/auth/oauth-model.ts index 322b69e3a..43909284f 100644 --- a/server/lib/auth/oauth-model.ts +++ b/server/lib/auth/oauth-model.ts | |||
@@ -1,11 +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' |
8 | import { UserRole } from '@shared/models/users/user-role' | 10 | import { AttributesOnly } from '@shared/typescript-utils' |
9 | import { logger } from '../../helpers/logger' | 11 | import { logger } from '../../helpers/logger' |
10 | import { CONFIG } from '../../initializers/config' | 12 | import { CONFIG } from '../../initializers/config' |
11 | import { OAuthClientModel } from '../../models/oauth/oauth-client' | 13 | import { OAuthClientModel } from '../../models/oauth/oauth-client' |
@@ -13,6 +15,7 @@ import { OAuthTokenModel } from '../../models/oauth/oauth-token' | |||
13 | import { UserModel } from '../../models/user/user' | 15 | import { UserModel } from '../../models/user/user' |
14 | import { findAvailableLocalActorName } from '../local-actor' | 16 | import { findAvailableLocalActorName } from '../local-actor' |
15 | import { buildUser, createUserAccountAndChannelAndPlaylist } from '../user' | 17 | import { buildUser, createUserAccountAndChannelAndPlaylist } from '../user' |
18 | import { ExternalUser } from './external-auth' | ||
16 | import { TokensCache } from './tokens-cache' | 19 | import { TokensCache } from './tokens-cache' |
17 | 20 | ||
18 | type TokenInfo = { | 21 | type TokenInfo = { |
@@ -26,12 +29,8 @@ export type BypassLogin = { | |||
26 | bypass: boolean | 29 | bypass: boolean |
27 | pluginName: string | 30 | pluginName: string |
28 | authName?: string | 31 | authName?: string |
29 | user: { | 32 | user: ExternalUser |
30 | username: string | 33 | userUpdater: RegisterServerAuthenticatedResult['userUpdater'] |
31 | email: string | ||
32 | displayName: string | ||
33 | role: UserRole | ||
34 | } | ||
35 | } | 34 | } |
36 | 35 | ||
37 | async function getAccessToken (bearerToken: string) { | 36 | async function getAccessToken (bearerToken: string) { |
@@ -89,7 +88,9 @@ async function getUser (usernameOrEmail?: string, password?: string, bypassLogin | |||
89 | logger.info('Bypassing oauth login by plugin %s.', bypassLogin.pluginName) | 88 | logger.info('Bypassing oauth login by plugin %s.', bypassLogin.pluginName) |
90 | 89 | ||
91 | let user = await UserModel.loadByEmail(bypassLogin.user.email) | 90 | let user = await UserModel.loadByEmail(bypassLogin.user.email) |
91 | |||
92 | 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) | ||
93 | 94 | ||
94 | // Cannot create a user | 95 | // Cannot create a user |
95 | 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.') |
@@ -219,16 +220,11 @@ export { | |||
219 | 220 | ||
220 | // --------------------------------------------------------------------------- | 221 | // --------------------------------------------------------------------------- |
221 | 222 | ||
222 | async function createUserFromExternal (pluginAuth: string, options: { | 223 | async function createUserFromExternal (pluginAuth: string, userOptions: ExternalUser) { |
223 | username: string | 224 | const username = await findAvailableLocalActorName(userOptions.username) |
224 | email: string | ||
225 | role: UserRole | ||
226 | displayName: string | ||
227 | }) { | ||
228 | const username = await findAvailableLocalActorName(options.username) | ||
229 | 225 | ||
230 | const userToCreate = buildUser({ | 226 | const userToCreate = buildUser({ |
231 | ...pick(options, [ 'email', 'role' ]), | 227 | ...pick(userOptions, [ 'email', 'role', 'adminFlags', 'videoQuota', 'videoQuotaDaily' ]), |
232 | 228 | ||
233 | username, | 229 | username, |
234 | emailVerified: null, | 230 | emailVerified: null, |
@@ -238,12 +234,57 @@ async function createUserFromExternal (pluginAuth: string, options: { | |||
238 | 234 | ||
239 | const { user } = await createUserAccountAndChannelAndPlaylist({ | 235 | const { user } = await createUserAccountAndChannelAndPlaylist({ |
240 | userToCreate, | 236 | userToCreate, |
241 | userDisplayName: options.displayName | 237 | userDisplayName: userOptions.displayName |
242 | }) | 238 | }) |
243 | 239 | ||
244 | return user | 240 | return user |
245 | } | 241 | } |
246 | 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 | |||
247 | function checkUserValidityOrThrow (user: MUser) { | 288 | function checkUserValidityOrThrow (user: MUser) { |
248 | if (user.blocked) throw new AccessDeniedError('User is blocked.') | 289 | if (user.blocked) throw new AccessDeniedError('User is blocked.') |
249 | } | 290 | } |
diff --git a/server/lib/auth/oauth.ts b/server/lib/auth/oauth.ts index bc0d4301f..887c4f7c9 100644 --- a/server/lib/auth/oauth.ts +++ b/server/lib/auth/oauth.ts | |||
@@ -10,20 +10,32 @@ import OAuth2Server, { | |||
10 | } from '@node-oauth/oauth2-server' | 10 | } from '@node-oauth/oauth2-server' |
11 | import { randomBytesPromise } from '@server/helpers/core-utils' | 11 | import { randomBytesPromise } from '@server/helpers/core-utils' |
12 | import { isOTPValid } from '@server/helpers/otp' | 12 | import { isOTPValid } from '@server/helpers/otp' |
13 | import { CONFIG } from '@server/initializers/config' | ||
14 | import { UserRegistrationModel } from '@server/models/user/user-registration' | ||
13 | import { MOAuthClient } from '@server/types/models' | 15 | import { MOAuthClient } from '@server/types/models' |
14 | import { sha1 } from '@shared/extra-utils' | 16 | import { sha1 } from '@shared/extra-utils' |
15 | import { HttpStatusCode } from '@shared/models' | 17 | import { HttpStatusCode, ServerErrorCode, UserRegistrationState } from '@shared/models' |
16 | import { OAUTH_LIFETIME, OTP } from '../../initializers/constants' | 18 | import { OTP } from '../../initializers/constants' |
17 | import { BypassLogin, getClient, getRefreshToken, getUser, revokeToken, saveToken } from './oauth-model' | 19 | import { BypassLogin, getClient, getRefreshToken, getUser, revokeToken, saveToken } from './oauth-model' |
18 | 20 | ||
19 | class MissingTwoFactorError extends Error { | 21 | class MissingTwoFactorError extends Error { |
20 | code = HttpStatusCode.UNAUTHORIZED_401 | 22 | code = HttpStatusCode.UNAUTHORIZED_401 |
21 | name = 'missing_two_factor' | 23 | name = ServerErrorCode.MISSING_TWO_FACTOR |
22 | } | 24 | } |
23 | 25 | ||
24 | class InvalidTwoFactorError extends Error { | 26 | class InvalidTwoFactorError extends Error { |
25 | code = HttpStatusCode.BAD_REQUEST_400 | 27 | code = HttpStatusCode.BAD_REQUEST_400 |
26 | name = 'invalid_two_factor' | 28 | name = ServerErrorCode.INVALID_TWO_FACTOR |
29 | } | ||
30 | |||
31 | class RegistrationWaitingForApproval extends Error { | ||
32 | code = HttpStatusCode.BAD_REQUEST_400 | ||
33 | name = ServerErrorCode.ACCOUNT_WAITING_FOR_APPROVAL | ||
34 | } | ||
35 | |||
36 | class RegistrationApprovalRejected extends Error { | ||
37 | code = HttpStatusCode.BAD_REQUEST_400 | ||
38 | name = ServerErrorCode.ACCOUNT_APPROVAL_REJECTED | ||
27 | } | 39 | } |
28 | 40 | ||
29 | /** | 41 | /** |
@@ -32,8 +44,9 @@ class InvalidTwoFactorError extends Error { | |||
32 | * | 44 | * |
33 | */ | 45 | */ |
34 | const oAuthServer = new OAuth2Server({ | 46 | const oAuthServer = new OAuth2Server({ |
35 | accessTokenLifetime: OAUTH_LIFETIME.ACCESS_TOKEN, | 47 | // Wants seconds |
36 | refreshTokenLifetime: OAUTH_LIFETIME.REFRESH_TOKEN, | 48 | accessTokenLifetime: CONFIG.OAUTH2.TOKEN_LIFETIME.ACCESS_TOKEN / 1000, |
49 | refreshTokenLifetime: CONFIG.OAUTH2.TOKEN_LIFETIME.REFRESH_TOKEN / 1000, | ||
37 | 50 | ||
38 | // See https://github.com/oauthjs/node-oauth2-server/wiki/Model-specification for the model specifications | 51 | // See https://github.com/oauthjs/node-oauth2-server/wiki/Model-specification for the model specifications |
39 | model: require('./oauth-model') | 52 | model: require('./oauth-model') |
@@ -126,7 +139,17 @@ async function handlePasswordGrant (options: { | |||
126 | } | 139 | } |
127 | 140 | ||
128 | const user = await getUser(request.body.username, request.body.password, bypassLogin) | 141 | const user = await getUser(request.body.username, request.body.password, bypassLogin) |
129 | if (!user) throw new InvalidGrantError('Invalid grant: user credentials are invalid') | 142 | if (!user) { |
143 | const registration = await UserRegistrationModel.loadByEmailOrUsername(request.body.username) | ||
144 | |||
145 | if (registration?.state === UserRegistrationState.REJECTED) { | ||
146 | throw new RegistrationApprovalRejected('Registration approval for this account has been rejected') | ||
147 | } else if (registration?.state === UserRegistrationState.PENDING) { | ||
148 | throw new RegistrationWaitingForApproval('Registration for this account is awaiting approval') | ||
149 | } | ||
150 | |||
151 | throw new InvalidGrantError('Invalid grant: user credentials are invalid') | ||
152 | } | ||
130 | 153 | ||
131 | if (user.otpSecret) { | 154 | if (user.otpSecret) { |
132 | if (!request.headers[OTP.HEADER_NAME]) { | 155 | if (!request.headers[OTP.HEADER_NAME]) { |
@@ -182,10 +205,10 @@ function generateRandomToken () { | |||
182 | 205 | ||
183 | function getTokenExpiresAt (type: 'access' | 'refresh') { | 206 | function getTokenExpiresAt (type: 'access' | 'refresh') { |
184 | const lifetime = type === 'access' | 207 | const lifetime = type === 'access' |
185 | ? OAUTH_LIFETIME.ACCESS_TOKEN | 208 | ? CONFIG.OAUTH2.TOKEN_LIFETIME.ACCESS_TOKEN |
186 | : OAUTH_LIFETIME.REFRESH_TOKEN | 209 | : CONFIG.OAUTH2.TOKEN_LIFETIME.REFRESH_TOKEN |
187 | 210 | ||
188 | return new Date(Date.now() + lifetime * 1000) | 211 | return new Date(Date.now() + lifetime) |
189 | } | 212 | } |
190 | 213 | ||
191 | async function buildToken () { | 214 | async function buildToken () { |
diff --git a/server/lib/auth/tokens-cache.ts b/server/lib/auth/tokens-cache.ts index 410708a35..43efc7d02 100644 --- a/server/lib/auth/tokens-cache.ts +++ b/server/lib/auth/tokens-cache.ts | |||
@@ -36,8 +36,8 @@ export class TokensCache { | |||
36 | const token = this.userHavingToken.get(userId) | 36 | const token = this.userHavingToken.get(userId) |
37 | 37 | ||
38 | if (token !== undefined) { | 38 | if (token !== undefined) { |
39 | this.accessTokenCache.del(token) | 39 | this.accessTokenCache.delete(token) |
40 | this.userHavingToken.del(userId) | 40 | this.userHavingToken.delete(userId) |
41 | } | 41 | } |
42 | } | 42 | } |
43 | 43 | ||
@@ -45,8 +45,8 @@ export class TokensCache { | |||
45 | const tokenModel = this.accessTokenCache.get(token) | 45 | const tokenModel = this.accessTokenCache.get(token) |
46 | 46 | ||
47 | if (tokenModel !== undefined) { | 47 | if (tokenModel !== undefined) { |
48 | this.userHavingToken.del(tokenModel.userId) | 48 | this.userHavingToken.delete(tokenModel.userId) |
49 | this.accessTokenCache.del(token) | 49 | this.accessTokenCache.delete(token) |
50 | } | 50 | } |
51 | } | 51 | } |
52 | } | 52 | } |
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts index 39b662eb2..f5c3e4745 100644 --- a/server/lib/emailer.ts +++ b/server/lib/emailer.ts | |||
@@ -3,13 +3,13 @@ import { merge } from 'lodash' | |||
3 | import { createTransport, Transporter } from 'nodemailer' | 3 | import { createTransport, Transporter } from 'nodemailer' |
4 | import { join } from 'path' | 4 | import { join } from 'path' |
5 | import { arrayify, root } from '@shared/core-utils' | 5 | import { arrayify, root } from '@shared/core-utils' |
6 | import { EmailPayload } from '@shared/models' | 6 | import { EmailPayload, UserRegistrationState } from '@shared/models' |
7 | import { SendEmailDefaultOptions } from '../../shared/models/server/emailer.model' | 7 | import { SendEmailDefaultOptions } from '../../shared/models/server/emailer.model' |
8 | import { isTestOrDevInstance } from '../helpers/core-utils' | 8 | import { isTestOrDevInstance } from '../helpers/core-utils' |
9 | import { bunyanLogger, logger } from '../helpers/logger' | 9 | import { bunyanLogger, logger } from '../helpers/logger' |
10 | import { CONFIG, isEmailEnabled } from '../initializers/config' | 10 | import { CONFIG, isEmailEnabled } from '../initializers/config' |
11 | import { WEBSERVER } from '../initializers/constants' | 11 | import { WEBSERVER } from '../initializers/constants' |
12 | import { MUser } from '../types/models' | 12 | import { MRegistration, MUser } from '../types/models' |
13 | import { JobQueue } from './job-queue' | 13 | import { JobQueue } from './job-queue' |
14 | 14 | ||
15 | const Email = require('email-templates') | 15 | const Email = require('email-templates') |
@@ -62,7 +62,9 @@ class Emailer { | |||
62 | subject: 'Reset your account password', | 62 | subject: 'Reset your account password', |
63 | locals: { | 63 | locals: { |
64 | username, | 64 | username, |
65 | resetPasswordUrl | 65 | resetPasswordUrl, |
66 | |||
67 | hideNotificationPreferencesLink: true | ||
66 | } | 68 | } |
67 | } | 69 | } |
68 | 70 | ||
@@ -76,21 +78,33 @@ class Emailer { | |||
76 | subject: 'Create your account password', | 78 | subject: 'Create your account password', |
77 | locals: { | 79 | locals: { |
78 | username, | 80 | username, |
79 | createPasswordUrl | 81 | createPasswordUrl, |
82 | |||
83 | hideNotificationPreferencesLink: true | ||
80 | } | 84 | } |
81 | } | 85 | } |
82 | 86 | ||
83 | return JobQueue.Instance.createJobAsync({ type: 'email', payload: emailPayload }) | 87 | return JobQueue.Instance.createJobAsync({ type: 'email', payload: emailPayload }) |
84 | } | 88 | } |
85 | 89 | ||
86 | addVerifyEmailJob (username: string, to: string, verifyEmailUrl: string) { | 90 | addVerifyEmailJob (options: { |
91 | username: string | ||
92 | isRegistrationRequest: boolean | ||
93 | to: string | ||
94 | verifyEmailUrl: string | ||
95 | }) { | ||
96 | const { username, isRegistrationRequest, to, verifyEmailUrl } = options | ||
97 | |||
87 | const emailPayload: EmailPayload = { | 98 | const emailPayload: EmailPayload = { |
88 | template: 'verify-email', | 99 | template: 'verify-email', |
89 | to: [ to ], | 100 | to: [ to ], |
90 | subject: `Verify your email on ${CONFIG.INSTANCE.NAME}`, | 101 | subject: `Verify your email on ${CONFIG.INSTANCE.NAME}`, |
91 | locals: { | 102 | locals: { |
92 | username, | 103 | username, |
93 | verifyEmailUrl | 104 | verifyEmailUrl, |
105 | isRegistrationRequest, | ||
106 | |||
107 | hideNotificationPreferencesLink: true | ||
94 | } | 108 | } |
95 | } | 109 | } |
96 | 110 | ||
@@ -123,7 +137,33 @@ class Emailer { | |||
123 | body, | 137 | body, |
124 | 138 | ||
125 | // There are not notification preferences for the contact form | 139 | // There are not notification preferences for the contact form |
126 | hideNotificationPreferences: true | 140 | hideNotificationPreferencesLink: true |
141 | } | ||
142 | } | ||
143 | |||
144 | return JobQueue.Instance.createJobAsync({ type: 'email', payload: emailPayload }) | ||
145 | } | ||
146 | |||
147 | addUserRegistrationRequestProcessedJob (registration: MRegistration) { | ||
148 | let template: string | ||
149 | let subject: string | ||
150 | if (registration.state === UserRegistrationState.ACCEPTED) { | ||
151 | template = 'user-registration-request-accepted' | ||
152 | subject = `Your registration request for ${registration.username} has been accepted` | ||
153 | } else { | ||
154 | template = 'user-registration-request-rejected' | ||
155 | subject = `Your registration request for ${registration.username} has been rejected` | ||
156 | } | ||
157 | |||
158 | const to = registration.email | ||
159 | const emailPayload: EmailPayload = { | ||
160 | to: [ to ], | ||
161 | template, | ||
162 | subject, | ||
163 | locals: { | ||
164 | username: registration.username, | ||
165 | moderationResponse: registration.moderationResponse, | ||
166 | loginLink: WEBSERVER.URL + '/login' | ||
127 | } | 167 | } |
128 | } | 168 | } |
129 | 169 | ||
diff --git a/server/lib/emails/common/base.pug b/server/lib/emails/common/base.pug index 6da5648e4..41e94564d 100644 --- a/server/lib/emails/common/base.pug +++ b/server/lib/emails/common/base.pug | |||
@@ -222,19 +222,9 @@ body(width="100%" style="margin: 0; padding: 0 !important; mso-line-height-rule: | |||
222 | td(aria-hidden='true' height='20' style='font-size: 0px; line-height: 0px;') | 222 | td(aria-hidden='true' height='20' style='font-size: 0px; line-height: 0px;') |
223 | br | 223 | br |
224 | //- Clear Spacer : END | 224 | //- Clear Spacer : END |
225 | //- 1 Column Text : BEGIN | ||
226 | if username | ||
227 | tr | ||
228 | td(style='background-color: #cccccc;') | ||
229 | table(role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%') | ||
230 | tr | ||
231 | td(style='padding: 20px; font-family: sans-serif; font-size: 15px; line-height: 20px; color: #555555;') | ||
232 | p(style='margin: 0;') | ||
233 | | You are receiving this email as part of your notification settings on #{instanceName} for your account #{username}. | ||
234 | //- 1 Column Text : END | ||
235 | //- Email Body : END | 225 | //- Email Body : END |
236 | //- Email Footer : BEGIN | 226 | //- Email Footer : BEGIN |
237 | unless hideNotificationPreferences | 227 | unless hideNotificationPreferencesLink |
238 | table(align='center' role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%' style='margin: auto;') | 228 | table(align='center' role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%' style='margin: auto;') |
239 | tr | 229 | tr |
240 | td(style='padding: 20px; padding-bottom: 0px; font-family: sans-serif; font-size: 12px; line-height: 15px; text-align: center; color: #888888;') | 230 | td(style='padding: 20px; padding-bottom: 0px; font-family: sans-serif; font-size: 12px; line-height: 15px; text-align: center; color: #888888;') |
diff --git a/server/lib/emails/user-registration-request-accepted/html.pug b/server/lib/emails/user-registration-request-accepted/html.pug new file mode 100644 index 000000000..7a52c3fe1 --- /dev/null +++ b/server/lib/emails/user-registration-request-accepted/html.pug | |||
@@ -0,0 +1,10 @@ | |||
1 | extends ../common/greetings | ||
2 | |||
3 | block title | ||
4 | | Congratulation #{username}, your registration request has been accepted! | ||
5 | |||
6 | block content | ||
7 | p Your registration request has been accepted. | ||
8 | p Moderators sent you the following message: | ||
9 | blockquote(style='white-space: pre-wrap') #{moderationResponse} | ||
10 | p Your account has been created and you can login on #[a(href=loginLink) #{loginLink}] | ||
diff --git a/server/lib/emails/user-registration-request-rejected/html.pug b/server/lib/emails/user-registration-request-rejected/html.pug new file mode 100644 index 000000000..ec0aa8dfe --- /dev/null +++ b/server/lib/emails/user-registration-request-rejected/html.pug | |||
@@ -0,0 +1,9 @@ | |||
1 | extends ../common/greetings | ||
2 | |||
3 | block title | ||
4 | | Registration request of your account #{username} has rejected | ||
5 | |||
6 | block content | ||
7 | p Your registration request has been rejected. | ||
8 | p Moderators sent you the following message: | ||
9 | blockquote(style='white-space: pre-wrap') #{moderationResponse} | ||
diff --git a/server/lib/emails/user-registration-request/html.pug b/server/lib/emails/user-registration-request/html.pug new file mode 100644 index 000000000..64898f3f2 --- /dev/null +++ b/server/lib/emails/user-registration-request/html.pug | |||
@@ -0,0 +1,9 @@ | |||
1 | extends ../common/greetings | ||
2 | |||
3 | block title | ||
4 | | A new user wants to register | ||
5 | |||
6 | block content | ||
7 | p User #{registration.username} wants to register on your PeerTube instance with the following reason: | ||
8 | blockquote(style='white-space: pre-wrap') #{registration.registrationReason} | ||
9 | p You can accept or reject the registration request in the #[a(href=`${WEBSERVER.URL}/admin/moderation/registrations/list`) administration]. | ||
diff --git a/server/lib/emails/verify-email/html.pug b/server/lib/emails/verify-email/html.pug index be9dde21b..19ef65f75 100644 --- a/server/lib/emails/verify-email/html.pug +++ b/server/lib/emails/verify-email/html.pug | |||
@@ -1,17 +1,19 @@ | |||
1 | extends ../common/greetings | 1 | extends ../common/greetings |
2 | 2 | ||
3 | block title | 3 | block title |
4 | | Account verification | 4 | | Email verification |
5 | 5 | ||
6 | block content | 6 | block content |
7 | p Welcome to #{instanceName}! | 7 | if isRegistrationRequest |
8 | p. | 8 | p You just requested an account on #[a(href=WEBSERVER.URL) #{instanceName}]. |
9 | You just created an account at #[a(href=WEBSERVER.URL) #{instanceName}]. | 9 | else |
10 | Your username there is: #{username}. | 10 | p You just created an account on #[a(href=WEBSERVER.URL) #{instanceName}]. |
11 | p. | 11 | |
12 | To start using your account you must verify your email first! | 12 | if isRegistrationRequest |
13 | Please follow #[a(href=verifyEmailUrl) this link] to verify this email belongs to you. | 13 | p To complete your registration request you must verify your email first! |
14 | p. | 14 | else |
15 | If you can't see the verification link above you can use the following link #[a(href=verifyEmailUrl) #{verifyEmailUrl}] | 15 | p To start using your account you must verify your email first! |
16 | p. | 16 | |
17 | If you are not the person who initiated this request, please ignore this email. | 17 | p Please follow #[a(href=verifyEmailUrl) this link] to verify this email belongs to you. |
18 | p If you can't see the verification link above you can use the following link #[a(href=verifyEmailUrl) #{verifyEmailUrl}] | ||
19 | p If you are not the person who initiated this request, please ignore this email. | ||
diff --git a/server/lib/job-queue/job-queue.ts b/server/lib/job-queue/job-queue.ts index 866aa1ed0..8597eb000 100644 --- a/server/lib/job-queue/job-queue.ts +++ b/server/lib/job-queue/job-queue.ts | |||
@@ -184,7 +184,7 @@ class JobQueue { | |||
184 | 184 | ||
185 | this.jobRedisPrefix = 'bull-' + WEBSERVER.HOST | 185 | this.jobRedisPrefix = 'bull-' + WEBSERVER.HOST |
186 | 186 | ||
187 | for (const handlerName of (Object.keys(handlers) as JobType[])) { | 187 | for (const handlerName of Object.keys(handlers)) { |
188 | this.buildWorker(handlerName) | 188 | this.buildWorker(handlerName) |
189 | this.buildQueue(handlerName) | 189 | this.buildQueue(handlerName) |
190 | this.buildQueueScheduler(handlerName) | 190 | this.buildQueueScheduler(handlerName) |
diff --git a/server/lib/notifier/notifier.ts b/server/lib/notifier/notifier.ts index 66cfc31c4..920c55df0 100644 --- a/server/lib/notifier/notifier.ts +++ b/server/lib/notifier/notifier.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { MUser, MUserDefault } from '@server/types/models/user' | 1 | import { MRegistration, MUser, MUserDefault } from '@server/types/models/user' |
2 | import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/models/video/video-blacklist' | 2 | import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/models/video/video-blacklist' |
3 | import { UserNotificationSettingValue } from '../../../shared/models/users' | 3 | import { UserNotificationSettingValue } from '../../../shared/models/users' |
4 | import { logger } from '../../helpers/logger' | 4 | import { logger } from '../../helpers/logger' |
@@ -13,6 +13,7 @@ import { | |||
13 | AbuseStateChangeForReporter, | 13 | AbuseStateChangeForReporter, |
14 | AutoFollowForInstance, | 14 | AutoFollowForInstance, |
15 | CommentMention, | 15 | CommentMention, |
16 | DirectRegistrationForModerators, | ||
16 | FollowForInstance, | 17 | FollowForInstance, |
17 | FollowForUser, | 18 | FollowForUser, |
18 | ImportFinishedForOwner, | 19 | ImportFinishedForOwner, |
@@ -30,7 +31,7 @@ import { | |||
30 | OwnedPublicationAfterAutoUnblacklist, | 31 | OwnedPublicationAfterAutoUnblacklist, |
31 | OwnedPublicationAfterScheduleUpdate, | 32 | OwnedPublicationAfterScheduleUpdate, |
32 | OwnedPublicationAfterTranscoding, | 33 | OwnedPublicationAfterTranscoding, |
33 | RegistrationForModerators, | 34 | RegistrationRequestForModerators, |
34 | StudioEditionFinishedForOwner, | 35 | StudioEditionFinishedForOwner, |
35 | UnblacklistForOwner | 36 | UnblacklistForOwner |
36 | } from './shared' | 37 | } from './shared' |
@@ -47,7 +48,8 @@ class Notifier { | |||
47 | newBlacklist: [ NewBlacklistForOwner ], | 48 | newBlacklist: [ NewBlacklistForOwner ], |
48 | unblacklist: [ UnblacklistForOwner ], | 49 | unblacklist: [ UnblacklistForOwner ], |
49 | importFinished: [ ImportFinishedForOwner ], | 50 | importFinished: [ ImportFinishedForOwner ], |
50 | userRegistration: [ RegistrationForModerators ], | 51 | directRegistration: [ DirectRegistrationForModerators ], |
52 | registrationRequest: [ RegistrationRequestForModerators ], | ||
51 | userFollow: [ FollowForUser ], | 53 | userFollow: [ FollowForUser ], |
52 | instanceFollow: [ FollowForInstance ], | 54 | instanceFollow: [ FollowForInstance ], |
53 | autoInstanceFollow: [ AutoFollowForInstance ], | 55 | autoInstanceFollow: [ AutoFollowForInstance ], |
@@ -138,13 +140,20 @@ class Notifier { | |||
138 | }) | 140 | }) |
139 | } | 141 | } |
140 | 142 | ||
141 | notifyOnNewUserRegistration (user: MUserDefault): void { | 143 | notifyOnNewDirectRegistration (user: MUserDefault): void { |
142 | const models = this.notificationModels.userRegistration | 144 | const models = this.notificationModels.directRegistration |
143 | 145 | ||
144 | this.sendNotifications(models, user) | 146 | this.sendNotifications(models, user) |
145 | .catch(err => logger.error('Cannot notify moderators of new user registration (%s).', user.username, { err })) | 147 | .catch(err => logger.error('Cannot notify moderators of new user registration (%s).', user.username, { err })) |
146 | } | 148 | } |
147 | 149 | ||
150 | notifyOnNewRegistrationRequest (registration: MRegistration): void { | ||
151 | const models = this.notificationModels.registrationRequest | ||
152 | |||
153 | this.sendNotifications(models, registration) | ||
154 | .catch(err => logger.error('Cannot notify moderators of new registration request (%s).', registration.username, { err })) | ||
155 | } | ||
156 | |||
148 | notifyOfNewUserFollow (actorFollow: MActorFollowFull): void { | 157 | notifyOfNewUserFollow (actorFollow: MActorFollowFull): void { |
149 | const models = this.notificationModels.userFollow | 158 | const models = this.notificationModels.userFollow |
150 | 159 | ||
diff --git a/server/lib/notifier/shared/instance/registration-for-moderators.ts b/server/lib/notifier/shared/instance/direct-registration-for-moderators.ts index e92467424..5044f2068 100644 --- a/server/lib/notifier/shared/instance/registration-for-moderators.ts +++ b/server/lib/notifier/shared/instance/direct-registration-for-moderators.ts | |||
@@ -6,7 +6,7 @@ import { MUserDefault, MUserWithNotificationSetting, UserNotificationModelForApi | |||
6 | import { UserNotificationType, UserRight } from '@shared/models' | 6 | import { UserNotificationType, UserRight } from '@shared/models' |
7 | import { AbstractNotification } from '../common/abstract-notification' | 7 | import { AbstractNotification } from '../common/abstract-notification' |
8 | 8 | ||
9 | export class RegistrationForModerators extends AbstractNotification <MUserDefault> { | 9 | export class DirectRegistrationForModerators extends AbstractNotification <MUserDefault> { |
10 | private moderators: MUserDefault[] | 10 | private moderators: MUserDefault[] |
11 | 11 | ||
12 | async prepare () { | 12 | async prepare () { |
@@ -40,7 +40,7 @@ export class RegistrationForModerators extends AbstractNotification <MUserDefaul | |||
40 | return { | 40 | return { |
41 | template: 'user-registered', | 41 | template: 'user-registered', |
42 | to, | 42 | to, |
43 | subject: `a new user registered on ${CONFIG.INSTANCE.NAME}: ${this.payload.username}`, | 43 | subject: `A new user registered on ${CONFIG.INSTANCE.NAME}: ${this.payload.username}`, |
44 | locals: { | 44 | locals: { |
45 | user: this.payload | 45 | user: this.payload |
46 | } | 46 | } |
diff --git a/server/lib/notifier/shared/instance/index.ts b/server/lib/notifier/shared/instance/index.ts index c3bb22aec..8c75a8ee9 100644 --- a/server/lib/notifier/shared/instance/index.ts +++ b/server/lib/notifier/shared/instance/index.ts | |||
@@ -1,3 +1,4 @@ | |||
1 | export * from './new-peertube-version-for-admins' | 1 | export * from './new-peertube-version-for-admins' |
2 | export * from './new-plugin-version-for-admins' | 2 | export * from './new-plugin-version-for-admins' |
3 | export * from './registration-for-moderators' | 3 | export * from './direct-registration-for-moderators' |
4 | export * from './registration-request-for-moderators' | ||
diff --git a/server/lib/notifier/shared/instance/registration-request-for-moderators.ts b/server/lib/notifier/shared/instance/registration-request-for-moderators.ts new file mode 100644 index 000000000..79920245a --- /dev/null +++ b/server/lib/notifier/shared/instance/registration-request-for-moderators.ts | |||
@@ -0,0 +1,48 @@ | |||
1 | import { logger } from '@server/helpers/logger' | ||
2 | import { UserModel } from '@server/models/user/user' | ||
3 | import { UserNotificationModel } from '@server/models/user/user-notification' | ||
4 | import { MRegistration, MUserDefault, MUserWithNotificationSetting, UserNotificationModelForApi } from '@server/types/models' | ||
5 | import { UserNotificationType, UserRight } from '@shared/models' | ||
6 | import { AbstractNotification } from '../common/abstract-notification' | ||
7 | |||
8 | export class RegistrationRequestForModerators extends AbstractNotification <MRegistration> { | ||
9 | private moderators: MUserDefault[] | ||
10 | |||
11 | async prepare () { | ||
12 | this.moderators = await UserModel.listWithRight(UserRight.MANAGE_REGISTRATIONS) | ||
13 | } | ||
14 | |||
15 | log () { | ||
16 | logger.info('Notifying %s moderators of new user registration request of %s.', this.moderators.length, this.payload.username) | ||
17 | } | ||
18 | |||
19 | getSetting (user: MUserWithNotificationSetting) { | ||
20 | return user.NotificationSetting.newUserRegistration | ||
21 | } | ||
22 | |||
23 | getTargetUsers () { | ||
24 | return this.moderators | ||
25 | } | ||
26 | |||
27 | createNotification (user: MUserWithNotificationSetting) { | ||
28 | const notification = UserNotificationModel.build<UserNotificationModelForApi>({ | ||
29 | type: UserNotificationType.NEW_USER_REGISTRATION_REQUEST, | ||
30 | userId: user.id, | ||
31 | userRegistrationId: this.payload.id | ||
32 | }) | ||
33 | notification.UserRegistration = this.payload | ||
34 | |||
35 | return notification | ||
36 | } | ||
37 | |||
38 | createEmail (to: string) { | ||
39 | return { | ||
40 | template: 'user-registration-request', | ||
41 | to, | ||
42 | subject: `A new user wants to register: ${this.payload.username}`, | ||
43 | locals: { | ||
44 | registration: this.payload | ||
45 | } | ||
46 | } | ||
47 | } | ||
48 | } | ||
diff --git a/server/lib/opentelemetry/metric-helpers/bittorrent-tracker-observers-builder.ts b/server/lib/opentelemetry/metric-helpers/bittorrent-tracker-observers-builder.ts new file mode 100644 index 000000000..ef40c0fa9 --- /dev/null +++ b/server/lib/opentelemetry/metric-helpers/bittorrent-tracker-observers-builder.ts | |||
@@ -0,0 +1,51 @@ | |||
1 | import { Meter } from '@opentelemetry/api' | ||
2 | |||
3 | export class BittorrentTrackerObserversBuilder { | ||
4 | |||
5 | constructor (private readonly meter: Meter, private readonly trackerServer: any) { | ||
6 | |||
7 | } | ||
8 | |||
9 | buildObservers () { | ||
10 | const activeInfohashes = this.meter.createObservableGauge('peertube_bittorrent_tracker_active_infohashes_total', { | ||
11 | description: 'Total active infohashes in the PeerTube BitTorrent Tracker' | ||
12 | }) | ||
13 | const inactiveInfohashes = this.meter.createObservableGauge('peertube_bittorrent_tracker_inactive_infohashes_total', { | ||
14 | description: 'Total inactive infohashes in the PeerTube BitTorrent Tracker' | ||
15 | }) | ||
16 | const peers = this.meter.createObservableGauge('peertube_bittorrent_tracker_peers_total', { | ||
17 | description: 'Total peers in the PeerTube BitTorrent Tracker' | ||
18 | }) | ||
19 | |||
20 | this.meter.addBatchObservableCallback(observableResult => { | ||
21 | const infohashes = Object.keys(this.trackerServer.torrents) | ||
22 | |||
23 | const counters = { | ||
24 | activeInfohashes: 0, | ||
25 | inactiveInfohashes: 0, | ||
26 | peers: 0, | ||
27 | uncompletedPeers: 0 | ||
28 | } | ||
29 | |||
30 | for (const infohash of infohashes) { | ||
31 | const content = this.trackerServer.torrents[infohash] | ||
32 | |||
33 | const peers = content.peers | ||
34 | if (peers.keys.length !== 0) counters.activeInfohashes++ | ||
35 | else counters.inactiveInfohashes++ | ||
36 | |||
37 | for (const peerId of peers.keys) { | ||
38 | const peer = peers.peek(peerId) | ||
39 | if (peer == null) return | ||
40 | |||
41 | counters.peers++ | ||
42 | } | ||
43 | } | ||
44 | |||
45 | observableResult.observe(activeInfohashes, counters.activeInfohashes) | ||
46 | observableResult.observe(inactiveInfohashes, counters.inactiveInfohashes) | ||
47 | observableResult.observe(peers, counters.peers) | ||
48 | }, [ activeInfohashes, inactiveInfohashes, peers ]) | ||
49 | } | ||
50 | |||
51 | } | ||
diff --git a/server/lib/opentelemetry/metric-helpers/index.ts b/server/lib/opentelemetry/metric-helpers/index.ts index 775d954ba..47b24a54f 100644 --- a/server/lib/opentelemetry/metric-helpers/index.ts +++ b/server/lib/opentelemetry/metric-helpers/index.ts | |||
@@ -1,3 +1,4 @@ | |||
1 | export * from './bittorrent-tracker-observers-builder' | ||
1 | export * from './lives-observers-builder' | 2 | export * from './lives-observers-builder' |
2 | export * from './job-queue-observers-builder' | 3 | export * from './job-queue-observers-builder' |
3 | export * from './nodejs-observers-builder' | 4 | export * from './nodejs-observers-builder' |
diff --git a/server/lib/opentelemetry/metrics.ts b/server/lib/opentelemetry/metrics.ts index 226d514c0..9cc067e4a 100644 --- a/server/lib/opentelemetry/metrics.ts +++ b/server/lib/opentelemetry/metrics.ts | |||
@@ -7,6 +7,7 @@ import { CONFIG } from '@server/initializers/config' | |||
7 | import { MVideoImmutable } from '@server/types/models' | 7 | import { MVideoImmutable } from '@server/types/models' |
8 | import { PlaybackMetricCreate } from '@shared/models' | 8 | import { PlaybackMetricCreate } from '@shared/models' |
9 | import { | 9 | import { |
10 | BittorrentTrackerObserversBuilder, | ||
10 | JobQueueObserversBuilder, | 11 | JobQueueObserversBuilder, |
11 | LivesObserversBuilder, | 12 | LivesObserversBuilder, |
12 | NodeJSObserversBuilder, | 13 | NodeJSObserversBuilder, |
@@ -41,7 +42,7 @@ class OpenTelemetryMetrics { | |||
41 | }) | 42 | }) |
42 | } | 43 | } |
43 | 44 | ||
44 | registerMetrics () { | 45 | registerMetrics (options: { trackerServer: any }) { |
45 | if (CONFIG.OPEN_TELEMETRY.METRICS.ENABLED !== true) return | 46 | if (CONFIG.OPEN_TELEMETRY.METRICS.ENABLED !== true) return |
46 | 47 | ||
47 | logger.info('Registering Open Telemetry metrics') | 48 | logger.info('Registering Open Telemetry metrics') |
@@ -80,6 +81,9 @@ class OpenTelemetryMetrics { | |||
80 | 81 | ||
81 | const viewersObserversBuilder = new ViewersObserversBuilder(this.meter) | 82 | const viewersObserversBuilder = new ViewersObserversBuilder(this.meter) |
82 | viewersObserversBuilder.buildObservers() | 83 | viewersObserversBuilder.buildObservers() |
84 | |||
85 | const bittorrentTrackerObserversBuilder = new BittorrentTrackerObserversBuilder(this.meter, options.trackerServer) | ||
86 | bittorrentTrackerObserversBuilder.buildObservers() | ||
83 | } | 87 | } |
84 | 88 | ||
85 | observePlaybackMetric (video: MVideoImmutable, metrics: PlaybackMetricCreate) { | 89 | observePlaybackMetric (video: MVideoImmutable, metrics: PlaybackMetricCreate) { |
diff --git a/server/lib/plugins/plugin-helpers-builder.ts b/server/lib/plugins/plugin-helpers-builder.ts index 7b1def6e3..66383af46 100644 --- a/server/lib/plugins/plugin-helpers-builder.ts +++ b/server/lib/plugins/plugin-helpers-builder.ts | |||
@@ -209,6 +209,10 @@ function buildConfigHelpers () { | |||
209 | return WEBSERVER.URL | 209 | return WEBSERVER.URL |
210 | }, | 210 | }, |
211 | 211 | ||
212 | getServerListeningConfig () { | ||
213 | return { hostname: CONFIG.LISTEN.HOSTNAME, port: CONFIG.LISTEN.PORT } | ||
214 | }, | ||
215 | |||
212 | getServerConfig () { | 216 | getServerConfig () { |
213 | return ServerConfigManager.Instance.getServerConfig() | 217 | return ServerConfigManager.Instance.getServerConfig() |
214 | } | 218 | } |
@@ -245,7 +249,7 @@ function buildUserHelpers () { | |||
245 | }, | 249 | }, |
246 | 250 | ||
247 | getAuthUser: (res: express.Response) => { | 251 | getAuthUser: (res: express.Response) => { |
248 | const user = res.locals.oauth?.token?.User | 252 | const user = res.locals.oauth?.token?.User || res.locals.videoFileToken?.user |
249 | if (!user) return undefined | 253 | if (!user) return undefined |
250 | 254 | ||
251 | return UserModel.loadByIdFull(user.id) | 255 | return UserModel.loadByIdFull(user.id) |
diff --git a/server/lib/redis.ts b/server/lib/redis.ts index c0e9aece7..3706d2228 100644 --- a/server/lib/redis.ts +++ b/server/lib/redis.ts | |||
@@ -8,9 +8,8 @@ import { | |||
8 | AP_CLEANER, | 8 | AP_CLEANER, |
9 | CONTACT_FORM_LIFETIME, | 9 | CONTACT_FORM_LIFETIME, |
10 | RESUMABLE_UPLOAD_SESSION_LIFETIME, | 10 | RESUMABLE_UPLOAD_SESSION_LIFETIME, |
11 | TRACKER_RATE_LIMITS, | ||
12 | TWO_FACTOR_AUTH_REQUEST_TOKEN_LIFETIME, | 11 | TWO_FACTOR_AUTH_REQUEST_TOKEN_LIFETIME, |
13 | USER_EMAIL_VERIFY_LIFETIME, | 12 | EMAIL_VERIFY_LIFETIME, |
14 | USER_PASSWORD_CREATE_LIFETIME, | 13 | USER_PASSWORD_CREATE_LIFETIME, |
15 | USER_PASSWORD_RESET_LIFETIME, | 14 | USER_PASSWORD_RESET_LIFETIME, |
16 | VIEW_LIFETIME, | 15 | VIEW_LIFETIME, |
@@ -125,16 +124,28 @@ class Redis { | |||
125 | 124 | ||
126 | /* ************ Email verification ************ */ | 125 | /* ************ Email verification ************ */ |
127 | 126 | ||
128 | async setVerifyEmailVerificationString (userId: number) { | 127 | async setUserVerifyEmailVerificationString (userId: number) { |
129 | const generatedString = await generateRandomString(32) | 128 | const generatedString = await generateRandomString(32) |
130 | 129 | ||
131 | await this.setValue(this.generateVerifyEmailKey(userId), generatedString, USER_EMAIL_VERIFY_LIFETIME) | 130 | await this.setValue(this.generateUserVerifyEmailKey(userId), generatedString, EMAIL_VERIFY_LIFETIME) |
132 | 131 | ||
133 | return generatedString | 132 | return generatedString |
134 | } | 133 | } |
135 | 134 | ||
136 | async getVerifyEmailLink (userId: number) { | 135 | async getUserVerifyEmailLink (userId: number) { |
137 | return this.getValue(this.generateVerifyEmailKey(userId)) | 136 | return this.getValue(this.generateUserVerifyEmailKey(userId)) |
137 | } | ||
138 | |||
139 | async setRegistrationVerifyEmailVerificationString (registrationId: number) { | ||
140 | const generatedString = await generateRandomString(32) | ||
141 | |||
142 | await this.setValue(this.generateRegistrationVerifyEmailKey(registrationId), generatedString, EMAIL_VERIFY_LIFETIME) | ||
143 | |||
144 | return generatedString | ||
145 | } | ||
146 | |||
147 | async getRegistrationVerifyEmailLink (registrationId: number) { | ||
148 | return this.getValue(this.generateRegistrationVerifyEmailKey(registrationId)) | ||
138 | } | 149 | } |
139 | 150 | ||
140 | /* ************ Contact form per IP ************ */ | 151 | /* ************ Contact form per IP ************ */ |
@@ -157,16 +168,6 @@ class Redis { | |||
157 | return this.exists(this.generateIPViewKey(ip, videoUUID)) | 168 | return this.exists(this.generateIPViewKey(ip, videoUUID)) |
158 | } | 169 | } |
159 | 170 | ||
160 | /* ************ Tracker IP block ************ */ | ||
161 | |||
162 | setTrackerBlockIP (ip: string) { | ||
163 | return this.setValue(this.generateTrackerBlockIPKey(ip), '1', TRACKER_RATE_LIMITS.BLOCK_IP_LIFETIME) | ||
164 | } | ||
165 | |||
166 | async doesTrackerBlockIPExist (ip: string) { | ||
167 | return this.exists(this.generateTrackerBlockIPKey(ip)) | ||
168 | } | ||
169 | |||
170 | /* ************ Video views stats ************ */ | 171 | /* ************ Video views stats ************ */ |
171 | 172 | ||
172 | addVideoViewStats (videoId: number) { | 173 | addVideoViewStats (videoId: number) { |
@@ -357,16 +358,16 @@ class Redis { | |||
357 | return 'two-factor-request-' + userId + '-' + token | 358 | return 'two-factor-request-' + userId + '-' + token |
358 | } | 359 | } |
359 | 360 | ||
360 | private generateVerifyEmailKey (userId: number) { | 361 | private generateUserVerifyEmailKey (userId: number) { |
361 | return 'verify-email-' + userId | 362 | return 'verify-email-user-' + userId |
362 | } | 363 | } |
363 | 364 | ||
364 | private generateIPViewKey (ip: string, videoUUID: string) { | 365 | private generateRegistrationVerifyEmailKey (registrationId: number) { |
365 | return `views-${videoUUID}-${ip}` | 366 | return 'verify-email-registration-' + registrationId |
366 | } | 367 | } |
367 | 368 | ||
368 | private generateTrackerBlockIPKey (ip: string) { | 369 | private generateIPViewKey (ip: string, videoUUID: string) { |
369 | return `tracker-block-ip-${ip}` | 370 | return `views-${videoUUID}-${ip}` |
370 | } | 371 | } |
371 | 372 | ||
372 | private generateContactFormKey (ip: string) { | 373 | private generateContactFormKey (ip: string) { |
diff --git a/server/lib/server-config-manager.ts b/server/lib/server-config-manager.ts index 78a9546ae..e87e2854f 100644 --- a/server/lib/server-config-manager.ts +++ b/server/lib/server-config-manager.ts | |||
@@ -261,10 +261,17 @@ class ServerConfigManager { | |||
261 | async getServerConfig (ip?: string): Promise<ServerConfig> { | 261 | async getServerConfig (ip?: string): Promise<ServerConfig> { |
262 | const { allowed } = await Hooks.wrapPromiseFun( | 262 | const { allowed } = await Hooks.wrapPromiseFun( |
263 | isSignupAllowed, | 263 | isSignupAllowed, |
264 | |||
264 | { | 265 | { |
265 | ip | 266 | ip, |
267 | signupMode: CONFIG.SIGNUP.REQUIRES_APPROVAL | ||
268 | ? 'request-registration' | ||
269 | : 'direct-registration' | ||
266 | }, | 270 | }, |
267 | 'filter:api.user.signup.allowed.result' | 271 | |
272 | CONFIG.SIGNUP.REQUIRES_APPROVAL | ||
273 | ? 'filter:api.user.request-signup.allowed.result' | ||
274 | : 'filter:api.user.signup.allowed.result' | ||
268 | ) | 275 | ) |
269 | 276 | ||
270 | const allowedForCurrentIP = isSignupAllowedForCurrentIP(ip) | 277 | const allowedForCurrentIP = isSignupAllowedForCurrentIP(ip) |
@@ -273,6 +280,7 @@ class ServerConfigManager { | |||
273 | allowed, | 280 | allowed, |
274 | allowedForCurrentIP, | 281 | allowedForCurrentIP, |
275 | minimumAge: CONFIG.SIGNUP.MINIMUM_AGE, | 282 | minimumAge: CONFIG.SIGNUP.MINIMUM_AGE, |
283 | requiresApproval: CONFIG.SIGNUP.REQUIRES_APPROVAL, | ||
276 | requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION | 284 | requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION |
277 | } | 285 | } |
278 | 286 | ||
diff --git a/server/lib/signup.ts b/server/lib/signup.ts index f094531eb..f19232621 100644 --- a/server/lib/signup.ts +++ b/server/lib/signup.ts | |||
@@ -4,11 +4,24 @@ import { UserModel } from '../models/user/user' | |||
4 | 4 | ||
5 | const isCidr = require('is-cidr') | 5 | const isCidr = require('is-cidr') |
6 | 6 | ||
7 | async function isSignupAllowed (): Promise<{ allowed: boolean, errorMessage?: string }> { | 7 | export type SignupMode = 'direct-registration' | 'request-registration' |
8 | |||
9 | async function isSignupAllowed (options: { | ||
10 | signupMode: SignupMode | ||
11 | |||
12 | ip: string // For plugins | ||
13 | body?: any | ||
14 | }): Promise<{ allowed: boolean, errorMessage?: string }> { | ||
15 | const { signupMode } = options | ||
16 | |||
8 | if (CONFIG.SIGNUP.ENABLED === false) { | 17 | if (CONFIG.SIGNUP.ENABLED === false) { |
9 | return { allowed: false } | 18 | return { allowed: false } |
10 | } | 19 | } |
11 | 20 | ||
21 | if (signupMode === 'direct-registration' && CONFIG.SIGNUP.REQUIRES_APPROVAL === true) { | ||
22 | return { allowed: false } | ||
23 | } | ||
24 | |||
12 | // No limit and signup is enabled | 25 | // No limit and signup is enabled |
13 | if (CONFIG.SIGNUP.LIMIT === -1) { | 26 | if (CONFIG.SIGNUP.LIMIT === -1) { |
14 | return { allowed: true } | 27 | return { allowed: true } |
diff --git a/server/lib/sync-channel.ts b/server/lib/sync-channel.ts index 10167ee38..3a805a943 100644 --- a/server/lib/sync-channel.ts +++ b/server/lib/sync-channel.ts | |||
@@ -76,7 +76,7 @@ export async function synchronizeChannel (options: { | |||
76 | 76 | ||
77 | await JobQueue.Instance.createJobWithChildren(parent, children) | 77 | await JobQueue.Instance.createJobWithChildren(parent, children) |
78 | } catch (err) { | 78 | } catch (err) { |
79 | logger.error(`Failed to import channel ${channel.name}`, { err }) | 79 | logger.error(`Failed to import ${externalChannelUrl} in channel ${channel.name}`, { err }) |
80 | channelSync.state = VideoChannelSyncState.FAILED | 80 | channelSync.state = VideoChannelSyncState.FAILED |
81 | await channelSync.save() | 81 | await channelSync.save() |
82 | } | 82 | } |
diff --git a/server/lib/user.ts b/server/lib/user.ts index 2e433da04..ffb57944a 100644 --- a/server/lib/user.ts +++ b/server/lib/user.ts | |||
@@ -10,7 +10,7 @@ import { sequelizeTypescript } from '../initializers/database' | |||
10 | import { AccountModel } from '../models/account/account' | 10 | import { AccountModel } from '../models/account/account' |
11 | import { UserNotificationSettingModel } from '../models/user/user-notification-setting' | 11 | import { UserNotificationSettingModel } from '../models/user/user-notification-setting' |
12 | import { MAccountDefault, MChannelActor } from '../types/models' | 12 | import { MAccountDefault, MChannelActor } from '../types/models' |
13 | import { MUser, MUserDefault, MUserId } from '../types/models/user' | 13 | import { MRegistration, MUser, MUserDefault, MUserId } from '../types/models/user' |
14 | import { generateAndSaveActorKeys } from './activitypub/actors' | 14 | import { generateAndSaveActorKeys } from './activitypub/actors' |
15 | import { getLocalAccountActivityPubUrl } from './activitypub/url' | 15 | import { getLocalAccountActivityPubUrl } from './activitypub/url' |
16 | import { Emailer } from './emailer' | 16 | import { Emailer } from './emailer' |
@@ -97,7 +97,7 @@ async function createUserAccountAndChannelAndPlaylist (parameters: { | |||
97 | }) | 97 | }) |
98 | userCreated.Account = accountCreated | 98 | userCreated.Account = accountCreated |
99 | 99 | ||
100 | const channelAttributes = await buildChannelAttributes(userCreated, t, channelNames) | 100 | const channelAttributes = await buildChannelAttributes({ user: userCreated, transaction: t, channelNames }) |
101 | const videoChannel = await createLocalVideoChannel(channelAttributes, accountCreated, t) | 101 | const videoChannel = await createLocalVideoChannel(channelAttributes, accountCreated, t) |
102 | 102 | ||
103 | const videoPlaylist = await createWatchLaterPlaylist(accountCreated, t) | 103 | const videoPlaylist = await createWatchLaterPlaylist(accountCreated, t) |
@@ -160,15 +160,28 @@ async function createApplicationActor (applicationId: number) { | |||
160 | // --------------------------------------------------------------------------- | 160 | // --------------------------------------------------------------------------- |
161 | 161 | ||
162 | async function sendVerifyUserEmail (user: MUser, isPendingEmail = false) { | 162 | async function sendVerifyUserEmail (user: MUser, isPendingEmail = false) { |
163 | const verificationString = await Redis.Instance.setVerifyEmailVerificationString(user.id) | 163 | const verificationString = await Redis.Instance.setUserVerifyEmailVerificationString(user.id) |
164 | let url = WEBSERVER.URL + '/verify-account/email?userId=' + user.id + '&verificationString=' + verificationString | 164 | let verifyEmailUrl = `${WEBSERVER.URL}/verify-account/email?userId=${user.id}&verificationString=${verificationString}` |
165 | 165 | ||
166 | if (isPendingEmail) url += '&isPendingEmail=true' | 166 | if (isPendingEmail) verifyEmailUrl += '&isPendingEmail=true' |
167 | |||
168 | const to = isPendingEmail | ||
169 | ? user.pendingEmail | ||
170 | : user.email | ||
167 | 171 | ||
168 | const email = isPendingEmail ? user.pendingEmail : user.email | ||
169 | const username = user.username | 172 | const username = user.username |
170 | 173 | ||
171 | Emailer.Instance.addVerifyEmailJob(username, email, url) | 174 | Emailer.Instance.addVerifyEmailJob({ username, to, verifyEmailUrl, isRegistrationRequest: false }) |
175 | } | ||
176 | |||
177 | async function sendVerifyRegistrationEmail (registration: MRegistration) { | ||
178 | const verificationString = await Redis.Instance.setRegistrationVerifyEmailVerificationString(registration.id) | ||
179 | const verifyEmailUrl = `${WEBSERVER.URL}/verify-account/email?registrationId=${registration.id}&verificationString=${verificationString}` | ||
180 | |||
181 | const to = registration.email | ||
182 | const username = registration.username | ||
183 | |||
184 | Emailer.Instance.addVerifyEmailJob({ username, to, verifyEmailUrl, isRegistrationRequest: true }) | ||
172 | } | 185 | } |
173 | 186 | ||
174 | // --------------------------------------------------------------------------- | 187 | // --------------------------------------------------------------------------- |
@@ -232,7 +245,10 @@ export { | |||
232 | createApplicationActor, | 245 | createApplicationActor, |
233 | createUserAccountAndChannelAndPlaylist, | 246 | createUserAccountAndChannelAndPlaylist, |
234 | createLocalAccountWithoutKeys, | 247 | createLocalAccountWithoutKeys, |
248 | |||
235 | sendVerifyUserEmail, | 249 | sendVerifyUserEmail, |
250 | sendVerifyRegistrationEmail, | ||
251 | |||
236 | isAbleToUploadVideo, | 252 | isAbleToUploadVideo, |
237 | buildUser | 253 | buildUser |
238 | } | 254 | } |
@@ -264,7 +280,13 @@ function createDefaultUserNotificationSettings (user: MUserId, t: Transaction | | |||
264 | return UserNotificationSettingModel.create(values, { transaction: t }) | 280 | return UserNotificationSettingModel.create(values, { transaction: t }) |
265 | } | 281 | } |
266 | 282 | ||
267 | async function buildChannelAttributes (user: MUser, transaction?: Transaction, channelNames?: ChannelNames) { | 283 | async function buildChannelAttributes (options: { |
284 | user: MUser | ||
285 | transaction?: Transaction | ||
286 | channelNames?: ChannelNames | ||
287 | }) { | ||
288 | const { user, transaction, channelNames } = options | ||
289 | |||
268 | if (channelNames) return channelNames | 290 | if (channelNames) return channelNames |
269 | 291 | ||
270 | const channelName = await findAvailableLocalActorName(user.username + '_channel', transaction) | 292 | const channelName = await findAvailableLocalActorName(user.username + '_channel', transaction) |
diff --git a/server/lib/video-comment.ts b/server/lib/video-comment.ts index 02f160fe8..6eb865f7f 100644 --- a/server/lib/video-comment.ts +++ b/server/lib/video-comment.ts | |||
@@ -1,30 +1,41 @@ | |||
1 | import express from 'express' | ||
1 | import { cloneDeep } from 'lodash' | 2 | import { cloneDeep } from 'lodash' |
2 | import * as Sequelize from 'sequelize' | 3 | import * as Sequelize from 'sequelize' |
3 | import express from 'express' | ||
4 | import { logger } from '@server/helpers/logger' | 4 | import { logger } from '@server/helpers/logger' |
5 | import { sequelizeTypescript } from '@server/initializers/database' | 5 | import { sequelizeTypescript } from '@server/initializers/database' |
6 | import { ResultList } from '../../shared/models' | 6 | import { ResultList } from '../../shared/models' |
7 | import { VideoCommentThreadTree } from '../../shared/models/videos/comment/video-comment.model' | 7 | import { VideoCommentThreadTree } from '../../shared/models/videos/comment/video-comment.model' |
8 | import { VideoCommentModel } from '../models/video/video-comment' | 8 | import { VideoCommentModel } from '../models/video/video-comment' |
9 | import { MAccountDefault, MComment, MCommentOwnerVideo, MCommentOwnerVideoReply, MVideoFullLight } from '../types/models' | 9 | import { |
10 | MAccountDefault, | ||
11 | MComment, | ||
12 | MCommentFormattable, | ||
13 | MCommentOwnerVideo, | ||
14 | MCommentOwnerVideoReply, | ||
15 | MVideoFullLight | ||
16 | } from '../types/models' | ||
10 | import { sendCreateVideoComment, sendDeleteVideoComment } from './activitypub/send' | 17 | import { sendCreateVideoComment, sendDeleteVideoComment } from './activitypub/send' |
11 | import { getLocalVideoCommentActivityPubUrl } from './activitypub/url' | 18 | import { getLocalVideoCommentActivityPubUrl } from './activitypub/url' |
12 | import { Hooks } from './plugins/hooks' | 19 | import { Hooks } from './plugins/hooks' |
13 | 20 | ||
14 | async function removeComment (videoCommentInstance: MCommentOwnerVideo, req: express.Request, res: express.Response) { | 21 | async function removeComment (commentArg: MComment, req: express.Request, res: express.Response) { |
15 | const videoCommentInstanceBefore = cloneDeep(videoCommentInstance) | 22 | let videoCommentInstanceBefore: MCommentOwnerVideo |
16 | 23 | ||
17 | await sequelizeTypescript.transaction(async t => { | 24 | await sequelizeTypescript.transaction(async t => { |
18 | if (videoCommentInstance.isOwned() || videoCommentInstance.Video.isOwned()) { | 25 | const comment = await VideoCommentModel.loadByUrlAndPopulateAccountAndVideo(commentArg.url, t) |
19 | await sendDeleteVideoComment(videoCommentInstance, t) | 26 | |
27 | videoCommentInstanceBefore = cloneDeep(comment) | ||
28 | |||
29 | if (comment.isOwned() || comment.Video.isOwned()) { | ||
30 | await sendDeleteVideoComment(comment, t) | ||
20 | } | 31 | } |
21 | 32 | ||
22 | videoCommentInstance.markAsDeleted() | 33 | comment.markAsDeleted() |
23 | 34 | ||
24 | await videoCommentInstance.save({ transaction: t }) | 35 | await comment.save({ transaction: t }) |
25 | }) | ||
26 | 36 | ||
27 | logger.info('Video comment %d deleted.', videoCommentInstance.id) | 37 | logger.info('Video comment %d deleted.', comment.id) |
38 | }) | ||
28 | 39 | ||
29 | Hooks.runAction('action:api.video-comment.deleted', { comment: videoCommentInstanceBefore, req, res }) | 40 | Hooks.runAction('action:api.video-comment.deleted', { comment: videoCommentInstanceBefore, req, res }) |
30 | } | 41 | } |
@@ -64,7 +75,7 @@ async function createVideoComment (obj: { | |||
64 | return savedComment | 75 | return savedComment |
65 | } | 76 | } |
66 | 77 | ||
67 | function buildFormattedCommentTree (resultList: ResultList<VideoCommentModel>): VideoCommentThreadTree { | 78 | function buildFormattedCommentTree (resultList: ResultList<MCommentFormattable>): VideoCommentThreadTree { |
68 | // Comments are sorted by id ASC | 79 | // Comments are sorted by id ASC |
69 | const comments = resultList.data | 80 | const comments = resultList.data |
70 | 81 | ||
diff --git a/server/lib/video-tokens-manager.ts b/server/lib/video-tokens-manager.ts index c43085d16..17aa29cdd 100644 --- a/server/lib/video-tokens-manager.ts +++ b/server/lib/video-tokens-manager.ts | |||
@@ -1,5 +1,7 @@ | |||
1 | import LRUCache from 'lru-cache' | 1 | import LRUCache from 'lru-cache' |
2 | import { LRU_CACHE } from '@server/initializers/constants' | 2 | import { LRU_CACHE } from '@server/initializers/constants' |
3 | import { MUserAccountUrl } from '@server/types/models' | ||
4 | import { pick } from '@shared/core-utils' | ||
3 | import { buildUUID } from '@shared/extra-utils' | 5 | import { buildUUID } from '@shared/extra-utils' |
4 | 6 | ||
5 | // --------------------------------------------------------------------------- | 7 | // --------------------------------------------------------------------------- |
@@ -10,19 +12,22 @@ class VideoTokensManager { | |||
10 | 12 | ||
11 | private static instance: VideoTokensManager | 13 | private static instance: VideoTokensManager |
12 | 14 | ||
13 | private readonly lruCache = new LRUCache<string, string>({ | 15 | private readonly lruCache = new LRUCache<string, { videoUUID: string, user: MUserAccountUrl }>({ |
14 | max: LRU_CACHE.VIDEO_TOKENS.MAX_SIZE, | 16 | max: LRU_CACHE.VIDEO_TOKENS.MAX_SIZE, |
15 | ttl: LRU_CACHE.VIDEO_TOKENS.TTL | 17 | ttl: LRU_CACHE.VIDEO_TOKENS.TTL |
16 | }) | 18 | }) |
17 | 19 | ||
18 | private constructor () {} | 20 | private constructor () {} |
19 | 21 | ||
20 | create (videoUUID: string) { | 22 | create (options: { |
23 | user: MUserAccountUrl | ||
24 | videoUUID: string | ||
25 | }) { | ||
21 | const token = buildUUID() | 26 | const token = buildUUID() |
22 | 27 | ||
23 | const expires = new Date(new Date().getTime() + LRU_CACHE.VIDEO_TOKENS.TTL) | 28 | const expires = new Date(new Date().getTime() + LRU_CACHE.VIDEO_TOKENS.TTL) |
24 | 29 | ||
25 | this.lruCache.set(token, videoUUID) | 30 | this.lruCache.set(token, pick(options, [ 'user', 'videoUUID' ])) |
26 | 31 | ||
27 | return { token, expires } | 32 | return { token, expires } |
28 | } | 33 | } |
@@ -34,7 +39,16 @@ class VideoTokensManager { | |||
34 | const value = this.lruCache.get(options.token) | 39 | const value = this.lruCache.get(options.token) |
35 | if (!value) return false | 40 | if (!value) return false |
36 | 41 | ||
37 | return value === options.videoUUID | 42 | return value.videoUUID === options.videoUUID |
43 | } | ||
44 | |||
45 | getUserFromToken (options: { | ||
46 | token: string | ||
47 | }) { | ||
48 | const value = this.lruCache.get(options.token) | ||
49 | if (!value) return undefined | ||
50 | |||
51 | return value.user | ||
38 | } | 52 | } |
39 | 53 | ||
40 | static get Instance () { | 54 | static get Instance () { |