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..835484ef 100644 --- a/app/gen-server/lib/HomeDBManager.ts +++ b/app/gen-server/lib/HomeDBManager.ts @@ -72,6 +72,7 @@ import { import uuidv4 from "uuid/v4"; import flatten = require('lodash/flatten'); import pick = require('lodash/pick'); +import moment = require('moment-timezone'); // Support transactions in Sqlite in async code. This is a monkey patch, affecting // the prototypes of various TypeORM classes. @@ -213,6 +214,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 +616,14 @@ export class HomeDBManager extends EventEmitter { // any automation for first logins if (!props.isFirstTimeUser) { isWelcomed = true; } } + if (props.newConnection === true) { + // set last connection to today (need date only, no time) + const today = moment().startOf('day'); + if (today !== moment(user.lastConnectionAt).startOf('day')) { + user.lastConnectionAt = today.toDate(); + needsSave = true; + } + } if (needsSave) { await user.save(); } diff --git a/app/gen-server/migration/1713186031023-UserLastConnection.ts b/app/gen-server/migration/1713186031023-UserLastConnection.ts new file mode 100644 index 00000000..793c0638 --- /dev/null +++ b/app/gen-server/migration/1713186031023-UserLastConnection.ts @@ -0,0 +1,16 @@ +import { MigrationInterface, QueryRunner, TableColumn} from 'typeorm'; + +export class UserLastConnection1713186031023 implements MigrationInterface { + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.addColumn('users', new TableColumn({ + name: 'last_connection_at', + type: "date", + 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 6424d6d9..c7b69cb1 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) {