From 4dcd3ed06c210f38417a6cf9c5cdc0dd5b70f84a Mon Sep 17 00:00:00 2001 From: = Date: Wed, 6 Aug 2025 11:57:02 +0530 Subject: [PATCH] feat: adds support for last logged in auth method field --- .../20250805151349_last-logged-auth-method.ts | 41 ++++ .../db/schemas/identity-org-memberships.ts | 3 +- backend/src/db/schemas/org-memberships.ts | 3 +- .../src/services/auth/auth-login-service.ts | 9 +- .../identity-alicloud-auth-service.ts | 9 +- .../identity-aws-auth-service.ts | 9 +- .../identity-azure-auth-service.ts | 9 +- .../identity-gcp-auth-service.ts | 9 +- .../identity-jwt-auth-service.ts | 9 +- .../identity-kubernetes-auth-service.ts | 9 +- .../identity-ldap-auth-service.ts | 9 +- .../identity-oci-auth-service.ts | 9 +- .../identity-oidc-auth-service.ts | 9 +- .../identity-tls-cert-auth-service.ts | 9 +- .../identity-token-auth-service.ts | 9 +- .../identity-ua/identity-ua-service.ts | 13 +- .../src/services/identity/identity-org-dal.ts | 10 +- .../org-membership/org-membership-dal.ts | 5 +- backend/src/services/org/org-dal.ts | 1 + .../LastLoginSection/LastLoginSection.tsx | 23 ++ .../organization/LastLoginSection/index.tsx | 1 + frontend/src/components/v2/Badge/Badge.tsx | 24 +- frontend/src/hooks/api/identities/types.ts | 1 + frontend/src/hooks/api/users/types.ts | 1 + .../IdentitySection/IdentityTable.tsx | 224 ++++++++++-------- .../OrgMembersSection/OrgMembersTable.tsx | 30 ++- .../components/IdentityDetailsSection.tsx | 4 + .../components/UserDetailsSection.tsx | 8 + 28 files changed, 366 insertions(+), 134 deletions(-) create mode 100644 backend/src/db/migrations/20250805151349_last-logged-auth-method.ts create mode 100644 frontend/src/components/organization/LastLoginSection/LastLoginSection.tsx create mode 100644 frontend/src/components/organization/LastLoginSection/index.tsx diff --git a/backend/src/db/migrations/20250805151349_last-logged-auth-method.ts b/backend/src/db/migrations/20250805151349_last-logged-auth-method.ts new file mode 100644 index 000000000..f32129906 --- /dev/null +++ b/backend/src/db/migrations/20250805151349_last-logged-auth-method.ts @@ -0,0 +1,41 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const lastUserLoggedInAuthMethod = await knex.schema.hasColumn(TableName.OrgMembership, "lastLoggedInAuthMethod"); + const lastIdentityLoggedInAuthMethod = await knex.schema.hasColumn( + TableName.IdentityOrgMembership, + "lastLoggedInAuthMethod" + ); + if (!lastUserLoggedInAuthMethod) { + await knex.schema.alterTable(TableName.OrgMembership, (t) => { + t.string("lastLoggedInAuthMethod").nullable(); + }); + } + + if (!lastIdentityLoggedInAuthMethod) { + await knex.schema.alterTable(TableName.IdentityOrgMembership, (t) => { + t.string("lastLoggedInAuthMethod").nullable(); + }); + } +} + +export async function down(knex: Knex): Promise { + const lastUserLoggedInAuthMethod = await knex.schema.hasColumn(TableName.OrgMembership, "lastLoggedInAuthMethod"); + const lastIdentityLoggedInAuthMethod = await knex.schema.hasColumn( + TableName.IdentityOrgMembership, + "lastLoggedInAuthMethod" + ); + if (!lastUserLoggedInAuthMethod) { + await knex.schema.alterTable(TableName.OrgMembership, (t) => { + t.dropColumn("lastLoggedInAuthMethod"); + }); + } + + if (!lastIdentityLoggedInAuthMethod) { + await knex.schema.alterTable(TableName.IdentityOrgMembership, (t) => { + t.dropColumn("lastLoggedInAuthMethod"); + }); + } +} diff --git a/backend/src/db/schemas/identity-org-memberships.ts b/backend/src/db/schemas/identity-org-memberships.ts index 2f29c52e4..2f0c81a3a 100644 --- a/backend/src/db/schemas/identity-org-memberships.ts +++ b/backend/src/db/schemas/identity-org-memberships.ts @@ -14,7 +14,8 @@ export const IdentityOrgMembershipsSchema = z.object({ orgId: z.string().uuid(), createdAt: z.date(), updatedAt: z.date(), - identityId: z.string().uuid() + identityId: z.string().uuid(), + lastLoggedInAuthMethod: z.string().nullable().optional() }); export type TIdentityOrgMemberships = z.infer; diff --git a/backend/src/db/schemas/org-memberships.ts b/backend/src/db/schemas/org-memberships.ts index 939033c71..5be83be00 100644 --- a/backend/src/db/schemas/org-memberships.ts +++ b/backend/src/db/schemas/org-memberships.ts @@ -19,7 +19,8 @@ export const OrgMembershipsSchema = z.object({ roleId: z.string().uuid().nullable().optional(), projectFavorites: z.string().array().nullable().optional(), isActive: z.boolean().default(true), - lastInvitedAt: z.date().nullable().optional() + lastInvitedAt: z.date().nullable().optional(), + lastLoggedInAuthMethod: z.string().nullable().optional() }); export type TOrgMemberships = z.infer; diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts index 7dd1d3aa4..4d6830834 100644 --- a/backend/src/services/auth/auth-login-service.ts +++ b/backend/src/services/auth/auth-login-service.ts @@ -148,9 +148,12 @@ export const authLoginServiceFactory = ({ if (organizationId) { const org = await orgDAL.findById(organizationId); - if (org && org.userTokenExpiration) { - tokenSessionExpiresIn = getMinExpiresIn(cfg.JWT_AUTH_LIFETIME, org.userTokenExpiration); - refreshTokenExpiresIn = org.userTokenExpiration; + if (org) { + await orgMembershipDAL.update({ userId: user.id, orgId: org.id }, { lastLoggedInAuthMethod: authMethod }); + if (org.userTokenExpiration) { + tokenSessionExpiresIn = getMinExpiresIn(cfg.JWT_AUTH_LIFETIME, org.userTokenExpiration); + refreshTokenExpiresIn = org.userTokenExpiration; + } } } diff --git a/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-service.ts b/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-service.ts index 819329a22..fe77aa2c2 100644 --- a/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-service.ts +++ b/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-service.ts @@ -38,7 +38,7 @@ type TIdentityAliCloudAuthServiceFactoryDep = { TIdentityAliCloudAuthDALFactory, "findOne" | "transaction" | "create" | "updateById" | "delete" >; - identityOrgMembershipDAL: Pick; + identityOrgMembershipDAL: Pick; licenseService: Pick; permissionService: Pick; }; @@ -87,6 +87,13 @@ export const identityAliCloudAuthServiceFactory = ({ // Generate the token const identityAccessToken = await identityAliCloudAuthDAL.transaction(async (tx) => { + await identityOrgMembershipDAL.updateById( + identityMembershipOrg.id, + { + lastLoggedInAuthMethod: IdentityAuthMethod.ALICLOUD_AUTH + }, + tx + ); const newToken = await identityAccessTokenDAL.create( { identityId: identityAliCloudAuth.identityId, diff --git a/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts b/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts index b5035946e..3743934ed 100644 --- a/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts +++ b/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts @@ -36,7 +36,7 @@ import { type TIdentityAwsAuthServiceFactoryDep = { identityAccessTokenDAL: Pick; identityAwsAuthDAL: Pick; - identityOrgMembershipDAL: Pick; + identityOrgMembershipDAL: Pick; licenseService: Pick; permissionService: Pick; }; @@ -152,6 +152,13 @@ export const identityAwsAuthServiceFactory = ({ } const identityAccessToken = await identityAwsAuthDAL.transaction(async (tx) => { + await identityOrgMembershipDAL.updateById( + identityMembershipOrg.id, + { + lastLoggedInAuthMethod: IdentityAuthMethod.AWS_AUTH + }, + tx + ); const newToken = await identityAccessTokenDAL.create( { identityId: identityAwsAuth.identityId, diff --git a/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts b/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts index 35103c8cf..bbd9194b1 100644 --- a/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts +++ b/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts @@ -33,7 +33,7 @@ type TIdentityAzureAuthServiceFactoryDep = { TIdentityAzureAuthDALFactory, "findOne" | "transaction" | "create" | "updateById" | "delete" >; - identityOrgMembershipDAL: Pick; + identityOrgMembershipDAL: Pick; identityAccessTokenDAL: Pick; permissionService: Pick; licenseService: Pick; @@ -80,6 +80,13 @@ export const identityAzureAuthServiceFactory = ({ } const identityAccessToken = await identityAzureAuthDAL.transaction(async (tx) => { + await identityOrgMembershipDAL.updateById( + identityMembershipOrg.id, + { + lastLoggedInAuthMethod: IdentityAuthMethod.AZURE_AUTH + }, + tx + ); const newToken = await identityAccessTokenDAL.create( { identityId: identityAzureAuth.identityId, diff --git a/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts b/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts index b83697e52..09a3511ef 100644 --- a/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts +++ b/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts @@ -31,7 +31,7 @@ import { type TIdentityGcpAuthServiceFactoryDep = { identityGcpAuthDAL: Pick; - identityOrgMembershipDAL: Pick; + identityOrgMembershipDAL: Pick; identityAccessTokenDAL: Pick; permissionService: Pick; licenseService: Pick; @@ -119,6 +119,13 @@ export const identityGcpAuthServiceFactory = ({ } const identityAccessToken = await identityGcpAuthDAL.transaction(async (tx) => { + await identityOrgMembershipDAL.updateById( + identityMembershipOrg.id, + { + lastLoggedInAuthMethod: IdentityAuthMethod.GCP_AUTH + }, + tx + ); const newToken = await identityAccessTokenDAL.create( { identityId: identityGcpAuth.identityId, diff --git a/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts b/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts index 35063ae70..3f0b1f5a5 100644 --- a/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts +++ b/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts @@ -43,7 +43,7 @@ import { type TIdentityJwtAuthServiceFactoryDep = { identityJwtAuthDAL: TIdentityJwtAuthDALFactory; - identityOrgMembershipDAL: Pick; + identityOrgMembershipDAL: Pick; identityAccessTokenDAL: Pick; permissionService: Pick; licenseService: Pick; @@ -209,6 +209,13 @@ export const identityJwtAuthServiceFactory = ({ } const identityAccessToken = await identityJwtAuthDAL.transaction(async (tx) => { + await identityOrgMembershipDAL.updateById( + identityMembershipOrg.id, + { + lastLoggedInAuthMethod: IdentityAuthMethod.JWT_AUTH + }, + tx + ); const newToken = await identityAccessTokenDAL.create( { identityId: identityJwtAuth.identityId, diff --git a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts index 41491ab2f..2329285f0 100644 --- a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts +++ b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts @@ -49,7 +49,7 @@ type TIdentityKubernetesAuthServiceFactoryDep = { "create" | "findOne" | "transaction" | "updateById" | "delete" >; identityAccessTokenDAL: Pick; - identityOrgMembershipDAL: Pick; + identityOrgMembershipDAL: Pick; permissionService: Pick; licenseService: Pick; kmsService: Pick; @@ -380,6 +380,13 @@ export const identityKubernetesAuthServiceFactory = ({ } const identityAccessToken = await identityKubernetesAuthDAL.transaction(async (tx) => { + await identityOrgMembershipDAL.updateById( + identityMembershipOrg.id, + { + lastLoggedInAuthMethod: IdentityAuthMethod.KUBERNETES_AUTH + }, + tx + ); const newToken = await identityAccessTokenDAL.create( { identityId: identityKubernetesAuth.identityId, diff --git a/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts b/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts index f38f53cf0..a94e93d11 100644 --- a/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts +++ b/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts @@ -44,7 +44,7 @@ type TIdentityLdapAuthServiceFactoryDep = { TIdentityLdapAuthDALFactory, "findOne" | "transaction" | "create" | "updateById" | "delete" >; - identityOrgMembershipDAL: Pick; + identityOrgMembershipDAL: Pick; licenseService: Pick; permissionService: Pick; kmsService: TKmsServiceFactory; @@ -144,6 +144,13 @@ export const identityLdapAuthServiceFactory = ({ } const identityAccessToken = await identityLdapAuthDAL.transaction(async (tx) => { + await identityOrgMembershipDAL.updateById( + identityMembershipOrg.id, + { + lastLoggedInAuthMethod: IdentityAuthMethod.LDAP_AUTH + }, + tx + ); const newToken = await identityAccessTokenDAL.create( { identityId: identityLdapAuth.identityId, diff --git a/backend/src/services/identity-oci-auth/identity-oci-auth-service.ts b/backend/src/services/identity-oci-auth/identity-oci-auth-service.ts index 3b6450e6e..baaa5b423 100644 --- a/backend/src/services/identity-oci-auth/identity-oci-auth-service.ts +++ b/backend/src/services/identity-oci-auth/identity-oci-auth-service.ts @@ -36,7 +36,7 @@ import { type TIdentityOciAuthServiceFactoryDep = { identityAccessTokenDAL: Pick; identityOciAuthDAL: Pick; - identityOrgMembershipDAL: Pick; + identityOrgMembershipDAL: Pick; licenseService: Pick; permissionService: Pick; }; @@ -91,6 +91,13 @@ export const identityOciAuthServiceFactory = ({ // Generate the token const identityAccessToken = await identityOciAuthDAL.transaction(async (tx) => { + await identityOrgMembershipDAL.updateById( + identityMembershipOrg.id, + { + lastLoggedInAuthMethod: IdentityAuthMethod.OCI_AUTH + }, + tx + ); const newToken = await identityAccessTokenDAL.create( { identityId: identityOciAuth.identityId, diff --git a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts index 08d53a344..c7e064428 100644 --- a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts +++ b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts @@ -43,7 +43,7 @@ import { type TIdentityOidcAuthServiceFactoryDep = { identityOidcAuthDAL: TIdentityOidcAuthDALFactory; - identityOrgMembershipDAL: Pick; + identityOrgMembershipDAL: Pick; identityAccessTokenDAL: Pick; permissionService: Pick; licenseService: Pick; @@ -178,6 +178,13 @@ export const identityOidcAuthServiceFactory = ({ } const identityAccessToken = await identityOidcAuthDAL.transaction(async (tx) => { + await identityOrgMembershipDAL.updateById( + identityMembershipOrg.id, + { + lastLoggedInAuthMethod: IdentityAuthMethod.OIDC_AUTH + }, + tx + ); const newToken = await identityAccessTokenDAL.create( { identityId: identityOidcAuth.identityId, diff --git a/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-service.ts b/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-service.ts index 742633b50..b99de47a3 100644 --- a/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-service.ts +++ b/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-service.ts @@ -30,7 +30,7 @@ type TIdentityTlsCertAuthServiceFactoryDep = { TIdentityTlsCertAuthDALFactory, "findOne" | "transaction" | "create" | "updateById" | "delete" >; - identityOrgMembershipDAL: Pick; + identityOrgMembershipDAL: Pick; licenseService: Pick; permissionService: Pick; kmsService: Pick; @@ -118,6 +118,13 @@ export const identityTlsCertAuthServiceFactory = ({ // Generate the token const identityAccessToken = await identityTlsCertAuthDAL.transaction(async (tx) => { + await identityOrgMembershipDAL.updateById( + identityMembershipOrg.id, + { + lastLoggedInAuthMethod: IdentityAuthMethod.TLS_CERT_AUTH + }, + tx + ); const newToken = await identityAccessTokenDAL.create( { identityId: identityTlsCertAuth.identityId, diff --git a/backend/src/services/identity-token-auth/identity-token-auth-service.ts b/backend/src/services/identity-token-auth/identity-token-auth-service.ts index 82949090e..7903732ac 100644 --- a/backend/src/services/identity-token-auth/identity-token-auth-service.ts +++ b/backend/src/services/identity-token-auth/identity-token-auth-service.ts @@ -35,7 +35,7 @@ type TIdentityTokenAuthServiceFactoryDep = { TIdentityTokenAuthDALFactory, "transaction" | "create" | "findOne" | "updateById" | "delete" >; - identityOrgMembershipDAL: Pick; + identityOrgMembershipDAL: Pick; identityAccessTokenDAL: Pick< TIdentityAccessTokenDALFactory, "create" | "find" | "update" | "findById" | "findOne" | "updateById" | "delete" @@ -345,6 +345,13 @@ export const identityTokenAuthServiceFactory = ({ const identityTokenAuth = await identityTokenAuthDAL.findOne({ identityId }); const identityAccessToken = await identityTokenAuthDAL.transaction(async (tx) => { + await identityOrgMembershipDAL.updateById( + identityMembershipOrg.id, + { + lastLoggedInAuthMethod: IdentityAuthMethod.TOKEN_AUTH + }, + tx + ); const newToken = await identityAccessTokenDAL.create( { identityId: identityTokenAuth.identityId, diff --git a/backend/src/services/identity-ua/identity-ua-service.ts b/backend/src/services/identity-ua/identity-ua-service.ts index cf5ccdeb7..5732992e5 100644 --- a/backend/src/services/identity-ua/identity-ua-service.ts +++ b/backend/src/services/identity-ua/identity-ua-service.ts @@ -59,6 +59,11 @@ export const identityUaServiceFactory = ({ } const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: identityUa.identityId }); + if (!identityMembershipOrg) { + throw new NotFoundError({ + message: "No identity with the org membership was found" + }); + } checkIPAgainstBlocklist({ ipAddress: ip, @@ -127,7 +132,13 @@ export const identityUaServiceFactory = ({ const identityAccessToken = await identityUaDAL.transaction(async (tx) => { const uaClientSecretDoc = await identityUaClientSecretDAL.incrementUsage(validClientSecretInfo!.id, tx); - + await identityOrgMembershipDAL.updateById( + identityMembershipOrg.id, + { + lastLoggedInAuthMethod: IdentityAuthMethod.UNIVERSAL_AUTH + }, + tx + ); const newToken = await identityAccessTokenDAL.create( { identityId: identityUa.identityId, diff --git a/backend/src/services/identity/identity-org-dal.ts b/backend/src/services/identity/identity-org-dal.ts index 3c5ca8ffe..6dbf4be9b 100644 --- a/backend/src/services/identity/identity-org-dal.ts +++ b/backend/src/services/identity/identity-org-dal.ts @@ -254,6 +254,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { db.ref("role").withSchema("paginatedIdentity"), db.ref("roleId").withSchema("paginatedIdentity"), db.ref("orgId").withSchema("paginatedIdentity"), + db.ref("lastLoggedInAuthMethod").withSchema("paginatedIdentity"), db.ref("createdAt").withSchema("paginatedIdentity"), db.ref("updatedAt").withSchema("paginatedIdentity"), db.ref("identityId").withSchema("paginatedIdentity").as("identityId"), @@ -319,7 +320,8 @@ export const identityOrgDALFactory = (db: TDbClient) => { ldapId, tlsCertId, createdAt, - updatedAt + updatedAt, + lastLoggedInAuthMethod }) => ({ role, roleId, @@ -328,6 +330,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { orgId, createdAt, updatedAt, + lastLoggedInAuthMethod, customRole: roleId ? { id: crId, @@ -497,6 +500,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { db.ref("orgId").withSchema(TableName.IdentityOrgMembership), db.ref("createdAt").withSchema(TableName.IdentityOrgMembership), db.ref("updatedAt").withSchema(TableName.IdentityOrgMembership), + db.ref("lastLoggedInAuthMethod").withSchema(TableName.IdentityOrgMembership), db.ref("identityId").withSchema(TableName.IdentityOrgMembership).as("identityId"), db.ref("name").withSchema(TableName.Identity).as("identityName"), db.ref("hasDeleteProtection").withSchema(TableName.Identity), @@ -576,7 +580,8 @@ export const identityOrgDALFactory = (db: TDbClient) => { tokenId, ldapId, createdAt, - updatedAt + updatedAt, + lastLoggedInAuthMethod }) => ({ role, roleId, @@ -586,6 +591,7 @@ export const identityOrgDALFactory = (db: TDbClient) => { orgId, createdAt, updatedAt, + lastLoggedInAuthMethod, customRole: roleId ? { id: crId, diff --git a/backend/src/services/org-membership/org-membership-dal.ts b/backend/src/services/org-membership/org-membership-dal.ts index ed4867025..98e89c832 100644 --- a/backend/src/services/org-membership/org-membership-dal.ts +++ b/backend/src/services/org-membership/org-membership-dal.ts @@ -32,6 +32,7 @@ export const orgMembershipDALFactory = (db: TDbClient) => { db.ref("roleId").withSchema(TableName.OrgMembership), db.ref("status").withSchema(TableName.OrgMembership), db.ref("isActive").withSchema(TableName.OrgMembership), + db.ref("lastLoggedInAuthMethod").withSchema(TableName.OrgMembership), db.ref("email").withSchema(TableName.Users), db.ref("username").withSchema(TableName.Users), db.ref("firstName").withSchema(TableName.Users), @@ -64,7 +65,8 @@ export const orgMembershipDALFactory = (db: TDbClient) => { role, status, isActive, - inviteEmail + inviteEmail, + lastLoggedInAuthMethod }) => ({ roleId, orgId, @@ -73,6 +75,7 @@ export const orgMembershipDALFactory = (db: TDbClient) => { status, isActive, inviteEmail, + lastLoggedInAuthMethod, user: { id: userId, email, diff --git a/backend/src/services/org/org-dal.ts b/backend/src/services/org/org-dal.ts index b96e800a7..9f085284a 100644 --- a/backend/src/services/org/org-dal.ts +++ b/backend/src/services/org/org-dal.ts @@ -285,6 +285,7 @@ export const orgDALFactory = (db: TDbClient) => { db.ref("roleId").withSchema(TableName.OrgMembership), db.ref("status").withSchema(TableName.OrgMembership), db.ref("isActive").withSchema(TableName.OrgMembership), + db.ref("lastLoggedInAuthMethod").withSchema(TableName.OrgMembership), db.ref("email").withSchema(TableName.Users), db.ref("isEmailVerified").withSchema(TableName.Users), db.ref("username").withSchema(TableName.Users), diff --git a/frontend/src/components/organization/LastLoginSection/LastLoginSection.tsx b/frontend/src/components/organization/LastLoginSection/LastLoginSection.tsx new file mode 100644 index 000000000..50ad653c3 --- /dev/null +++ b/frontend/src/components/organization/LastLoginSection/LastLoginSection.tsx @@ -0,0 +1,23 @@ +import { faEnvelope } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +type Props = { + lastLoggedInAuthMethod: string; +}; + +export const LastLoginSection = ({ lastLoggedInAuthMethod }: Props) => ( +
+
+
Last Login Details
+
+
+
+ +
+
+
Authentication Method
+
{lastLoggedInAuthMethod}
+
+
+
+); diff --git a/frontend/src/components/organization/LastLoginSection/index.tsx b/frontend/src/components/organization/LastLoginSection/index.tsx new file mode 100644 index 000000000..607358130 --- /dev/null +++ b/frontend/src/components/organization/LastLoginSection/index.tsx @@ -0,0 +1 @@ +export { LastLoginSection } from "./LastLoginSection"; diff --git a/frontend/src/components/v2/Badge/Badge.tsx b/frontend/src/components/v2/Badge/Badge.tsx index 4355b0251..0e6addd77 100644 --- a/frontend/src/components/v2/Badge/Badge.tsx +++ b/frontend/src/components/v2/Badge/Badge.tsx @@ -1,3 +1,4 @@ +import { forwardRef } from "react"; import { cva, VariantProps } from "cva"; import { twMerge } from "tailwind-merge"; @@ -24,13 +25,16 @@ const badgeVariants = cva( export type BadgeProps = VariantProps & IProps; -export const Badge = ({ children, className, variant, ...props }: BadgeProps) => { - return ( -
- {children} -
- ); -}; +export const Badge = forwardRef( + ({ children, className, variant, ...props }, ref) => { + return ( +
+ {children} +
+ ); + } +); diff --git a/frontend/src/hooks/api/identities/types.ts b/frontend/src/hooks/api/identities/types.ts index c0e49e987..13472347b 100644 --- a/frontend/src/hooks/api/identities/types.ts +++ b/frontend/src/hooks/api/identities/types.ts @@ -41,6 +41,7 @@ export type IdentityMembershipOrg = { id: string; identity: Identity; organization: string; + lastLoggedInAuthMethod?: IdentityAuthMethod; metadata: { key: string; value: string; id: string }[]; role: "admin" | "member" | "viewer" | "no-access" | "custom"; customRole?: TOrgRole; diff --git a/frontend/src/hooks/api/users/types.ts b/frontend/src/hooks/api/users/types.ts index 48da6f7d1..ff25846cb 100644 --- a/frontend/src/hooks/api/users/types.ts +++ b/frontend/src/hooks/api/users/types.ts @@ -68,6 +68,7 @@ export type OrgUser = { deniedPermissions: any[]; roleId: string; isActive: boolean; + lastLoggedInAuthMethod?: AuthMethod; }; export type TProjectMembership = { diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx index 407ee3a55..0a5124013 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx @@ -7,6 +7,7 @@ import { faEdit, faEllipsisV, faFilter, + faInfoCircle, faMagnifyingGlass, faServer, faTrash @@ -16,6 +17,7 @@ import { useNavigate } from "@tanstack/react-router"; import { twMerge } from "tailwind-merge"; import { createNotification } from "@app/components/notifications"; +import { LastLoginSection } from "@app/components/organization/LastLoginSection"; import { OrgPermissionCan } from "@app/components/permissions"; import { DropdownMenu, @@ -40,6 +42,7 @@ import { Td, Th, THead, + Tooltip, Tr } from "@app/components/v2"; import { OrgPermissionIdentityActions, OrgPermissionSubjects, useOrganization } from "@app/context"; @@ -284,112 +287,129 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => { {isPending && } {!isPending && - data?.identities?.map(({ identity: { id, name }, role, customRole }) => { - return ( - - navigate({ - to: "/organization/identities/$identityId", - params: { - identityId: id - } - }) - } - > - {name} - - - {(isAllowed) => { - return ( - - ); - }} - - - - - - { + return ( + + navigate({ + to: "/organization/identities/$identityId", + params: { + identityId: id + } + }) + } + > + + {name} + {lastLoggedInAuthMethod && ( + + } > - - - - - - {(isAllowed) => ( - } - onClick={(e) => { - e.stopPropagation(); - navigate({ - to: "/organization/identities/$identityId", - params: { - identityId: id - } - }); - }} + + + )} + + + + {(isAllowed) => { + return ( + + ); + }} + + + + + + + + + + + + {(isAllowed) => ( + } + onClick={(e) => { + e.stopPropagation(); + navigate({ + to: "/organization/identities/$identityId", + params: { + identityId: id + } + }); + }} + isDisabled={!isAllowed} + > + Edit Identity + + )} + + + {(isAllowed) => ( + { + e.stopPropagation(); + handlePopUpOpen("deleteIdentity", { + identityId: id, + name + }); + }} + isDisabled={!isAllowed} + icon={} + > + Delete Identity + + )} + + + + + + ); + } + )} {!isPending && data && totalCount > 0 && ( diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx index 679e9f664..4788c51cd 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx @@ -7,6 +7,7 @@ import { faEdit, faEllipsisV, faFilter, + faInfoCircle, faMagnifyingGlass, faSearch, faUsers, @@ -19,6 +20,7 @@ import { useNavigate } from "@tanstack/react-router"; import { twMerge } from "tailwind-merge"; import { createNotification } from "@app/components/notifications"; +import { LastLoginSection } from "@app/components/organization/LastLoginSection"; import { OrgPermissionCan } from "@app/components/permissions"; import { Badge, @@ -471,7 +473,16 @@ export const OrgMembersTable = ({ {isLoading && } {!isLoading && filteredMembersPage.map( - ({ user: u, inviteEmail, role, roleId, id: orgMembershipId, status, isActive }) => { + ({ + user: u, + inviteEmail, + role, + roleId, + id: orgMembershipId, + status, + isActive, + lastLoggedInAuthMethod + }) => { const name = u && u.firstName ? `${u.firstName} ${u.lastName ?? ""}`.trim() : null; const email = u?.email || inviteEmail; @@ -504,7 +515,9 @@ export const OrgMembersTable = ({ }} /> - +

{name ?? Not Set} @@ -517,6 +530,19 @@ export const OrgMembersTable = ({ )} + {lastLoggedInAuthMethod && ( + + } + > + + + )}

diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityDetailsSection.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityDetailsSection.tsx index 328c5505d..339dac41d 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityDetailsSection.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityDetailsSection.tsx @@ -138,6 +138,10 @@ export const IdentityDetailsSection = ({ identityId, handlePopUpOpen }: Props) =

Name

{data.identity.name}

+
+

Last Login Auth Method

+

{data.lastLoggedInAuthMethod || "-"}

+

Delete Protection

diff --git a/frontend/src/pages/organization/UserDetailsByIDPage/components/UserDetailsSection.tsx b/frontend/src/pages/organization/UserDetailsByIDPage/components/UserDetailsSection.tsx index a54750aa6..50f3804d0 100644 --- a/frontend/src/pages/organization/UserDetailsByIDPage/components/UserDetailsSection.tsx +++ b/frontend/src/pages/organization/UserDetailsByIDPage/components/UserDetailsSection.tsx @@ -159,6 +159,14 @@ export const UserDetailsSection = ({ membershipId, handlePopUpOpen }: Props) =>

+
+

Last Login Auth Method

+
+

+ {membership.lastLoggedInAuthMethod || "-"} +

+
+

Organization Role

{roleName ?? "-"}