From 58bf056ddbd5a8554c1141a31b7d59103ca2e94f Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Thu, 9 May 2024 17:46:31 +0530 Subject: [PATCH] fix: auth redirection issues in the web, space and admin apps (#4414) * fix: login redirection * dev: log the user out when deactivating the account * dev: update redirect uris for google and github * fix: redirection url and invitation api and add redirection to god mode in nginx * dev: add reset password redirection * dev: update nginx headers * dev: fix setup sh and env example and put validation for use minio when fetching project covers * dev: stabilize dev setup * fix: handled redirection error in web, space, and admin apps * fix: resovled build errors --------- Co-authored-by: pablohashescobar --- admin/Dockerfile.dev | 12 +++++ admin/app/layout.tsx | 2 +- admin/app/login/layout.tsx | 19 -------- admin/app/login/page.tsx | 18 ------- admin/app/page.tsx | 34 +++++++------ .../components/admin-sidebar/help-section.tsx | 2 +- .../components => components/login}/index.ts | 0 .../login}/sign-in-form.tsx | 0 admin/components/new-user-popup.tsx | 2 +- admin/lib/store-context.tsx | 2 +- admin/lib/wrappers/auth-wrapper.tsx | 48 +++++++++---------- admin/lib/wrappers/instance-wrapper.tsx | 15 ++---- admin/services/api.service.ts | 5 +- admin/store/root-store.ts | 4 +- admin/store/user.store.ts | 36 ++++++++++---- apiserver/.env.example | 2 +- apiserver/plane/app/views/project/base.py | 18 +++++-- apiserver/plane/app/views/user/base.py | 10 ++++ .../plane/authentication/adapter/exception.py | 8 +--- .../authentication/provider/oauth/github.py | 4 +- .../authentication/provider/oauth/google.py | 4 +- apiserver/plane/authentication/utils/host.py | 2 +- .../authentication/utils/redirection_path.py | 11 +++-- .../plane/authentication/views/common.py | 2 +- apiserver/plane/settings/local.py | 2 + apiserver/requirements/base.txt | 2 +- docker-compose-local.yml | 15 ++++++ nginx/env.sh | 1 + nginx/nginx.conf.dev | 6 ++- nginx/nginx.conf.template | 2 + setup.sh | 2 + space/.env.example | 2 + space/hooks/store/user/use-user.ts | 2 +- space/lib/store-context.tsx | 2 +- space/pages/_app.tsx | 2 +- space/services/api.service.ts | 5 +- space/store/root.store.ts | 2 +- space/store/user/{index.store.ts => index.ts} | 18 ++++++- .../account/auth-forms/auth-root.tsx | 5 +- .../account/auth-forms/password.tsx | 38 +++++++++------ .../account/auth-forms/unique-code.tsx | 4 +- .../account/deactivate-account-modal.tsx | 14 ++---- web/lib/store-context.tsx | 2 +- web/services/api.service.ts | 8 +++- web/store/user/index.ts | 18 +++++++ yarn.lock | 10 ++-- 46 files changed, 250 insertions(+), 172 deletions(-) create mode 100644 admin/Dockerfile.dev delete mode 100644 admin/app/login/layout.tsx delete mode 100644 admin/app/login/page.tsx rename admin/{app/login/components => components/login}/index.ts (100%) rename admin/{app/login/components => components/login}/sign-in-form.tsx (100%) create mode 100644 space/.env.example rename space/store/user/{index.store.ts => index.ts} (92%) diff --git a/admin/Dockerfile.dev b/admin/Dockerfile.dev new file mode 100644 index 000000000..0cbbdc8af --- /dev/null +++ b/admin/Dockerfile.dev @@ -0,0 +1,12 @@ +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 +EXPOSE 3000 +VOLUME [ "/app/node_modules", "/app/admin/node_modules" ] +CMD ["yarn", "dev", "--filter=admin"] diff --git a/admin/app/layout.tsx b/admin/app/layout.tsx index 80f85f0c2..d991f9d82 100644 --- a/admin/app/layout.tsx +++ b/admin/app/layout.tsx @@ -15,7 +15,7 @@ interface RootLayoutProps { } const RootLayout = ({ children, ...pageProps }: RootLayoutProps) => { - const prefix = parseInt(process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX || "0") === 0 ? "/" : "/god-mode/"; + const prefix = "/god-mode/"; return ( diff --git a/admin/app/login/layout.tsx b/admin/app/login/layout.tsx deleted file mode 100644 index 84152390f..000000000 --- a/admin/app/login/layout.tsx +++ /dev/null @@ -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) => ( - - {children} - -); - -export default LoginLayout; diff --git a/admin/app/login/page.tsx b/admin/app/login/page.tsx deleted file mode 100644 index e7edc3fd7..000000000 --- a/admin/app/login/page.tsx +++ /dev/null @@ -1,18 +0,0 @@ -"use client"; - -// layouts -import { DefaultLayout } from "@/layouts"; -// components -import { PageHeader } from "@/components/core"; -import { InstanceSignInForm } from "./components"; - -const LoginPage = () => ( - <> - - - - - -); - -export default LoginPage; diff --git a/admin/app/page.tsx b/admin/app/page.tsx index 3b19fb3d6..05e9e8237 100644 --- a/admin/app/page.tsx +++ b/admin/app/page.tsx @@ -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 = () => ( + <> + + + + + + + + + +); - useEffect(() => router.push("/login"), [router]); - - return ( - <> - - - ); -}; - -export default RootPage; +export default LoginPage; diff --git a/admin/components/admin-sidebar/help-section.tsx b/admin/components/admin-sidebar/help-section.tsx index 606d1bd89..c1da25b28 100644 --- a/admin/components/admin-sidebar/help-section.tsx +++ b/admin/components/admin-sidebar/help-section.tsx @@ -36,7 +36,7 @@ export const HelpSection: FC = () => { // refs const helpOptionsRef = useRef(null); - const redirectionLink = `${process.env.NEXT_PUBLIC_APP_URL ? `${process.env.NEXT_PUBLIC_APP_URL}/create-workspace` : `${process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX === "1" ? `/god-mode/` : `/`}`}`; + const redirectionLink = `${process.env.NEXT_PUBLIC_APP_URL ? `${process.env.NEXT_PUBLIC_APP_URL}/create-workspace` : `/god-mode/`}`; return (
{ // 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 = `${process.env.NEXT_PUBLIC_APP_URL ? `${process.env.NEXT_PUBLIC_APP_URL}/create-workspace` : `/god-mode/`}`; if (!isNewUserPopup) return <>; return ( diff --git a/admin/lib/store-context.tsx b/admin/lib/store-context.tsx index 37bba1a71..8893f1a78 100644 --- a/admin/lib/store-context.tsx +++ b/admin/lib/store-context.tsx @@ -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); diff --git a/admin/lib/wrappers/auth-wrapper.tsx b/admin/lib/wrappers/auth-wrapper.tsx index bd3770376..75e7c2acc 100644 --- a/admin/lib/wrappers/auth-wrapper.tsx +++ b/admin/lib/wrappers/auth-wrapper.tsx @@ -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"; // 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 = 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 (
); - if (userStatus && userStatus?.status === EUserStatus.ERROR) - return ( -
- Something went wrong. please try again later -
- ); + 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 <>; } } } diff --git a/admin/lib/wrappers/instance-wrapper.tsx b/admin/lib/wrappers/instance-wrapper.tsx index a4444a208..da02992aa 100644 --- a/admin/lib/wrappers/instance-wrapper.tsx +++ b/admin/lib/wrappers/instance-wrapper.tsx @@ -12,7 +12,7 @@ import { InstanceNotReady } from "@/components/instance"; // hooks import { useInstance } from "@/hooks"; // helpers -import { EInstancePageType, EInstanceStatus } from "@/helpers"; +import { EInstancePageType } from "@/helpers"; type TInstanceWrapper = { children: ReactNode; @@ -24,26 +24,19 @@ export const InstanceWrapper: FC = 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, }); - if (isLoading) + if (isSWRLoading || isLoading) return (
); - if (instanceStatus && instanceStatus?.status === EInstanceStatus.ERROR) - return ( -
- Something went wrong. please try again later -
- ); - if (instance?.instance?.is_setup_done === false && authEnabled === "1") return ( diff --git a/admin/services/api.service.ts b/admin/services/api.service.ts index 9d692a6c6..344cd4f54 100644 --- a/admin/services/api.service.ts +++ b/admin/services/api.service.ts @@ -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); } ); diff --git a/admin/store/root-store.ts b/admin/store/root-store.ts index 85b2a5a8b..c05cce37f 100644 --- a/admin/store/root-store.ts +++ b/admin/store/root-store.ts @@ -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); } } diff --git a/admin/store/user.store.ts b/admin/store/user.store.ts index 77b81446f..10b5eab81 100644 --- a/admin/store/user.store.ts +++ b/admin/store/user.store.ts @@ -16,7 +16,8 @@ export interface IUserStore { currentUser: IUser | undefined; // fetch actions fetchCurrentUser: () => Promise; - signOut: () => Promise; + 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(); }; } diff --git a/apiserver/.env.example b/apiserver/.env.example index d8554f400..52d8d1c50 100644 --- a/apiserver/.env.example +++ b/apiserver/.env.example @@ -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="" diff --git a/apiserver/plane/app/views/project/base.py b/apiserver/plane/app/views/project/base.py index 6017a420f..1d23bd3aa 100644 --- a/apiserver/plane/app/views/project/base.py +++ b/apiserver/plane/app/views/project/base.py @@ -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/", diff --git a/apiserver/plane/app/views/user/base.py b/apiserver/plane/app/views/user/base.py index 60823a5a7..9fb514d11 100644 --- a/apiserver/plane/app/views/user/base.py +++ b/apiserver/plane/app/views/user/base.py @@ -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) diff --git a/apiserver/plane/authentication/adapter/exception.py b/apiserver/plane/authentication/adapter/exception.py index 2086046a4..12845ea02 100644 --- a/apiserver/plane/authentication/adapter/exception.py +++ b/apiserver/plane/authentication/adapter/exception.py @@ -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 diff --git a/apiserver/plane/authentication/provider/oauth/github.py b/apiserver/plane/authentication/provider/oauth/github.py index 29e1772b5..b52fecf97 100644 --- a/apiserver/plane/authentication/provider/oauth/github.py +++ b/apiserver/plane/authentication/provider/oauth/github.py @@ -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, diff --git a/apiserver/plane/authentication/provider/oauth/google.py b/apiserver/plane/authentication/provider/oauth/google.py index d0893d23b..5de1ac8e2 100644 --- a/apiserver/plane/authentication/provider/oauth/google.py +++ b/apiserver/plane/authentication/provider/oauth/google.py @@ -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, diff --git a/apiserver/plane/authentication/utils/host.py b/apiserver/plane/authentication/utils/host.py index d5a81a249..b9dc7189b 100644 --- a/apiserver/plane/authentication/utils/host.py +++ b/apiserver/plane/authentication/utils/host.py @@ -6,7 +6,7 @@ def base_host(request): return ( 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()}""" ) diff --git a/apiserver/plane/authentication/utils/redirection_path.py b/apiserver/plane/authentication/utils/redirection_path.py index bf9e15673..12de25cc2 100644 --- a/apiserver/plane/authentication/utils/redirection_path.py +++ b/apiserver/plane/authentication/utils/redirection_path.py @@ -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, diff --git a/apiserver/plane/authentication/views/common.py b/apiserver/plane/authentication/views/common.py index 22fbb0a5c..a66326b1a 100644 --- a/apiserver/plane/authentication/views/common.py +++ b/apiserver/plane/authentication/views/common.py @@ -206,7 +206,7 @@ class ResetPasswordEndpoint(View): url = urljoin( base_host(request=request), - "accounts/sign-in?" + urlencode({"success", True}), + "accounts/sign-in?" + urlencode({"success": True}), ) return HttpResponseRedirect(url) except DjangoUnicodeDecodeError: diff --git a/apiserver/plane/settings/local.py b/apiserver/plane/settings/local.py index 4f67e638b..2290262ae 100644 --- a/apiserver/plane/settings/local.py +++ b/apiserver/plane/settings/local.py @@ -31,6 +31,8 @@ MEDIA_URL = "/uploads/" MEDIA_ROOT = os.path.join(BASE_DIR, "uploads") # noqa CORS_ALLOWED_ORIGINS = [ + "http://localhost", + "http://127.0.0.1", "http://localhost:3000", "http://127.0.0.1:3000", "http://localhost:4000", diff --git a/apiserver/requirements/base.txt b/apiserver/requirements/base.txt index 05789af59..a6bd2ab50 100644 --- a/apiserver/requirements/base.txt +++ b/apiserver/requirements/base.txt @@ -60,4 +60,4 @@ zxcvbn==4.4.28 # timezone pytz==2024.1 # jwt -jwt==1.3.1 \ No newline at end of file +PyJWT==2.8.0 \ No newline at end of file diff --git a/docker-compose-local.yml b/docker-compose-local.yml index d79fa54d3..3dce85f3a 100644 --- a/docker-compose-local.yml +++ b/docker-compose-local.yml @@ -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 diff --git a/nginx/env.sh b/nginx/env.sh index 7db471eca..dbd59d5b7 100644 --- a/nginx/env.sh +++ b/nginx/env.sh @@ -2,5 +2,6 @@ export dollar="$" export http_upgrade="http_upgrade" +export scheme="scheme" envsubst < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf exec nginx -g 'daemon off;' diff --git a/nginx/nginx.conf.dev b/nginx/nginx.conf.dev index 872ff6748..d13897166 100644 --- a/nginx/nginx.conf.dev +++ b/nginx/nginx.conf.dev @@ -15,6 +15,8 @@ http { add_header Referrer-Policy "no-referrer-when-downgrade" always; add_header Permissions-Policy "interest-cohort=()" always; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Forwarded-Proto "${dollar}scheme"; + add_header Host "${dollar}host"; location / { proxy_pass http://web:3000/; @@ -23,8 +25,8 @@ http { proxy_set_header Connection "upgrade"; } - location /god-mode { - proxy_pass http://godmode:3000/; + location /god-mode/ { + proxy_pass http://admin:3000/god-mode/; } location /api/ { diff --git a/nginx/nginx.conf.template b/nginx/nginx.conf.template index 829c32eed..35b021e81 100644 --- a/nginx/nginx.conf.template +++ b/nginx/nginx.conf.template @@ -15,6 +15,8 @@ http { add_header Referrer-Policy "no-referrer-when-downgrade" always; add_header Permissions-Policy "interest-cohort=()" always; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + add_header X-Forwarded-Proto "${dollar}scheme"; + add_header Host "${dollar}host"; location / { proxy_pass http://web:3000/; diff --git a/setup.sh b/setup.sh index a1d9bcbe1..838f5bbac 100755 --- a/setup.sh +++ b/setup.sh @@ -7,6 +7,8 @@ export LC_CTYPE=C cp ./web/.env.example ./web/.env cp ./apiserver/.env.example ./apiserver/.env +cp ./space/.env.example ./space/.env +cp ./admin/.env.example ./admin/.env # Generate the SECRET_KEY that will be used by django echo "SECRET_KEY=\"$(tr -dc 'a-z0-9' < /dev/urandom | head -c50)\"" >> ./apiserver/.env \ No newline at end of file diff --git a/space/.env.example b/space/.env.example new file mode 100644 index 000000000..fbd4ad4f9 --- /dev/null +++ b/space/.env.example @@ -0,0 +1,2 @@ +NEXT_PUBLIC_APP_URL= +NEXT_PUBLIC_API_BASE_URL= \ No newline at end of file diff --git a/space/hooks/store/user/use-user.ts b/space/hooks/store/user/use-user.ts index a99480213..e491d88a2 100644 --- a/space/hooks/store/user/use-user.ts +++ b/space/hooks/store/user/use-user.ts @@ -1,7 +1,7 @@ import { useContext } from "react"; // store import { StoreContext } from "@/lib/store-context"; -import { IUserStore } from "@/store/user/index.store"; +import { IUserStore } from "@/store/user"; export const useUser = (): IUserStore => { const context = useContext(StoreContext); diff --git a/space/lib/store-context.tsx b/space/lib/store-context.tsx index e1493f227..1eff1ddde 100644 --- a/space/lib/store-context.tsx +++ b/space/lib/store-context.tsx @@ -2,7 +2,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); diff --git a/space/pages/_app.tsx b/space/pages/_app.tsx index 6f326a182..363b61510 100644 --- a/space/pages/_app.tsx +++ b/space/pages/_app.tsx @@ -11,7 +11,7 @@ import { StoreProvider } from "@/lib/store-context"; // wrappers import { InstanceWrapper } from "@/lib/wrappers"; -const prefix = parseInt(process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX || "0") === 0 ? "/" : "/spaces/"; +const prefix = "/spaces/"; function MyApp({ Component, pageProps }: AppProps) { return ( diff --git a/space/services/api.service.ts b/space/services/api.service.ts index 788cd57c6..b6d353ccc 100644 --- a/space/services/api.service.ts +++ b/space/services/api.service.ts @@ -1,5 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import axios, { AxiosInstance } from "axios"; +// store +import { rootStore } from "@/lib/store-context"; abstract class APIService { protected baseURL: string; @@ -19,7 +21,8 @@ abstract class APIService { this.axiosInstance.interceptors.response.use( (response) => response, (error) => { - if (error.response && error.response.status === 401) window.location.href = "/"; + const store = rootStore; + if (error.response && error.response.status === 401 && store.user.data) store.user.reset(); return Promise.reject(error); } ); diff --git a/space/store/root.store.ts b/space/store/root.store.ts index 8cb4991fc..fa0a25aaf 100644 --- a/space/store/root.store.ts +++ b/space/store/root.store.ts @@ -3,7 +3,7 @@ import { enableStaticRendering } from "mobx-react-lite"; // store imports import { IInstanceStore, InstanceStore } from "@/store/instance.store"; import { IProjectStore, ProjectStore } from "@/store/project"; -import { IUserStore, UserStore } from "@/store/user/index.store"; +import { IUserStore, UserStore } from "@/store/user"; import { IProfileStore, ProfileStore } from "@/store/user/profile.store"; import IssueStore, { IIssueStore } from "./issue"; diff --git a/space/store/user/index.store.ts b/space/store/user/index.ts similarity index 92% rename from space/store/user/index.store.ts rename to space/store/user/index.ts index d7f905974..e5bfc41c6 100644 --- a/space/store/user/index.store.ts +++ b/space/store/user/index.ts @@ -2,8 +2,6 @@ import set from "lodash/set"; import { action, computed, makeObservable, observable, runInAction } from "mobx"; // types import { IUser } from "@plane/types"; -// helpers -// import { API_BASE_URL } from "@/helpers/common.helper"; // services import { AuthService } from "@/services/authentication.service"; import { UserService } from "@/services/user.service"; @@ -30,6 +28,7 @@ export interface IUserStore { // actions fetchCurrentUser: () => Promise; updateCurrentUser: (data: Partial) => Promise; + reset: () => void; signOut: () => Promise; } @@ -65,6 +64,7 @@ export class UserStore implements IUserStore { // actions fetchCurrentUser: action, updateCurrentUser: action, + reset: action, signOut: action, }); } @@ -153,6 +153,20 @@ export class UserStore implements IUserStore { } }; + /** + * @description resets the user store + * @returns {void} + */ + reset = (): void => { + runInAction(() => { + this.isAuthenticated = false; + this.isLoading = false; + this.error = undefined; + this.data = undefined; + this.userProfile = new ProfileStore(this.store); + }); + }; + /** * @description signs out the current user * @returns {Promise} diff --git a/web/components/account/auth-forms/auth-root.tsx b/web/components/account/auth-forms/auth-root.tsx index c383f190e..1494f91f1 100644 --- a/web/components/account/auth-forms/auth-root.tsx +++ b/web/components/account/auth-forms/auth-root.tsx @@ -88,7 +88,10 @@ export const AuthRoot: FC = observer((props) => { if (authMode === EAuthModes.SIGN_IN) { if (response.is_password_autoset) setAuthStep(EAuthSteps.UNIQUE_CODE); else setAuthStep(EAuthSteps.PASSWORD); - } else setAuthStep(EAuthSteps.PASSWORD); + } else { + if (instance && instance?.config?.is_smtp_configured) setAuthStep(EAuthSteps.UNIQUE_CODE); + else setAuthStep(EAuthSteps.PASSWORD); + } }) .catch((error) => { const errorhandler = authErrorHandler(error?.error_code.toString(), data?.email || undefined); diff --git a/web/components/account/auth-forms/password.tsx b/web/components/account/auth-forms/password.tsx index de3d813e1..37c6d6506 100644 --- a/web/components/account/auth-forms/password.tsx +++ b/web/components/account/auth-forms/password.tsx @@ -40,15 +40,22 @@ const authService = new AuthService(); export const AuthPasswordForm: React.FC = observer((props: Props) => { const { email, handleStepChange, handleEmailClear, mode } = props; - // states - const [passwordFormData, setPasswordFormData] = useState({ ...defaultValues, email }); - const [showPassword, setShowPassword] = useState(false); - const [csrfToken, setCsrfToken] = useState(undefined); - const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false); - const [isSubmitting, setIsSubmitting] = useState(false); // hooks const { instance } = useInstance(); const { captureEvent } = useEventTracker(); + // states + const [csrfToken, setCsrfToken] = useState(undefined); + const [passwordFormData, setPasswordFormData] = useState({ ...defaultValues, email }); + const [showPassword, setShowPassword] = useState({ + password: false, + retypePassword: false, + }); + const [isSubmitting, setIsSubmitting] = useState(false); + const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false); + + const handleShowPassword = (key: keyof typeof showPassword) => + setShowPassword((prev) => ({ ...prev, [key]: !prev[key] })); + // derived values const isSmtpConfigured = instance?.config?.is_smtp_configured; @@ -116,9 +123,9 @@ export const AuthPasswordForm: React.FC = observer((props: Props) => { type="email" value={passwordFormData.email} onChange={(e) => handleFormChange("email", e.target.value)} - // hasError={Boolean(errors.email)} placeholder="name@company.com" className="h-[46px] w-full border border-onboarding-border-100 pr-12 placeholder:text-onboarding-text-400" + disabled /> {passwordFormData.email.length > 0 && ( = observer((props: Props) => { /> )}
+
handleFormChange("password", e.target.value)} @@ -144,15 +152,15 @@ export const AuthPasswordForm: React.FC = observer((props: Props) => { onBlur={() => setIsPasswordInputFocused(false)} autoFocus /> - {showPassword ? ( + {showPassword?.password ? ( setShowPassword(false)} + onClick={() => handleShowPassword("password")} /> ) : ( setShowPassword(true)} + onClick={() => handleShowPassword("password")} /> )}
@@ -165,22 +173,22 @@ export const AuthPasswordForm: React.FC = observer((props: Props) => {
handleFormChange("confirm_password", e.target.value)} placeholder="Confirm password" className="h-[46px] w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400" /> - {showPassword ? ( + {showPassword?.retypePassword ? ( setShowPassword(false)} + onClick={() => handleShowPassword("retypePassword")} /> ) : ( setShowPassword(true)} + onClick={() => handleShowPassword("retypePassword")} /> )}
diff --git a/web/components/account/auth-forms/unique-code.tsx b/web/components/account/auth-forms/unique-code.tsx index c8b08d6e6..79386f20e 100644 --- a/web/components/account/auth-forms/unique-code.tsx +++ b/web/components/account/auth-forms/unique-code.tsx @@ -111,10 +111,9 @@ export const AuthUniqueCodeForm: React.FC = (props) => { type="email" value={uniqueCodeFormData.email} onChange={(e) => handleFormChange("email", e.target.value)} - // FIXME: - // hasError={Boolean(errors.email)} placeholder="name@company.com" className="h-[46px] w-full border border-onboarding-border-100 pr-12 placeholder:text-onboarding-text-400" + disabled /> {uniqueCodeFormData.email.length > 0 && ( = (props) => { onClick={handleEmailClear} /> )} +
diff --git a/web/components/account/deactivate-account-modal.tsx b/web/components/account/deactivate-account-modal.tsx index a020bd43b..ee5100e36 100644 --- a/web/components/account/deactivate-account-modal.tsx +++ b/web/components/account/deactivate-account-modal.tsx @@ -1,7 +1,5 @@ import React, { useState } from "react"; import { useRouter } from "next/router"; -import { useTheme } from "next-themes"; -import { mutate } from "swr"; import { Trash2 } from "lucide-react"; import { Dialog, Transition } from "@headlessui/react"; // hooks @@ -15,17 +13,14 @@ type Props = { }; export const DeactivateAccountModal: React.FC = (props) => { + const router = useRouter(); const { isOpen, onClose } = props; + // hooks + const { deactivateAccount, signOut } = useUser(); // states const [isDeactivating, setIsDeactivating] = useState(false); - const { deactivateAccount } = useUser(); - - const router = useRouter(); - - const { setTheme } = useTheme(); - const handleClose = () => { setIsDeactivating(false); onClose(); @@ -41,8 +36,7 @@ export const DeactivateAccountModal: React.FC = (props) => { title: "Success!", message: "Account deactivated successfully.", }); - mutate("CURRENT_USER_DETAILS", null); - setTheme("system"); + signOut(); router.push("/"); handleClose(); }) diff --git a/web/lib/store-context.tsx b/web/lib/store-context.tsx index c8d49066d..0170cfe37 100644 --- a/web/lib/store-context.tsx +++ b/web/lib/store-context.tsx @@ -2,7 +2,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); diff --git a/web/services/api.service.ts b/web/services/api.service.ts index 81f9dd2b8..d5e8f8951 100644 --- a/web/services/api.service.ts +++ b/web/services/api.service.ts @@ -1,5 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import axios, { AxiosInstance } from "axios"; +// store +import { rootStore } from "@/lib/store-context"; export abstract class APIService { protected baseURL: string; @@ -19,7 +21,11 @@ export abstract class APIService { this.axiosInstance.interceptors.response.use( (response) => response, (error) => { - if (error.response && error.response.status === 401) window.location.href = "/accounts/sign-in"; + const store = rootStore; + if (error.response && error.response.status === 401 && store.user.data) { + store.user.reset(); + store.resetOnSignOut(); + } return Promise.reject(error); } ); diff --git a/web/store/user/index.ts b/web/store/user/index.ts index 7010d7097..f22507a12 100644 --- a/web/store/user/index.ts +++ b/web/store/user/index.ts @@ -36,6 +36,7 @@ export interface IUserStore { updateCurrentUser: (data: Partial) => Promise; handleSetPassword: (csrfToken: string, data: { password: string }) => Promise; deactivateAccount: () => Promise; + reset: () => void; signOut: () => Promise; } @@ -79,6 +80,7 @@ export class UserStore implements IUserStore { updateCurrentUser: action, handleSetPassword: action, deactivateAccount: action, + reset: action, signOut: action, }); } @@ -191,6 +193,22 @@ export class UserStore implements IUserStore { this.store.resetOnSignOut(); }; + /** + * @description resets the user store + * @returns {void} + */ + reset = (): void => { + runInAction(() => { + this.isAuthenticated = false; + this.isLoading = false; + this.error = undefined; + this.data = undefined; + this.userProfile = new ProfileStore(this.store); + this.userSettings = new UserSettingsStore(); + this.membership = new UserMembershipStore(this.store); + }); + }; + /** * @description signs out the current user * @returns {Promise} diff --git a/yarn.lock b/yarn.lock index e3230e816..c25eddcda 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2658,7 +2658,7 @@ resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-3.0.5.tgz#1e78a3ac2428e6d7e6c05c1665c242023a4601d8" integrity sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw== -"@types/lodash@^4.14.202", "@types/lodash@^4.17.0": +"@types/lodash@^4.14.202", "@types/lodash@^4.17.0", "@types/lodash@^4.17.1": version "4.17.1" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.1.tgz#0fabfcf2f2127ef73b119d98452bd317c4a17eb8" integrity sha512-X+2qazGS3jxLAIz5JDXDzglAF3KpijdhFxlf/V1+hEsOUc+HnWi81L/uv/EvGuV90WY+7mPGFCUDGfQC3Gj95Q== @@ -6935,10 +6935,10 @@ postcss@^8.4.21, postcss@^8.4.23, postcss@^8.4.29: picocolors "^1.0.0" source-map-js "^1.2.0" -posthog-js@^1.105.0: - version "1.131.2" - resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.131.2.tgz#c82b16a4074f773eaf41df47187fb88cc5aef28c" - integrity sha512-un5c5CbDhJ1LRBDgy4I1D5a1++P8/mNl4CS9C5A1z95qIF7iY8OuA6XPW7sIA6tKSdda4PGwfa2Gmfz1nvnywQ== +posthog-js@^1.131.3: + version "1.131.3" + resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.131.3.tgz#bd3e6123dc715f089825a92d3ec62480b7ec0a76" + integrity sha512-ds/TADDS+rT/WgUyeW4cJ+X+fX+O1KdkOyssNI/tP90PrFf0IJsck5B42YOLhfz87U2vgTyBaKHkdlMgWuOFog== dependencies: fflate "^0.4.8" preact "^10.19.3"