Compare commits
55
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96565b912a | ||
|
|
2c35461c45 | ||
|
|
9e70073d96 | ||
|
|
854025ec03 | ||
|
|
dcb0de1b4e | ||
|
|
f9d2858209 | ||
|
|
febf19ccc0 | ||
|
|
087d54a261 | ||
|
|
743708a4ee | ||
|
|
c2ce3ada14 | ||
|
|
dbc0d7019b | ||
|
|
2593dc8afc | ||
|
|
9122d34e8b | ||
|
|
7ca7b80c89 | ||
|
|
6b4952ab78 | ||
|
|
803ce802f1 | ||
|
|
28c6025349 | ||
|
|
55a173d3e3 | ||
|
|
18ba4009e0 | ||
|
|
198a2a63f2 | ||
|
|
9eac78ae83 | ||
|
|
2501a8bbab | ||
|
|
f94696b4dc | ||
|
|
2f4cf47630 | ||
|
|
3723ece8d5 | ||
|
|
781c0e8ae4 | ||
|
|
91a66a757a | ||
|
|
4aed6e7aed | ||
|
|
16d8dfc86e | ||
|
|
3355be9c9c | ||
|
|
2ef3c06da0 | ||
|
|
0ad8bf7664 | ||
|
|
88ebda42ff | ||
|
|
2d1201cc92 | ||
|
|
b725c69882 | ||
|
|
74eb50aa1a | ||
|
|
dc77e4afdb | ||
|
|
27315a8aa2 | ||
|
|
da78933c61 | ||
|
|
0ab2a98777 | ||
|
|
40560109b5 | ||
|
|
0037377c8f | ||
|
|
0af55e7bbb | ||
|
|
57eda34082 | ||
|
|
e396424db7 | ||
|
|
243680132e | ||
|
|
547a76ae55 | ||
|
|
2320b33189 | ||
|
|
45bb1153ee | ||
|
|
b8768d7274 | ||
|
|
58bf056ddb | ||
|
|
692f570258 | ||
|
|
11cd9f57d7 | ||
|
|
3a66986785 | ||
|
|
d8ab3e0087 |
+1
-1
@@ -4,7 +4,7 @@ module.exports = {
|
||||
extends: ["custom"],
|
||||
settings: {
|
||||
next: {
|
||||
rootDir: ["web/", "space/"],
|
||||
rootDir: ["web/", "space/", "admin/"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -14,10 +14,10 @@ jobs:
|
||||
space_changed: ${{ steps.changed-files.outputs.space_any_changed }}
|
||||
web_changed: ${{ steps.changed-files.outputs.web_any_changed }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v41
|
||||
uses: tj-actions/changed-files@v44
|
||||
with:
|
||||
files_yaml: |
|
||||
apiserver:
|
||||
@@ -49,9 +49,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: needs.get-changed-files.outputs.apiserver_changed == 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.x" # Specify the Python version you need
|
||||
- name: Install Pylint
|
||||
@@ -66,9 +66,9 @@ jobs:
|
||||
if: needs.get-changed-files.outputs.admin_changed == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
- run: yarn install
|
||||
@@ -79,9 +79,9 @@ jobs:
|
||||
if: needs.get-changed-files.outputs.space_changed == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
- run: yarn install
|
||||
@@ -92,9 +92,9 @@ jobs:
|
||||
if: needs.get-changed-files.outputs.web_changed == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
- run: yarn install
|
||||
@@ -104,9 +104,9 @@ jobs:
|
||||
needs: lint-admin
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
- run: yarn install
|
||||
@@ -116,9 +116,9 @@ jobs:
|
||||
needs: lint-space
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
- run: yarn install
|
||||
@@ -128,9 +128,9 @@ jobs:
|
||||
needs: lint-web
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.x
|
||||
- run: yarn install
|
||||
|
||||
@@ -5,17 +5,17 @@ on:
|
||||
inputs:
|
||||
web-build:
|
||||
required: false
|
||||
description: 'Build Web'
|
||||
description: "Build Web"
|
||||
type: boolean
|
||||
default: true
|
||||
space-build:
|
||||
required: false
|
||||
description: 'Build Space'
|
||||
description: "Build Space"
|
||||
type: boolean
|
||||
default: false
|
||||
admin-build:
|
||||
required: false
|
||||
description: 'Build Admin'
|
||||
description: "Build Admin"
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
echo "BUILD_SPACE=$BUILD_SPACE"
|
||||
echo "BUILD_ADMIN=$BUILD_ADMIN"
|
||||
outputs:
|
||||
web-build: ${{ env.BUILD_WEB}}
|
||||
web-build: ${{ env.BUILD_WEB}}
|
||||
space-build: ${{env.BUILD_SPACE}}
|
||||
admin-build: ${{env.BUILD_ADMIN}}
|
||||
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
node-version: "18"
|
||||
- name: Install AWS cli
|
||||
run: |
|
||||
sudo apt-get update
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
|
||||
FILE_EXPIRY=$(date -u -d "+2 days" +"%Y-%m-%dT%H:%M:%SZ")
|
||||
aws s3 cp $TAR_NAME s3://${{ env.AWS_BUCKET }}/${{github.sha}}/$TAR_NAME --expires $FILE_EXPIRY
|
||||
|
||||
|
||||
feature-build-space:
|
||||
if: ${{ needs.setup-feature-build.outputs.space-build == 'true' }}
|
||||
needs: setup-feature-build
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
AWS_ACCESS_KEY_ID: ${{ vars.FEATURE_PREVIEW_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.FEATURE_PREVIEW_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_BUCKET: ${{ vars.FEATURE_PREVIEW_AWS_BUCKET }}
|
||||
NEXT_PUBLIC_DEPLOY_WITH_NGINX: 1
|
||||
NEXT_PUBLIC_SPACE_BASE_PATH: "/spaces"
|
||||
NEXT_PUBLIC_API_BASE_URL: ${{ vars.FEATURE_PREVIEW_NEXT_PUBLIC_API_BASE_URL }}
|
||||
outputs:
|
||||
do-build: ${{ needs.setup-feature-build.outputs.space-build }}
|
||||
@@ -98,7 +98,7 @@ jobs:
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
node-version: "18"
|
||||
- name: Install AWS cli
|
||||
run: |
|
||||
sudo apt-get update
|
||||
@@ -134,7 +134,7 @@ jobs:
|
||||
AWS_ACCESS_KEY_ID: ${{ vars.FEATURE_PREVIEW_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.FEATURE_PREVIEW_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_BUCKET: ${{ vars.FEATURE_PREVIEW_AWS_BUCKET }}
|
||||
NEXT_PUBLIC_DEPLOY_WITH_NGINX: 1
|
||||
NEXT_PUBLIC_ADMIN_BASE_PATH: "/god-mode"
|
||||
NEXT_PUBLIC_API_BASE_URL: ${{ vars.FEATURE_PREVIEW_NEXT_PUBLIC_API_BASE_URL }}
|
||||
outputs:
|
||||
do-build: ${{ needs.setup-feature-build.outputs.admin-build }}
|
||||
@@ -143,7 +143,7 @@ jobs:
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
node-version: "18"
|
||||
- name: Install AWS cli
|
||||
run: |
|
||||
sudo apt-get update
|
||||
@@ -172,7 +172,13 @@ jobs:
|
||||
|
||||
feature-deploy:
|
||||
if: ${{ always() && (needs.setup-feature-build.outputs.web-build == 'true' || needs.setup-feature-build.outputs.space-build == 'true' || needs.setup-feature-build.outputs.admin-build == 'true') }}
|
||||
needs: [setup-feature-build, feature-build-web, feature-build-space, feature-build-admin]
|
||||
needs:
|
||||
[
|
||||
setup-feature-build,
|
||||
feature-build-web,
|
||||
feature-build-space,
|
||||
feature-build-admin,
|
||||
]
|
||||
name: Feature Deploy
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
|
||||
+3
-2
@@ -1,2 +1,3 @@
|
||||
NEXT_PUBLIC_APP_URL=
|
||||
NEXT_PUBLIC_API_BASE_URL=
|
||||
NEXT_PUBLIC_API_BASE_URL=""
|
||||
NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
|
||||
NEXT_PUBLIC_WEB_BASE_URL=""
|
||||
+30
-7
@@ -1,3 +1,6 @@
|
||||
# *****************************************************************************
|
||||
# STAGE 1: Build the project
|
||||
# *****************************************************************************
|
||||
FROM node:18-alpine AS builder
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
@@ -7,6 +10,9 @@ COPY . .
|
||||
|
||||
RUN turbo prune --scope=admin --docker
|
||||
|
||||
# *****************************************************************************
|
||||
# STAGE 2: Install dependencies & build the project
|
||||
# *****************************************************************************
|
||||
FROM node:18-alpine AS installer
|
||||
|
||||
RUN apk add --no-cache libc6-compat
|
||||
@@ -21,13 +27,25 @@ COPY --from=builder /app/out/full/ .
|
||||
COPY turbo.json turbo.json
|
||||
|
||||
ARG NEXT_PUBLIC_API_BASE_URL=""
|
||||
ARG NEXT_PUBLIC_DEPLOY_WITH_NGINX=1
|
||||
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||
ENV NEXT_PUBLIC_DEPLOY_WITH_NGINX=$NEXT_PUBLIC_DEPLOY_WITH_NGINX
|
||||
|
||||
ARG NEXT_PUBLIC_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
|
||||
|
||||
RUN yarn turbo run build --filter=admin
|
||||
|
||||
# *****************************************************************************
|
||||
# STAGE 3: Copy the project and start it
|
||||
# *****************************************************************************
|
||||
FROM node:18-alpine AS runner
|
||||
WORKDIR /app
|
||||
|
||||
@@ -38,12 +56,17 @@ COPY --from=installer /app/admin/.next/standalone ./
|
||||
COPY --from=installer /app/admin/.next/static ./admin/.next/static
|
||||
COPY --from=installer /app/admin/public ./admin/public
|
||||
|
||||
|
||||
ARG NEXT_PUBLIC_API_BASE_URL=""
|
||||
ARG NEXT_PUBLIC_DEPLOY_WITH_NGINX=1
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||
ENV NEXT_PUBLIC_DEPLOY_WITH_NGINX=$NEXT_PUBLIC_DEPLOY_WITH_NGINX
|
||||
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
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
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"]
|
||||
@@ -6,7 +6,7 @@ import { IFormattedInstanceConfiguration, TInstanceAIConfigurationKeys } from "@
|
||||
// components
|
||||
import { ControllerInput, TControllerInputFormField } from "components/common";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks";
|
||||
import { useInstance } from "@/hooks/store";
|
||||
|
||||
type IInstanceAIForm = {
|
||||
config: IFormattedInstanceConfiguration;
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Loader } from "@plane/ui";
|
||||
import { PageHeader } from "@/components/core";
|
||||
import { InstanceAIForm } from "./components";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks";
|
||||
import { useInstance } from "@/hooks/store";
|
||||
|
||||
const InstanceAIPage = observer(() => {
|
||||
// store
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks";
|
||||
import { useInstance } from "@/hooks/store";
|
||||
// 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";
|
||||
import { useInstance } from "@/hooks/store";
|
||||
// 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";
|
||||
import { useInstance } from "@/hooks/store";
|
||||
// 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";
|
||||
import { useInstance } from "@/hooks/store";
|
||||
// ui
|
||||
import { ToggleSwitch, getButtonStyling } from "@plane/ui";
|
||||
// icons
|
||||
|
||||
@@ -11,7 +11,7 @@ import { PageHeader } from "@/components/core";
|
||||
import { AuthenticationMethodCard } from "../components";
|
||||
import { InstanceGithubConfigForm } from "./components";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks";
|
||||
import { useInstance } from "@/hooks/store";
|
||||
// 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";
|
||||
import { useInstance } from "@/hooks/store";
|
||||
// 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";
|
||||
import { useInstance } from "@/hooks/store";
|
||||
// ui
|
||||
import { ToggleSwitch, getButtonStyling } from "@plane/ui";
|
||||
// icons
|
||||
|
||||
@@ -10,7 +10,7 @@ import { PageHeader } from "@/components/core";
|
||||
import { AuthenticationMethodCard } from "../components";
|
||||
import { InstanceGoogleConfigForm } from "./components";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks";
|
||||
import { useInstance } from "@/hooks/store";
|
||||
// icons
|
||||
import GoogleLogo from "@/public/logos/google-logo.svg";
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import { GoogleConfiguration } from "./google/components";
|
||||
import { GithubConfiguration } from "./github/components";
|
||||
import { PageHeader } from "@/components/core";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks";
|
||||
import { useInstance } from "@/hooks/store";
|
||||
// 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";
|
||||
import { useInstance } from "@/hooks/store";
|
||||
// ui
|
||||
import { Button, CustomSelect, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// components
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Loader } from "@plane/ui";
|
||||
import { PageHeader } from "@/components/core";
|
||||
import { InstanceEmailForm } from "./components";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks";
|
||||
import { useInstance } from "@/hooks/store";
|
||||
|
||||
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";
|
||||
import { useInstance } from "@/hooks/store";
|
||||
|
||||
export interface IGeneralConfigurationForm {
|
||||
instance: IInstance["instance"];
|
||||
|
||||
@@ -5,7 +5,7 @@ import { observer } from "mobx-react-lite";
|
||||
import { PageHeader } from "@/components/core";
|
||||
import { GeneralConfigurationForm } from "./components";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks";
|
||||
import { useInstance } from "@/hooks/store";
|
||||
|
||||
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";
|
||||
import { useInstance } from "@/hooks/store";
|
||||
|
||||
type IInstanceImageConfigForm = {
|
||||
config: IFormattedInstanceConfiguration;
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Loader } from "@plane/ui";
|
||||
import { PageHeader } from "@/components/core";
|
||||
import { InstanceImageConfigForm } from "./components";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks";
|
||||
import { useInstance } from "@/hooks/store";
|
||||
|
||||
const InstanceImagePage = observer(() => {
|
||||
// store
|
||||
|
||||
+28
-30
@@ -7,6 +7,8 @@ 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";
|
||||
|
||||
@@ -14,35 +16,31 @@ interface RootLayoutProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
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>
|
||||
);
|
||||
|
||||
export default RootLayout;
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
"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;
|
||||
@@ -1,18 +0,0 @@
|
||||
"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;
|
||||
+20
-14
@@ -1,20 +1,26 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
// layouts
|
||||
import { DefaultLayout } from "@/layouts";
|
||||
// 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 RootPage = () => {
|
||||
const router = useRouter();
|
||||
const LoginPage = () => (
|
||||
<>
|
||||
<PageHeader title="Login - God Mode" />
|
||||
<InstanceWrapper pageType={EInstancePageType.POST_SETUP}>
|
||||
<AuthWrapper authType={EAuthenticationPageType.NOT_AUTHENTICATED}>
|
||||
<DefaultLayout>
|
||||
<InstanceSignInForm />
|
||||
</DefaultLayout>
|
||||
</AuthWrapper>
|
||||
</InstanceWrapper>
|
||||
</>
|
||||
);
|
||||
|
||||
useEffect(() => router.push("/login"), [router]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader title="Plane - God Mode" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RootPage;
|
||||
export default LoginPage;
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import { FC, useState, useRef } from "react";
|
||||
import { Transition } from "@headlessui/react";
|
||||
import Link from "next/link";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Transition } from "@headlessui/react";
|
||||
import { ExternalLink, FileText, HelpCircle, MoveLeft } from "lucide-react";
|
||||
import { DiscordIcon, GithubIcon, Tooltip } from "@plane/ui";
|
||||
// hooks
|
||||
import { useTheme } from "@/hooks";
|
||||
import { useTheme } from "@/hooks/store";
|
||||
// assets
|
||||
import packageJson from "package.json";
|
||||
import { WEB_BASE_URL } from "@/helpers/common.helper";
|
||||
|
||||
const helpOptions = [
|
||||
{
|
||||
@@ -28,7 +30,7 @@ const helpOptions = [
|
||||
},
|
||||
];
|
||||
|
||||
export const HelpSection: FC = () => {
|
||||
export const HelpSection: FC = observer(() => {
|
||||
// states
|
||||
const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false);
|
||||
// store
|
||||
@@ -36,7 +38,7 @@ export const HelpSection: FC = () => {
|
||||
// refs
|
||||
const helpOptionsRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
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/` : `/`}`}`;
|
||||
const redirectionLink = encodeURI(WEB_BASE_URL + "/create-workspace");
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -128,4 +130,4 @@ export const HelpSection: FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { FC, useEffect, useRef } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useTheme } from "@/hooks";
|
||||
import { useTheme } from "@/hooks/store";
|
||||
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";
|
||||
import { useTheme, useUser } from "@/hooks/store";
|
||||
// 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";
|
||||
import { useTheme } from "@/hooks/store";
|
||||
// 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";
|
||||
import { useTheme } from "@/hooks/store";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
import { Button } from "@plane/ui";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
description?: React.ReactNode;
|
||||
image?: any;
|
||||
primaryButton?: {
|
||||
icon?: any;
|
||||
text: string;
|
||||
onClick: () => void;
|
||||
};
|
||||
secondaryButton?: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const EmptyState: React.FC<Props> = ({
|
||||
title,
|
||||
description,
|
||||
image,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
disabled = false,
|
||||
}) => (
|
||||
<div className={`flex h-full w-full items-center justify-center`}>
|
||||
<div className="flex w-full flex-col items-center text-center">
|
||||
{image && <Image src={image} className="w-52 sm:w-60" alt={primaryButton?.text || "button image"} />}
|
||||
<h6 className="mb-3 mt-6 text-xl font-semibold sm:mt-8">{title}</h6>
|
||||
{description && <p className="mb-7 px-5 text-custom-text-300 sm:mb-8">{description}</p>}
|
||||
<div className="flex items-center gap-4">
|
||||
{primaryButton && (
|
||||
<Button
|
||||
variant="primary"
|
||||
prependIcon={primaryButton.icon}
|
||||
onClick={primaryButton.onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
{primaryButton.text}
|
||||
</Button>
|
||||
)}
|
||||
{secondaryButton}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -4,3 +4,4 @@ export * from "./controller-input";
|
||||
export * from "./copy-field";
|
||||
export * from "./password-strength-meter";
|
||||
export * from "./banner";
|
||||
export * from "./empty-state";
|
||||
|
||||
@@ -8,18 +8,20 @@ import { useTheme as nextUseTheme } from "next-themes";
|
||||
import { Button, getButtonStyling } from "@plane/ui";
|
||||
// helpers
|
||||
import { resolveGeneralTheme } from "helpers/common.helper";
|
||||
// hooks
|
||||
import { useInstance, useTheme } from "@/hooks/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 = `${process.env.NEXT_PUBLIC_APP_URL ? `${process.env.NEXT_PUBLIC_APP_URL}/create-workspace` : `${process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX === "1" ? `/god-mode/` : `/`}`}`;
|
||||
const redirectionLink = `${instance?.config?.app_base_url ? `${instance?.config?.app_base_url}/create-workspace` : `/god-mode/`}`;
|
||||
|
||||
if (!isNewUserPopup) return <></>;
|
||||
return (
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { ReactNode } from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
export enum EPageTypes {
|
||||
"PUBLIC" = "PUBLIC",
|
||||
"NON_AUTHENTICATED" = "NON_AUTHENTICATED",
|
||||
"ONBOARDING" = "ONBOARDING",
|
||||
"AUTHENTICATED" = "AUTHENTICATED",
|
||||
PUBLIC = "PUBLIC",
|
||||
NON_AUTHENTICATED = "NON_AUTHENTICATED",
|
||||
SET_PASSWORD = "SET_PASSWORD",
|
||||
ONBOARDING = "ONBOARDING",
|
||||
AUTHENTICATED = "AUTHENTICATED",
|
||||
}
|
||||
|
||||
export enum EAuthModes {
|
||||
@@ -18,28 +20,26 @@ export enum EAuthSteps {
|
||||
UNIQUE_CODE = "UNIQUE_CODE",
|
||||
}
|
||||
|
||||
export enum EAuthenticationErrorCodes {
|
||||
INSTANCE_NOT_CONFIGURED = "5000",
|
||||
// Admin
|
||||
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",
|
||||
}
|
||||
|
||||
export enum EErrorAlertType {
|
||||
BANNER_ALERT = "BANNER_ALERT",
|
||||
TOAST_ALERT = "TOAST_ALERT",
|
||||
INLINE_FIRST_NAME = "INLINE_FIRST_NAME",
|
||||
INLINE_EMAIL = "INLINE_EMAIL",
|
||||
INLINE_PASSWORD = "INLINE_PASSWORD",
|
||||
INLINE_EMAIL_CODE = "INLINE_EMAIL_CODE",
|
||||
}
|
||||
|
||||
export enum EAuthenticationErrorCodes {
|
||||
// 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",
|
||||
}
|
||||
|
||||
export type TAuthErrorInfo = {
|
||||
type: EErrorAlertType;
|
||||
code: EAuthenticationErrorCodes;
|
||||
@@ -50,41 +50,54 @@ export type TAuthErrorInfo = {
|
||||
const errorCodeMessages: {
|
||||
[key in EAuthenticationErrorCodes]: { title: string; message: (email?: string | undefined) => ReactNode };
|
||||
} = {
|
||||
[EAuthenticationErrorCodes.INSTANCE_NOT_CONFIGURED]: {
|
||||
title: "Instance not configured",
|
||||
message: () => "Please contact your administrator to configure the instance.",
|
||||
},
|
||||
// admin
|
||||
[EAuthenticationErrorCodes.ADMIN_ALREADY_EXIST]: {
|
||||
title: "Admin already exists",
|
||||
message: () => "Admin already exists. Please sign in.",
|
||||
title: `Admin already exists`,
|
||||
message: () => `Admin already exists. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME]: {
|
||||
title: "Required",
|
||||
message: () => "Please enter email, password and first name.",
|
||||
title: `Email, password and first name required`,
|
||||
message: () => `Email, password and first name required. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.INVALID_ADMIN_EMAIL]: {
|
||||
title: "Invalid email",
|
||||
message: () => "Please enter a valid email.",
|
||||
title: `Invalid admin email`,
|
||||
message: () => `Invalid admin email. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.INVALID_ADMIN_PASSWORD]: {
|
||||
title: "Invalid password",
|
||||
message: () => "Password must be at least 8 characters long.",
|
||||
title: `Invalid admin password`,
|
||||
message: () => `Invalid admin password. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD]: {
|
||||
title: "Required",
|
||||
message: () => "Please enter email and password.",
|
||||
title: `Email and password required`,
|
||||
message: () => `Email and password required. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.ADMIN_AUTHENTICATION_FAILED]: {
|
||||
title: "Authentication failed",
|
||||
message: () => "Please check your email and password and try again.",
|
||||
title: `Authentication failed`,
|
||||
message: () => `Authentication failed. Please try again.`,
|
||||
},
|
||||
[EAuthenticationErrorCodes.ADMIN_USER_ALREADY_EXIST]: {
|
||||
title: "User already exists",
|
||||
message: () => "User already exists. Please sign in.",
|
||||
title: `Admin user already exists`,
|
||||
message: () => (
|
||||
<div>
|
||||
Admin user already exists.
|
||||
<Link className="underline underline-offset-4 font-medium hover:font-bold transition-all" href={`/admin`}>
|
||||
Sign In
|
||||
</Link>
|
||||
now.
|
||||
</div>
|
||||
),
|
||||
},
|
||||
[EAuthenticationErrorCodes.ADMIN_USER_DOES_NOT_EXIST]: {
|
||||
title: "User does not exist",
|
||||
message: () => "User does not exist. Please sign up.",
|
||||
title: `Admin user does not exist`,
|
||||
message: () => (
|
||||
<div>
|
||||
Admin user does not exist.
|
||||
<Link className="underline underline-offset-4 font-medium hover:font-bold transition-all" href={`/admin`}>
|
||||
Sign In
|
||||
</Link>
|
||||
now.
|
||||
</div>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -92,28 +105,16 @@ export const authErrorHandler = (
|
||||
errorCode: EAuthenticationErrorCodes,
|
||||
email?: string | undefined
|
||||
): TAuthErrorInfo | undefined => {
|
||||
const toastAlertErrorCodes = [
|
||||
EAuthenticationErrorCodes.ADMIN_ALREADY_EXIST,
|
||||
const bannerAlertErrorCodes = [
|
||||
EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME,
|
||||
EAuthenticationErrorCodes.INVALID_ADMIN_EMAIL,
|
||||
EAuthenticationErrorCodes.INVALID_ADMIN_PASSWORD,
|
||||
EAuthenticationErrorCodes.ADMIN_AUTHENTICATION_FAILED,
|
||||
];
|
||||
const bannerAlertErrorCodes = [
|
||||
EAuthenticationErrorCodes.INSTANCE_NOT_CONFIGURED,
|
||||
EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME,
|
||||
EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD,
|
||||
EAuthenticationErrorCodes.ADMIN_AUTHENTICATION_FAILED,
|
||||
EAuthenticationErrorCodes.ADMIN_USER_ALREADY_EXIST,
|
||||
EAuthenticationErrorCodes.ADMIN_USER_DOES_NOT_EXIST,
|
||||
];
|
||||
|
||||
if (toastAlertErrorCodes.includes(errorCode))
|
||||
return {
|
||||
type: EErrorAlertType.TOAST_ALERT,
|
||||
code: errorCode,
|
||||
title: errorCodeMessages[errorCode]?.title || "Error",
|
||||
message: errorCodeMessages[errorCode]?.message(email) || "Something went wrong. Please try again.",
|
||||
};
|
||||
|
||||
if (bannerAlertErrorCodes.includes(errorCode))
|
||||
return {
|
||||
type: EErrorAlertType.BANNER_ALERT,
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ? process.env.NEXT_PUBLIC_API_BASE_URL : "";
|
||||
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 cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
export * from "./use-outside-click-detector";
|
||||
|
||||
// store-hooks
|
||||
export * from "./store/use-theme";
|
||||
export * from "./store/use-instance";
|
||||
export * from "./store/use-user";
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./use-theme";
|
||||
export * from "./use-instance";
|
||||
export * from "./use-user";
|
||||
@@ -4,7 +4,7 @@ import { ReactElement, createContext } from "react";
|
||||
// mobx store
|
||||
import { RootStore } from "@/store/root-store";
|
||||
|
||||
let rootStore = new RootStore();
|
||||
export let rootStore = new RootStore();
|
||||
|
||||
export const StoreContext = createContext<RootStore>(rootStore);
|
||||
|
||||
|
||||
@@ -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";
|
||||
import { useTheme, useUser } from "@/hooks/store";
|
||||
// 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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
import { useInstance, useUser } from "@/hooks/store";
|
||||
// helpers
|
||||
import { EAuthenticationPageType, EUserStatus } from "@/helpers";
|
||||
import { redirect } from "next/navigation";
|
||||
import { EAuthenticationPageType } from "@/helpers";
|
||||
|
||||
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, fetchInstanceAdmins } = useInstance();
|
||||
const { isLoading, userStatus, currentUser, fetchCurrentUser } = useUser();
|
||||
const { instance } = useInstance();
|
||||
const { isLoading, currentUser, fetchCurrentUser } = useUser();
|
||||
|
||||
useSWR("CURRENT_USER_DETAILS", () => fetchCurrentUser(), {
|
||||
shouldRetryOnError: false,
|
||||
});
|
||||
useSWR("INSTANCE_ADMINS", () => fetchInstanceAdmins(), {
|
||||
const { isLoading: isSWRLoading } = useSWR("CURRENT_USER_DETAILS", () => fetchCurrentUser(), {
|
||||
shouldRetryOnError: false,
|
||||
});
|
||||
|
||||
if (isLoading)
|
||||
if (isSWRLoading || isLoading)
|
||||
return (
|
||||
<div className="relative flex h-screen w-full items-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
|
||||
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.NOT_AUTHENTICATED) {
|
||||
if (currentUser === undefined) return <>{children}</>;
|
||||
else {
|
||||
router.push("/general/");
|
||||
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/");
|
||||
if (authType === EAuthenticationPageType.AUTHENTICATED) {
|
||||
if (currentUser) return <>{children}</>;
|
||||
else {
|
||||
if (instance && instance?.instance?.is_setup_done) {
|
||||
router.push("/");
|
||||
return <></>;
|
||||
} else {
|
||||
router.push("/setup/");
|
||||
return <></>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,10 @@ import { DefaultLayout } from "@/layouts";
|
||||
// components
|
||||
import { InstanceNotReady } from "@/components/instance";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks";
|
||||
import { useInstance } from "@/hooks/store";
|
||||
// helpers
|
||||
import { EInstancePageType, EInstanceStatus } from "@/helpers";
|
||||
import { EInstancePageType } from "@/helpers";
|
||||
import { EmptyState } from "@/components/common";
|
||||
|
||||
type TInstanceWrapper = {
|
||||
children: ReactNode;
|
||||
@@ -24,25 +25,30 @@ export const InstanceWrapper: FC<TInstanceWrapper> = observer((props) => {
|
||||
const searchparams = useSearchParams();
|
||||
const authEnabled = searchparams.get("auth_enabled") || "1";
|
||||
// hooks
|
||||
const { isLoading, instanceStatus, instance, fetchInstanceInfo } = useInstance();
|
||||
const { isLoading, instance, fetchInstanceInfo } = useInstance();
|
||||
|
||||
useSWR("INSTANCE_INFORMATION", () => fetchInstanceInfo(), {
|
||||
const { isLoading: isSWRLoading } = useSWR("INSTANCE_INFORMATION", () => fetchInstanceInfo(), {
|
||||
revalidateOnFocus: false,
|
||||
revalidateIfStale: false,
|
||||
revalidateOnReconnect: false,
|
||||
errorRetryCount: 0,
|
||||
});
|
||||
|
||||
if (isLoading)
|
||||
if (isSWRLoading || isLoading)
|
||||
return (
|
||||
<div className="relative flex h-screen w-full items-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
|
||||
if (instanceStatus && instanceStatus?.status === EInstanceStatus.ERROR)
|
||||
if (!instance) {
|
||||
return (
|
||||
<div className="relative flex h-screen w-screen items-center justify-center">
|
||||
Something went wrong. please try again later
|
||||
</div>
|
||||
<EmptyState
|
||||
title="Your instance wasn't configured successfully."
|
||||
description="Please try re-installing Plane to fix the problem. If the issue still persists please reach out to support@plane.so."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (instance?.instance?.is_setup_done === false && authEnabled === "1")
|
||||
return (
|
||||
|
||||
@@ -7,7 +7,7 @@ const nextConfig = {
|
||||
images: {
|
||||
unoptimized: true,
|
||||
},
|
||||
basePath: "/god-mode",
|
||||
basePath: process.env.NEXT_PUBLIC_ADMIN_BASE_PATH || "",
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
|
||||
+3
-2
@@ -4,7 +4,7 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "turbo run develop",
|
||||
"develop": "next dev --port 3333",
|
||||
"develop": "next dev --port 3001",
|
||||
"build": "next build",
|
||||
"preview": "next build && next start",
|
||||
"start": "next start",
|
||||
@@ -14,6 +14,7 @@
|
||||
"@headlessui/react": "^1.7.19",
|
||||
"@plane/types": "*",
|
||||
"@plane/ui": "*",
|
||||
"@plane/constants": "*",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@types/lodash": "^4.17.0",
|
||||
"autoprefixer": "10.4.14",
|
||||
@@ -25,7 +26,7 @@
|
||||
"mobx-react-lite": "^4.0.5",
|
||||
"next": "^14.2.3",
|
||||
"next-themes": "^0.2.1",
|
||||
"postcss": "8.4.23",
|
||||
"postcss": "^8.4.38",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.51.0",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
|
||||
// store
|
||||
import { rootStore } from "@/lib/store-context";
|
||||
|
||||
export abstract class APIService {
|
||||
protected baseURL: string;
|
||||
@@ -18,7 +20,8 @@ export abstract class APIService {
|
||||
this.axiosInstance.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response && error.response.status === 401) window.location.href = "/login";
|
||||
const store = rootStore;
|
||||
if (error.response && error.response.status === 401 && store.user.currentUser) store.user.reset();
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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 { API_BASE_URL } from "@/helpers/common.helper";
|
||||
import { APIService } from "@/services/api.service";
|
||||
|
||||
export class InstanceService extends APIService {
|
||||
constructor() {
|
||||
|
||||
@@ -18,8 +18,10 @@ export class RootStore {
|
||||
}
|
||||
|
||||
resetOnSignOut() {
|
||||
this.theme = new ThemeStore(this);
|
||||
localStorage.setItem("theme", "system");
|
||||
|
||||
this.instance = new InstanceStore(this);
|
||||
this.user = new UserStore(this);
|
||||
this.theme = new ThemeStore(this);
|
||||
}
|
||||
}
|
||||
|
||||
+26
-10
@@ -16,7 +16,8 @@ export interface IUserStore {
|
||||
currentUser: IUser | undefined;
|
||||
// fetch actions
|
||||
fetchCurrentUser: () => Promise<IUser>;
|
||||
signOut: () => Promise<void>;
|
||||
reset: () => void;
|
||||
signOut: () => void;
|
||||
}
|
||||
|
||||
export class UserStore implements IUserStore {
|
||||
@@ -28,8 +29,6 @@ export class UserStore implements IUserStore {
|
||||
// services
|
||||
userService;
|
||||
authService;
|
||||
// rootStore
|
||||
rootStore;
|
||||
|
||||
constructor(private store: RootStore) {
|
||||
makeObservable(this, {
|
||||
@@ -40,10 +39,11 @@ 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,11 +54,20 @@ export class UserStore implements IUserStore {
|
||||
try {
|
||||
if (this.currentUser === undefined) this.isLoading = true;
|
||||
const currentUser = await this.userService.currentUser();
|
||||
runInAction(() => {
|
||||
this.isUserLoggedIn = true;
|
||||
this.currentUser = currentUser;
|
||||
this.isLoading = false;
|
||||
});
|
||||
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;
|
||||
});
|
||||
}
|
||||
return currentUser;
|
||||
} catch (error: any) {
|
||||
this.isLoading = false;
|
||||
@@ -77,7 +86,14 @@ export class UserStore implements IUserStore {
|
||||
}
|
||||
};
|
||||
|
||||
reset = async () => {
|
||||
this.isUserLoggedIn = false;
|
||||
this.currentUser = undefined;
|
||||
this.isLoading = false;
|
||||
this.userStatus = undefined;
|
||||
};
|
||||
|
||||
signOut = async () => {
|
||||
this.rootStore.resetOnSignOut();
|
||||
this.store.resetOnSignOut();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Backend
|
||||
# Debug value for api server use it as 0 for production use
|
||||
DEBUG=0
|
||||
CORS_ALLOWED_ORIGINS=""
|
||||
CORS_ALLOWED_ORIGINS="http://localhost"
|
||||
|
||||
# Error logs
|
||||
SENTRY_DSN=""
|
||||
@@ -44,3 +44,8 @@ WEB_URL="http://localhost"
|
||||
|
||||
# Gunicorn Workers
|
||||
GUNICORN_WORKERS=2
|
||||
|
||||
# Base URLs
|
||||
ADMIN_BASE_URL=
|
||||
SPACE_BASE_URL=
|
||||
APP_BASE_URL=
|
||||
|
||||
@@ -315,7 +315,7 @@ class IssueLinkSerializer(BaseSerializer):
|
||||
if IssueLink.objects.filter(
|
||||
url=validated_data.get("url"),
|
||||
issue_id=instance.issue_id,
|
||||
).exists():
|
||||
).exclude(pk=instance.id).exists():
|
||||
raise serializers.ValidationError(
|
||||
{"error": "URL already exists for this Issue"}
|
||||
)
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
# Python imports
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import zoneinfo
|
||||
|
||||
# Django imports
|
||||
|
||||
@@ -462,7 +462,7 @@ class IssueLinkSerializer(BaseSerializer):
|
||||
if IssueLink.objects.filter(
|
||||
url=validated_data.get("url"),
|
||||
issue_id=instance.issue_id,
|
||||
).exists():
|
||||
).exclude(pk=instance.id).exists():
|
||||
raise serializers.ValidationError(
|
||||
{"error": "URL already exists for this Issue"}
|
||||
)
|
||||
|
||||
@@ -602,11 +602,19 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView):
|
||||
@cache_response(60 * 60 * 24, user=False)
|
||||
def get(self, request):
|
||||
files = []
|
||||
s3 = boto3.client(
|
||||
"s3",
|
||||
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
|
||||
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
|
||||
)
|
||||
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,
|
||||
)
|
||||
params = {
|
||||
"Bucket": settings.AWS_STORAGE_BUCKET_NAME,
|
||||
"Prefix": "static/project-cover/",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# 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
|
||||
@@ -26,6 +28,7 @@ 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):
|
||||
@@ -166,7 +169,14 @@ 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)
|
||||
|
||||
|
||||
|
||||
@@ -96,6 +96,7 @@ class WorkSpaceViewSet(BaseViewSet):
|
||||
|
||||
@invalidate_cache(path="/api/workspaces/", user=False)
|
||||
@invalidate_cache(path="/api/users/me/workspaces/")
|
||||
@invalidate_cache(path="/api/instances/", user=False)
|
||||
def create(self, request):
|
||||
try:
|
||||
serializer = WorkSpaceSerializer(data=request.data)
|
||||
@@ -151,8 +152,12 @@ class WorkSpaceViewSet(BaseViewSet):
|
||||
return super().partial_update(request, *args, **kwargs)
|
||||
|
||||
@invalidate_cache(path="/api/workspaces/", user=False)
|
||||
@invalidate_cache(path="/api/users/me/workspaces/", multiple=True, user=False)
|
||||
@invalidate_cache(path="/api/users/me/settings/", multiple=True, user=False)
|
||||
@invalidate_cache(
|
||||
path="/api/users/me/workspaces/", multiple=True, user=False
|
||||
)
|
||||
@invalidate_cache(
|
||||
path="/api/users/me/settings/", multiple=True, user=False
|
||||
)
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
return super().destroy(request, *args, **kwargs)
|
||||
|
||||
|
||||
@@ -100,6 +100,12 @@ class Adapter:
|
||||
user.save()
|
||||
Profile.objects.create(user=user)
|
||||
|
||||
if not user.is_active:
|
||||
raise AuthenticationException(
|
||||
AUTHENTICATION_ERROR_CODES["USER_ACCOUNT_DEACTIVATED"],
|
||||
error_message="USER_ACCOUNT_DEACTIVATED",
|
||||
)
|
||||
|
||||
# Update user details
|
||||
user.last_login_medium = self.provider
|
||||
user.last_active = timezone.now()
|
||||
|
||||
@@ -1,51 +1,57 @@
|
||||
AUTHENTICATION_ERROR_CODES = {
|
||||
# Global
|
||||
"INSTANCE_NOT_CONFIGURED": 5000,
|
||||
"INVALID_EMAIL": 5012,
|
||||
"EMAIL_REQUIRED": 5013,
|
||||
"SIGNUP_DISABLED": 5001,
|
||||
"INVALID_EMAIL": 5005,
|
||||
"EMAIL_REQUIRED": 5010,
|
||||
"SIGNUP_DISABLED": 5015,
|
||||
"MAGIC_LINK_LOGIN_DISABLED": 5016,
|
||||
"PASSWORD_LOGIN_DISABLED": 5018,
|
||||
"USER_ACCOUNT_DEACTIVATED": 5019,
|
||||
# Password strength
|
||||
"INVALID_PASSWORD": 5002,
|
||||
"SMTP_NOT_CONFIGURED": 5007,
|
||||
"INVALID_PASSWORD": 5020,
|
||||
"SMTP_NOT_CONFIGURED": 5025,
|
||||
# Sign Up
|
||||
"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,
|
||||
"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,
|
||||
# Sign In
|
||||
"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,
|
||||
"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,
|
||||
# Oauth
|
||||
"GOOGLE_NOT_CONFIGURED": 5010,
|
||||
"GITHUB_NOT_CONFIGURED": 5011,
|
||||
"GOOGLE_OAUTH_PROVIDER_ERROR": 5021,
|
||||
"GITHUB_OAUTH_PROVIDER_ERROR": 5020,
|
||||
"GOOGLE_NOT_CONFIGURED": 5105,
|
||||
"GITHUB_NOT_CONFIGURED": 5110,
|
||||
"GOOGLE_OAUTH_PROVIDER_ERROR": 5115,
|
||||
"GITHUB_OAUTH_PROVIDER_ERROR": 5120,
|
||||
# Reset Password
|
||||
"INVALID_PASSWORD_TOKEN": 5024,
|
||||
"EXPIRED_PASSWORD_TOKEN": 5025,
|
||||
"INVALID_PASSWORD_TOKEN": 5125,
|
||||
"EXPIRED_PASSWORD_TOKEN": 5130,
|
||||
# Change password
|
||||
"INCORRECT_OLD_PASSWORD": 5026,
|
||||
"INVALID_NEW_PASSWORD": 5027,
|
||||
"INCORRECT_OLD_PASSWORD": 5135,
|
||||
"MISSING_PASSWORD": 5138,
|
||||
"INVALID_NEW_PASSWORD": 5140,
|
||||
# set passowrd
|
||||
"PASSWORD_ALREADY_SET": 5028,
|
||||
"PASSWORD_ALREADY_SET": 5145,
|
||||
# Admin
|
||||
"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,
|
||||
"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_USER_DEACTIVATED": 5190,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -7,12 +7,6 @@ def auth_exception_handler(exc, context):
|
||||
response = exception_handler(exc, context)
|
||||
# Check if an AuthenticationFailed exception is raised.
|
||||
if isinstance(exc, NotAuthenticated):
|
||||
# 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
|
||||
response.status_code = 401
|
||||
|
||||
return response
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
# Python imports
|
||||
import os
|
||||
|
||||
# Module imports
|
||||
from plane.authentication.adapter.credential import CredentialAdapter
|
||||
from plane.db.models import User
|
||||
@@ -5,6 +8,7 @@ from plane.authentication.adapter.error import (
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
AuthenticationException,
|
||||
)
|
||||
from plane.license.utils.instance_value import get_configuration_value
|
||||
|
||||
|
||||
class EmailProvider(CredentialAdapter):
|
||||
@@ -23,6 +27,21 @@ class EmailProvider(CredentialAdapter):
|
||||
self.code = code
|
||||
self.is_signup = is_signup
|
||||
|
||||
(ENABLE_EMAIL_PASSWORD,) = get_configuration_value(
|
||||
[
|
||||
{
|
||||
"key": "ENABLE_EMAIL_PASSWORD",
|
||||
"default": os.environ.get("ENABLE_EMAIL_PASSWORD"),
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
if ENABLE_EMAIL_PASSWORD == "0":
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["ENABLE_EMAIL_PASSWORD"],
|
||||
error_message="ENABLE_EMAIL_PASSWORD",
|
||||
)
|
||||
|
||||
def set_user_data(self):
|
||||
if self.is_signup:
|
||||
# Check if the user already exists
|
||||
|
||||
@@ -26,23 +26,20 @@ class MagicCodeProvider(CredentialAdapter):
|
||||
code=None,
|
||||
):
|
||||
|
||||
(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"),
|
||||
},
|
||||
]
|
||||
)
|
||||
(
|
||||
EMAIL_HOST,
|
||||
ENABLE_MAGIC_LINK_LOGIN,
|
||||
) = get_configuration_value(
|
||||
[
|
||||
{
|
||||
"key": "EMAIL_HOST",
|
||||
"default": os.environ.get("EMAIL_HOST"),
|
||||
},
|
||||
{
|
||||
"key": "ENABLE_MAGIC_LINK_LOGIN",
|
||||
"default": os.environ.get("ENABLE_MAGIC_LINK_LOGIN", "1"),
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
if not (EMAIL_HOST):
|
||||
@@ -52,6 +49,15 @@ class MagicCodeProvider(CredentialAdapter):
|
||||
payload={"email": str(self.key)},
|
||||
)
|
||||
|
||||
if ENABLE_MAGIC_LINK_LOGIN == "0":
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"MAGIC_LINK_LOGIN_DISABLED"
|
||||
],
|
||||
error_message="MAGIC_LINK_LOGIN_DISABLED",
|
||||
payload={"email": str(self.key)},
|
||||
)
|
||||
|
||||
super().__init__(request, self.provider)
|
||||
self.key = key
|
||||
self.code = code
|
||||
@@ -77,7 +83,13 @@ class MagicCodeProvider(CredentialAdapter):
|
||||
current_attempt = data["current_attempt"] + 1
|
||||
|
||||
if data["current_attempt"] > 2:
|
||||
return key, ""
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"EMAIL_CODE_ATTEMPT_EXHAUSTED"
|
||||
],
|
||||
error_message="EMAIL_CODE_ATTEMPT_EXHAUSTED",
|
||||
payload={"email": self.key},
|
||||
)
|
||||
|
||||
value = {
|
||||
"current_attempt": current_attempt,
|
||||
|
||||
@@ -46,9 +46,7 @@ class GitHubOAuthProvider(OauthAdapter):
|
||||
client_id = GITHUB_CLIENT_ID
|
||||
client_secret = GITHUB_CLIENT_SECRET
|
||||
|
||||
redirect_uri = (
|
||||
f"{request.scheme}://{request.get_host()}/auth/github/callback/"
|
||||
)
|
||||
redirect_uri = f"""{"https" if request.is_secure() else "http"}://{request.get_host()}/auth/github/callback/"""
|
||||
url_params = {
|
||||
"client_id": client_id,
|
||||
"redirect_uri": redirect_uri,
|
||||
|
||||
@@ -43,9 +43,7 @@ class GoogleOAuthProvider(OauthAdapter):
|
||||
client_id = GOOGLE_CLIENT_ID
|
||||
client_secret = GOOGLE_CLIENT_SECRET
|
||||
|
||||
redirect_uri = (
|
||||
f"{request.scheme}://{request.get_host()}/auth/google/callback/"
|
||||
)
|
||||
redirect_uri = f"""{"https" if request.is_secure() else "http"}://{request.get_host()}/auth/google/callback/"""
|
||||
url_params = {
|
||||
"client_id": client_id,
|
||||
"scope": self.scope,
|
||||
|
||||
@@ -7,6 +7,7 @@ from .views import (
|
||||
ForgotPasswordEndpoint,
|
||||
SetUserPasswordEndpoint,
|
||||
ResetPasswordEndpoint,
|
||||
ChangePasswordEndpoint,
|
||||
# App
|
||||
GitHubCallbackEndpoint,
|
||||
GitHubOauthInitiateEndpoint,
|
||||
@@ -18,6 +19,8 @@ from .views import (
|
||||
SignInAuthEndpoint,
|
||||
SignOutAuthEndpoint,
|
||||
SignUpAuthEndpoint,
|
||||
ForgotPasswordSpaceEndpoint,
|
||||
ResetPasswordSpaceEndpoint,
|
||||
# Space
|
||||
EmailCheckEndpoint,
|
||||
GitHubCallbackSpaceEndpoint,
|
||||
@@ -176,6 +179,21 @@ urlpatterns = [
|
||||
ResetPasswordEndpoint.as_view(),
|
||||
name="forgot-password",
|
||||
),
|
||||
path(
|
||||
"spaces/forgot-password/",
|
||||
ForgotPasswordSpaceEndpoint.as_view(),
|
||||
name="forgot-password",
|
||||
),
|
||||
path(
|
||||
"spaces/reset-password/<uidb64>/<token>/",
|
||||
ResetPasswordSpaceEndpoint.as_view(),
|
||||
name="forgot-password",
|
||||
),
|
||||
path(
|
||||
"change-password/",
|
||||
ChangePasswordEndpoint.as_view(),
|
||||
name="forgot-password",
|
||||
),
|
||||
path(
|
||||
"set-password/",
|
||||
SetUserPasswordEndpoint.as_view(),
|
||||
|
||||
@@ -1,14 +1,42 @@
|
||||
# Python imports
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
# Django imports
|
||||
from django.conf import settings
|
||||
|
||||
def base_host(request):
|
||||
|
||||
def base_host(request, is_admin=False, is_space=False, is_app=False):
|
||||
"""Utility function to return host / origin from the request"""
|
||||
return (
|
||||
# Calculate the base origin from request
|
||||
base_origin = str(
|
||||
request.META.get("HTTP_ORIGIN")
|
||||
or f"{urlsplit(request.META.get('HTTP_REFERER')).scheme}://{urlsplit(request.META.get('HTTP_REFERER')).netloc}"
|
||||
or f"{request.scheme}://{request.get_host()}"
|
||||
or f"""{"https" if request.is_secure() else "http"}://{request.get_host()}"""
|
||||
)
|
||||
|
||||
# Admin redirections
|
||||
if is_admin:
|
||||
if settings.ADMIN_BASE_URL:
|
||||
return settings.ADMIN_BASE_URL
|
||||
else:
|
||||
return base_origin + "/god-mode/"
|
||||
|
||||
# Space redirections
|
||||
if is_space:
|
||||
if settings.SPACE_BASE_URL:
|
||||
return settings.SPACE_BASE_URL
|
||||
else:
|
||||
return base_origin + "/spaces/"
|
||||
|
||||
# App Redirection
|
||||
if is_app:
|
||||
if settings.APP_BASE_URL:
|
||||
return settings.APP_BASE_URL
|
||||
else:
|
||||
return base_origin
|
||||
|
||||
return base_origin
|
||||
|
||||
|
||||
def user_ip(request):
|
||||
return str(request.META.get("REMOTE_ADDR"))
|
||||
|
||||
@@ -5,12 +5,17 @@ from django.contrib.auth import login
|
||||
from plane.authentication.utils.host import base_host
|
||||
|
||||
|
||||
def user_login(request, user):
|
||||
def user_login(request, user, is_app=False, is_admin=False, is_space=False):
|
||||
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),
|
||||
"domain": base_host(
|
||||
request=request,
|
||||
is_app=is_app,
|
||||
is_admin=is_admin,
|
||||
is_space=is_space,
|
||||
),
|
||||
}
|
||||
request.session["device_info"] = device_info
|
||||
request.session.save()
|
||||
|
||||
@@ -10,10 +10,13 @@ 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,
|
||||
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()
|
||||
):
|
||||
workspace = Workspace.objects.filter(
|
||||
pk=profile.last_workspace_id,
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
from .common import (
|
||||
ChangePasswordEndpoint,
|
||||
CSRFTokenEndpoint,
|
||||
ForgotPasswordEndpoint,
|
||||
ResetPasswordEndpoint,
|
||||
SetUserPasswordEndpoint,
|
||||
)
|
||||
|
||||
@@ -50,3 +48,12 @@ from .space.magic import (
|
||||
from .space.signout import SignOutAuthSpaceEndpoint
|
||||
|
||||
from .space.check import EmailCheckEndpoint
|
||||
|
||||
from .space.password_management import (
|
||||
ForgotPasswordSpaceEndpoint,
|
||||
ResetPasswordSpaceEndpoint,
|
||||
)
|
||||
from .app.password_management import (
|
||||
ForgotPasswordEndpoint,
|
||||
ResetPasswordEndpoint,
|
||||
)
|
||||
|
||||
@@ -24,62 +24,59 @@ class EmailCheckSignUpEndpoint(APIView):
|
||||
]
|
||||
|
||||
def post(self, request):
|
||||
# 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 = request.data.get("email", False)
|
||||
|
||||
# Return error if email is not present
|
||||
if not email:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["EMAIL_REQUIRED"],
|
||||
error_message="EMAIL_REQUIRED",
|
||||
)
|
||||
return Response(
|
||||
exc.get_error_dict(),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Validate email
|
||||
try:
|
||||
# Check instance configuration
|
||||
instance = Instance.objects.first()
|
||||
if instance is None or not instance.is_setup_done:
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"INSTANCE_NOT_CONFIGURED"
|
||||
],
|
||||
error_message="INSTANCE_NOT_CONFIGURED",
|
||||
)
|
||||
email = request.data.get("email", False)
|
||||
|
||||
# Return error if email is not present
|
||||
if not email:
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["EMAIL_REQUIRED"],
|
||||
error_message="EMAIL_REQUIRED",
|
||||
)
|
||||
|
||||
# Validate email
|
||||
validate_email(email)
|
||||
|
||||
existing_user = User.objects.filter(email=email).first()
|
||||
|
||||
if existing_user:
|
||||
# check if the account is the deactivated
|
||||
if not existing_user.is_active:
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"USER_ACCOUNT_DEACTIVATED"
|
||||
],
|
||||
error_message="USER_ACCOUNT_DEACTIVATED",
|
||||
)
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"USER_ALREADY_EXIST"
|
||||
],
|
||||
error_message="USER_ALREADY_EXIST",
|
||||
)
|
||||
return Response(
|
||||
{"status": True},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
except ValidationError:
|
||||
exc = AuthenticationException(
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["INVALID_EMAIL"],
|
||||
error_message="INVALID_EMAIL",
|
||||
)
|
||||
except AuthenticationException as e:
|
||||
return Response(
|
||||
exc.get_error_dict(),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
e.get_error_dict(), status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
existing_user = User.objects.filter(email=email).first()
|
||||
|
||||
if existing_user:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["USER_ALREADY_EXIST"],
|
||||
error_message="USER_ALREADY_EXIST",
|
||||
)
|
||||
return Response(
|
||||
exc.get_error_dict(),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
return Response(
|
||||
{"status": True},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
|
||||
class EmailCheckSignInEndpoint(APIView):
|
||||
|
||||
@@ -88,61 +85,59 @@ class EmailCheckSignInEndpoint(APIView):
|
||||
]
|
||||
|
||||
def post(self, request):
|
||||
# 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 = request.data.get("email", False)
|
||||
|
||||
# Return error if email is not present
|
||||
if not email:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["EMAIL_REQUIRED"],
|
||||
error_message="EMAIL_REQUIRED",
|
||||
)
|
||||
return Response(
|
||||
exc.get_error_dict(),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Validate email
|
||||
try:
|
||||
# Check instance configuration
|
||||
instance = Instance.objects.first()
|
||||
if instance is None or not instance.is_setup_done:
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"INSTANCE_NOT_CONFIGURED"
|
||||
],
|
||||
error_message="INSTANCE_NOT_CONFIGURED",
|
||||
)
|
||||
|
||||
email = request.data.get("email", False)
|
||||
|
||||
# Return error if email is not present
|
||||
if not email:
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["EMAIL_REQUIRED"],
|
||||
error_message="EMAIL_REQUIRED",
|
||||
)
|
||||
|
||||
# Validate email
|
||||
validate_email(email)
|
||||
|
||||
existing_user = User.objects.filter(email=email).first()
|
||||
|
||||
# If existing user
|
||||
if existing_user:
|
||||
# Raise different exception when user is not active
|
||||
if not existing_user.is_active:
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"USER_ACCOUNT_DEACTIVATED"
|
||||
],
|
||||
error_message="USER_ACCOUNT_DEACTIVATED",
|
||||
)
|
||||
|
||||
return Response(
|
||||
{
|
||||
"status": True,
|
||||
"is_password_autoset": existing_user.is_password_autoset,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["USER_DOES_NOT_EXIST"],
|
||||
error_message="USER_DOES_NOT_EXIST",
|
||||
)
|
||||
except ValidationError:
|
||||
exc = AuthenticationException(
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["INVALID_EMAIL"],
|
||||
error_message="INVALID_EMAIL",
|
||||
)
|
||||
except AuthenticationException as e:
|
||||
return Response(
|
||||
exc.get_error_dict(),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
e.get_error_dict(), status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
existing_user = User.objects.filter(email=email).first()
|
||||
|
||||
if existing_user:
|
||||
return Response(
|
||||
{
|
||||
"status": True,
|
||||
"is_password_autoset": existing_user.is_password_autoset,
|
||||
},
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -42,8 +42,8 @@ class SignInAuthEndpoint(View):
|
||||
params["next_path"] = str(next_path)
|
||||
# Base URL join
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
"accounts/sign-in?" + urlencode(params),
|
||||
base_host(request=request, is_app=True),
|
||||
"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),
|
||||
"accounts/sign-in?" + urlencode(params),
|
||||
base_host(request=request, is_app=True),
|
||||
"sign-in?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -85,12 +85,14 @@ class SignInAuthEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
"accounts/sign-in?" + urlencode(params),
|
||||
base_host(request=request, is_app=True),
|
||||
"sign-in?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
if not User.objects.filter(email=email).exists():
|
||||
existing_user = User.objects.filter(email=email).first()
|
||||
|
||||
if not existing_user:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["USER_DOES_NOT_EXIST"],
|
||||
error_message="USER_DOES_NOT_EXIST",
|
||||
@@ -100,8 +102,24 @@ class SignInAuthEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
"accounts/sign-in?" + urlencode(params),
|
||||
base_host(request=request, is_app=True),
|
||||
"sign-in?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
if not existing_user.is_active:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"USER_ACCOUNT_DEACTIVATED"
|
||||
],
|
||||
error_message="USER_ACCOUNT_DEACTIVATED",
|
||||
)
|
||||
params = exc.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),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -111,7 +129,7 @@ class SignInAuthEndpoint(View):
|
||||
)
|
||||
user = provider.authenticate()
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user)
|
||||
user_login(request=request, user=user, is_app=True)
|
||||
# Process workspace and project invitations
|
||||
process_workspace_project_invitations(user=user)
|
||||
# Get the redirection path
|
||||
@@ -121,15 +139,15 @@ class SignInAuthEndpoint(View):
|
||||
path = get_redirection_path(user=user)
|
||||
|
||||
# redirect to referer path
|
||||
url = urljoin(base_host(request=request), path)
|
||||
url = urljoin(base_host(request=request, is_app=True), 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),
|
||||
"accounts/sign-in?" + urlencode(params),
|
||||
base_host(request=request, is_app=True),
|
||||
"sign-in?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -152,7 +170,7 @@ class SignUpAuthEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
base_host(request=request, is_app=True),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -173,7 +191,7 @@ class SignUpAuthEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
base_host(request=request, is_app=True),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -192,12 +210,31 @@ class SignUpAuthEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
base_host(request=request, is_app=True),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
if User.objects.filter(email=email).exists():
|
||||
existing_user = User.objects.filter(email=email).first()
|
||||
|
||||
if existing_user:
|
||||
# Existing User
|
||||
if not existing_user.is_active:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"USER_ACCOUNT_DEACTIVATED"
|
||||
],
|
||||
error_message="USER_ACCOUNT_DEACTIVATED",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["USER_ALREADY_EXIST"],
|
||||
error_message="USER_ALREADY_EXIST",
|
||||
@@ -207,7 +244,7 @@ class SignUpAuthEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
base_host(request=request, is_app=True),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -218,7 +255,7 @@ class SignUpAuthEndpoint(View):
|
||||
)
|
||||
user = provider.authenticate()
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user)
|
||||
user_login(request=request, user=user, is_app=True)
|
||||
# Process workspace and project invitations
|
||||
process_workspace_project_invitations(user=user)
|
||||
# Get the redirection path
|
||||
@@ -227,14 +264,14 @@ class SignUpAuthEndpoint(View):
|
||||
else:
|
||||
path = get_redirection_path(user=user)
|
||||
# redirect to referer path
|
||||
url = urljoin(base_host(request=request), path)
|
||||
url = urljoin(base_host(request=request, is_app=True), 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),
|
||||
base_host(request=request, is_app=True),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -2,7 +2,6 @@ import uuid
|
||||
from urllib.parse import urlencode, urljoin
|
||||
|
||||
# Django import
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.views import View
|
||||
|
||||
@@ -25,7 +24,7 @@ class GitHubOauthInitiateEndpoint(View):
|
||||
|
||||
def get(self, request):
|
||||
# Get host and next path
|
||||
request.session["host"] = base_host(request=request)
|
||||
request.session["host"] = base_host(request=request, is_app=True)
|
||||
next_path = request.GET.get("next_path")
|
||||
if next_path:
|
||||
request.session["next_path"] = str(next_path)
|
||||
@@ -43,7 +42,7 @@ class GitHubOauthInitiateEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
base_host(request=request, is_app=True),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -58,7 +57,7 @@ class GitHubOauthInitiateEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
base_host(request=request, is_app=True),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -111,7 +110,7 @@ class GitHubCallbackEndpoint(View):
|
||||
)
|
||||
user = provider.authenticate()
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user)
|
||||
user_login(request=request, user=user, is_app=True)
|
||||
# Process workspace and project invitations
|
||||
process_workspace_project_invitations(user=user)
|
||||
# Get the redirection path
|
||||
|
||||
@@ -3,18 +3,17 @@ import uuid
|
||||
from urllib.parse import urlencode, urljoin
|
||||
|
||||
# Django import
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.views import View
|
||||
|
||||
|
||||
# Module imports
|
||||
from plane.authentication.provider.oauth.google import GoogleOAuthProvider
|
||||
from plane.authentication.utils.login import user_login
|
||||
from plane.authentication.utils.redirection_path import get_redirection_path
|
||||
from plane.authentication.utils.workspace_project_join import (
|
||||
process_workspace_project_invitations,
|
||||
)
|
||||
|
||||
# Module imports
|
||||
from plane.license.models import Instance
|
||||
from plane.authentication.utils.host import base_host
|
||||
from plane.authentication.adapter.error import (
|
||||
@@ -25,7 +24,7 @@ from plane.authentication.adapter.error import (
|
||||
|
||||
class GoogleOauthInitiateEndpoint(View):
|
||||
def get(self, request):
|
||||
request.session["host"] = base_host(request=request)
|
||||
request.session["host"] = base_host(request=request, is_app=True)
|
||||
next_path = request.GET.get("next_path")
|
||||
if next_path:
|
||||
request.session["next_path"] = str(next_path)
|
||||
@@ -43,7 +42,7 @@ class GoogleOauthInitiateEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
base_host(request=request, is_app=True),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -59,7 +58,7 @@ class GoogleOauthInitiateEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
base_host(request=request, is_app=True),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -109,7 +108,7 @@ class GoogleCallbackEndpoint(View):
|
||||
)
|
||||
user = provider.authenticate()
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user)
|
||||
user_login(request=request, user=user, is_app=True)
|
||||
# Process workspace and project invitations
|
||||
process_workspace_project_invitations(user=user)
|
||||
# Get the redirection path
|
||||
|
||||
@@ -90,12 +90,31 @@ class MagicSignInEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
"accounts/sign-in?" + urlencode(params),
|
||||
base_host(request=request, is_app=True),
|
||||
"sign-in?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
if not User.objects.filter(email=email).exists():
|
||||
# Existing User
|
||||
existing_user = User.objects.filter(email=email).first()
|
||||
|
||||
if not existing_user:
|
||||
if not existing_user.is_active:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"USER_ACCOUNT_DEACTIVATED"
|
||||
],
|
||||
error_message="USER_ACCOUNT_DEACTIVATED",
|
||||
)
|
||||
params = exc.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),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["USER_DOES_NOT_EXIST"],
|
||||
error_message="USER_DOES_NOT_EXIST",
|
||||
@@ -104,8 +123,8 @@ class MagicSignInEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
"accounts/sign-in?" + urlencode(params),
|
||||
base_host(request=request, is_app=True),
|
||||
"sign-in?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -116,7 +135,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)
|
||||
user_login(request=request, user=user, is_app=True)
|
||||
# Process workspace and project invitations
|
||||
process_workspace_project_invitations(user=user)
|
||||
if user.is_password_autoset and profile.is_onboarded:
|
||||
@@ -129,7 +148,7 @@ class MagicSignInEndpoint(View):
|
||||
else str(process_workspace_project_invitations(user=user))
|
||||
)
|
||||
# redirect to referer path
|
||||
url = urljoin(base_host(request=request), path)
|
||||
url = urljoin(base_host(request=request, is_app=True), path)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
except AuthenticationException as e:
|
||||
@@ -137,8 +156,8 @@ class MagicSignInEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
"accounts/sign-in?" + urlencode(params),
|
||||
base_host(request=request, is_app=True),
|
||||
"sign-in?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -163,12 +182,29 @@ class MagicSignUpEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
base_host(request=request, is_app=True),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
# Existing user
|
||||
existing_user = User.objects.filter(email=email).first()
|
||||
if not existing_user:
|
||||
if not existing_user.is_active:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"USER_ACCOUNT_DEACTIVATED"
|
||||
],
|
||||
error_message="USER_ACCOUNT_DEACTIVATED",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_app=True),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
if User.objects.filter(email=email).exists():
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["USER_ALREADY_EXIST"],
|
||||
error_message="USER_ALREADY_EXIST",
|
||||
@@ -177,7 +213,7 @@ class MagicSignUpEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
base_host(request=request, is_app=True),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -188,7 +224,7 @@ class MagicSignUpEndpoint(View):
|
||||
)
|
||||
user = provider.authenticate()
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user)
|
||||
user_login(request=request, user=user, is_app=True)
|
||||
# Process workspace and project invitations
|
||||
process_workspace_project_invitations(user=user)
|
||||
# Get the redirection path
|
||||
@@ -197,7 +233,7 @@ class MagicSignUpEndpoint(View):
|
||||
else:
|
||||
path = get_redirection_path(user=user)
|
||||
# redirect to referer path
|
||||
url = urljoin(base_host(request=request), path)
|
||||
url = urljoin(base_host(request=request, is_app=True), path)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
except AuthenticationException as e:
|
||||
@@ -205,7 +241,7 @@ class MagicSignUpEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
base_host(request=request, is_app=True),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
# 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,) = get_configuration_value(
|
||||
[
|
||||
{
|
||||
"key": "EMAIL_HOST",
|
||||
"default": os.environ.get("EMAIL_HOST"),
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
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 urlencode, urljoin
|
||||
from urllib.parse import urljoin
|
||||
|
||||
# Django imports
|
||||
from django.views import View
|
||||
@@ -23,12 +23,9 @@ class SignOutAuthEndpoint(View):
|
||||
user.save()
|
||||
# Log the user out
|
||||
logout(request)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
"accounts/sign-in?" + urlencode({"success": "true"}),
|
||||
)
|
||||
url = urljoin(base_host(request=request, is_app=True), "sign-in")
|
||||
return HttpResponseRedirect(url)
|
||||
except Exception:
|
||||
return HttpResponseRedirect(
|
||||
base_host(request=request), "accounts/sign-in"
|
||||
base_host(request=request, is_app=True), "sign-in"
|
||||
)
|
||||
|
||||
@@ -1,21 +1,3 @@
|
||||
# Python imports
|
||||
import os
|
||||
from urllib.parse import urlencode, urljoin
|
||||
|
||||
# Django imports
|
||||
from django.contrib.auth.tokens import PasswordResetTokenGenerator
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import validate_email
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.middleware.csrf import get_token
|
||||
from django.utils.encoding import (
|
||||
DjangoUnicodeDecodeError,
|
||||
smart_bytes,
|
||||
smart_str,
|
||||
)
|
||||
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
|
||||
from django.views import View
|
||||
|
||||
# Third party imports
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import AllowAny
|
||||
@@ -29,15 +11,12 @@ from plane.app.serializers import (
|
||||
UserSerializer,
|
||||
)
|
||||
from plane.authentication.utils.login import user_login
|
||||
from plane.bgtasks.forgot_password_task import forgot_password
|
||||
from plane.db.models import User
|
||||
from plane.license.models import Instance
|
||||
from plane.license.utils.instance_value import get_configuration_value
|
||||
from plane.authentication.utils.host import base_host
|
||||
from plane.authentication.adapter.error import (
|
||||
AuthenticationException,
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
)
|
||||
from django.middleware.csrf import get_token
|
||||
|
||||
|
||||
class CSRFTokenEndpoint(APIView):
|
||||
@@ -55,225 +34,62 @@ 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)
|
||||
user = User.objects.get(pk=request.user.id)
|
||||
if serializer.is_valid():
|
||||
if not user.check_password(serializer.data.get("old_password")):
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"INCORRECT_OLD_PASSWORD"
|
||||
],
|
||||
error_message="INCORRECT_OLD_PASSWORD",
|
||||
payload={"error": "Old password is not correct"},
|
||||
)
|
||||
return Response(
|
||||
exc.get_error_dict(),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# check the password score
|
||||
results = zxcvbn(serializer.data.get("new_password"))
|
||||
if results["score"] < 3:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"INVALID_NEW_PASSWORD"
|
||||
],
|
||||
error_message="INVALID_NEW_PASSWORD",
|
||||
)
|
||||
return Response(
|
||||
exc.get_error_dict(),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
old_password = request.data.get("old_password", False)
|
||||
new_password = request.data.get("new_password", False)
|
||||
|
||||
# set_password also hashes the password that the user will get
|
||||
user.set_password(serializer.data.get("new_password"))
|
||||
user.is_password_autoset = False
|
||||
user.save()
|
||||
user_login(user=user, request=request)
|
||||
if not old_password or not new_password:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["MISSING_PASSWORD"],
|
||||
error_message="MISSING_PASSWORD",
|
||||
payload={"error": "Old or new password is missing"},
|
||||
)
|
||||
return Response(
|
||||
{"message": "Password updated successfully"},
|
||||
status=status.HTTP_200_OK,
|
||||
exc.get_error_dict(),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["INVALID_PASSWORD"],
|
||||
error_message="INVALID_PASSWORD",
|
||||
)
|
||||
return Response(
|
||||
exc.get_error_dict(),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
if not user.check_password(old_password):
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"INCORRECT_OLD_PASSWORD"
|
||||
],
|
||||
error_message="INCORRECT_OLD_PASSWORD",
|
||||
payload={"error": "Old password is not correct"},
|
||||
)
|
||||
return Response(
|
||||
exc.get_error_dict(),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# check the password score
|
||||
results = zxcvbn(new_password)
|
||||
if results["score"] < 3:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"INVALID_NEW_PASSWORD"
|
||||
],
|
||||
error_message="INVALID_NEW_PASSWORD",
|
||||
)
|
||||
return Response(
|
||||
exc.get_error_dict(),
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# set_password also hashes the password that the user will get
|
||||
user.set_password(new_password)
|
||||
user.is_password_autoset = False
|
||||
user.save()
|
||||
user_login(user=user, request=request, is_app=True)
|
||||
return Response(
|
||||
{"message": "Password updated successfully"},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
class SetUserPasswordEndpoint(APIView):
|
||||
def post(self, request):
|
||||
user = User.objects.get(pk=request.user.id)
|
||||
@@ -320,7 +136,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)
|
||||
user_login(user=user, request=request, is_app=True)
|
||||
# Return the user
|
||||
serializer = UserSerializer(user)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -68,6 +68,17 @@ class EmailCheckEndpoint(APIView):
|
||||
|
||||
# If existing user
|
||||
if existing_user:
|
||||
if not existing_user.is_active:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"USER_ACCOUNT_DEACTIVATED"
|
||||
],
|
||||
error_message="USER_ACCOUNT_DEACTIVATED",
|
||||
)
|
||||
return Response(
|
||||
exc.get_error_dict(), status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
return Response(
|
||||
{
|
||||
"existing": True,
|
||||
|
||||
@@ -37,8 +37,8 @@ class SignInAuthSpaceEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
"accounts/sign-in?" + urlencode(params),
|
||||
base_host(request=request, is_space=True),
|
||||
"?" + 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),
|
||||
"spaces/accounts/sign-in?" + urlencode(params),
|
||||
base_host(request=request, is_space=True),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -78,12 +78,15 @@ class SignInAuthSpaceEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
"spaces/accounts/sign-in?" + urlencode(params),
|
||||
base_host(request=request, is_space=True),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
if not User.objects.filter(email=email).exists():
|
||||
# Existing User
|
||||
existing_user = User.objects.filter(email=email).first()
|
||||
|
||||
if not existing_user:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["USER_DOES_NOT_EXIST"],
|
||||
error_message="USER_DOES_NOT_EXIST",
|
||||
@@ -93,8 +96,24 @@ class SignInAuthSpaceEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
"spaces/accounts/sign-in?" + urlencode(params),
|
||||
base_host(request=request, is_space=True),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
if not existing_user.is_active:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"USER_ACCOUNT_DEACTIVATED"
|
||||
],
|
||||
error_message="USER_ACCOUNT_DEACTIVATED",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_space=True),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -104,11 +123,11 @@ class SignInAuthSpaceEndpoint(View):
|
||||
)
|
||||
user = provider.authenticate()
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user)
|
||||
user_login(request=request, user=user, is_space=True)
|
||||
# redirect to next path
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
str(next_path) if next_path else "/",
|
||||
base_host(request=request, is_space=True),
|
||||
str(next_path) if next_path else "",
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
except AuthenticationException as e:
|
||||
@@ -116,8 +135,8 @@ class SignInAuthSpaceEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
"spaces/accounts/sign-in?" + urlencode(params),
|
||||
base_host(request=request, is_space=True),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -140,8 +159,8 @@ class SignUpAuthSpaceEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
"spaces?" + urlencode(params),
|
||||
base_host(request=request, is_space=True),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -161,8 +180,8 @@ class SignUpAuthSpaceEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
"spaces?" + urlencode(params),
|
||||
base_host(request=request, is_space=True),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
# Validate the email
|
||||
@@ -180,12 +199,31 @@ class SignUpAuthSpaceEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
"spaces?" + urlencode(params),
|
||||
base_host(request=request, is_space=True),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
if User.objects.filter(email=email).exists():
|
||||
# Existing User
|
||||
existing_user = User.objects.filter(email=email).first()
|
||||
|
||||
if existing_user:
|
||||
if not existing_user.is_active:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"USER_ACCOUNT_DEACTIVATED"
|
||||
],
|
||||
error_message="USER_ACCOUNT_DEACTIVATED",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_space=True),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["USER_ALREADY_EXIST"],
|
||||
error_message="USER_ALREADY_EXIST",
|
||||
@@ -195,8 +233,8 @@ class SignUpAuthSpaceEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
"spaces?" + urlencode(params),
|
||||
base_host(request=request, is_space=True),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -206,11 +244,11 @@ class SignUpAuthSpaceEndpoint(View):
|
||||
)
|
||||
user = provider.authenticate()
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user)
|
||||
user_login(request=request, user=user, is_space=True)
|
||||
# redirect to referer path
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
str(next_path) if next_path else "spaces",
|
||||
base_host(request=request, is_space=True),
|
||||
str(next_path) if next_path else "",
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
except AuthenticationException as e:
|
||||
@@ -218,7 +256,7 @@ class SignUpAuthSpaceEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
"spaces?" + urlencode(params),
|
||||
base_host(request=request, is_space=True),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -3,7 +3,6 @@ import uuid
|
||||
from urllib.parse import urlencode, urljoin
|
||||
|
||||
# Django import
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.views import View
|
||||
|
||||
@@ -22,7 +21,7 @@ class GitHubOauthInitiateSpaceEndpoint(View):
|
||||
|
||||
def get(self, request):
|
||||
# Get host and next path
|
||||
request.session["host"] = base_host(request=request)
|
||||
request.session["host"] = base_host(request=request, is_space=True)
|
||||
next_path = request.GET.get("next_path")
|
||||
if next_path:
|
||||
request.session["next_path"] = str(next_path)
|
||||
@@ -40,7 +39,7 @@ class GitHubOauthInitiateSpaceEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
base_host(request=request, is_space=True),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -56,7 +55,7 @@ class GitHubOauthInitiateSpaceEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
base_host(request=request, is_space=True),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -109,10 +108,10 @@ class GitHubCallbackSpaceEndpoint(View):
|
||||
)
|
||||
user = provider.authenticate()
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user)
|
||||
user_login(request=request, user=user, is_space=True)
|
||||
# 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)
|
||||
request.session["host"] = base_host(request=request, is_space=True)
|
||||
next_path = request.GET.get("next_path")
|
||||
if next_path:
|
||||
request.session["next_path"] = str(next_path)
|
||||
@@ -37,7 +37,7 @@ class GoogleOauthInitiateSpaceEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
base_host(request=request, is_space=True),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -53,7 +53,7 @@ class GoogleOauthInitiateSpaceEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
base_host(request=request, is_space=True),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -103,7 +103,7 @@ class GoogleCallbackSpaceEndpoint(View):
|
||||
)
|
||||
user = provider.authenticate()
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user)
|
||||
user_login(request=request, user=user, is_space=True)
|
||||
# redirect to referer path
|
||||
url = urljoin(
|
||||
base_host, str(next_path) if next_path else "/spaces"
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
from urllib.parse import urlencode, urljoin
|
||||
|
||||
# Django imports
|
||||
from django.core.exceptions import ImproperlyConfigured, ValidationError
|
||||
from django.core.validators import validate_email
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.views import View
|
||||
@@ -48,7 +47,7 @@ class MagicGenerateSpaceEndpoint(APIView):
|
||||
exc.get_error_dict(), status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
origin = base_host(request=request)
|
||||
origin = base_host(request=request, is_space=True)
|
||||
email = request.data.get("email", False)
|
||||
try:
|
||||
# Clean up the email
|
||||
@@ -86,39 +85,58 @@ class MagicSignInSpaceEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
"spaces/accounts/sign-in?" + urlencode(params),
|
||||
base_host(request=request, is_space=True),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
if not User.objects.filter(email=email).exists():
|
||||
params = {
|
||||
"error_code": "USER_DOES_NOT_EXIST",
|
||||
"error_message": "User could not be found with the given email.",
|
||||
}
|
||||
existing_user = User.objects.filter(email=email).first()
|
||||
|
||||
if not existing_user:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["USER_DOES_NOT_EXIST"],
|
||||
error_message="USER_DOES_NOT_EXIST",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
"accounts/sign-in?" + urlencode(params),
|
||||
base_host(request=request, is_space=True),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
# Active User
|
||||
if not existing_user.is_active:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"USER_ACCOUNT_DEACTIVATED"
|
||||
],
|
||||
error_message="USER_ACCOUNT_DEACTIVATED",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_space=True),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
try:
|
||||
provider = MagicCodeProvider(
|
||||
request=request, key=f"magic_{email}", code=code
|
||||
)
|
||||
user = provider.authenticate()
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user)
|
||||
user_login(request=request, user=user, is_space=True)
|
||||
# redirect to referer path
|
||||
profile = Profile.objects.get(user=user)
|
||||
if user.is_password_autoset and profile.is_onboarded:
|
||||
path = "spaces/accounts/set-password"
|
||||
path = "accounts/set-password"
|
||||
else:
|
||||
# Get the redirection path
|
||||
path = str(next_path) if next_path else "spaces"
|
||||
url = urljoin(base_host(request=request), path)
|
||||
path = str(next_path) if next_path else ""
|
||||
url = urljoin(base_host(request=request, is_space=True), path)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
except AuthenticationException as e:
|
||||
@@ -126,8 +144,8 @@ class MagicSignInSpaceEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
"spaces/accounts/sign-in?" + urlencode(params),
|
||||
base_host(request=request, is_space=True),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -152,12 +170,29 @@ class MagicSignUpSpaceEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
"spaces/accounts/sign-in?" + urlencode(params),
|
||||
base_host(request=request, is_space=True),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
# Existing User
|
||||
existing_user = User.objects.filter(email=email).first()
|
||||
if existing_user:
|
||||
if not existing_user.is_active:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"USER_ACCOUNT_DEACTIVATED"
|
||||
],
|
||||
error_message="USER_ACCOUNT_DEACTIVATED",
|
||||
)
|
||||
params = exc.get_error_dict()
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_space=True),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
if User.objects.filter(email=email).exists():
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["USER_ALREADY_EXIST"],
|
||||
error_message="USER_ALREADY_EXIST",
|
||||
@@ -166,7 +201,7 @@ class MagicSignUpSpaceEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
base_host(request=request, is_space=True),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -177,10 +212,10 @@ class MagicSignUpSpaceEndpoint(View):
|
||||
)
|
||||
user = provider.authenticate()
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user)
|
||||
user_login(request=request, user=user, is_space=True)
|
||||
# redirect to referer path
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
base_host(request=request, is_space=True),
|
||||
str(next_path) if next_path else "spaces",
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
@@ -190,7 +225,7 @@ class MagicSignUpSpaceEndpoint(View):
|
||||
if next_path:
|
||||
params["next_path"] = str(next_path)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
"spaces/accounts/sign-in?" + urlencode(params),
|
||||
base_host(request=request, is_space=True),
|
||||
"?" + urlencode(params),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
# 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)
|
||||
@@ -1,6 +1,3 @@
|
||||
# Python imports
|
||||
from urllib.parse import urlencode, urljoin
|
||||
|
||||
# Django imports
|
||||
from django.views import View
|
||||
from django.contrib.auth import logout
|
||||
@@ -23,12 +20,10 @@ class SignOutAuthSpaceEndpoint(View):
|
||||
user.save()
|
||||
# Log the user out
|
||||
logout(request)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
"accounts/sign-in?" + urlencode({"success": "true"}),
|
||||
return HttpResponseRedirect(
|
||||
base_host(request=request, is_space=True)
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
except Exception:
|
||||
return HttpResponseRedirect(
|
||||
base_host(request=request), "accounts/sign-in"
|
||||
base_host(request=request, is_space=True)
|
||||
)
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
from django.core.mail import EmailMultiAlternatives, get_connection
|
||||
from django.core.management import BaseCommand, CommandError
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.html import strip_tags
|
||||
|
||||
# Module imports
|
||||
from plane.license.utils.instance_value import get_email_configuration
|
||||
|
||||
|
||||
@@ -37,10 +40,10 @@ class Command(BaseCommand):
|
||||
timeout=30,
|
||||
)
|
||||
# Prepare email details
|
||||
subject = "Email Notification from Plane"
|
||||
message = (
|
||||
"This is a sample email notification sent from Plane application."
|
||||
)
|
||||
subject = "Test email from Plane"
|
||||
|
||||
html_content = render_to_string("emails/test_email.html")
|
||||
text_content = strip_tags(html_content)
|
||||
|
||||
self.stdout.write(self.style.SUCCESS("Trying to send test email..."))
|
||||
|
||||
@@ -48,11 +51,14 @@ class Command(BaseCommand):
|
||||
try:
|
||||
msg = EmailMultiAlternatives(
|
||||
subject=subject,
|
||||
body=message,
|
||||
body=text_content,
|
||||
from_email=EMAIL_FROM,
|
||||
to=[receiver_email],
|
||||
to=[
|
||||
receiver_email,
|
||||
],
|
||||
connection=connection,
|
||||
)
|
||||
msg.attach_alternative(html_content, "text/html")
|
||||
msg.send()
|
||||
self.stdout.write(self.style.SUCCESS("Email successfully sent"))
|
||||
except Exception as e:
|
||||
|
||||
@@ -106,8 +106,8 @@ class InstanceAdminSignUpEndpoint(View):
|
||||
error_message="INSTANCE_NOT_CONFIGURED",
|
||||
)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
"god-mode/setup?" + urlencode(exc.get_error_dict()),
|
||||
base_host(request=request, is_admin=True),
|
||||
"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),
|
||||
"god-mode/setup?" + urlencode(exc.get_error_dict()),
|
||||
base_host(request=request, is_admin=True),
|
||||
"setup?" + urlencode(exc.get_error_dict()),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -147,8 +147,8 @@ class InstanceAdminSignUpEndpoint(View):
|
||||
},
|
||||
)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
"god-mode/setup?" + urlencode(exc.get_error_dict()),
|
||||
base_host(request=request, is_admin=True),
|
||||
"setup?" + urlencode(exc.get_error_dict()),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -169,8 +169,8 @@ class InstanceAdminSignUpEndpoint(View):
|
||||
},
|
||||
)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
"god-mode/setup?" + urlencode(exc.get_error_dict()),
|
||||
base_host(request=request, is_admin=True),
|
||||
"setup?" + urlencode(exc.get_error_dict()),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -191,8 +191,8 @@ class InstanceAdminSignUpEndpoint(View):
|
||||
},
|
||||
)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
"god-mode/setup?" + urlencode(exc.get_error_dict()),
|
||||
base_host(request=request, is_admin=True),
|
||||
"setup?" + urlencode(exc.get_error_dict()),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
else:
|
||||
@@ -213,8 +213,8 @@ class InstanceAdminSignUpEndpoint(View):
|
||||
},
|
||||
)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
"god-mode/setup?" + urlencode(exc.get_error_dict()),
|
||||
base_host(request=request, is_admin=True),
|
||||
"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)
|
||||
url = urljoin(base_host(request=request), "god-mode/general")
|
||||
user_login(request=request, user=user, is_admin=True)
|
||||
url = urljoin(base_host(request=request, is_admin=True), "general")
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
|
||||
@@ -269,8 +269,8 @@ class InstanceAdminSignInEndpoint(View):
|
||||
error_message="INSTANCE_NOT_CONFIGURED",
|
||||
)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
"god-mode/login?" + urlencode(exc.get_error_dict()),
|
||||
base_host(request=request, is_admin=True),
|
||||
"?" + urlencode(exc.get_error_dict()),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -290,8 +290,8 @@ class InstanceAdminSignInEndpoint(View):
|
||||
},
|
||||
)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
"god-mode/login?" + urlencode(exc.get_error_dict()),
|
||||
base_host(request=request, is_admin=True),
|
||||
"?" + urlencode(exc.get_error_dict()),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -308,14 +308,28 @@ class InstanceAdminSignInEndpoint(View):
|
||||
},
|
||||
)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
"god-mode/login?" + urlencode(exc.get_error_dict()),
|
||||
base_host(request=request, is_admin=True),
|
||||
"?" + urlencode(exc.get_error_dict()),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
# Fetch the user
|
||||
user = User.objects.filter(email=email).first()
|
||||
|
||||
# is_active
|
||||
if not user.is_active:
|
||||
exc = AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"ADMIN_USER_DEACTIVATED"
|
||||
],
|
||||
error_message="ADMIN_USER_DEACTIVATED",
|
||||
)
|
||||
url = urljoin(
|
||||
base_host(request=request, is_admin=True),
|
||||
"?" + urlencode(exc.get_error_dict()),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
# Error out if the user is not present
|
||||
if not user:
|
||||
exc = AuthenticationException(
|
||||
@@ -328,8 +342,8 @@ class InstanceAdminSignInEndpoint(View):
|
||||
},
|
||||
)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
"god-mode/login?" + urlencode(exc.get_error_dict()),
|
||||
base_host(request=request, is_admin=True),
|
||||
"?" + urlencode(exc.get_error_dict()),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -345,8 +359,8 @@ class InstanceAdminSignInEndpoint(View):
|
||||
},
|
||||
)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
"god-mode/login?" + urlencode(exc.get_error_dict()),
|
||||
base_host(request=request, is_admin=True),
|
||||
"?" + urlencode(exc.get_error_dict()),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
@@ -362,8 +376,8 @@ class InstanceAdminSignInEndpoint(View):
|
||||
},
|
||||
)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
"god-mode/login?" + urlencode(exc.get_error_dict()),
|
||||
base_host(request=request, is_admin=True),
|
||||
"?" + urlencode(exc.get_error_dict()),
|
||||
)
|
||||
return HttpResponseRedirect(url)
|
||||
# settings last active for the user
|
||||
@@ -376,8 +390,8 @@ class InstanceAdminSignInEndpoint(View):
|
||||
user.save()
|
||||
|
||||
# get tokens for user
|
||||
user_login(request=request, user=user)
|
||||
url = urljoin(base_host(request=request), "god-mode/general")
|
||||
user_login(request=request, user=user, is_admin=True)
|
||||
url = urljoin(base_host(request=request, is_admin=True), "general")
|
||||
return HttpResponseRedirect(url)
|
||||
|
||||
|
||||
@@ -410,12 +424,9 @@ class InstanceAdminSignOutEndpoint(View):
|
||||
user.save()
|
||||
# Log the user out
|
||||
logout(request)
|
||||
url = urljoin(
|
||||
base_host(request=request),
|
||||
"accounts/sign-in?" + urlencode({"success": "true"}),
|
||||
)
|
||||
url = urljoin(base_host(request=request, is_admin=True))
|
||||
return HttpResponseRedirect(url)
|
||||
except Exception:
|
||||
return HttpResponseRedirect(
|
||||
base_host(request=request), "accounts/sign-in"
|
||||
base_host(request=request, is_admin=True)
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import os
|
||||
|
||||
# Django imports
|
||||
from django.conf import settings
|
||||
|
||||
# Third party imports
|
||||
from rest_framework import status
|
||||
@@ -37,6 +38,8 @@ class InstanceEndpoint(BaseAPIView):
|
||||
@cache_response(60 * 60 * 2, user=False)
|
||||
def get(self, request):
|
||||
instance = Instance.objects.first()
|
||||
|
||||
print("Instance: ", instance)
|
||||
# get the instance
|
||||
if instance is None:
|
||||
return Response(
|
||||
@@ -148,9 +151,13 @@ class InstanceEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
# is smtp configured
|
||||
data["is_smtp_configured"] = (
|
||||
bool(EMAIL_HOST)
|
||||
)
|
||||
data["is_smtp_configured"] = bool(EMAIL_HOST)
|
||||
|
||||
# Base URL
|
||||
data["admin_base_url"] = settings.ADMIN_BASE_URL
|
||||
data["space_base_url"] = settings.SPACE_BASE_URL
|
||||
data["app_base_url"] = settings.APP_BASE_URL
|
||||
|
||||
instance_data = serializer.data
|
||||
instance_data["workspaces_exist"] = Workspace.objects.count() > 1
|
||||
|
||||
|
||||
@@ -342,3 +342,8 @@ CSRF_COOKIE_SECURE = secure_origins
|
||||
CSRF_COOKIE_HTTPONLY = True
|
||||
CSRF_TRUSTED_ORIGINS = cors_allowed_origins
|
||||
CSRF_COOKIE_DOMAIN = os.environ.get("COOKIE_DOMAIN", None)
|
||||
|
||||
# Base URLs
|
||||
ADMIN_BASE_URL = os.environ.get("ADMIN_BASE_URL", None)
|
||||
SPACE_BASE_URL = os.environ.get("SPACE_BASE_URL", None)
|
||||
APP_BASE_URL = os.environ.get("APP_BASE_URL") or os.environ.get("WEB_URL")
|
||||
|
||||
@@ -30,17 +30,6 @@ INTERNAL_IPS = ("127.0.0.1",)
|
||||
MEDIA_URL = "/uploads/"
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, "uploads") # noqa
|
||||
|
||||
CORS_ALLOWED_ORIGINS = [
|
||||
"http://localhost:3000",
|
||||
"http://127.0.0.1:3000",
|
||||
"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
|
||||
|
||||
LOG_DIR = os.path.join(BASE_DIR, "logs") # noqa
|
||||
|
||||
if not os.path.exists(LOG_DIR):
|
||||
|
||||
@@ -12,8 +12,6 @@ SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||||
|
||||
INSTALLED_APPS += ("scout_apm.django",) # noqa
|
||||
|
||||
# Honor the 'X-Forwarded-Proto' header for request.is_secure()
|
||||
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||||
|
||||
# Scout Settings
|
||||
SCOUT_MONITOR = os.environ.get("SCOUT_MONITOR", False)
|
||||
|
||||
@@ -21,6 +21,7 @@ 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:
|
||||
@@ -49,6 +50,10 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
|
||||
SearchFilter,
|
||||
)
|
||||
|
||||
authentication_classes = [
|
||||
BaseSessionAuthentication,
|
||||
]
|
||||
|
||||
filterset_fields = []
|
||||
|
||||
search_fields = []
|
||||
@@ -146,6 +151,10 @@ class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
|
||||
|
||||
search_fields = []
|
||||
|
||||
authentication_classes = [
|
||||
BaseSessionAuthentication,
|
||||
]
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
for backend in list(self.filter_backends):
|
||||
queryset = backend().filter_queryset(self.request, queryset, self)
|
||||
|
||||
@@ -60,4 +60,4 @@ zxcvbn==4.4.28
|
||||
# timezone
|
||||
pytz==2024.1
|
||||
# jwt
|
||||
jwt==1.3.1
|
||||
PyJWT==2.8.0
|
||||
@@ -1 +1 @@
|
||||
python-3.11.9
|
||||
python-3.12.3
|
||||
@@ -0,0 +1,6 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html>
|
||||
<p>This is a test email sent to verify if email configuration is working as expected in your Plane instance.</p>
|
||||
|
||||
<p>Regards,</br> Team Plane </p>
|
||||
</html>
|
||||
@@ -96,7 +96,7 @@ This will prompt you with the below options.
|
||||
|
||||
```bash
|
||||
Select a Action you want to perform:
|
||||
1) Install (arm64)
|
||||
1) Install (x86_64)
|
||||
2) Start
|
||||
3) Stop
|
||||
4) Restart
|
||||
@@ -344,7 +344,7 @@ Similarly, you can view the logs of other services.
|
||||
|
||||
There would a time when you might want to backup your data from docker volumes to external storage like S3 or drives.
|
||||
|
||||
Lets again run the `./setup.sh` command. You will again be prompted with the below options. This time select `8` to view logs.
|
||||
Lets again run the `./setup.sh` command. You will again be prompted with the below options. This time select `7` to Backup the data.
|
||||
|
||||
```bash
|
||||
Select a Action you want to perform:
|
||||
|
||||
@@ -33,14 +33,19 @@ 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: /usr/local/bin/start.sh web/server.js web
|
||||
command: node web/server.js web
|
||||
deploy:
|
||||
replicas: ${WEB_REPLICAS:-1}
|
||||
depends_on:
|
||||
@@ -50,22 +55,24 @@ 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: /usr/local/bin/start.sh space/server.js space
|
||||
command: node space/server.js space
|
||||
deploy:
|
||||
replicas: ${SPACE_REPLICAS:-1}
|
||||
depends_on:
|
||||
- api
|
||||
- worker
|
||||
- web
|
||||
|
||||
|
||||
admin:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-space:${APP_RELEASE:-stable}
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-admin:${APP_RELEASE:-stable}
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
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:
|
||||
@@ -75,6 +82,7 @@ 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
|
||||
@@ -89,6 +97,7 @@ 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
|
||||
@@ -102,6 +111,7 @@ 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
|
||||
@@ -115,6 +125,7 @@ services:
|
||||
migrator:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: ${PULL_POLICY:-always}
|
||||
restart: no
|
||||
command: >
|
||||
@@ -134,6 +145,7 @@ services:
|
||||
command: postgres -c 'max_connections=1000'
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
|
||||
plane-redis:
|
||||
<<: *app-env
|
||||
image: redis:7.2.4-alpine
|
||||
@@ -144,7 +156,7 @@ services:
|
||||
|
||||
plane-minio:
|
||||
<<: *app-env
|
||||
image: minio/minio
|
||||
image: minio/minio:latest
|
||||
pull_policy: if_not_present
|
||||
restart: unless-stopped
|
||||
command: server /export --console-address ":9090"
|
||||
@@ -155,6 +167,7 @@ services:
|
||||
proxy:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-proxy:${APP_RELEASE:-stable}
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: ${PULL_POLICY:-always}
|
||||
ports:
|
||||
- ${NGINX_PORT}:80
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
BRANCH=master
|
||||
SCRIPT_DIR=$PWD
|
||||
PLANE_INSTALL_DIR=$PWD/plane-app
|
||||
SERVICE_FOLDER=plane-app
|
||||
PLANE_INSTALL_DIR=$PWD/$SERVICE_FOLDER
|
||||
export APP_RELEASE=$BRANCH
|
||||
export DOCKERHUB_USER=makeplane
|
||||
export PULL_POLICY=always
|
||||
@@ -140,7 +141,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=plane-app-migrator")
|
||||
local migrator_container_id=$(docker container ls -aq -f "name=$SERVICE_FOLDER-migrator")
|
||||
if [ -n "$migrator_container_id" ]; then
|
||||
local idx=0
|
||||
while docker inspect --format='{{.State.Status}}' $migrator_container_id | grep -q "running"; do
|
||||
@@ -168,7 +169,7 @@ function startServices() {
|
||||
fi
|
||||
fi
|
||||
|
||||
local api_container_id=$(docker container ls -q -f "name=plane-app-api")
|
||||
local api_container_id=$(docker container ls -q -f "name=$SERVICE_FOLDER-api")
|
||||
local idx2=0
|
||||
while ! docker logs $api_container_id 2>&1 | grep -m 1 -i "Application startup complete" | grep -q ".";
|
||||
do
|
||||
@@ -389,7 +390,7 @@ fi
|
||||
|
||||
# CPU ARCHITECHTURE BASED SETTINGS
|
||||
CPU_ARCH=$(uname -m)
|
||||
if [[ $CPU_ARCH == "amd64" || $CPU_ARCH == "x86_64" || ( $BRANCH == "master" && ( $CPU_ARCH == "arm64" || $CPU_ARCH == "aarch64" ) ) ]];
|
||||
if [[ $FORCE_CPU == "amd64" || $CPU_ARCH == "amd64" || $CPU_ARCH == "x86_64" || ( $BRANCH == "master" && ( $CPU_ARCH == "arm64" || $CPU_ARCH == "aarch64" ) ) ]];
|
||||
then
|
||||
USE_GLOBAL_IMAGES=1
|
||||
DOCKERHUB_USER=makeplane
|
||||
@@ -408,7 +409,8 @@ fi
|
||||
# REMOVE SPECIAL CHARACTERS FROM BRANCH NAME
|
||||
if [ "$BRANCH" != "master" ];
|
||||
then
|
||||
PLANE_INSTALL_DIR=$PWD/plane-app-$(echo $BRANCH | sed -r 's@(\/|" "|\.)@-@g')
|
||||
SERVICE_FOLDER=plane-app-$(echo $BRANCH | sed -r 's@(\/|" "|\.)@-@g')
|
||||
PLANE_INSTALL_DIR=$PWD/$SERVICE_FOLDER
|
||||
fi
|
||||
mkdir -p $PLANE_INSTALL_DIR/archive
|
||||
|
||||
|
||||
@@ -43,3 +43,6 @@ 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
|
||||
@@ -73,6 +73,20 @@ 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
|
||||
@@ -167,3 +181,4 @@ services:
|
||||
- web
|
||||
- api
|
||||
- space
|
||||
- admin
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user