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..a373e0a91 --- /dev/null +++ b/backend/src/db/migrations/20250805151349_last-logged-auth-method.ts @@ -0,0 +1,65 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const lastUserLoggedInAuthMethod = await knex.schema.hasColumn(TableName.OrgMembership, "lastLoginAuthMethod"); + const lastIdentityLoggedInAuthMethod = await knex.schema.hasColumn( + TableName.IdentityOrgMembership, + "lastLoginAuthMethod" + ); + const lastUserLoggedInTime = await knex.schema.hasColumn(TableName.OrgMembership, "lastLoginTime"); + const lastIdentityLoggedInTime = await knex.schema.hasColumn(TableName.IdentityOrgMembership, "lastLoginTime"); + if (!lastUserLoggedInAuthMethod || !lastUserLoggedInTime) { + await knex.schema.alterTable(TableName.OrgMembership, (t) => { + if (!lastUserLoggedInAuthMethod) { + t.string("lastLoginAuthMethod").nullable(); + } + if (!lastUserLoggedInTime) { + t.datetime("lastLoginTime").nullable(); + } + }); + } + + if (!lastIdentityLoggedInAuthMethod || !lastIdentityLoggedInTime) { + await knex.schema.alterTable(TableName.IdentityOrgMembership, (t) => { + if (!lastIdentityLoggedInAuthMethod) { + t.string("lastLoginAuthMethod").nullable(); + } + if (!lastIdentityLoggedInTime) { + t.datetime("lastLoginTime").nullable(); + } + }); + } +} + +export async function down(knex: Knex): Promise { + const lastUserLoggedInAuthMethod = await knex.schema.hasColumn(TableName.OrgMembership, "lastLoginAuthMethod"); + const lastIdentityLoggedInAuthMethod = await knex.schema.hasColumn( + TableName.IdentityOrgMembership, + "lastLoginAuthMethod" + ); + const lastUserLoggedInTime = await knex.schema.hasColumn(TableName.OrgMembership, "lastLoginTime"); + const lastIdentityLoggedInTime = await knex.schema.hasColumn(TableName.IdentityOrgMembership, "lastLoginTime"); + if (lastUserLoggedInAuthMethod || lastUserLoggedInTime) { + await knex.schema.alterTable(TableName.OrgMembership, (t) => { + if (lastUserLoggedInAuthMethod) { + t.dropColumn("lastLoginAuthMethod"); + } + if (lastUserLoggedInTime) { + t.dropColumn("lastLoginTime"); + } + }); + } + + if (lastIdentityLoggedInAuthMethod || lastIdentityLoggedInTime) { + await knex.schema.alterTable(TableName.IdentityOrgMembership, (t) => { + if (lastIdentityLoggedInAuthMethod) { + t.dropColumn("lastLoginAuthMethod"); + } + if (lastIdentityLoggedInTime) { + t.dropColumn("lastLoginTime"); + } + }); + } +} diff --git a/backend/src/db/schemas/identity-org-memberships.ts b/backend/src/db/schemas/identity-org-memberships.ts index 2f29c52e4..85cba8dfd 100644 --- a/backend/src/db/schemas/identity-org-memberships.ts +++ b/backend/src/db/schemas/identity-org-memberships.ts @@ -14,7 +14,9 @@ export const IdentityOrgMembershipsSchema = z.object({ orgId: z.string().uuid(), createdAt: z.date(), updatedAt: z.date(), - identityId: z.string().uuid() + identityId: z.string().uuid(), + lastLoginAuthMethod: z.string().nullable().optional(), + lastLoginTime: z.date().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..2112870d1 100644 --- a/backend/src/db/schemas/org-memberships.ts +++ b/backend/src/db/schemas/org-memberships.ts @@ -19,7 +19,9 @@ 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(), + lastLoginAuthMethod: z.string().nullable().optional(), + lastLoginTime: z.date().nullable().optional() }); export type TOrgMemberships = z.infer; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 64cfb140e..c4f0de34a 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -45,6 +45,8 @@ import { groupServiceFactory } from "@app/ee/services/group/group-service"; import { userGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; import { hsmServiceFactory } from "@app/ee/services/hsm/hsm-service"; import { HsmModule } from "@app/ee/services/hsm/hsm-types"; +import { identityAuthTemplateDALFactory } from "@app/ee/services/identity-auth-template/identity-auth-template-dal"; +import { identityAuthTemplateServiceFactory } from "@app/ee/services/identity-auth-template/identity-auth-template-service"; import { identityProjectAdditionalPrivilegeDALFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal"; import { identityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service"; import { identityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service"; @@ -179,8 +181,6 @@ import { identityAccessTokenDALFactory } from "@app/services/identity-access-tok import { identityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service"; import { identityAliCloudAuthDALFactory } from "@app/services/identity-alicloud-auth/identity-alicloud-auth-dal"; import { identityAliCloudAuthServiceFactory } from "@app/services/identity-alicloud-auth/identity-alicloud-auth-service"; -import { identityAuthTemplateDALFactory } from "@app/ee/services/identity-auth-template/identity-auth-template-dal"; -import { identityAuthTemplateServiceFactory } from "@app/ee/services/identity-auth-template/identity-auth-template-service"; import { identityAwsAuthDALFactory } from "@app/services/identity-aws-auth/identity-aws-auth-dal"; import { identityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/identity-aws-auth-service"; import { identityAzureAuthDALFactory } from "@app/services/identity-azure-auth/identity-azure-auth-dal"; diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts index 7dd1d3aa4..ab0f7deba 100644 --- a/backend/src/services/auth/auth-login-service.ts +++ b/backend/src/services/auth/auth-login-service.ts @@ -148,9 +148,15 @@ 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 }, + { lastLoginAuthMethod: authMethod, lastLoginTime: new Date() } + ); + if (org.userTokenExpiration) { + tokenSessionExpiresIn = getMinExpiresIn(cfg.JWT_AUTH_LIFETIME, org.userTokenExpiration); + refreshTokenExpiresIn = org.userTokenExpiration; + } } } diff --git a/backend/src/services/certificate-authority/internal/internal-certificate-authority-fns.ts b/backend/src/services/certificate-authority/internal/internal-certificate-authority-fns.ts index 6811476f8..5671435f6 100644 --- a/backend/src/services/certificate-authority/internal/internal-certificate-authority-fns.ts +++ b/backend/src/services/certificate-authority/internal/internal-certificate-authority-fns.ts @@ -32,8 +32,8 @@ import { keyAlgorithmToAlgCfg } from "../certificate-authority-fns"; import { TCertificateAuthoritySecretDALFactory } from "../certificate-authority-secret-dal"; -import { TIssueCertWithTemplateDTO } from "./internal-certificate-authority-types"; import { validateAndMapAltNameType } from "../certificate-authority-validators"; +import { TIssueCertWithTemplateDTO } from "./internal-certificate-authority-types"; type TInternalCertificateAuthorityFnsDeps = { certificateAuthorityDAL: Pick; diff --git a/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts b/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts index 510a160e9..9fed186bc 100644 --- a/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/internal/internal-certificate-authority-service.ts @@ -52,6 +52,7 @@ import { } from "../certificate-authority-fns"; import { TCertificateAuthorityQueueFactory } from "../certificate-authority-queue"; import { TCertificateAuthoritySecretDALFactory } from "../certificate-authority-secret-dal"; +import { validateAndMapAltNameType } from "../certificate-authority-validators"; import { TInternalCertificateAuthorityDALFactory } from "./internal-certificate-authority-dal"; import { TCreateCaDTO, @@ -68,7 +69,6 @@ import { TSignIntermediateDTO, TUpdateCaDTO } from "./internal-certificate-authority-types"; -import { validateAndMapAltNameType } from "../certificate-authority-validators"; type TInternalCertificateAuthorityServiceFactoryDep = { certificateAuthorityDAL: Pick< 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..af94c79c9 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; }; @@ -64,6 +64,8 @@ export const identityAliCloudAuthServiceFactory = ({ identityId: identityAliCloudAuth.identityId }); + if (!identityMembershipOrg) throw new UnauthorizedError({ message: "Identity not attached to a organization" }); + const requestUrl = new URL("https://sts.aliyuncs.com"); for (const key of Object.keys(params)) { @@ -87,6 +89,14 @@ export const identityAliCloudAuthServiceFactory = ({ // Generate the token const identityAccessToken = await identityAliCloudAuthDAL.transaction(async (tx) => { + await identityOrgMembershipDAL.updateById( + identityMembershipOrg.id, + { + lastLoginAuthMethod: IdentityAuthMethod.ALICLOUD_AUTH, + lastLoginTime: new Date() + }, + 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..3dff47403 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; }; @@ -91,6 +91,7 @@ export const identityAwsAuthServiceFactory = ({ } const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: identityAwsAuth.identityId }); + if (!identityMembershipOrg) throw new UnauthorizedError({ message: "Identity not attached to a organization" }); const headers: TAwsGetCallerIdentityHeaders = JSON.parse(Buffer.from(iamRequestHeaders, "base64").toString()); const body: string = Buffer.from(iamRequestBody, "base64").toString(); @@ -152,6 +153,14 @@ export const identityAwsAuthServiceFactory = ({ } const identityAccessToken = await identityAwsAuthDAL.transaction(async (tx) => { + await identityOrgMembershipDAL.updateById( + identityMembershipOrg.id, + { + lastLoginAuthMethod: IdentityAuthMethod.AWS_AUTH, + lastLoginTime: new Date() + }, + 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..9a2426a7c 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,14 @@ export const identityAzureAuthServiceFactory = ({ } const identityAccessToken = await identityAzureAuthDAL.transaction(async (tx) => { + await identityOrgMembershipDAL.updateById( + identityMembershipOrg.id, + { + lastLoginAuthMethod: IdentityAuthMethod.AZURE_AUTH, + lastLoginTime: new Date() + }, + 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..388c24d48 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,14 @@ export const identityGcpAuthServiceFactory = ({ } const identityAccessToken = await identityGcpAuthDAL.transaction(async (tx) => { + await identityOrgMembershipDAL.updateById( + identityMembershipOrg.id, + { + lastLoginAuthMethod: IdentityAuthMethod.GCP_AUTH, + lastLoginTime: new Date() + }, + 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..7b0a19414 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,14 @@ export const identityJwtAuthServiceFactory = ({ } const identityAccessToken = await identityJwtAuthDAL.transaction(async (tx) => { + await identityOrgMembershipDAL.updateById( + identityMembershipOrg.id, + { + lastLoginAuthMethod: IdentityAuthMethod.JWT_AUTH, + lastLoginTime: new Date() + }, + 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..9584b122a 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,14 @@ export const identityKubernetesAuthServiceFactory = ({ } const identityAccessToken = await identityKubernetesAuthDAL.transaction(async (tx) => { + await identityOrgMembershipDAL.updateById( + identityMembershipOrg.id, + { + lastLoginAuthMethod: IdentityAuthMethod.KUBERNETES_AUTH, + lastLoginTime: new Date() + }, + 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..47188e26d 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,14 @@ export const identityLdapAuthServiceFactory = ({ } const identityAccessToken = await identityLdapAuthDAL.transaction(async (tx) => { + await identityOrgMembershipDAL.updateById( + identityMembershipOrg.id, + { + lastLoginAuthMethod: IdentityAuthMethod.LDAP_AUTH, + lastLoginTime: new Date() + }, + 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..a4294250c 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; }; @@ -57,6 +57,7 @@ export const identityOciAuthServiceFactory = ({ } const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: identityOciAuth.identityId }); + if (!identityMembershipOrg) throw new UnauthorizedError({ message: "Identity not attached to a organization" }); // Validate OCI host format. Ensures that the host is in "identity..oraclecloud.com" format. if (!headers.host || !new RE2("^identity\\.([a-z]{2}-[a-z]+-[1-9])\\.oraclecloud\\.com$").test(headers.host)) { @@ -91,6 +92,14 @@ export const identityOciAuthServiceFactory = ({ // Generate the token const identityAccessToken = await identityOciAuthDAL.transaction(async (tx) => { + await identityOrgMembershipDAL.updateById( + identityMembershipOrg.id, + { + lastLoginAuthMethod: IdentityAuthMethod.OCI_AUTH, + lastLoginTime: new Date() + }, + 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..617b21a1f 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,14 @@ export const identityOidcAuthServiceFactory = ({ } const identityAccessToken = await identityOidcAuthDAL.transaction(async (tx) => { + await identityOrgMembershipDAL.updateById( + identityMembershipOrg.id, + { + lastLoginAuthMethod: IdentityAuthMethod.OIDC_AUTH, + lastLoginTime: new Date() + }, + 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..ef2463eec 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,14 @@ export const identityTlsCertAuthServiceFactory = ({ // Generate the token const identityAccessToken = await identityTlsCertAuthDAL.transaction(async (tx) => { + await identityOrgMembershipDAL.updateById( + identityMembershipOrg.id, + { + lastLoginAuthMethod: IdentityAuthMethod.TLS_CERT_AUTH, + lastLoginTime: new Date() + }, + 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..d3743bd96 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,14 @@ export const identityTokenAuthServiceFactory = ({ const identityTokenAuth = await identityTokenAuthDAL.findOne({ identityId }); const identityAccessToken = await identityTokenAuthDAL.transaction(async (tx) => { + await identityOrgMembershipDAL.updateById( + identityMembershipOrg.id, + { + lastLoginAuthMethod: IdentityAuthMethod.TOKEN_AUTH, + lastLoginTime: new Date() + }, + 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..cd7211b5c 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,14 @@ export const identityUaServiceFactory = ({ const identityAccessToken = await identityUaDAL.transaction(async (tx) => { const uaClientSecretDoc = await identityUaClientSecretDAL.incrementUsage(validClientSecretInfo!.id, tx); - + await identityOrgMembershipDAL.updateById( + identityMembershipOrg.id, + { + lastLoginAuthMethod: IdentityAuthMethod.UNIVERSAL_AUTH, + lastLoginTime: new Date() + }, + 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..c083df5aa 100644 --- a/backend/src/services/identity/identity-org-dal.ts +++ b/backend/src/services/identity/identity-org-dal.ts @@ -254,6 +254,8 @@ export const identityOrgDALFactory = (db: TDbClient) => { db.ref("role").withSchema("paginatedIdentity"), db.ref("roleId").withSchema("paginatedIdentity"), db.ref("orgId").withSchema("paginatedIdentity"), + db.ref("lastLoginAuthMethod").withSchema("paginatedIdentity"), + db.ref("lastLoginTime").withSchema("paginatedIdentity"), db.ref("createdAt").withSchema("paginatedIdentity"), db.ref("updatedAt").withSchema("paginatedIdentity"), db.ref("identityId").withSchema("paginatedIdentity").as("identityId"), @@ -319,7 +321,9 @@ export const identityOrgDALFactory = (db: TDbClient) => { ldapId, tlsCertId, createdAt, - updatedAt + updatedAt, + lastLoginAuthMethod, + lastLoginTime }) => ({ role, roleId, @@ -328,6 +332,8 @@ export const identityOrgDALFactory = (db: TDbClient) => { orgId, createdAt, updatedAt, + lastLoginAuthMethod, + lastLoginTime, customRole: roleId ? { id: crId, @@ -497,6 +503,8 @@ 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("lastLoginAuthMethod").withSchema(TableName.IdentityOrgMembership), + db.ref("lastLoginTime").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), @@ -531,10 +539,10 @@ export const identityOrgDALFactory = (db: TDbClient) => { } else if (orderBy === OrgIdentityOrderBy.Role) { void query.orderByRaw( ` - CASE - WHEN ??.role = ? - THEN ??.slug - ELSE ??.role + CASE + WHEN ??.role = ? + THEN ??.slug + ELSE ??.role END ? `, [ @@ -576,7 +584,9 @@ export const identityOrgDALFactory = (db: TDbClient) => { tokenId, ldapId, createdAt, - updatedAt + updatedAt, + lastLoginTime, + lastLoginAuthMethod }) => ({ role, roleId, @@ -586,6 +596,8 @@ export const identityOrgDALFactory = (db: TDbClient) => { orgId, createdAt, updatedAt, + lastLoginTime, + lastLoginAuthMethod, 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..8f2ca01f0 100644 --- a/backend/src/services/org-membership/org-membership-dal.ts +++ b/backend/src/services/org-membership/org-membership-dal.ts @@ -32,6 +32,8 @@ 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("lastLoginAuthMethod").withSchema(TableName.OrgMembership), + db.ref("lastLoginTime").withSchema(TableName.OrgMembership), db.ref("email").withSchema(TableName.Users), db.ref("username").withSchema(TableName.Users), db.ref("firstName").withSchema(TableName.Users), @@ -64,7 +66,9 @@ export const orgMembershipDALFactory = (db: TDbClient) => { role, status, isActive, - inviteEmail + inviteEmail, + lastLoginAuthMethod, + lastLoginTime }) => ({ roleId, orgId, @@ -73,6 +77,8 @@ export const orgMembershipDALFactory = (db: TDbClient) => { status, isActive, inviteEmail, + lastLoginAuthMethod, + lastLoginTime, user: { id: userId, email, diff --git a/backend/src/services/org/org-dal.ts b/backend/src/services/org/org-dal.ts index b96e800a7..b46efc46c 100644 --- a/backend/src/services/org/org-dal.ts +++ b/backend/src/services/org/org-dal.ts @@ -285,6 +285,8 @@ 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("lastLoginAuthMethod").withSchema(TableName.OrgMembership), + db.ref("lastLoginTime").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..f03ff077d --- /dev/null +++ b/frontend/src/components/organization/LastLoginSection/LastLoginSection.tsx @@ -0,0 +1,34 @@ +import { faClock, faShield } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { format } from "date-fns"; + +type Props = { + lastLoginAuthMethod: string; + lastLoginTime: string; +}; + +export const LastLoginSection = ({ lastLoginTime, lastLoginAuthMethod }: Props) => ( +
+
+
Last Login
+
+
+
+ +
+
+
Authentication Method
+
{lastLoginAuthMethod}
+
+
+
+
+ +
+
+
Time
+
{format(lastLoginTime, "PPpp")}
+
+
+
+); 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..7098ce244 100644 --- a/frontend/src/hooks/api/identities/types.ts +++ b/frontend/src/hooks/api/identities/types.ts @@ -41,6 +41,8 @@ export type IdentityMembershipOrg = { id: string; identity: Identity; organization: string; + lastLoginAuthMethod?: IdentityAuthMethod; + lastLoginTime?: string; 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..3af283b7e 100644 --- a/frontend/src/hooks/api/users/types.ts +++ b/frontend/src/hooks/api/users/types.ts @@ -68,6 +68,8 @@ export type OrgUser = { deniedPermissions: any[]; roleId: string; isActive: boolean; + lastLoginAuthMethod?: AuthMethod; + lastLoginTime?: string; }; 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..74902046a 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"; @@ -49,7 +52,12 @@ import { setUserTablePreference } from "@app/helpers/userTablePreferences"; import { usePagination, useResetPageHelper } from "@app/hooks"; -import { useGetOrgRoles, useSearchIdentities, useUpdateIdentity } from "@app/hooks/api"; +import { + identityAuthToNameMap, + useGetOrgRoles, + useSearchIdentities, + useUpdateIdentity +} from "@app/hooks/api"; import { OrderByDirection } from "@app/hooks/api/generic/types"; import { OrgIdentityOrderBy } from "@app/hooks/api/organization/types"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -284,112 +292,138 @@ 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} + {lastLoginAuthMethod && lastLoginTime && ( + + } > - - - - - - {(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..b0a96399e 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,17 @@ 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, + lastLoginAuthMethod, + lastLoginTime + }) => { const name = u && u.firstName ? `${u.firstName} ${u.lastName ?? ""}`.trim() : null; const email = u?.email || inviteEmail; @@ -504,7 +516,9 @@ export const OrgMembersTable = ({ }} /> - +

{name ?? Not Set} @@ -517,6 +531,22 @@ export const OrgMembersTable = ({ )} + {lastLoginAuthMethod && lastLoginTime && ( + + } + > + + + )}

diff --git a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityDetailsSection.tsx b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityDetailsSection.tsx index 328c5505d..b6fc4519b 100644 --- a/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityDetailsSection.tsx +++ b/frontend/src/pages/organization/IdentityDetailsByIDPage/components/IdentityDetailsSection.tsx @@ -7,6 +7,7 @@ import { faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { format } from "date-fns"; import { twMerge } from "tailwind-merge"; import { OrgPermissionCan } from "@app/components/permissions"; @@ -22,7 +23,7 @@ import { } from "@app/components/v2"; import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/context"; import { useTimedReset } from "@app/hooks"; -import { useGetIdentityById } from "@app/hooks/api"; +import { identityAuthToNameMap, useGetIdentityById } from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; type Props = { @@ -138,6 +139,18 @@ export const IdentityDetailsSection = ({ identityId, handlePopUpOpen }: Props) =

Name

{data.identity.name}

+
+

Last Login Auth Method

+

+ {data.lastLoginAuthMethod ? identityAuthToNameMap[data.lastLoginAuthMethod] : "-"} +

+
+
+

Last Login Time

+

+ {data.lastLoginTime ? format(data.lastLoginTime, "PPpp") : "-"} +

+

Delete Protection

diff --git a/frontend/src/pages/organization/UserDetailsByIDPage/components/UserDetailsSection.tsx b/frontend/src/pages/organization/UserDetailsByIDPage/components/UserDetailsSection.tsx index a54750aa6..6d3d7e1ae 100644 --- a/frontend/src/pages/organization/UserDetailsByIDPage/components/UserDetailsSection.tsx +++ b/frontend/src/pages/organization/UserDetailsByIDPage/components/UserDetailsSection.tsx @@ -7,6 +7,7 @@ import { faPencil } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { format } from "date-fns"; import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions"; @@ -159,6 +160,22 @@ export const UserDetailsSection = ({ membershipId, handlePopUpOpen }: Props) =>

+
+

Last Login Auth Method

+
+

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

+
+
+
+

Last Login Time

+
+

+ {membership.lastLoginTime ? format(membership.lastLoginTime, "PPpp") : "-"} +

+
+

Organization Role

{roleName ?? "-"}