Compare commits

..

1 Commits

Author SHA1 Message Date
Palanikannan1437 5746b187c5 feat: a try at flat list type of behavior 2024-05-09 08:18:15 +05:30
190 changed files with 1828 additions and 3099 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ module.exports = {
extends: ["custom"],
settings: {
next: {
rootDir: ["web/", "space/", "admin/"],
rootDir: ["web/", "space/"],
},
},
};
+11 -17
View File
@@ -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
View File
@@ -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
View File
@@ -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
-17
View File
@@ -1,17 +0,0 @@
FROM node:18-alpine
RUN apk add --no-cache libc6-compat
# Set working directory
WORKDIR /app
COPY . .
RUN yarn global add turbo
RUN yarn install
ENV NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
EXPOSE 3000
VOLUME [ "/app/node_modules", "/app/admin/node_modules" ]
CMD ["yarn", "dev", "--filter=admin"]
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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";
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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"];
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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
View File
@@ -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 = parseInt(process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX || "0") === 0 ? "/" : "/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;
+19
View File
@@ -0,0 +1,19 @@
"use client";
import { ReactNode } from "react";
// lib
import { AuthWrapper, InstanceWrapper } from "@/lib/wrappers";
// helpers
import { EAuthenticationPageType, EInstancePageType } from "@/helpers";
interface LoginLayoutProps {
children: ReactNode;
}
const LoginLayout = ({ children }: LoginLayoutProps) => (
<InstanceWrapper pageType={EInstancePageType.POST_SETUP}>
<AuthWrapper authType={EAuthenticationPageType.NOT_AUTHENTICATED}>{children}</AuthWrapper>
</InstanceWrapper>
);
export default LoginLayout;
+18
View File
@@ -0,0 +1,18 @@
"use client";
// layouts
import { DefaultLayout } from "@/layouts";
// components
import { PageHeader } from "@/components/core";
import { InstanceSignInForm } from "./components";
const LoginPage = () => (
<>
<PageHeader title="Setup - God Mode" />
<DefaultLayout>
<InstanceSignInForm />
</DefaultLayout>
</>
);
export default LoginPage;
+14 -20
View File
@@ -1,26 +1,20 @@
"use client";
// layouts
import { DefaultLayout } from "@/layouts";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
// components
import { PageHeader } from "@/components/core";
import { InstanceSignInForm } from "@/components/login";
// lib
import { AuthWrapper, InstanceWrapper } from "@/lib/wrappers";
// helpers
import { EAuthenticationPageType, EInstancePageType } from "@/helpers";
const LoginPage = () => (
<>
<PageHeader title="Login - God Mode" />
<InstanceWrapper pageType={EInstancePageType.POST_SETUP}>
<AuthWrapper authType={EAuthenticationPageType.NOT_AUTHENTICATED}>
<DefaultLayout>
<InstanceSignInForm />
</DefaultLayout>
</AuthWrapper>
</InstanceWrapper>
</>
);
const RootPage = () => {
const router = useRouter();
export default LoginPage;
useEffect(() => router.push("/login"), [router]);
return (
<>
<PageHeader title="Plane - God Mode" />
</>
);
};
export default RootPage;
@@ -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` : `${process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX === "1" ? `/god-mode/` : `/`}`}`;
return (
<div
@@ -131,4 +128,4 @@ export const HelpSection: FC = observer(() => {
</div>
</div>
);
});
};
+1 -1
View File
@@ -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";
-46
View File
@@ -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>
);
-1
View File
@@ -4,4 +4,3 @@ export * from "./controller-input";
export * from "./copy-field";
export * from "./password-strength-meter";
export * from "./banner";
export * from "./empty-state";
+2 -4
View File
@@ -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` : `${process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX === "1" ? `/god-mode/` : `/`}`}`;
if (!isNewUserPopup) return <></>;
return (
+1 -10
View File
@@ -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));
+6
View File
@@ -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";
-3
View File
@@ -1,3 +0,0 @@
export * from "./use-theme";
export * from "./use-instance";
export * from "./use-user";
+1 -1
View File
@@ -4,7 +4,7 @@ import { ReactElement, createContext } from "react";
// mobx store
import { RootStore } from "@/store/root-store";
export let rootStore = new RootStore();
let rootStore = new RootStore();
export const StoreContext = createContext<RootStore>(rootStore);
+2 -2
View File
@@ -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";
+25 -25
View File
@@ -1,14 +1,14 @@
"use client";
import { FC, ReactNode } from "react";
import { useRouter } from "next/navigation";
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";
import { EAuthenticationPageType, EUserStatus } from "@/helpers";
import { redirect } from "next/navigation";
export interface IAuthWrapper {
children: ReactNode;
@@ -16,41 +16,41 @@ export interface IAuthWrapper {
}
export const AuthWrapper: FC<IAuthWrapper> = observer((props) => {
const router = useRouter();
// props
const { children, authType = EAuthenticationPageType.AUTHENTICATED } = props;
// hooks
const { instance } = useInstance();
const { isLoading, currentUser, fetchCurrentUser } = useUser();
const { instance, fetchInstanceAdmins } = useInstance();
const { isLoading, userStatus, currentUser, fetchCurrentUser } = useUser();
const { isLoading: isSWRLoading } = useSWR("CURRENT_USER_DETAILS", () => fetchCurrentUser(), {
useSWR("CURRENT_USER_DETAILS", () => fetchCurrentUser(), {
shouldRetryOnError: false,
});
useSWR("INSTANCE_ADMINS", () => fetchInstanceAdmins(), {
shouldRetryOnError: false,
});
if (isSWRLoading || isLoading)
if (isLoading)
return (
<div className="relative flex h-screen w-full items-center justify-center">
<Spinner />
</div>
);
if (authType === EAuthenticationPageType.NOT_AUTHENTICATED) {
if (currentUser === undefined) return <>{children}</>;
else {
router.push("/general/");
return <></>;
}
}
if (userStatus && userStatus?.status === EUserStatus.ERROR)
return (
<div className="relative flex h-screen w-screen items-center justify-center">
Something went wrong. please try again later
</div>
);
if (authType === EAuthenticationPageType.AUTHENTICATED) {
if (currentUser) return <>{children}</>;
else {
if (instance && instance?.instance?.is_setup_done) {
router.push("/");
return <></>;
} else {
router.push("/setup/");
return <></>;
if ([EAuthenticationPageType.AUTHENTICATED, EAuthenticationPageType.NOT_AUTHENTICATED].includes(authType)) {
if (authType === EAuthenticationPageType.NOT_AUTHENTICATED) {
if (currentUser === undefined) return <>{children}</>;
else redirect("/general/");
} else {
if (currentUser) return <>{children}</>;
else {
if (instance?.instance?.is_setup_done) redirect("/login/");
else redirect("/setup/");
}
}
}
+9 -15
View File
@@ -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";
import { EInstancePageType, EInstanceStatus } from "@/helpers";
type TInstanceWrapper = {
children: ReactNode;
@@ -25,30 +24,25 @@ export const InstanceWrapper: FC<TInstanceWrapper> = observer((props) => {
const searchparams = useSearchParams();
const authEnabled = searchparams.get("auth_enabled") || "1";
// hooks
const { isLoading, instance, fetchInstanceInfo } = useInstance();
const { isLoading, instanceStatus, instance, fetchInstanceInfo } = useInstance();
const { isLoading: isSWRLoading } = useSWR("INSTANCE_INFORMATION", () => fetchInstanceInfo(), {
useSWR("INSTANCE_INFORMATION", () => fetchInstanceInfo(), {
revalidateOnFocus: false,
revalidateIfStale: false,
revalidateOnReconnect: false,
errorRetryCount: 0,
});
if (isSWRLoading || isLoading)
if (isLoading)
return (
<div className="relative flex h-screen w-full items-center justify-center">
<Spinner />
</div>
);
if (!instance) {
if (instanceStatus && instanceStatus?.status === EInstanceStatus.ERROR)
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."
/>
<div className="relative flex h-screen w-screen items-center justify-center">
Something went wrong. please try again later
</div>
);
}
if (instance?.instance?.is_setup_done === false && authEnabled === "1")
return (
+1 -1
View File
@@ -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
View File
@@ -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 -4
View File
@@ -1,6 +1,4 @@
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
// store
import { rootStore } from "@/lib/store-context";
export abstract class APIService {
protected baseURL: string;
@@ -20,8 +18,7 @@ export abstract class APIService {
this.axiosInstance.interceptors.response.use(
(response) => response,
(error) => {
const store = rootStore;
if (error.response && error.response.status === 401 && store.user.currentUser) store.user.reset();
if (error.response && error.response.status === 401) window.location.href = "/login";
return Promise.reject(error);
}
);
+2 -2
View File
@@ -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() {
+1 -3
View File
@@ -18,10 +18,8 @@ export class RootStore {
}
resetOnSignOut() {
localStorage.setItem("theme", "system");
this.theme = new ThemeStore(this);
this.instance = new InstanceStore(this);
this.user = new UserStore(this);
this.theme = new ThemeStore(this);
}
}
+10 -26
View File
@@ -16,8 +16,7 @@ export interface IUserStore {
currentUser: IUser | undefined;
// fetch actions
fetchCurrentUser: () => Promise<IUser>;
reset: () => void;
signOut: () => void;
signOut: () => Promise<void>;
}
export class UserStore implements IUserStore {
@@ -29,6 +28,8 @@ export class UserStore implements IUserStore {
// services
userService;
authService;
// rootStore
rootStore;
constructor(private store: RootStore) {
makeObservable(this, {
@@ -39,11 +40,10 @@ export class UserStore implements IUserStore {
currentUser: observable,
// action
fetchCurrentUser: action,
reset: action,
signOut: action,
});
this.userService = new UserService();
this.authService = new AuthService();
this.rootStore = store;
}
/**
@@ -54,20 +54,11 @@ export class UserStore implements IUserStore {
try {
if (this.currentUser === undefined) this.isLoading = true;
const currentUser = await this.userService.currentUser();
if (currentUser) {
await this.store.instance.fetchInstanceAdmins();
runInAction(() => {
this.isUserLoggedIn = true;
this.currentUser = currentUser;
this.isLoading = false;
});
} else {
runInAction(() => {
this.isUserLoggedIn = false;
this.currentUser = undefined;
this.isLoading = false;
});
}
runInAction(() => {
this.isUserLoggedIn = true;
this.currentUser = currentUser;
this.isLoading = false;
});
return currentUser;
} catch (error: any) {
this.isLoading = false;
@@ -86,14 +77,7 @@ export class UserStore implements IUserStore {
}
};
reset = async () => {
this.isUserLoggedIn = false;
this.currentUser = undefined;
this.isLoading = false;
this.userStatus = undefined;
};
signOut = async () => {
this.store.resetOnSignOut();
this.rootStore.resetOnSignOut();
};
}
+1 -6
View File
@@ -1,7 +1,7 @@
# Backend
# Debug value for api server use it as 0 for production use
DEBUG=0
CORS_ALLOWED_ORIGINS="http://localhost"
CORS_ALLOWED_ORIGINS=""
# Error logs
SENTRY_DSN=""
@@ -44,8 +44,3 @@ WEB_URL="http://localhost"
# Gunicorn Workers
GUNICORN_WORKERS=2
# Base URLs
ADMIN_BASE_URL=
SPACE_BASE_URL=
APP_BASE_URL=
+2
View File
@@ -1,4 +1,6 @@
# Python imports
from urllib.parse import urlparse
import zoneinfo
# Django imports
+5 -13
View File
@@ -602,19 +602,11 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView):
@cache_response(60 * 60 * 24, user=False)
def get(self, request):
files = []
if settings.USE_MINIO:
s3 = boto3.client(
"s3",
endpoint_url=settings.AWS_S3_ENDPOINT_URL,
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
)
else:
s3 = boto3.client(
"s3",
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
)
s3 = boto3.client(
"s3",
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
)
params = {
"Bucket": settings.AWS_STORAGE_BUCKET_NAME,
"Prefix": "static/project-cover/",
-10
View File
@@ -1,7 +1,5 @@
# Django imports
from django.db.models import Case, Count, IntegerField, Q, When
from django.contrib.auth import logout
from django.utils import timezone
# Third party imports
from rest_framework import status
@@ -28,7 +26,6 @@ from plane.db.models import (
from plane.license.models import Instance, InstanceAdmin
from plane.utils.cache import cache_response, invalidate_cache
from plane.utils.paginator import BasePaginator
from plane.authentication.utils.host import user_ip
class UserEndpoint(BaseViewSet):
@@ -169,14 +166,7 @@ class UserEndpoint(BaseViewSet):
"workspace_invite": False,
}
profile.save()
# User log out
user.last_logout_ip = user_ip(request=request)
user.last_logout_time = timezone.now()
user.save()
# Logout the user
logout(request)
return Response(status=status.HTTP_204_NO_CONTENT)
+37 -38
View File
@@ -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,
}
@@ -7,6 +7,12 @@ def auth_exception_handler(exc, context):
response = exception_handler(exc, context)
# Check if an AuthenticationFailed exception is raised.
if isinstance(exc, NotAuthenticated):
response.status_code = 401
# Return 403 if the users me api fails
request = context["request"]
if request.path == "/api/users/me/":
response.status_code = 403
# else return 401
else:
response.status_code = 401
return response
@@ -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,
@@ -46,7 +46,9 @@ class GitHubOAuthProvider(OauthAdapter):
client_id = GITHUB_CLIENT_ID
client_secret = GITHUB_CLIENT_SECRET
redirect_uri = f"""{"https" if request.is_secure() else "http"}://{request.get_host()}/auth/github/callback/"""
redirect_uri = (
f"{request.scheme}://{request.get_host()}/auth/github/callback/"
)
url_params = {
"client_id": client_id,
"redirect_uri": redirect_uri,
@@ -43,7 +43,9 @@ class GoogleOAuthProvider(OauthAdapter):
client_id = GOOGLE_CLIENT_ID
client_secret = GOOGLE_CLIENT_SECRET
redirect_uri = f"""{"https" if request.is_secure() else "http"}://{request.get_host()}/auth/google/callback/"""
redirect_uri = (
f"{request.scheme}://{request.get_host()}/auth/google/callback/"
)
url_params = {
"client_id": client_id,
"scope": self.scope,
-18
View File
@@ -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(),
+3 -31
View File
@@ -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()}"""
or f"{request.scheme}://{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()
@@ -10,13 +10,10 @@ def get_redirection_path(user):
return "onboarding"
# Redirect to the last workspace if the user has last workspace
if (
profile.last_workspace_id
and Workspace.objects.filter(
pk=profile.last_workspace_id,
workspace_member__member_id=user.id,
workspace_member__is_active=True,
).exists()
if profile.last_workspace_id and Workspace.objects.filter(
pk=profile.last_workspace_id,
workspace_member__member_id=user.id,
workspace_member__is_active=True,
):
workspace = Workspace.objects.filter(
pk=profile.last_workspace_id,
@@ -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)
@@ -2,6 +2,7 @@ import uuid
from urllib.parse import urlencode, urljoin
# Django import
from django.core.exceptions import ImproperlyConfigured
from django.http import HttpResponseRedirect
from django.views import View
@@ -24,7 +25,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 +43,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 +58,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 +111,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
@@ -3,17 +3,18 @@ import uuid
from urllib.parse import urlencode, urljoin
# Django import
from django.core.exceptions import ImproperlyConfigured
from django.http import HttpResponseRedirect
from django.views import View
# Module imports
from plane.authentication.provider.oauth.google import GoogleOAuthProvider
from plane.authentication.utils.login import user_login
from plane.authentication.utils.redirection_path import get_redirection_path
from plane.authentication.utils.workspace_project_join import (
process_workspace_project_invitations,
)
# Module imports
from plane.license.models import Instance
from plane.authentication.utils.host import base_host
from plane.authentication.adapter.error import (
@@ -24,7 +25,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 +43,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 +59,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 +109,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"
)
+192 -3
View File
@@ -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)
@@ -3,6 +3,7 @@ import uuid
from urllib.parse import urlencode, urljoin
# Django import
from django.core.exceptions import ImproperlyConfigured
from django.http import HttpResponseRedirect
from django.views import View
@@ -21,7 +22,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 +40,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 +56,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 +109,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"
@@ -2,6 +2,7 @@
from urllib.parse import urlencode, urljoin
# Django imports
from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.core.validators import validate_email
from django.http import HttpResponseRedirect
from django.views import View
@@ -47,7 +48,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 +86,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 +99,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 +110,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 +126,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 +152,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 +166,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 +177,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 +190,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"
)
+33 -30
View File
@@ -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
-5
View File
@@ -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")
+4 -6
View File
@@ -31,14 +31,12 @@ MEDIA_URL = "/uploads/"
MEDIA_ROOT = os.path.join(BASE_DIR, "uploads") # noqa
CORS_ALLOWED_ORIGINS = [
"http://localhost",
"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
+2
View File
@@ -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)
-5
View File
@@ -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 = []
+1 -1
View File
@@ -60,4 +60,4 @@ zxcvbn==4.4.28
# timezone
pytz==2024.1
# jwt
PyJWT==2.8.0
jwt==1.3.1
+1 -1
View File
@@ -1 +1 @@
python-3.12.3
python-3.11.9
+6 -19
View File
@@ -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
+4 -6
View File
@@ -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
-3
View File
@@ -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
-15
View File
@@ -73,20 +73,6 @@ services:
- worker
- web
admin:
build:
context: .
dockerfile: ./admin/Dockerfile.dev
restart: unless-stopped
networks:
- dev_env
volumes:
- ./admin:/app/admin
depends_on:
- api
- worker
- web
api:
build:
context: ./apiserver
@@ -181,4 +167,3 @@ services:
- web
- api
- space
- admin
+3 -2
View File
@@ -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
-1
View File
@@ -2,6 +2,5 @@
export dollar="$"
export http_upgrade="http_upgrade"
export scheme="scheme"
envsubst < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf
exec nginx -g 'daemon off;'
+2 -4
View File
@@ -15,8 +15,6 @@ http {
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Permissions-Policy "interest-cohort=()" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Forwarded-Proto "${dollar}scheme";
add_header Host "${dollar}host";
location / {
proxy_pass http://web:3000/;
@@ -25,8 +23,8 @@ http {
proxy_set_header Connection "upgrade";
}
location /god-mode/ {
proxy_pass http://admin:3000/god-mode/;
location /god-mode {
proxy_pass http://godmode:3000/;
}
location /api/ {
-2
View File
@@ -15,8 +15,6 @@ http {
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Permissions-Policy "interest-cohort=()" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Forwarded-Proto "${dollar}scheme";
add_header Host "${dollar}host";
location / {
proxy_pass http://web:3000/;
@@ -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;
}
@@ -1,6 +1,6 @@
import { Extension } from "@tiptap/core";
import { handleBackspace, handleDelete } from "src/ui/extensions/custom-list-keymap/list-helpers";
import { handleBackspace, handleDelete } from "src/ui/extensions/custom-list-keymap/list-utils";
export type ListKeymapOptions = {
listTypes: Array<{
@@ -1,6 +1,26 @@
import { EditorState } from "@tiptap/pm/state";
import { Command, EditorState, TextSelection } from "@tiptap/pm/state";
import { Editor, getNodeType, getNodeAtPosition, isAtEndOfNode, isAtStartOfNode, isNodeActive } from "@tiptap/core";
import { Node, NodeType } from "@tiptap/pm/model";
import { liftTarget } from "@tiptap/pm/transform";
const isCursorAtFirstListItem = (editor: Editor, typeOrName: string): boolean => {
const { $from } = editor.state.selection;
// const listItem = getNodeType(typeOrName, editor.state.schema);
// Check if the current node is a list item
// console.log($from.parent.type);
// if ($from.parent.type !== listItem) {
// console.log("--------------------");
// return false;
// }
// Check if the cursor is at the start of the list item
if ($from.index($from.depth - 1) === 0) {
return true;
}
return false;
};
const findListItemPos = (typeOrName: string | NodeType, state: EditorState) => {
const { $from } = state.selection;
@@ -29,6 +49,20 @@ const findListItemPos = (typeOrName: string | NodeType, state: EditorState) => {
return { $pos: state.doc.resolve(currentPos), depth: targetDepth };
};
const prevListIsHigher = (typeOrName: string, state: EditorState) => {
const listDepth = getPrevListDepth(typeOrName, state);
const listItemPos = findListItemPos(typeOrName, state);
if (!listItemPos || !listDepth) {
return false;
}
if (listDepth < listItemPos.depth) {
return true;
}
return false;
};
const nextListIsDeeper = (typeOrName: string, state: EditorState) => {
const listDepth = getNextListDepth(typeOrName, state);
const listItemPos = findListItemPos(typeOrName, state);
@@ -63,6 +97,18 @@ const getPrevListDepth = (typeOrName: string, state: EditorState) => {
return false;
}
const [, depth] = getNodeAtPosition(state, typeOrName, listItemPos.$pos.pos - 4);
return depth;
};
const getPrevParentListDepth = (typeOrName: string, state: EditorState) => {
const listItemPos = findListItemPos(typeOrName, state);
if (!listItemPos) {
return false;
}
let depth = 0;
const pos = listItemPos.$pos;
@@ -147,9 +193,11 @@ export const handleBackspace = (editor: Editor, name: string, parentListTypes: s
return false;
}
const isParaSibling = isCurrentParagraphASibling(editor.state);
const isCurrentListItemSublist = prevListIsHigher(name, editor.state);
const currentListItemIsSublist = prevParentListIsHigher(name, editor.state);
const listItemPos = findListItemPos(name, editor.state);
const nextListItemIsSibling = nextListIsSibling(name, editor.state);
// __AUTO_GENERATED_PRINT_VAR_START__
console.log("handleBackspace nextListItemIsSibling: %s", nextListItemIsSibling); // __AUTO_GENERATED_PRINT_VAR_END__
if (!listItemPos) {
return false;
@@ -158,41 +206,119 @@ export const handleBackspace = (editor: Editor, name: string, parentListTypes: s
const currentNode = listItemPos.$pos.node(listItemPos.depth);
const currentListItemHasSubList = listItemHasSubList(name, editor.state, currentNode);
if (currentListItemHasSubList && isCurrentListItemSublist && isParaSibling) {
const isFirstListItemAtDepth = prevListIsHigher(name, editor.state);
// current list is sublist and has sublist and the cursor is at a paragraph node is sibling inside a list item
if (currentListItemHasSubList && currentListItemIsSublist && isParaSibling) {
console.log("1");
return false;
}
if (currentListItemHasSubList && isCurrentListItemSublist) {
editor.chain().liftListItem(name).run();
return editor.commands.joinItemBackward();
if (currentListItemHasSubList && currentListItemIsSublist) {
if (isFirstListItemAtDepth) {
console.log("2a");
editor.chain().liftListItem(name).run();
return editor.commands.joinItemBackward();
}
console.log("2");
return liftListItem(editor.state, editor.view.dispatch);
}
if (isCurrentListItemSublist && nextListItemIsSibling) {
return false;
}
// if (currentListItemIsSublist && nextListItemIsSibling) {
// console.log("3");
// return liftListItem(editor.state, editor.view.dispatch);
// }
if (isCurrentListItemSublist) {
if (currentListItemIsSublist) {
console.log("4");
return liftListItem(editor.state, editor.view.dispatch);
return false;
}
if (currentListItemHasSubList) {
console.log("5");
const { $anchor } = editor.state.selection;
const $listPos = editor.state.doc.resolve($anchor.before() - 1);
const listDescendants: Array<{ node: Node; pos: number }> = [];
$listPos.node().descendants((node, pos) => {
if (node.type.name === name) {
listDescendants.push({ node, pos });
}
});
const lastItem = listDescendants.at(-1);
if (!lastItem) {
return false;
}
const $lastItemPos = editor.state.doc.resolve($listPos.start() + lastItem.pos + 1);
// Check if positions are within the valid range
const startPos = $anchor.start() - 1;
const endPos = $anchor.end() + 1;
if (startPos < 0 || endPos > editor.state.doc.content.size) {
return false; // Invalid position, abort operation
}
return editor.chain().cut({ from: startPos, to: endPos }, $lastItemPos.end()).joinForward().run();
return false;
}
if (hasListItemBefore(name, editor.state)) {
console.log("6");
return editor.chain().liftListItem(name).run();
}
if (!currentListItemHasSubList) {
console.log("7");
return false;
}
console.log("8");
// otherwise in the end, a backspace should
// always just lift the list item if
// joining / merging is not possible
return editor.chain().liftListItem(name).run();
};
/// If the cursor is in a list item (empty or not), attempt to lift the
/// list item out of its parent list.
export const liftListItem: Command = (state, dispatch) => {
let { $cursor } = state.selection as TextSelection;
if (!$cursor) return false;
// Check if the current node is a list item
let listItem = state.schema.nodes.listItem;
// Find the depth of the list item in the current selection
let listItemDepth = $cursor.depth;
while (listItemDepth > 0 && $cursor.node(listItemDepth).type !== listItem) {
listItemDepth--;
}
// If not inside a list item, return false
if ($cursor.node(listItemDepth).type !== listItem) return false;
// Attempt to lift the list item
let range = $cursor.blockRange(),
target = range && liftTarget(range);
// __AUTO_GENERATED_PRINT_VAR_START__
console.log("liftListItem range: %s", range); // __AUTO_GENERATED_PRINT_VAR_END__
if (target == null) {
console.log("==============");
return false;
}
if (dispatch) dispatch(state.tr.lift(range!, target).scrollIntoView());
return true;
};
export const handleDelete = (editor: Editor, name: string) => {
// if the cursor is not inside the current node type
// do nothing and proceed
@@ -237,8 +363,8 @@ const hasListBefore = (editorState: EditorState, name: string, parentListTypes:
return true;
};
const prevListIsHigher = (typeOrName: string, state: EditorState) => {
const listDepth = getPrevListDepth(typeOrName, state);
const prevParentListIsHigher = (typeOrName: string, state: EditorState) => {
const listDepth = getPrevParentListDepth(typeOrName, state);
const listItemPos = findListItemPos(typeOrName, state);
if (!listItemPos || !listDepth) {
@@ -253,14 +379,16 @@ const prevListIsHigher = (typeOrName: string, state: EditorState) => {
};
const nextListIsSibling = (typeOrName: string, state: EditorState) => {
const listDepth = getNextListDepth(typeOrName, state);
const nextListDepth = getNextListDepth(typeOrName, state);
const listItemPos = findListItemPos(typeOrName, state);
// __AUTO_GENERATED_PRINT_VAR_START__
console.log("nextListIsSibling listDepth: %s", nextListDepth, listItemPos?.depth); // __AUTO_GENERATED_PRINT_VAR_END__
if (!listItemPos || !listDepth) {
if (!listItemPos || !nextListDepth) {
return false;
}
if (listDepth === listItemPos.depth) {
if (nextListDepth === listItemPos.depth) {
return true;
}
@@ -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,
+1
View File
@@ -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;
}
}
@@ -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
View File
@@ -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;
};
}

Some files were not shown because too many files have changed in this diff Show More