From b8768d72749e965d61ddd710a96348c276647bae Mon Sep 17 00:00:00 2001
From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com>
Date: Thu, 9 May 2024 19:06:39 +0530
Subject: [PATCH 01/37] fix: spreadsheet layout sticky column (#4416)
* fix: spreadsheet layout sticky column
* fix: spreadsheet layout sticky column
---
.../issues/issue-layouts/spreadsheet/issue-row.tsx | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx b/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx
index 00f2b2138..1bd8a8808 100644
--- a/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx
+++ b/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx
@@ -202,14 +202,13 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
return (
<>
-
+ |
handleIssuePeekOverview(issueDetail)}
className={cn(
- "group clickable cursor-pointer sticky left-0 h-11 w-[28rem] flex items-center bg-custom-background-100 text-sm after:absolute border-r-[0.5px] z-10 border-custom-border-200",
+ "group clickable cursor-pointer h-11 w-[28rem] flex items-center bg-custom-background-100 text-sm after:absolute border-r-[0.5px] z-10 border-custom-border-200",
{
"border-b-[0.5px]": !getIsIssuePeeked(issueDetail.id),
"border border-custom-primary-70 hover:border-custom-primary-70": getIsIssuePeeked(issueDetail.id),
From 45bb1153ee562991d9c2afc415e913262c31674d Mon Sep 17 00:00:00 2001
From: sriram veeraghanta
Date: Thu, 9 May 2024 21:05:51 +0530
Subject: [PATCH 02/37] fix: removing deploy with nginx env
---
.github/workflows/feature-deployment.yml | 28 ++++++++++++++----------
admin/Dockerfile.admin | 10 ++++-----
admin/Dockerfile.dev | 7 +++++-
admin/next.config.js | 2 +-
space/Dockerfile.dev | 6 +++--
space/Dockerfile.space | 8 +++----
space/next.config.js | 10 ++++-----
space/pages/onboarding/index.tsx | 2 +-
turbo.json | 4 ++--
9 files changed, 45 insertions(+), 32 deletions(-)
diff --git a/.github/workflows/feature-deployment.yml b/.github/workflows/feature-deployment.yml
index a0a9dc7f1..e848dc36d 100644
--- a/.github/workflows/feature-deployment.yml
+++ b/.github/workflows/feature-deployment.yml
@@ -5,17 +5,17 @@ on:
inputs:
web-build:
required: false
- description: 'Build Web'
+ description: "Build Web"
type: boolean
default: true
space-build:
required: false
- description: 'Build Space'
+ description: "Build Space"
type: boolean
default: false
admin-build:
required: false
- description: 'Build Admin'
+ description: "Build Admin"
type: boolean
default: false
@@ -35,7 +35,7 @@ jobs:
echo "BUILD_SPACE=$BUILD_SPACE"
echo "BUILD_ADMIN=$BUILD_ADMIN"
outputs:
- web-build: ${{ env.BUILD_WEB}}
+ web-build: ${{ env.BUILD_WEB}}
space-build: ${{env.BUILD_SPACE}}
admin-build: ${{env.BUILD_ADMIN}}
@@ -53,7 +53,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
- node-version: '18'
+ node-version: "18"
- name: Install AWS cli
run: |
sudo apt-get update
@@ -79,7 +79,7 @@ jobs:
FILE_EXPIRY=$(date -u -d "+2 days" +"%Y-%m-%dT%H:%M:%SZ")
aws s3 cp $TAR_NAME s3://${{ env.AWS_BUCKET }}/${{github.sha}}/$TAR_NAME --expires $FILE_EXPIRY
-
+
feature-build-space:
if: ${{ needs.setup-feature-build.outputs.space-build == 'true' }}
needs: setup-feature-build
@@ -89,7 +89,7 @@ jobs:
AWS_ACCESS_KEY_ID: ${{ vars.FEATURE_PREVIEW_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.FEATURE_PREVIEW_AWS_SECRET_ACCESS_KEY }}
AWS_BUCKET: ${{ vars.FEATURE_PREVIEW_AWS_BUCKET }}
- NEXT_PUBLIC_DEPLOY_WITH_NGINX: 1
+ NEXT_PUBLIC_SPACE_BASE_PATH: "/spaces"
NEXT_PUBLIC_API_BASE_URL: ${{ vars.FEATURE_PREVIEW_NEXT_PUBLIC_API_BASE_URL }}
outputs:
do-build: ${{ needs.setup-feature-build.outputs.space-build }}
@@ -98,7 +98,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
- node-version: '18'
+ node-version: "18"
- name: Install AWS cli
run: |
sudo apt-get update
@@ -134,7 +134,7 @@ jobs:
AWS_ACCESS_KEY_ID: ${{ vars.FEATURE_PREVIEW_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.FEATURE_PREVIEW_AWS_SECRET_ACCESS_KEY }}
AWS_BUCKET: ${{ vars.FEATURE_PREVIEW_AWS_BUCKET }}
- NEXT_PUBLIC_DEPLOY_WITH_NGINX: 1
+ NEXT_PUBLIC_ADMIN_BASE_PATH: "/god-mode"
NEXT_PUBLIC_API_BASE_URL: ${{ vars.FEATURE_PREVIEW_NEXT_PUBLIC_API_BASE_URL }}
outputs:
do-build: ${{ needs.setup-feature-build.outputs.admin-build }}
@@ -143,7 +143,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
- node-version: '18'
+ node-version: "18"
- name: Install AWS cli
run: |
sudo apt-get update
@@ -172,7 +172,13 @@ jobs:
feature-deploy:
if: ${{ always() && (needs.setup-feature-build.outputs.web-build == 'true' || needs.setup-feature-build.outputs.space-build == 'true' || needs.setup-feature-build.outputs.admin-build == 'true') }}
- needs: [setup-feature-build, feature-build-web, feature-build-space, feature-build-admin]
+ needs:
+ [
+ setup-feature-build,
+ feature-build-web,
+ feature-build-space,
+ feature-build-admin,
+ ]
name: Feature Deploy
runs-on: ubuntu-latest
env:
diff --git a/admin/Dockerfile.admin b/admin/Dockerfile.admin
index 9abc5daef..f752df6d9 100644
--- a/admin/Dockerfile.admin
+++ b/admin/Dockerfile.admin
@@ -21,10 +21,10 @@ COPY --from=builder /app/out/full/ .
COPY turbo.json turbo.json
ARG NEXT_PUBLIC_API_BASE_URL=""
-ARG NEXT_PUBLIC_DEPLOY_WITH_NGINX=1
+ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
-ENV NEXT_PUBLIC_DEPLOY_WITH_NGINX=$NEXT_PUBLIC_DEPLOY_WITH_NGINX
+ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH
RUN yarn turbo run build --filter=admin
@@ -40,10 +40,10 @@ COPY --from=installer /app/admin/public ./admin/public
ARG NEXT_PUBLIC_API_BASE_URL=""
-ARG NEXT_PUBLIC_DEPLOY_WITH_NGINX=1
+ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
-ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
-ENV NEXT_PUBLIC_DEPLOY_WITH_NGINX=$NEXT_PUBLIC_DEPLOY_WITH_NGINX
+ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
+ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH
ENV NEXT_TELEMETRY_DISABLED 1
ENV TURBO_TELEMETRY_DISABLED 1
diff --git a/admin/Dockerfile.dev b/admin/Dockerfile.dev
index 0cbbdc8af..1ed84e78e 100644
--- a/admin/Dockerfile.dev
+++ b/admin/Dockerfile.dev
@@ -3,10 +3,15 @@ RUN apk add --no-cache libc6-compat
# Set working directory
WORKDIR /app
-
COPY . .
+
RUN yarn global add turbo
RUN yarn install
+
+ENV NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
+
EXPOSE 3000
+
VOLUME [ "/app/node_modules", "/app/admin/node_modules" ]
+
CMD ["yarn", "dev", "--filter=admin"]
diff --git a/admin/next.config.js b/admin/next.config.js
index 26ff4b870..07f6664af 100644
--- a/admin/next.config.js
+++ b/admin/next.config.js
@@ -7,7 +7,7 @@ const nextConfig = {
images: {
unoptimized: true,
},
- basePath: "/god-mode",
+ basePath: process.env.NEXT_PUBLIC_ADMIN_BASE_PATH || "",
};
module.exports = nextConfig;
diff --git a/space/Dockerfile.dev b/space/Dockerfile.dev
index 862210c33..213f3fb3c 100644
--- a/space/Dockerfile.dev
+++ b/space/Dockerfile.dev
@@ -3,12 +3,14 @@ RUN apk add --no-cache libc6-compat
# Set working directory
WORKDIR /app
-
COPY . .
+
RUN yarn global add turbo
RUN yarn install
+
EXPOSE 4000
-ENV NEXT_PUBLIC_DEPLOY_WITH_NGINX=1
+
+ENV NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
VOLUME [ "/app/node_modules", "/app/space/node_modules"]
CMD ["yarn","dev", "--filter=space"]
diff --git a/space/Dockerfile.space b/space/Dockerfile.space
index a10690642..095f06722 100644
--- a/space/Dockerfile.space
+++ b/space/Dockerfile.space
@@ -21,10 +21,10 @@ COPY --from=builder /app/out/full/ .
COPY turbo.json turbo.json
ARG NEXT_PUBLIC_API_BASE_URL=""
-ARG NEXT_PUBLIC_DEPLOY_WITH_NGINX=1
+ARG NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
-ENV NEXT_PUBLIC_DEPLOY_WITH_NGINX=$NEXT_PUBLIC_DEPLOY_WITH_NGINX
+ENV NEXT_PUBLIC_SPACE_BASE_PATH=$NEXT_PUBLIC_SPACE_BASE_PATH
RUN yarn turbo run build --filter=space
@@ -40,10 +40,10 @@ COPY --from=installer /app/space/.next ./space/.next
COPY --from=installer /app/space/public ./space/public
ARG NEXT_PUBLIC_API_BASE_URL=""
-ARG NEXT_PUBLIC_DEPLOY_WITH_NGINX=1
+ARG NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
-ENV NEXT_PUBLIC_DEPLOY_WITH_NGINX=$NEXT_PUBLIC_DEPLOY_WITH_NGINX
+ENV NEXT_PUBLIC_SPACE_BASE_PATH=$NEXT_PUBLIC_SPACE_BASE_PATH
COPY start.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/start.sh
diff --git a/space/next.config.js b/space/next.config.js
index c3153b694..eb9dde88a 100644
--- a/space/next.config.js
+++ b/space/next.config.js
@@ -5,17 +5,18 @@ const { withSentryConfig } = require("@sentry/nextjs");
const nextConfig = {
trailingSlash: true,
+ output: "standalone",
+ basePath: process.env.NEXT_PUBLIC_SPACE_BASE_PATH || "",
+ reactStrictMode: false,
+ swcMinify: true,
async headers() {
return [
{
source: "/",
- headers: [{ key: "X-Frame-Options", value: "SAMEORIGIN" }],
+ headers: [{ key: "X-Frame-Options", value: "SAMEORIGIN" }], // clickjacking protection
},
];
},
- basePath: "/spaces",
- reactStrictMode: false,
- swcMinify: true,
images: {
remotePatterns: [
{
@@ -25,7 +26,6 @@ const nextConfig = {
],
unoptimized: true,
},
- output: "standalone",
};
if (parseInt(process.env.NEXT_PUBLIC_ENABLE_SENTRY || "0", 10)) {
diff --git a/space/pages/onboarding/index.tsx b/space/pages/onboarding/index.tsx
index 6ba95126a..8318f0346 100644
--- a/space/pages/onboarding/index.tsx
+++ b/space/pages/onboarding/index.tsx
@@ -17,7 +17,7 @@ import { AuthWrapper } from "@/lib/wrappers";
import ProfileSetupDark from "public/onboarding/profile-setup-dark.svg";
import ProfileSetup from "public/onboarding/profile-setup.svg";
-const imagePrefix = Boolean(parseInt(process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX || "0")) ? "/spaces" : "";
+const imagePrefix = process.env.NEXT_PUBLIC_SPACE_BASE_PATH || "";
const OnBoardingPage = observer(() => {
// router
diff --git a/turbo.json b/turbo.json
index d9a63f30e..211d83048 100644
--- a/turbo.json
+++ b/turbo.json
@@ -15,11 +15,11 @@
"NEXT_PUBLIC_ENABLE_SESSION_RECORDER",
"NEXT_PUBLIC_SESSION_RECORDER_KEY",
"NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS",
- "NEXT_PUBLIC_DEPLOY_WITH_NGINX",
"NEXT_PUBLIC_POSTHOG_KEY",
"NEXT_PUBLIC_POSTHOG_HOST",
"NEXT_PUBLIC_POSTHOG_DEBUG",
- "SENTRY_AUTH_TOKEN"
+ "SENTRY_AUTH_TOKEN",
+ "NEXT_PUBLIC_SPACE_BASE_PATH"
],
"pipeline": {
"build": {
From 547a76ae55f02b4ae10220a9e80811649babacd0 Mon Sep 17 00:00:00 2001
From: sriram veeraghanta
Date: Fri, 10 May 2024 02:32:42 +0530
Subject: [PATCH 03/37] 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
Co-authored-by: guru_sainath
---
admin/.env.example | 7 +-
admin/Dockerfile.admin | 33 ++-
.../components/admin-sidebar/help-section.tsx | 13 +-
admin/components/new-user-popup.tsx | 6 +-
apiserver/.env.example | 5 +
apiserver/plane/api/views/base.py | 2 -
apiserver/plane/authentication/urls.py | 18 ++
apiserver/plane/authentication/utils/host.py | 13 +-
.../plane/authentication/views/__init__.py | 11 +-
.../plane/authentication/views/app/github.py | 1 -
.../plane/authentication/views/app/google.py | 5 +-
.../views/app/password_management.py | 202 ++++++++++++++++++
.../plane/authentication/views/common.py | 191 +----------------
.../plane/authentication/views/space/email.py | 24 +--
.../authentication/views/space/github.py | 5 +-
.../authentication/views/space/google.py | 6 +-
.../plane/authentication/views/space/magic.py | 19 +-
.../views/space/password_management.py | 202 ++++++++++++++++++
.../authentication/views/space/signout.py | 4 +-
apiserver/plane/license/api/views/admin.py | 36 ++--
apiserver/plane/license/api/views/instance.py | 11 +-
apiserver/plane/settings/common.py | 5 +
deploy/selfhost/docker-compose.yml | 12 +-
docker-compose.yml | 5 +-
packages/types/src/instance/base.d.ts | 3 +
space/.env.example | 5 +-
space/Dockerfile.space | 26 ++-
space/pages/404.tsx | 55 +++--
space/pages/project-not-published/index.tsx | 60 +++---
turbo.json | 11 +-
web/.env.example | 9 +-
web/Dockerfile.web | 49 +++--
web/components/headers/project-issues.tsx | 8 +-
web/components/instance/not-ready-view.tsx | 17 +-
.../project/publish-project/modal.tsx | 8 +-
web/helpers/common.helper.ts | 10 +-
web/next.config.js | 16 +-
37 files changed, 746 insertions(+), 367 deletions(-)
create mode 100644 apiserver/plane/authentication/views/app/password_management.py
create mode 100644 apiserver/plane/authentication/views/space/password_management.py
diff --git a/admin/.env.example b/admin/.env.example
index fbd4ad4f9..a86a8b4fb 100644
--- a/admin/.env.example
+++ b/admin/.env.example
@@ -1,2 +1,5 @@
-NEXT_PUBLIC_APP_URL=
-NEXT_PUBLIC_API_BASE_URL=
\ No newline at end of file
+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"
\ No newline at end of file
diff --git a/admin/Dockerfile.admin b/admin/Dockerfile.admin
index f752df6d9..901c39e27 100644
--- a/admin/Dockerfile.admin
+++ b/admin/Dockerfile.admin
@@ -1,3 +1,6 @@
+# *****************************************************************************
+# STAGE 1: Build the project
+# *****************************************************************************
FROM node:18-alpine AS builder
RUN apk add --no-cache libc6-compat
WORKDIR /app
@@ -7,6 +10,9 @@ COPY . .
RUN turbo prune --scope=admin --docker
+# *****************************************************************************
+# STAGE 2: Install dependencies & build the project
+# *****************************************************************************
FROM node:18-alpine AS installer
RUN apk add --no-cache libc6-compat
@@ -21,13 +27,25 @@ COPY --from=builder /app/out/full/ .
COPY turbo.json turbo.json
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
+
+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_TELEMETRY_DISABLED 1
+ENV TURBO_TELEMETRY_DISABLED 1
+
RUN yarn turbo run build --filter=admin
+# *****************************************************************************
+# STAGE 3: Copy the project and start it
+# *****************************************************************************
FROM node:18-alpine AS runner
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/public ./admin/public
-
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
+
+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_TELEMETRY_DISABLED 1
diff --git a/admin/components/admin-sidebar/help-section.tsx b/admin/components/admin-sidebar/help-section.tsx
index c1da25b28..ba8f2cba5 100644
--- a/admin/components/admin-sidebar/help-section.tsx
+++ b/admin/components/admin-sidebar/help-section.tsx
@@ -1,12 +1,13 @@
"use client";
import { FC, useState, useRef } from "react";
-import { Transition } from "@headlessui/react";
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 { DiscordIcon, GithubIcon, Tooltip } from "@plane/ui";
// hooks
-import { useTheme } from "@/hooks";
+import { useInstance, useTheme } from "@/hooks";
// assets
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
const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false);
// store
@@ -36,7 +39,7 @@ export const HelpSection: FC = () => {
// refs
const helpOptionsRef = useRef(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 (
{
);
-};
+});
diff --git a/admin/components/new-user-popup.tsx b/admin/components/new-user-popup.tsx
index fa7c01abf..d17e99d5e 100644
--- a/admin/components/new-user-popup.tsx
+++ b/admin/components/new-user-popup.tsx
@@ -8,18 +8,20 @@ import { useTheme as nextUseTheme } from "next-themes";
import { Button, getButtonStyling } from "@plane/ui";
// helpers
import { resolveGeneralTheme } from "helpers/common.helper";
+// hooks
+import { useInstance, useTheme } from "@/hooks";
// icons
import TakeoffIconLight from "/public/logos/takeoff-icon-light.svg";
import TakeoffIconDark from "/public/logos/takeoff-icon-dark.svg";
-import { useTheme } from "@/hooks";
export const NewUserPopup: React.FC = observer(() => {
// hooks
const { isNewUserPopup, toggleNewUserPopup } = useTheme();
+ const { instance } = useInstance();
// theme
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 <>>;
return (
diff --git a/apiserver/.env.example b/apiserver/.env.example
index 52d8d1c50..e6590f831 100644
--- a/apiserver/.env.example
+++ b/apiserver/.env.example
@@ -44,3 +44,8 @@ WEB_URL="http://localhost"
# Gunicorn Workers
GUNICORN_WORKERS=2
+
+# Base URLs
+ADMIN_BASE_URL=
+SPACE_BASE_URL=
+APP_BASE_URL=
diff --git a/apiserver/plane/api/views/base.py b/apiserver/plane/api/views/base.py
index 1f6bd70af..fee508a30 100644
--- a/apiserver/plane/api/views/base.py
+++ b/apiserver/plane/api/views/base.py
@@ -1,6 +1,4 @@
# Python imports
-from urllib.parse import urlparse
-
import zoneinfo
# Django imports
diff --git a/apiserver/plane/authentication/urls.py b/apiserver/plane/authentication/urls.py
index 451b17e4e..4a6f8c3f4 100644
--- a/apiserver/plane/authentication/urls.py
+++ b/apiserver/plane/authentication/urls.py
@@ -7,6 +7,7 @@ from .views import (
ForgotPasswordEndpoint,
SetUserPasswordEndpoint,
ResetPasswordEndpoint,
+ ChangePasswordEndpoint,
# App
GitHubCallbackEndpoint,
GitHubOauthInitiateEndpoint,
@@ -18,6 +19,8 @@ from .views import (
SignInAuthEndpoint,
SignOutAuthEndpoint,
SignUpAuthEndpoint,
+ ForgotPasswordSpaceEndpoint,
+ ResetPasswordSpaceEndpoint,
# Space
EmailCheckEndpoint,
GitHubCallbackSpaceEndpoint,
@@ -176,6 +179,21 @@ urlpatterns = [
ResetPasswordEndpoint.as_view(),
name="forgot-password",
),
+ path(
+ "spaces/forgot-password/",
+ ForgotPasswordSpaceEndpoint.as_view(),
+ name="forgot-password",
+ ),
+ path(
+ "spaces/reset-password///",
+ ResetPasswordSpaceEndpoint.as_view(),
+ name="forgot-password",
+ ),
+ path(
+ "change-password/",
+ ChangePasswordEndpoint.as_view(),
+ name="forgot-password",
+ ),
path(
"set-password/",
SetUserPasswordEndpoint.as_view(),
diff --git a/apiserver/plane/authentication/utils/host.py b/apiserver/plane/authentication/utils/host.py
index b9dc7189b..b670eed41 100644
--- a/apiserver/plane/authentication/utils/host.py
+++ b/apiserver/plane/authentication/utils/host.py
@@ -1,8 +1,19 @@
+# Python imports
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"""
+
+ 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 (
request.META.get("HTTP_ORIGIN")
or f"{urlsplit(request.META.get('HTTP_REFERER')).scheme}://{urlsplit(request.META.get('HTTP_REFERER')).netloc}"
diff --git a/apiserver/plane/authentication/views/__init__.py b/apiserver/plane/authentication/views/__init__.py
index 4bd920e29..a5aadf728 100644
--- a/apiserver/plane/authentication/views/__init__.py
+++ b/apiserver/plane/authentication/views/__init__.py
@@ -1,8 +1,6 @@
from .common import (
ChangePasswordEndpoint,
CSRFTokenEndpoint,
- ForgotPasswordEndpoint,
- ResetPasswordEndpoint,
SetUserPasswordEndpoint,
)
@@ -50,3 +48,12 @@ from .space.magic import (
from .space.signout import SignOutAuthSpaceEndpoint
from .space.check import EmailCheckEndpoint
+
+from .space.password_management import (
+ ForgotPasswordSpaceEndpoint,
+ ResetPasswordSpaceEndpoint,
+)
+from .app.password_management import (
+ ForgotPasswordEndpoint,
+ ResetPasswordEndpoint,
+)
diff --git a/apiserver/plane/authentication/views/app/github.py b/apiserver/plane/authentication/views/app/github.py
index e7184b16e..48b7e09d9 100644
--- a/apiserver/plane/authentication/views/app/github.py
+++ b/apiserver/plane/authentication/views/app/github.py
@@ -2,7 +2,6 @@ import uuid
from urllib.parse import urlencode, urljoin
# Django import
-from django.core.exceptions import ImproperlyConfigured
from django.http import HttpResponseRedirect
from django.views import View
diff --git a/apiserver/plane/authentication/views/app/google.py b/apiserver/plane/authentication/views/app/google.py
index 19c59691c..690a9778b 100644
--- a/apiserver/plane/authentication/views/app/google.py
+++ b/apiserver/plane/authentication/views/app/google.py
@@ -3,18 +3,17 @@ import uuid
from urllib.parse import urlencode, urljoin
# Django import
-from django.core.exceptions import ImproperlyConfigured
from django.http import HttpResponseRedirect
from django.views import View
+
+# Module imports
from plane.authentication.provider.oauth.google import GoogleOAuthProvider
from plane.authentication.utils.login import user_login
from plane.authentication.utils.redirection_path import get_redirection_path
from plane.authentication.utils.workspace_project_join import (
process_workspace_project_invitations,
)
-
-# Module imports
from plane.license.models import Instance
from plane.authentication.utils.host import base_host
from plane.authentication.adapter.error import (
diff --git a/apiserver/plane/authentication/views/app/password_management.py b/apiserver/plane/authentication/views/app/password_management.py
new file mode 100644
index 000000000..80803cd25
--- /dev/null
+++ b/apiserver/plane/authentication/views/app/password_management.py
@@ -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)
diff --git a/apiserver/plane/authentication/views/common.py b/apiserver/plane/authentication/views/common.py
index a66326b1a..4b93010de 100644
--- a/apiserver/plane/authentication/views/common.py
+++ b/apiserver/plane/authentication/views/common.py
@@ -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
from rest_framework import status
from rest_framework.permissions import AllowAny
@@ -29,15 +11,12 @@ from plane.app.serializers import (
UserSerializer,
)
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.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 (
AuthenticationException,
AUTHENTICATION_ERROR_CODES,
)
+from django.middleware.csrf import get_token
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):
def post(self, request):
serializer = ChangePasswordSerializer(data=request.data)
diff --git a/apiserver/plane/authentication/views/space/email.py b/apiserver/plane/authentication/views/space/email.py
index b0a82fe6f..4505332eb 100644
--- a/apiserver/plane/authentication/views/space/email.py
+++ b/apiserver/plane/authentication/views/space/email.py
@@ -37,7 +37,7 @@ class SignInAuthSpaceEndpoint(View):
if next_path:
params["next_path"] = str(next_path)
url = urljoin(
- base_host(request=request),
+ base_host(request=request, is_space=True),
"accounts/sign-in?" + urlencode(params),
)
return HttpResponseRedirect(url)
@@ -59,7 +59,7 @@ class SignInAuthSpaceEndpoint(View):
if next_path:
params["next_path"] = str(next_path)
url = urljoin(
- base_host(request=request),
+ base_host(request=request, is_space=True),
"spaces/accounts/sign-in?" + urlencode(params),
)
return HttpResponseRedirect(url)
@@ -78,7 +78,7 @@ class SignInAuthSpaceEndpoint(View):
if next_path:
params["next_path"] = str(next_path)
url = urljoin(
- base_host(request=request),
+ base_host(request=request, is_space=True),
"spaces/accounts/sign-in?" + urlencode(params),
)
return HttpResponseRedirect(url)
@@ -93,7 +93,7 @@ class SignInAuthSpaceEndpoint(View):
if next_path:
params["next_path"] = str(next_path)
url = urljoin(
- base_host(request=request),
+ base_host(request=request, is_space=True),
"spaces/accounts/sign-in?" + urlencode(params),
)
return HttpResponseRedirect(url)
@@ -107,7 +107,7 @@ class SignInAuthSpaceEndpoint(View):
user_login(request=request, user=user)
# redirect to next path
url = urljoin(
- base_host(request=request),
+ base_host(request=request, is_space=True),
str(next_path) if next_path else "/",
)
return HttpResponseRedirect(url)
@@ -116,7 +116,7 @@ class SignInAuthSpaceEndpoint(View):
if next_path:
params["next_path"] = str(next_path)
url = urljoin(
- base_host(request=request),
+ base_host(request=request, is_space=True),
"spaces/accounts/sign-in?" + urlencode(params),
)
return HttpResponseRedirect(url)
@@ -140,7 +140,7 @@ class SignUpAuthSpaceEndpoint(View):
if next_path:
params["next_path"] = str(next_path)
url = urljoin(
- base_host(request=request),
+ base_host(request=request, is_space=True),
"spaces?" + urlencode(params),
)
return HttpResponseRedirect(url)
@@ -161,7 +161,7 @@ class SignUpAuthSpaceEndpoint(View):
if next_path:
params["next_path"] = str(next_path)
url = urljoin(
- base_host(request=request),
+ base_host(request=request, is_space=True),
"spaces?" + urlencode(params),
)
return HttpResponseRedirect(url)
@@ -180,7 +180,7 @@ class SignUpAuthSpaceEndpoint(View):
if next_path:
params["next_path"] = str(next_path)
url = urljoin(
- base_host(request=request),
+ base_host(request=request, is_space=True),
"spaces?" + urlencode(params),
)
return HttpResponseRedirect(url)
@@ -195,7 +195,7 @@ class SignUpAuthSpaceEndpoint(View):
if next_path:
params["next_path"] = str(next_path)
url = urljoin(
- base_host(request=request),
+ base_host(request=request, is_space=True),
"spaces?" + urlencode(params),
)
return HttpResponseRedirect(url)
@@ -209,7 +209,7 @@ class SignUpAuthSpaceEndpoint(View):
user_login(request=request, user=user)
# redirect to referer path
url = urljoin(
- base_host(request=request),
+ base_host(request=request, is_space=True),
str(next_path) if next_path else "spaces",
)
return HttpResponseRedirect(url)
@@ -218,7 +218,7 @@ class SignUpAuthSpaceEndpoint(View):
if next_path:
params["next_path"] = str(next_path)
url = urljoin(
- base_host(request=request),
+ base_host(request=request, is_space=True),
"spaces?" + urlencode(params),
)
return HttpResponseRedirect(url)
diff --git a/apiserver/plane/authentication/views/space/github.py b/apiserver/plane/authentication/views/space/github.py
index 192f0d039..4a0f23098 100644
--- a/apiserver/plane/authentication/views/space/github.py
+++ b/apiserver/plane/authentication/views/space/github.py
@@ -3,7 +3,6 @@ import uuid
from urllib.parse import urlencode, urljoin
# Django import
-from django.core.exceptions import ImproperlyConfigured
from django.http import HttpResponseRedirect
from django.views import View
@@ -22,7 +21,7 @@ class GitHubOauthInitiateSpaceEndpoint(View):
def get(self, request):
# 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")
if next_path:
request.session["next_path"] = str(next_path)
@@ -40,7 +39,7 @@ class GitHubOauthInitiateSpaceEndpoint(View):
if next_path:
params["next_path"] = str(next_path)
url = urljoin(
- base_host(request=request),
+ base_host(request=request, is_space=True),
"?" + urlencode(params),
)
return HttpResponseRedirect(url)
diff --git a/apiserver/plane/authentication/views/space/google.py b/apiserver/plane/authentication/views/space/google.py
index 86632ebb4..2f6b57699 100644
--- a/apiserver/plane/authentication/views/space/google.py
+++ b/apiserver/plane/authentication/views/space/google.py
@@ -19,7 +19,7 @@ from plane.authentication.adapter.error import (
class GoogleOauthInitiateSpaceEndpoint(View):
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")
if next_path:
request.session["next_path"] = str(next_path)
@@ -37,7 +37,7 @@ class GoogleOauthInitiateSpaceEndpoint(View):
if next_path:
params["next_path"] = str(next_path)
url = urljoin(
- base_host(request=request),
+ base_host(request=request, is_space=True),
"?" + urlencode(params),
)
return HttpResponseRedirect(url)
@@ -53,7 +53,7 @@ class GoogleOauthInitiateSpaceEndpoint(View):
if next_path:
params["next_path"] = str(next_path)
url = urljoin(
- base_host(request=request),
+ base_host(request=request, is_space=True),
"?" + urlencode(params),
)
return HttpResponseRedirect(url)
diff --git a/apiserver/plane/authentication/views/space/magic.py b/apiserver/plane/authentication/views/space/magic.py
index 390f6021d..52771f71b 100644
--- a/apiserver/plane/authentication/views/space/magic.py
+++ b/apiserver/plane/authentication/views/space/magic.py
@@ -2,7 +2,6 @@
from urllib.parse import urlencode, urljoin
# Django imports
-from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.core.validators import validate_email
from django.http import HttpResponseRedirect
from django.views import View
@@ -48,7 +47,7 @@ class MagicGenerateSpaceEndpoint(APIView):
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)
try:
# Clean up the email
@@ -86,7 +85,7 @@ class MagicSignInSpaceEndpoint(View):
if next_path:
params["next_path"] = str(next_path)
url = urljoin(
- base_host(request=request),
+ base_host(request=request, is_space=True),
"spaces/accounts/sign-in?" + urlencode(params),
)
return HttpResponseRedirect(url)
@@ -99,7 +98,7 @@ class MagicSignInSpaceEndpoint(View):
if next_path:
params["next_path"] = str(next_path)
url = urljoin(
- base_host(request=request),
+ base_host(request=request, is_space=True),
"accounts/sign-in?" + urlencode(params),
)
return HttpResponseRedirect(url)
@@ -118,7 +117,7 @@ class MagicSignInSpaceEndpoint(View):
else:
# Get the redirection path
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)
except AuthenticationException as e:
@@ -126,7 +125,7 @@ class MagicSignInSpaceEndpoint(View):
if next_path:
params["next_path"] = str(next_path)
url = urljoin(
- base_host(request=request),
+ base_host(request=request, is_space=True),
"spaces/accounts/sign-in?" + urlencode(params),
)
return HttpResponseRedirect(url)
@@ -152,7 +151,7 @@ class MagicSignUpSpaceEndpoint(View):
if next_path:
params["next_path"] = str(next_path)
url = urljoin(
- base_host(request=request),
+ base_host(request=request, is_space=True),
"spaces/accounts/sign-in?" + urlencode(params),
)
return HttpResponseRedirect(url)
@@ -166,7 +165,7 @@ class MagicSignUpSpaceEndpoint(View):
if next_path:
params["next_path"] = str(next_path)
url = urljoin(
- base_host(request=request),
+ base_host(request=request, is_space=True),
"?" + urlencode(params),
)
return HttpResponseRedirect(url)
@@ -180,7 +179,7 @@ class MagicSignUpSpaceEndpoint(View):
user_login(request=request, user=user)
# redirect to referer path
url = urljoin(
- base_host(request=request),
+ base_host(request=request, is_space=True),
str(next_path) if next_path else "spaces",
)
return HttpResponseRedirect(url)
@@ -190,7 +189,7 @@ class MagicSignUpSpaceEndpoint(View):
if next_path:
params["next_path"] = str(next_path)
url = urljoin(
- base_host(request=request),
+ base_host(request=request, is_space=True),
"spaces/accounts/sign-in?" + urlencode(params),
)
return HttpResponseRedirect(url)
diff --git a/apiserver/plane/authentication/views/space/password_management.py b/apiserver/plane/authentication/views/space/password_management.py
new file mode 100644
index 000000000..aeac9776d
--- /dev/null
+++ b/apiserver/plane/authentication/views/space/password_management.py
@@ -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)
diff --git a/apiserver/plane/authentication/views/space/signout.py b/apiserver/plane/authentication/views/space/signout.py
index b993fb78c..3cfd6d471 100644
--- a/apiserver/plane/authentication/views/space/signout.py
+++ b/apiserver/plane/authentication/views/space/signout.py
@@ -24,11 +24,11 @@ class SignOutAuthSpaceEndpoint(View):
# Log the user out
logout(request)
url = urljoin(
- base_host(request=request),
+ base_host(request=request, is_space=True),
"accounts/sign-in?" + urlencode({"success": "true"}),
)
return HttpResponseRedirect(url)
except Exception:
return HttpResponseRedirect(
- base_host(request=request), "accounts/sign-in"
+ base_host(request=request, is_space=True), "accounts/sign-in"
)
diff --git a/apiserver/plane/license/api/views/admin.py b/apiserver/plane/license/api/views/admin.py
index 8833c7a7c..ed3c00f17 100644
--- a/apiserver/plane/license/api/views/admin.py
+++ b/apiserver/plane/license/api/views/admin.py
@@ -106,7 +106,7 @@ class InstanceAdminSignUpEndpoint(View):
error_message="INSTANCE_NOT_CONFIGURED",
)
url = urljoin(
- base_host(request=request),
+ base_host(request=request, is_admin=True),
"god-mode/setup?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
@@ -118,7 +118,7 @@ class InstanceAdminSignUpEndpoint(View):
error_message="ADMIN_ALREADY_EXIST",
)
url = urljoin(
- base_host(request=request),
+ base_host(request=request, is_admin=True),
"god-mode/setup?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
@@ -147,7 +147,7 @@ class InstanceAdminSignUpEndpoint(View):
},
)
url = urljoin(
- base_host(request=request),
+ base_host(request=request, is_admin=True),
"god-mode/setup?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
@@ -169,7 +169,7 @@ class InstanceAdminSignUpEndpoint(View):
},
)
url = urljoin(
- base_host(request=request),
+ base_host(request=request, is_admin=True),
"god-mode/setup?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
@@ -191,7 +191,7 @@ class InstanceAdminSignUpEndpoint(View):
},
)
url = urljoin(
- base_host(request=request),
+ base_host(request=request, is_admin=True),
"god-mode/setup?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
@@ -213,7 +213,7 @@ class InstanceAdminSignUpEndpoint(View):
},
)
url = urljoin(
- base_host(request=request),
+ base_host(request=request, is_admin=True),
"god-mode/setup?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
@@ -248,7 +248,9 @@ class InstanceAdminSignUpEndpoint(View):
# get tokens for 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)
@@ -269,7 +271,7 @@ class InstanceAdminSignInEndpoint(View):
error_message="INSTANCE_NOT_CONFIGURED",
)
url = urljoin(
- base_host(request=request),
+ base_host(request=request, is_admin=True),
"god-mode/login?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
@@ -290,7 +292,7 @@ class InstanceAdminSignInEndpoint(View):
},
)
url = urljoin(
- base_host(request=request),
+ base_host(request=request, is_admin=True),
"god-mode/login?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
@@ -308,7 +310,7 @@ class InstanceAdminSignInEndpoint(View):
},
)
url = urljoin(
- base_host(request=request),
+ base_host(request=request, is_admin=True),
"god-mode/login?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
@@ -328,7 +330,7 @@ class InstanceAdminSignInEndpoint(View):
},
)
url = urljoin(
- base_host(request=request),
+ base_host(request=request, is_admin=True),
"god-mode/login?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
@@ -345,7 +347,7 @@ class InstanceAdminSignInEndpoint(View):
},
)
url = urljoin(
- base_host(request=request),
+ base_host(request=request, is_admin=True),
"god-mode/login?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
@@ -362,7 +364,7 @@ class InstanceAdminSignInEndpoint(View):
},
)
url = urljoin(
- base_host(request=request),
+ base_host(request=request, is_admin=True),
"god-mode/login?" + urlencode(exc.get_error_dict()),
)
return HttpResponseRedirect(url)
@@ -377,7 +379,9 @@ class InstanceAdminSignInEndpoint(View):
# get tokens for 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)
@@ -411,11 +415,11 @@ class InstanceAdminSignOutEndpoint(View):
# Log the user out
logout(request)
url = urljoin(
- base_host(request=request),
+ base_host(request=request, is_admin=True),
"accounts/sign-in?" + urlencode({"success": "true"}),
)
return HttpResponseRedirect(url)
except Exception:
return HttpResponseRedirect(
- base_host(request=request), "accounts/sign-in"
+ base_host(request=request, is_admin=True), "accounts/sign-in"
)
diff --git a/apiserver/plane/license/api/views/instance.py b/apiserver/plane/license/api/views/instance.py
index 2247bbeb1..45c1f872d 100644
--- a/apiserver/plane/license/api/views/instance.py
+++ b/apiserver/plane/license/api/views/instance.py
@@ -2,6 +2,7 @@
import os
# Django imports
+from django.conf import settings
# Third party imports
from rest_framework import status
@@ -148,9 +149,13 @@ class InstanceEndpoint(BaseAPIView):
)
# is smtp configured
- data["is_smtp_configured"] = (
- bool(EMAIL_HOST)
- )
+ data["is_smtp_configured"] = 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["workspaces_exist"] = Workspace.objects.count() > 1
diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py
index 7ee1bdf55..4f5e6d4ee 100644
--- a/apiserver/plane/settings/common.py
+++ b/apiserver/plane/settings/common.py
@@ -342,3 +342,8 @@ CSRF_COOKIE_SECURE = secure_origins
CSRF_COOKIE_HTTPONLY = True
CSRF_TRUSTED_ORIGINS = cors_allowed_origins
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)
diff --git a/deploy/selfhost/docker-compose.yml b/deploy/selfhost/docker-compose.yml
index 6f12abd61..67f61d0ef 100644
--- a/deploy/selfhost/docker-compose.yml
+++ b/deploy/selfhost/docker-compose.yml
@@ -33,6 +33,10 @@ x-app-env: &app-env
- MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD:-"secret-key"}
- BUCKET_NAME=${BUCKET_NAME:-uploads}
- 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:
web:
@@ -40,7 +44,7 @@ services:
image: ${DOCKERHUB_USER:-makeplane}/plane-frontend:${APP_RELEASE:-stable}
pull_policy: ${PULL_POLICY:-always}
restart: unless-stopped
- command: /usr/local/bin/start.sh web/server.js web
+ command: node web/server.js web
deploy:
replicas: ${WEB_REPLICAS:-1}
depends_on:
@@ -52,20 +56,20 @@ services:
image: ${DOCKERHUB_USER:-makeplane}/plane-space:${APP_RELEASE:-stable}
pull_policy: ${PULL_POLICY:-always}
restart: unless-stopped
- command: /usr/local/bin/start.sh space/server.js space
+ command: node space/server.js space
deploy:
replicas: ${SPACE_REPLICAS:-1}
depends_on:
- api
- worker
- web
-
+
admin:
<<: *app-env
image: ${DOCKERHUB_USER:-makeplane}/plane-space:${APP_RELEASE:-stable}
pull_policy: ${PULL_POLICY:-always}
restart: unless-stopped
- command: node admin/server.js admin
+ command: node admin/server.js admin
deploy:
replicas: ${ADMIN_REPLICAS:-1}
depends_on:
diff --git a/docker-compose.yml b/docker-compose.yml
index bf8066055..be1008193 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -7,7 +7,7 @@ services:
args:
DOCKER_BUILDKIT: 1
restart: always
- command: /usr/local/bin/start.sh web/server.js web
+ command: node web/server.js web
depends_on:
- api
@@ -32,7 +32,7 @@ services:
args:
DOCKER_BUILDKIT: 1
restart: always
- command: /usr/local/bin/start.sh space/server.js space
+ command: node space/server.js space
depends_on:
- api
- web
@@ -134,7 +134,6 @@ services:
MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID}
MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY}
-
# Comment this if you already have a reverse proxy running
proxy:
container_name: proxy
diff --git a/packages/types/src/instance/base.d.ts b/packages/types/src/instance/base.d.ts
index 87f03c68f..efc47b15d 100644
--- a/packages/types/src/instance/base.d.ts
+++ b/packages/types/src/instance/base.d.ts
@@ -43,6 +43,9 @@ export interface IInstance {
has_openai_configured: boolean;
file_size_limit: number | undefined;
is_smtp_configured: boolean;
+ app_base_url: string | undefined;
+ space_base_url: string | undefined;
+ admin_base_url: string | undefined;
};
}
diff --git a/space/.env.example b/space/.env.example
index fbd4ad4f9..33939c7e2 100644
--- a/space/.env.example
+++ b/space/.env.example
@@ -1,2 +1,3 @@
-NEXT_PUBLIC_APP_URL=
-NEXT_PUBLIC_API_BASE_URL=
\ No newline at end of file
+NEXT_PUBLIC_API_BASE_URL=""
+NEXT_PUBLIC_WEB_BASE_URL=""
+NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
\ No newline at end of file
diff --git a/space/Dockerfile.space b/space/Dockerfile.space
index 095f06722..229585818 100644
--- a/space/Dockerfile.space
+++ b/space/Dockerfile.space
@@ -1,3 +1,6 @@
+# *****************************************************************************
+# STAGE 1: Build the project
+# *****************************************************************************
FROM node:18-alpine AS builder
RUN apk add --no-cache libc6-compat
WORKDIR /app
@@ -7,6 +10,9 @@ COPY . .
RUN turbo prune --scope=space --docker
+# *****************************************************************************
+# STAGE 2: Install dependencies & build the project
+# *****************************************************************************
FROM node:18-alpine AS installer
RUN apk add --no-cache libc6-compat
@@ -21,13 +27,19 @@ COPY --from=builder /app/out/full/ .
COPY turbo.json turbo.json
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
RUN yarn turbo run build --filter=space
+# *****************************************************************************
+# STAGE 3: Copy the project and start it
+# *****************************************************************************
FROM node:18-alpine AS runner
WORKDIR /app
@@ -40,14 +52,14 @@ COPY --from=installer /app/space/.next ./space/.next
COPY --from=installer /app/space/public ./space/public
ARG 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_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/
-RUN chmod +x /usr/local/bin/start.sh
-
ENV NEXT_TELEMETRY_DISABLED 1
ENV TURBO_TELEMETRY_DISABLED 1
diff --git a/space/pages/404.tsx b/space/pages/404.tsx
index 07e415a19..4591f71f8 100644
--- a/space/pages/404.tsx
+++ b/space/pages/404.tsx
@@ -1,31 +1,42 @@
// next imports
+import { observer } from "mobx-react-lite";
import Image from "next/image";
+// hooks
+import { useInstance } from "@/hooks/store";
+// images
import notFoundImage from "public/404.svg";
-const Custom404Error = () => (
- |