From 643dacafbfcfb195783a92b387004c9ddb6da5d2 Mon Sep 17 00:00:00 2001 From: CamilleLegeron Date: Mon, 15 Apr 2024 15:20:16 +0200 Subject: [PATCH] Create user last connection datetime --- app/gen-server/entity/User.ts | 3 +++ app/gen-server/lib/HomeDBManager.ts | 18 +++++++++++++++--- .../1713186031023-UserLastConnection.ts | 18 ++++++++++++++++++ app/server/lib/Client.ts | 4 ++++ app/server/lib/requestUtils.ts | 6 +++--- test/gen-server/migrations.ts | 4 +++- 6 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 app/gen-server/migration/1713186031023-UserLastConnection.ts diff --git a/app/gen-server/entity/User.ts b/app/gen-server/entity/User.ts index 2ed10169..c93837cb 100644 --- a/app/gen-server/entity/User.ts +++ b/app/gen-server/entity/User.ts @@ -29,6 +29,9 @@ export class User extends BaseEntity { @Column({name: 'first_login_at', type: Date, nullable: true}) public firstLoginAt: Date | null; + @Column({name: 'last_connection_at', type: Date, nullable: true}) + public lastConnectionAt: Date | null; + @OneToOne(type => Organization, organization => organization.owner) public personalOrg: Organization; diff --git a/app/gen-server/lib/HomeDBManager.ts b/app/gen-server/lib/HomeDBManager.ts index 1a46573b..e5fc2eeb 100644 --- a/app/gen-server/lib/HomeDBManager.ts +++ b/app/gen-server/lib/HomeDBManager.ts @@ -213,6 +213,7 @@ function isNonGuestGroup(group: Group): group is NonGuestGroup { export interface UserProfileChange { name?: string; isFirstTimeUser?: boolean; + newConnection?: boolean; } // Identifies a request to access a document. This combination of values is also used for caching @@ -614,6 +615,14 @@ export class HomeDBManager extends EventEmitter { // any automation for first logins if (!props.isFirstTimeUser) { isWelcomed = true; } } + if (props.newConnection === true) { + // set last connection time to now (remove milliseconds for compatibility with other + // timestamps in db set by typeorm, and since second level precision is fine) + const nowish = new Date(); + nowish.setMilliseconds(0); + user.lastConnectionAt = nowish; + needsSave = true; + } if (needsSave) { await user.save(); } @@ -701,12 +710,15 @@ export class HomeDBManager extends EventEmitter { user.name = (profile && (profile.name || email.split('@')[0])) || ''; needUpdate = true; } - if (profile && !user.firstLoginAt) { - // set first login time to now (remove milliseconds for compatibility with other + if (profile) { + // set first login time and last connection time to now (remove milliseconds for compatibility with other // timestamps in db set by typeorm, and since second level precision is fine) const nowish = new Date(); nowish.setMilliseconds(0); - user.firstLoginAt = nowish; + user.lastConnectionAt = nowish; + if (!user.firstLoginAt) { + user.firstLoginAt = nowish; + } needUpdate = true; } if (!user.picture && profile && profile.picture) { diff --git a/app/gen-server/migration/1713186031023-UserLastConnection.ts b/app/gen-server/migration/1713186031023-UserLastConnection.ts new file mode 100644 index 00000000..e1688502 --- /dev/null +++ b/app/gen-server/migration/1713186031023-UserLastConnection.ts @@ -0,0 +1,18 @@ +import { MigrationInterface, QueryRunner, TableColumn} from 'typeorm'; + +export class UserLastConnection1713186031023 implements MigrationInterface { + + public async up(queryRunner: QueryRunner): Promise { + const sqlite = queryRunner.connection.driver.options.type === 'sqlite'; + const datetime = sqlite ? "datetime" : "timestamp with time zone"; + await queryRunner.addColumn('users', new TableColumn({ + name: 'last_connection_at', + type: datetime, + isNullable: true + })); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropColumn('users', 'last_connection_at'); + } +} diff --git a/app/server/lib/Client.ts b/app/server/lib/Client.ts index 0364ca36..ca2ba8aa 100644 --- a/app/server/lib/Client.ts +++ b/app/server/lib/Client.ts @@ -501,6 +501,10 @@ export class Client { const user = this._profile ? await this._fetchUser(dbManager) : dbManager.getAnonymousUser(); this._user = user ? dbManager.makeFullUser(user) : undefined; this._firstLoginAt = user?.firstLoginAt || null; + if (this._user) { + // Send the information to the dbManager that the user has a new activity + await dbManager.updateUser(this._user.id, {newConnection: true}); + } } private async _onMessage(message: string): Promise { diff --git a/app/server/lib/requestUtils.ts b/app/server/lib/requestUtils.ts index a20923f5..274ebc6c 100644 --- a/app/server/lib/requestUtils.ts +++ b/app/server/lib/requestUtils.ts @@ -21,9 +21,9 @@ export const TEST_HTTPS_OFFSET = process.env.GRIST_TEST_HTTPS_OFFSET ? // Database fields that we permit in entities but don't want to cross the api. const INTERNAL_FIELDS = new Set([ - 'apiKey', 'billingAccountId', 'firstLoginAt', 'filteredOut', 'ownerId', 'gracePeriodStart', 'stripeCustomerId', - 'stripeSubscriptionId', 'stripePlanId', 'stripeProductId', 'userId', 'isFirstTimeUser', 'allowGoogleLogin', - 'authSubject', 'usage', 'createdBy' + 'apiKey', 'billingAccountId', 'firstLoginAt', 'lastConnectionAt', 'filteredOut', 'ownerId', 'gracePeriodStart', + 'stripeCustomerId', 'stripeSubscriptionId', 'stripePlanId', 'stripeProductId', 'userId', 'isFirstTimeUser', + 'allowGoogleLogin', 'authSubject', 'usage', 'createdBy' ]); /** diff --git a/test/gen-server/migrations.ts b/test/gen-server/migrations.ts index 0dd60c16..a7faeb39 100644 --- a/test/gen-server/migrations.ts +++ b/test/gen-server/migrations.ts @@ -41,6 +41,8 @@ import {ForkIndexes1678737195050 as ForkIndexes} from 'app/gen-server/migration/ import {ActivationPrefs1682636695021 as ActivationPrefs} from 'app/gen-server/migration/1682636695021-ActivationPrefs'; import {AssistantLimit1685343047786 as AssistantLimit} from 'app/gen-server/migration/1685343047786-AssistantLimit'; import {Shares1701557445716 as Shares} from 'app/gen-server/migration/1701557445716-Shares'; +import {UserLastConnection1713186031023 + as UserLastConnection} from 'app/gen-server/migration/1713186031023-UserLastConnection'; const home: HomeDBManager = new HomeDBManager(); @@ -49,7 +51,7 @@ const migrations = [Initial, Login, PinDocs, UserPicture, DisplayEmail, DisplayE CustomerIndex, ExtraIndexes, OrgHost, DocRemovedAt, Prefs, ExternalBilling, DocOptions, Secret, UserOptions, GracePeriodStart, DocumentUsage, Activations, UserConnectId, UserUUID, UserUniqueRefUUID, - Forks, ForkIndexes, ActivationPrefs, AssistantLimit, Shares]; + Forks, ForkIndexes, ActivationPrefs, AssistantLimit, Shares, UserLastConnection]; // Assert that the "members" acl rule and group exist (or not). function assertMembersGroup(org: Organization, exists: boolean) {