Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b2df70de4 | ||
|
|
43c1df2ee3 | ||
|
|
b01a8a3c5f | ||
|
|
6c1f707325 | ||
|
|
3eaf39439a | ||
|
|
e6d8594679 | ||
|
|
e9b307d372 | ||
|
|
b028d833c2 | ||
|
|
08b7370dbc | ||
|
|
2624a5c6e4 | ||
|
|
806cf4df1e | ||
|
|
d75efb3cfd | ||
|
|
ab9a6b4466 | ||
|
|
2f0ea09f15 | ||
|
|
841388a1d5 | ||
|
|
e7cf8fa925 | ||
|
|
280c3cec3d | ||
|
|
d874227de3 | ||
|
|
f75d40f85a | ||
|
|
e43d9b65ed | ||
|
|
6a09e51541 |
@@ -1,13 +1,14 @@
|
||||
# Django imports
|
||||
from django.utils import timezone
|
||||
from django.db.models import Q
|
||||
from django.urls import resolve
|
||||
|
||||
# Third party imports
|
||||
from rest_framework import authentication
|
||||
from rest_framework.exceptions import AuthenticationFailed
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import APIToken
|
||||
from plane.db.models import APIToken, Workspace
|
||||
|
||||
|
||||
class APIKeyAuthentication(authentication.BaseAuthentication):
|
||||
@@ -22,13 +23,20 @@ class APIKeyAuthentication(authentication.BaseAuthentication):
|
||||
def get_api_token(self, request):
|
||||
return request.headers.get(self.auth_header_name)
|
||||
|
||||
def validate_api_token(self, token):
|
||||
def validate_api_token(self, token, workspace_slug):
|
||||
try:
|
||||
api_token = APIToken.objects.get(
|
||||
Q(Q(expired_at__gt=timezone.now()) | Q(expired_at__isnull=True)),
|
||||
token=token,
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
if workspace_slug:
|
||||
workspace = Workspace.objects.get(slug=workspace_slug)
|
||||
|
||||
if api_token.workspace_id != workspace.id:
|
||||
raise AuthenticationFailed("Given API token is not valid")
|
||||
|
||||
except APIToken.DoesNotExist:
|
||||
raise AuthenticationFailed("Given API token is not valid")
|
||||
|
||||
@@ -38,10 +46,12 @@ class APIKeyAuthentication(authentication.BaseAuthentication):
|
||||
return (api_token.user, api_token.token)
|
||||
|
||||
def authenticate(self, request):
|
||||
workspace_slug = resolve(request.path_info).kwargs.get("slug")
|
||||
|
||||
token = self.get_api_token(request=request)
|
||||
if not token:
|
||||
return None
|
||||
|
||||
# Validate the API token
|
||||
user, token = self.validate_api_token(token)
|
||||
user, token = self.validate_api_token(token, workspace_slug)
|
||||
return user, token
|
||||
|
||||
@@ -3,6 +3,7 @@ import os
|
||||
|
||||
# Third party imports
|
||||
from rest_framework.throttling import SimpleRateThrottle
|
||||
from plane.db.models import APIToken
|
||||
|
||||
|
||||
class ApiKeyRateThrottle(SimpleRateThrottle):
|
||||
@@ -85,3 +86,41 @@ class ServiceTokenRateThrottle(SimpleRateThrottle):
|
||||
request.META["X-RateLimit-Reset"] = reset_time
|
||||
|
||||
return allowed
|
||||
|
||||
|
||||
class WorkspaceTokenRateThrottle(SimpleRateThrottle):
|
||||
scope = "workspace_token"
|
||||
rate = "60/minute"
|
||||
|
||||
def get_cache_key(self, request, view):
|
||||
api_key = request.headers.get("X-Api-Key")
|
||||
if not api_key:
|
||||
return None
|
||||
|
||||
return f"{self.scope}:{api_key}"
|
||||
|
||||
def allow_request(self, request, view):
|
||||
api_key = request.headers.get("X-Api-Key")
|
||||
|
||||
if api_key:
|
||||
token = APIToken.objects.filter(token=api_key).only("allowed_rate_limit").first()
|
||||
if token and token.allowed_rate_limit:
|
||||
self.rate = token.allowed_rate_limit
|
||||
|
||||
self.num_requests, self.duration = self.parse_rate(self.rate)
|
||||
|
||||
allowed = super().allow_request(request, view)
|
||||
|
||||
if allowed:
|
||||
now = self.timer()
|
||||
history = self.cache.get(self.key, [])
|
||||
|
||||
while history and history[-1] <= now - self.duration:
|
||||
history.pop()
|
||||
|
||||
available = self.num_requests - len(history)
|
||||
|
||||
request.META["X-RateLimit-Remaining"] = max(0, available)
|
||||
request.META["X-RateLimit-Reset"] = int(now + self.duration)
|
||||
|
||||
return allowed
|
||||
|
||||
@@ -20,7 +20,7 @@ from rest_framework.generics import GenericAPIView
|
||||
# Module imports
|
||||
from plane.db.models.api import APIToken
|
||||
from plane.api.middleware.api_authentication import APIKeyAuthentication
|
||||
from plane.api.rate_limit import ApiKeyRateThrottle, ServiceTokenRateThrottle
|
||||
from plane.api.rate_limit import ApiKeyRateThrottle, ServiceTokenRateThrottle, WorkspaceTokenRateThrottle
|
||||
from plane.utils.exception_logger import log_exception
|
||||
from plane.utils.paginator import BasePaginator
|
||||
from plane.utils.core.mixins import ReadReplicaControlMixin
|
||||
@@ -60,12 +60,20 @@ class BaseAPIView(TimezoneMixin, GenericAPIView, ReadReplicaControlMixin, BasePa
|
||||
api_key = self.request.headers.get("X-Api-Key")
|
||||
|
||||
if api_key:
|
||||
service_token = APIToken.objects.filter(token=api_key, is_service=True).first()
|
||||
api_token = APIToken.objects.filter(token=api_key)
|
||||
|
||||
service_token = api_token.filter(is_service=True).first()
|
||||
|
||||
workspace_token = api_token.filter(workspace_id__isnull=False).first()
|
||||
|
||||
if service_token:
|
||||
throttle_classes.append(ServiceTokenRateThrottle())
|
||||
return throttle_classes
|
||||
|
||||
if workspace_token:
|
||||
throttle_classes.append(WorkspaceTokenRateThrottle())
|
||||
return throttle_classes
|
||||
|
||||
throttle_classes.append(ApiKeyRateThrottle())
|
||||
|
||||
return throttle_classes
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
# Django import
|
||||
from django.utils import timezone
|
||||
|
||||
# Third party import
|
||||
from rest_framework import serializers
|
||||
|
||||
# Module import
|
||||
from .base import BaseSerializer
|
||||
from plane.db.models import APIToken, APIActivityLog
|
||||
from rest_framework import serializers
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
class APITokenSerializer(BaseSerializer):
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from django.urls import path
|
||||
from plane.app.views import ApiTokenEndpoint, ServiceApiTokenEndpoint
|
||||
from plane.app.views import ApiTokenEndpoint, ServiceApiTokenEndpoint, WorkspaceAPITokenEndpoint
|
||||
|
||||
urlpatterns = [
|
||||
# API Tokens
|
||||
@@ -18,5 +18,15 @@ urlpatterns = [
|
||||
ServiceApiTokenEndpoint.as_view(),
|
||||
name="service-api-tokens",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/api-tokens/",
|
||||
WorkspaceAPITokenEndpoint.as_view(),
|
||||
name="workspace-api-tokens",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/api-tokens/<uuid:pk>/",
|
||||
WorkspaceAPITokenEndpoint.as_view(),
|
||||
name="workspace-api-tokens-details",
|
||||
),
|
||||
## End API Tokens
|
||||
]
|
||||
|
||||
@@ -161,7 +161,7 @@ from .module.issue import ModuleIssueViewSet
|
||||
|
||||
from .module.archive import ModuleArchiveUnarchiveEndpoint
|
||||
|
||||
from .api import ApiTokenEndpoint, ServiceApiTokenEndpoint
|
||||
from .api import ApiTokenEndpoint, ServiceApiTokenEndpoint, WorkspaceAPITokenEndpoint
|
||||
|
||||
from .page.base import (
|
||||
PageViewSet,
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
from .base import ApiTokenEndpoint
|
||||
from .service import ServiceApiTokenEndpoint
|
||||
from .workspace import WorkspaceAPITokenEndpoint
|
||||
@@ -8,10 +8,9 @@ from rest_framework.request import Request
|
||||
from rest_framework import status
|
||||
|
||||
# Module import
|
||||
from .base import BaseAPIView
|
||||
from plane.db.models import APIToken, Workspace
|
||||
from plane.app.views.base import BaseAPIView
|
||||
from plane.db.models import APIToken
|
||||
from plane.app.serializers import APITokenSerializer, APITokenReadSerializer
|
||||
from plane.app.permissions import WorkspaceEntityPermission
|
||||
|
||||
|
||||
class ApiTokenEndpoint(BaseAPIView):
|
||||
@@ -37,11 +36,11 @@ class ApiTokenEndpoint(BaseAPIView):
|
||||
|
||||
def get(self, request: Request, pk: Optional[str] = None) -> Response:
|
||||
if pk is None:
|
||||
api_tokens = APIToken.objects.filter(user=request.user, is_service=False)
|
||||
api_tokens = APIToken.objects.filter(user=request.user, is_service=False, workspace_id__isnull=True)
|
||||
serializer = APITokenReadSerializer(api_tokens, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
else:
|
||||
api_tokens = APIToken.objects.get(user=request.user, pk=pk)
|
||||
api_tokens = APIToken.objects.get(user=request.user, pk=pk, workspace_id__isnull=True)
|
||||
serializer = APITokenReadSerializer(api_tokens)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -57,28 +56,3 @@ class ApiTokenEndpoint(BaseAPIView):
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class ServiceApiTokenEndpoint(BaseAPIView):
|
||||
permission_classes = [WorkspaceEntityPermission]
|
||||
|
||||
def post(self, request: Request, slug: str) -> Response:
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
|
||||
api_token = APIToken.objects.filter(workspace=workspace, is_service=True).first()
|
||||
|
||||
if api_token:
|
||||
return Response({"token": str(api_token.token)}, status=status.HTTP_200_OK)
|
||||
else:
|
||||
# Check the user type
|
||||
user_type = 1 if request.user.is_bot else 0
|
||||
|
||||
api_token = APIToken.objects.create(
|
||||
label=str(uuid4().hex),
|
||||
description="Service Token",
|
||||
user=request.user,
|
||||
workspace=workspace,
|
||||
user_type=user_type,
|
||||
is_service=True,
|
||||
)
|
||||
return Response({"token": str(api_token.token)}, status=status.HTTP_201_CREATED)
|
||||
@@ -0,0 +1,37 @@
|
||||
# Python import
|
||||
from uuid import uuid4
|
||||
|
||||
# Third party
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.request import Request
|
||||
from rest_framework import status
|
||||
|
||||
# Module import
|
||||
from .base import BaseAPIView
|
||||
from plane.db.models import APIToken, Workspace
|
||||
from plane.app.permissions import WorkspaceEntityPermission
|
||||
|
||||
|
||||
class ServiceApiTokenEndpoint(BaseAPIView):
|
||||
permission_classes = [WorkspaceEntityPermission]
|
||||
|
||||
def post(self, request: Request, slug: str) -> Response:
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
|
||||
api_token = APIToken.objects.filter(workspace=workspace, is_service=True).first()
|
||||
|
||||
if api_token:
|
||||
return Response({"token": str(api_token.token)}, status=status.HTTP_200_OK)
|
||||
else:
|
||||
# Check the user type
|
||||
user_type = 1 if request.user.is_bot else 0
|
||||
|
||||
api_token = APIToken.objects.create(
|
||||
label=str(uuid4().hex),
|
||||
description="Service Token",
|
||||
user=request.user,
|
||||
workspace=workspace,
|
||||
user_type=user_type,
|
||||
is_service=True,
|
||||
)
|
||||
return Response({"token": str(api_token.token)}, status=status.HTTP_201_CREATED)
|
||||
@@ -0,0 +1,59 @@
|
||||
# Python import
|
||||
from typing import Optional
|
||||
from uuid import uuid4
|
||||
|
||||
|
||||
# Third party
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.request import Request
|
||||
from rest_framework import status
|
||||
|
||||
# Module import
|
||||
from plane.app.views import BaseAPIView
|
||||
from plane.db.models import APIToken, Workspace
|
||||
from plane.app.serializers import APITokenSerializer, APITokenReadSerializer
|
||||
from plane.app.permissions import WorkSpaceAdminPermission
|
||||
|
||||
|
||||
class WorkspaceAPITokenEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
WorkSpaceAdminPermission,
|
||||
]
|
||||
|
||||
def post(self, request: Request, slug: str) -> Response:
|
||||
label = request.data.get("label", str(uuid4().hex))
|
||||
description = request.data.get("description", "")
|
||||
expired_at = request.data.get("expired_at", None)
|
||||
|
||||
# Check the user type
|
||||
user_type = 1 if request.user.is_bot else 0
|
||||
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
|
||||
api_token = APIToken.objects.create(
|
||||
label=label,
|
||||
description=description,
|
||||
user=request.user,
|
||||
user_type=user_type,
|
||||
expired_at=expired_at,
|
||||
workspace=workspace,
|
||||
)
|
||||
|
||||
serializer = APITokenSerializer(api_token)
|
||||
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
def get(self, request: Request, slug: str, pk: Optional[str] = None) -> Response:
|
||||
if pk is None:
|
||||
api_tokens = APIToken.objects.filter(workspace__slug=slug, is_service=False, user=request.user)
|
||||
serializer = APITokenReadSerializer(api_tokens, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
else:
|
||||
api_tokens = APIToken.objects.get(workspace__slug=slug, pk=pk, user=request.user)
|
||||
serializer = APITokenReadSerializer(api_tokens)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
def delete(self, request: Request, slug: str, pk: str) -> Response:
|
||||
api_token = APIToken.objects.get(workspace__slug=slug, pk=pk, is_service=False, user=request.user)
|
||||
api_token.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import useSWR from "swr";
|
||||
// plane imports
|
||||
import { EUserPermissions, EUserPermissionsLevel, WORKSPACE_SETTINGS_TRACKER_ELEMENTS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { EmptyStateCompact } from "@plane/propel/empty-state";
|
||||
import { WorkspaceAPITokenService } from "@plane/services";
|
||||
// component
|
||||
import { CreateApiTokenModal } from "@/components/api-token/modal/create-token-modal";
|
||||
import { ApiTokenListItem } from "@/components/api-token/token-list-item";
|
||||
import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view";
|
||||
import { PageHead } from "@/components/core/page-title";
|
||||
import { SettingsContentWrapper } from "@/components/settings/content-wrapper";
|
||||
import { SettingsHeading } from "@/components/settings/heading";
|
||||
import { APITokenSettingsLoader } from "@/components/ui/loader/settings/api-token";
|
||||
// constants
|
||||
import { WORKSPACE_API_TOKENS_LIST } from "@/constants/fetch-keys";
|
||||
// helpers
|
||||
import { captureClick } from "@/helpers/event-tracker.helper";
|
||||
// store hooks
|
||||
import { useWorkspace } from "@/hooks/store/use-workspace";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
import type { Route } from "./+types/page";
|
||||
|
||||
const workspaceApiTokenService = new WorkspaceAPITokenService();
|
||||
|
||||
function ApiTokensPage({ params }: Route.ComponentProps) {
|
||||
// states
|
||||
const [isCreateTokenModalOpen, setIsCreateTokenModalOpen] = useState(false);
|
||||
// router
|
||||
const { workspaceSlug } = params;
|
||||
// plane hooks
|
||||
const { t } = useTranslation();
|
||||
// store hooks
|
||||
const { workspaceUserInfo, allowPermissions } = useUserPermissions();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
// derived values
|
||||
const canPerformWorkspaceAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE);
|
||||
|
||||
const { data: tokens } = useSWR(
|
||||
canPerformWorkspaceAdminActions ? WORKSPACE_API_TOKENS_LIST(workspaceSlug) : null,
|
||||
canPerformWorkspaceAdminActions ? () => workspaceApiTokenService.list(workspaceSlug) : null
|
||||
);
|
||||
|
||||
const pageTitle = currentWorkspace?.name
|
||||
? `${currentWorkspace.name} - ${t("workspace_settings.settings.api_tokens.title")}`
|
||||
: undefined;
|
||||
|
||||
if (workspaceUserInfo && !canPerformWorkspaceAdminActions) {
|
||||
return <NotAuthorizedView section="settings" className="h-auto" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsContentWrapper>
|
||||
<PageHead title={pageTitle} />
|
||||
{!tokens ? (
|
||||
<APITokenSettingsLoader title={t("workspace_settings.settings.api_tokens.title")} />
|
||||
) : (
|
||||
<div className="w-full">
|
||||
<CreateApiTokenModal
|
||||
isOpen={isCreateTokenModalOpen}
|
||||
onClose={() => setIsCreateTokenModalOpen(false)}
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
<SettingsHeading
|
||||
title={t("workspace_settings.settings.api_tokens.heading")}
|
||||
description={t("workspace_settings.settings.api_tokens.description")}
|
||||
button={{
|
||||
label: t("workspace_settings.settings.api_tokens.add_token"),
|
||||
onClick: () => {
|
||||
captureClick({
|
||||
elementName: WORKSPACE_SETTINGS_TRACKER_ELEMENTS.HEADER_ADD_PAT_BUTTON,
|
||||
});
|
||||
setIsCreateTokenModalOpen(true);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{tokens.length > 0 ? (
|
||||
<div className="flex h-full w-full flex-col">
|
||||
<div className="h-full w-full overflow-y-auto">
|
||||
{tokens.map((token) => (
|
||||
<ApiTokenListItem key={token.id} token={token} workspaceSlug={workspaceSlug} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full w-full flex-col">
|
||||
<div className="h-full w-full flex items-center justify-center">
|
||||
<EmptyStateCompact
|
||||
assetKey="token"
|
||||
title={t("settings_empty_state.tokens.title")}
|
||||
description={t("settings_empty_state.tokens.description")}
|
||||
actions={[
|
||||
{
|
||||
label: t("settings_empty_state.tokens.cta_primary"),
|
||||
onClick: () => {
|
||||
captureClick({
|
||||
elementName: WORKSPACE_SETTINGS_TRACKER_ELEMENTS.EMPTY_STATE_ADD_PAT_BUTTON,
|
||||
});
|
||||
setIsCreateTokenModalOpen(true);
|
||||
},
|
||||
},
|
||||
]}
|
||||
align="start"
|
||||
rootClassName="py-20"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</SettingsContentWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(ApiTokensPage);
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
import { ArrowUpToLine, Building, CreditCard, Users, Webhook } from "lucide-react";
|
||||
import { ArrowUpToLine, Building, CreditCard, KeyRound, Users, Webhook } from "lucide-react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
// plane imports
|
||||
import {
|
||||
@@ -25,6 +25,7 @@ export const WORKSPACE_SETTINGS_ICONS: Record<keyof typeof WORKSPACE_SETTINGS, L
|
||||
export: ArrowUpToLine,
|
||||
"billing-and-plans": CreditCard,
|
||||
webhooks: Webhook,
|
||||
"api-tokens": KeyRound,
|
||||
};
|
||||
|
||||
export function WorkspaceActionIcons({ type, size, className }: { type: string; size?: number; className?: string }) {
|
||||
|
||||
+37
-55
@@ -1,4 +1,5 @@
|
||||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
import type { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import useSWR from "swr";
|
||||
// plane imports
|
||||
@@ -19,10 +20,9 @@ import { useWorkspace } from "@/hooks/store/use-workspace";
|
||||
|
||||
const apiTokenService = new APITokenService();
|
||||
|
||||
function ApiTokensPage() {
|
||||
const ApiTokensPage: FC = observer(function ApiTokensPage() {
|
||||
// states
|
||||
const [isCreateTokenModalOpen, setIsCreateTokenModalOpen] = useState(false);
|
||||
// router
|
||||
// plane hooks
|
||||
const { t } = useTranslation();
|
||||
// store hooks
|
||||
@@ -31,11 +31,11 @@ function ApiTokensPage() {
|
||||
const { data: tokens } = useSWR(API_TOKENS_LIST, () => apiTokenService.list());
|
||||
|
||||
const pageTitle = currentWorkspace?.name
|
||||
? `${currentWorkspace.name} - ${t("workspace_settings.settings.api_tokens.title")}`
|
||||
? `${currentWorkspace.name} - ${t("account_settings.api_tokens.title")}`
|
||||
: undefined;
|
||||
|
||||
if (!tokens) {
|
||||
return <APITokenSettingsLoader />;
|
||||
return <APITokenSettingsLoader title={t("account_settings.api_tokens.title")} />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -43,67 +43,49 @@ function ApiTokensPage() {
|
||||
<PageHead title={pageTitle} />
|
||||
<CreateApiTokenModal isOpen={isCreateTokenModalOpen} onClose={() => setIsCreateTokenModalOpen(false)} />
|
||||
<section className="w-full">
|
||||
<SettingsHeading
|
||||
title={t("account_settings.api_tokens.heading")}
|
||||
description={t("account_settings.api_tokens.description")}
|
||||
button={{
|
||||
label: t("workspace_settings.settings.api_tokens.add_token"),
|
||||
onClick: () => {
|
||||
captureClick({
|
||||
elementName: PROFILE_SETTINGS_TRACKER_ELEMENTS.HEADER_ADD_PAT_BUTTON,
|
||||
});
|
||||
setIsCreateTokenModalOpen(true);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{tokens.length > 0 ? (
|
||||
<>
|
||||
<SettingsHeading
|
||||
title={t("account_settings.api_tokens.heading")}
|
||||
description={t("account_settings.api_tokens.description")}
|
||||
button={{
|
||||
label: t("workspace_settings.settings.api_tokens.add_token"),
|
||||
onClick: () => {
|
||||
captureClick({
|
||||
elementName: PROFILE_SETTINGS_TRACKER_ELEMENTS.HEADER_ADD_PAT_BUTTON,
|
||||
});
|
||||
setIsCreateTokenModalOpen(true);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
{tokens.map((token) => (
|
||||
<ApiTokenListItem key={token.id} token={token} />
|
||||
))}
|
||||
</div>
|
||||
{tokens.map((token) => (
|
||||
<ApiTokenListItem key={token.id} token={token} />
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<div className="flex h-full w-full flex-col py-">
|
||||
<SettingsHeading
|
||||
title={t("account_settings.api_tokens.heading")}
|
||||
description={t("account_settings.api_tokens.description")}
|
||||
button={{
|
||||
label: t("workspace_settings.settings.api_tokens.add_token"),
|
||||
<EmptyStateCompact
|
||||
assetKey="token"
|
||||
assetClassName="size-20"
|
||||
title={t("settings_empty_state.tokens.title")}
|
||||
description={t("settings_empty_state.tokens.description")}
|
||||
actions={[
|
||||
{
|
||||
label: t("settings_empty_state.tokens.cta_primary"),
|
||||
onClick: () => {
|
||||
captureClick({
|
||||
elementName: PROFILE_SETTINGS_TRACKER_ELEMENTS.HEADER_ADD_PAT_BUTTON,
|
||||
elementName: PROFILE_SETTINGS_TRACKER_ELEMENTS.EMPTY_STATE_ADD_PAT_BUTTON,
|
||||
});
|
||||
setIsCreateTokenModalOpen(true);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<EmptyStateCompact
|
||||
assetKey="token"
|
||||
assetClassName="size-20"
|
||||
title={t("settings_empty_state.tokens.title")}
|
||||
description={t("settings_empty_state.tokens.description")}
|
||||
actions={[
|
||||
{
|
||||
label: t("settings_empty_state.tokens.cta_primary"),
|
||||
onClick: () => {
|
||||
captureClick({
|
||||
elementName: PROFILE_SETTINGS_TRACKER_ELEMENTS.EMPTY_STATE_ADD_PAT_BUTTON,
|
||||
});
|
||||
setIsCreateTokenModalOpen(true);
|
||||
},
|
||||
},
|
||||
]}
|
||||
align="start"
|
||||
rootClassName="py-20"
|
||||
/>
|
||||
</div>
|
||||
},
|
||||
]}
|
||||
align="start"
|
||||
rootClassName="py-20"
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default observer(ApiTokensPage);
|
||||
export default ApiTokensPage;
|
||||
|
||||
@@ -276,6 +276,10 @@ export const coreRoutes: RouteConfigEntry[] = [
|
||||
":workspaceSlug/settings/webhooks/:webhookId",
|
||||
"./(all)/[workspaceSlug]/(settings)/settings/(workspace)/webhooks/[webhookId]/page.tsx"
|
||||
),
|
||||
route(
|
||||
":workspaceSlug/settings/api-tokens",
|
||||
"./(all)/[workspaceSlug]/(settings)/settings/(workspace)/api-tokens/page.tsx"
|
||||
),
|
||||
]),
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
|
||||
@@ -1,28 +1,29 @@
|
||||
import type { FC } from "react";
|
||||
import { useState } from "react";
|
||||
import { mutate } from "swr";
|
||||
// types
|
||||
import { PROFILE_SETTINGS_TRACKER_EVENTS } from "@plane/constants";
|
||||
import { PROFILE_SETTINGS_TRACKER_EVENTS, WORKSPACE_SETTINGS_TRACKER_EVENTS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import { APITokenService } from "@plane/services";
|
||||
import { APITokenService, WorkspaceAPITokenService } from "@plane/services";
|
||||
import type { IApiToken } from "@plane/types";
|
||||
// ui
|
||||
import { AlertModalCore } from "@plane/ui";
|
||||
// fetch-keys
|
||||
import { API_TOKENS_LIST } from "@/constants/fetch-keys";
|
||||
import { API_TOKENS_LIST, WORKSPACE_API_TOKENS_LIST } from "@/constants/fetch-keys";
|
||||
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
tokenId: string;
|
||||
workspaceSlug?: string;
|
||||
};
|
||||
|
||||
const apiTokenService = new APITokenService();
|
||||
const workspaceApiTokenService = new WorkspaceAPITokenService();
|
||||
|
||||
export function DeleteApiTokenModal(props: Props) {
|
||||
const { isOpen, onClose, tokenId } = props;
|
||||
const { isOpen, onClose, tokenId, workspaceSlug } = props;
|
||||
// states
|
||||
const [deleteLoading, setDeleteLoading] = useState<boolean>(false);
|
||||
// router params
|
||||
@@ -36,8 +37,11 @@ export function DeleteApiTokenModal(props: Props) {
|
||||
const handleDeletion = async () => {
|
||||
setDeleteLoading(true);
|
||||
|
||||
await apiTokenService
|
||||
.destroy(tokenId)
|
||||
const apiCall = workspaceSlug
|
||||
? workspaceApiTokenService.destroy(workspaceSlug, tokenId)
|
||||
: apiTokenService.destroy(tokenId);
|
||||
|
||||
await apiCall
|
||||
.then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
@@ -46,12 +50,14 @@ export function DeleteApiTokenModal(props: Props) {
|
||||
});
|
||||
|
||||
mutate<IApiToken[]>(
|
||||
API_TOKENS_LIST,
|
||||
workspaceSlug ? WORKSPACE_API_TOKENS_LIST(workspaceSlug) : API_TOKENS_LIST,
|
||||
(prevData) => (prevData ?? []).filter((token) => token.id !== tokenId),
|
||||
false
|
||||
);
|
||||
captureSuccess({
|
||||
eventName: PROFILE_SETTINGS_TRACKER_EVENTS.pat_deleted,
|
||||
eventName: workspaceSlug
|
||||
? WORKSPACE_SETTINGS_TRACKER_EVENTS.pat_deleted
|
||||
: PROFILE_SETTINGS_TRACKER_EVENTS.pat_deleted,
|
||||
payload: {
|
||||
token: tokenId,
|
||||
},
|
||||
@@ -68,7 +74,9 @@ export function DeleteApiTokenModal(props: Props) {
|
||||
)
|
||||
.catch((err) => {
|
||||
captureError({
|
||||
eventName: PROFILE_SETTINGS_TRACKER_EVENTS.pat_deleted,
|
||||
eventName: workspaceSlug
|
||||
? WORKSPACE_SETTINGS_TRACKER_EVENTS.pat_deleted
|
||||
: PROFILE_SETTINGS_TRACKER_EVENTS.pat_deleted,
|
||||
payload: {
|
||||
token: tokenId,
|
||||
},
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import React, { useState } from "react";
|
||||
import { mutate } from "swr";
|
||||
// plane imports
|
||||
import { PROFILE_SETTINGS_TRACKER_EVENTS } from "@plane/constants";
|
||||
import { PROFILE_SETTINGS_TRACKER_EVENTS, WORKSPACE_SETTINGS_TRACKER_EVENTS } from "@plane/constants";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
import { APITokenService } from "@plane/services";
|
||||
import { APITokenService, WorkspaceAPITokenService } from "@plane/services";
|
||||
import type { IApiToken } from "@plane/types";
|
||||
import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
|
||||
import { renderFormattedDate, csvDownload } from "@plane/utils";
|
||||
// constants
|
||||
import { API_TOKENS_LIST } from "@/constants/fetch-keys";
|
||||
import { API_TOKENS_LIST, WORKSPACE_API_TOKENS_LIST } from "@/constants/fetch-keys";
|
||||
// helpers
|
||||
import { captureError, captureSuccess } from "@/helpers/event-tracker.helper";
|
||||
// local imports
|
||||
@@ -18,13 +18,15 @@ import { GeneratedTokenDetails } from "./generated-token-details";
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
workspaceSlug?: string;
|
||||
};
|
||||
|
||||
// services
|
||||
const apiTokenService = new APITokenService();
|
||||
const workspaceApiTokenService = new WorkspaceAPITokenService();
|
||||
|
||||
export function CreateApiTokenModal(props: Props) {
|
||||
const { isOpen, onClose } = props;
|
||||
const { isOpen, onClose, workspaceSlug } = props;
|
||||
// states
|
||||
const [neverExpires, setNeverExpires] = useState<boolean>(false);
|
||||
const [generatedToken, setGeneratedToken] = useState<IApiToken | null | undefined>(null);
|
||||
@@ -51,14 +53,14 @@ export function CreateApiTokenModal(props: Props) {
|
||||
|
||||
const handleCreateToken = async (data: Partial<IApiToken>) => {
|
||||
// make the request to generate the token
|
||||
await apiTokenService
|
||||
.create(data)
|
||||
const apiCall = workspaceSlug ? workspaceApiTokenService.create(workspaceSlug, data) : apiTokenService.create(data);
|
||||
await apiCall
|
||||
.then((res) => {
|
||||
setGeneratedToken(res);
|
||||
downloadSecretKey(res);
|
||||
|
||||
mutate<IApiToken[]>(
|
||||
API_TOKENS_LIST,
|
||||
workspaceSlug ? WORKSPACE_API_TOKENS_LIST(workspaceSlug) : API_TOKENS_LIST,
|
||||
(prevData) => {
|
||||
if (!prevData) return;
|
||||
|
||||
@@ -67,7 +69,9 @@ export function CreateApiTokenModal(props: Props) {
|
||||
false
|
||||
);
|
||||
captureSuccess({
|
||||
eventName: PROFILE_SETTINGS_TRACKER_EVENTS.pat_created,
|
||||
eventName: workspaceSlug
|
||||
? WORKSPACE_SETTINGS_TRACKER_EVENTS.pat_created
|
||||
: PROFILE_SETTINGS_TRACKER_EVENTS.pat_created,
|
||||
payload: {
|
||||
token: res.id,
|
||||
},
|
||||
@@ -81,7 +85,9 @@ export function CreateApiTokenModal(props: Props) {
|
||||
});
|
||||
|
||||
captureError({
|
||||
eventName: PROFILE_SETTINGS_TRACKER_EVENTS.pat_created,
|
||||
eventName: workspaceSlug
|
||||
? WORKSPACE_SETTINGS_TRACKER_EVENTS.pat_created
|
||||
: PROFILE_SETTINGS_TRACKER_EVENTS.pat_created,
|
||||
});
|
||||
|
||||
throw err;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import { XCircle } from "lucide-react";
|
||||
// plane imports
|
||||
import { PROFILE_SETTINGS_TRACKER_ELEMENTS } from "@plane/constants";
|
||||
import { PROFILE_SETTINGS_TRACKER_ELEMENTS, WORKSPACE_SETTINGS_TRACKER_ELEMENTS } from "@plane/constants";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import type { IApiToken } from "@plane/types";
|
||||
import { renderFormattedDate, calculateTimeAgo, renderFormattedTime } from "@plane/utils";
|
||||
@@ -12,24 +12,34 @@ import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
|
||||
type Props = {
|
||||
token: IApiToken;
|
||||
workspaceSlug?: string;
|
||||
};
|
||||
|
||||
export function ApiTokenListItem(props: Props) {
|
||||
const { token } = props;
|
||||
const { token, workspaceSlug } = props;
|
||||
// states
|
||||
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
||||
// hooks
|
||||
const { isMobile } = usePlatformOS();
|
||||
|
||||
const trackerElement = workspaceSlug
|
||||
? WORKSPACE_SETTINGS_TRACKER_ELEMENTS.LIST_ITEM_DELETE_ICON
|
||||
: PROFILE_SETTINGS_TRACKER_ELEMENTS.LIST_ITEM_DELETE_ICON;
|
||||
|
||||
return (
|
||||
<>
|
||||
<DeleteApiTokenModal isOpen={deleteModalOpen} onClose={() => setDeleteModalOpen(false)} tokenId={token.id} />
|
||||
<DeleteApiTokenModal
|
||||
isOpen={deleteModalOpen}
|
||||
onClose={() => setDeleteModalOpen(false)}
|
||||
tokenId={token.id}
|
||||
workspaceSlug={workspaceSlug}
|
||||
/>
|
||||
<div className="group relative flex flex-col justify-center border-b border-subtle py-3">
|
||||
<Tooltip tooltipContent="Delete token" isMobile={isMobile}>
|
||||
<button
|
||||
onClick={() => setDeleteModalOpen(true)}
|
||||
className="absolute right-4 hidden place-items-center group-hover:grid"
|
||||
data-ph-element={PROFILE_SETTINGS_TRACKER_ELEMENTS.LIST_ITEM_DELETE_ICON}
|
||||
data-ph-element={trackerElement}
|
||||
>
|
||||
<XCircle className="h-4 w-4 text-red-500" />
|
||||
</button>
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { range } from "lodash-es";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
export function APITokenSettingsLoader() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
};
|
||||
|
||||
export function APITokenSettingsLoader(props: Props) {
|
||||
const { title } = props;
|
||||
return (
|
||||
<section className="w-full overflow-y-auto">
|
||||
<div className="mb-2 flex items-center justify-between border-b border-subtle pb-3.5">
|
||||
<h3 className="text-18 font-medium">{t("workspace_settings.settings.api_tokens.title")}</h3>
|
||||
<h3 className="text-xl font-medium">{title}</h3>
|
||||
<span className="h-8 w-28 bg-layer-1 rounded-sm" />
|
||||
</div>
|
||||
<div className="divide-y-[0.5px] divide-subtle-1">
|
||||
|
||||
@@ -143,6 +143,8 @@ export const USER_PROFILE_PROJECT_SEGREGATION = (workspaceSlug: string, userId:
|
||||
|
||||
// api-tokens
|
||||
export const API_TOKENS_LIST = `API_TOKENS_LIST`;
|
||||
export const WORKSPACE_API_TOKENS_LIST = (workspaceSlug: string) =>
|
||||
`WORKSPACE_API_TOKENS_LIST_${workspaceSlug.toUpperCase()}`;
|
||||
|
||||
// marketplace
|
||||
export const APPLICATIONS_LIST = (workspaceSlug: string) => `APPLICATIONS_LIST_${workspaceSlug.toUpperCase()}`;
|
||||
|
||||
@@ -483,6 +483,9 @@ export const WORKSPACE_SETTINGS_TRACKER_EVENTS = {
|
||||
webhook_toggled: "webhook_toggled",
|
||||
webhook_details_page_toggled: "webhook_details_page_toggled",
|
||||
webhook_updated: "webhook_updated",
|
||||
// PAT
|
||||
pat_created: "workspace_pat_created",
|
||||
pat_deleted: "workspace_pat_deleted",
|
||||
};
|
||||
|
||||
export const WORKSPACE_SETTINGS_TRACKER_ELEMENTS = {
|
||||
@@ -499,4 +502,8 @@ export const WORKSPACE_SETTINGS_TRACKER_ELEMENTS = {
|
||||
WEBHOOK_DETAILS_PAGE_TOGGLE_SWITCH: "webhook_details_page_toggle_switch",
|
||||
WEBHOOK_DELETE_BUTTON: "webhook_delete_button",
|
||||
WEBHOOK_UPDATE_BUTTON: "webhook_update_button",
|
||||
// PAT
|
||||
HEADER_ADD_PAT_BUTTON: "workspace_header_add_pat_button",
|
||||
EMPTY_STATE_ADD_PAT_BUTTON: "workspace_empty_state_add_pat_button",
|
||||
LIST_ITEM_DELETE_ICON: "workspace_list_item_delete_icon",
|
||||
};
|
||||
|
||||
@@ -37,7 +37,7 @@ export const GROUPED_WORKSPACE_SETTINGS = {
|
||||
WORKSPACE_SETTINGS["export"],
|
||||
],
|
||||
[WORKSPACE_SETTINGS_CATEGORY.FEATURES]: [],
|
||||
[WORKSPACE_SETTINGS_CATEGORY.DEVELOPER]: [WORKSPACE_SETTINGS["webhooks"]],
|
||||
[WORKSPACE_SETTINGS_CATEGORY.DEVELOPER]: [WORKSPACE_SETTINGS["webhooks"], WORKSPACE_SETTINGS["api-tokens"]],
|
||||
};
|
||||
|
||||
export const GROUPED_PROFILE_SETTINGS = {
|
||||
|
||||
@@ -107,6 +107,13 @@ export const WORKSPACE_SETTINGS = {
|
||||
access: [EUserWorkspaceRoles.ADMIN],
|
||||
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/webhooks/`,
|
||||
},
|
||||
"api-tokens": {
|
||||
key: "api-tokens",
|
||||
i18n_label: "workspace_settings.settings.api_tokens.title",
|
||||
href: `/settings/api-tokens`,
|
||||
access: [EUserWorkspaceRoles.ADMIN],
|
||||
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/settings/api-tokens/`,
|
||||
},
|
||||
};
|
||||
|
||||
export const WORKSPACE_SETTINGS_ACCESS = Object.fromEntries(
|
||||
@@ -125,6 +132,7 @@ export const WORKSPACE_SETTINGS_LINKS: {
|
||||
WORKSPACE_SETTINGS["billing-and-plans"],
|
||||
WORKSPACE_SETTINGS["export"],
|
||||
WORKSPACE_SETTINGS["webhooks"],
|
||||
WORKSPACE_SETTINGS["api-tokens"],
|
||||
];
|
||||
|
||||
export const ROLE = {
|
||||
|
||||
@@ -1709,7 +1709,7 @@ export default {
|
||||
},
|
||||
api_tokens: {
|
||||
title: "API Tokeny",
|
||||
add_token: "Přidat API token",
|
||||
add_token: "Přidat token přístupu k pracovnímu prostoru",
|
||||
create_token: "Vytvořit token",
|
||||
never_expires: "Nikdy neexpiruje",
|
||||
generate_token: "Generovat token",
|
||||
|
||||
@@ -1729,7 +1729,7 @@ export default {
|
||||
},
|
||||
api_tokens: {
|
||||
title: "API-Tokens",
|
||||
add_token: "API-Token hinzufügen",
|
||||
add_token: "Workspace-Zugriffstoken hinzufügen",
|
||||
create_token: "Token erstellen",
|
||||
never_expires: "Läuft nie ab",
|
||||
generate_token: "Token generieren",
|
||||
|
||||
@@ -1571,8 +1571,10 @@ export default {
|
||||
},
|
||||
},
|
||||
api_tokens: {
|
||||
title: "Personal Access Tokens",
|
||||
add_token: "Add personal access token",
|
||||
heading: "Workspace Access Tokens",
|
||||
description: "Generate secure API tokens to integrate your data with external systems and applications.",
|
||||
title: "Workspace Access Tokens",
|
||||
add_token: "Add Workspace access token",
|
||||
create_token: "Create token",
|
||||
never_expires: "Never expires",
|
||||
generate_token: "Generate token",
|
||||
|
||||
@@ -1734,7 +1734,7 @@ export default {
|
||||
},
|
||||
api_tokens: {
|
||||
title: "Tokens de API",
|
||||
add_token: "Agregar token de API",
|
||||
add_token: "Agregar token de acceso al espacio de trabajo",
|
||||
create_token: "Crear token",
|
||||
never_expires: "Nunca expira",
|
||||
generate_token: "Generar token",
|
||||
|
||||
@@ -1732,7 +1732,7 @@ export default {
|
||||
},
|
||||
api_tokens: {
|
||||
title: "Jetons API",
|
||||
add_token: "Ajouter un jeton API",
|
||||
add_token: "Ajouter un jeton d'accès à l'espace de travail",
|
||||
create_token: "Créer un jeton",
|
||||
never_expires: "N’expire jamais",
|
||||
generate_token: "Générer un jeton",
|
||||
|
||||
@@ -1720,7 +1720,7 @@ export default {
|
||||
},
|
||||
api_tokens: {
|
||||
title: "Token API",
|
||||
add_token: "Tambah token API",
|
||||
add_token: "Tambah token akses ruang kerja",
|
||||
create_token: "Buat token",
|
||||
never_expires: "Tidak pernah kedaluwarsa",
|
||||
generate_token: "Hasilkan token",
|
||||
|
||||
@@ -1724,7 +1724,7 @@ export default {
|
||||
},
|
||||
api_tokens: {
|
||||
title: "Token API",
|
||||
add_token: "Aggiungi token API",
|
||||
add_token: "Aggiungi token di accesso allo spazio di lavoro",
|
||||
create_token: "Crea token",
|
||||
never_expires: "Non scade mai",
|
||||
generate_token: "Genera token",
|
||||
|
||||
@@ -1710,7 +1710,7 @@ export default {
|
||||
},
|
||||
api_tokens: {
|
||||
title: "APIトークン",
|
||||
add_token: "APIトークンを追加",
|
||||
add_token: "ワークスペースアクセストークンを追加",
|
||||
create_token: "トークンを作成",
|
||||
never_expires: "無期限",
|
||||
generate_token: "トークンを生成",
|
||||
|
||||
@@ -1703,7 +1703,7 @@ export default {
|
||||
},
|
||||
api_tokens: {
|
||||
title: "API 토큰",
|
||||
add_token: "API 토큰 추가",
|
||||
add_token: "워크스페이스 액세스 토큰 추가",
|
||||
create_token: "토큰 생성",
|
||||
never_expires: "만료되지 않음",
|
||||
generate_token: "토큰 생성",
|
||||
|
||||
@@ -1712,7 +1712,7 @@ export default {
|
||||
},
|
||||
api_tokens: {
|
||||
title: "Tokeny API",
|
||||
add_token: "Dodaj token API",
|
||||
add_token: "Dodaj token dostępu do obszaru roboczego",
|
||||
create_token: "Utwórz token",
|
||||
never_expires: "Nigdy nie wygasa",
|
||||
generate_token: "Wygeneruj token",
|
||||
|
||||
@@ -1732,7 +1732,7 @@ export default {
|
||||
},
|
||||
api_tokens: {
|
||||
title: "Tokens de API",
|
||||
add_token: "Adicionar token de API",
|
||||
add_token: "Adicionar token de acesso ao espaço de trabalho",
|
||||
create_token: "Criar token",
|
||||
never_expires: "Nunca expira",
|
||||
generate_token: "Gerar token",
|
||||
|
||||
@@ -1724,7 +1724,7 @@ export default {
|
||||
},
|
||||
api_tokens: {
|
||||
title: "Chei secrete API",
|
||||
add_token: "Adaugă cheie secretă API",
|
||||
add_token: "Adaugă token de acces la spațiul de lucru",
|
||||
create_token: "Creează cheie secretă",
|
||||
never_expires: "Nu expiră niciodată",
|
||||
generate_token: "Generează cheie secretă",
|
||||
|
||||
@@ -1714,7 +1714,7 @@ export default {
|
||||
},
|
||||
api_tokens: {
|
||||
title: "API-токены",
|
||||
add_token: "Добавить токен",
|
||||
add_token: "Добавить токен доступа к рабочему пространству",
|
||||
create_token: "Создать токен",
|
||||
never_expires: "Бессрочный",
|
||||
generate_token: "Сгенерировать токен",
|
||||
|
||||
@@ -1711,7 +1711,7 @@ export default {
|
||||
},
|
||||
api_tokens: {
|
||||
title: "API Tokeny",
|
||||
add_token: "Pridať API token",
|
||||
add_token: "Pridať token prístupu k pracovnému priestoru",
|
||||
create_token: "Vytvoriť token",
|
||||
never_expires: "Nikdy neexpiruje",
|
||||
generate_token: "Generovať token",
|
||||
|
||||
@@ -1720,7 +1720,7 @@ export default {
|
||||
},
|
||||
api_tokens: {
|
||||
title: "API Token'ları",
|
||||
add_token: "API Token'ı ekle",
|
||||
add_token: "Çalışma alanı erişim token'ı ekle",
|
||||
create_token: "Token oluştur",
|
||||
never_expires: "Süresi dolmaz",
|
||||
generate_token: "Token oluştur",
|
||||
|
||||
@@ -1715,7 +1715,7 @@ export default {
|
||||
},
|
||||
api_tokens: {
|
||||
title: "API токени",
|
||||
add_token: "Додати API токен",
|
||||
add_token: "Додати токен доступу до робочого простору",
|
||||
create_token: "Створити токен",
|
||||
never_expires: "Ніколи не спливає",
|
||||
generate_token: "Згенерувати токен",
|
||||
|
||||
@@ -1720,7 +1720,7 @@ export default {
|
||||
},
|
||||
api_tokens: {
|
||||
title: "Token API",
|
||||
add_token: "Thêm token API",
|
||||
add_token: "Thêm token truy cập không gian làm việc",
|
||||
create_token: "Tạo token",
|
||||
never_expires: "Không bao giờ hết hạn",
|
||||
generate_token: "Tạo token",
|
||||
|
||||
@@ -1691,7 +1691,7 @@ export default {
|
||||
},
|
||||
api_tokens: {
|
||||
title: "API 令牌",
|
||||
add_token: "添加 API 令牌",
|
||||
add_token: "添加工作区访问令牌",
|
||||
create_token: "创建令牌",
|
||||
never_expires: "永不过期",
|
||||
generate_token: "生成令牌",
|
||||
|
||||
@@ -1692,7 +1692,7 @@ export default {
|
||||
},
|
||||
api_tokens: {
|
||||
title: "API 權杖",
|
||||
add_token: "新增 API 權杖",
|
||||
add_token: "新增工作區存取權杖",
|
||||
create_token: "建立權杖",
|
||||
never_expires: "永不過期",
|
||||
generate_token: "產生權杖",
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from "./api-token.service";
|
||||
export * from "./webhook.service";
|
||||
export * from "./workspace-api-token.service";
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { API_BASE_URL } from "@plane/constants";
|
||||
import type { IApiToken } from "@plane/types";
|
||||
import { APIService } from "../api.service";
|
||||
|
||||
/**
|
||||
* Service class for managing API tokens for a workspace
|
||||
* Handles CRUD operations for API tokens
|
||||
* @extends {APIService}
|
||||
*/
|
||||
export class WorkspaceAPITokenService extends APIService {
|
||||
constructor(BASE_URL?: string) {
|
||||
super(BASE_URL || API_BASE_URL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all API tokens for a workspace
|
||||
* @param {string} workspaceSlug - The unique slug identifier for the workspace
|
||||
* @returns {Promise<IApiToken[]>} Promise resolving to array of API tokens
|
||||
* @throws {Error} If the API request fails
|
||||
*/
|
||||
async list(workspaceSlug: string): Promise<IApiToken[]> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/api-tokens/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves details of a specific API token
|
||||
* @param {string} workspaceSlug - The unique slug identifier for the workspace
|
||||
* @param {string} tokenId - The unique identifier of the API token
|
||||
* @returns {Promise<IApiToken>} Promise resolving to API token details
|
||||
* @throws {Error} If the API request fails
|
||||
*/
|
||||
async retrieve(workspaceSlug: string, tokenId: string): Promise<IApiToken> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/api-tokens/${tokenId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new API token for a workspace
|
||||
* @param {string} workspaceSlug - The unique slug identifier for the workspace
|
||||
* @param {Partial<IApiToken>} data - API token configuration data
|
||||
* @returns {Promise<IApiToken>} Promise resolving to the created API token
|
||||
* @throws {Error} If the API request fails
|
||||
*/
|
||||
async create(workspaceSlug: string, data: Partial<IApiToken>): Promise<IApiToken> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/api-tokens/`, data)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a specific API token from the workspace
|
||||
* @param {string} workspaceSlug - The unique slug identifier for the workspace
|
||||
* @param {string} tokenId - The unique identifier of the API token
|
||||
* @returns {Promise<void>} Promise resolving when API token is deleted
|
||||
* @throws {Error} If the API request fails
|
||||
*/
|
||||
async destroy(workspaceSlug: string, tokenId: string): Promise<void> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/api-tokens/${tokenId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user