Create user last connection datetime

This commit is contained in:
CamilleLegeron 2024-04-15 15:20:16 +02:00
parent fe9cc80ccc
commit 643dacafbf
6 changed files with 46 additions and 7 deletions

View File

@ -29,6 +29,9 @@ export class User extends BaseEntity {
@Column({name: 'first_login_at', type: Date, nullable: true}) @Column({name: 'first_login_at', type: Date, nullable: true})
public firstLoginAt: Date | null; public firstLoginAt: Date | null;
@Column({name: 'last_connection_at', type: Date, nullable: true})
public lastConnectionAt: Date | null;
@OneToOne(type => Organization, organization => organization.owner) @OneToOne(type => Organization, organization => organization.owner)
public personalOrg: Organization; public personalOrg: Organization;

View File

@ -213,6 +213,7 @@ function isNonGuestGroup(group: Group): group is NonGuestGroup {
export interface UserProfileChange { export interface UserProfileChange {
name?: string; name?: string;
isFirstTimeUser?: boolean; isFirstTimeUser?: boolean;
newConnection?: boolean;
} }
// Identifies a request to access a document. This combination of values is also used for caching // 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 // any automation for first logins
if (!props.isFirstTimeUser) { isWelcomed = true; } 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) { if (needsSave) {
await user.save(); await user.save();
} }
@ -701,12 +710,15 @@ export class HomeDBManager extends EventEmitter {
user.name = (profile && (profile.name || email.split('@')[0])) || ''; user.name = (profile && (profile.name || email.split('@')[0])) || '';
needUpdate = true; needUpdate = true;
} }
if (profile && !user.firstLoginAt) { if (profile) {
// set first login time to now (remove milliseconds for compatibility with other // 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) // timestamps in db set by typeorm, and since second level precision is fine)
const nowish = new Date(); const nowish = new Date();
nowish.setMilliseconds(0); nowish.setMilliseconds(0);
user.lastConnectionAt = nowish;
if (!user.firstLoginAt) {
user.firstLoginAt = nowish; user.firstLoginAt = nowish;
}
needUpdate = true; needUpdate = true;
} }
if (!user.picture && profile && profile.picture) { if (!user.picture && profile && profile.picture) {

View File

@ -0,0 +1,18 @@
import { MigrationInterface, QueryRunner, TableColumn} from 'typeorm';
export class UserLastConnection1713186031023 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<any> {
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<any> {
await queryRunner.dropColumn('users', 'last_connection_at');
}
}

View File

@ -501,6 +501,10 @@ export class Client {
const user = this._profile ? await this._fetchUser(dbManager) : dbManager.getAnonymousUser(); const user = this._profile ? await this._fetchUser(dbManager) : dbManager.getAnonymousUser();
this._user = user ? dbManager.makeFullUser(user) : undefined; this._user = user ? dbManager.makeFullUser(user) : undefined;
this._firstLoginAt = user?.firstLoginAt || null; 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<void> { private async _onMessage(message: string): Promise<void> {

View File

@ -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. // Database fields that we permit in entities but don't want to cross the api.
const INTERNAL_FIELDS = new Set([ const INTERNAL_FIELDS = new Set([
'apiKey', 'billingAccountId', 'firstLoginAt', 'filteredOut', 'ownerId', 'gracePeriodStart', 'stripeCustomerId', 'apiKey', 'billingAccountId', 'firstLoginAt', 'lastConnectionAt', 'filteredOut', 'ownerId', 'gracePeriodStart',
'stripeSubscriptionId', 'stripePlanId', 'stripeProductId', 'userId', 'isFirstTimeUser', 'allowGoogleLogin', 'stripeCustomerId', 'stripeSubscriptionId', 'stripePlanId', 'stripeProductId', 'userId', 'isFirstTimeUser',
'authSubject', 'usage', 'createdBy' 'allowGoogleLogin', 'authSubject', 'usage', 'createdBy'
]); ]);
/** /**

View File

@ -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 {ActivationPrefs1682636695021 as ActivationPrefs} from 'app/gen-server/migration/1682636695021-ActivationPrefs';
import {AssistantLimit1685343047786 as AssistantLimit} from 'app/gen-server/migration/1685343047786-AssistantLimit'; import {AssistantLimit1685343047786 as AssistantLimit} from 'app/gen-server/migration/1685343047786-AssistantLimit';
import {Shares1701557445716 as Shares} from 'app/gen-server/migration/1701557445716-Shares'; 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(); const home: HomeDBManager = new HomeDBManager();
@ -49,7 +51,7 @@ const migrations = [Initial, Login, PinDocs, UserPicture, DisplayEmail, DisplayE
CustomerIndex, ExtraIndexes, OrgHost, DocRemovedAt, Prefs, CustomerIndex, ExtraIndexes, OrgHost, DocRemovedAt, Prefs,
ExternalBilling, DocOptions, Secret, UserOptions, GracePeriodStart, ExternalBilling, DocOptions, Secret, UserOptions, GracePeriodStart,
DocumentUsage, Activations, UserConnectId, UserUUID, UserUniqueRefUUID, 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). // Assert that the "members" acl rule and group exist (or not).
function assertMembersGroup(org: Organization, exists: boolean) { function assertMembersGroup(org: Organization, exists: boolean) {