diff --git a/packages/app-builder/public/locales/en/common.json b/packages/app-builder/public/locales/en/common.json index a9a5cc4f0..7dc960523 100644 --- a/packages/app-builder/public/locales/en/common.json +++ b/packages/app-builder/public/locales/en/common.json @@ -23,7 +23,7 @@ "delete": "Delete", "close": "Close", "clipboard.aria-label": "Copy to clipboard: {{value}}", - "clipboard.copy": "Copied in clipboard: {{value}}", + "clipboard.copy": "Copied in clipboard: {{value}}", "empty_scenario_iteration_list": "You don't have any rules. Click on create rule to make a new one", "success.save": "Saved successfully", "success.add_to_case": "Decision successfully added to case", diff --git a/packages/app-builder/public/locales/en/settings.json b/packages/app-builder/public/locales/en/settings.json index 46ab3300b..9c3bef720 100644 --- a/packages/app-builder/public/locales/en/settings.json +++ b/packages/app-builder/public/locales/en/settings.json @@ -23,6 +23,15 @@ "users.inbox_user_role.member_count_other": "MEMBER in {{count}} inboxes", "users.inbox_user_role.unknown": "unknown", "api_keys": "API Keys", + "api_keys.new_api_key": "New API Key", + "api_keys.copy_api_key": "Make sure to copy it now and store it in a secure place as you will not be able to see this again.", + "api_keys.description": "Description", + "api_keys.role": "API key role", + "api_keys.role.api_client": "API_CLIENT", + "api_keys.role.unknown": "UNKNOWN", + "api_keys.create": "Create new API Key", + "api_keys.delete": "Delete API Key", + "api_keys.delete.content": "You are about to revoke this API key. This action is irreversible, do you want to proceed?", "case_manager": "Case Manager", "inboxes": "Inboxes", "inboxes.new_inbox": "New inbox", diff --git a/packages/app-builder/src/components/CopyToClipboardButton.tsx b/packages/app-builder/src/components/CopyToClipboardButton.tsx index 193486d6e..f2ae451e0 100644 --- a/packages/app-builder/src/components/CopyToClipboardButton.tsx +++ b/packages/app-builder/src/components/CopyToClipboardButton.tsx @@ -12,7 +12,7 @@ export const CopyToClipboardButton = forwardRef< return (
diff --git a/packages/app-builder/src/models/api-keys.ts b/packages/app-builder/src/models/api-keys.ts new file mode 100644 index 000000000..f2619b032 --- /dev/null +++ b/packages/app-builder/src/models/api-keys.ts @@ -0,0 +1,43 @@ +import { type ApiKeyDto, type CreatedApiKeyDto } from 'marble-api'; +import { assertNever } from 'typescript-utils'; + +export const apiKeyRoleOptions = ['API_CLIENT'] as const; +type ApiKeyRole = (typeof apiKeyRoleOptions)[number]; + +function isApiKeyRole(role: string): role is ApiKeyRole { + return apiKeyRoleOptions.includes(role as ApiKeyRole); +} + +export interface ApiKey { + id: string; + organizationId: string; + description: string; + role: ApiKeyRole | 'UNKNWON'; +} + +export function adaptApiKey(apiKeyDto: ApiKeyDto): ApiKey { + const apiKey: ApiKey = { + id: apiKeyDto.id, + organizationId: apiKeyDto.organization_id, + description: apiKeyDto.description, + role: 'UNKNWON', + }; + if (isApiKeyRole(apiKeyDto.role)) { + apiKey.role = apiKeyDto.role; + } else { + // @ts-expect-error should be unreachable if all roles are handled + assertNever('[ApiKeyDto] Unknown role', apiKeyDto.role); + } + return apiKey; +} + +export type CreatedApiKey = ApiKey & { + key: string; +}; + +export function adaptCreatedApiKey(apiKey: CreatedApiKeyDto): CreatedApiKey { + return { + ...adaptApiKey(apiKey), + key: apiKey.key, + }; +} diff --git a/packages/app-builder/src/models/marble-session.ts b/packages/app-builder/src/models/marble-session.ts index bfef0bb67..ea0131e6e 100644 --- a/packages/app-builder/src/models/marble-session.ts +++ b/packages/app-builder/src/models/marble-session.ts @@ -1,11 +1,13 @@ import { type Session } from '@remix-run/node'; import { type Token } from 'marble-api'; +import { type CreatedApiKey } from './api-keys'; import { type AuthErrors } from './auth-errors'; import { type CurrentUser } from './user'; export type AuthData = { authToken: Token; lng: string; user: CurrentUser }; export type AuthFlashData = { authError: { message: AuthErrors }; + createdApiKey: CreatedApiKey; }; export type AuthSession = Session; diff --git a/packages/app-builder/src/repositories/ApiKeyRepository.ts b/packages/app-builder/src/repositories/ApiKeyRepository.ts new file mode 100644 index 000000000..a29f91f85 --- /dev/null +++ b/packages/app-builder/src/repositories/ApiKeyRepository.ts @@ -0,0 +1,37 @@ +import { type MarbleApi } from '@app-builder/infra/marble-api'; +import { + adaptApiKey, + adaptCreatedApiKey, + type ApiKey, + type CreatedApiKey, +} from '@app-builder/models/api-keys'; + +export interface ApiKeyRepository { + listApiKeys(): Promise; + createApiKey(args: { + description: string; + role: string; + }): Promise; + deleteApiKey(args: { apiKeyId: string }): Promise; +} + +export function getApiKeyRepository() { + return (marbleApiClient: MarbleApi): ApiKeyRepository => ({ + listApiKeys: async () => { + const { api_keys } = await marbleApiClient.listApiKeys(); + + return api_keys.map(adaptApiKey); + }, + createApiKey: async ({ description, role }) => { + const { api_key } = await marbleApiClient.createApiKey({ + description, + role, + }); + + return adaptCreatedApiKey(api_key); + }, + deleteApiKey: async ({ apiKeyId }) => { + await marbleApiClient.deleteApiKey(apiKeyId); + }, + }); +} diff --git a/packages/app-builder/src/repositories/init.server.ts b/packages/app-builder/src/repositories/init.server.ts index ef5d5b0fb..04216d368 100644 --- a/packages/app-builder/src/repositories/init.server.ts +++ b/packages/app-builder/src/repositories/init.server.ts @@ -1,5 +1,6 @@ import { type GetMarbleAPIClient } from '@app-builder/infra/marble-api'; +import { getApiKeyRepository } from './ApiKeyRepository'; import { getCaseRepository } from './CaseRepository'; import { getDataModelRepository } from './DataModelRepository'; import { getDecisionRepository } from './DecisionRepository'; @@ -40,6 +41,7 @@ export function makeServerRepositories({ scenarioRepository: getScenarioRepository(), organizationRepository: getOrganizationRepository(), dataModelRepository: getDataModelRepository(), + apiKeyRepository: getApiKeyRepository(), }; } diff --git a/packages/app-builder/src/routes/_builder+/settings+/_layout.tsx b/packages/app-builder/src/routes/_builder+/settings+/_layout.tsx index 3ab4f1b4a..86d95e8e0 100644 --- a/packages/app-builder/src/routes/_builder+/settings+/_layout.tsx +++ b/packages/app-builder/src/routes/_builder+/settings+/_layout.tsx @@ -39,12 +39,12 @@ export default function Settings() { to={getRoute('/settings/users')} /> - {/*
  • - -
  • */} +
  • + +
  • (); + export default function ApiKeys() { - return
    ApiKeys
    ; + const { t } = useTranslation(['settings']); + const { apiKeys, createdApiKey } = useLoaderData(); + + const columns = useMemo(() => { + return [ + columnHelper.accessor((row) => row.description, { + id: 'description', + header: t('settings:api_keys.description'), + size: 300, + }), + columnHelper.accessor((row) => row.role, { + id: 'role', + header: t('settings:api_keys.role'), + size: 150, + cell: ({ getValue }) => t(tKeyForApiKeyRole(getValue())), + }), + columnHelper.display({ + id: 'actions', + size: 100, + cell: ({ cell }) => { + return ( +
    + +
    + ); + }, + }), + ]; + }, [t]); + + const { table, getBodyProps, rows, getContainerProps } = useTable({ + data: apiKeys, + columns, + columnResizeMode: 'onChange', + getCoreRowModel: getCoreRowModel(), + }); + + return ( + + + {createdApiKey ? : null} + + + {t('settings:tags')} + + + + + + + {rows.map((row) => { + return ( + + ); + })} + + + + + + + ); +} + +function CreatedAPIKey({ createdApiKey }: { createdApiKey: CreatedApiKey }) { + const { t } = useTranslation(['settings']); + return ( + +
    + {t('settings:api_keys.new_api_key')} + {t('settings:api_keys.copy_api_key')} + + {createdApiKey.key} + +
    +
    + ); } diff --git a/packages/app-builder/src/routes/ressources+/auth+/refresh.tsx b/packages/app-builder/src/routes/ressources+/auth+/refresh.tsx index 1c3a1366e..284a66f60 100644 --- a/packages/app-builder/src/routes/ressources+/auth+/refresh.tsx +++ b/packages/app-builder/src/routes/ressources+/auth+/refresh.tsx @@ -34,7 +34,7 @@ export function useRefreshToken() { .authenticationClientRepository; firebaseIdToken().then( - (idToken) => { + (idToken: string) => { submit( { idToken, csrf }, { method: 'POST', action: getRoute('/ressources/auth/refresh') }, diff --git a/packages/app-builder/src/routes/ressources+/settings+/api-keys+/create.tsx b/packages/app-builder/src/routes/ressources+/settings+/api-keys+/create.tsx new file mode 100644 index 000000000..5f3b6ab4c --- /dev/null +++ b/packages/app-builder/src/routes/ressources+/settings+/api-keys+/create.tsx @@ -0,0 +1,156 @@ +import { FormError } from '@app-builder/components/Form/FormError'; +import { FormField } from '@app-builder/components/Form/FormField'; +import { FormInput } from '@app-builder/components/Form/FormInput'; +import { FormLabel } from '@app-builder/components/Form/FormLabel'; +import { FormSelect } from '@app-builder/components/Form/FormSelect'; +import { setToastMessage } from '@app-builder/components/MarbleToaster'; +import { apiKeyRoleOptions } from '@app-builder/models/api-keys'; +import { tKeyForApiKeyRole } from '@app-builder/services/i18n/translation-keys/api-key'; +import { serverServices } from '@app-builder/services/init.server'; +import { getRoute } from '@app-builder/utils/routes'; +import { useForm } from '@conform-to/react'; +import { getFieldsetConstraint, parse } from '@conform-to/zod'; +import { type ActionFunctionArgs, json, redirect } from '@remix-run/node'; +import { useFetcher, useNavigation } from '@remix-run/react'; +import { useEffect, useId, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { AuthenticityTokenInput } from 'remix-utils/csrf/react'; +import { Button, Modal } from 'ui-design-system'; +import { Icon } from 'ui-icons'; +import { z } from 'zod'; + +const createApiKeyFormSchema = z.object({ + description: z.string().min(1), + role: z.enum(apiKeyRoleOptions), +}); + +export async function action({ request }: ActionFunctionArgs) { + const { authService, csrfService, toastSessionService, authSessionService } = + serverServices; + const { apiKey } = await authService.isAuthenticated(request, { + failureRedirect: getRoute('/sign-in'), + }); + await csrfService.validate(request); + + const formData = await request.formData(); + const submission = parse(formData, { schema: createApiKeyFormSchema }); + + if (submission.intent !== 'submit' || !submission.value) { + return json(submission); + } + + try { + const createdApiKey = await apiKey.createApiKey(submission.value); + + const authSession = await authSessionService.getSession(request); + authSession.flash('createdApiKey', createdApiKey); + + return redirect(getRoute('/settings/api-keys'), { + headers: { + 'Set-Cookie': await authSessionService.commitSession(authSession), + }, + }); + } catch (error) { + const toastSession = await toastSessionService.getSession(request); + setToastMessage(toastSession, { + type: 'error', + messageKey: 'common:errors.unknown', + }); + return json(submission, { + headers: { + 'Set-Cookie': await toastSessionService.commitSession(toastSession), + }, + }); + } +} + +export function CreateApiKey() { + const { t } = useTranslation(['settings']); + const [open, setOpen] = useState(false); + + const navigation = useNavigation(); + useEffect(() => { + if (navigation.state === 'loading') { + setOpen(false); + } + }, [navigation.state]); + + return ( + + e.stopPropagation()} asChild> + + + e.stopPropagation()}> + + + + ); +} + +const CreateApiKeyContent = () => { + const { t } = useTranslation(['settings', 'common']); + const fetcher = useFetcher(); + + const formId = useId(); + const [form, { description, role }] = useForm({ + id: formId, + defaultValue: { description: '', role: 'API_CLIENT' }, + lastSubmission: fetcher.data, + constraint: getFieldsetConstraint(createApiKeyFormSchema), + onValidate({ formData }) { + return parse(formData, { + schema: createApiKeyFormSchema, + }); + }, + }); + + return ( + + {t('settings:api_keys.new_api_key')} +
    + + + {t('settings:api_keys.description')} + + + + + {t('settings:api_keys.role')} + + {apiKeyRoleOptions.map((role) => ( + + {t(tKeyForApiKeyRole(role))} + + ))} + + + +
    + + + + +
    +
    +
    + ); +}; diff --git a/packages/app-builder/src/routes/ressources+/settings+/api-keys+/delete.tsx b/packages/app-builder/src/routes/ressources+/settings+/api-keys+/delete.tsx new file mode 100644 index 000000000..278bdc834 --- /dev/null +++ b/packages/app-builder/src/routes/ressources+/settings+/api-keys+/delete.tsx @@ -0,0 +1,91 @@ +import { type ApiKey } from '@app-builder/models/api-keys'; +import { serverServices } from '@app-builder/services/init.server'; +import { parseForm } from '@app-builder/utils/input-validation'; +import { getRoute } from '@app-builder/utils/routes'; +import { type ActionFunctionArgs, redirect } from '@remix-run/node'; +import { Form, useNavigation } from '@remix-run/react'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Button, Modal } from 'ui-design-system'; +import { Icon } from 'ui-icons'; +import { z } from 'zod'; + +const deleteApiKeyFormSchema = z.object({ + apiKeyId: z.string().uuid(), +}); + +export async function action({ request }: ActionFunctionArgs) { + const { authService } = serverServices; + const { apiKey } = await authService.isAuthenticated(request, { + failureRedirect: getRoute('/sign-in'), + }); + + const formData = await parseForm(request, deleteApiKeyFormSchema); + + await apiKey.deleteApiKey(formData); + return redirect(getRoute('/settings/api-keys')); +} + +export function DeleteApiKey({ apiKey }: { apiKey: ApiKey }) { + const { t } = useTranslation(['settings']); + + const [open, setOpen] = useState(false); + + const navigation = useNavigation(); + useEffect(() => { + if (navigation.state === 'loading') { + setOpen(false); + } + }, [navigation.state]); + + return ( + + + + + + + + + ); +} + +function DeleteApiKeyContent({ apiKey }: { apiKey: ApiKey }) { + const { t } = useTranslation(['settings', 'common']); + + return ( +
    + {t('settings:api_keys.delete')} +
    +
    + +

    {t('settings:api_keys.delete.content')}

    +
    +
    + + + + +
    +
    +
    + ); +} diff --git a/packages/app-builder/src/routes/ressources+/settings+/inboxes+/inbox-users.create.tsx b/packages/app-builder/src/routes/ressources+/settings+/inboxes+/inbox-users.create.tsx index cf88a6eef..0e3f19b7b 100644 --- a/packages/app-builder/src/routes/ressources+/settings+/inboxes+/inbox-users.create.tsx +++ b/packages/app-builder/src/routes/ressources+/settings+/inboxes+/inbox-users.create.tsx @@ -15,7 +15,7 @@ import { useFetcher, useNavigation } from '@remix-run/react'; import { type Namespace } from 'i18next'; import { useEffect, useId, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Button, Modal, Select } from 'ui-design-system'; +import { Button, Modal } from 'ui-design-system'; import { Icon } from 'ui-icons'; import { z } from 'zod'; @@ -136,10 +136,7 @@ export function CreateInboxUserContent({
    - + {t('settings:inboxes.inbox_details.user')} {userOptions.map(({ id, name }) => ( @@ -154,9 +151,9 @@ export function CreateInboxUserContent({ {t('settings:inboxes.inbox_details.role')} {roleOptions.map((role) => ( - + {t(tKeyForInboxUserRole(role))} - + ))} diff --git a/packages/app-builder/src/routes/ressources+/settings+/inboxes+/inbox-users.update.tsx b/packages/app-builder/src/routes/ressources+/settings+/inboxes+/inbox-users.update.tsx index 27f45a76f..943cf8a9e 100644 --- a/packages/app-builder/src/routes/ressources+/settings+/inboxes+/inbox-users.update.tsx +++ b/packages/app-builder/src/routes/ressources+/settings+/inboxes+/inbox-users.update.tsx @@ -129,10 +129,7 @@ export function UpdateInboxUserContent({
    - + {t('settings:inboxes.inbox_details.role')} {roleOptions.map((role) => ( diff --git a/packages/app-builder/src/routes/ressources+/settings+/tags+/create.tsx b/packages/app-builder/src/routes/ressources+/settings+/tags+/create.tsx index fba54de9e..7fdb39b67 100644 --- a/packages/app-builder/src/routes/ressources+/settings+/tags+/create.tsx +++ b/packages/app-builder/src/routes/ressources+/settings+/tags+/create.tsx @@ -116,7 +116,7 @@ const CreateTagContent = () => { > {t('settings:tags.new_tag')}
    -
    +
    {t('settings:tags.name')} diff --git a/packages/app-builder/src/routes/ressources+/settings+/tags+/update.tsx b/packages/app-builder/src/routes/ressources+/settings+/tags+/update.tsx index 07ca7de85..b71071ec1 100644 --- a/packages/app-builder/src/routes/ressources+/settings+/tags+/update.tsx +++ b/packages/app-builder/src/routes/ressources+/settings+/tags+/update.tsx @@ -113,7 +113,7 @@ const UpdateTagContent = ({ tag }: { tag: Tag }) => { > {t('settings:tags.new_tag')}
    -
    +
    {t('settings:tags.name')} diff --git a/packages/app-builder/src/routes/ressources+/settings+/users+/create.tsx b/packages/app-builder/src/routes/ressources+/settings+/users+/create.tsx index 3b84e58b9..bb4380e00 100644 --- a/packages/app-builder/src/routes/ressources+/settings+/users+/create.tsx +++ b/packages/app-builder/src/routes/ressources+/settings+/users+/create.tsx @@ -135,7 +135,7 @@ const CreateUserContent = ({ orgId }: { orgId: string }) => { > {t('settings:users.new_user')}
    -
    +
    diff --git a/packages/app-builder/src/routes/ressources+/settings+/users+/update.tsx b/packages/app-builder/src/routes/ressources+/settings+/users+/update.tsx index 684af75aa..5069e17e6 100644 --- a/packages/app-builder/src/routes/ressources+/settings+/users+/update.tsx +++ b/packages/app-builder/src/routes/ressources+/settings+/users+/update.tsx @@ -111,7 +111,7 @@ const UpdateUserContent = ({ user }: { user: User }) => { > {t('settings:users.update_user')}
    -
    +
    diff --git a/packages/app-builder/src/services/auth/auth.server.ts b/packages/app-builder/src/services/auth/auth.server.ts index b89d9b8c6..7e4748815 100644 --- a/packages/app-builder/src/services/auth/auth.server.ts +++ b/packages/app-builder/src/services/auth/auth.server.ts @@ -5,6 +5,7 @@ import { type AuthFlashData, type CurrentUser, } from '@app-builder/models'; +import { type ApiKeyRepository } from '@app-builder/repositories/ApiKeyRepository'; import { type CaseRepository } from '@app-builder/repositories/CaseRepository'; import { type DataModelRepository } from '@app-builder/repositories/DataModelRepository'; import { type DecisionRepository } from '@app-builder/repositories/DecisionRepository'; @@ -31,6 +32,7 @@ interface AuthenticatedInfo { decision: DecisionRepository; cases: CaseRepository; dataModelRepository: DataModelRepository; + apiKey: ApiKeyRepository; organization: OrganizationRepository; scenario: ScenarioRepository; user: CurrentUser; @@ -91,6 +93,7 @@ export function makeAuthenticationServerService( ) => OrganizationRepository, scenarioRepository: (marbleApiClient: MarbleApi) => ScenarioRepository, dataModelRepository: (marbleApiClient: MarbleApi) => DataModelRepository, + apiKeysRepository: (marbleApiClient: MarbleApi) => ApiKeyRepository, authSessionService: SessionService, csrfService: CSRF, ) { @@ -246,6 +249,7 @@ export function makeAuthenticationServerService( scenario: scenarioRepository(apiClient), organization: organizationRepository(apiClient, user.organizationId), dataModelRepository: dataModelRepository(apiClient), + apiKey: apiKeysRepository(apiClient), user, inbox: inboxRepository(apiClient), }; diff --git a/packages/app-builder/src/services/i18n/translation-keys/api-key.ts b/packages/app-builder/src/services/i18n/translation-keys/api-key.ts new file mode 100644 index 000000000..80c33cafb --- /dev/null +++ b/packages/app-builder/src/services/i18n/translation-keys/api-key.ts @@ -0,0 +1,10 @@ +import { type ApiKey } from '@app-builder/models/api-keys'; + +export function tKeyForApiKeyRole(role: ApiKey['role']) { + switch (role) { + case 'API_CLIENT': + return 'settings:api_keys.role.api_client'; + default: + return 'settings:api_keys.role.unknown'; + } +} diff --git a/packages/app-builder/src/services/init.server.ts b/packages/app-builder/src/services/init.server.ts index f22f11a9d..f4577f4b5 100644 --- a/packages/app-builder/src/services/init.server.ts +++ b/packages/app-builder/src/services/init.server.ts @@ -36,6 +36,7 @@ function makeServerServices(repositories: ServerRepositories) { repositories.organizationRepository, repositories.scenarioRepository, repositories.dataModelRepository, + repositories.apiKeyRepository, authSessionService, csrfService, ), diff --git a/packages/app-builder/src/utils/routes/routes.ts b/packages/app-builder/src/utils/routes/routes.ts index dca817ef1..9378218e7 100644 --- a/packages/app-builder/src/utils/routes/routes.ts +++ b/packages/app-builder/src/utils/routes/routes.ts @@ -350,6 +350,16 @@ export const routes = [ "path": "ressources/scenarios/update", "file": "routes/ressources+/scenarios+/update.tsx" }, + { + "id": "routes/ressources+/settings+/api-keys+/create", + "path": "ressources/settings/api-keys/create", + "file": "routes/ressources+/settings+/api-keys+/create.tsx" + }, + { + "id": "routes/ressources+/settings+/api-keys+/delete", + "path": "ressources/settings/api-keys/delete", + "file": "routes/ressources+/settings+/api-keys+/delete.tsx" + }, { "id": "routes/ressources+/settings+/inboxes+/create", "path": "ressources/settings/inboxes/create", diff --git a/packages/app-builder/src/utils/routes/types.ts b/packages/app-builder/src/utils/routes/types.ts index 063a540f1..cdfaf90d5 100644 --- a/packages/app-builder/src/utils/routes/types.ts +++ b/packages/app-builder/src/utils/routes/types.ts @@ -59,6 +59,8 @@ export type RoutePath = | '/ressources/scenarios/create' | '/ressources/scenarios/deployment' | '/ressources/scenarios/update' + | '/ressources/settings/api-keys/create' + | '/ressources/settings/api-keys/delete' | '/ressources/settings/inboxes/create' | '/ressources/settings/inboxes/delete' | '/ressources/settings/inboxes/inbox-users/create' @@ -141,6 +143,8 @@ export type RouteID = | 'routes/ressources+/scenarios+/create' | 'routes/ressources+/scenarios+/deployment' | 'routes/ressources+/scenarios+/update' + | 'routes/ressources+/settings+/api-keys+/create' + | 'routes/ressources+/settings+/api-keys+/delete' | 'routes/ressources+/settings+/inboxes+/create' | 'routes/ressources+/settings+/inboxes+/delete' | 'routes/ressources+/settings+/inboxes+/inbox-users.create' diff --git a/packages/app-builder/src/utils/use-get-copy-to-clipboard.ts b/packages/app-builder/src/utils/use-get-copy-to-clipboard.ts deleted file mode 100644 index 1125c52f3..000000000 --- a/packages/app-builder/src/utils/use-get-copy-to-clipboard.ts +++ /dev/null @@ -1,28 +0,0 @@ -import toast from 'react-hot-toast'; -import { useTranslation } from 'react-i18next'; - -export function useGetCopyToClipboard() { - const { t } = useTranslation('common'); - return (value: string) => ({ - type: 'button', - 'aria-label': t('clipboard.aria-label', { - replace: { - value, - }, - }), - onClick: async () => { - try { - await navigator.clipboard.writeText(value); - toast.success( - t('clipboard.copy', { - replace: { - value, - }, - }), - ); - } catch (err) { - toast.error(t('errors.unknown')); - } - }, - }); -} diff --git a/packages/app-builder/src/utils/use-get-copy-to-clipboard.tsx b/packages/app-builder/src/utils/use-get-copy-to-clipboard.tsx new file mode 100644 index 000000000..4d3439938 --- /dev/null +++ b/packages/app-builder/src/utils/use-get-copy-to-clipboard.tsx @@ -0,0 +1,37 @@ +import toast from 'react-hot-toast'; +import { Trans, useTranslation } from 'react-i18next'; + +export function useGetCopyToClipboard() { + const { t } = useTranslation('common'); + return (value: string) => ({ + type: 'button', + 'aria-label': t('clipboard.aria-label', { + replace: { + value, + }, + }), + onClick: async () => { + try { + await navigator.clipboard.writeText(value); + toast.success(() => ( + + + ), + }} + values={{ + value, + }} + /> + + )); + } catch (err) { + toast.error(t('errors.unknown')); + } + }, + }); +} diff --git a/packages/marble-api/scripts/openapi.yaml b/packages/marble-api/scripts/openapi.yaml index 18278d4d3..fc6f62582 100644 --- a/packages/marble-api/scripts/openapi.yaml +++ b/packages/marble-api/scripts/openapi.yaml @@ -1892,7 +1892,64 @@ paths: api_keys: type: array items: - $ref: '#/components/schemas/ApiKey' + $ref: '#/components/schemas/ApiKeyDto' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + post: + tags: + - Authorization + summary: Create an api key + operationId: createApiKey + security: + - bearerAuth: [] + requestBody: + description: 'Describe the api key to create' + content: + application/json: + schema: + $ref: '#/components/schemas/CreateApiKey' + required: true + responses: + '200': + description: The created api key + content: + application/json: + schema: + type: object + required: + - api_key + properties: + api_key: + $ref: '#/components/schemas/CreatedApiKeyDto' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + /apikeys/{apiKeyId}: + delete: + tags: + - Authorization + summary: Delete an api key + operationId: deleteApiKey + security: + - bearerAuth: [] + parameters: + - name: apiKeyId + description: ID of the api key that need to be deleted + in: path + required: true + schema: + type: string + format: uuid + responses: + '204': + description: The api key has been deleted '401': $ref: '#/components/responses/401' '403': @@ -2838,23 +2895,42 @@ components: type: string last_name: type: string - ApiKey: + ApiKeyDto: type: object required: - - api_key_id + - id - organization_id - - key + - description - role properties: - api_key_id: + id: type: string organization_id: type: string format: uuid - key: + description: type: string role: type: string + CreateApiKey: + type: object + required: + - description + - role + properties: + description: + type: string + role: + type: string + CreatedApiKeyDto: + allOf: + - $ref: '#/components/schemas/ApiKeyDto' + - type: object + required: + - key + properties: + key: + type: string LinkToSingleDto: type: object required: diff --git a/packages/marble-api/src/generated/marble-api.ts b/packages/marble-api/src/generated/marble-api.ts index ac238040b..a9cafe111 100644 --- a/packages/marble-api/src/generated/marble-api.ts +++ b/packages/marble-api/src/generated/marble-api.ts @@ -414,12 +414,19 @@ export type OpenApiSpec = { securitySchemes?: object; }; }; -export type ApiKey = { - api_key_id: string; +export type ApiKeyDto = { + id: string; organization_id: string; - key: string; + description: string; + role: string; +}; +export type CreateApiKey = { + description: string; role: string; }; +export type CreatedApiKeyDto = ApiKeyDto & { + key: string; +}; export type UserDto = { user_id: string; email: string; @@ -1718,7 +1725,7 @@ export function listApiKeys(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; data: { - api_keys: ApiKey[]; + api_keys: ApiKeyDto[]; }; } | { status: 401; @@ -1733,6 +1740,50 @@ export function listApiKeys(opts?: Oazapfts.RequestOpts) { ...opts })); } +/** + * Create an api key + */ +export function createApiKey(createApiKey: CreateApiKey, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: { + api_key: CreatedApiKeyDto; + }; + } | { + status: 401; + data: string; + } | { + status: 403; + data: string; + } | { + status: 404; + data: string; + }>("/apikeys", oazapfts.json({ + ...opts, + method: "POST", + body: createApiKey + }))); +} +/** + * Delete an api key + */ +export function deleteApiKey(apiKeyId: string, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 204; + } | { + status: 401; + data: string; + } | { + status: 403; + data: string; + } | { + status: 404; + data: string; + }>(`/apikeys/${encodeURIComponent(apiKeyId)}`, { + ...opts, + method: "DELETE" + })); +} /** * List all users present in the database */