Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
750364833b |
@@ -78,17 +78,10 @@ pnpm-workspace.yaml
|
||||
.npmrc
|
||||
.secrets
|
||||
tmp/
|
||||
|
||||
## packages
|
||||
dist
|
||||
.temp/
|
||||
deploy/selfhost/plane-app/
|
||||
|
||||
## Storybook
|
||||
*storybook.log
|
||||
output.css
|
||||
|
||||
dev-editor
|
||||
# Redis
|
||||
*.rdb
|
||||
*.rdb.gz
|
||||
|
||||
@@ -16,10 +16,10 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://plane.so/"><b>Website</b></a> •
|
||||
<a href="https://github.com/makeplane/plane/releases"><b>Releases</b></a> •
|
||||
<a href="https://twitter.com/planepowers"><b>Twitter</b></a> •
|
||||
<a href="https://docs.plane.so/"><b>Documentation</b></a>
|
||||
<a href="https://dub.sh/plane-website-readme"><b>Website</b></a> •
|
||||
<a href="https://git.new/releases"><b>Releases</b></a> •
|
||||
<a href="https://dub.sh/planepowershq"><b>Twitter</b></a> •
|
||||
<a href="https://dub.sh/planedocs"><b>Documentation</b></a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
@@ -39,7 +39,7 @@
|
||||
</a>
|
||||
</p>
|
||||
|
||||
Meet [Plane](https://plane.so/), an open-source project management tool to track issues, run ~sprints~ cycles, and manage product roadmaps without the chaos of managing the tool itself. 🧘♀️
|
||||
Meet [Plane](https://dub.sh/plane-website-readme), an open-source project management tool to track issues, run ~sprints~ cycles, and manage product roadmaps without the chaos of managing the tool itself. 🧘♀️
|
||||
|
||||
> Plane is evolving every day. Your suggestions, ideas, and reported bugs help us immensely. Do not hesitate to join in the conversation on [Discord](https://discord.com/invite/A92xrEGCge) or raise a GitHub issue. We read everything and respond to most.
|
||||
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@
|
||||
"lucide-react": "^0.469.0",
|
||||
"mobx": "^6.12.0",
|
||||
"mobx-react": "^9.1.1",
|
||||
"next": "^14.2.26",
|
||||
"next": "^14.2.25",
|
||||
"next-themes": "^0.2.1",
|
||||
"postcss": "^8.4.38",
|
||||
"react": "^18.3.1",
|
||||
|
||||
@@ -39,7 +39,7 @@ from plane.db.models import (
|
||||
UserFavorite,
|
||||
)
|
||||
from plane.utils.analytics_plot import burndown_plot
|
||||
from plane.utils.host import base_host
|
||||
|
||||
from .base import BaseAPIView
|
||||
from plane.bgtasks.webhook_task import model_activity
|
||||
|
||||
@@ -259,7 +259,7 @@ class CycleAPIEndpoint(BaseAPIView):
|
||||
current_instance=None,
|
||||
actor_id=request.user.id,
|
||||
slug=slug,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -331,7 +331,7 @@ class CycleAPIEndpoint(BaseAPIView):
|
||||
current_instance=current_instance,
|
||||
actor_id=request.user.id,
|
||||
slug=slug,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -702,7 +702,7 @@ class CycleIssueAPIEndpoint(BaseAPIView):
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
# Return all Cycle Issues
|
||||
return Response(
|
||||
@@ -1176,7 +1176,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
return Response({"message": "Success"}, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -18,7 +18,7 @@ from plane.api.serializers import IntakeIssueSerializer, IssueSerializer
|
||||
from plane.app.permissions import ProjectLitePermission
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
from plane.db.models import Intake, IntakeIssue, Issue, Project, ProjectMember, State
|
||||
from plane.utils.host import base_host
|
||||
|
||||
from .base import BaseAPIView
|
||||
|
||||
|
||||
@@ -297,7 +297,7 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
|
||||
current_instance=current_instance,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=False,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
intake=str(intake_issue.id),
|
||||
)
|
||||
|
||||
|
||||
@@ -56,7 +56,6 @@ from plane.db.models import (
|
||||
from plane.settings.storage import S3Storage
|
||||
from plane.bgtasks.storage_metadata_task import get_asset_object_metadata
|
||||
from .base import BaseAPIView
|
||||
from plane.utils.host import base_host
|
||||
|
||||
|
||||
class WorkspaceIssueAPIEndpoint(BaseAPIView):
|
||||
@@ -1049,7 +1048,7 @@ class IssueAttachmentEndpoint(BaseAPIView):
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
# Get the storage metadata
|
||||
@@ -1109,7 +1108,7 @@ class IssueAttachmentEndpoint(BaseAPIView):
|
||||
current_instance=json.dumps(serializer.data, cls=DjangoJSONEncoder),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
# Update the attachment
|
||||
|
||||
@@ -33,7 +33,6 @@ from plane.db.models import (
|
||||
|
||||
from .base import BaseAPIView
|
||||
from plane.bgtasks.webhook_task import model_activity
|
||||
from plane.utils.host import base_host
|
||||
|
||||
|
||||
class ModuleAPIEndpoint(BaseAPIView):
|
||||
@@ -175,7 +174,7 @@ class ModuleAPIEndpoint(BaseAPIView):
|
||||
current_instance=None,
|
||||
actor_id=request.user.id,
|
||||
slug=slug,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
module = Module.objects.get(pk=serializer.data["id"])
|
||||
serializer = ModuleSerializer(module)
|
||||
@@ -227,7 +226,7 @@ class ModuleAPIEndpoint(BaseAPIView):
|
||||
current_instance=current_instance,
|
||||
actor_id=request.user.id,
|
||||
slug=slug,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
@@ -281,7 +280,6 @@ class ModuleAPIEndpoint(BaseAPIView):
|
||||
project_id=str(project_id),
|
||||
current_instance=json.dumps({"module_name": str(module.name)}),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
origin=base_host(request=request, is_app=True),
|
||||
)
|
||||
module.delete()
|
||||
# Delete the module issues
|
||||
@@ -451,7 +449,6 @@ class ModuleIssueAPIEndpoint(BaseAPIView):
|
||||
}
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
origin=base_host(request=request, is_app=True),
|
||||
)
|
||||
|
||||
return Response(
|
||||
|
||||
@@ -30,7 +30,7 @@ from plane.db.models import (
|
||||
)
|
||||
from plane.bgtasks.webhook_task import model_activity, webhook_activity
|
||||
from .base import BaseAPIView
|
||||
from plane.utils.host import base_host
|
||||
|
||||
|
||||
class ProjectAPIEndpoint(BaseAPIView):
|
||||
"""Project Endpoints to create, update, list, retrieve and delete endpoint"""
|
||||
@@ -228,7 +228,7 @@ class ProjectAPIEndpoint(BaseAPIView):
|
||||
current_instance=None,
|
||||
actor_id=request.user.id,
|
||||
slug=slug,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
serializer = ProjectSerializer(project)
|
||||
@@ -297,7 +297,7 @@ class ProjectAPIEndpoint(BaseAPIView):
|
||||
current_instance=current_instance,
|
||||
actor_id=request.user.id,
|
||||
slug=slug,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
serializer = ProjectSerializer(project)
|
||||
@@ -334,7 +334,7 @@ class ProjectAPIEndpoint(BaseAPIView):
|
||||
new_value=None,
|
||||
actor_id=request.user.id,
|
||||
slug=slug,
|
||||
current_site=base_host(request=request, is_app=True),
|
||||
current_site=request.META.get("HTTP_ORIGIN"),
|
||||
event_id=project.id,
|
||||
old_identifier=None,
|
||||
new_identifier=None,
|
||||
|
||||
@@ -51,7 +51,8 @@ from plane.db.models import (
|
||||
)
|
||||
from plane.utils.analytics_plot import burndown_plot
|
||||
from plane.bgtasks.recent_visited_task import recent_visited_task
|
||||
from plane.utils.host import base_host
|
||||
|
||||
# Module imports
|
||||
from .. import BaseAPIView, BaseViewSet
|
||||
from plane.bgtasks.webhook_task import model_activity
|
||||
from plane.utils.timezone_converter import convert_to_utc, user_timezone_converter
|
||||
@@ -334,7 +335,7 @@ class CycleViewSet(BaseViewSet):
|
||||
current_instance=None,
|
||||
actor_id=request.user.id,
|
||||
slug=slug,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
return Response(cycle, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -427,7 +428,7 @@ class CycleViewSet(BaseViewSet):
|
||||
current_instance=current_instance,
|
||||
actor_id=request.user.id,
|
||||
slug=slug,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
return Response(cycle, status=status.HTTP_200_OK)
|
||||
@@ -540,7 +541,7 @@ class CycleViewSet(BaseViewSet):
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
# TODO: Soft delete the cycle break the onetoone relationship with cycle issue
|
||||
cycle.delete()
|
||||
@@ -1079,7 +1080,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
return Response({"message": "Success"}, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -27,7 +27,7 @@ from plane.utils.issue_filters import issue_filters
|
||||
from plane.utils.order_queryset import order_issue_queryset
|
||||
from plane.utils.paginator import GroupedOffsetPaginator, SubGroupedOffsetPaginator
|
||||
from plane.app.permissions import allow_permission, ROLE
|
||||
from plane.utils.host import base_host
|
||||
|
||||
|
||||
class CycleIssueViewSet(BaseViewSet):
|
||||
serializer_class = CycleIssueSerializer
|
||||
@@ -291,7 +291,7 @@ class CycleIssueViewSet(BaseViewSet):
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
return Response({"message": "success"}, status=status.HTTP_201_CREATED)
|
||||
|
||||
@@ -317,7 +317,7 @@ class CycleIssueViewSet(BaseViewSet):
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
cycle_issue.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -37,7 +37,6 @@ from plane.app.serializers import (
|
||||
)
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
from plane.utils.host import base_host
|
||||
|
||||
|
||||
class IntakeViewSet(BaseViewSet):
|
||||
@@ -284,7 +283,7 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
intake=str(intake_issue.id),
|
||||
)
|
||||
intake_issue = (
|
||||
@@ -408,7 +407,7 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
intake=str(intake_issue.id),
|
||||
)
|
||||
issue_serializer.save()
|
||||
@@ -468,7 +467,7 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
current_instance=current_instance,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=False,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
intake=(intake_issue.id),
|
||||
)
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ from plane.utils.order_queryset import order_issue_queryset
|
||||
from plane.utils.paginator import GroupedOffsetPaginator, SubGroupedOffsetPaginator
|
||||
from plane.app.permissions import allow_permission, ROLE
|
||||
from plane.utils.error_codes import ERROR_CODES
|
||||
from plane.utils.host import base_host
|
||||
|
||||
# Module imports
|
||||
from .. import BaseViewSet, BaseAPIView
|
||||
|
||||
@@ -259,7 +259,7 @@ class IssueArchiveViewSet(BaseViewSet):
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
issue.archived_at = timezone.now().date()
|
||||
issue.save()
|
||||
@@ -287,7 +287,7 @@ class IssueArchiveViewSet(BaseViewSet):
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
issue.archived_at = None
|
||||
issue.save()
|
||||
@@ -333,7 +333,7 @@ class BulkArchiveIssuesEndpoint(BaseAPIView):
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
issue.archived_at = timezone.now().date()
|
||||
bulk_archive_issues.append(issue)
|
||||
|
||||
@@ -21,7 +21,7 @@ from plane.bgtasks.issue_activities_task import issue_activity
|
||||
from plane.app.permissions import allow_permission, ROLE
|
||||
from plane.settings.storage import S3Storage
|
||||
from plane.bgtasks.storage_metadata_task import get_asset_object_metadata
|
||||
from plane.utils.host import base_host
|
||||
|
||||
|
||||
class IssueAttachmentEndpoint(BaseAPIView):
|
||||
serializer_class = IssueAttachmentSerializer
|
||||
@@ -48,7 +48,7 @@ class IssueAttachmentEndpoint(BaseAPIView):
|
||||
current_instance=json.dumps(serializer.data, cls=DjangoJSONEncoder),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -67,7 +67,7 @@ class IssueAttachmentEndpoint(BaseAPIView):
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
@@ -155,7 +155,7 @@ class IssueAttachmentV2Endpoint(BaseAPIView):
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
@@ -213,7 +213,7 @@ class IssueAttachmentV2Endpoint(BaseAPIView):
|
||||
current_instance=json.dumps(serializer.data, cls=DjangoJSONEncoder),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
# Update the attachment
|
||||
|
||||
@@ -61,7 +61,7 @@ from plane.bgtasks.recent_visited_task import recent_visited_task
|
||||
from plane.utils.global_paginator import paginate
|
||||
from plane.bgtasks.webhook_task import model_activity
|
||||
from plane.bgtasks.issue_description_version_task import issue_description_version_task
|
||||
from plane.utils.host import base_host
|
||||
|
||||
|
||||
class IssueListEndpoint(BaseAPIView):
|
||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
||||
@@ -379,7 +379,7 @@ class IssueViewSet(BaseViewSet):
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
issue = (
|
||||
issue_queryset_grouper(
|
||||
@@ -429,7 +429,7 @@ class IssueViewSet(BaseViewSet):
|
||||
current_instance=None,
|
||||
actor_id=request.user.id,
|
||||
slug=slug,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
# updated issue description version
|
||||
issue_description_version_task.delay(
|
||||
@@ -650,7 +650,7 @@ class IssueViewSet(BaseViewSet):
|
||||
current_instance=current_instance,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
model_activity.delay(
|
||||
model_name="issue",
|
||||
@@ -659,7 +659,7 @@ class IssueViewSet(BaseViewSet):
|
||||
current_instance=current_instance,
|
||||
actor_id=request.user.id,
|
||||
slug=slug,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
# updated issue description version
|
||||
issue_description_version_task.delay(
|
||||
@@ -691,8 +691,7 @@ class IssueViewSet(BaseViewSet):
|
||||
current_instance={},
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
subscriber=False,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ from plane.app.serializers import IssueCommentSerializer, CommentReactionSeriali
|
||||
from plane.app.permissions import allow_permission, ROLE
|
||||
from plane.db.models import IssueComment, ProjectMember, CommentReaction, Project, Issue
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
from plane.utils.host import base_host
|
||||
|
||||
|
||||
class IssueCommentViewSet(BaseViewSet):
|
||||
serializer_class = IssueCommentSerializer
|
||||
@@ -87,7 +87,7 @@ class IssueCommentViewSet(BaseViewSet):
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -105,13 +105,7 @@ class IssueCommentViewSet(BaseViewSet):
|
||||
issue_comment, data=request.data, partial=True
|
||||
)
|
||||
if serializer.is_valid():
|
||||
if (
|
||||
"comment_html" in request.data
|
||||
and request.data["comment_html"] != issue_comment.comment_html
|
||||
):
|
||||
serializer.save(edited_at=timezone.now())
|
||||
else:
|
||||
serializer.save()
|
||||
serializer.save()
|
||||
issue_activity.delay(
|
||||
type="comment.activity.updated",
|
||||
requested_data=requested_data,
|
||||
@@ -121,7 +115,7 @@ class IssueCommentViewSet(BaseViewSet):
|
||||
current_instance=current_instance,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -144,7 +138,7 @@ class IssueCommentViewSet(BaseViewSet):
|
||||
current_instance=current_instance,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -188,7 +182,7 @@ class CommentReactionViewSet(BaseViewSet):
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -222,7 +216,7 @@ class CommentReactionViewSet(BaseViewSet):
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
comment_reaction.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -15,7 +15,7 @@ from plane.app.serializers import IssueLinkSerializer
|
||||
from plane.app.permissions import ProjectEntityPermission
|
||||
from plane.db.models import IssueLink
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
from plane.utils.host import base_host
|
||||
|
||||
|
||||
class IssueLinkViewSet(BaseViewSet):
|
||||
permission_classes = [ProjectEntityPermission]
|
||||
@@ -52,7 +52,7 @@ class IssueLinkViewSet(BaseViewSet):
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -77,7 +77,7 @@ class IssueLinkViewSet(BaseViewSet):
|
||||
current_instance=current_instance,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -98,7 +98,7 @@ class IssueLinkViewSet(BaseViewSet):
|
||||
current_instance=current_instance,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
issue_link.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -15,7 +15,7 @@ from plane.app.serializers import IssueReactionSerializer
|
||||
from plane.app.permissions import allow_permission, ROLE
|
||||
from plane.db.models import IssueReaction
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
from plane.utils.host import base_host
|
||||
|
||||
|
||||
class IssueReactionViewSet(BaseViewSet):
|
||||
serializer_class = IssueReactionSerializer
|
||||
@@ -53,7 +53,7 @@ class IssueReactionViewSet(BaseViewSet):
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -78,7 +78,7 @@ class IssueReactionViewSet(BaseViewSet):
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
issue_reaction.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -27,7 +27,7 @@ from plane.db.models import (
|
||||
)
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
from plane.utils.issue_relation_mapper import get_actual_relation
|
||||
from plane.utils.host import base_host
|
||||
|
||||
|
||||
class IssueRelationViewSet(BaseViewSet):
|
||||
serializer_class = IssueRelationSerializer
|
||||
@@ -253,7 +253,7 @@ class IssueRelationViewSet(BaseViewSet):
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
if relation_type in ["blocking", "start_after", "finish_after"]:
|
||||
@@ -290,6 +290,6 @@ class IssueRelationViewSet(BaseViewSet):
|
||||
current_instance=current_instance,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -22,7 +22,7 @@ from plane.db.models import Issue, IssueLink, FileAsset, CycleIssue
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
from plane.utils.timezone_converter import user_timezone_converter
|
||||
from collections import defaultdict
|
||||
from plane.utils.host import base_host
|
||||
|
||||
|
||||
class SubIssuesEndpoint(BaseAPIView):
|
||||
permission_classes = [ProjectEntityPermission]
|
||||
@@ -176,7 +176,7 @@ class SubIssuesEndpoint(BaseAPIView):
|
||||
current_instance=json.dumps({"parent": str(sub_issue_id)}),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
for sub_issue_id in sub_issue_ids
|
||||
]
|
||||
|
||||
@@ -61,7 +61,7 @@ from plane.utils.timezone_converter import user_timezone_converter
|
||||
from plane.bgtasks.webhook_task import model_activity
|
||||
from .. import BaseAPIView, BaseViewSet
|
||||
from plane.bgtasks.recent_visited_task import recent_visited_task
|
||||
from plane.utils.host import base_host
|
||||
|
||||
|
||||
class ModuleViewSet(BaseViewSet):
|
||||
model = Module
|
||||
@@ -376,7 +376,7 @@ class ModuleViewSet(BaseViewSet):
|
||||
current_instance=None,
|
||||
actor_id=request.user.id,
|
||||
slug=slug,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
datetime_fields = ["created_at", "updated_at"]
|
||||
module = user_timezone_converter(
|
||||
@@ -768,7 +768,7 @@ class ModuleViewSet(BaseViewSet):
|
||||
current_instance=current_instance,
|
||||
actor_id=request.user.id,
|
||||
slug=slug,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
datetime_fields = ["created_at", "updated_at"]
|
||||
@@ -795,7 +795,7 @@ class ModuleViewSet(BaseViewSet):
|
||||
current_instance=json.dumps({"module_name": str(module.name)}),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
for issue in module_issues
|
||||
]
|
||||
|
||||
@@ -34,7 +34,7 @@ from plane.utils.paginator import GroupedOffsetPaginator, SubGroupedOffsetPagina
|
||||
|
||||
# Module imports
|
||||
from .. import BaseViewSet
|
||||
from plane.utils.host import base_host
|
||||
|
||||
|
||||
class ModuleIssueViewSet(BaseViewSet):
|
||||
serializer_class = ModuleIssueSerializer
|
||||
@@ -221,7 +221,7 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
for issue in issues
|
||||
]
|
||||
@@ -261,7 +261,7 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
for module in modules
|
||||
]
|
||||
@@ -284,7 +284,7 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
module_issue.delete()
|
||||
|
||||
@@ -309,7 +309,7 @@ class ModuleIssueViewSet(BaseViewSet):
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
module_issue.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -39,7 +39,7 @@ from plane.utils.cache import cache_response
|
||||
from plane.bgtasks.webhook_task import model_activity, webhook_activity
|
||||
from plane.bgtasks.recent_visited_task import recent_visited_task
|
||||
from plane.utils.exception_logger import log_exception
|
||||
from plane.utils.host import base_host
|
||||
|
||||
|
||||
class ProjectViewSet(BaseViewSet):
|
||||
serializer_class = ProjectListSerializer
|
||||
@@ -331,7 +331,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
current_instance=None,
|
||||
actor_id=request.user.id,
|
||||
slug=slug,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
serializer = ProjectListSerializer(project)
|
||||
@@ -409,7 +409,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
current_instance=current_instance,
|
||||
actor_id=request.user.id,
|
||||
slug=slug,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
serializer = ProjectListSerializer(project)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
@@ -454,7 +454,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
new_value=None,
|
||||
actor_id=request.user.id,
|
||||
slug=slug,
|
||||
current_site=base_host(request=request, is_app=True),
|
||||
current_site=request.META.get("HTTP_ORIGIN"),
|
||||
event_id=project.id,
|
||||
old_identifier=None,
|
||||
new_identifier=None,
|
||||
|
||||
@@ -27,7 +27,7 @@ from plane.db.models import (
|
||||
IssueUserProperty,
|
||||
)
|
||||
from plane.db.models.project import ProjectNetwork
|
||||
from plane.utils.host import base_host
|
||||
|
||||
|
||||
class ProjectInvitationsViewset(BaseViewSet):
|
||||
serializer_class = ProjectMemberInviteSerializer
|
||||
@@ -99,7 +99,7 @@ class ProjectInvitationsViewset(BaseViewSet):
|
||||
project_invitations = ProjectMemberInvite.objects.bulk_create(
|
||||
project_invitations, batch_size=10, ignore_conflicts=True
|
||||
)
|
||||
current_site = base_host(request=request, is_app=True)
|
||||
current_site = request.META.get("HTTP_ORIGIN")
|
||||
|
||||
# Send invitations
|
||||
for invitation in project_invitations:
|
||||
|
||||
@@ -21,161 +21,221 @@ class TimezoneEndpoint(APIView):
|
||||
|
||||
throttle_classes = [AuthenticationThrottle]
|
||||
|
||||
@method_decorator(cache_page(60 * 60 * 2))
|
||||
@method_decorator(cache_page(60 * 60 * 24))
|
||||
def get(self, request):
|
||||
timezone_locations = [
|
||||
('Midway Island', 'Pacific/Midway'), # UTC-11:00
|
||||
('American Samoa', 'Pacific/Pago_Pago'), # UTC-11:00
|
||||
('Hawaii', 'Pacific/Honolulu'), # UTC-10:00
|
||||
('Aleutian Islands', 'America/Adak'), # UTC-10:00 (DST: UTC-09:00)
|
||||
('Marquesas Islands', 'Pacific/Marquesas'), # UTC-09:30
|
||||
('Alaska', 'America/Anchorage'), # UTC-09:00 (DST: UTC-08:00)
|
||||
('Gambier Islands', 'Pacific/Gambier'), # UTC-09:00
|
||||
('Pacific Time (US and Canada)', 'America/Los_Angeles'), # UTC-08:00 (DST: UTC-07:00)
|
||||
('Baja California', 'America/Tijuana'), # UTC-08:00 (DST: UTC-07:00)
|
||||
('Mountain Time (US and Canada)', 'America/Denver'), # UTC-07:00 (DST: UTC-06:00)
|
||||
('Arizona', 'America/Phoenix'), # UTC-07:00
|
||||
('Chihuahua, Mazatlan', 'America/Chihuahua'), # UTC-07:00 (DST: UTC-06:00)
|
||||
('Central Time (US and Canada)', 'America/Chicago'), # UTC-06:00 (DST: UTC-05:00)
|
||||
('Saskatchewan', 'America/Regina'), # UTC-06:00
|
||||
('Guadalajara, Mexico City, Monterrey', 'America/Mexico_City'), # UTC-06:00 (DST: UTC-05:00)
|
||||
('Tegucigalpa, Honduras', 'America/Tegucigalpa'), # UTC-06:00
|
||||
('Costa Rica', 'America/Costa_Rica'), # UTC-06:00
|
||||
('Eastern Time (US and Canada)', 'America/New_York'), # UTC-05:00 (DST: UTC-04:00)
|
||||
('Lima', 'America/Lima'), # UTC-05:00
|
||||
('Bogota', 'America/Bogota'), # UTC-05:00
|
||||
('Quito', 'America/Guayaquil'), # UTC-05:00
|
||||
('Chetumal', 'America/Cancun'), # UTC-05:00 (DST: UTC-04:00)
|
||||
('Caracas (Old Venezuela Time)', 'America/Caracas'), # UTC-04:30
|
||||
('Atlantic Time (Canada)', 'America/Halifax'), # UTC-04:00 (DST: UTC-03:00)
|
||||
('Caracas', 'America/Caracas'), # UTC-04:00
|
||||
('Santiago', 'America/Santiago'), # UTC-04:00 (DST: UTC-03:00)
|
||||
('La Paz', 'America/La_Paz'), # UTC-04:00
|
||||
('Manaus', 'America/Manaus'), # UTC-04:00
|
||||
('Georgetown', 'America/Guyana'), # UTC-04:00
|
||||
('Bermuda', 'Atlantic/Bermuda'), # UTC-04:00 (DST: UTC-03:00)
|
||||
('Newfoundland Time (Canada)', 'America/St_Johns'), # UTC-03:30 (DST: UTC-02:30)
|
||||
('Buenos Aires', 'America/Argentina/Buenos_Aires'), # UTC-03:00
|
||||
('Brasilia', 'America/Sao_Paulo'), # UTC-03:00
|
||||
('Greenland', 'America/Godthab'), # UTC-03:00 (DST: UTC-02:00)
|
||||
('Montevideo', 'America/Montevideo'), # UTC-03:00
|
||||
('Falkland Islands', 'Atlantic/Stanley'), # UTC-03:00
|
||||
('South Georgia and the South Sandwich Islands', 'Atlantic/South_Georgia'), # UTC-02:00
|
||||
('Azores', 'Atlantic/Azores'), # UTC-01:00 (DST: UTC+00:00)
|
||||
('Cape Verde Islands', 'Atlantic/Cape_Verde'), # UTC-01:00
|
||||
('Dublin', 'Europe/Dublin'), # UTC+00:00 (DST: UTC+01:00)
|
||||
('Reykjavik', 'Atlantic/Reykjavik'), # UTC+00:00
|
||||
('Lisbon', 'Europe/Lisbon'), # UTC+00:00 (DST: UTC+01:00)
|
||||
('Monrovia', 'Africa/Monrovia'), # UTC+00:00
|
||||
('Casablanca', 'Africa/Casablanca'), # UTC+00:00 (DST: UTC+01:00)
|
||||
('Central European Time (Berlin, Rome, Paris)', 'Europe/Paris'), # UTC+01:00 (DST: UTC+02:00)
|
||||
('West Central Africa', 'Africa/Lagos'), # UTC+01:00
|
||||
('Algiers', 'Africa/Algiers'), # UTC+01:00
|
||||
('Lagos', 'Africa/Lagos'), # UTC+01:00
|
||||
('Tunis', 'Africa/Tunis'), # UTC+01:00
|
||||
('Eastern European Time (Cairo, Helsinki, Kyiv)', 'Europe/Kiev'), # UTC+02:00 (DST: UTC+03:00)
|
||||
('Athens', 'Europe/Athens'), # UTC+02:00 (DST: UTC+03:00)
|
||||
('Jerusalem', 'Asia/Jerusalem'), # UTC+02:00 (DST: UTC+03:00)
|
||||
('Johannesburg', 'Africa/Johannesburg'), # UTC+02:00
|
||||
('Harare, Pretoria', 'Africa/Harare'), # UTC+02:00
|
||||
('Moscow Time', 'Europe/Moscow'), # UTC+03:00
|
||||
('Baghdad', 'Asia/Baghdad'), # UTC+03:00
|
||||
('Nairobi', 'Africa/Nairobi'), # UTC+03:00
|
||||
('Kuwait, Riyadh', 'Asia/Riyadh'), # UTC+03:00
|
||||
('Tehran', 'Asia/Tehran'), # UTC+03:30 (DST: UTC+04:30)
|
||||
('Abu Dhabi', 'Asia/Dubai'), # UTC+04:00
|
||||
('Baku', 'Asia/Baku'), # UTC+04:00 (DST: UTC+05:00)
|
||||
('Yerevan', 'Asia/Yerevan'), # UTC+04:00 (DST: UTC+05:00)
|
||||
('Astrakhan', 'Europe/Astrakhan'), # UTC+04:00
|
||||
('Tbilisi', 'Asia/Tbilisi'), # UTC+04:00
|
||||
('Mauritius', 'Indian/Mauritius'), # UTC+04:00
|
||||
('Islamabad', 'Asia/Karachi'), # UTC+05:00
|
||||
('Karachi', 'Asia/Karachi'), # UTC+05:00
|
||||
('Tashkent', 'Asia/Tashkent'), # UTC+05:00
|
||||
('Yekaterinburg', 'Asia/Yekaterinburg'), # UTC+05:00
|
||||
('Maldives', 'Indian/Maldives'), # UTC+05:00
|
||||
('Chagos', 'Indian/Chagos'), # UTC+05:00
|
||||
('Chennai', 'Asia/Kolkata'), # UTC+05:30
|
||||
('Kolkata', 'Asia/Kolkata'), # UTC+05:30
|
||||
('Mumbai', 'Asia/Kolkata'), # UTC+05:30
|
||||
('New Delhi', 'Asia/Kolkata'), # UTC+05:30
|
||||
('Sri Jayawardenepura', 'Asia/Colombo'), # UTC+05:30
|
||||
('Kathmandu', 'Asia/Kathmandu'), # UTC+05:45
|
||||
('Dhaka', 'Asia/Dhaka'), # UTC+06:00
|
||||
('Almaty', 'Asia/Almaty'), # UTC+06:00
|
||||
('Bishkek', 'Asia/Bishkek'), # UTC+06:00
|
||||
('Thimphu', 'Asia/Thimphu'), # UTC+06:00
|
||||
('Yangon (Rangoon)', 'Asia/Yangon'), # UTC+06:30
|
||||
('Cocos Islands', 'Indian/Cocos'), # UTC+06:30
|
||||
('Bangkok', 'Asia/Bangkok'), # UTC+07:00
|
||||
('Hanoi', 'Asia/Ho_Chi_Minh'), # UTC+07:00
|
||||
('Jakarta', 'Asia/Jakarta'), # UTC+07:00
|
||||
('Novosibirsk', 'Asia/Novosibirsk'), # UTC+07:00
|
||||
('Krasnoyarsk', 'Asia/Krasnoyarsk'), # UTC+07:00
|
||||
('Beijing', 'Asia/Shanghai'), # UTC+08:00
|
||||
('Singapore', 'Asia/Singapore'), # UTC+08:00
|
||||
('Perth', 'Australia/Perth'), # UTC+08:00
|
||||
('Hong Kong', 'Asia/Hong_Kong'), # UTC+08:00
|
||||
('Ulaanbaatar', 'Asia/Ulaanbaatar'), # UTC+08:00
|
||||
('Palau', 'Pacific/Palau'), # UTC+08:00
|
||||
('Eucla', 'Australia/Eucla'), # UTC+08:45
|
||||
('Tokyo', 'Asia/Tokyo'), # UTC+09:00
|
||||
('Seoul', 'Asia/Seoul'), # UTC+09:00
|
||||
('Yakutsk', 'Asia/Yakutsk'), # UTC+09:00
|
||||
('Adelaide', 'Australia/Adelaide'), # UTC+09:30 (DST: UTC+10:30)
|
||||
('Darwin', 'Australia/Darwin'), # UTC+09:30
|
||||
('Sydney', 'Australia/Sydney'), # UTC+10:00 (DST: UTC+11:00)
|
||||
('Brisbane', 'Australia/Brisbane'), # UTC+10:00
|
||||
('Guam', 'Pacific/Guam'), # UTC+10:00
|
||||
('Vladivostok', 'Asia/Vladivostok'), # UTC+10:00
|
||||
('Tahiti', 'Pacific/Tahiti'), # UTC+10:00
|
||||
('Lord Howe Island', 'Australia/Lord_Howe'), # UTC+10:30 (DST: UTC+11:00)
|
||||
('Solomon Islands', 'Pacific/Guadalcanal'), # UTC+11:00
|
||||
('Magadan', 'Asia/Magadan'), # UTC+11:00
|
||||
('Norfolk Island', 'Pacific/Norfolk'), # UTC+11:00
|
||||
('Bougainville Island', 'Pacific/Bougainville'), # UTC+11:00
|
||||
('Chokurdakh', 'Asia/Srednekolymsk'), # UTC+11:00
|
||||
('Auckland', 'Pacific/Auckland'), # UTC+12:00 (DST: UTC+13:00)
|
||||
('Wellington', 'Pacific/Auckland'), # UTC+12:00 (DST: UTC+13:00)
|
||||
('Fiji Islands', 'Pacific/Fiji'), # UTC+12:00 (DST: UTC+13:00)
|
||||
('Anadyr', 'Asia/Anadyr'), # UTC+12:00
|
||||
('Chatham Islands', 'Pacific/Chatham'), # UTC+12:45 (DST: UTC+13:45)
|
||||
("Nuku'alofa", 'Pacific/Tongatapu'), # UTC+13:00
|
||||
('Samoa', 'Pacific/Apia'), # UTC+13:00 (DST: UTC+14:00)
|
||||
('Kiritimati Island', 'Pacific/Kiritimati') # UTC+14:00
|
||||
]
|
||||
timezone_mapping = {
|
||||
"-1100": [
|
||||
("Midway Island", "Pacific/Midway"),
|
||||
("American Samoa", "Pacific/Pago_Pago"),
|
||||
],
|
||||
"-1000": [
|
||||
("Hawaii", "Pacific/Honolulu"),
|
||||
("Aleutian Islands", "America/Adak"),
|
||||
],
|
||||
"-0930": [("Marquesas Islands", "Pacific/Marquesas")],
|
||||
"-0900": [
|
||||
("Alaska", "America/Anchorage"),
|
||||
("Gambier Islands", "Pacific/Gambier"),
|
||||
],
|
||||
"-0800": [
|
||||
("Pacific Time (US and Canada)", "America/Los_Angeles"),
|
||||
("Baja California", "America/Tijuana"),
|
||||
],
|
||||
"-0700": [
|
||||
("Mountain Time (US and Canada)", "America/Denver"),
|
||||
("Arizona", "America/Phoenix"),
|
||||
("Chihuahua, Mazatlan", "America/Chihuahua"),
|
||||
],
|
||||
"-0600": [
|
||||
("Central Time (US and Canada)", "America/Chicago"),
|
||||
("Saskatchewan", "America/Regina"),
|
||||
("Guadalajara, Mexico City, Monterrey", "America/Mexico_City"),
|
||||
("Tegucigalpa, Honduras", "America/Tegucigalpa"),
|
||||
("Costa Rica", "America/Costa_Rica"),
|
||||
],
|
||||
"-0500": [
|
||||
("Eastern Time (US and Canada)", "America/New_York"),
|
||||
("Lima", "America/Lima"),
|
||||
("Bogota", "America/Bogota"),
|
||||
("Quito", "America/Guayaquil"),
|
||||
("Chetumal", "America/Cancun"),
|
||||
],
|
||||
"-0430": [("Caracas (Old Venezuela Time)", "America/Caracas")],
|
||||
"-0400": [
|
||||
("Atlantic Time (Canada)", "America/Halifax"),
|
||||
("Caracas", "America/Caracas"),
|
||||
("Santiago", "America/Santiago"),
|
||||
("La Paz", "America/La_Paz"),
|
||||
("Manaus", "America/Manaus"),
|
||||
("Georgetown", "America/Guyana"),
|
||||
("Bermuda", "Atlantic/Bermuda"),
|
||||
],
|
||||
"-0330": [("Newfoundland Time (Canada)", "America/St_Johns")],
|
||||
"-0300": [
|
||||
("Buenos Aires", "America/Argentina/Buenos_Aires"),
|
||||
("Brasilia", "America/Sao_Paulo"),
|
||||
("Greenland", "America/Godthab"),
|
||||
("Montevideo", "America/Montevideo"),
|
||||
("Falkland Islands", "Atlantic/Stanley"),
|
||||
],
|
||||
"-0200": [
|
||||
(
|
||||
"South Georgia and the South Sandwich Islands",
|
||||
"Atlantic/South_Georgia",
|
||||
)
|
||||
],
|
||||
"-0100": [
|
||||
("Azores", "Atlantic/Azores"),
|
||||
("Cape Verde Islands", "Atlantic/Cape_Verde"),
|
||||
],
|
||||
"+0000": [
|
||||
("Dublin", "Europe/Dublin"),
|
||||
("Reykjavik", "Atlantic/Reykjavik"),
|
||||
("Lisbon", "Europe/Lisbon"),
|
||||
("Monrovia", "Africa/Monrovia"),
|
||||
("Casablanca", "Africa/Casablanca"),
|
||||
],
|
||||
"+0100": [
|
||||
("Central European Time (Berlin, Rome, Paris)", "Europe/Paris"),
|
||||
("West Central Africa", "Africa/Lagos"),
|
||||
("Algiers", "Africa/Algiers"),
|
||||
("Lagos", "Africa/Lagos"),
|
||||
("Tunis", "Africa/Tunis"),
|
||||
],
|
||||
"+0200": [
|
||||
("Eastern European Time (Cairo, Helsinki, Kyiv)", "Europe/Kiev"),
|
||||
("Athens", "Europe/Athens"),
|
||||
("Jerusalem", "Asia/Jerusalem"),
|
||||
("Johannesburg", "Africa/Johannesburg"),
|
||||
("Harare, Pretoria", "Africa/Harare"),
|
||||
],
|
||||
"+0300": [
|
||||
("Moscow Time", "Europe/Moscow"),
|
||||
("Baghdad", "Asia/Baghdad"),
|
||||
("Nairobi", "Africa/Nairobi"),
|
||||
("Kuwait, Riyadh", "Asia/Riyadh"),
|
||||
],
|
||||
"+0330": [("Tehran", "Asia/Tehran")],
|
||||
"+0400": [
|
||||
("Abu Dhabi", "Asia/Dubai"),
|
||||
("Baku", "Asia/Baku"),
|
||||
("Yerevan", "Asia/Yerevan"),
|
||||
("Astrakhan", "Europe/Astrakhan"),
|
||||
("Tbilisi", "Asia/Tbilisi"),
|
||||
("Mauritius", "Indian/Mauritius"),
|
||||
],
|
||||
"+0500": [
|
||||
("Islamabad", "Asia/Karachi"),
|
||||
("Karachi", "Asia/Karachi"),
|
||||
("Tashkent", "Asia/Tashkent"),
|
||||
("Yekaterinburg", "Asia/Yekaterinburg"),
|
||||
("Maldives", "Indian/Maldives"),
|
||||
("Chagos", "Indian/Chagos"),
|
||||
],
|
||||
"+0530": [
|
||||
("Chennai", "Asia/Kolkata"),
|
||||
("Kolkata", "Asia/Kolkata"),
|
||||
("Mumbai", "Asia/Kolkata"),
|
||||
("New Delhi", "Asia/Kolkata"),
|
||||
("Sri Jayawardenepura", "Asia/Colombo"),
|
||||
],
|
||||
"+0545": [("Kathmandu", "Asia/Kathmandu")],
|
||||
"+0600": [
|
||||
("Dhaka", "Asia/Dhaka"),
|
||||
("Almaty", "Asia/Almaty"),
|
||||
("Bishkek", "Asia/Bishkek"),
|
||||
("Thimphu", "Asia/Thimphu"),
|
||||
],
|
||||
"+0630": [
|
||||
("Yangon (Rangoon)", "Asia/Yangon"),
|
||||
("Cocos Islands", "Indian/Cocos"),
|
||||
],
|
||||
"+0700": [
|
||||
("Bangkok", "Asia/Bangkok"),
|
||||
("Hanoi", "Asia/Ho_Chi_Minh"),
|
||||
("Jakarta", "Asia/Jakarta"),
|
||||
("Novosibirsk", "Asia/Novosibirsk"),
|
||||
("Krasnoyarsk", "Asia/Krasnoyarsk"),
|
||||
],
|
||||
"+0800": [
|
||||
("Beijing", "Asia/Shanghai"),
|
||||
("Singapore", "Asia/Singapore"),
|
||||
("Perth", "Australia/Perth"),
|
||||
("Hong Kong", "Asia/Hong_Kong"),
|
||||
("Ulaanbaatar", "Asia/Ulaanbaatar"),
|
||||
("Palau", "Pacific/Palau"),
|
||||
],
|
||||
"+0845": [("Eucla", "Australia/Eucla")],
|
||||
"+0900": [
|
||||
("Tokyo", "Asia/Tokyo"),
|
||||
("Seoul", "Asia/Seoul"),
|
||||
("Yakutsk", "Asia/Yakutsk"),
|
||||
],
|
||||
"+0930": [
|
||||
("Adelaide", "Australia/Adelaide"),
|
||||
("Darwin", "Australia/Darwin"),
|
||||
],
|
||||
"+1000": [
|
||||
("Sydney", "Australia/Sydney"),
|
||||
("Brisbane", "Australia/Brisbane"),
|
||||
("Guam", "Pacific/Guam"),
|
||||
("Vladivostok", "Asia/Vladivostok"),
|
||||
("Tahiti", "Pacific/Tahiti"),
|
||||
],
|
||||
"+1030": [("Lord Howe Island", "Australia/Lord_Howe")],
|
||||
"+1100": [
|
||||
("Solomon Islands", "Pacific/Guadalcanal"),
|
||||
("Magadan", "Asia/Magadan"),
|
||||
("Norfolk Island", "Pacific/Norfolk"),
|
||||
("Bougainville Island", "Pacific/Bougainville"),
|
||||
("Chokurdakh", "Asia/Srednekolymsk"),
|
||||
],
|
||||
"+1200": [
|
||||
("Auckland", "Pacific/Auckland"),
|
||||
("Wellington", "Pacific/Auckland"),
|
||||
("Fiji Islands", "Pacific/Fiji"),
|
||||
("Anadyr", "Asia/Anadyr"),
|
||||
],
|
||||
"+1245": [("Chatham Islands", "Pacific/Chatham")],
|
||||
"+1300": [("Nuku'alofa", "Pacific/Tongatapu"), ("Samoa", "Pacific/Apia")],
|
||||
"+1400": [("Kiritimati Island", "Pacific/Kiritimati")],
|
||||
}
|
||||
|
||||
timezone_list = []
|
||||
now = datetime.now()
|
||||
|
||||
# Process timezone mapping
|
||||
for friendly_name, tz_identifier in timezone_locations:
|
||||
for offset, locations in timezone_mapping.items():
|
||||
sign = "-" if offset.startswith("-") else "+"
|
||||
hours = offset[1:3]
|
||||
minutes = offset[3:] if len(offset) > 3 else "00"
|
||||
|
||||
try:
|
||||
tz = pytz.timezone(tz_identifier)
|
||||
current_offset = now.astimezone(tz).strftime("%z")
|
||||
for friendly_name, tz_identifier in locations:
|
||||
try:
|
||||
tz = pytz.timezone(tz_identifier)
|
||||
current_offset = now.astimezone(tz).strftime("%z")
|
||||
|
||||
# converting and formatting UTC offset to GMT offset
|
||||
current_utc_offset = now.astimezone(tz).utcoffset()
|
||||
total_seconds = int(current_utc_offset.total_seconds())
|
||||
hours_offset = total_seconds // 3600
|
||||
minutes_offset = abs(total_seconds % 3600) // 60
|
||||
offset = (
|
||||
f"{'+' if hours_offset >= 0 else '-'}"
|
||||
f"{abs(hours_offset):02}:{minutes_offset:02}"
|
||||
)
|
||||
# converting and formatting UTC offset to GMT offset
|
||||
current_utc_offset = now.astimezone(tz).utcoffset()
|
||||
total_seconds = int(current_utc_offset.total_seconds())
|
||||
hours_offset = total_seconds // 3600
|
||||
minutes_offset = abs(total_seconds % 3600) // 60
|
||||
gmt_offset = (
|
||||
f"GMT{'+' if hours_offset >= 0 else '-'}"
|
||||
f"{abs(hours_offset):02}:{minutes_offset:02}"
|
||||
)
|
||||
|
||||
timezone_value = {
|
||||
"offset": int(current_offset),
|
||||
"utc_offset": f"UTC{offset}",
|
||||
"gmt_offset": f"GMT{offset}",
|
||||
"value": tz_identifier,
|
||||
"label": f"{friendly_name}",
|
||||
}
|
||||
timezone_value = {
|
||||
"offset": int(current_offset),
|
||||
"utc_offset": f"UTC{sign}{hours}:{minutes}",
|
||||
"gmt_offset": gmt_offset,
|
||||
"value": tz_identifier,
|
||||
"label": f"{friendly_name}",
|
||||
}
|
||||
|
||||
timezone_list.append(timezone_value)
|
||||
except pytz.exceptions.UnknownTimeZoneError:
|
||||
continue
|
||||
timezone_list.append(timezone_value)
|
||||
except pytz.exceptions.UnknownTimeZoneError:
|
||||
continue
|
||||
|
||||
# Sort by offset and then by label
|
||||
timezone_list.sort(key=lambda x: (x["offset"], x["label"]))
|
||||
|
||||
@@ -36,7 +36,7 @@ from plane.db.models import (
|
||||
from .. import BaseViewSet
|
||||
from plane.bgtasks.issue_activities_task import issue_activity
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
from plane.utils.host import base_host
|
||||
|
||||
|
||||
class WorkspaceDraftIssueViewSet(BaseViewSet):
|
||||
model = DraftIssue
|
||||
@@ -241,7 +241,7 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
if request.data.get("cycle_id", None):
|
||||
@@ -270,7 +270,7 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
|
||||
if request.data.get("module_ids", []):
|
||||
@@ -300,7 +300,7 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
|
||||
current_instance=None,
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=base_host(request=request, is_app=True),
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
for module in request.data.get("module_ids", [])
|
||||
]
|
||||
|
||||
@@ -7,6 +7,7 @@ import jwt
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import validate_email
|
||||
from django.db.models import Count
|
||||
from django.utils import timezone
|
||||
|
||||
# Third party modules
|
||||
@@ -25,8 +26,7 @@ from plane.bgtasks.event_tracking_task import workspace_invite_event
|
||||
from plane.bgtasks.workspace_invitation_task import workspace_invitation
|
||||
from plane.db.models import User, Workspace, WorkspaceMember, WorkspaceMemberInvite
|
||||
from plane.utils.cache import invalidate_cache, invalidate_cache_directly
|
||||
from plane.utils.host import base_host
|
||||
from plane.utils.ip_address import get_client_ip
|
||||
|
||||
from .. import BaseViewSet
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ class WorkspaceInvitationsViewset(BaseViewSet):
|
||||
workspace_invitations, batch_size=10, ignore_conflicts=True
|
||||
)
|
||||
|
||||
current_site = base_host(request=request, is_app=True)
|
||||
current_site = request.META.get("HTTP_ORIGIN")
|
||||
|
||||
# Send invitations
|
||||
for invitation in workspace_invitations:
|
||||
@@ -213,7 +213,7 @@ class WorkspaceJoinEndpoint(BaseAPIView):
|
||||
user=user.id if user is not None else None,
|
||||
email=email,
|
||||
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||
ip=get_client_ip(request=request),
|
||||
ip=request.META.get("REMOTE_ADDR"),
|
||||
event_name="MEMBER_ACCEPTED",
|
||||
accepted_from="EMAIL",
|
||||
)
|
||||
|
||||
@@ -15,8 +15,8 @@ from plane.db.models import Profile, User, WorkspaceMemberInvite
|
||||
from plane.license.utils.instance_value import get_configuration_value
|
||||
from .error import AuthenticationException, AUTHENTICATION_ERROR_CODES
|
||||
from plane.bgtasks.user_activation_email_task import user_activation_email
|
||||
from plane.utils.host import base_host
|
||||
from plane.utils.ip_address import get_client_ip
|
||||
from plane.authentication.utils.host import base_host
|
||||
|
||||
|
||||
class Adapter:
|
||||
"""Common interface for all auth providers"""
|
||||
@@ -108,7 +108,7 @@ class Adapter:
|
||||
user.last_login_medium = self.provider
|
||||
user.last_active = timezone.now()
|
||||
user.last_login_time = timezone.now()
|
||||
user.last_login_ip = get_client_ip(request=self.request)
|
||||
user.last_login_ip = self.request.META.get("REMOTE_ADDR")
|
||||
user.last_login_uagent = self.request.META.get("HTTP_USER_AGENT")
|
||||
user.token_updated_at = timezone.now()
|
||||
# If user is not active, send the activation email and set the user as active
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
# Python imports
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
# Django imports
|
||||
from django.conf import settings
|
||||
from django.http import HttpRequest
|
||||
# Third party imports
|
||||
from rest_framework.request import Request
|
||||
|
||||
# Module imports
|
||||
from plane.utils.ip_address import get_client_ip
|
||||
|
||||
def base_host(request: Request | HttpRequest, is_admin: bool = False, is_space: bool = False, is_app: bool = False) -> str:
|
||||
def base_host(request, is_admin=False, is_space=False, is_app=False):
|
||||
"""Utility function to return host / origin from the request"""
|
||||
# Calculate the base origin from request
|
||||
base_origin = settings.WEB_URL or settings.APP_BASE_URL
|
||||
base_origin = str(
|
||||
request.META.get("HTTP_ORIGIN")
|
||||
or f"{urlsplit(request.META.get('HTTP_REFERER')).scheme}://{urlsplit(request.META.get('HTTP_REFERER')).netloc}"
|
||||
or f"""{"https" if request.is_secure() else "http"}://{request.get_host()}"""
|
||||
)
|
||||
|
||||
# Admin redirections
|
||||
if is_admin:
|
||||
@@ -36,5 +38,5 @@ def base_host(request: Request | HttpRequest, is_admin: bool = False, is_space:
|
||||
return base_origin
|
||||
|
||||
|
||||
def user_ip(request: Request | HttpRequest) -> str:
|
||||
return get_client_ip(request=request)
|
||||
def user_ip(request):
|
||||
return str(request.META.get("REMOTE_ADDR"))
|
||||
|
||||
@@ -3,8 +3,8 @@ from django.contrib.auth import login
|
||||
from django.conf import settings
|
||||
|
||||
# Module imports
|
||||
from plane.utils.host import base_host
|
||||
from plane.utils.ip_address import get_client_ip
|
||||
from plane.authentication.utils.host import base_host
|
||||
|
||||
|
||||
def user_login(request, user, is_app=False, is_admin=False, is_space=False):
|
||||
login(request=request, user=user)
|
||||
@@ -15,7 +15,7 @@ def user_login(request, user, is_app=False, is_admin=False, is_space=False):
|
||||
|
||||
device_info = {
|
||||
"user_agent": request.META.get("HTTP_USER_AGENT", ""),
|
||||
"ip_address": get_client_ip(request=request),
|
||||
"ip_address": request.META.get("REMOTE_ADDR", ""),
|
||||
"domain": base_host(
|
||||
request=request, is_app=is_app, is_admin=is_admin, is_space=is_space
|
||||
),
|
||||
|
||||
@@ -19,7 +19,7 @@ from plane.authentication.adapter.error import (
|
||||
AuthenticationException,
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
)
|
||||
from plane.utils.path_validator import validate_next_path
|
||||
|
||||
|
||||
class SignInAuthEndpoint(View):
|
||||
def post(self, request):
|
||||
@@ -34,7 +34,7 @@ class SignInAuthEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
# Base URL join
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "sign-in?" + urlencode(params)
|
||||
@@ -58,7 +58,7 @@ class SignInAuthEndpoint(View):
|
||||
params = exc.get_error_dict()
|
||||
# Next path
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "sign-in?" + urlencode(params)
|
||||
)
|
||||
@@ -76,7 +76,7 @@ class SignInAuthEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "sign-in?" + urlencode(params)
|
||||
)
|
||||
@@ -92,7 +92,7 @@ class SignInAuthEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "sign-in?" + urlencode(params)
|
||||
)
|
||||
@@ -111,7 +111,7 @@ class SignInAuthEndpoint(View):
|
||||
user_login(request=request, user=user, is_app=True)
|
||||
# Get the redirection path
|
||||
if next_path:
|
||||
path = str(validate_next_path(next_path))
|
||||
path = str(next_path)
|
||||
else:
|
||||
path = get_redirection_path(user=user)
|
||||
|
||||
@@ -121,7 +121,7 @@ class SignInAuthEndpoint(View):
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "sign-in?" + urlencode(params)
|
||||
)
|
||||
@@ -141,7 +141,7 @@ class SignUpAuthEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "?" + urlencode(params)
|
||||
)
|
||||
@@ -161,7 +161,7 @@ class SignUpAuthEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "?" + urlencode(params)
|
||||
)
|
||||
@@ -179,7 +179,7 @@ class SignUpAuthEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "?" + urlencode(params)
|
||||
)
|
||||
@@ -197,7 +197,7 @@ class SignUpAuthEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "?" + urlencode(params)
|
||||
)
|
||||
@@ -216,7 +216,7 @@ class SignUpAuthEndpoint(View):
|
||||
user_login(request=request, user=user, is_app=True)
|
||||
# Get the redirection path
|
||||
if next_path:
|
||||
path = str(validate_next_path(next_path))
|
||||
path = next_path
|
||||
else:
|
||||
path = get_redirection_path(user=user)
|
||||
# redirect to referer path
|
||||
@@ -225,7 +225,7 @@ class SignUpAuthEndpoint(View):
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "?" + urlencode(params)
|
||||
)
|
||||
|
||||
@@ -16,7 +16,7 @@ from plane.authentication.adapter.error import (
|
||||
AuthenticationException,
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
)
|
||||
from plane.utils.path_validator import validate_next_path
|
||||
|
||||
|
||||
class GitHubOauthInitiateEndpoint(View):
|
||||
def get(self, request):
|
||||
@@ -35,7 +35,7 @@ class GitHubOauthInitiateEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "?" + urlencode(params)
|
||||
)
|
||||
@@ -49,7 +49,7 @@ class GitHubOauthInitiateEndpoint(View):
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "?" + urlencode(params)
|
||||
)
|
||||
@@ -70,7 +70,7 @@ class GitHubCallbackEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(base_host, "?" + urlencode(params))
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -81,7 +81,7 @@ class GitHubCallbackEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(base_host, "?" + urlencode(params))
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -94,7 +94,7 @@ class GitHubCallbackEndpoint(View):
|
||||
user_login(request=request, user=user, is_app=True)
|
||||
# Get the redirection path
|
||||
if next_path:
|
||||
path = str(validate_next_path(next_path))
|
||||
path = next_path
|
||||
else:
|
||||
path = get_redirection_path(user=user)
|
||||
# redirect to referer path
|
||||
@@ -103,6 +103,6 @@ class GitHubCallbackEndpoint(View):
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(base_host, "?" + urlencode(params))
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -16,7 +16,7 @@ from plane.authentication.adapter.error import (
|
||||
AuthenticationException,
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
)
|
||||
from plane.utils.path_validator import validate_next_path
|
||||
|
||||
|
||||
class GitLabOauthInitiateEndpoint(View):
|
||||
def get(self, request):
|
||||
@@ -24,7 +24,7 @@ class GitLabOauthInitiateEndpoint(View):
|
||||
request.session["host"] = base_host(request=request, is_app=True)
|
||||
next_path = request.GET.get("next_path")
|
||||
if next_path:
|
||||
request.session["next_path"] = str(validate_next_path(next_path))
|
||||
request.session["next_path"] = str(next_path)
|
||||
|
||||
# Check instance configuration
|
||||
instance = Instance.objects.first()
|
||||
@@ -35,7 +35,7 @@ class GitLabOauthInitiateEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "?" + urlencode(params)
|
||||
)
|
||||
@@ -49,7 +49,7 @@ class GitLabOauthInitiateEndpoint(View):
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "?" + urlencode(params)
|
||||
)
|
||||
@@ -81,7 +81,7 @@ class GitLabCallbackEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(base_host, "?" + urlencode(params))
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -94,7 +94,7 @@ class GitLabCallbackEndpoint(View):
|
||||
user_login(request=request, user=user, is_app=True)
|
||||
# Get the redirection path
|
||||
if next_path:
|
||||
path = str(validate_next_path(next_path))
|
||||
path = next_path
|
||||
else:
|
||||
path = get_redirection_path(user=user)
|
||||
# redirect to referer path
|
||||
@@ -103,6 +103,6 @@ class GitLabCallbackEndpoint(View):
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(base_host, "?" + urlencode(params))
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -18,7 +18,7 @@ from plane.authentication.adapter.error import (
|
||||
AuthenticationException,
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
)
|
||||
from plane.utils.path_validator import validate_next_path
|
||||
|
||||
|
||||
class GoogleOauthInitiateEndpoint(View):
|
||||
def get(self, request):
|
||||
@@ -36,7 +36,7 @@ class GoogleOauthInitiateEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "?" + urlencode(params)
|
||||
)
|
||||
@@ -51,7 +51,7 @@ class GoogleOauthInitiateEndpoint(View):
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "?" + urlencode(params)
|
||||
)
|
||||
@@ -72,7 +72,7 @@ class GoogleCallbackEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(base_host, "?" + urlencode(params))
|
||||
return HttpResponseRedirect(url)
|
||||
if not code:
|
||||
@@ -82,7 +82,7 @@ class GoogleCallbackEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = next_path
|
||||
url = urljoin(base_host, "?" + urlencode(params))
|
||||
return HttpResponseRedirect(url)
|
||||
try:
|
||||
@@ -95,11 +95,11 @@ class GoogleCallbackEndpoint(View):
|
||||
# Get the redirection path
|
||||
path = get_redirection_path(user=user)
|
||||
# redirect to referer path
|
||||
url = urljoin(base_host, str(validate_next_path(next_path)) if next_path else path)
|
||||
url = urljoin(base_host, str(next_path) if next_path else path)
|
||||
return HttpResponseRedirect(url)
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(base_host, "?" + urlencode(params))
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -26,7 +26,6 @@ from plane.authentication.adapter.error import (
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
)
|
||||
from plane.authentication.rate_limit import AuthenticationThrottle
|
||||
from plane.utils.path_validator import validate_next_path
|
||||
|
||||
|
||||
class MagicGenerateEndpoint(APIView):
|
||||
@@ -44,13 +43,14 @@ class MagicGenerateEndpoint(APIView):
|
||||
)
|
||||
return Response(exc.get_error_dict(), status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
origin = request.META.get("HTTP_ORIGIN", "/")
|
||||
email = request.data.get("email", "").strip().lower()
|
||||
try:
|
||||
validate_email(email)
|
||||
adapter = MagicCodeProvider(request=request, key=email)
|
||||
key, token = adapter.initiate()
|
||||
# If the smtp is configured send through here
|
||||
magic_link.delay(email, key, token)
|
||||
magic_link.delay(email, key, token, origin)
|
||||
return Response({"key": str(key)}, status=status.HTTP_200_OK)
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
@@ -73,7 +73,7 @@ class MagicSignInEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "sign-in?" + urlencode(params)
|
||||
)
|
||||
@@ -89,7 +89,7 @@ class MagicSignInEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "sign-in?" + urlencode(params)
|
||||
)
|
||||
@@ -122,7 +122,7 @@ class MagicSignInEndpoint(View):
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "sign-in?" + urlencode(params)
|
||||
)
|
||||
@@ -145,7 +145,7 @@ class MagicSignUpEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "?" + urlencode(params)
|
||||
)
|
||||
@@ -159,7 +159,7 @@ class MagicSignUpEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "?" + urlencode(params)
|
||||
)
|
||||
@@ -177,7 +177,7 @@ class MagicSignUpEndpoint(View):
|
||||
user_login(request=request, user=user, is_app=True)
|
||||
# Get the redirection path
|
||||
if next_path:
|
||||
path = str(validate_next_path(next_path))
|
||||
path = str(next_path)
|
||||
else:
|
||||
path = get_redirection_path(user=user)
|
||||
# redirect to referer path
|
||||
@@ -187,7 +187,7 @@ class MagicSignUpEndpoint(View):
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True), "?" + urlencode(params)
|
||||
)
|
||||
|
||||
@@ -80,7 +80,7 @@ class ForgotPasswordEndpoint(APIView):
|
||||
if user:
|
||||
# Get the reset token for user
|
||||
uidb64, token = generate_password_token(user=user)
|
||||
current_site = base_host(request=request, is_app=True)
|
||||
current_site = request.META.get("HTTP_ORIGIN")
|
||||
# send the forgot password email
|
||||
forgot_password.delay(
|
||||
user.first_name, user.email, uidb64, token, current_site
|
||||
|
||||
@@ -44,21 +44,10 @@ class ChangePasswordEndpoint(APIView):
|
||||
def post(self, request):
|
||||
user = User.objects.get(pk=request.user.id)
|
||||
|
||||
# If the user password is not autoset then we need to check the old passwords
|
||||
if not user.is_password_autoset:
|
||||
old_password = request.data.get("old_password", False)
|
||||
if not old_password:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["MISSING_PASSWORD"],
|
||||
error_message="MISSING_PASSWORD",
|
||||
payload={"error": "Old password is missing"},
|
||||
)
|
||||
return Response(exc.get_error_dict(), status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Get the new password
|
||||
old_password = request.data.get("old_password", False)
|
||||
new_password = request.data.get("new_password", False)
|
||||
|
||||
if not new_password:
|
||||
if not old_password or not new_password:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["MISSING_PASSWORD"],
|
||||
error_message="MISSING_PASSWORD",
|
||||
@@ -66,9 +55,7 @@ class ChangePasswordEndpoint(APIView):
|
||||
)
|
||||
return Response(exc.get_error_dict(), status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
# If the user password is not autoset then we need to check the old passwords
|
||||
if not user.is_password_autoset and not user.check_password(old_password):
|
||||
if not user.check_password(old_password):
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["INCORRECT_OLD_PASSWORD"],
|
||||
error_message="INCORRECT_OLD_PASSWORD",
|
||||
|
||||
@@ -17,7 +17,6 @@ from plane.authentication.adapter.error import (
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
AuthenticationException,
|
||||
)
|
||||
from plane.utils.path_validator import validate_next_path
|
||||
|
||||
|
||||
class SignInAuthSpaceEndpoint(View):
|
||||
@@ -33,7 +32,7 @@ class SignInAuthSpaceEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -52,7 +51,7 @@ class SignInAuthSpaceEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -68,7 +67,7 @@ class SignInAuthSpaceEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -83,7 +82,7 @@ class SignInAuthSpaceEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -100,7 +99,7 @@ class SignInAuthSpaceEndpoint(View):
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -118,7 +117,7 @@ class SignUpAuthSpaceEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -136,7 +135,7 @@ class SignUpAuthSpaceEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
# Validate the email
|
||||
@@ -152,7 +151,7 @@ class SignUpAuthSpaceEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -167,7 +166,7 @@ class SignUpAuthSpaceEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -184,6 +183,6 @@ class SignUpAuthSpaceEndpoint(View):
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -15,7 +15,6 @@ from plane.authentication.adapter.error import (
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
AuthenticationException,
|
||||
)
|
||||
from plane.utils.path_validator import validate_next_path
|
||||
|
||||
|
||||
class GitHubOauthInitiateSpaceEndpoint(View):
|
||||
@@ -35,7 +34,7 @@ class GitHubOauthInitiateSpaceEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -67,7 +66,7 @@ class GitHubCallbackSpaceEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -78,7 +77,7 @@ class GitHubCallbackSpaceEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -94,6 +93,6 @@ class GitHubCallbackSpaceEndpoint(View):
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -15,7 +15,6 @@ from plane.authentication.adapter.error import (
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
AuthenticationException,
|
||||
)
|
||||
from plane.utils.path_validator import validate_next_path
|
||||
|
||||
|
||||
class GitLabOauthInitiateSpaceEndpoint(View):
|
||||
@@ -35,7 +34,7 @@ class GitLabOauthInitiateSpaceEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -67,7 +66,7 @@ class GitLabCallbackSpaceEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -78,7 +77,7 @@ class GitLabCallbackSpaceEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -94,6 +93,6 @@ class GitLabCallbackSpaceEndpoint(View):
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -15,7 +15,6 @@ from plane.authentication.adapter.error import (
|
||||
AuthenticationException,
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
)
|
||||
from plane.utils.path_validator import validate_next_path
|
||||
|
||||
|
||||
class GoogleOauthInitiateSpaceEndpoint(View):
|
||||
@@ -34,7 +33,7 @@ class GoogleOauthInitiateSpaceEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -47,7 +46,7 @@ class GoogleOauthInitiateSpaceEndpoint(View):
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -66,7 +65,7 @@ class GoogleCallbackSpaceEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
if not code:
|
||||
@@ -76,7 +75,7 @@ class GoogleCallbackSpaceEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = next_path
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
try:
|
||||
@@ -90,6 +89,6 @@ class GoogleCallbackSpaceEndpoint(View):
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -23,7 +23,7 @@ from plane.authentication.adapter.error import (
|
||||
AuthenticationException,
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
)
|
||||
from plane.utils.path_validator import validate_next_path
|
||||
|
||||
|
||||
class MagicGenerateSpaceEndpoint(APIView):
|
||||
permission_classes = [AllowAny]
|
||||
@@ -38,14 +38,14 @@ class MagicGenerateSpaceEndpoint(APIView):
|
||||
)
|
||||
return Response(exc.get_error_dict(), status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
origin = base_host(request=request, is_space=True)
|
||||
email = request.data.get("email", "").strip().lower()
|
||||
try:
|
||||
validate_email(email)
|
||||
adapter = MagicCodeProvider(request=request, key=email)
|
||||
key, token = adapter.initiate()
|
||||
# If the smtp is configured send through here
|
||||
magic_link.delay(email, key, token)
|
||||
magic_link.delay(email, key, token, origin)
|
||||
return Response({"key": str(key)}, status=status.HTTP_200_OK)
|
||||
except AuthenticationException as e:
|
||||
return Response(e.get_error_dict(), status=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -67,7 +67,7 @@ class MagicSignInSpaceEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -80,7 +80,7 @@ class MagicSignInSpaceEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -121,7 +121,7 @@ class MagicSignUpSpaceEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
# Existing User
|
||||
@@ -134,7 +134,7 @@ class MagicSignUpSpaceEndpoint(View):
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -152,6 +152,6 @@ class MagicSignUpSpaceEndpoint(View):
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(validate_next_path(next_path))
|
||||
params["next_path"] = str(next_path)
|
||||
url = f"{base_host(request=request, is_space=True)}?{urlencode(params)}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -90,7 +90,7 @@ class ForgotPasswordSpaceEndpoint(APIView):
|
||||
if user:
|
||||
# Get the reset token for user
|
||||
uidb64, token = generate_password_token(user=user)
|
||||
current_site = base_host(request=request, is_space=True)
|
||||
current_site = request.META.get("HTTP_ORIGIN")
|
||||
# send the forgot password email
|
||||
forgot_password.delay(
|
||||
user.first_name, user.email, uidb64, token, current_site
|
||||
|
||||
@@ -7,7 +7,6 @@ from django.utils import timezone
|
||||
# Module imports
|
||||
from plane.authentication.utils.host import base_host, user_ip
|
||||
from plane.db.models import User
|
||||
from plane.utils.path_validator import validate_next_path
|
||||
|
||||
|
||||
class SignOutAuthSpaceEndpoint(View):
|
||||
@@ -22,8 +21,8 @@ class SignOutAuthSpaceEndpoint(View):
|
||||
user.save()
|
||||
# Log the user out
|
||||
logout(request)
|
||||
url = f"{base_host(request=request, is_space=True)}{str(validate_next_path(next_path)) if next_path else ''}"
|
||||
url = f"{base_host(request=request, is_space=True)}{next_path}"
|
||||
return HttpResponseRedirect(url)
|
||||
except Exception:
|
||||
url = f"{base_host(request=request, is_space=True)}{str(validate_next_path(next_path)) if next_path else ''}"
|
||||
url = f"{base_host(request=request, is_space=True)}{next_path}"
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -16,7 +16,7 @@ from plane.utils.exception_logger import log_exception
|
||||
|
||||
|
||||
@shared_task
|
||||
def magic_link(email, key, token):
|
||||
def magic_link(email, key, token, current_site):
|
||||
try:
|
||||
(
|
||||
EMAIL_HOST,
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
import time
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db import transaction
|
||||
from plane.db.models import Workspace
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Updates the slug of a soft-deleted workspace by appending the epoch timestamp"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"slug",
|
||||
type=str,
|
||||
help="The slug of the workspace to update",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Run the command without making any changes",
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
slug = options["slug"]
|
||||
dry_run = options["dry_run"]
|
||||
|
||||
# Get the workspace with the specified slug
|
||||
try:
|
||||
workspace = Workspace.all_objects.get(slug=slug)
|
||||
except Workspace.DoesNotExist:
|
||||
self.stdout.write(
|
||||
self.style.ERROR(f"Workspace with slug '{slug}' not found.")
|
||||
)
|
||||
return
|
||||
|
||||
# Check if the workspace is soft-deleted
|
||||
if workspace.deleted_at is None:
|
||||
self.stdout.write(
|
||||
self.style.WARNING(
|
||||
f"Workspace '{workspace.name}' (slug: {workspace.slug}) is not deleted."
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
# Check if the slug already has a timestamp appended
|
||||
if "__" in workspace.slug and workspace.slug.split("__")[-1].isdigit():
|
||||
self.stdout.write(
|
||||
self.style.WARNING(
|
||||
f"Workspace '{workspace.name}' (slug: {workspace.slug}) already has a timestamp appended."
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
# Get the deletion timestamp
|
||||
deletion_timestamp = int(workspace.deleted_at.timestamp())
|
||||
|
||||
# Create the new slug with the deletion timestamp
|
||||
new_slug = f"{workspace.slug}__{deletion_timestamp}"
|
||||
|
||||
if dry_run:
|
||||
self.stdout.write(
|
||||
f"Would update workspace '{workspace.name}' slug from '{workspace.slug}' to '{new_slug}'"
|
||||
)
|
||||
else:
|
||||
try:
|
||||
with transaction.atomic():
|
||||
workspace.slug = new_slug
|
||||
workspace.save(update_fields=["slug"])
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f"Updated workspace '{workspace.name}' slug from '{workspace.slug}' to '{new_slug}'"
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
self.stdout.write(
|
||||
self.style.ERROR(
|
||||
f"Error updating workspace '{workspace.name}': {str(e)}"
|
||||
)
|
||||
)
|
||||
@@ -1,9 +1,6 @@
|
||||
# Python imports
|
||||
from django.db.models.functions import Ln
|
||||
import pytz
|
||||
import time
|
||||
from django.utils import timezone
|
||||
from typing import Optional, Any, Tuple, Dict
|
||||
|
||||
# Django imports
|
||||
from django.conf import settings
|
||||
@@ -152,34 +149,6 @@ class Workspace(BaseModel):
|
||||
return self.logo
|
||||
return None
|
||||
|
||||
def delete(
|
||||
self,
|
||||
using: Optional[str] = None,
|
||||
soft: bool = True,
|
||||
*args: Any,
|
||||
**kwargs: Any
|
||||
):
|
||||
"""
|
||||
Override the delete method to append epoch timestamp to the slug when soft deleting.
|
||||
|
||||
Args:
|
||||
using: The database alias to use for the deletion.
|
||||
soft: Whether to perform a soft delete (True) or hard delete (False).
|
||||
*args: Additional positional arguments.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
# Call the parent class's delete method first
|
||||
result = super().delete(using=using, soft=soft, *args, **kwargs)
|
||||
|
||||
# If it's a soft delete and the model still exists (not hard deleted)
|
||||
if soft and hasattr(self, 'deleted_at') and self.deleted_at:
|
||||
# Use the deleted_at timestamp to update the slug
|
||||
deletion_timestamp: int = int(self.deleted_at.timestamp())
|
||||
self.slug = f"{self.slug}__{deletion_timestamp}"
|
||||
self.save(update_fields=["slug"])
|
||||
|
||||
return result
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Workspace"
|
||||
verbose_name_plural = "Workspaces"
|
||||
@@ -422,7 +391,7 @@ class WorkspaceHomePreference(BaseModel):
|
||||
class WorkspaceUserPreference(BaseModel):
|
||||
"""Preference for the workspace for a user"""
|
||||
|
||||
class UserPreferenceKeys(models.TextChoices):
|
||||
class UserPreferenceKeys(models.TextChoices):
|
||||
VIEWS = "views", "Views"
|
||||
ACTIVE_CYCLES = "active_cycles", "Active Cycles"
|
||||
ANALYTICS = "analytics", "Analytics"
|
||||
|
||||
@@ -33,7 +33,6 @@ from plane.authentication.adapter.error import (
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
AuthenticationException,
|
||||
)
|
||||
from plane.utils.ip_address import get_client_ip
|
||||
|
||||
|
||||
class InstanceAdminEndpoint(BaseAPIView):
|
||||
@@ -218,7 +217,7 @@ class InstanceAdminSignUpEndpoint(View):
|
||||
user.is_active = True
|
||||
user.last_active = timezone.now()
|
||||
user.last_login_time = timezone.now()
|
||||
user.last_login_ip = get_client_ip(request=request)
|
||||
user.last_login_ip = request.META.get("REMOTE_ADDR")
|
||||
user.last_login_uagent = request.META.get("HTTP_USER_AGENT")
|
||||
user.token_updated_at = timezone.now()
|
||||
user.save()
|
||||
@@ -345,7 +344,7 @@ class InstanceAdminSignInEndpoint(View):
|
||||
user.is_active = True
|
||||
user.last_active = timezone.now()
|
||||
user.last_login_time = timezone.now()
|
||||
user.last_login_ip = get_client_ip(request=request)
|
||||
user.last_login_ip = request.META.get("REMOTE_ADDR")
|
||||
user.last_login_uagent = request.META.get("HTTP_USER_AGENT")
|
||||
user.token_updated_at = timezone.now()
|
||||
user.save()
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
# Module imports
|
||||
from plane.db.models import APIActivityLog
|
||||
from plane.utils.ip_address import get_client_ip
|
||||
|
||||
|
||||
class APITokenLogMiddleware:
|
||||
def __init__(self, get_response):
|
||||
@@ -29,7 +28,7 @@ class APITokenLogMiddleware:
|
||||
response.content.decode("utf-8") if response.content else None
|
||||
),
|
||||
response_code=response.status_code,
|
||||
ip_address=get_client_ip(request=request),
|
||||
ip_address=request.META.get("REMOTE_ADDR", None),
|
||||
user_agent=request.META.get("HTTP_USER_AGENT", None),
|
||||
)
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ SECRET_KEY = os.environ.get("SECRET_KEY", get_random_secret_key())
|
||||
DEBUG = int(os.environ.get("DEBUG", "0"))
|
||||
|
||||
# Allowed Hosts
|
||||
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "*").split(",")
|
||||
ALLOWED_HOSTS = ["*"]
|
||||
|
||||
# Application definition
|
||||
INSTALLED_APPS = [
|
||||
@@ -314,7 +314,7 @@ ADMIN_BASE_URL = os.environ.get("ADMIN_BASE_URL", None)
|
||||
SPACE_BASE_URL = os.environ.get("SPACE_BASE_URL", None)
|
||||
APP_BASE_URL = os.environ.get("APP_BASE_URL")
|
||||
LIVE_BASE_URL = os.environ.get("LIVE_BASE_URL")
|
||||
WEB_URL = os.environ.get("WEB_URL")
|
||||
|
||||
|
||||
HARD_DELETE_AFTER_DAYS = int(os.environ.get("HARD_DELETE_AFTER_DAYS", 60))
|
||||
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
# Python imports
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
# Django imports
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.http import HttpRequest
|
||||
|
||||
# Third party imports
|
||||
from rest_framework.request import Request
|
||||
|
||||
# Module imports
|
||||
from plane.utils.ip_address import get_client_ip
|
||||
|
||||
def base_host(request: Request | HttpRequest, is_admin: bool = False, is_space: bool = False, is_app: bool = False) -> str:
|
||||
def base_host(request, is_admin=False, is_space=False, is_app=False):
|
||||
"""Utility function to return host / origin from the request"""
|
||||
# Calculate the base origin from request
|
||||
base_origin = settings.WEB_URL or settings.APP_BASE_URL
|
||||
|
||||
if not base_origin:
|
||||
raise ImproperlyConfigured("APP_BASE_URL or WEB_URL is not set")
|
||||
base_origin = str(
|
||||
request.META.get("HTTP_ORIGIN")
|
||||
or f"{urlsplit(request.META.get('HTTP_REFERER')).scheme}://{urlsplit(request.META.get('HTTP_REFERER')).netloc}"
|
||||
or f"""{"https" if request.is_secure() else "http"}://{request.get_host()}"""
|
||||
)
|
||||
|
||||
# Admin redirections
|
||||
if is_admin:
|
||||
@@ -41,5 +38,5 @@ def base_host(request: Request | HttpRequest, is_admin: bool = False, is_space:
|
||||
return base_origin
|
||||
|
||||
|
||||
def user_ip(request: Request | HttpRequest) -> str:
|
||||
return get_client_ip(request=request)
|
||||
def user_ip(request):
|
||||
return str(request.META.get("REMOTE_ADDR"))
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
# Python imports
|
||||
from urllib.parse import urlparse
|
||||
|
||||
|
||||
def validate_next_path(next_path: str) -> str:
|
||||
"""Validates that next_path is a valid path and extracts only the path component."""
|
||||
parsed_url = urlparse(next_path)
|
||||
|
||||
# Ensure next_path is not an absolute URL
|
||||
if parsed_url.scheme or parsed_url.netloc:
|
||||
next_path = parsed_url.path # Extract only the path component
|
||||
|
||||
# Ensure it starts with a forward slash (indicating a valid relative path)
|
||||
if not next_path.startswith("/"):
|
||||
return ""
|
||||
|
||||
# Ensure it does not contain dangerous path traversal sequences
|
||||
if ".." in next_path:
|
||||
return ""
|
||||
|
||||
return next_path
|
||||
@@ -51,7 +51,6 @@ x-app-env: &app-env
|
||||
API_KEY_RATE_LIMIT: ${API_KEY_RATE_LIMIT:-60/minute}
|
||||
MINIO_ENDPOINT_SSL: ${MINIO_ENDPOINT_SSL:-0}
|
||||
|
||||
|
||||
services:
|
||||
web:
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-frontend:${APP_RELEASE:-stable}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"modules": false
|
||||
}
|
||||
],
|
||||
"@babel/preset-typescript"
|
||||
],
|
||||
"plugins": [
|
||||
[
|
||||
"module-resolver",
|
||||
{
|
||||
"root": ["./src"],
|
||||
"alias": {
|
||||
"@/core": "./src/core",
|
||||
"@/plane-live": "./src/ce"
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
+15
-15
@@ -3,15 +3,13 @@
|
||||
"version": "0.25.3",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "A realtime collaborative server powers Plane's rich text editor",
|
||||
"main": "./dist/start.js",
|
||||
"module": "./dist/start.mjs",
|
||||
"types": "./dist/start.d.ts",
|
||||
"main": "./src/server.ts",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "tsup --watch --onSuccess 'node --env-file=.env dist/start.js'",
|
||||
"build": "tsup",
|
||||
"start": "node --env-file=.env dist/start.js",
|
||||
"dev": "PORT=3100 concurrently \"babel src --out-dir dist --extensions '.ts,.js' --watch\" \"nodemon dist/server.js\"",
|
||||
"build": "babel src --out-dir dist --extensions \".ts,.js\"",
|
||||
"start": "node dist/server.js",
|
||||
"lint": "eslint src --ext .ts,.tsx",
|
||||
"lint:errors": "eslint src --ext .ts,.tsx --quiet"
|
||||
},
|
||||
@@ -23,17 +21,14 @@
|
||||
"@hocuspocus/extension-redis": "^2.15.0",
|
||||
"@hocuspocus/server": "^2.15.0",
|
||||
"@plane/constants": "*",
|
||||
"@plane/decorators": "*",
|
||||
"@plane/editor": "*",
|
||||
"@plane/logger": "*",
|
||||
"@plane/types": "*",
|
||||
"@tiptap/core": "2.10.4",
|
||||
"@tiptap/html": "2.11.0",
|
||||
"axios": "^1.8.3",
|
||||
"compression": "^1.7.4",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.7",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.21.2",
|
||||
"express-ws": "^5.0.2",
|
||||
"helmet": "^7.1.0",
|
||||
@@ -42,22 +37,27 @@
|
||||
"morgan": "^1.10.0",
|
||||
"pino-http": "^10.3.0",
|
||||
"pino-pretty": "^11.2.2",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"uuid": "^10.0.0",
|
||||
"y-prosemirror": "^1.2.15",
|
||||
"y-protocols": "^1.0.6",
|
||||
"yjs": "^13.6.20",
|
||||
"zod": "^3.24.2"
|
||||
"yjs": "^13.6.20"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.25.6",
|
||||
"@babel/core": "^7.25.2",
|
||||
"@babel/preset-env": "^7.25.4",
|
||||
"@babel/preset-typescript": "^7.24.7",
|
||||
"@types/compression": "^1.7.5",
|
||||
"@types/cookie-parser": "^1.4.8",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/dotenv": "^8.2.0",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/express-ws": "^3.0.4",
|
||||
"@types/node": "^20.14.9",
|
||||
"tsup": "8.4.0",
|
||||
"babel-plugin-module-resolver": "^5.0.2",
|
||||
"concurrently": "^9.0.1",
|
||||
"nodemon": "^3.1.7",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsup": "^8.4.0",
|
||||
"typescript": "5.3.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./register";
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./transformers";
|
||||
export * from "./project-page-handler";
|
||||
@@ -1,74 +0,0 @@
|
||||
import {
|
||||
DocumentHandler,
|
||||
DocumentFetchParams,
|
||||
DocumentStoreParams,
|
||||
HandlerDefinition,
|
||||
} from "@/core/types/document-handler";
|
||||
import { handlerFactory } from "@/core/handlers/document-handlers/handler-factory";
|
||||
import { PageService } from "@/services/page.service";
|
||||
import { transformHTMLToBinary } from "./transformers";
|
||||
import { getAllDocumentFormatsFromBinaryData } from "@/core/helpers/page";
|
||||
|
||||
const pageService = new PageService();
|
||||
|
||||
/**
|
||||
* Handler for "project_page" document type
|
||||
*/
|
||||
export const projectPageHandler: DocumentHandler = {
|
||||
/**
|
||||
* Fetch project page description
|
||||
*/
|
||||
fetch: async ({ pageId, params, context }: DocumentFetchParams) => {
|
||||
const { cookie } = context;
|
||||
const workspaceSlug = params.get("workspaceSlug")?.toString();
|
||||
const projectId = params.get("projectId")?.toString();
|
||||
if (!workspaceSlug || !projectId || !cookie) return null;
|
||||
|
||||
const response = await pageService.fetchDescriptionBinary(workspaceSlug, projectId, pageId, cookie);
|
||||
const binaryData = new Uint8Array(response);
|
||||
|
||||
if (binaryData.byteLength === 0) {
|
||||
const binary = await transformHTMLToBinary(workspaceSlug, projectId, pageId, cookie);
|
||||
if (binary) {
|
||||
return binary;
|
||||
}
|
||||
}
|
||||
|
||||
return binaryData;
|
||||
},
|
||||
|
||||
/**
|
||||
* Store project page description
|
||||
*/
|
||||
store: async ({ pageId, state, params, context }: DocumentStoreParams) => {
|
||||
const { cookie } = context;
|
||||
if (!(state instanceof Uint8Array)) {
|
||||
throw new Error("Invalid state: must be an instance of Uint8Array");
|
||||
}
|
||||
|
||||
const workspaceSlug = params?.get("workspaceSlug")?.toString();
|
||||
const projectId = params?.get("projectId")?.toString();
|
||||
if (!workspaceSlug || !projectId || !cookie) return;
|
||||
|
||||
const { contentBinaryEncoded, contentHTML, contentJSON } = getAllDocumentFormatsFromBinaryData(state);
|
||||
const payload = {
|
||||
description_binary: contentBinaryEncoded,
|
||||
description_html: contentHTML,
|
||||
description: contentJSON,
|
||||
};
|
||||
|
||||
await pageService.updateDescription(workspaceSlug, projectId, pageId, payload, cookie);
|
||||
},
|
||||
};
|
||||
|
||||
// Define the project page handler definition
|
||||
export const projectPageHandlerDefinition: HandlerDefinition = {
|
||||
selector: (context) => context.documentType === "project_page",
|
||||
handler: projectPageHandler,
|
||||
priority: 10, // Standard priority
|
||||
};
|
||||
|
||||
// Register the handler directly from CE
|
||||
export function registerProjectPageHandler() {
|
||||
handlerFactory.register(projectPageHandlerDefinition);
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { PageService } from "@/services/page.service";
|
||||
import { getBinaryDataFromHTMLString } from "@/core/helpers/page";
|
||||
import { logger } from "@plane/logger";
|
||||
|
||||
const pageService = new PageService();
|
||||
|
||||
/**
|
||||
* Transforms HTML description to binary format
|
||||
*/
|
||||
export const transformHTMLToBinary = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
pageId: string,
|
||||
cookie: string
|
||||
) => {
|
||||
if (!workspaceSlug || !projectId || !cookie) return;
|
||||
|
||||
try {
|
||||
const pageDetails = await pageService.fetchDetails(workspaceSlug, projectId, pageId, cookie);
|
||||
const { contentBinary } = getBinaryDataFromHTMLString(pageDetails.description_html ?? "<p></p>");
|
||||
return contentBinary;
|
||||
} catch (error) {
|
||||
logger.error("Error while transforming from HTML to Uint8Array", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
import { registerProjectPageHandler } from "./project-page/project-page-handler";
|
||||
|
||||
export function initializeDocumentHandlers() {
|
||||
registerProjectPageHandler();
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// types
|
||||
import { TDocumentTypes } from "@/core/types/common.js";
|
||||
|
||||
type TArgs = {
|
||||
cookie: string | undefined;
|
||||
documentType: TDocumentTypes | undefined;
|
||||
pageId: string;
|
||||
params: URLSearchParams;
|
||||
}
|
||||
|
||||
export const fetchDocument = async (args: TArgs): Promise<Uint8Array | null> => {
|
||||
const { documentType } = args;
|
||||
throw Error(`Fetch failed: Invalid document type ${documentType} provided.`);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// types
|
||||
import { TDocumentTypes } from "@/core/types/common.js";
|
||||
|
||||
type TArgs = {
|
||||
cookie: string | undefined;
|
||||
documentType: TDocumentTypes | undefined;
|
||||
pageId: string;
|
||||
params: URLSearchParams;
|
||||
updatedDescription: Uint8Array;
|
||||
}
|
||||
|
||||
export const updateDocument = async (args: TArgs): Promise<void> => {
|
||||
const { documentType } = args;
|
||||
throw Error(`Update failed: Invalid document type ${documentType} provided.`);
|
||||
}
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
export type TAdditionalDocumentTypes = string;
|
||||
export type TAdditionalDocumentTypes = {};
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
import type { Request } from "express";
|
||||
import type { WebSocket as WS } from "ws";
|
||||
import type { Hocuspocus } from "@hocuspocus/server";
|
||||
import { ErrorCategory } from "@/lib/error-handling/error-handler";
|
||||
import { logger } from "@plane/logger";
|
||||
import Errors from "@/lib/error-handling/error-factory";
|
||||
import { Controller, WebSocket } from "@plane/decorators";
|
||||
|
||||
@Controller("/collaboration")
|
||||
export class CollaborationController {
|
||||
private metrics = {
|
||||
errors: 0,
|
||||
};
|
||||
|
||||
constructor(private readonly hocusPocusServer: Hocuspocus) {}
|
||||
|
||||
@WebSocket("/")
|
||||
handleConnection(ws: WS, req: Request) {
|
||||
const clientInfo = {
|
||||
ip: req.ip,
|
||||
userAgent: req.get("user-agent"),
|
||||
requestId: req.id || crypto.randomUUID(),
|
||||
};
|
||||
|
||||
try {
|
||||
// Initialize the connection with Hocuspocus
|
||||
this.hocusPocusServer.handleConnection(ws, req);
|
||||
|
||||
// Set up error handling for the connection
|
||||
ws.on("error", (error) => {
|
||||
this.handleConnectionError(error, clientInfo, ws);
|
||||
});
|
||||
} catch (error) {
|
||||
this.handleConnectionError(error, clientInfo, ws);
|
||||
}
|
||||
}
|
||||
|
||||
private handleConnectionError(error: unknown, clientInfo: Record<string, any>, ws: WS) {
|
||||
// Convert to AppError if needed
|
||||
const appError = Errors.convertError(error instanceof Error ? error : new Error(String(error)), {
|
||||
context: {
|
||||
...clientInfo,
|
||||
component: "WebSocketConnection",
|
||||
},
|
||||
});
|
||||
|
||||
// Log at appropriate level based on error category
|
||||
if (appError.category === ErrorCategory.OPERATIONAL) {
|
||||
logger.info(`WebSocket operational error: ${appError.message}`, {
|
||||
error: appError,
|
||||
clientInfo,
|
||||
});
|
||||
} else {
|
||||
logger.error(`WebSocket error: ${appError.message}`, {
|
||||
error: appError,
|
||||
clientInfo,
|
||||
stack: appError.stack,
|
||||
});
|
||||
}
|
||||
|
||||
// Alert if error threshold is reached
|
||||
if (this.metrics.errors % 10 === 0) {
|
||||
logger.warn(`High WebSocket error rate detected: ${this.metrics.errors} total errors`);
|
||||
}
|
||||
|
||||
// Try to send error to client before closing
|
||||
try {
|
||||
if (ws.readyState === ws.OPEN) {
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "error",
|
||||
message: appError.category === ErrorCategory.OPERATIONAL ? appError.message : "Internal server error",
|
||||
})
|
||||
);
|
||||
}
|
||||
} catch (sendError) {
|
||||
// Ignore send errors at this point
|
||||
}
|
||||
|
||||
// Close with informative message if connection is still open
|
||||
if (ws.readyState === ws.OPEN) {
|
||||
ws.close(
|
||||
1011,
|
||||
appError.category === ErrorCategory.OPERATIONAL
|
||||
? `Error: ${appError.message}. Reconnect with exponential backoff.`
|
||||
: "Internal server error. Please retry in a few moments."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
getErrorMetrics() {
|
||||
return {
|
||||
errors: this.metrics.errors,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
import type { Request, Response } from "express";
|
||||
import { z } from "zod";
|
||||
// helpers
|
||||
import { convertHTMLDocumentToAllFormats } from "@/core/helpers/convert-document";
|
||||
// types
|
||||
import { TConvertDocumentRequestBody } from "@/core/types/common";
|
||||
// decorators
|
||||
import { CatchErrors } from "@/lib/error";
|
||||
// logger
|
||||
import { logger } from "@plane/logger";
|
||||
import { Controller, Post } from "@plane/decorators";
|
||||
import { AppError } from "@/lib/error-handling/error-handler";
|
||||
import { handleError } from "@/lib/error-handling/error-factory";
|
||||
|
||||
// Define the schema with more robust validation
|
||||
const convertDocumentSchema = z.object({
|
||||
description_html: z
|
||||
.string()
|
||||
.min(1, "HTML content cannot be empty")
|
||||
.refine((html) => html.trim().length > 0, "HTML content cannot be just whitespace")
|
||||
.refine((html) => html.includes("<") && html.includes(">"), "Content must be valid HTML"),
|
||||
variant: z.enum(["rich", "document"]),
|
||||
});
|
||||
|
||||
@Controller("/convert-document")
|
||||
export class DocumentController {
|
||||
private metrics = {
|
||||
conversions: 0,
|
||||
errors: 0,
|
||||
};
|
||||
|
||||
@Post("/")
|
||||
@CatchErrors()
|
||||
async convertDocument(req: Request, res: Response) {
|
||||
const requestId = req.id || crypto.randomUUID();
|
||||
const clientInfo = {
|
||||
ip: req.ip,
|
||||
userAgent: req.get("user-agent"),
|
||||
requestId,
|
||||
};
|
||||
|
||||
try {
|
||||
// Validate request body
|
||||
const validatedData = convertDocumentSchema.parse(req.body as TConvertDocumentRequestBody);
|
||||
const { description_html, variant } = validatedData;
|
||||
|
||||
// Log validated data
|
||||
logger.info("Validated document conversion request", {
|
||||
...clientInfo,
|
||||
variant,
|
||||
contentLength: description_html.length,
|
||||
});
|
||||
|
||||
// Process document conversion
|
||||
const { description, description_binary } = convertHTMLDocumentToAllFormats({
|
||||
document_html: description_html,
|
||||
variant,
|
||||
});
|
||||
|
||||
// Update metrics
|
||||
this.metrics.conversions++;
|
||||
|
||||
// Log successful conversion
|
||||
logger.info("Document conversion successful", {
|
||||
...clientInfo,
|
||||
variant,
|
||||
outputLength: description_html.length,
|
||||
});
|
||||
|
||||
// Return successful response
|
||||
res.status(200).json({
|
||||
description,
|
||||
description_binary,
|
||||
});
|
||||
} catch (error) {
|
||||
// Update error metrics
|
||||
this.metrics.errors++;
|
||||
|
||||
let appError: AppError;
|
||||
|
||||
if (error instanceof z.ZodError) {
|
||||
// Handle validation errors
|
||||
appError = handleError(error, {
|
||||
errorType: "unprocessable-entity",
|
||||
message: "Invalid request data",
|
||||
component: "document-conversion-controller",
|
||||
operation: "convertDocument",
|
||||
extraContext: {
|
||||
...clientInfo,
|
||||
validationErrors: error.errors.map((err) => ({
|
||||
path: err.path.join("."),
|
||||
message: err.message,
|
||||
})),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// Handle other errors
|
||||
appError = handleError(error, {
|
||||
errorType: "internal",
|
||||
message: "Internal server error",
|
||||
component: "document-conversion-controller",
|
||||
operation: "convertDocument",
|
||||
extraContext: clientInfo,
|
||||
});
|
||||
}
|
||||
|
||||
// Log the error
|
||||
logger.error("Document conversion failed", {
|
||||
error: appError,
|
||||
status: appError.status,
|
||||
context: appError.context,
|
||||
});
|
||||
|
||||
res.status(appError.status).json({
|
||||
message: appError.message,
|
||||
status: appError.status,
|
||||
context: appError.context,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getMetrics() {
|
||||
return {
|
||||
conversions: this.metrics.conversions,
|
||||
errors: this.metrics.errors,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { CatchErrors } from "@/lib/error";
|
||||
import { Controller, Get } from "@plane/decorators";
|
||||
import type { Request, Response } from "express";
|
||||
|
||||
@Controller("/health")
|
||||
export class HealthController {
|
||||
@Get("/")
|
||||
@CatchErrors()
|
||||
async healthCheck(_req: Request, res: Response) {
|
||||
res.status(200).json({
|
||||
status: "OK",
|
||||
timestamp: new Date().toISOString(),
|
||||
version: process.env.APP_VERSION || "1.0.0",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { HealthController } from "./health.controller";
|
||||
import { DocumentController } from "./document.controller";
|
||||
import { CollaborationController } from "./collaboration.controller";
|
||||
|
||||
export const REST_CONTROLLERS = [HealthController, DocumentController];
|
||||
|
||||
export const WEBSOCKET_CONTROLLERS = [CollaborationController];
|
||||
@@ -1,116 +0,0 @@
|
||||
import { Database } from "@hocuspocus/extension-database";
|
||||
import { catchAsync } from "@/lib/error-handling/error-handler";
|
||||
import { handleError } from "@/lib/error-handling/error-factory";
|
||||
import { getDocumentHandler } from "../handlers/document-handlers";
|
||||
import { type HocusPocusServerContext, type TDocumentTypes } from "@/core/types/common";
|
||||
|
||||
export const createDatabaseExtension = () => {
|
||||
return new Database({
|
||||
fetch: handleFetch,
|
||||
store: handleStore,
|
||||
});
|
||||
};
|
||||
|
||||
const handleFetch = async ({
|
||||
context,
|
||||
documentName: pageId,
|
||||
requestParameters,
|
||||
}: {
|
||||
context: HocusPocusServerContext;
|
||||
documentName: TDocumentTypes;
|
||||
requestParameters: URLSearchParams;
|
||||
}) => {
|
||||
const { documentType } = context;
|
||||
const params = requestParameters;
|
||||
|
||||
let fetchedData = null;
|
||||
fetchedData = await catchAsync(
|
||||
async () => {
|
||||
if (!documentType) {
|
||||
handleError(null, {
|
||||
errorType: "bad-request",
|
||||
message: "Document type is required",
|
||||
component: "database-extension",
|
||||
operation: "fetch",
|
||||
extraContext: { pageId },
|
||||
throw: true,
|
||||
});
|
||||
}
|
||||
|
||||
const documentHandler = getDocumentHandler(documentType, context);
|
||||
fetchedData = await documentHandler.fetch({
|
||||
context: context as HocusPocusServerContext,
|
||||
pageId,
|
||||
params,
|
||||
});
|
||||
|
||||
if (!fetchedData) {
|
||||
handleError(null, {
|
||||
errorType: "not-found",
|
||||
message: `Failed to fetch document: ${pageId}`,
|
||||
component: "database-extension",
|
||||
operation: "fetch",
|
||||
extraContext: { documentType, pageId },
|
||||
});
|
||||
}
|
||||
|
||||
return fetchedData;
|
||||
},
|
||||
{
|
||||
params: { pageId, documentType: context.documentType },
|
||||
extra: { operation: "fetch" },
|
||||
}
|
||||
)();
|
||||
return fetchedData;
|
||||
};
|
||||
|
||||
const handleStore = async ({
|
||||
context,
|
||||
state,
|
||||
documentName: pageId,
|
||||
requestParameters,
|
||||
}: {
|
||||
context: HocusPocusServerContext;
|
||||
state: Buffer;
|
||||
documentName: TDocumentTypes;
|
||||
requestParameters: URLSearchParams;
|
||||
}) => {
|
||||
catchAsync(
|
||||
async () => {
|
||||
if (!state) {
|
||||
handleError(null, {
|
||||
errorType: "bad-request",
|
||||
message: "Loaded binary state is required",
|
||||
component: "database-extension",
|
||||
operation: "store",
|
||||
extraContext: { pageId },
|
||||
throw: true,
|
||||
});
|
||||
}
|
||||
const { documentType } = context as HocusPocusServerContext;
|
||||
const params = requestParameters;
|
||||
if (!documentType) {
|
||||
handleError(null, {
|
||||
errorType: "bad-request",
|
||||
message: "Document type is required",
|
||||
component: "database-extension",
|
||||
operation: "store",
|
||||
extraContext: { pageId },
|
||||
throw: true,
|
||||
});
|
||||
}
|
||||
|
||||
const documentHandler = getDocumentHandler(documentType, context);
|
||||
await documentHandler.store({
|
||||
context: context as HocusPocusServerContext,
|
||||
pageId,
|
||||
state,
|
||||
params,
|
||||
});
|
||||
},
|
||||
{
|
||||
params: { pageId, documentType: context.documentType },
|
||||
extra: { operation: "store" },
|
||||
}
|
||||
)();
|
||||
};
|
||||
@@ -1,26 +1,142 @@
|
||||
// hocuspocus extensions and core
|
||||
// Third-party libraries
|
||||
import { Redis } from "ioredis";
|
||||
// Hocuspocus extensions and core
|
||||
import { Database } from "@hocuspocus/extension-database";
|
||||
import { Extension } from "@hocuspocus/server";
|
||||
import { Logger } from "@hocuspocus/extension-logger";
|
||||
import { setupRedisExtension } from "@/core/extensions/redis";
|
||||
import { createDatabaseExtension } from "@/core/extensions/database";
|
||||
import { logger } from "@plane/logger";
|
||||
import { Redis as HocusPocusRedis } from "@hocuspocus/extension-redis";
|
||||
// core helpers and utilities
|
||||
import { manualLogger } from "@/core/helpers/logger.js";
|
||||
import { getRedisUrl } from "@/core/lib/utils/redis-url.js";
|
||||
// core libraries
|
||||
import {
|
||||
fetchPageDescriptionBinary,
|
||||
updatePageDescription,
|
||||
} from "@/core/lib/page.js";
|
||||
// plane live libraries
|
||||
import { fetchDocument } from "@/plane-live/lib/fetch-document.js";
|
||||
import { updateDocument } from "@/plane-live/lib/update-document.js";
|
||||
// types
|
||||
import {
|
||||
type HocusPocusServerContext,
|
||||
type TDocumentTypes,
|
||||
} from "@/core/types/common.js";
|
||||
|
||||
export const getExtensions = async (): Promise<Extension[]> => {
|
||||
export const getExtensions: () => Promise<Extension[]> = async () => {
|
||||
const extensions: Extension[] = [
|
||||
new Logger({
|
||||
onChange: false,
|
||||
log: (message) => {
|
||||
logger.info(message);
|
||||
manualLogger.info(message);
|
||||
},
|
||||
}),
|
||||
new Database({
|
||||
fetch: async ({ context, documentName: pageId, requestParameters }) => {
|
||||
const cookie = (context as HocusPocusServerContext).cookie;
|
||||
// query params
|
||||
const params = requestParameters;
|
||||
const documentType = params.get("documentType")?.toString() as
|
||||
| TDocumentTypes
|
||||
| undefined;
|
||||
// TODO: Fix this lint error.
|
||||
// eslint-disable-next-line no-async-promise-executor
|
||||
return new Promise(async (resolve) => {
|
||||
try {
|
||||
let fetchedData = null;
|
||||
if (documentType === "project_page") {
|
||||
fetchedData = await fetchPageDescriptionBinary(
|
||||
params,
|
||||
pageId,
|
||||
cookie,
|
||||
);
|
||||
} else {
|
||||
fetchedData = await fetchDocument({
|
||||
cookie,
|
||||
documentType,
|
||||
pageId,
|
||||
params,
|
||||
});
|
||||
}
|
||||
resolve(fetchedData);
|
||||
} catch (error) {
|
||||
manualLogger.error("Error in fetching document", error);
|
||||
}
|
||||
});
|
||||
},
|
||||
store: async ({
|
||||
context,
|
||||
state,
|
||||
documentName: pageId,
|
||||
requestParameters,
|
||||
}) => {
|
||||
const cookie = (context as HocusPocusServerContext).cookie;
|
||||
// query params
|
||||
const params = requestParameters;
|
||||
const documentType = params.get("documentType")?.toString() as
|
||||
| TDocumentTypes
|
||||
| undefined;
|
||||
|
||||
// TODO: Fix this lint error.
|
||||
// eslint-disable-next-line no-async-promise-executor
|
||||
return new Promise(async () => {
|
||||
try {
|
||||
if (documentType === "project_page") {
|
||||
await updatePageDescription(params, pageId, state, cookie);
|
||||
} else {
|
||||
await updateDocument({
|
||||
cookie,
|
||||
documentType,
|
||||
pageId,
|
||||
params,
|
||||
updatedDescription: state,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
manualLogger.error("Error in updating document:", error);
|
||||
}
|
||||
});
|
||||
},
|
||||
}),
|
||||
createDatabaseExtension(),
|
||||
];
|
||||
|
||||
// Add Redis extensions if Redis is available
|
||||
const redisExtension = await setupRedisExtension();
|
||||
if (redisExtension) {
|
||||
logger.info("HocusPocus Redis extension configured ✅");
|
||||
extensions.push(redisExtension);
|
||||
const redisUrl = getRedisUrl();
|
||||
|
||||
if (redisUrl) {
|
||||
try {
|
||||
const redisClient = new Redis(redisUrl);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
redisClient.on("error", (error: any) => {
|
||||
if (
|
||||
error?.code === "ENOTFOUND" ||
|
||||
error.message.includes("WRONGPASS") ||
|
||||
error.message.includes("NOAUTH")
|
||||
) {
|
||||
redisClient.disconnect();
|
||||
}
|
||||
manualLogger.warn(
|
||||
`Redis Client wasn't able to connect, continuing without Redis (you won't be able to sync data between multiple plane live servers)`,
|
||||
error,
|
||||
);
|
||||
reject(error);
|
||||
});
|
||||
|
||||
redisClient.on("ready", () => {
|
||||
extensions.push(new HocusPocusRedis({ redis: redisClient }));
|
||||
manualLogger.info("Redis Client connected ✅");
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
manualLogger.warn(
|
||||
`Redis Client wasn't able to connect, continuing without Redis (you won't be able to sync data between multiple plane live servers)`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
manualLogger.warn(
|
||||
"Redis URL is not set, continuing without Redis (you won't be able to sync data between multiple plane live servers)",
|
||||
);
|
||||
}
|
||||
|
||||
return extensions;
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { Redis as HocusPocusRedis } from "@hocuspocus/extension-redis";
|
||||
// core helpers and utilities
|
||||
import { logger } from "@plane/logger";
|
||||
import { getRedisClient } from "@/core/lib/redis-manager";
|
||||
|
||||
/**
|
||||
* Sets up the Redis extension for HocusPocus using the RedisManager singleton
|
||||
* @returns Promise that resolves to a Redis extension array
|
||||
*/
|
||||
export const setupRedisExtension = () => {
|
||||
// Wait for Redis connection
|
||||
return new HocusPocusRedis({
|
||||
redis: getRedisClient(),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { DocumentHandler, HandlerContext, HandlerDefinition } from "@/core/types/document-handler";
|
||||
|
||||
/**
|
||||
* Class that manages handler selection based on multiple criteria
|
||||
*/
|
||||
export class DocumentHandlerFactory {
|
||||
private handlers: HandlerDefinition[] = [];
|
||||
|
||||
/**
|
||||
* Register a handler with its selection criteria
|
||||
*/
|
||||
register(definition: HandlerDefinition): void {
|
||||
this.handlers.push(definition);
|
||||
// Sort handlers by priority (highest first)
|
||||
this.handlers.sort((a, b) => b.priority - a.priority);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the appropriate handler based on the provided context
|
||||
*/
|
||||
getHandler(context: HandlerContext): DocumentHandler {
|
||||
// Find the first handler whose selector returns true
|
||||
const matchingHandler = this.handlers.find(h => h.selector(context));
|
||||
|
||||
// Return the matching handler or fall back to null/undefined
|
||||
// (This will cause an error if no handlers match, which is good for debugging)
|
||||
return matchingHandler?.handler as DocumentHandler;
|
||||
}
|
||||
}
|
||||
|
||||
// Create the singleton instance
|
||||
export const handlerFactory = new DocumentHandlerFactory();
|
||||
@@ -1,31 +0,0 @@
|
||||
import { DocumentHandler, HandlerContext } from "@/core/types/document-handler";
|
||||
import { handlerFactory } from "@/core/handlers/document-handlers/handler-factory";
|
||||
|
||||
import { HocusPocusServerContext } from "@/core/types/common";
|
||||
import { initializeDocumentHandlers } from "@/plane-live/document-types";
|
||||
|
||||
// Initialize all CE document handlers
|
||||
initializeDocumentHandlers();
|
||||
|
||||
/**
|
||||
* Get a document handler based on the provided context criteria
|
||||
* @param documentType The primary document type
|
||||
* @param additionalContext Optional additional context criteria
|
||||
* @returns The appropriate document handler
|
||||
*/
|
||||
export function getDocumentHandler(
|
||||
documentType: string,
|
||||
additionalContext: Omit<HocusPocusServerContext, "documentType">
|
||||
): DocumentHandler {
|
||||
// Create a context object with all criteria
|
||||
const context: HandlerContext = {
|
||||
documentType: documentType as any,
|
||||
...additionalContext,
|
||||
};
|
||||
|
||||
// Use the factory to get the appropriate handler
|
||||
return handlerFactory.getHandler(context);
|
||||
}
|
||||
|
||||
// Export the factory for direct access if needed
|
||||
export { handlerFactory };
|
||||
@@ -0,0 +1,21 @@
|
||||
import { ErrorRequestHandler } from "express";
|
||||
import { manualLogger } from "@/core/helpers/logger.js";
|
||||
|
||||
export const errorHandler: ErrorRequestHandler = (err, _req, res) => {
|
||||
// Log the error
|
||||
manualLogger.error(err);
|
||||
|
||||
// Set the response status
|
||||
res.status(err.status || 500);
|
||||
|
||||
// Send the response
|
||||
res.json({
|
||||
error: {
|
||||
message:
|
||||
process.env.NODE_ENV === "production"
|
||||
? "An unexpected error occurred"
|
||||
: err.message,
|
||||
...(process.env.NODE_ENV !== "production" && { stack: err.stack }),
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,267 +0,0 @@
|
||||
import { handleError } from "../../lib/error-handling/error-factory";
|
||||
|
||||
/**
|
||||
* A simple validation utility that integrates with our error system.
|
||||
*
|
||||
* This provides a fluent interface for validating data and throwing
|
||||
* appropriate errors if validation fails.
|
||||
*/
|
||||
export class Validator<T> {
|
||||
constructor(
|
||||
private readonly data: T,
|
||||
private readonly name: string = "data"
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Ensures a value is defined (not undefined or null)
|
||||
*/
|
||||
required(message?: string): Validator<T> {
|
||||
if (this.data === undefined || this.data === null) {
|
||||
throw handleError(new ValidationError(this.name, message || `${this.name} is required`), {
|
||||
errorType: "bad-request",
|
||||
component: "validation",
|
||||
operation: "validateRequired",
|
||||
extraContext: { field: this.name },
|
||||
throw: true,
|
||||
});
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures a value is a string
|
||||
*/
|
||||
string(message?: string): Validator<T> {
|
||||
if (typeof this.data !== "string") {
|
||||
throw handleError(new ValidationError(this.name, message || `${this.name} must be a string`), {
|
||||
errorType: "bad-request",
|
||||
component: "validation",
|
||||
operation: "validateString",
|
||||
extraContext: { field: this.name },
|
||||
throw: true,
|
||||
});
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures a string is not empty
|
||||
*/
|
||||
notEmpty(message?: string): Validator<T> {
|
||||
if (typeof this.data === "string" && this.data.trim() === "") {
|
||||
throw handleError(new ValidationError(this.name, message || `${this.name} cannot be empty`), {
|
||||
errorType: "bad-request",
|
||||
component: "validation",
|
||||
operation: "validateNonEmptyString",
|
||||
extraContext: { field: this.name },
|
||||
throw: true,
|
||||
});
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures a value is a number
|
||||
*/
|
||||
number(message?: string): Validator<T> {
|
||||
if (typeof this.data !== "number" || isNaN(this.data)) {
|
||||
throw handleError(new ValidationError(this.name, message || `${this.name} must be a valid number`), {
|
||||
errorType: "bad-request",
|
||||
component: "validation",
|
||||
operation: "validateNumber",
|
||||
extraContext: { field: this.name },
|
||||
throw: true,
|
||||
});
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures an array is not empty
|
||||
*/
|
||||
nonEmptyArray(message?: string): Validator<T> {
|
||||
if (!Array.isArray(this.data) || this.data.length === 0) {
|
||||
throw handleError(new ValidationError(this.name, message || `${this.name} must be a non-empty array`), {
|
||||
errorType: "bad-request",
|
||||
component: "validation",
|
||||
operation: "validateArray",
|
||||
extraContext: { field: this.name },
|
||||
throw: true,
|
||||
});
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures a value matches a regular expression
|
||||
*/
|
||||
match(regex: RegExp, message?: string): Validator<T> {
|
||||
if (typeof this.data !== "string" || !regex.test(this.data)) {
|
||||
throw handleError(new ValidationError(this.name, message || `${this.name} has an invalid format`), {
|
||||
errorType: "bad-request",
|
||||
component: "validation",
|
||||
operation: "validateFormat",
|
||||
extraContext: { field: this.name, format: regex.toString() },
|
||||
throw: true,
|
||||
});
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures a value is one of the allowed values
|
||||
*/
|
||||
oneOf(allowedValues: any[], message?: string): Validator<T> {
|
||||
if (!allowedValues.includes(this.data)) {
|
||||
throw handleError(
|
||||
new ValidationError(this.name, message || `${this.name} must be one of: ${allowedValues.join(", ")}`),
|
||||
{
|
||||
errorType: "bad-request",
|
||||
component: "validation",
|
||||
operation: "validateEnum",
|
||||
extraContext: { field: this.name, allowedValues },
|
||||
throw: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom validation function
|
||||
*/
|
||||
custom(validationFn: (value: T) => boolean, message?: string): Validator<T> {
|
||||
if (!validationFn(this.data)) {
|
||||
throw handleError(new ValidationError(this.name, message || `${this.name} is invalid`), {
|
||||
errorType: "bad-request",
|
||||
component: "validation",
|
||||
operation: "validateCustom",
|
||||
extraContext: { field: this.name },
|
||||
throw: true,
|
||||
});
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validated data
|
||||
*/
|
||||
get(): T {
|
||||
return this.data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new validator for a value
|
||||
*/
|
||||
export const validate = <T>(data: T, name?: string): Validator<T> => {
|
||||
return new Validator(data, name);
|
||||
};
|
||||
|
||||
export default validate;
|
||||
|
||||
export class ValidationError extends Error {
|
||||
constructor(
|
||||
public name: string,
|
||||
message: string
|
||||
) {
|
||||
super(message);
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
export const validateRequired = (value: any, name: string, message?: string) => {
|
||||
if (value === undefined || value === null) {
|
||||
throw handleError(new ValidationError(name, message || `${name} is required`), {
|
||||
errorType: "bad-request",
|
||||
component: "validation",
|
||||
operation: "validateRequired",
|
||||
extraContext: { field: name },
|
||||
throw: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const validateString = (value: any, name: string, message?: string) => {
|
||||
if (typeof value !== "string") {
|
||||
throw handleError(new ValidationError(name, message || `${name} must be a string`), {
|
||||
errorType: "bad-request",
|
||||
component: "validation",
|
||||
operation: "validateString",
|
||||
extraContext: { field: name },
|
||||
throw: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const validateNonEmptyString = (value: string, name: string, message?: string) => {
|
||||
if (!value.trim()) {
|
||||
throw handleError(new ValidationError(name, message || `${name} cannot be empty`), {
|
||||
errorType: "bad-request",
|
||||
component: "validation",
|
||||
operation: "validateNonEmptyString",
|
||||
extraContext: { field: name },
|
||||
throw: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const validateNumber = (value: any, name: string, message?: string) => {
|
||||
if (typeof value !== "number" || isNaN(value)) {
|
||||
throw handleError(new ValidationError(name, message || `${name} must be a valid number`), {
|
||||
errorType: "bad-request",
|
||||
component: "validation",
|
||||
operation: "validateNumber",
|
||||
extraContext: { field: name },
|
||||
throw: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const validateArray = (value: any, name: string, message?: string) => {
|
||||
if (!Array.isArray(value) || value.length === 0) {
|
||||
throw handleError(new ValidationError(name, message || `${name} must be a non-empty array`), {
|
||||
errorType: "bad-request",
|
||||
component: "validation",
|
||||
operation: "validateArray",
|
||||
extraContext: { field: name },
|
||||
throw: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const validateFormat = (value: string, name: string, format: RegExp, message?: string) => {
|
||||
if (!format.test(value)) {
|
||||
throw handleError(new ValidationError(name, message || `${name} has an invalid format`), {
|
||||
errorType: "bad-request",
|
||||
component: "validation",
|
||||
operation: "validateFormat",
|
||||
extraContext: { field: name, format: format.toString() },
|
||||
throw: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const validateEnum = (value: any, name: string, allowedValues: any[], message?: string) => {
|
||||
if (!allowedValues.includes(value)) {
|
||||
throw handleError(new ValidationError(name, message || `${name} must be one of: ${allowedValues.join(", ")}`), {
|
||||
errorType: "bad-request",
|
||||
component: "validation",
|
||||
operation: "validateEnum",
|
||||
extraContext: { field: name, allowedValues },
|
||||
throw: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const validateCustom = (value: any, name: string, validator: (value: any) => boolean, message?: string) => {
|
||||
if (!validator(value)) {
|
||||
throw handleError(new ValidationError(name, message || `${name} is invalid`), {
|
||||
errorType: "bad-request",
|
||||
component: "validation",
|
||||
operation: "validateCustom",
|
||||
extraContext: { field: name },
|
||||
throw: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
import { Server } from "@hocuspocus/server";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
// lib
|
||||
import { handleAuthentication } from "@/core/lib/authentication.js";
|
||||
// extensions
|
||||
import { getExtensions } from "@/core/extensions/index.js";
|
||||
import {
|
||||
DocumentCollaborativeEvents,
|
||||
TDocumentEventsServer,
|
||||
} from "@plane/editor/lib";
|
||||
// editor types
|
||||
import { TUserDetails } from "@plane/editor";
|
||||
// types
|
||||
import { type HocusPocusServerContext } from "@/core/types/common.js";
|
||||
|
||||
export const getHocusPocusServer = async () => {
|
||||
const extensions = await getExtensions();
|
||||
const serverName = process.env.HOSTNAME || uuidv4();
|
||||
return Server.configure({
|
||||
name: serverName,
|
||||
onAuthenticate: async ({
|
||||
requestHeaders,
|
||||
context,
|
||||
// user id used as token for authentication
|
||||
token,
|
||||
}) => {
|
||||
let cookie: string | undefined = undefined;
|
||||
let userId: string | undefined = undefined;
|
||||
|
||||
// Extract cookie (fallback to request headers) and userId from token (for scenarios where
|
||||
// the cookies are not passed in the request headers)
|
||||
try {
|
||||
const parsedToken = JSON.parse(token) as TUserDetails;
|
||||
userId = parsedToken.id;
|
||||
cookie = parsedToken.cookie;
|
||||
} catch (error) {
|
||||
// If token parsing fails, fallback to request headers
|
||||
console.error("Token parsing failed, using request headers:", error);
|
||||
} finally {
|
||||
// If cookie is still not found, fallback to request headers
|
||||
if (!cookie) {
|
||||
cookie = requestHeaders.cookie?.toString();
|
||||
}
|
||||
}
|
||||
|
||||
if (!cookie || !userId) {
|
||||
throw new Error("Credentials not provided");
|
||||
}
|
||||
|
||||
// set cookie in context, so it can be used throughout the ws connection
|
||||
(context as HocusPocusServerContext).cookie = cookie;
|
||||
|
||||
try {
|
||||
await handleAuthentication({
|
||||
cookie,
|
||||
userId,
|
||||
});
|
||||
} catch (error) {
|
||||
throw Error("Authentication unsuccessful!");
|
||||
}
|
||||
},
|
||||
async onStateless({ payload, document }) {
|
||||
// broadcast the client event (derived from the server event) to all the clients so that they can update their state
|
||||
const response =
|
||||
DocumentCollaborativeEvents[payload as TDocumentEventsServer].client;
|
||||
if (response) {
|
||||
document.broadcastStateless(response);
|
||||
}
|
||||
},
|
||||
extensions,
|
||||
debounce: 10000,
|
||||
});
|
||||
};
|
||||
@@ -1,47 +1,27 @@
|
||||
// services
|
||||
import { UserService } from "@/services/user.service";
|
||||
import { handleError } from "@/lib/error-handling/error-factory";
|
||||
import { UserService } from "@/core/services/user.service.js";
|
||||
// core helpers
|
||||
import { manualLogger } from "@/core/helpers/logger.js";
|
||||
|
||||
const userService = new UserService();
|
||||
|
||||
type Props = {
|
||||
cookie: string;
|
||||
userId: string;
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
export const handleAuthentication = async (props: Props) => {
|
||||
const { cookie, userId, workspaceSlug } = props;
|
||||
const { cookie, userId } = props;
|
||||
// fetch current user info
|
||||
let response;
|
||||
try {
|
||||
response = await userService.currentUser(cookie);
|
||||
} catch (error) {
|
||||
console.log("caught?");
|
||||
handleError(error, {
|
||||
errorType: "unauthorized",
|
||||
message: "Failed to authenticate user",
|
||||
component: "authentication",
|
||||
operation: "fetch-current-user",
|
||||
extraContext: {
|
||||
userId,
|
||||
workspaceSlug,
|
||||
},
|
||||
throw: true,
|
||||
});
|
||||
manualLogger.error("Failed to fetch current user:", error);
|
||||
throw error;
|
||||
}
|
||||
if (response.id !== userId) {
|
||||
handleError(null, {
|
||||
errorType: "unauthorized",
|
||||
message: "Authentication failed: Token doesn't match the current user.",
|
||||
component: "authentication",
|
||||
operation: "validate-user",
|
||||
extraContext: {
|
||||
userId,
|
||||
workspaceSlug,
|
||||
},
|
||||
throw: true,
|
||||
});
|
||||
throw Error("Authentication failed: Token doesn't match the current user.");
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
+72
-24
@@ -1,64 +1,112 @@
|
||||
// helpers
|
||||
import { getAllDocumentFormatsFromBinaryData, getBinaryDataFromHTMLString } from "@/core/helpers/page";
|
||||
import {
|
||||
getAllDocumentFormatsFromBinaryData,
|
||||
getBinaryDataFromHTMLString,
|
||||
} from "@/core/helpers/page.js";
|
||||
// services
|
||||
import { PageService } from "@/services/page.service";
|
||||
import { PageService } from "@/core/services/page.service.js";
|
||||
import { manualLogger } from "../helpers/logger.js";
|
||||
const pageService = new PageService();
|
||||
|
||||
export const updatePageDescription = async (
|
||||
params: URLSearchParams,
|
||||
pageId: string,
|
||||
updatedDescription: Uint8Array,
|
||||
cookie: string | undefined
|
||||
cookie: string | undefined,
|
||||
) => {
|
||||
if (!(updatedDescription instanceof Uint8Array)) {
|
||||
throw new Error("Invalid updatedDescription: must be an instance of Uint8Array");
|
||||
throw new Error(
|
||||
"Invalid updatedDescription: must be an instance of Uint8Array",
|
||||
);
|
||||
}
|
||||
|
||||
const workspaceSlug = params.get("workspaceSlug")?.toString();
|
||||
const projectId = params.get("projectId")?.toString();
|
||||
if (!workspaceSlug || !projectId || !cookie) return;
|
||||
|
||||
const { contentBinaryEncoded, contentHTML, contentJSON } = getAllDocumentFormatsFromBinaryData(updatedDescription);
|
||||
const payload = {
|
||||
description_binary: contentBinaryEncoded,
|
||||
description_html: contentHTML,
|
||||
description: contentJSON,
|
||||
};
|
||||
const { contentBinaryEncoded, contentHTML, contentJSON } =
|
||||
getAllDocumentFormatsFromBinaryData(updatedDescription);
|
||||
try {
|
||||
const payload = {
|
||||
description_binary: contentBinaryEncoded,
|
||||
description_html: contentHTML,
|
||||
description: contentJSON,
|
||||
};
|
||||
|
||||
await pageService.updateDescription(workspaceSlug, projectId, pageId, payload, cookie);
|
||||
await pageService.updateDescription(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
pageId,
|
||||
payload,
|
||||
cookie,
|
||||
);
|
||||
} catch (error) {
|
||||
manualLogger.error("Update error:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchDescriptionHTMLAndTransform = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
pageId: string,
|
||||
cookie: string
|
||||
cookie: string,
|
||||
) => {
|
||||
if (!workspaceSlug || !projectId || !cookie) return;
|
||||
|
||||
const pageDetails = await pageService.fetchDetails(workspaceSlug, projectId, pageId, cookie);
|
||||
const { contentBinary } = getBinaryDataFromHTMLString(pageDetails.description_html ?? "<p></p>");
|
||||
return contentBinary;
|
||||
try {
|
||||
const pageDetails = await pageService.fetchDetails(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
pageId,
|
||||
cookie,
|
||||
);
|
||||
const { contentBinary } = getBinaryDataFromHTMLString(
|
||||
pageDetails.description_html ?? "<p></p>",
|
||||
);
|
||||
return contentBinary;
|
||||
} catch (error) {
|
||||
manualLogger.error(
|
||||
"Error while transforming from HTML to Uint8Array",
|
||||
error,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchPageDescriptionBinary = async (
|
||||
params: URLSearchParams,
|
||||
pageId: string,
|
||||
cookie: string | undefined
|
||||
cookie: string | undefined,
|
||||
) => {
|
||||
const workspaceSlug = params.get("workspaceSlug")?.toString();
|
||||
const projectId = params.get("projectId")?.toString();
|
||||
if (!workspaceSlug || !projectId || !cookie) return null;
|
||||
|
||||
const response = await pageService.fetchDescriptionBinary(workspaceSlug, projectId, pageId, cookie);
|
||||
const binaryData = new Uint8Array(response);
|
||||
try {
|
||||
const response = await pageService.fetchDescriptionBinary(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
pageId,
|
||||
cookie,
|
||||
);
|
||||
const binaryData = new Uint8Array(response);
|
||||
|
||||
if (binaryData.byteLength === 0) {
|
||||
const binary = await fetchDescriptionHTMLAndTransform(workspaceSlug, projectId, pageId, cookie);
|
||||
if (binary) {
|
||||
return binary;
|
||||
if (binaryData.byteLength === 0) {
|
||||
const binary = await fetchDescriptionHTMLAndTransform(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
pageId,
|
||||
cookie,
|
||||
);
|
||||
if (binary) {
|
||||
return binary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return binaryData;
|
||||
return binaryData;
|
||||
} catch (error) {
|
||||
manualLogger.error("Fetch error:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import { Redis } from "ioredis";
|
||||
import { logger } from "@plane/logger";
|
||||
|
||||
let redisClient: Redis | null = null;
|
||||
|
||||
export async function initializeRedis(): Promise<Redis> {
|
||||
const redisUrl = getRedisUrl();
|
||||
|
||||
if (!redisUrl) {
|
||||
logger.error("Redis URL is not configured. Please set REDIS_URL environment variable.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
redisClient = new Redis(redisUrl);
|
||||
|
||||
redisClient.on("error", (error) => {
|
||||
logger.error("Redis connection error:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Wait for the connection to be ready
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
redisClient!.on("ready", () => {
|
||||
logger.info("Redis connection established successfully");
|
||||
resolve();
|
||||
});
|
||||
|
||||
redisClient!.on("error", (error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
|
||||
return redisClient;
|
||||
} catch (error) {
|
||||
logger.error("Failed to initialize Redis:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
export function getRedisClient(): Redis {
|
||||
if (!redisClient) {
|
||||
throw new Error("Redis client not initialized. Call initializeRedis() first.");
|
||||
}
|
||||
return redisClient;
|
||||
}
|
||||
|
||||
function getRedisUrl() {
|
||||
const redisUrl = process.env.REDIS_URL?.trim();
|
||||
const redisHost = process.env.REDIS_HOST?.trim();
|
||||
const redisPort = process.env.REDIS_PORT?.trim();
|
||||
|
||||
if (redisUrl) {
|
||||
return redisUrl;
|
||||
}
|
||||
|
||||
if (redisHost && redisPort && !Number.isNaN(Number(redisPort))) {
|
||||
return `redis://${redisHost}:${redisPort}`;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export function getRedisUrl() {
|
||||
const redisUrl = process.env.REDIS_URL?.trim();
|
||||
const redisHost = process.env.REDIS_HOST?.trim();
|
||||
const redisPort = process.env.REDIS_PORT?.trim();
|
||||
|
||||
if (redisUrl) {
|
||||
return redisUrl;
|
||||
}
|
||||
|
||||
if (redisHost && redisPort && !Number.isNaN(Number(redisPort))) {
|
||||
return `redis://${redisHost}:${redisPort}`;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// types
|
||||
import { TPage } from "@plane/types";
|
||||
// services
|
||||
import { API_BASE_URL, APIService } from "@/core/services/api.service.js";
|
||||
|
||||
export class PageService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async fetchDetails(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
pageId: string,
|
||||
cookie: string
|
||||
): Promise<TPage> {
|
||||
return this.get(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/`,
|
||||
{
|
||||
headers: {
|
||||
Cookie: cookie,
|
||||
},
|
||||
}
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async fetchDescriptionBinary(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
pageId: string,
|
||||
cookie: string
|
||||
): Promise<any> {
|
||||
return this.get(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/description/`,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/octet-stream",
|
||||
Cookie: cookie,
|
||||
},
|
||||
responseType: "arraybuffer",
|
||||
}
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async updateDescription(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
pageId: string,
|
||||
data: {
|
||||
description_binary: string;
|
||||
description_html: string;
|
||||
description: object;
|
||||
},
|
||||
cookie: string
|
||||
): Promise<any> {
|
||||
return this.patch(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/description/`,
|
||||
data,
|
||||
{
|
||||
headers: {
|
||||
Cookie: cookie,
|
||||
},
|
||||
}
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// types
|
||||
import type { IUser } from "@plane/types";
|
||||
// services
|
||||
import { API_BASE_URL, APIService } from "@/services/api.service";
|
||||
import { API_BASE_URL, APIService } from "@/core/services/api.service.js";
|
||||
|
||||
export class UserService extends APIService {
|
||||
constructor() {
|
||||
Vendored
+1
-6
@@ -1,15 +1,10 @@
|
||||
// types
|
||||
import { TAdditionalDocumentTypes } from "@/plane-live/types/common";
|
||||
import { TAdditionalDocumentTypes } from "@/plane-live/types/common.js";
|
||||
|
||||
export type TDocumentTypes = "project_page" | TAdditionalDocumentTypes;
|
||||
|
||||
export type HocusPocusServerContext = {
|
||||
cookie: string;
|
||||
projectId: string;
|
||||
workspaceSlug: string;
|
||||
documentType: TDocumentTypes;
|
||||
userId: string;
|
||||
agentId: string;
|
||||
};
|
||||
|
||||
export type TConvertDocumentRequestBody = {
|
||||
|
||||
-62
@@ -1,62 +0,0 @@
|
||||
import { HocusPocusServerContext, TDocumentTypes } from "@/core/types/common";
|
||||
|
||||
/**
|
||||
* Parameters for document fetch operations
|
||||
*/
|
||||
export interface DocumentFetchParams {
|
||||
context: HocusPocusServerContext;
|
||||
pageId: string;
|
||||
params: URLSearchParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for document store operations
|
||||
*/
|
||||
export interface DocumentStoreParams {
|
||||
context: HocusPocusServerContext;
|
||||
pageId: string;
|
||||
state: any;
|
||||
params: URLSearchParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface defining a document handler
|
||||
*/
|
||||
export interface DocumentHandler {
|
||||
/**
|
||||
* Fetch a document
|
||||
*/
|
||||
fetch: (params: DocumentFetchParams) => Promise<any>;
|
||||
|
||||
/**
|
||||
* Store a document
|
||||
*/
|
||||
store: (params: DocumentStoreParams) => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler context interface - extend this to add new criteria for handler selection
|
||||
*/
|
||||
export interface HandlerContext {
|
||||
documentType?: TDocumentTypes;
|
||||
agentId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler selector function type - determines if a handler should be used based on context
|
||||
*/
|
||||
export type HandlerSelector = (context: HandlerContext) => boolean;
|
||||
|
||||
/**
|
||||
* Handler definition combining a selector and implementation
|
||||
*/
|
||||
export interface HandlerDefinition {
|
||||
selector: HandlerSelector;
|
||||
handler: DocumentHandler;
|
||||
priority: number; // Higher number means higher priority
|
||||
}
|
||||
|
||||
/**
|
||||
* Type for a handler registration function
|
||||
*/
|
||||
export type RegisterHandler = (definition: HandlerDefinition) => void;
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./register";
|
||||
@@ -1,5 +0,0 @@
|
||||
import { registerProjectPageHandler } from "@/ce/document-types/project-page";
|
||||
|
||||
export function initializeDocumentHandlers() {
|
||||
registerProjectPageHandler();
|
||||
}
|
||||
@@ -1,2 +1 @@
|
||||
export * from "../../core/lib/authentication";
|
||||
|
||||
export * from "../../ce/lib/authentication.js"
|
||||
@@ -1,2 +1 @@
|
||||
export * from "../../ce/lib/fetch-document";
|
||||
|
||||
export * from "../../ce/lib/fetch-document.js"
|
||||
@@ -1,2 +1 @@
|
||||
export * from "../../ce/lib/update-document";
|
||||
|
||||
export * from "../../ce/lib/update-document.js"
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
export * from "../../ce/types/common"
|
||||
export * from "../../ce/types/common.js"
|
||||
@@ -1,42 +0,0 @@
|
||||
import * as dotenv from "dotenv";
|
||||
import { z } from "zod";
|
||||
|
||||
// Load environment variables from .env file
|
||||
dotenv.config();
|
||||
|
||||
// Define environment schema with validation
|
||||
const envSchema = z.object({
|
||||
// Server configuration
|
||||
NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
|
||||
PORT: z.string().default("3000").transform(Number),
|
||||
LIVE_BASE_PATH: z.string().default("/live"),
|
||||
|
||||
// CORS configuration
|
||||
CORS_ALLOWED_ORIGINS: z.string().default("*"),
|
||||
// Compression options
|
||||
COMPRESSION_LEVEL: z.string().default("6").transform(Number),
|
||||
COMPRESSION_THRESHOLD: z.string().default("5000").transform(Number),
|
||||
|
||||
// Hocuspocus server configuration
|
||||
HOCUSPOCUS_URL: z.string().optional(),
|
||||
HOCUSPOCUS_USERNAME: z.string().optional(),
|
||||
HOCUSPOCUS_PASSWORD: z.string().optional(),
|
||||
|
||||
// Graceful termination timeout
|
||||
SHUTDOWN_TIMEOUT: z.string().default("10000").transform(Number),
|
||||
});
|
||||
|
||||
// Validate the environment variables
|
||||
function validateEnv() {
|
||||
const result = envSchema.safeParse(process.env);
|
||||
|
||||
if (!result.success) {
|
||||
console.error("❌ Invalid environment variables:", JSON.stringify(result.error.format(), null, 4));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return result.data;
|
||||
}
|
||||
|
||||
// Export the validated environment
|
||||
export const env = validateEnv();
|
||||
@@ -1,145 +0,0 @@
|
||||
import { DocumentCollaborativeEvents, TDocumentEventsServer } from "@plane/editor/lib";
|
||||
|
||||
import { Logger } from "@hocuspocus/extension-logger";
|
||||
import { Database } from "@hocuspocus/extension-database";
|
||||
import { Redis } from "@hocuspocus/extension-redis";
|
||||
import { Hocuspocus } from "@hocuspocus/server";
|
||||
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { getRedisClient } from "./redis";
|
||||
import { UserService } from "./services/user.service";
|
||||
|
||||
// import { handleError } from "@/core/helpers/error-handling/error-factory";
|
||||
// import { TDocumentTypes } from "@/core/types/common";
|
||||
// import { handleAuthentication } from "@/core/lib/authentication";
|
||||
// import { IncomingHttpHeaders } from "http";
|
||||
|
||||
export const createHocusPocus = () => {
|
||||
const serverName = process.env.HOSTNAME || uuidv4();
|
||||
return new Hocuspocus({
|
||||
name: serverName,
|
||||
onAuthenticate: onAuthenticate(),
|
||||
onStateless: onStateless(),
|
||||
extensions: [
|
||||
new Logger(),
|
||||
new Database({
|
||||
fetch: handleDataFetch,
|
||||
store: handleDataStore,
|
||||
}),
|
||||
new Redis({ redis: getRedisClient() }),
|
||||
],
|
||||
debounce: 1000,
|
||||
});
|
||||
};
|
||||
|
||||
const validateToken = async (token: string | undefined) => {
|
||||
try {
|
||||
if (!token) {
|
||||
throw new Error("Token not provided");
|
||||
}
|
||||
const userService = new UserService();
|
||||
const response = await userService.currentUser(token);
|
||||
return response;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const onAuthenticate = () => {
|
||||
return async ({ token }: { token: string | undefined }) => {
|
||||
const user = await validateToken(token);
|
||||
if (!user) {
|
||||
throw new Error("Invalid token");
|
||||
}
|
||||
return user;
|
||||
};
|
||||
};
|
||||
|
||||
const onStateless = () => {
|
||||
return async ({ payload, document }: any) => {
|
||||
// broadcast the client event (derived from the server event) to all the clients so that they can update their state
|
||||
const response = DocumentCollaborativeEvents[payload as TDocumentEventsServer].client;
|
||||
if (response) {
|
||||
document.broadcastStateless(response);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const handleDataFetch = async (data: any) => {
|
||||
try {
|
||||
const { context, documentName, requestParameters } = data;
|
||||
console.log("handleDataFetch", context);
|
||||
console.log("handleDataFetch", documentName);
|
||||
console.log("handleDataFetch", requestParameters);
|
||||
return documentName; // TODO: remove this once the API integration is done
|
||||
// fetch the data using page service
|
||||
// const pageService = new PageService();
|
||||
// const page = await pageService.getPage(documentName);
|
||||
// return page;
|
||||
} catch (error) {
|
||||
console.error("handleDataFetch", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDataStore = async (data: any) => {
|
||||
try {
|
||||
const { context, documentName, requestParameters } = data;
|
||||
console.log("handleDataStore", context);
|
||||
console.log("handleDataStore", documentName);
|
||||
console.log("handleDataStore", requestParameters);
|
||||
return documentName; // TODO: remove this once the API integration is done
|
||||
// store the data using page service
|
||||
// const pageService = new PageService();
|
||||
// const page = await pageService.updatePage(documentName, requestParameters);
|
||||
// return page;
|
||||
} catch (error) {
|
||||
console.error("handleDataStore", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// async ({
|
||||
// token,
|
||||
// requestParameters,
|
||||
// requestHeaders,
|
||||
// }: {
|
||||
// token: string;
|
||||
// requestParameters: URLSearchParams;
|
||||
// requestHeaders: IncomingHttpHeaders;
|
||||
// }) => {
|
||||
// let cookie: string | undefined = undefined;
|
||||
// let userId: string | undefined = undefined;
|
||||
|
||||
// try {
|
||||
// const parsedToken = JSON.parse(token) as { id: string; cookie: string };
|
||||
// userId = parsedToken.id;
|
||||
// cookie = parsedToken.cookie;
|
||||
// } catch (error) {
|
||||
// console.error("Token parsing failed, using request headers:", error);
|
||||
// } finally {
|
||||
// if (!cookie) {
|
||||
// cookie = requestHeaders.cookie?.toString();
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (!cookie || !userId) {
|
||||
// handleError(null, {
|
||||
// errorType: "unauthorized",
|
||||
// message: "Credentials not provided",
|
||||
// component: "hocuspocus",
|
||||
// operation: "authenticate",
|
||||
// extraContext: { tokenProvided: !!token },
|
||||
// throw: true,
|
||||
// });
|
||||
// }
|
||||
|
||||
// const documentType = requestParameters.get("documentType")?.toString() as TDocumentTypes;
|
||||
// const workspaceSlug = requestParameters.get("workspaceSlug")?.toString() as string;
|
||||
|
||||
// return await handleAuthentication({
|
||||
// cookie,
|
||||
// userId,
|
||||
// workspaceSlug,
|
||||
// });
|
||||
// }
|
||||
@@ -1,278 +0,0 @@
|
||||
import { AppError, HttpStatusCode, ErrorCategory } from "./error-handler";
|
||||
|
||||
/**
|
||||
* Map of error types to their corresponding factory functions
|
||||
* This ensures that error types and their implementations stay in sync
|
||||
*/
|
||||
interface ErrorFactory {
|
||||
statusCode: number;
|
||||
category: ErrorCategory;
|
||||
defaultMessage: string;
|
||||
createError: (message?: string, context?: Record<string, any>) => AppError;
|
||||
}
|
||||
|
||||
const ERROR_FACTORIES = {
|
||||
"bad-request": {
|
||||
statusCode: HttpStatusCode.BAD_REQUEST,
|
||||
category: ErrorCategory.OPERATIONAL,
|
||||
defaultMessage: "Bad Request",
|
||||
createError: (message = "Bad Request", context?) =>
|
||||
new AppError(message, HttpStatusCode.BAD_REQUEST, ErrorCategory.OPERATIONAL, context),
|
||||
},
|
||||
unauthorized: {
|
||||
statusCode: HttpStatusCode.UNAUTHORIZED,
|
||||
category: ErrorCategory.OPERATIONAL,
|
||||
defaultMessage: "Unauthorized",
|
||||
createError: (message = "Unauthorized", context?) =>
|
||||
new AppError(message, HttpStatusCode.UNAUTHORIZED, ErrorCategory.OPERATIONAL, context),
|
||||
},
|
||||
forbidden: {
|
||||
statusCode: HttpStatusCode.FORBIDDEN,
|
||||
category: ErrorCategory.OPERATIONAL,
|
||||
defaultMessage: "Forbidden",
|
||||
createError: (message = "Forbidden", context?) =>
|
||||
new AppError(message, HttpStatusCode.FORBIDDEN, ErrorCategory.OPERATIONAL, context),
|
||||
},
|
||||
"not-found": {
|
||||
statusCode: HttpStatusCode.NOT_FOUND,
|
||||
category: ErrorCategory.OPERATIONAL,
|
||||
defaultMessage: "Resource not found",
|
||||
createError: (message = "Resource not found", context?) =>
|
||||
new AppError(message, HttpStatusCode.NOT_FOUND, ErrorCategory.OPERATIONAL, context),
|
||||
},
|
||||
conflict: {
|
||||
statusCode: HttpStatusCode.CONFLICT,
|
||||
category: ErrorCategory.OPERATIONAL,
|
||||
defaultMessage: "Resource conflict",
|
||||
createError: (message = "Resource conflict", context?) =>
|
||||
new AppError(message, HttpStatusCode.CONFLICT, ErrorCategory.OPERATIONAL, context),
|
||||
},
|
||||
"unprocessable-entity": {
|
||||
statusCode: HttpStatusCode.UNPROCESSABLE_ENTITY,
|
||||
category: ErrorCategory.OPERATIONAL,
|
||||
defaultMessage: "Unprocessable Entity",
|
||||
createError: (message = "Unprocessable Entity", context?) =>
|
||||
new AppError(message, HttpStatusCode.UNPROCESSABLE_ENTITY, ErrorCategory.OPERATIONAL, context),
|
||||
},
|
||||
"too-many-requests": {
|
||||
statusCode: HttpStatusCode.TOO_MANY_REQUESTS,
|
||||
category: ErrorCategory.OPERATIONAL,
|
||||
defaultMessage: "Too many requests",
|
||||
createError: (message = "Too many requests", context?) =>
|
||||
new AppError(message, HttpStatusCode.TOO_MANY_REQUESTS, ErrorCategory.OPERATIONAL, context),
|
||||
},
|
||||
internal: {
|
||||
statusCode: HttpStatusCode.INTERNAL_SERVER,
|
||||
category: ErrorCategory.PROGRAMMING,
|
||||
defaultMessage: "Internal Server Error",
|
||||
createError: (message = "Internal Server Error", context?) =>
|
||||
new AppError(message, HttpStatusCode.INTERNAL_SERVER, ErrorCategory.PROGRAMMING, context),
|
||||
},
|
||||
"service-unavailable": {
|
||||
statusCode: HttpStatusCode.SERVICE_UNAVAILABLE,
|
||||
category: ErrorCategory.SYSTEM,
|
||||
defaultMessage: "Service Unavailable",
|
||||
createError: (message = "Service Unavailable", context?) =>
|
||||
new AppError(message, HttpStatusCode.SERVICE_UNAVAILABLE, ErrorCategory.SYSTEM, context),
|
||||
},
|
||||
fatal: {
|
||||
statusCode: HttpStatusCode.INTERNAL_SERVER,
|
||||
category: ErrorCategory.FATAL,
|
||||
defaultMessage: "Fatal Error",
|
||||
createError: (message = "Fatal Error", context?) =>
|
||||
new AppError(message, HttpStatusCode.INTERNAL_SERVER, ErrorCategory.FATAL, context),
|
||||
},
|
||||
} satisfies Record<string, ErrorFactory>;
|
||||
|
||||
// Create the type from the keys of the error factories map
|
||||
export type ErrorType = keyof typeof ERROR_FACTORIES;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Primary public API - Recommended for most use cases
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Base options for handleError function
|
||||
*/
|
||||
type BaseErrorHandlerOptions = {
|
||||
// Error classification options
|
||||
errorType?: ErrorType;
|
||||
message?: string;
|
||||
|
||||
// Context information
|
||||
component: string;
|
||||
operation: string;
|
||||
extraContext?: Record<string, any>;
|
||||
|
||||
// Behavior options
|
||||
rethrowIfAppError?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Options for throwing variant of handleError - discriminated by throw: true
|
||||
*/
|
||||
export type ThrowingOptions = BaseErrorHandlerOptions & {
|
||||
throw: true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Options for non-throwing variant of handleError - default behavior
|
||||
*/
|
||||
export type NonThrowingOptions = BaseErrorHandlerOptions;
|
||||
|
||||
/**
|
||||
* Unified error handler that encapsulates common error handling patterns
|
||||
*
|
||||
* @param error The error to handle
|
||||
* @param options Configuration options with throw: true to throw the error instead of returning it
|
||||
* @returns Never returns - always throws
|
||||
* @example
|
||||
* // Throwing version
|
||||
* handleError(error, {
|
||||
* errorType: 'not-found',
|
||||
* component: 'user-service',
|
||||
* operation: 'getUserById',
|
||||
* throw: true
|
||||
* });
|
||||
*/
|
||||
export function handleError(error: unknown, options: ThrowingOptions): never;
|
||||
|
||||
/**
|
||||
* Unified error handler that encapsulates common error handling patterns
|
||||
*
|
||||
* @param error The error to handle
|
||||
* @param options Configuration options (non-throwing by default)
|
||||
* @returns The AppError instance
|
||||
* @example
|
||||
* // Non-throwing version (default)
|
||||
* const appError = handleError(error, {
|
||||
* errorType: 'not-found',
|
||||
* component: 'user-service',
|
||||
* operation: 'getUserById'
|
||||
* });
|
||||
* return { error: appError.output() };
|
||||
*/
|
||||
// eslint-disable-next-line no-redeclare
|
||||
export function handleError(error: unknown, options: NonThrowingOptions): AppError;
|
||||
|
||||
/**
|
||||
* Implementation of handleError that handles both throwing and non-throwing cases
|
||||
*/
|
||||
// eslint-disable-next-line no-redeclare
|
||||
export function handleError(error: unknown, options: ThrowingOptions | NonThrowingOptions): AppError | never {
|
||||
// Only throw if throw is explicitly true
|
||||
const shouldThrow = (options as ThrowingOptions).throw === true;
|
||||
|
||||
// If the error is already an AppError and we want to rethrow it as is
|
||||
if (options.rethrowIfAppError !== false && error instanceof AppError) {
|
||||
if (shouldThrow) {
|
||||
throw error;
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
// Format the error message
|
||||
const errorMessage = options.message
|
||||
? error instanceof Error
|
||||
? `${options.message}: ${error.message}`
|
||||
: error
|
||||
? `${options.message}: ${String(error)}`
|
||||
: options.message
|
||||
: error instanceof Error
|
||||
? error.message
|
||||
: error
|
||||
? String(error)
|
||||
: "Unknown error occurred";
|
||||
|
||||
// Build context object
|
||||
const context = {
|
||||
component: options.component,
|
||||
operation: options.operation,
|
||||
originalError: error,
|
||||
...(options.extraContext || {}),
|
||||
};
|
||||
|
||||
// Create the appropriate error type using our factory map
|
||||
const errorType = options.errorType || "internal";
|
||||
const factory = ERROR_FACTORIES[errorType];
|
||||
|
||||
if (!factory) {
|
||||
// If no factory found, default to internal error
|
||||
return ERROR_FACTORIES.internal.createError(errorMessage, context);
|
||||
}
|
||||
|
||||
// Create the error with the factory
|
||||
const appError = factory.createError(errorMessage, context);
|
||||
|
||||
// If we should throw, do so now
|
||||
if (shouldThrow) {
|
||||
throw appError;
|
||||
}
|
||||
|
||||
return appError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to convert errors or enhance existing AppErrors
|
||||
*/
|
||||
export const convertError = (
|
||||
error: Error,
|
||||
options?: {
|
||||
statusCode?: number;
|
||||
message?: string;
|
||||
category?: ErrorCategory;
|
||||
context?: Record<string, any>;
|
||||
}
|
||||
): AppError => {
|
||||
if (error instanceof AppError) {
|
||||
// If it's already an AppError and no overrides, return as is
|
||||
if (!options?.statusCode && !options?.message && !options?.category) {
|
||||
return error;
|
||||
}
|
||||
|
||||
// Create a new AppError with the original as context
|
||||
return new AppError(
|
||||
options?.message || error.message,
|
||||
options?.statusCode || error.status,
|
||||
options?.category || error.category,
|
||||
{
|
||||
...(error.context || {}),
|
||||
...(options?.context || {}),
|
||||
originalError: error,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Determine the appropriate error type based on status code
|
||||
let errorType: ErrorType = "internal";
|
||||
if (options?.statusCode) {
|
||||
// Find the error type that matches the status code
|
||||
const entry = Object.entries(ERROR_FACTORIES).find(([_, factory]) => factory.statusCode === options.statusCode);
|
||||
if (entry) {
|
||||
errorType = entry[0] as ErrorType;
|
||||
}
|
||||
}
|
||||
|
||||
// Return a new AppError using the factory
|
||||
return handleError(error, {
|
||||
errorType: errorType,
|
||||
message: options?.message,
|
||||
component: options?.context?.component || "unknown",
|
||||
operation: options?.context?.operation || "convert-error",
|
||||
extraContext: options?.context,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if an error is an AppError
|
||||
*/
|
||||
export const isAppError = (err: any, statusCode?: number): boolean => {
|
||||
return err instanceof AppError && (!statusCode || err.status === statusCode);
|
||||
};
|
||||
|
||||
// Export only the public API
|
||||
export default {
|
||||
handleError,
|
||||
convertError,
|
||||
isAppError,
|
||||
};
|
||||
@@ -1,384 +0,0 @@
|
||||
import { ErrorRequestHandler, Request, Response, NextFunction } from "express";
|
||||
|
||||
import { env } from "@/env";
|
||||
import { logger } from "@plane/logger";
|
||||
import { handleError } from "./error-factory";
|
||||
import { ErrorContext, reportError } from "./error-reporting";
|
||||
import { manualLogger } from "../../core/helpers/logger";
|
||||
|
||||
/**
|
||||
* HTTP Status Codes
|
||||
*/
|
||||
export enum HttpStatusCode {
|
||||
// 2xx Success
|
||||
OK = 200,
|
||||
CREATED = 201,
|
||||
ACCEPTED = 202,
|
||||
NO_CONTENT = 204,
|
||||
|
||||
// 4xx Client Errors
|
||||
BAD_REQUEST = 400,
|
||||
UNAUTHORIZED = 401,
|
||||
FORBIDDEN = 403,
|
||||
NOT_FOUND = 404,
|
||||
METHOD_NOT_ALLOWED = 405,
|
||||
CONFLICT = 409,
|
||||
GONE = 410,
|
||||
UNPROCESSABLE_ENTITY = 422,
|
||||
TOO_MANY_REQUESTS = 429,
|
||||
|
||||
// 5xx Server Errors
|
||||
INTERNAL_SERVER = 500,
|
||||
NOT_IMPLEMENTED = 501,
|
||||
BAD_GATEWAY = 502,
|
||||
SERVICE_UNAVAILABLE = 503,
|
||||
GATEWAY_TIMEOUT = 504,
|
||||
}
|
||||
|
||||
/**
|
||||
* Error categories to classify errors
|
||||
*/
|
||||
export enum ErrorCategory {
|
||||
OPERATIONAL = "operational", // Expected errors that are part of normal operation (e.g. validation failures)
|
||||
PROGRAMMING = "programming", // Unexpected errors that indicate bugs (e.g. null references)
|
||||
SYSTEM = "system", // System errors (e.g. out of memory, connection failures)
|
||||
FATAL = "fatal", // Severe errors that should crash the app (e.g. unrecoverable state)
|
||||
}
|
||||
|
||||
/**
|
||||
* Base Application Error Class
|
||||
* All custom errors extend this class
|
||||
*/
|
||||
export class AppError extends Error {
|
||||
readonly status: number;
|
||||
readonly category: ErrorCategory;
|
||||
readonly context?: Record<string, any>;
|
||||
readonly isOperational: boolean; // Kept for backward compatibility
|
||||
|
||||
constructor(
|
||||
message: string,
|
||||
status: number = HttpStatusCode.INTERNAL_SERVER,
|
||||
category: ErrorCategory = ErrorCategory.PROGRAMMING,
|
||||
context?: Record<string, any>
|
||||
) {
|
||||
super(message);
|
||||
|
||||
// Set error properties
|
||||
this.name = this.constructor.name;
|
||||
this.status = status;
|
||||
this.category = category;
|
||||
this.isOperational = category === ErrorCategory.OPERATIONAL;
|
||||
this.context = context;
|
||||
|
||||
// Capture stack trace, excluding the constructor call from the stack
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
|
||||
// Automatically report the error (unless it's being constructed by the error utilities)
|
||||
if (!context?.skipReporting) {
|
||||
this.report();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a formatted representation of the error
|
||||
*/
|
||||
output() {
|
||||
return {
|
||||
statusCode: this.status,
|
||||
payload: {
|
||||
statusCode: this.status,
|
||||
error: this.getErrorName(),
|
||||
message: this.message,
|
||||
category: this.category,
|
||||
},
|
||||
headers: {},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a descriptive name for the error based on status code
|
||||
*/
|
||||
private getErrorName(): string {
|
||||
const statusCodes: Record<number, string> = {
|
||||
400: "Bad Request",
|
||||
401: "Unauthorized",
|
||||
403: "Forbidden",
|
||||
404: "Not Found",
|
||||
405: "Method Not Allowed",
|
||||
409: "Conflict",
|
||||
410: "Gone",
|
||||
422: "Unprocessable Entity",
|
||||
429: "Too Many Requests",
|
||||
500: "Internal Server Error",
|
||||
501: "Not Implemented",
|
||||
502: "Bad Gateway",
|
||||
503: "Service Unavailable",
|
||||
504: "Gateway Timeout",
|
||||
};
|
||||
|
||||
return statusCodes[this.status] || "Unknown Error";
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports the error to logging and monitoring systems
|
||||
*/
|
||||
private report(): void {
|
||||
// Different logging based on error category
|
||||
if (this.category === ErrorCategory.OPERATIONAL) {
|
||||
manualLogger.error(`Operational error: ${this.message}`, {
|
||||
errorName: this.name,
|
||||
errorStatus: this.status,
|
||||
errorCategory: this.category,
|
||||
context: this.context,
|
||||
});
|
||||
} else if (this.category === ErrorCategory.FATAL) {
|
||||
manualLogger.error(`FATAL error: ${this.message}`, {
|
||||
errorName: this.name,
|
||||
errorStatus: this.status,
|
||||
errorCategory: this.category,
|
||||
stack: this.stack,
|
||||
context: this.context,
|
||||
});
|
||||
} else {
|
||||
manualLogger.error(`${this.category} error: ${this.message}`, {
|
||||
errorName: this.name,
|
||||
errorStatus: this.status,
|
||||
errorCategory: this.category,
|
||||
stack: this.stack,
|
||||
context: this.context,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class FatalError extends AppError {
|
||||
constructor(message: string, context?: Record<string, any>) {
|
||||
super(message, HttpStatusCode.INTERNAL_SERVER, ErrorCategory.FATAL, context);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main Express error handler middleware
|
||||
*/
|
||||
export const errorHandler: ErrorRequestHandler = (err, req, res, next) => {
|
||||
// Already sent response, let default Express error handler deal with it
|
||||
if (res.headersSent) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
// Convert to AppError if it's not already one
|
||||
const error = handleError(err, {
|
||||
component: "express",
|
||||
operation: "error-handler",
|
||||
extraContext: {
|
||||
originalError: err,
|
||||
url: req.originalUrl,
|
||||
method: req.method,
|
||||
},
|
||||
});
|
||||
|
||||
// Normalize status code
|
||||
const statusCode = error.status;
|
||||
|
||||
// Set the response status
|
||||
res.status(statusCode);
|
||||
|
||||
// Set any custom headers if provided in the error object
|
||||
if (err.headers && typeof err.headers === "object") {
|
||||
Object.entries(err.headers).forEach(([key, value]) => {
|
||||
res.set(key, value as string);
|
||||
});
|
||||
}
|
||||
|
||||
// Prepare error response
|
||||
const errorResponse: {
|
||||
error: {
|
||||
message: string;
|
||||
status: number;
|
||||
stack?: string;
|
||||
};
|
||||
} = {
|
||||
error: {
|
||||
message:
|
||||
error.category === ErrorCategory.OPERATIONAL || env.NODE_ENV !== "production"
|
||||
? error.message
|
||||
: "An unexpected error occurred",
|
||||
status: statusCode,
|
||||
},
|
||||
};
|
||||
|
||||
// Add stack trace in non-production environments
|
||||
if (env.NODE_ENV !== "production") {
|
||||
errorResponse.error.stack = error.stack;
|
||||
}
|
||||
|
||||
// Send the response
|
||||
res.json(errorResponse);
|
||||
|
||||
// For fatal errors, log but NEVER terminate the app
|
||||
if (error.category === ErrorCategory.FATAL) {
|
||||
logger.error(`FATAL ERROR OCCURRED BUT APP WILL CONTINUE RUNNING: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const asyncHandler = (fn: Function) => {
|
||||
return (req: any, res: any, next: any) => {
|
||||
Promise.resolve(fn(req, res, next)).catch((error) => {
|
||||
// Convert to AppError if needed and pass to Express error middleware
|
||||
const appError = handleError(error, {
|
||||
errorType: "internal",
|
||||
component: "express",
|
||||
operation: "route-handler",
|
||||
extraContext: {
|
||||
url: req.originalUrl,
|
||||
method: req.method,
|
||||
body: req.body,
|
||||
query: req.query,
|
||||
params: req.params,
|
||||
},
|
||||
});
|
||||
|
||||
next(appError);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export interface CatchAsyncOptions<T, E = Error> {
|
||||
/** Default value to return in case of error, null by default */
|
||||
defaultValue?: T | null;
|
||||
|
||||
/** Whether to report non-AppErrors automatically */
|
||||
reportErrors?: boolean;
|
||||
|
||||
/** Whether to rethrow the error after handling it */
|
||||
rethrow?: boolean;
|
||||
|
||||
/** Custom error transformer function */
|
||||
transformError?: (error: unknown) => E;
|
||||
|
||||
/** Custom error handler function that runs before standard handling */
|
||||
onError?: (error: unknown) => void | Promise<void>;
|
||||
|
||||
/** Custom handler for specific error types */
|
||||
errorHandlers?: {
|
||||
[key: string]: (error: any) => T | null | Promise<T>;
|
||||
};
|
||||
}
|
||||
|
||||
export const catchAsync = <T, E = Error>(
|
||||
fn: () => Promise<T>,
|
||||
context?: ErrorContext,
|
||||
options: CatchAsyncOptions<T, E> = {}
|
||||
): (() => Promise<T | null>) => {
|
||||
const { defaultValue = null, onError, rethrow = false } = options;
|
||||
|
||||
return async () => {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (error) {
|
||||
// Apply custom error handler if provided
|
||||
if (onError) {
|
||||
await Promise.resolve(onError(error));
|
||||
}
|
||||
|
||||
reportError(error, context);
|
||||
if (error instanceof AppError) {
|
||||
error.context;
|
||||
}
|
||||
|
||||
if (rethrow) {
|
||||
// Use handleError to ensure consistent error handling when rethrowing
|
||||
handleError(error, {
|
||||
component: context?.extra?.component || "unknown",
|
||||
operation: context?.extra?.operation || "unknown",
|
||||
extraContext: {
|
||||
...context,
|
||||
...(error instanceof AppError ? error.context : {}),
|
||||
originalError: error,
|
||||
},
|
||||
throw: true,
|
||||
});
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Set up global error handlers for uncaught exceptions and unhandled rejections
|
||||
* @param gracefulTerminationHandler Function to call for graceful termination
|
||||
*/
|
||||
export const setupGlobalErrorHandlers = (gracefulTerminationHandler: () => Promise<void>): void => {
|
||||
// Handle promise rejections
|
||||
process.on("unhandledRejection", (reason: unknown) => {
|
||||
logger.error("Unhandled Promise Rejection", { reason });
|
||||
|
||||
// Convert to AppError and handle
|
||||
const appError = handleError(reason, {
|
||||
errorType: "internal",
|
||||
message: reason instanceof Error ? reason.message : String(reason),
|
||||
component: "process",
|
||||
operation: "unhandledRejection",
|
||||
extraContext: { source: "unhandledRejection" },
|
||||
});
|
||||
|
||||
// Log the error but never terminate
|
||||
logger.error(`Unhandled rejection caught and contained: ${appError.message}`);
|
||||
});
|
||||
|
||||
// Handle exceptions
|
||||
process.on("uncaughtException", (error: Error) => {
|
||||
logger.error("Uncaught Exception", {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
});
|
||||
|
||||
// Convert to AppError if needed
|
||||
const appError = handleError(error, {
|
||||
errorType: "internal",
|
||||
component: "process",
|
||||
operation: "uncaughtException",
|
||||
extraContext: {
|
||||
source: "uncaughtException",
|
||||
},
|
||||
});
|
||||
|
||||
// Log the error but never terminate
|
||||
logger.warn(`Uncaught exception contained: ${appError.message}`);
|
||||
});
|
||||
|
||||
// Handle termination signals
|
||||
process.on("SIGTERM", () => {
|
||||
logger.info("SIGTERM received. Starting graceful termination...");
|
||||
gracefulTerminationHandler();
|
||||
});
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
logger.info("SIGINT received. Starting graceful termination...");
|
||||
gracefulTerminationHandler();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Configure error handling middleware for the Express app
|
||||
* @param app Express application instance
|
||||
*/
|
||||
export function configureErrorHandlers(app: any): void {
|
||||
// Global error handling middleware
|
||||
app.use(errorHandler);
|
||||
|
||||
// 404 handler must be last
|
||||
app.use((_req: Request, _res: Response, next: NextFunction) => {
|
||||
next(
|
||||
handleError(null, {
|
||||
errorType: "not-found",
|
||||
message: "Resource not found",
|
||||
component: "express",
|
||||
operation: "route-handler",
|
||||
extraContext: { path: _req.path },
|
||||
throw: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import { AppError, ErrorCategory } from "./error-handler";
|
||||
import { logger } from "@plane/logger";
|
||||
import { handleError } from "./error-factory";
|
||||
|
||||
export interface ErrorContext {
|
||||
url?: string;
|
||||
method?: string;
|
||||
body?: any;
|
||||
query?: any;
|
||||
params?: any;
|
||||
extra?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to report errors that aren't instances of AppError
|
||||
* AppError instances automatically report themselves on creation
|
||||
* Only use this for external errors that don't use our error system
|
||||
*/
|
||||
export const reportError = (error: Error | unknown, context?: ErrorContext): void => {
|
||||
if (error instanceof AppError) {
|
||||
// if it's an app error, don't report it as it's already been reported
|
||||
return;
|
||||
}
|
||||
|
||||
logger.error(`External error: ${error instanceof Error ? error.stack || error.message : String(error)}`, {
|
||||
error,
|
||||
context,
|
||||
});
|
||||
};
|
||||
|
||||
export const handleFatalError = (error: Error | unknown, context?: ErrorContext): void => {
|
||||
// Convert to fatal AppError
|
||||
const fatalError = handleError(error, {
|
||||
errorType: "fatal",
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
component: context?.extra?.component || "system",
|
||||
operation: context?.extra?.operation || "fatal-error-handler",
|
||||
extraContext: {
|
||||
...context,
|
||||
originalError: error,
|
||||
},
|
||||
});
|
||||
|
||||
process.emit("uncaughtException", fatalError);
|
||||
};
|
||||
@@ -1,62 +0,0 @@
|
||||
import { Redis } from "ioredis";
|
||||
import { logger } from "@plane/logger";
|
||||
|
||||
let redisClient: Redis | null = null;
|
||||
|
||||
export async function initializeRedis(): Promise<Redis> {
|
||||
const redisUrl = getRedisUrl();
|
||||
|
||||
if (!redisUrl) {
|
||||
logger.error("Redis URL is not configured. Please set REDIS_URL environment variable.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
redisClient = new Redis(redisUrl);
|
||||
|
||||
redisClient.on("error", (error) => {
|
||||
logger.error("Redis connection error:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Wait for the connection to be ready
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
redisClient!.on("ready", () => {
|
||||
logger.info("Redis connection established successfully");
|
||||
resolve();
|
||||
});
|
||||
|
||||
redisClient!.on("error", (error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
|
||||
return redisClient;
|
||||
} catch (error) {
|
||||
logger.error("Failed to initialize Redis:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
export function getRedisClient(): Redis {
|
||||
if (!redisClient) {
|
||||
throw new Error("Redis client not initialized. Call initializeRedis() first.");
|
||||
}
|
||||
return redisClient;
|
||||
}
|
||||
|
||||
function getRedisUrl() {
|
||||
const redisUrl = process.env.REDIS_URL?.trim();
|
||||
const redisHost = process.env.REDIS_HOST?.trim();
|
||||
const redisPort = process.env.REDIS_PORT?.trim();
|
||||
|
||||
if (redisUrl) {
|
||||
return redisUrl;
|
||||
}
|
||||
|
||||
if (redisHost && redisPort && !Number.isNaN(Number(redisPort))) {
|
||||
return `redis://${redisHost}:${redisPort}`;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
+122
-106
@@ -1,119 +1,135 @@
|
||||
import cookieParser from "cookie-parser";
|
||||
import compression from "compression";
|
||||
import cors from "cors";
|
||||
import express, { Application } from "express";
|
||||
import expressWs from "express-ws";
|
||||
import express from "express";
|
||||
import helmet from "helmet";
|
||||
import path from "path";
|
||||
import { Hocuspocus } from "@hocuspocus/server";
|
||||
// controllers
|
||||
// hocuspocus server
|
||||
import { getHocusPocusServer } from "@/core/hocuspocus-server.js";
|
||||
// helpers
|
||||
import { convertHTMLDocumentToAllFormats } from "@/core/helpers/convert-document.js";
|
||||
import { logger, manualLogger } from "@/core/helpers/logger.js";
|
||||
import { errorHandler } from "@/core/helpers/error-handler.js";
|
||||
// types
|
||||
import { TConvertDocumentRequestBody } from "@/core/types/common.js";
|
||||
|
||||
import { initializeRedis } from "./core/lib/redis-manager";
|
||||
import { logger } from "@plane/logger";
|
||||
import { createHocusPocus } from "./hocuspocus";
|
||||
const app: any = express();
|
||||
expressWs(app);
|
||||
|
||||
import { registerWebSocketController, registerController } from "@plane/decorators";
|
||||
import { REST_CONTROLLERS, WEBSOCKET_CONTROLLERS } from "./controllers";
|
||||
app.set("port", process.env.PORT || 3000);
|
||||
|
||||
export default class Server {
|
||||
app: Application;
|
||||
PORT: number;
|
||||
BASE_PATH: string;
|
||||
CORS_ALLOWED_ORIGINS: string;
|
||||
hocuspocusServer: Hocuspocus | null = null;
|
||||
private httpServer: any;
|
||||
// Security middleware
|
||||
app.use(helmet());
|
||||
|
||||
constructor() {
|
||||
this.PORT = parseInt(process.env.PORT || "3000");
|
||||
this.BASE_PATH = process.env.LIVE_BASE_PATH || "/";
|
||||
this.CORS_ALLOWED_ORIGINS = process.env.CORS_ALLOWED_ORIGINS || "*";
|
||||
// Initialize express app
|
||||
this.app = express();
|
||||
expressWs(this.app as any);
|
||||
// Security middleware
|
||||
this.app.use(helmet());
|
||||
// cors
|
||||
this.setupCors();
|
||||
// Cookie parsing
|
||||
this.app.use(cookieParser());
|
||||
// Body parsing middleware
|
||||
this.app.use(express.json());
|
||||
this.app.use(express.urlencoded({ extended: true }));
|
||||
// static files
|
||||
this.app.use(express.static(path.join(__dirname, "public")));
|
||||
// setup redis
|
||||
initializeRedis();
|
||||
// setup hocuspocus server
|
||||
this.hocuspocusServer = createHocusPocus();
|
||||
// setup controllers
|
||||
this.setupControllers();
|
||||
// Middleware for response compression
|
||||
app.use(
|
||||
compression({
|
||||
level: 6,
|
||||
threshold: 5 * 1000,
|
||||
})
|
||||
);
|
||||
|
||||
// Logging middleware
|
||||
app.use(logger);
|
||||
|
||||
// Body parsing middleware
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
// cors middleware
|
||||
app.use(cors());
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const HocusPocusServer = await getHocusPocusServer().catch((err) => {
|
||||
manualLogger.error("Failed to initialize HocusPocusServer:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
router.get("/health", (_req, res) => {
|
||||
res.status(200).json({ status: "OK" });
|
||||
});
|
||||
|
||||
router.ws("/collaboration", (ws, req) => {
|
||||
try {
|
||||
HocusPocusServer.handleConnection(ws, req);
|
||||
} catch (err) {
|
||||
manualLogger.error("WebSocket connection error:", err);
|
||||
ws.close();
|
||||
}
|
||||
});
|
||||
|
||||
private setupCors() {
|
||||
const origins = this.CORS_ALLOWED_ORIGINS.split(",").map((origin) => origin.trim());
|
||||
this.app.use(
|
||||
cors({
|
||||
origin: origins,
|
||||
credentials: true,
|
||||
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||
allowedHeaders: ["Content-Type", "Authorization", "x-api-key"],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private setupControllers() {
|
||||
const router = express.Router();
|
||||
|
||||
REST_CONTROLLERS.forEach((controller: any) => {
|
||||
registerController(router, controller, [this.hocuspocusServer]);
|
||||
});
|
||||
|
||||
WEBSOCKET_CONTROLLERS.forEach((controller: any) => {
|
||||
registerWebSocketController(router, controller, [this.hocuspocusServer]);
|
||||
});
|
||||
|
||||
this.app.use(this.BASE_PATH, router);
|
||||
}
|
||||
|
||||
start() {
|
||||
this.httpServer = this.app.listen(this.PORT, () => {
|
||||
console.log(`Plane Live server has started at port ${this.PORT}`);
|
||||
});
|
||||
|
||||
// Setup graceful shutdown
|
||||
process.on("SIGTERM", () => this.shutdown("Received SIGTERM"));
|
||||
process.on("SIGINT", () => this.shutdown("Received SIGINT"));
|
||||
|
||||
process.on("uncaughtException", (error) => {
|
||||
logger.error("Uncaught exception:", error);
|
||||
});
|
||||
|
||||
// Handle unhandled promise rejections - create AppError but DON'T terminate
|
||||
process.on("unhandledRejection", (error) => {
|
||||
logger.error("Unhandled rejection:", error);
|
||||
});
|
||||
}
|
||||
|
||||
private async shutdown(error: string): Promise<void> {
|
||||
logger.info(`Initiating graceful shutdown: ${error}`);
|
||||
|
||||
if (!this.httpServer) {
|
||||
logger.info("No HTTP server to close");
|
||||
router.post("/convert-document", (req, res) => {
|
||||
const { description_html, variant } = req.body as TConvertDocumentRequestBody;
|
||||
try {
|
||||
if (description_html === undefined || variant === undefined) {
|
||||
res.status(400).send({
|
||||
message: "Missing required fields",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Close all existing connections
|
||||
this.httpServer.closeAllConnections?.();
|
||||
|
||||
// Close the server
|
||||
return new Promise<void>((resolve) => {
|
||||
this.httpServer.close((error: Error | undefined) => {
|
||||
if (error) {
|
||||
logger.error("Error closing HTTP server:", error);
|
||||
} else {
|
||||
logger.info("HTTP server closed successfully");
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
const { description, description_binary } = convertHTMLDocumentToAllFormats({
|
||||
document_html: description_html,
|
||||
variant,
|
||||
});
|
||||
res.status(200).json({
|
||||
description,
|
||||
description_binary,
|
||||
});
|
||||
} catch (error) {
|
||||
manualLogger.error("Error in /convert-document endpoint:", error);
|
||||
res.status(500).send({
|
||||
message: `Internal server error. ${error}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.use(process.env.LIVE_BASE_PATH || "/live", router);
|
||||
|
||||
app.use((_req, res) => {
|
||||
res.status(404).send("Not Found");
|
||||
});
|
||||
|
||||
app.use(errorHandler);
|
||||
|
||||
const liveServer = app.listen(app.get("port"), () => {
|
||||
manualLogger.info(`Plane Live server has started at port ${app.get("port")}`);
|
||||
});
|
||||
|
||||
const gracefulShutdown = async () => {
|
||||
manualLogger.info("Starting graceful shutdown...");
|
||||
|
||||
try {
|
||||
// Close the HocusPocus server WebSocket connections
|
||||
await HocusPocusServer.destroy();
|
||||
manualLogger.info("HocusPocus server WebSocket connections closed gracefully.");
|
||||
|
||||
// Close the Express server
|
||||
liveServer.close(() => {
|
||||
manualLogger.info("Express server closed gracefully.");
|
||||
process.exit(1);
|
||||
});
|
||||
} catch (err) {
|
||||
manualLogger.error("Error during shutdown:", err);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Forcefully shut down after 10 seconds if not closed
|
||||
setTimeout(() => {
|
||||
manualLogger.error("Forcing shutdown...");
|
||||
process.exit(1);
|
||||
}, 10000);
|
||||
};
|
||||
|
||||
// Graceful shutdown on unhandled rejection
|
||||
process.on("unhandledRejection", (err: any) => {
|
||||
manualLogger.info("Unhandled Rejection: ", err);
|
||||
manualLogger.info(`UNHANDLED REJECTION! 💥 Shutting down...`);
|
||||
gracefulShutdown();
|
||||
});
|
||||
|
||||
// Graceful shutdown on uncaught exception
|
||||
process.on("uncaughtException", (err: any) => {
|
||||
manualLogger.info("Uncaught Exception: ", err);
|
||||
manualLogger.info(`UNCAUGHT EXCEPTION! 💥 Shutting down...`);
|
||||
gracefulShutdown();
|
||||
});
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
// types
|
||||
import { TPage } from "@plane/types";
|
||||
// services
|
||||
import { API_BASE_URL, APIService } from "@/services/api.service";
|
||||
|
||||
export class PageService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async fetchDetails(workspaceSlug: string, projectId: string, pageId: string, cookie: string): Promise<TPage> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/`, {
|
||||
headers: {
|
||||
Cookie: cookie,
|
||||
},
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async fetchDescriptionBinary(workspaceSlug: string, projectId: string, pageId: string, cookie: string): Promise<any> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/description/`, {
|
||||
headers: {
|
||||
"Content-Type": "application/octet-stream",
|
||||
Cookie: cookie,
|
||||
},
|
||||
responseType: "arraybuffer",
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async updateDescription(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
pageId: string,
|
||||
data: {
|
||||
description_binary: string;
|
||||
description_html: string;
|
||||
description: object;
|
||||
},
|
||||
cookie: string
|
||||
): Promise<any> {
|
||||
return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/description/`, data, {
|
||||
headers: {
|
||||
Cookie: cookie,
|
||||
},
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import Server from "./server";
|
||||
import { env } from "./env";
|
||||
import { logger } from "@plane/logger";
|
||||
|
||||
// Log server startup details
|
||||
logger.info(`Starting Plane Live server in ${env.NODE_ENV} environment`);
|
||||
|
||||
// Initialize and start the server
|
||||
const server = new Server();
|
||||
server.start();
|
||||
+2
-4
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"extends": "@plane/typescript-config/base.json",
|
||||
"compilerOptions": {
|
||||
"module": "ES2015",
|
||||
"moduleResolution": "Bundler",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"lib": ["ES2015"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": ".",
|
||||
@@ -16,8 +16,6 @@
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"inlineSources": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"sourceRoot": "/"
|
||||
},
|
||||
"include": ["src/**/*.ts", "tsup.config.ts"],
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user