This commit is contained in:
DCCONSTRUCTIONS
2026-04-18 18:39:25 +03:00
commit 3ba092b60c
4944 changed files with 497564 additions and 0 deletions
@@ -0,0 +1,67 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.
# 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:
"""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
# Admin redirection
if is_admin:
admin_base_path = getattr(settings, "ADMIN_BASE_PATH", None)
if not isinstance(admin_base_path, str):
admin_base_path = "/god-mode/"
if not admin_base_path.startswith("/"):
admin_base_path = "/" + admin_base_path
if not admin_base_path.endswith("/"):
admin_base_path += "/"
if settings.ADMIN_BASE_URL:
return settings.ADMIN_BASE_URL + admin_base_path
else:
return base_origin + admin_base_path
# Space redirection
if is_space:
space_base_path = getattr(settings, "SPACE_BASE_PATH", None)
if not isinstance(space_base_path, str):
space_base_path = "/spaces/"
if not space_base_path.startswith("/"):
space_base_path = "/" + space_base_path
if not space_base_path.endswith("/"):
space_base_path += "/"
if settings.SPACE_BASE_URL:
return settings.SPACE_BASE_URL + space_base_path
else:
return base_origin + space_base_path
# App Redirection
if is_app:
if settings.APP_BASE_URL:
return settings.APP_BASE_URL
else:
return base_origin
return base_origin
def user_ip(request: Request | HttpRequest) -> str:
return get_client_ip(request=request)
@@ -0,0 +1,28 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.
# Django imports
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
def user_login(request, user, is_app=False, is_admin=False, is_space=False):
login(request=request, user=user)
# If is admin cookie set the custom age
if is_admin:
request.session.set_expiry(settings.ADMIN_SESSION_COOKIE_AGE)
device_info = {
"user_agent": request.META.get("HTTP_USER_AGENT", ""),
"ip_address": get_client_ip(request=request),
"domain": base_host(request=request, is_app=is_app, is_admin=is_admin, is_space=is_space),
}
request.session["device_info"] = device_info
request.session.save()
return
@@ -0,0 +1,35 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.
import re
from zxcvbn import zxcvbn
PASSWORD_MIN_LENGTH = 8
PASSWORD_UPPERCASE_REGEX = re.compile(r"[A-Z]")
PASSWORD_LOWERCASE_REGEX = re.compile(r"[a-z]")
PASSWORD_DIGIT_REGEX = re.compile(r"[0-9]")
PASSWORD_SPECIAL_CHAR_REGEX = re.compile(r"""[!@#$%^&*()\-_+=\[\]{}|;:'",.<>?/]""")
def has_required_password_complexity(password: str) -> bool:
if len(password) < PASSWORD_MIN_LENGTH:
return False
return all(
(
PASSWORD_UPPERCASE_REGEX.search(password),
PASSWORD_LOWERCASE_REGEX.search(password),
PASSWORD_DIGIT_REGEX.search(password),
PASSWORD_SPECIAL_CHAR_REGEX.search(password),
)
)
def is_password_policy_valid(password: str) -> bool:
if not has_required_password_complexity(password):
return False
results = zxcvbn(password)
return results["score"] >= 3
@@ -0,0 +1,46 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.
from plane.db.models import Profile, Workspace, WorkspaceMemberInvite
def get_redirection_path(user):
# Handle redirections
profile, _ = Profile.objects.get_or_create(user=user)
# Redirect to onboarding if the user is not onboarded yet
if not profile.is_onboarded:
return "onboarding"
# Redirect to the last workspace if the user has last workspace
if (
profile.last_workspace_id
and Workspace.objects.filter(
pk=profile.last_workspace_id,
workspace_member__member_id=user.id,
workspace_member__is_active=True,
).exists()
):
workspace = Workspace.objects.filter(
pk=profile.last_workspace_id,
workspace_member__member_id=user.id,
workspace_member__is_active=True,
).first()
return f"{workspace.slug}"
fallback_workspace = (
Workspace.objects.filter(workspace_member__member_id=user.id, workspace_member__is_active=True)
.order_by("created_at")
.first()
)
# Redirect to fallback workspace
if fallback_workspace:
return f"{fallback_workspace.slug}"
# Redirect to invitations if the user has unaccepted invitations
if WorkspaceMemberInvite.objects.filter(email=user.email).count():
return "invitations"
# Redirect the user to create workspace
return "create-workspace"
@@ -0,0 +1,9 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.
from .workspace_project_join import process_workspace_project_invitations
def post_user_auth_workflow(user, is_signup, request):
process_workspace_project_invitations(user=user)
@@ -0,0 +1,91 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.
# Django imports
from django.utils import timezone
# Module imports
from plane.db.models import (
ProjectMember,
ProjectMemberInvite,
WorkspaceMember,
WorkspaceMemberInvite,
)
from plane.utils.cache import invalidate_cache_directly
from plane.bgtasks.event_tracking_task import track_event
from plane.utils.analytics_events import USER_JOINED_WORKSPACE
def process_workspace_project_invitations(user):
"""This function takes in User and adds him to all workspace and projects that the user has accepted invited of"""
# Check if user has any accepted invites for workspace and add them to workspace
workspace_member_invites = WorkspaceMemberInvite.objects.filter(email=user.email, accepted=True)
WorkspaceMember.objects.bulk_create(
[
WorkspaceMember(
workspace_id=workspace_member_invite.workspace_id,
member=user,
role=workspace_member_invite.role,
)
for workspace_member_invite in workspace_member_invites
],
ignore_conflicts=True,
)
for workspace_member_invite in workspace_member_invites:
invalidate_cache_directly(
path=f"/api/workspaces/{str(workspace_member_invite.workspace.slug)}/members/",
url_params=False,
user=False,
multiple=True,
)
track_event.delay(
user_id=user.id,
event_name=USER_JOINED_WORKSPACE,
slug=workspace_member_invite.workspace.slug,
event_properties={
"user_id": user.id,
"workspace_id": workspace_member_invite.workspace.id,
"workspace_slug": workspace_member_invite.workspace.slug,
"role": workspace_member_invite.role,
"joined_at": str(timezone.now().isoformat()),
},
)
# Check if user has any project invites
project_member_invites = ProjectMemberInvite.objects.filter(email=user.email, accepted=True)
# Add user to workspace
WorkspaceMember.objects.bulk_create(
[
WorkspaceMember(
workspace_id=project_member_invite.workspace_id,
role=(project_member_invite.role if project_member_invite.role in [5, 15] else 15),
member=user,
created_by_id=project_member_invite.created_by_id,
)
for project_member_invite in project_member_invites
],
ignore_conflicts=True,
)
# Now add the users to project
ProjectMember.objects.bulk_create(
[
ProjectMember(
workspace_id=project_member_invite.workspace_id,
role=(project_member_invite.role if project_member_invite.role in [5, 15] else 15),
member=user,
created_by_id=project_member_invite.created_by_id,
)
for project_member_invite in project_member_invites
],
ignore_conflicts=True,
)
# Delete all the invites
workspace_member_invites.delete()
project_member_invites.delete()