From 3cc665f48fd233d09f778d7e887488dde6f03ef6 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 7 May 2020 10:39:09 +0200 Subject: [PATCH] Add last login date to users --- client/src/app/shared/users/user.model.ts | 3 ++ server/controllers/api/server/stats.ts | 8 ++++- server/initializers/constants.ts | 2 +- .../migrations/0505-user-last-login-date.ts | 26 ++++++++++++++ server/lib/oauth-model.ts | 4 +++ server/models/account/user.ts | 29 +++++++++++++-- server/tests/api/server/stats.ts | 35 ++++++++++++++++--- server/tests/api/users/users.ts | 3 ++ shared/models/server/server-stats.model.ts | 4 +++ shared/models/users/user.model.ts | 2 ++ 10 files changed, 107 insertions(+), 9 deletions(-) create mode 100644 server/initializers/migrations/0505-user-last-login-date.ts diff --git a/client/src/app/shared/users/user.model.ts b/client/src/app/shared/users/user.model.ts index 3f6743bef..3348fe75f 100644 --- a/client/src/app/shared/users/user.model.ts +++ b/client/src/app/shared/users/user.model.ts @@ -71,6 +71,8 @@ export class User implements UserServerModel { pluginAuth: string | null + lastLoginDate: Date | null + createdAt: Date constructor (hash: Partial) { @@ -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) diff --git a/server/controllers/api/server/stats.ts b/server/controllers/api/server/stats.ts index f6a85d0c0..f07301a04 100644 --- a/server/controllers/api/server/stats.ts +++ b/server/controllers/api/server/stats.ts @@ -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 } diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 298322e3d..134560717 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -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 index 000000000..29d970802 --- /dev/null +++ b/server/initializers/migrations/0505-user-last-login-date.ts @@ -0,0 +1,26 @@ +import * as Sequelize from 'sequelize' + +async function up (utils: { + transaction: Sequelize.Transaction + queryInterface: Sequelize.QueryInterface + sequelize: Sequelize.Sequelize +}): Promise { + + { + 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 +} diff --git a/server/lib/oauth-model.ts b/server/lib/oauth-model.ts index 136abd0c4..dbcba897a 100644 --- a/server/lib/oauth-model.ts +++ b/server/lib/oauth-model.ts @@ -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 }) } diff --git a/server/models/account/user.ts b/server/models/account/user.ts index 260c1b28e..fbd3080c6 100644 --- a/server/models/account/user.ts +++ b/server/models/account/user.ts @@ -353,6 +353,11 @@ export class UserModel extends Model { @Column pluginAuth: string + @AllowNull(true) + @Default(null) + @Column + lastLoginDate: Date + @CreatedAt createdAt: Date @@ -691,10 +696,28 @@ export class UserModel extends Model { } 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 { createdAt: this.createdAt, - pluginAuth: this.pluginAuth + pluginAuth: this.pluginAuth, + + lastLoginDate: this.lastLoginDate } if (parameters.withAdminFlags) { diff --git a/server/tests/api/server/stats.ts b/server/tests/api/server/stats.ts index fe956413c..637525ff8 100644 --- a/server/tests/api/server/stats.ts +++ b/server/tests/api/server/stats.ts @@ -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) }) diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts index f3b732632..c0cbce360 100644 --- a/server/tests/api/users/users.ts +++ b/server/tests/api/users/users.ts @@ -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 }) diff --git a/shared/models/server/server-stats.model.ts b/shared/models/server/server-stats.model.ts index 11778e6ed..75d7dc554 100644 --- a/shared/models/server/server-stats.model.ts +++ b/shared/models/server/server-stats.model.ts @@ -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 diff --git a/shared/models/users/user.model.ts b/shared/models/users/user.model.ts index 42be04289..6c959ceea 100644 --- a/shared/models/users/user.model.ts +++ b/shared/models/users/user.model.ts @@ -52,6 +52,8 @@ export interface User { createdAt: Date pluginAuth: string | null + + lastLoginDate: Date | null } export interface MyUserSpecialPlaylist {