mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
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:
parent
2320b33189
commit
547a76ae55
@ -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"
|
@ -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
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -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 (
|
||||||
|
@ -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=
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
# Python imports
|
# Python imports
|
||||||
from urllib.parse import urlparse
|
|
||||||
|
|
||||||
import zoneinfo
|
import zoneinfo
|
||||||
|
|
||||||
# Django imports
|
# Django imports
|
||||||
|
@ -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(),
|
||||||
|
@ -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}"
|
||||||
|
@ -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,
|
||||||
|
)
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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 (
|
||||||
|
202
apiserver/plane/authentication/views/app/password_management.py
Normal file
202
apiserver/plane/authentication/views/app/password_management.py
Normal 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)
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
@ -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"
|
||||||
)
|
)
|
||||||
|
@ -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"
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
3
packages/types/src/instance/base.d.ts
vendored
3
packages/types/src/instance/base.d.ts
vendored
@ -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;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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"
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
11
turbo.json
11
turbo.json
@ -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": {
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
|
@ -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"
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -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;
|
||||||
|
@ -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 : "";
|
|
||||||
|
@ -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;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user