diff --git a/services/api/database/migrations/20241213000000_user_last_accessed.js b/services/api/database/migrations/20241213000000_user_last_accessed.js new file mode 100644 index 0000000000..f6c7a9860f --- /dev/null +++ b/services/api/database/migrations/20241213000000_user_last_accessed.js @@ -0,0 +1,26 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = async function(knex) { + organizations = await knex.schema.hasTable('user'); + if (!organizations) { + return knex.schema + .createTable('user', function (table) { + table.specificType('usid', 'CHAR(36)'); + table.datetime('last_accessed'); + table.primary(['usid']); + }) + } + else { + return knex.schema + } +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = async function(knex) { + return knex.schema +}; diff --git a/services/api/src/models/user.ts b/services/api/src/models/user.ts index 5285bde369..9fc18134c6 100644 --- a/services/api/src/models/user.ts +++ b/services/api/src/models/user.ts @@ -8,6 +8,7 @@ import { Group, GroupType, KeycloakLagoonGroup } from './group'; import { Sql } from '../resources/user/sql'; import { getConfigFromEnv } from '../util/config'; import { Helpers as groupHelpers } from '../resources/group/helpers'; +import { logger } from '../loggers/logger'; interface IUserAttributes { comment?: [string]; @@ -20,6 +21,7 @@ export interface User { firstName?: string; lastName?: string; comment?: string; + created?: string; lastAccessed?: string; gitlabId?: string; attributes?: IUserAttributes; @@ -178,23 +180,22 @@ export const User = (clients: { R.pipe( R.pick(['id', 'email', 'username', 'firstName', 'lastName', 'attributes', 'admin', 'owner', 'organizationRole', 'platformRoles']), // @ts-ignore - R.set(commentLens, R.view(attrCommentLens, keycloakUser)) + R.set(commentLens, R.view(attrCommentLens, keycloakUser)), + // set the user created time + R.set(R.lensPath(['created']), new Date(keycloakUser.createdTimestamp).toISOString().slice(0, 19).replace('T', ' ') || null), )(keycloakUser) ); let usersWithGitlabIdFetch = []; for (const user of users) { - // set the lastaccessed attribute - // @TODO: no op last accessed for the time being due to raciness - // @TODO: refactor later - /* - let date = null; - if (user['attributes'] && user['attributes']['last_accessed']) { - date = new Date(user['attributes']['last_accessed']*1000).toISOString() - user.lastAccessed = date + const userdate = await query( + sqlClientPool, + Sql.selectLastAccessed(user.id) + ); + if (userdate.length) { + user.lastAccessed = userdate[0].lastAccessed } - */ usersWithGitlabIdFetch.push({ ...user, gitlabId: await fetchGitlabId(user) @@ -648,8 +649,14 @@ export const User = (clients: { const userLastAccessed = async (userInput: User): Promise => { // set the last accessed as a unix timestamp on the user attributes - // @TODO: no op last accessed for the time being due to raciness - // @TODO: refactor later + try { + await query( + sqlClientPool, + Sql.updateLastAccessed(userInput.id) + ); + } catch (err) { + logger.warn(`Error updating user: ${err.message}`); + } return true }; @@ -754,6 +761,11 @@ export const User = (clients: { sqlClientPool, Sql.deleteFromUserSshKeys(id) ); + // delete from the user table + await query( + sqlClientPool, + Sql.deleteFromUser(id) + ); await keycloakAdminClient.users.del({ id }); } catch (err) { diff --git a/services/api/src/resources/user/sql.ts b/services/api/src/resources/user/sql.ts index 8bf7d58202..9cfdbbaeb3 100644 --- a/services/api/src/resources/user/sql.ts +++ b/services/api/src/resources/user/sql.ts @@ -38,4 +38,23 @@ export const Sql = { .where('sk.key_fingerprint', keyFingerprint) .select('user_ssh_key.usid') .toString(), + updateLastAccessed: (id: string) => + knex('user') + .insert({ + usid: id, + lastAccessed: knex.fn.now(), + }) + .onConflict('usid') + .merge() + .toString(), + selectLastAccessed: (id: string) => + knex('user') + .select('last_accessed') + .where('usid','=',id) + .toString(), + deleteFromUser: (id: string) => + knex('user') + .where('usid', id) + .delete() + .toString(), }; diff --git a/services/api/src/typeDefs.js b/services/api/src/typeDefs.js index 6693c162e7..b3fb60c37c 100644 --- a/services/api/src/typeDefs.js +++ b/services/api/src/typeDefs.js @@ -459,10 +459,9 @@ const typeDefs = gql` # This just returns the group name, id and the role the user has in that group. # This is a neat way to visualize a users specific access without having to get all members of a group groupRoles: [GroupRoleInterface] - # @TODO: no op last accessed for the time being due to raciness - # @TODO: refactor later - # lastAccessed: String platformRoles: [PlatformRole] + created: String + lastAccessed: String } enum PlatformRole {