Create user last connection datetime (#935)

Each time the a Grist page is reload the `last_connection_at` of the user is updated

resolve [#924](https://github.com/gristlabs/grist-core/issues/924)
This commit is contained in:
CamilleLegeron
2024-07-01 15:13:39 +02:00
committed by GitHub
parent 0e777b1fcf
commit 61421e8251
7 changed files with 101 additions and 21 deletions

View File

@@ -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;

View File

@@ -395,14 +395,6 @@ export class UsersManager {
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
// timestamps in db set by typeorm, and since second level precision is fine)
const nowish = new Date();
nowish.setMilliseconds(0);
user.firstLoginAt = nowish;
needUpdate = true;
}
if (!user.picture && profile && profile.picture) {
// Set the user's profile picture if our provider knows it.
user.picture = profile.picture;
@@ -432,6 +424,25 @@ export class UsersManager {
user.options = {...(user.options ?? {}), authSubject: userOptions.authSubject};
needUpdate = true;
}
// get date of 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);
if (profile && !user.firstLoginAt) {
// set first login time to now
user.firstLoginAt = nowish;
needUpdate = true;
}
const getTimestampStartOfDay = (date: Date) => {
const timestamp = Math.floor(date.getTime() / 1000); // unix timestamp seconds from epoc
const startOfDay = timestamp - (timestamp % 86400 /*24h*/); // start of a day in seconds since epoc
return startOfDay;
};
if (!user.lastConnectionAt || getTimestampStartOfDay(user.lastConnectionAt) !== getTimestampStartOfDay(nowish)) {
user.lastConnectionAt = nowish;
needUpdate = true;
}
if (needUpdate) {
login.user = user;
await manager.save([user, login]);

View File

@@ -1,5 +1,5 @@
import {User} from 'app/gen-server/entity/User';
import {makeId} from 'app/server/lib/idUtils';
import {chunk} from 'lodash';
import {MigrationInterface, QueryRunner, TableColumn} from "typeorm";
export class UserUUID1663851423064 implements MigrationInterface {
@@ -16,11 +16,20 @@ export class UserUUID1663851423064 implements MigrationInterface {
// Updating so many rows in a multiple queries is not ideal. We will send updates in chunks.
// 300 seems to be a good number, for 24k rows we have 80 queries.
const userList = await queryRunner.manager.createQueryBuilder()
.select("users")
.from(User, "users")
.select(["users.id", "users.ref"])
.from("users", "users")
.getMany();
userList.forEach(u => u.ref = makeId());
await queryRunner.manager.save(userList, { chunk: 300 });
const userChunks = chunk(userList, 300);
for (const users of userChunks) {
await queryRunner.connection.transaction(async manager => {
const queries = users.map((user: any, _index: number, _array: any[]) => {
return queryRunner.manager.update("users", user.id, user);
});
await Promise.all(queries);
});
}
// We are not making this column unique yet, because it can fail
// if there are some old workers still running, and any new user

View File

@@ -1,5 +1,5 @@
import {User} from 'app/gen-server/entity/User';
import {makeId} from 'app/server/lib/idUtils';
import {chunk} from 'lodash';
import {MigrationInterface, QueryRunner} from "typeorm";
export class UserRefUnique1664528376930 implements MigrationInterface {
@@ -9,12 +9,21 @@ export class UserRefUnique1664528376930 implements MigrationInterface {
// Update users that don't have unique ref set.
const userList = await queryRunner.manager.createQueryBuilder()
.select("users")
.from(User, "users")
.where("ref is null")
.getMany();
.select(["users.id", "users.ref"])
.from("users", "users")
.where("users.ref is null")
.getMany();
userList.forEach(u => u.ref = makeId());
await queryRunner.manager.save(userList, {chunk: 300});
const userChunks = chunk(userList, 300);
for (const users of userChunks) {
await queryRunner.connection.transaction(async manager => {
const queries = users.map((user: any, _index: number, _array: any[]) => {
return queryRunner.manager.update("users", user.id, user);
});
await Promise.all(queries);
});
}
// Mark column as unique and non-nullable.
const users = (await queryRunner.getTable('users'))!;

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');
}
}