Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create user last connection datetime #935

Merged
merged 66 commits into from
Jul 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
643daca
Create user last connection datetime
CamilleLegeron Apr 15, 2024
cd8667a
use date and not datetime for lastConnectionAt
CamilleLegeron Apr 16, 2024
feafda8
Merge branch 'main' into last-connection
CamilleLegeron Apr 30, 2024
62acdf3
use moment to get today date
CamilleLegeron Apr 30, 2024
f36f044
Merge branch 'main' into last-connection
CamilleLegeron May 17, 2024
4d57c60
update user last connection on each authorization request
CamilleLegeron May 17, 2024
6a07bcb
fix (migration-test): use SQL queries to avoid using User entity in m…
CamilleLegeron May 20, 2024
73420a7
update lastConnectionAt directly in getUserByLogin
CamilleLegeron May 22, 2024
6610ab2
feat [last-connection]: use chunk in migration and parameterizing the…
CamilleLegeron May 28, 2024
bf9bc76
Merge branch 'main' into last-connection
CamilleLegeron May 28, 2024
22114c7
remove Trailing spaces
CamilleLegeron May 28, 2024
c418a0b
reorganize imports
CamilleLegeron May 29, 2024
4428d8c
reorder import
CamilleLegeron May 29, 2024
2c696c9
simplification of addRefToUserList + add testing
CamilleLegeron Jun 3, 2024
6d64b87
test: add check all users have unique ref
CamilleLegeron Jun 18, 2024
2f1ec75
Update error message app/gen-server/sqlUtils.ts
CamilleLegeron Jun 18, 2024
5ffbf62
(core) Fix and move filter tests to grist-core
georgegevoian May 24, 2024
71d8d47
(core) Fix for ACL page for temporary documents.
berhalak May 24, 2024
7d628db
(core) Removing virtual tables when they are not needed
georgegevoian May 28, 2024
eb6a6bd
Translated using Weblate (French)
May 28, 2024
519fdfa
feat: add new translations (#1004)
CamilleLegeron May 29, 2024
ae48fb6
Removes spacing from admin page auth translation key (#1001)
Spoffy May 30, 2024
0ce7cda
Translated using Weblate (Slovak)
h0r0m May 30, 2024
fa5aa6a
automated update to translation keys (#987)
github-actions[bot] May 31, 2024
6b061a6
Translated using Weblate (French)
May 31, 2024
403a480
Translated using Weblate (Spanish)
gallegonovato May 31, 2024
d480eca
Translated using Weblate (Slovenian)
fprijate Jun 1, 2024
5a1d41a
Translated using Weblate (Romanian)
florentinap May 31, 2024
51aa4e5
Translated using Weblate (Slovenian)
fprijate Jun 2, 2024
b701349
Translated using Weblate (Slovak)
h0r0m Jun 1, 2024
cc50893
Bump minio to v8.0.0 (#991)
SleepyLeslie Jun 3, 2024
0e78637
(core) Support `user` variable in dropdown conditions
georgegevoian May 29, 2024
7ed70b9
(core) Fix for flaky GridViewNewColumnMenu test which may have been f…
dsagal Jun 10, 2024
b4344c1
Translated using Weblate (Portuguese (Brazil))
Jun 3, 2024
f4f0c2b
Translated using Weblate (German)
Jun 3, 2024
b9600c5
Translated using Weblate (Russian)
Vladimir-Va Jun 3, 2024
42742b8
Translated using Weblate (Romanian)
florentinap Jun 4, 2024
2c74062
Translated using Weblate (Spanish)
Jun 4, 2024
792dc90
Translated using Weblate (Spanish)
gallegonovato Jun 4, 2024
5d80db5
Add user id middleware to form pages (#1020)
georgegevoian Jun 5, 2024
a7fbc2e
include pyodide in the docker image (#1019)
paulfitz Jun 5, 2024
fe10c80
Dockerfile: use tini to reap zombie processes
fflorent May 29, 2024
7c62a38
admin: fix warning in websocket probe
jordigh Jun 6, 2024
117717d
Attempts to make DropdownConditionEditor tests less flaky (#1026)
Spoffy Jun 7, 2024
07bf8ef
Fixes flaky ViewLayoutCollapse test (#1027)
Spoffy Jun 10, 2024
0a3978c
(core) Renaming installationId metadata for checkUpdateAPI telemetry …
berhalak Jun 12, 2024
2b49dee
Translated using Weblate (Slovak)
h0r0m Jun 10, 2024
ce9b1a8
v1.1.15
jordigh Jun 12, 2024
224dbdf
make the example key on admin panel without auth work when insecure (…
paulfitz Jun 12, 2024
f7bd265
(core) Makes EE frontend behave as core if EE isn't activated
Spoffy Jun 13, 2024
76c2218
(core) Update documentation of certain functions
dsagal Jun 12, 2024
4ebdae6
(core) Restoring GRIST_DEFAULT_PRODUCT functionality
berhalak Jun 14, 2024
8864643
Update README to rebrand grist-electron (#1039)
SleepyLeslie Jun 13, 2024
8468fa5
(core) Adding fixSiteProducts that changes orgs from teamFree to Free…
berhalak Jun 18, 2024
822aefc
(core) Disable formula timing UI for non-owners
dsagal Jun 18, 2024
54e0d7e
(core) Removing dry option from fixSiteProducts
berhalak Jun 18, 2024
bd27669
automated update to translation keys (#1053)
github-actions[bot] Jun 18, 2024
883b8e9
HomeDBManager refactoration: extract method related to Users manageme…
fflorent Jun 18, 2024
1081d63
supervisor: new file
jordigh Jun 13, 2024
ec55ec0
FlexServer: add new admin restart endpoint
jordigh Jun 8, 2024
8fab3aa
Dockerfile: use docker-runner.mjs as new entrypoint
jordigh Jun 13, 2024
4db9e7e
Merge branch 'main' into last-connection
CamilleLegeron Jun 20, 2024
015dc5e
fix: use only needed columns on db query
CamilleLegeron Jun 20, 2024
1fa3fb4
fix: check if lastConnectionAt exist and use moment isSame function f…
CamilleLegeron Jun 20, 2024
4ffb749
fix: use update instead of save function to avoid using user entity
CamilleLegeron Jun 20, 2024
fc6de6b
fix: use datetime and timestamp for lastConnectionAt
CamilleLegeron Jun 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/gen-server/entity/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
27 changes: 19 additions & 8 deletions app/gen-server/lib/homedb/UsersManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) => {
berhalak marked this conversation as resolved.
Show resolved Hide resolved
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]);
Expand Down
17 changes: 13 additions & 4 deletions app/gen-server/migration/1663851423064-UserUUID.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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
Expand Down
21 changes: 15 additions & 6 deletions app/gen-server/migration/1664528376930-UserRefUnique.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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")
Copy link
Contributor

@berhalak berhalak Jun 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @CamilleLegeron. I have a question, can you try updating this migration, and the one above (UserUUID) with a code that tries not to use User entity. From my brief tests, it should work.

    const userList = await queryRunner.manager.createQueryBuilder()
      .select(["users.id", "users.ref"])
      .from("users", "users")
      .where("ref is null")
      .getMany();
    userList.forEach(u => u.ref = makeId());

Here, I'm trying to force typeorm to not produce full SELECT, and I'm only specifying columns that I know exist.
If that works, it will simplify this PR a lot.

Copy link
Collaborator Author

@CamilleLegeron CamilleLegeron Jun 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ooh wuaah yes it's works for the query function
but not for the queryRunner.manager.save(userList, { chunk: 300 }); that use the entity
I'm working on it searching other possibility

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seems we can not use save function with partial entity, but it's possible with update function. The weakness it's that we can't use chunk option

.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'))!;
Expand Down
18 changes: 18 additions & 0 deletions app/gen-server/migration/1713186031023-UserLastConnection.ts
Original file line number Diff line number Diff line change
@@ -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');
}
}
4 changes: 2 additions & 2 deletions app/server/lib/requestUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ 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', 'stripeProductId', 'userId', 'isFirstTimeUser', 'allowGoogleLogin',
'apiKey', 'billingAccountId', 'firstLoginAt', 'lastConnectionAt', 'filteredOut', 'ownerId', 'gracePeriodStart',
'stripeCustomerId', 'stripeSubscriptionId', 'stripeProductId', 'userId', 'isFirstTimeUser', 'allowGoogleLogin',
'authSubject', 'usage', 'createdBy'
]);

Expand Down
32 changes: 31 additions & 1 deletion test/gen-server/migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ import {ActivationPrefs1682636695021 as ActivationPrefs} from 'app/gen-server/mi
import {AssistantLimit1685343047786 as AssistantLimit} from 'app/gen-server/migration/1685343047786-AssistantLimit';
import {Shares1701557445716 as Shares} from 'app/gen-server/migration/1701557445716-Shares';
import {Billing1711557445716 as BillingFeatures} from 'app/gen-server/migration/1711557445716-Billing';
import {UserLastConnection1713186031023
as UserLastConnection} from 'app/gen-server/migration/1713186031023-UserLastConnection';

const home: HomeDBManager = new HomeDBManager();

Expand All @@ -50,7 +52,8 @@ 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, BillingFeatures];
Forks, ForkIndexes, ActivationPrefs, AssistantLimit, Shares, BillingFeatures,
UserLastConnection];

// Assert that the "members" acl rule and group exist (or not).
function assertMembersGroup(org: Organization, exists: boolean) {
Expand Down Expand Up @@ -113,6 +116,33 @@ describe('migrations', function() {
// be doing something.
});

it('can migrate UserUUID and UserUniqueRefUUID with user in table', async function() {
this.timeout(60000);
const runner = home.connection.createQueryRunner();

// Create 400 users to test the chunk (each chunk is 300 users)
const nbUsersToCreate = 400;
for (const migration of migrations) {
if (migration === UserUUID) {
for (let i = 0; i < nbUsersToCreate; i++) {
await runner.query(`INSERT INTO users (id, name, is_first_time_user) VALUES (${i}, 'name${i}', true)`);
}
}

await (new migration()).up(runner);
}

// Check that all refs are unique
const userList = await runner.manager.createQueryBuilder()
.select(["users.id", "users.ref"])
.from("users", "users")
.getMany();
const setOfUserRefs = new Set(userList.map(u => u.ref));
assert.equal(nbUsersToCreate, userList.length);
assert.equal(setOfUserRefs.size, userList.length);
await addSeedData(home.connection);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding a test. One small suggestion: could you add a check here that goes through all the users and checks they each have a unique ref? A simple way to check might be to map all of their refs, drop them in a Set, and check that you have 400.

});

it('can correctly switch display_email column to non-null with data', async function() {
this.timeout(60000);
const sqlite = home.connection.driver.options.type === 'sqlite';
Expand Down
Loading