diff --git a/otoroshi/app/next/models/Api.scala b/otoroshi/app/next/models/Api.scala index c44073148..9f446bf66 100644 --- a/otoroshi/app/next/models/Api.scala +++ b/otoroshi/app/next/models/Api.scala @@ -312,33 +312,33 @@ object ApiConsumer { id = json.select("id").asString, name = json.select("name").asString, description = json.select("description").asOptString, - autoValidation = json.select("autoValidation").asOpt[Boolean].getOrElse(false), - consumerKind = json.select("consumer_kind").asOptString.map(_.toLowerCase match { + autoValidation = json.select("auto_validation").asOpt[Boolean].getOrElse(false), + consumerKind = json.select("consumer_kind").asString.toLowerCase match { case "apikey" => ApiConsumerKind.Apikey case "mtls" => ApiConsumerKind.Mtls case "keyless" => ApiConsumerKind.Keyless case "oauth2" => ApiConsumerKind.OAuth2 case "jwt" => ApiConsumerKind.JWT - }).getOrElse(ApiConsumerKind.Apikey), - settings = (json \ "settings" \ "name").asString match { + }, + settings = json.select("consumer_kind").asString.toLowerCase match { case "apikey" => { ApiConsumerSettings.Apikey( - throttlingQuota = (json \ "settings" \ "config" \ "throttlingQuota").as[Long], - monthlyQuota = (json \ "settings" \ "config" \ "monthlyQuota").as[Long], - dailyQuota = (json \ "settings" \ "config" \ "dailyQuota").as[Long], + throttlingQuota = (json \ "settings" \ "throttlingQuota").as[Long], + monthlyQuota = (json \ "settings" \ "monthlyQuota").as[Long], + dailyQuota = (json \ "settings" \ "dailyQuota").as[Long], name = "apikey") } case "mtls" => ApiConsumerSettings.Mtls( - caRefs = (json \ "settings" \ "config" \ "caRefs").as[Seq[String]], - certRefs = (json \ "settings" \ "config" \ "certRefs").as[Seq[String]], + caRefs = (json \ "settings" \ "caRefs").asOpt[Seq[String]].getOrElse(Seq.empty), + certRefs = (json \ "settings" \ "certRefs").asOpt[Seq[String]].getOrElse(Seq.empty), name = "mtls" ) case "keyless" => ApiConsumerSettings.Keyless(name = "Keyless") case "oauth2" => ApiConsumerSettings.OAuth2( - config = (json \ "settings" \ "config").as[JsValue], + config = (json \ "settings").as[JsValue], name = "oauth2") case "jwt" => ApiConsumerSettings.JWT( - jwtVerifierRefs = (json \ "settings" \ "config" \ "jwtVerifierRefs").as[Seq[String]], + jwtVerifierRefs = (json \ "settings" \ "jwtVerifierRefs").asOpt[Seq[String]].getOrElse(Seq.empty), name = "jwt") }, status = json.select("status").asString.toLowerCase match { @@ -363,7 +363,7 @@ object ApiConsumer { "id" -> o.id, "name" -> o.name, "description" -> o.description, - "autoValidation" -> o.autoValidation, + "auto_validation" -> o.autoValidation, "consumer_kind" -> o.consumerKind.name, "settings" -> o.settings.json, "status" -> o.status.name, @@ -376,6 +376,7 @@ case class ApiConsumerSubscriptionDates( created_at: DateTime, processed_at: DateTime, started_at: DateTime, + paused_at: DateTime, ending_at: DateTime, closed_at: DateTime ) @@ -387,6 +388,7 @@ object ApiConsumerSubscriptionDates { created_at = json.select("created_at").asOpt[Long].map(l => new DateTime(l)).getOrElse(DateTime.now()), processed_at = json.select("processed_at").asOpt[Long].map(l => new DateTime(l)).getOrElse(DateTime.now()), started_at = json.select("started_at").asOpt[Long].map(l => new DateTime(l)).getOrElse(DateTime.now()), + paused_at = json.select("paused_at").asOpt[Long].map(l => new DateTime(l)).getOrElse(DateTime.now()), ending_at = json.select("ending_at").asOpt[Long].map(l => new DateTime(l)).getOrElse(DateTime.now()), closed_at = json.select("closed_at").asOpt[Long].map(l => new DateTime(l)).getOrElse(DateTime.now()) ) @@ -399,6 +401,7 @@ object ApiConsumerSubscriptionDates { "created_at" -> o.created_at.getMillis, "processed_at" -> o.processed_at.getMillis, "started_at" -> o.started_at.getMillis, + "paused_at" -> o.paused_at.getMillis, "ending_at" -> o.ending_at.getMillis, "closed_at" -> o.closed_at.getMillis ) @@ -459,13 +462,23 @@ object ApiConsumerSubscription { ).left def addSubscriptionToConsumer(api: Api): Future[Boolean] = { - env.datastores.apiDataStore.set(api.copy(consumers = api.consumers.map(consumer => { - if (consumer.id == entity.consumerRef) { - consumer.copy(subscriptions = consumer.subscriptions :+ ApiConsumerSubscriptionRef(entity.id)) + env.datastores.apiDataStore.set(api.copy(consumers = api.consumers.map(consumer => { + if (consumer.id == entity.consumerRef) { + if(action == WriteAction.Update) { + consumer.copy(subscriptions = consumer.subscriptions.map(subscription => { + if (subscription.ref == entity.id) { + subscription.copy(entity.id) + } else { + subscription + } + })) } else { - consumer + consumer.copy(subscriptions = consumer.subscriptions :+ ApiConsumerSubscriptionRef(entity.id)) } - }))) + } else { + consumer + } + }))) } // println(s"write validation foo: ${singularName} - ${id} - ${action} - ${body.prettify}") @@ -562,41 +575,26 @@ object ApiConsumerSettings { dailyQuota: Long = RemainingQuotas.MaxValue, monthlyQuota: Long = RemainingQuotas.MaxValue) extends ApiConsumerSettings { def json: JsValue = Json.obj( - "name" -> name, - "config" -> Json.obj( - "throttlingQuota" -> throttlingQuota, - "dailyQuota" -> dailyQuota, - "monthlyQuota" -> monthlyQuota - ), + "throttlingQuota" -> throttlingQuota, + "dailyQuota" -> dailyQuota, + "monthlyQuota" -> monthlyQuota ) } case class Mtls(name: String, caRefs: Seq[String], certRefs: Seq[String]) extends ApiConsumerSettings { def json: JsValue = Json.obj( - "name" -> name, - "config" -> Json.obj( - "caRefs" -> caRefs, - "certRefs" -> certRefs, - ) + "caRefs" -> caRefs, + "certRefs" -> certRefs, ) } case class Keyless(name: String) extends ApiConsumerSettings { - def json: JsValue = Json.obj( - "name" -> name, - "config" -> Json.obj() - ) + def json: JsValue = Json.obj() } case class OAuth2(name: String, config: JsValue) extends ApiConsumerSettings { // using client credential stuff - def json: JsValue = Json.obj( - "name" -> name, - "config" -> config - ) + def json: JsValue = Json.obj() } case class JWT(name: String, jwtVerifierRefs: Seq[String]) extends ApiConsumerSettings { def json: JsValue = Json.obj( - "name" -> name, - "config" -> Json.obj( - "jwtVerifierRefs" -> jwtVerifierRefs - ) + "jwtVerifierRefs" -> jwtVerifierRefs ) } } @@ -970,11 +968,12 @@ trait ApiConsumerSubscriptionDataStore extends BasicStore[ApiConsumerSubscriptio tags = Seq.empty, enabled = true, dates = ApiConsumerSubscriptionDates( - created_at = DateTime.now(), - processed_at = DateTime.now(), - started_at = DateTime.now(), - ending_at = DateTime.now(), - closed_at = DateTime.now() + created_at = DateTime.now(), + processed_at = DateTime.now(), + started_at = DateTime.now(), + paused_at = DateTime.now(), + ending_at = DateTime.now(), + closed_at = DateTime.now() ), ownerRef = "", consumerRef = "", diff --git a/otoroshi/conf/routes b/otoroshi/conf/routes index 1c5e6ea0c..e76d4c739 100644 --- a/otoroshi/conf/routes +++ b/otoroshi/conf/routes @@ -616,6 +616,7 @@ GET /apis/cluster/node/metrics otoroshi.api.Gener GET /apis/cluster otoroshi.controllers.adminapi.ClusterController.getClusterMembers() GET /apis/api.otoroshi.io/v1/apis/:id/live otoroshi.next.controllers.adminapi.ApisController.liveStats(id, every: Option[Int]) +GET /apis/apis.otoroshi.io/v1/apiconsumersubscriptions/:id/_validate otoroshi.next.controllers.adminapi.ApisController.liveStats(id, every: Option[Int]) # New admin API - generic apis POST /apis/:group/:version/:entity/_bulk otoroshi.api.GenericApiController.bulkCreate(group, version, entity) diff --git a/otoroshi/javascript/src/forms/ng_plugins/NgBackend.js b/otoroshi/javascript/src/forms/ng_plugins/NgBackend.js index 84996506f..71c7e974d 100644 --- a/otoroshi/javascript/src/forms/ng_plugins/NgBackend.js +++ b/otoroshi/javascript/src/forms/ng_plugins/NgBackend.js @@ -510,7 +510,7 @@ export default { type: 'bool', }, load_balancing: { - label: 'load_balancing', + label: 'Load Balancing', type: 'form', collapsable: true, collapsed: true, @@ -518,8 +518,8 @@ export default { type: { type: 'select', help: 'The load balancing algorithm used', + label: 'Type', props: { - label: 'type', options: [ 'BestResponseTime', 'IpAddressHash', diff --git a/otoroshi/javascript/src/pages/ApiEditor/index.js b/otoroshi/javascript/src/pages/ApiEditor/index.js index 504eba7b0..f1e2d171f 100644 --- a/otoroshi/javascript/src/pages/ApiEditor/index.js +++ b/otoroshi/javascript/src/pages/ApiEditor/index.js @@ -53,7 +53,7 @@ export default function ApiEditor(props) { - + @@ -71,136 +71,289 @@ export default function ApiEditor(props) { } -function Subscriptions() { - return
- Subscriptions view -
+function Subscriptions(props) { + const history = useHistory() + const params = useParams() + + const columns = [ + { + title: 'Name', + filterId: 'name', + content: (item) => item.name, + } + ]; + + useEffect(() => { + props.setTitle('Subscriptions') + + return () => props.setTitle('') + }, []) + + const client = nextClient.forEntityNext(nextClient.ENTITIES.API_CONSUMER_SUBSCRIPTIONS) + + const rawSubscriptions = useQuery(["getSubscriptions"], () => { + return client.findAllWithPagination({ + page: 1, + pageSize: 15, + filtered: [{ + id: 'api_ref', + value: params.apiId + }] + }) + }) + + const deleteItem = item => client.delete(item.id) + .then(() => window.location.reload()) + + const fields = [] + + return + + history.push(`/apis/${params.apiId}/subscriptions/${item.id}/edit`)} + navigateOnEdit={(item) => history.push(`/apis/${params.apiId}/subscriptions/${item.id}/edit`)} + selfUrl="subscriptions" + defaultTitle="Subscription" + itemName="Subscription" + columns={columns} + fields={fields} + deleteItem={deleteItem} + fetchTemplate={client.template} + fetchItems={() => Promise.resolve(rawSubscriptions.data || [])} + defaultSort="name" + defaultSortDesc="true" + showActions={true} + showLink={false} + extractKey={(item) => item.id} + rowNavigation={true} + hideAddItemAction={true} + itemUrl={(i) => `/bo/dashboard/apis/${params.apiId}/subscriptions/${i.id}/edit`} + rawEditUrl={true} + displayTrash={(item) => item.id === props.globalEnv.adminApiId} + injectTopBar={() => ( +
+ + Create new subscription + + {props.injectTopBar} +
+ )} /> + } -function SubscriptionDesigner() { +function SubscriptionDesigner(props) { const params = useParams() const history = useHistory() - const [subscription, setSubscription] = useState({}) + const [subscription, setSubscription] = useState() const rawAPI = useQuery(["getAPI", params.apiId], () => nextClient.forEntityNext(nextClient.ENTITIES.APIS).findById(params.apiId) ) + const rawSubscription = useQuery(["getSubscription", params.subscriptionId], + () => nextClient.forEntityNext(nextClient.ENTITIES.API_CONSUMER_SUBSCRIPTIONS).findById(params.subscriptionId), + { + onSuccess: setSubscription + } + ) + + // prevent schema to have a empty consumers list + if (rawAPI.isLoading || !subscription) + return null + const schema = { + location: { + type: 'location' + }, name: { type: 'string', - label: 'Route name', - placeholder: 'My users route' + label: 'Name' }, - frontend: { - type: 'form', - label: 'Frontend', - schema: NgFrontend.schema, - props: { - v2: { - folded: ['domains', 'methods'], - flow: NgFrontend.flow, - } - } + description: { + type: 'string', + label: 'Description' }, - flow_ref: { - type: 'select', - label: 'Flow ID', - props: { - options: data.flows, - optionsTransformer: { - label: 'name', - value: 'id', - } - }, + enabled: { + type: 'boolean', + label: 'Enabled' }, - backend: { + owner_ref: { + type: 'string', + label: 'Owner' + }, + consumer_ref: { type: 'select', - label: 'Backend', + label: 'Consumer', props: { - options: [...data.backends, ...backends], + options: rawAPI.data.consumers, optionsTransformer: { value: 'id', label: 'name' } } + }, + token_refs: { + type: 'array', + label: 'Token refs' } } const flow = [ + 'location', { type: 'group', - name: 'Domains information', - collapsable: true, - fields: ['frontend'] + name: 'Informations', + collapsable: false, + fields: ['name', 'description', 'enabled'], }, { type: 'group', - collapsable: true, - collapsed: true, - name: 'Selected flow', - fields: ['flow_ref'], + name: 'Ownership', + collapsable: false, + fields: ['owner_ref', 'consumer_ref', 'token_refs'], }, + ] + + const updateSubscription = () => { + return nextClient + .forEntityNext(nextClient.ENTITIES.API_CONSUMER_SUBSCRIPTIONS) + .update(subscription) + .then(() => history.push(`/apis/${params.apiId}/subscriptions`)) + } + + return + + + +
+ +
+
+} + +function NewSubscription(props) { + const params = useParams() + const history = useHistory() + + const [subscription, setSubscription] = useState() + + const rawAPI = useQuery(["getAPI", params.apiId], + () => nextClient.forEntityNext(nextClient.ENTITIES.APIS).findById(params.apiId)) + + const templatesQuery = useQuery(["getTemplate"], + () => nextClient.forEntityNext(nextClient.ENTITIES.API_CONSUMER_SUBSCRIPTIONS).template(), + { + enabled: !!rawAPI.data, + onSuccess: sub => setSubscription({ + ...sub, + consumer_ref: rawAPI.data.consumers?.length > 0 ? rawAPI.data.consumers[0]?.id : undefined + }) + } + ) + + // prevent schema to have a empty consumers list + if (rawAPI.isLoading || !subscription) + return null + + const schema = { + location: { + type: 'location' + }, + name: { + type: 'string', + label: 'Name' + }, + description: { + type: 'string', + label: 'Description' + }, + enabled: { + type: 'boolean', + label: 'Enabled' + }, + owner_ref: { + type: 'string', + label: 'Owner' + }, + consumer_ref: { + type: 'select', + label: 'Consumer', + props: { + options: rawAPI.data.consumers, + optionsTransformer: { + value: 'id', + label: 'name' + } + } + }, + token_refs: { + type: 'array', + label: 'Token refs' + } + } + + const flow = [ + 'location', { type: 'group', - collapsable: true, - collapsed: true, - name: 'Backend configuration', - fields: ['backend'], + name: 'Informations', + collapsable: false, + fields: ['name', 'description', 'enabled'], }, { type: 'group', - collapsable: true, - collapsed: true, - name: 'Additional informations', - fields: ['name'], - } + name: 'Ownership', + collapsable: false, + fields: ['owner_ref', 'consumer_ref', 'token_refs'], + }, ] - const updateRoute = () => { + const updateSubscription = () => { return nextClient - .forEntityNext(nextClient.ENTITIES.APIS) - .update({ - ...rawAPI.data, - routes: rawAPI.data.routes.map(item => { - if (item.id === route.id) - return route - return item - }) + .forEntityNext(nextClient.ENTITIES.API_CONSUMER_SUBSCRIPTIONS) + .create({ + ...subscription, + api_ref: params.apiId }) - .then(() => history.push(`/apis/${params.apiId}/routes`)) + .then(() => history.push(`/apis/${params.apiId}/subscriptions`)) } - return + return - {/* */} + />
- {/* setRoute(newValue)} /> */} + flow={flow} + onChange={setSubscription} />
} -function NewSubscription() { - return
- New Subscription -
-} - function RouteDesigner(props) { const params = useParams() const history = useHistory() @@ -611,49 +764,14 @@ function Consumers(props) { const TEMPLATES = { apikey: { - name: 'apikey', - config: { - throttlingQuota: 1000, - dailyQuota: 1000, - monthlyQuota: 1000 - } - }, - mtls: { - name: 'mtls', - config: {} + throttlingQuota: 1000, + dailyQuota: 1000, + monthlyQuota: 1000 }, - keyless: { - name: 'keyless', - config: {} - }, - oauth2: { - name: 'oauth2', - config: {} - }, - jwt: { - name: 'jwt', - config: { - strict: true, - source: { - type: "InHeader", - name: "X-JWT-Token", - remove: "" - }, - algoSettings: { - "type": "HSAlgoSettings", - "size": 512, - "secret": "secret", - "base64": false - }, - strategy: { - type: 'PassThrough', - verificationSettings: { - fields: {}, - arrayFields: {} - } - } - } - } + mtls: {}, + keyless: {}, + oauth2: {}, + jwt: {} } function NewConsumer(props) { @@ -722,6 +840,13 @@ function NewConsumer(props) { } }, + auto_validation: { + type: 'box-bool', + label: 'Auto-validation', + props: { + description: "When creating a customer, you can enable subscription auto-validation to immediately approve subscription requests. If Auto validate subscription is disabled, the API publisher must approve all subscription requests." + } + }, settings: { type: 'json', label: 'Plan configuration' @@ -823,6 +948,13 @@ function ConsumerDesigner(props) { } }, + auto_validation: { + type: 'box-bool', + label: 'Auto-validation', + props: { + description: "When creating a customer, you can enable subscription auto-validation to immediately approve subscription requests. If Auto validate subscription is disabled, the API publisher must approve all subscription requests." + } + }, settings: { type: 'json', label: 'Plan configuration' @@ -1636,7 +1768,14 @@ function Dashboard(props) { /> {hasCreateConsumer && - c.subscriptions).length <= 0 ? 'Souscriptions will appear here' : ''} /> + c.subscriptions).length <= 0 ? 'Souscriptions will appear here' : ''} + actions={