Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17c539658b | ||
|
|
6f190ea6ee | ||
|
|
4ccdd29b9e | ||
|
|
25868bf07d | ||
|
|
f277aa02b6 | ||
|
|
16fc90930a | ||
|
|
7dada8a7a0 | ||
|
|
a9b45ed2ad | ||
|
|
c009a45887 |
+1
-1
@@ -4,7 +4,7 @@ module.exports = {
|
||||
extends: ["custom"],
|
||||
settings: {
|
||||
next: {
|
||||
rootDir: ["web/", "space/", "admin/"],
|
||||
rootDir: ["web/", "space/"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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_SPACE_BASE_PATH: "/spaces"
|
||||
NEXT_PUBLIC_DEPLOY_WITH_NGINX: 1
|
||||
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_ADMIN_BASE_PATH: "/god-mode"
|
||||
NEXT_PUBLIC_DEPLOY_WITH_NGINX: 1
|
||||
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,13 +172,7 @@ 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:
|
||||
|
||||
+2
-3
@@ -1,3 +1,2 @@
|
||||
NEXT_PUBLIC_API_BASE_URL=""
|
||||
NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
|
||||
NEXT_PUBLIC_WEB_BASE_URL=""
|
||||
NEXT_PUBLIC_APP_URL=
|
||||
NEXT_PUBLIC_API_BASE_URL=
|
||||
+7
-30
@@ -1,6 +1,3 @@
|
||||
# *****************************************************************************
|
||||
# STAGE 1: Build the project
|
||||
# *****************************************************************************
|
||||
FROM node:18-alpine AS builder
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
@@ -10,9 +7,6 @@ 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
|
||||
@@ -27,25 +21,13 @@ COPY --from=builder /app/out/full/ .
|
||||
COPY turbo.json turbo.json
|
||||
|
||||
ARG NEXT_PUBLIC_API_BASE_URL=""
|
||||
ARG NEXT_PUBLIC_DEPLOY_WITH_NGINX=1
|
||||
|
||||
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="/spaces"
|
||||
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
|
||||
ENV NEXT_PUBLIC_DEPLOY_WITH_NGINX=$NEXT_PUBLIC_DEPLOY_WITH_NGINX
|
||||
|
||||
RUN yarn turbo run build --filter=admin
|
||||
|
||||
# *****************************************************************************
|
||||
# STAGE 3: Copy the project and start it
|
||||
# *****************************************************************************
|
||||
FROM node:18-alpine AS runner
|
||||
WORKDIR /app
|
||||
|
||||
@@ -56,17 +38,12 @@ 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=""
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||
ARG NEXT_PUBLIC_DEPLOY_WITH_NGINX=1
|
||||
|
||||
ARG NEXT_PUBLIC_WEB_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_SPACE_BASE_URL="/spaces"
|
||||
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_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||
ENV NEXT_PUBLIC_DEPLOY_WITH_NGINX=$NEXT_PUBLIC_DEPLOY_WITH_NGINX
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
ENV TURBO_TELEMETRY_DISABLED 1
|
||||
|
||||
@@ -3,15 +3,10 @@ RUN apk add --no-cache libc6-compat
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
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"]
|
||||
|
||||
@@ -6,7 +6,7 @@ import { IFormattedInstanceConfiguration, TInstanceAIConfigurationKeys } from "@
|
||||
// components
|
||||
import { ControllerInput, TControllerInputFormField } from "components/common";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
import { useInstance } from "@/hooks";
|
||||
|
||||
type IInstanceAIForm = {
|
||||
config: IFormattedInstanceConfiguration;
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Loader } from "@plane/ui";
|
||||
import { PageHeader } from "@/components/core";
|
||||
import { InstanceAIForm } from "./components";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
import { useInstance } from "@/hooks";
|
||||
|
||||
const InstanceAIPage = observer(() => {
|
||||
// store
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
import { useInstance } from "@/hooks";
|
||||
// ui
|
||||
import { ToggleSwitch } from "@plane/ui";
|
||||
// types
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
import { useInstance } from "@/hooks";
|
||||
// ui
|
||||
import { ToggleSwitch } from "@plane/ui";
|
||||
// types
|
||||
|
||||
@@ -2,7 +2,7 @@ import { FC, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import Link from "next/link";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
import { useInstance } from "@/hooks";
|
||||
// ui
|
||||
import { Button, TOAST_TYPE, getButtonStyling, setToast } from "@plane/ui";
|
||||
// components
|
||||
|
||||
@@ -4,7 +4,7 @@ import React from "react";
|
||||
import Link from "next/link";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
import { useInstance } from "@/hooks";
|
||||
// ui
|
||||
import { ToggleSwitch, getButtonStyling } from "@plane/ui";
|
||||
// icons
|
||||
|
||||
@@ -11,7 +11,7 @@ import { PageHeader } from "@/components/core";
|
||||
import { AuthenticationMethodCard } from "../components";
|
||||
import { InstanceGithubConfigForm } from "./components";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
import { useInstance } from "@/hooks";
|
||||
// helpers
|
||||
import { resolveGeneralTheme } from "@/helpers/common.helper";
|
||||
// icons
|
||||
|
||||
@@ -2,7 +2,7 @@ import { FC, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import Link from "next/link";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
import { useInstance } from "@/hooks";
|
||||
// ui
|
||||
import { Button, TOAST_TYPE, getButtonStyling, setToast } from "@plane/ui";
|
||||
// components
|
||||
|
||||
@@ -4,7 +4,7 @@ import React from "react";
|
||||
import Link from "next/link";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
import { useInstance } from "@/hooks";
|
||||
// ui
|
||||
import { ToggleSwitch, getButtonStyling } from "@plane/ui";
|
||||
// icons
|
||||
|
||||
@@ -10,7 +10,7 @@ import { PageHeader } from "@/components/core";
|
||||
import { AuthenticationMethodCard } from "../components";
|
||||
import { InstanceGoogleConfigForm } from "./components";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
import { useInstance } from "@/hooks";
|
||||
// icons
|
||||
import GoogleLogo from "@/public/logos/google-logo.svg";
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import { GoogleConfiguration } from "./google/components";
|
||||
import { GithubConfiguration } from "./github/components";
|
||||
import { PageHeader } from "@/components/core";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
import { useInstance } from "@/hooks";
|
||||
// helpers
|
||||
import { resolveGeneralTheme } from "@/helpers/common.helper";
|
||||
// images
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { FC, useMemo, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
import { useInstance } from "@/hooks";
|
||||
// ui
|
||||
import { Button, CustomSelect, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Loader } from "@plane/ui";
|
||||
import { PageHeader } from "@/components/core";
|
||||
import { InstanceEmailForm } from "./components";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
import { useInstance } from "@/hooks";
|
||||
|
||||
const InstanceEmailPage = observer(() => {
|
||||
// store
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Button, Input, TOAST_TYPE, ToggleSwitch, setToast } from "@plane/ui";
|
||||
// components
|
||||
import { ControllerInput } from "components/common";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
import { useInstance } from "@/hooks";
|
||||
|
||||
export interface IGeneralConfigurationForm {
|
||||
instance: IInstance["instance"];
|
||||
|
||||
@@ -5,7 +5,7 @@ import { observer } from "mobx-react-lite";
|
||||
import { PageHeader } from "@/components/core";
|
||||
import { GeneralConfigurationForm } from "./components";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
import { useInstance } from "@/hooks";
|
||||
|
||||
const GeneralPage = observer(() => {
|
||||
const { instance, instanceAdmins } = useInstance();
|
||||
|
||||
@@ -5,7 +5,7 @@ import { IFormattedInstanceConfiguration, TInstanceImageConfigurationKeys } from
|
||||
// components
|
||||
import { ControllerInput } from "components/common";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
import { useInstance } from "@/hooks";
|
||||
|
||||
type IInstanceImageConfigForm = {
|
||||
config: IFormattedInstanceConfiguration;
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Loader } from "@plane/ui";
|
||||
import { PageHeader } from "@/components/core";
|
||||
import { InstanceImageConfigForm } from "./components";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
import { useInstance } from "@/hooks";
|
||||
|
||||
const InstanceImagePage = observer(() => {
|
||||
// store
|
||||
|
||||
+30
-28
@@ -7,8 +7,6 @@ import { StoreProvider } from "@/lib/store-context";
|
||||
import { AppWrapper } from "@/lib/wrappers";
|
||||
// constants
|
||||
import { SITE_NAME, SITE_DESCRIPTION, SITE_URL, TWITTER_USER_NAME, SITE_KEYWORDS, SITE_TITLE } from "@/constants/seo";
|
||||
// helpers
|
||||
import { ASSET_PREFIX } from "@/helpers/common.helper";
|
||||
// styles
|
||||
import "./globals.css";
|
||||
|
||||
@@ -16,31 +14,35 @@ interface RootLayoutProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const RootLayout = ({ children, ...pageProps }: RootLayoutProps) => (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{SITE_TITLE}</title>
|
||||
<meta property="og:site_name" content={SITE_NAME} />
|
||||
<meta property="og:title" content={SITE_TITLE} />
|
||||
<meta property="og:url" content={SITE_URL} />
|
||||
<meta name="description" content={SITE_DESCRIPTION} />
|
||||
<meta property="og:description" content={SITE_DESCRIPTION} />
|
||||
<meta name="keywords" content={SITE_KEYWORDS} />
|
||||
<meta name="twitter:site" content={`@${TWITTER_USER_NAME}`} />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href={`${ASSET_PREFIX}/favicon/apple-touch-icon.png`} />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href={`${ASSET_PREFIX}/favicon/favicon-32x32.png`} />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href={`${ASSET_PREFIX}/favicon/favicon-16x16.png`} />
|
||||
<link rel="manifest" href={`${ASSET_PREFIX}/site.webmanifest.json`} />
|
||||
<link rel="shortcut icon" href={`${ASSET_PREFIX}/favicon/favicon.ico`} />
|
||||
</head>
|
||||
<body className={`antialiased`}>
|
||||
<StoreProvider {...pageProps}>
|
||||
<ThemeProvider themes={["light", "dark"]} defaultTheme="system" enableSystem>
|
||||
<AppWrapper>{children}</AppWrapper>
|
||||
</ThemeProvider>
|
||||
</StoreProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
const RootLayout = ({ children, ...pageProps }: RootLayoutProps) => {
|
||||
const prefix = "/god-mode/";
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{SITE_TITLE}</title>
|
||||
<meta property="og:site_name" content={SITE_NAME} />
|
||||
<meta property="og:title" content={SITE_TITLE} />
|
||||
<meta property="og:url" content={SITE_URL} />
|
||||
<meta name="description" content={SITE_DESCRIPTION} />
|
||||
<meta property="og:description" content={SITE_DESCRIPTION} />
|
||||
<meta name="keywords" content={SITE_KEYWORDS} />
|
||||
<meta name="twitter:site" content={`@${TWITTER_USER_NAME}`} />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href={`${prefix}favicon/apple-touch-icon.png`} />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href={`${prefix}favicon/favicon-32x32.png`} />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href={`${prefix}favicon/favicon-16x16.png`} />
|
||||
<link rel="manifest" href={`${prefix}site.webmanifest.json`} />
|
||||
<link rel="shortcut icon" href={`${prefix}favicon/favicon.ico`} />
|
||||
</head>
|
||||
<body className={`antialiased`}>
|
||||
<StoreProvider {...pageProps}>
|
||||
<ThemeProvider themes={["light", "dark"]} defaultTheme="system" enableSystem>
|
||||
<AppWrapper>{children}</AppWrapper>
|
||||
</ThemeProvider>
|
||||
</StoreProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
};
|
||||
|
||||
export default RootLayout;
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { FC, useState, useRef } from "react";
|
||||
import Link from "next/link";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Transition } from "@headlessui/react";
|
||||
import Link from "next/link";
|
||||
import { ExternalLink, FileText, HelpCircle, MoveLeft } from "lucide-react";
|
||||
import { DiscordIcon, GithubIcon, Tooltip } from "@plane/ui";
|
||||
// hooks
|
||||
import { useInstance, useTheme } from "@/hooks/store";
|
||||
import { useTheme } from "@/hooks";
|
||||
// assets
|
||||
import packageJson from "package.json";
|
||||
|
||||
@@ -29,9 +28,7 @@ const helpOptions = [
|
||||
},
|
||||
];
|
||||
|
||||
export const HelpSection: FC = observer(() => {
|
||||
// hooks
|
||||
const { instance } = useInstance();
|
||||
export const HelpSection: FC = () => {
|
||||
// states
|
||||
const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false);
|
||||
// store
|
||||
@@ -39,7 +36,7 @@ export const HelpSection: FC = observer(() => {
|
||||
// refs
|
||||
const helpOptionsRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const redirectionLink = `${instance?.config?.app_base_url ? `${instance?.config?.app_base_url}/create-workspace` : `/god-mode/`}`;
|
||||
const redirectionLink = `${process.env.NEXT_PUBLIC_APP_URL ? `${process.env.NEXT_PUBLIC_APP_URL}/create-workspace` : `/god-mode/`}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -131,4 +128,4 @@ export const HelpSection: FC = observer(() => {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { FC, useEffect, useRef } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useTheme } from "@/hooks/store";
|
||||
import { useTheme } from "@/hooks";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// components
|
||||
import { HelpSection, SidebarMenu, SidebarDropdown } from "@/components/admin-sidebar";
|
||||
|
||||
@@ -7,7 +7,7 @@ import { LogOut, UserCog2, Palette } from "lucide-react";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
import { Avatar } from "@plane/ui";
|
||||
// hooks
|
||||
import { useTheme, useUser } from "@/hooks/store";
|
||||
import { useTheme, useUser } from "@/hooks";
|
||||
// helpers
|
||||
import { API_BASE_URL, cn } from "@/helpers/common.helper";
|
||||
// services
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useTheme } from "@/hooks/store";
|
||||
import { useTheme } from "@/hooks";
|
||||
// icons
|
||||
import { Menu } from "lucide-react";
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { observer } from "mobx-react-lite";
|
||||
import { Image, BrainCog, Cog, Lock, Mail } from "lucide-react";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// hooks
|
||||
import { useTheme } from "@/hooks/store";
|
||||
import { useTheme } from "@/hooks";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
import { Button } from "@plane/ui";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
description?: React.ReactNode;
|
||||
image?: any;
|
||||
primaryButton?: {
|
||||
icon?: any;
|
||||
text: string;
|
||||
onClick: () => void;
|
||||
};
|
||||
secondaryButton?: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const EmptyState: React.FC<Props> = ({
|
||||
title,
|
||||
description,
|
||||
image,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
disabled = false,
|
||||
}) => (
|
||||
<div className={`flex h-full w-full items-center justify-center`}>
|
||||
<div className="flex w-full flex-col items-center text-center">
|
||||
{image && <Image src={image} className="w-52 sm:w-60" alt={primaryButton?.text || "button image"} />}
|
||||
<h6 className="mb-3 mt-6 text-xl font-semibold sm:mt-8">{title}</h6>
|
||||
{description && <p className="mb-7 px-5 text-custom-text-300 sm:mb-8">{description}</p>}
|
||||
<div className="flex items-center gap-4">
|
||||
{primaryButton && (
|
||||
<Button
|
||||
variant="primary"
|
||||
prependIcon={primaryButton.icon}
|
||||
onClick={primaryButton.onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
{primaryButton.text}
|
||||
</Button>
|
||||
)}
|
||||
{secondaryButton}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -4,4 +4,3 @@ export * from "./controller-input";
|
||||
export * from "./copy-field";
|
||||
export * from "./password-strength-meter";
|
||||
export * from "./banner";
|
||||
export * from "./empty-state";
|
||||
|
||||
@@ -8,20 +8,18 @@ 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/store";
|
||||
// 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 = `${instance?.config?.app_base_url ? `${instance?.config?.app_base_url}/create-workspace` : `/god-mode/`}`;
|
||||
const redirectionLink = `${process.env.NEXT_PUBLIC_APP_URL ? `${process.env.NEXT_PUBLIC_APP_URL}/create-workspace` : `/god-mode/`}`;
|
||||
|
||||
if (!isNewUserPopup) return <></>;
|
||||
return (
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_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 WEB_BASE_URL = process.env.NEXT_PUBLIC_WEB_BASE_URL || "";
|
||||
|
||||
export const ASSET_PREFIX = ADMIN_BASE_PATH;
|
||||
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ? process.env.NEXT_PUBLIC_API_BASE_URL : "";
|
||||
|
||||
export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export * from "./use-outside-click-detector";
|
||||
|
||||
// store-hooks
|
||||
export * from "./store/use-theme";
|
||||
export * from "./store/use-instance";
|
||||
export * from "./store/use-user";
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from "./use-theme";
|
||||
export * from "./use-instance";
|
||||
export * from "./use-user";
|
||||
@@ -4,11 +4,11 @@ import { FC, ReactNode, useEffect, Suspense } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { SWRConfig } from "swr";
|
||||
// hooks
|
||||
import { useTheme, useUser } from "@/hooks/store";
|
||||
import { useTheme, useUser } from "@/hooks";
|
||||
// ui
|
||||
import { Toast } from "@plane/ui";
|
||||
// constants
|
||||
import { SWR_CONFIG } from "@/constants/swr-config";
|
||||
import { SWR_CONFIG } from "constants/swr-config";
|
||||
// helpers
|
||||
import { resolveGeneralTheme } from "helpers/common.helper";
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { observer } from "mobx-react-lite";
|
||||
import useSWR from "swr";
|
||||
import { Spinner } from "@plane/ui";
|
||||
// hooks
|
||||
import { useInstance, useUser } from "@/hooks/store";
|
||||
import { useInstance, useUser } from "@/hooks";
|
||||
// helpers
|
||||
import { EAuthenticationPageType } from "@/helpers";
|
||||
|
||||
|
||||
@@ -10,10 +10,9 @@ import { DefaultLayout } from "@/layouts";
|
||||
// components
|
||||
import { InstanceNotReady } from "@/components/instance";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
import { useInstance } from "@/hooks";
|
||||
// helpers
|
||||
import { EInstancePageType } from "@/helpers";
|
||||
import { EmptyState } from "@/components/common";
|
||||
|
||||
type TInstanceWrapper = {
|
||||
children: ReactNode;
|
||||
@@ -29,9 +28,6 @@ export const InstanceWrapper: FC<TInstanceWrapper> = observer((props) => {
|
||||
|
||||
const { isLoading: isSWRLoading } = useSWR("INSTANCE_INFORMATION", () => fetchInstanceInfo(), {
|
||||
revalidateOnFocus: false,
|
||||
revalidateIfStale: false,
|
||||
revalidateOnReconnect: false,
|
||||
errorRetryCount: 0,
|
||||
});
|
||||
|
||||
if (isSWRLoading || isLoading)
|
||||
@@ -41,15 +37,6 @@ export const InstanceWrapper: FC<TInstanceWrapper> = observer((props) => {
|
||||
</div>
|
||||
);
|
||||
|
||||
if (!instance) {
|
||||
return (
|
||||
<EmptyState
|
||||
title="Your instance wasn't configured successfully."
|
||||
description="Please try re-installing Plane to fix the problem. If the issue still persists please reach out to support@plane.so."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (instance?.instance?.is_setup_done === false && authEnabled === "1")
|
||||
return (
|
||||
<DefaultLayout withoutBackground>
|
||||
|
||||
@@ -7,7 +7,7 @@ const nextConfig = {
|
||||
images: {
|
||||
unoptimized: true,
|
||||
},
|
||||
basePath: process.env.NEXT_PUBLIC_ADMIN_BASE_PATH || "",
|
||||
basePath: "/god-mode",
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "turbo run develop",
|
||||
"develop": "next dev --port 3001",
|
||||
"develop": "next dev --port 3333",
|
||||
"build": "next build",
|
||||
"preview": "next build && next start",
|
||||
"start": "next start",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { APIService } from "services/api.service";
|
||||
// types
|
||||
import type { IFormattedInstanceConfiguration, IInstance, IInstanceAdmin, IInstanceConfiguration } from "@plane/types";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||
import { APIService } from "@/services/api.service";
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
|
||||
export class InstanceService extends APIService {
|
||||
constructor() {
|
||||
|
||||
@@ -44,8 +44,3 @@ WEB_URL="http://localhost"
|
||||
|
||||
# Gunicorn Workers
|
||||
GUNICORN_WORKERS=2
|
||||
|
||||
# Base URLs
|
||||
ADMIN_BASE_URL=
|
||||
SPACE_BASE_URL=
|
||||
APP_BASE_URL=
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# Python imports
|
||||
|
||||
import zoneinfo
|
||||
|
||||
# Django imports
|
||||
|
||||
@@ -105,9 +105,13 @@ class PageSerializer(BaseSerializer):
|
||||
|
||||
class PageDetailSerializer(PageSerializer):
|
||||
description_html = serializers.CharField()
|
||||
description_yjs = serializers.CharField()
|
||||
|
||||
class Meta(PageSerializer.Meta):
|
||||
fields = PageSerializer.Meta.fields + ["description_html"]
|
||||
fields = PageSerializer.Meta.fields + [
|
||||
"description_html",
|
||||
"description_yjs",
|
||||
]
|
||||
|
||||
|
||||
class SubPageSerializer(BaseSerializer):
|
||||
|
||||
@@ -6,6 +6,7 @@ from plane.app.views import (
|
||||
PageFavoriteViewSet,
|
||||
PageLogEndpoint,
|
||||
SubPagesEndpoint,
|
||||
PagesDescriptionViewSet,
|
||||
)
|
||||
|
||||
|
||||
@@ -79,4 +80,14 @@ urlpatterns = [
|
||||
SubPagesEndpoint.as_view(),
|
||||
name="sub-page",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/description/",
|
||||
PagesDescriptionViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"patch": "partial_update",
|
||||
}
|
||||
),
|
||||
name="page-description",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -177,6 +177,7 @@ from .page.base import (
|
||||
PageFavoriteViewSet,
|
||||
PageLogEndpoint,
|
||||
SubPagesEndpoint,
|
||||
PagesDescriptionViewSet,
|
||||
)
|
||||
|
||||
from .search import GlobalSearchEndpoint, IssueSearchEndpoint
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Python imports
|
||||
import json
|
||||
import y_py as Y
|
||||
import base64
|
||||
from datetime import datetime
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
|
||||
@@ -8,6 +10,7 @@ from django.db import connection
|
||||
from django.db.models import Exists, OuterRef, Q
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.gzip import gzip_page
|
||||
from django.http import StreamingHttpResponse
|
||||
|
||||
# Third party imports
|
||||
from rest_framework import status
|
||||
@@ -387,3 +390,63 @@ class SubPagesEndpoint(BaseAPIView):
|
||||
return Response(
|
||||
SubPageSerializer(pages, many=True).data, status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
|
||||
class PagesDescriptionViewSet(BaseViewSet):
|
||||
|
||||
def retrieve(self, request, slug, project_id, pk):
|
||||
page = Page.objects.get(
|
||||
pk=pk, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
binary_data = page.description_yjs
|
||||
|
||||
def stream_data():
|
||||
if binary_data:
|
||||
yield binary_data
|
||||
else:
|
||||
yield b""
|
||||
|
||||
response = StreamingHttpResponse(
|
||||
stream_data(), content_type="application/octet-stream"
|
||||
)
|
||||
response["Content-Disposition"] = (
|
||||
'attachment; filename="page_description.bin"'
|
||||
)
|
||||
return response
|
||||
|
||||
def partial_update(self, request, slug, project_id, pk):
|
||||
page = Page.objects.get(
|
||||
pk=pk, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
|
||||
base64_data = request.data.get("description_yjs")
|
||||
|
||||
if base64_data:
|
||||
# Decode the base64 data to bytes
|
||||
new_binary_data = base64.b64decode(base64_data)
|
||||
|
||||
# Load the existing data into a YDoc
|
||||
existing_doc = Y.YDoc()
|
||||
if page.description_yjs:
|
||||
Y.apply_update(existing_doc, page.description_yjs)
|
||||
|
||||
# # Load the new data into a separate YDoc
|
||||
# new_doc = Y.YDoc()
|
||||
Y.apply_update(existing_doc, new_binary_data)
|
||||
|
||||
# # Merge the new data into the existing data
|
||||
# # This will automatically resolve any conflicts
|
||||
# new_state_vector = Y.encode_state_vector(new_doc)
|
||||
# diff = Y.encode_state_as_update(existing_doc, new_state_vector)
|
||||
# Y.apply_update(existing_doc, diff)
|
||||
|
||||
# # Encode the updated state as binary data
|
||||
updated_binary_data = Y.encode_state_as_update(existing_doc)
|
||||
|
||||
# Store the updated binary data
|
||||
page.description_yjs = updated_binary_data
|
||||
page.description_html = request.data.get("description_html")
|
||||
page.save()
|
||||
return Response({"message": "Updated successfully"})
|
||||
else:
|
||||
return Response({"error": "No binary data provided"})
|
||||
|
||||
@@ -1,52 +1,51 @@
|
||||
AUTHENTICATION_ERROR_CODES = {
|
||||
# Global
|
||||
"INSTANCE_NOT_CONFIGURED": 5000,
|
||||
"INVALID_EMAIL": 5005,
|
||||
"EMAIL_REQUIRED": 5010,
|
||||
"SIGNUP_DISABLED": 5015,
|
||||
"INVALID_EMAIL": 5012,
|
||||
"EMAIL_REQUIRED": 5013,
|
||||
"SIGNUP_DISABLED": 5001,
|
||||
# Password strength
|
||||
"INVALID_PASSWORD": 5020,
|
||||
"SMTP_NOT_CONFIGURED": 5025,
|
||||
"INVALID_PASSWORD": 5002,
|
||||
"SMTP_NOT_CONFIGURED": 5007,
|
||||
# Sign Up
|
||||
"USER_ALREADY_EXIST": 5030,
|
||||
"AUTHENTICATION_FAILED_SIGN_UP": 5035,
|
||||
"REQUIRED_EMAIL_PASSWORD_SIGN_UP": 5040,
|
||||
"INVALID_EMAIL_SIGN_UP": 5045,
|
||||
"INVALID_EMAIL_MAGIC_SIGN_UP": 5050,
|
||||
"MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED": 5055,
|
||||
"USER_ALREADY_EXIST": 5003,
|
||||
"AUTHENTICATION_FAILED_SIGN_UP": 5006,
|
||||
"REQUIRED_EMAIL_PASSWORD_SIGN_UP": 5015,
|
||||
"INVALID_EMAIL_SIGN_UP": 5017,
|
||||
"INVALID_EMAIL_MAGIC_SIGN_UP": 5019,
|
||||
"MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED": 5023,
|
||||
# Sign In
|
||||
"USER_DOES_NOT_EXIST": 5060,
|
||||
"AUTHENTICATION_FAILED_SIGN_IN": 5065,
|
||||
"REQUIRED_EMAIL_PASSWORD_SIGN_IN": 5070,
|
||||
"INVALID_EMAIL_SIGN_IN": 5075,
|
||||
"INVALID_EMAIL_MAGIC_SIGN_IN": 5080,
|
||||
"MAGIC_SIGN_IN_EMAIL_CODE_REQUIRED": 5085,
|
||||
# Both Sign in and Sign up for magic
|
||||
"INVALID_MAGIC_CODE": 5090,
|
||||
"EXPIRED_MAGIC_CODE": 5095,
|
||||
"EMAIL_CODE_ATTEMPT_EXHAUSTED": 5100,
|
||||
"USER_DOES_NOT_EXIST": 5004,
|
||||
"AUTHENTICATION_FAILED_SIGN_IN": 5005,
|
||||
"REQUIRED_EMAIL_PASSWORD_SIGN_IN": 5014,
|
||||
"INVALID_EMAIL_SIGN_IN": 5016,
|
||||
"INVALID_EMAIL_MAGIC_SIGN_IN": 5018,
|
||||
"MAGIC_SIGN_IN_EMAIL_CODE_REQUIRED": 5022,
|
||||
# Both Sign in and Sign up
|
||||
"INVALID_MAGIC_CODE": 5008,
|
||||
"EXPIRED_MAGIC_CODE": 5009,
|
||||
# Oauth
|
||||
"GOOGLE_NOT_CONFIGURED": 5105,
|
||||
"GITHUB_NOT_CONFIGURED": 5110,
|
||||
"GOOGLE_OAUTH_PROVIDER_ERROR": 5115,
|
||||
"GITHUB_OAUTH_PROVIDER_ERROR": 5120,
|
||||
"GOOGLE_NOT_CONFIGURED": 5010,
|
||||
"GITHUB_NOT_CONFIGURED": 5011,
|
||||
"GOOGLE_OAUTH_PROVIDER_ERROR": 5021,
|
||||
"GITHUB_OAUTH_PROVIDER_ERROR": 5020,
|
||||
# Reset Password
|
||||
"INVALID_PASSWORD_TOKEN": 5125,
|
||||
"EXPIRED_PASSWORD_TOKEN": 5130,
|
||||
"INVALID_PASSWORD_TOKEN": 5024,
|
||||
"EXPIRED_PASSWORD_TOKEN": 5025,
|
||||
# Change password
|
||||
"INCORRECT_OLD_PASSWORD": 5135,
|
||||
"INVALID_NEW_PASSWORD": 5140,
|
||||
"INCORRECT_OLD_PASSWORD": 5026,
|
||||
"INVALID_NEW_PASSWORD": 5027,
|
||||
# set passowrd
|
||||
"PASSWORD_ALREADY_SET": 5145,
|
||||
"PASSWORD_ALREADY_SET": 5028,
|
||||
# Admin
|
||||
"ADMIN_ALREADY_EXIST": 5150,
|
||||
"REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME": 5155,
|
||||
"INVALID_ADMIN_EMAIL": 5160,
|
||||
"INVALID_ADMIN_PASSWORD": 5165,
|
||||
"REQUIRED_ADMIN_EMAIL_PASSWORD": 5170,
|
||||
"ADMIN_AUTHENTICATION_FAILED": 5175,
|
||||
"ADMIN_USER_ALREADY_EXIST": 5180,
|
||||
"ADMIN_USER_DOES_NOT_EXIST": 5185,
|
||||
"ADMIN_ALREADY_EXIST": 5029,
|
||||
"REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME": 5030,
|
||||
"INVALID_ADMIN_EMAIL": 5031,
|
||||
"INVALID_ADMIN_PASSWORD": 5032,
|
||||
"REQUIRED_ADMIN_EMAIL_PASSWORD": 5033,
|
||||
"ADMIN_AUTHENTICATION_FAILED": 5034,
|
||||
"ADMIN_USER_ALREADY_EXIST": 5035,
|
||||
"ADMIN_USER_DOES_NOT_EXIST": 5036,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -77,13 +77,7 @@ class MagicCodeProvider(CredentialAdapter):
|
||||
current_attempt = data["current_attempt"] + 1
|
||||
|
||||
if data["current_attempt"] > 2:
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"EMAIL_CODE_ATTEMPT_EXHAUSTED"
|
||||
],
|
||||
error_message="EMAIL_CODE_ATTEMPT_EXHAUSTED",
|
||||
payload={"email": self.key},
|
||||
)
|
||||
return key, ""
|
||||
|
||||
value = {
|
||||
"current_attempt": current_attempt,
|
||||
|
||||
@@ -7,7 +7,6 @@ from .views import (
|
||||
ForgotPasswordEndpoint,
|
||||
SetUserPasswordEndpoint,
|
||||
ResetPasswordEndpoint,
|
||||
ChangePasswordEndpoint,
|
||||
# App
|
||||
GitHubCallbackEndpoint,
|
||||
GitHubOauthInitiateEndpoint,
|
||||
@@ -19,8 +18,6 @@ from .views import (
|
||||
SignInAuthEndpoint,
|
||||
SignOutAuthEndpoint,
|
||||
SignUpAuthEndpoint,
|
||||
ForgotPasswordSpaceEndpoint,
|
||||
ResetPasswordSpaceEndpoint,
|
||||
# Space
|
||||
EmailCheckEndpoint,
|
||||
GitHubCallbackSpaceEndpoint,
|
||||
@@ -179,21 +176,6 @@ urlpatterns = [
|
||||
ResetPasswordEndpoint.as_view(),
|
||||
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(
|
||||
"set-password/",
|
||||
SetUserPasswordEndpoint.as_view(),
|
||||
|
||||
@@ -1,42 +1,14 @@
|
||||
# Python imports
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
# Django imports
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def base_host(request, is_admin=False, is_space=False, is_app=False):
|
||||
def base_host(request):
|
||||
"""Utility function to return host / origin from the request"""
|
||||
# Calculate the base origin from request
|
||||
base_origin = str(
|
||||
return (
|
||||
request.META.get("HTTP_ORIGIN")
|
||||
or f"{urlsplit(request.META.get('HTTP_REFERER')).scheme}://{urlsplit(request.META.get('HTTP_REFERER')).netloc}"
|
||||
or f"""{"https" if request.is_secure() else "http"}://{request.get_host()}"""
|
||||
)
|
||||
|
||||
# Admin redirections
|
||||
if is_admin:
|
||||
if settings.ADMIN_BASE_URL:
|
||||
return settings.ADMIN_BASE_URL
|
||||
else:
|
||||
return base_origin + "/god-mode/"
|
||||
|
||||
# Space redirections
|
||||
if is_space:
|
||||
if settings.SPACE_BASE_URL:
|
||||
return settings.SPACE_BASE_URL
|
||||
else:
|
||||
return base_origin + "/spaces/"
|
||||
|
||||
# App Redirection
|
||||
if is_app:
|
||||
if settings.APP_BASE_URL:
|
||||
return settings.APP_BASE_URL
|
||||
else:
|
||||
return base_origin
|
||||
|
||||
return base_origin
|
||||
|
||||
|
||||
def user_ip(request):
|
||||
return str(request.META.get("REMOTE_ADDR"))
|
||||
|
||||
@@ -5,17 +5,12 @@ from django.contrib.auth import login
|
||||
from plane.authentication.utils.host import base_host
|
||||
|
||||
|
||||
def user_login(request, user, is_app=False, is_admin=False, is_space=False):
|
||||
def user_login(request, user):
|
||||
login(request=request, user=user)
|
||||
device_info = {
|
||||
"user_agent": request.META.get("HTTP_USER_AGENT", ""),
|
||||
"ip_address": request.META.get("REMOTE_ADDR", ""),
|
||||
"domain": base_host(
|
||||
request=request,
|
||||
is_app=is_app,
|
||||
is_admin=is_admin,
|
||||
is_space=is_space,
|
||||
),
|
||||
"domain": base_host(request=request),
|
||||
}
|
||||
request.session["device_info"] = device_info
|
||||
request.session.save()
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from .common import (
|
||||
ChangePasswordEndpoint,
|
||||
CSRFTokenEndpoint,
|
||||
ForgotPasswordEndpoint,
|
||||
ResetPasswordEndpoint,
|
||||
SetUserPasswordEndpoint,
|
||||
)
|
||||
|
||||
@@ -48,12 +50,3 @@ 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,
|
||||
)
|
||||
|
||||
@@ -42,8 +42,8 @@ class SignInAuthEndpoint(View):
|
||||
params["next_path"] = str(next_path)
|
||||
# Base URL join
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True),
|
||||
"sign-in?" + urlencode(params),
|
||||
base_host(request=request),
|
||||
"accounts/sign-in?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -66,8 +66,8 @@ class SignInAuthEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True),
|
||||
"sign-in?" + urlencode(params),
|
||||
base_host(request=request),
|
||||
"accounts/sign-in?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -85,8 +85,8 @@ class SignInAuthEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True),
|
||||
"sign-in?" + urlencode(params),
|
||||
base_host(request=request),
|
||||
"accounts/sign-in?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -100,8 +100,8 @@ class SignInAuthEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True),
|
||||
"sign-in?" + urlencode(params),
|
||||
base_host(request=request),
|
||||
"accounts/sign-in?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -111,7 +111,7 @@ class SignInAuthEndpoint(View):
|
||||
)
|
||||
user = provider.authenticate()
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user, is_app=True)
|
||||
user_login(request=request, user=user)
|
||||
# Process workspace and project invitations
|
||||
process_workspace_project_invitations(user=user)
|
||||
# Get the redirection path
|
||||
@@ -121,15 +121,15 @@ class SignInAuthEndpoint(View):
|
||||
path = get_redirection_path(user=user)
|
||||
|
||||
# redirect to referer path
|
||||
url = urljoin(base_host(request=request, is_app=True), path)
|
||||
url = urljoin(base_host(request=request), path)
|
||||
return HttpResponseRedirect(url)
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True),
|
||||
"sign-in?" + urlencode(params),
|
||||
base_host(request=request),
|
||||
"accounts/sign-in?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -152,7 +152,7 @@ class SignUpAuthEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True),
|
||||
base_host(request=request),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -173,7 +173,7 @@ class SignUpAuthEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True),
|
||||
base_host(request=request),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -192,7 +192,7 @@ class SignUpAuthEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True),
|
||||
base_host(request=request),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -207,7 +207,7 @@ class SignUpAuthEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True),
|
||||
base_host(request=request),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -218,7 +218,7 @@ class SignUpAuthEndpoint(View):
|
||||
)
|
||||
user = provider.authenticate()
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user, is_app=True)
|
||||
user_login(request=request, user=user)
|
||||
# Process workspace and project invitations
|
||||
process_workspace_project_invitations(user=user)
|
||||
# Get the redirection path
|
||||
@@ -227,14 +227,14 @@ class SignUpAuthEndpoint(View):
|
||||
else:
|
||||
path = get_redirection_path(user=user)
|
||||
# redirect to referer path
|
||||
url = urljoin(base_host(request=request, is_app=True), path)
|
||||
url = urljoin(base_host(request=request), path)
|
||||
return HttpResponseRedirect(url)
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True),
|
||||
base_host(request=request),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -24,7 +24,7 @@ class GitHubOauthInitiateEndpoint(View):
|
||||
|
||||
def get(self, request):
|
||||
# Get host and next path
|
||||
request.session["host"] = base_host(request=request, is_app=True)
|
||||
request.session["host"] = base_host(request=request)
|
||||
next_path = request.GET.get("next_path")
|
||||
if next_path:
|
||||
request.session["next_path"] = str(next_path)
|
||||
@@ -42,7 +42,7 @@ class GitHubOauthInitiateEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True),
|
||||
base_host(request=request),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -57,7 +57,7 @@ class GitHubOauthInitiateEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True),
|
||||
base_host(request=request),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -110,7 +110,7 @@ class GitHubCallbackEndpoint(View):
|
||||
)
|
||||
user = provider.authenticate()
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user, is_app=True)
|
||||
user_login(request=request, user=user)
|
||||
# Process workspace and project invitations
|
||||
process_workspace_project_invitations(user=user)
|
||||
# Get the redirection path
|
||||
|
||||
@@ -6,14 +6,14 @@ from urllib.parse import urlencode, urljoin
|
||||
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 (
|
||||
@@ -24,7 +24,7 @@ from plane.authentication.adapter.error import (
|
||||
|
||||
class GoogleOauthInitiateEndpoint(View):
|
||||
def get(self, request):
|
||||
request.session["host"] = base_host(request=request, is_app=True)
|
||||
request.session["host"] = base_host(request=request)
|
||||
next_path = request.GET.get("next_path")
|
||||
if next_path:
|
||||
request.session["next_path"] = str(next_path)
|
||||
@@ -42,7 +42,7 @@ class GoogleOauthInitiateEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True),
|
||||
base_host(request=request),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -58,7 +58,7 @@ class GoogleOauthInitiateEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True),
|
||||
base_host(request=request),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -108,7 +108,7 @@ class GoogleCallbackEndpoint(View):
|
||||
)
|
||||
user = provider.authenticate()
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user, is_app=True)
|
||||
user_login(request=request, user=user)
|
||||
# Process workspace and project invitations
|
||||
process_workspace_project_invitations(user=user)
|
||||
# Get the redirection path
|
||||
|
||||
@@ -90,8 +90,8 @@ class MagicSignInEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True),
|
||||
"sign-in?" + urlencode(params),
|
||||
base_host(request=request),
|
||||
"accounts/sign-in?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -104,8 +104,8 @@ class MagicSignInEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True),
|
||||
"sign-in?" + urlencode(params),
|
||||
base_host(request=request),
|
||||
"accounts/sign-in?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -116,7 +116,7 @@ class MagicSignInEndpoint(View):
|
||||
user = provider.authenticate()
|
||||
profile = Profile.objects.get(user=user)
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user, is_app=True)
|
||||
user_login(request=request, user=user)
|
||||
# Process workspace and project invitations
|
||||
process_workspace_project_invitations(user=user)
|
||||
if user.is_password_autoset and profile.is_onboarded:
|
||||
@@ -129,7 +129,7 @@ class MagicSignInEndpoint(View):
|
||||
else str(process_workspace_project_invitations(user=user))
|
||||
)
|
||||
# redirect to referer path
|
||||
url = urljoin(base_host(request=request, is_app=True), path)
|
||||
url = urljoin(base_host(request=request), path)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
except AuthenticationException as e:
|
||||
@@ -137,8 +137,8 @@ class MagicSignInEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True),
|
||||
"sign-in?" + urlencode(params),
|
||||
base_host(request=request),
|
||||
"accounts/sign-in?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -163,7 +163,7 @@ class MagicSignUpEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True),
|
||||
base_host(request=request),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -177,7 +177,7 @@ class MagicSignUpEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True),
|
||||
base_host(request=request),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -188,7 +188,7 @@ class MagicSignUpEndpoint(View):
|
||||
)
|
||||
user = provider.authenticate()
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user, is_app=True)
|
||||
user_login(request=request, user=user)
|
||||
# Process workspace and project invitations
|
||||
process_workspace_project_invitations(user=user)
|
||||
# Get the redirection path
|
||||
@@ -197,7 +197,7 @@ class MagicSignUpEndpoint(View):
|
||||
else:
|
||||
path = get_redirection_path(user=user)
|
||||
# redirect to referer path
|
||||
url = urljoin(base_host(request=request, is_app=True), path)
|
||||
url = urljoin(base_host(request=request), path)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
except AuthenticationException as e:
|
||||
@@ -205,7 +205,7 @@ class MagicSignUpEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True),
|
||||
base_host(request=request),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -1,203 +0,0 @@
|
||||
# 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, is_app=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_app=True),
|
||||
"accounts/reset-password?"
|
||||
+ 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_app=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_app=True),
|
||||
"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_app=True),
|
||||
"accounts/reset-password?" + urlencode(exc.get_error_dict()),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -1,5 +1,5 @@
|
||||
# Python imports
|
||||
from urllib.parse import urljoin
|
||||
from urllib.parse import urlencode, urljoin
|
||||
|
||||
# Django imports
|
||||
from django.views import View
|
||||
@@ -23,9 +23,12 @@ class SignOutAuthEndpoint(View):
|
||||
user.save()
|
||||
# Log the user out
|
||||
logout(request)
|
||||
url = urljoin(base_host(request=request, is_app=True), "sign-in")
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
"accounts/sign-in?" + urlencode({"success": "true"}),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
except Exception:
|
||||
return HttpResponseRedirect(
|
||||
base_host(request=request, is_app=True), "sign-in"
|
||||
base_host(request=request), "accounts/sign-in"
|
||||
)
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
# 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
|
||||
@@ -11,12 +29,15 @@ 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):
|
||||
@@ -34,6 +55,174 @@ 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)
|
||||
@@ -70,7 +259,7 @@ class ChangePasswordEndpoint(APIView):
|
||||
user.set_password(serializer.data.get("new_password"))
|
||||
user.is_password_autoset = False
|
||||
user.save()
|
||||
user_login(user=user, request=request, is_app=True)
|
||||
user_login(user=user, request=request)
|
||||
return Response(
|
||||
{"message": "Password updated successfully"},
|
||||
status=status.HTTP_200_OK,
|
||||
@@ -131,7 +320,7 @@ class SetUserPasswordEndpoint(APIView):
|
||||
user.is_password_autoset = False
|
||||
user.save()
|
||||
# Login the user as the session is invalidated
|
||||
user_login(user=user, request=request, is_app=True)
|
||||
user_login(user=user, request=request)
|
||||
# Return the user
|
||||
serializer = UserSerializer(user)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -37,8 +37,8 @@ class SignInAuthSpaceEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_space=True),
|
||||
"?" + urlencode(params),
|
||||
base_host(request=request),
|
||||
"accounts/sign-in?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -59,8 +59,8 @@ class SignInAuthSpaceEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_space=True),
|
||||
"?" + urlencode(params),
|
||||
base_host(request=request),
|
||||
"spaces/accounts/sign-in?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -78,8 +78,8 @@ class SignInAuthSpaceEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_space=True),
|
||||
"?" + urlencode(params),
|
||||
base_host(request=request),
|
||||
"spaces/accounts/sign-in?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -93,8 +93,8 @@ class SignInAuthSpaceEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_space=True),
|
||||
"?" + urlencode(params),
|
||||
base_host(request=request),
|
||||
"spaces/accounts/sign-in?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -104,11 +104,11 @@ class SignInAuthSpaceEndpoint(View):
|
||||
)
|
||||
user = provider.authenticate()
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user, is_space=True)
|
||||
user_login(request=request, user=user)
|
||||
# redirect to next path
|
||||
url = urljoin(
|
||||
base_host(request=request, is_space=True),
|
||||
str(next_path) if next_path else "",
|
||||
base_host(request=request),
|
||||
str(next_path) if next_path else "/",
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
except AuthenticationException as e:
|
||||
@@ -116,8 +116,8 @@ class SignInAuthSpaceEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_space=True),
|
||||
"?" + urlencode(params),
|
||||
base_host(request=request),
|
||||
"spaces/accounts/sign-in?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -140,8 +140,8 @@ class SignUpAuthSpaceEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_space=True),
|
||||
"?" + urlencode(params),
|
||||
base_host(request=request),
|
||||
"spaces?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -161,8 +161,8 @@ class SignUpAuthSpaceEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_space=True),
|
||||
"?" + urlencode(params),
|
||||
base_host(request=request),
|
||||
"spaces?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
# Validate the email
|
||||
@@ -180,8 +180,8 @@ class SignUpAuthSpaceEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_space=True),
|
||||
"?" + urlencode(params),
|
||||
base_host(request=request),
|
||||
"spaces?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -195,8 +195,8 @@ class SignUpAuthSpaceEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_space=True),
|
||||
"?" + urlencode(params),
|
||||
base_host(request=request),
|
||||
"spaces?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -206,11 +206,11 @@ class SignUpAuthSpaceEndpoint(View):
|
||||
)
|
||||
user = provider.authenticate()
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user, is_space=True)
|
||||
user_login(request=request, user=user)
|
||||
# redirect to referer path
|
||||
url = urljoin(
|
||||
base_host(request=request, is_space=True),
|
||||
str(next_path) if next_path else "",
|
||||
base_host(request=request),
|
||||
str(next_path) if next_path else "spaces",
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
except AuthenticationException as e:
|
||||
@@ -218,7 +218,7 @@ class SignUpAuthSpaceEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_space=True),
|
||||
"?" + urlencode(params),
|
||||
base_host(request=request),
|
||||
"spaces?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -21,7 +21,7 @@ class GitHubOauthInitiateSpaceEndpoint(View):
|
||||
|
||||
def get(self, request):
|
||||
# Get host and next path
|
||||
request.session["host"] = base_host(request=request, is_space=True)
|
||||
request.session["host"] = base_host(request=request)
|
||||
next_path = request.GET.get("next_path")
|
||||
if next_path:
|
||||
request.session["next_path"] = str(next_path)
|
||||
@@ -39,7 +39,7 @@ class GitHubOauthInitiateSpaceEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_space=True),
|
||||
base_host(request=request),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -55,7 +55,7 @@ class GitHubOauthInitiateSpaceEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_space=True),
|
||||
base_host(request=request),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -108,10 +108,10 @@ class GitHubCallbackSpaceEndpoint(View):
|
||||
)
|
||||
user = provider.authenticate()
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user, is_space=True)
|
||||
user_login(request=request, user=user)
|
||||
# Process workspace and project invitations
|
||||
# redirect to referer path
|
||||
url = urljoin(base_host, str(next_path) if next_path else "")
|
||||
url = urljoin(base_host, str(next_path) if next_path else "/")
|
||||
return HttpResponseRedirect(url)
|
||||
except AuthenticationException as e:
|
||||
params = e.get_error_dict()
|
||||
|
||||
@@ -19,7 +19,7 @@ from plane.authentication.adapter.error import (
|
||||
|
||||
class GoogleOauthInitiateSpaceEndpoint(View):
|
||||
def get(self, request):
|
||||
request.session["host"] = base_host(request=request, is_space=True)
|
||||
request.session["host"] = base_host(request=request)
|
||||
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, is_space=True),
|
||||
base_host(request=request),
|
||||
"?" + 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, is_space=True),
|
||||
base_host(request=request),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -103,7 +103,7 @@ class GoogleCallbackSpaceEndpoint(View):
|
||||
)
|
||||
user = provider.authenticate()
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user, is_space=True)
|
||||
user_login(request=request, user=user)
|
||||
# redirect to referer path
|
||||
url = urljoin(
|
||||
base_host, str(next_path) if next_path else "/spaces"
|
||||
|
||||
@@ -47,7 +47,7 @@ class MagicGenerateSpaceEndpoint(APIView):
|
||||
exc.get_error_dict(), status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
origin = base_host(request=request, is_space=True)
|
||||
origin = base_host(request=request)
|
||||
email = request.data.get("email", False)
|
||||
try:
|
||||
# Clean up the email
|
||||
@@ -85,8 +85,8 @@ class MagicSignInSpaceEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_space=True),
|
||||
"?" + urlencode(params),
|
||||
base_host(request=request),
|
||||
"spaces/accounts/sign-in?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -98,8 +98,8 @@ class MagicSignInSpaceEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_space=True),
|
||||
"?" + urlencode(params),
|
||||
base_host(request=request),
|
||||
"accounts/sign-in?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -109,15 +109,15 @@ class MagicSignInSpaceEndpoint(View):
|
||||
)
|
||||
user = provider.authenticate()
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user, is_space=True)
|
||||
user_login(request=request, user=user)
|
||||
# redirect to referer path
|
||||
profile = Profile.objects.get(user=user)
|
||||
if user.is_password_autoset and profile.is_onboarded:
|
||||
path = "accounts/set-password"
|
||||
path = "spaces/accounts/set-password"
|
||||
else:
|
||||
# Get the redirection path
|
||||
path = str(next_path) if next_path else ""
|
||||
url = urljoin(base_host(request=request, is_space=True), path)
|
||||
path = str(next_path) if next_path else "spaces"
|
||||
url = urljoin(base_host(request=request), path)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
except AuthenticationException as e:
|
||||
@@ -125,8 +125,8 @@ class MagicSignInSpaceEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_space=True),
|
||||
"?" + urlencode(params),
|
||||
base_host(request=request),
|
||||
"spaces/accounts/sign-in?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -151,8 +151,8 @@ class MagicSignUpSpaceEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_space=True),
|
||||
"?" + urlencode(params),
|
||||
base_host(request=request),
|
||||
"spaces/accounts/sign-in?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -165,7 +165,7 @@ class MagicSignUpSpaceEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_space=True),
|
||||
base_host(request=request),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -176,10 +176,10 @@ class MagicSignUpSpaceEndpoint(View):
|
||||
)
|
||||
user = provider.authenticate()
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user, is_space=True)
|
||||
user_login(request=request, user=user)
|
||||
# redirect to referer path
|
||||
url = urljoin(
|
||||
base_host(request=request, is_space=True),
|
||||
base_host(request=request),
|
||||
str(next_path) if next_path else "spaces",
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -189,7 +189,7 @@ class MagicSignUpSpaceEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_space=True),
|
||||
"?" + urlencode(params),
|
||||
base_host(request=request),
|
||||
"spaces/accounts/sign-in?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
# 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()
|
||||
|
||||
return HttpResponseRedirect(
|
||||
base_host(request=request, is_space=True)
|
||||
)
|
||||
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)
|
||||
@@ -23,10 +23,12 @@ class SignOutAuthSpaceEndpoint(View):
|
||||
user.save()
|
||||
# Log the user out
|
||||
logout(request)
|
||||
return HttpResponseRedirect(
|
||||
base_host(request=request, is_space=True)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
"accounts/sign-in?" + urlencode({"success": "true"}),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
except Exception:
|
||||
return HttpResponseRedirect(
|
||||
base_host(request=request, is_space=True)
|
||||
base_host(request=request), "accounts/sign-in"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.11 on 2024-05-08 09:23
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0064_auto_20240409_1134'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='page',
|
||||
name='description_yjs',
|
||||
field=models.BinaryField(null=True),
|
||||
),
|
||||
]
|
||||
@@ -18,6 +18,7 @@ def get_view_props():
|
||||
class Page(ProjectBaseModel):
|
||||
name = models.CharField(max_length=255)
|
||||
description = models.JSONField(default=dict, blank=True)
|
||||
description_yjs = models.BinaryField(null=True)
|
||||
description_html = models.TextField(blank=True, default="<p></p>")
|
||||
description_stripped = models.TextField(blank=True, null=True)
|
||||
owned_by = models.ForeignKey(
|
||||
|
||||
@@ -106,8 +106,8 @@ class InstanceAdminSignUpEndpoint(View):
|
||||
error_message="INSTANCE_NOT_CONFIGURED",
|
||||
)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_admin=True),
|
||||
"setup?" + urlencode(exc.get_error_dict()),
|
||||
base_host(request=request),
|
||||
"god-mode/setup?" + urlencode(exc.get_error_dict()),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -118,8 +118,8 @@ class InstanceAdminSignUpEndpoint(View):
|
||||
error_message="ADMIN_ALREADY_EXIST",
|
||||
)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_admin=True),
|
||||
"setup?" + urlencode(exc.get_error_dict()),
|
||||
base_host(request=request),
|
||||
"god-mode/setup?" + urlencode(exc.get_error_dict()),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -147,8 +147,8 @@ class InstanceAdminSignUpEndpoint(View):
|
||||
},
|
||||
)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_admin=True),
|
||||
"setup?" + urlencode(exc.get_error_dict()),
|
||||
base_host(request=request),
|
||||
"god-mode/setup?" + urlencode(exc.get_error_dict()),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -169,8 +169,8 @@ class InstanceAdminSignUpEndpoint(View):
|
||||
},
|
||||
)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_admin=True),
|
||||
"setup?" + urlencode(exc.get_error_dict()),
|
||||
base_host(request=request),
|
||||
"god-mode/setup?" + urlencode(exc.get_error_dict()),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -191,8 +191,8 @@ class InstanceAdminSignUpEndpoint(View):
|
||||
},
|
||||
)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_admin=True),
|
||||
"setup?" + urlencode(exc.get_error_dict()),
|
||||
base_host(request=request),
|
||||
"god-mode/setup?" + urlencode(exc.get_error_dict()),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
else:
|
||||
@@ -213,8 +213,8 @@ class InstanceAdminSignUpEndpoint(View):
|
||||
},
|
||||
)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_admin=True),
|
||||
"setup?" + urlencode(exc.get_error_dict()),
|
||||
base_host(request=request),
|
||||
"god-mode/setup?" + urlencode(exc.get_error_dict()),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -247,8 +247,8 @@ class InstanceAdminSignUpEndpoint(View):
|
||||
instance.save()
|
||||
|
||||
# get tokens for user
|
||||
user_login(request=request, user=user, is_admin=True)
|
||||
url = urljoin(base_host(request=request, is_admin=True), "general")
|
||||
user_login(request=request, user=user)
|
||||
url = urljoin(base_host(request=request), "god-mode/general")
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
|
||||
@@ -269,8 +269,8 @@ class InstanceAdminSignInEndpoint(View):
|
||||
error_message="INSTANCE_NOT_CONFIGURED",
|
||||
)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_admin=True),
|
||||
"?" + urlencode(exc.get_error_dict()),
|
||||
base_host(request=request),
|
||||
"god-mode/login?" + urlencode(exc.get_error_dict()),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -290,8 +290,8 @@ class InstanceAdminSignInEndpoint(View):
|
||||
},
|
||||
)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_admin=True),
|
||||
"?" + urlencode(exc.get_error_dict()),
|
||||
base_host(request=request),
|
||||
"god-mode/login?" + urlencode(exc.get_error_dict()),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -308,8 +308,8 @@ class InstanceAdminSignInEndpoint(View):
|
||||
},
|
||||
)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_admin=True),
|
||||
"?" + urlencode(exc.get_error_dict()),
|
||||
base_host(request=request),
|
||||
"god-mode/login?" + urlencode(exc.get_error_dict()),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -328,8 +328,8 @@ class InstanceAdminSignInEndpoint(View):
|
||||
},
|
||||
)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_admin=True),
|
||||
"?" + urlencode(exc.get_error_dict()),
|
||||
base_host(request=request),
|
||||
"god-mode/login?" + urlencode(exc.get_error_dict()),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -345,8 +345,8 @@ class InstanceAdminSignInEndpoint(View):
|
||||
},
|
||||
)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_admin=True),
|
||||
"?" + urlencode(exc.get_error_dict()),
|
||||
base_host(request=request),
|
||||
"god-mode/login?" + urlencode(exc.get_error_dict()),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -362,8 +362,8 @@ class InstanceAdminSignInEndpoint(View):
|
||||
},
|
||||
)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_admin=True),
|
||||
"?" + urlencode(exc.get_error_dict()),
|
||||
base_host(request=request),
|
||||
"god-mode/login?" + urlencode(exc.get_error_dict()),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
# settings last active for the user
|
||||
@@ -376,8 +376,8 @@ class InstanceAdminSignInEndpoint(View):
|
||||
user.save()
|
||||
|
||||
# get tokens for user
|
||||
user_login(request=request, user=user, is_admin=True)
|
||||
url = urljoin(base_host(request=request, is_admin=True), "general")
|
||||
user_login(request=request, user=user)
|
||||
url = urljoin(base_host(request=request), "god-mode/general")
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
|
||||
@@ -410,9 +410,12 @@ class InstanceAdminSignOutEndpoint(View):
|
||||
user.save()
|
||||
# Log the user out
|
||||
logout(request)
|
||||
url = urljoin(base_host(request=request, is_admin=True))
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
"accounts/sign-in?" + urlencode({"success": "true"}),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
except Exception:
|
||||
return HttpResponseRedirect(
|
||||
base_host(request=request, is_admin=True)
|
||||
base_host(request=request), "accounts/sign-in"
|
||||
)
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import os
|
||||
|
||||
# Django imports
|
||||
from django.conf import settings
|
||||
|
||||
# Third party imports
|
||||
from rest_framework import status
|
||||
@@ -149,13 +148,9 @@ class InstanceEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
# is smtp configured
|
||||
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
|
||||
|
||||
data["is_smtp_configured"] = (
|
||||
bool(EMAIL_HOST)
|
||||
)
|
||||
instance_data = serializer.data
|
||||
instance_data["workspaces_exist"] = Workspace.objects.count() > 1
|
||||
|
||||
|
||||
@@ -342,8 +342,3 @@ 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("APP_BASE_URL") or os.environ.get("WEB_URL")
|
||||
|
||||
@@ -35,10 +35,10 @@ CORS_ALLOWED_ORIGINS = [
|
||||
"http://127.0.0.1",
|
||||
"http://localhost:3000",
|
||||
"http://127.0.0.1:3000",
|
||||
"http://localhost:3001",
|
||||
"http://127.0.0.1:3001",
|
||||
"http://localhost:3002",
|
||||
"http://127.0.0.1:3002",
|
||||
"http://localhost:4000",
|
||||
"http://127.0.0.1:4000",
|
||||
"http://localhost:3333",
|
||||
"http://127.0.0.1:3333",
|
||||
]
|
||||
CSRF_TRUSTED_ORIGINS = CORS_ALLOWED_ORIGINS
|
||||
CORS_ALLOW_ALL_ORIGINS = True
|
||||
|
||||
@@ -12,6 +12,8 @@ SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||||
|
||||
INSTALLED_APPS += ("scout_apm.django",) # noqa
|
||||
|
||||
# Honor the 'X-Forwarded-Proto' header for request.is_secure()
|
||||
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||||
|
||||
# Scout Settings
|
||||
SCOUT_MONITOR = os.environ.get("SCOUT_MONITOR", False)
|
||||
|
||||
@@ -21,7 +21,6 @@ from rest_framework.viewsets import ModelViewSet
|
||||
# Module imports
|
||||
from plane.utils.exception_logger import log_exception
|
||||
from plane.utils.paginator import BasePaginator
|
||||
from plane.authentication.session import BaseSessionAuthentication
|
||||
|
||||
|
||||
class TimezoneMixin:
|
||||
@@ -50,10 +49,6 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
|
||||
SearchFilter,
|
||||
)
|
||||
|
||||
authentication_classes = [
|
||||
BaseSessionAuthentication,
|
||||
]
|
||||
|
||||
filterset_fields = []
|
||||
|
||||
search_fields = []
|
||||
|
||||
@@ -60,4 +60,6 @@ zxcvbn==4.4.28
|
||||
# timezone
|
||||
pytz==2024.1
|
||||
# jwt
|
||||
PyJWT==2.8.0
|
||||
PyJWT==2.8.0
|
||||
# real-time
|
||||
y-py==0.6.2
|
||||
@@ -1 +1 @@
|
||||
python-3.12.3
|
||||
python-3.11.9
|
||||
@@ -33,19 +33,14 @@ 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:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-frontend:${APP_RELEASE:-stable}
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: ${PULL_POLICY:-always}
|
||||
restart: unless-stopped
|
||||
command: node web/server.js web
|
||||
command: /usr/local/bin/start.sh web/server.js web
|
||||
deploy:
|
||||
replicas: ${WEB_REPLICAS:-1}
|
||||
depends_on:
|
||||
@@ -55,24 +50,22 @@ services:
|
||||
space:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-space:${APP_RELEASE:-stable}
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: ${PULL_POLICY:-always}
|
||||
restart: unless-stopped
|
||||
command: node space/server.js space
|
||||
command: /usr/local/bin/start.sh space/server.js space
|
||||
deploy:
|
||||
replicas: ${SPACE_REPLICAS:-1}
|
||||
depends_on:
|
||||
- api
|
||||
- worker
|
||||
- web
|
||||
|
||||
|
||||
admin:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-admin:${APP_RELEASE:-stable}
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
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:
|
||||
@@ -82,7 +75,6 @@ services:
|
||||
api:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: ${PULL_POLICY:-always}
|
||||
restart: unless-stopped
|
||||
command: ./bin/takeoff
|
||||
@@ -97,7 +89,6 @@ services:
|
||||
worker:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: ${PULL_POLICY:-always}
|
||||
restart: unless-stopped
|
||||
command: ./bin/worker
|
||||
@@ -111,7 +102,6 @@ services:
|
||||
beat-worker:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: ${PULL_POLICY:-always}
|
||||
restart: unless-stopped
|
||||
command: ./bin/beat
|
||||
@@ -125,7 +115,6 @@ services:
|
||||
migrator:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: ${PULL_POLICY:-always}
|
||||
restart: no
|
||||
command: >
|
||||
@@ -145,7 +134,6 @@ services:
|
||||
command: postgres -c 'max_connections=1000'
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
|
||||
plane-redis:
|
||||
<<: *app-env
|
||||
image: redis:7.2.4-alpine
|
||||
@@ -156,7 +144,7 @@ services:
|
||||
|
||||
plane-minio:
|
||||
<<: *app-env
|
||||
image: minio/minio:latest
|
||||
image: minio/minio
|
||||
pull_policy: if_not_present
|
||||
restart: unless-stopped
|
||||
command: server /export --console-address ":9090"
|
||||
@@ -167,7 +155,6 @@ services:
|
||||
proxy:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-proxy:${APP_RELEASE:-stable}
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: ${PULL_POLICY:-always}
|
||||
ports:
|
||||
- ${NGINX_PORT}:80
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
BRANCH=master
|
||||
SCRIPT_DIR=$PWD
|
||||
SERVICE_FOLDER=plane-app
|
||||
PLANE_INSTALL_DIR=$PWD/$SERVICE_FOLDER
|
||||
PLANE_INSTALL_DIR=$PWD/plane-app
|
||||
export APP_RELEASE=$BRANCH
|
||||
export DOCKERHUB_USER=makeplane
|
||||
export PULL_POLICY=always
|
||||
@@ -141,7 +140,7 @@ function download() {
|
||||
function startServices() {
|
||||
/bin/bash -c "$COMPOSE_CMD -f $DOCKER_FILE_PATH --env-file=$DOCKER_ENV_PATH up -d --quiet-pull"
|
||||
|
||||
local migrator_container_id=$(docker container ls -aq -f "name=$SERVICE_FOLDER-migrator")
|
||||
local migrator_container_id=$(docker container ls -aq -f "name=plane-app-migrator")
|
||||
if [ -n "$migrator_container_id" ]; then
|
||||
local idx=0
|
||||
while docker inspect --format='{{.State.Status}}' $migrator_container_id | grep -q "running"; do
|
||||
@@ -169,7 +168,7 @@ function startServices() {
|
||||
fi
|
||||
fi
|
||||
|
||||
local api_container_id=$(docker container ls -q -f "name=$SERVICE_FOLDER-api")
|
||||
local api_container_id=$(docker container ls -q -f "name=plane-app-api")
|
||||
local idx2=0
|
||||
while ! docker logs $api_container_id 2>&1 | grep -m 1 -i "Application startup complete" | grep -q ".";
|
||||
do
|
||||
@@ -409,8 +408,7 @@ fi
|
||||
# REMOVE SPECIAL CHARACTERS FROM BRANCH NAME
|
||||
if [ "$BRANCH" != "master" ];
|
||||
then
|
||||
SERVICE_FOLDER=plane-app-$(echo $BRANCH | sed -r 's@(\/|" "|\.)@-@g')
|
||||
PLANE_INSTALL_DIR=$PWD/$SERVICE_FOLDER
|
||||
PLANE_INSTALL_DIR=$PWD/plane-app-$(echo $BRANCH | sed -r 's@(\/|" "|\.)@-@g')
|
||||
fi
|
||||
mkdir -p $PLANE_INSTALL_DIR/archive
|
||||
|
||||
|
||||
@@ -43,6 +43,3 @@ FILE_SIZE_LIMIT=5242880
|
||||
|
||||
# Gunicorn Workers
|
||||
GUNICORN_WORKERS=1
|
||||
|
||||
# UNCOMMENT `DOCKER_PLATFORM` IF YOU ARE ON `ARM64` AND DOCKER IMAGE IS NOT AVAILABLE FOR RESPECTIVE `APP_RELEASE`
|
||||
# DOCKER_PLATFORM=linux/amd64
|
||||
+3
-2
@@ -7,7 +7,7 @@ services:
|
||||
args:
|
||||
DOCKER_BUILDKIT: 1
|
||||
restart: always
|
||||
command: node web/server.js web
|
||||
command: /usr/local/bin/start.sh web/server.js web
|
||||
depends_on:
|
||||
- api
|
||||
|
||||
@@ -32,7 +32,7 @@ services:
|
||||
args:
|
||||
DOCKER_BUILDKIT: 1
|
||||
restart: always
|
||||
command: node space/server.js space
|
||||
command: /usr/local/bin/start.sh space/server.js space
|
||||
depends_on:
|
||||
- api
|
||||
- web
|
||||
@@ -134,6 +134,7 @@ 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
|
||||
|
||||
@@ -12,18 +12,17 @@ import { insertContentAtSavedSelection } from "src/helpers/insert-content-at-cur
|
||||
import { EditorMenuItemNames, getEditorMenuItems } from "src/ui/menus/menu-items";
|
||||
import { EditorRefApi } from "src/types/editor-ref-api";
|
||||
import { IMarking, scrollSummary } from "src/helpers/scroll-to-node";
|
||||
|
||||
interface CustomEditorProps {
|
||||
export interface CustomEditorProps {
|
||||
id?: string;
|
||||
uploadFile: UploadImage;
|
||||
restoreFile: RestoreImage;
|
||||
deleteFile: DeleteImage;
|
||||
cancelUploadImage?: () => void;
|
||||
initialValue: string;
|
||||
initialValue?: string;
|
||||
editorClassName: string;
|
||||
// undefined when prop is not passed, null if intentionally passed to stop
|
||||
// swr syncing
|
||||
value: string | null | undefined;
|
||||
value?: string | null | undefined;
|
||||
onChange?: (json: object, html: string) => void;
|
||||
extensions?: any;
|
||||
editorProps?: EditorProps;
|
||||
|
||||
@@ -24,6 +24,7 @@ export * from "src/ui/menus/menu-items";
|
||||
export * from "src/lib/editor-commands";
|
||||
|
||||
// types
|
||||
export type { CustomEditorProps } from "src/hooks/use-editor";
|
||||
export type { DeleteImage } from "src/types/delete-image";
|
||||
export type { UploadImage } from "src/types/upload-image";
|
||||
export type { EditorRefApi, EditorReadOnlyRefApi } from "src/types/editor-ref-api";
|
||||
|
||||
@@ -97,9 +97,6 @@ const replaceCodeBlockWithContent = (editor: Editor) => {
|
||||
const startPos = pos;
|
||||
const endPos = pos + node.nodeSize;
|
||||
const textContent = node.textContent;
|
||||
if (textContent.length === 0) {
|
||||
editor.chain().focus().toggleCodeBlock().run();
|
||||
}
|
||||
replaceCodeBlock(startPos, endPos, textContent);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -218,21 +218,15 @@ export const Table = Node.create({
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
Tab: () => {
|
||||
if (this.editor.isActive("table")) {
|
||||
if (this.editor.isActive("listItem") || this.editor.isActive("taskItem")) {
|
||||
return false;
|
||||
}
|
||||
if (this.editor.commands.goToNextCell()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!this.editor.can().addRowAfter()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.editor.chain().addRowAfter().goToNextCell().run();
|
||||
if (this.editor.commands.goToNextCell()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
if (!this.editor.can().addRowAfter()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.editor.chain().addRowAfter().goToNextCell().run();
|
||||
},
|
||||
"Shift-Tab": () => this.editor.commands.goToPreviousCell(),
|
||||
Backspace: deleteTableWhenAllCellsSelected,
|
||||
|
||||
@@ -15,6 +15,7 @@ export function CoreEditorProps(editorClassName: string): EditorProps {
|
||||
if (["ArrowUp", "ArrowDown", "Enter"].includes(event.key)) {
|
||||
const slashCommand = document.querySelector("#slash-command");
|
||||
if (slashCommand) {
|
||||
console.log("registered");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,12 +34,17 @@
|
||||
"@plane/ui": "*",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
"@tiptap/core": "^2.1.13",
|
||||
"@tiptap/extension-collaboration": "^2.3.2",
|
||||
"@tiptap/pm": "^2.1.13",
|
||||
"@tiptap/suggestion": "^2.1.13",
|
||||
"lucide-react": "^0.378.0",
|
||||
"react-popper": "^2.3.0",
|
||||
"tippy.js": "^6.3.7",
|
||||
"uuid": "^9.0.1"
|
||||
"uuid": "^9.0.1",
|
||||
"y-indexeddb": "^9.0.12",
|
||||
"y-prosemirror": "^1.2.5",
|
||||
"y-protocols": "^1.0.6",
|
||||
"yjs": "^13.6.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "18.15.3",
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { useLayoutEffect, useMemo } from "react";
|
||||
import {
|
||||
DeleteImage,
|
||||
EditorRefApi,
|
||||
IMentionHighlight,
|
||||
IMentionSuggestion,
|
||||
RestoreImage,
|
||||
UploadImage,
|
||||
useEditor,
|
||||
} from "@plane/editor-core";
|
||||
import * as Y from "yjs";
|
||||
import { CollaborationProvider } from "src/providers/collaboration-provider";
|
||||
import { DocumentEditorExtensions } from "src/ui/extensions";
|
||||
import { IndexeddbPersistence } from "y-indexeddb";
|
||||
import { EditorProps } from "@tiptap/pm/view";
|
||||
|
||||
type DocumentEditorProps = {
|
||||
id?: string;
|
||||
uploadFile: UploadImage;
|
||||
restoreFile: RestoreImage;
|
||||
deleteFile: DeleteImage;
|
||||
cancelUploadImage?: () => void;
|
||||
value: Uint8Array;
|
||||
editorClassName: string;
|
||||
onChange: (binaryString: string, html: string) => void;
|
||||
extensions?: any;
|
||||
editorProps?: EditorProps;
|
||||
forwardedRef?: React.MutableRefObject<EditorRefApi | null>;
|
||||
mentionHandler: {
|
||||
highlights: () => Promise<IMentionHighlight[]>;
|
||||
suggestions?: () => Promise<IMentionSuggestion[]>;
|
||||
};
|
||||
handleEditorReady?: (value: boolean) => void;
|
||||
placeholder?: string | ((isFocused: boolean, value: string) => string);
|
||||
setHideDragHandleFunction: (hideDragHandlerFromDragDrop: () => void) => void;
|
||||
tabIndex?: number;
|
||||
};
|
||||
|
||||
export const useDocumentEditor = ({
|
||||
uploadFile,
|
||||
id = "",
|
||||
deleteFile,
|
||||
cancelUploadImage,
|
||||
editorProps = {},
|
||||
value,
|
||||
editorClassName,
|
||||
onChange,
|
||||
forwardedRef,
|
||||
tabIndex,
|
||||
restoreFile,
|
||||
handleEditorReady,
|
||||
mentionHandler,
|
||||
placeholder,
|
||||
setHideDragHandleFunction,
|
||||
}: DocumentEditorProps) => {
|
||||
const provider = useMemo(
|
||||
() =>
|
||||
new CollaborationProvider({
|
||||
name: id,
|
||||
onChange,
|
||||
}),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[id]
|
||||
);
|
||||
|
||||
const yDoc = useMemo(() => {
|
||||
if (value.byteLength !== 0) Y.applyUpdate(provider.document, value);
|
||||
return provider.document;
|
||||
}, [value, provider.document]);
|
||||
console.log("yDoc", yDoc);
|
||||
|
||||
// indexedDB provider
|
||||
useLayoutEffect(() => {
|
||||
const localProvider = new IndexeddbPersistence(id, provider.document);
|
||||
return () => {
|
||||
localProvider?.destroy();
|
||||
};
|
||||
}, [provider, id]);
|
||||
|
||||
const editor = useEditor({
|
||||
id,
|
||||
editorProps,
|
||||
editorClassName,
|
||||
restoreFile,
|
||||
uploadFile,
|
||||
deleteFile,
|
||||
cancelUploadImage,
|
||||
handleEditorReady,
|
||||
forwardedRef,
|
||||
mentionHandler,
|
||||
extensions: DocumentEditorExtensions({
|
||||
uploadFile,
|
||||
setHideDragHandle: setHideDragHandleFunction,
|
||||
provider,
|
||||
}),
|
||||
placeholder,
|
||||
tabIndex,
|
||||
});
|
||||
|
||||
return editor;
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
import * as Y from "yjs";
|
||||
|
||||
export interface CompleteCollaboratorProviderConfiguration {
|
||||
/**
|
||||
* The identifier/name of your document
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* The actual Y.js document
|
||||
*/
|
||||
document: Y.Doc;
|
||||
/**
|
||||
* onChange callback
|
||||
*/
|
||||
onChange: (binaryString: string, html: string) => void;
|
||||
}
|
||||
|
||||
export type CollaborationProviderConfiguration = Required<Pick<CompleteCollaboratorProviderConfiguration, "name">> &
|
||||
Partial<CompleteCollaboratorProviderConfiguration>;
|
||||
|
||||
export class CollaborationProvider {
|
||||
public configuration: CompleteCollaboratorProviderConfiguration = {
|
||||
name: "",
|
||||
// @ts-expect-error cannot be undefined
|
||||
document: undefined,
|
||||
onChange: () => {},
|
||||
};
|
||||
|
||||
intervals: any = {
|
||||
forceSync: null,
|
||||
};
|
||||
|
||||
timeoutId: any;
|
||||
|
||||
constructor(configuration: CollaborationProviderConfiguration) {
|
||||
this.setConfiguration(configuration);
|
||||
|
||||
this.timeoutId = null;
|
||||
|
||||
this.configuration.document = configuration.document ?? new Y.Doc();
|
||||
this.document.on("update", this.documentUpdateHandler.bind(this));
|
||||
}
|
||||
|
||||
public setConfiguration(configuration: Partial<CompleteCollaboratorProviderConfiguration> = {}): void {
|
||||
this.configuration = {
|
||||
...this.configuration,
|
||||
...configuration,
|
||||
};
|
||||
}
|
||||
|
||||
get document() {
|
||||
return this.configuration.document;
|
||||
}
|
||||
|
||||
documentUpdateHandler(update: Uint8Array, origin: any) {
|
||||
if (origin === this) return;
|
||||
|
||||
// debounce onChange call
|
||||
if (this.timeoutId !== null) clearTimeout(this.timeoutId);
|
||||
|
||||
this.timeoutId = setTimeout(() => {
|
||||
const docAsUint8Array = Y.encodeStateAsUpdate(this.document);
|
||||
const base64Doc = Buffer.from(docAsUint8Array).toString("base64");
|
||||
// const base64Doc = Buffer.from(update).toString("base64");
|
||||
|
||||
this.configuration.onChange?.(base64Doc, "<p></p>");
|
||||
this.timeoutId = null;
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,20 @@ import { IssueWidgetPlaceholder } from "src/ui/extensions/widgets/issue-embed-wi
|
||||
|
||||
import { SlashCommand, DragAndDrop } from "@plane/editor-extensions";
|
||||
import { UploadImage } from "@plane/editor-core";
|
||||
import { CollaborationProvider } from "src/providers/collaboration-provider";
|
||||
import Collaboration from "@tiptap/extension-collaboration";
|
||||
|
||||
type TArguments = {
|
||||
uploadFile: UploadImage;
|
||||
setHideDragHandle?: (hideDragHandlerFromDragDrop: () => void) => void;
|
||||
provider: CollaborationProvider;
|
||||
};
|
||||
|
||||
export const DocumentEditorExtensions = ({ uploadFile, setHideDragHandle }: TArguments) => [
|
||||
export const DocumentEditorExtensions = ({ uploadFile, setHideDragHandle, provider }: TArguments) => [
|
||||
SlashCommand(uploadFile),
|
||||
DragAndDrop(setHideDragHandle),
|
||||
IssueWidgetPlaceholder(),
|
||||
Collaboration.configure({
|
||||
document: provider.document,
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -4,17 +4,16 @@ import {
|
||||
DeleteImage,
|
||||
RestoreImage,
|
||||
getEditorClassNames,
|
||||
useEditor,
|
||||
EditorRefApi,
|
||||
IMentionHighlight,
|
||||
IMentionSuggestion,
|
||||
} from "@plane/editor-core";
|
||||
import { DocumentEditorExtensions } from "src/ui/extensions";
|
||||
import { PageRenderer } from "src/ui/components/page-renderer";
|
||||
import { useDocumentEditor } from "src/hooks/use-document-editor";
|
||||
|
||||
interface IDocumentEditor {
|
||||
initialValue: string;
|
||||
value?: string;
|
||||
id: string;
|
||||
value: Uint8Array;
|
||||
fileHandler: {
|
||||
cancel: () => void;
|
||||
delete: DeleteImage;
|
||||
@@ -24,7 +23,7 @@ interface IDocumentEditor {
|
||||
handleEditorReady?: (value: boolean) => void;
|
||||
containerClassName?: string;
|
||||
editorClassName?: string;
|
||||
onChange: (json: object, html: string) => void;
|
||||
onChange: (binaryString: string, html: string) => void;
|
||||
forwardedRef?: React.MutableRefObject<EditorRefApi | null>;
|
||||
mentionHandler: {
|
||||
highlights: () => Promise<IMentionHighlight[]>;
|
||||
@@ -37,8 +36,9 @@ interface IDocumentEditor {
|
||||
const DocumentEditor = (props: IDocumentEditor) => {
|
||||
const {
|
||||
onChange,
|
||||
initialValue,
|
||||
id,
|
||||
value,
|
||||
// value,
|
||||
fileHandler,
|
||||
containerClassName,
|
||||
editorClassName = "",
|
||||
@@ -56,26 +56,22 @@ const DocumentEditor = (props: IDocumentEditor) => {
|
||||
const setHideDragHandleFunction = (hideDragHandlerFromDragDrop: () => void) => {
|
||||
setHideDragHandleOnMouseLeave(() => hideDragHandlerFromDragDrop);
|
||||
};
|
||||
// use editor
|
||||
const editor = useEditor({
|
||||
onChange(json, html) {
|
||||
onChange(json, html);
|
||||
},
|
||||
|
||||
// use document editor
|
||||
const editor = useDocumentEditor({
|
||||
id,
|
||||
editorClassName,
|
||||
restoreFile: fileHandler.restore,
|
||||
uploadFile: fileHandler.upload,
|
||||
deleteFile: fileHandler.delete,
|
||||
cancelUploadImage: fileHandler.cancel,
|
||||
initialValue,
|
||||
value,
|
||||
onChange,
|
||||
handleEditorReady,
|
||||
forwardedRef,
|
||||
mentionHandler,
|
||||
extensions: DocumentEditorExtensions({
|
||||
uploadFile: fileHandler.upload,
|
||||
setHideDragHandle: setHideDragHandleFunction,
|
||||
}),
|
||||
placeholder,
|
||||
setHideDragHandleFunction,
|
||||
tabIndex,
|
||||
});
|
||||
|
||||
|
||||
@@ -14,21 +14,6 @@ export interface DragHandleOptions {
|
||||
};
|
||||
}
|
||||
|
||||
export const DragAndDrop = (setHideDragHandle?: (hideDragHandlerFromDragDrop: () => void) => void) =>
|
||||
Extension.create({
|
||||
name: "dragAndDrop",
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
DragHandle({
|
||||
dragHandleWidth: 24,
|
||||
scrollThreshold: { up: 300, down: 100 },
|
||||
setHideDragHandle,
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
function createDragHandleElement(): HTMLElement {
|
||||
const dragHandleElement = document.createElement("div");
|
||||
dragHandleElement.draggable = true;
|
||||
@@ -64,31 +49,23 @@ function absoluteRect(node: Element) {
|
||||
}
|
||||
|
||||
function nodeDOMAtCoords(coords: { x: number; y: number }) {
|
||||
const elements = document.elementsFromPoint(coords.x, coords.y);
|
||||
const generalSelectors = [
|
||||
"li",
|
||||
"p:not(:first-child)",
|
||||
".code-block",
|
||||
"blockquote",
|
||||
"h1, h2, h3",
|
||||
".table-wrapper",
|
||||
"[data-type=horizontalRule]",
|
||||
].join(", ");
|
||||
|
||||
for (const elem of elements) {
|
||||
// if the element is a <p> tag that is the first child of a td or th
|
||||
if (
|
||||
(elem.matches("td > p:first-child") || elem.matches("th > p:first-child")) &&
|
||||
elem?.textContent?.trim() !== ""
|
||||
) {
|
||||
return elem; // Return only if p tag is not empty
|
||||
}
|
||||
// apply general selector
|
||||
if (elem.matches(generalSelectors)) {
|
||||
return elem;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return document
|
||||
.elementsFromPoint(coords.x, coords.y)
|
||||
.find(
|
||||
(elem: Element) =>
|
||||
elem.parentElement?.matches?.(".ProseMirror") ||
|
||||
elem.matches(
|
||||
[
|
||||
"li",
|
||||
"p:not(:first-child)",
|
||||
".code-block",
|
||||
"blockquote",
|
||||
"h1, h2, h3",
|
||||
"table",
|
||||
"[data-type=horizontalRule]",
|
||||
].join(", ")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function nodePosAtDOM(node: Element, view: EditorView, options: DragHandleOptions) {
|
||||
@@ -109,19 +86,15 @@ function nodePosAtDOMForBlockquotes(node: Element, view: EditorView) {
|
||||
})?.inside;
|
||||
}
|
||||
|
||||
function calcNodePos(pos: number, view: EditorView, node: Element) {
|
||||
function calcNodePos(pos: number, view: EditorView) {
|
||||
const maxPos = view.state.doc.content.size;
|
||||
const safePos = Math.max(0, Math.min(pos, maxPos));
|
||||
const $pos = view.state.doc.resolve(safePos);
|
||||
|
||||
if ($pos.depth > 1) {
|
||||
if (node.matches("ul:not([data-type=taskList]) li, ol li")) {
|
||||
// only for nested lists
|
||||
const newPos = $pos.before($pos.depth);
|
||||
return Math.max(0, Math.min(newPos, maxPos));
|
||||
}
|
||||
const newPos = $pos.before($pos.depth);
|
||||
return Math.max(0, Math.min(newPos, maxPos));
|
||||
}
|
||||
|
||||
return safePos;
|
||||
}
|
||||
|
||||
@@ -141,12 +114,12 @@ function DragHandle(options: DragHandleOptions) {
|
||||
|
||||
let draggedNodePos = nodePosAtDOM(node, view, options);
|
||||
if (draggedNodePos == null || draggedNodePos < 0) return;
|
||||
draggedNodePos = calcNodePos(draggedNodePos, view, node);
|
||||
draggedNodePos = calcNodePos(draggedNodePos, view);
|
||||
|
||||
const { from, to } = view.state.selection;
|
||||
const diff = from - to;
|
||||
|
||||
const fromSelectionPos = calcNodePos(from, view, node);
|
||||
const fromSelectionPos = calcNodePos(from, view);
|
||||
let differentNodeSelected = false;
|
||||
|
||||
const nodePos = view.state.doc.resolve(fromSelectionPos);
|
||||
@@ -175,19 +148,6 @@ function DragHandle(options: DragHandleOptions) {
|
||||
listType = node.parentElement!.tagName;
|
||||
}
|
||||
|
||||
if (node.matches("blockquote")) {
|
||||
let nodePosForBlockquotes = nodePosAtDOMForBlockquotes(node, view);
|
||||
if (nodePosForBlockquotes === null || nodePosForBlockquotes === undefined) return;
|
||||
|
||||
const docSize = view.state.doc.content.size;
|
||||
nodePosForBlockquotes = Math.max(0, Math.min(nodePosForBlockquotes, docSize));
|
||||
|
||||
if (nodePosForBlockquotes >= 0 && nodePosForBlockquotes <= docSize) {
|
||||
const nodeSelection = NodeSelection.create(view.state.doc, nodePosForBlockquotes);
|
||||
view.dispatch(view.state.tr.setSelection(nodeSelection));
|
||||
}
|
||||
}
|
||||
|
||||
const slice = view.state.selection.content();
|
||||
const { dom, text } = __serializeForClipboard(view, slice);
|
||||
|
||||
@@ -230,7 +190,7 @@ function DragHandle(options: DragHandleOptions) {
|
||||
if (nodePos === null || nodePos === undefined) return;
|
||||
|
||||
// Adjust the nodePos to point to the start of the node, ensuring NodeSelection can be applied
|
||||
nodePos = calcNodePos(nodePos, view, node);
|
||||
nodePos = calcNodePos(nodePos, view);
|
||||
|
||||
// Use NodeSelection to select the node at the calculated position
|
||||
const nodeSelection = NodeSelection.create(view.state.doc, nodePos);
|
||||
@@ -319,11 +279,9 @@ function DragHandle(options: DragHandleOptions) {
|
||||
|
||||
// Li markers
|
||||
if (node.matches("ul:not([data-type=taskList]) li, ol li")) {
|
||||
rect.top += 4;
|
||||
rect.left -= 18;
|
||||
}
|
||||
if (node.matches(".table-wrapper")) {
|
||||
rect.top += 8;
|
||||
}
|
||||
|
||||
rect.width = options.dragHandleWidth;
|
||||
|
||||
@@ -394,3 +352,18 @@ function DragHandle(options: DragHandleOptions) {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export const DragAndDrop = (setHideDragHandle?: (hideDragHandlerFromDragDrop: () => void) => void) =>
|
||||
Extension.create({
|
||||
name: "dragAndDrop",
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
DragHandle({
|
||||
dragHandleWidth: 24,
|
||||
scrollThreshold: { up: 300, down: 100 },
|
||||
setHideDragHandle,
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
@@ -315,10 +315,7 @@ const CommandList = ({ items, command }: { items: CommandItemProps[]; command: a
|
||||
"bg-custom-background-80": index === selectedIndex,
|
||||
}
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
selectItem(index);
|
||||
}}
|
||||
onClick={() => selectItem(index)}
|
||||
>
|
||||
<span className="grid place-items-center flex-shrink-0">{item.icon}</span>
|
||||
<p className="flex-grow truncate">{item.title}</p>
|
||||
|
||||
-3
@@ -43,9 +43,6 @@ 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;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+2
-3
@@ -1,3 +1,2 @@
|
||||
NEXT_PUBLIC_API_BASE_URL=""
|
||||
NEXT_PUBLIC_WEB_BASE_URL=""
|
||||
NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
|
||||
NEXT_PUBLIC_APP_URL=
|
||||
NEXT_PUBLIC_API_BASE_URL=
|
||||
@@ -3,14 +3,12 @@ RUN apk add --no-cache libc6-compat
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
COPY . .
|
||||
RUN yarn global add turbo
|
||||
RUN yarn install
|
||||
|
||||
EXPOSE 4000
|
||||
|
||||
ENV NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
|
||||
ENV NEXT_PUBLIC_DEPLOY_WITH_NGINX=1
|
||||
|
||||
VOLUME [ "/app/node_modules", "/app/space/node_modules"]
|
||||
CMD ["yarn","dev", "--filter=space"]
|
||||
|
||||
+8
-20
@@ -1,6 +1,3 @@
|
||||
# *****************************************************************************
|
||||
# STAGE 1: Build the project
|
||||
# *****************************************************************************
|
||||
FROM node:18-alpine AS builder
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
@@ -10,9 +7,6 @@ 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
|
||||
@@ -27,19 +21,13 @@ COPY --from=builder /app/out/full/ .
|
||||
COPY turbo.json turbo.json
|
||||
|
||||
ARG NEXT_PUBLIC_API_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||
ARG NEXT_PUBLIC_DEPLOY_WITH_NGINX=1
|
||||
|
||||
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_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||
ENV NEXT_PUBLIC_DEPLOY_WITH_NGINX=$NEXT_PUBLIC_DEPLOY_WITH_NGINX
|
||||
|
||||
RUN yarn turbo run build --filter=space
|
||||
|
||||
# *****************************************************************************
|
||||
# STAGE 3: Copy the project and start it
|
||||
# *****************************************************************************
|
||||
FROM node:18-alpine AS runner
|
||||
WORKDIR /app
|
||||
|
||||
@@ -52,13 +40,13 @@ 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_DEPLOY_WITH_NGINX=1
|
||||
|
||||
ARG NEXT_PUBLIC_WEB_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||
ENV NEXT_PUBLIC_DEPLOY_WITH_NGINX=$NEXT_PUBLIC_DEPLOY_WITH_NGINX
|
||||
|
||||
ARG NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
|
||||
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
|
||||
|
||||
@@ -140,11 +140,8 @@ export const OnBoardingForm: React.FC<Props> = observer((props) => {
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<div className="space-y-1 w-full">
|
||||
<label
|
||||
className="text-sm text-onboarding-text-300 font-medium after:content-['*'] after:ml-0.5 after:text-red-500"
|
||||
htmlFor="first_name"
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="first_name">
|
||||
First name
|
||||
</label>
|
||||
<Controller
|
||||
@@ -174,11 +171,8 @@ export const OnBoardingForm: React.FC<Props> = observer((props) => {
|
||||
/>
|
||||
{errors.first_name && <span className="text-sm text-red-500">{errors.first_name.message}</span>}
|
||||
</div>
|
||||
<div className="space-y-1 w-full">
|
||||
<label
|
||||
className="text-sm text-onboarding-text-300 font-medium after:content-['*'] after:ml-0.5 after:text-red-500"
|
||||
htmlFor="last_name"
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="last_name">
|
||||
Last name
|
||||
</label>
|
||||
<Controller
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || "";
|
||||
|
||||
export const SPACE_BASE_PATH = process.env.NEXT_PUBLIC_SPACE_BASE_PATH || "";
|
||||
|
||||
export const WEB_BASE_URL = process.env.NEXT_PUBLIC_WEB_BASE_URL || "";
|
||||
|
||||
export const ASSET_PREFIX = SPACE_BASE_PATH;
|
||||
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? "";
|
||||
|
||||
export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));
|
||||
|
||||
@@ -6,5 +6,5 @@ import { IProfileStore } from "@/store/user/profile.store";
|
||||
export const useUserProfile = (): IProfileStore => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useUserProfile must be used within StoreProvider");
|
||||
return context.user.userProfile;
|
||||
return context.profile;
|
||||
};
|
||||
|
||||
@@ -32,17 +32,13 @@ export const AuthWrapper: FC<TAuthWrapper> = observer((props) => {
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
|
||||
if (pageType === EPageTypes.PUBLIC) return <>{children}</>;
|
||||
|
||||
if (pageType === EPageTypes.INIT) {
|
||||
if (!currentUser?.id) return <>{children}</>;
|
||||
else {
|
||||
if (
|
||||
currentUserProfile &&
|
||||
currentUserProfile?.id &&
|
||||
Boolean(currentUserProfile?.onboarding_step?.profile_complete)
|
||||
)
|
||||
return <>{children}</>;
|
||||
if (currentUserProfile?.id && currentUserProfile?.onboarding_step?.profile_complete) return <>{children}</>;
|
||||
else {
|
||||
router.push(`/onboarding`);
|
||||
return <></>;
|
||||
|
||||
@@ -5,18 +5,17 @@ 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" }], // clickjacking protection
|
||||
headers: [{ key: "X-Frame-Options", value: "SAMEORIGIN" }],
|
||||
},
|
||||
];
|
||||
},
|
||||
basePath: "/spaces",
|
||||
reactStrictMode: false,
|
||||
swcMinify: true,
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
@@ -26,6 +25,7 @@ const nextConfig = {
|
||||
],
|
||||
unoptimized: true,
|
||||
},
|
||||
output: "standalone",
|
||||
};
|
||||
|
||||
if (parseInt(process.env.NEXT_PUBLIC_ENABLE_SENTRY || "0", 10)) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user