-
-
Notifications
You must be signed in to change notification settings - Fork 320
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
Create user last connection datetime #935
Conversation
test/gen-server/migrations.ts
Outdated
@@ -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]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tests fails with error no such column: users.last_connection_at
triggered when run migration UserUUID
because it uses the entity User
that has no last_connection_at
for the moment (the migration come later) : see here
Locally when I put the migration UserLastConnection
before UserUUID
it works but I think it's not a good fixe.
What about duplicate the entity inside the migration as it has to be at this point ? (Not convince about this approach)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'll need to update UserUUID
and any other affected migrations to not depend on the User
entity. I don't know of any workarounds, unfortunately.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How do you imagine updating UserUUID
to not depend ont the User
entity has it exists to modify this entity ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replacing the methods called on the User
entity with equivalent raw SQL is one idea. (typeorm supports executing raw SQL via the query
method.)
The chunk
option could be replaced with a for loop that does the UPDATE
calls in chunks.
After a quick glance, it looks fine to me. Though I wonder what happens if a user continues to be logged in as their session won't expire? In such a case, even if that's not really probable that their session won't never expire in years (that being said, maybe this can happen?), the data may not reflect correctly the last connection. Maybe we could take advantage of the Authorizer and specifically of the grist-core/app/server/lib/Authorizer.ts Line 141 in 4567fad
We may only store the date without the time (so a And if an admin wants to know the activity of a user to the nearest second, they may take a look at the logs for that. |
I'm agree and I can change that
In the code I made the last connection is updated when the page is loaded or reloaded don't no matter user login. But in your proposal we updated it on each action, it's more precise |
a27256a
to
cd8667a
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I pushed a comment, but I would need to take time to make this more helpful regarding the timezone.
app/server/lib/Authorizer.ts
Outdated
@@ -358,6 +359,10 @@ export async function addRequestUser( | |||
mreq.user = user; | |||
mreq.userId = user.id; | |||
mreq.userIsAuthorized = true; | |||
const today = moment().startOf('day'); | |||
if (today !== moment(user.lastConnectionAt).startOf('day')) { | |||
await dbManager.updateUser(mreq.userId, {lastConnectionAt: today.toDate()}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we do this from getUserByLoginWithRetry
instead? It also (optionally) updates the user.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The functionality seems basically ok, thanks @CamilleLegeron . I am grateful that the user record isn't updated excessively. I have a collection of very nitpicky, linty comments.
app/gen-server/sqlUtils.ts
Outdated
const queries = users.map((user: any, _index: number, _array: any[]) => { | ||
return manager.query( | ||
`UPDATE users | ||
SET ref = ${addParamToQuery(dbType, 1)} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall with the addParamToQuery
function included this feels quite messy compared to the original queryRunner.manager.save
setup? I probably missed it earlier in development, but what was the main reason for the change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's some context here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, I understand now. Would it be possible to narrow the scope of the selects, like for example in
grist-core/app/gen-server/migration/1568238234987-TeamMembers.ts
Lines 10 to 33 in 5956c20
public async up(queryRunner: QueryRunner): Promise<any> { | |
// Get all orgs and add a team member ACL (with group) to each. | |
const orgs = await queryRunner.manager.createQueryBuilder() | |
.select("orgs.id") | |
.from(Organization, "orgs") | |
.getMany(); | |
for (const org of orgs) { | |
const groupInsert = await queryRunner.manager.createQueryBuilder() | |
.insert() | |
.into(Group) | |
.values([{name: roles.MEMBER}]) | |
.execute(); | |
const groupId = groupInsert.identifiers[0].id; | |
await queryRunner.manager.createQueryBuilder() | |
.insert() | |
.into(AclRuleOrg) | |
.values([{ | |
permissions: Permissions.VIEW, | |
organization: {id: org.id}, | |
group: groupId | |
}]) | |
.execute(); | |
} | |
} |
Bundling does seem important since there are a lot of users. But I'm afraid if this code had a subtle bug we wouldn't catch it, and previous times we have had subtle bugs in migrations it has been a mess. I think I need to ask for this method's functionality to be given a set of unit tests. The addParamToQuery function that does one thing for Postgres and then ignores an argument for SQLite, relying on the user to call things in the right order, also feels dangerous - does it really need to be exported, can it be at least kept private?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In my last commit I added a test to be sure it enters inside the for loop of the addRefToUserList
function and I remove the addParamToQuery
function that is not safe
Currently translated at 13.7% (184 of 1334 strings) Translation: Grist/client Translate-URL: https://hosted.weblate.org/projects/grist/client/sk/
…ristlabs#1024) The example key shown on the admin panel to users who are not known to be administrators is generated using a method that is only available in secure environments. This adds a fallback for insecure environments. The key is less solid but again, it is just an example, and for an insecure environment. Tested manually running locally and using a hostname set in /etc/hosts.
Summary: - Makes EE decide which ActivationPage to use - Makes ProductUpgrades use core implementation if not activated - Changes banners to proxy to core implementation if EE not activated - [Fix] Enables new site creation in EE as in Core: - Core enables people to freely create new team sites. - Enterprise currently redirects to the pricing page. - This enables enterprise to also create team sites, instead of redirecting. Test Plan: Manually test in EE, unit tests in Jenkins Reviewers: paulfitz, jordigh Reviewed By: paulfitz Differential Revision: https://phab.getgrist.com/D4264
Summary: - lookupOne/lookupRecords explain `sort_by` param better, and link to more detailed article. - Incorporate a typo fix from Help Center - Fix the omission of TASTEME never having been documented. Test Plan: Corresponding update to Help Center can be reviewed at gristlabs/grist-help#351 Reviewers: jarek Reviewed By: jarek Subscribers: jarek Differential Revision: https://phab.getgrist.com/D4269
Summary: The GRIST_DEFAULT_PRODUCT wasn't used for grist-ee, now it is respected. Test Plan: I've build grist-ee docker image from github and run it using our instruction (both for recreating the issue and confirming it is fixed) ``` docker run -p 8484:8484 \ -v $PWD:/persist \ -e GRIST_SESSION_SECRET=invent-a-secret-here \ -e GRIST_SINGLE_ORG=cool-beans -it gristlabs/grist-ee ``` For grist-core I recreated/confirmed it is fixed it just by `GRIST_SINGLE_ORG=team npm start` in the core folder. I also created some team sites using stubbed UI and confirmed that they were using the GRIST_DEFAULT_PRODUCT product. Reviewers: paulfitz Reviewed By: paulfitz Subscribers: paulfitz Differential Revision: https://phab.getgrist.com/D4271
… product if it was set be default Summary: After release on 2024-06-12 (1.1.15) the GRIST_DEFAULT_PRODUCT env variable wasn't respected by the method that started the server in single org mode. In all deployments (apart from saas), the default product used for new sites is set to `Free`, but the code that starts the server enforced `teamFree` product. This change adds a fix routine that fixes this issue by rewriting team sites from `teamFree` product to `Free` product only if: - The default product is set to `Free` - The deployment type is something other then 'saas'. Additionally there is a test that will fail after 2024.10.01, as this fix should be removed before this date. Test Plan: Added test Reviewers: paulfitz Reviewed By: paulfitz Subscribers: paulfitz Differential Revision: https://phab.getgrist.com/D4272
Summary: For non-owners, the timing section of Document Settings is now disabled. For non-editors, the "Reload" section is disabled. Test Plan: Added a test case for timing being disabled. Reviewers: jarek Reviewed By: jarek Differential Revision: https://phab.getgrist.com/D4275
Summary: fixSiteProducts was always called with a dry option. This option was just added for debuging test failure, it should have been removed. Test Plan: Manual. - on grist core, prepare site with `teamFree` product - then to recreate run the previous version as `GRIST_SINGLE_ORG=cool-beans GRIST_DEFAULT_PRODUCT=Free npm start` - then to confirm it is fixed, run the same command as above Site should be changed from `teamFree` to `Free`. Reviewers: paulfitz Reviewed By: paulfitz Subscribers: paulfitz Differential Revision: https://phab.getgrist.com/D4276
Co-authored-by: Paul's Grist Bot <paul+bot@getgrist.com>
…nt in its own module (gristlabs#1049) The HomeDBManager remains the exposed class to the other parts of the code: any module under gen-server/lib/homedb like UsersManager is intended to be used solely by HomeDBManager, and in order to use their methods, an indirection has to be created to pass through HomeDBManager.
This is a new entrypoint, mostly intended for Docker, so we have one simple process controlling the main Grist process. The purpose of this is to be able to make Grist easily restartable with a new environment.
This adds an endpoint for the admin user to be able to signal to a controlling process to restart the server. This is intended for `docker-runner.mjs`.
@@ -432,6 +433,11 @@ export class UsersManager { | |||
user.options = {...(user.options ?? {}), authSubject: userOptions.authSubject}; | |||
needUpdate = true; | |||
} | |||
const today = moment().startOf('day'); | |||
if (!user.lastConnectionAt || !today.isSame(moment(user.lastConnectionAt).startOf('day'))) { | |||
user.lastConnectionAt = today.toDate(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In line 403 we have this line:
nowish.setMilliseconds(0);
Do you think we need it here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, we does not need it because we take the moment()
starting on the day
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, one last question about the date
column. I'm a little bit concerned here, what is actually stored and saved when we have 2 home servers in two different timezones. I did a bit of debugging and in Sqlite it stored:
2024-06-20 22:00:00.000
(so UTC time of day start
in my timezone +2), but in postgress only 2024-06-21
. But in code both those values are mapped to Date
type, and I think represented in local time.
So the concern (and question). Is it possible, that this code will update user multiple times a day, if requests will hit 2 different servers randomly?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm thinking, if it would be simpler if we just use datetime
column in both Sqlite and Postgres. And treat it as unix timestamp
, the same way as we do for any other created_at
or updated_at
columns. It will be easier to compare those if needed, and it is simpler to reason about, as having unix timestamps in database is a common approach.
Math here is also very simple:
const timeStamp = Math.floor(Date.now() / 1000); // unix timestamp seconds from epoc
const startOfDay = timestamp - (timestamp % 86400 /*24h*/); // start of a day in seconds since epoc
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh yes, I hadn't seen that, thanks. In fact I think it's a good thing to use datetime, I will change it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @CamilleLegeron.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you @CamilleLegeron!
resolve #924
Each time the a Grist page is reload the
last_connection_at
of the user is updated