base
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user