diff --git a/apiserver/plane/app/urls/api.py b/apiserver/plane/app/urls/api.py index 592ff53b5..c74aeddbf 100644 --- a/apiserver/plane/app/urls/api.py +++ b/apiserver/plane/app/urls/api.py @@ -4,14 +4,14 @@ from plane.app.views import ApiTokenEndpoint, ServiceApiTokenEndpoint urlpatterns = [ # API Tokens path( - "workspaces//api-tokens/", + "users/api-tokens/", ApiTokenEndpoint.as_view(), name="api-tokens", ), path( - "workspaces//api-tokens//", + "users/api-tokens//", ApiTokenEndpoint.as_view(), - name="api-tokens", + name="api-tokens-details", ), path( "workspaces//service-api-tokens/", diff --git a/apiserver/plane/app/views/api.py b/apiserver/plane/app/views/api.py index 98a2588a1..fa7cc7466 100644 --- a/apiserver/plane/app/views/api.py +++ b/apiserver/plane/app/views/api.py @@ -1,8 +1,10 @@ # Python import from uuid import uuid4 +from typing import Optional # Third party from rest_framework.response import Response +from rest_framework.request import Request from rest_framework import status # Module import @@ -13,12 +15,9 @@ from plane.app.permissions import WorkspaceEntityPermission class ApiTokenEndpoint(BaseAPIView): - permission_classes = [WorkspaceEntityPermission] - - def post(self, request, slug): + def post(self, request: Request) -> Response: label = request.data.get("label", str(uuid4().hex)) description = request.data.get("description", "") - workspace = Workspace.objects.get(slug=slug) expired_at = request.data.get("expired_at", None) # Check the user type @@ -28,7 +27,6 @@ class ApiTokenEndpoint(BaseAPIView): label=label, description=description, user=request.user, - workspace=workspace, user_type=user_type, expired_at=expired_at, ) @@ -37,29 +35,23 @@ class ApiTokenEndpoint(BaseAPIView): # Token will be only visible while creating return Response(serializer.data, status=status.HTTP_201_CREATED) - def get(self, request, slug, pk=None): + def get(self, request: Request, pk: Optional[str] = None) -> Response: if pk is None: - api_tokens = APIToken.objects.filter( - user=request.user, workspace__slug=slug, is_service=False - ) + api_tokens = APIToken.objects.filter(user=request.user, is_service=False) serializer = APITokenReadSerializer(api_tokens, many=True) return Response(serializer.data, status=status.HTTP_200_OK) else: - api_tokens = APIToken.objects.get( - user=request.user, workspace__slug=slug, pk=pk - ) + api_tokens = APIToken.objects.get(user=request.user, pk=pk) serializer = APITokenReadSerializer(api_tokens) return Response(serializer.data, status=status.HTTP_200_OK) - def delete(self, request, slug, pk): - api_token = APIToken.objects.get( - workspace__slug=slug, user=request.user, pk=pk, is_service=False - ) + def delete(self, request: Request, pk: str) -> Response: + api_token = APIToken.objects.get(user=request.user, pk=pk, is_service=False) api_token.delete() return Response(status=status.HTTP_204_NO_CONTENT) - def patch(self, request, slug, pk): - api_token = APIToken.objects.get(workspace__slug=slug, user=request.user, pk=pk) + def patch(self, request: Request, pk: str) -> Response: + api_token = APIToken.objects.get(user=request.user, pk=pk) serializer = APITokenSerializer(api_token, data=request.data, partial=True) if serializer.is_valid(): serializer.save() @@ -70,7 +62,7 @@ class ApiTokenEndpoint(BaseAPIView): class ServiceApiTokenEndpoint(BaseAPIView): permission_classes = [WorkspaceEntityPermission] - def post(self, request, slug): + def post(self, request: Request, slug: str) -> Response: workspace = Workspace.objects.get(slug=slug) api_token = APIToken.objects.filter( diff --git a/apiserver/plane/tests/conftest.py b/apiserver/plane/tests/conftest.py index ce0d3be2b..832558810 100644 --- a/apiserver/plane/tests/conftest.py +++ b/apiserver/plane/tests/conftest.py @@ -27,7 +27,7 @@ def user_data(): "email": "test@plane.so", "password": "test-password", "first_name": "Test", - "last_name": "User" + "last_name": "User", } @@ -37,7 +37,7 @@ def create_user(db, user_data): user = User.objects.create( email=user_data["email"], first_name=user_data["first_name"], - last_name=user_data["last_name"] + last_name=user_data["last_name"], ) user.set_password(user_data["password"]) user.save() @@ -69,10 +69,52 @@ def session_client(api_client, create_user): return api_client +@pytest.fixture +def create_bot_user(db): + """Create and return a bot user instance""" + from uuid import uuid4 + + unique_id = uuid4().hex[:8] + user = User.objects.create( + email=f"bot-{unique_id}@plane.so", + username=f"bot_user_{unique_id}", + first_name="Bot", + last_name="User", + is_bot=True, + ) + user.set_password("bot@123") + user.save() + return user + + +@pytest.fixture +def api_token_data(): + """Return sample API token data for testing""" + from django.utils import timezone + from datetime import timedelta + + return { + "label": "Test API Token", + "description": "Test description for API token", + "expired_at": (timezone.now() + timedelta(days=30)).isoformat(), + } + + +@pytest.fixture +def create_api_token_for_user(db, create_user): + """Create and return an API token for a specific user""" + return APIToken.objects.create( + label="Test Token", + description="Test token description", + user=create_user, + user_type=0, + ) + + @pytest.fixture def plane_server(live_server): """ Renamed version of live_server fixture to avoid name clashes. Returns a live Django server for testing HTTP requests. """ - return live_server \ No newline at end of file + return live_server diff --git a/apiserver/plane/tests/contract/app/test_api_token.py b/apiserver/plane/tests/contract/app/test_api_token.py new file mode 100644 index 000000000..5160788de --- /dev/null +++ b/apiserver/plane/tests/contract/app/test_api_token.py @@ -0,0 +1,372 @@ +import pytest +from datetime import timedelta +from uuid import uuid4 +from django.urls import reverse +from django.utils import timezone +from rest_framework import status + +from plane.db.models import APIToken, User + + +@pytest.mark.contract +class TestApiTokenEndpoint: + """Test cases for ApiTokenEndpoint""" + + # POST /user/api-tokens/ tests + @pytest.mark.django_db + def test_create_api_token_success( + self, session_client, create_user, api_token_data + ): + """Test successful API token creation""" + # Arrange + session_client.force_authenticate(user=create_user) + url = reverse("api-tokens") + + # Act + response = session_client.post(url, api_token_data, format="json") + + # Assert + assert response.status_code == status.HTTP_201_CREATED + assert "token" in response.data + assert response.data["label"] == api_token_data["label"] + assert response.data["description"] == api_token_data["description"] + assert response.data["user_type"] == 0 # Human user + + # Verify token was created in database + token = APIToken.objects.get(pk=response.data["id"]) + assert token.user == create_user + assert token.label == api_token_data["label"] + + @pytest.mark.django_db + def test_create_api_token_for_bot_user( + self, session_client, create_bot_user, api_token_data + ): + """Test API token creation for bot user""" + # Arrange + session_client.force_authenticate(user=create_bot_user) + url = reverse("api-tokens") + + # Act + response = session_client.post(url, api_token_data, format="json") + + # Assert + assert response.status_code == status.HTTP_201_CREATED + assert response.data["user_type"] == 1 # Bot user + + @pytest.mark.django_db + def test_create_api_token_minimal_data(self, session_client, create_user): + """Test API token creation with minimal data""" + # Arrange + session_client.force_authenticate(user=create_user) + url = reverse("api-tokens") + + # Act + response = session_client.post(url, {}, format="json") + + # Assert + assert response.status_code == status.HTTP_201_CREATED + assert "token" in response.data + assert len(response.data["label"]) == 32 # UUID hex length + assert response.data["description"] == "" + + @pytest.mark.django_db + def test_create_api_token_with_expiry(self, session_client, create_user): + """Test API token creation with expiry date""" + # Arrange + session_client.force_authenticate(user=create_user) + url = reverse("api-tokens") + future_date = timezone.now() + timedelta(days=30) + data = {"label": "Expiring Token", "expired_at": future_date.isoformat()} + + # Act + response = session_client.post(url, data, format="json") + + # Assert + assert response.status_code == status.HTTP_201_CREATED + + # Verify expiry date was set + token = APIToken.objects.get(pk=response.data["id"]) + assert token.expired_at is not None + + @pytest.mark.django_db + def test_create_api_token_unauthenticated(self, api_client, api_token_data): + """Test API token creation without authentication""" + # Arrange + url = reverse("api-tokens") + + # Act + response = api_client.post(url, api_token_data, format="json") + + # Assert + assert response.status_code == status.HTTP_401_UNAUTHORIZED + + # GET /user/api-tokens/ tests + @pytest.mark.django_db + def test_get_all_api_tokens(self, session_client, create_user): + """Test retrieving all API tokens for user""" + # Arrange + session_client.force_authenticate(user=create_user) + + # Create multiple tokens + APIToken.objects.create(label="Token 1", user=create_user, user_type=0) + APIToken.objects.create(label="Token 2", user=create_user, user_type=0) + # Create a service token (should be excluded) + APIToken.objects.create( + label="Service Token", user=create_user, user_type=0, is_service=True + ) + url = reverse("api-tokens") + + # Act + response = session_client.get(url) + + # Assert + assert response.status_code == status.HTTP_200_OK + assert len(response.data) == 2 # Only non-service tokens + assert all(token["is_service"] is False for token in response.data) + + @pytest.mark.django_db + def test_get_empty_api_tokens_list(self, session_client, create_user): + """Test retrieving API tokens when none exist""" + # Arrange + session_client.force_authenticate(user=create_user) + url = reverse("api-tokens") + + # Act + response = session_client.get(url) + + # Assert + assert response.status_code == status.HTTP_200_OK + assert response.data == [] + + # GET /user/api-tokens// tests + @pytest.mark.django_db + def test_get_specific_api_token( + self, session_client, create_user, create_api_token_for_user + ): + """Test retrieving a specific API token""" + # Arrange + session_client.force_authenticate(user=create_user) + url = reverse("api-tokens", kwargs={"pk": create_api_token_for_user.pk}) + + # Act + response = session_client.get(url) + + # Assert + assert response.status_code == status.HTTP_200_OK + assert str(response.data["id"]) == str(create_api_token_for_user.pk) + assert response.data["label"] == create_api_token_for_user.label + assert ( + "token" not in response.data + ) # Token should not be visible in read serializer + + @pytest.mark.django_db + def test_get_nonexistent_api_token(self, session_client, create_user): + """Test retrieving a non-existent API token""" + # Arrange + session_client.force_authenticate(user=create_user) + fake_pk = uuid4() + url = reverse("api-tokens", kwargs={"pk": fake_pk}) + + # Act + response = session_client.get(url) + + # Assert + assert response.status_code == status.HTTP_404_NOT_FOUND + + @pytest.mark.django_db + def test_get_other_users_api_token(self, session_client, create_user, db): + """Test retrieving another user's API token (should fail)""" + # Arrange + # Create another user and their token with unique email and username + unique_id = uuid4().hex[:8] + unique_email = f"other-{unique_id}@plane.so" + unique_username = f"other_user_{unique_id}" + other_user = User.objects.create(email=unique_email, username=unique_username) + other_token = APIToken.objects.create( + label="Other Token", user=other_user, user_type=0 + ) + session_client.force_authenticate(user=create_user) + url = reverse("api-tokens", kwargs={"pk": other_token.pk}) + + # Act + response = session_client.get(url) + + # Assert + assert response.status_code == status.HTTP_404_NOT_FOUND + + # DELETE /user/api-tokens// tests + @pytest.mark.django_db + def test_delete_api_token_success( + self, session_client, create_user, create_api_token_for_user + ): + """Test successful API token deletion""" + # Arrange + session_client.force_authenticate(user=create_user) + url = reverse("api-tokens", kwargs={"pk": create_api_token_for_user.pk}) + + # Act + response = session_client.delete(url) + + # Assert + assert response.status_code == status.HTTP_204_NO_CONTENT + assert not APIToken.objects.filter(pk=create_api_token_for_user.pk).exists() + + @pytest.mark.django_db + def test_delete_nonexistent_api_token(self, session_client, create_user): + """Test deleting a non-existent API token""" + # Arrange + session_client.force_authenticate(user=create_user) + fake_pk = uuid4() + url = reverse("api-tokens", kwargs={"pk": fake_pk}) + + # Act + response = session_client.delete(url) + + # Assert + assert response.status_code == status.HTTP_404_NOT_FOUND + + @pytest.mark.django_db + def test_delete_other_users_api_token(self, session_client, create_user, db): + """Test deleting another user's API token (should fail)""" + # Arrange + # Create another user and their token with unique email and username + unique_id = uuid4().hex[:8] + unique_email = f"delete-other-{unique_id}@plane.so" + unique_username = f"delete_other_user_{unique_id}" + other_user = User.objects.create(email=unique_email, username=unique_username) + other_token = APIToken.objects.create( + label="Other Token", user=other_user, user_type=0 + ) + session_client.force_authenticate(user=create_user) + url = reverse("api-tokens", kwargs={"pk": other_token.pk}) + + # Act + response = session_client.delete(url) + + # Assert + assert response.status_code == status.HTTP_404_NOT_FOUND + # Verify token still exists + assert APIToken.objects.filter(pk=other_token.pk).exists() + + @pytest.mark.django_db + def test_delete_service_api_token_forbidden(self, session_client, create_user): + """Test deleting a service API token (should fail)""" + # Arrange + service_token = APIToken.objects.create( + label="Service Token", user=create_user, user_type=0, is_service=True + ) + session_client.force_authenticate(user=create_user) + url = reverse("api-tokens", kwargs={"pk": service_token.pk}) + + # Act + response = session_client.delete(url) + + # Assert + assert response.status_code == status.HTTP_404_NOT_FOUND + # Verify token still exists + assert APIToken.objects.filter(pk=service_token.pk).exists() + + # PATCH /user/api-tokens// tests + @pytest.mark.django_db + def test_patch_api_token_success( + self, session_client, create_user, create_api_token_for_user + ): + """Test successful API token update""" + # Arrange + session_client.force_authenticate(user=create_user) + url = reverse("api-tokens", kwargs={"pk": create_api_token_for_user.pk}) + update_data = { + "label": "Updated Token Label", + "description": "Updated description", + } + + # Act + response = session_client.patch(url, update_data, format="json") + + # Assert + assert response.status_code == status.HTTP_200_OK + assert response.data["label"] == update_data["label"] + assert response.data["description"] == update_data["description"] + + # Verify database was updated + create_api_token_for_user.refresh_from_db() + assert create_api_token_for_user.label == update_data["label"] + assert create_api_token_for_user.description == update_data["description"] + + @pytest.mark.django_db + def test_patch_api_token_partial_update( + self, session_client, create_user, create_api_token_for_user + ): + """Test partial API token update""" + # Arrange + session_client.force_authenticate(user=create_user) + url = reverse("api-tokens", kwargs={"pk": create_api_token_for_user.pk}) + original_description = create_api_token_for_user.description + update_data = {"label": "Only Label Updated"} + + # Act + response = session_client.patch(url, update_data, format="json") + + # Assert + assert response.status_code == status.HTTP_200_OK + assert response.data["label"] == update_data["label"] + assert response.data["description"] == original_description + + @pytest.mark.django_db + def test_patch_nonexistent_api_token(self, session_client, create_user): + """Test updating a non-existent API token""" + # Arrange + session_client.force_authenticate(user=create_user) + fake_pk = uuid4() + url = reverse("api-tokens", kwargs={"pk": fake_pk}) + update_data = {"label": "New Label"} + + # Act + response = session_client.patch(url, update_data, format="json") + + # Assert + assert response.status_code == status.HTTP_404_NOT_FOUND + + @pytest.mark.django_db + def test_patch_other_users_api_token(self, session_client, create_user, db): + """Test updating another user's API token (should fail)""" + # Arrange + # Create another user and their token with unique email and username + unique_id = uuid4().hex[:8] + unique_email = f"patch-other-{unique_id}@plane.so" + unique_username = f"patch_other_user_{unique_id}" + other_user = User.objects.create(email=unique_email, username=unique_username) + other_token = APIToken.objects.create( + label="Other Token", user=other_user, user_type=0 + ) + session_client.force_authenticate(user=create_user) + url = reverse("api-tokens", kwargs={"pk": other_token.pk}) + update_data = {"label": "Hacked Label"} + + # Act + response = session_client.patch(url, update_data, format="json") + + # Assert + assert response.status_code == status.HTTP_404_NOT_FOUND + + # Verify token was not updated + other_token.refresh_from_db() + assert other_token.label == "Other Token" + + # Authentication tests + @pytest.mark.django_db + def test_all_endpoints_require_authentication(self, api_client): + """Test that all endpoints require authentication""" + # Arrange + endpoints = [ + (reverse("api-tokens"), "get"), + (reverse("api-tokens"), "post"), + (reverse("api-tokens", kwargs={"pk": uuid4()}), "get"), + (reverse("api-tokens", kwargs={"pk": uuid4()}), "patch"), + (reverse("api-tokens", kwargs={"pk": uuid4()}), "delete"), + ] + + # Act & Assert + for url, method in endpoints: + response = getattr(api_client, method)(url) + assert response.status_code == status.HTTP_401_UNAUTHORIZED diff --git a/packages/services/src/developer/api-token.service.ts b/packages/services/src/developer/api-token.service.ts index 74dc9135d..703ec9d32 100644 --- a/packages/services/src/developer/api-token.service.ts +++ b/packages/services/src/developer/api-token.service.ts @@ -9,12 +9,11 @@ export class APITokenService extends APIService { /** * Retrieves all API tokens for a specific workspace - * @param {string} workspaceSlug - The unique identifier for the workspace * @returns {Promise} Array of API tokens associated with the workspace * @throws {Error} Throws response data if the request fails */ - async list(workspaceSlug: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/api-tokens/`) + async list(): Promise { + return this.get(`/api/users/api-tokens/`) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; @@ -23,13 +22,12 @@ export class APITokenService extends APIService { /** * Retrieves a specific API token by its ID - * @param {string} workspaceSlug - The unique identifier for the workspace * @param {string} tokenId - The unique identifier of the API token * @returns {Promise} The requested API token's details * @throws {Error} Throws response data if the request fails */ - async retrieve(workspaceSlug: string, tokenId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/api-tokens/${tokenId}`) + async retrieve(tokenId: string): Promise { + return this.get(`/api/users/api-tokens/${tokenId}`) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; @@ -38,13 +36,12 @@ export class APITokenService extends APIService { /** * Creates a new API token for a workspace - * @param {string} workspaceSlug - The unique identifier for the workspace * @param {Partial} data - The data for creating the new API token * @returns {Promise} The newly created API token * @throws {Error} Throws response data if the request fails */ - async create(workspaceSlug: string, data: Partial): Promise { - return this.post(`/api/workspaces/${workspaceSlug}/api-tokens/`, data) + async create(data: Partial): Promise { + return this.post(`/api/users/api-tokens/`, data) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; @@ -53,13 +50,12 @@ export class APITokenService extends APIService { /** * Deletes a specific API token from the workspace - * @param {string} workspaceSlug - The unique identifier for the workspace * @param {string} tokenId - The unique identifier of the API token to delete * @returns {Promise} The deleted API token's details * @throws {Error} Throws response data if the request fails */ - async destroy(workspaceSlug: string, tokenId: string): Promise { - return this.delete(`/api/workspaces/${workspaceSlug}/api-tokens/${tokenId}`) + async destroy(tokenId: string): Promise { + return this.delete(`/api/users/api-tokens/${tokenId}`) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; diff --git a/web/app/(all)/[workspaceSlug]/(settings)/settings/account/api-tokens/page.tsx b/web/app/(all)/[workspaceSlug]/(settings)/settings/account/api-tokens/page.tsx index cbbdb3a55..9a1883255 100644 --- a/web/app/(all)/[workspaceSlug]/(settings)/settings/account/api-tokens/page.tsx +++ b/web/app/(all)/[workspaceSlug]/(settings)/settings/account/api-tokens/page.tsx @@ -2,24 +2,21 @@ import React, { useState } from "react"; import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; import useSWR from "swr"; // plane imports -import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; // component +import { APITokenService } from "@plane/services"; import { ApiTokenListItem, CreateApiTokenModal } from "@/components/api-token"; -import { NotAuthorizedView } from "@/components/auth-screens"; import { PageHead } from "@/components/core"; import { DetailedEmptyState } from "@/components/empty-state"; import { SettingsHeading } from "@/components/settings"; import { APITokenSettingsLoader } from "@/components/ui"; import { API_TOKENS_LIST } from "@/constants/fetch-keys"; // store hooks -import { useUserPermissions, useWorkspace } from "@/hooks/store"; +import { useWorkspace } from "@/hooks/store"; import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; // services -import { APITokenService } from "@/services/api_token.service"; const apiTokenService = new APITokenService(); @@ -27,30 +24,19 @@ const ApiTokensPage = observer(() => { // states const [isCreateTokenModalOpen, setIsCreateTokenModalOpen] = useState(false); // router - const { workspaceSlug } = useParams(); // plane hooks const { t } = useTranslation(); // store hooks const { currentWorkspace } = useWorkspace(); - const { workspaceUserInfo, allowPermissions } = useUserPermissions(); // derived values - const canPerformWorkspaceAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE); const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/workspace-settings/api-tokens" }); - const { data: tokens } = useSWR( - workspaceSlug && canPerformWorkspaceAdminActions ? API_TOKENS_LIST(workspaceSlug.toString()) : null, - () => - workspaceSlug && canPerformWorkspaceAdminActions ? apiTokenService.getApiTokens(workspaceSlug.toString()) : null - ); + const { data: tokens } = useSWR(API_TOKENS_LIST, () => apiTokenService.list()); const pageTitle = currentWorkspace?.name ? `${currentWorkspace.name} - ${t("workspace_settings.settings.api_tokens.title")}` : undefined; - if (workspaceUserInfo && !canPerformWorkspaceAdminActions) { - return ; - } - if (!tokens) { return ; } diff --git a/web/app/(all)/[workspaceSlug]/(settings)/settings/account/sidebar.tsx b/web/app/(all)/[workspaceSlug]/(settings)/settings/account/sidebar.tsx index addc59596..7153e11be 100644 --- a/web/app/(all)/[workspaceSlug]/(settings)/settings/account/sidebar.tsx +++ b/web/app/(all)/[workspaceSlug]/(settings)/settings/account/sidebar.tsx @@ -2,18 +2,12 @@ import { observer } from "mobx-react"; import { useParams, usePathname } from "next/navigation"; import { CircleUser, Activity, Bell, CircleUserRound, KeyRound, Settings2, Blocks, Lock } from "lucide-react"; // plane imports -import { - EUserPermissions, - EUserPermissionsLevel, - GROUPED_PROFILE_SETTINGS, - PROFILE_SETTINGS_CATEGORIES, - PROFILE_SETTINGS_CATEGORY, -} from "@plane/constants"; +import { GROUPED_PROFILE_SETTINGS, PROFILE_SETTINGS_CATEGORIES } from "@plane/constants"; import { getFileURL } from "@plane/utils"; // components import { SettingsSidebar } from "@/components/settings"; // hooks -import { useUser, useUserPermissions } from "@/hooks/store/user"; +import { useUser } from "@/hooks/store/user"; const ICONS = { profile: CircleUser, @@ -44,14 +38,10 @@ export const ProfileSidebar = observer((props: TProfileSidebarProps) => { // store hooks const { data: currentUser } = useUser(); - const { allowPermissions } = useUserPermissions(); - const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE); return ( isAdmin || category !== PROFILE_SETTINGS_CATEGORY.DEVELOPER - )} + categories={PROFILE_SETTINGS_CATEGORIES} groupedSettings={GROUPED_PROFILE_SETTINGS} workspaceSlug={workspaceSlug?.toString() ?? ""} isActive={(data: { href: string }) => pathname === `/${workspaceSlug}${data.href}/`} diff --git a/web/core/components/api-token/delete-token-modal.tsx b/web/core/components/api-token/delete-token-modal.tsx index ecc85a558..eed0ecdb9 100644 --- a/web/core/components/api-token/delete-token-modal.tsx +++ b/web/core/components/api-token/delete-token-modal.tsx @@ -1,17 +1,15 @@ "use client"; import { useState, FC } from "react"; -import { useParams } from "next/navigation"; import { mutate } from "swr"; // types import { useTranslation } from "@plane/i18n"; +import { APITokenService } from "@plane/services"; import { IApiToken } from "@plane/types"; // ui import { AlertModalCore, TOAST_TYPE, setToast } from "@plane/ui"; // fetch-keys import { API_TOKENS_LIST } from "@/constants/fetch-keys"; -// services -import { APITokenService } from "@/services/api_token.service"; type Props = { isOpen: boolean; @@ -26,7 +24,6 @@ export const DeleteApiTokenModal: FC = (props) => { // states const [deleteLoading, setDeleteLoading] = useState(false); // router params - const { workspaceSlug } = useParams(); const { t } = useTranslation(); const handleClose = () => { @@ -35,12 +32,10 @@ export const DeleteApiTokenModal: FC = (props) => { }; const handleDeletion = async () => { - if (!workspaceSlug) return; - setDeleteLoading(true); await apiTokenService - .deleteApiToken(workspaceSlug.toString(), tokenId) + .destroy(tokenId) .then(() => { setToast({ type: TOAST_TYPE.SUCCESS, @@ -49,7 +44,7 @@ export const DeleteApiTokenModal: FC = (props) => { }); mutate( - API_TOKENS_LIST(workspaceSlug.toString()), + API_TOKENS_LIST, (prevData) => (prevData ?? []).filter((token) => token.id !== tokenId), false ); diff --git a/web/core/components/api-token/modal/create-token-modal.tsx b/web/core/components/api-token/modal/create-token-modal.tsx index a848520b1..94d72c56d 100644 --- a/web/core/components/api-token/modal/create-token-modal.tsx +++ b/web/core/components/api-token/modal/create-token-modal.tsx @@ -1,9 +1,9 @@ "use client"; import React, { useState } from "react"; -import { useParams } from "next/navigation"; import { mutate } from "swr"; // types +import { APITokenService } from "@plane/services"; import { IApiToken } from "@plane/types"; // ui import { EModalPosition, EModalWidth, ModalCore, TOAST_TYPE, setToast } from "@plane/ui"; @@ -14,7 +14,6 @@ import { CreateApiTokenForm, GeneratedTokenDetails } from "@/components/api-toke import { API_TOKENS_LIST } from "@/constants/fetch-keys"; // helpers // services -import { APITokenService } from "@/services/api_token.service"; type Props = { isOpen: boolean; @@ -29,8 +28,6 @@ export const CreateApiTokenModal: React.FC = (props) => { // states const [neverExpires, setNeverExpires] = useState(false); const [generatedToken, setGeneratedToken] = useState(null); - // router - const { workspaceSlug } = useParams(); const handleClose = () => { onClose(); @@ -53,17 +50,15 @@ export const CreateApiTokenModal: React.FC = (props) => { }; const handleCreateToken = async (data: Partial) => { - if (!workspaceSlug) return; - // make the request to generate the token await apiTokenService - .createApiToken(workspaceSlug.toString(), data) + .create(data) .then((res) => { setGeneratedToken(res); downloadSecretKey(res); mutate( - API_TOKENS_LIST(workspaceSlug.toString()), + API_TOKENS_LIST, (prevData) => { if (!prevData) return; @@ -76,7 +71,7 @@ export const CreateApiTokenModal: React.FC = (props) => { setToast({ type: TOAST_TYPE.ERROR, title: "Error!", - message: err.message, + message: err.message || err.detail, }); throw err; diff --git a/web/core/components/settings/heading.tsx b/web/core/components/settings/heading.tsx index 40405a1ec..620801d33 100644 --- a/web/core/components/settings/heading.tsx +++ b/web/core/components/settings/heading.tsx @@ -20,14 +20,14 @@ export const SettingsHeading = ({ customButton, showButton = true, }: Props) => ( -
+
{typeof title === "string" ?

{title}

: title} {description &&
{description}
}
{showButton && customButton} {button && showButton && ( - )} diff --git a/web/core/constants/fetch-keys.ts b/web/core/constants/fetch-keys.ts index f0c9551a4..537a7e3c1 100644 --- a/web/core/constants/fetch-keys.ts +++ b/web/core/constants/fetch-keys.ts @@ -114,4 +114,4 @@ export const USER_PROFILE_PROJECT_SEGREGATION = (workspaceSlug: string, userId: `USER_PROFILE_PROJECT_SEGREGATION_${workspaceSlug.toUpperCase()}_${userId.toUpperCase()}`; // api-tokens -export const API_TOKENS_LIST = (workspaceSlug: string) => `API_TOKENS_LIST_${workspaceSlug.toUpperCase()}`; +export const API_TOKENS_LIST = `API_TOKENS_LIST`; diff --git a/web/core/services/api_token.service.ts b/web/core/services/api_token.service.ts deleted file mode 100644 index ba0acfb39..000000000 --- a/web/core/services/api_token.service.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { API_BASE_URL } from "@plane/constants"; -import { IApiToken } from "@plane/types"; -import { APIService } from "./api.service"; - -export class APITokenService extends APIService { - constructor() { - super(API_BASE_URL); - } - - async getApiTokens(workspaceSlug: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/api-tokens/`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - - async retrieveApiToken(workspaceSlug: string, tokenId: string): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/api-tokens/${tokenId}`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - - async createApiToken(workspaceSlug: string, data: Partial): Promise { - return this.post(`/api/workspaces/${workspaceSlug}/api-tokens/`, data) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } - - async deleteApiToken(workspaceSlug: string, tokenId: string): Promise { - return this.delete(`/api/workspaces/${workspaceSlug}/api-tokens/${tokenId}`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } -} diff --git a/web/core/store/workspace/api-token.store.ts b/web/core/store/workspace/api-token.store.ts index c6cf6e82a..2f51e6605 100644 --- a/web/core/store/workspace/api-token.store.ts +++ b/web/core/store/workspace/api-token.store.ts @@ -1,9 +1,9 @@ import { action, observable, makeObservable, runInAction } from "mobx"; import { computedFn } from "mobx-utils"; // types +import { APITokenService } from "@plane/services"; import { IApiToken } from "@plane/types"; // services -import { APITokenService } from "@/services/api_token.service"; // store import { CoreRootStore } from "../root.store"; @@ -13,11 +13,11 @@ export interface IApiTokenStore { // computed actions getApiTokenById: (apiTokenId: string) => IApiToken | null; // fetch actions - fetchApiTokens: (workspaceSlug: string) => Promise; - fetchApiTokenDetails: (workspaceSlug: string, tokenId: string) => Promise; + fetchApiTokens: () => Promise; + fetchApiTokenDetails: (tokenId: string) => Promise; // crud actions - createApiToken: (workspaceSlug: string, data: Partial) => Promise; - deleteApiToken: (workspaceSlug: string, tokenId: string) => Promise; + createApiToken: (data: Partial) => Promise; + deleteApiToken: (tokenId: string) => Promise; } export class ApiTokenStore implements IApiTokenStore { @@ -55,11 +55,10 @@ export class ApiTokenStore implements IApiTokenStore { }); /** - * fetch all the API tokens for a workspace - * @param workspaceSlug + * fetch all the API tokens */ - fetchApiTokens = async (workspaceSlug: string) => - await this.apiTokenService.getApiTokens(workspaceSlug).then((response) => { + fetchApiTokens = async () => + await this.apiTokenService.list().then((response) => { const apiTokensObject: { [apiTokenId: string]: IApiToken } = response.reduce((accumulator, currentWebhook) => { if (currentWebhook && currentWebhook.id) { return { ...accumulator, [currentWebhook.id]: currentWebhook }; @@ -74,11 +73,10 @@ export class ApiTokenStore implements IApiTokenStore { /** * fetch API token details using token id - * @param workspaceSlug * @param tokenId */ - fetchApiTokenDetails = async (workspaceSlug: string, tokenId: string) => - await this.apiTokenService.retrieveApiToken(workspaceSlug, tokenId).then((response) => { + fetchApiTokenDetails = async (tokenId: string) => + await this.apiTokenService.retrieve(tokenId).then((response) => { runInAction(() => { this.apiTokens = { ...this.apiTokens, [response.id]: response }; }); @@ -87,11 +85,10 @@ export class ApiTokenStore implements IApiTokenStore { /** * create API token using data - * @param workspaceSlug * @param data */ - createApiToken = async (workspaceSlug: string, data: Partial) => - await this.apiTokenService.createApiToken(workspaceSlug, data).then((response) => { + createApiToken = async (data: Partial) => + await this.apiTokenService.create(data).then((response) => { runInAction(() => { this.apiTokens = { ...this.apiTokens, [response.id]: response }; }); @@ -100,11 +97,10 @@ export class ApiTokenStore implements IApiTokenStore { /** * delete API token using token id - * @param workspaceSlug * @param tokenId */ - deleteApiToken = async (workspaceSlug: string, tokenId: string) => - await this.apiTokenService.deleteApiToken(workspaceSlug, tokenId).then(() => { + deleteApiToken = async (tokenId: string) => + await this.apiTokenService.destroy(tokenId).then(() => { const updatedApiTokens = { ...this.apiTokens }; delete updatedApiTokens[tokenId]; runInAction(() => {