Add last login date to users
authorChocobozzz <me@florianbigard.com>
Thu, 7 May 2020 08:39:09 +0000 (10:39 +0200)
committerChocobozzz <me@florianbigard.com>
Thu, 7 May 2020 08:39:09 +0000 (10:39 +0200)
client/src/app/shared/users/user.model.ts
server/controllers/api/server/stats.ts
server/initializers/constants.ts
server/initializers/migrations/0505-user-last-login-date.ts [new file with mode: 0644]
server/lib/oauth-model.ts
server/models/account/user.ts
server/tests/api/server/stats.ts
server/tests/api/users/users.ts
shared/models/server/server-stats.model.ts
shared/models/users/user.model.ts

index 3f6743befbd901435f3da560bc038973c931d22e..3348fe75f42db35ad1959622531a611b9cfb16af 100644 (file)
@@ -71,6 +71,8 @@ export class User implements UserServerModel {
 
   pluginAuth: string | null
 
+  lastLoginDate: Date | null
+
   createdAt: Date
 
   constructor (hash: Partial<UserServerModel>) {
@@ -115,6 +117,7 @@ export class User implements UserServerModel {
     this.createdAt = hash.createdAt
 
     this.pluginAuth = hash.pluginAuth
+    this.lastLoginDate = hash.lastLoginDate
 
     if (hash.account !== undefined) {
       this.account = new Account(hash.account)
index f6a85d0c066d3cebcdd4cb209586ece23e853890..f07301a04791ca29eaaba5cd36929674e20ec8d7 100644 (file)
@@ -22,7 +22,7 @@ statsRouter.get('/stats',
 async function getStats (req: express.Request, res: express.Response) {
   const { totalLocalVideos, totalLocalVideoViews, totalVideos } = await VideoModel.getStats()
   const { totalLocalVideoComments, totalVideoComments } = await VideoCommentModel.getStats()
-  const { totalUsers } = await UserModel.getStats()
+  const { totalUsers, totalDailyActiveUsers, totalWeeklyActiveUsers, totalMonthlyActiveUsers } = await UserModel.getStats()
   const { totalInstanceFollowers, totalInstanceFollowing } = await ActorFollowModel.getStats()
   const { totalLocalVideoFilesSize } = await VideoFileModel.getStats()
 
@@ -48,9 +48,15 @@ async function getStats (req: express.Request, res: express.Response) {
     totalLocalVideoComments,
     totalVideos,
     totalVideoComments,
+
     totalUsers,
+    totalDailyActiveUsers,
+    totalWeeklyActiveUsers,
+    totalMonthlyActiveUsers,
+
     totalInstanceFollowers,
     totalInstanceFollowing,
+
     videosRedundancy: videosRedundancyStats
   }
 
index 298322e3d3e8854169235dd1de7bc6cf538845f5..13456071717889718ca6e48179a40da48778de83 100644 (file)
@@ -14,7 +14,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
 
 // ---------------------------------------------------------------------------
 
-const LAST_MIGRATION_VERSION = 500
+const LAST_MIGRATION_VERSION = 505
 
 // ---------------------------------------------------------------------------
 
diff --git a/server/initializers/migrations/0505-user-last-login-date.ts b/server/initializers/migrations/0505-user-last-login-date.ts
new file mode 100644 (file)
index 0000000..29d9708
--- /dev/null
@@ -0,0 +1,26 @@
+import * as Sequelize from 'sequelize'
+
+async function up (utils: {
+  transaction: Sequelize.Transaction
+  queryInterface: Sequelize.QueryInterface
+  sequelize: Sequelize.Sequelize
+}): Promise<void> {
+
+  {
+    const field = {
+      type: Sequelize.DATE,
+      allowNull: true
+    }
+    await utils.queryInterface.addColumn('user', 'lastLoginDate', field)
+  }
+
+}
+
+function down (options) {
+  throw new Error('Not implemented.')
+}
+
+export {
+  up,
+  down
+}
index 136abd0c45746db4af8bebca9ce3deeb655903cd..dbcba897a39d9fb037232251c8c9c19d3fa8abcb 100644 (file)
@@ -180,6 +180,10 @@ async function saveToken (token: TokenInfo, client: OAuthClientModel, user: User
   }
 
   const tokenCreated = await OAuthTokenModel.create(tokenToCreate)
+
+  user.lastLoginDate = new Date()
+  await user.save()
+
   return Object.assign(tokenCreated, { client, user })
 }
 
index 260c1b28eba443921817f28a31ab01cb17b4aef6..fbd3080c6a82c5803c716f970bb4ea3d083537b4 100644 (file)
@@ -353,6 +353,11 @@ export class UserModel extends Model<UserModel> {
   @Column
   pluginAuth: string
 
+  @AllowNull(true)
+  @Default(null)
+  @Column
+  lastLoginDate: Date
+
   @CreatedAt
   createdAt: Date
 
@@ -691,10 +696,28 @@ export class UserModel extends Model<UserModel> {
   }
 
   static async getStats () {
+    function getActiveUsers (days: number) {
+      const query = {
+        where: {
+          [Op.and]: [
+            literal(`"lastLoginDate" > NOW() - INTERVAL '${days}d'`)
+          ]
+        }
+      }
+
+      return UserModel.count(query)
+    }
+
     const totalUsers = await UserModel.count()
+    const totalDailyActiveUsers = await getActiveUsers(1)
+    const totalWeeklyActiveUsers = await getActiveUsers(7)
+    const totalMonthlyActiveUsers = await getActiveUsers(30)
 
     return {
-      totalUsers
+      totalUsers,
+      totalDailyActiveUsers,
+      totalWeeklyActiveUsers,
+      totalMonthlyActiveUsers
     }
   }
 
@@ -808,7 +831,9 @@ export class UserModel extends Model<UserModel> {
 
       createdAt: this.createdAt,
 
-      pluginAuth: this.pluginAuth
+      pluginAuth: this.pluginAuth,
+
+      lastLoginDate: this.lastLoginDate
     }
 
     if (parameters.withAdminFlags) {
index fe956413c088e177db42bb484166981db8d40d71..637525ff8c97c9b6b8d84d152c7f264bd8b1540c 100644 (file)
@@ -12,7 +12,8 @@ import {
   ServerInfo, unfollow,
   uploadVideo,
   viewVideo,
-  wait
+  wait,
+  userLogin
 } from '../../../../shared/extra-utils'
 import { setAccessTokensToServers } from '../../../../shared/extra-utils/index'
 import { getStats } from '../../../../shared/extra-utils/server/stats'
@@ -23,6 +24,10 @@ const expect = chai.expect
 
 describe('Test stats (excluding redundancy)', function () {
   let servers: ServerInfo[] = []
+  const user = {
+    username: 'user1',
+    password: 'super_password'
+  }
 
   before(async function () {
     this.timeout(60000)
@@ -31,10 +36,6 @@ describe('Test stats (excluding redundancy)', function () {
 
     await doubleFollow(servers[0], servers[1])
 
-    const user = {
-      username: 'user1',
-      password: 'super_password'
-    }
     await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username: user.username, password: user.password })
 
     const resVideo = await uploadVideo(servers[0].url, servers[0].accessToken, { fixture: 'video_short.webm' })
@@ -96,6 +97,8 @@ describe('Test stats (excluding redundancy)', function () {
   })
 
   it('Should have the correct total videos stats after an unfollow', async function () {
+    this.timeout(15000)
+
     await unfollow(servers[2].url, servers[2].accessToken, servers[0])
     await waitJobs(servers)
 
@@ -105,6 +108,28 @@ describe('Test stats (excluding redundancy)', function () {
     expect(data.totalVideos).to.equal(0)
   })
 
+  it('Should have the correct active users stats', async function () {
+    const server = servers[0]
+
+    {
+      const res = await getStats(server.url)
+      const data: ServerStats = res.body
+      expect(data.totalDailyActiveUsers).to.equal(1)
+      expect(data.totalWeeklyActiveUsers).to.equal(1)
+      expect(data.totalMonthlyActiveUsers).to.equal(1)
+    }
+
+    {
+      await userLogin(server, user)
+
+      const res = await getStats(server.url)
+      const data: ServerStats = res.body
+      expect(data.totalDailyActiveUsers).to.equal(2)
+      expect(data.totalWeeklyActiveUsers).to.equal(2)
+      expect(data.totalMonthlyActiveUsers).to.equal(2)
+    }
+  })
+
   after(async function () {
     await cleanupTests(servers)
   })
index f3b73263256930cd5238d1ffd4119953d1d3949e..c0cbce36099b0db6ca760d95d3d0981c5cb2f6c8 100644 (file)
@@ -418,6 +418,9 @@ describe('Test users', function () {
       expect(rootUser.email).to.equal('admin' + server.internalServerNumber + '@example.com')
       expect(user.nsfwPolicy).to.equal('display')
 
+      expect(rootUser.lastLoginDate).to.exist
+      expect(user.lastLoginDate).to.exist
+
       userId = user.id
     })
 
index 11778e6ed5da3b93e43770eb5ccd9269bef7b714..75d7dc554aa92b95534503b093ea20080e8fcd6b 100644 (file)
@@ -2,6 +2,10 @@ import { VideoRedundancyStrategyWithManual } from '../redundancy'
 
 export interface ServerStats {
   totalUsers: number
+  totalDailyActiveUsers: number
+  totalWeeklyActiveUsers: number
+  totalMonthlyActiveUsers: number
+
   totalLocalVideos: number
   totalLocalVideoViews: number
   totalLocalVideoComments: number
index 42be042894b4e2249831faf921f30ad131668d22..6c959ceeaa690256c195400317ec71603e88abbc 100644 (file)
@@ -52,6 +52,8 @@ export interface User {
   createdAt: Date
 
   pluginAuth: string | null
+
+  lastLoginDate: Date | null
 }
 
 export interface MyUserSpecialPlaylist {