From 20dd4bfe80352f21d53a859a618c4c8a1a202063 Mon Sep 17 00:00:00 2001 From: HelaKaraa Date: Fri, 6 Sep 2024 16:52:36 +0200 Subject: [PATCH 01/14] feat #728: add validUntil field to API key subscriptions --- daikoku/app/controllers/ApiController.scala | 1 + daikoku/app/domain/SchemaDefinition.scala | 1 + daikoku/app/domain/apikeyEntities.scala | 2 + daikoku/app/domain/json.scala | 2 + daikoku/app/utils/ApiService.scala | 2 + .../backoffice/apis/TeamApiSubscriptions.tsx | 164 +++++----- .../modals/SubscriptionMetadataModal.tsx | 281 +++++++++++------- .../src/locales/en/translation.json | 3 + .../src/locales/fr/translation.json | 3 + daikoku/javascript/src/services/index.ts | 1 + daikoku/javascript/src/types/api.ts | 2 + 11 files changed, 281 insertions(+), 181 deletions(-) diff --git a/daikoku/app/controllers/ApiController.scala b/daikoku/app/controllers/ApiController.scala index 7e20ef0ea..92d5e455e 100644 --- a/daikoku/app/controllers/ApiController.scala +++ b/daikoku/app/controllers/ApiController.scala @@ -1072,6 +1072,7 @@ class ApiController( apiKey = data.apiKey, plan = data.plan, createdAt = DateTime.now(), + validUntil = DateTime.now(), team = data.team, api = data.api, by = ctx.user.id, diff --git a/daikoku/app/domain/SchemaDefinition.scala b/daikoku/app/domain/SchemaDefinition.scala index 64f73e029..04e520b46 100644 --- a/daikoku/app/domain/SchemaDefinition.scala +++ b/daikoku/app/domain/SchemaDefinition.scala @@ -2109,6 +2109,7 @@ object SchemaDefinition { ) ), Field("createdAt", DateTimeUnitype, resolve = _.value.createdAt), + Field("validUntil", DateTimeUnitype, resolve = _.value.validUntil), Field( "team", OptionType(TeamObjectType), diff --git a/daikoku/app/domain/apikeyEntities.scala b/daikoku/app/domain/apikeyEntities.scala index 907bd60e4..ae19d536e 100644 --- a/daikoku/app/domain/apikeyEntities.scala +++ b/daikoku/app/domain/apikeyEntities.scala @@ -59,6 +59,7 @@ case class ApiSubscription( apiKey: OtoroshiApiKey, // TODO: add the actual plan at the time of the subscription plan: UsagePlanId, createdAt: DateTime, + validUntil: DateTime, team: TeamId, api: ApiId, by: UserId, @@ -108,6 +109,7 @@ case class ApiSubscription( "team" -> json.TeamIdFormat.writes(team), "api" -> json.ApiIdFormat.writes(api), "createdAt" -> json.DateTimeFormat.writes(createdAt), + "validUntil" -> json.DateTimeFormat.writes(validUntil), "customName" -> customName .map(id => JsString(id)) .getOrElse(JsNull) diff --git a/daikoku/app/domain/json.scala b/daikoku/app/domain/json.scala index e632c15b9..058ac77aa 100644 --- a/daikoku/app/domain/json.scala +++ b/daikoku/app/domain/json.scala @@ -2731,6 +2731,7 @@ object json { team = (json \ "team").as(TeamIdFormat), api = (json \ "api").as(ApiIdFormat), createdAt = (json \ "createdAt").as(DateTimeFormat), + validUntil = (json \ "validUntil").as(DateTimeFormat), by = (json \ "by").as(UserIdFormat), customName = (json \ "customName").asOpt[String], adminCustomName = (json \ "adminCustomName").asOpt[String], @@ -2779,6 +2780,7 @@ object json { "team" -> TeamIdFormat.writes(o.team), "api" -> ApiIdFormat.writes(o.api), "createdAt" -> DateTimeFormat.writes(o.createdAt), + "validUntil"-> DateTimeFormat.writes(o.validUntil), "by" -> UserIdFormat.writes(o.by), "customName" -> o.customName .map(id => JsString(id)) diff --git a/daikoku/app/utils/ApiService.scala b/daikoku/app/utils/ApiService.scala index 2d88f3803..6edde4729 100644 --- a/daikoku/app/utils/ApiService.scala +++ b/daikoku/app/utils/ApiService.scala @@ -256,6 +256,7 @@ class ApiService( apiKey = tunedApiKey.asOtoroshiApiKey, plan = plan.id, createdAt = DateTime.now(), + validUntil = DateTime.now(), team = team.id, api = api.id, by = user.id, @@ -359,6 +360,7 @@ class ApiService( apiKey = OtoroshiApiKey(clientName, clientId, clientSecret), plan = plan.id, createdAt = DateTime.now(), + validUntil = DateTime.now(), team = team.id, api = api.id, by = user.id, diff --git a/daikoku/javascript/src/components/backoffice/apis/TeamApiSubscriptions.tsx b/daikoku/javascript/src/components/backoffice/apis/TeamApiSubscriptions.tsx index d6e42ef74..025e64b80 100644 --- a/daikoku/javascript/src/components/backoffice/apis/TeamApiSubscriptions.tsx +++ b/daikoku/javascript/src/components/backoffice/apis/TeamApiSubscriptions.tsx @@ -1,13 +1,13 @@ -import { getApolloContext } from '@apollo/client'; -import { format, type } from '@maif/react-forms'; -import { createColumnHelper } from '@tanstack/react-table'; -import { useContext, useEffect, useRef, useState } from 'react'; -import { toast } from 'sonner'; +import { getApolloContext } from "@apollo/client"; +import { format, type } from "@maif/react-forms"; +import { createColumnHelper } from "@tanstack/react-table"; +import { useContext, useEffect, useRef, useState } from "react"; +import { toast } from "sonner"; -import { ModalContext } from '../../../contexts'; -import { CustomSubscriptionData } from '../../../contexts/modals/SubscriptionMetadataModal'; -import { I18nContext } from '../../../contexts'; -import * as Services from '../../../services'; +import { ModalContext } from "../../../contexts"; +import { CustomSubscriptionData } from "../../../contexts/modals/SubscriptionMetadataModal"; +import { I18nContext } from "../../../contexts"; +import * as Services from "../../../services"; import { IApi, isError, @@ -16,8 +16,8 @@ import { ITeamSimple, IUsagePlan, ResponseError, -} from '../../../types'; -import { SwitchButton, Table, TableRef } from '../../inputs'; +} from "../../../types"; +import { SwitchButton, Table, TableRef } from "../../inputs"; import { api as API, BeautifulTitle, @@ -27,8 +27,9 @@ import { manage, Option, Spinner, -} from '../../utils'; -import { useQuery, useQueryClient, useMutation } from '@tanstack/react-query'; +} from "../../utils"; +import { useQuery, useQueryClient, useMutation } from "@tanstack/react-query"; +import { cp } from "fs"; type TeamApiSubscriptionsProps = { api: IApi; @@ -58,6 +59,7 @@ interface IApiSubscriptionGql extends ISubscriptionCustomization { type: string; }; createdAt: string; + validUntil: string; api: { _id: string; }; @@ -106,17 +108,17 @@ export const TeamApiSubscriptions = ({ useContext(ModalContext); const plansQuery = useQuery({ - queryKey: ['plans'], + queryKey: ["plans"], queryFn: () => Services.getAllPlanOfApi(api.team, api._id, api.currentVersion), }); const subscriptionsQuery = useQuery({ - queryKey: ['subscriptions'], + queryKey: ["subscriptions"], queryFn: () => client! .query<{ apiApiSubscriptions: Array }>({ query: Services.graphql.getApiSubscriptions, - fetchPolicy: 'no-cache', + fetchPolicy: "no-cache", variables: { apiId: api._id, teamId: currentTeam._id, @@ -186,7 +188,7 @@ export const TeamApiSubscriptions = ({ }); useEffect(() => { - document.title = `${currentTeam.name} - ${translate('Subscriptions')}`; + document.title = `${currentTeam.name} - ${translate("Subscriptions")}`; }, []); useEffect(() => { @@ -204,9 +206,9 @@ export const TeamApiSubscriptions = ({ columnHelper.accessor( (row) => row.adminCustomName || row.apiKey.clientName, { - id: 'adminCustomName', - header: translate('Name'), - meta: { style: { textAlign: 'left' } }, + id: "adminCustomName", + header: translate("Name"), + meta: { style: { textAlign: "left" } }, filterFn: (row, _, value) => { const sub = row.original; const displayed: string = @@ -218,34 +220,52 @@ export const TeamApiSubscriptions = ({ .toLocaleLowerCase() .includes(value.toLocaleLowerCase()); }, - sortingFn: 'basic', + sortingFn: "basic", cell: (info) => { const sub = info.row.original; + const titleDate = `
+ ${translate("validationDate.apikey.badge.title")} : + ${sub.validUntil ? formatDate(sub.validUntil, language) : "N/A"} +
`; if (sub.parent) { const title = `
- ${translate('aggregated.apikey.badge.title')} + ${translate("aggregated.apikey.badge.title")}
    -
  • ${translate('Api')}: ${sub.parent.api.name}
  • -
  • ${translate('Plan')}: ${sub.parent.plan.customName}
  • -
  • ${translate('aggregated.apikey.badge.apikey.name')}: ${sub.parent.adminCustomName}
  • +
  • ${translate("Api")}: ${sub.parent.api.name}
  • +
  • ${translate("Plan")}: ${sub.parent.plan.customName}
  • +
  • ${translate("aggregated.apikey.badge.apikey.name")}: ${sub.parent.adminCustomName}
`; return (
{info.getValue()} - -
A
-
+
+ +
V
+
+ + +
A
+
+
); } - return
{info.getValue()}
; + + return ( +
+ {info.getValue()} + +
V
+
+
+ ); }, } ), - columnHelper.accessor('plan', { - header: translate('Plan'), - meta: { style: { textAlign: 'left' } }, + columnHelper.accessor("plan", { + header: translate("Plan"), + meta: { style: { textAlign: "left" } }, cell: (info) => Option(usagePlans.find((pp) => pp._id === info.getValue()._id)) .map((p: IUsagePlan) => p.customName || formatPlanType(p, translate)) @@ -255,16 +275,16 @@ export const TeamApiSubscriptions = ({ usagePlans.find((pp) => pp._id === row.original.plan._id) ) .map((p: IUsagePlan) => p.customName || formatPlanType(p, translate)) - .getOrElse(''); + .getOrElse(""); return displayed .toLocaleLowerCase() .includes(value.toLocaleLowerCase()); }, }), - columnHelper.accessor('team', { - header: translate('Team'), - meta: { style: { textAlign: 'left' } }, + columnHelper.accessor("team", { + header: translate("Team"), + meta: { style: { textAlign: "left" } }, cell: (info) => info.getValue().name, filterFn: (row, columnId, value) => { const displayed: string = row.original.team.name; @@ -274,11 +294,11 @@ export const TeamApiSubscriptions = ({ .includes(value.toLocaleLowerCase()); }, }), - columnHelper.accessor('enabled', { - header: translate('Enabled'), + columnHelper.accessor("enabled", { + header: translate("Enabled"), enableColumnFilter: false, enableSorting: false, - meta: { style: { textAlign: 'center' } }, + meta: { style: { textAlign: "center" } }, cell: (info) => { const sub = info.row.original; return ( @@ -291,7 +311,7 @@ export const TeamApiSubscriptions = ({ !sub.enabled ).then(() => { tableRef.current?.update(); - queryClient.invalidateQueries({ queryKey: ['subscriptions'] }); + queryClient.invalidateQueries({ queryKey: ["subscriptions"] }); }) } checked={sub.enabled} @@ -299,38 +319,38 @@ export const TeamApiSubscriptions = ({ ); }, }), - columnHelper.accessor('createdAt', { + columnHelper.accessor("createdAt", { enableColumnFilter: false, - header: translate('Created at'), - meta: { style: { textAlign: 'left' } }, + header: translate("Created at"), + meta: { style: { textAlign: "left" } }, cell: (info) => { const date = info.getValue(); if (!!date) { return formatDate(date, language); } - return translate('N/A'); + return translate("N/A"); }, }), - columnHelper.accessor('lastUsage', { + columnHelper.accessor("lastUsage", { enableColumnFilter: false, - header: translate('apisubscription.lastUsage.label'), - meta: { style: { textAlign: 'left' } }, + header: translate("apisubscription.lastUsage.label"), + meta: { style: { textAlign: "left" } }, cell: (info) => { const date = info.getValue(); if (!!date) { return formatDate(date, language); } - return translate('N/A'); + return translate("N/A"); }, }), columnHelper.display({ - header: translate('Actions'), - meta: { style: { textAlign: 'center', width: '120px' } }, + header: translate("Actions"), + meta: { style: { textAlign: "center", width: "120px" } }, cell: (info) => { const sub = info.row.original; return (
- + - + - + {!!filters && (
(props: SubscriptionMetadataModalProps & IBaseModalProps) => { + customMetadata: { [key: string]: string }; + customMaxPerSecond: number; + customMaxPerDay: number; + customMaxPerMonth: number; + customReadOnly: boolean; + adminCustomName: string; + validUntil: Date; +}; +export const SubscriptionMetadataModal = ( + props: SubscriptionMetadataModalProps & IBaseModalProps +) => { const { translate, Translation } = useContext(I18nContext); - const formRef = useRef() - + const formRef = useRef(); const apiQuery = useQuery({ - queryKey: ['api'], + queryKey: ["api"], queryFn: () => Services.getVisibleApiWithId(props.api!), - enabled: !!props.api - }) + enabled: !!props.api, + }); const planQuery = useQuery({ - queryKey: ['plan'], + queryKey: ["plan"], queryFn: () => { - const api = apiQuery.data as IApi - return Services.getVisiblePlan(api._humanReadableId, api.currentVersion, props.plan!) + const api = apiQuery.data as IApi; + return Services.getVisiblePlan( + api._humanReadableId, + api.currentVersion, + props.plan! + ); }, - enabled: !!props.plan && !!apiQuery.data && !isError(apiQuery.data) - }) + enabled: !!props.plan && !!apiQuery.data && !isError(apiQuery.data), + }); const actionAndClose = (formData) => { const subProps: CustomSubscriptionData = { @@ -60,10 +65,11 @@ export const SubscriptionMetadataModal = (props: Subscri customMaxPerDay: formData.customQuotas.customMaxPerDay, customMaxPerMonth: formData.customQuotas.customMaxPerMonth, customReadOnly: formData.customReadOnly, - adminCustomName: formData.adminCustomName + adminCustomName: formData.adminCustomName, + validUntil: formData.validUntil, }; - const res = props.save(subProps) + const res = props.save(subProps); if (res instanceof Promise) { res.then(() => !props.noClose && props.close()); } else if (!props.noClose) { @@ -74,55 +80,68 @@ export const SubscriptionMetadataModal = (props: Subscri const schema = { customMetadata: { type: type.object, - label: translate('Additional metadata'), + label: translate("Additional metadata"), }, customQuotas: { type: type.object, format: format.form, - label: translate('Custom quotas'), + label: translate("Custom quotas"), schema: { customMaxPerSecond: { type: type.number, - label: translate('Max. requests per second'), + label: translate("Max. requests per second"), constraints: [ - constraints.min(0, translate('constraints.min.0')) //todo: translate - ] + constraints.min(0, translate("constraints.min.0")), //todo: translate + ], }, customMaxPerDay: { type: type.number, - label: translate('Max. requests per day'), + label: translate("Max. requests per day"), constraints: [ - constraints.min(0, translate('constraints.min.0')) //todo: translate - ] + constraints.min(0, translate("constraints.min.0")), //todo: translate + ], }, customMaxPerMonth: { type: type.number, - label: translate('Max. requests per month'), + label: translate("Max. requests per month"), constraints: [ - constraints.min(0, translate('constraints.min.0')) //todo: translate - ] + constraints.min(0, translate("constraints.min.0")), //todo: translate + ], }, - } + }, }, customReadOnly: { type: type.bool, - label: translate('Read only apikey') + label: translate("Read only apikey"), }, adminCustomName: { type: type.string, - label: translate('sub.meta.modal.admin.custom.name.label'), - help: translate('sub.meta.modal.admin.custom.name.help'), - } - } + label: translate("sub.meta.modal.admin.custom.name.label"), + help: translate("sub.meta.modal.admin.custom.name.help"), + }, + validUntil: { + type: type.date, + label: translate("sub.meta.modal.valid.until.label"), + help: translate("sub.meta.modal.valid.until.help"), + }, + }; const mandatoryMetadataSchema = (plan?: IUsagePlan) => ({ metadata: { type: type.object, format: format.form, visible: !!plan, - label: translate({ key: 'mandatory.metadata.label', replacements: [plan?.otoroshiTarget?.apikeyCustomization.customMetadata.length.toString() || ''] }), - schema: sortBy(plan?.otoroshiTarget?.apikeyCustomization.customMetadata, ['key']) - .map((meta: { key: string, possibleValues: Array }) => { + label: translate({ + key: "mandatory.metadata.label", + replacements: [ + plan?.otoroshiTarget?.apikeyCustomization.customMetadata.length.toString() || + "", + ], + }), + schema: sortBy(plan?.otoroshiTarget?.apikeyCustomization.customMetadata, [ + "key", + ]) + .map((meta: { key: string; possibleValues: Array }) => { return { key: meta.key, schemaEntry: { @@ -131,40 +150,68 @@ export const SubscriptionMetadataModal = (props: Subscri createOption: true, options: meta.possibleValues, constraints: [ - constraints.required(translate('constraints.required.value')) - ] - } - } + constraints.required(translate("constraints.required.value")), + ], + }, + }; }) .reduce((acc, curr) => { - return { ...acc, [curr.key]: curr.schemaEntry } + return { ...acc, [curr.key]: curr.schemaEntry }; }, {}), }, - }) + }); - if (!!props.api && apiQuery.isLoading) { - return (
) + if (!!props.api && apiQuery.isLoading) { + return ( +
+ +
+ ); } else if (props.plan && planQuery.isLoading) { - return (
) + return ( +
+ +
+ ); } else if (apiQuery.error || planQuery.error) { - } - - if (!!props.api && apiQuery.isLoading || props.plan && planQuery.isLoading) { - return
- } else if (!props.api && planQuery.data || (apiQuery.data && !isError(apiQuery.data))) { - const plan = !!props.plan ? !isError(planQuery.data) ? planQuery.data : undefined : undefined + if ( + (!!props.api && apiQuery.isLoading) || + (props.plan && planQuery.isLoading) + ) { + return ( +
+ +
+ ); + } else if ( + (!props.api && planQuery.data) || + (apiQuery.data && !isError(apiQuery.data)) + ) { + const plan = !!props.plan + ? !isError(planQuery.data) + ? planQuery.data + : undefined + : undefined; const maybeSubMetadata = Option(props.subscription?.customMetadata) .orElse(props.config?.customMetadata) - .orElse({...props.subscriptionDemand?.motivation, ...props.subscriptionDemand?.customMetadata}) + .orElse({ + ...props.subscriptionDemand?.motivation, + ...props.subscriptionDemand?.customMetadata, + }) .map((v) => Object.entries(v)) .getOrElse([]); const [maybeMetadata, maybeCustomMetadata] = maybeSubMetadata.reduce( ([accMeta, accCustomMeta]: any, item: any) => { - if (plan && plan.otoroshiTarget?.apikeyCustomization.customMetadata.some((x: any) => x.key === item[0])) { + if ( + plan && + plan.otoroshiTarget?.apikeyCustomization.customMetadata.some( + (x: any) => x.key === item[0] + ) + ) { return [[...accMeta, item], accCustomMeta]; } return [accMeta, [...accCustomMeta, item]]; @@ -195,45 +242,61 @@ export const SubscriptionMetadataModal = (props: Subscri .getOrNull(), adminCustomName: Option(props.subscription?.adminCustomName) .orElse(props.subscriptionDemand?.adminCustomName) - .getOrNull() - } - - + .getOrNull(), + validUntil: Option(props.subscription?.validUntil) + .orElse(props.subscription?.validUntil) + .getOrNull(), + }; - const _schema = { ...mandatoryMetadataSchema(plan), ...schema } - return (
-
-
- Subscription metadata -
-
-
- {props.description &&
{props.description}
} -
<>} - className='mb-1' - /> - -
- + const _schema = { ...mandatoryMetadataSchema(plan), ...schema }; + return ( +
+
+
+ + Subscription metadata + +
+ className="btn-close" + aria-label="Close" + onClick={props.close} + /> +
+
+ {props.description && ( +
{props.description}
+ )} + <>} + className="mb-1" + /> + +
+ + +
-
); + ); } else { - return
Error while fetching metadata
+ return
Error while fetching metadata
; } - }; diff --git a/daikoku/javascript/src/locales/en/translation.json b/daikoku/javascript/src/locales/en/translation.json index ea55b3aca..5ad2609e4 100644 --- a/daikoku/javascript/src/locales/en/translation.json +++ b/daikoku/javascript/src/locales/en/translation.json @@ -1371,6 +1371,8 @@ "motivation.form.sample.button.label": "Test", "sub.meta.modal.admin.custom.name.label": "Custom key name", "sub.meta.modal.admin.custom.name.help": "this custom name will only be visible to the admin team", + "sub.meta.modal.valid.until.label": "Valid until", + "sub.meta.modal.valid.until.help": "The date until which the subscription will be valid", "documentation.add.page.btn.label": "Add page", "DisplayMode": "Display mode", "display.environment.label": "Environment", @@ -1394,6 +1396,7 @@ "aggregated.apikey.badge.apikey.name": "API key custom name", "apisubscription.lastUsage.label": "Last usage", "N/A": "N/A", + "validationDate.apikey.badge.title": "Valid Until", "semver.error.message": "Can't create a version with special characters : %s", "version.creation.success.message": "New version of API created successfully", "error.message.creation.security.enabled": "You're not authorized to create an API, please contact your administrator.", diff --git a/daikoku/javascript/src/locales/fr/translation.json b/daikoku/javascript/src/locales/fr/translation.json index ae3718b39..31135dd6d 100644 --- a/daikoku/javascript/src/locales/fr/translation.json +++ b/daikoku/javascript/src/locales/fr/translation.json @@ -1371,6 +1371,8 @@ "motivation.form.sample.button.label": "Tester", "sub.meta.modal.admin.custom.name.label": "Nom personnalisé de la clé", "sub.meta.modal.admin.custom.name.help": "ce nom personnalisé ne sera visible que de l'équipe d'administration", + "sub.meta.modal.valid.until.label": "Valide jusqu'à", + "sub.meta.modal.valid.until.help": "La date de fin de validité de la clé", "documentation.add.page.btn.label": "Ajouter une page", "DisplayMode": "Affichage", "display.environment.label": "Environnement", @@ -1394,6 +1396,7 @@ "aggregated.apikey.badge.apikey.name": "Nom personnalisée de la clé", "apisubscription.lastUsage.label": "Dernier usage", "N/A": "N/A", + "validationDate.apikey.badge.title": "Date de validation", "semver.error.message": "Une version ne peut pas être créée avec des caractères spéciaux : %s", "version.creation.success.message": "La nouvelle version de l'API a été créée avec succès", "error.message.creation.security.enabled": "Vous n'êtes pas autorisé à créer d'API, merci de contacter votre administrateur.", diff --git a/daikoku/javascript/src/services/index.ts b/daikoku/javascript/src/services/index.ts index 1a430bfb7..16fdd1994 100644 --- a/daikoku/javascript/src/services/index.ts +++ b/daikoku/javascript/src/services/index.ts @@ -1601,6 +1601,7 @@ export const graphql = { type } createdAt + validUntil api { _id name diff --git a/daikoku/javascript/src/types/api.ts b/daikoku/javascript/src/types/api.ts index edf8614ee..723bd68b5 100644 --- a/daikoku/javascript/src/types/api.ts +++ b/daikoku/javascript/src/types/api.ts @@ -378,6 +378,7 @@ export interface IBaseSubscription { team: string; api: string; createdAt: string; + validUntil: string; by: string; customName: string | null; enabled: boolean; @@ -442,6 +443,7 @@ export interface ISubscriptionCustomization { customMaxPerDay?: number; customReadOnly?: boolean; adminCustomName?: string; + validUntil: string; } export interface ISubscriptionExtended extends ISubscription { From d8e480b5d44609d1d3b71ea50d72c392eada1a1f Mon Sep 17 00:00:00 2001 From: HelaKaraa Date: Thu, 12 Sep 2024 11:08:39 +0200 Subject: [PATCH 02/14] feat #728: Modify validUntil type to String --- daikoku/app/controllers/ApiController.scala | 5 +++-- daikoku/app/domain/SchemaDefinition.scala | 4 +++- daikoku/app/domain/apikeyEntities.scala | 4 ++-- daikoku/app/domain/json.scala | 7 +++++-- daikoku/app/utils/ApiService.scala | 4 ++-- 5 files changed, 15 insertions(+), 9 deletions(-) diff --git a/daikoku/app/controllers/ApiController.scala b/daikoku/app/controllers/ApiController.scala index 92d5e455e..f4a7f0fdf 100644 --- a/daikoku/app/controllers/ApiController.scala +++ b/daikoku/app/controllers/ApiController.scala @@ -1072,7 +1072,7 @@ class ApiController( apiKey = data.apiKey, plan = data.plan, createdAt = DateTime.now(), - validUntil = DateTime.now(), + validUntil = None, team = data.team, api = data.api, by = ctx.user.id, @@ -1750,7 +1750,8 @@ class ApiController( customMaxPerDay = (body \ "customMaxPerDay").asOpt[Long], customMaxPerMonth = (body \ "customMaxPerMonth").asOpt[Long], customReadOnly = (body \ "customReadOnly").asOpt[Boolean], - adminCustomName = (body \ "adminCustomName").asOpt[String] + adminCustomName = (body \ "adminCustomName").asOpt[String], + validUntil = (body \ "validUntil").asOpt[String], ) result <- EitherT(apiService.updateSubscription(ctx.tenant, subToSave, plan)) diff --git a/daikoku/app/domain/SchemaDefinition.scala b/daikoku/app/domain/SchemaDefinition.scala index 04e520b46..8c206956b 100644 --- a/daikoku/app/domain/SchemaDefinition.scala +++ b/daikoku/app/domain/SchemaDefinition.scala @@ -2109,7 +2109,9 @@ object SchemaDefinition { ) ), Field("createdAt", DateTimeUnitype, resolve = _.value.createdAt), - Field("validUntil", DateTimeUnitype, resolve = _.value.validUntil), + Field("validUntil", + OptionType(StringType), + resolve = _.value.validUntil), Field( "team", OptionType(TeamObjectType), diff --git a/daikoku/app/domain/apikeyEntities.scala b/daikoku/app/domain/apikeyEntities.scala index ae19d536e..844396cd3 100644 --- a/daikoku/app/domain/apikeyEntities.scala +++ b/daikoku/app/domain/apikeyEntities.scala @@ -59,7 +59,7 @@ case class ApiSubscription( apiKey: OtoroshiApiKey, // TODO: add the actual plan at the time of the subscription plan: UsagePlanId, createdAt: DateTime, - validUntil: DateTime, + validUntil: Option[String] = None, team: TeamId, api: ApiId, by: UserId, @@ -109,7 +109,7 @@ case class ApiSubscription( "team" -> json.TeamIdFormat.writes(team), "api" -> json.ApiIdFormat.writes(api), "createdAt" -> json.DateTimeFormat.writes(createdAt), - "validUntil" -> json.DateTimeFormat.writes(validUntil), + "validUntil" -> validUntil.map(JsString).getOrElse(JsNull).as[JsValue], "customName" -> customName .map(id => JsString(id)) .getOrElse(JsNull) diff --git a/daikoku/app/domain/json.scala b/daikoku/app/domain/json.scala index 058ac77aa..5872278a1 100644 --- a/daikoku/app/domain/json.scala +++ b/daikoku/app/domain/json.scala @@ -2731,7 +2731,7 @@ object json { team = (json \ "team").as(TeamIdFormat), api = (json \ "api").as(ApiIdFormat), createdAt = (json \ "createdAt").as(DateTimeFormat), - validUntil = (json \ "validUntil").as(DateTimeFormat), + validUntil = (json \ "validUntil").asOpt[String], by = (json \ "by").as(UserIdFormat), customName = (json \ "customName").asOpt[String], adminCustomName = (json \ "adminCustomName").asOpt[String], @@ -2780,7 +2780,10 @@ object json { "team" -> TeamIdFormat.writes(o.team), "api" -> ApiIdFormat.writes(o.api), "createdAt" -> DateTimeFormat.writes(o.createdAt), - "validUntil"-> DateTimeFormat.writes(o.validUntil), + "validUntil"-> o.validUntil + .map(id => JsString(id)) + .getOrElse(JsNull) + .as[JsValue], "by" -> UserIdFormat.writes(o.by), "customName" -> o.customName .map(id => JsString(id)) diff --git a/daikoku/app/utils/ApiService.scala b/daikoku/app/utils/ApiService.scala index 6edde4729..21b42b2cb 100644 --- a/daikoku/app/utils/ApiService.scala +++ b/daikoku/app/utils/ApiService.scala @@ -256,7 +256,7 @@ class ApiService( apiKey = tunedApiKey.asOtoroshiApiKey, plan = plan.id, createdAt = DateTime.now(), - validUntil = DateTime.now(), + validUntil = None, team = team.id, api = api.id, by = user.id, @@ -360,7 +360,7 @@ class ApiService( apiKey = OtoroshiApiKey(clientName, clientId, clientSecret), plan = plan.id, createdAt = DateTime.now(), - validUntil = DateTime.now(), + validUntil = None, team = team.id, api = api.id, by = user.id, From 0b12f59ed4eb38427be41ab412f90393548d6522 Mon Sep 17 00:00:00 2001 From: HelaKaraa Date: Thu, 12 Sep 2024 17:02:49 +0200 Subject: [PATCH 03/14] feat #728: Aggregation Mode for Validation date --- .../backoffice/apis/TeamApiSubscriptions.tsx | 14 ++++++++++---- .../contexts/modals/SubscriptionMetadataModal.tsx | 12 +++++++----- daikoku/javascript/src/services/index.ts | 1 + daikoku/javascript/src/types/api.ts | 5 +++-- 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/daikoku/javascript/src/components/backoffice/apis/TeamApiSubscriptions.tsx b/daikoku/javascript/src/components/backoffice/apis/TeamApiSubscriptions.tsx index 025e64b80..3f866b1b6 100644 --- a/daikoku/javascript/src/components/backoffice/apis/TeamApiSubscriptions.tsx +++ b/daikoku/javascript/src/components/backoffice/apis/TeamApiSubscriptions.tsx @@ -77,6 +77,7 @@ interface IApiSubscriptionGql extends ISubscriptionCustomization { _id: string; adminCustomName: string; enabled: boolean; + validUntil: string; api: { _id: string; name: string; @@ -196,7 +197,6 @@ export const TeamApiSubscriptions = ({ tableRef.current?.update(); } }, [api, subscriptionsQuery.data]); - useEffect(() => { tableRef.current?.update(); }, [filters]); @@ -211,6 +211,7 @@ export const TeamApiSubscriptions = ({ meta: { style: { textAlign: "left" } }, filterFn: (row, _, value) => { const sub = row.original; + const displayed: string = sub.team._id === currentTeam._id ? sub.customName || sub.apiKey.clientName @@ -223,7 +224,7 @@ export const TeamApiSubscriptions = ({ sortingFn: "basic", cell: (info) => { const sub = info.row.original; - const titleDate = `
+ let titleDate = `
${translate("validationDate.apikey.badge.title")} : ${sub.validUntil ? formatDate(sub.validUntil, language) : "N/A"}
`; @@ -236,6 +237,10 @@ export const TeamApiSubscriptions = ({
  • ${translate("aggregated.apikey.badge.apikey.name")}: ${sub.parent.adminCustomName}
  • `; + titleDate = `
    + ${translate("validationDate.apikey.badge.title")} : + ${sub.parent.validUntil ? formatDate(sub.parent.validUntil, language) : "N/A"} +
    `; return (
    {info.getValue()} @@ -386,8 +391,8 @@ export const TeamApiSubscriptions = ({ }), ]; - const updateMeta = (sub: IApiSubscriptionGql) => - openSubMetadataModal({ + const updateMeta = (sub: IApiSubscriptionGql) => { + return openSubMetadataModal({ save: (updates: CustomSubscriptionData) => { Services.updateSubscription(currentTeam, { ...sub, ...updates }).then( () => { @@ -404,6 +409,7 @@ export const TeamApiSubscriptions = ({ (p) => sub.plan._id === p._id )!, }); + }; const regenerateApiKeySecret = useMutation({ mutationFn: (sub: IApiSubscriptionGql) => diff --git a/daikoku/javascript/src/contexts/modals/SubscriptionMetadataModal.tsx b/daikoku/javascript/src/contexts/modals/SubscriptionMetadataModal.tsx index 628ff8546..08ca52573 100644 --- a/daikoku/javascript/src/contexts/modals/SubscriptionMetadataModal.tsx +++ b/daikoku/javascript/src/contexts/modals/SubscriptionMetadataModal.tsx @@ -77,7 +77,7 @@ export const SubscriptionMetadataModal = ( } }; - const schema = { + let schema = { customMetadata: { type: type.object, label: translate("Additional metadata"), @@ -119,12 +119,15 @@ export const SubscriptionMetadataModal = ( label: translate("sub.meta.modal.admin.custom.name.label"), help: translate("sub.meta.modal.admin.custom.name.help"), }, - validUntil: { + }; + + if (!props.subscription?.parent) { + schema["validUntil"] = { type: type.date, label: translate("sub.meta.modal.valid.until.label"), help: translate("sub.meta.modal.valid.until.help"), - }, - }; + }; + } const mandatoryMetadataSchema = (plan?: IUsagePlan) => ({ metadata: { @@ -218,7 +221,6 @@ export const SubscriptionMetadataModal = ( }, [[], []] ); - const value = { metadata: Object.fromEntries(maybeMetadata), customMetadata: Object.fromEntries(maybeCustomMetadata), diff --git a/daikoku/javascript/src/services/index.ts b/daikoku/javascript/src/services/index.ts index 16fdd1994..a91666290 100644 --- a/daikoku/javascript/src/services/index.ts +++ b/daikoku/javascript/src/services/index.ts @@ -1620,6 +1620,7 @@ export const graphql = { _id adminCustomName enabled + validUntil api { _id name diff --git a/daikoku/javascript/src/types/api.ts b/daikoku/javascript/src/types/api.ts index 723bd68b5..9c89451f9 100644 --- a/daikoku/javascript/src/types/api.ts +++ b/daikoku/javascript/src/types/api.ts @@ -378,7 +378,7 @@ export interface IBaseSubscription { team: string; api: string; createdAt: string; - validUntil: string; + validUntil?: string; by: string; customName: string | null; enabled: boolean; @@ -443,7 +443,8 @@ export interface ISubscriptionCustomization { customMaxPerDay?: number; customReadOnly?: boolean; adminCustomName?: string; - validUntil: string; + validUntil?: string; + parent : string | null; } export interface ISubscriptionExtended extends ISubscription { From 3dcfb9a12e9d2ecc4183d24bb75b90025da49682 Mon Sep 17 00:00:00 2001 From: HelaKaraa Date: Mon, 16 Sep 2024 13:55:36 +0200 Subject: [PATCH 04/14] feat #728: modify the type of validUntil --- daikoku/app/controllers/ApiController.scala | 2 +- .../OtoroshiSettingsController.scala | 3 ++- daikoku/app/domain/SchemaDefinition.scala | 6 ++---- daikoku/app/domain/apikeyEntities.scala | 13 +++++++++---- daikoku/app/domain/json.scala | 18 ++++++++++++------ daikoku/app/utils/ApiService.scala | 7 +++++-- daikoku/javascript/src/types/api.ts | 6 ++++-- .../daikoku/ConsumptionControllerSpec.scala | 3 ++- 8 files changed, 37 insertions(+), 21 deletions(-) diff --git a/daikoku/app/controllers/ApiController.scala b/daikoku/app/controllers/ApiController.scala index f4a7f0fdf..6405921e6 100644 --- a/daikoku/app/controllers/ApiController.scala +++ b/daikoku/app/controllers/ApiController.scala @@ -1751,7 +1751,7 @@ class ApiController( customMaxPerMonth = (body \ "customMaxPerMonth").asOpt[Long], customReadOnly = (body \ "customReadOnly").asOpt[Boolean], adminCustomName = (body \ "adminCustomName").asOpt[String], - validUntil = (body \ "validUntil").asOpt[String], + validUntil = (body \ "validUntil").asOpt(DateTimeFormat), ) result <- EitherT(apiService.updateSubscription(ctx.tenant, subToSave, plan)) diff --git a/daikoku/app/controllers/OtoroshiSettingsController.scala b/daikoku/app/controllers/OtoroshiSettingsController.scala index e2f3da746..312f76d2d 100644 --- a/daikoku/app/controllers/OtoroshiSettingsController.scala +++ b/daikoku/app/controllers/OtoroshiSettingsController.scala @@ -531,7 +531,8 @@ class OtoroshiSettingsController( .flatMap(_.asOpt[Map[String, String]]) .getOrElse(Map.empty[String, String]), rotation = None, - readOnly = readOnlyOpt.getOrElse(false) + readOnly = readOnlyOpt.getOrElse(false), + validUntil = None, ) } diff --git a/daikoku/app/domain/SchemaDefinition.scala b/daikoku/app/domain/SchemaDefinition.scala index 8c206956b..4fd1c5fe3 100644 --- a/daikoku/app/domain/SchemaDefinition.scala +++ b/daikoku/app/domain/SchemaDefinition.scala @@ -2109,9 +2109,7 @@ object SchemaDefinition { ) ), Field("createdAt", DateTimeUnitype, resolve = _.value.createdAt), - Field("validUntil", - OptionType(StringType), - resolve = _.value.validUntil), + Field("validUntil", OptionType(DateTimeUnitype), resolve = _.value.validUntil), Field( "team", OptionType(TeamObjectType), @@ -3569,7 +3567,7 @@ object SchemaDefinition { ), ReplaceField( "validUntil", - Field("validUntil", DateTimeUnitype, resolve = _.value.validUntil) + Field("validUntil", OptionType(DateTimeUnitype), resolve = _.value.validUntil) ) ) lazy val TranslationType = diff --git a/daikoku/app/domain/apikeyEntities.scala b/daikoku/app/domain/apikeyEntities.scala index 844396cd3..a6417040f 100644 --- a/daikoku/app/domain/apikeyEntities.scala +++ b/daikoku/app/domain/apikeyEntities.scala @@ -25,7 +25,8 @@ case class ApikeyCustomization( metadata: JsObject = play.api.libs.json.Json.obj(), customMetadata: Seq[CustomMetadata] = Seq.empty, tags: JsArray = play.api.libs.json.Json.arr(), - restrictions: ApiKeyRestrictions = ApiKeyRestrictions() + restrictions: ApiKeyRestrictions = ApiKeyRestrictions(), + validUntil: Option[DateTime] = None, ) extends CanJson[ApikeyCustomization] { def asJson: JsValue = json.ApikeyCustomizationFormat.writes(this) } @@ -59,7 +60,7 @@ case class ApiSubscription( apiKey: OtoroshiApiKey, // TODO: add the actual plan at the time of the subscription plan: UsagePlanId, createdAt: DateTime, - validUntil: Option[String] = None, + validUntil: Option[DateTime] = None, team: TeamId, api: ApiId, by: UserId, @@ -109,7 +110,10 @@ case class ApiSubscription( "team" -> json.TeamIdFormat.writes(team), "api" -> json.ApiIdFormat.writes(api), "createdAt" -> json.DateTimeFormat.writes(createdAt), - "validUntil" -> validUntil.map(JsString).getOrElse(JsNull).as[JsValue], + "validUntil" -> validUntil + .map(json.DateTimeFormat.writes) + .getOrElse(JsNull) + .as[JsValue], "customName" -> customName .map(id => JsString(id)) .getOrElse(JsNull) @@ -137,7 +141,8 @@ case class ActualOtoroshiApiKey( tags: Set[String] = Set.empty[String], metadata: Map[String, String] = Map.empty[String, String], restrictions: ApiKeyRestrictions = ApiKeyRestrictions(), - rotation: Option[ApiKeyRotation] + rotation: Option[ApiKeyRotation], + validUntil : Option[Long] = None ) extends CanJson[OtoroshiApiKey] { override def asJson: JsValue = json.ActualOtoroshiApiKeyFormat.writes(this) def asOtoroshiApiKey: OtoroshiApiKey = diff --git a/daikoku/app/domain/json.scala b/daikoku/app/domain/json.scala index 5872278a1..66c456ba2 100644 --- a/daikoku/app/domain/json.scala +++ b/daikoku/app/domain/json.scala @@ -1662,7 +1662,8 @@ object json { .asOpt(SeqCustomMetadataFormat) .getOrElse(Seq.empty), tags = (json \ "tags").asOpt[JsArray].getOrElse(Json.arr()), - restrictions = (json \ "restrictions").as(ApiKeyRestrictionsFormat) + restrictions = (json \ "restrictions").as(ApiKeyRestrictionsFormat), + validUntil = (json \ "validUntil").asOpt(DateTimeFormat) ) ) } recover { @@ -1679,7 +1680,9 @@ object json { o.customMetadata.map(CustomMetadataFormat.writes) ), "tags" -> o.tags, - "restrictions" -> o.restrictions.asJson + "restrictions" -> o.restrictions.asJson, + "validUntil" -> o.validUntil.map(DateTimeFormat.writes) + .getOrElse(JsNull).as[JsValue] ) } val ApiKeyRestrictionsFormat = new Format[ApiKeyRestrictions] { @@ -2731,7 +2734,7 @@ object json { team = (json \ "team").as(TeamIdFormat), api = (json \ "api").as(ApiIdFormat), createdAt = (json \ "createdAt").as(DateTimeFormat), - validUntil = (json \ "validUntil").asOpt[String], + validUntil = (json \ "validUntil").asOpt(DateTimeFormat), by = (json \ "by").as(UserIdFormat), customName = (json \ "customName").asOpt[String], adminCustomName = (json \ "adminCustomName").asOpt[String], @@ -2781,7 +2784,7 @@ object json { "api" -> ApiIdFormat.writes(o.api), "createdAt" -> DateTimeFormat.writes(o.createdAt), "validUntil"-> o.validUntil - .map(id => JsString(id)) + .map(DateTimeFormat.writes) .getOrElse(JsNull) .as[JsValue], "by" -> UserIdFormat.writes(o.by), @@ -3130,7 +3133,9 @@ object json { "rotation" -> apk.rotation .map(ApiKeyRotationFormat.writes) .getOrElse(JsNull) - .as[JsValue] + .as[JsValue], + "validUntil" -> apk.validUntil + ) override def reads(json: JsValue): JsResult[ActualOtoroshiApiKey] = @@ -3165,7 +3170,8 @@ object json { .asOpt[Set[String]] .getOrElse(Set.empty[String]), restrictions = (json \ "restrictions").as(ApiKeyRestrictionsFormat), - rotation = (json \ "rotation").asOpt(ApiKeyRotationFormat) + rotation = (json \ "rotation").asOpt(ApiKeyRotationFormat), + validUntil = (json \ "validUntil").asOpt[Long] ) } map { case sd => JsSuccess(sd) diff --git a/daikoku/app/utils/ApiService.scala b/daikoku/app/utils/ApiService.scala index 21b42b2cb..f438c81d6 100644 --- a/daikoku/app/utils/ApiService.scala +++ b/daikoku/app/utils/ApiService.scala @@ -162,7 +162,9 @@ class ApiService( "daikoku__tags" -> processedTags.mkString(" | ") ) ++ processedMetadata, rotation = - plan.autoRotation.map(enabled => ApiKeyRotation(enabled = enabled)) + plan.autoRotation.map(enabled => ApiKeyRotation(enabled = enabled)), + validUntil = plan.otoroshiTarget.flatMap(_.apikeyCustomization + .validUntil.map(_.getMillis)) ) plan match { @@ -761,7 +763,8 @@ class ApiService( metadata = apiKey.metadata ++ subscription.customMetadata .flatMap(_.asOpt[Map[String, String]]) .getOrElse(Map.empty[String, String]), - readOnly = subscription.customReadOnly.getOrElse(apiKey.readOnly) + readOnly = subscription.customReadOnly.getOrElse(apiKey.readOnly), + validUntil = subscription.validUntil.map(_.getMillis) ) )(otoSettings) ) diff --git a/daikoku/javascript/src/types/api.ts b/daikoku/javascript/src/types/api.ts index 9c89451f9..25f8de190 100644 --- a/daikoku/javascript/src/types/api.ts +++ b/daikoku/javascript/src/types/api.ts @@ -305,6 +305,7 @@ interface IOtoroshiTarget { forbidden: Array; notFound: Array; }; + validUntil? : string; }; } @@ -391,7 +392,7 @@ export interface IBaseSubscription { customMaxPerDay?: number; customReadOnly?: boolean; adminCustomName?: string; - parent: string | null; + parent? : object; parentUp: boolean; } @@ -444,9 +445,10 @@ export interface ISubscriptionCustomization { customReadOnly?: boolean; adminCustomName?: string; validUntil?: string; - parent : string | null; + parent? : object; } + export interface ISubscriptionExtended extends ISubscription { parentUp: boolean; planType: string; diff --git a/daikoku/test/daikoku/ConsumptionControllerSpec.scala b/daikoku/test/daikoku/ConsumptionControllerSpec.scala index b7cdca243..e24ed8cef 100644 --- a/daikoku/test/daikoku/ConsumptionControllerSpec.scala +++ b/daikoku/test/daikoku/ConsumptionControllerSpec.scala @@ -245,7 +245,8 @@ class ConsumptionControllerSpec() tags = Set.empty[String], restrictions = ApiKeyRestrictions(), metadata = Map(), - rotation = None + rotation = None, + validUntil = payperUserSub.validUntil.map(_.getMillis) ) wireMockServer.isRunning mustBe true From 543a792235bc2a2cd560e31712ce10774bb1c9ba Mon Sep 17 00:00:00 2001 From: Quentin AUBERT Date: Mon, 16 Sep 2024 14:55:51 +0200 Subject: [PATCH 05/14] add test --- daikoku/app/domain/SchemaDefinition.scala | 8 ++ daikoku/test/daikoku/ApiControllerSpec.scala | 116 +++++++++++++++++++ 2 files changed, 124 insertions(+) diff --git a/daikoku/app/domain/SchemaDefinition.scala b/daikoku/app/domain/SchemaDefinition.scala index 4fd1c5fe3..0064193ac 100644 --- a/daikoku/app/domain/SchemaDefinition.scala +++ b/daikoku/app/domain/SchemaDefinition.scala @@ -480,6 +480,14 @@ object SchemaDefinition { ApiKeyRestrictionsType, resolve = _.value.restrictions ) + ), + ReplaceField( + "validUntil", + Field( + "validUntil", + OptionType(DateTimeUnitype), + resolve = _.value.validUntil + ) ) ) diff --git a/daikoku/test/daikoku/ApiControllerSpec.scala b/daikoku/test/daikoku/ApiControllerSpec.scala index fcb3250df..a1fd302f8 100644 --- a/daikoku/test/daikoku/ApiControllerSpec.scala +++ b/daikoku/test/daikoku/ApiControllerSpec.scala @@ -1712,6 +1712,122 @@ class ApiControllerSpec() respOrg.status mustBe 200 } + + "setup validUntil date for a subscription to his api" in { + val parentPlan = FreeWithoutQuotas( + id = UsagePlanId("parent.dev"), + 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)) + ) + ) + ) + ), + 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(UsagePlanId("parent.dev")), + defaultUsagePlan = UsagePlanId("parent.dev").some + ) + + val parentSub = ApiSubscription( + id = ApiSubscriptionId("parent_sub"), + tenant = tenant.id, + apiKey = parentApiKey, + plan = parentPlan.id, + createdAt = DateTime.now(), + team = teamConsumerId, + api = parentApi.id, + by = userTeamAdminId, + customName = None, + rotation = None, + integrationToken = "parent_token" + ) + + setupEnvBlocking( + tenants = Seq( + tenant.copy( + aggregationApiKeysSecurity = Some(true), + 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 + ) + ) + ) + ), + users = Seq(userAdmin), + teams = Seq(teamOwner, teamConsumer, defaultAdminTeam), + usagePlans = Seq(parentPlan, adminApiPlan), + apis = Seq(parentApi, adminApi), + subscriptions = Seq(parentSub)) + + val session = loginWithBlocking(userAdmin, tenant) + //check validnuntil dans oto + val respPreOto = httpJsonCallBlocking( + path = s"/api/apikeys/${parentSub.apiKey.clientId}", + baseUrl = "http://otoroshi-api.oto.tools", + headers = Map( + "Otoroshi-Client-Id" -> otoroshiAdminApiKey.clientId, + "Otoroshi-Client-Secret" -> otoroshiAdminApiKey.clientSecret, + "Host" -> "otoroshi-api.oto.tools" + ), + port = container.mappedPort(8080) + )(tenant, session) + + (respPreOto.json \ "validUntil").asOpt[Boolean] mustBe None + + + + //update subscription + val validUntil = DateTime.now().plusHours(1) + val respUpdate = httpJsonCallBlocking( + path = s"/api/teams/${teamOwnerId.value}/subscriptions/${parentSub.id.value}", + method = "PUT", + body = Some( + parentSub + .copy( + validUntil = validUntil.some + ) + .asSafeJson + ) + )(tenant, session) + respUpdate.status mustBe 200 + + //check validUntil dans oto + val respUpdateOto = httpJsonCallBlocking( + path = s"/api/apikeys/${parentSub.apiKey.clientId}", + baseUrl = "http://otoroshi-api.oto.tools", + headers = Map( + "Otoroshi-Client-Id" -> otoroshiAdminApiKey.clientId, + "Otoroshi-Client-Secret" -> otoroshiAdminApiKey.clientSecret, + "Host" -> "otoroshi-api.oto.tools" + ), + port = container.mappedPort(8080) + )(tenant, session) + (respUpdateOto.json \ "validUntil").asOpt[Long] mustBe validUntil.getMillis + ) + } } "a api editor" can { From c48de171c584d0fea8e305a5849e7a4736c03ba0 Mon Sep 17 00:00:00 2001 From: Quentin AUBERT Date: Mon, 16 Sep 2024 15:08:59 +0200 Subject: [PATCH 06/14] FIX test --- daikoku/test/daikoku/ApiControllerSpec.scala | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/daikoku/test/daikoku/ApiControllerSpec.scala b/daikoku/test/daikoku/ApiControllerSpec.scala index f241daa1d..b5a110c51 100644 --- a/daikoku/test/daikoku/ApiControllerSpec.scala +++ b/daikoku/test/daikoku/ApiControllerSpec.scala @@ -3505,8 +3505,7 @@ class ApiControllerSpec() ), port = container.mappedPort(8080) )(tenant, session) - (respUpdateOto.json \ "validUntil").asOpt[Long] mustBe validUntil.getMillis - ) + (respUpdateOto.json \ "validUntil").asOpt[Long] mustBe validUntil.getMillis.some } } From b82e063dd22b653bdc843e6db3eb4c1e65cc14b9 Mon Sep 17 00:00:00 2001 From: HelaKaraa Date: Mon, 16 Sep 2024 15:32:08 +0200 Subject: [PATCH 07/14] feat #728: Change validUntil in front to number and remove the badge of validationDate --- .../backoffice/apis/TeamApiSubscriptions.tsx | 23 ++----------------- .../modals/SubscriptionMetadataModal.tsx | 4 ++-- .../src/locales/en/translation.json | 1 - .../src/locales/fr/translation.json | 1 - daikoku/javascript/src/types/api.ts | 4 ++-- 5 files changed, 6 insertions(+), 27 deletions(-) diff --git a/daikoku/javascript/src/components/backoffice/apis/TeamApiSubscriptions.tsx b/daikoku/javascript/src/components/backoffice/apis/TeamApiSubscriptions.tsx index 3f866b1b6..626429de5 100644 --- a/daikoku/javascript/src/components/backoffice/apis/TeamApiSubscriptions.tsx +++ b/daikoku/javascript/src/components/backoffice/apis/TeamApiSubscriptions.tsx @@ -59,7 +59,7 @@ interface IApiSubscriptionGql extends ISubscriptionCustomization { type: string; }; createdAt: string; - validUntil: string; + validUntil: number; api: { _id: string; }; @@ -77,7 +77,7 @@ interface IApiSubscriptionGql extends ISubscriptionCustomization { _id: string; adminCustomName: string; enabled: boolean; - validUntil: string; + validUntil: number; api: { _id: string; name: string; @@ -224,10 +224,6 @@ export const TeamApiSubscriptions = ({ sortingFn: "basic", cell: (info) => { const sub = info.row.original; - let titleDate = `
    - ${translate("validationDate.apikey.badge.title")} : - ${sub.validUntil ? formatDate(sub.validUntil, language) : "N/A"} -
    `; if (sub.parent) { const title = `
    ${translate("aggregated.apikey.badge.title")} @@ -237,33 +233,18 @@ export const TeamApiSubscriptions = ({
  • ${translate("aggregated.apikey.badge.apikey.name")}: ${sub.parent.adminCustomName}
  • `; - titleDate = `
    - ${translate("validationDate.apikey.badge.title")} : - ${sub.parent.validUntil ? formatDate(sub.parent.validUntil, language) : "N/A"} -
    `; return (
    {info.getValue()} -
    - -
    V
    -
    -
    A
    -
    ); } return ( -
    {info.getValue()} - -
    V
    -
    -
    ); }, } diff --git a/daikoku/javascript/src/contexts/modals/SubscriptionMetadataModal.tsx b/daikoku/javascript/src/contexts/modals/SubscriptionMetadataModal.tsx index 08ca52573..df50cd4fe 100644 --- a/daikoku/javascript/src/contexts/modals/SubscriptionMetadataModal.tsx +++ b/daikoku/javascript/src/contexts/modals/SubscriptionMetadataModal.tsx @@ -27,7 +27,7 @@ export type CustomSubscriptionData = { customMaxPerMonth: number; customReadOnly: boolean; adminCustomName: string; - validUntil: Date; + validUntil: number; }; export const SubscriptionMetadataModal = ( @@ -66,7 +66,7 @@ export const SubscriptionMetadataModal = ( customMaxPerMonth: formData.customQuotas.customMaxPerMonth, customReadOnly: formData.customReadOnly, adminCustomName: formData.adminCustomName, - validUntil: formData.validUntil, + validUntil: Date.parse(formData.validUntil), }; const res = props.save(subProps); diff --git a/daikoku/javascript/src/locales/en/translation.json b/daikoku/javascript/src/locales/en/translation.json index 38dd7294f..3f0a790e3 100644 --- a/daikoku/javascript/src/locales/en/translation.json +++ b/daikoku/javascript/src/locales/en/translation.json @@ -1390,7 +1390,6 @@ "aggregated.apikey.badge.apikey.name": "API key custom name", "apisubscription.lastUsage.label": "Last usage", "N/A": "N/A", - "validationDate.apikey.badge.title": "Valid Until", "semver.error.message": "Can't create a version with special characters : %s", "version.creation.success.message": "New version of API created successfully", "error.message.creation.security.enabled": "You're not authorized to create an API, please contact your administrator.", diff --git a/daikoku/javascript/src/locales/fr/translation.json b/daikoku/javascript/src/locales/fr/translation.json index 15155aefd..78b600e78 100644 --- a/daikoku/javascript/src/locales/fr/translation.json +++ b/daikoku/javascript/src/locales/fr/translation.json @@ -1390,7 +1390,6 @@ "aggregated.apikey.badge.apikey.name": "Nom personnalisée de la clé", "apisubscription.lastUsage.label": "Dernier usage", "N/A": "N/A", - "validationDate.apikey.badge.title": "Date de validation", "semver.error.message": "Une version ne peut pas être créée avec des caractères spéciaux : %s", "version.creation.success.message": "La nouvelle version de l'API a été créée avec succès", "error.message.creation.security.enabled": "Vous n'êtes pas autorisé à créer d'API, merci de contacter votre administrateur.", diff --git a/daikoku/javascript/src/types/api.ts b/daikoku/javascript/src/types/api.ts index 25f8de190..fb468336b 100644 --- a/daikoku/javascript/src/types/api.ts +++ b/daikoku/javascript/src/types/api.ts @@ -379,7 +379,7 @@ export interface IBaseSubscription { team: string; api: string; createdAt: string; - validUntil?: string; + validUntil?: number; by: string; customName: string | null; enabled: boolean; @@ -444,7 +444,7 @@ export interface ISubscriptionCustomization { customMaxPerDay?: number; customReadOnly?: boolean; adminCustomName?: string; - validUntil?: string; + validUntil?: number; parent? : object; } From 46cdc66ba0260cf3b81971be5b23a484dc0b51a9 Mon Sep 17 00:00:00 2001 From: HelaKaraa Date: Mon, 16 Sep 2024 16:18:07 +0200 Subject: [PATCH 08/14] feat #728: fix playwright --- .../src/contexts/modals/SubscriptionMetadataModal.tsx | 2 +- daikoku/javascript/src/types/api.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/daikoku/javascript/src/contexts/modals/SubscriptionMetadataModal.tsx b/daikoku/javascript/src/contexts/modals/SubscriptionMetadataModal.tsx index df50cd4fe..c62c1a4ef 100644 --- a/daikoku/javascript/src/contexts/modals/SubscriptionMetadataModal.tsx +++ b/daikoku/javascript/src/contexts/modals/SubscriptionMetadataModal.tsx @@ -121,7 +121,7 @@ export const SubscriptionMetadataModal = ( }, }; - if (!props.subscription?.parent) { + if (!props.subscription?.["parent"]) { schema["validUntil"] = { type: type.date, label: translate("sub.meta.modal.valid.until.label"), diff --git a/daikoku/javascript/src/types/api.ts b/daikoku/javascript/src/types/api.ts index fb468336b..729f8bbfc 100644 --- a/daikoku/javascript/src/types/api.ts +++ b/daikoku/javascript/src/types/api.ts @@ -392,7 +392,7 @@ export interface IBaseSubscription { customMaxPerDay?: number; customReadOnly?: boolean; adminCustomName?: string; - parent? : object; + parent: string | null; parentUp: boolean; } @@ -445,7 +445,6 @@ export interface ISubscriptionCustomization { customReadOnly?: boolean; adminCustomName?: string; validUntil?: number; - parent? : object; } From 5f9eafedc8af7f4221e7b3465a8b5207c7bd23ca Mon Sep 17 00:00:00 2001 From: Quentin AUBERT Date: Tue, 17 Sep 2024 14:50:35 +0200 Subject: [PATCH 09/14] FIX import plan from root version --- daikoku/app/controllers/ApiController.scala | 29 +++-- .../backoffice/apikeys/TeamApiKeysForApi.tsx | 111 ++++++++++-------- .../components/frontend/api/ApiPricing.tsx | 98 ++++++++++++++-- .../src/contexts/modals/ApiSelectModal.tsx | 21 ++-- .../src/locales/en/translation.json | 11 +- .../src/locales/fr/translation.json | 13 +- daikoku/javascript/src/types/api.ts | 2 + 7 files changed, 200 insertions(+), 85 deletions(-) diff --git a/daikoku/app/controllers/ApiController.scala b/daikoku/app/controllers/ApiController.scala index ca2bd2b57..f9ff5f3a2 100644 --- a/daikoku/app/controllers/ApiController.scala +++ b/daikoku/app/controllers/ApiController.scala @@ -1783,6 +1783,7 @@ class ApiController( def subscriptionToJson( api: Api, + apiTeam: Team, plan: UsagePlan, sub: ApiSubscription, parentSub: Option[ApiSubscription] @@ -1799,8 +1800,9 @@ class ApiController( Json.obj("planName" -> name) ++ Json.obj("apiName" -> api.name) ++ Json.obj("_humanReadableId" -> api.humanReadableId) ++ - Json.obj("parentUp" -> false) - + Json.obj("parentUp" -> false) ++ + Json.obj("apiLink" -> s"/${apiTeam.humanReadableId}/${api.humanReadableId}/${api.currentVersion.value}/description") ++ + Json.obj("planLink" -> s"/${apiTeam.humanReadableId}/${api.humanReadableId}/${api.currentVersion.value}/pricing") sub.parent match { case None => FastFuture.successful(r) case Some(parentId) => @@ -1873,14 +1875,21 @@ class ApiController( case None => FastFuture.successful(Json.obj()) //FIXME case Some(plan) => - subscriptionToJson( - api = api, - plan = plan, - sub = sub, - parentSub = sub.parent.flatMap(p => - subscriptions.find(s => s.id == p) - ) - ) + env.dataStore.teamRepo.forTenant(ctx.tenant) + .findByIdNotDeleted(api.team) + .flatMap { + case Some(team) => subscriptionToJson( + api = api, + apiTeam = team, + plan = plan, + sub = sub, + parentSub = sub.parent.flatMap(p => + subscriptions.find(s => s.id == p) + ) + ) + case None => FastFuture.successful(Json.obj()) //FIXME + } + } case None => FastFuture.successful(Json.obj()) diff --git a/daikoku/javascript/src/components/backoffice/apikeys/TeamApiKeysForApi.tsx b/daikoku/javascript/src/components/backoffice/apikeys/TeamApiKeysForApi.tsx index e7c69fa2a..a5437e8e3 100644 --- a/daikoku/javascript/src/components/backoffice/apikeys/TeamApiKeysForApi.tsx +++ b/daikoku/javascript/src/components/backoffice/apikeys/TeamApiKeysForApi.tsx @@ -495,12 +495,12 @@ type ApiKeyCardProps = { graceperiod: number ) => Promise; regenerateSecret: () => void; - currentTeam: ITeamSimple; + currentTeam?: ITeamSimple; subscribedApis: Array; transferKey: () => void; }; -const ApiKeyCard = ({ +export const ApiKeyCard = ({ api, subscription, updateCustomName, @@ -591,6 +591,7 @@ const ApiKeyCard = ({ planQuery.data.customName || planQuery.data.type + console.debug({ subscription }) return (
    @@ -598,14 +599,16 @@ const ApiKeyCard = ({ {subscription.children.length === 0 && } {subscription.children.length > 0 && - } + }
    - {subscription.enabled ? translate("subscription.enable.label") : translate("subscription.disable.label")} + + {subscription.enabled ? translate("subscription.enable.label") : translate("subscription.disable.label")} +
    {_customName}
    {`${subscription.apiKey.clientId}:${subscription.apiKey.clientSecret}`}
    - - - + + + + + + + + +
    { - translate({ + translate("subscription.for")} + {subscription.apiName}/{subscription.planName} + {translate({ key: 'subscription.create.at', replacements: [moment(subscription.createdAt).format(translate('moment.date.format.without.hours'))] }) - }
    + }
    @@ -685,7 +696,7 @@ const ApiKeyCard = ({ aria-expanded="false" id="dropdownMenuButton" /> -
    +
    openFormModal({ @@ -730,7 +741,7 @@ const ApiKeyCard = ({ return (
    {`${aggregate.apiName}/${aggregate.planName || aggregate.planType}`} diff --git a/daikoku/javascript/src/components/frontend/api/ApiPricing.tsx b/daikoku/javascript/src/components/frontend/api/ApiPricing.tsx index 40fc51a32..208a59d04 100644 --- a/daikoku/javascript/src/components/frontend/api/ApiPricing.tsx +++ b/daikoku/javascript/src/components/frontend/api/ApiPricing.tsx @@ -37,6 +37,7 @@ import { formatPlanType } from '../../utils/formatters'; import { ApiDocumentation } from './ApiDocumentation'; import { ApiRedoc } from './ApiRedoc'; import { ApiSwagger } from './ApiSwagger'; +import { ApiKeyCard } from '../../backoffice/apikeys/TeamApiKeysForApi'; export const currency = (plan?: IBaseUsagePlan) => { if (!plan) { @@ -69,6 +70,7 @@ const ApiPricingCard = (props: ApiPricingCardProps) => { openApiKeySelectModal, openCustomModal, close, + openRightPanel } = useContext(ModalContext); const { client } = useContext(getApolloContext()); @@ -227,6 +229,51 @@ const ApiPricingCard = (props: ApiPricingCardProps) => { }) } +// const displaySubscription = () => { +// Services.getMySubscriptions(props.api._id, props.api.currentVersion) +// .then(r => { +// openRightPanel({ +// title: "test", +// content:
    +// {r.subscriptions.map(subscription => { +// return ( +// Promise.resolve()} +// toggle={console.debug} +// makeUniqueApiKey={console.debug} +// deleteApiKey={console.debug} +// toggleRotation={( +// plan, +// enabled, +// rotationEvery, +// gracePeriod +// ) => +// Promise.resolve() +// } +// regenerateSecret={console.debug} +// transferKey={console.debug} +// /> +// ) +// })} +//
    +// }) +// }) +// } + return (
    { className="card-img-top card-link card-header" data-holder-rendered="true" > + {/*
    +