diff --git a/apps/backend/src/api/routes/users.controller.ts b/apps/backend/src/api/routes/users.controller.ts index 7f195a60..79882c38 100644 --- a/apps/backend/src/api/routes/users.controller.ts +++ b/apps/backend/src/api/routes/users.controller.ts @@ -140,6 +140,12 @@ export class UsersController { return this._userService.updateEmailNotifications(user.id, body); } + @Post('/api-key/rotate') + @CheckPolicies([AuthorizationActions.Create, Sections.ADMIN]) + async rotateApiKey(@GetOrgFromRequest() organization: Organization) { + return this._orgService.updateApiKey(organization.id); + } + @Get('/subscription') @CheckPolicies([AuthorizationActions.Create, Sections.ADMIN]) async getSubscription(@GetOrgFromRequest() organization: Organization) { diff --git a/apps/frontend/src/components/public-api/public.component.tsx b/apps/frontend/src/components/public-api/public.component.tsx index 570342bc..ef3f9b7e 100644 --- a/apps/frontend/src/components/public-api/public.component.tsx +++ b/apps/frontend/src/components/public-api/public.component.tsx @@ -1,16 +1,22 @@ 'use client'; import { useState, useCallback } from 'react'; +import { useSWRConfig } from 'swr'; import { useUser } from '../layout/user.context'; import { Button } from '@gitroom/react/form/button'; import copy from 'copy-to-clipboard'; import { useToaster } from '@gitroom/react/toaster/toaster'; import { useVariables } from '@gitroom/react/helpers/variable.context'; import { useT } from '@gitroom/react/translation/get.transation.service.client'; +import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; +import { useDecisionModal } from '@gitroom/frontend/components/layout/new-modal'; export const PublicComponent = () => { const user = useUser(); const { backendUrl, frontEndUrl } = useVariables(); const toaster = useToaster(); + const fetch = useFetch(); + const decision = useDecisionModal(); + const { mutate } = useSWRConfig(); const [reveal, setReveal] = useState(false); const [reveal2, setReveal2] = useState(false); const copyToClipboard = useCallback(() => { @@ -22,6 +28,22 @@ export const PublicComponent = () => { copy(`${backendUrl}/mcp/` + user?.publicApi); }, [user]); + const rotateKey = useCallback(async () => { + const approved = await decision.open({ + title: 'Rotate API Key?', + description: + 'This will generate a new API key and invalidate the current one. Any integrations using the old key will stop working.', + approveLabel: 'Rotate', + cancelLabel: 'Cancel', + }); + if (!approved) return; + await fetch('/user/api-key/rotate', { method: 'POST' }); + await mutate('/user/self'); + setReveal(false); + setReveal2(false); + toaster.show('API Key rotated successfully', 'success'); + }, [decision, fetch, mutate, toaster]); + const t = useT(); if (!user || !user.publicApi) { @@ -80,6 +102,11 @@ export const PublicComponent = () => { )} +
+ +