fix: admin and space redirections (#4419)

* dev: add admin and space base url

* fix: formatting

* dev: add app,space and admin base url to the api env

* fix: updated app base urls redirection

* dev: add change password endpoint

* dev: add none as default for base url

* dev: space password management endpoints

* fix: docker env update

* fix: docker and env settings

* fix: docker changes

* fix: next config update

---------

Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
Co-authored-by: guru_sainath <gurusainath007@gmail.com>
This commit is contained in:
sriram veeraghanta 2024-05-10 02:32:42 +05:30 committed by GitHub
parent 2320b33189
commit 547a76ae55
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 746 additions and 367 deletions

View File

@ -1,2 +1,5 @@
NEXT_PUBLIC_APP_URL= NEXT_PUBLIC_API_BASE_URL=""
NEXT_PUBLIC_API_BASE_URL= NEXT_PUBLIC_ADMIN_BASE_URL=""
NEXT_PUBLIC_SPACE_BASE_URL=""
NEXT_PUBLIC_WEB_BASE_URL=""
NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"

View File

@ -1,3 +1,6 @@
# *****************************************************************************
# STAGE 1: Build the project
# *****************************************************************************
FROM node:18-alpine AS builder FROM node:18-alpine AS builder
RUN apk add --no-cache libc6-compat RUN apk add --no-cache libc6-compat
WORKDIR /app WORKDIR /app
@ -7,6 +10,9 @@ COPY . .
RUN turbo prune --scope=admin --docker RUN turbo prune --scope=admin --docker
# *****************************************************************************
# STAGE 2: Install dependencies & build the project
# *****************************************************************************
FROM node:18-alpine AS installer FROM node:18-alpine AS installer
RUN apk add --no-cache libc6-compat RUN apk add --no-cache libc6-compat
@ -21,13 +27,25 @@ COPY --from=builder /app/out/full/ .
COPY turbo.json turbo.json COPY turbo.json turbo.json
ARG NEXT_PUBLIC_API_BASE_URL="" ARG NEXT_PUBLIC_API_BASE_URL=""
ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
ARG NEXT_PUBLIC_WEB_BASE_URL=""
ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
ARG NEXT_PUBLIC_SPACE_BASE_URL=""
ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL
ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH
ENV NEXT_TELEMETRY_DISABLED 1
ENV TURBO_TELEMETRY_DISABLED 1
RUN yarn turbo run build --filter=admin RUN yarn turbo run build --filter=admin
# *****************************************************************************
# STAGE 3: Copy the project and start it
# *****************************************************************************
FROM node:18-alpine AS runner FROM node:18-alpine AS runner
WORKDIR /app WORKDIR /app
@ -38,11 +56,16 @@ COPY --from=installer /app/admin/.next/standalone ./
COPY --from=installer /app/admin/.next/static ./admin/.next/static COPY --from=installer /app/admin/.next/static ./admin/.next/static
COPY --from=installer /app/admin/public ./admin/public COPY --from=installer /app/admin/public ./admin/public
ARG NEXT_PUBLIC_API_BASE_URL="" ARG NEXT_PUBLIC_API_BASE_URL=""
ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
ARG NEXT_PUBLIC_WEB_BASE_URL=""
ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
ARG NEXT_PUBLIC_SPACE_BASE_URL=""
ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL
ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH
ENV NEXT_TELEMETRY_DISABLED 1 ENV NEXT_TELEMETRY_DISABLED 1

View File

@ -1,12 +1,13 @@
"use client"; "use client";
import { FC, useState, useRef } from "react"; import { FC, useState, useRef } from "react";
import { Transition } from "@headlessui/react";
import Link from "next/link"; import Link from "next/link";
import { observer } from "mobx-react-lite";
import { Transition } from "@headlessui/react";
import { ExternalLink, FileText, HelpCircle, MoveLeft } from "lucide-react"; import { ExternalLink, FileText, HelpCircle, MoveLeft } from "lucide-react";
import { DiscordIcon, GithubIcon, Tooltip } from "@plane/ui"; import { DiscordIcon, GithubIcon, Tooltip } from "@plane/ui";
// hooks // hooks
import { useTheme } from "@/hooks"; import { useInstance, useTheme } from "@/hooks";
// assets // assets
import packageJson from "package.json"; import packageJson from "package.json";
@ -28,7 +29,9 @@ const helpOptions = [
}, },
]; ];
export const HelpSection: FC = () => { export const HelpSection: FC = observer(() => {
// hooks
const { instance } = useInstance();
// states // states
const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false); const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false);
// store // store
@ -36,7 +39,7 @@ export const HelpSection: FC = () => {
// refs // refs
const helpOptionsRef = useRef<HTMLDivElement | null>(null); const helpOptionsRef = useRef<HTMLDivElement | null>(null);
const redirectionLink = `${process.env.NEXT_PUBLIC_APP_URL ? `${process.env.NEXT_PUBLIC_APP_URL}/create-workspace` : `/god-mode/`}`; const redirectionLink = `${instance?.config?.app_base_url ? `${instance?.config?.app_base_url}/create-workspace` : `/god-mode/`}`;
return ( return (
<div <div
@ -128,4 +131,4 @@ export const HelpSection: FC = () => {
</div> </div>
</div> </div>
); );
}; });

View File

@ -8,18 +8,20 @@ import { useTheme as nextUseTheme } from "next-themes";
import { Button, getButtonStyling } from "@plane/ui"; import { Button, getButtonStyling } from "@plane/ui";
// helpers // helpers
import { resolveGeneralTheme } from "helpers/common.helper"; import { resolveGeneralTheme } from "helpers/common.helper";
// hooks
import { useInstance, useTheme } from "@/hooks";
// icons // icons
import TakeoffIconLight from "/public/logos/takeoff-icon-light.svg"; import TakeoffIconLight from "/public/logos/takeoff-icon-light.svg";
import TakeoffIconDark from "/public/logos/takeoff-icon-dark.svg"; import TakeoffIconDark from "/public/logos/takeoff-icon-dark.svg";
import { useTheme } from "@/hooks";
export const NewUserPopup: React.FC = observer(() => { export const NewUserPopup: React.FC = observer(() => {
// hooks // hooks
const { isNewUserPopup, toggleNewUserPopup } = useTheme(); const { isNewUserPopup, toggleNewUserPopup } = useTheme();
const { instance } = useInstance();
// theme // theme
const { resolvedTheme } = nextUseTheme(); const { resolvedTheme } = nextUseTheme();
const redirectionLink = `${process.env.NEXT_PUBLIC_APP_URL ? `${process.env.NEXT_PUBLIC_APP_URL}/create-workspace` : `/god-mode/`}`; const redirectionLink = `${instance?.config?.app_base_url ? `${instance?.config?.app_base_url}/create-workspace` : `/god-mode/`}`;
if (!isNewUserPopup) return <></>; if (!isNewUserPopup) return <></>;
return ( return (

View File

@ -44,3 +44,8 @@ WEB_URL="http://localhost"
# Gunicorn Workers # Gunicorn Workers
GUNICORN_WORKERS=2 GUNICORN_WORKERS=2
# Base URLs
ADMIN_BASE_URL=
SPACE_BASE_URL=
APP_BASE_URL=

View File

@ -1,6 +1,4 @@
# Python imports # Python imports
from urllib.parse import urlparse
import zoneinfo import zoneinfo
# Django imports # Django imports

View File

@ -7,6 +7,7 @@ from .views import (
ForgotPasswordEndpoint, ForgotPasswordEndpoint,
SetUserPasswordEndpoint, SetUserPasswordEndpoint,
ResetPasswordEndpoint, ResetPasswordEndpoint,
ChangePasswordEndpoint,
# App # App
GitHubCallbackEndpoint, GitHubCallbackEndpoint,
GitHubOauthInitiateEndpoint, GitHubOauthInitiateEndpoint,
@ -18,6 +19,8 @@ from .views import (
SignInAuthEndpoint, SignInAuthEndpoint,
SignOutAuthEndpoint, SignOutAuthEndpoint,
SignUpAuthEndpoint, SignUpAuthEndpoint,
ForgotPasswordSpaceEndpoint,
ResetPasswordSpaceEndpoint,
# Space # Space
EmailCheckEndpoint, EmailCheckEndpoint,
GitHubCallbackSpaceEndpoint, GitHubCallbackSpaceEndpoint,
@ -176,6 +179,21 @@ urlpatterns = [
ResetPasswordEndpoint.as_view(), ResetPasswordEndpoint.as_view(),
name="forgot-password", name="forgot-password",
), ),
path(
"spaces/forgot-password/",
ForgotPasswordSpaceEndpoint.as_view(),
name="forgot-password",
),
path(
"spaces/reset-password/<uidb64>/<token>/",
ResetPasswordSpaceEndpoint.as_view(),
name="forgot-password",
),
path(
"change-password/",
ChangePasswordEndpoint.as_view(),
name="forgot-password",
),
path( path(
"set-password/", "set-password/",
SetUserPasswordEndpoint.as_view(), SetUserPasswordEndpoint.as_view(),

View File

@ -1,8 +1,19 @@
# Python imports
from urllib.parse import urlsplit from urllib.parse import urlsplit
# Django imports
from django.conf import settings
def base_host(request):
def base_host(request, is_admin=False, is_space=False):
"""Utility function to return host / origin from the request""" """Utility function to return host / origin from the request"""
if is_admin and settings.ADMIN_BASE_URL:
return settings.ADMIN_BASE_URL
if is_space and settings.SPACE_BASE_URL:
return settings.SPACE_BASE_URL
return ( return (
request.META.get("HTTP_ORIGIN") request.META.get("HTTP_ORIGIN")
or f"{urlsplit(request.META.get('HTTP_REFERER')).scheme}://{urlsplit(request.META.get('HTTP_REFERER')).netloc}" or f"{urlsplit(request.META.get('HTTP_REFERER')).scheme}://{urlsplit(request.META.get('HTTP_REFERER')).netloc}"

View File

@ -1,8 +1,6 @@
from .common import ( from .common import (
ChangePasswordEndpoint, ChangePasswordEndpoint,
CSRFTokenEndpoint, CSRFTokenEndpoint,
ForgotPasswordEndpoint,
ResetPasswordEndpoint,
SetUserPasswordEndpoint, SetUserPasswordEndpoint,
) )
@ -50,3 +48,12 @@ from .space.magic import (
from .space.signout import SignOutAuthSpaceEndpoint from .space.signout import SignOutAuthSpaceEndpoint
from .space.check import EmailCheckEndpoint from .space.check import EmailCheckEndpoint
from .space.password_management import (
ForgotPasswordSpaceEndpoint,
ResetPasswordSpaceEndpoint,
)
from .app.password_management import (
ForgotPasswordEndpoint,
ResetPasswordEndpoint,
)

View File

@ -2,7 +2,6 @@ import uuid
from urllib.parse import urlencode, urljoin from urllib.parse import urlencode, urljoin
# Django import # Django import
from django.core.exceptions import ImproperlyConfigured
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.views import View from django.views import View

View File

@ -3,18 +3,17 @@ import uuid
from urllib.parse import urlencode, urljoin from urllib.parse import urlencode, urljoin
# Django import # Django import
from django.core.exceptions import ImproperlyConfigured
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.views import View from django.views import View
# Module imports
from plane.authentication.provider.oauth.google import GoogleOAuthProvider from plane.authentication.provider.oauth.google import GoogleOAuthProvider
from plane.authentication.utils.login import user_login from plane.authentication.utils.login import user_login
from plane.authentication.utils.redirection_path import get_redirection_path from plane.authentication.utils.redirection_path import get_redirection_path
from plane.authentication.utils.workspace_project_join import ( from plane.authentication.utils.workspace_project_join import (
process_workspace_project_invitations, process_workspace_project_invitations,
) )
# Module imports
from plane.license.models import Instance from plane.license.models import Instance
from plane.authentication.utils.host import base_host from plane.authentication.utils.host import base_host
from plane.authentication.adapter.error import ( from plane.authentication.adapter.error import (

View File

@ -0,0 +1,202 @@
# Python imports
import os
from urllib.parse import urlencode, urljoin
# Third party imports
from rest_framework import status
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.views import APIView
from zxcvbn import zxcvbn
# Django imports
from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.core.exceptions import ValidationError
from django.core.validators import validate_email
from django.http import HttpResponseRedirect
from django.utils.encoding import (
DjangoUnicodeDecodeError,
smart_bytes,
smart_str,
)
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
from django.views import View
# Module imports
from plane.bgtasks.forgot_password_task import forgot_password
from plane.license.models import Instance
from plane.db.models import User
from plane.license.utils.instance_value import get_configuration_value
from plane.authentication.utils.host import base_host
from plane.authentication.adapter.error import (
AuthenticationException,
AUTHENTICATION_ERROR_CODES,
)
def generate_password_token(user):
uidb64 = urlsafe_base64_encode(smart_bytes(user.id))
token = PasswordResetTokenGenerator().make_token(user)
return uidb64, token
class ForgotPasswordEndpoint(APIView):
permission_classes = [
AllowAny,
]
def post(self, request):
email = request.data.get("email")
# Check instance configuration
instance = Instance.objects.first()
if instance is None or not instance.is_setup_done:
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES[
"INSTANCE_NOT_CONFIGURED"
],
error_message="INSTANCE_NOT_CONFIGURED",
)
return Response(
exc.get_error_dict(),
status=status.HTTP_400_BAD_REQUEST,
)
(EMAIL_HOST, EMAIL_HOST_USER, EMAIL_HOST_PASSWORD) = (
get_configuration_value(
[
{
"key": "EMAIL_HOST",
"default": os.environ.get("EMAIL_HOST"),
},
{
"key": "EMAIL_HOST_USER",
"default": os.environ.get("EMAIL_HOST_USER"),
},
{
"key": "EMAIL_HOST_PASSWORD",
"default": os.environ.get("EMAIL_HOST_PASSWORD"),
},
]
)
)
if not (EMAIL_HOST):
exc = AuthenticationException(
error_message="SMTP_NOT_CONFIGURED",
error_code=AUTHENTICATION_ERROR_CODES["SMTP_NOT_CONFIGURED"],
)
return Response(
exc.get_error_dict(),
status=status.HTTP_400_BAD_REQUEST,
)
try:
validate_email(email)
except ValidationError:
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["INVALID_EMAIL"],
error_message="INVALID_EMAIL",
)
return Response(
exc.get_error_dict(),
status=status.HTTP_400_BAD_REQUEST,
)
# Get the user
user = User.objects.filter(email=email).first()
if user:
# Get the reset token for user
uidb64, token = generate_password_token(user=user)
current_site = request.META.get("HTTP_ORIGIN")
# send the forgot password email
forgot_password.delay(
user.first_name, user.email, uidb64, token, current_site
)
return Response(
{"message": "Check your email to reset your password"},
status=status.HTTP_200_OK,
)
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["USER_DOES_NOT_EXIST"],
error_message="USER_DOES_NOT_EXIST",
)
return Response(
exc.get_error_dict(),
status=status.HTTP_400_BAD_REQUEST,
)
class ResetPasswordEndpoint(View):
def post(self, request, uidb64, token):
try:
# Decode the id from the uidb64
id = smart_str(urlsafe_base64_decode(uidb64))
user = User.objects.get(id=id)
# check if the token is valid for the user
if not PasswordResetTokenGenerator().check_token(user, token):
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES[
"INVALID_PASSWORD_TOKEN"
],
error_message="INVALID_PASSWORD_TOKEN",
)
params = exc.get_error_dict()
url = urljoin(
base_host(request=request),
"accounts/reset-password?" + urlencode(params),
)
return HttpResponseRedirect(url)
password = request.POST.get("password", False)
if not password:
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["INVALID_PASSWORD"],
error_message="INVALID_PASSWORD",
)
url = urljoin(
base_host(request=request),
"?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
# Check the password complexity
results = zxcvbn(password)
if results["score"] < 3:
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["INVALID_PASSWORD"],
error_message="INVALID_PASSWORD",
)
url = urljoin(
base_host(request=request),
"accounts/reset-password?"
+ urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
# set_password also hashes the password that the user will get
user.set_password(password)
user.is_password_autoset = False
user.save()
url = urljoin(
base_host(request=request),
"accounts/sign-in?" + urlencode({"success": True}),
)
return HttpResponseRedirect(url)
except DjangoUnicodeDecodeError:
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES[
"EXPIRED_PASSWORD_TOKEN"
],
error_message="EXPIRED_PASSWORD_TOKEN",
)
url = urljoin(
base_host(request=request),
"accounts/reset-password?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)

View File

@ -1,21 +1,3 @@
# Python imports
import os
from urllib.parse import urlencode, urljoin
# Django imports
from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.core.exceptions import ValidationError
from django.core.validators import validate_email
from django.http import HttpResponseRedirect
from django.middleware.csrf import get_token
from django.utils.encoding import (
DjangoUnicodeDecodeError,
smart_bytes,
smart_str,
)
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
from django.views import View
# Third party imports # Third party imports
from rest_framework import status from rest_framework import status
from rest_framework.permissions import AllowAny from rest_framework.permissions import AllowAny
@ -29,15 +11,12 @@ from plane.app.serializers import (
UserSerializer, UserSerializer,
) )
from plane.authentication.utils.login import user_login from plane.authentication.utils.login import user_login
from plane.bgtasks.forgot_password_task import forgot_password
from plane.db.models import User from plane.db.models import User
from plane.license.models import Instance
from plane.license.utils.instance_value import get_configuration_value
from plane.authentication.utils.host import base_host
from plane.authentication.adapter.error import ( from plane.authentication.adapter.error import (
AuthenticationException, AuthenticationException,
AUTHENTICATION_ERROR_CODES, AUTHENTICATION_ERROR_CODES,
) )
from django.middleware.csrf import get_token
class CSRFTokenEndpoint(APIView): class CSRFTokenEndpoint(APIView):
@ -55,174 +34,6 @@ class CSRFTokenEndpoint(APIView):
) )
def generate_password_token(user):
uidb64 = urlsafe_base64_encode(smart_bytes(user.id))
token = PasswordResetTokenGenerator().make_token(user)
return uidb64, token
class ForgotPasswordEndpoint(APIView):
permission_classes = [
AllowAny,
]
def post(self, request):
email = request.data.get("email")
# Check instance configuration
instance = Instance.objects.first()
if instance is None or not instance.is_setup_done:
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES[
"INSTANCE_NOT_CONFIGURED"
],
error_message="INSTANCE_NOT_CONFIGURED",
)
return Response(
exc.get_error_dict(),
status=status.HTTP_400_BAD_REQUEST,
)
(EMAIL_HOST, EMAIL_HOST_USER, EMAIL_HOST_PASSWORD) = (
get_configuration_value(
[
{
"key": "EMAIL_HOST",
"default": os.environ.get("EMAIL_HOST"),
},
{
"key": "EMAIL_HOST_USER",
"default": os.environ.get("EMAIL_HOST_USER"),
},
{
"key": "EMAIL_HOST_PASSWORD",
"default": os.environ.get("EMAIL_HOST_PASSWORD"),
},
]
)
)
if not (EMAIL_HOST):
exc = AuthenticationException(
error_message="SMTP_NOT_CONFIGURED",
error_code=AUTHENTICATION_ERROR_CODES["SMTP_NOT_CONFIGURED"],
)
return Response(
exc.get_error_dict(),
status=status.HTTP_400_BAD_REQUEST,
)
try:
validate_email(email)
except ValidationError:
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["INVALID_EMAIL"],
error_message="INVALID_EMAIL",
)
return Response(
exc.get_error_dict(),
status=status.HTTP_400_BAD_REQUEST,
)
# Get the user
user = User.objects.filter(email=email).first()
if user:
# Get the reset token for user
uidb64, token = generate_password_token(user=user)
current_site = request.META.get("HTTP_ORIGIN")
# send the forgot password email
forgot_password.delay(
user.first_name, user.email, uidb64, token, current_site
)
return Response(
{"message": "Check your email to reset your password"},
status=status.HTTP_200_OK,
)
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["USER_DOES_NOT_EXIST"],
error_message="USER_DOES_NOT_EXIST",
)
return Response(
exc.get_error_dict(),
status=status.HTTP_400_BAD_REQUEST,
)
class ResetPasswordEndpoint(View):
def post(self, request, uidb64, token):
try:
# Decode the id from the uidb64
id = smart_str(urlsafe_base64_decode(uidb64))
user = User.objects.get(id=id)
# check if the token is valid for the user
if not PasswordResetTokenGenerator().check_token(user, token):
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES[
"INVALID_PASSWORD_TOKEN"
],
error_message="INVALID_PASSWORD_TOKEN",
)
params = exc.get_error_dict()
url = urljoin(
base_host(request=request),
"accounts/reset-password?" + urlencode(params),
)
return HttpResponseRedirect(url)
password = request.POST.get("password", False)
if not password:
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["INVALID_PASSWORD"],
error_message="INVALID_PASSWORD",
)
url = urljoin(
base_host(request=request),
"?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
# Check the password complexity
results = zxcvbn(password)
if results["score"] < 3:
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["INVALID_PASSWORD"],
error_message="INVALID_PASSWORD",
)
url = urljoin(
base_host(request=request),
"accounts/reset-password?"
+ urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
# set_password also hashes the password that the user will get
user.set_password(password)
user.is_password_autoset = False
user.save()
url = urljoin(
base_host(request=request),
"accounts/sign-in?" + urlencode({"success": True}),
)
return HttpResponseRedirect(url)
except DjangoUnicodeDecodeError:
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES[
"EXPIRED_PASSWORD_TOKEN"
],
error_message="EXPIRED_PASSWORD_TOKEN",
)
url = urljoin(
base_host(request=request),
"accounts/reset-password?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
class ChangePasswordEndpoint(APIView): class ChangePasswordEndpoint(APIView):
def post(self, request): def post(self, request):
serializer = ChangePasswordSerializer(data=request.data) serializer = ChangePasswordSerializer(data=request.data)

View File

@ -37,7 +37,7 @@ class SignInAuthSpaceEndpoint(View):
if next_path: if next_path:
params["next_path"] = str(next_path) params["next_path"] = str(next_path)
url = urljoin( url = urljoin(
base_host(request=request), base_host(request=request, is_space=True),
"accounts/sign-in?" + urlencode(params), "accounts/sign-in?" + urlencode(params),
) )
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@ -59,7 +59,7 @@ class SignInAuthSpaceEndpoint(View):
if next_path: if next_path:
params["next_path"] = str(next_path) params["next_path"] = str(next_path)
url = urljoin( url = urljoin(
base_host(request=request), base_host(request=request, is_space=True),
"spaces/accounts/sign-in?" + urlencode(params), "spaces/accounts/sign-in?" + urlencode(params),
) )
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@ -78,7 +78,7 @@ class SignInAuthSpaceEndpoint(View):
if next_path: if next_path:
params["next_path"] = str(next_path) params["next_path"] = str(next_path)
url = urljoin( url = urljoin(
base_host(request=request), base_host(request=request, is_space=True),
"spaces/accounts/sign-in?" + urlencode(params), "spaces/accounts/sign-in?" + urlencode(params),
) )
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@ -93,7 +93,7 @@ class SignInAuthSpaceEndpoint(View):
if next_path: if next_path:
params["next_path"] = str(next_path) params["next_path"] = str(next_path)
url = urljoin( url = urljoin(
base_host(request=request), base_host(request=request, is_space=True),
"spaces/accounts/sign-in?" + urlencode(params), "spaces/accounts/sign-in?" + urlencode(params),
) )
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@ -107,7 +107,7 @@ class SignInAuthSpaceEndpoint(View):
user_login(request=request, user=user) user_login(request=request, user=user)
# redirect to next path # redirect to next path
url = urljoin( url = urljoin(
base_host(request=request), base_host(request=request, is_space=True),
str(next_path) if next_path else "/", str(next_path) if next_path else "/",
) )
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@ -116,7 +116,7 @@ class SignInAuthSpaceEndpoint(View):
if next_path: if next_path:
params["next_path"] = str(next_path) params["next_path"] = str(next_path)
url = urljoin( url = urljoin(
base_host(request=request), base_host(request=request, is_space=True),
"spaces/accounts/sign-in?" + urlencode(params), "spaces/accounts/sign-in?" + urlencode(params),
) )
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@ -140,7 +140,7 @@ class SignUpAuthSpaceEndpoint(View):
if next_path: if next_path:
params["next_path"] = str(next_path) params["next_path"] = str(next_path)
url = urljoin( url = urljoin(
base_host(request=request), base_host(request=request, is_space=True),
"spaces?" + urlencode(params), "spaces?" + urlencode(params),
) )
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@ -161,7 +161,7 @@ class SignUpAuthSpaceEndpoint(View):
if next_path: if next_path:
params["next_path"] = str(next_path) params["next_path"] = str(next_path)
url = urljoin( url = urljoin(
base_host(request=request), base_host(request=request, is_space=True),
"spaces?" + urlencode(params), "spaces?" + urlencode(params),
) )
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@ -180,7 +180,7 @@ class SignUpAuthSpaceEndpoint(View):
if next_path: if next_path:
params["next_path"] = str(next_path) params["next_path"] = str(next_path)
url = urljoin( url = urljoin(
base_host(request=request), base_host(request=request, is_space=True),
"spaces?" + urlencode(params), "spaces?" + urlencode(params),
) )
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@ -195,7 +195,7 @@ class SignUpAuthSpaceEndpoint(View):
if next_path: if next_path:
params["next_path"] = str(next_path) params["next_path"] = str(next_path)
url = urljoin( url = urljoin(
base_host(request=request), base_host(request=request, is_space=True),
"spaces?" + urlencode(params), "spaces?" + urlencode(params),
) )
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@ -209,7 +209,7 @@ class SignUpAuthSpaceEndpoint(View):
user_login(request=request, user=user) user_login(request=request, user=user)
# redirect to referer path # redirect to referer path
url = urljoin( url = urljoin(
base_host(request=request), base_host(request=request, is_space=True),
str(next_path) if next_path else "spaces", str(next_path) if next_path else "spaces",
) )
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@ -218,7 +218,7 @@ class SignUpAuthSpaceEndpoint(View):
if next_path: if next_path:
params["next_path"] = str(next_path) params["next_path"] = str(next_path)
url = urljoin( url = urljoin(
base_host(request=request), base_host(request=request, is_space=True),
"spaces?" + urlencode(params), "spaces?" + urlencode(params),
) )
return HttpResponseRedirect(url) return HttpResponseRedirect(url)

View File

@ -3,7 +3,6 @@ import uuid
from urllib.parse import urlencode, urljoin from urllib.parse import urlencode, urljoin
# Django import # Django import
from django.core.exceptions import ImproperlyConfigured
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.views import View from django.views import View
@ -22,7 +21,7 @@ class GitHubOauthInitiateSpaceEndpoint(View):
def get(self, request): def get(self, request):
# Get host and next path # Get host and next path
request.session["host"] = base_host(request=request) request.session["host"] = base_host(request=request, is_space=True)
next_path = request.GET.get("next_path") next_path = request.GET.get("next_path")
if next_path: if next_path:
request.session["next_path"] = str(next_path) request.session["next_path"] = str(next_path)
@ -40,7 +39,7 @@ class GitHubOauthInitiateSpaceEndpoint(View):
if next_path: if next_path:
params["next_path"] = str(next_path) params["next_path"] = str(next_path)
url = urljoin( url = urljoin(
base_host(request=request), base_host(request=request, is_space=True),
"?" + urlencode(params), "?" + urlencode(params),
) )
return HttpResponseRedirect(url) return HttpResponseRedirect(url)

View File

@ -19,7 +19,7 @@ from plane.authentication.adapter.error import (
class GoogleOauthInitiateSpaceEndpoint(View): class GoogleOauthInitiateSpaceEndpoint(View):
def get(self, request): def get(self, request):
request.session["host"] = base_host(request=request) request.session["host"] = base_host(request=request, is_space=True)
next_path = request.GET.get("next_path") next_path = request.GET.get("next_path")
if next_path: if next_path:
request.session["next_path"] = str(next_path) request.session["next_path"] = str(next_path)
@ -37,7 +37,7 @@ class GoogleOauthInitiateSpaceEndpoint(View):
if next_path: if next_path:
params["next_path"] = str(next_path) params["next_path"] = str(next_path)
url = urljoin( url = urljoin(
base_host(request=request), base_host(request=request, is_space=True),
"?" + urlencode(params), "?" + urlencode(params),
) )
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@ -53,7 +53,7 @@ class GoogleOauthInitiateSpaceEndpoint(View):
if next_path: if next_path:
params["next_path"] = str(next_path) params["next_path"] = str(next_path)
url = urljoin( url = urljoin(
base_host(request=request), base_host(request=request, is_space=True),
"?" + urlencode(params), "?" + urlencode(params),
) )
return HttpResponseRedirect(url) return HttpResponseRedirect(url)

View File

@ -2,7 +2,6 @@
from urllib.parse import urlencode, urljoin from urllib.parse import urlencode, urljoin
# Django imports # Django imports
from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.core.validators import validate_email from django.core.validators import validate_email
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.views import View from django.views import View
@ -48,7 +47,7 @@ class MagicGenerateSpaceEndpoint(APIView):
exc.get_error_dict(), status=status.HTTP_400_BAD_REQUEST exc.get_error_dict(), status=status.HTTP_400_BAD_REQUEST
) )
origin = base_host(request=request) origin = base_host(request=request, is_space=True)
email = request.data.get("email", False) email = request.data.get("email", False)
try: try:
# Clean up the email # Clean up the email
@ -86,7 +85,7 @@ class MagicSignInSpaceEndpoint(View):
if next_path: if next_path:
params["next_path"] = str(next_path) params["next_path"] = str(next_path)
url = urljoin( url = urljoin(
base_host(request=request), base_host(request=request, is_space=True),
"spaces/accounts/sign-in?" + urlencode(params), "spaces/accounts/sign-in?" + urlencode(params),
) )
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@ -99,7 +98,7 @@ class MagicSignInSpaceEndpoint(View):
if next_path: if next_path:
params["next_path"] = str(next_path) params["next_path"] = str(next_path)
url = urljoin( url = urljoin(
base_host(request=request), base_host(request=request, is_space=True),
"accounts/sign-in?" + urlencode(params), "accounts/sign-in?" + urlencode(params),
) )
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@ -118,7 +117,7 @@ class MagicSignInSpaceEndpoint(View):
else: else:
# Get the redirection path # Get the redirection path
path = str(next_path) if next_path else "spaces" path = str(next_path) if next_path else "spaces"
url = urljoin(base_host(request=request), path) url = urljoin(base_host(request=request, is_space=True), path)
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
except AuthenticationException as e: except AuthenticationException as e:
@ -126,7 +125,7 @@ class MagicSignInSpaceEndpoint(View):
if next_path: if next_path:
params["next_path"] = str(next_path) params["next_path"] = str(next_path)
url = urljoin( url = urljoin(
base_host(request=request), base_host(request=request, is_space=True),
"spaces/accounts/sign-in?" + urlencode(params), "spaces/accounts/sign-in?" + urlencode(params),
) )
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@ -152,7 +151,7 @@ class MagicSignUpSpaceEndpoint(View):
if next_path: if next_path:
params["next_path"] = str(next_path) params["next_path"] = str(next_path)
url = urljoin( url = urljoin(
base_host(request=request), base_host(request=request, is_space=True),
"spaces/accounts/sign-in?" + urlencode(params), "spaces/accounts/sign-in?" + urlencode(params),
) )
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@ -166,7 +165,7 @@ class MagicSignUpSpaceEndpoint(View):
if next_path: if next_path:
params["next_path"] = str(next_path) params["next_path"] = str(next_path)
url = urljoin( url = urljoin(
base_host(request=request), base_host(request=request, is_space=True),
"?" + urlencode(params), "?" + urlencode(params),
) )
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@ -180,7 +179,7 @@ class MagicSignUpSpaceEndpoint(View):
user_login(request=request, user=user) user_login(request=request, user=user)
# redirect to referer path # redirect to referer path
url = urljoin( url = urljoin(
base_host(request=request), base_host(request=request, is_space=True),
str(next_path) if next_path else "spaces", str(next_path) if next_path else "spaces",
) )
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@ -190,7 +189,7 @@ class MagicSignUpSpaceEndpoint(View):
if next_path: if next_path:
params["next_path"] = str(next_path) params["next_path"] = str(next_path)
url = urljoin( url = urljoin(
base_host(request=request), base_host(request=request, is_space=True),
"spaces/accounts/sign-in?" + urlencode(params), "spaces/accounts/sign-in?" + urlencode(params),
) )
return HttpResponseRedirect(url) return HttpResponseRedirect(url)

View File

@ -0,0 +1,202 @@
# Python imports
import os
from urllib.parse import urlencode, urljoin
# Third party imports
from rest_framework import status
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.views import APIView
from zxcvbn import zxcvbn
# Django imports
from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.core.exceptions import ValidationError
from django.core.validators import validate_email
from django.http import HttpResponseRedirect
from django.utils.encoding import (
DjangoUnicodeDecodeError,
smart_bytes,
smart_str,
)
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
from django.views import View
# Module imports
from plane.bgtasks.forgot_password_task import forgot_password
from plane.license.models import Instance
from plane.db.models import User
from plane.license.utils.instance_value import get_configuration_value
from plane.authentication.utils.host import base_host
from plane.authentication.adapter.error import (
AuthenticationException,
AUTHENTICATION_ERROR_CODES,
)
def generate_password_token(user):
uidb64 = urlsafe_base64_encode(smart_bytes(user.id))
token = PasswordResetTokenGenerator().make_token(user)
return uidb64, token
class ForgotPasswordSpaceEndpoint(APIView):
permission_classes = [
AllowAny,
]
def post(self, request):
email = request.data.get("email")
# Check instance configuration
instance = Instance.objects.first()
if instance is None or not instance.is_setup_done:
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES[
"INSTANCE_NOT_CONFIGURED"
],
error_message="INSTANCE_NOT_CONFIGURED",
)
return Response(
exc.get_error_dict(),
status=status.HTTP_400_BAD_REQUEST,
)
(EMAIL_HOST, EMAIL_HOST_USER, EMAIL_HOST_PASSWORD) = (
get_configuration_value(
[
{
"key": "EMAIL_HOST",
"default": os.environ.get("EMAIL_HOST"),
},
{
"key": "EMAIL_HOST_USER",
"default": os.environ.get("EMAIL_HOST_USER"),
},
{
"key": "EMAIL_HOST_PASSWORD",
"default": os.environ.get("EMAIL_HOST_PASSWORD"),
},
]
)
)
if not (EMAIL_HOST):
exc = AuthenticationException(
error_message="SMTP_NOT_CONFIGURED",
error_code=AUTHENTICATION_ERROR_CODES["SMTP_NOT_CONFIGURED"],
)
return Response(
exc.get_error_dict(),
status=status.HTTP_400_BAD_REQUEST,
)
try:
validate_email(email)
except ValidationError:
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["INVALID_EMAIL"],
error_message="INVALID_EMAIL",
)
return Response(
exc.get_error_dict(),
status=status.HTTP_400_BAD_REQUEST,
)
# Get the user
user = User.objects.filter(email=email).first()
if user:
# Get the reset token for user
uidb64, token = generate_password_token(user=user)
current_site = request.META.get("HTTP_ORIGIN")
# send the forgot password email
forgot_password.delay(
user.first_name, user.email, uidb64, token, current_site
)
return Response(
{"message": "Check your email to reset your password"},
status=status.HTTP_200_OK,
)
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["USER_DOES_NOT_EXIST"],
error_message="USER_DOES_NOT_EXIST",
)
return Response(
exc.get_error_dict(),
status=status.HTTP_400_BAD_REQUEST,
)
class ResetPasswordSpaceEndpoint(View):
def post(self, request, uidb64, token):
try:
# Decode the id from the uidb64
id = smart_str(urlsafe_base64_decode(uidb64))
user = User.objects.get(id=id)
# check if the token is valid for the user
if not PasswordResetTokenGenerator().check_token(user, token):
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES[
"INVALID_PASSWORD_TOKEN"
],
error_message="INVALID_PASSWORD_TOKEN",
)
params = exc.get_error_dict()
url = urljoin(
base_host(request=request, is_space=True),
"accounts/reset-password?" + urlencode(params),
)
return HttpResponseRedirect(url)
password = request.POST.get("password", False)
if not password:
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["INVALID_PASSWORD"],
error_message="INVALID_PASSWORD",
)
url = urljoin(
base_host(request=request, is_space=True),
"?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
# Check the password complexity
results = zxcvbn(password)
if results["score"] < 3:
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["INVALID_PASSWORD"],
error_message="INVALID_PASSWORD",
)
url = urljoin(
base_host(request=request, is_space=True),
"accounts/reset-password?"
+ urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
# set_password also hashes the password that the user will get
user.set_password(password)
user.is_password_autoset = False
user.save()
url = urljoin(
base_host(request=request, is_space=True),
"accounts/sign-in?" + urlencode({"success": True}),
)
return HttpResponseRedirect(url)
except DjangoUnicodeDecodeError:
exc = AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES[
"EXPIRED_PASSWORD_TOKEN"
],
error_message="EXPIRED_PASSWORD_TOKEN",
)
url = urljoin(
base_host(request=request, is_space=True),
"accounts/reset-password?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)

View File

@ -24,11 +24,11 @@ class SignOutAuthSpaceEndpoint(View):
# Log the user out # Log the user out
logout(request) logout(request)
url = urljoin( url = urljoin(
base_host(request=request), base_host(request=request, is_space=True),
"accounts/sign-in?" + urlencode({"success": "true"}), "accounts/sign-in?" + urlencode({"success": "true"}),
) )
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
except Exception: except Exception:
return HttpResponseRedirect( return HttpResponseRedirect(
base_host(request=request), "accounts/sign-in" base_host(request=request, is_space=True), "accounts/sign-in"
) )

View File

@ -106,7 +106,7 @@ class InstanceAdminSignUpEndpoint(View):
error_message="INSTANCE_NOT_CONFIGURED", error_message="INSTANCE_NOT_CONFIGURED",
) )
url = urljoin( url = urljoin(
base_host(request=request), base_host(request=request, is_admin=True),
"god-mode/setup?" + urlencode(exc.get_error_dict()), "god-mode/setup?" + urlencode(exc.get_error_dict()),
) )
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@ -118,7 +118,7 @@ class InstanceAdminSignUpEndpoint(View):
error_message="ADMIN_ALREADY_EXIST", error_message="ADMIN_ALREADY_EXIST",
) )
url = urljoin( url = urljoin(
base_host(request=request), base_host(request=request, is_admin=True),
"god-mode/setup?" + urlencode(exc.get_error_dict()), "god-mode/setup?" + urlencode(exc.get_error_dict()),
) )
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@ -147,7 +147,7 @@ class InstanceAdminSignUpEndpoint(View):
}, },
) )
url = urljoin( url = urljoin(
base_host(request=request), base_host(request=request, is_admin=True),
"god-mode/setup?" + urlencode(exc.get_error_dict()), "god-mode/setup?" + urlencode(exc.get_error_dict()),
) )
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@ -169,7 +169,7 @@ class InstanceAdminSignUpEndpoint(View):
}, },
) )
url = urljoin( url = urljoin(
base_host(request=request), base_host(request=request, is_admin=True),
"god-mode/setup?" + urlencode(exc.get_error_dict()), "god-mode/setup?" + urlencode(exc.get_error_dict()),
) )
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@ -191,7 +191,7 @@ class InstanceAdminSignUpEndpoint(View):
}, },
) )
url = urljoin( url = urljoin(
base_host(request=request), base_host(request=request, is_admin=True),
"god-mode/setup?" + urlencode(exc.get_error_dict()), "god-mode/setup?" + urlencode(exc.get_error_dict()),
) )
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@ -213,7 +213,7 @@ class InstanceAdminSignUpEndpoint(View):
}, },
) )
url = urljoin( url = urljoin(
base_host(request=request), base_host(request=request, is_admin=True),
"god-mode/setup?" + urlencode(exc.get_error_dict()), "god-mode/setup?" + urlencode(exc.get_error_dict()),
) )
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@ -248,7 +248,9 @@ class InstanceAdminSignUpEndpoint(View):
# get tokens for user # get tokens for user
user_login(request=request, user=user) user_login(request=request, user=user)
url = urljoin(base_host(request=request), "god-mode/general") url = urljoin(
base_host(request=request, is_admin=True), "god-mode/general"
)
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@ -269,7 +271,7 @@ class InstanceAdminSignInEndpoint(View):
error_message="INSTANCE_NOT_CONFIGURED", error_message="INSTANCE_NOT_CONFIGURED",
) )
url = urljoin( url = urljoin(
base_host(request=request), base_host(request=request, is_admin=True),
"god-mode/login?" + urlencode(exc.get_error_dict()), "god-mode/login?" + urlencode(exc.get_error_dict()),
) )
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@ -290,7 +292,7 @@ class InstanceAdminSignInEndpoint(View):
}, },
) )
url = urljoin( url = urljoin(
base_host(request=request), base_host(request=request, is_admin=True),
"god-mode/login?" + urlencode(exc.get_error_dict()), "god-mode/login?" + urlencode(exc.get_error_dict()),
) )
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@ -308,7 +310,7 @@ class InstanceAdminSignInEndpoint(View):
}, },
) )
url = urljoin( url = urljoin(
base_host(request=request), base_host(request=request, is_admin=True),
"god-mode/login?" + urlencode(exc.get_error_dict()), "god-mode/login?" + urlencode(exc.get_error_dict()),
) )
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@ -328,7 +330,7 @@ class InstanceAdminSignInEndpoint(View):
}, },
) )
url = urljoin( url = urljoin(
base_host(request=request), base_host(request=request, is_admin=True),
"god-mode/login?" + urlencode(exc.get_error_dict()), "god-mode/login?" + urlencode(exc.get_error_dict()),
) )
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@ -345,7 +347,7 @@ class InstanceAdminSignInEndpoint(View):
}, },
) )
url = urljoin( url = urljoin(
base_host(request=request), base_host(request=request, is_admin=True),
"god-mode/login?" + urlencode(exc.get_error_dict()), "god-mode/login?" + urlencode(exc.get_error_dict()),
) )
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@ -362,7 +364,7 @@ class InstanceAdminSignInEndpoint(View):
}, },
) )
url = urljoin( url = urljoin(
base_host(request=request), base_host(request=request, is_admin=True),
"god-mode/login?" + urlencode(exc.get_error_dict()), "god-mode/login?" + urlencode(exc.get_error_dict()),
) )
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@ -377,7 +379,9 @@ class InstanceAdminSignInEndpoint(View):
# get tokens for user # get tokens for user
user_login(request=request, user=user) user_login(request=request, user=user)
url = urljoin(base_host(request=request), "god-mode/general") url = urljoin(
base_host(request=request, is_admin=True), "god-mode/general"
)
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@ -411,11 +415,11 @@ class InstanceAdminSignOutEndpoint(View):
# Log the user out # Log the user out
logout(request) logout(request)
url = urljoin( url = urljoin(
base_host(request=request), base_host(request=request, is_admin=True),
"accounts/sign-in?" + urlencode({"success": "true"}), "accounts/sign-in?" + urlencode({"success": "true"}),
) )
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
except Exception: except Exception:
return HttpResponseRedirect( return HttpResponseRedirect(
base_host(request=request), "accounts/sign-in" base_host(request=request, is_admin=True), "accounts/sign-in"
) )

View File

@ -2,6 +2,7 @@
import os import os
# Django imports # Django imports
from django.conf import settings
# Third party imports # Third party imports
from rest_framework import status from rest_framework import status
@ -148,9 +149,13 @@ class InstanceEndpoint(BaseAPIView):
) )
# is smtp configured # is smtp configured
data["is_smtp_configured"] = ( data["is_smtp_configured"] = bool(EMAIL_HOST)
bool(EMAIL_HOST)
) # Base URL
data["admin_base_url"] = settings.ADMIN_BASE_URL
data["space_base_url"] = settings.SPACE_BASE_URL
data["app_base_url"] = settings.APP_BASE_URL
instance_data = serializer.data instance_data = serializer.data
instance_data["workspaces_exist"] = Workspace.objects.count() > 1 instance_data["workspaces_exist"] = Workspace.objects.count() > 1

View File

@ -342,3 +342,8 @@ CSRF_COOKIE_SECURE = secure_origins
CSRF_COOKIE_HTTPONLY = True CSRF_COOKIE_HTTPONLY = True
CSRF_TRUSTED_ORIGINS = cors_allowed_origins CSRF_TRUSTED_ORIGINS = cors_allowed_origins
CSRF_COOKIE_DOMAIN = os.environ.get("COOKIE_DOMAIN", None) CSRF_COOKIE_DOMAIN = os.environ.get("COOKIE_DOMAIN", None)
# Base URLs
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("ADMIN_BASE_URL", None)

View File

@ -33,6 +33,10 @@ x-app-env: &app-env
- MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD:-"secret-key"} - MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD:-"secret-key"}
- BUCKET_NAME=${BUCKET_NAME:-uploads} - BUCKET_NAME=${BUCKET_NAME:-uploads}
- FILE_SIZE_LIMIT=${FILE_SIZE_LIMIT:-5242880} - FILE_SIZE_LIMIT=${FILE_SIZE_LIMIT:-5242880}
# Admin and Space URLs
- ADMIN_BASE_URL=${ADMIN_BASE_URL}
- SPACE_BASE_URL=${SPACE_BASE_URL}
- APP_BASE_URL=${APP_BASE_URL}
services: services:
web: web:
@ -40,7 +44,7 @@ services:
image: ${DOCKERHUB_USER:-makeplane}/plane-frontend:${APP_RELEASE:-stable} image: ${DOCKERHUB_USER:-makeplane}/plane-frontend:${APP_RELEASE:-stable}
pull_policy: ${PULL_POLICY:-always} pull_policy: ${PULL_POLICY:-always}
restart: unless-stopped restart: unless-stopped
command: /usr/local/bin/start.sh web/server.js web command: node web/server.js web
deploy: deploy:
replicas: ${WEB_REPLICAS:-1} replicas: ${WEB_REPLICAS:-1}
depends_on: depends_on:
@ -52,7 +56,7 @@ services:
image: ${DOCKERHUB_USER:-makeplane}/plane-space:${APP_RELEASE:-stable} image: ${DOCKERHUB_USER:-makeplane}/plane-space:${APP_RELEASE:-stable}
pull_policy: ${PULL_POLICY:-always} pull_policy: ${PULL_POLICY:-always}
restart: unless-stopped restart: unless-stopped
command: /usr/local/bin/start.sh space/server.js space command: node space/server.js space
deploy: deploy:
replicas: ${SPACE_REPLICAS:-1} replicas: ${SPACE_REPLICAS:-1}
depends_on: depends_on:

View File

@ -7,7 +7,7 @@ services:
args: args:
DOCKER_BUILDKIT: 1 DOCKER_BUILDKIT: 1
restart: always restart: always
command: /usr/local/bin/start.sh web/server.js web command: node web/server.js web
depends_on: depends_on:
- api - api
@ -32,7 +32,7 @@ services:
args: args:
DOCKER_BUILDKIT: 1 DOCKER_BUILDKIT: 1
restart: always restart: always
command: /usr/local/bin/start.sh space/server.js space command: node space/server.js space
depends_on: depends_on:
- api - api
- web - web
@ -134,7 +134,6 @@ services:
MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID} MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID}
MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY} MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY}
# Comment this if you already have a reverse proxy running # Comment this if you already have a reverse proxy running
proxy: proxy:
container_name: proxy container_name: proxy

View File

@ -43,6 +43,9 @@ export interface IInstance {
has_openai_configured: boolean; has_openai_configured: boolean;
file_size_limit: number | undefined; file_size_limit: number | undefined;
is_smtp_configured: boolean; is_smtp_configured: boolean;
app_base_url: string | undefined;
space_base_url: string | undefined;
admin_base_url: string | undefined;
}; };
} }

View File

@ -1,2 +1,3 @@
NEXT_PUBLIC_APP_URL= NEXT_PUBLIC_API_BASE_URL=""
NEXT_PUBLIC_API_BASE_URL= NEXT_PUBLIC_WEB_BASE_URL=""
NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"

View File

@ -1,3 +1,6 @@
# *****************************************************************************
# STAGE 1: Build the project
# *****************************************************************************
FROM node:18-alpine AS builder FROM node:18-alpine AS builder
RUN apk add --no-cache libc6-compat RUN apk add --no-cache libc6-compat
WORKDIR /app WORKDIR /app
@ -7,6 +10,9 @@ COPY . .
RUN turbo prune --scope=space --docker RUN turbo prune --scope=space --docker
# *****************************************************************************
# STAGE 2: Install dependencies & build the project
# *****************************************************************************
FROM node:18-alpine AS installer FROM node:18-alpine AS installer
RUN apk add --no-cache libc6-compat RUN apk add --no-cache libc6-compat
@ -21,13 +27,19 @@ COPY --from=builder /app/out/full/ .
COPY turbo.json turbo.json COPY turbo.json turbo.json
ARG NEXT_PUBLIC_API_BASE_URL="" ARG NEXT_PUBLIC_API_BASE_URL=""
ARG NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
ARG NEXT_PUBLIC_WEB_BASE_URL=""
ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
ARG NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
ENV NEXT_PUBLIC_SPACE_BASE_PATH=$NEXT_PUBLIC_SPACE_BASE_PATH ENV NEXT_PUBLIC_SPACE_BASE_PATH=$NEXT_PUBLIC_SPACE_BASE_PATH
RUN yarn turbo run build --filter=space RUN yarn turbo run build --filter=space
# *****************************************************************************
# STAGE 3: Copy the project and start it
# *****************************************************************************
FROM node:18-alpine AS runner FROM node:18-alpine AS runner
WORKDIR /app WORKDIR /app
@ -40,13 +52,13 @@ COPY --from=installer /app/space/.next ./space/.next
COPY --from=installer /app/space/public ./space/public COPY --from=installer /app/space/public ./space/public
ARG NEXT_PUBLIC_API_BASE_URL="" ARG NEXT_PUBLIC_API_BASE_URL=""
ARG NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
ENV NEXT_PUBLIC_SPACE_BASE_PATH=$NEXT_PUBLIC_SPACE_BASE_PATH
COPY start.sh /usr/local/bin/ ARG NEXT_PUBLIC_WEB_BASE_URL=""
RUN chmod +x /usr/local/bin/start.sh ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
ARG NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
ENV NEXT_PUBLIC_SPACE_BASE_PATH=$NEXT_PUBLIC_SPACE_BASE_PATH
ENV NEXT_TELEMETRY_DISABLED 1 ENV NEXT_TELEMETRY_DISABLED 1
ENV TURBO_TELEMETRY_DISABLED 1 ENV TURBO_TELEMETRY_DISABLED 1

View File

@ -1,8 +1,18 @@
// next imports // next imports
import { observer } from "mobx-react-lite";
import Image from "next/image"; import Image from "next/image";
// hooks
import { useInstance } from "@/hooks/store";
// images
import notFoundImage from "public/404.svg"; import notFoundImage from "public/404.svg";
const Custom404Error = () => ( const Custom404Error = observer(() => {
// hooks
const { instance } = useInstance();
const redirectionUrl = instance?.config?.app_base_url || "/";
return (
<div className="relative flex h-full min-h-screen w-screen items-center justify-center py-5"> <div className="relative flex h-full min-h-screen w-screen items-center justify-center py-5">
<div className="max-w-[700px] space-y-5"> <div className="max-w-[700px] space-y-5">
<div className="flex flex-col items-center gap-3 text-center"> <div className="flex flex-col items-center gap-3 text-center">
@ -18,7 +28,7 @@ const Custom404Error = () => (
<div className="flex items-center justify-center text-center"> <div className="flex items-center justify-center text-center">
<a <a
href={`https://app.plane.so/`} href={redirectionUrl}
className="cursor-pointer select-none rounded-sm border border-gray-200 bg-gray-50 p-1.5 px-2.5 text-sm font-medium text-gray-700 transition-all hover:scale-105 hover:bg-gray-100 hover:text-gray-800" className="cursor-pointer select-none rounded-sm border border-gray-200 bg-gray-50 p-1.5 px-2.5 text-sm font-medium text-gray-700 transition-all hover:scale-105 hover:bg-gray-100 hover:text-gray-800"
> >
Go to your Workspace Go to your Workspace
@ -26,6 +36,7 @@ const Custom404Error = () => (
</div> </div>
</div> </div>
</div> </div>
); );
});
export default Custom404Error; export default Custom404Error;

View File

@ -1,13 +1,22 @@
// next imports // next imports
import { observer } from "mobx-react-lite";
import Image from "next/image"; import Image from "next/image";
// helpers // helpers
import { EPageTypes } from "@/helpers/authentication.helper"; import { EPageTypes } from "@/helpers/authentication.helper";
// hooks
import { useInstance } from "@/hooks/store";
// wrappers // wrappers
import { AuthWrapper } from "@/lib/wrappers"; import { AuthWrapper } from "@/lib/wrappers";
// images // images
import projectNotPublishedImage from "@/public/project-not-published.svg"; import projectNotPublishedImage from "@/public/project-not-published.svg";
const CustomProjectNotPublishedError = () => ( const CustomProjectNotPublishedError = observer(() => {
// hooks
const { instance } = useInstance();
const redirectionUrl = instance?.config?.app_base_url || "/";
return (
<AuthWrapper pageType={EPageTypes.PUBLIC}> <AuthWrapper pageType={EPageTypes.PUBLIC}>
<div className="relative flex h-full min-h-screen w-screen items-center justify-center py-5"> <div className="relative flex h-full min-h-screen w-screen items-center justify-center py-5">
<div className="max-w-[700px] space-y-5"> <div className="max-w-[700px] space-y-5">
@ -25,7 +34,7 @@ const CustomProjectNotPublishedError = () => (
<div className="flex items-center justify-center text-center"> <div className="flex items-center justify-center text-center">
<a <a
href={`https://app.plane.so/`} href={redirectionUrl}
className="cursor-pointer select-none rounded-sm border border-gray-200 bg-gray-50 p-1.5 px-2.5 text-sm font-medium text-gray-700 transition-all hover:scale-105 hover:bg-gray-100 hover:text-gray-800" className="cursor-pointer select-none rounded-sm border border-gray-200 bg-gray-50 p-1.5 px-2.5 text-sm font-medium text-gray-700 transition-all hover:scale-105 hover:bg-gray-100 hover:text-gray-800"
> >
Go to your Workspace Go to your Workspace
@ -34,6 +43,7 @@ const CustomProjectNotPublishedError = () => (
</div> </div>
</div> </div>
</AuthWrapper> </AuthWrapper>
); );
});
export default CustomProjectNotPublishedError; export default CustomProjectNotPublishedError;

View File

@ -3,9 +3,11 @@
"globalEnv": [ "globalEnv": [
"NODE_ENV", "NODE_ENV",
"NEXT_PUBLIC_API_BASE_URL", "NEXT_PUBLIC_API_BASE_URL",
"NEXT_PUBLIC_APP_URL", "NEXT_PUBLIC_ADMIN_BASE_URL",
"NEXT_PUBLIC_DEPLOY_URL", "NEXT_PUBLIC_ADMIN_BASE_PATH",
"NEXT_PUBLIC_GOD_MODE_URL", "NEXT_PUBLIC_SPACE_BASE_URL",
"NEXT_PUBLIC_SPACE_BASE_PATH",
"NEXT_PUBLIC_WEB_BASE_URL",
"NEXT_PUBLIC_SENTRY_DSN", "NEXT_PUBLIC_SENTRY_DSN",
"NEXT_PUBLIC_SENTRY_ENVIRONMENT", "NEXT_PUBLIC_SENTRY_ENVIRONMENT",
"NEXT_PUBLIC_ENABLE_SENTRY", "NEXT_PUBLIC_ENABLE_SENTRY",
@ -18,8 +20,7 @@
"NEXT_PUBLIC_POSTHOG_KEY", "NEXT_PUBLIC_POSTHOG_KEY",
"NEXT_PUBLIC_POSTHOG_HOST", "NEXT_PUBLIC_POSTHOG_HOST",
"NEXT_PUBLIC_POSTHOG_DEBUG", "NEXT_PUBLIC_POSTHOG_DEBUG",
"SENTRY_AUTH_TOKEN", "SENTRY_AUTH_TOKEN"
"NEXT_PUBLIC_SPACE_BASE_PATH"
], ],
"pipeline": { "pipeline": {
"build": { "build": {

View File

@ -1,2 +1,7 @@
# Public boards deploy URL NEXT_PUBLIC_API_BASE_URL=""
NEXT_PUBLIC_DEPLOY_URL="http://localhost/spaces"
NEXT_PUBLIC_ADMIN_BASE_URL=""
NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
NEXT_PUBLIC_SPACE_BASE_URL=""
NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"

View File

@ -1,6 +1,6 @@
# ****************************************** # *****************************************************************************
# STAGE 1: Build the project # STAGE 1: Build the project
# ****************************************** # *****************************************************************************
FROM node:18-alpine AS builder FROM node:18-alpine AS builder
RUN apk add --no-cache libc6-compat RUN apk add --no-cache libc6-compat
# Set working directory # Set working directory
@ -11,17 +11,14 @@ COPY . .
RUN turbo prune --scope=web --docker RUN turbo prune --scope=web --docker
# *****************************************************************************
# ******************************************
# STAGE 2: Install dependencies & build the project # STAGE 2: Install dependencies & build the project
# ****************************************** # *****************************************************************************
# Add lockfile and package.json's of isolated subworkspace # Add lockfile and package.json's of isolated subworkspace
FROM node:18-alpine AS installer FROM node:18-alpine AS installer
RUN apk add --no-cache libc6-compat RUN apk add --no-cache libc6-compat
WORKDIR /app WORKDIR /app
ARG NEXT_PUBLIC_API_BASE_URL=""
ARG NEXT_PUBLIC_DEPLOY_URL=""
# First install the dependencies (as they change less often) # First install the dependencies (as they change less often)
COPY .gitignore .gitignore COPY .gitignore .gitignore
@ -33,16 +30,29 @@ RUN yarn install --network-timeout 500000
COPY --from=builder /app/out/full/ . COPY --from=builder /app/out/full/ .
COPY turbo.json turbo.json COPY turbo.json turbo.json
ARG NEXT_PUBLIC_API_BASE_URL=""
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
ENV NEXT_PUBLIC_DEPLOY_URL=$NEXT_PUBLIC_DEPLOY_URL
ARG NEXT_PUBLIC_ADMIN_BASE_URL=""
ENV NEXT_PUBLIC_ADMIN_BASE_URL=$NEXT_PUBLIC_ADMIN_BASE_URL
ARG NEXT_PUBLIC_ADMIN_BASE_PATH=""
ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH
ARG NEXT_PUBLIC_SPACE_BASE_URL=""
ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL
ARG NEXT_PUBLIC_SPACE_BASE_PATH=""
ENV NEXT_PUBLIC_SPACE_BASE_PATH=$NEXT_PUBLIC_SPACE_BASE_PATH
ENV NEXT_TELEMETRY_DISABLED 1
ENV TURBO_TELEMETRY_DISABLED 1
RUN yarn turbo run build --filter=web RUN yarn turbo run build --filter=web
# *****************************************************************************
# ******************************************
# STAGE 3: Copy the project and start it # STAGE 3: Copy the project and start it
# ****************************************** # *****************************************************************************
FROM node:18-alpine AS runner FROM node:18-alpine AS runner
WORKDIR /app WORKDIR /app
@ -56,12 +66,19 @@ COPY --from=installer /app/web/.next ./web/.next
COPY --from=installer /app/web/public ./web/public COPY --from=installer /app/web/public ./web/public
ARG NEXT_PUBLIC_API_BASE_URL="" ARG NEXT_PUBLIC_API_BASE_URL=""
ARG NEXT_PUBLIC_DEPLOY_URL=""
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
ENV NEXT_PUBLIC_DEPLOY_URL=$NEXT_PUBLIC_DEPLOY_URL
COPY start.sh /usr/local/bin/ ARG NEXT_PUBLIC_ADMIN_BASE_URL=""
RUN chmod +x /usr/local/bin/start.sh ENV NEXT_PUBLIC_ADMIN_BASE_URL=$NEXT_PUBLIC_ADMIN_BASE_URL
ARG NEXT_PUBLIC_ADMIN_BASE_PATH=""
ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH
ARG NEXT_PUBLIC_SPACE_BASE_URL=""
ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL
ARG NEXT_PUBLIC_SPACE_BASE_PATH=""
ENV NEXT_PUBLIC_SPACE_BASE_PATH=$NEXT_PUBLIC_SPACE_BASE_PATH
ENV NEXT_TELEMETRY_DISABLED 1 ENV NEXT_TELEMETRY_DISABLED 1
ENV TURBO_TELEMETRY_DISABLED 1 ENV TURBO_TELEMETRY_DISABLED 1

View File

@ -16,6 +16,7 @@ import { ProjectLogo } from "@/components/project";
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue"; import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue";
import { EUserProjectRoles } from "@/constants/project"; import { EUserProjectRoles } from "@/constants/project";
// helpers // helpers
import { SPACE_BASE_PATH, SPACE_BASE_URL } from "@/helpers/common.helper";
import { calculateTotalFilters } from "@/helpers/filter.helper"; import { calculateTotalFilters } from "@/helpers/filter.helper";
// hooks // hooks
import { import {
@ -99,7 +100,8 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
[workspaceSlug, projectId, updateFilters] [workspaceSlug, projectId, updateFilters]
); );
const deployUrl = process.env.NEXT_PUBLIC_DEPLOY_URL; const DEPLOY_URL = SPACE_BASE_URL + SPACE_BASE_PATH;
const canUserCreateIssue = const canUserCreateIssue =
currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole);
@ -163,9 +165,9 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
</Tooltip> </Tooltip>
) : null} ) : null}
</div> </div>
{currentProjectDetails?.is_deployed && deployUrl && ( {currentProjectDetails?.is_deployed && DEPLOY_URL && (
<a <a
href={`${deployUrl}/${workspaceSlug}/${currentProjectDetails?.id}`} href={`${DEPLOY_URL}/${workspaceSlug}/${currentProjectDetails?.id}`}
className="group flex items-center gap-1.5 rounded bg-custom-primary-100/10 px-2.5 py-1 text-xs font-medium text-custom-primary-100" className="group flex items-center gap-1.5 rounded bg-custom-primary-100/10 px-2.5 py-1 text-xs font-medium text-custom-primary-100"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"

View File

@ -1,13 +1,20 @@
import { FC } from "react"; import { FC } from "react";
import { observer } from "mobx-react";
import Image from "next/image"; import Image from "next/image";
import { Button } from "@plane/ui"; import { Button } from "@plane/ui";
// helpers
import { ADMIN_BASE_URL, ADMIN_BASE_PATH } from "@/helpers/common.helper";
// hooks
// import { useInstance } from "@/hooks/store";
// images // images
import PlaneTakeOffImage from "@/public/plane-takeoff.png"; import PlaneTakeOffImage from "@/public/plane-takeoff.png";
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png"; import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
export const InstanceNotReady: FC = () => { export const InstanceNotReady: FC = observer(() => {
// hooks
// const { instance } = useInstance();
const planeGodModeUrl = `${process.env.NEXT_PUBLIC_GOD_MODE_URL}/god-mode/setup/?auth_enabled=0`; const GOD_MODE_URL = encodeURI(ADMIN_BASE_URL + ADMIN_BASE_PATH + "setup/?auth_enabled=0");
return ( return (
<div className="relative h-screen max-h-max w-full overflow-hidden overflow-y-auto flex flex-col"> <div className="relative h-screen max-h-max w-full overflow-hidden overflow-y-auto flex flex-col">
@ -30,7 +37,7 @@ export const InstanceNotReady: FC = () => {
</p> </p>
</div> </div>
<div> <div>
<a href={planeGodModeUrl}> <a href={GOD_MODE_URL}>
<Button size="lg" className="w-full"> <Button size="lg" className="w-full">
Get started Get started
</Button> </Button>
@ -41,4 +48,4 @@ export const InstanceNotReady: FC = () => {
</div> </div>
</div> </div>
); );
}; });

View File

@ -10,7 +10,7 @@ import { IProject } from "@plane/types";
// ui // ui
import { Button, Loader, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui"; import { Button, Loader, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui";
// hooks // hooks
import { useProjectPublish } from "@/hooks/store"; import { useInstance, useProjectPublish } from "@/hooks/store";
// store // store
import { IProjectPublishSettings, TProjectPublishViews } from "@/store/project/project-publish.store"; import { IProjectPublishSettings, TProjectPublishViews } from "@/store/project/project-publish.store";
// types // types
@ -54,14 +54,14 @@ const viewOptions: {
export const PublishProjectModal: React.FC<Props> = observer((props) => { export const PublishProjectModal: React.FC<Props> = observer((props) => {
const { isOpen, project, onClose } = props; const { isOpen, project, onClose } = props;
// hooks
const { instance } = useInstance();
// states // states
const [isUnPublishing, setIsUnPublishing] = useState(false); const [isUnPublishing, setIsUnPublishing] = useState(false);
const [isUpdateRequired, setIsUpdateRequired] = useState(false); const [isUpdateRequired, setIsUpdateRequired] = useState(false);
let plane_deploy_url = process.env.NEXT_PUBLIC_DEPLOY_URL; const plane_deploy_url = instance?.config?.space_base_url || "";
if (typeof window !== "undefined" && !plane_deploy_url)
plane_deploy_url = window.location.protocol + "//" + window.location.host + "/spaces";
// router // router
const router = useRouter(); const router = useRouter();
const { workspaceSlug } = router.query; const { workspaceSlug } = router.query;

View File

@ -1,6 +1,14 @@
import { clsx, type ClassValue } from "clsx"; import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || "";
export const ADMIN_BASE_URL = process.env.NEXT_PUBLIC_ADMIN_BASE_URL || "";
export const ADMIN_BASE_PATH = process.env.NEXT_PUBLIC_ADMIN_BASE_PATH || "";
export const SPACE_BASE_URL = process.env.NEXT_PUBLIC_SPACE_BASE_URL || "";
export const SPACE_BASE_PATH = process.env.NEXT_PUBLIC_SPACE_BASE_PATH || "";
export const debounce = (func: any, wait: number, immediate: boolean = false) => { export const debounce = (func: any, wait: number, immediate: boolean = false) => {
let timeout: any; let timeout: any;
@ -21,5 +29,3 @@ export const debounce = (func: any, wait: number, immediate: boolean = false) =>
}; };
export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs)); export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ? process.env.NEXT_PUBLIC_API_BASE_URL : "";

View File

@ -31,7 +31,7 @@ const nextConfig = {
unoptimized: true, unoptimized: true,
}, },
async rewrites() { async rewrites() {
return [ const rewrites = [
{ {
source: "/ingest/static/:path*", source: "/ingest/static/:path*",
destination: "https://us-assets.i.posthog.com/static/:path*", destination: "https://us-assets.i.posthog.com/static/:path*",
@ -40,11 +40,17 @@ const nextConfig = {
source: "/ingest/:path*", source: "/ingest/:path*",
destination: "https://us.i.posthog.com/:path*", destination: "https://us.i.posthog.com/:path*",
}, },
{
source: "/god-mode/:path*",
destination: `${process.env.NEXT_PUBLIC_GOD_MODE_URL || ""}/:path*`,
},
]; ];
if (process.env.NEXT_PUBLIC_ADMIN_BASE_URL || process.env.NEXT_PUBLIC_ADMIN_BASE_PATH) {
const ADMIN_BASE_URL = process.env.NEXT_PUBLIC_ADMIN_BASE_URL || ""
const ADMIN_BASE_PATH = process.env.NEXT_PUBLIC_ADMIN_BASE_PATH || ""
const GOD_MODE_BASE_URL = ADMIN_BASE_URL + ADMIN_BASE_PATH
rewrites.push({
source: "/god-mode/:path*",
destination: `${GOD_MODE_BASE_URL}/:path*`,
})
}
return rewrites;
}, },
}; };