From ca9698f8343256b25f40aa01522032de1c1c4bc0 Mon Sep 17 00:00:00 2001 From: Quentin AUBERT Date: Fri, 30 Aug 2024 18:07:16 +0200 Subject: [PATCH 01/44] WIP #708 - expose an api to transfer apiSubscription - tests OK --- daikoku/app/controllers/ApiController.scala | 35 ++ .../controllers/NotificationController.scala | 7 +- daikoku/app/domain/json.scala | 69 ++++ daikoku/app/domain/teamEntities.scala | 10 + daikoku/app/utils/ApiService.scala | 71 ++++ daikoku/conf/messages.en | 3 +- daikoku/conf/messages.fr | 3 +- daikoku/conf/routes | 1 + daikoku/test/daikoku/ApiControllerSpec.scala | 368 +++++++++++++++++- daikoku/test/daikoku/suites.scala | 255 ++++++------ 10 files changed, 682 insertions(+), 140 deletions(-) diff --git a/daikoku/app/controllers/ApiController.scala b/daikoku/app/controllers/ApiController.scala index 7e20ef0ea..42ad1b53a 100644 --- a/daikoku/app/controllers/ApiController.scala +++ b/daikoku/app/controllers/ApiController.scala @@ -2075,6 +2075,41 @@ class ApiController( } } + def transferSubscription(teamId: String, subscriptionId: String) = + DaikokuAction.async(parse.json) { ctx => + TeamAdminOnly( + AuditTrailEvent(s"@{user.name} has ask to transfer subscription @{subscriptionId} to team @{teamId}"))(teamId, ctx) { team => { + val newTeamId = (ctx.request.body \ "team").as[String] + + (for { + newTeam <- EitherT.fromOptionF[Future, AppError, Team](env.dataStore.teamRepo.forTenant(ctx.tenant).findByIdOrHrIdNotDeleted(newTeamId), + AppError.TeamNotFound) + subscription <- EitherT.fromOptionF[Future, AppError, ApiSubscription](env.dataStore.apiSubscriptionRepo.forTenant(ctx.tenant).findByIdOrHrIdNotDeleted(subscriptionId), + AppError.SubscriptionNotFound) + _ <- EitherT.cond[Future][AppError, Unit](subscription.parent.isEmpty, (), AppError.EntityConflict("Subscription is part of aggregation")) + _ <- EitherT.liftF[Future, AppError, Boolean](env.dataStore.notificationRepo.forTenant(ctx.tenant).save( + Notification( + id = NotificationId( + IdGenerator.token(32) + ), + tenant = ctx.tenant.id, + sender = ctx.user.asNotificationSender, + action = + NotificationAction.ApiSubscriptionTransfer( + subscription = subscription.id + ), + notificationType = + NotificationType.AcceptOrReject, + team = newTeam.id.some + ) + )) + } yield Ok(Json.obj("done" -> true))) + .leftMap(_.render()) + .merge + } + } + } + def makeUniqueSubscription(teamId: String, subscriptionId: String) = DaikokuAction.async { ctx => TeamApiKeyAction( diff --git a/daikoku/app/controllers/NotificationController.scala b/daikoku/app/controllers/NotificationController.scala index 28d1ff001..4eb305132 100644 --- a/daikoku/app/controllers/NotificationController.scala +++ b/daikoku/app/controllers/NotificationController.scala @@ -316,6 +316,7 @@ class NotificationController( notification.sender ) ) + case ApiSubscriptionTransfer(subscriptionId) => apiService.transferSubscription(team, subscriptionId, ctx.tenant, ctx.user) case TeamInvitation(_, user) if user != ctx.user.id => EitherT.leftT[Future, Unit](ForbiddenAction) case TeamInvitation(team, user) => @@ -386,7 +387,7 @@ class NotificationController( AuditTrailEvent( s"@{user.name} has rejected a notifications for team @{team.name} - @{team.id} => @{notification.id}" ) - )(teamId.value, ctx) { _ => + )(teamId.value, ctx) { team => { ctx.setCtxValue("notification.id", notification.id) @@ -531,6 +532,10 @@ class NotificationController( ) ) } yield body + case ApiSubscriptionTransfer(subscriptionId) => apiService.declineSubscriptionTransfer(subscriptionId, ctx.tenant, ctx.user) + .leftMap(_.getErrorMessage()) + .merge + case TransferApiOwnership(team, api) => val result = for { api <- diff --git a/daikoku/app/domain/json.scala b/daikoku/app/domain/json.scala index e632c15b9..474552d45 100644 --- a/daikoku/app/domain/json.scala +++ b/daikoku/app/domain/json.scala @@ -3219,6 +3219,9 @@ object json { case "NewIssueOpen" => NewIssueOpenFormat.reads(json) case "NewCommentOnIssue" => NewCommentOnIssueFormat.reads(json) case "TransferApiOwnership" => TransferApiOwnershipFormat.reads(json) + case "ApiSubscriptionTransfer" => ApiSubscriptionTransferFormat.reads(json) + case "ApiSubscriptionTransferSuccess" => ApiSubscriptionTransferSuccessFormat.reads(json) + case "ApiSubscriptionTransferReject" => ApiSubscriptionTransferRejectFormat.reads(json) case "CheckoutForSubscription" => CheckoutForSubscriptionFormat.reads(json) case str => JsError(s"Bad notification value: $str") @@ -3238,6 +3241,18 @@ object json { ApiSubscriptionDemandFormat.writes(p).as[JsObject] ++ Json.obj( "type" -> "ApiSubscription" ) + case p: ApiSubscriptionTransfer => + ApiSubscriptionTransferFormat.writes(p).as[JsObject] ++ Json.obj( + "type" -> "ApiSubscriptionTransfer" + ) + case p: ApiSubscriptionTransferSuccess => + ApiSubscriptionTransferSuccessFormat.writes(p).as[JsObject] ++ Json.obj( + "type" -> "ApiSubscriptionTransferSuccess" + ) + case p: ApiSubscriptionTransferReject => + ApiSubscriptionTransferRejectFormat.writes(p).as[JsObject] ++ Json.obj( + "type" -> "ApiSubscriptionTransferReject" + ) case p: ApiSubscriptionReject => ApiSubscriptionRejectFormat.writes(p).as[JsObject] ++ Json.obj( "type" -> "ApiSubscriptionReject" @@ -3481,6 +3496,60 @@ object json { .as[JsValue] ) } + val ApiSubscriptionTransferFormat = new Format[ApiSubscriptionTransfer] { + + override def reads(json: JsValue): JsResult[ApiSubscriptionTransfer] = + Try { + JsSuccess( + ApiSubscriptionTransfer( + subscription = (json \ "subscription").as(ApiSubscriptionIdFormat) + ) + ) + } recover { + case e => JsError(e.getMessage) + } get + + override def writes(o: ApiSubscriptionTransfer): JsValue = + Json.obj( + "subscription" -> o.subscription.asJson + ) + } + val ApiSubscriptionTransferSuccessFormat = new Format[ApiSubscriptionTransferSuccess] { + + override def reads(json: JsValue): JsResult[ApiSubscriptionTransferSuccess] = + Try { + JsSuccess( + ApiSubscriptionTransferSuccess( + subscription = (json \ "subscription").as(ApiSubscriptionIdFormat) + ) + ) + } recover { + case e => JsError(e.getMessage) + } get + + override def writes(o: ApiSubscriptionTransferSuccess): JsValue = + Json.obj( + "subscription" -> o.subscription.asJson + ) + } + val ApiSubscriptionTransferRejectFormat = new Format[ApiSubscriptionTransferReject] { + + override def reads(json: JsValue): JsResult[ApiSubscriptionTransferReject] = + Try { + JsSuccess( + ApiSubscriptionTransferReject( + subscription = (json \ "subscription").as(ApiSubscriptionIdFormat) + ) + ) + } recover { + case e => JsError(e.getMessage) + } get + + override def writes(o: ApiSubscriptionTransferReject): JsValue = + Json.obj( + "subscription" -> o.subscription.asJson + ) + } val ApiSubscriptionRejectFormat = new Format[ApiSubscriptionReject] { override def reads(json: JsValue): JsResult[ApiSubscriptionReject] = Try { diff --git a/daikoku/app/domain/teamEntities.scala b/daikoku/app/domain/teamEntities.scala index 73421b6d1..b95cc5850 100644 --- a/daikoku/app/domain/teamEntities.scala +++ b/daikoku/app/domain/teamEntities.scala @@ -227,6 +227,16 @@ object NotificationAction { motivation: Option[String] ) extends NotificationAction + case class ApiSubscriptionTransfer( + subscription: ApiSubscriptionId, + ) extends NotificationAction + case class ApiSubscriptionTransferSuccess( + subscription: ApiSubscriptionId, + ) extends NotificationAction + case class ApiSubscriptionTransferReject( + subscription: ApiSubscriptionId, + ) extends NotificationAction + case class OtoroshiSyncSubscriptionError( subscription: ApiSubscription, message: String diff --git a/daikoku/app/utils/ApiService.scala b/daikoku/app/utils/ApiService.scala index 2d88f3803..d34a40ca1 100644 --- a/daikoku/app/utils/ApiService.scala +++ b/daikoku/app/utils/ApiService.scala @@ -2607,4 +2607,75 @@ class ApiService( } yield Ok(Json.obj("creation" -> "refused")) } + def transferSubscription(maybeTeam: Option[Team], subscriptionId: ApiSubscriptionId, tenant: Tenant, user: User) = + for { + newTeam <- EitherT.fromOption[Future][AppError, Team](maybeTeam, AppError.TeamNotFound) + subscription <- EitherT.fromOptionF[Future, AppError, ApiSubscription](env.dataStore.apiSubscriptionRepo.forTenant(tenant).findByIdNotDeleted(subscriptionId), + AppError.SubscriptionNotFound) + _ <- EitherT.cond[Future][AppError, Unit](subscription.parent.isEmpty, (), AppError.EntityConflict("Subscription is part of aggregation")) + childs <- EitherT.liftF[Future, AppError, Seq[ApiSubscription]]( + env.dataStore.apiSubscriptionRepo.forTenant(tenant).findNotDeleted(Json.obj("parent" -> subscription.id.asJson))) + result <- EitherT.liftF[Future, AppError, Long](env.dataStore.apiSubscriptionRepo.forTenant(tenant).updateManyByQuery( + Json.obj( + "_id" -> Json + .obj("$in" -> JsArray(childs.map(_.id.asJson) :+ subscription.id.asJson)) + ), + Json.obj( + "$set" -> Json.obj("team" -> newTeam.id.asJson) + ) + )) + _ <- EitherT.liftF[Future, AppError, Boolean](env.dataStore.notificationRepo.forTenant(tenant).save( + Notification( + id = NotificationId( + IdGenerator.token(32) + ), + tenant = tenant.id, + sender = user.asNotificationSender, + action = + NotificationAction.ApiSubscriptionTransferSuccess( + subscription = subscription.id + ), + notificationType = + NotificationType.AcceptOnly, + team = newTeam.id.some + ) + )) + + //todo: update apkname ? + //todo: update apk metadata to set new team name ? + } yield result + + def declineSubscriptionTransfer(subscriptionId: ApiSubscriptionId, tenant: Tenant, user: User)(implicit lang: String) = + for { + subscription <- EitherT.fromOptionF[Future, AppError, ApiSubscription](env.dataStore.apiSubscriptionRepo.forTenant(tenant).findById(subscriptionId), + AppError.SubscriptionNotFound) + api <- EitherT.fromOptionF[Future, AppError, Api](env.dataStore.apiSubscriptionRepo.forTenant(tenant).findById(subscriptionId), + AppError.ApiNotFound) + plan <- EitherT.fromOptionF[Future, AppError, UsagePlan](env.dataStore.apiSubscriptionRepo.forTenant(tenant).findById(subscriptionId), + AppError.SubscriptionNotFound) + ownerTeam <- EitherT.fromOptionF[Future, AppError, Team](env.dataStore.teamRepo.forTenant(tenant).findById(subscription.team), AppError.TeamNotFound) + _ <- EitherT.liftF[Future, AppError, Boolean](env.dataStore.notificationRepo.forTenant(tenant).save( + Notification( + id = NotificationId( + IdGenerator.token(32) + ), + tenant = tenant.id, + sender = user.asNotificationSender, + action = + NotificationAction.ApiSubscriptionTransferReject( + subscription = subscription.id + ), + notificationType = + NotificationType.AcceptOnly, + team = ownerTeam.id.some + ) + )) + body <- EitherT.liftF[Future, AppError, String](translator.translate( + "mail.api.subscription.transfer.rejection.body", + tenant, + Map( + "subscription" -> s"${api.name}/${plan.customName.getOrElse(plan.typeName)}" + ) + )) + } yield body } diff --git a/daikoku/conf/messages.en b/daikoku/conf/messages.en index 3d1a7eae7..627e91c1b 100644 --- a/daikoku/conf/messages.en +++ b/daikoku/conf/messages.en @@ -94,4 +94,5 @@ mail.subscription.validation.body = Hello,\ mail.checkout.title = You can checkout your subscription mail.checkout.body = Your subscription demand for [api.name]/[api.plan] has been accepted\ you can now checkout.\ -please click the following link to Checkout \ No newline at end of file +please click the following link to Checkout +mail.api.subscription.transfer.rejection.body = Your request to transfer to your subscription [subscription] has been rejected. \ No newline at end of file diff --git a/daikoku/conf/messages.fr b/daikoku/conf/messages.fr index c91b6f767..cb5ba41e4 100644 --- a/daikoku/conf/messages.fr +++ b/daikoku/conf/messages.fr @@ -94,4 +94,5 @@ mail.subscription.validation.body = Bonjour,\ mail.checkout.title = Un paiement en attente mail.checkout.body = Votre demande de souscription pour l'api [api.name] et le plan [api.plan] a été accepté\ Vous pouvez effectuer maintenant le reglement.\ -Vous pouvez finaliser votre demande : Reglement \ No newline at end of file +Vous pouvez finaliser votre demande : Reglement +mail.api.subscription.transfer.rejection.body = Votre demande de transfert de le souscription [subscription] a été rejetée. \ No newline at end of file diff --git a/daikoku/conf/routes b/daikoku/conf/routes index f2bf2d1ca..ea6870919 100644 --- a/daikoku/conf/routes +++ b/daikoku/conf/routes @@ -114,6 +114,7 @@ GET /api/apis/:apiId/plan/:planId/pages/:pageId fr.maif.otoroshi POST /api/teams/:teamId/subscriptions/:id/name fr.maif.otoroshi.daikoku.ctrls.ApiController.updateApiSubscriptionCustomName(teamId, id) PUT /api/teams/:teamId/subscriptions/:id/_archive fr.maif.otoroshi.daikoku.ctrls.ApiController.toggleApiSubscription(teamId, id, enabled: Option[Boolean] ?= Some(false)) PUT /api/teams/:teamId/subscriptions/:id/_archiveByOwner fr.maif.otoroshi.daikoku.ctrls.ApiController.toggleApiSubscriptionByApiOwner(teamId, id, enabled: Option[Boolean] ?= Some(false)) +PUT /api/teams/:teamId/subscriptions/:id/_transfer fr.maif.otoroshi.daikoku.ctrls.ApiController.transferSubscription(teamId, id) POST /api/teams/:teamId/subscriptions/:id/_makeUnique fr.maif.otoroshi.daikoku.ctrls.ApiController.makeUniqueSubscription(teamId, id) PUT /api/teams/:teamId/subscriptions/:id fr.maif.otoroshi.daikoku.ctrls.ApiController.updateApiSubscription(teamId, id) DELETE /api/teams/:teamId/subscriptions/:id fr.maif.otoroshi.daikoku.ctrls.ApiController.deleteApiSubscription(teamId, id, action: Option[String] ?= Some("promotion"), child: Option[String] ?= None) diff --git a/daikoku/test/daikoku/ApiControllerSpec.scala b/daikoku/test/daikoku/ApiControllerSpec.scala index fcb3250df..e6a40af72 100644 --- a/daikoku/test/daikoku/ApiControllerSpec.scala +++ b/daikoku/test/daikoku/ApiControllerSpec.scala @@ -1712,6 +1712,372 @@ class ApiControllerSpec() respOrg.status mustBe 200 } + + "transfer subscriptions to another team" in { + + //creer un apk otoroshi a transferer + Json.obj( + "_loc" -> Json.obj( + "tenant" -> "default", + "teams" -> Json.arr("default") + ), + "clientId" -> parentApiKey.clientId, + "clientSecret" -> parentApiKey.clientSecret, + "clientName" -> parentApiKey.clientName, + "description" -> "", + "authorizedGroup" -> JsNull, + "authorizedEntities" -> Json.arr( + s"route_$parentRouteId", + ), + "authorizations" -> Json.arr( + Json.obj( + "kind" -> "route", + "id" -> parentRouteId + ) + ), + "enabled" -> true, + "readOnly" -> false, + "allowClientIdOnly" -> false, + "throttlingQuota" -> 10000000, + "dailyQuota" -> 10000000, + "monthlyQuota" -> 10000000, + "constrainedServicesOnly" -> false, + "restrictions" -> Json.obj( + "enabled" -> false, + "allowLast" -> true, + "allowed" -> Json.arr(), + "forbidden" -> Json.arr(), + "notFound" -> Json.arr() + ), + "rotation" -> Json.obj( + "enabled" -> false, + "rotationEvery" -> 744, + "gracePeriod" -> 168, + "nextSecret" -> JsNull + ), + "validUntil" -> JsNull, + "tags" -> Json.arr(), + "metadata" -> Json.obj( + "daikoku__metadata" -> "| foo", + "foo" -> "bar" + ) + ) + + //update otoroshi + Await.result(cleanOtoroshiServer(container.mappedPort(8080)), 5.seconds) + + //setup dk + val usagePlan = FreeWithoutQuotas( + id = UsagePlanId("test.plan"), + tenant = tenant.id, + billingDuration = BillingDuration(1, BillingTimeUnit.Month), + currency = Currency("EUR"), + customName = None, + customDescription = None, + otoroshiTarget = Some( + OtoroshiTarget( + containerizedOtoroshi, + Some( + AuthorizedEntities( + routes = Set(OtoroshiRouteId(parentRouteId)) + ) + ), + ApikeyCustomization( + metadata = Json.obj("foo" -> "bar") + ) + ), + ), + allowMultipleKeys = Some(false), + subscriptionProcess = Seq.empty, + integrationProcess = IntegrationProcess.ApiKey, + autoRotation = Some(false), + aggregationApiKeysSecurity = Some(true), + ) + + + val api = defaultApi.api.copy( + id = ApiId("test-api-id"), + name = "test API", + team = teamOwnerId, + possibleUsagePlans = Seq(usagePlan.id), + defaultUsagePlan = usagePlan.id.some + ) + + val subscription = ApiSubscription( + id = ApiSubscriptionId("test_sub"), + tenant = tenant.id, + apiKey = parentApiKey, + plan = usagePlan.id, + createdAt = DateTime.now(), + team = teamConsumerId, + api = api.id, + by = userTeamAdminId, + customName = None, + rotation = None, + integrationToken = "token", + metadata = Json.obj("foo" -> "bar").some + ) + //2 equipes + //une api / un plan + //une souscription + + setupEnvBlocking( + tenants = Seq(tenantEnvMode.copy( + otoroshiSettings = Set( + OtoroshiSettings( + id = containerizedOtoroshi, + url = + s"http://otoroshi.oto.tools:${container.mappedPort(8080)}", + host = "otoroshi-api.oto.tools", + clientSecret = otoroshiAdminApiKey.clientSecret, + clientId = otoroshiAdminApiKey.clientId + ) + ), + environmentAggregationApiKeysSecurity = Some(true), + aggregationApiKeysSecurity = Some(true) + )), + users = Seq(userAdmin), + teams = Seq( + defaultAdminTeam, + teamOwner, + teamConsumer), + apis = Seq(api), + usagePlans = Seq(usagePlan), + subscriptions = Seq(subscription) + ) + + //trasferer la souscription + val session = loginWithBlocking(userAdmin, tenant) + val resp = httpJsonCallBlocking( + path = s"/api/teams/${teamConsumer.id.value}/subscriptions/${subscription.id.value}/_transfer", + method = "PUT", + body = Json.obj("team" -> teamOwner.id.asJson).some + )(tenant, session) + resp.status mustBe 200 + + //accepter la notif + val getNotif = httpJsonCallBlocking(s"/api/teams/${teamOwner.id.value}/notifications")(tenant, session) + getNotif.status mustBe 200 + val notifications = (getNotif.json \ "notifications").as(json.SeqNotificationFormat) + notifications.length mustBe 1 + val transferNotif = notifications.head + + + val acceptNotif = httpJsonCallBlocking( + path = s"/api/notifications/${transferNotif.id.value}/accept", + method = "PUT", + body = Some(Json.obj()) + )(tenant, session) + acceptNotif.status mustBe 200 + + val consumerSubsReq = httpJsonCallBlocking(s"/api/subscriptions/teams/${teamConsumer.id.value}")(tenant, session) + consumerSubsReq.status mustBe 200 + val maybeConsumerSubs = json.SeqApiSubscriptionFormat.reads(consumerSubsReq.json) + maybeConsumerSubs.isSuccess mustBe true + val consumerSubs = maybeConsumerSubs.get + consumerSubs.length mustBe 0 + + + val ownerSubsReq = httpJsonCallBlocking(s"/api/subscriptions/teams/${teamOwner.id.value}")(tenant, session) + ownerSubsReq.status mustBe 200 + val maybeOwnerSubs = json.SeqApiSubscriptionFormat.reads(ownerSubsReq.json) + maybeOwnerSubs.isSuccess mustBe true + val ownerSubs = maybeOwnerSubs.get + ownerSubs.length mustBe 1 + ownerSubs.head.id mustBe subscription.id + + //eventuellement verifier les metadata de l'apk pour voir la team + } + + "not transfer child subscriptions to another team but parent subscription" in { + val parentPlanProd = FreeWithoutQuotas( + id = UsagePlanId("parent.dev"), + tenant = tenant.id, + billingDuration = BillingDuration(1, BillingTimeUnit.Month), + currency = Currency("EUR"), + customName = envModeProd.some, + customDescription = None, + otoroshiTarget = Some( + OtoroshiTarget( + containerizedOtoroshi, + Some( + AuthorizedEntities( + routes = Set(OtoroshiRouteId(parentRouteId)) + ) + ) + ) + ), + allowMultipleKeys = Some(false), + subscriptionProcess = Seq.empty, + integrationProcess = IntegrationProcess.ApiKey, + autoRotation = Some(false), + aggregationApiKeysSecurity = Some(true) + ) + val childPlanProd = FreeWithoutQuotas( + id = UsagePlanId("child.dev"), + tenant = tenant.id, + billingDuration = BillingDuration(1, BillingTimeUnit.Month), + currency = Currency("EUR"), + customName = envModeProd.some, + customDescription = None, + otoroshiTarget = Some( + OtoroshiTarget( + containerizedOtoroshi, + Some( + AuthorizedEntities( + routes = Set(OtoroshiRouteId(childRouteId)) + ) + ) + ) + ), + allowMultipleKeys = Some(false), + subscriptionProcess = Seq.empty, + integrationProcess = IntegrationProcess.ApiKey, + autoRotation = Some(false), + aggregationApiKeysSecurity = Some(true) + ) + + val parentApi = defaultApi.api.copy( + id = ApiId("parent-id"), + name = "parent API", + team = teamOwnerId, + possibleUsagePlans = Seq(parentPlanProd.id), + defaultUsagePlan = parentPlanProd.id.some + ) + val childApi = defaultApi.api.copy( + id = ApiId("child-id"), + name = "child API", + team = teamOwnerId, + possibleUsagePlans = Seq(childPlanProd.id), + defaultUsagePlan = childPlanProd.id.some + ) + + val parentSub = ApiSubscription( + id = ApiSubscriptionId("parent_sub"), + tenant = tenant.id, + apiKey = parentApiKeyWith2childs, + plan = parentPlanProd.id, + createdAt = DateTime.now(), + team = teamConsumerId, + api = parentApi.id, + by = userTeamAdminId, + customName = None, + rotation = None, + integrationToken = "parent_token" + ) + + val childSub = ApiSubscription( + id = ApiSubscriptionId("child_sub"), + tenant = tenant.id, + apiKey = parentApiKeyWith2childs, + plan = parentPlanProd.id, + createdAt = DateTime.now(), + team = teamConsumerId, + api = parentApi.id, + by = userTeamAdminId, + customName = None, + rotation = None, + integrationToken = "parent_token", + parent = parentSub.id.some + ) + + setupEnvBlocking( + tenants = Seq(tenantEnvMode.copy( + otoroshiSettings = Set( + OtoroshiSettings( + id = containerizedOtoroshi, + url = + s"http://otoroshi.oto.tools:${container.mappedPort(8080)}", + host = "otoroshi-api.oto.tools", + clientSecret = otoroshiAdminApiKey.clientSecret, + clientId = otoroshiAdminApiKey.clientId + ) + ), + environmentAggregationApiKeysSecurity = Some(true), + aggregationApiKeysSecurity = Some(true) + )), + users = Seq(user, userAdmin), + teams = Seq( + defaultAdminTeam, + teamOwner, + teamConsumer), + apis = Seq(parentApi, childApi), + usagePlans = Seq(parentPlanProd, childPlanProd), + subscriptions = Seq(parentSub, childSub) + ) + + setupEnvBlocking( + tenants = Seq(tenantEnvMode.copy( + otoroshiSettings = Set( + OtoroshiSettings( + id = containerizedOtoroshi, + url = + s"http://otoroshi.oto.tools:${container.mappedPort(8080)}", + host = "otoroshi-api.oto.tools", + clientSecret = otoroshiAdminApiKey.clientSecret, + clientId = otoroshiAdminApiKey.clientId + ) + ), + aggregationApiKeysSecurity = Some(true) + )), + users = Seq(userAdmin), + teams = Seq( + defaultAdminTeam, + teamOwner, + teamConsumer), + apis = Seq(parentApi, childApi), + usagePlans = Seq(parentPlanProd, childPlanProd), + subscriptions = Seq(parentSub, childSub) + ) + + val session = loginWithBlocking(userAdmin, tenant) + val respChild = httpJsonCallBlocking( + path = s"/api/teams/${teamConsumer.id.value}/subscriptions/${childSub.id.value}/_transfer", + method = "PUT", + body = Json.obj("team" -> teamOwner.id.asJson).some + )(tenant, session) + respChild.status mustBe 409 + + val respParent = httpJsonCallBlocking( + path = s"/api/teams/${teamConsumer.id.value}/subscriptions/${parentSub.id.value}/_transfer", + method = "PUT", + body = Json.obj("team" -> teamOwner.id.asJson).some + )(tenant, session) + respParent.status mustBe 200 + + val getNotif = httpJsonCallBlocking(s"/api/teams/${teamOwner.id.value}/notifications")(tenant, session) + getNotif.status mustBe 200 + val notifications = (getNotif.json \ "notifications").as(json.SeqNotificationFormat) + notifications.length mustBe 1 + val transferNotif = notifications.head + + + val acceptNotif = httpJsonCallBlocking( + path = s"/api/notifications/${transferNotif.id.value}/accept", + method = "PUT", + body = Some(Json.obj()) + )(tenant, session) + acceptNotif.status mustBe 200 + + val consumerSubsReq = httpJsonCallBlocking(s"/api/subscriptions/teams/${teamConsumer.id.value}")(tenant, session) + consumerSubsReq.status mustBe 200 + val maybeConsumerSubs = json.SeqApiSubscriptionFormat.reads(consumerSubsReq.json) + maybeConsumerSubs.isSuccess mustBe true + val consumerSubs = maybeConsumerSubs.get + consumerSubs.length mustBe 0 + + + val ownerSubsReq = httpJsonCallBlocking(s"/api/subscriptions/teams/${teamOwner.id.value}")(tenant, session) + ownerSubsReq.status mustBe 200 + val maybeOwnerSubs = json.SeqApiSubscriptionFormat.reads(ownerSubsReq.json) + maybeOwnerSubs.isSuccess mustBe true + val ownerSubs = maybeOwnerSubs.get + ownerSubs.length mustBe 2 + ownerSubs.exists(s => s.id == parentSub.id) mustBe true + ownerSubs.exists(s => s.id == childSub.id) mustBe true + + //eventuellement verifier les metadata de l'apk pour voir la team + } } "a api editor" can { @@ -7033,7 +7399,7 @@ class ApiControllerSpec() keys2.contains("foo2") mustBe true } - "not be controlled by a security tenant in environment mode" in { + "be controlled by a security tenant in environment mode" in { val parentPlanProd = FreeWithoutQuotas( id = UsagePlanId("parent.dev"), tenant = tenant.id, diff --git a/daikoku/test/daikoku/suites.scala b/daikoku/test/daikoku/suites.scala index 4fefae264..8a9eac490 100644 --- a/daikoku/test/daikoku/suites.scala +++ b/daikoku/test/daikoku/suites.scala @@ -724,112 +724,116 @@ object utils { promise.future } - def cleanOtoroshiServer(otoroshiPort: Int) = { - val parent2ApkAsJson = Json.obj( - "_loc" -> Json.obj( - "tenant" -> "default", - "teams" -> Json.arr("default") - ), - "clientId" -> "fu283imnfv8jdt4e", - "clientSecret" -> "yaodpdfu283imnfv8jdt4eivaow6ipvh6ta9dwvd3tor9vf9wovxs6i5a2v7ep6m", - "clientName" -> "daikoku_test_parent_key_2_childs", - "description" -> "", - "authorizedGroup" -> JsNull, - "authorizedEntities" -> Json.arr( - "route_route_d74ea8b27-b8be-4177-82d9-c50722416c50", - "route_route_8ce030cbd-6c07-43d4-9c61-4a330ae0975d", - "route_route_d74ea8b27-b8be-4177-82d9-c50722416c51" - ), - "authorizations" -> Json.arr( - Json.obj( - "kind" -> "route", - "id" -> "route_d74ea8b27-b8be-4177-82d9-c50722416c50" - ), - Json.obj( - "kind" -> "route", - "id" -> "route_8ce030cbd-6c07-43d4-9c61-4a330ae0975d" - ), - Json.obj( - "kind" -> "route", - "id" -> "route_d74ea8b27-b8be-4177-82d9-c50722416c51" - ) - ), - "enabled" -> true, - "readOnly" -> false, - "allowClientIdOnly" -> false, - "throttlingQuota" -> 10000000, - "dailyQuota" -> 10000000, - "monthlyQuota" -> 10000000, - "constrainedServicesOnly" -> false, - "restrictions" -> Json.obj( - "enabled" -> false, - "allowLast" -> true, - "allowed" -> Json.arr(), - "forbidden" -> Json.arr(), - "notFound" -> Json.arr() + val parentRouteId = "route_d74ea8b27-b8be-4177-82d9-c50722416c50" + val childRouteId = "route_8ce030cbd-6c07-43d4-9c61-4a330ae0975d" + val otherRouteId = "route_d74ea8b27-b8be-4177-82d9-c50722416c51" + val parent2ApkAsJson = Json.obj( + "_loc" -> Json.obj( + "tenant" -> "default", + "teams" -> Json.arr("default") + ), + "clientId" -> "fu283imnfv8jdt4e", + "clientSecret" -> "yaodpdfu283imnfv8jdt4eivaow6ipvh6ta9dwvd3tor9vf9wovxs6i5a2v7ep6m", + "clientName" -> "daikoku_test_parent_key_2_childs", + "description" -> "", + "authorizedGroup" -> JsNull, + "authorizedEntities" -> Json.arr( + s"route_$parentRouteId", + s"route_$childRouteId", + s"route_$otherRouteId" + ), + "authorizations" -> Json.arr( + Json.obj( + "kind" -> "route", + "id" -> parentRouteId ), - "rotation" -> Json.obj( - "enabled" -> false, - "rotationEvery" -> 744, - "gracePeriod" -> 168, - "nextSecret" -> JsNull + Json.obj( + "kind" -> "route", + "id" -> childRouteId ), - "validUntil" -> JsNull, - "tags" -> Json.arr(), - "metadata" -> Json.obj( - "daikoku__metadata" -> "| foo", - "foo" -> "bar" + Json.obj( + "kind" -> "route", + "id" -> otherRouteId ) + ), + "enabled" -> true, + "readOnly" -> false, + "allowClientIdOnly" -> false, + "throttlingQuota" -> 10000000, + "dailyQuota" -> 10000000, + "monthlyQuota" -> 10000000, + "constrainedServicesOnly" -> false, + "restrictions" -> Json.obj( + "enabled" -> false, + "allowLast" -> true, + "allowed" -> Json.arr(), + "forbidden" -> Json.arr(), + "notFound" -> Json.arr() + ), + "rotation" -> Json.obj( + "enabled" -> false, + "rotationEvery" -> 744, + "gracePeriod" -> 168, + "nextSecret" -> JsNull + ), + "validUntil" -> JsNull, + "tags" -> Json.arr(), + "metadata" -> Json.obj( + "daikoku__metadata" -> "| foo", + "foo" -> "bar" ) - val parentApkAsJson = Json.obj( - "_loc" -> Json.obj( - "tenant" -> "default", - "teams" -> Json.arr("default") - ), - "clientId" -> "5w24yl2ly3dlnn92", - "clientSecret" -> "8iwm9fhbns0rmybnyul5evq9l1o4dxza0rh7rt4flay69jolw3okbz1owfl6w2db", - "clientName" -> "daikoku_test_parent_key", - "description" -> "", - "authorizedGroup" -> JsNull, - "authorizedEntities" -> Json.arr( - "route_route_d74ea8b27-b8be-4177-82d9-c50722416c50", - "route_route_8ce030cbd-6c07-43d4-9c61-4a330ae0975d" - ), - "authorizations" -> Json.arr( - Json.obj( - "kind" -> "route", - "id" -> "route_d74ea8b27-b8be-4177-82d9-c50722416c50" - ), - Json.obj( - "kind" -> "route", - "id" -> "route_8ce030cbd-6c07-43d4-9c61-4a330ae0975d" - ) - ), - "enabled" -> true, - "readOnly" -> false, - "allowClientIdOnly" -> false, - "throttlingQuota" -> 10000000, - "dailyQuota" -> 10000000, - "monthlyQuota" -> 10000000, - "constrainedServicesOnly" -> false, - "restrictions" -> Json.obj( - "enabled" -> false, - "allowLast" -> true, - "allowed" -> Json.arr(), - "forbidden" -> Json.arr(), - "notFound" -> Json.arr() - ), - "rotation" -> Json.obj( - "enabled" -> false, - "rotationEvery" -> 744, - "gracePeriod" -> 168, - "nextSecret" -> JsNull + ) + val parentApkAsJson = Json.obj( + "_loc" -> Json.obj( + "tenant" -> "default", + "teams" -> Json.arr("default") + ), + "clientId" -> "5w24yl2ly3dlnn92", + "clientSecret" -> "8iwm9fhbns0rmybnyul5evq9l1o4dxza0rh7rt4flay69jolw3okbz1owfl6w2db", + "clientName" -> "daikoku_test_parent_key", + "description" -> "", + "authorizedGroup" -> JsNull, + "authorizedEntities" -> Json.arr( + s"route_$parentRouteId", + s"route_$childRouteId" + ), + "authorizations" -> Json.arr( + Json.obj( + "kind" -> "route", + "id" -> parentRouteId ), - "validUntil" -> JsNull, - "tags" -> Json.arr(), - "metadata" -> Json.obj() - ) + Json.obj( + "kind" -> "route", + "id" -> childRouteId + ) + ), + "enabled" -> true, + "readOnly" -> false, + "allowClientIdOnly" -> false, + "throttlingQuota" -> 10000000, + "dailyQuota" -> 10000000, + "monthlyQuota" -> 10000000, + "constrainedServicesOnly" -> false, + "restrictions" -> Json.obj( + "enabled" -> false, + "allowLast" -> true, + "allowed" -> Json.arr(), + "forbidden" -> Json.arr(), + "notFound" -> Json.arr() + ), + "rotation" -> Json.obj( + "enabled" -> false, + "rotationEvery" -> 744, + "gracePeriod" -> 168, + "nextSecret" -> JsNull + ), + "validUntil" -> JsNull, + "tags" -> Json.arr(), + "metadata" -> Json.obj() + ) + + def cleanOtoroshiServer(otoroshiPort: Int, apks: Seq[JsValue] = Seq(parentApkAsJson, parent2ApkAsJson)) = { val apikeys = daikokuComponents.env.wsClient .url(s"http://otoroshi-api.oto.tools:$otoroshiPort/api/apikeys") .withHttpHeaders( @@ -874,38 +878,21 @@ object utils { } }) .runWith(Sink.ignore) - _ <- - daikokuComponents.env.wsClient - .url(s"http://otoroshi-api.oto.tools:$otoroshiPort/api/apikeys") - .withHttpHeaders( - Map( - "Otoroshi-Client-Id" -> otoroshiAdminApiKey.clientId, - "Otoroshi-Client-Secret" -> otoroshiAdminApiKey.clientSecret, - "Host" -> "otoroshi-api.oto.tools" - ).toSeq: _* - ) - .withFollowRedirects(false) - .withRequestTimeout(10.seconds) - .withMethod("POST") - .withBody(parentApkAsJson) - .execute() - .map(_ => true) - _ <- - daikokuComponents.env.wsClient - .url(s"http://otoroshi-api.oto.tools:$otoroshiPort/api/apikeys") - .withHttpHeaders( - Map( - "Otoroshi-Client-Id" -> otoroshiAdminApiKey.clientId, - "Otoroshi-Client-Secret" -> otoroshiAdminApiKey.clientSecret, - "Host" -> "otoroshi-api.oto.tools" - ).toSeq: _* - ) - .withFollowRedirects(false) - .withRequestTimeout(10.seconds) - .withMethod("POST") - .withBody(parent2ApkAsJson) - .execute() - .map(_ => true) + _ <- Future.sequence(apks.map(apk => daikokuComponents.env.wsClient + .url(s"http://otoroshi-api.oto.tools:$otoroshiPort/api/apikeys") + .withHttpHeaders( + Map( + "Otoroshi-Client-Id" -> otoroshiAdminApiKey.clientId, + "Otoroshi-Client-Secret" -> otoroshiAdminApiKey.clientSecret, + "Host" -> "otoroshi-api.oto.tools" + ).toSeq: _* + ) + .withFollowRedirects(false) + .withRequestTimeout(10.seconds) + .withMethod("POST") + .withBody(apk) + .execute() + .map(_ => true))) } yield true } @@ -944,10 +931,6 @@ object utils { "yaodpdfu283imnfv8jdt4eivaow6ipvh6ta9dwvd3tor9vf9wovxs6i5a2v7ep6m" ) - val parentRouteId = "route_d74ea8b27-b8be-4177-82d9-c50722416c50" - val childRouteId = "route_8ce030cbd-6c07-43d4-9c61-4a330ae0975d" - val otherRouteId = "route_d74ea8b27-b8be-4177-82d9-c50722416c51" - val teamOwnerId = TeamId("team-owner") val teamConsumerId = TeamId("team-consumer") val teamAdminId = TeamId("team-admin") From 8babd056b47c96fd0f76a473dd2aba5f961ced9a Mon Sep 17 00:00:00 2001 From: baudelotphilippe Date: Tue, 3 Sep 2024 13:57:34 +0200 Subject: [PATCH 02/44] add default template files --- .../src/style/template/brutalism.css | 58 +++++++++ .../javascript/src/style/template/default.css | 117 ++++++++++++++++++ 2 files changed, 175 insertions(+) create mode 100644 daikoku/javascript/src/style/template/brutalism.css create mode 100644 daikoku/javascript/src/style/template/default.css diff --git a/daikoku/javascript/src/style/template/brutalism.css b/daikoku/javascript/src/style/template/brutalism.css new file mode 100644 index 000000000..352f710a3 --- /dev/null +++ b/daikoku/javascript/src/style/template/brutalism.css @@ -0,0 +1,58 @@ +:root { + --body_bg-color: pink; + --body_text-color: green; + --body_link-color:red; + --body_link-hover-color:yellow; + + --level2_bg-color: orange; + --level2_text-color: blue; + --level2_link-color: red; + --level2_link-hover-color: brown; + + --level3_bg-color : #bbd700; + --level3_text-color : #222; + --level3_link-color: #4c4c4d; + --level3_link-hover-color : #000; + --level3_link-hover-bg-color :grey; + + --sidebar-bg-color: beige; + --sidebar-text-color: brown; + --sidebar-text-hover-color:orange; + + --menu-bg-color: #fba979; + --menu-text-color: #ac6407; + --menu-text-hover-bg-color: cadetblue; + --menu-text-hover-color: yellow; + --menu-divider-color: grey; + + --card_header-bg-color : indigo; + --card_header-text-color: gold; + --card_bg-color: lavender; + --card_text-color: black; + --card_link-color: lightseagreen; + --card_link-hover-color : lime; + + --btn-bg-color: #fff; + --btn-text-color: #495057; + --btn-border-color: #97b0c7; + + --badge-tags-bg-color: #ffc107; + --badge-tags-bg-hover-color: #ffe1a7; + --badge-tags-text-color: #212529; + + --form-text-color: #000; + --form-border-color: #586069; + --form-bg-color: #ff5e8c; + + --form-select-focused-color: lightgrey; + --form-select-focused-text-color: white; + --form-select-heading-color: yellow; + --form-select-hover-color: lightgrey; + --form-select-hover-text-color: white; + + --error-color:#dc3545; + --info-color: #17a2b8; + --success-color: #65B741; + --warning-color: #ffc107; + --danger-color: #dc3545; + } \ No newline at end of file diff --git a/daikoku/javascript/src/style/template/default.css b/daikoku/javascript/src/style/template/default.css new file mode 100644 index 000000000..d60d8fe7f --- /dev/null +++ b/daikoku/javascript/src/style/template/default.css @@ -0,0 +1,117 @@ +:root { + --body_bg-color: #f1f3f6; + --body_text-color: #8a8a8a; + --body_link-color:#4c4c4d; + --body_link-hover-color:orange; + + --level2_bg-color: #e5e7ea; + --level2_text-color: #4c4c4d; + --level2_link-color: #605c5c; + --level2_link-hover-color: #000; + + --level3_bg-color : #fff; + --level3_text-color : #222; + --level3_link-color: #4c4c4d; + --level3_link-hover-color : #000; + --level3_link-hover-bg-color :grey; + + --sidebar-bg-color: #e5e7ea; + --sidebar-text-color: #4c4c4d; + --sidebar-text-hover-color:orange; + + --menu-bg-color: #fff; + --menu-text-color: #aaa; + --menu-text-hover-bg-color: #444; + --menu-text-hover-color: #fff; + --menu-link-color: #666;  + + --card_header-bg-color: #404040; + --card_header-text-color: #fff; + --card_bg-color: #282828; + --card_text-color: #fff; + --card_link-color: #ffe1a7; + --card_link-hover-color : #ffc107; + + --btn-bg-color: #fff; + --btn-text-color: #495057; + --btn-border-color: #97b0c7; + + --badge-tags-bg-color: #ffc107; + --badge-tags-bg-hover-color: #ffe1a7; + --badge-tags-text-color: #212529; + + --form-text-color: #000; + --form-border-color: #586069; + --form-bg-color: #fff; + + --form-select-focused-color: lightgrey; + --form-select-focused-text-color: white; + --form-select-heading-color: yellow; + --form-select-hover-color: lightgrey; + --form-select-hover-text-color: white; + + --error-color:#dc3545; + --info-color: #17a2b8; + --success-color: #65B741; + --warning-color: #ffc107; + --danger-color: #dc3545; + } + + :root[data-theme="DARK"] { + --body_bg-color: #000; + --body_text-color: #b3b3b3; + --body_link-color:#b3b3b3; + --body_link-hover-color:orange; + + --level2_bg-color: #121212; + --level2_text-color: #b3b3b3; + --level2_link-color: #9f9e9e; + --level2_link-hover-color: #fff; + + --level3_bg-color : #242424; + --level3_text-color : #e8e8e8; + --level3_link-color: #9f9e9e; + --level3_link-hover-color : #fff; + --level3_link-hover-bg-color : grey; + + --sidebar-bg-color: #121212; + --sidebar-text-color: #b3b3b3; + --sidebar-text-hover-color:orange; + + --menu-bg-color: #242424; + --menu-text-color: #fff; + --menu-text-hover-bg-color: #121212; + --menu-text-hover-color: #fff; + --menu-link-color: #b3b3b3; + + --card_header-bg-color : #404040; + --card_header-text-color: #fff; + --card_bg-color: #282828; + --card_text-color: #fff; + --card_link-color: #ffe1a7; + --card_link-hover-color : #ffc107; + + --btn-bg-color: #fff; + --btn-text-color: #495057; + --btn-border-color: #97b0c7; + + --badge-tags-bg-color: #ffc107; + --badge-tags-bg-hover-color: #ffe1a7; + --badge-tags-text-color: #212529; + + --form-text-color: #000; + --form-border-color: #586069; + --form-bg-color: #fff; + + --form-select-focused-color: grey; + --form-select-focused-text-color: white; + --form-select-heading-color: yellow; + --form-select-hover-color: grey; + --form-select-hover-text-color: white; + + --error-color:#dc3545; +   --info-color: #17a2b8; + --success-color: #65B741; + --warning-color: #ffc107; + --danger-color: #dc3545; + } \ No newline at end of file From dff50f6e315ce52b2afac04ce67306e1b4e38b00 Mon Sep 17 00:00:00 2001 From: Quentin AUBERT Date: Wed, 4 Sep 2024 08:53:56 +0200 Subject: [PATCH 03/44] WIP front --- daikoku/app/utils/ApiService.scala | 4 +- .../backoffice/apikeys/TeamApiKeysForApi.tsx | 352 +++++++++++++++--- .../src/locales/en/translation.json | 3 +- .../src/locales/fr/translation.json | 3 +- .../src/style/components/apiSubscription.scss | 71 ++++ daikoku/javascript/src/style/main.scss | 1 + 6 files changed, 377 insertions(+), 57 deletions(-) create mode 100644 daikoku/javascript/src/style/components/apiSubscription.scss diff --git a/daikoku/app/utils/ApiService.scala b/daikoku/app/utils/ApiService.scala index d34a40ca1..5d687ad33 100644 --- a/daikoku/app/utils/ApiService.scala +++ b/daikoku/app/utils/ApiService.scala @@ -2649,9 +2649,9 @@ class ApiService( for { subscription <- EitherT.fromOptionF[Future, AppError, ApiSubscription](env.dataStore.apiSubscriptionRepo.forTenant(tenant).findById(subscriptionId), AppError.SubscriptionNotFound) - api <- EitherT.fromOptionF[Future, AppError, Api](env.dataStore.apiSubscriptionRepo.forTenant(tenant).findById(subscriptionId), + api <- EitherT.fromOptionF[Future, AppError, Api](env.dataStore.apiRepo.forTenant(tenant).findById(subscription.api), AppError.ApiNotFound) - plan <- EitherT.fromOptionF[Future, AppError, UsagePlan](env.dataStore.apiSubscriptionRepo.forTenant(tenant).findById(subscriptionId), + plan <- EitherT.fromOptionF[Future, AppError, UsagePlan](env.dataStore.usagePlanRepo.forTenant(tenant).findById(subscription.plan), AppError.SubscriptionNotFound) ownerTeam <- EitherT.fromOptionF[Future, AppError, Team](env.dataStore.teamRepo.forTenant(tenant).findById(subscription.team), AppError.TeamNotFound) _ <- EitherT.liftF[Future, AppError, Boolean](env.dataStore.notificationRepo.forTenant(tenant).save( diff --git a/daikoku/javascript/src/components/backoffice/apikeys/TeamApiKeysForApi.tsx b/daikoku/javascript/src/components/backoffice/apikeys/TeamApiKeysForApi.tsx index d782d1156..b63c36fbf 100644 --- a/daikoku/javascript/src/components/backoffice/apikeys/TeamApiKeysForApi.tsx +++ b/daikoku/javascript/src/components/backoffice/apikeys/TeamApiKeysForApi.tsx @@ -1,12 +1,14 @@ import { getApolloContext } from '@apollo/client'; import { Form, constraints, type, format } from '@maif/react-forms'; -import classNames from 'classnames'; import sortBy from 'lodash/sortBy'; +import classNames from 'classnames'; import React, { useContext, useEffect, useState } from 'react'; import { Link, useLocation, useParams } from 'react-router-dom'; import { toast } from 'sonner'; - +import Key from 'react-feather/dist/icons/key'; import { useQuery, useQueryClient } from '@tanstack/react-query'; +import moment from 'moment'; + import { I18nContext, ModalContext, @@ -185,17 +187,17 @@ export const TeamApiKeysForApi = () => { format: format.select, label: translate("apikeys.delete.child.label"), options: subscription.children, - transformer: (s: ISubscriptionExtended) => ({value: s._id, label: `${s.apiName}/${s.planName}`}), + transformer: (s: ISubscriptionExtended) => ({ value: s._id, label: `${s.apiName}/${s.planName}` }), visible: (d) => d.rawValues.choice === 'promotion', } }, - onSubmit: ({choice, childId}) => openFormModal( + onSubmit: ({ choice, childId }) => openFormModal( { title: translate("apikeys.delete.confirm.modal.title"), schema: { validation: { type: type.string, - label: translate({ key: "apikeys.delete.confirm.label", replacements: [`${subscription.apiName}/${subscription.customName ?? subscription.planName}`]}), + label: translate({ key: "apikeys.delete.confirm.label", replacements: [`${subscription.apiName}/${subscription.customName ?? subscription.planName}`] }), constraints: [ constraints.required(translate('constraints.required.value')), constraints.matches(new RegExp(`${subscription.apiName}/${subscription.customName ?? subscription.planName}`), translate('constraints.match.subscription')) @@ -302,24 +304,24 @@ export const TeamApiKeysForApi = () => { search === '' ? subscriptions : subscriptions.filter((subs) => { - if ( - subs.apiKey.clientName - .replace('-', ' ') - .toLowerCase() - .includes(search) - ) { - return true; - } else if ( - subs.customName && - subs.customName.toLowerCase().includes(search) - ) { - return true; - } else { - return formatPlanType(subs.planType, translate) - .toLowerCase() - .includes(search); - } - }); + if ( + subs.apiKey.clientName + .replace('-', ' ') + .toLowerCase() + .includes(search) + ) { + return true; + } else if ( + subs.customName && + subs.customName.toLowerCase().includes(search) + ) { + return true; + } else { + return formatPlanType(subs.planType, translate) + .toLowerCase() + .includes(search); + } + }); const sorted = sortBy(filteredApiKeys, ['plan', 'customName', 'parent']); const sortedApiKeys = sorted @@ -346,13 +348,11 @@ export const TeamApiKeysForApi = () => {

Api keys for   - {api.name} -

- + className="cursor-pointer" + >{api.name} +
Promise; statsLink: string; archiveApiKey: () => void; @@ -455,7 +455,6 @@ const ApiKeyCard = ({ const [hide, setHide] = useState(true); const [settingMode, setSettingMode] = useState(false); const [customName, setCustomName] = useState(); - const [editMode, setEditMode] = useState(false); const [activeTab, setActiveTab] = useState<'apikey' | 'token' | 'basicAuth'>( @@ -472,6 +471,7 @@ const ApiKeyCard = ({ const { _id, integrationToken } = subscription; const { translate, Translation } = useContext(I18nContext); + const { openFormModal } = useContext(ModalContext); const planQuery = useQuery({ queryKey: ['plan', subscription.plan], @@ -545,10 +545,11 @@ const ApiKeyCard = ({ const handleCustomNameChange = () => { const _customName = inputRef.current?.value.trim(); if (_customName) { - updateCustomName(_customName).then(() => { - setCustomName(_customName); - setEditMode(false); - }); + updateCustomName(_customName) + .then(() => { + setCustomName(_customName); + setEditMode(false); + }); } }; @@ -567,17 +568,262 @@ const ApiKeyCard = ({ rotation.enabled, rotation.rotationEvery, rotation.gracePeriod - ).then(() => setSettingMode(false)); + ) + .then(() => setSettingMode(false)); } }; const disableRotation = api.visibility === 'AdminOnly' || !!plan.autoRotation; + + const _customName = subscription.customName || + planQuery.data.customName || + planQuery.data.type + + return ( +
+
+
+ +
activated
+
+ +
+
{_customName}
+
{`${subscription.apiKey.clientId}:${subscription.apiKey.clientSecret}`}
+
+ + + +
+
{ + translate({ + key: 'subscription.create.at', replacements: [moment(subscription.createdAt).format(translate('moment.date.format.without.hours'))] + }) + }
+
+
+
+ Check out the informations to using the API or explore the statistics of your API key. +
+
+