+
+ {subscription.children.length === 0 &&
}
+ {subscription.children.length > 0 &&
}
+
+ {subscription.enabled ? translate("subscription.enable.label") : translate("subscription.disable.label")}
+
-
- {!settingMode &&
- (!editMode ? (
-
-
- {customName}
-
-
-
- ) : (
-
- ))}
- {settingMode ? (
-
-
- ApiKey rotation
-
-
- ) : (
-
- Type : {formatPlanType(plan, translate)}
-
- )}
+
+
+
+
{_customName}
+
{`${subscription.apiKey.clientId}:${subscription.apiKey.clientSecret}`}
+
+
+
+
+
{
+ translate({
+ key: 'subscription.create.at', replacements: [moment(subscription.createdAt).format(translate('moment.date.format.without.hours'))]
+ })
+ }
-
- {!settingMode && (
-
-
-
- {!subscription.parent && (
-
-
-
- )}
-
-
-
-
-
-
-
-
-
-
- {!subscription.parent && !disableRotation && (
-
-
-
- )}
-
-
-
-
-
-
- {subscription.parent && (
-
-
-
- )}
-
-
- {subscription.apiKey && (
-
-
- -
- setActiveTab('apikey')}
- >
- ApiKey
-
-
- {!disableRotation && (
- -
- setActiveTab('token')}
- >
-
- Integration token
-
-
-
- )}
- -
- setActiveTab('basicAuth')}
- >
-
- Basic auth
-
-
-
-
-
- )}
- {activeTab == 'apikey' && (
- <>
-
-
-
-
-
- {
- if (subscription.enabled) {
- setHide(!hide);
- }
- }}
- className={classNames('input-group-text', {
- 'cursor-pointer': subscription.enabled,
- 'cursor-forbidden': !subscription.enabled,
- })}
- id={`client-secret-addon-${_id}`}
- >
- {hide ? (
-
- ) : (
-
- )}
-
-
-
- >
- )}
- {activeTab == 'token' && (
- <>
-
-
-
-
-
-
- >
- )}
- {activeTab == 'basicAuth' && (
- <>
-
-
-
-
-
-
- >
- )}
-
- {subscription.children.length > 0 && (
- <>
- {showAggregatePlan && (
-
-
- {translate('team_apikey_aggregatePlans_title')}
-
-
- {subscription.children.map((aggregate) => {
- const api = subscribedApis.find(
- (a) => a._id === aggregate.api
- );
- return (
-
-
- {`${aggregate.apiName}/${aggregate.planName || aggregate.planType}`}
-
-
- );
- })}
-
-
- )}
-
- >
- )}
-
- )}
-
- {settingMode && (
-
- {!plan.autoRotation && (
-
+
+ {translate("subscription.nota.part.1")}
+ {translate("subscription.nota.link.api")}
+ {translate("subscription.nota.part.2")}
+ {translate("subscription.nota.link.statistics")}
+
+
+
+
+
openFormModal({
+ title: translate("subscription.custom.name.update.label"),
+ actionLabel: translate('Save'),
+ schema: {
+ customName: {
+ type: type.string,
+ placeholder: translate('subscription.custom.name.update.placeholder'),
+ label: translate('subscription.custom.name.update.message'),
+ }
+ },
+ onSubmit: (data) => {
+ updateCustomName(data.customName ?? '')
+ },
+ value: { customName: subscription.customName }
+ })}
+ >
+ {translate("subscription.custom.name.update.label")}
+
+ {!subscription.parent && !disableRotation &&
openFormModal({
+ title: translate("ApiKey rotation"),
+ actionLabel: translate('Save'),
+ schema: settingsSchema,
+ onSubmit: (data) => handleChanges(data),
+ value: subscription.rotation
+ })}
+ >
+ {translate("subscription.rotation.update.label")}
+ }
+ {subscription.children.length > 0 &&
openRightPanel({
+ title: translate('team_apikey_aggregatePlans_title'), content:
+
+ {subscription.children.map((aggregate) => {
+ const api = subscribedApis.find(
+ (a) => a._id === aggregate.api
+ );
return (
-
-
-
+ {`${aggregate.apiName}/${aggregate.planName || aggregate.planType}`}
+
);
- }}
- />
- )}
-
- )}
+ })}
+
+
+ })}
+ >
+ {translate("subscription.show.aggregate.label")}
+ }
+
+ {!subscription.parent &&
+ {translate("subscription.reset.secret.label")}
+ }
+ {!subscription.parent &&
+ {translate("subscription.transfer.label")}
+ }
+
{
+ // if (subscription.parent && subscription.parentUp) {
+ toggle()
+ // }
+ }}
+ >
+ {subscription.enabled ? translate("subscription.disable.button.label") : translate("subscription.enable.button.label")}
+
+
+ {subscription.parent &&
+ {translate("subscription.extract.button.label")}
+ }
+
+ {translate("subscription.delete.button.label")}
+
- );
+ )
} else {
return
Error while fetching usage plan
;
}
diff --git a/daikoku/javascript/src/components/backoffice/apis/TeamApiInfo.tsx b/daikoku/javascript/src/components/backoffice/apis/TeamApiInfo.tsx
index ad3cfde82..96e47a309 100644
--- a/daikoku/javascript/src/components/backoffice/apis/TeamApiInfo.tsx
+++ b/daikoku/javascript/src/components/backoffice/apis/TeamApiInfo.tsx
@@ -233,13 +233,13 @@ export const teamApiInfoForm = (translate: any, team: ITeamSimple, tenant: ITena
name: {
type: type.string,
disabled: true,
- props: { label: translate('Name'), placeholder: 'New Api' },
+ label: translate('Name'),
},
smallDescription: {
type: type.string,
format: format.text,
disabled: true,
- props: { label: translate('Small desc.') },
+ label: translate('Small desc.'),
},
};
diff --git a/daikoku/javascript/src/components/backoffice/notifications/SimpleNotification.tsx b/daikoku/javascript/src/components/backoffice/notifications/SimpleNotification.tsx
index 4f78214f6..0cb2e8a64 100644
--- a/daikoku/javascript/src/components/backoffice/notifications/SimpleNotification.tsx
+++ b/daikoku/javascript/src/components/backoffice/notifications/SimpleNotification.tsx
@@ -417,9 +417,10 @@ export function SimpleNotification(props: ISimpleNotificationProps) {
};
const fromFormatter = (action: any, sender: any) => {
+ console.debug({props, sender})
switch (action.__typename) {
case 'ApiAccess':
- return `${sender.name}/${Option(props.notification.action.team!.name).getOrNull()}`;
+ return `${sender.name}/${props.notification.action.team?.name ?? translate("Unknown team") }`;
case 'TeamAccess':
case 'NewPostPublished':
case 'NewIssueOpen':
@@ -430,10 +431,9 @@ export function SimpleNotification(props: ISimpleNotificationProps) {
case 'ApiSubscriptionReject':
case 'ApiSubscriptionAccept':
case 'TeamInvitation':
- return props.notification.action.team!.name;
+ return props.notification.action.team?.name ?? translate("Unknown team");
case 'ApiSubscriptionDemand':
- return `${sender.name}/${Option(props.notification.action.team!.name)
- .getOrNull()}`;
+ return `${sender.name}/${props.notification.action.team?.name ?? translate("Unknown team")}`;
case 'OtoroshiSyncSubscriptionError':
return 'Otoroshi verifier job';
case 'OtoroshiSyncApiError':
diff --git a/daikoku/javascript/src/components/backoffice/widgets/teamDemands.tsx b/daikoku/javascript/src/components/backoffice/widgets/teamDemands.tsx
index 84174f91b..0f4284c84 100644
--- a/daikoku/javascript/src/components/backoffice/widgets/teamDemands.tsx
+++ b/daikoku/javascript/src/components/backoffice/widgets/teamDemands.tsx
@@ -1,6 +1,6 @@
import { getApolloContext, gql } from '@apollo/client';
-import { useQuery } from '@tanstack/react-query';
+import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useContext } from 'react';
import { I18nContext } from '../../../contexts';
@@ -54,6 +54,7 @@ export const LastDemands = (props: LastDemandsProps) => {
const { confirm } = useContext(ModalContext);
const { client } = useContext(getApolloContext());
+ const queryClient = useQueryClient()
const { isLoading, isError, data } = useQuery({
queryKey: ["widget", "widget_team_last_demands"],
queryFn: () => client?.query({
@@ -61,7 +62,7 @@ export const LastDemands = (props: LastDemandsProps) => {
variables: { teamId: props.team._id, offset: 0, limit: 5 }
})
})
- const isAdmin = !!props.team.users.find(u => u.userId === connectedUser._id && u.teamPermission === 'Administrator')
+ const isAdmin = !!props.team.users.find(u => u.userId === connectedUser._id && u.teamPermission === 'Administrator') || connectedUser.isDaikokuAdmin
const handleCheckout = (demandId: string) => {
return Services.rerunProcess(props.team._id, demandId)
@@ -76,6 +77,7 @@ export const LastDemands = (props: LastDemandsProps) => {
message: translate('demand.delete.modal.message')
})
.then(() => Services.cancelProcess(props.team._id, demandId))
+ .then(() => queryClient.invalidateQueries({ queryKey: ["widget", "widget_team_last_demands"] }));
}
return (
diff --git a/daikoku/javascript/src/components/frontend/SubscriptionRetrieve.tsx b/daikoku/javascript/src/components/frontend/SubscriptionRetrieve.tsx
new file mode 100644
index 000000000..9ccbae6e6
--- /dev/null
+++ b/daikoku/javascript/src/components/frontend/SubscriptionRetrieve.tsx
@@ -0,0 +1,70 @@
+import { useContext, useEffect } from 'react';
+import { I18nContext, ModalContext } from '../../contexts';
+import { useQuery, useQueryClient } from '@tanstack/react-query';
+import * as Services from "../../services";
+import { IApi, isError, ISubscription, ITeamSimple, IUsagePlan } from '../../types';
+import { GlobalContext } from '../../contexts/globalContext';
+import { useNavigate, useParams } from 'react-router-dom';
+
+export const SubscriptionRetrieve = () => {
+ const { connectedUser, tenant } = useContext(GlobalContext);
+ const { translate } = useContext(I18nContext);
+ const navigate = useNavigate();
+
+ const params = new URLSearchParams(window.location.search);
+ const token = params.get('token') ?? ""
+
+ const { openTeamSelectorModal, confirm, openLoginOrRegisterModal, alert } = useContext(ModalContext);
+ const checkRequest = useQuery({
+ queryKey: ["check"],
+ queryFn: () => Services.checkTransferlink(token)
+ })
+ const teamRequest = useQuery({
+ queryKey: ["teams"],
+ queryFn: () => Services.myTeams(),
+ enabled: !connectedUser.isGuest && checkRequest.data && !isError(checkRequest.data)
+ })
+
+
+ useEffect(() => {
+ if (connectedUser.isGuest) {
+ openLoginOrRegisterModal({
+ tenant,
+ title: translate("login.required"),
+ message: translate("subscription.retrieve.login.required.message")
+ })
+ } else if (checkRequest.data && isError(checkRequest.data)) {
+ alert({
+ message: translate("subscription.retrieve.token.unavalaible")
+ })
+ } else if (checkRequest.data && !isError(checkRequest.data) && teamRequest.data && !isError(teamRequest.data)) {
+ const data = (checkRequest.data as { subscription: ISubscription, api: IApi, plan: IUsagePlan, ownerTeam: ITeamSimple })
+ openTeamSelectorModal({
+ title: translate("subscription.retrieve.select.team.modal.title"),
+ description: translate({
+ key: "subscription.retrieve.select.team.modal.message",
+ replacements: [
+ data.ownerTeam.name,
+ `${data.subscription.customName ?? ""} (${data.api.name}/${data.plan.customName ?? data.plan.type})`
+ ]
+ }),
+ teams: teamRequest.data,
+ action: (team) => confirm({
+ title: translate("subscription.retrieve.confirm.modal.title"),
+ message: translate({
+ key: "subscription.retrieve.confirm.modal.message",
+ replacements: [(teamRequest.data as ITeamSimple[]).find(t => t._id === team[0])?.name || ""]
+ }),
+ okLabel: translate("subscription.retrieve.confirm.modal.ok.button.label"),
+ })
+ .then((_) => Services.retrieveSubscription(token, team[0], data.subscription._id)
+ .then(() => navigate("/apis"))
+ ),
+ allowMultipleDemand: false,
+ actionLabel: translate("subscription.retrieve.select.team.button.label")
+ });
+ }
+ }, [teamRequest.data, checkRequest.data]);
+
+ return <>>;
+};
\ No newline at end of file
diff --git a/daikoku/javascript/src/components/frontend/api/ApiCard.tsx b/daikoku/javascript/src/components/frontend/api/ApiCard.tsx
index 1b062b658..c1113962f 100644
--- a/daikoku/javascript/src/components/frontend/api/ApiCard.tsx
+++ b/daikoku/javascript/src/components/frontend/api/ApiCard.tsx
@@ -44,7 +44,7 @@ export const ApiCard = (props: {
) {
return (
auth.pending).map((auth: any) => auth.team)}
acceptedTeams={authorizations
@@ -103,7 +103,7 @@ export const ApiCard = (props: {
{api.smallDescription}
{props.teamVisible && team && (
props.handleTeamSelect(team)}
>
@@ -119,7 +119,7 @@ export const ApiCard = (props: {
return (
-
+
{`${api.name}${props.groupView && props.apiWithAutho.length > 1 ? ` - ${api.currentVersion}` : ''}`}
@@ -181,7 +181,7 @@ export const ApiCard = (props: {
{props.teamVisible && team && (
props.handleTeamSelect(team)}
>
diff --git a/daikoku/javascript/src/components/frontend/api/ApiHome.tsx b/daikoku/javascript/src/components/frontend/api/ApiHome.tsx
index 2eac94cf1..39c4ab501 100644
--- a/daikoku/javascript/src/components/frontend/api/ApiHome.tsx
+++ b/daikoku/javascript/src/components/frontend/api/ApiHome.tsx
@@ -313,7 +313,7 @@ export const ApiHome = ({
>) : (<>
{translate('request_api_access')}
();
const maybeTeams = searchedTeam
- ? teams.filter((team) => team.name.toLowerCase().includes(searchedTeam))
+ ? teams.filter((team) => team.name.toLocaleLowerCase().includes(searchedTeam.toLocaleLowerCase()))
: teams;
return (
diff --git a/daikoku/javascript/src/components/frontend/team/MyHome.tsx b/daikoku/javascript/src/components/frontend/team/MyHome.tsx
index b1dd2201d..6c80bbfc9 100644
--- a/daikoku/javascript/src/components/frontend/team/MyHome.tsx
+++ b/daikoku/javascript/src/components/frontend/team/MyHome.tsx
@@ -94,21 +94,19 @@ export const MyHome = () => {
return (
-
-
-
-
-
-
-
- {tenant.title ? tenant.title : translate('Your APIs center')}
-
-
-
+
+
+
+
+
+
+ {tenant.title ? tenant.title : translate('Your APIs center')}
+
+
diff --git a/daikoku/javascript/src/components/frontend/team/TeamCard.tsx b/daikoku/javascript/src/components/frontend/team/TeamCard.tsx
index 89ac35e48..65c7bdf0b 100644
--- a/daikoku/javascript/src/components/frontend/team/TeamCard.tsx
+++ b/daikoku/javascript/src/components/frontend/team/TeamCard.tsx
@@ -3,56 +3,56 @@ import { Can, read, team as TEAM } from '../../utils';
import { I18nContext } from '../../../contexts';
type Props = {
- user?: any;
- team: any;
- askToJoin?: (...args: any[]) => any;
- redirectToTeamPage?: (...args: any[]) => any;
- redirectToTeamSettings?: (...args: any[]) => any;
+ user?: any;
+ team: any;
+ askToJoin?: (...args: any[]) => any;
+ redirectToTeamPage?: (...args: any[]) => any;
+ redirectToTeamSettings?: (...args: any[]) => any;
};
export function TeamCard(props: Props) {
- const { Translation } = useContext(I18nContext);
+ const { Translation } = useContext(I18nContext);
const { team } = props;
return (
-
-
-
+
+
-
-
-
-
-
+
+
+
+
+
{props.team.name}
-
-
+
-
+
-
+
{team.description}
-
-
+
+
{team.canJoin && !team.alreadyJoin && (
-
diff --git a/daikoku/javascript/src/components/inputs/TableFilters.tsx b/daikoku/javascript/src/components/inputs/TableFilters.tsx
index 0aaf9e9c3..eb96621df 100644
--- a/daikoku/javascript/src/components/inputs/TableFilters.tsx
+++ b/daikoku/javascript/src/components/inputs/TableFilters.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { Sliders } from 'react-feather';
+import Sliders from 'react-feather/dist/icons/sliders';
import { SwitchButton } from './Switch';
diff --git a/daikoku/javascript/src/components/utils/dnd/components/Handle.tsx b/daikoku/javascript/src/components/utils/dnd/components/Handle.tsx
index b0c5427af..43b4ec800 100644
--- a/daikoku/javascript/src/components/utils/dnd/components/Handle.tsx
+++ b/daikoku/javascript/src/components/utils/dnd/components/Handle.tsx
@@ -1,5 +1,5 @@
import React, { forwardRef } from 'react';
-import { Move } from 'react-feather';
+import Move from 'react-feather/dist/icons/move';
import { Action, Props as ActionProps } from './Action';
diff --git a/daikoku/javascript/src/components/utils/dnd/components/Remove.tsx b/daikoku/javascript/src/components/utils/dnd/components/Remove.tsx
index 544bc0abb..7e4e8539c 100644
--- a/daikoku/javascript/src/components/utils/dnd/components/Remove.tsx
+++ b/daikoku/javascript/src/components/utils/dnd/components/Remove.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { Trash2 } from 'react-feather';
+import Trash2 from 'react-feather/dist/icons/trash-2';
import {Action, Props as ActionProps} from './Action';
diff --git a/daikoku/javascript/src/components/utils/sidebar/RightPanel.tsx b/daikoku/javascript/src/components/utils/sidebar/RightPanel.tsx
new file mode 100644
index 000000000..765d72ea5
--- /dev/null
+++ b/daikoku/javascript/src/components/utils/sidebar/RightPanel.tsx
@@ -0,0 +1,58 @@
+import classNames from 'classnames';
+import { useContext, useEffect } from 'react';
+import ArrowRight from 'react-feather/dist/icons/arrow-right';
+
+import { ModalContext } from '../../../contexts';
+
+
+export const RightPanel = () => {
+ const { rightPanelContent, closeRightPanel } = useContext(ModalContext);
+
+
+ useEffect(() => {
+ closeRightPanel();
+ }, [location]);
+
+ const closeOnEsc = (e: any) => {
+ if (e.key == 'Escape' || e.key == 'Esc') {
+ e.preventDefault();
+ closeRightPanel();
+ return false;
+ }
+ };
+ useEffect(() => {
+ window.addEventListener('keydown', closeOnEsc, true);
+
+ return () => {
+ window.removeEventListener('keydown', closeOnEsc, true);
+ };
+ }, []);
+
+ return (
+
+
+
+
+ {rightPanelContent?.title}
+
+
+ {rightPanelContent?.content}
+
+
+
+
+ );
+};
diff --git a/daikoku/javascript/src/components/utils/sidebar/SideBar.tsx b/daikoku/javascript/src/components/utils/sidebar/SideBar.tsx
index 0c608a609..7956d9565 100644
--- a/daikoku/javascript/src/components/utils/sidebar/SideBar.tsx
+++ b/daikoku/javascript/src/components/utils/sidebar/SideBar.tsx
@@ -6,6 +6,7 @@ import MessageSquare from 'react-feather/dist/icons/message-square';
import Plus from 'react-feather/dist/icons/plus';
import Search from 'react-feather/dist/icons/search';
import Zap from 'react-feather/dist/icons/zap';
+import More from 'react-feather/dist/icons/more-vertical';
import { Link, useLocation } from 'react-router-dom';
import RectangleList from 'react-feather/dist/icons/list';
import { I18nContext } from '../../../contexts/i18n-context';
@@ -14,6 +15,7 @@ import * as Services from '../../../services';
import { MessagesContext } from '../../backoffice';
import { Companion } from './companions';
import { AddPanel, DarkModeActivator, GuestPanel, MessagePanel, SearchPanel, SettingsPanel } from './panels';
+import { MorePanel } from './panels/MorePanel';
export const state = {
@@ -60,24 +62,24 @@ export const SideBar = () => {
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
{!connectedUser.isGuest && (
<>
@@ -103,16 +105,15 @@ export const SideBar = () => {
>
)}
{!connectedUser.isGuest && (
-
-
-
+
+ {
+ setPanelState(state.opened);
+ setPanelContent();
+ }} />
+
)}
diff --git a/daikoku/javascript/src/components/utils/sidebar/panels/MessagePanel.tsx b/daikoku/javascript/src/components/utils/sidebar/panels/MessagePanel.tsx
index ff103ec1a..42626eae5 100644
--- a/daikoku/javascript/src/components/utils/sidebar/panels/MessagePanel.tsx
+++ b/daikoku/javascript/src/components/utils/sidebar/panels/MessagePanel.tsx
@@ -1,6 +1,6 @@
import classNames from 'classnames';
import { useContext, useEffect, useState } from 'react';
-import { Send } from 'react-feather';
+import Send from 'react-feather/dist/icons/send';
import { I18nContext } from '../../../../contexts/i18n-context';
import { GlobalContext } from '../../../../contexts/globalContext';
diff --git a/daikoku/javascript/src/components/utils/sidebar/panels/MorePanel.tsx b/daikoku/javascript/src/components/utils/sidebar/panels/MorePanel.tsx
new file mode 100644
index 000000000..1768daf74
--- /dev/null
+++ b/daikoku/javascript/src/components/utils/sidebar/panels/MorePanel.tsx
@@ -0,0 +1,36 @@
+import { useContext } from 'react';
+import { Link } from 'react-router-dom';
+
+import { GlobalContext } from '../../../../contexts/globalContext';
+import { I18nContext } from '../../../../contexts/i18n-context';
+
+export const MorePanel = () => {
+ const { translate } = useContext(I18nContext);
+ const { connectedUser } = useContext(GlobalContext);
+
+ return (
+
+
+
{translate('more.nav.options')}
+
+
+
+
+ {!connectedUser.isGuest && (
+
+ {translate('fastMode.access')}
+
+
+
+
+ )}
+
+
+
+
+ );
+};
diff --git a/daikoku/javascript/src/components/utils/sidebar/panels/SearchPanel.tsx b/daikoku/javascript/src/components/utils/sidebar/panels/SearchPanel.tsx
index 5dfab42f2..1238def50 100644
--- a/daikoku/javascript/src/components/utils/sidebar/panels/SearchPanel.tsx
+++ b/daikoku/javascript/src/components/utils/sidebar/panels/SearchPanel.tsx
@@ -22,31 +22,9 @@ export const SearchPanel = () => {
}, []);
const search = (inputValue: string) => {
- const options = [
- {
- value: 'me',
- label: translate('My profile'),
- type: 'link',
- url: '/me',
- },
- ];
- if (connectedUser?.isDaikokuAdmin)
- options.push({
- value: 'daikoku',
- label: translate('Daikoku settings'),
- type: 'link',
- url: `/settings/tenants`,
- });
-
- const utils = {
- label: 'Daikoku',
- options: options.filter((i) => i.label.toLowerCase().includes(inputValue.toLowerCase())),
- };
-
return Services.search(inputValue)
.then((result) =>
setResults([
- utils,
...result.map((item: any) => ({
...item,
label: translate(item.label)
diff --git a/daikoku/javascript/src/contexts/modalContext.tsx b/daikoku/javascript/src/contexts/modalContext.tsx
index 78f7a09d0..38df13ef8 100644
--- a/daikoku/javascript/src/contexts/modalContext.tsx
+++ b/daikoku/javascript/src/contexts/modalContext.tsx
@@ -26,6 +26,7 @@ import {
IContactModalComponentProps,
IFormModalProps,
ILoginOrRegisterModalProps,
+ IRightPanelProps,
ISaverOrCancelModalProps,
ITeamInvitationModalProps,
PromptModalProps,
@@ -56,13 +57,17 @@ const init: TModalContext = {
openApiSelectModal: () => { },
openApiKeySelectModal: () => { },
openCustomModal: () => {},
- close: () => {}
+ close: () => {},
+ openRightPanel: () => {},
+ closeRightPanel: () => {},
+ rightPanelContent: undefined
+
}
export const ModalContext = React.createContext
(init);
export const ModalProvider = (props: { children: JSX.Element | Array }) => {
- const { open, close, modal, modalContent } = useModal();
+ const { open, close, modal, modalContent, openRightPanel, closeRightPanel, rightPanelContent } = useModal();
const alert = (props: AlertModalProps) => new Promise((success) => {
open(
{props.children}
@@ -181,6 +189,7 @@ const Modal = ({ modal, modalContent }) => {
const useModal = () => {
const [modal, setModal] = useState(false);
const [modalContent, setModalContent] = useState();
+ const [rightPanelContent, setRightPanelContent] = useState()
const open = (content: JSX.Element) => {
setModal(true)
@@ -191,5 +200,14 @@ const useModal = () => {
setModalContent(undefined)
};
- return { modal, modalContent, setModalContent, open, close };
-};
\ No newline at end of file
+ const closeRightPanel = () => {
+ setRightPanelContent(undefined)
+ }
+
+ const openRightPanel = (p: IRightPanelProps) => {
+ console.debug({p})
+ setRightPanelContent(p)
+ }
+
+ return { modal, modalContent, setModalContent, open, close, closeRightPanel, openRightPanel, rightPanelContent };
+}
\ No newline at end of file
diff --git a/daikoku/javascript/src/contexts/modals/CustomModal.tsx b/daikoku/javascript/src/contexts/modals/CustomModal.tsx
index 1a371f9d3..841b01180 100644
--- a/daikoku/javascript/src/contexts/modals/CustomModal.tsx
+++ b/daikoku/javascript/src/contexts/modals/CustomModal.tsx
@@ -25,11 +25,15 @@ import { IBaseModalProps } from './types';
* >
*
*/
+export interface ICustomModalProps extends IModalProps {
+ actions?: (close) => JSX.Element
+}
export const CustomModal = ({
title,
content,
- close
-}: IModalProps & IBaseModalProps) => {
+ close,
+ actions
+}: ICustomModalProps & IBaseModalProps) => {
return (
@@ -38,7 +42,14 @@ export const CustomModal = ({
{title}
close()} />
- {content}
+
+ {content}
+
+ {actions && (
+
+ {actions(close)}
+
+ )}
);
};
diff --git a/daikoku/javascript/src/contexts/modals/LoginOrRegisterModal.tsx b/daikoku/javascript/src/contexts/modals/LoginOrRegisterModal.tsx
index 294b8d22a..02fc39851 100644
--- a/daikoku/javascript/src/contexts/modals/LoginOrRegisterModal.tsx
+++ b/daikoku/javascript/src/contexts/modals/LoginOrRegisterModal.tsx
@@ -13,7 +13,7 @@ export const LoginOrRegisterModal = (props: ILoginOrRegisterModalProps & IBaseMo
{!props.showOnlyMessage && (
-
{translate('consume.apikey')}
+
{props.title ?? translate('consume.apikey')}
- {props.showOnlyMessage ? props.message : translate('get.apikey.requires.login')}
+ {props.message ?? translate('get.apikey.requires.login')}
void;
}
+export type IRightPanelProps = {
+ title: string;
+ content: JSX.Element;
+};
+
export type TModalContext = {
alert: (p: AlertModalProps) => Promise
;
confirm: (p: ConfirmModalProps) => Promise;
@@ -45,8 +51,11 @@ export type TModalContext = {
openAssetSelectorModal: (p: IAssetSelectorModalProps) => void;
openApiKeySelectModal: (p: IApiKeySelectModalProps) => void;
openApiSelectModal: (p: IApiSelectModalProps) => void;
- openCustomModal: (p: IModalProps) => void;
+ openCustomModal: (p: ICustomModalProps) => void;
close: () => void;
+ openRightPanel: (p: IRightPanelProps) => void;
+ closeRightPanel: () => void;
+ rightPanelContent?: IRightPanelProps;
};
export type ConfirmModalProps = {
message: JSX.Element | string | ((ok: () => void, cancel: () => void) => JSX.Element | string);
@@ -215,6 +224,7 @@ export interface ISaverOrCancelModalProps {
export interface ILoginOrRegisterModalProps {
tenant: ITenant;
+ title?: string;
message?: string;
showOnlyMessage?: boolean;
}
diff --git a/daikoku/javascript/src/locales/en/translation.json b/daikoku/javascript/src/locales/en/translation.json
index 5ad2609e4..38dd7294f 100644
--- a/daikoku/javascript/src/locales/en/translation.json
+++ b/daikoku/javascript/src/locales/en/translation.json
@@ -35,7 +35,7 @@
"filter.preview.tag": "tagged",
"filter.preview.team": "produced by",
"clear filter": "clear filter",
- "Search your API...": "Search your API...",
+ "Search your API...": "Search your API",
"Previous": "Previous",
"Next": "Next",
"All": "All",
@@ -608,13 +608,6 @@
"help.apikey.grace.period": "Period during which the new client secret and the old are both active. The rotation period includes this period.",
"constraint.apikey.grace.period": "Grace period must be inferior to rotation period",
"reset.secret.confirm": "Are you sure you want to reset this secret?",
- "View usage statistics": "View usage statistics",
- "View api": "View API",
- "Copy to clipboard": "Copy to clipboard",
- "Setup rotation": "Setup rotation",
- "Enable/Disable": "Enable/Disable",
- "subscription.enable.button.label": "Enable subscription",
- "subscription.disable.button.label": "Disable subscription",
"Integration": "Integration",
"Force apikey auto-rotation": "Force API key auto-rotation",
"ApiKey": "API key",
@@ -755,6 +748,7 @@
"moment.duration.months": "%s m",
"moment.duration.years": "%s y",
"Unknown user": "Unknown user",
+ "Unknown teal": "Unknown team",
"European server": "European server",
"API creation security": "API creation security",
"creation.security.help": "if enabled, only authorized teams will be able to create APIs",
@@ -1276,9 +1270,9 @@
"select.team.placeholder": "Select a team",
"otoroshi.missing.entities": "Missing Otoroshi entities",
"otoroshi.missing.target": "Missing Otoroshi target",
- "apiList.tag.search": "Search a tag",
- "apiList.category.search": "Search a category",
- "apiList.team.search": "Search a team",
+ "apiList.tag.search": "By tag",
+ "apiList.category.search": "By category",
+ "apiList.team.search": "By team",
"team.email.verification.send": "Mail sent to %s for verify this email address.",
"team.email.alreadyVerified": "This mail is already verified",
"team.email.notVerified": "Send a mail",
@@ -1438,10 +1432,53 @@
"apikeys.delete.choice.delete": "Permanently delete the subscription and all its associated children.",
"apikeys.delete.choice.extraction": "Each child will be extracted and become a unique and independent subscription.",
"apikeys.delete.child.label": "Select a subscription to promote",
- "apikeys.delete.modal.title": "Delete a Subscription",
- "apikeys.delete.confirm.modal.title": "Confirm Deletion",
+ "apikeys.delete.modal.title": "Delete a subscription",
+ "apikeys.delete.confirm.modal.title": "Confirm the deletion",
"apikeys.delete.confirm.label": "To confirm the deletion, please enter the exact name of the subscription: %s",
"apikeys.delete.success.message": "The subscription has been successfully deleted",
"apikeys.view.api": "View API",
- "apikeys.view.apikeys": "View APIkeys"
+ "apikeys.view.apikeys": "View APIkeys",
+ "subscription.create.at": "Subscription created on %s",
+ "subscription.nota.part.1": "Check out the documentation",
+ "subscription.nota.part.2": "or explore",
+ "subscription.nota.link.api": "to using the API",
+ "subscription.nota.link.statistics": "the statistics of your subscription",
+ "subscription.custom.name.successfuly.updated": "Your custom subscription name has been successfully updated",
+ "subscription.rotation.successfully.setup": "Your secret rotation has been successfully set up",
+ "subscription.successfully.enabled": "Your subscription has been successfully enabled",
+ "subscription.successfully.disabled": "Your subscription has been successfully disabled",
+ "subscription.custom.name.update.label": "Update custom subscription name",
+ "subscription.custom.name.update.message":"Subscription name",
+ "subscription.custom.name.update.placeholder":"Custom name",
+ "subscription.rotation.update.label": "Setup secret rotation",
+ "subscription.reset.secret.label": "Reset secret",
+ "subscription.show.aggregate.label": "Show aggregate",
+ "subscription.transfer.label": "Transfer subscription",
+ "subscription.enable.button.label": "Enable subscription",
+ "subscription.disable.button.label": "Disable subscription",
+ "subscription.delete.button.label": "Delete",
+ "subscription.extract.button.label": "Extract from agg.",
+ "subscription.enable.label": "Enabled",
+ "subscription.disable.label": "Disabled",
+ "subscriptions.hide.link": "Hide link",
+ "subscriptions.display.link": "Display link",
+ "subscriptions.link.explanation.1": "To transfer your subscription to another team, please follow the steps below:",
+ "subscriptions.link.explanation.2": "Copy the transfer link provided below.",
+ "subscriptions.link.explanation.3": "Send the link to the manager of the team to which you want to transfer the subscription.",
+ "subscriptions.link.explanation.4": "The team manager can follow this link, select the target team, and then confirm the transfer.",
+ "subscriptions.copy.link.button.label": "Copy link",
+ "subscriptions.transfer.modal.title": "Transfer API subscription",
+ "subscription.retrieve.select.team.modal.title": "Select the Receiving Team",
+ "subscription.retrieve.select.team.modal.message": "The team %s wants to transfer its subscription %s and has provided you with a transfer link.Choose the team to which you want to transfer the admin rights for this subscription. Only the teams where you are an administrator are available for selection.",
+ "subscription.retrieve.select.team.button.label": "Select team",
+ "subscription.retrieve.confirm.modal.title": "Transfer Confirmation",
+ "subscription.retrieve.confirm.modal.message": "The transfer of the subscription to the team %s has been validated. You can finalize this action by confirming below. This transfer will grant the team admin rights for the subscription.",
+ "subscription.retrieve.confirm.modal.ok.button.label": "Confirm transfer",
+ "login.required": "Login required",
+ "subscription.retrieve.login.required.message": "Please log in to continue. You need to be authenticated to access this feature.",
+ "subscription.retrieve.token.unavalaible": "token unavailable",
+ "Daikoku.home" : "Daikoku home",
+ "API.list" : "APIs list",
+ "more.nav.options": "More options",
+ "api.access.modal.title": "Api access request"
}
\ No newline at end of file
diff --git a/daikoku/javascript/src/locales/fr/translation.json b/daikoku/javascript/src/locales/fr/translation.json
index 31135dd6d..15155aefd 100644
--- a/daikoku/javascript/src/locales/fr/translation.json
+++ b/daikoku/javascript/src/locales/fr/translation.json
@@ -607,14 +607,7 @@
"help.apikey.rotation.period": "Période après laquelle le secret sera automatiquement modifié",
"help.apikey.grace.period": "Période pendant laquelle le nouveau secret et l'ancien sont tous deux actifs (cette période est inclue dans la période de rotation).",
"constraint.apikey.grace.period": "La période de tuilage doit être inferieur à la période de rotation",
- "reset.secret.confirm": "Êtes-vous certain de vouloir réinitialiser le secret",
- "View usage statistics": "Voir les statistiques d'usage",
- "View api": "Voir l'API",
- "Copy to clipboard": "Copier dans le presse-papier",
- "Setup rotation": "Paramètrer la rotation",
- "Enable/Disable": "Activer/Désactiver",
- "subscription.enable.button.label": "Activer la souscription",
- "subscription.disable.button.label": "Désactiver la souscription",
+ "reset.secret.confirm": "Êtes-vous certain de vouloir réinitialiser le secret ?",
"Integration": "Intégration",
"Force apikey auto-rotation": "Forcer la rotation auto. des secrets",
"ApiKey": "Clé d'API",
@@ -755,6 +748,7 @@
"moment.duration.months": "%s m",
"moment.duration.years": "%s a",
"Unknown user": "Utilisateur inconnu",
+ "Unknown team": "Équipe inconnue",
"European server": "Serveur européen",
"API creation security": "Sécurité - création d'API",
"creation.security.help": "si activé, seules les équipes autorisées pourront créer des APIs",
@@ -1276,9 +1270,9 @@
"select.team.placeholder": "Sélectionner une équipe",
"otoroshi.missing.entities": "Entités Otoroshi manquantes",
"otoroshi.missing.target": "Cible Otoroshi manquante",
- "apiList.tag.search": "Recherchez un tag",
- "apiList.category.search": "Recherchez une catégorie",
- "apiList.team.search": "Recherchez une équipe",
+ "apiList.tag.search": "Par tag",
+ "apiList.category.search": "Par catégorie",
+ "apiList.team.search": "Par équipe",
"team.email.verification.send": "Mail envoyé à %s pour vérifier cette adresse email.",
"team.email.alreadyVerified": "Cet email est déjà vérifié",
"team.email.notVerified": "Envoyer un mail",
@@ -1443,5 +1437,48 @@
"apikeys.delete.confirm.label": "Pour confirmer la suppression, veuillez saisir le nom exact de la souscription : %s",
"apikeys.delete.success.message": "La souscription a été correctement supprimée",
"apikeys.view.api": "Voir l'API",
- "apikeys.view.apikeys": "Voir les clés d'API"
+ "apikeys.view.apikeys": "Voir les clés d'API",
+ "subscription.create.at": "Souscription crée le %s",
+ "subscription.nota.part.1": "Consultez la documentation",
+ "subscription.nota.part.2": "ou explorez",
+ "subscription.nota.link.api": "pour utiliser l'API",
+ "subscription.nota.link.statistics": "les statistiques de votre souscription",
+ "subscription.custom.name.successfuly.updated": "Le nom personnalisé de votre souscription a été mis à jour avec succès",
+ "subscription.rotation.successfully.setup": "La rotation du secret a été configurée avec succès",
+ "subscription.successfully.enabled": "Votre souscription a été activée avec succès",
+ "subscription.successfully.disabled": "Votre souscription a été désactivée avec succès",
+ "subscription.custom.name.update.label": "Mettre à jour le nom perso.",
+ "subscription.custom.name.update.message":"Nom de la souscription",
+ "subscription.custom.name.update.placeholder":"Nom personnalisé",
+ "subscription.rotation.update.label": "Paramétrer la rotation",
+ "subscription.reset.secret.label": "Réinit. le secret",
+ "subscription.show.agregate.label": "Voir l'agrégat",
+ "subscription.transfer.label": "Transférer la souscription",
+ "subscription.enable.button.label": "Activer la souscription",
+ "subscription.disable.button.label": "Désactiver la souscription",
+ "subscription.delete.button.label": "Supprimer",
+ "subscription.extract.button.label": "Extraire de l'agrégat",
+ "subscription.enable.label": "Activé",
+ "subscription.disable.label": "Désactivé",
+ "subscriptions.hide.link": "Cacher le lien",
+ "subscriptions.display.link": "Afficher le lien",
+ "subscriptions.link.explanation.1": "Pour transférer votre souscription à une autre équipe, veuillez suivre les étapes ci-dessous :",
+ "subscriptions.link.explanation.2": "Copiez le lien de transfert présenté ci-dessous.",
+ "subscriptions.link.explanation.3": "Envoyez le lien au responsable de l'équipe à laquelle vous souhaitez transférer la souscription.",
+ "subscriptions.link.explanation.4": "Le responsable de l'équipe pourra suivre ce lien, sélectionner l'équipe cible, puis valider le transfert.",
+ "subscriptions.copy.link.button.label": "Copier le lien",
+ "subscriptions.transfer.modal.title": "Transférer une souscription",
+ "subscription.retrieve.select.team.modal.title": "Sélectionnez l'équipe destinataire",
+ "subscription.retrieve.select.team.modal.message": "L'équipe %s souhaite transférer sa souscription %s et vous a fourni un lien de transfert. Choisissez l'équipe à laquelle vous souhaitez transférer les droits d'administration pour cette souscription. Seules les équipes dont vous êtes administrateur sont disponibles pour la sélection.",
+ "subscription.retrieve.select.team.button.label": "Selectionner",
+ "subscription.retrieve.confirm.modal.title": "Confirmation du transfert",
+ "subscription.retrieve.confirm.modal.message": "Le transfert de la souscription vers l'équipe %s a été validé. Vous pouvez finaliser cette action en confirmant ci-dessous. Ce transfert accordera à cette équipe les droits d'administration de la souscription.",
+ "subscription.retrieve.confirm.modal.ok.button.label": "Confirmer le transfert",
+ "login.required": "Connexion requise",
+ "subscription.retrieve.login.required.message": "Veuillez vous connecter pour continuer. Vous devez être authentifié pour accéder à cette fonctionnalité.",
+ "subscription.retrieve.token.unavalaible": "Le jeton n'est plus valable",
+ "Daikoku.home" : "Accueil Daikoku",
+ "API.list" : "Liste des APIs",
+ "more.nav.options": "Plus d'options",
+ "api.access.modal.title": "demande d'accès à une API"
}
\ No newline at end of file
diff --git a/daikoku/javascript/src/services/index.ts b/daikoku/javascript/src/services/index.ts
index a91666290..324da35a8 100644
--- a/daikoku/javascript/src/services/index.ts
+++ b/daikoku/javascript/src/services/index.ts
@@ -43,6 +43,7 @@ import {
ResponseDone,
ResponseError,
} from '../types/api';
+import { Token } from 'graphql';
const HEADERS = {
Accept: 'application/json',
@@ -2026,3 +2027,24 @@ export const getSubscriptionsLastUsages = (
method: 'POST',
body: JSON.stringify({ subscriptions }),
});
+
+export const getSubscriptionTransferLink = (
+ teamId: string,
+ subscriptionId: string
+): PromiseWithError<{ link: string }> =>
+ customFetch(`/api/teams/${teamId}/subscriptions/${subscriptionId}/_transfer`);
+
+export const checkTransferlink = (
+ token: string
+): PromiseWithError<{ subscription: ISubscription; api: IApi; plan: IUsagePlan }> =>
+ customFetch(`/api/me/subscription/_retrieve?token=${token}`);
+
+export const retrieveSubscription = (
+ token: string,
+ teamId: string,
+ subscription: string
+): PromiseWithError =>
+ customFetch(`/api/teams/${teamId}/subscriptions/${subscription}/_retrieve`, {
+ method: 'PUT',
+ body: JSON.stringify({ token }),
+ });
diff --git a/daikoku/javascript/src/style/components/apiSubscription.scss b/daikoku/javascript/src/style/components/apiSubscription.scss
new file mode 100644
index 000000000..d662d227c
--- /dev/null
+++ b/daikoku/javascript/src/style/components/apiSubscription.scss
@@ -0,0 +1,134 @@
+@keyframes pulseBig {
+ 0% {
+ opacity: 0.5;
+ }
+
+ 70% {
+ opacity: 0;
+ transform: scale(1.6);
+ }
+
+ 100% {
+ opacity: 0;
+ }
+}
+
+.api-subscription {
+ position: relative;
+ width: 70%;
+ // display: flex;
+ gap: 1rem;
+
+
+ border: 1px solid $level3_text-color;
+ background-color: $level3_bg-color;
+ border-radius: 0.375rem;
+ padding: 1rem 1rem 0 1rem;
+ margin-bottom: 10px;
+
+ .api-subscription__container {
+ display: flex;
+ margin-bottom: 1rem;
+ }
+
+ .api-subscription__icon {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 1rem;
+ width: 18%;
+
+ grid-column-start: first;
+ grid-column-end: infos;
+ grid-row: main;
+
+ i.icon {
+ font-size: 2rem;
+ margin-bottom: 0.375rem;
+ }
+
+ .api-subscription__value__type {
+ border: 1px solid $level1_link-color;
+ border-radius: 1.9rem;
+ padding: 0 0.25rem;
+ display: flex;
+ align-items: center;
+ line-height: 1rem;
+ padding: .35rem .75rem;
+
+ }
+
+ .dot {
+ display: block;
+ margin-right: 10px;
+ width: 15px;
+ height: 15px;
+ border-radius: 15px;
+ position: relative;
+ margin-left: .5rem;
+
+ &.enabled {
+ background-color: $success-color;
+ }
+
+ &.disabled {
+ background-color: $error-color;
+ }
+ }
+
+ .dot::before {
+ content: '';
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ animation: pulseBig 2s infinite;
+ opacity: 1;
+ border-radius: 50%;
+ top: 0;
+ left: 0;
+ background-color: inherit;
+ }
+ }
+
+ .api-subscription__infos {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+
+ .api-subscription__infos__name {
+ font-weight: bold;
+ }
+
+ .api-subscription__infos__value {
+ font-size: 12px;
+ word-break: break-all;
+ }
+
+ .api-subscription__infos__state {
+ color: $success-color;
+ }
+ .api-subscription__infos__creation {
+ font-size: 12px;
+ }
+ }
+
+ .api-subscriptions__links {
+ font-size: .75rem;
+ }
+
+
+
+
+}
+
+.api-susbcription__display-link {
+ width: 100%;
+ border: 1px solid black;
+ padding: 10px;
+ text-wrap: wrap;
+ word-wrap: break-word;
+ word-break: keep-all;
+
+ background-color: $level2_bg-color;
+ color: $level2_text-color;
+}
\ No newline at end of file
diff --git a/daikoku/javascript/src/style/layout/navbar.scss b/daikoku/javascript/src/style/layout/navbar.scss
index f9eb4c14b..641bc58b2 100644
--- a/daikoku/javascript/src/style/layout/navbar.scss
+++ b/daikoku/javascript/src/style/layout/navbar.scss
@@ -1,13 +1,14 @@
-
.navbar-brand {
font-size: 18px;
}
-a.navbar-brand, #teamSelector:hover {
- color:$sidebar-text-color;
-}
+a.navbar-brand,
+#teamSelector:hover {
+ color: $sidebar-text-color;
+}
-.navbar .Select.has-value.Select--single > .Select-control .Select-value .Select-value-label, .Select.has-value.is-pseudo-focused.Select--single > .Select-control .Select-value .Select-value-label {
+.navbar .Select.has-value.Select--single>.Select-control .Select-value .Select-value-label,
+.Select.has-value.is-pseudo-focused.Select--single>.Select-control .Select-value .Select-value-label {
color: #fff;
}
@@ -18,6 +19,7 @@ a.navbar-brand, #teamSelector:hover {
.logo-anonymous {
border-radius: 50%;
+
&:hover {
cursor: pointer;
}
@@ -26,18 +28,32 @@ a.navbar-brand, #teamSelector:hover {
.dropdown-menu {
background-color: $level3_bg-color;
}
+
.dropdown-item {
- color:$level3_link-color;
+ color: $level3_link-color;
+
&:hover {
background-color: $level3_link-hover-bg-color;
color: $level3_link-hover-color
}
+
+ &.danger {
+ color: $danger-color;
+
+ &:hover {
+ background-color: $danger-color;
+ color: #fff;
+ }
+ }
}
-.dropdown-divider{
- border-color: $level3_link-color;
+
+.dropdown-divider {
+ border-color: $level3_link-color;
}
-.add-member-select, .general-search{
+
+.add-member-select,
+.general-search {
width: 400px;
}
@@ -45,16 +61,18 @@ a.navbar-brand, #teamSelector:hover {
z-index: 100;
}
-.period-select:hover>div,.general-search:hover>div,.add-member-select:hover>div{
+.period-select:hover>div,
+.general-search:hover>div,
+.add-member-select:hover>div {
cursor: pointer !important;
}
-.general-search>div{
+.general-search>div {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
background-color: $form-bg-color;
- color:$form-text-color;
- border-color:$form-border-color;
+ color: $form-text-color;
+ border-color: $form-border-color;
}
.full-width-select {
@@ -63,7 +81,7 @@ a.navbar-brand, #teamSelector:hover {
.navbar-toggle {
@media screen and (min-width: 768px) {
- display: none;
+ display: none;
}
}
@@ -109,8 +127,9 @@ a.navbar-brand, #teamSelector:hover {
&.opened {
width: calc(75vw - 55px);
- overflow-y: auto;
- @media screen and (min-width:768px){
+ overflow-y: auto;
+
+ @media screen and (min-width:768px) {
width: calc(35vw - 55px);
}
}
@@ -118,10 +137,11 @@ a.navbar-brand, #teamSelector:hover {
.navbar-panel__back {
height: 40px;
width: 40px;
+
&:hover {
border-radius: 50%;
- background-color: $companion_text-hover-bg-color;
- color: var( --companion-link-hover-text-color);
+ background-color: $companion_text-hover-bg-color;
+ color: var(--companion-link-hover-text-color);
}
}
}
@@ -131,7 +151,8 @@ a.navbar-brand, #teamSelector:hover {
width: 0;
height: 100vh;
z-index: 1000;
- top: 0; right: 0;
+ top: 0;
+ right: 0;
background-color: #000;
opacity: 0.3;
width: 0;
@@ -140,7 +161,8 @@ a.navbar-brand, #teamSelector:hover {
&.opened {
width: 25vw;
- @media screen and (min-width:768px){
+
+ @media screen and (min-width:768px) {
width: 65vw;
}
}
@@ -157,6 +179,7 @@ a.navbar-brand, #teamSelector:hover {
font-weight: 600;
font-size: 12px;
}
+
.block__entries {
.block__entry__link {
cursor: pointer;
@@ -166,6 +189,7 @@ a.navbar-brand, #teamSelector:hover {
line-height: 40px;
border-radius: 4px;
padding-left: .5rem;
+
&:hover {
background-color: $companion_text-hover-bg-color;
color: $companion_text-hover-color;
@@ -186,7 +210,8 @@ a.navbar-brand, #teamSelector:hover {
height: 100vh;
position: relative;
min-width: 20px;
- width: 20px; height: 100vh;
+ width: 20px;
+ height: 100vh;
transition: width .2s ease-in-out;
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
@@ -219,7 +244,7 @@ a.navbar-brand, #teamSelector:hover {
color: $companion_text-color;
font-weight: 600;
- h3{
+ h3 {
white-space: normal;
word-break: break-word;
}
@@ -228,7 +253,7 @@ a.navbar-brand, #teamSelector:hover {
.blocks {
flex-grow: 1;
-
+
.block {
.block__entries {
.block__entry__link {
@@ -239,9 +264,10 @@ a.navbar-brand, #teamSelector:hover {
line-height: 40px;
border-radius: 4px;
padding-left: .5rem;
+
&.active {
background-color: $companion_text-hover-bg-color;
- color:$companion_text-hover-color;
+ color: $companion_text-hover-color;
}
&.disabled {
@@ -272,12 +298,12 @@ a.navbar-brand, #teamSelector:hover {
background-color: $companion_text-hover-bg-color;
color: $companion_text-hover-color;
}
-
+
&.disabled {
color: lighten(#000, 50%);
cursor: default;
}
-
+
&:hover {
// border-bottom: 1px solid $companion_text-hover-bg-color;
background-color: $companion_text-hover-bg-color;
@@ -295,8 +321,12 @@ a.navbar-brand, #teamSelector:hover {
}
.companion-button {
- position: absolute; top: 20px; right: -12px; z-index: 1000;
- width: 24px; height: 24px;
+ position: absolute;
+ top: 20px;
+ right: -12px;
+ z-index: 1000;
+ width: 24px;
+ height: 24px;
cursor: pointer;
display: flex;
align-items: center;
@@ -328,7 +358,7 @@ a.navbar-brand, #teamSelector:hover {
}
-.page__menu__entry{
+.page__menu__entry {
cursor: pointer;
font-size: 14px;
color: $sidebar-text-color;
@@ -336,6 +366,7 @@ a.navbar-brand, #teamSelector:hover {
line-height: 40px;
border-radius: 4px;
padding-left: .5rem;
+
&.active {
background-color: $companion_text-hover-bg-color;
color: $companion_text-hover-color;
diff --git a/daikoku/javascript/src/style/layout/rightPanel.scss b/daikoku/javascript/src/style/layout/rightPanel.scss
new file mode 100644
index 000000000..b24e1457f
--- /dev/null
+++ b/daikoku/javascript/src/style/layout/rightPanel.scss
@@ -0,0 +1,73 @@
+.right-panel-container {
+ height: 100vh;
+ // background-color: $navbar-bg-color;
+ // position: 'relative';
+ // color: $navbar-text-color;
+
+
+ .right-panel {
+ height: 100%;
+ width: 0;
+ min-height: 300px;
+ overflow-y: hidden;
+ }
+}
+
+.right-panel {
+ position: absolute;
+ overflow: hidden;
+ width: 0;
+ height: 100vh;
+ z-index: 10000;
+ top: 0;
+ right: 0;
+ background-color: $companion_bg-color;
+ color: $companion_text-color;
+ filter: brightness(105%);
+ transition: width .2s ease-in-out;
+
+ &.opened {
+ width: calc(75vw);
+ overflow-y: auto;
+
+ @media screen and (min-width:768px) {
+ width: calc(35vw);
+ }
+ }
+
+ .right-panel__back {
+ height: 40px;
+ width: 40px;
+ position: absolute;
+ top: .5rem;
+ right: .5rem;
+
+ &:hover {
+ border-radius: 50%;
+ background-color: $companion_text-hover-bg-color;
+ color: var(--companion-link-hover-text-color);
+ }
+ }
+}
+
+.right-panel-background {
+ position: absolute;
+ width: 0;
+ height: 100vh;
+ z-index: 1000;
+ top: 0;
+ left: 0;
+ background-color: #000;
+ opacity: 0.3;
+ width: 0;
+
+ transition: width .2s ease-in-out;
+
+ &.opened {
+ width: 25vw;
+
+ @media screen and (min-width:768px) {
+ width: 65vw;
+ }
+ }
+}
\ No newline at end of file
diff --git a/daikoku/javascript/src/style/layout/style.scss b/daikoku/javascript/src/style/layout/style.scss
index 1ea819129..9fb96d08f 100644
--- a/daikoku/javascript/src/style/layout/style.scss
+++ b/daikoku/javascript/src/style/layout/style.scss
@@ -41,6 +41,12 @@ a {
color: $level1_link-color;
}
+.a-fake, a {
+ &.underline {
+ text-decoration: underline;
+ }
+}
+
.a-fake {
cursor: pointer;
}
@@ -50,7 +56,7 @@ a:hover {
color: $level1_link-hover-color;
}
-.underline-on-hover:hover {
+.underline-on-hover:hover, .underline {
text-decoration: underline;
}
diff --git a/daikoku/javascript/src/style/main.scss b/daikoku/javascript/src/style/main.scss
index cc81fcde0..0091a38eb 100644
--- a/daikoku/javascript/src/style/main.scss
+++ b/daikoku/javascript/src/style/main.scss
@@ -6,6 +6,7 @@
@import 'layout/navbar';
@import 'layout/login';
@import 'layout/footer.scss';
+@import 'layout/rightPanel.scss';
@import "components/antCalandar";
@import 'components/avatarWithAction';
@@ -39,3 +40,4 @@
@import 'components/navDocumentation';
@import 'components/swagger';
@import 'components/tabs';
+@import 'components/apiSubscription.scss'
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..658fb2057
--- /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
diff --git a/daikoku/javascript/tests/anonymousUser.spec.ts b/daikoku/javascript/tests/anonymousUser.spec.ts
index f2b452071..10b6b9523 100644
--- a/daikoku/javascript/tests/anonymousUser.spec.ts
+++ b/daikoku/javascript/tests/anonymousUser.spec.ts
@@ -65,7 +65,7 @@ test('[public tenant] - external user can join a team', async ({ page }) => {
await page.getByRole('link', { name: 'Access to the notifications' }).click();
await expect(page.getByText('Admin, as admin of Consumers')).toBeVisible();
await page.getByRole('link', { name: 'Accept' }).click();
- await page.getByRole('link', { name: 'Go home' }).click();
+ await page.getByRole('link', { name: 'APIs list' }).click();
await expect(page.getByText('Consumers')).toBeVisible();
})
@@ -123,7 +123,7 @@ test('[private tenant] - external user can join a team', async ({ page, request
await page.getByRole('link', { name: 'Access to the notifications' }).click();
await expect(page.getByText('Admin, as admin of Consumers')).toBeVisible();
await page.getByRole('link', { name: 'Accept' }).click();
- await page.getByRole('link', { name: 'Go home' }).click();
+ await page.getByRole('link', { name: 'APIs list' }).click();
await expect(page.getByText('Consumers')).toBeVisible();
})
@@ -185,7 +185,7 @@ test('[public tenant] - external user can signup', async ({ page }) => {
await page.getByRole('img', { name: 'user menu' }).click();
await page.getByRole('link', { name: 'Create an account' }).click();
await page.getByLabel('Name').fill('fifou');
- await page.getByLabel('Email address').fill('fifou@foo.bar');
+ await page.locator(".signup-form").getByLabel('Email address').fill('fifou@foo.bar');
await page.locator('form.signup-form input[name="password"]').fill('Pa$$w0rd');
await page.getByLabel('Confirm password').fill('Pa$$w0rd');
await page.getByRole('button', { name: 'Create account' }).click();
@@ -260,7 +260,7 @@ test('[private tenant] - unlogged user can accept subscription demand', async ({
await page.getByText('Consumers').click();
await page.getByText('API keys').click();
await page.getByRole('row', { name: 'test API 2.0.0' }).getByLabel('View APIkeys').click();
- await expect(page.locator('.card-header')).toContainText('not test plan');
+ await expect(page.locator('.api-subscription__infos__name')).toContainText('not test plan');
})
//anonymous user can accept demand
test('[public tenant] - unlogged user can accept subscription demand', async ({ page, request}) => {
@@ -308,5 +308,5 @@ test('[public tenant] - unlogged user can accept subscription demand', async ({
await page.getByText('Consumers').click();
await page.getByText('API keys').click();
await page.getByRole('row', { name: 'test API 2.0.0' }).getByLabel('View APIkeys').click();
- await expect(page.locator('.card-header')).toContainText('not test plan');
+ await expect(page.locator('.api-subscription__infos__name')).toContainText('not test plan');
})
\ No newline at end of file
diff --git a/daikoku/javascript/tests/completeJourney.spec.ts b/daikoku/javascript/tests/completeJourney.spec.ts
index ef5333906..141f18d90 100644
--- a/daikoku/javascript/tests/completeJourney.spec.ts
+++ b/daikoku/javascript/tests/completeJourney.spec.ts
@@ -41,7 +41,7 @@ test('test a complete user journey', async ({ page }) => {
//create a new API
await page.locator('div:nth-child(4) > .notification-link').first().click();
await page.locator('span').filter({ hasText: 'API' }).first().click();
- await page.locator('div').filter({ hasText: /^The A team$/ }).click();
+ await page.locator('div').filter({ hasText: /^The A team$/ }).nth(1).click();
await page.getByRole('button', { name: 'Published' }).click();
await page.getByPlaceholder('New Api').fill('Test API');
@@ -62,6 +62,8 @@ test('test a complete user journey', async ({ page }) => {
await page.getByRole('button', { name: 'Add plan' }).click();
await page.locator('.react-form-select__input-container').click();
await page.getByText('Free without quotas', { exact: true }).click();
+ await page.getByPlaceholder('Plan name').click();
+ await page.keyboard.type('dev plan')
await page.getByPlaceholder('Plan name').fill('dev plan');
await page.getByPlaceholder('Plan description').fill('a dev plan to test the API with unlimited usage');
await page.getByRole('button', { name: 'Next' }).click();
@@ -114,7 +116,7 @@ test('test a complete user journey', async ({ page }) => {
//subscribe
await page.getByText('Subscriptions').click();
await expect(page.getByRole('main')).toContainText('0 Result');
- await page.getByRole('link', { name: 'Go home' }).click();
+ await page.getByRole('link', { name: 'APIs list' }).click();
await expect(page.getByRole('main')).toContainText('second test api');
@@ -140,10 +142,10 @@ test('test a complete user journey', async ({ page }) => {
// await expect(page.getByRole('main')).toContainText('Lorem ipsum');
// await page.getByText('News').click();
// await expect(page.getByRole('main')).toContainText('Our API is alive');
- await page.getByRole('link', { name: 'Go home' }).click();
+ await page.getByRole('link', { name: 'APIs list' }).click();
await page.locator('span').filter({ hasText: 'The A team' }).click();
await page.getByText('API keys').click();
await expect(page.getByRole('main')).toContainText('1 Result');
await page.getByLabel('View APIkeys').click();
- await expect(page.locator('.card-header')).toContainText('dev plan');
+ await expect(page.locator('.api-subscription__infos__name')).toContainText('dev plan');
});
\ No newline at end of file
diff --git a/daikoku/javascript/tests/connectedUser.spec.ts b/daikoku/javascript/tests/connectedUser.spec.ts
index 0f9a05520..75e7da737 100644
--- a/daikoku/javascript/tests/connectedUser.spec.ts
+++ b/daikoku/javascript/tests/connectedUser.spec.ts
@@ -309,7 +309,7 @@ test('Create & manage API', async ({ page }) => {
await page.getByText('Validate a subscription', { exact: true }).click();
await page.getByRole('link', { name: 'Accept' }).click();
await page.getByRole('link', { name: 'Access to the notifications' }).click();
- await page.getByRole('link', { name: 'Go home' }).click();
+ await page.getByRole('link', { name: 'APIs list' }).click();
await page.getByText('Consumers').click();
await page.getByText('API keys').click();
await page.getByRole('row', { name: 'test API 2 1.0.0' }).getByLabel('View APIkeys').click();
@@ -317,7 +317,7 @@ test('Create & manage API', async ({ page }) => {
// await expect(page.locator('#tooltip-TwFQ')).toBeVisible();
//FIXME: due to small viewport``
- await expect(page.locator('.card-header').filter({ hasText: 'public & automatic' })).toBeVisible()
+ await expect(page.locator('.api-subscription__infos__name').filter({ hasText: 'public & automatic' })).toBeVisible()
})
/**
@@ -381,7 +381,7 @@ test('aggregation mode', async ({ page, request }) => {
await page.locator('.usage-plan__card').filter({ hasText: 'not test plan' }).getByRole('button').click();
await page.locator('div').filter({ hasText: /^Consumers$/ }).click();
// await page.getByRole('button', { name: 'Subscribe with a new api key' }).click();
- await page.getByRole('link', { name: 'Go home' }).click();
+ await page.getByRole('link', { name: 'APIs list' }).click();
//subscribe second api with aggregation
await page.getByRole('heading', { name: 'test API 2' }).click();
@@ -392,7 +392,7 @@ test('aggregation mode', async ({ page, request }) => {
await page.getByText('test API/not test plan').click();
//go to subscriptions
- await page.getByRole('link', { name: 'Go home' }).click();
+ await page.getByRole('link', { name: 'APIs list' }).click();
await page.locator('.top__container').filter({ hasText: 'Your teams' })
.getByText('Consumers').click()
// await page.getByLabel('Notifications alt+T').getByRole('button').click();
@@ -400,26 +400,33 @@ test('aggregation mode', async ({ page, request }) => {
await page.getByRole('row', { name: 'test API 2 1.0.0' }).getByLabel('View APIkeys').click();
//get the client id value to check
- const clientId = await page.getByLabel('Client Id').inputValue()
+ const apikey = await page.locator('.api-subscription__infos__value').innerText()
await page.getByText('API keys', { exact: true }).click();
await page.getByRole('row', { name: 'test API 2.0.0' }).getByLabel('view APikey').click();
- await expect(page.getByLabel('Client Id').first()).toHaveValue(clientId);
- await page.getByRole('button', { name: 'Show aggregate subscriptions' }).click();
+ await expect(page.locator('.api-subscription__infos__value').first()).toHaveText(apikey);
+ await page.locator('.api-subscription').locator('.dropdown').click();
+ await page.getByText('Show aggregate').click();
await expect(page.getByRole('link', { name: 'test API 2/test plan' })).toBeVisible();
+ await page.locator('.right-panel-background.opened').click();
await page.getByText('API keys', { exact: true }).click();
await page.getByRole('row', { name: 'test API 2 1.0.0' }).getByLabel('view APikey').click();
- await page.getByRole('button', { name: 'make unique' }).click();
+ await page.locator('.api-subscription').locator('.dropdown').click();
+ await page.getByText('Extract from agg.').click();
+ // await page.getByRole('button', { name: 'make unique' }).click();
await expect(page.getByRole('paragraph')).toContainText('Are you sure to make this API key unique and separate from his parent plan?');
- await page.getByRole('button', { name: 'Ok' }).click();
- await expect(page.getByLabel('Client Id').first()).not.toHaveValue(clientId);
+ await page.getByRole('button', { name: 'Ok', exact: true }).click();
+ await expect(page.locator('.api-subscription__infos__value').first()).not.toHaveText(apikey);
// //test archive apikey & clean archive apikeys
- await page.getByRole('button', { name: 'Disable subscription' }).click();
- await expect(page.getByRole('button', { name: 'Enable subscription' })).toBeVisible();
-
- await page.getByLabel('Delete').click();
- await expect(page.locator('h5')).toContainText('Confirm Deletion');
+ await page.locator('.api-subscription').locator('.dropdown').click();
+ await page.getByText('Disable subscription').click();
+ await expect(page.locator('.api-subscription__value__type')).toHaveText('Disabled')
+ // await expect(page.getByRole('button', { name: 'Enable subscription' })).toBeVisible();
+
+ await page.locator('.api-subscription').locator('.dropdown').click();
+ await page.getByText('Delete').click();
+ await expect(page.locator('h5')).toContainText('Confirm the deletion');
await page.getByLabel('To confirm the deletion,').fill('test API 2/test plan');
await page.getByRole('button', { name: 'Confirm' }).click();
await page.getByText('API keys', { exact: true }).click();
@@ -463,7 +470,7 @@ test('do search', async ({ page, request }) => {
await expect(page.locator('.navbar-panel.opened .block__entry__link')).toHaveCount(1)
await expect(page.getByRole('link', { name: 'Testers' })).toBeVisible();
await page.getByRole('link', { name: 'Testers' }).click();
- await expect(page.getByRole('heading', { name: 'In progress demands' })).toBeVisible();
+ await expect(page.getByRole('heading', { name: 'My pending requests' })).toBeVisible();
//search an API
await page.locator('.notification-link').first().click();
@@ -473,14 +480,6 @@ test('do search', async ({ page, request }) => {
await expect(page.getByRole('link', { name: 'test API - 1.0.0' })).toBeVisible();
await page.getByRole('link', { name: 'test API - 2.0.0' }).click();
await expect(page.getByRole('heading', { name: 'test API 2.0.0' })).toBeVisible();
- await page.locator('.notification-link').first().click();
-
- //go to profile page
- await page.getByPlaceholder('Search for API, team and more').fill('');
- await page.waitForResponse(r => r.url().includes('/api/_search') && r.status() === 200)
- await page.getByRole('link', { name: 'My profile' }).click();
- await expect(page.getByLabel('Name')).toHaveValue("Tester"); // expect = admin
- await page.locator('.notification-link').first().click();
});
test('API admin can transfer his own API ownership', async ({ page }) => {
@@ -500,7 +499,7 @@ test('API admin can transfer his own API ownership', async ({ page }) => {
await page.getByRole('link', { name: 'Access to the notifications' }).click();
await expect(page.locator('#app')).toContainText('Consumersrequest to transfer the ownership of test APITestera few seconds');
await page.getByRole('link', { name: 'Accept' }).nth(1).click();
- await page.getByRole('link', { name: 'Go home' }).click();
+ await page.getByRole('link', { name: 'APIs list' }).click();
await page.locator('h3').filter({ hasText: 'test API' }).waitFor({ state: 'visible' })
const consumerSelector = page.locator('small').filter({ hasText: 'Consumers' })
// console.log(consumerSelector)
@@ -558,13 +557,15 @@ test('Filter API List', async ({ page, request }) => {
await page.getByRole('option', { name: 'external' }).click();
await expect(page.locator('.preview')).toContainText('1 result categorized in external');
await page.getByText('clear filter').click();
- await page.getByPlaceholder('Search your API...').fill('test');
+ await page.getByPlaceholder('Search your API').fill('test');
await expect(page.locator('.preview')).toContainText('2 results matching test');
await page.locator('.reactSelect__indicator').first().click();
await page.getByRole('option', { name: 'Testers' }).click();
- await page.locator('div').filter({ hasText: /^option Testers, selected\.TestersSearch a tagSearch a category$/ }).locator('svg').nth(2).click();
+ await page.locator('.reactSelect__control').nth(1).locator('svg').click()
+ // await page.locator('div').filter({ hasText: /^option Testers, selected\.By tagBy category$/ }).locator('svg').nth(2).click();
await page.getByRole('option', { name: 'test' }).click();
- await page.locator('div').filter({ hasText: /^Testersoption test, selected\.testSearch a category$/ }).locator('svg').nth(4).click();
+ await page.locator('.reactSelect__control').nth(2).locator('svg').click()
+ // await page.locator('div').filter({ hasText: /^Testersoption test, selected\.testBy category$/ }).locator('svg').nth(4).click();
await page.getByRole('option', { name: 'internal' }).click();
await expect(page.locator('.preview')).toContainText('1 result matching test categorized in internal tagged test produced by Testers');
await page.locator('.category__selector > .reactSelect__control > .reactSelect__value-container > .reactSelect__input-container').click();
@@ -582,4 +583,42 @@ test('Filter API List', async ({ page, request }) => {
await page.getByText('clear filter').click();
await page.locator('small').filter({ hasText: 'external' }).click();
await expect(page.locator('.preview')).toContainText('1 result categorized in external');
-})
\ No newline at end of file
+})
+
+
+test('test', async ({ page }) => {
+ await page.goto('http://localhost:5173/apis');
+ await page.getByRole('img', { name: 'user menu' }).click();
+ await page.getByPlaceholder('Email address').fill('tester@foo.bar');
+ await page.getByPlaceholder('Password').fill('password');
+ await page.getByRole('button', { name: 'Login' }).click();
+ await page.waitForResponse(r => r.url().includes('/api/me/context') && r.status() === 200)
+ await page.waitForSelector('.apis__pagination')
+ await page.getByRole('heading', { name: 'test API' }).click();
+
+ //tester l'url pour verifier que c'est bien la v2
+ await page.getByText('Plans').click();
+
+ await page.locator('div').filter({ hasText: /^fake prod plan/ }).getByRole('button').click();
+ await page.getByText('Consumers').click();
+ await page.getByLabel('Notifications').getByRole('img').nth(1).click();
+ const apikey = await page.locator('.api-subscription__infos__value').innerText();
+
+
+
+ await page.locator('#dropdownMenuButton').click();
+ await page.getByText('Transfer subscription').click();
+ await page.getByText('Display link').click();
+ const link = await page.locator('.api-susbcription__display-link').innerText();
+
+
+
+ await page.goto(link);
+ await page.getByText('Testers').click();
+ await page.getByRole('button', { name: 'Confirm transfer' }).click();
+
+ await page.getByText('Testers').click();
+ await page.getByText('API keys').click();
+ await page.getByRole('row', { name: 'test API 2.0.0 View API View' }).getByLabel('View APIkeys').click();
+ expect(page.locator('.api-subscription__infos__value')).toHaveText(apikey)
+});
\ No newline at end of file
diff --git a/daikoku/javascript/tests/envMode.spec.ts b/daikoku/javascript/tests/envMode.spec.ts
index 1ee8292c3..fc9fba824 100644
--- a/daikoku/javascript/tests/envMode.spec.ts
+++ b/daikoku/javascript/tests/envMode.spec.ts
@@ -71,7 +71,7 @@ test('aggregation security works', async ({ page, request }) => {
await page.getByText('Environments').click();
await page.locator('.usage-plan__card').filter({ hasText: 'prod' }).getByRole('button').click();
await page.getByText('Consumers').click();
- await page.getByLabel('Go home').click();
+ await page.getByLabel('APIs list').click();
await page.getByRole('heading', { name: 'Child API' }).click();
await page.getByText('Environments').click();
@@ -89,7 +89,7 @@ test('aggregation security works', async ({ page, request }) => {
await page.getByText('Parent API/prod').click();
// await expect(page.getByText('API key to plan Free without')).toBeVisible();
- await page.getByLabel('Go home').click();
+ await page.getByLabel('APIs list').click();
await page.getByText('Consumers', { exact: true }).click();
await page.getByText('API keys').click();
await expect(page.getByRole('cell', { name: 'Child API' })).toBeVisible();
@@ -109,7 +109,7 @@ test('aggregation security works', async ({ page, request }) => {
// await page.getByText('Environments').click();
// await page.locator('.usage-plan__card').filter({ hasText: 'prod' }).getByRole('button').click();
// await page.locator('div').filter({ hasText: /^Consumers$/ }).click();
- // await page.getByRole('link', { name: 'Go home' }).click();
+ // await page.getByRole('link', { name: 'APIs list' }).click();
// //subscribe second api with aggregation
// await page.getByRole('heading', { name: 'Child API' }).click();
@@ -120,7 +120,7 @@ test('aggregation security works', async ({ page, request }) => {
// //subscribe second api with aggregation
- // await page.getByRole('link', { name: 'Go home' }).click();
+ // await page.getByRole('link', { name: 'APIs list' }).click();
// await page.getByRole('heading', { name: 'Child API' }).click();
// await page.getByText('Environments').click();
// await page.locator('.usage-plan__card').filter({ hasText: 'prod' }).getByRole('button').click();
@@ -129,7 +129,7 @@ test('aggregation security works', async ({ page, request }) => {
// await page.getByText('parent API/prod').click();
// //go to subscriptions
- // await page.getByRole('link', { name: 'Go home' }).click();
+ // await page.getByRole('link', { name: 'APIs list' }).click();
// await page.locator('.top__container').filter({ hasText: 'Your teams' })
// .getByText('Consumers').click()
// // await page.getByLabel('Notifications alt+T').getByRole('button').click();
diff --git a/daikoku/javascript/tests/resources/daikoku-test-export.ndjson b/daikoku/javascript/tests/resources/daikoku-test-export.ndjson
index 078f42b23..13099e907 100644
--- a/daikoku/javascript/tests/resources/daikoku-test-export.ndjson
+++ b/daikoku/javascript/tests/resources/daikoku-test-export.ndjson
@@ -1,4 +1,4 @@
-{"type":"tenants","payload":{"_id":"default","name":"Evil Corp.","style":{"js":"","css":"","logo":"/assets/images/daikoku.svg","jsUrl":null,"title":"Evil Corp.","cssUrl":null,"footer":null,"cacheTTL":60000,"colorTheme":":root {\n --error-color: #ff6347;\n --success-color: #65B741;\n\n --link-color: #7f96af;\n --link--hover-color: #8fa6bf;\n\n --body-bg-color: #fff;\n --body-text-color: #212529;\n --navbar-bg-color: #7f96af;\n --navbar-brand-color: #fff;\n --menu-bg-color: #fff;\n --menu-text-color: #212529;\n --menu-text-hover-bg-color: #9bb0c5;\n --menu-text-hover-color: #fff;\n --section-bg-color: #f8f9fa;\n --section-text-color: #6c757d;\n --section-bottom-color: #eee;\n --addContent-bg-color: #e9ecef;\n --addContent-text-color: #000;\n --sidebar-bg-color: #f8f9fa;\n\n --btn-bg-color: #fff;\n --btn-text-color: #495057;\n --btn-border-color: #97b0c7;\n\n --badge-tags-bg-color: #ffc107;\n --badge-tags-bg-color: #ffe1a7;\n --badge-tags-text-color: #212529;\n\n --pagination-text-color: #586069;\n --pagination-border-color: #586069;\n\n --table-bg-color: #f8f9fa;\n\n --apicard-visibility-color: #586069;\n --apicard-visibility-border-color: rgba(27,31,35,.15);\n --modal-selection-bg-color: rgba(27,31,35,.15);\n}","faviconUrl":null,"description":"A new organization to host very fine APIs","homeCmsPage":"63c67b5150010024104b0e8c","unloggedHome":"","fontFamilyUrl":null,"homePageVisible":false,"notFoundCmsPage":null,"cmsHistoryLength":10,"authenticatedCmsPage":null},"domain":"localhost","contact":"contact@foo.bar","enabled":true,"_deleted":false,"adminApi":"admin-api-tenant-default","robotTxt":null,"isPrivate":false,"tenantMode":"Default","authProvider":"Local","bucketSettings":null,"defaultMessage":null,"mailerSettings":{"host":"localhost","port":"1025","type":"smtpClient","template":null,"fromEmail":"no-reply@daikoku.io","fromTitle":"daikoku"},"defaultLanguage":"En","_humanReadableId":"evil-corp.","auditTrailConfig":{"kafkaConfig":null,"alertsEmails":[],"auditWebhooks":[],"elasticConfigs":null},"creationSecurity":false,"otoroshiSettings":[{"_id":"default","url":"http://127.0.0.1:9000/fakeotoroshi","host":"otoroshi-api.foo.bar","clientId":"admin-api-apikey-id","clientSecret":"admin-api-apikey-id"}],"adminSubscriptions":[],"authProviderSettings":{"sessionMaxAge":86400},"subscriptionSecurity":true,"apiReferenceHideForGuest":true,"thirdPartyPaymentSettings":[],"aggregationApiKeysSecurity":false}}
+{"type":"tenants","payload":{"_id":"default","name":"Evil Corp.","style":{"js":"","css":"","logo":"/assets/images/daikoku.svg","jsUrl":null,"title":"Evil Corp.","cssUrl":null,"footer":null,"cacheTTL":60000,"colorTheme":":root {\n --body_bg-color: #f1f3f6;\n --body_text-color: #8a8a8a;\n --body_link-color:#4c4c4d;\n --body_link-hover-color:orange;\n \n --level2_bg-color: #e5e7ea;\n --level2_text-color: #4c4c4d;\n --level2_link-color: #605c5c;\n --level2_link-hover-color: #000; \n \n --level3_bg-color : #fff;\n --level3_text-color : #222;\n --level3_link-color: #4c4c4d;\n --level3_link-hover-color : #000;\n --level3_link-hover-bg-color :grey;\n\n --sidebar-bg-color: #e5e7ea;\n --sidebar-text-color: #4c4c4d;\n --sidebar-text-hover-color:orange;\n \n --menu-bg-color: #fff;\n --menu-text-color: #aaa;\n --menu-text-hover-bg-color: #444;\n --menu-text-hover-color: #fff;\n --menu-link-color: #666;\n \n --card_header-bg-color: #404040;\n --card_header-text-color: #fff;\n --card_bg-color: #282828;\n --card_text-color: #fff;\n --card_link-color: #ffe1a7;\n --card_link-hover-color : #ffc107;\n \n --btn-bg-color: #fff;\n --btn-text-color: #495057;\n --btn-border-color: #97b0c7;\n \n --badge-tags-bg-color: #ffc107;\n --badge-tags-bg-hover-color: #ffe1a7;\n --badge-tags-text-color: #212529; \n \n --form-text-color: #000;\n --form-border-color: #586069;\n --form-bg-color: #fff;\n\n --form-select-focused-color: lightgrey;\n --form-select-focused-text-color: white;\n --form-select-heading-color: yellow;\n --form-select-hover-color: lightgrey;\n --form-select-hover-text-color: white;\n\n --error-color:#dc3545;\n --info-color: #17a2b8;\n --success-color: #65B741;\n --warning-color: #ffc107;\n --danger-color: #dc3545; \n }\n \n :root[data-theme=\"DARK\"] { \n --body_bg-color: #000;\n --body_text-color: #b3b3b3;\n --body_link-color:#b3b3b3;\n --body_link-hover-color:orange;\n \n --level2_bg-color: #121212;\n --level2_text-color: #b3b3b3;\n --level2_link-color: #9f9e9e;\n --level2_link-hover-color: #fff;\n \n --level3_bg-color : #242424;\n --level3_text-color : #e8e8e8;\n --level3_link-color: #9f9e9e;\n --level3_link-hover-color : #fff;\n --level3_link-hover-bg-color : grey; \n \n --sidebar-bg-color: #121212;\n --sidebar-text-color: #b3b3b3;\n --sidebar-text-hover-color:orange;\n \n --menu-bg-color: #242424;\n --menu-text-color: #fff;\n --menu-text-hover-bg-color: #121212;\n --menu-text-hover-color: #fff;\n --menu-link-color: #b3b3b3; \n \n --card_header-bg-color : #404040;\n --card_header-text-color: #fff;\n --card_bg-color: #282828;\n --card_text-color: #fff;\n --card_link-color: #ffe1a7;\n --card_link-hover-color : #ffc107;\n \n --btn-bg-color: #fff;\n --btn-text-color: #495057;\n --btn-border-color: #97b0c7;\n \n --badge-tags-bg-color: #ffc107;\n --badge-tags-bg-hover-color: #ffe1a7;\n --badge-tags-text-color: #212529;\n \n --form-text-color: #000;\n --form-border-color: #586069;\n --form-bg-color: #fff;\n \n --form-select-focused-color: grey;\n --form-select-focused-text-color: white;\n --form-select-heading-color: yellow;\n --form-select-hover-color: grey;\n --form-select-hover-text-color: white; \n\n --error-color:#dc3545;\n --info-color: #17a2b8;\n --success-color: #65B741;\n --warning-color: #ffc107;\n --danger-color: #dc3545;\n }","faviconUrl":null,"description":"A new organization to host very fine APIs","homeCmsPage":"63c67b5150010024104b0e8c","unloggedHome":"","fontFamilyUrl":null,"homePageVisible":false,"notFoundCmsPage":null,"cmsHistoryLength":10,"authenticatedCmsPage":null},"domain":"localhost","contact":"contact@foo.bar","enabled":true,"_deleted":false,"adminApi":"admin-api-tenant-default","robotTxt":null,"isPrivate":false,"tenantMode":"Default","authProvider":"Local","bucketSettings":null,"defaultMessage":null,"mailerSettings":{"host":"localhost","port":"1025","type":"smtpClient","template":null,"fromEmail":"no-reply@daikoku.io","fromTitle":"daikoku"},"defaultLanguage":"En","_humanReadableId":"evil-corp.","auditTrailConfig":{"kafkaConfig":null,"alertsEmails":[],"auditWebhooks":[],"elasticConfigs":null},"creationSecurity":false,"otoroshiSettings":[{"_id":"default","url":"http://127.0.0.1:9000/fakeotoroshi","host":"otoroshi-api.foo.bar","clientId":"admin-api-apikey-id","clientSecret":"admin-api-apikey-id"}],"adminSubscriptions":[],"authProviderSettings":{"sessionMaxAge":86400},"subscriptionSecurity":true,"apiReferenceHideForGuest":true,"thirdPartyPaymentSettings":[],"aggregationApiKeysSecurity":false}}
{"type":"users","payload":{"_id":"7cxa2bvog4k8e8d3uswmwnf90w9x8nea","name":"User","email":"user@foo.bar","isGuest":false,"origins":["Local"],"picture":"/assets/images/anonymous.jpg","tenants":["default"],"_deleted":false,"metadata":{},"password":"$2a$10$/Vpj1lFN0AcCfbutd7FJwO0j1vU4X0fR6t.4vvWBqSEqtoUFFxfDG","invitation":null,"lastTenant":null,"starredApis":[],"personalToken":"m775o13fduvnmpygimyyd6jpgh6ephl7","isDaikokuAdmin":false,"defaultLanguage":null,"_humanReadableId":"user-foo.bar","pictureFromProvider":true,"twoFactorAuthentication":null,"hardwareKeyRegistrations":[]}}
{"type":"users","payload":{"_id":"mom9ff7opam5nv576rpo98n9sqpxag5l","name":"Tester","email":"tester@foo.bar","isGuest":false,"origins":["Local"],"picture":"https://www.gravatar.com/avatar/7075180bc9c01c6ece2925b9822b6d77?size=128&d=robohash","tenants":["default"],"_deleted":false,"metadata":{},"password":"$2a$10$/Vpj1lFN0AcCfbutd7FJwO0j1vU4X0fR6t.4vvWBqSEqtoUFFxfDG","invitation":null,"lastTenant":null,"starredApis":[],"personalToken":"z05xtwwlgf1vlj4q1xkr6j0kbdjrz8q0","isDaikokuAdmin":false,"defaultLanguage":null,"_humanReadableId":"tester-foo.bar","pictureFromProvider":true,"twoFactorAuthentication":null,"hardwareKeyRegistrations":[]}}
{"type":"users","payload":{"_id":"SR9OKNdEcz1CZwe8WEMB6QYvFQPGfDTs6RXHDGYMCIva0SjEA11EOpmbMzYvBrQL","name":"Admin","email":"admin@foo.bar","isGuest":false,"origins":["Local"],"picture":"https://www.gravatar.com/avatar/87d47b53c88259042a50fa24a6d6b0f4?size=128&d=robohash","tenants":["default"],"_deleted":false,"metadata":{},"password":"$2a$10$/Vpj1lFN0AcCfbutd7FJwO0j1vU4X0fR6t.4vvWBqSEqtoUFFxfDG","invitation":null,"lastTenant":null,"starredApis":[],"personalToken":"bvBvXclSTmCmdAmEN2W03zTDwga7nKtZ","isDaikokuAdmin":true,"defaultLanguage":null,"_humanReadableId":"admin-foo.bar","pictureFromProvider":true,"twoFactorAuthentication":null,"hardwareKeyRegistrations":[]}}
diff --git a/daikoku/public/images/keys.svg b/daikoku/public/images/keys.svg
new file mode 100644
index 000000000..01a08fc8b
--- /dev/null
+++ b/daikoku/public/images/keys.svg
@@ -0,0 +1,11 @@
+
+
+
\ No newline at end of file
diff --git a/daikoku/public/themes/brutalism.css b/daikoku/public/themes/brutalism.css
index 48b2fd5f7..d053cef5f 100644
--- a/daikoku/public/themes/brutalism.css
+++ b/daikoku/public/themes/brutalism.css
@@ -1,70 +1,58 @@
:root {
- /* colors*/
- --error-color: #dc3545;
- --info-color: #17a2b8;
- --success-color: #65B741;
- --warning-color: #ffc107;
- --danger-color: #dc3545;
-
- /*level 1*/
-
- --body_bg-color: FireBrick;
- --body_text-color: DarkTurquoise;
- --body_link-color: Cyan;
- --body_link-hover-color: LightCyan;
-
- /*level 2*/
- --level2_bg-color: SandyBrown;
- --level2_text-color: Wheat;
- --level2_link-color: Sienna;
- --level2_link-hover-color: Brown;
-
- /*level 3*/
- --level3_bg-color: BlanchedAlmond;
- --level3_text-color: SandyBrown;
+ --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: grey;
- --level3_link-hover-bg-color: white;
-
- /*sidebar*/
- --sidebar-bg-color: PeachPuff;
- --sidebar-text-color: DarkKhaki;
- --sidebar-text-hover-color: PaleGoldenrod;
-
- /*companion*/
- --menu-bg-color: DarkOrange;
- --menu-text-color: OrangeRed;
- --menu-text-hover-bg-color: SlateGray;
- --menu-text-hover-color: white;
- --menu-link-color: DimGray;
-
-
- /* NEW cards */
- --card_header-bg-color: DarkGreen;
- --card_header-text-color: white;
- --card_bg-color: YellowGreen;
- --card_text-color: white;
- --card_link-color: olive;
- --card_link-hover-color: orange;
-
- /*btns*/
- --btn-bg-color: MediumPurple;
- --btn-text-color: Yellow;
- --btn-border-color: Violet;
+ --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*/
--badge-tags-bg-color: #ffc107;
--badge-tags-bg-hover-color: #ffe1a7;
--badge-tags-text-color: #212529;
- /*form*/
- --form-text-color: white;
- --form-border-color: white;
- --form-bg-color: black;
+ --form-text-color: #000;
+ --form-border-color: #586069;
+ --form-bg-color: #ff5e8c;
- --form-select-focused-color: red;
- --form-select-focused-text-color: green;
+ --form-select-focused-color: lightgrey;
+ --form-select-focused-text-color: white;
--form-select-heading-color: yellow;
- --form-select-hover-color: black;
- --form-select-hover-text-color: brown;
+ --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/public/themes/dark.css b/daikoku/public/themes/dark.css
index 3f21a3502..571d91b27 100644
--- a/daikoku/public/themes/dark.css
+++ b/daikoku/public/themes/dark.css
@@ -1,44 +1,37 @@
-:root[data-theme="DARK"] {
- --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;
+ --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_bg-color : #242424;
+ --level3_text-color : #e8e8e8;
--level3_link-color: #9f9e9e;
- --level3_link-hover-color: #fff;
- --level3_link-hover-bg-color: grey;
+ --level3_link-hover-color : #fff;
+ --level3_link-hover-bg-color : grey;
--sidebar-bg-color: #121212;
--sidebar-text-color: #b3b3b3;
- --sidebar-text-hover-color: orange;
+ --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;
+ --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: #b3b3b3;
- --card_link-hover-color: orange;
-
-
+ --card_link-color: #ffe1a7;
+ --card_link-hover-color : #ffc107;
+
--btn-bg-color: #fff;
--btn-text-color: #495057;
--btn-border-color: #97b0c7;
@@ -55,5 +48,11 @@
--form-select-focused-text-color: white;
--form-select-heading-color: yellow;
--form-select-hover-color: grey;
- --form-select-hover-text-color: white;
+ --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/public/themes/light.css b/daikoku/public/themes/light.css
index d99d6f142..e70b6723b 100644
--- a/daikoku/public/themes/light.css
+++ b/daikoku/public/themes/light.css
@@ -1,44 +1,36 @@
:root {
- --error-color: #dc3545;
- --info-color: #17a2b8;
- --success-color: #65B741;
- --warning-color: #ffc107;
- --danger-color: #dc3545;
-
--body_bg-color: #f1f3f6;
--body_text-color: #8a8a8a;
- --body_link-color: #4c4c4d;
- --body_link-hover-color: orange;
-
+ --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;
-
+ --level2_link-hover-color: #000;
- --level3_bg-color: #fff;
- --level3_text-color: #222;
+ --level3_bg-color : #fff;
+ --level3_text-color : #222;
--level3_link-color: #4c4c4d;
- --level3_link-hover-color: #000;
- --level3_link-hover-bg-color: grey;
+ --level3_link-hover-color : #000;
+ --level3_link-hover-bg-color :grey;
--sidebar-bg-color: #e5e7ea;
--sidebar-text-color: #4c4c4d;
- --sidebar-text-hover-color: orange;
+ --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;
-
+ --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: #b3b3b3;
- --card_link-hover-color: orange;
+ --card_link-color: #ffe1a7;
+ --card_link-hover-color : #ffc107;
--btn-bg-color: #fff;
--btn-text-color: #495057;
@@ -46,7 +38,7 @@
--badge-tags-bg-color: #ffc107;
--badge-tags-bg-hover-color: #ffe1a7;
- --badge-tags-text-color: #212529;
+ --badge-tags-text-color: #212529;
--form-text-color: #000;
--form-border-color: #586069;
@@ -57,4 +49,10 @@
--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/test/daikoku/ApiControllerSpec.scala b/daikoku/test/daikoku/ApiControllerSpec.scala
index a1fd302f8..f241daa1d 100644
--- a/daikoku/test/daikoku/ApiControllerSpec.scala
+++ b/daikoku/test/daikoku/ApiControllerSpec.scala
@@ -9,14 +9,21 @@ import com.github.tomakehurst.wiremock.client.WireMock._
import com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig
import controllers.AppError
import controllers.AppError.SubscriptionAggregationDisabled
-import fr.maif.otoroshi.daikoku.domain.NotificationAction.{ApiAccess, ApiSubscriptionDemand}
+import fr.maif.otoroshi.daikoku.domain.NotificationAction.{
+ ApiAccess,
+ ApiSubscriptionDemand
+}
import fr.maif.otoroshi.daikoku.domain.NotificationType.AcceptOrReject
import fr.maif.otoroshi.daikoku.domain.TeamPermission.Administrator
import fr.maif.otoroshi.daikoku.domain.UsagePlan._
import fr.maif.otoroshi.daikoku.domain.UsagePlanVisibility.{Private, Public}
import fr.maif.otoroshi.daikoku.domain._
-import fr.maif.otoroshi.daikoku.domain.json.{ApiFormat, SeqApiSubscriptionFormat}
+import fr.maif.otoroshi.daikoku.domain.json.{
+ ApiFormat,
+ SeqApiSubscriptionFormat
+}
import fr.maif.otoroshi.daikoku.tests.utils.DaikokuSpecHelper
+import fr.maif.otoroshi.daikoku.utils.Cypher.encrypt
import fr.maif.otoroshi.daikoku.utils.IdGenerator
import org.joda.time.DateTime
import org.scalatest.concurrent.IntegrationPatience
@@ -25,8 +32,8 @@ import org.scalatestplus.play.PlaySpec
import org.testcontainers.containers.BindMode
import play.api.http.Status
import play.api.libs.json._
-import scala.concurrent.duration._
+import scala.concurrent.duration._
import scala.concurrent.Await
import scala.util.Random
@@ -543,9 +550,9 @@ class ApiControllerSpec()
resp.status mustBe 403
}
- "see his teams" in {
+ "see his teams (graphQl)" in {
setupEnvBlocking(
- tenants = Seq(tenant),
+ tenants = Seq(tenant.copy(subscriptionSecurity = Some(true))),
users = Seq(userAdmin),
teams = Seq(teamOwner, teamConsumer)
)
@@ -578,7 +585,147 @@ class ApiControllerSpec()
resp.status mustBe 200
val result = (resp.json \ "data" \ "myTeams").as[JsArray]
- result.value.length mustBe 3
+ result.value.length mustBe 2
+
+ setupEnvBlocking(
+ tenants = Seq(tenant.copy(subscriptionSecurity = Some(false))),
+ users = Seq(userAdmin),
+ teams = Seq(teamOwner, teamConsumer)
+ )
+ val session2 = loginWithBlocking(userAdmin, tenant)
+ val resp2 = httpJsonCallBlocking(
+ "/api/search",
+ "POST",
+ body = Some(
+ Json.obj(
+ "query" ->
+ """
+ |query MyTeams {
+ | myTeams {
+ | name
+ | _humanReadableId
+ | _id
+ | type
+ | users {
+ | user {
+ | userId: id
+ | }
+ | teamPermission
+ | }
+ | }
+ | }
+ |""".stripMargin
+ )
+ )
+ )(tenant, session2)
+ resp2.status mustBe 200
+
+ val result2 = (resp2.json \ "data" \ "myTeams").as[JsArray]
+ result2.value.length mustBe 3
+ }
+ "see his teams" in {
+ setupEnvBlocking(
+ tenants = Seq(tenant.copy(subscriptionSecurity = Some(true))),
+ users = Seq(userAdmin),
+ teams = Seq(teamOwner, teamConsumer)
+ )
+ val session = loginWithBlocking(userAdmin, tenant)
+ val resp = httpJsonCallBlocking("/api/me/teams")(tenant, session)
+ resp.status mustBe 200
+
+ val result = resp.json.as[JsArray]
+ result.value.length mustBe 2
+
+ setupEnvBlocking(
+ tenants = Seq(tenant.copy(subscriptionSecurity = Some(false))),
+ users = Seq(userAdmin),
+ teams = Seq(teamOwner, teamConsumer)
+ )
+ val session2 = loginWithBlocking(userAdmin, tenant)
+ val resp2 = httpJsonCallBlocking("/api/me/teams")(tenant, session2)
+ resp2.status mustBe 200
+
+ val result2 = resp2.json.as[JsArray]
+ result2.value.length mustBe 3
+ }
+
+ "search a team" in {
+ setupEnvBlocking(
+ tenants = Seq(tenant.copy(subscriptionSecurity = Some(true))),
+ users = Seq(userAdmin),
+ teams = Seq(teamOwner, teamConsumer)
+ )
+ val session = loginWithBlocking(userAdmin, tenant)
+
+ val resp =
+ httpJsonCallBlocking(
+ path = s"/api/_search",
+ method = "POST",
+ body = Some(Json.obj("search" -> ""))
+ )(tenant, session)
+
+ resp.status mustBe 200
+ val maybeValue = resp.json
+ .as[JsArray]
+ .value
+ .find(entry => (entry \ "label").as[String] == "Teams")
+ maybeValue.isDefined mustBe true
+ (maybeValue.get \ "options").as[JsArray].value.length mustBe 2
+
+ val resp2 =
+ httpJsonCallBlocking(
+ path = s"/api/_search",
+ method = "POST",
+ body = Some(Json.obj("search" -> "Admin"))
+ )(tenant, session)
+
+ resp2.status mustBe 200
+ val maybeValue2 = resp2.json
+ .as[JsArray]
+ .value
+ .find(entry => (entry \ "label").as[String] == "Teams")
+ maybeValue2.isDefined mustBe true
+ (maybeValue2.get \ "options").as[JsArray].value.length mustBe 0
+
+ //disable subscription security
+
+ setupEnvBlocking(
+ tenants = Seq(tenant.copy(subscriptionSecurity = Some(false))),
+ users = Seq(userAdmin, userApiEditor),
+ teams = Seq(teamOwner, teamConsumer)
+ )
+ val session2 = loginWithBlocking(userAdmin, tenant)
+
+ val resp3 =
+ httpJsonCallBlocking(
+ path = s"/api/_search",
+ method = "POST",
+ body = Some(Json.obj("search" -> ""))
+ )(tenant, session2)
+
+ resp3.status mustBe 200
+ val maybeValue3 = resp3.json
+ .as[JsArray]
+ .value
+ .find(entry => (entry \ "label").as[String] == "Teams")
+ maybeValue3.isDefined mustBe true
+ (maybeValue3.get \ "options").as[JsArray].value.length mustBe 3
+
+ val resp4 =
+ httpJsonCallBlocking(
+ path = s"/api/_search",
+ method = "POST",
+ body = Some(Json.obj("search" -> "Admin"))
+ )(tenant, session2)
+
+ resp4.status mustBe 200
+ val maybeValue4 = resp4.json
+ .as[JsArray]
+ .value
+ .find(entry => (entry \ "label").as[String] == "Teams")
+ maybeValue4.isDefined mustBe true
+ (maybeValue4.get \ "options").as[JsArray].value.length mustBe 1
+
}
"see one of his teams" in {
@@ -1537,180 +1684,1713 @@ class ApiControllerSpec()
val plan = QuotasWithLimits(
id = UsagePlanId("3"),
tenant = tenant.id,
- maxPerSecond = 10000,
- maxPerMonth = 10000,
- maxPerDay = 10000,
- costPerMonth = BigDecimal(10.0),
+ maxPerSecond = 10000,
+ maxPerMonth = 10000,
+ maxPerDay = 10000,
+ costPerMonth = BigDecimal(10.0),
+ billingDuration = BillingDuration(1, BillingTimeUnit.Month),
+ trialPeriod = None,
+ currency = Currency("EUR"),
+ customName = None,
+ customDescription = None,
+ otoroshiTarget = Some(
+ OtoroshiTarget(
+ OtoroshiSettingsId("default"),
+ Some(
+ AuthorizedEntities(groups = Set(OtoroshiServiceGroupId("12345")))
+ ),
+ ApikeyCustomization(
+ customMetadata = Seq(
+ CustomMetadata("meta1", Set.empty)
+ )
+ )
+ )
+ ),
+ allowMultipleKeys = Some(false),
+ subscriptionProcess = Seq(
+ ValidationStep.TeamAdmin(
+ id = IdGenerator.token,
+ team = defaultApi.api.team,
+ title = "team.name"
+ )
+ ),
+ integrationProcess = IntegrationProcess.ApiKey,
+ autoRotation = Some(false)
+ )
+ val demand = SubscriptionDemand(
+ id = SubscriptionDemandId("1"),
+ tenant = tenant.id,
+ api = defaultApi.api.id,
+ plan = UsagePlanId("3"),
+ steps = Seq(
+ SubscriptionDemandStep(
+ id = SubscriptionDemandStepId("demandStep_1"),
+ state = SubscriptionDemandState.InProgress,
+ step = ValidationStep.TeamAdmin(
+ id = "step_1",
+ team = defaultApi.api.team,
+ title = "Admin"
+ ),
+ metadata = Json.obj()
+ )
+ ),
+ state = SubscriptionDemandState.InProgress,
+ team = teamConsumerId,
+ from = userAdmin.id,
+ date = DateTime.now().minusDays(1),
+ motivation =
+ Json.obj("motivation" -> Json.obj("type" -> "string")).some,
+ parentSubscriptionId = None,
+ customReadOnly = None,
+ customMetadata = None,
+ customMaxPerSecond = None,
+ customMaxPerDay = None,
+ customMaxPerMonth = None
+ )
+
+ val untreatedNotification = Notification(
+ id = NotificationId("untreated-notification"),
+ tenant = tenant.id,
+ team = Some(teamOwnerId),
+ sender = NotificationSender(user.name, user.email, user.id.some),
+ notificationType = AcceptOrReject,
+ action = ApiAccess(defaultApi.api.id, teamConsumerId)
+ )
+
+ setupEnvBlocking(
+ tenants = Seq(tenant),
+ users = Seq(userAdmin),
+ teams = Seq(teamConsumer, teamOwner),
+ usagePlans = Seq(plan),
+ apis = Seq(
+ defaultApi.api.copy(
+ possibleUsagePlans = Seq(plan.id)
+ )
+ ),
+ subscriptionDemands = Seq(demand),
+ notifications = Seq(
+ untreatedNotification.copy(
+ action = ApiSubscriptionDemand(
+ api = defaultApi.api.id,
+ plan = UsagePlanId("3"),
+ team = teamConsumerId,
+ demand = demand.id,
+ step = demand.steps.head.id,
+ motivation = Some("motivation")
+ )
+ )
+ )
+ )
+ val session = loginWithBlocking(userAdmin, tenant)
+ val resp = httpJsonCallBlocking(
+ path = s"/api/notifications/${untreatedNotification.id.value}/accept",
+ method = "PUT",
+ body = Some(Json.obj())
+ )(tenant, session)
+
+ resp.status mustBe 400
+ (resp.json \ "error")
+ .as[String] mustBe AppError.ApiKeyCustomMetadataNotPrivided
+ .getErrorMessage()
+ }
+
+ "not manipulate api if tenant api creation security is enabled & team.apisCreationPermission is disabled" in {
+ setupEnvBlocking(
+ tenants = Seq(tenant.copy(creationSecurity = Some(true))),
+ users = Seq(userAdmin),
+ teams = Seq(teamOwner)
+ )
+
+ val api = generateApi("0", tenant.id, teamOwnerId, Seq.empty)
+ val session = loginWithBlocking(userAdmin, tenant)
+ val resp = httpJsonCallBlocking(
+ path = s"/api/teams/${teamOwnerId.value}/apis",
+ method = "POST",
+ body = Some(api.api.asJson)
+ )(tenant, session)
+
+ resp.status mustBe 403
+ }
+
+ "manipulate api if tenant api creation security is enabled & team.apisCreationPermission is enabled" in {
+ setupEnvBlocking(
+ tenants = Seq(tenant.copy(creationSecurity = Some(true))),
+ users = Seq(userAdmin),
+ teams = Seq(teamOwner.copy(apisCreationPermission = Some(true)))
+ )
+
+ val api = generateApi("0", tenant.id, teamOwnerId, Seq.empty)
+ val session = loginWithBlocking(userAdmin, tenant)
+ val resp = httpJsonCallBlocking(
+ path = s"/api/teams/${teamOwnerId.value}/apis",
+ method = "POST",
+ body = Some(api.api.asJson)
+ )(tenant, session)
+
+ resp.status mustBe 201
+ }
+
+ "not subscribe to an api with his personnal team if tenant enabled subscription security" in {
+ setupEnvBlocking(
+ tenants = Seq(tenant),
+ users = Seq(userAdmin),
+ teams = Seq(teamConsumer.copy(`type` = TeamType.Personal), teamOwner),
+ usagePlans = defaultApi.plans,
+ apis = Seq(defaultApi.api)
+ )
+
+ val session = loginWithBlocking(userAdmin, tenant)
+ val plan = "1"
+ val respPersonal = httpJsonCallBlocking(
+ path =
+ s"/api/apis/${defaultApi.api.id.value}/plan/$plan/team/${teamConsumer.id.value}/_subscribe",
+ method = "POST",
+ body = Json.obj().some
+ )(tenant, session)
+
+ respPersonal.status mustBe 403
+
+ val respOrg = httpJsonCallBlocking(
+ path =
+ s"/api/apis/${defaultApi.api.id.value}/plan/$plan/team/${teamOwnerId.value}/_subscribe",
+ method = "POST",
+ body = Json.obj().some
+ )(tenant, session)
+
+ 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)
+ )
+
+ //get transfer link (no need to give team)
+ val session = loginWithBlocking(userAdmin, tenant)
+ val respLink = httpJsonCallBlocking(
+ path =
+ s"/api/teams/${teamConsumer.id.value}/subscriptions/${subscription.id.value}/_transfer"
+ )(tenant, session)
+ respLink.status mustBe 200
+ val link = (respLink.json \ "link").as[String]
+ val token = link.split("token=").lastOption.getOrElse("")
+
+ //follow link
+ val respRetrieve = httpJsonCallBlocking(
+ path =
+ s"/api/teams/${teamOwner.id.value}/subscriptions/${subscription.id.value}/_retrieve",
+ method = "PUT",
+ body = Json.obj("token" -> token).some
+ )(tenant, session)
+ logger.info(Json.stringify(respRetrieve.json))
+ respRetrieve.status mustBe 200
+
+ val consumerSubsReq = httpJsonCallBlocking(
+ s"/api/subscriptions/teams/${teamConsumer.id.value}"
+ )(tenant, session)
+ logger.info(Json.stringify(consumerSubsReq.json))
+ 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)
+ logger.info(Json.stringify(ownerSubsReq.json))
+ 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
+
+ //TODO: verifier le nouveau nom de la subscription
+
+ }
+
+ "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)
+ )
+
+ val session = loginWithBlocking(userAdmin, tenant)
+ val respLink = httpJsonCallBlocking(
+ path =
+ s"/api/teams/${teamConsumer.id.value}/subscriptions/${childSub.id.value}/_transfer"
+ )(tenant, session)
+ logger.debug(Json.stringify(respLink.json))
+ respLink.status mustBe 409
+ }
+
+ "not transfer child subscriptions to another team which have already a 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
+ )
+
+ val parentOwnerSub = ApiSubscription(
+ id = ApiSubscriptionId("parent_owner_sub"),
+ tenant = tenant.id,
+ apiKey = parentApiKeyWith2childs,
+ plan = parentPlanProd.id,
+ createdAt = DateTime.now(),
+ team = teamOwnerId,
+ api = parentApi.id,
+ by = userTeamAdminId,
+ customName = None,
+ rotation = None,
+ integrationToken = "parent_owner_token"
+ )
+ val childOwnerSub = ApiSubscription(
+ id = ApiSubscriptionId("child_owner_sub"),
+ tenant = tenant.id,
+ apiKey = parentApiKeyWith2childs,
+ plan = parentPlanProd.id,
+ createdAt = DateTime.now(),
+ team = teamOwnerId,
+ api = parentApi.id,
+ by = userTeamAdminId,
+ customName = None,
+ rotation = None,
+ integrationToken = "parent_owner_token",
+ parent = parentSub.id.some
+ )
+
+ setupEnvBlocking(
+ tenants = Seq(
+ tenant.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,
+ parentOwnerSub
+ )
+ )
+
+ val session = loginWithBlocking(userAdmin, tenant)
+ val respLink = httpJsonCallBlocking(
+ path =
+ s"/api/teams/${teamConsumer.id.value}/subscriptions/${parentSub.id.value}/_transfer"
+ )(tenant, session)
+ respLink.status mustBe 200
+ val link = (respLink.json \ "link").as[String]
+ val token = link.split("token=").lastOption.getOrElse("")
+
+ //todo: test with a team has already a parentSub
+
+ val respRetrieve = httpJsonCallBlocking(
+ path =
+ s"/api/teams/${teamOwner.id.value}/subscriptions/${parentSub.id.value}/_retrieve",
+ method = "PUT",
+ body = Json.obj("token" -> token).some
+ )(tenant, session)
+ logger.info(Json.stringify(respRetrieve.json))
+ respRetrieve.status mustBe 409
+ }
+ "not transfer child subscriptions to another team which have already a child 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
+ )
+
+ val parentOwnerSub = ApiSubscription(
+ id = ApiSubscriptionId("parent_owner_sub"),
+ tenant = tenant.id,
+ apiKey = parentApiKeyWith2childs,
+ plan = parentPlanProd.id,
+ createdAt = DateTime.now(),
+ team = teamOwnerId,
+ api = parentApi.id,
+ by = userTeamAdminId,
+ customName = None,
+ rotation = None,
+ integrationToken = "parent_owner_token"
+ )
+ val childOwnerSub = ApiSubscription(
+ id = ApiSubscriptionId("child_owner_sub"),
+ tenant = tenant.id,
+ apiKey = parentApiKeyWith2childs,
+ plan = parentPlanProd.id,
+ createdAt = DateTime.now(),
+ team = teamOwnerId,
+ api = parentApi.id,
+ by = userTeamAdminId,
+ customName = None,
+ rotation = None,
+ integrationToken = "parent_owner_token",
+ parent = parentSub.id.some
+ )
+
+ setupEnvBlocking(
+ tenants = Seq(
+ tenant.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,
+ childOwnerSub
+ )
+ )
+
+ val session = loginWithBlocking(userAdmin, tenant)
+ val respLink = httpJsonCallBlocking(
+ path =
+ s"/api/teams/${teamConsumer.id.value}/subscriptions/${parentSub.id.value}/_transfer"
+ )(tenant, session)
+ respLink.status mustBe 200
+ val link = (respLink.json \ "link").as[String]
+ val token = link.split("token=").lastOption.getOrElse("")
+
+ //todo: test with a team has already a parentSub
+
+ val respRetrieve = httpJsonCallBlocking(
+ path =
+ s"/api/teams/${teamOwner.id.value}/subscriptions/${parentSub.id.value}/_retrieve",
+ method = "PUT",
+ body = Json.obj("token" -> token).some
+ )(tenant, session)
+ logger.info(Json.stringify(respRetrieve.json))
+ respRetrieve.status mustBe 409
+ }
+
+ "transfer child subscriptions to another team which have already a subscription when parent plan allow it" 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(true),
+ 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(true),
+ 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
+ )
+
+ val parentOwnerSub = ApiSubscription(
+ id = ApiSubscriptionId("parent_owner_sub"),
+ tenant = tenant.id,
+ apiKey = parentApiKeyWith2childs,
+ plan = parentPlanProd.id,
+ createdAt = DateTime.now(),
+ team = teamOwnerId,
+ api = parentApi.id,
+ by = userTeamAdminId,
+ customName = None,
+ rotation = None,
+ integrationToken = "parent_owner_token"
+ )
+ val childOwnerSub = ApiSubscription(
+ id = ApiSubscriptionId("child_owner_sub"),
+ tenant = tenant.id,
+ apiKey = parentApiKeyWith2childs,
+ plan = parentPlanProd.id,
+ createdAt = DateTime.now(),
+ team = teamOwnerId,
+ api = parentApi.id,
+ by = userTeamAdminId,
+ customName = None,
+ rotation = None,
+ integrationToken = "parent_owner_token",
+ parent = parentSub.id.some
+ )
+
+ setupEnvBlocking(
+ tenants = Seq(
+ tenant.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,
+ parentOwnerSub
+ )
+ )
+
+ val session = loginWithBlocking(userAdmin, tenant)
+ val respLink = httpJsonCallBlocking(
+ path =
+ s"/api/teams/${teamConsumer.id.value}/subscriptions/${parentSub.id.value}/_transfer"
+ )(tenant, session)
+ respLink.status mustBe 200
+ val link = (respLink.json \ "link").as[String]
+ val token = link.split("token=").lastOption.getOrElse("")
+
+ //todo: test with a team has already a parentSub
+
+ val respRetrieve = httpJsonCallBlocking(
+ path =
+ s"/api/teams/${teamOwner.id.value}/subscriptions/${parentSub.id.value}/_retrieve",
+ method = "PUT",
+ body = Json.obj("token" -> token).some
+ )(tenant, session)
+ logger.info(Json.stringify(respRetrieve.json))
+ respRetrieve.status mustBe 200
+ }
+ "transfer child subscriptions to another team which have already a subscription when child plan allow it" 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(true),
+ 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(true),
+ 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
+ )
+
+ val parentOwnerSub = ApiSubscription(
+ id = ApiSubscriptionId("parent_owner_sub"),
+ tenant = tenant.id,
+ apiKey = parentApiKeyWith2childs,
+ plan = parentPlanProd.id,
+ createdAt = DateTime.now(),
+ team = teamOwnerId,
+ api = parentApi.id,
+ by = userTeamAdminId,
+ customName = None,
+ rotation = None,
+ integrationToken = "parent_owner_token"
+ )
+ val childOwnerSub = ApiSubscription(
+ id = ApiSubscriptionId("child_owner_sub"),
+ tenant = tenant.id,
+ apiKey = parentApiKeyWith2childs,
+ plan = parentPlanProd.id,
+ createdAt = DateTime.now(),
+ team = teamOwnerId,
+ api = parentApi.id,
+ by = userTeamAdminId,
+ customName = None,
+ rotation = None,
+ integrationToken = "parent_owner_token",
+ parent = parentSub.id.some
+ )
+
+ setupEnvBlocking(
+ tenants = Seq(
+ tenant.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,
+ parentOwnerSub,
+ childOwnerSub
+ )
+ )
+
+ val session = loginWithBlocking(userAdmin, tenant)
+ val respLink = httpJsonCallBlocking(
+ path =
+ s"/api/teams/${teamConsumer.id.value}/subscriptions/${parentSub.id.value}/_transfer"
+ )(tenant, session)
+ respLink.status mustBe 200
+ val link = (respLink.json \ "link").as[String]
+ val token = link.split("token=").lastOption.getOrElse("")
+
+ //todo: test with a team has already a parentSub
+
+ val respRetrieve = httpJsonCallBlocking(
+ path =
+ s"/api/teams/${teamOwner.id.value}/subscriptions/${parentSub.id.value}/_retrieve",
+ method = "PUT",
+ body = Json.obj("token" -> token).some
+ )(tenant, session)
+ logger.info(Json.stringify(respRetrieve.json))
+ respRetrieve.status mustBe 200
+ }
+
+ "not transfer subscriptions to another team unauthorized on parent api" 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,
+ visibility = ApiVisibility.Private,
+ authorizedTeams = Seq(teamOwner.id)
+ )
+ 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 = teamOwnerId,
+ 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 = teamOwnerId,
+ api = parentApi.id,
+ by = userTeamAdminId,
+ customName = None,
+ rotation = None,
+ integrationToken = "parent_token",
+ parent = parentSub.id.some
+ )
+
+ setupEnvBlocking(
+ tenants = Seq(
+ tenant.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
+ )
+ )
+
+ val session = loginWithBlocking(userAdmin, tenant)
+ val respLink = httpJsonCallBlocking(
+ path =
+ s"/api/teams/${teamOwner.id.value}/subscriptions/${parentSub.id.value}/_transfer"
+ )(tenant, session)
+ respLink.status mustBe 200
+ val link = (respLink.json \ "link").as[String]
+ val token = link.split("token=").lastOption.getOrElse("")
+
+ //todo: test with a team has already a parentSub
+
+ val respRetrieve = httpJsonCallBlocking(
+ path =
+ s"/api/teams/${teamConsumer.id.value}/subscriptions/${parentSub.id.value}/_retrieve",
+ method = "PUT",
+ body = Json.obj("token" -> token).some
+ )(tenant, session)
+ logger.info(Json.stringify(respRetrieve.json))
+ respRetrieve.status mustBe 401
+ }
+ "not transfer subscriptions to another team unauthorized on parent plan" 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),
+ visibility = UsagePlanVisibility.Private,
+ authorizedTeams = Seq(teamOwner.id)
+ )
+ 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 = teamOwnerId,
+ 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 = teamOwnerId,
+ api = parentApi.id,
+ by = userTeamAdminId,
+ customName = None,
+ rotation = None,
+ integrationToken = "parent_token",
+ parent = parentSub.id.some
+ )
+
+ setupEnvBlocking(
+ tenants = Seq(
+ tenant.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
+ )
+ )
+
+ val session = loginWithBlocking(userAdmin, tenant)
+ val respLink = httpJsonCallBlocking(
+ path =
+ s"/api/teams/${teamOwner.id.value}/subscriptions/${parentSub.id.value}/_transfer"
+ )(tenant, session)
+ respLink.status mustBe 200
+ val link = (respLink.json \ "link").as[String]
+ val token = link.split("token=").lastOption.getOrElse("")
+
+ //todo: test with a team has already a parentSub
+
+ val respRetrieve = httpJsonCallBlocking(
+ path =
+ s"/api/teams/${teamConsumer.id.value}/subscriptions/${parentSub.id.value}/_retrieve",
+ method = "PUT",
+ body = Json.obj("token" -> token).some
+ )(tenant, session)
+ logger.info(Json.stringify(respRetrieve.json))
+ respRetrieve.status mustBe 401
+ }
+
+ "not transfer subscriptions to another team unauthorized on child api" 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,
+ visibility = ApiVisibility.Private,
+ authorizedTeams = Seq(teamOwner.id)
+ )
+
+ val parentSub = ApiSubscription(
+ id = ApiSubscriptionId("parent_sub"),
+ tenant = tenant.id,
+ apiKey = parentApiKeyWith2childs,
+ plan = parentPlanProd.id,
+ createdAt = DateTime.now(),
+ team = teamOwnerId,
+ 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 = childPlanProd.id,
+ createdAt = DateTime.now(),
+ team = teamOwnerId,
+ api = childApi.id,
+ by = userTeamAdminId,
+ customName = None,
+ rotation = None,
+ integrationToken = "parent_token",
+ parent = parentSub.id.some
+ )
+
+ setupEnvBlocking(
+ tenants = Seq(
+ tenant.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
+ )
+ )
+
+ val session = loginWithBlocking(userAdmin, tenant)
+ val respLink = httpJsonCallBlocking(
+ path =
+ s"/api/teams/${teamOwner.id.value}/subscriptions/${parentSub.id.value}/_transfer"
+ )(tenant, session)
+ respLink.status mustBe 200
+ val link = (respLink.json \ "link").as[String]
+ val token = link.split("token=").lastOption.getOrElse("")
+
+ //todo: test with a team has already a parentSub
+
+ val respRetrieve = httpJsonCallBlocking(
+ path =
+ s"/api/teams/${teamConsumer.id.value}/subscriptions/${parentSub.id.value}/_retrieve",
+ method = "PUT",
+ body = Json.obj("token" -> token).some
+ )(tenant, session)
+ logger.info(Json.stringify(respRetrieve.json))
+ respRetrieve.status mustBe 401
+ }
+ "not transfer subscriptions to another team unauthorized on child plan" in {
+ val parentPlanProd = FreeWithoutQuotas(
+ id = UsagePlanId("parent.dev"),
+ tenant = tenant.id,
billingDuration = BillingDuration(1, BillingTimeUnit.Month),
- trialPeriod = None,
currency = Currency("EUR"),
- customName = None,
+ customName = envModeProd.some,
customDescription = None,
otoroshiTarget = Some(
OtoroshiTarget(
- OtoroshiSettingsId("default"),
+ containerizedOtoroshi,
Some(
- AuthorizedEntities(groups = Set(OtoroshiServiceGroupId("12345")))
- ),
- ApikeyCustomization(
- customMetadata = Seq(
- CustomMetadata("meta1", Set.empty)
+ AuthorizedEntities(
+ routes = Set(OtoroshiRouteId(parentRouteId))
)
)
)
),
allowMultipleKeys = Some(false),
- subscriptionProcess = Seq(
- ValidationStep.TeamAdmin(
- id = IdGenerator.token,
- team = defaultApi.api.team,
- title = "team.name"
- )
- ),
+ subscriptionProcess = Seq.empty,
integrationProcess = IntegrationProcess.ApiKey,
- autoRotation = Some(false)
+ autoRotation = Some(false),
+ aggregationApiKeysSecurity = Some(true)
)
- val demand = SubscriptionDemand(
- id = SubscriptionDemandId("1"),
+ val childPlanProd = FreeWithoutQuotas(
+ id = UsagePlanId("child.dev"),
tenant = tenant.id,
- api = defaultApi.api.id,
- plan = UsagePlanId("3"),
- steps = Seq(
- SubscriptionDemandStep(
- id = SubscriptionDemandStepId("demandStep_1"),
- state = SubscriptionDemandState.InProgress,
- step = ValidationStep.TeamAdmin(
- id = "step_1",
- team = defaultApi.api.team,
- title = "Admin"
- ),
- metadata = Json.obj()
+ billingDuration = BillingDuration(1, BillingTimeUnit.Month),
+ currency = Currency("EUR"),
+ customName = envModeProd.some,
+ customDescription = None,
+ otoroshiTarget = Some(
+ OtoroshiTarget(
+ containerizedOtoroshi,
+ Some(
+ AuthorizedEntities(
+ routes = Set(OtoroshiRouteId(childRouteId))
+ )
+ )
)
),
- state = SubscriptionDemandState.InProgress,
- team = teamConsumerId,
- from = userAdmin.id,
- date = DateTime.now().minusDays(1),
- motivation =
- Json.obj("motivation" -> Json.obj("type" -> "string")).some,
- parentSubscriptionId = None,
- customReadOnly = None,
- customMetadata = None,
- customMaxPerSecond = None,
- customMaxPerDay = None,
- customMaxPerMonth = None
+ allowMultipleKeys = Some(false),
+ subscriptionProcess = Seq.empty,
+ integrationProcess = IntegrationProcess.ApiKey,
+ autoRotation = Some(false),
+ aggregationApiKeysSecurity = Some(true),
+ visibility = UsagePlanVisibility.Private,
+ authorizedTeams = Seq(teamOwner.id)
)
- val untreatedNotification = Notification(
- id = NotificationId("untreated-notification"),
- tenant = tenant.id,
- team = Some(teamOwnerId),
- sender = NotificationSender(user.name, user.email, user.id.some),
- notificationType = AcceptOrReject,
- action = ApiAccess(defaultApi.api.id, teamConsumerId)
+ val parentApi = defaultApi.api.copy(
+ id = ApiId("parent-id"),
+ name = "parent API",
+ team = teamOwnerId,
+ possibleUsagePlans = Seq(parentPlanProd.id),
+ defaultUsagePlan = parentPlanProd.id.some
)
-
- setupEnvBlocking(
- tenants = Seq(tenant),
- users = Seq(userAdmin),
- teams = Seq(teamConsumer, teamOwner),
- usagePlans = Seq(plan),
- apis = Seq(
- defaultApi.api.copy(
- possibleUsagePlans = Seq(plan.id)
- )
- ),
- subscriptionDemands = Seq(demand),
- notifications = Seq(
- untreatedNotification.copy(
- action = ApiSubscriptionDemand(
- api = defaultApi.api.id,
- plan = UsagePlanId("3"),
- team = teamConsumerId,
- demand = demand.id,
- step = demand.steps.head.id,
- motivation = Some("motivation")
- )
- )
- )
+ val childApi = defaultApi.api.copy(
+ id = ApiId("child-id"),
+ name = "child API",
+ team = teamOwnerId,
+ possibleUsagePlans = Seq(childPlanProd.id),
+ defaultUsagePlan = childPlanProd.id.some
)
- val session = loginWithBlocking(userAdmin, tenant)
- val resp = httpJsonCallBlocking(
- path = s"/api/notifications/${untreatedNotification.id.value}/accept",
- method = "PUT",
- body = Some(Json.obj())
- )(tenant, session)
-
- resp.status mustBe 400
- (resp.json \ "error")
- .as[String] mustBe AppError.ApiKeyCustomMetadataNotPrivided
- .getErrorMessage()
- }
- "not manipulate api if tenant api creation security is enabled & team.apisCreationPermission is disabled" in {
- setupEnvBlocking(
- tenants = Seq(tenant.copy(creationSecurity = Some(true))),
- users = Seq(userAdmin),
- teams = Seq(teamOwner)
+ val parentSub = ApiSubscription(
+ id = ApiSubscriptionId("parent_sub"),
+ tenant = tenant.id,
+ apiKey = parentApiKeyWith2childs,
+ plan = parentPlanProd.id,
+ createdAt = DateTime.now(),
+ team = teamOwnerId,
+ api = parentApi.id,
+ by = userTeamAdminId,
+ customName = None,
+ rotation = None,
+ integrationToken = "parent_token"
)
-
- val api = generateApi("0", tenant.id, teamOwnerId, Seq.empty)
- val session = loginWithBlocking(userAdmin, tenant)
- val resp = httpJsonCallBlocking(
- path = s"/api/teams/${teamOwnerId.value}/apis",
- method = "POST",
- body = Some(api.api.asJson)
- )(tenant, session)
-
- resp.status mustBe 403
- }
-
- "manipulate api if tenant api creation security is enabled & team.apisCreationPermission is enabled" in {
- setupEnvBlocking(
- tenants = Seq(tenant.copy(creationSecurity = Some(true))),
- users = Seq(userAdmin),
- teams = Seq(teamOwner.copy(apisCreationPermission = Some(true)))
+ val childSub = ApiSubscription(
+ id = ApiSubscriptionId("child_sub"),
+ tenant = tenant.id,
+ apiKey = parentApiKeyWith2childs,
+ plan = childPlanProd.id,
+ createdAt = DateTime.now(),
+ team = teamOwnerId,
+ api = childApi.id,
+ by = userTeamAdminId,
+ customName = None,
+ rotation = None,
+ integrationToken = "parent_token",
+ parent = parentSub.id.some
)
- val api = generateApi("0", tenant.id, teamOwnerId, Seq.empty)
- val session = loginWithBlocking(userAdmin, tenant)
- val resp = httpJsonCallBlocking(
- path = s"/api/teams/${teamOwnerId.value}/apis",
- method = "POST",
- body = Some(api.api.asJson)
- )(tenant, session)
-
- resp.status mustBe 201
- }
-
- "not subscribe to an api with his personnal team if tenant enabled subscription security" in {
setupEnvBlocking(
- tenants = Seq(tenant),
- users = Seq(userAdmin),
- teams = Seq(teamConsumer.copy(`type` = TeamType.Personal), teamOwner),
- usagePlans = defaultApi.plans,
- apis = Seq(defaultApi.api)
+ tenants = Seq(
+ tenant.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
+ )
)
val session = loginWithBlocking(userAdmin, tenant)
- val plan = "1"
- val respPersonal = httpJsonCallBlocking(
+ val respLink = httpJsonCallBlocking(
path =
- s"/api/apis/${defaultApi.api.id.value}/plan/$plan/team/${teamConsumer.id.value}/_subscribe",
- method = "POST",
- body = Json.obj().some
+ s"/api/teams/${teamOwner.id.value}/subscriptions/${parentSub.id.value}/_transfer"
)(tenant, session)
+ respLink.status mustBe 200
+ val link = (respLink.json \ "link").as[String]
+ val token = link.split("token=").lastOption.getOrElse("")
- respPersonal.status mustBe 403
+ //todo: test with a team has already a parentSub
- val respOrg = httpJsonCallBlocking(
+ val respRetrieve = httpJsonCallBlocking(
path =
- s"/api/apis/${defaultApi.api.id.value}/plan/$plan/team/${teamOwnerId.value}/_subscribe",
- method = "POST",
- body = Json.obj().some
+ s"/api/teams/${teamConsumer.id.value}/subscriptions/${parentSub.id.value}/_retrieve",
+ method = "PUT",
+ body = Json.obj("token" -> token).some
)(tenant, session)
-
- respOrg.status mustBe 200
+ logger.info(Json.stringify(respRetrieve.json))
+ respRetrieve.status mustBe 401
}
"setup validUntil date for a subscription to his api" in {
@@ -1866,7 +3546,7 @@ class ApiControllerSpec()
resp.status mustBe 200
val result = (resp.json \ "data" \ "myTeams").as[JsArray]
- result.value.length mustBe 2
+ result.value.length mustBe 1
}
"see one of his teams" in {
@@ -2483,7 +4163,7 @@ class ApiControllerSpec()
resp.status mustBe 200
val result = (resp.json \ "data" \ "myTeams").as[JsArray]
- result.value.length mustBe 3
+ result.value.length mustBe 2
}
"see one of his teams" in {
@@ -6307,6 +7987,7 @@ class ApiControllerSpec()
(respPreVerifOtoParent.json \ "enabled").as[Boolean] mustBe true
val preMetadata = (respPreVerifOtoParent.json \ "metadata").as[JsObject]
+ logger.info(Json.stringify(respPreVerifOtoParent.json))
val preKeys = preMetadata.keys.filter(key => !key.startsWith("daikoku_"))
preKeys.size mustBe 1
(preMetadata \ "foo").as[String] mustBe "bar"
@@ -7149,7 +8830,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,
@@ -7250,7 +8931,8 @@ class ApiControllerSpec()
)
setupEnvBlocking(
- tenants = Seq(tenantEnvMode.copy(
+ tenants = Seq(
+ tenantEnvMode.copy(
otoroshiSettings = Set(
OtoroshiSettings(
id = containerizedOtoroshi,
@@ -7261,14 +8943,12 @@ class ApiControllerSpec()
clientId = otoroshiAdminApiKey.clientId
)
),
- environmentAggregationApiKeysSecurity = Some(true),
- aggregationApiKeysSecurity = Some(true)
- )),
+ environmentAggregationApiKeysSecurity = Some(true),
+ aggregationApiKeysSecurity = Some(true)
+ )
+ ),
users = Seq(user, userAdmin),
- teams = Seq(
- defaultAdminTeam,
- teamOwner,
- teamConsumer),
+ teams = Seq(defaultAdminTeam, teamOwner, teamConsumer),
apis = Seq(parentApi, childApi),
usagePlans = Seq(parentPlanProd, childPlanProd, childPlanDev),
subscriptions = Seq(parentSub)
@@ -7285,7 +8965,6 @@ class ApiControllerSpec()
)(tenant, consumerSession)
respDev.status mustBe 403
-
//test extend parent prod sub with child prod ==> OK
val respProd = httpJsonCallBlocking(
path =
@@ -7297,25 +8976,24 @@ class ApiControllerSpec()
//disabled security
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(false),
- aggregationApiKeysSecurity = Some(true)
- )),
+ 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(false),
+ aggregationApiKeysSecurity = Some(true)
+ )
+ ),
users = Seq(user, userAdmin),
- teams = Seq(
- defaultAdminTeam,
- teamOwner,
- teamConsumer),
+ teams = Seq(defaultAdminTeam, teamOwner, teamConsumer),
apis = Seq(parentApi, childApi),
usagePlans = Seq(parentPlanProd, childPlanProd, childPlanDev),
subscriptions = Seq(parentSub)
@@ -7331,7 +9009,6 @@ class ApiControllerSpec()
)(tenant, consumerSession2)
respDev2.status mustBe 200
-
//test extend parent prod sub with child prod ==> OK
val respProd2 = httpJsonCallBlocking(
path =
@@ -7342,25 +9019,24 @@ class ApiControllerSpec()
respProd2.status mustBe 200
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 = None,
- aggregationApiKeysSecurity = Some(true)
- )),
+ 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 = None,
+ aggregationApiKeysSecurity = Some(true)
+ )
+ ),
users = Seq(user, userAdmin),
- teams = Seq(
- defaultAdminTeam,
- teamOwner,
- teamConsumer),
+ teams = Seq(defaultAdminTeam, teamOwner, teamConsumer),
apis = Seq(parentApi, childApi),
usagePlans = Seq(parentPlanProd, childPlanProd, childPlanDev),
subscriptions = Seq(parentSub)
@@ -7376,7 +9052,6 @@ class ApiControllerSpec()
)(tenant, consumerSession3)
respDev3.status mustBe 200
-
//test extend parent prod sub with child prod ==> OK
val respProd3 = httpJsonCallBlocking(
path =
diff --git a/daikoku/test/daikoku/TeamControllerSpec.scala b/daikoku/test/daikoku/TeamControllerSpec.scala
index cba7e6143..a25365de8 100644
--- a/daikoku/test/daikoku/TeamControllerSpec.scala
+++ b/daikoku/test/daikoku/TeamControllerSpec.scala
@@ -186,6 +186,81 @@ class TeamControllerSpec()
)(tenant, session)
respDelete.status mustBe 403
}
+
+ "list all teams" in {
+ setupEnvBlocking(
+ tenants = Seq(tenant.copy(subscriptionSecurity = Some(true))),
+ users = Seq(userAdmin, tenantAdmin),
+ teams = Seq(teamOwner, teamConsumer, defaultAdminTeam)
+ )
+ val session = loginWithBlocking(tenantAdmin, tenant)
+ val resp = httpJsonCallBlocking(
+ "/api/search",
+ "POST",
+ body = Some(
+ Json.obj(
+ "query" ->
+ """
+ |query getAllteams($research: String, $limit: Int, $offset: Int) {
+ | teamsPagination(research: $research, limit: $limit, offset: $offset) {
+ | teams {
+ | _id
+ | _humanReadableId
+ | name
+ | avatar
+ | type
+ | }
+ | total
+ | }
+ | }
+ |""".stripMargin,
+ "variables" -> Json.obj(
+ "research" -> "",
+ "limit" -> 10,
+ "offset" -> 0
+ )
+ )
+ )
+ )(tenant, session)
+
+ resp.status mustBe 200
+ val result = (resp.json \ "data" \ "teamsPagination" \ "total").as[Int]
+ result mustBe 2
+
+ setupEnvBlocking(
+ tenants = Seq(tenant.copy(subscriptionSecurity = Some(false))),
+ users = Seq(userAdmin, tenantAdmin),
+ teams = Seq(teamOwner, teamConsumer, defaultAdminTeam)
+ )
+ val session2 = loginWithBlocking(tenantAdmin, tenant)
+ val resp2 = httpJsonCallBlocking(
+ "/api/search",
+ "POST",
+ body = Some(
+ Json.obj(
+ "query" ->
+ """
+ |query getAllteams {
+ | teamsPagination {
+ | teams {
+ | _id
+ | _humanReadableId
+ | name
+ | avatar
+ | type
+ | }
+ | total
+ | }
+ | }
+ |""".stripMargin
+ )
+ )
+ )(tenant, session2)
+ resp2.status mustBe 200
+
+ val result2 = (resp2.json \ "data" \ "teamsPagination" \ "total").as[Int]
+ result2 mustBe 4
+ }
}
"a team administrator" can {
diff --git a/daikoku/test/daikoku/suites.scala b/daikoku/test/daikoku/suites.scala
index 4fefae264..5125e7651 100644
--- a/daikoku/test/daikoku/suites.scala
+++ b/daikoku/test/daikoku/suites.scala
@@ -724,112 +724,118 @@ 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 +880,25 @@ 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 +937,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")
diff --git a/manual/docs/02-usages/07-adminusage/4-importexport.md b/manual/docs/02-usages/07-adminusage/4-importexport.md
index b1dd9a7f9..67a43738c 100644
--- a/manual/docs/02-usages/07-adminusage/4-importexport.md
+++ b/manual/docs/02-usages/07-adminusage/4-importexport.md
@@ -19,7 +19,7 @@ If you want to restore an export, Go to `settings (avatar icon) / Organizations
Since v1.1.1 Daikoku supports Postgresql databases. If you want to migrate you MongoDB to Postgresql, it's dead simple like the following instructions.
:::danger
-Since **v17.5.0-rc.1**, Daikoku does not support MongoDB anymore. To run database migration, you need to be in **16.3.6 max**.
+Since **v17.5.0-rc.2**, Daikoku does not support MongoDB anymore. To run database migration, you need to be in **16.3.6 max**.
:::
1. Add your Postgresql access in Daikoku configuration