Cache user token
authorChocobozzz <me@florianbigard.com>
Thu, 20 Sep 2018 09:31:48 +0000 (11:31 +0200)
committerChocobozzz <me@florianbigard.com>
Thu, 20 Sep 2018 09:45:59 +0000 (11:45 +0200)
server/controllers/api/users/index.ts
server/controllers/api/users/me.ts
server/controllers/api/video-channel.ts
server/lib/avatar.ts
server/lib/oauth-model.ts
server/models/account/user.ts
server/models/oauth/oauth-token.ts

index d1163900bd3709a7b6a04846bea75d283813c660..8b8ebcd234ead49b36991125fc9fda579a388017 100644 (file)
@@ -37,6 +37,7 @@ import { UserModel } from '../../../models/account/user'
 import { OAuthTokenModel } from '../../../models/oauth/oauth-token'
 import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger'
 import { meRouter } from './me'
+import { deleteUserToken } from '../../../lib/oauth-model'
 
 const auditLogger = auditLoggerFactory('users')
 
@@ -267,7 +268,7 @@ async function updateUser (req: express.Request, res: express.Response, next: ex
   const user = await userToUpdate.save()
 
   // Destroy user token to refresh rights
-  if (roleChanged) await OAuthTokenModel.deleteUserToken(userToUpdate.id)
+  if (roleChanged) await deleteUserToken(userToUpdate.id)
 
   auditLogger.update(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()), oldUserAuditView)
 
@@ -330,7 +331,7 @@ async function changeUserBlock (res: express.Response, user: UserModel, block: b
   user.blockedReason = reason || null
 
   await sequelizeTypescript.transaction(async t => {
-    await OAuthTokenModel.deleteUserToken(user.id, t)
+    await deleteUserToken(user.id, t)
 
     await user.save({ transaction: t })
   })
index eba1e7eddbe5278e852c148835b273b50ffa3536..ff3a87b7f181d7d30fb9aa3065057ab11c6c76f1 100644 (file)
@@ -353,7 +353,7 @@ async function updateMyAvatar (req: express.Request, res: express.Response, next
 
   const userAccount = await AccountModel.load(user.Account.id)
 
-  const avatar = await updateActorAvatarFile(avatarPhysicalFile, userAccount.Actor, userAccount)
+  const avatar = await updateActorAvatarFile(avatarPhysicalFile, userAccount)
 
   auditLogger.update(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()), oldUserAuditView)
 
index 8fc34022430bb61dc3aff14ced7164581d71fda2..ff6bbe44c0b068ba533fbac53cc283a6b01ae7d7 100644 (file)
@@ -56,7 +56,7 @@ videoChannelRouter.post('/:nameWithHost/avatar/pick',
   // Check the rights
   asyncMiddleware(videoChannelsUpdateValidator),
   updateAvatarValidator,
-  asyncMiddleware(updateVideoChannelAvatar)
+  asyncRetryTransactionMiddleware(updateVideoChannelAvatar)
 )
 
 videoChannelRouter.put('/:nameWithHost',
@@ -107,13 +107,9 @@ async function updateVideoChannelAvatar (req: express.Request, res: express.Resp
   const videoChannel = res.locals.videoChannel as VideoChannelModel
   const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON())
 
-  const avatar = await updateActorAvatarFile(avatarPhysicalFile, videoChannel.Actor, videoChannel)
+  const avatar = await updateActorAvatarFile(avatarPhysicalFile, videoChannel)
 
-  auditLogger.update(
-    getAuditIdFromRes(res),
-    new VideoChannelAuditView(videoChannel.toFormattedJSON()),
-    oldVideoChannelAuditKeys
-  )
+  auditLogger.update(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannel.toFormattedJSON()), oldVideoChannelAuditKeys)
 
   return res
     .json({
index 5cfb81fc75e17b8b89f37b28a42deb0c87d89b6d..14f0a05f58829f5d93a175c2e616392ce9cde1ec 100644 (file)
@@ -3,23 +3,18 @@ import { sendUpdateActor } from './activitypub/send'
 import { AVATARS_SIZE, CONFIG, sequelizeTypescript } from '../initializers'
 import { updateActorAvatarInstance } from './activitypub'
 import { processImage } from '../helpers/image-utils'
-import { ActorModel } from '../models/activitypub/actor'
 import { AccountModel } from '../models/account/account'
 import { VideoChannelModel } from '../models/video/video-channel'
 import { extname, join } from 'path'
 
-async function updateActorAvatarFile (
-  avatarPhysicalFile: Express.Multer.File,
-  actor: ActorModel,
-  accountOrChannel: AccountModel | VideoChannelModel
-) {
+async function updateActorAvatarFile (avatarPhysicalFile: Express.Multer.File, accountOrChannel: AccountModel | VideoChannelModel) {
   const extension = extname(avatarPhysicalFile.filename)
-  const avatarName = actor.uuid + extension
+  const avatarName = accountOrChannel.Actor.uuid + extension
   const destination = join(CONFIG.STORAGE.AVATARS_DIR, avatarName)
   await processImage(avatarPhysicalFile, destination, AVATARS_SIZE)
 
   return sequelizeTypescript.transaction(async t => {
-    const updatedActor = await updateActorAvatarInstance(actor, avatarName, t)
+    const updatedActor = await updateActorAvatarInstance(accountOrChannel.Actor, avatarName, t)
     await updatedActor.save({ transaction: t })
 
     await sendUpdateActor(accountOrChannel, t)
index 2f8667e198bb05086fdf012b5caf439315bf50a1..5cbe60b82c626ca8148ee5545d2da7a05c90fcf3 100644 (file)
@@ -4,15 +4,50 @@ import { UserModel } from '../models/account/user'
 import { OAuthClientModel } from '../models/oauth/oauth-client'
 import { OAuthTokenModel } from '../models/oauth/oauth-token'
 import { CONFIG } from '../initializers/constants'
+import { Transaction } from 'sequelize'
 
 type TokenInfo = { accessToken: string, refreshToken: string, accessTokenExpiresAt: Date, refreshTokenExpiresAt: Date }
+const accessTokenCache: { [ accessToken: string ]: OAuthTokenModel } = {}
+const userHavingToken: { [ userId: number ]: string } = {}
 
 // ---------------------------------------------------------------------------
 
+function deleteUserToken (userId: number, t?: Transaction) {
+  clearCacheByUserId(userId)
+
+  return OAuthTokenModel.deleteUserToken(userId, t)
+}
+
+function clearCacheByUserId (userId: number) {
+  const token = userHavingToken[userId]
+  if (token !== undefined) {
+    accessTokenCache[ token ] = undefined
+    userHavingToken[ userId ] = undefined
+  }
+}
+
+function clearCacheByToken (token: string) {
+  const tokenModel = accessTokenCache[ token ]
+  if (tokenModel !== undefined) {
+    userHavingToken[tokenModel.userId] = undefined
+    accessTokenCache[ token ] = undefined
+  }
+}
+
 function getAccessToken (bearerToken: string) {
   logger.debug('Getting access token (bearerToken: ' + bearerToken + ').')
 
+  if (accessTokenCache[bearerToken] !== undefined) return accessTokenCache[bearerToken]
+
   return OAuthTokenModel.getByTokenAndPopulateUser(bearerToken)
+    .then(tokenModel => {
+      if (tokenModel) {
+        accessTokenCache[ bearerToken ] = tokenModel
+        userHavingToken[ tokenModel.userId ] = tokenModel.accessToken
+      }
+
+      return tokenModel
+    })
 }
 
 function getClient (clientId: string, clientSecret: string) {
@@ -48,6 +83,8 @@ async function getUser (usernameOrEmail: string, password: string) {
 async function revokeToken (tokenInfo: TokenInfo) {
   const token = await OAuthTokenModel.getByRefreshTokenAndPopulateUser(tokenInfo.refreshToken)
   if (token) {
+    clearCacheByToken(token.accessToken)
+
     token.destroy()
          .catch(err => logger.error('Cannot destroy token when revoking token.', { err }))
   }
@@ -85,6 +122,9 @@ async function saveToken (token: TokenInfo, client: OAuthClientModel, user: User
 
 // See https://github.com/oauthjs/node-oauth2-server/wiki/Model-specification for the model specifications
 export {
+  deleteUserToken,
+  clearCacheByUserId,
+  clearCacheByToken,
   getAccessToken,
   getClient,
   getRefreshToken,
index 680b1d52d9debb806818162f9de09ebf5972ec38..e56b0bf40b06bb988e8cf9c04f82ace36cd8ec50 100644 (file)
@@ -1,5 +1,7 @@
 import * as Sequelize from 'sequelize'
 import {
+  AfterDelete,
+  AfterUpdate,
   AllowNull,
   BeforeCreate,
   BeforeUpdate,
@@ -39,6 +41,7 @@ import { AccountModel } from './account'
 import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type'
 import { values } from 'lodash'
 import { NSFW_POLICY_TYPES } from '../../initializers'
+import { clearCacheByUserId } from '../../lib/oauth-model'
 
 enum ScopeNames {
   WITH_VIDEO_CHANNEL = 'WITH_VIDEO_CHANNEL'
@@ -168,6 +171,12 @@ export class UserModel extends Model<UserModel> {
     }
   }
 
+  @AfterUpdate
+  @AfterDelete
+  static removeTokenCache (instance: UserModel) {
+    return clearCacheByUserId(instance.id)
+  }
+
   static countTotal () {
     return this.count()
   }
index 1dd5e02891ebedd98d3da04f2287cf967d5777f7..ef9592c04fc50d9dfbcdf59ce8036c034c41c044 100644 (file)
@@ -1,10 +1,23 @@
-import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
+import {
+  AfterDelete,
+  AfterUpdate,
+  AllowNull,
+  BelongsTo,
+  Column,
+  CreatedAt,
+  ForeignKey,
+  Model,
+  Scopes,
+  Table,
+  UpdatedAt
+} from 'sequelize-typescript'
 import { logger } from '../../helpers/logger'
 import { UserModel } from '../account/user'
 import { OAuthClientModel } from './oauth-client'
 import { Transaction } from 'sequelize'
 import { AccountModel } from '../account/account'
 import { ActorModel } from '../activitypub/actor'
+import { clearCacheByToken } from '../../lib/oauth-model'
 
 export type OAuthTokenInfo = {
   refreshToken: string
@@ -112,6 +125,12 @@ export class OAuthTokenModel extends Model<OAuthTokenModel> {
   })
   OAuthClients: OAuthClientModel[]
 
+  @AfterUpdate
+  @AfterDelete
+  static removeTokenCache (token: OAuthTokenModel) {
+    return clearCacheByToken(token.accessToken)
+  }
+
   static getByRefreshTokenAndPopulateClient (refreshToken: string) {
     const query = {
       where: {