diff options
-rw-r--r-- | client/src/app/shared/users/user.model.ts | 3 | ||||
-rw-r--r-- | server/controllers/api/server/stats.ts | 8 | ||||
-rw-r--r-- | server/initializers/constants.ts | 2 | ||||
-rw-r--r-- | server/initializers/migrations/0505-user-last-login-date.ts | 26 | ||||
-rw-r--r-- | server/lib/oauth-model.ts | 4 | ||||
-rw-r--r-- | server/models/account/user.ts | 29 | ||||
-rw-r--r-- | server/tests/api/server/stats.ts | 35 | ||||
-rw-r--r-- | server/tests/api/users/users.ts | 3 | ||||
-rw-r--r-- | shared/models/server/server-stats.model.ts | 4 | ||||
-rw-r--r-- | shared/models/users/user.model.ts | 2 |
10 files changed, 107 insertions, 9 deletions
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 { | |||
71 | 71 | ||
72 | pluginAuth: string | null | 72 | pluginAuth: string | null |
73 | 73 | ||
74 | lastLoginDate: Date | null | ||
75 | |||
74 | createdAt: Date | 76 | createdAt: Date |
75 | 77 | ||
76 | constructor (hash: Partial<UserServerModel>) { | 78 | constructor (hash: Partial<UserServerModel>) { |
@@ -115,6 +117,7 @@ export class User implements UserServerModel { | |||
115 | this.createdAt = hash.createdAt | 117 | this.createdAt = hash.createdAt |
116 | 118 | ||
117 | this.pluginAuth = hash.pluginAuth | 119 | this.pluginAuth = hash.pluginAuth |
120 | this.lastLoginDate = hash.lastLoginDate | ||
118 | 121 | ||
119 | if (hash.account !== undefined) { | 122 | if (hash.account !== undefined) { |
120 | this.account = new Account(hash.account) | 123 | 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', | |||
22 | async function getStats (req: express.Request, res: express.Response) { | 22 | async function getStats (req: express.Request, res: express.Response) { |
23 | const { totalLocalVideos, totalLocalVideoViews, totalVideos } = await VideoModel.getStats() | 23 | const { totalLocalVideos, totalLocalVideoViews, totalVideos } = await VideoModel.getStats() |
24 | const { totalLocalVideoComments, totalVideoComments } = await VideoCommentModel.getStats() | 24 | const { totalLocalVideoComments, totalVideoComments } = await VideoCommentModel.getStats() |
25 | const { totalUsers } = await UserModel.getStats() | 25 | const { totalUsers, totalDailyActiveUsers, totalWeeklyActiveUsers, totalMonthlyActiveUsers } = await UserModel.getStats() |
26 | const { totalInstanceFollowers, totalInstanceFollowing } = await ActorFollowModel.getStats() | 26 | const { totalInstanceFollowers, totalInstanceFollowing } = await ActorFollowModel.getStats() |
27 | const { totalLocalVideoFilesSize } = await VideoFileModel.getStats() | 27 | const { totalLocalVideoFilesSize } = await VideoFileModel.getStats() |
28 | 28 | ||
@@ -48,9 +48,15 @@ async function getStats (req: express.Request, res: express.Response) { | |||
48 | totalLocalVideoComments, | 48 | totalLocalVideoComments, |
49 | totalVideos, | 49 | totalVideos, |
50 | totalVideoComments, | 50 | totalVideoComments, |
51 | |||
51 | totalUsers, | 52 | totalUsers, |
53 | totalDailyActiveUsers, | ||
54 | totalWeeklyActiveUsers, | ||
55 | totalMonthlyActiveUsers, | ||
56 | |||
52 | totalInstanceFollowers, | 57 | totalInstanceFollowers, |
53 | totalInstanceFollowing, | 58 | totalInstanceFollowing, |
59 | |||
54 | videosRedundancy: videosRedundancyStats | 60 | videosRedundancy: videosRedundancyStats |
55 | } | 61 | } |
56 | 62 | ||
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' | |||
14 | 14 | ||
15 | // --------------------------------------------------------------------------- | 15 | // --------------------------------------------------------------------------- |
16 | 16 | ||
17 | const LAST_MIGRATION_VERSION = 500 | 17 | const LAST_MIGRATION_VERSION = 505 |
18 | 18 | ||
19 | // --------------------------------------------------------------------------- | 19 | // --------------------------------------------------------------------------- |
20 | 20 | ||
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 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async function up (utils: { | ||
4 | transaction: Sequelize.Transaction | ||
5 | queryInterface: Sequelize.QueryInterface | ||
6 | sequelize: Sequelize.Sequelize | ||
7 | }): Promise<void> { | ||
8 | |||
9 | { | ||
10 | const field = { | ||
11 | type: Sequelize.DATE, | ||
12 | allowNull: true | ||
13 | } | ||
14 | await utils.queryInterface.addColumn('user', 'lastLoginDate', field) | ||
15 | } | ||
16 | |||
17 | } | ||
18 | |||
19 | function down (options) { | ||
20 | throw new Error('Not implemented.') | ||
21 | } | ||
22 | |||
23 | export { | ||
24 | up, | ||
25 | down | ||
26 | } | ||
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 | |||
180 | } | 180 | } |
181 | 181 | ||
182 | const tokenCreated = await OAuthTokenModel.create(tokenToCreate) | 182 | const tokenCreated = await OAuthTokenModel.create(tokenToCreate) |
183 | |||
184 | user.lastLoginDate = new Date() | ||
185 | await user.save() | ||
186 | |||
183 | return Object.assign(tokenCreated, { client, user }) | 187 | return Object.assign(tokenCreated, { client, user }) |
184 | } | 188 | } |
185 | 189 | ||
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<UserModel> { | |||
353 | @Column | 353 | @Column |
354 | pluginAuth: string | 354 | pluginAuth: string |
355 | 355 | ||
356 | @AllowNull(true) | ||
357 | @Default(null) | ||
358 | @Column | ||
359 | lastLoginDate: Date | ||
360 | |||
356 | @CreatedAt | 361 | @CreatedAt |
357 | createdAt: Date | 362 | createdAt: Date |
358 | 363 | ||
@@ -691,10 +696,28 @@ export class UserModel extends Model<UserModel> { | |||
691 | } | 696 | } |
692 | 697 | ||
693 | static async getStats () { | 698 | static async getStats () { |
699 | function getActiveUsers (days: number) { | ||
700 | const query = { | ||
701 | where: { | ||
702 | [Op.and]: [ | ||
703 | literal(`"lastLoginDate" > NOW() - INTERVAL '${days}d'`) | ||
704 | ] | ||
705 | } | ||
706 | } | ||
707 | |||
708 | return UserModel.count(query) | ||
709 | } | ||
710 | |||
694 | const totalUsers = await UserModel.count() | 711 | const totalUsers = await UserModel.count() |
712 | const totalDailyActiveUsers = await getActiveUsers(1) | ||
713 | const totalWeeklyActiveUsers = await getActiveUsers(7) | ||
714 | const totalMonthlyActiveUsers = await getActiveUsers(30) | ||
695 | 715 | ||
696 | return { | 716 | return { |
697 | totalUsers | 717 | totalUsers, |
718 | totalDailyActiveUsers, | ||
719 | totalWeeklyActiveUsers, | ||
720 | totalMonthlyActiveUsers | ||
698 | } | 721 | } |
699 | } | 722 | } |
700 | 723 | ||
@@ -808,7 +831,9 @@ export class UserModel extends Model<UserModel> { | |||
808 | 831 | ||
809 | createdAt: this.createdAt, | 832 | createdAt: this.createdAt, |
810 | 833 | ||
811 | pluginAuth: this.pluginAuth | 834 | pluginAuth: this.pluginAuth, |
835 | |||
836 | lastLoginDate: this.lastLoginDate | ||
812 | } | 837 | } |
813 | 838 | ||
814 | if (parameters.withAdminFlags) { | 839 | 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 { | |||
12 | ServerInfo, unfollow, | 12 | ServerInfo, unfollow, |
13 | uploadVideo, | 13 | uploadVideo, |
14 | viewVideo, | 14 | viewVideo, |
15 | wait | 15 | wait, |
16 | userLogin | ||
16 | } from '../../../../shared/extra-utils' | 17 | } from '../../../../shared/extra-utils' |
17 | import { setAccessTokensToServers } from '../../../../shared/extra-utils/index' | 18 | import { setAccessTokensToServers } from '../../../../shared/extra-utils/index' |
18 | import { getStats } from '../../../../shared/extra-utils/server/stats' | 19 | import { getStats } from '../../../../shared/extra-utils/server/stats' |
@@ -23,6 +24,10 @@ const expect = chai.expect | |||
23 | 24 | ||
24 | describe('Test stats (excluding redundancy)', function () { | 25 | describe('Test stats (excluding redundancy)', function () { |
25 | let servers: ServerInfo[] = [] | 26 | let servers: ServerInfo[] = [] |
27 | const user = { | ||
28 | username: 'user1', | ||
29 | password: 'super_password' | ||
30 | } | ||
26 | 31 | ||
27 | before(async function () { | 32 | before(async function () { |
28 | this.timeout(60000) | 33 | this.timeout(60000) |
@@ -31,10 +36,6 @@ describe('Test stats (excluding redundancy)', function () { | |||
31 | 36 | ||
32 | await doubleFollow(servers[0], servers[1]) | 37 | await doubleFollow(servers[0], servers[1]) |
33 | 38 | ||
34 | const user = { | ||
35 | username: 'user1', | ||
36 | password: 'super_password' | ||
37 | } | ||
38 | await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username: user.username, password: user.password }) | 39 | await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username: user.username, password: user.password }) |
39 | 40 | ||
40 | const resVideo = await uploadVideo(servers[0].url, servers[0].accessToken, { fixture: 'video_short.webm' }) | 41 | const resVideo = await uploadVideo(servers[0].url, servers[0].accessToken, { fixture: 'video_short.webm' }) |
@@ -96,6 +97,8 @@ describe('Test stats (excluding redundancy)', function () { | |||
96 | }) | 97 | }) |
97 | 98 | ||
98 | it('Should have the correct total videos stats after an unfollow', async function () { | 99 | it('Should have the correct total videos stats after an unfollow', async function () { |
100 | this.timeout(15000) | ||
101 | |||
99 | await unfollow(servers[2].url, servers[2].accessToken, servers[0]) | 102 | await unfollow(servers[2].url, servers[2].accessToken, servers[0]) |
100 | await waitJobs(servers) | 103 | await waitJobs(servers) |
101 | 104 | ||
@@ -105,6 +108,28 @@ describe('Test stats (excluding redundancy)', function () { | |||
105 | expect(data.totalVideos).to.equal(0) | 108 | expect(data.totalVideos).to.equal(0) |
106 | }) | 109 | }) |
107 | 110 | ||
111 | it('Should have the correct active users stats', async function () { | ||
112 | const server = servers[0] | ||
113 | |||
114 | { | ||
115 | const res = await getStats(server.url) | ||
116 | const data: ServerStats = res.body | ||
117 | expect(data.totalDailyActiveUsers).to.equal(1) | ||
118 | expect(data.totalWeeklyActiveUsers).to.equal(1) | ||
119 | expect(data.totalMonthlyActiveUsers).to.equal(1) | ||
120 | } | ||
121 | |||
122 | { | ||
123 | await userLogin(server, user) | ||
124 | |||
125 | const res = await getStats(server.url) | ||
126 | const data: ServerStats = res.body | ||
127 | expect(data.totalDailyActiveUsers).to.equal(2) | ||
128 | expect(data.totalWeeklyActiveUsers).to.equal(2) | ||
129 | expect(data.totalMonthlyActiveUsers).to.equal(2) | ||
130 | } | ||
131 | }) | ||
132 | |||
108 | after(async function () { | 133 | after(async function () { |
109 | await cleanupTests(servers) | 134 | await cleanupTests(servers) |
110 | }) | 135 | }) |
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 () { | |||
418 | expect(rootUser.email).to.equal('admin' + server.internalServerNumber + '@example.com') | 418 | expect(rootUser.email).to.equal('admin' + server.internalServerNumber + '@example.com') |
419 | expect(user.nsfwPolicy).to.equal('display') | 419 | expect(user.nsfwPolicy).to.equal('display') |
420 | 420 | ||
421 | expect(rootUser.lastLoginDate).to.exist | ||
422 | expect(user.lastLoginDate).to.exist | ||
423 | |||
421 | userId = user.id | 424 | userId = user.id |
422 | }) | 425 | }) |
423 | 426 | ||
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' | |||
2 | 2 | ||
3 | export interface ServerStats { | 3 | export interface ServerStats { |
4 | totalUsers: number | 4 | totalUsers: number |
5 | totalDailyActiveUsers: number | ||
6 | totalWeeklyActiveUsers: number | ||
7 | totalMonthlyActiveUsers: number | ||
8 | |||
5 | totalLocalVideos: number | 9 | totalLocalVideos: number |
6 | totalLocalVideoViews: number | 10 | totalLocalVideoViews: number |
7 | totalLocalVideoComments: number | 11 | 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 { | |||
52 | createdAt: Date | 52 | createdAt: Date |
53 | 53 | ||
54 | pluginAuth: string | null | 54 | pluginAuth: string | null |
55 | |||
56 | lastLoginDate: Date | null | ||
55 | } | 57 | } |
56 | 58 | ||
57 | export interface MyUserSpecialPlaylist { | 59 | export interface MyUserSpecialPlaylist { |