From f80d1b32b76fa9be13bb0eadcada61d3fc34edb5 Mon Sep 17 00:00:00 2001 From: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com> Date: Tue, 26 Nov 2024 13:58:13 +0530 Subject: [PATCH] chore: Auth module revamp (#4335) Co-authored-by: pandeymangg Co-authored-by: Matthias Nannt --- .../app/self-hosting/configuration/page.mdx | 10 +- .../[environmentId]/connect/invite/page.tsx | 2 +- .../environments/[environmentId]/layout.tsx | 2 +- .../[environmentId]/xm-templates/page.tsx | 2 +- .../[organizationId]/landing/layout.tsx | 2 +- .../[organizationId]/landing/page.tsx | 2 +- .../organizations/[organizationId]/layout.tsx | 2 +- .../[organizationId]/products/layout.tsx | 2 +- .../products/new/channel/page.tsx | 2 +- .../products/new/mode/page.tsx | 2 +- .../products/new/settings/page.tsx | 2 +- .../(onboarding)/organizations/actions.ts | 2 +- .../environments/[environmentId]/layout.tsx | 2 +- .../surveys/[surveyId]/edit/page.tsx | 2 +- .../surveys/templates/page.tsx | 2 +- .../(people)/attributes/page.tsx | 2 +- .../[environmentId]/(people)/layout.tsx | 2 +- .../[personId]/components/ResponseSection.tsx | 2 +- .../[personId]/components/ResponsesFeed.tsx | 2 +- .../(people)/people/[personId]/page.tsx | 2 +- .../[environmentId]/(people)/people/page.tsx | 2 +- .../(people)/segments/page.tsx | 2 +- .../[environmentId]/actions/page.tsx | 2 +- .../integrations/airtable/page.tsx | 2 +- .../integrations/google-sheets/actions.ts | 2 +- .../integrations/google-sheets/page.tsx | 2 +- .../integrations/notion/page.tsx | 2 +- .../[environmentId]/integrations/page.tsx | 2 +- .../integrations/slack/page.tsx | 2 +- .../integrations/webhooks/page.tsx | 2 +- .../environments/[environmentId]/layout.tsx | 2 +- .../environments/[environmentId]/page.tsx | 2 +- .../[environmentId]/product/api-keys/page.tsx | 2 +- .../general/components/DeleteProduct.tsx | 2 +- .../[environmentId]/product/general/page.tsx | 2 +- .../product/languages/page.tsx | 2 +- .../[environmentId]/product/layout.tsx | 2 +- .../[environmentId]/product/look/page.tsx | 2 +- .../[environmentId]/product/tags/page.tsx | 2 +- .../settings/(account)/layout.tsx | 2 +- .../settings/(account)/notifications/page.tsx | 2 +- .../settings/(account)/profile/actions.ts | 33 -- .../profile/components/AccountSecurity.tsx | 22 +- .../components/DisableTwoFactorModal.tsx | 171 -------- .../components/EditProfileAvatarForm.tsx | 6 +- .../components/EnableTwoFactorModal.tsx | 348 ----------------- .../settings/(account)/profile/page.tsx | 10 +- .../(organization)/enterprise/page.tsx | 2 +- .../(organization)/general/actions.ts | 1 + .../settings/(organization)/general/page.tsx | 2 +- .../settings/(organization)/layout.tsx | 2 +- .../surveys/[surveyId]/(analysis)/layout.tsx | 2 +- .../[surveyId]/(analysis)/responses/page.tsx | 2 +- .../[surveyId]/(analysis)/summary/page.tsx | 2 +- .../[environmentId]/surveys/page.tsx | 2 +- apps/web/app/(app)/layout.tsx | 2 +- .../components/PasswordResetForm/index.tsx | 77 ---- .../auth/forgot-password/email-sent/page.tsx | 23 +- .../app/(auth)/auth/forgot-password/page.tsx | 12 +- .../components/ResetPasswordForm/index.tsx | 106 ----- .../auth/forgot-password/reset/page.tsx | 12 +- .../forgot-password/reset/success/page.tsx | 23 +- apps/web/app/(auth)/auth/layout.tsx | 30 +- apps/web/app/(auth)/auth/login/page.tsx | 49 +-- .../page.tsx | 22 +- .../auth/signup/components/SignupForm.tsx | 115 ------ apps/web/app/(auth)/auth/signup/page.tsx | 57 +-- .../auth/verification-requested/page.tsx | 47 +-- apps/web/app/(auth)/auth/verify/page.tsx | 19 +- apps/web/app/(auth)/invite/page.tsx | 2 +- .../organizations/[organizationId]/route.ts | 2 +- .../(redirects)/products/[productId]/route.ts | 2 +- .../api/(internal)/csv-conversion/route.ts | 2 +- .../api/(internal)/excel-conversion/route.ts | 2 +- apps/web/app/api/auth/[...nextauth]/route.ts | 2 +- apps/web/app/api/google-sheet/route.ts | 2 +- .../integrations/airtable/callback/route.ts | 2 +- .../app/api/v1/integrations/airtable/route.ts | 2 +- .../v1/integrations/airtable/tables/route.ts | 2 +- .../app/api/v1/integrations/notion/route.ts | 2 +- .../app/api/v1/integrations/slack/route.ts | 2 +- .../api/v1/management/storage/local/route.ts | 2 +- .../app/api/v1/management/storage/route.ts | 2 +- .../app/api/v1/users/forgot-password/route.ts | 28 -- apps/web/app/api/v1/users/me/route.ts | 146 ------- .../app/api/v1/users/reset-password/route.ts | 38 -- apps/web/app/api/v1/users/route.ts | 163 -------- .../api/v1/users/verification-email/route.ts | 28 -- apps/web/app/lib/api/apiHelper.ts | 2 +- apps/web/app/middleware/bucket.ts | 21 +- apps/web/app/middleware/endpointValidator.ts | 15 +- apps/web/app/page.tsx | 2 +- .../web/app/setup/(fresh-instance)/layout.tsx | 2 +- .../setup/(fresh-instance)/signup/page.tsx | 22 +- .../[organizationId]/invite/actions.ts | 1 + .../[organizationId]/invite/page.tsx | 2 +- .../app/setup/organization/create/page.tsx | 2 +- .../[accessType]/[fileName]/route.ts | 2 +- apps/web/i18n/request.ts | 2 +- apps/web/lib/utils/action-client.ts | 3 +- apps/web/middleware.ts | 43 +- .../{components/SignupOptions => }/actions.ts | 3 +- .../components/IsPasswordValid.tsx | 75 ---- .../auth/components/back-to-login-button.tsx} | 0 .../auth/components/form-wrapper.tsx} | 6 +- .../auth/components/testimonial.tsx} | 0 .../modules/auth/forgot-password/actions.ts | 20 + .../components/forgot-password-form.tsx | 85 ++++ .../auth/forgot-password/email-sent/page.tsx | 20 + .../web/modules/auth/forgot-password/page.tsx | 10 + .../auth/forgot-password/reset/actions.ts | 30 ++ .../reset/components/reset-password-form.tsx | 106 +++++ .../auth/forgot-password/reset/page.tsx | 10 + .../forgot-password/reset/success/page.tsx | 20 + apps/web/modules/auth/layout.tsx | 32 ++ apps/web/modules/auth/lib/authOptions.ts | 209 ++++++++++ .../web/modules/auth}/lib/totp.ts | 0 apps/web/modules/auth/lib/user.ts | 145 +++++++ .../web/modules/auth/lib}/utils.ts | 0 .../components/login-form.tsx} | 163 +++----- apps/web/modules/auth/login/page.tsx | 47 +++ .../page.tsx | 19 + apps/web/modules/auth/signup/actions.ts | 110 ++++++ .../signup/components/password-checks.tsx | 63 +++ .../components/signup-form.tsx} | 189 +++++---- .../signup/components/terms-privacy-links.tsx | 32 ++ apps/web/modules/auth/signup/page.tsx | 60 +++ .../auth/verification-requested/actions.ts | 25 ++ .../request-verification-email.tsx} | 18 +- .../auth/verification-requested/page.tsx | 49 +++ .../auth/verify/components/sign-in.tsx} | 0 apps/web/modules/auth/verify/page.tsx | 15 + apps/web/modules/ee/billing/page.tsx | 2 +- .../modules/ee/insights/experience/page.tsx | 2 +- .../web/modules/ee/license-check/lib/utils.ts | 24 +- .../license-check/types/enterprise-license.ts | 2 + .../sso/components/azure-button.tsx} | 16 +- .../sso/components/github-button.tsx} | 13 +- .../sso/components/google-button.tsx} | 13 +- .../sso/components/open-id-button.tsx} | 19 +- .../modules/ee/sso/components/sso-options.tsx | 38 ++ apps/web/modules/ee/sso/lib/providers.ts | 59 +++ apps/web/modules/ee/sso/lib/sso-handlers.ts | 119 ++++++ .../modules/ee/teams/product-teams/page.tsx | 2 +- .../modules/ee/teams/team-details/page.tsx | 2 +- apps/web/modules/ee/teams/team-list/page.tsx | 2 +- .../web/modules/ee/two-factor-auth/actions.ts | 52 +++ .../components/confirm-password-form.tsx | 111 ++++++ .../components/disable-two-factor-modal.tsx | 205 ++++++++++ .../components/display-backup-codes.tsx | 70 ++++ .../components/enable-two-factor-modal.tsx | 60 +++ .../two-factor-auth/components/enter-code.tsx | 101 +++++ .../components/scan-qr-code.tsx | 59 +++ .../components/two-factor-backup.tsx} | 0 .../components/two-factor.tsx} | 0 .../ee/two-factor-auth/lib/two-factor-auth.ts | 57 +-- apps/web/modules/email/index.tsx | 38 +- apps/web/playwright/signup.spec.ts | 2 +- packages/lib/authOptions.ts | 368 ------------------ packages/lib/constants.ts | 1 + packages/lib/customerio.ts | 15 +- packages/lib/invite/service.ts | 17 +- packages/lib/membership/hooks/actions.ts | 15 +- .../membership/hooks/useMembershipRole.tsx | 6 +- packages/lib/messages/de-DE.json | 3 + packages/lib/messages/en-US.json | 3 + packages/lib/messages/pt-BR.json | 3 + packages/lib/user/service.ts | 40 +- packages/lib/utils/users.ts | 106 ----- packages/types/user.ts | 21 +- 170 files changed, 2484 insertions(+), 2582 deletions(-) delete mode 100644 apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/DisableTwoFactorModal.tsx delete mode 100644 apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/EnableTwoFactorModal.tsx delete mode 100644 apps/web/app/(auth)/auth/forgot-password/components/PasswordResetForm/index.tsx delete mode 100644 apps/web/app/(auth)/auth/forgot-password/reset/components/ResetPasswordForm/index.tsx delete mode 100644 apps/web/app/(auth)/auth/signup/components/SignupForm.tsx delete mode 100644 apps/web/app/api/v1/users/forgot-password/route.ts delete mode 100644 apps/web/app/api/v1/users/me/route.ts delete mode 100644 apps/web/app/api/v1/users/reset-password/route.ts delete mode 100644 apps/web/app/api/v1/users/route.ts delete mode 100644 apps/web/app/api/v1/users/verification-email/route.ts rename apps/web/modules/auth/{components/SignupOptions => }/actions.ts (83%) delete mode 100644 apps/web/modules/auth/components/SignupOptions/components/IsPasswordValid.tsx rename apps/web/{app/(auth)/auth/components/BackToLoginButton.tsx => modules/auth/components/back-to-login-button.tsx} (100%) rename apps/web/{app/(auth)/auth/components/FormWrapper.tsx => modules/auth/components/form-wrapper.tsx} (77%) rename apps/web/{app/(auth)/auth/components/Testimonial.tsx => modules/auth/components/testimonial.tsx} (100%) create mode 100644 apps/web/modules/auth/forgot-password/actions.ts create mode 100644 apps/web/modules/auth/forgot-password/components/forgot-password-form.tsx create mode 100644 apps/web/modules/auth/forgot-password/email-sent/page.tsx create mode 100644 apps/web/modules/auth/forgot-password/page.tsx create mode 100644 apps/web/modules/auth/forgot-password/reset/actions.ts create mode 100644 apps/web/modules/auth/forgot-password/reset/components/reset-password-form.tsx create mode 100644 apps/web/modules/auth/forgot-password/reset/page.tsx create mode 100644 apps/web/modules/auth/forgot-password/reset/success/page.tsx create mode 100644 apps/web/modules/auth/layout.tsx create mode 100644 apps/web/modules/auth/lib/authOptions.ts rename {packages => apps/web/modules/auth}/lib/totp.ts (100%) create mode 100644 apps/web/modules/auth/lib/user.ts rename {packages/lib/auth => apps/web/modules/auth/lib}/utils.ts (100%) rename apps/web/modules/auth/{components/SigninForm/index.tsx => login/components/login-form.tsx} (67%) create mode 100644 apps/web/modules/auth/login/page.tsx create mode 100644 apps/web/modules/auth/signup-without-verification-success/page.tsx create mode 100644 apps/web/modules/auth/signup/actions.ts create mode 100644 apps/web/modules/auth/signup/components/password-checks.tsx rename apps/web/modules/auth/{components/SignupOptions/index.tsx => signup/components/signup-form.tsx} (58%) create mode 100644 apps/web/modules/auth/signup/components/terms-privacy-links.tsx create mode 100644 apps/web/modules/auth/signup/page.tsx create mode 100644 apps/web/modules/auth/verification-requested/actions.ts rename apps/web/{app/(auth)/auth/verification-requested/components/RequestVerificationEmail.tsx => modules/auth/verification-requested/components/request-verification-email.tsx} (64%) create mode 100644 apps/web/modules/auth/verification-requested/page.tsx rename apps/web/{app/(auth)/auth/verify/components/SignIn.tsx => modules/auth/verify/components/sign-in.tsx} (100%) create mode 100644 apps/web/modules/auth/verify/page.tsx rename apps/web/modules/{auth/components/SignupOptions/components/AzureButton.tsx => ee/sso/components/azure-button.tsx} (86%) rename apps/web/modules/{auth/components/SignupOptions/components/GithubButton.tsx => ee/sso/components/github-button.tsx} (87%) rename apps/web/modules/{auth/components/SignupOptions/components/GoogleButton.tsx => ee/sso/components/google-button.tsx} (87%) rename apps/web/modules/{auth/components/SignupOptions/components/OpenIdButton.tsx => ee/sso/components/open-id-button.tsx} (84%) create mode 100644 apps/web/modules/ee/sso/components/sso-options.tsx create mode 100644 apps/web/modules/ee/sso/lib/providers.ts create mode 100644 apps/web/modules/ee/sso/lib/sso-handlers.ts create mode 100644 apps/web/modules/ee/two-factor-auth/actions.ts create mode 100644 apps/web/modules/ee/two-factor-auth/components/confirm-password-form.tsx create mode 100644 apps/web/modules/ee/two-factor-auth/components/disable-two-factor-modal.tsx create mode 100644 apps/web/modules/ee/two-factor-auth/components/display-backup-codes.tsx create mode 100644 apps/web/modules/ee/two-factor-auth/components/enable-two-factor-modal.tsx create mode 100644 apps/web/modules/ee/two-factor-auth/components/enter-code.tsx create mode 100644 apps/web/modules/ee/two-factor-auth/components/scan-qr-code.tsx rename apps/web/modules/{auth/components/SigninForm/components/TwoFactorBackup.tsx => ee/two-factor-auth/components/two-factor-backup.tsx} (100%) rename apps/web/modules/{auth/components/SigninForm/components/TwoFactor.tsx => ee/two-factor-auth/components/two-factor.tsx} (100%) rename packages/lib/auth/service.ts => apps/web/modules/ee/two-factor-auth/lib/two-factor-auth.ts (69%) delete mode 100644 packages/lib/authOptions.ts delete mode 100644 packages/lib/utils/users.ts diff --git a/apps/docs/app/self-hosting/configuration/page.mdx b/apps/docs/app/self-hosting/configuration/page.mdx index 1954329eb1..85969dc801 100644 --- a/apps/docs/app/self-hosting/configuration/page.mdx +++ b/apps/docs/app/self-hosting/configuration/page.mdx @@ -71,13 +71,15 @@ These variables are present inside your machine’s docker-compose file. Restart | OIDC_SIGNING_ALGORITHM | Signing Algorithm for Custom OpenID Connect Provider | optional | RS256 | | OPENTELEMETRY_LISTENER_URL | URL for OpenTelemetry listener inside Formbricks. | optional | | | CUSTOM_CACHE_DISABLED | Disables custom cache handler if set to 1 (required for deployment on Vercel) | optional | | -| `` | | | | -| | | | | Note: If you want to configure something that is not possible via above, please open an issue on our GitHub repo here or reach out to us on Discord and we’ll try our best to work out a solution with you. ## OAuth Configuration + + Single Sign-On (SSO) functionality, including OAuth integrations with Google, Microsoft Entra ID, Github and OpenID Connect, requires a valid Formbricks Enterprise License. + + ### Google OAuth Integrating Google OAuth with your Formbricks instance allows users to log in using their Google credentials, ensuring a secure and streamlined user experience. This guide will walk you through the process of setting up Google OAuth for your Formbricks instance. @@ -113,10 +115,12 @@ Integrating Google OAuth with your Formbricks instance allows users to log in us ``` {{ title: "Redirect & Origin URLs" }} Authorized JavaScript origins: {WEBAPP_URL} - Authorized redirect URIs: {WEBAPP_URL}/api/auth/callback/google ``` + Authorized redirect URIs: {WEBAPP_URL}/api/auth/callback/google + ``` + 5. **Update Environment Variables in Docker**: - To integrate the Google OAuth, you have two options: either update the environment variables in the docker-compose file or directly add them to the running container. - In your Docker setup directory, open the `.env` file, and add or update the following lines with the `Client ID` and `Client Secret` obtained from Google Cloud Platform: diff --git a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/invite/page.tsx b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/invite/page.tsx index dab38731c3..79a0b7c7b3 100644 --- a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/invite/page.tsx +++ b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/invite/page.tsx @@ -1,11 +1,11 @@ import { InviteOrganizationMember } from "@/app/(app)/(onboarding)/environments/[environmentId]/connect/components/InviteOrganizationMember"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { Button } from "@/modules/ui/components/button"; import { Header } from "@/modules/ui/components/header"; import { XIcon } from "lucide-react"; import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; import { notFound, redirect } from "next/navigation"; -import { authOptions } from "@formbricks/lib/authOptions"; import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service"; diff --git a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/layout.tsx b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/layout.tsx index 7e0d592871..9e9b19810b 100644 --- a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/layout.tsx +++ b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/layout.tsx @@ -1,6 +1,6 @@ +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getServerSession } from "next-auth"; import { redirect } from "next/navigation"; -import { authOptions } from "@formbricks/lib/authOptions"; import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; import { AuthorizationError } from "@formbricks/types/errors"; diff --git a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/page.tsx b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/page.tsx index a20d3e758d..061e289ae7 100644 --- a/apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/page.tsx +++ b/apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/page.tsx @@ -1,11 +1,11 @@ import { XMTemplateList } from "@/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/components/XMTemplateList"; import { getOrganizationIdFromEnvironmentId } from "@/lib/utils/helper"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { Button } from "@/modules/ui/components/button"; import { Header } from "@/modules/ui/components/header"; import { XIcon } from "lucide-react"; import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; -import { authOptions } from "@formbricks/lib/authOptions"; import { getEnvironment } from "@formbricks/lib/environment/service"; import { getProductByEnvironmentId, getUserProducts } from "@formbricks/lib/product/service"; import { getUser } from "@formbricks/lib/user/service"; diff --git a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/landing/layout.tsx b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/landing/layout.tsx index 5d80204ba7..6ed6f4ed16 100644 --- a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/landing/layout.tsx +++ b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/landing/layout.tsx @@ -1,6 +1,6 @@ +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getServerSession } from "next-auth"; import { notFound, redirect } from "next/navigation"; -import { authOptions } from "@formbricks/lib/authOptions"; import { getEnvironments } from "@formbricks/lib/environment/service"; import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; import { getUserProducts } from "@formbricks/lib/product/service"; diff --git a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/landing/page.tsx b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/landing/page.tsx index 68e2ded919..83b0ed7a11 100644 --- a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/landing/page.tsx +++ b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/landing/page.tsx @@ -1,10 +1,10 @@ import { LandingSidebar } from "@/app/(app)/(onboarding)/organizations/[organizationId]/landing/components/landing-sidebar"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getEnterpriseLicense } from "@/modules/ee/license-check/lib/utils"; import { Header } from "@/modules/ui/components/header"; import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; import { notFound, redirect } from "next/navigation"; -import { authOptions } from "@formbricks/lib/authOptions"; import { getOrganization, getOrganizationsByUserId } from "@formbricks/lib/organization/service"; import { getUser } from "@formbricks/lib/user/service"; diff --git a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/layout.tsx b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/layout.tsx index 2640188717..7b332fa200 100644 --- a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/layout.tsx +++ b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/layout.tsx @@ -1,9 +1,9 @@ import { PosthogIdentify } from "@/app/(app)/environments/[environmentId]/components/PosthogIdentify"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { ToasterClient } from "@/modules/ui/components/toaster-client"; import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; import { redirect } from "next/navigation"; -import { authOptions } from "@formbricks/lib/authOptions"; import { canUserAccessOrganization } from "@formbricks/lib/organization/auth"; import { getOrganization } from "@formbricks/lib/organization/service"; import { getUser } from "@formbricks/lib/user/service"; diff --git a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/products/layout.tsx b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/products/layout.tsx index 1207d01f53..56c92071b1 100644 --- a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/products/layout.tsx +++ b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/products/layout.tsx @@ -1,6 +1,6 @@ +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getServerSession } from "next-auth"; import { notFound, redirect } from "next/navigation"; -import { authOptions } from "@formbricks/lib/authOptions"; import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; import { getAccessFlags } from "@formbricks/lib/membership/utils"; diff --git a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/products/new/channel/page.tsx b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/products/new/channel/page.tsx index af98c65ee9..5db046354f 100644 --- a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/products/new/channel/page.tsx +++ b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/products/new/channel/page.tsx @@ -1,11 +1,11 @@ import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { Button } from "@/modules/ui/components/button"; import { Header } from "@/modules/ui/components/header"; import { GlobeIcon, GlobeLockIcon, LinkIcon, XIcon } from "lucide-react"; import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; import { redirect } from "next/navigation"; -import { authOptions } from "@formbricks/lib/authOptions"; import { getUserProducts } from "@formbricks/lib/product/service"; interface ChannelPageProps { diff --git a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/products/new/mode/page.tsx b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/products/new/mode/page.tsx index 4acc38e6db..df60770415 100644 --- a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/products/new/mode/page.tsx +++ b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/products/new/mode/page.tsx @@ -1,11 +1,11 @@ import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { Button } from "@/modules/ui/components/button"; import { Header } from "@/modules/ui/components/header"; import { HeartIcon, ListTodoIcon, XIcon } from "lucide-react"; import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; import { redirect } from "next/navigation"; -import { authOptions } from "@formbricks/lib/authOptions"; import { getUserProducts } from "@formbricks/lib/product/service"; interface ModePageProps { diff --git a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/products/new/settings/page.tsx b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/products/new/settings/page.tsx index 8da43e2490..d33fb08cff 100644 --- a/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/products/new/settings/page.tsx +++ b/apps/web/app/(app)/(onboarding)/organizations/[organizationId]/products/new/settings/page.tsx @@ -1,6 +1,7 @@ import { getTeamsByOrganizationId } from "@/app/(app)/(onboarding)/lib/onboarding"; import { getCustomHeadline } from "@/app/(app)/(onboarding)/lib/utils"; import { ProductSettings } from "@/app/(app)/(onboarding)/organizations/[organizationId]/products/new/settings/components/ProductSettings"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getRoleManagementPermission } from "@/modules/ee/license-check/lib/utils"; import { Button } from "@/modules/ui/components/button"; import { Header } from "@/modules/ui/components/header"; @@ -8,7 +9,6 @@ import { XIcon } from "lucide-react"; import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; import { redirect } from "next/navigation"; -import { authOptions } from "@formbricks/lib/authOptions"; import { DEFAULT_BRAND_COLOR, DEFAULT_LOCALE } from "@formbricks/lib/constants"; import { getOrganization } from "@formbricks/lib/organization/service"; import { getUserProducts } from "@formbricks/lib/product/service"; diff --git a/apps/web/app/(app)/(onboarding)/organizations/actions.ts b/apps/web/app/(app)/(onboarding)/organizations/actions.ts index 9d3c80439f..d28dbc2fdc 100644 --- a/apps/web/app/(app)/(onboarding)/organizations/actions.ts +++ b/apps/web/app/(app)/(onboarding)/organizations/actions.ts @@ -23,7 +23,6 @@ export const inviteOrganizationMemberAction = authenticatedActionClient if (INVITE_DISABLED) { throw new AuthenticationError("Invite disabled"); } - await checkAuthorizationUpdated({ userId: ctx.user.id, organizationId: parsedInput.organizationId, @@ -42,6 +41,7 @@ export const inviteOrganizationMemberAction = authenticatedActionClient name: "", role: parsedInput.role, }, + currentUserId: ctx.user.id, }); if (invite) { diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/layout.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/layout.tsx index b19da2f5fe..1d6447a398 100644 --- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/layout.tsx +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/layout.tsx @@ -1,12 +1,12 @@ import { FormbricksClient } from "@/app/(app)/components/FormbricksClient"; import { PosthogIdentify } from "@/app/(app)/environments/[environmentId]/components/PosthogIdentify"; import { ResponseFilterProvider } from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { DevEnvironmentBanner } from "@/modules/ui/components/dev-environment-banner"; import { ToasterClient } from "@/modules/ui/components/toaster-client"; import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; import { redirect } from "next/navigation"; -import { authOptions } from "@formbricks/lib/authOptions"; import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; import { getEnvironment } from "@formbricks/lib/environment/service"; import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service"; diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/page.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/page.tsx index d57e2f0272..3ef8049e3c 100644 --- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/page.tsx +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/page.tsx @@ -1,4 +1,5 @@ import { getUserEmail } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/user"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getAdvancedTargetingPermission, getMultiLanguagePermission, @@ -11,7 +12,6 @@ import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; import { getActionClasses } from "@formbricks/lib/actionClass/service"; import { getAttributeClasses } from "@formbricks/lib/attributeClass/service"; -import { authOptions } from "@formbricks/lib/authOptions"; import { DEFAULT_LOCALE, IS_FORMBRICKS_CLOUD, diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/page.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/page.tsx index 2d03c61fa6..3d13753940 100644 --- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/page.tsx +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/page.tsx @@ -1,9 +1,9 @@ +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles"; import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams"; import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; import { redirect } from "next/navigation"; -import { authOptions } from "@formbricks/lib/authOptions"; import { getEnvironment } from "@formbricks/lib/environment/service"; import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; import { getAccessFlags } from "@formbricks/lib/membership/utils"; diff --git a/apps/web/app/(app)/environments/[environmentId]/(people)/attributes/page.tsx b/apps/web/app/(app)/environments/[environmentId]/(people)/attributes/page.tsx index dc9283d35e..da07257660 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(people)/attributes/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(people)/attributes/page.tsx @@ -1,4 +1,5 @@ import { PersonSecondaryNavigation } from "@/app/(app)/environments/[environmentId]/(people)/people/components/PersonSecondaryNavigation"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles"; import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams"; import { Button } from "@/modules/ui/components/button"; @@ -9,7 +10,6 @@ import { Metadata } from "next"; import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; import { getAttributeClasses } from "@formbricks/lib/attributeClass/service"; -import { authOptions } from "@formbricks/lib/authOptions"; import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; import { getAccessFlags } from "@formbricks/lib/membership/utils"; import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service"; diff --git a/apps/web/app/(app)/environments/[environmentId]/(people)/layout.tsx b/apps/web/app/(app)/environments/[environmentId]/(people)/layout.tsx index be43881824..82e7ca27ce 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(people)/layout.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(people)/layout.tsx @@ -1,7 +1,7 @@ +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; import { redirect } from "next/navigation"; -import { authOptions } from "@formbricks/lib/authOptions"; import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; import { getAccessFlags } from "@formbricks/lib/membership/utils"; diff --git a/apps/web/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/ResponseSection.tsx b/apps/web/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/ResponseSection.tsx index 2eb7c0823c..70941ea598 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/ResponseSection.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/ResponseSection.tsx @@ -1,8 +1,8 @@ import { ResponseTimeline } from "@/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/ResponseTimeline"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles"; import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; -import { authOptions } from "@formbricks/lib/authOptions"; import { getProductByEnvironmentId } from "@formbricks/lib/product/service"; import { getResponsesByPersonId } from "@formbricks/lib/response/service"; import { getSurveys } from "@formbricks/lib/survey/service"; diff --git a/apps/web/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/ResponsesFeed.tsx b/apps/web/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/ResponsesFeed.tsx index b32991a66b..f43a031993 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/ResponsesFeed.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/ResponsesFeed.tsx @@ -104,7 +104,7 @@ const ResponseSurveyCard = ({ return survey.id === response.surveyId; }); - const { membershipRole } = useMembershipRole(survey?.environmentId || ""); + const { membershipRole } = useMembershipRole(survey?.environmentId || "", user.id); const { isMember } = getAccessFlags(membershipRole); const { hasReadAccess } = getTeamPermissionFlags(productPermission); diff --git a/apps/web/app/(app)/environments/[environmentId]/(people)/people/[personId]/page.tsx b/apps/web/app/(app)/environments/[environmentId]/(people)/people/[personId]/page.tsx index 7a2ff8911f..a91ed1ffe4 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(people)/people/[personId]/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(people)/people/[personId]/page.tsx @@ -1,6 +1,7 @@ import { AttributesSection } from "@/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/AttributesSection"; import { DeletePersonButton } from "@/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/DeletePersonButton"; import { ResponseSection } from "@/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/ResponseSection"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles"; import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams"; import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper"; @@ -9,7 +10,6 @@ import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; import { getAttributes } from "@formbricks/lib/attribute/service"; import { getAttributeClasses } from "@formbricks/lib/attributeClass/service"; -import { authOptions } from "@formbricks/lib/authOptions"; import { getEnvironment } from "@formbricks/lib/environment/service"; import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; import { getAccessFlags } from "@formbricks/lib/membership/utils"; diff --git a/apps/web/app/(app)/environments/[environmentId]/(people)/people/page.tsx b/apps/web/app/(app)/environments/[environmentId]/(people)/people/page.tsx index 73c468f26e..8f4434832c 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(people)/people/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(people)/people/page.tsx @@ -1,5 +1,6 @@ import { PersonDataView } from "@/app/(app)/environments/[environmentId]/(people)/people/components/PersonDataView"; import { PersonSecondaryNavigation } from "@/app/(app)/environments/[environmentId]/(people)/people/components/PersonSecondaryNavigation"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles"; import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams"; import { Button } from "@/modules/ui/components/button"; @@ -8,7 +9,6 @@ import { PageHeader } from "@/modules/ui/components/page-header"; import { CircleHelpIcon } from "lucide-react"; import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; -import { authOptions } from "@formbricks/lib/authOptions"; import { ITEMS_PER_PAGE } from "@formbricks/lib/constants"; import { getEnvironment } from "@formbricks/lib/environment/service"; import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; diff --git a/apps/web/app/(app)/environments/[environmentId]/(people)/segments/page.tsx b/apps/web/app/(app)/environments/[environmentId]/(people)/segments/page.tsx index 3f23976b31..7a91a8faa6 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(people)/segments/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(people)/segments/page.tsx @@ -1,6 +1,7 @@ import { PersonSecondaryNavigation } from "@/app/(app)/environments/[environmentId]/(people)/people/components/PersonSecondaryNavigation"; import { BasicCreateSegmentModal } from "@/app/(app)/environments/[environmentId]/(people)/segments/components/BasicCreateSegmentModal"; import { SegmentTable } from "@/app/(app)/environments/[environmentId]/(people)/segments/components/SegmentTable"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { CreateSegmentModal } from "@/modules/ee/advanced-targeting/components/create-segment-modal"; import { getAdvancedTargetingPermission } from "@/modules/ee/license-check/lib/utils"; import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles"; @@ -10,7 +11,6 @@ import { PageHeader } from "@/modules/ui/components/page-header"; import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; import { getAttributeClasses } from "@formbricks/lib/attributeClass/service"; -import { authOptions } from "@formbricks/lib/authOptions"; import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; import { getEnvironment } from "@formbricks/lib/environment/service"; import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; diff --git a/apps/web/app/(app)/environments/[environmentId]/actions/page.tsx b/apps/web/app/(app)/environments/[environmentId]/actions/page.tsx index 48e387ad61..24ec7f7c96 100644 --- a/apps/web/app/(app)/environments/[environmentId]/actions/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/actions/page.tsx @@ -2,6 +2,7 @@ import { ActionClassesTable } from "@/app/(app)/environments/[environmentId]/act import { ActionClassDataRow } from "@/app/(app)/environments/[environmentId]/actions/components/ActionRowData"; import { ActionTableHeading } from "@/app/(app)/environments/[environmentId]/actions/components/ActionTableHeading"; import { AddActionModal } from "@/app/(app)/environments/[environmentId]/actions/components/AddActionModal"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles"; import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams"; import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper"; @@ -11,7 +12,6 @@ import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; import { redirect } from "next/navigation"; import { getActionClasses } from "@formbricks/lib/actionClass/service"; -import { authOptions } from "@formbricks/lib/authOptions"; import { getEnvironments } from "@formbricks/lib/environment/service"; import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; import { getAccessFlags } from "@formbricks/lib/membership/utils"; diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/page.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/page.tsx index e1905f6744..2139ac59c3 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/airtable/page.tsx @@ -1,4 +1,5 @@ import { AirtableWrapper } from "@/app/(app)/environments/[environmentId]/integrations/airtable/components/AirtableWrapper"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles"; import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams"; import { GoBackButton } from "@/modules/ui/components/go-back-button"; @@ -9,7 +10,6 @@ import { getTranslations } from "next-intl/server"; import { redirect } from "next/navigation"; import { getAirtableTables } from "@formbricks/lib/airtable/service"; import { getAttributeClasses } from "@formbricks/lib/attributeClass/service"; -import { authOptions } from "@formbricks/lib/authOptions"; import { AIRTABLE_CLIENT_ID, WEBAPP_URL } from "@formbricks/lib/constants"; import { getEnvironment } from "@formbricks/lib/environment/service"; import { getIntegrations } from "@formbricks/lib/integration/service"; diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/actions.ts b/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/actions.ts index ca90fb831c..a95358353e 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/actions.ts @@ -1,7 +1,7 @@ "use server"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getServerSession } from "next-auth"; -import { authOptions } from "@formbricks/lib/authOptions"; import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; import { getSpreadsheetNameById } from "@formbricks/lib/googleSheet/service"; import { AuthorizationError } from "@formbricks/types/errors"; diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/page.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/page.tsx index 6d6c3c36ea..5686620e05 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/google-sheets/page.tsx @@ -1,4 +1,5 @@ import { GoogleSheetWrapper } from "@/app/(app)/environments/[environmentId]/integrations/google-sheets/components/GoogleSheetWrapper"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles"; import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams"; import { GoBackButton } from "@/modules/ui/components/go-back-button"; @@ -8,7 +9,6 @@ import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; import { redirect } from "next/navigation"; import { getAttributeClasses } from "@formbricks/lib/attributeClass/service"; -import { authOptions } from "@formbricks/lib/authOptions"; import { GOOGLE_SHEETS_CLIENT_ID, GOOGLE_SHEETS_CLIENT_SECRET, diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/notion/page.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/notion/page.tsx index f96a9b9eb6..6080183929 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/notion/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/notion/page.tsx @@ -1,4 +1,5 @@ import { NotionWrapper } from "@/app/(app)/environments/[environmentId]/integrations/notion/components/NotionWrapper"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles"; import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams"; import { GoBackButton } from "@/modules/ui/components/go-back-button"; @@ -8,7 +9,6 @@ import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; import { redirect } from "next/navigation"; import { getAttributeClasses } from "@formbricks/lib/attributeClass/service"; -import { authOptions } from "@formbricks/lib/authOptions"; import { NOTION_AUTH_URL, NOTION_OAUTH_CLIENT_ID, diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/page.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/page.tsx index 98e265882f..47263728ea 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/page.tsx @@ -7,6 +7,7 @@ import notionLogo from "@/images/notion.png"; import SlackLogo from "@/images/slacklogo.png"; import WebhookLogo from "@/images/webhook.png"; import ZapierLogo from "@/images/zapier-small.png"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles"; import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams"; import { Card } from "@/modules/ui/components/integration-card"; @@ -16,7 +17,6 @@ import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; import Image from "next/image"; import { redirect } from "next/navigation"; -import { authOptions } from "@formbricks/lib/authOptions"; import { getEnvironment } from "@formbricks/lib/environment/service"; import { getIntegrations } from "@formbricks/lib/integration/service"; import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/slack/page.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/slack/page.tsx index 39b638deed..57cc28bb39 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/slack/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/slack/page.tsx @@ -1,4 +1,5 @@ import { SlackWrapper } from "@/app/(app)/environments/[environmentId]/integrations/slack/components/SlackWrapper"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles"; import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams"; import { GoBackButton } from "@/modules/ui/components/go-back-button"; @@ -8,7 +9,6 @@ import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; import { redirect } from "next/navigation"; import { getAttributeClasses } from "@formbricks/lib/attributeClass/service"; -import { authOptions } from "@formbricks/lib/authOptions"; import { SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, WEBAPP_URL } from "@formbricks/lib/constants"; import { getEnvironment } from "@formbricks/lib/environment/service"; import { getIntegrationByType } from "@formbricks/lib/integration/service"; diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/page.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/page.tsx index c3dc77c1b7..2a0a06b414 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/webhooks/page.tsx @@ -2,6 +2,7 @@ import { AddWebhookButton } from "@/app/(app)/environments/[environmentId]/integ import { WebhookRowData } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookRowData"; import { WebhookTable } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookTable"; import { WebhookTableHeading } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/components/WebhookTableHeading"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles"; import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams"; import { GoBackButton } from "@/modules/ui/components/go-back-button"; @@ -9,7 +10,6 @@ import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper import { PageHeader } from "@/modules/ui/components/page-header"; import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; -import { authOptions } from "@formbricks/lib/authOptions"; import { getEnvironment } from "@formbricks/lib/environment/service"; import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; import { getAccessFlags } from "@formbricks/lib/membership/utils"; diff --git a/apps/web/app/(app)/environments/[environmentId]/layout.tsx b/apps/web/app/(app)/environments/[environmentId]/layout.tsx index 30b7c0d933..e1e9eb6f6f 100644 --- a/apps/web/app/(app)/environments/[environmentId]/layout.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/layout.tsx @@ -1,10 +1,10 @@ import { EnvironmentLayout } from "@/app/(app)/environments/[environmentId]/components/EnvironmentLayout"; import { ResponseFilterProvider } from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { ToasterClient } from "@/modules/ui/components/toaster-client"; import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; import { notFound, redirect } from "next/navigation"; -import { authOptions } from "@formbricks/lib/authOptions"; import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service"; diff --git a/apps/web/app/(app)/environments/[environmentId]/page.tsx b/apps/web/app/(app)/environments/[environmentId]/page.tsx index 0841ed044c..daf42779c6 100644 --- a/apps/web/app/(app)/environments/[environmentId]/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/page.tsx @@ -1,7 +1,7 @@ +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; import { redirect } from "next/navigation"; -import { authOptions } from "@formbricks/lib/authOptions"; import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; import { getAccessFlags } from "@formbricks/lib/membership/utils"; import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service"; diff --git a/apps/web/app/(app)/environments/[environmentId]/product/api-keys/page.tsx b/apps/web/app/(app)/environments/[environmentId]/product/api-keys/page.tsx index 58ec8e3bc9..9f44a7c064 100644 --- a/apps/web/app/(app)/environments/[environmentId]/product/api-keys/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/product/api-keys/page.tsx @@ -1,4 +1,5 @@ import { ProductConfigNavigation } from "@/app/(app)/environments/[environmentId]/product/components/ProductConfigNavigation"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getMultiLanguagePermission, getRoleManagementPermission, @@ -10,7 +11,6 @@ import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper import { PageHeader } from "@/modules/ui/components/page-header"; import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; -import { authOptions } from "@formbricks/lib/authOptions"; import { getEnvironment } from "@formbricks/lib/environment/service"; import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; import { getAccessFlags } from "@formbricks/lib/membership/utils"; diff --git a/apps/web/app/(app)/environments/[environmentId]/product/general/components/DeleteProduct.tsx b/apps/web/app/(app)/environments/[environmentId]/product/general/components/DeleteProduct.tsx index 7ac0f5127c..0a5c36e0b9 100644 --- a/apps/web/app/(app)/environments/[environmentId]/product/general/components/DeleteProduct.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/product/general/components/DeleteProduct.tsx @@ -1,7 +1,7 @@ import { DeleteProductRender } from "@/app/(app)/environments/[environmentId]/product/general/components/DeleteProductRender"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; -import { authOptions } from "@formbricks/lib/authOptions"; import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service"; import { getUserProducts } from "@formbricks/lib/product/service"; import { TProduct } from "@formbricks/types/product"; diff --git a/apps/web/app/(app)/environments/[environmentId]/product/general/page.tsx b/apps/web/app/(app)/environments/[environmentId]/product/general/page.tsx index 54c45ca1b6..e165233a60 100644 --- a/apps/web/app/(app)/environments/[environmentId]/product/general/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/product/general/page.tsx @@ -1,4 +1,5 @@ import { ProductConfigNavigation } from "@/app/(app)/environments/[environmentId]/product/components/ProductConfigNavigation"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getMultiLanguagePermission, getRoleManagementPermission, @@ -11,7 +12,6 @@ import { SettingsId } from "@/modules/ui/components/settings-id"; import packageJson from "@/package.json"; import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; -import { authOptions } from "@formbricks/lib/authOptions"; import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; import { getAccessFlags } from "@formbricks/lib/membership/utils"; diff --git a/apps/web/app/(app)/environments/[environmentId]/product/languages/page.tsx b/apps/web/app/(app)/environments/[environmentId]/product/languages/page.tsx index 8b9561e5d0..ae1080c7de 100644 --- a/apps/web/app/(app)/environments/[environmentId]/product/languages/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/product/languages/page.tsx @@ -1,5 +1,6 @@ import { ProductConfigNavigation } from "@/app/(app)/environments/[environmentId]/product/components/ProductConfigNavigation"; import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getMultiLanguagePermission, getRoleManagementPermission, @@ -12,7 +13,6 @@ import { PageHeader } from "@/modules/ui/components/page-header"; import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; import { notFound } from "next/navigation"; -import { authOptions } from "@formbricks/lib/authOptions"; import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; import { getAccessFlags } from "@formbricks/lib/membership/utils"; import { getOrganization } from "@formbricks/lib/organization/service"; diff --git a/apps/web/app/(app)/environments/[environmentId]/product/layout.tsx b/apps/web/app/(app)/environments/[environmentId]/product/layout.tsx index 8b7ae51acd..e4f9ea4e98 100644 --- a/apps/web/app/(app)/environments/[environmentId]/product/layout.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/product/layout.tsx @@ -1,8 +1,8 @@ +import { authOptions } from "@/modules/auth/lib/authOptions"; import { Metadata } from "next"; import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; import { redirect } from "next/navigation"; -import { authOptions } from "@formbricks/lib/authOptions"; import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; import { getAccessFlags } from "@formbricks/lib/membership/utils"; import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service"; diff --git a/apps/web/app/(app)/environments/[environmentId]/product/look/page.tsx b/apps/web/app/(app)/environments/[environmentId]/product/look/page.tsx index 34de6e815f..09a5fb65a4 100644 --- a/apps/web/app/(app)/environments/[environmentId]/product/look/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/product/look/page.tsx @@ -1,5 +1,6 @@ import { ProductConfigNavigation } from "@/app/(app)/environments/[environmentId]/product/components/ProductConfigNavigation"; import { EditLogo } from "@/app/(app)/environments/[environmentId]/product/look/components/EditLogo"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getMultiLanguagePermission, getRemoveInAppBrandingPermission, @@ -13,7 +14,6 @@ import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper import { PageHeader } from "@/modules/ui/components/page-header"; import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; -import { authOptions } from "@formbricks/lib/authOptions"; import { cn } from "@formbricks/lib/cn"; import { DEFAULT_LOCALE, SURVEY_BG_COLORS, UNSPLASH_ACCESS_KEY } from "@formbricks/lib/constants"; import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; diff --git a/apps/web/app/(app)/environments/[environmentId]/product/tags/page.tsx b/apps/web/app/(app)/environments/[environmentId]/product/tags/page.tsx index 07c5b7f62c..542aa53e81 100644 --- a/apps/web/app/(app)/environments/[environmentId]/product/tags/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/product/tags/page.tsx @@ -1,5 +1,6 @@ import { ProductConfigNavigation } from "@/app/(app)/environments/[environmentId]/product/components/ProductConfigNavigation"; import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getMultiLanguagePermission, getRoleManagementPermission, @@ -10,7 +11,6 @@ import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper import { PageHeader } from "@/modules/ui/components/page-header"; import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; -import { authOptions } from "@formbricks/lib/authOptions"; import { getEnvironment } from "@formbricks/lib/environment/service"; import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; import { getAccessFlags } from "@formbricks/lib/membership/utils"; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/layout.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/layout.tsx index e5afe8d17c..9689fdecc1 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/layout.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/layout.tsx @@ -1,6 +1,6 @@ +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; -import { authOptions } from "@formbricks/lib/authOptions"; import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service"; import { getProductByEnvironmentId } from "@formbricks/lib/product/service"; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/page.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/page.tsx index c548aeb3d4..feab128a50 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/notifications/page.tsx @@ -1,11 +1,11 @@ import { AccountSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(account)/components/AccountSettingsNavbar"; import { SettingsCard } from "@/app/(app)/environments/[environmentId]/settings/components/SettingsCard"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper"; import { PageHeader } from "@/modules/ui/components/page-header"; import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; import { prisma } from "@formbricks/database"; -import { authOptions } from "@formbricks/lib/authOptions"; import { getUser } from "@formbricks/lib/user/service"; import { TUserNotificationSettings } from "@formbricks/types/user"; import { EditAlerts } from "./components/EditAlerts"; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/actions.ts b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/actions.ts index 87ae62ecf1..fda5844b9f 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/actions.ts @@ -2,7 +2,6 @@ import { authenticatedActionClient } from "@/lib/utils/action-client"; import { z } from "zod"; -import { disableTwoFactorAuth, enableTwoFactorAuth, setupTwoFactorAuth } from "@formbricks/lib/auth/service"; import { deleteFile } from "@formbricks/lib/storage/service"; import { getFileNameWithIdFromUrl } from "@formbricks/lib/storage/utils"; import { updateUser } from "@formbricks/lib/user/service"; @@ -15,38 +14,6 @@ export const updateUserAction = authenticatedActionClient return await updateUser(ctx.user.id, parsedInput); }); -const ZSetupTwoFactorAuthAction = z.object({ - password: z.string(), -}); - -export const setupTwoFactorAuthAction = authenticatedActionClient - .schema(ZSetupTwoFactorAuthAction) - .action(async ({ parsedInput, ctx }) => { - return await setupTwoFactorAuth(ctx.user.id, parsedInput.password); - }); - -const ZEnableTwoFactorAuthAction = z.object({ - code: z.string(), -}); - -export const enableTwoFactorAuthAction = authenticatedActionClient - .schema(ZEnableTwoFactorAuthAction) - .action(async ({ parsedInput, ctx }) => { - return await enableTwoFactorAuth(ctx.user.id, parsedInput.code); - }); - -const ZDisableTwoFactorAuthAction = z.object({ - code: z.string(), - password: z.string(), - backupCode: z.string().optional(), -}); - -export const disableTwoFactorAuthAction = authenticatedActionClient - .schema(ZDisableTwoFactorAuthAction) - .action(async ({ parsedInput, ctx }) => { - return await disableTwoFactorAuth(ctx.user.id, parsedInput); - }); - const ZUpdateAvatarAction = z.object({ avatarUrl: z.string(), }); diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/AccountSecurity.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/AccountSecurity.tsx index 4f9d039525..5784f7e843 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/AccountSecurity.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/AccountSecurity.tsx @@ -1,13 +1,20 @@ "use client"; -import { DisableTwoFactorModal } from "@/app/(app)/environments/[environmentId]/settings/(account)/profile/components/DisableTwoFactorModal"; -import { EnableTwoFactorModal } from "@/app/(app)/environments/[environmentId]/settings/(account)/profile/components/EnableTwoFactorModal"; +import { DisableTwoFactorModal } from "@/modules/ee/two-factor-auth/components/disable-two-factor-modal"; +import { EnableTwoFactorModal } from "@/modules/ee/two-factor-auth/components/enable-two-factor-modal"; import { Switch } from "@/modules/ui/components/switch"; +import { UpgradePlanNotice } from "@/modules/ui/components/upgrade-plan-notice"; import { useTranslations } from "next-intl"; import { useState } from "react"; import { TUser } from "@formbricks/types/user"; -export const AccountSecurity = ({ user }: { user: TUser }) => { +interface AccountSecurityProps { + user: TUser; + isTwoFactorAuthEnabled: boolean; + environmentId: string; +} + +export const AccountSecurity = ({ user, isTwoFactorAuthEnabled, environmentId }: AccountSecurityProps) => { const t = useTranslations(); const [twoFactorModalOpen, setTwoFactorModalOpen] = useState(false); const [disableTwoFactorModalOpen, setDisableTwoFactorModalOpen] = useState(false); @@ -17,6 +24,7 @@ export const AccountSecurity = ({ user }: { user: TUser }) => {
{ if (checked) { setTwoFactorModalOpen(true); @@ -35,7 +43,13 @@ export const AccountSecurity = ({ user }: { user: TUser }) => {

- + {!isTwoFactorAuthEnabled && !user.twoFactorEnabled && ( + + )} diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/DisableTwoFactorModal.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/DisableTwoFactorModal.tsx deleted file mode 100644 index 35fabf8ddd..0000000000 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/DisableTwoFactorModal.tsx +++ /dev/null @@ -1,171 +0,0 @@ -"use client"; - -import { disableTwoFactorAuthAction } from "@/app/(app)/environments/[environmentId]/settings/(account)/profile/actions"; -import { getFormattedErrorMessage } from "@/lib/utils/helper"; -import { Button } from "@/modules/ui/components/button"; -import { Input } from "@/modules/ui/components/input"; -import { Modal } from "@/modules/ui/components/modal"; -import { OTPInput } from "@/modules/ui/components/otp-input"; -import { PasswordInput } from "@/modules/ui/components/password-input"; -import { useTranslations } from "next-intl"; -import { useRouter } from "next/navigation"; -import React, { useEffect, useState } from "react"; -import { Controller, SubmitHandler, useForm } from "react-hook-form"; -import toast from "react-hot-toast"; - -type TDisableTwoFactorFormState = { - password: string; - code: string; - backupCode?: string; -}; - -type TDisableTwoFactorModalProps = { - open: boolean; - setOpen: (open: boolean) => void; -}; - -export const DisableTwoFactorModal = ({ open, setOpen }: TDisableTwoFactorModalProps) => { - const t = useTranslations(); - const router = useRouter(); - const { handleSubmit, control, setValue } = useForm(); - const [backupCodeInputVisible, setBackupCodeInputVisible] = useState(false); - - useEffect(() => { - setValue("backupCode", ""); - setValue("code", ""); - }, [backupCodeInputVisible, setValue]); - - const resetState = () => { - setBackupCodeInputVisible(false); - setValue("password", ""); - setValue("backupCode", ""); - setValue("code", ""); - setOpen(false); - }; - - const onSubmit: SubmitHandler = async (data) => { - const { code, password, backupCode } = data; - - const disableTwoFactorAuthResponse = await disableTwoFactorAuthAction({ code, password, backupCode }); - - if (disableTwoFactorAuthResponse?.data) { - toast.success(disableTwoFactorAuthResponse.data.message); - - router.refresh(); - resetState(); - } else { - const errorMessage = getFormattedErrorMessage(disableTwoFactorAuthResponse); - toast.error(errorMessage); - } - }; - - return ( - resetState()} noPadding> - <> -
-
-

- {t("environments.settings.profile.disable_two_factor_authentication")} -

-

- {t("environments.settings.profile.disable_two_factor_authentication_description")} -

-
- -
-
- - ( - <> - - - {errors.password && ( -

- {errors.password.message} -

- )} - - )} - /> -
- -
-
- - -

- {backupCodeInputVisible - ? t( - "environments.settings.profile.each_backup_code_can_be_used_exactly_once_to_grant_access_without_your_authenticator" - ) - : t( - "environments.settings.profile.two_factor_authentication_enabled_please_enter_the_six_digit_code_from_your_authenticator_app" - )} -

-
- - {backupCodeInputVisible ? ( - } - /> - ) : ( - ( - - )} - /> - )} -
- -
-
- -
-
- - - -
-
-
-
- -
- ); -}; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/EditProfileAvatarForm.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/EditProfileAvatarForm.tsx index 4293edf749..5ba6973917 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/EditProfileAvatarForm.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/components/EditProfileAvatarForm.tsx @@ -27,7 +27,7 @@ export const EditProfileAvatarForm = ({ session, environmentId, imageUrl }: Edit const inputRef = useRef(null); const [isLoading, setIsLoading] = useState(false); const router = useRouter(); - const t = useTranslations("environments.settings.profile"); + const t = useTranslations(); const fileSchema = typeof window !== "undefined" ? z @@ -136,7 +136,9 @@ export const EditProfileAvatarForm = ({ session, environmentId, imageUrl }: Edit onClick={() => { inputRef.current?.click(); }}> - {imageUrl ? t("change_image") : t("upload_image")} + {imageUrl + ? t("environments.settings.profile.change_image") + : t("environments.settings.profile.upload_image")} void; -}; - -type TConfirmPasswordFormProps = { - setCurrentStep: (step: TStep) => void; - setBackupCodes: (codes: string[]) => void; - setDataUri: (dataUri: string) => void; - setSecret: (secret: string) => void; - setOpen: (open: boolean) => void; -}; -const ConfirmPasswordForm = ({ - setBackupCodes, - setCurrentStep, - setDataUri, - setSecret, - setOpen, -}: TConfirmPasswordFormProps) => { - const { control, handleSubmit, setError } = useForm(); - const t = useTranslations(); - const onSubmit: SubmitHandler = async (data) => { - const setupTwoFactorAuthResponse = await setupTwoFactorAuthAction({ password: data.password }); - - if (setupTwoFactorAuthResponse?.data) { - const { backupCodes, dataUri, secret } = setupTwoFactorAuthResponse.data; - setBackupCodes(backupCodes); - setDataUri(dataUri); - setSecret(secret); - setCurrentStep("scanQRCode"); - } else { - const errorMessage = getFormattedErrorMessage(setupTwoFactorAuthResponse); - setError("password", { message: errorMessage }); - } - }; - - return ( -
-
-

- {t("environments.settings.profile.two_factor_authentication")} -

-

- {t("environments.settings.profile.confirm_your_current_password_to_get_started")} -

-
-
-
- - ( - <> - - - {errors.password && ( -

- {errors.password.message} -

- )} - - )} - /> -
- -
- - - -
-
-
- ); -}; - -type TScanQRCodeProps = { - setCurrentStep: (step: TStep) => void; - dataUri: string; - secret: string; - setOpen: (open: boolean) => void; -}; -const ScanQRCode = ({ dataUri, secret, setCurrentStep, setOpen }: TScanQRCodeProps) => { - const t = useTranslations(); - return ( -
-
-

- {t("environments.settings.profile.enable_two_factor_authentication")} -

-

- {t("environments.settings.profile.scan_the_qr_code_below_with_your_authenticator_app")} -

-
- -
- QR code -

- {t("environments.settings.profile.or_enter_the_following_code_manually")} -

-

{secret}

-
- -
- - - -
-
- ); -}; - -type TEnableCodeProps = { - setCurrentStep: (step: TStep) => void; - setOpen: (open: boolean) => void; - refreshData: () => void; -}; -const EnterCode = ({ setCurrentStep, setOpen, refreshData }: TEnableCodeProps) => { - const t = useTranslations(); - const { control, handleSubmit } = useForm({ - defaultValues: { - code: "", - }, - }); - - const onSubmit: SubmitHandler = async (data) => { - try { - const enableTwoFactorAuthResponse = await enableTwoFactorAuthAction({ code: data.code }); - if (enableTwoFactorAuthResponse?.data) { - toast.success(enableTwoFactorAuthResponse.data.message); - setCurrentStep("backupCodes"); - - // refresh data to update the UI - refreshData(); - } else { - toast.error(t("environments.settings.profile.the_2fa_otp_is_incorrect_please_try_again")); - } - } catch (err) { - toast.error(err.message); - } - }; - - return ( - <> -
-
-

- {t("environments.settings.profile.enable_two_factor_authentication")} -

-

- {t("environments.settings.profile.enter_the_code_from_your_authenticator_app_below")} -

-
- -
-
- - ( - <> - - - {errors.code && ( -

- {errors.code.message} -

- )} - - )} - /> -
- -
- - - -
-
-
- - ); -}; - -type TDisplayBackupCodesProps = { - backupCodes: string[]; - setOpen: (open: boolean) => void; -}; - -const DisplayBackupCodes = ({ backupCodes, setOpen }: TDisplayBackupCodesProps) => { - const t = useTranslations(); - const formatBackupCode = (code: string) => `${code.slice(0, 5)}-${code.slice(5, 10)}`; - - const handleDownloadBackupCode = () => { - const formattedCodes = backupCodes.map((code) => formatBackupCode(code)).join("\n"); - const blob = new Blob([formattedCodes], { type: "text/plain" }); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = "formbricks-backup-codes.txt"; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - }; - - return ( -
-
-

- {t("environments.settings.profile.enable_two_factor_authentication")} -

-

- {t("environments.settings.profile.save_the_following_backup_codes_in_a_safe_place")} -

-
- -
- {backupCodes.map((code) => ( -

- {formatBackupCode(code)} -

- ))} -
- -
- - - - - -
-
- ); -}; - -export const EnableTwoFactorModal = ({ open, setOpen }: TEnableTwoFactorModalProps) => { - const router = useRouter(); - const [currentStep, setCurrentStep] = useState("confirmPassword"); - const [backupCodes, setBackupCodes] = useState([]); - const [dataUri, setDataUri] = useState(""); - const [secret, setSecret] = useState(""); - - const refreshData = () => { - router.refresh(); - }; - - const resetState = () => { - setCurrentStep("confirmPassword"); - setBackupCodes([]); - setDataUri(""); - setSecret(""); - setOpen(false); - }; - - return ( - resetState()} noPadding> - {currentStep === "confirmPassword" && ( - - )} - - {currentStep === "scanQRCode" && ( - - )} - - {currentStep === "enterCode" && ( - - )} - - {currentStep === "backupCodes" && } - - ); -}; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/page.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/page.tsx index c77a9cd568..a6bdd51444 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(account)/profile/page.tsx @@ -1,11 +1,12 @@ import { AccountSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(account)/components/AccountSettingsNavbar"; import { AccountSecurity } from "@/app/(app)/environments/[environmentId]/settings/(account)/profile/components/AccountSecurity"; +import { authOptions } from "@/modules/auth/lib/authOptions"; +import { getIsTwoFactorAuthEnabled } from "@/modules/ee/license-check/lib/utils"; import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper"; import { PageHeader } from "@/modules/ui/components/page-header"; import { SettingsId } from "@/modules/ui/components/settings-id"; import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; -import { authOptions } from "@formbricks/lib/authOptions"; import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service"; @@ -16,6 +17,7 @@ import { EditProfileAvatarForm } from "./components/EditProfileAvatarForm"; import { EditProfileDetailsForm } from "./components/EditProfileDetailsForm"; const Page = async (props: { params: Promise<{ environmentId: string }> }) => { + const isTwoFactorAuthEnabled = await getIsTwoFactorAuthEnabled(); const params = await props.params; const t = await getTranslations(); const { environmentId } = params; @@ -65,7 +67,11 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => { - + )} diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/enterprise/page.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/enterprise/page.tsx index e4a032de3d..2b8c27852e 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/enterprise/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/enterprise/page.tsx @@ -1,4 +1,5 @@ import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getEnterpriseLicense, getRoleManagementPermission } from "@/modules/ee/license-check/lib/utils"; import { Button } from "@/modules/ui/components/button"; import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper"; @@ -7,7 +8,6 @@ import { CheckIcon } from "lucide-react"; import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; import { notFound } from "next/navigation"; -import { authOptions } from "@formbricks/lib/authOptions"; import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; import { getAccessFlags } from "@formbricks/lib/membership/utils"; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/actions.ts b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/actions.ts index 87c5fed477..78a2ac3151 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/actions.ts @@ -275,6 +275,7 @@ export const inviteUserAction = authenticatedActionClient name: parsedInput.name, role: parsedInput.role, }, + currentUserId: ctx.user.id, }); if (invite) { diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/page.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/page.tsx index b5180207a5..7949c06ef4 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/general/page.tsx @@ -2,6 +2,7 @@ import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmen import { AIToggle } from "@/app/(app)/environments/[environmentId]/settings/(organization)/general/components/AIToggle"; import { OrganizationActions } from "@/app/(app)/environments/[environmentId]/settings/(organization)/general/components/EditMemberships/OrganizationActions"; import { getMembershipsByUserId } from "@/app/(app)/environments/[environmentId]/settings/(organization)/general/lib/membership"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getIsMultiOrgEnabled, getIsOrganizationAIReady, @@ -13,7 +14,6 @@ import { SettingsId } from "@/modules/ui/components/settings-id"; import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; import { Suspense } from "react"; -import { authOptions } from "@formbricks/lib/authOptions"; import { INVITE_DISABLED, IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; import { getAccessFlags } from "@formbricks/lib/membership/utils"; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/layout.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/layout.tsx index 1504743b40..cbef90a105 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/layout.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/(organization)/layout.tsx @@ -1,6 +1,6 @@ +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; -import { authOptions } from "@formbricks/lib/authOptions"; import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service"; import { getProductByEnvironmentId } from "@formbricks/lib/product/service"; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/layout.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/layout.tsx index 080d80506e..fe5477082e 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/layout.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/layout.tsx @@ -1,6 +1,6 @@ +import { authOptions } from "@/modules/auth/lib/authOptions"; import { Metadata } from "next"; import { getServerSession } from "next-auth"; -import { authOptions } from "@formbricks/lib/authOptions"; import { getResponseCountBySurveyId } from "@formbricks/lib/response/service"; import { getSurvey } from "@formbricks/lib/survey/service"; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/page.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/page.tsx index 9c97475e53..452cc83d96 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/page.tsx @@ -3,6 +3,7 @@ import { ResponsePage } from "@/app/(app)/environments/[environmentId]/surveys/[ import { EnableInsightsBanner } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/EnableInsightsBanner"; import { SurveyAnalysisCTA } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA"; import { needsInsightsGeneration } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/utils"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getIsAIEnabled } from "@/modules/ee/license-check/lib/utils"; import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles"; import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams"; @@ -10,7 +11,6 @@ import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper import { PageHeader } from "@/modules/ui/components/page-header"; import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; -import { authOptions } from "@formbricks/lib/authOptions"; import { MAX_RESPONSES_FOR_INSIGHT_GENERATION, RESPONSES_PER_PAGE, diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/page.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/page.tsx index 4140473b90..ec8c8706c9 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/page.tsx @@ -3,6 +3,7 @@ import { EnableInsightsBanner } from "@/app/(app)/environments/[environmentId]/s import { SummaryPage } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryPage"; import { SurveyAnalysisCTA } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SurveyAnalysisCTA"; import { needsInsightsGeneration } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/utils"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getIsAIEnabled } from "@/modules/ee/license-check/lib/utils"; import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles"; import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams"; @@ -12,7 +13,6 @@ import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; import { notFound } from "next/navigation"; import { getAttributeClasses } from "@formbricks/lib/attributeClass/service"; -import { authOptions } from "@formbricks/lib/authOptions"; import { DEFAULT_LOCALE, DOCUMENTS_PER_PAGE, diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/page.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/page.tsx index 38163868b2..383937923f 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/page.tsx @@ -1,4 +1,5 @@ import { SurveysList } from "@/app/(app)/environments/[environmentId]/surveys/components/SurveyList"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getProductPermissionByUserId } from "@/modules/ee/teams/lib/roles"; import { getTeamPermissionFlags } from "@/modules/ee/teams/utils/teams"; import { TemplateList } from "@/modules/surveys/components/TemplateList"; @@ -10,7 +11,6 @@ import { Metadata } from "next"; import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; import { redirect } from "next/navigation"; -import { authOptions } from "@formbricks/lib/authOptions"; import { SURVEYS_PER_PAGE, WEBAPP_URL } from "@formbricks/lib/constants"; import { getEnvironment, getEnvironments } from "@formbricks/lib/environment/service"; import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; diff --git a/apps/web/app/(app)/layout.tsx b/apps/web/app/(app)/layout.tsx index 31c24cef91..9185cbd502 100644 --- a/apps/web/app/(app)/layout.tsx +++ b/apps/web/app/(app)/layout.tsx @@ -1,10 +1,10 @@ import { FormbricksClient } from "@/app/(app)/components/FormbricksClient"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { NoMobileOverlay } from "@/modules/ui/components/no-mobile-overlay"; import { PHProvider, PostHogPageview } from "@/modules/ui/components/post-hog-client"; import { ToasterClient } from "@/modules/ui/components/toaster-client"; import { getServerSession } from "next-auth"; import { Suspense } from "react"; -import { authOptions } from "@formbricks/lib/authOptions"; import { getUser } from "@formbricks/lib/user/service"; const AppLayout = async ({ children }) => { diff --git a/apps/web/app/(auth)/auth/forgot-password/components/PasswordResetForm/index.tsx b/apps/web/app/(auth)/auth/forgot-password/components/PasswordResetForm/index.tsx deleted file mode 100644 index c9c92b74f3..0000000000 --- a/apps/web/app/(auth)/auth/forgot-password/components/PasswordResetForm/index.tsx +++ /dev/null @@ -1,77 +0,0 @@ -"use client"; - -import { Button } from "@/modules/ui/components/button"; -import { XCircleIcon } from "lucide-react"; -import { useTranslations } from "next-intl"; -import { useRouter } from "next/navigation"; -import { useState } from "react"; -import { forgotPassword } from "@formbricks/lib/utils/users"; - -export const PasswordResetForm = ({}) => { - const router = useRouter(); - const [error, setError] = useState(""); - const [loading, setLoading] = useState(false); - const t = useTranslations(); - const handleSubmit = async (e) => { - e.preventDefault(); - setLoading(true); - try { - await forgotPassword(e.target.elements.email.value); - router.push("/auth/forgot-password/email-sent"); - } catch (e) { - setError(e.message); - } finally { - setLoading(false); - } - }; - - return ( - <> - {error && ( -
-
-
-
-
-

- {t("auth.forgot-password.an_error_occurred_when_logging")} -

-
-

{error}

-
-
-
-
- )} -
-
- -
- -
-
- -
- -
- -
-
-
- - ); -}; diff --git a/apps/web/app/(auth)/auth/forgot-password/email-sent/page.tsx b/apps/web/app/(auth)/auth/forgot-password/email-sent/page.tsx index e90657e20f..8a59ae7082 100644 --- a/apps/web/app/(auth)/auth/forgot-password/email-sent/page.tsx +++ b/apps/web/app/(auth)/auth/forgot-password/email-sent/page.tsx @@ -1,22 +1,3 @@ -import { BackToLoginButton } from "@/app/(auth)/auth/components/BackToLoginButton"; -import { FormWrapper } from "@/app/(auth)/auth/components/FormWrapper"; -import { getTranslations } from "next-intl/server"; +import { EmailSentPage } from "@/modules/auth/forgot-password/email-sent/page"; -const Page = async () => { - const t = await getTranslations(); - return ( - -
-

- {t("auth.forgot-password.email-sent.heading")} -

-

{t("auth.forgot-password.email-sent.text")}

-
- -
-
-
- ); -}; - -export default Page; +export default EmailSentPage; diff --git a/apps/web/app/(auth)/auth/forgot-password/page.tsx b/apps/web/app/(auth)/auth/forgot-password/page.tsx index 26385dec07..2b0e9dcbf5 100644 --- a/apps/web/app/(auth)/auth/forgot-password/page.tsx +++ b/apps/web/app/(auth)/auth/forgot-password/page.tsx @@ -1,11 +1,3 @@ -import { FormWrapper } from "@/app/(auth)/auth/components/FormWrapper"; -import { PasswordResetForm } from "@/app/(auth)/auth/forgot-password/components/PasswordResetForm"; +import { ForgotPasswordPage } from "@/modules/auth/forgot-password/page"; -const Page = () => { - return ( - - - - ); -}; -export default Page; +export default ForgotPasswordPage; diff --git a/apps/web/app/(auth)/auth/forgot-password/reset/components/ResetPasswordForm/index.tsx b/apps/web/app/(auth)/auth/forgot-password/reset/components/ResetPasswordForm/index.tsx deleted file mode 100644 index bdfd4f3b57..0000000000 --- a/apps/web/app/(auth)/auth/forgot-password/reset/components/ResetPasswordForm/index.tsx +++ /dev/null @@ -1,106 +0,0 @@ -"use client"; - -import { IsPasswordValid } from "@/modules/auth/components/SignupOptions/components/IsPasswordValid"; -import { Button } from "@/modules/ui/components/button"; -import { PasswordInput } from "@/modules/ui/components/password-input"; -import { XCircleIcon } from "lucide-react"; -import { useTranslations } from "next-intl"; -import { useRouter, useSearchParams } from "next/navigation"; -import { useState } from "react"; -import { toast } from "react-hot-toast"; -import { resetPassword } from "@formbricks/lib/utils/users"; - -export const ResetPasswordForm = () => { - const t = useTranslations(); - const searchParams = useSearchParams(); - const router = useRouter(); - const [error, setError] = useState(""); - const [password, setPassword] = useState(null); - const [confirmPassword, setConfirmPassword] = useState(null); - const [isValid, setIsValid] = useState(false); - const [loading, setLoading] = useState(false); - - const handleSubmit = async (e) => { - e.preventDefault(); - if (password !== confirmPassword) { - toast.error(t("auth.forgot-password.reset.passwords_do_not_match")); - return; - } - setLoading(true); - const token = searchParams?.get("token"); - try { - if (!token) throw new Error(t("auth.forgot-password.reset.no_token_provided")); - await resetPassword(token, e.target.elements.password.value); - - router.push("/auth/forgot-password/reset/success"); - } catch (e) { - setError(e.message); - } finally { - setLoading(false); - } - }; - - return ( - <> - {error && ( -
-
-
-
-
-

- {t("auth.forgot-password.an_error_occurred_when_logging_you_in")} -

-
-

{error}

-
-
-
-
- )} -
-
-
- - setPassword(e.target.value)} - autoComplete="current-password" - placeholder="*******" - required - className="focus:border-brand-dark focus:ring-brand-dark mt-2 block w-full rounded-md border-slate-300 shadow-sm sm:text-sm" - /> -
-
- - setConfirmPassword(e.target.value)} - autoComplete="current-password" - placeholder="*******" - required - className="focus:border-brand-dark focus:ring-brand-dark mt-2 block w-full rounded-md border-slate-300 shadow-sm sm:text-sm" - /> -
- - -
- -
- -
-
- - ); -}; diff --git a/apps/web/app/(auth)/auth/forgot-password/reset/page.tsx b/apps/web/app/(auth)/auth/forgot-password/reset/page.tsx index 11d76d5c43..7743f90210 100644 --- a/apps/web/app/(auth)/auth/forgot-password/reset/page.tsx +++ b/apps/web/app/(auth)/auth/forgot-password/reset/page.tsx @@ -1,11 +1,3 @@ -import { FormWrapper } from "@/app/(auth)/auth/components/FormWrapper"; -import { ResetPasswordForm } from "@/app/(auth)/auth/forgot-password/reset/components/ResetPasswordForm"; +import { ResetPasswordPage } from "@/modules/auth/forgot-password/reset/page"; -const Page = () => { - return ( - - - - ); -}; -export default Page; +export default ResetPasswordPage; diff --git a/apps/web/app/(auth)/auth/forgot-password/reset/success/page.tsx b/apps/web/app/(auth)/auth/forgot-password/reset/success/page.tsx index 558bc17a01..cfd3253405 100644 --- a/apps/web/app/(auth)/auth/forgot-password/reset/success/page.tsx +++ b/apps/web/app/(auth)/auth/forgot-password/reset/success/page.tsx @@ -1,22 +1,3 @@ -import { BackToLoginButton } from "@/app/(auth)/auth/components/BackToLoginButton"; -import { FormWrapper } from "@/app/(auth)/auth/components/FormWrapper"; -import { getTranslations } from "next-intl/server"; +import { ResetPasswordSuccessPage } from "@/modules/auth/forgot-password/reset/success/page"; -const Page = async () => { - const t = await getTranslations(); - return ( - -
-

- {t("auth.forgot-password.reset.success.heading")} -

-

{t("auth.forgot-password.reset.success.text")}

-
- -
-
-
- ); -}; - -export default Page; +export default ResetPasswordSuccessPage; diff --git a/apps/web/app/(auth)/auth/layout.tsx b/apps/web/app/(auth)/auth/layout.tsx index 7bf1f80ed6..b44d9469e3 100644 --- a/apps/web/app/(auth)/auth/layout.tsx +++ b/apps/web/app/(auth)/auth/layout.tsx @@ -1,31 +1,3 @@ -import { getIsMultiOrgEnabled } from "@/modules/ee/license-check/lib/utils"; -import { getServerSession } from "next-auth"; -import { redirect } from "next/navigation"; -import { Toaster } from "react-hot-toast"; -import { authOptions } from "@formbricks/lib/authOptions"; -import { getIsFreshInstance } from "@formbricks/lib/instance/service"; - -const AuthLayout = async ({ children }: { children: React.ReactNode }) => { - const session = await getServerSession(authOptions); - const isFreshInstance = await getIsFreshInstance(); - const isMultiOrgEnabled = await getIsMultiOrgEnabled(); - if (session) { - redirect(`/`); - } - - if (isFreshInstance && !isMultiOrgEnabled) { - redirect("/setup/intro"); - } - return ( - <> - -
-
-
{children}
-
-
- - ); -}; +import { AuthLayout } from "@/modules/auth/layout"; export default AuthLayout; diff --git a/apps/web/app/(auth)/auth/login/page.tsx b/apps/web/app/(auth)/auth/login/page.tsx index 570f78ac27..3ef2005e4b 100644 --- a/apps/web/app/(auth)/auth/login/page.tsx +++ b/apps/web/app/(auth)/auth/login/page.tsx @@ -1,48 +1,3 @@ -import { FormWrapper } from "@/app/(auth)/auth/components/FormWrapper"; -import { Testimonial } from "@/app/(auth)/auth/components/Testimonial"; -import { SigninForm } from "@/modules/auth/components/SigninForm"; -import { getIsMultiOrgEnabled } from "@/modules/ee/license-check/lib/utils"; -import { Metadata } from "next"; -import { - AZURE_OAUTH_ENABLED, - EMAIL_AUTH_ENABLED, - GITHUB_OAUTH_ENABLED, - GOOGLE_OAUTH_ENABLED, - OIDC_DISPLAY_NAME, - OIDC_OAUTH_ENABLED, - PASSWORD_RESET_DISABLED, - SIGNUP_ENABLED, -} from "@formbricks/lib/constants"; +import { LoginPage } from "@/modules/auth/login/page"; -export const metadata: Metadata = { - title: "Login", - description: "Open-source Experience Management. Free & open source.", -}; - -const Page = async () => { - const isMultiOrgEnabled = await getIsMultiOrgEnabled(); - return ( -
-
- -
-
- - - -
-
- ); -}; - -export default Page; +export default LoginPage; diff --git a/apps/web/app/(auth)/auth/signup-without-verification-success/page.tsx b/apps/web/app/(auth)/auth/signup-without-verification-success/page.tsx index 06938d8e17..af5b3ba0e4 100644 --- a/apps/web/app/(auth)/auth/signup-without-verification-success/page.tsx +++ b/apps/web/app/(auth)/auth/signup-without-verification-success/page.tsx @@ -1,21 +1,3 @@ -import { BackToLoginButton } from "@/app/(auth)/auth/components/BackToLoginButton"; -import { FormWrapper } from "@/app/(auth)/auth/components/FormWrapper"; -import { getTranslations } from "next-intl/server"; +import { SignupWithoutVerificationSuccessPage } from "@/modules/auth/signup-without-verification-success/page"; -const Page = async () => { - const t = await getTranslations(); - return ( - -

- {t("auth.signup_without_verification_success.user_successfully_created")} -

-

- {t("auth.signup_without_verification_success.user_successfully_created_description")} -

-
- -
- ); -}; - -export default Page; +export default SignupWithoutVerificationSuccessPage; diff --git a/apps/web/app/(auth)/auth/signup/components/SignupForm.tsx b/apps/web/app/(auth)/auth/signup/components/SignupForm.tsx deleted file mode 100644 index 6b4325702c..0000000000 --- a/apps/web/app/(auth)/auth/signup/components/SignupForm.tsx +++ /dev/null @@ -1,115 +0,0 @@ -"use client"; - -import { SignupOptions } from "@/modules/auth/components/SignupOptions"; -import { XCircleIcon } from "lucide-react"; -import { useTranslations } from "next-intl"; -import Link from "next/link"; -import { useSearchParams } from "next/navigation"; -import { useMemo, useState } from "react"; - -interface SignupFormProps { - webAppUrl: string; - privacyUrl: string | undefined; - termsUrl: string | undefined; - emailVerificationDisabled: boolean; - emailAuthEnabled: boolean; - googleOAuthEnabled: boolean; - githubOAuthEnabled: boolean; - azureOAuthEnabled: boolean; - oidcOAuthEnabled: boolean; - oidcDisplayName?: string; - userLocale: string; -} - -export const SignupForm = ({ - webAppUrl, - privacyUrl, - termsUrl, - emailVerificationDisabled, - emailAuthEnabled, - googleOAuthEnabled, - githubOAuthEnabled, - azureOAuthEnabled, - oidcOAuthEnabled, - oidcDisplayName, - userLocale, -}: SignupFormProps) => { - const searchParams = useSearchParams(); - const [error, setError] = useState(""); - const t = useTranslations(); - const inviteToken = searchParams?.get("inviteToken"); - const callbackUrl = useMemo(() => { - if (inviteToken) { - return webAppUrl + "/invite?token=" + inviteToken; - } else { - return webAppUrl; - } - }, [inviteToken, webAppUrl]); - - return ( - <> - {error && ( -
-
-
-
-
-

{t("auth.signup.error")}

-
-

{error}

-
-
-
-
- )} -
-

{t("auth.signup.title")}

- - {(termsUrl || privacyUrl) && ( -
- {t("auth.signup.terms_of_service")} -
- {termsUrl && ( - - {t("auth.signup.terms_of_service")} - - )} - {termsUrl && privacyUrl && {t("common.and")} } - {privacyUrl && ( - - {t("auth.signup.privacy_policy")} - - )} - {/*
- We'll occasionally send you account related emails. */} -
-
- )} - -
- {t("auth.signup.have_an_account")} -
- - {t("auth.signup.log_in")} - -
-
- - ); -}; diff --git a/apps/web/app/(auth)/auth/signup/page.tsx b/apps/web/app/(auth)/auth/signup/page.tsx index d6e322abc1..926931607d 100644 --- a/apps/web/app/(auth)/auth/signup/page.tsx +++ b/apps/web/app/(auth)/auth/signup/page.tsx @@ -1,56 +1,3 @@ -import { FormWrapper } from "@/app/(auth)/auth/components/FormWrapper"; -import { Testimonial } from "@/app/(auth)/auth/components/Testimonial"; -import { SignupForm } from "@/app/(auth)/auth/signup/components/SignupForm"; -import { getIsMultiOrgEnabled } from "@/modules/ee/license-check/lib/utils"; -import { notFound } from "next/navigation"; -import { - AZURE_OAUTH_ENABLED, - EMAIL_AUTH_ENABLED, - EMAIL_VERIFICATION_DISABLED, - GITHUB_OAUTH_ENABLED, - GOOGLE_OAUTH_ENABLED, - OIDC_DISPLAY_NAME, - OIDC_OAUTH_ENABLED, - PRIVACY_URL, - SIGNUP_ENABLED, - TERMS_URL, - WEBAPP_URL, -} from "@formbricks/lib/constants"; -import { findMatchingLocale } from "@formbricks/lib/utils/locale"; +import { SignupPage } from "@/modules/auth/signup/page"; -const Page = async (props: { searchParams: Promise<{ [key: string]: string | string[] | undefined }> }) => { - const searchParams = await props.searchParams; - const inviteToken = searchParams["inviteToken"] ?? null; - const isMultOrgEnabled = await getIsMultiOrgEnabled(); - const locale = await findMatchingLocale(); - if (!inviteToken && (!SIGNUP_ENABLED || !isMultOrgEnabled)) { - notFound(); - } - - return ( -
-
- -
-
- - - -
-
- ); -}; - -export default Page; +export default SignupPage; diff --git a/apps/web/app/(auth)/auth/verification-requested/page.tsx b/apps/web/app/(auth)/auth/verification-requested/page.tsx index 3aefdbbcf2..cadf5149a5 100644 --- a/apps/web/app/(auth)/auth/verification-requested/page.tsx +++ b/apps/web/app/(auth)/auth/verification-requested/page.tsx @@ -1,46 +1,3 @@ -import { FormWrapper } from "@/app/(auth)/auth/components/FormWrapper"; -import { RequestVerificationEmail } from "@/app/(auth)/auth/verification-requested/components/RequestVerificationEmail"; -import { getTranslations } from "next-intl/server"; -import { z } from "zod"; -import { getEmailFromEmailToken } from "@formbricks/lib/jwt"; +import { VerificationRequestedPage } from "@/modules/auth/verification-requested/page"; -const VerificationPageSchema = z.string().email(); - -const Page = async (props) => { - const searchParams = await props.searchParams; - const t = await getTranslations(); - const email = getEmailFromEmailToken(searchParams.token); - try { - const parsedEmail = VerificationPageSchema.parse(email).toLowerCase(); - return ( - - <> -

- {t("auth.verification-requested.please_confirm_your_email_address")} -

-

- {t.rich("auth.verification-requested.we_sent_an_email_to", { - email: () => {email}, - })} - {t("auth.verification-requested.please_click_the_link_in_the_email_to_activate_your_account")} -

-
-

- {t("auth.verification-requested.you_didnt_receive_an_email_or_your_link_expired")} -

-
- -
- -
- ); - } catch (error) { - return ( - -

{t("auth.verification-requested.invalid_email_address")}

-
- ); - } -}; - -export default Page; +export default VerificationRequestedPage; diff --git a/apps/web/app/(auth)/auth/verify/page.tsx b/apps/web/app/(auth)/auth/verify/page.tsx index acc12d5f79..9a2106bdc8 100644 --- a/apps/web/app/(auth)/auth/verify/page.tsx +++ b/apps/web/app/(auth)/auth/verify/page.tsx @@ -1,18 +1,3 @@ -import { FormWrapper } from "@/app/(auth)/auth/components/FormWrapper"; -import { SignIn } from "@/app/(auth)/auth/verify/components/SignIn"; -import { getTranslations } from "next-intl/server"; +import { VerifyPage } from "@/modules/auth/verify/page"; -const Page = async (props) => { - const searchParams = await props.searchParams; - const t = await getTranslations(); - return searchParams && searchParams.token ? ( - -

{t("auth.verify.verifying")}

- -
- ) : ( -

{t("auth.verify.no_token_provided")}

- ); -}; - -export default Page; +export default VerifyPage; diff --git a/apps/web/app/(auth)/invite/page.tsx b/apps/web/app/(auth)/invite/page.tsx index f7a20cec62..1b81195cf7 100644 --- a/apps/web/app/(auth)/invite/page.tsx +++ b/apps/web/app/(auth)/invite/page.tsx @@ -1,8 +1,8 @@ +import { authOptions } from "@/modules/auth/lib/authOptions"; import { sendInviteAcceptedEmail } from "@/modules/email"; import { Button } from "@/modules/ui/components/button"; import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; -import { authOptions } from "@formbricks/lib/authOptions"; import { DEFAULT_LOCALE, WEBAPP_URL } from "@formbricks/lib/constants"; import { deleteInvite, getInvite } from "@formbricks/lib/invite/service"; import { verifyInviteToken } from "@formbricks/lib/jwt"; diff --git a/apps/web/app/(redirects)/organizations/[organizationId]/route.ts b/apps/web/app/(redirects)/organizations/[organizationId]/route.ts index f196b40b8c..d53215e76f 100644 --- a/apps/web/app/(redirects)/organizations/[organizationId]/route.ts +++ b/apps/web/app/(redirects)/organizations/[organizationId]/route.ts @@ -1,8 +1,8 @@ import { hasOrganizationAccess } from "@/app/lib/api/apiHelper"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getServerSession } from "next-auth"; import { redirect } from "next/navigation"; import { notFound } from "next/navigation"; -import { authOptions } from "@formbricks/lib/authOptions"; import { getEnvironments } from "@formbricks/lib/environment/service"; import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; import { getAccessFlags } from "@formbricks/lib/membership/utils"; diff --git a/apps/web/app/(redirects)/products/[productId]/route.ts b/apps/web/app/(redirects)/products/[productId]/route.ts index 28277b67d3..761c40fe71 100644 --- a/apps/web/app/(redirects)/products/[productId]/route.ts +++ b/apps/web/app/(redirects)/products/[productId]/route.ts @@ -1,7 +1,7 @@ import { hasOrganizationAccess } from "@/app/lib/api/apiHelper"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getServerSession } from "next-auth"; import { notFound, redirect } from "next/navigation"; -import { authOptions } from "@formbricks/lib/authOptions"; import { getEnvironments } from "@formbricks/lib/environment/service"; import { getProduct } from "@formbricks/lib/product/service"; import { AuthenticationError, AuthorizationError } from "@formbricks/types/errors"; diff --git a/apps/web/app/api/(internal)/csv-conversion/route.ts b/apps/web/app/api/(internal)/csv-conversion/route.ts index 2f1fb4bd36..5e668022c9 100755 --- a/apps/web/app/api/(internal)/csv-conversion/route.ts +++ b/apps/web/app/api/(internal)/csv-conversion/route.ts @@ -1,8 +1,8 @@ import { responses } from "@/app/lib/api/response"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { AsyncParser } from "@json2csv/node"; import { getServerSession } from "next-auth"; import { NextRequest } from "next/server"; -import { authOptions } from "@formbricks/lib/authOptions"; export const POST = async (request: NextRequest) => { const session = await getServerSession(authOptions); diff --git a/apps/web/app/api/(internal)/excel-conversion/route.ts b/apps/web/app/api/(internal)/excel-conversion/route.ts index 550937caf7..76c092303a 100755 --- a/apps/web/app/api/(internal)/excel-conversion/route.ts +++ b/apps/web/app/api/(internal)/excel-conversion/route.ts @@ -1,8 +1,8 @@ import { responses } from "@/app/lib/api/response"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getServerSession } from "next-auth"; import { NextRequest } from "next/server"; import * as xlsx from "xlsx"; -import { authOptions } from "@formbricks/lib/authOptions"; export const POST = async (request: NextRequest) => { const session = await getServerSession(authOptions); diff --git a/apps/web/app/api/auth/[...nextauth]/route.ts b/apps/web/app/api/auth/[...nextauth]/route.ts index 97c039fc37..80275570a1 100644 --- a/apps/web/app/api/auth/[...nextauth]/route.ts +++ b/apps/web/app/api/auth/[...nextauth]/route.ts @@ -1,5 +1,5 @@ +import { authOptions } from "@/modules/auth/lib/authOptions"; import NextAuth from "next-auth"; -import { authOptions } from "@formbricks/lib/authOptions"; export const fetchCache = "force-no-store"; diff --git a/apps/web/app/api/google-sheet/route.ts b/apps/web/app/api/google-sheet/route.ts index 3841631e1d..72b6310c1f 100644 --- a/apps/web/app/api/google-sheet/route.ts +++ b/apps/web/app/api/google-sheet/route.ts @@ -1,8 +1,8 @@ import { responses } from "@/app/lib/api/response"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { google } from "googleapis"; import { getServerSession } from "next-auth"; import { NextRequest } from "next/server"; -import { authOptions } from "@formbricks/lib/authOptions"; import { GOOGLE_SHEETS_CLIENT_ID, GOOGLE_SHEETS_CLIENT_SECRET, diff --git a/apps/web/app/api/v1/integrations/airtable/callback/route.ts b/apps/web/app/api/v1/integrations/airtable/callback/route.ts index 432ebccf49..b2a6b51cb1 100644 --- a/apps/web/app/api/v1/integrations/airtable/callback/route.ts +++ b/apps/web/app/api/v1/integrations/airtable/callback/route.ts @@ -1,9 +1,9 @@ import { responses } from "@/app/lib/api/response"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getServerSession } from "next-auth"; import { NextRequest } from "next/server"; import * as z from "zod"; import { fetchAirtableAuthToken } from "@formbricks/lib/airtable/service"; -import { authOptions } from "@formbricks/lib/authOptions"; import { AIRTABLE_CLIENT_ID, WEBAPP_URL } from "@formbricks/lib/constants"; import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; import { createOrUpdateIntegration } from "@formbricks/lib/integration/service"; diff --git a/apps/web/app/api/v1/integrations/airtable/route.ts b/apps/web/app/api/v1/integrations/airtable/route.ts index 6c08eb9e7f..3045ecd087 100644 --- a/apps/web/app/api/v1/integrations/airtable/route.ts +++ b/apps/web/app/api/v1/integrations/airtable/route.ts @@ -1,8 +1,8 @@ import { responses } from "@/app/lib/api/response"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import crypto from "crypto"; import { getServerSession } from "next-auth"; import { NextRequest } from "next/server"; -import { authOptions } from "@formbricks/lib/authOptions"; import { AIRTABLE_CLIENT_ID, WEBAPP_URL } from "@formbricks/lib/constants"; import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; diff --git a/apps/web/app/api/v1/integrations/airtable/tables/route.ts b/apps/web/app/api/v1/integrations/airtable/tables/route.ts index f72eb24ac8..08056b4f0f 100644 --- a/apps/web/app/api/v1/integrations/airtable/tables/route.ts +++ b/apps/web/app/api/v1/integrations/airtable/tables/route.ts @@ -1,9 +1,9 @@ import { responses } from "@/app/lib/api/response"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getServerSession } from "next-auth"; import { NextRequest } from "next/server"; import * as z from "zod"; import { getTables } from "@formbricks/lib/airtable/service"; -import { authOptions } from "@formbricks/lib/authOptions"; import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; import { getIntegrationByType } from "@formbricks/lib/integration/service"; import { TIntegrationAirtable } from "@formbricks/types/integration/airtable"; diff --git a/apps/web/app/api/v1/integrations/notion/route.ts b/apps/web/app/api/v1/integrations/notion/route.ts index 622290b305..d707e583d4 100644 --- a/apps/web/app/api/v1/integrations/notion/route.ts +++ b/apps/web/app/api/v1/integrations/notion/route.ts @@ -1,7 +1,7 @@ import { responses } from "@/app/lib/api/response"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getServerSession } from "next-auth"; import { NextRequest } from "next/server"; -import { authOptions } from "@formbricks/lib/authOptions"; import { NOTION_AUTH_URL, NOTION_OAUTH_CLIENT_ID, diff --git a/apps/web/app/api/v1/integrations/slack/route.ts b/apps/web/app/api/v1/integrations/slack/route.ts index e8c9583f00..46fa8fb339 100644 --- a/apps/web/app/api/v1/integrations/slack/route.ts +++ b/apps/web/app/api/v1/integrations/slack/route.ts @@ -1,7 +1,7 @@ import { responses } from "@/app/lib/api/response"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getServerSession } from "next-auth"; import { NextRequest } from "next/server"; -import { authOptions } from "@formbricks/lib/authOptions"; import { SLACK_AUTH_URL, SLACK_CLIENT_ID, SLACK_CLIENT_SECRET } from "@formbricks/lib/constants"; import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; diff --git a/apps/web/app/api/v1/management/storage/local/route.ts b/apps/web/app/api/v1/management/storage/local/route.ts index 68de8350ab..f4e8c8f00d 100644 --- a/apps/web/app/api/v1/management/storage/local/route.ts +++ b/apps/web/app/api/v1/management/storage/local/route.ts @@ -2,10 +2,10 @@ // body -> should be a valid file object (buffer) // method -> PUT (to be the same as the signedUrl method) import { responses } from "@/app/lib/api/response"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getServerSession } from "next-auth"; import { headers } from "next/headers"; import { NextRequest } from "next/server"; -import { authOptions } from "@formbricks/lib/authOptions"; import { ENCRYPTION_KEY, UPLOADS_DIR } from "@formbricks/lib/constants"; import { validateLocalSignedUrl } from "@formbricks/lib/crypto"; import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; diff --git a/apps/web/app/api/v1/management/storage/route.ts b/apps/web/app/api/v1/management/storage/route.ts index 1623b67c8f..1f0ecdc86b 100644 --- a/apps/web/app/api/v1/management/storage/route.ts +++ b/apps/web/app/api/v1/management/storage/route.ts @@ -1,7 +1,7 @@ import { responses } from "@/app/lib/api/response"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getServerSession } from "next-auth"; import { NextRequest } from "next/server"; -import { authOptions } from "@formbricks/lib/authOptions"; import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; import { getSignedUrlForPublicFile } from "./lib/getSignedUrl"; diff --git a/apps/web/app/api/v1/users/forgot-password/route.ts b/apps/web/app/api/v1/users/forgot-password/route.ts deleted file mode 100644 index 749891a12a..0000000000 --- a/apps/web/app/api/v1/users/forgot-password/route.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { sendForgotPasswordEmail } from "@/modules/email"; -import { prisma } from "@formbricks/database"; - -export const POST = async (request: Request) => { - const { email } = await request.json(); - - try { - const foundUser = await prisma.user.findUnique({ - where: { - email: email.toLowerCase(), - }, - }); - - if (foundUser) { - await sendForgotPasswordEmail(foundUser, foundUser.locale); - } - - return Response.json({}); - } catch (e) { - return Response.json( - { - error: e.message, - errorCode: e.code, - }, - { status: 500 } - ); - } -}; diff --git a/apps/web/app/api/v1/users/me/route.ts b/apps/web/app/api/v1/users/me/route.ts deleted file mode 100644 index d1ffd1394c..0000000000 --- a/apps/web/app/api/v1/users/me/route.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { getSessionUser } from "@/app/lib/api/apiHelper"; -import { OrganizationRole } from "@prisma/client"; -import { NextRequest } from "next/server"; -import { prisma } from "@formbricks/database"; -import { TOrganizationRole } from "@formbricks/types/memberships"; - -interface Membership { - role: TOrganizationRole; - userId: string; -} - -export const GET = async () => { - const sessionUser = await getSessionUser(); - if (!sessionUser) { - return new Response("Not authenticated", { - status: 401, - }); - } - - const user = await prisma.user.findUnique({ - where: { - id: sessionUser.id, - }, - }); - - return Response.json(user); -}; - -export const PUT = async (request: NextRequest) => { - const sessionUser = await getSessionUser(); - if (!sessionUser) { - return new Response("Not authenticated", { - status: 401, - }); - } - const body = await request.json(); - - const user = await prisma.user.update({ - where: { - id: sessionUser.id, - }, - data: body, - }); - - return Response.json(user); -}; - -const deleteUser = async (userId: string) => { - await prisma.user.delete({ - where: { - id: userId, - }, - }); -}; - -const updateUserMembership = async (organizationId: string, userId: string, role: OrganizationRole) => { - await prisma.membership.update({ - where: { - userId_organizationId: { - userId, - organizationId, - }, - }, - data: { - role, - }, - }); -}; - -const getManagerMemberships = (memberships: Membership[]) => - memberships.filter((membership) => membership.role === OrganizationRole.manager); - -const getOwnerMemberships = (memberships: Membership[]) => - memberships.filter((membership) => membership.role === OrganizationRole.owner); - -const deleteOrganization = async (organizationId: string) => { - await prisma.organization.delete({ - where: { - id: organizationId, - }, - }); -}; - -export const DELETE = async () => { - try { - const currentUser = await getSessionUser(); - - if (!currentUser) { - return new Response("Not authenticated", { - status: 401, - }); - } - - const currentUserMemberships = await prisma.membership.findMany({ - where: { - userId: currentUser.id, - }, - include: { - organization: { - select: { - id: true, - name: true, - memberships: { - select: { - userId: true, - role: true, - }, - }, - }, - }, - }, - }); - - for (const currentUserMembership of currentUserMemberships) { - const organizationMemberships = currentUserMembership.organization.memberships; - const role = currentUserMembership.role; - const organizationId = currentUserMembership.organizationId; - - const organizationManagerMemberships = getManagerMemberships(organizationMemberships); - const organizationOwnerMemberships = getOwnerMemberships(organizationMemberships); - const organizationHasAtLeastOneManager = organizationManagerMemberships.length > 0; - const organizationHasOnlyOneMember = organizationMemberships.length === 1; - const organizationHasMoreThanOneOwner = organizationOwnerMemberships.length > 1; - const currentUserIsOrganizationOwner = role === OrganizationRole.owner; - - if (organizationHasOnlyOneMember) { - await deleteOrganization(organizationId); - } else if ( - currentUserIsOrganizationOwner && - organizationHasAtLeastOneManager && - !organizationHasMoreThanOneOwner - ) { - const firstManager = organizationManagerMemberships[0]; - await updateUserMembership(organizationId, firstManager.userId, OrganizationRole.owner); - } else if (currentUserIsOrganizationOwner) { - await deleteOrganization(organizationId); - } - } - - await deleteUser(currentUser.id); - - return Response.json({ deletedUser: currentUser }, { status: 200 }); - } catch (error) { - return Response.json({ message: error.message }, { status: 500 }); - } -}; diff --git a/apps/web/app/api/v1/users/reset-password/route.ts b/apps/web/app/api/v1/users/reset-password/route.ts deleted file mode 100644 index 7100ab6627..0000000000 --- a/apps/web/app/api/v1/users/reset-password/route.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { sendPasswordResetNotifyEmail } from "@/modules/email"; -import { prisma } from "@formbricks/database"; -import { verifyToken } from "@formbricks/lib/jwt"; - -export const POST = async (request: Request) => { - const { token, hashedPassword } = await request.json(); - - try { - const { id } = await verifyToken(token); - const user = await prisma.user.findUnique({ - where: { - id, - }, - select: { - id: true, - email: true, - locale: true, - }, - }); - if (!user) { - return Response.json({ error: "Invalid token provided or no longer valid" }, { status: 409 }); - } - await prisma.user.update({ - where: { id: user.id }, - data: { password: hashedPassword }, - }); - await sendPasswordResetNotifyEmail(user); - return Response.json({}); - } catch (e) { - return Response.json( - { - error: e.message, - errorCode: e.code, - }, - { status: 500 } - ); - } -}; diff --git a/apps/web/app/api/v1/users/route.ts b/apps/web/app/api/v1/users/route.ts deleted file mode 100644 index f8166888fb..0000000000 --- a/apps/web/app/api/v1/users/route.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { getIsMultiOrgEnabled } from "@/modules/ee/license-check/lib/utils"; -import { sendInviteAcceptedEmail, sendVerificationEmail } from "@/modules/email"; -import { prisma } from "@formbricks/database"; -import { - DEFAULT_ORGANIZATION_ID, - DEFAULT_ORGANIZATION_ROLE, - EMAIL_AUTH_ENABLED, - EMAIL_VERIFICATION_DISABLED, - INVITE_DISABLED, - SIGNUP_ENABLED, -} from "@formbricks/lib/constants"; -import { getIsFreshInstance } from "@formbricks/lib/instance/service"; -import { deleteInvite } from "@formbricks/lib/invite/service"; -import { verifyInviteToken } from "@formbricks/lib/jwt"; -import { createMembership } from "@formbricks/lib/membership/service"; -import { createOrganization, getOrganization } from "@formbricks/lib/organization/service"; -import { createUser, updateUser } from "@formbricks/lib/user/service"; - -export const POST = async (request: Request) => { - let { inviteToken, ...user } = await request.json(); - const isMultiOrgEnabled = await getIsMultiOrgEnabled(); - const isFreshInstance = await getIsFreshInstance(); - if ( - !isFreshInstance && - (!EMAIL_AUTH_ENABLED || inviteToken ? INVITE_DISABLED : !SIGNUP_ENABLED || !isMultiOrgEnabled) - ) { - return Response.json({ error: "Signup disabled" }, { status: 403 }); - } - - let inviteId; - - try { - let invite; - let isInviteValid = false; - - if (inviteToken) { - let inviteTokenData = await verifyInviteToken(inviteToken); - inviteId = inviteTokenData?.inviteId; - - invite = await prisma.invite.findUnique({ - where: { id: inviteId }, - include: { - creator: true, - }, - }); - - if (!invite) { - return Response.json({ error: "Invalid invite ID" }, { status: 400 }); - } - isInviteValid = true; - } - - user = { - ...user, - ...{ email: user.email.toLowerCase() }, - }; - - // create the user - user = await createUser(user); - - // User is invited to organization - if (isInviteValid) { - // assign user to existing organization - await createMembership(invite.organizationId, user.id, { - accepted: true, - role: invite.role, - }); - - await updateUser(user.id, { - notificationSettings: { - alert: {}, - weeklySummary: {}, - unsubscribedOrganizationIds: [invite.organizationId], - }, - }); - - if (!EMAIL_VERIFICATION_DISABLED) { - await sendVerificationEmail(user); - } - - await sendInviteAcceptedEmail(invite.creator.name, user.name, invite.creator.email, user.locale); - await deleteInvite(inviteId); - - return Response.json(user); - } - - // User signs up without invite - // Default organization assignment is enabled - if (DEFAULT_ORGANIZATION_ID && DEFAULT_ORGANIZATION_ID.length > 0) { - // check if organization exists - let organization = await getOrganization(DEFAULT_ORGANIZATION_ID); - let isNewOrganization = false; - if (!organization) { - // create organization with id from env - organization = await createOrganization({ - id: DEFAULT_ORGANIZATION_ID, - name: user.name + "'s Organization", - }); - isNewOrganization = true; - } - const role = isNewOrganization ? "owner" : DEFAULT_ORGANIZATION_ROLE || "owner"; - await createMembership(organization.id, user.id, { role: role, accepted: true }); - const updatedNotificationSettings = { - ...user.notificationSettings, - unsubscribedOrganizationIds: Array.from( - new Set([...(user.notificationSettings?.unsubscribedOrganizationIds || []), organization.id]) - ), - }; - - await updateUser(user.id, { - notificationSettings: updatedNotificationSettings, - }); - } - // Without default organization assignment - else { - if (isMultiOrgEnabled) { - const organization = await createOrganization({ name: user.name + "'s Organization" }); - await createMembership(organization.id, user.id, { role: "owner", accepted: true }); - - const updatedNotificationSettings = { - ...user.notificationSettings, - alert: { - ...user.notificationSettings?.alert, - }, - weeklySummary: { - ...user.notificationSettings?.weeklySummary, - }, - unsubscribedOrganizationIds: Array.from( - new Set([...(user.notificationSettings?.unsubscribedOrganizationIds || []), organization.id]) - ), - }; - - await updateUser(user.id, { - notificationSettings: updatedNotificationSettings, - }); - } - } - // send verification email amd return user - if (!EMAIL_VERIFICATION_DISABLED) { - await sendVerificationEmail(user); - } - - return Response.json(user); - } catch (e) { - if (e.message === "User with this email already exists") { - return Response.json( - { - error: "user with this email address already exists", - errorCode: e.code, - }, - { status: 409 } - ); - } else { - return Response.json( - { - error: e.message, - errorCode: e.code, - }, - { status: 500 } - ); - } - } -}; diff --git a/apps/web/app/api/v1/users/verification-email/route.ts b/apps/web/app/api/v1/users/verification-email/route.ts deleted file mode 100644 index 31653a2a73..0000000000 --- a/apps/web/app/api/v1/users/verification-email/route.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { sendVerificationEmail } from "@/modules/email"; -import { prisma } from "@formbricks/database"; - -export const POST = async (request: Request) => { - const { email } = await request.json(); - // check for user in DB - try { - const user = await prisma.user.findUnique({ - where: { email }, - }); - if (!user) { - return Response.json({ error: "No user with this email address found" }, { status: 404 }); - } - if (user.emailVerified) { - return Response.json({ error: "Email address has already been verified" }, { status: 400 }); - } - await sendVerificationEmail(user); - return Response.json(user); - } catch (e) { - return Response.json( - { - error: e.message, - errorCode: e.code, - }, - { status: 500 } - ); - } -}; diff --git a/apps/web/app/lib/api/apiHelper.ts b/apps/web/app/lib/api/apiHelper.ts index 0421f0f8e8..3c43026d8b 100644 --- a/apps/web/app/lib/api/apiHelper.ts +++ b/apps/web/app/lib/api/apiHelper.ts @@ -1,9 +1,9 @@ +import { authOptions } from "@/modules/auth/lib/authOptions"; import { createHash } from "crypto"; import { NextApiRequest, NextApiResponse } from "next"; import type { Session } from "next-auth"; import { getServerSession } from "next-auth"; import { prisma } from "@formbricks/database"; -import { authOptions } from "@formbricks/lib/authOptions"; import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; export const hashApiKey = (key: string): string => createHash("sha256").update(key).digest("hex"); diff --git a/apps/web/app/middleware/bucket.ts b/apps/web/app/middleware/bucket.ts index 4d6aa91af0..42362c377f 100644 --- a/apps/web/app/middleware/bucket.ts +++ b/apps/web/app/middleware/bucket.ts @@ -3,32 +3,27 @@ import { CLIENT_SIDE_API_RATE_LIMIT, FORGET_PASSWORD_RATE_LIMIT, LOGIN_RATE_LIMIT, - RESET_PASSWORD_RATE_LIMIT, SHARE_RATE_LIMIT, SIGNUP_RATE_LIMIT, SYNC_USER_IDENTIFICATION_RATE_LIMIT, VERIFY_EMAIL_RATE_LIMIT, } from "@formbricks/lib/constants"; -export const signUpLimiter = rateLimit({ +export const loginLimiter = rateLimit({ + interval: LOGIN_RATE_LIMIT.interval, + allowedPerInterval: LOGIN_RATE_LIMIT.allowedPerInterval, +}); +export const signupLimiter = rateLimit({ interval: SIGNUP_RATE_LIMIT.interval, allowedPerInterval: SIGNUP_RATE_LIMIT.allowedPerInterval, }); -export const forgetPasswordLimiter = rateLimit({ - interval: FORGET_PASSWORD_RATE_LIMIT.interval, - allowedPerInterval: FORGET_PASSWORD_RATE_LIMIT.allowedPerInterval, -}); -export const resetPasswordLimiter = rateLimit({ - interval: RESET_PASSWORD_RATE_LIMIT.interval, - allowedPerInterval: RESET_PASSWORD_RATE_LIMIT.allowedPerInterval, -}); export const verifyEmailLimiter = rateLimit({ interval: VERIFY_EMAIL_RATE_LIMIT.interval, allowedPerInterval: VERIFY_EMAIL_RATE_LIMIT.allowedPerInterval, }); -export const loginLimiter = rateLimit({ - interval: LOGIN_RATE_LIMIT.interval, - allowedPerInterval: LOGIN_RATE_LIMIT.allowedPerInterval, +export const forgotPasswordLimiter = rateLimit({ + interval: FORGET_PASSWORD_RATE_LIMIT.interval, + allowedPerInterval: FORGET_PASSWORD_RATE_LIMIT.allowedPerInterval, }); export const clientSideApiEndpointsLimiter = rateLimit({ interval: CLIENT_SIDE_API_RATE_LIMIT.interval, diff --git a/apps/web/app/middleware/endpointValidator.ts b/apps/web/app/middleware/endpointValidator.ts index 6f7917f6fd..fea1f20f10 100644 --- a/apps/web/app/middleware/endpointValidator.ts +++ b/apps/web/app/middleware/endpointValidator.ts @@ -1,14 +1,12 @@ -export const loginRoute = (url: string) => url === "/api/auth/callback/credentials"; +export const isLoginRoute = (url: string) => url === "/api/auth/callback/credentials"; -export const signupRoute = (url: string) => url === "/api/v1/users"; +export const isSignupRoute = (url: string) => url === "/auth/signup"; -export const resetPasswordRoute = (url: string) => url === "/api/v1/users/reset-password"; +export const isVerifyEmailRoute = (url: string) => url === "/auth/verify-email"; -export const forgetPasswordRoute = (url: string) => url === "/api/v1/users/forgot-password"; +export const isForgotPasswordRoute = (url: string) => url === "/auth/forgot-password"; -export const verifyEmailRoute = (url: string) => url === "/api/v1/users/verification-email"; - -export const clientSideApiRoute = (url: string): boolean => { +export const isClientSideApiRoute = (url: string): boolean => { if (url.includes("/api/packages/")) return true; if (url.includes("/api/v1/js/actions")) return true; if (url.includes("/api/v1/client/storage")) return true; @@ -16,7 +14,7 @@ export const clientSideApiRoute = (url: string): boolean => { return regex.test(url); }; -export const shareUrlRoute = (url: string): boolean => { +export const isShareUrlRoute = (url: string): boolean => { const regex = /\/share\/[A-Za-z0-9]+\/(summary|responses)/; return regex.test(url); }; @@ -27,6 +25,7 @@ export const isAuthProtectedRoute = (url: string): boolean => { return protectedRoutes.some((route) => url.startsWith(route)); }; + export const isSyncWithUserIdentificationEndpoint = ( url: string ): { environmentId: string; userId: string } | false => { diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 08e3390f16..91bed7907d 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -1,10 +1,10 @@ import ClientEnvironmentRedirect from "@/app/ClientEnvironmentRedirect"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { ClientLogout } from "@/modules/ui/components/client-logout"; import type { Session } from "next-auth"; import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; import { redirect } from "next/navigation"; -import { authOptions } from "@formbricks/lib/authOptions"; import { getFirstEnvironmentIdByUserId } from "@formbricks/lib/environment/service"; import { getIsFreshInstance } from "@formbricks/lib/instance/service"; import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; diff --git a/apps/web/app/setup/(fresh-instance)/layout.tsx b/apps/web/app/setup/(fresh-instance)/layout.tsx index 287f242a75..ef578e92dd 100644 --- a/apps/web/app/setup/(fresh-instance)/layout.tsx +++ b/apps/web/app/setup/(fresh-instance)/layout.tsx @@ -1,6 +1,6 @@ +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getServerSession } from "next-auth"; import { notFound } from "next/navigation"; -import { authOptions } from "@formbricks/lib/authOptions"; import { getIsFreshInstance } from "@formbricks/lib/instance/service"; const FreshInstanceLayout = async ({ children }: { children: React.ReactNode }) => { diff --git a/apps/web/app/setup/(fresh-instance)/signup/page.tsx b/apps/web/app/setup/(fresh-instance)/signup/page.tsx index 246fd6943d..91cec25261 100644 --- a/apps/web/app/setup/(fresh-instance)/signup/page.tsx +++ b/apps/web/app/setup/(fresh-instance)/signup/page.tsx @@ -1,14 +1,20 @@ -import { SignupOptions } from "@/modules/auth/components/SignupOptions"; +import { SignupForm } from "@/modules/auth/signup/components/signup-form"; +import { getIsSSOEnabled } from "@/modules/ee/license-check/lib/utils"; import { Metadata } from "next"; import { getTranslations } from "next-intl/server"; import { AZURE_OAUTH_ENABLED, + DEFAULT_ORGANIZATION_ID, + DEFAULT_ORGANIZATION_ROLE, EMAIL_AUTH_ENABLED, EMAIL_VERIFICATION_DISABLED, GITHUB_OAUTH_ENABLED, GOOGLE_OAUTH_ENABLED, OIDC_DISPLAY_NAME, OIDC_OAUTH_ENABLED, + PRIVACY_URL, + TERMS_URL, + WEBAPP_URL, } from "@formbricks/lib/constants"; import { findMatchingLocale } from "@formbricks/lib/utils/locale"; @@ -19,24 +25,28 @@ export const metadata: Metadata = { const Page = async () => { const locale = await findMatchingLocale(); + const isSSOEnabled = await getIsSSOEnabled(); const t = await getTranslations(); return (

{t("setup.signup.create_administrator")}

{t("setup.signup.this_user_has_all_the_power")}


-
); diff --git a/apps/web/app/setup/organization/[organizationId]/invite/actions.ts b/apps/web/app/setup/organization/[organizationId]/invite/actions.ts index 09c8132cfe..dbc4db7f69 100644 --- a/apps/web/app/setup/organization/[organizationId]/invite/actions.ts +++ b/apps/web/app/setup/organization/[organizationId]/invite/actions.ts @@ -42,6 +42,7 @@ export const inviteOrganizationMemberAction = authenticatedActionClient name: "", role: "manager", }, + currentUserId: ctx.user.id, }); if (invite) { diff --git a/apps/web/app/setup/organization/[organizationId]/invite/page.tsx b/apps/web/app/setup/organization/[organizationId]/invite/page.tsx index 89e581a973..16baa17d25 100644 --- a/apps/web/app/setup/organization/[organizationId]/invite/page.tsx +++ b/apps/web/app/setup/organization/[organizationId]/invite/page.tsx @@ -1,9 +1,9 @@ import { InviteMembers } from "@/app/setup/organization/[organizationId]/invite/components/InviteMembers"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { Metadata } from "next"; import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; import { notFound } from "next/navigation"; -import { authOptions } from "@formbricks/lib/authOptions"; import { SMTP_HOST, SMTP_PASSWORD, SMTP_PORT, SMTP_USER } from "@formbricks/lib/constants"; import { verifyUserRoleAccess } from "@formbricks/lib/organization/auth"; import { AuthenticationError } from "@formbricks/types/errors"; diff --git a/apps/web/app/setup/organization/create/page.tsx b/apps/web/app/setup/organization/create/page.tsx index 4294e2958f..e5f43fd901 100644 --- a/apps/web/app/setup/organization/create/page.tsx +++ b/apps/web/app/setup/organization/create/page.tsx @@ -1,11 +1,11 @@ import { RemovedFromOrganization } from "@/app/setup/organization/create/components/RemovedFromOrganization"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getIsMultiOrgEnabled } from "@/modules/ee/license-check/lib/utils"; import { ClientLogout } from "@/modules/ui/components/client-logout"; import { Metadata } from "next"; import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; import { notFound } from "next/navigation"; -import { authOptions } from "@formbricks/lib/authOptions"; import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; import { gethasNoOrganizations } from "@formbricks/lib/instance/service"; import { getOrganizationsByUserId } from "@formbricks/lib/organization/service"; diff --git a/apps/web/app/storage/[environmentId]/[accessType]/[fileName]/route.ts b/apps/web/app/storage/[environmentId]/[accessType]/[fileName]/route.ts index ccc32fa28c..540fc33780 100644 --- a/apps/web/app/storage/[environmentId]/[accessType]/[fileName]/route.ts +++ b/apps/web/app/storage/[environmentId]/[accessType]/[fileName]/route.ts @@ -2,9 +2,9 @@ import { authenticateRequest } from "@/app/api/v1/auth"; import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; import { handleDeleteFile } from "@/app/storage/[environmentId]/[accessType]/[fileName]/lib/deleteFile"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getServerSession } from "next-auth"; import { NextRequest } from "next/server"; -import { authOptions } from "@formbricks/lib/authOptions"; import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; import { ZStorageRetrievalParams } from "@formbricks/types/storage"; import { getFile } from "./lib/getFile"; diff --git a/apps/web/i18n/request.ts b/apps/web/i18n/request.ts index 47d67464f6..c1ca7d78ec 100644 --- a/apps/web/i18n/request.ts +++ b/apps/web/i18n/request.ts @@ -1,6 +1,6 @@ +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getServerSession } from "next-auth"; import { getRequestConfig } from "next-intl/server"; -import { authOptions } from "@formbricks/lib/authOptions"; import { DEFAULT_LOCALE } from "@formbricks/lib/constants"; import { getUserLocale } from "@formbricks/lib/user/service"; import { findMatchingLocale } from "@formbricks/lib/utils/locale"; diff --git a/apps/web/lib/utils/action-client.ts b/apps/web/lib/utils/action-client.ts index f6435baa3d..6497b41230 100644 --- a/apps/web/lib/utils/action-client.ts +++ b/apps/web/lib/utils/action-client.ts @@ -1,6 +1,6 @@ +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getServerSession } from "next-auth"; import { DEFAULT_SERVER_ERROR_MESSAGE, createSafeActionClient } from "next-safe-action"; -import { authOptions } from "@formbricks/lib/authOptions"; import { getUser } from "@formbricks/lib/user/service"; import { AuthenticationError, @@ -18,6 +18,7 @@ export const actionClient = createSafeActionClient({ e instanceof AuthorizationError || e instanceof InvalidInputError || e instanceof UnknownError || + e instanceof AuthenticationError || e instanceof OperationNotAllowedError ) { return e.message; diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts index 10c356b69f..0549b346f4 100644 --- a/apps/web/middleware.ts +++ b/apps/web/middleware.ts @@ -1,23 +1,21 @@ import { clientSideApiEndpointsLimiter, - forgetPasswordLimiter, + forgotPasswordLimiter, loginLimiter, - resetPasswordLimiter, shareUrlLimiter, - signUpLimiter, + signupLimiter, syncUserIdentificationLimiter, verifyEmailLimiter, } from "@/app/middleware/bucket"; import { - clientSideApiRoute, - forgetPasswordRoute, isAuthProtectedRoute, + isClientSideApiRoute, + isForgotPasswordRoute, + isLoginRoute, + isShareUrlRoute, + isSignupRoute, isSyncWithUserIdentificationEndpoint, - loginRoute, - resetPasswordRoute, - shareUrlRoute, - signupRoute, - verifyEmailRoute, + isVerifyEmailRoute, } from "@/app/middleware/endpointValidator"; import { ipAddress } from "@vercel/functions"; import { getToken } from "next-auth/jwt"; @@ -53,17 +51,15 @@ export const middleware = async (request: NextRequest) => { if (ip) { try { - if (loginRoute(request.nextUrl.pathname)) { + if (isLoginRoute(request.nextUrl.pathname)) { await loginLimiter(`login-${ip}`); - } else if (signupRoute(request.nextUrl.pathname)) { - await signUpLimiter(`signup-${ip}`); - } else if (forgetPasswordRoute(request.nextUrl.pathname)) { - await forgetPasswordLimiter(`forget-password-${ip}`); - } else if (verifyEmailRoute(request.nextUrl.pathname)) { + } else if (isSignupRoute(request.nextUrl.pathname)) { + await signupLimiter(`signup-${ip}`); + } else if (isVerifyEmailRoute(request.nextUrl.pathname)) { await verifyEmailLimiter(`verify-email-${ip}`); - } else if (resetPasswordRoute(request.nextUrl.pathname)) { - await resetPasswordLimiter(`reset-password-${ip}`); - } else if (clientSideApiRoute(request.nextUrl.pathname)) { + } else if (isForgotPasswordRoute(request.nextUrl.pathname)) { + await forgotPasswordLimiter(`forgot-password-${ip}`); + } else if (isClientSideApiRoute(request.nextUrl.pathname)) { await clientSideApiEndpointsLimiter(`client-side-api-${ip}`); const envIdAndUserId = isSyncWithUserIdentificationEndpoint(request.nextUrl.pathname); @@ -71,7 +67,7 @@ export const middleware = async (request: NextRequest) => { const { environmentId, userId } = envIdAndUserId; await syncUserIdentificationLimiter(`sync-${environmentId}-${userId}`); } - } else if (shareUrlRoute(request.nextUrl.pathname)) { + } else if (isShareUrlRoute(request.nextUrl.pathname)) { await shareUrlLimiter(`share-${ip}`); } return NextResponse.next(); @@ -86,10 +82,6 @@ export const middleware = async (request: NextRequest) => { export const config = { matcher: [ "/api/auth/callback/credentials", - "/api/v1/users", - "/api/v1/users/forgot-password", - "/api/v1/users/verification-email", - "/api/v1/users/reset-password", "/api/(.*)/client/:path*", "/api/v1/js/actions", "/api/v1/client/storage", @@ -98,6 +90,9 @@ export const config = { "/setup/organization/:path*", "/api/auth/signout", "/auth/login", + "/auth/signup", "/api/packages/:path*", + "/auth/verification-requested", + "/auth/forgot-password", ], }; diff --git a/apps/web/modules/auth/components/SignupOptions/actions.ts b/apps/web/modules/auth/actions.ts similarity index 83% rename from apps/web/modules/auth/components/SignupOptions/actions.ts rename to apps/web/modules/auth/actions.ts index a8526c132e..717e8ef250 100644 --- a/apps/web/modules/auth/components/SignupOptions/actions.ts +++ b/apps/web/modules/auth/actions.ts @@ -4,6 +4,7 @@ import { actionClient } from "@/lib/utils/action-client"; import { z } from "zod"; import { createEmailToken } from "@formbricks/lib/jwt"; import { getUserByEmail } from "@formbricks/lib/user/service"; +import { InvalidInputError } from "@formbricks/types/errors"; const ZCreateEmailTokenAction = z.object({ email: z.string().min(5).max(255).email({ message: "Invalid email" }), @@ -14,7 +15,7 @@ export const createEmailTokenAction = actionClient .action(async ({ parsedInput }) => { const user = await getUserByEmail(parsedInput.email); if (!user) { - throw new Error("Invalid request"); + throw new InvalidInputError("Invalid request"); } return createEmailToken(parsedInput.email); diff --git a/apps/web/modules/auth/components/SignupOptions/components/IsPasswordValid.tsx b/apps/web/modules/auth/components/SignupOptions/components/IsPasswordValid.tsx deleted file mode 100644 index f55eaa87fb..0000000000 --- a/apps/web/modules/auth/components/SignupOptions/components/IsPasswordValid.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { CheckIcon } from "lucide-react"; -import { useTranslations } from "next-intl"; -import { useEffect, useState } from "react"; - -interface Validation { - label: string; - state: boolean; -} - -const PASSWORD_REGEX = { - UPPER_AND_LOWER: /^(?=.*[A-Z])(?=.*[a-z])/, - NUMBER: /\d/, -}; - -const DEFAULT_VALIDATIONS = [ - { label: "auth.signup.password_validation_uppercase_and_lowercase", state: false }, - { label: "auth.signup.password_validation_minimum_8_and_maximum_128_characters", state: false }, - { label: "auth.signup.password_validation_contain_at_least_1_number", state: false }, -]; - -export const IsPasswordValid = ({ - password, - setIsValid, -}: { - password: string | null; - setIsValid: (isValid: boolean) => void; -}) => { - const t = useTranslations(); - const [validations, setValidations] = useState(DEFAULT_VALIDATIONS); - - useEffect(() => { - let newValidations = [...DEFAULT_VALIDATIONS]; - - const checkValidation = (prevValidations: Validation[], index: number, state: boolean) => { - const updatedValidations = [...prevValidations]; - updatedValidations[index].state = state; - return updatedValidations; - }; - - if (password !== null) { - newValidations = checkValidation(newValidations, 0, PASSWORD_REGEX.UPPER_AND_LOWER.test(password)); - newValidations = checkValidation(newValidations, 1, password.length >= 8 && password.length <= 128); - newValidations = checkValidation(newValidations, 2, PASSWORD_REGEX.NUMBER.test(password)); - } - setIsValid(newValidations.every((validation) => validation.state === true)); - setValidations(newValidations); - }, [password, setIsValid]); - - const renderIcon = (state: boolean) => { - if (state === false) { - return ( - - - - ); - } else { - return ; - } - }; - - return ( -
-
    - {validations.map((validation, index) => ( -
  • -
    - {renderIcon(validation.state)} - {t(validation.label)} -
    -
  • - ))} -
-
- ); -}; diff --git a/apps/web/app/(auth)/auth/components/BackToLoginButton.tsx b/apps/web/modules/auth/components/back-to-login-button.tsx similarity index 100% rename from apps/web/app/(auth)/auth/components/BackToLoginButton.tsx rename to apps/web/modules/auth/components/back-to-login-button.tsx diff --git a/apps/web/app/(auth)/auth/components/FormWrapper.tsx b/apps/web/modules/auth/components/form-wrapper.tsx similarity index 77% rename from apps/web/app/(auth)/auth/components/FormWrapper.tsx rename to apps/web/modules/auth/components/form-wrapper.tsx index e1ab8f82e0..85c74459de 100644 --- a/apps/web/app/(auth)/auth/components/FormWrapper.tsx +++ b/apps/web/modules/auth/components/form-wrapper.tsx @@ -1,6 +1,10 @@ import { Logo } from "@/modules/ui/components/logo"; -export const FormWrapper = ({ children }: { children: React.ReactNode }) => { +interface FormWrapperProps { + children: React.ReactNode; +} + +export const FormWrapper = ({ children }: FormWrapperProps) => { return (
diff --git a/apps/web/app/(auth)/auth/components/Testimonial.tsx b/apps/web/modules/auth/components/testimonial.tsx similarity index 100% rename from apps/web/app/(auth)/auth/components/Testimonial.tsx rename to apps/web/modules/auth/components/testimonial.tsx diff --git a/apps/web/modules/auth/forgot-password/actions.ts b/apps/web/modules/auth/forgot-password/actions.ts new file mode 100644 index 0000000000..744598562b --- /dev/null +++ b/apps/web/modules/auth/forgot-password/actions.ts @@ -0,0 +1,20 @@ +"use server"; + +import { actionClient } from "@/lib/utils/action-client"; +import { getUserByEmail } from "@/modules/auth/lib/user"; +import { sendForgotPasswordEmail } from "@/modules/email"; +import { z } from "zod"; + +const ZForgotPasswordAction = z.object({ + email: z.string().max(255).email({ message: "Invalid email" }), +}); + +export const forgotPasswordAction = actionClient + .schema(ZForgotPasswordAction) + .action(async ({ parsedInput }) => { + const user = await getUserByEmail(parsedInput.email); + if (user) { + await sendForgotPasswordEmail(user); + } + return { success: true }; + }); diff --git a/apps/web/modules/auth/forgot-password/components/forgot-password-form.tsx b/apps/web/modules/auth/forgot-password/components/forgot-password-form.tsx new file mode 100644 index 0000000000..d9383c8ada --- /dev/null +++ b/apps/web/modules/auth/forgot-password/components/forgot-password-form.tsx @@ -0,0 +1,85 @@ +"use client"; + +import { getFormattedErrorMessage } from "@/lib/utils/helper"; +import { forgotPasswordAction } from "@/modules/auth/forgot-password/actions"; +import { Button } from "@/modules/ui/components/button"; +import { FormControl, FormError, FormField, FormItem } from "@/modules/ui/components/form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useTranslations } from "next-intl"; +import { useRouter } from "next/navigation"; +import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; +import toast from "react-hot-toast"; +import { z } from "zod"; + +const ZForgotPasswordForm = z.object({ + email: z.string().email(), +}); + +type TForgotPasswordForm = z.infer; + +export const ForgotPasswordForm = () => { + const router = useRouter(); + const t = useTranslations(); + const form = useForm({ + defaultValues: { + email: "", + }, + resolver: zodResolver(ZForgotPasswordForm), + }); + + const handleSubmit: SubmitHandler = async (data) => { + const forgotPasswordResponse = await forgotPasswordAction({ email: data.email }); + if (forgotPasswordResponse?.data) { + router.push("/auth/forgot-password/email-sent"); + } else { + const errorMessage = getFormattedErrorMessage(forgotPasswordResponse); + toast.error(errorMessage); + } + }; + + return ( + +
+
+ +
+ ( + + + field.onChange(e)} + autoComplete="email" + required + className="focus:border-brand-dark focus:ring-brand-dark block w-full rounded-md border-slate-300 shadow-sm sm:text-sm" + /> + + {error?.message && {error.message}} + + )} + /> +
+
+ +
+ +
+ +
+
+
+
+ ); +}; diff --git a/apps/web/modules/auth/forgot-password/email-sent/page.tsx b/apps/web/modules/auth/forgot-password/email-sent/page.tsx new file mode 100644 index 0000000000..272cc21e6b --- /dev/null +++ b/apps/web/modules/auth/forgot-password/email-sent/page.tsx @@ -0,0 +1,20 @@ +import { BackToLoginButton } from "@/modules/auth/components/back-to-login-button"; +import { FormWrapper } from "@/modules/auth/components/form-wrapper"; +import { getTranslations } from "next-intl/server"; + +export const EmailSentPage = async () => { + const t = await getTranslations(); + return ( + +
+

+ {t("auth.forgot-password.email-sent.heading")} +

+

{t("auth.forgot-password.email-sent.text")}

+
+ +
+
+
+ ); +}; diff --git a/apps/web/modules/auth/forgot-password/page.tsx b/apps/web/modules/auth/forgot-password/page.tsx new file mode 100644 index 0000000000..45fd249fd0 --- /dev/null +++ b/apps/web/modules/auth/forgot-password/page.tsx @@ -0,0 +1,10 @@ +import { FormWrapper } from "@/modules/auth/components/form-wrapper"; +import { ForgotPasswordForm } from "@/modules/auth/forgot-password/components/forgot-password-form"; + +export const ForgotPasswordPage = () => { + return ( + + + + ); +}; diff --git a/apps/web/modules/auth/forgot-password/reset/actions.ts b/apps/web/modules/auth/forgot-password/reset/actions.ts new file mode 100644 index 0000000000..afaf06aaf8 --- /dev/null +++ b/apps/web/modules/auth/forgot-password/reset/actions.ts @@ -0,0 +1,30 @@ +"use server"; + +import { actionClient } from "@/lib/utils/action-client"; +import { updateUser } from "@/modules/auth/lib/user"; +import { getUser } from "@/modules/auth/lib/user"; +import { sendPasswordResetNotifyEmail } from "@/modules/email"; +import { z } from "zod"; +import { hashPassword } from "@formbricks/lib/auth"; +import { verifyToken } from "@formbricks/lib/jwt"; +import { ResourceNotFoundError } from "@formbricks/types/errors"; +import { ZUserPassword } from "@formbricks/types/user"; + +const ZResetPasswordAction = z.object({ + token: z.string(), + password: ZUserPassword, +}); + +export const resetPasswordAction = actionClient + .schema(ZResetPasswordAction) + .action(async ({ parsedInput }) => { + const hashedPassword = await hashPassword(parsedInput.password); + const { id } = await verifyToken(parsedInput.token); + const user = await getUser(id); + if (!user) { + throw new ResourceNotFoundError("user", id); + } + const updatedUser = await updateUser(id, { password: hashedPassword }); + await sendPasswordResetNotifyEmail(updatedUser); + return { success: true }; + }); diff --git a/apps/web/modules/auth/forgot-password/reset/components/reset-password-form.tsx b/apps/web/modules/auth/forgot-password/reset/components/reset-password-form.tsx new file mode 100644 index 0000000000..a19057a20a --- /dev/null +++ b/apps/web/modules/auth/forgot-password/reset/components/reset-password-form.tsx @@ -0,0 +1,106 @@ +"use client"; + +import { getFormattedErrorMessage } from "@/lib/utils/helper"; +import { resetPasswordAction } from "@/modules/auth/forgot-password/reset/actions"; +import { PasswordChecks } from "@/modules/auth/signup/components/password-checks"; +import { Button } from "@/modules/ui/components/button"; +import { FormField } from "@/modules/ui/components/form"; +import { PasswordInput } from "@/modules/ui/components/password-input"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useTranslations } from "next-intl"; +import { useRouter, useSearchParams } from "next/navigation"; +import { SubmitHandler, useForm } from "react-hook-form"; +import { toast } from "react-hot-toast"; +import { z } from "zod"; +import { ZUserPassword } from "@formbricks/types/user"; + +const ZPasswordResetForm = z.object({ + password: ZUserPassword, + confirmPassword: ZUserPassword, +}); + +type TPasswordResetForm = z.infer; + +const passwordInputProps = { + autoComplete: "current-password", + placeholder: "*******", + required: true, + className: + "focus:border-brand-dark focus:ring-brand-dark mt-2 block w-full rounded-md border-slate-300 shadow-sm sm:text-sm", +}; + +export const ResetPasswordForm = () => { + const t = useTranslations(); + const searchParams = useSearchParams(); + const router = useRouter(); + + const form = useForm({ + defaultValues: { + password: "", + confirmPassword: "", + }, + resolver: zodResolver(ZPasswordResetForm), + }); + + const handleSubmit: SubmitHandler = async (data) => { + if (data.password !== data.confirmPassword) { + toast.error(t("auth.forgot-password.reset.passwords_do_not_match")); + return; + } + const token = searchParams?.get("token"); + if (!token) { + toast.error(t("auth.forgot-password.reset.no_token_provided")); + return; + } + const resetPasswordResponse = await resetPasswordAction({ token, password: data.password }); + if (resetPasswordResponse?.data) { + router.push("/auth/forgot-password/reset/success"); + } else { + const errorMessage = getFormattedErrorMessage(resetPasswordResponse); + toast.error(errorMessage); + } + }; + + return ( + <> +
+
+
+ + } + /> +
+
+ + ( + + )} + /> +
+ + +
+ +
+ +
+
+ + ); +}; diff --git a/apps/web/modules/auth/forgot-password/reset/page.tsx b/apps/web/modules/auth/forgot-password/reset/page.tsx new file mode 100644 index 0000000000..e7aea0800f --- /dev/null +++ b/apps/web/modules/auth/forgot-password/reset/page.tsx @@ -0,0 +1,10 @@ +import { FormWrapper } from "@/modules/auth/components/form-wrapper"; +import { ResetPasswordForm } from "@/modules/auth/forgot-password/reset/components/reset-password-form"; + +export const ResetPasswordPage = () => { + return ( + + + + ); +}; diff --git a/apps/web/modules/auth/forgot-password/reset/success/page.tsx b/apps/web/modules/auth/forgot-password/reset/success/page.tsx new file mode 100644 index 0000000000..44a9f938f8 --- /dev/null +++ b/apps/web/modules/auth/forgot-password/reset/success/page.tsx @@ -0,0 +1,20 @@ +import { BackToLoginButton } from "@/modules/auth/components/back-to-login-button"; +import { FormWrapper } from "@/modules/auth/components/form-wrapper"; +import { getTranslations } from "next-intl/server"; + +export const ResetPasswordSuccessPage = async () => { + const t = await getTranslations(); + return ( + +
+

+ {t("auth.forgot-password.reset.success.heading")} +

+

{t("auth.forgot-password.reset.success.text")}

+
+ +
+
+
+ ); +}; diff --git a/apps/web/modules/auth/layout.tsx b/apps/web/modules/auth/layout.tsx new file mode 100644 index 0000000000..adefb87862 --- /dev/null +++ b/apps/web/modules/auth/layout.tsx @@ -0,0 +1,32 @@ +import { authOptions } from "@/modules/auth/lib/authOptions"; +import { getIsMultiOrgEnabled } from "@/modules/ee/license-check/lib/utils"; +import { getServerSession } from "next-auth"; +import { redirect } from "next/navigation"; +import { Toaster } from "react-hot-toast"; +import { getIsFreshInstance } from "@formbricks/lib/instance/service"; + +export const AuthLayout = async ({ children }: { children: React.ReactNode }) => { + const [session, isFreshInstance, isMultiOrgEnabled] = await Promise.all([ + getServerSession(authOptions), + getIsFreshInstance(), + getIsMultiOrgEnabled(), + ]); + + if (session) { + redirect(`/`); + } + + if (isFreshInstance && !isMultiOrgEnabled) { + redirect("/setup/intro"); + } + return ( + <> + +
+
+
{children}
+
+
+ + ); +}; diff --git a/apps/web/modules/auth/lib/authOptions.ts b/apps/web/modules/auth/lib/authOptions.ts new file mode 100644 index 0000000000..f883088bf3 --- /dev/null +++ b/apps/web/modules/auth/lib/authOptions.ts @@ -0,0 +1,209 @@ +import { getUserByEmail, updateUser } from "@/modules/auth/lib/user"; +import { verifyPassword } from "@/modules/auth/lib/utils"; +import { getSSOProviders } from "@/modules/ee/sso/lib/providers"; +import { handleSSOCallback } from "@/modules/ee/sso/lib/sso-handlers"; +import type { Account, NextAuthOptions } from "next-auth"; +import CredentialsProvider from "next-auth/providers/credentials"; +import { prisma } from "@formbricks/database"; +import { + EMAIL_VERIFICATION_DISABLED, + ENCRYPTION_KEY, + ENTERPRISE_LICENSE_KEY, +} from "@formbricks/lib/constants"; +import { symmetricDecrypt, symmetricEncrypt } from "@formbricks/lib/crypto"; +import { verifyToken } from "@formbricks/lib/jwt"; +import { TUser } from "@formbricks/types/user"; + +export const authOptions: NextAuthOptions = { + providers: [ + CredentialsProvider({ + id: "credentials", + // The name to display on the sign in form (e.g. "Sign in with...") + name: "Credentials", + // The credentials is used to generate a suitable form on the sign in page. + // You can specify whatever fields you are expecting to be submitted. + // e.g. domain, username, password, 2FA token, etc. + // You can pass any HTML attribute to the tag through the object. + credentials: { + email: { + label: "Email Address", + type: "email", + placeholder: "Your email address", + }, + password: { + label: "Password", + type: "password", + placeholder: "Your password", + }, + totpCode: { label: "Two-factor Code", type: "input", placeholder: "Code from authenticator app" }, + backupCode: { label: "Backup Code", type: "input", placeholder: "Two-factor backup code" }, + }, + async authorize(credentials, _req) { + let user; + try { + user = await prisma.user.findUnique({ + where: { + email: credentials?.email, + }, + }); + } catch (e) { + console.error(e); + throw Error("Internal server error. Please try again later"); + } + if (!user || !credentials) { + throw new Error("Invalid credentials"); + } + if (!user.password) { + throw new Error("Invalid credentials"); + } + + const isValid = await verifyPassword(credentials.password, user.password); + + if (!isValid) { + throw new Error("Invalid credentials"); + } + + if (user.twoFactorEnabled && credentials.backupCode) { + if (!ENCRYPTION_KEY) { + console.error("Missing encryption key; cannot proceed with backup code login."); + throw new Error("Internal Server Error"); + } + + if (!user.backupCodes) throw new Error("No backup codes found"); + + const backupCodes = JSON.parse(symmetricDecrypt(user.backupCodes, ENCRYPTION_KEY)); + + // check if user-supplied code matches one + const index = backupCodes.indexOf(credentials.backupCode.replaceAll("-", "")); + if (index === -1) throw new Error("Invalid backup code"); + + // delete verified backup code and re-encrypt remaining + backupCodes[index] = null; + await prisma.user.update({ + where: { + id: user.id, + }, + data: { + backupCodes: symmetricEncrypt(JSON.stringify(backupCodes), ENCRYPTION_KEY), + }, + }); + } else if (user.twoFactorEnabled) { + if (!credentials.totpCode) { + throw new Error("second factor required"); + } + + if (!user.twoFactorSecret) { + throw new Error("Internal Server Error"); + } + + if (!ENCRYPTION_KEY) { + throw new Error("Internal Server Error"); + } + + const secret = symmetricDecrypt(user.twoFactorSecret, ENCRYPTION_KEY); + if (secret.length !== 32) { + throw new Error("Internal Server Error"); + } + + const isValidToken = (await import("./totp")).totpAuthenticatorCheck(credentials.totpCode, secret); + if (!isValidToken) { + throw new Error("Invalid second factor code"); + } + } + + return { + id: user.id, + email: user.email, + emailVerified: user.emailVerified, + imageUrl: user.imageUrl, + }; + }, + }), + CredentialsProvider({ + id: "token", + // The name to display on the sign in form (e.g. "Sign in with...") + name: "Token", + // The credentials is used to generate a suitable form on the sign in page. + // You can specify whatever fields you are expecting to be submitted. + // e.g. domain, username, password, 2FA token, etc. + // You can pass any HTML attribute to the tag through the object. + credentials: { + token: { + label: "Verification Token", + type: "string", + }, + }, + async authorize(credentials, _req) { + let user; + try { + if (!credentials?.token) { + throw new Error("Token not found"); + } + const { id } = await verifyToken(credentials?.token); + user = await prisma.user.findUnique({ + where: { + id: id, + }, + }); + } catch (e) { + console.error(e); + throw new Error("Either a user does not match the provided token or the token is invalid"); + } + + if (!user) { + throw new Error("Either a user does not match the provided token or the token is invalid"); + } + + if (user.emailVerified) { + throw new Error("Email already verified"); + } + + user = await updateUser(user.id, { emailVerified: new Date() }); + + return user; + }, + }), + // Conditionally add enterprise SSO providers + ...(ENTERPRISE_LICENSE_KEY ? getSSOProviders() : []), + ], + callbacks: { + async jwt({ token }) { + const existingUser = await getUserByEmail(token?.email!); + + if (!existingUser) { + return token; + } + + return { + ...token, + profile: { id: existingUser.id }, + }; + }, + async session({ session, token }) { + // @ts-expect-error + session.user.id = token?.id; + // @ts-expect-error + session.user = token.profile; + + return session; + }, + async signIn({ user, account }: { user: TUser; account: Account }) { + if (account?.provider === "credentials" || account?.provider === "token") { + // check if user's email is verified or not + if (!user.emailVerified && !EMAIL_VERIFICATION_DISABLED) { + throw new Error("Email Verification is Pending"); + } + return true; + } + if (ENTERPRISE_LICENSE_KEY) { + return handleSSOCallback({ user, account }); + } + return true; + }, + }, + pages: { + signIn: "/auth/login", + signOut: "/auth/logout", + error: "/auth/login", // Error code passed in query string as ?error= + }, +}; diff --git a/packages/lib/totp.ts b/apps/web/modules/auth/lib/totp.ts similarity index 100% rename from packages/lib/totp.ts rename to apps/web/modules/auth/lib/totp.ts diff --git a/apps/web/modules/auth/lib/user.ts b/apps/web/modules/auth/lib/user.ts new file mode 100644 index 0000000000..e156a6ef28 --- /dev/null +++ b/apps/web/modules/auth/lib/user.ts @@ -0,0 +1,145 @@ +import { Prisma } from "@prisma/client"; +import { cache as reactCache } from "react"; +import { prisma } from "@formbricks/database"; +import { cache } from "@formbricks/lib/cache"; +import { createCustomerIoCustomer } from "@formbricks/lib/customerio"; +import { userCache } from "@formbricks/lib/user/cache"; +import { validateInputs } from "@formbricks/lib/utils/validate"; +import { ZId } from "@formbricks/types/common"; +import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors"; +import { TUserCreateInput, TUserUpdateInput, ZUserEmail, ZUserUpdateInput } from "@formbricks/types/user"; + +export const updateUser = async (id: string, data: TUserUpdateInput) => { + validateInputs([id, ZId], [data, ZUserUpdateInput.partial()]); + + try { + const updatedUser = await prisma.user.update({ + where: { + id, + }, + data: data, + select: { + id: true, + email: true, + locale: true, + emailVerified: true, + }, + }); + + userCache.revalidate({ + email: updatedUser.email, + id: updatedUser.id, + }); + + return updatedUser; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2016") { + throw new ResourceNotFoundError("User", id); + } + throw error; + } +}; + +export const getUserByEmail = reactCache(async (email: string) => + cache( + async () => { + validateInputs([email, ZUserEmail]); + + try { + const user = await prisma.user.findFirst({ + where: { + email, + }, + select: { + id: true, + locale: true, + email: true, + emailVerified: true, + }, + }); + + return user; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + + throw error; + } + }, + [`getUserByEmail-${email}`], + { + tags: [userCache.tag.byEmail(email)], + } + )() +); + +export const getUser = reactCache(async (id: string) => + cache( + async () => { + validateInputs([id, ZId]); + + try { + const user = await prisma.user.findUnique({ + where: { + id, + }, + select: { + id: true, + }, + }); + + if (!user) { + return null; + } + return user; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + + throw error; + } + }, + [`getUser-${id}`], + { + tags: [userCache.tag.byId(id)], + } + )() +); + +export const createUser = async (data: TUserCreateInput) => { + validateInputs([data, ZUserUpdateInput]); + try { + const user = await prisma.user.create({ + data: data, + select: { + name: true, + notificationSettings: true, + id: true, + email: true, + locale: true, + }, + }); + + userCache.revalidate({ + email: user.email, + id: user.id, + count: true, + }); + + // send new user customer.io to customer.io + createCustomerIoCustomer({ id: user.id, email: user.email }); + return user; + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") { + throw new InvalidInputError("User with this email already exists"); + } + + if (error instanceof Prisma.PrismaClientKnownRequestError) { + throw new DatabaseError(error.message); + } + + throw error; + } +}; diff --git a/packages/lib/auth/utils.ts b/apps/web/modules/auth/lib/utils.ts similarity index 100% rename from packages/lib/auth/utils.ts rename to apps/web/modules/auth/lib/utils.ts diff --git a/apps/web/modules/auth/components/SigninForm/index.tsx b/apps/web/modules/auth/login/components/login-form.tsx similarity index 67% rename from apps/web/modules/auth/components/SigninForm/index.tsx rename to apps/web/modules/auth/login/components/login-form.tsx index 5d9fa9fcc9..20c40f19a1 100644 --- a/apps/web/modules/auth/components/SigninForm/index.tsx +++ b/apps/web/modules/auth/login/components/login-form.tsx @@ -1,35 +1,35 @@ "use client"; -import { TwoFactor } from "@/modules/auth/components/SigninForm/components/TwoFactor"; -import { TwoFactorBackup } from "@/modules/auth/components/SigninForm/components/TwoFactorBackup"; -import { createEmailTokenAction } from "@/modules/auth/components/SignupOptions/actions"; -import { AzureButton } from "@/modules/auth/components/SignupOptions/components/AzureButton"; -import { GithubButton } from "@/modules/auth/components/SignupOptions/components/GithubButton"; -import { GoogleButton } from "@/modules/auth/components/SignupOptions/components/GoogleButton"; -import { OpenIdButton } from "@/modules/auth/components/SignupOptions/components/OpenIdButton"; +import { getFormattedErrorMessage } from "@/lib/utils/helper"; +import { createEmailTokenAction } from "@/modules/auth/actions"; +import { SSOOptions } from "@/modules/ee/sso/components/sso-options"; +import { TwoFactor } from "@/modules/ee/two-factor-auth/components/two-factor"; +import { TwoFactorBackup } from "@/modules/ee/two-factor-auth/components/two-factor-backup"; import { Button } from "@/modules/ui/components/button"; import { FormControl, FormError, FormField, FormItem } from "@/modules/ui/components/form"; import { PasswordInput } from "@/modules/ui/components/password-input"; import { zodResolver } from "@hookform/resolvers/zod"; -import { XCircleIcon } from "lucide-react"; import { signIn } from "next-auth/react"; import { useTranslations } from "next-intl"; import Link from "next/dist/client/link"; import { useRouter, useSearchParams } from "next/navigation"; import { useEffect, useMemo, useRef, useState } from "react"; import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; +import { toast } from "react-hot-toast"; import { z } from "zod"; import { cn } from "@formbricks/lib/cn"; import { FORMBRICKS_LOGGED_IN_WITH_LS } from "@formbricks/lib/localStorage"; -interface TSigninFormState { - email: string; - password: string; - totpCode: string; - backupCode: string; -} +const ZLoginForm = z.object({ + email: z.string().email(), + password: z.string().min(8), + totpCode: z.string().optional(), + backupCode: z.string().optional(), +}); -interface SignInFormProps { +type TLoginForm = z.infer; + +interface LoginFormProps { emailAuthEnabled: boolean; publicSignUpEnabled: boolean; passwordResetEnabled: boolean; @@ -39,9 +39,10 @@ interface SignInFormProps { oidcOAuthEnabled: boolean; oidcDisplayName?: string; isMultiOrgEnabled: boolean; + isSSOEnabled: boolean; } -export const SigninForm = ({ +export const LoginForm = ({ emailAuthEnabled, publicSignUpEnabled, passwordResetEnabled, @@ -51,32 +52,25 @@ export const SigninForm = ({ oidcOAuthEnabled, oidcDisplayName, isMultiOrgEnabled, -}: SignInFormProps) => { + isSSOEnabled, +}: LoginFormProps) => { const router = useRouter(); const searchParams = useSearchParams(); const emailRef = useRef(null); - const formMethods = useForm(); - const callbackUrl = searchParams?.get("callbackUrl"); - const ZSignInInput = z.object({ - email: z.string().email(), - password: z.string().min(8), - totpCode: z.string().optional(), - backupCode: z.string().optional(), - }); + const callbackUrl = searchParams?.get("callbackUrl") || ""; + const t = useTranslations(); - type TSignInInput = z.infer; - const form = useForm({ + const form = useForm({ defaultValues: { - email: "", + email: searchParams?.get("email") || "", password: "", totpCode: "", backupCode: "", }, - resolver: zodResolver(ZSignInInput), + resolver: zodResolver(ZLoginForm), }); - const t = useTranslations(); - const onSubmit: SubmitHandler = async (data) => { - setLoggingIn(true); + + const onSubmit: SubmitHandler = async (data) => { if (typeof window !== "undefined") { localStorage.setItem(FORMBRICKS_LOGGED_IN_WITH_LS, "Email"); } @@ -92,23 +86,22 @@ export const SigninForm = ({ if (signInResponse?.error === "second factor required") { setTotpLogin(true); - setLoggingIn(false); return; } if (signInResponse?.error === "Email Verification is Pending") { const emailTokenActionResponse = await createEmailTokenAction({ email: data.email }); - if (emailTokenActionResponse?.serverError) { - setSignInError(emailTokenActionResponse.serverError); - return; + if (emailTokenActionResponse?.data) { + router.push(`/auth/verification-requested?token=${emailTokenActionResponse?.data}`); + } else { + const errorMessage = getFormattedErrorMessage(emailTokenActionResponse); + toast.error(errorMessage); } - router.push(`/auth/verification-requested?token=${emailTokenActionResponse?.data}`); return; } if (signInResponse?.error) { - setLoggingIn(false); - setSignInError(signInResponse.error); + toast.error(signInResponse.error); return; } @@ -116,39 +109,24 @@ export const SigninForm = ({ router.push(searchParams?.get("callbackUrl") || "/"); } } catch (error) { - const errorMessage = error.toString(); - const errorFeedback = errorMessage.includes("Invalid URL") - ? t("auth.login.too_many_requests_please_try_again_after_some_time") - : error.message; - setSignInError(errorFeedback); - } finally { - setLoggingIn(false); + toast.error(error.toString()); } }; - const [loggingIn, setLoggingIn] = useState(false); const [showLogin, setShowLogin] = useState(false); const [isPasswordFocused, setIsPasswordFocused] = useState(false); const [totpLogin, setTotpLogin] = useState(false); const [totpBackup, setTotpBackup] = useState(false); - const [signInError, setSignInError] = useState(""); const formRef = useRef(null); - const error = searchParams?.get("error"); const inviteToken = callbackUrl ? new URL(callbackUrl).searchParams.get("token") : null; - const [lastLoggedInWith, setLastLoginWith] = useState(""); + const [lastLoggedInWith, setLastLoggedInWith] = useState(""); useEffect(() => { if (typeof window !== "undefined") { - setLastLoginWith(localStorage.getItem(FORMBRICKS_LOGGED_IN_WITH_LS) || ""); + setLastLoggedInWith(localStorage.getItem(FORMBRICKS_LOGGED_IN_WITH_LS) || ""); } }, []); - useEffect(() => { - if (error) { - setSignInError(error); - } - }, [error]); - const formLabel = useMemo(() => { if (totpBackup) { return t("auth.login.enter_your_backup_code"); @@ -174,7 +152,7 @@ export const SigninForm = ({ }, [totpBackup, totpLogin]); return ( - +

{formLabel}

@@ -198,7 +176,6 @@ export const SigninForm = ({ value={field.value} onChange={(email) => field.onChange(email)} placeholder="work@email.com" - defaultValue={searchParams?.get("email") || ""} className="focus:border-brand-dark focus:ring-brand-dark block w-full rounded-md border-slate-300 shadow-sm sm:text-sm" /> {error?.message && {error.message}} @@ -255,7 +232,7 @@ export const SigninForm = ({ } }} className="relative w-full justify-center" - loading={loggingIn}> + loading={form.formState.isSubmitting}> {totpLogin ? t("common.submit") : t("auth.login.login_with_email")} {lastLoggedInWith && lastLoggedInWith === "Email" ? ( {t("auth.last_used")} @@ -263,45 +240,15 @@ export const SigninForm = ({ )} - - {googleOAuthEnabled && !totpLogin && ( - <> - - - )} - - {githubOAuthEnabled && !totpLogin && ( - <> - - - )} - - {azureOAuthEnabled && !totpLogin && ( - <> - - - )} - - {oidcOAuthEnabled && !totpLogin && ( - <> - - + {isSSOEnabled && ( + )}
@@ -356,24 +303,6 @@ export const SigninForm = ({
)} - - {signInError && ( -
-
-
-
-
-

- {t("auth.login.an_error_occurred_when_logging_you_in")} -

-
-

{signInError}

-
-
-
-
- )} ); }; diff --git a/apps/web/modules/auth/login/page.tsx b/apps/web/modules/auth/login/page.tsx new file mode 100644 index 0000000000..c702632bc1 --- /dev/null +++ b/apps/web/modules/auth/login/page.tsx @@ -0,0 +1,47 @@ +import { FormWrapper } from "@/modules/auth/components/form-wrapper"; +import { Testimonial } from "@/modules/auth/components/testimonial"; +import { getIsMultiOrgEnabled, getIsSSOEnabled } from "@/modules/ee/license-check/lib/utils"; +import { Metadata } from "next"; +import { + AZURE_OAUTH_ENABLED, + EMAIL_AUTH_ENABLED, + GITHUB_OAUTH_ENABLED, + GOOGLE_OAUTH_ENABLED, + OIDC_DISPLAY_NAME, + OIDC_OAUTH_ENABLED, + PASSWORD_RESET_DISABLED, + SIGNUP_ENABLED, +} from "@formbricks/lib/constants"; +import { LoginForm } from "./components/login-form"; + +export const metadata: Metadata = { + title: "Login", + description: "Open-source Experience Management. Free & open source.", +}; + +export const LoginPage = async () => { + const [isMultiOrgEnabled, isSSOEnabled] = await Promise.all([getIsMultiOrgEnabled(), getIsSSOEnabled()]); + return ( +
+
+ +
+
+ + + +
+
+ ); +}; diff --git a/apps/web/modules/auth/signup-without-verification-success/page.tsx b/apps/web/modules/auth/signup-without-verification-success/page.tsx new file mode 100644 index 0000000000..2f7ab4a493 --- /dev/null +++ b/apps/web/modules/auth/signup-without-verification-success/page.tsx @@ -0,0 +1,19 @@ +import { BackToLoginButton } from "@/modules/auth/components/back-to-login-button"; +import { FormWrapper } from "@/modules/auth/components/form-wrapper"; +import { getTranslations } from "next-intl/server"; + +export const SignupWithoutVerificationSuccessPage = async () => { + const t = await getTranslations(); + return ( + +

+ {t("auth.signup_without_verification_success.user_successfully_created")} +

+

+ {t("auth.signup_without_verification_success.user_successfully_created_description")} +

+
+ +
+ ); +}; diff --git a/apps/web/modules/auth/signup/actions.ts b/apps/web/modules/auth/signup/actions.ts new file mode 100644 index 0000000000..c248f727ce --- /dev/null +++ b/apps/web/modules/auth/signup/actions.ts @@ -0,0 +1,110 @@ +"use server"; + +import { actionClient } from "@/lib/utils/action-client"; +import { createUser } from "@/modules/auth/lib/user"; +import { updateUser } from "@/modules/auth/lib/user"; +import { getIsMultiOrgEnabled } from "@/modules/ee/license-check/lib/utils"; +import { sendInviteAcceptedEmail, sendVerificationEmail } from "@/modules/email"; +import { z } from "zod"; +import { hashPassword } from "@formbricks/lib/auth"; +import { getInvite } from "@formbricks/lib/invite/service"; +import { deleteInvite } from "@formbricks/lib/invite/service"; +import { verifyInviteToken } from "@formbricks/lib/jwt"; +import { createMembership } from "@formbricks/lib/membership/service"; +import { createOrganization, getOrganization } from "@formbricks/lib/organization/service"; +import { TOrganizationRole, ZOrganizationRole } from "@formbricks/types/memberships"; +import { ZUserLocale, ZUserName } from "@formbricks/types/user"; + +const ZCreateUserAction = z.object({ + name: ZUserName, + email: z.string().max(255).email({ message: "Invalid email" }), + password: z.string().min(8), + inviteToken: z.string().optional(), + userLocale: ZUserLocale.optional(), + defaultOrganizationId: z.string().optional(), + defaultOrganizationRole: ZOrganizationRole.optional(), + emailVerificationDisabled: z.boolean().optional(), +}); + +export const createUserAction = actionClient.schema(ZCreateUserAction).action(async ({ parsedInput }) => { + const { inviteToken, emailVerificationDisabled } = parsedInput; + const hashedPassword = await hashPassword(parsedInput.password); + const user = await createUser({ + email: parsedInput.email.toLowerCase(), + name: parsedInput.name, + password: hashedPassword, + locale: parsedInput.userLocale, + }); + + // Handle invite flow + if (inviteToken) { + const inviteTokenData = verifyInviteToken(inviteToken); + const invite = await getInvite(inviteTokenData.inviteId); + if (!invite) { + throw new Error("Invalid invite ID"); + } + + await createMembership(invite.organizationId, user.id, { + accepted: true, + role: invite.role, + }); + + await updateUser(user.id, { + notificationSettings: { + alert: {}, + weeklySummary: {}, + unsubscribedOrganizationIds: [invite.organizationId], + }, + }); + + await sendInviteAcceptedEmail(invite.creator.name ?? "", user.name, invite.creator.email, user.locale); + await deleteInvite(invite.id); + } + // Handle organization assignment + else { + let organizationId: string | undefined; + let role: TOrganizationRole = "owner"; + + if (parsedInput.defaultOrganizationId) { + // Use existing or create organization with specific ID + let organization = await getOrganization(parsedInput.defaultOrganizationId); + if (!organization) { + organization = await createOrganization({ + id: parsedInput.defaultOrganizationId, + name: `${user.name}'s Organization`, + }); + } else { + role = parsedInput.defaultOrganizationRole || "owner"; + } + organizationId = organization.id; + } else { + const isMultiOrgEnabled = await getIsMultiOrgEnabled(); + if (isMultiOrgEnabled) { + // Create new organization + const organization = await createOrganization({ name: `${user.name}'s Organization` }); + organizationId = organization.id; + } + } + + if (organizationId) { + await createMembership(organizationId, user.id, { role, accepted: true }); + await updateUser(user.id, { + notificationSettings: { + ...user.notificationSettings, + alert: { ...user.notificationSettings?.alert }, + weeklySummary: { ...user.notificationSettings?.weeklySummary }, + unsubscribedOrganizationIds: Array.from( + new Set([...(user.notificationSettings?.unsubscribedOrganizationIds || []), organizationId]) + ), + }, + }); + } + } + + // Send verification email if enabled + if (!emailVerificationDisabled) { + await sendVerificationEmail(user); + } + + return user; +}); diff --git a/apps/web/modules/auth/signup/components/password-checks.tsx b/apps/web/modules/auth/signup/components/password-checks.tsx new file mode 100644 index 0000000000..9a3ca470e5 --- /dev/null +++ b/apps/web/modules/auth/signup/components/password-checks.tsx @@ -0,0 +1,63 @@ +import { CheckIcon } from "lucide-react"; +import { useTranslations } from "next-intl"; +import { useMemo } from "react"; + +interface PasswordChecksProps { + password: string | null; +} + +const PASSWORD_REGEX = { + UPPER_AND_LOWER: /^(?=.*[A-Z])(?=.*[a-z])/, + NUMBER: /\d/, +}; + +const DEFAULT_VALIDATIONS = [ + { label: "auth.signup.password_validation_uppercase_and_lowercase", state: false }, + { label: "auth.signup.password_validation_minimum_8_and_maximum_128_characters", state: false }, + { label: "auth.signup.password_validation_contain_at_least_1_number", state: false }, +]; + +const ValidationIcon = ({ state }: { state: boolean }) => + state ? ( + + ) : ( + + + + ); + +export const PasswordChecks = ({ password }: PasswordChecksProps) => { + const t = useTranslations(); + + const validations = useMemo(() => { + if (password === null) return DEFAULT_VALIDATIONS; + + return [ + { + label: "auth.signup.password_validation_uppercase_and_lowercase", + state: PASSWORD_REGEX.UPPER_AND_LOWER.test(password), + }, + { + label: "auth.signup.password_validation_minimum_8_and_maximum_128_characters", + state: password.length >= 8 && password.length <= 128, + }, + { + label: "auth.signup.password_validation_contain_at_least_1_number", + state: PASSWORD_REGEX.NUMBER.test(password), + }, + ]; + }, [password]); + + return ( +
+
    + {validations.map((validation) => ( +
  • + + {t(validation.label)} +
  • + ))} +
+
+ ); +}; diff --git a/apps/web/modules/auth/components/SignupOptions/index.tsx b/apps/web/modules/auth/signup/components/signup-form.tsx similarity index 58% rename from apps/web/modules/auth/components/SignupOptions/index.tsx rename to apps/web/modules/auth/signup/components/signup-form.tsx index d10daf7e3c..8c18233ff3 100644 --- a/apps/web/modules/auth/components/SignupOptions/index.tsx +++ b/apps/web/modules/auth/signup/components/signup-form.tsx @@ -1,69 +1,87 @@ "use client"; -import { createEmailTokenAction } from "@/modules/auth/components/SignupOptions/actions"; -import { AzureButton } from "@/modules/auth/components/SignupOptions/components/AzureButton"; -import { GithubButton } from "@/modules/auth/components/SignupOptions/components/GithubButton"; -import { GoogleButton } from "@/modules/auth/components/SignupOptions/components/GoogleButton"; -import { IsPasswordValid } from "@/modules/auth/components/SignupOptions/components/IsPasswordValid"; -import { OpenIdButton } from "@/modules/auth/components/SignupOptions/components/OpenIdButton"; +import { getFormattedErrorMessage } from "@/lib/utils/helper"; +import { createUserAction } from "@/modules/auth/signup/actions"; +import { TermsPrivacyLinks } from "@/modules/auth/signup/components/terms-privacy-links"; +import { SSOOptions } from "@/modules/ee/sso/components/sso-options"; import { Button } from "@/modules/ui/components/button"; import { FormControl, FormError, FormField, FormItem } from "@/modules/ui/components/form"; import { Input } from "@/modules/ui/components/input"; import { PasswordInput } from "@/modules/ui/components/password-input"; import { zodResolver } from "@hookform/resolvers/zod"; import { useTranslations } from "next-intl"; +import Link from "next/link"; import { useRouter } from "next/navigation"; -import { useRef, useState } from "react"; +import { useSearchParams } from "next/navigation"; +import { useMemo, useState } from "react"; import { FormProvider, useForm } from "react-hook-form"; import toast from "react-hot-toast"; import { z } from "zod"; -import { createUser } from "@formbricks/lib/utils/users"; -import { ZUserName } from "@formbricks/types/user"; +import { TOrganizationRole } from "@formbricks/types/memberships"; +import { TUserLocale, ZUserName } from "@formbricks/types/user"; +import { createEmailTokenAction } from "../../../auth/actions"; +import { PasswordChecks } from "./password-checks"; -interface SignupOptionsProps { +const ZSignupInput = z.object({ + name: ZUserName, + email: z.string().email(), + password: z + .string() + .min(8) + .regex(/^(?=.*[A-Z])(?=.*\d).*$/), +}); + +type TSignupInput = z.infer; + +interface SignupFormProps { + webAppUrl: string; + privacyUrl: string | undefined; + termsUrl: string | undefined; emailAuthEnabled: boolean; - emailFromSearchParams: string; - setError?: (error: string) => void; - emailVerificationDisabled: boolean; googleOAuthEnabled: boolean; githubOAuthEnabled: boolean; azureOAuthEnabled: boolean; oidcOAuthEnabled: boolean; - inviteToken: string | null; - callbackUrl: string; oidcDisplayName?: string; - userLocale: string; + userLocale: TUserLocale; + emailFromSearchParams?: string; + emailVerificationDisabled: boolean; + defaultOrganizationId?: string; + defaultOrganizationRole?: TOrganizationRole; + isSSOEnabled: boolean; } -export const SignupOptions = ({ +export const SignupForm = ({ + webAppUrl, + privacyUrl, + termsUrl, emailAuthEnabled, - emailFromSearchParams, - setError, - emailVerificationDisabled, googleOAuthEnabled, githubOAuthEnabled, azureOAuthEnabled, oidcOAuthEnabled, - inviteToken, - callbackUrl, oidcDisplayName, userLocale, -}: SignupOptionsProps) => { + emailFromSearchParams, + emailVerificationDisabled, + defaultOrganizationId, + defaultOrganizationRole, + isSSOEnabled, +}: SignupFormProps) => { const [showLogin, setShowLogin] = useState(false); - const [isValid, setIsValid] = useState(false); - const [signingUp, setSigningUp] = useState(false); + const searchParams = useSearchParams(); const t = useTranslations(); + const inviteToken = searchParams?.get("inviteToken"); + const router = useRouter(); - const ZSignupInput = z.object({ - name: ZUserName, - email: z.string().email(), - password: z - .string() - .min(8) - .regex(/^(?=.*[A-Z])(?=.*\d).*$/), - }); + const callbackUrl = useMemo(() => { + if (inviteToken) { + return webAppUrl + "/invite?token=" + inviteToken; + } else { + return webAppUrl; + } + }, [inviteToken, webAppUrl]); - type TSignupInput = z.infer; const form = useForm({ defaultValues: { name: "", @@ -73,43 +91,47 @@ export const SignupOptions = ({ resolver: zodResolver(ZSignupInput), }); - const router = useRouter(); - - const nameRef = useRef(null); - const handleSubmit = async (data: TSignupInput) => { - if (!isValid) { - return; - } - - setSigningUp(true); - try { - await createUser(data.name, data.email, data.password, userLocale, inviteToken || ""); - const emailTokenActionResponse = await createEmailTokenAction({ email: data.email }); - if (emailTokenActionResponse?.serverError) { - toast.error(emailTokenActionResponse.serverError); - return; - } - const token = emailTokenActionResponse?.data; - const url = emailVerificationDisabled - ? `/auth/signup-without-verification-success` - : `/auth/verification-requested?token=${token}`; + const createUserResponse = await createUserAction({ + name: data.name, + email: data.email, + password: data.password, + userLocale, + inviteToken: inviteToken || "", + emailVerificationDisabled, + defaultOrganizationId, + defaultOrganizationRole, + }); - router.push(url); - } catch (e: any) { - if (setError) { - setError(e.message); + if (createUserResponse?.data) { + const emailTokenActionResponse = await createEmailTokenAction({ email: data.email }); + if (emailTokenActionResponse?.data) { + const token = emailTokenActionResponse?.data; + const url = emailVerificationDisabled + ? `/auth/signup-without-verification-success` + : `/auth/verification-requested?token=${token}`; + + router.push(url); + } else { + const errorMessage = getFormattedErrorMessage(emailTokenActionResponse); + toast.error(errorMessage); + } + } else { + const errorMessage = getFormattedErrorMessage(createUserResponse); + toast.error(errorMessage); } - setSigningUp(false); + } catch (e: any) { + toast.error(e.message); } }; return ( -
+
+

{t("auth.signup.title")}

{emailAuthEnabled && ( -
+ {showLogin && (
@@ -145,7 +167,6 @@ export const SignupOptions = ({ value={field.value} name="email" onChange={(email) => field.onChange(email)} - defaultValue={emailFromSearchParams} placeholder="work@email.com" className="bg-white" /> @@ -180,14 +201,14 @@ export const SignupOptions = ({ )} />
- +
)} {showLogin && ( @@ -198,8 +219,6 @@ export const SignupOptions = ({ type="button" onClick={() => { setShowLogin(true); - // Add a slight delay before focusing the input field to ensure it's visible - setTimeout(() => nameRef.current?.focus(), 100); }} className="h-10 w-full justify-center"> {t("auth.continue_with_email")} @@ -208,26 +227,26 @@ export const SignupOptions = ({
)} - {googleOAuthEnabled && ( - <> - - - )} - {githubOAuthEnabled && ( - <> - - - )} - {azureOAuthEnabled && ( - <> - - - )} - {oidcOAuthEnabled && ( - <> - - + {isSSOEnabled && ( + )} + +
+ {t("auth.signup.have_an_account")} +
+ + {t("auth.signup.log_in")} + +
); }; diff --git a/apps/web/modules/auth/signup/components/terms-privacy-links.tsx b/apps/web/modules/auth/signup/components/terms-privacy-links.tsx new file mode 100644 index 0000000000..35aec66c72 --- /dev/null +++ b/apps/web/modules/auth/signup/components/terms-privacy-links.tsx @@ -0,0 +1,32 @@ +import { useTranslations } from "next-intl"; +import Link from "next/link"; + +interface TermsPrivacyLinksProps { + termsUrl?: string; + privacyUrl?: string; +} + +export const TermsPrivacyLinks = ({ termsUrl, privacyUrl }: TermsPrivacyLinksProps) => { + const t = useTranslations(); + + if (!termsUrl && !privacyUrl) return null; + + return ( +
+ {t("auth.signup.terms_of_service")} +
+ {termsUrl && ( + + {t("auth.signup.terms_of_service")} + + )} + {termsUrl && privacyUrl && {t("common.and")} } + {privacyUrl && ( + + {t("auth.signup.privacy_policy")} + + )} +
+
+ ); +}; diff --git a/apps/web/modules/auth/signup/page.tsx b/apps/web/modules/auth/signup/page.tsx new file mode 100644 index 0000000000..4d947f15c7 --- /dev/null +++ b/apps/web/modules/auth/signup/page.tsx @@ -0,0 +1,60 @@ +import { FormWrapper } from "@/modules/auth/components/form-wrapper"; +import { Testimonial } from "@/modules/auth/components/testimonial"; +import { getIsMultiOrgEnabled, getIsSSOEnabled } from "@/modules/ee/license-check/lib/utils"; +import { notFound } from "next/navigation"; +import { + AZURE_OAUTH_ENABLED, + DEFAULT_ORGANIZATION_ID, + DEFAULT_ORGANIZATION_ROLE, + EMAIL_AUTH_ENABLED, + EMAIL_VERIFICATION_DISABLED, + GITHUB_OAUTH_ENABLED, + GOOGLE_OAUTH_ENABLED, + OIDC_DISPLAY_NAME, + OIDC_OAUTH_ENABLED, + PRIVACY_URL, + SIGNUP_ENABLED, + TERMS_URL, + WEBAPP_URL, +} from "@formbricks/lib/constants"; +import { findMatchingLocale } from "@formbricks/lib/utils/locale"; +import { SignupForm } from "./components/signup-form"; + +export const SignupPage = async ({ searchParams }) => { + const inviteToken = searchParams["inviteToken"] ?? null; + const [isMultOrgEnabled, isSSOEnabled] = await Promise.all([getIsMultiOrgEnabled(), getIsSSOEnabled()]); + const locale = await findMatchingLocale(); + if (!inviteToken && (!SIGNUP_ENABLED || !isMultOrgEnabled)) { + notFound(); + } + const emailFromSearchParams = searchParams["email"]; + + return ( +
+
+ +
+
+ + + +
+
+ ); +}; diff --git a/apps/web/modules/auth/verification-requested/actions.ts b/apps/web/modules/auth/verification-requested/actions.ts new file mode 100644 index 0000000000..64d279b4f8 --- /dev/null +++ b/apps/web/modules/auth/verification-requested/actions.ts @@ -0,0 +1,25 @@ +"use server"; + +import { actionClient } from "@/lib/utils/action-client"; +import { getUserByEmail } from "@/modules/auth/lib/user"; +import { sendVerificationEmail } from "@/modules/email"; +import { z } from "zod"; +import { InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors"; + +const ZResendVerificationEmailAction = z.object({ + email: z.string().max(255).email({ message: "Invalid email" }), +}); + +export const resendVerificationEmailAction = actionClient + .schema(ZResendVerificationEmailAction) + .action(async ({ parsedInput }) => { + const user = await getUserByEmail(parsedInput.email); + if (!user) { + throw new ResourceNotFoundError("user", parsedInput.email); + } + if (user.emailVerified) { + throw new InvalidInputError("Email address has already been verified"); + } + await sendVerificationEmail(user); + return { success: true }; + }); diff --git a/apps/web/app/(auth)/auth/verification-requested/components/RequestVerificationEmail.tsx b/apps/web/modules/auth/verification-requested/components/request-verification-email.tsx similarity index 64% rename from apps/web/app/(auth)/auth/verification-requested/components/RequestVerificationEmail.tsx rename to apps/web/modules/auth/verification-requested/components/request-verification-email.tsx index d293cdfcfa..399e751170 100644 --- a/apps/web/app/(auth)/auth/verification-requested/components/RequestVerificationEmail.tsx +++ b/apps/web/modules/auth/verification-requested/components/request-verification-email.tsx @@ -1,16 +1,17 @@ "use client"; +import { getFormattedErrorMessage } from "@/lib/utils/helper"; import { Button } from "@/modules/ui/components/button"; import { useTranslations } from "next-intl"; import { useEffect } from "react"; import toast from "react-hot-toast"; -import { resendVerificationEmail } from "@formbricks/lib/utils/users"; +import { resendVerificationEmailAction } from "../actions"; -interface RequestEmailVerificationProps { +interface RequestVerificationEmailProps { email: string | null; } -export const RequestVerificationEmail = ({ email }: RequestEmailVerificationProps) => { +export const RequestVerificationEmail = ({ email }: RequestVerificationEmailProps) => { const t = useTranslations(); useEffect(() => { const handleVisibilityChange = () => { @@ -27,12 +28,13 @@ export const RequestVerificationEmail = ({ email }: RequestEmailVerificationProp }, []); const requestVerificationEmail = async () => { - try { - if (!email) throw new Error("No email provided"); - await resendVerificationEmail(email); + if (!email) return toast.error(t("auth.verification-requested.no_email_provided")); + const response = await resendVerificationEmailAction({ email }); + if (response?.data) { toast.success(t("auth.verification-requested.verification_email_successfully_sent")); - } catch (e) { - toast.error(`Error: ${e.message}`); + } else { + const errorMessage = getFormattedErrorMessage(response); + toast.error(errorMessage); } }; diff --git a/apps/web/modules/auth/verification-requested/page.tsx b/apps/web/modules/auth/verification-requested/page.tsx new file mode 100644 index 0000000000..bd1c5eef1b --- /dev/null +++ b/apps/web/modules/auth/verification-requested/page.tsx @@ -0,0 +1,49 @@ +import { FormWrapper } from "@/modules/auth/components/form-wrapper"; +import { RequestVerificationEmail } from "@/modules/auth/verification-requested/components/request-verification-email"; +import { getTranslations } from "next-intl/server"; +import { getEmailFromEmailToken } from "@formbricks/lib/jwt"; +import { ZUserEmail } from "@formbricks/types/user"; + +export const VerificationRequestedPage = async ({ searchParams }) => { + const t = await getTranslations(); + try { + const email = getEmailFromEmailToken(searchParams.token); + const parsedEmail = ZUserEmail.safeParse(email); + if (parsedEmail.success) { + return ( + + <> +

+ {t("auth.verification-requested.please_confirm_your_email_address")} +

+

+ {t.rich("auth.verification-requested.we_sent_an_email_to", { + email: () => {email}, + })} + {t("auth.verification-requested.please_click_the_link_in_the_email_to_activate_your_account")} +

+
+

+ {t("auth.verification-requested.you_didnt_receive_an_email_or_your_link_expired")} +

+
+ +
+ +
+ ); + } else { + return ( + +

{t("auth.verification-requested.invalid_email_address")}

+
+ ); + } + } catch (error) { + return ( + +

{t("auth.verification-requested.invalid_token")}

+
+ ); + } +}; diff --git a/apps/web/app/(auth)/auth/verify/components/SignIn.tsx b/apps/web/modules/auth/verify/components/sign-in.tsx similarity index 100% rename from apps/web/app/(auth)/auth/verify/components/SignIn.tsx rename to apps/web/modules/auth/verify/components/sign-in.tsx diff --git a/apps/web/modules/auth/verify/page.tsx b/apps/web/modules/auth/verify/page.tsx new file mode 100644 index 0000000000..19319d2bf8 --- /dev/null +++ b/apps/web/modules/auth/verify/page.tsx @@ -0,0 +1,15 @@ +import { FormWrapper } from "@/modules/auth/components/form-wrapper"; +import { SignIn } from "@/modules/auth/verify/components/sign-in"; +import { getTranslations } from "next-intl/server"; + +export const VerifyPage = async ({ searchParams }) => { + const t = await getTranslations(); + return searchParams && searchParams.token ? ( + +

{t("auth.verify.verifying")}

+ +
+ ) : ( +

{t("auth.verify.no_token_provided")}

+ ); +}; diff --git a/apps/web/modules/ee/billing/page.tsx b/apps/web/modules/ee/billing/page.tsx index 4be1bd8391..9a1f05fe29 100644 --- a/apps/web/modules/ee/billing/page.tsx +++ b/apps/web/modules/ee/billing/page.tsx @@ -1,11 +1,11 @@ import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getRoleManagementPermission } from "@/modules/ee/license-check/lib/utils"; import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper"; import { PageHeader } from "@/modules/ui/components/page-header"; import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; import { notFound } from "next/navigation"; -import { authOptions } from "@formbricks/lib/authOptions"; import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; import { PRODUCT_FEATURE_KEYS, STRIPE_PRICE_LOOKUP_KEYS } from "@formbricks/lib/constants"; import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; diff --git a/apps/web/modules/ee/insights/experience/page.tsx b/apps/web/modules/ee/insights/experience/page.tsx index 7765239eda..1c9e9ee72e 100644 --- a/apps/web/modules/ee/insights/experience/page.tsx +++ b/apps/web/modules/ee/insights/experience/page.tsx @@ -1,9 +1,9 @@ +import { authOptions } from "@/modules/auth/lib/authOptions"; import { Dashboard } from "@/modules/ee/insights/experience/components/dashboard"; import { getIsAIEnabled } from "@/modules/ee/license-check/lib/utils"; import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper"; import { getServerSession } from "next-auth"; import { notFound } from "next/navigation"; -import { authOptions } from "@formbricks/lib/authOptions"; import { DOCUMENTS_PER_PAGE, INSIGHTS_PER_PAGE } from "@formbricks/lib/constants"; import { getEnvironment } from "@formbricks/lib/environment/service"; import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; diff --git a/apps/web/modules/ee/license-check/lib/utils.ts b/apps/web/modules/ee/license-check/lib/utils.ts index d22b05dc3a..91fa7463e5 100644 --- a/apps/web/modules/ee/license-check/lib/utils.ts +++ b/apps/web/modules/ee/license-check/lib/utils.ts @@ -81,7 +81,7 @@ const fetchLicenseForE2ETesting = async (): Promise<{ // first call const newResult = { active: true, - features: { isMultiOrgEnabled: true }, + features: { isMultiOrgEnabled: true, twoFactorAuth: true, sso: true }, lastChecked: currentTime, }; await setPreviousResult(newResult); @@ -138,7 +138,7 @@ export const getEnterpriseLicense = async (): Promise<{ if (isValid === null) { const newResult = { active: false, - features: { isMultiOrgEnabled: false }, + features: { isMultiOrgEnabled: false, twoFactorAuth: false, sso: false }, lastChecked: new Date(), }; @@ -311,6 +311,26 @@ export const getIsMultiOrgEnabled = async (): Promise => { return licenseFeatures.isMultiOrgEnabled; }; +export const getIsTwoFactorAuthEnabled = async (): Promise => { + if (E2E_TESTING) { + const previousResult = await fetchLicenseForE2ETesting(); + return previousResult && previousResult.features ? previousResult.features.twoFactorAuth : false; + } + const licenseFeatures = await getLicenseFeatures(); + if (!licenseFeatures) return false; + return licenseFeatures.twoFactorAuth; +}; + +export const getIsSSOEnabled = async (): Promise => { + if (E2E_TESTING) { + const previousResult = await fetchLicenseForE2ETesting(); + return previousResult && previousResult.features ? previousResult.features.sso : false; + } + const licenseFeatures = await getLicenseFeatures(); + if (!licenseFeatures) return false; + return licenseFeatures.sso; +}; + export const getIsOrganizationAIReady = async (billingPlan: TOrganizationBillingPlan) => { // TODO: We'll remove the IS_FORMBRICKS_CLOUD check once we have the AI feature available for self-hosted customers if (IS_FORMBRICKS_CLOUD) { diff --git a/apps/web/modules/ee/license-check/types/enterprise-license.ts b/apps/web/modules/ee/license-check/types/enterprise-license.ts index 9152670563..ee3a0a1531 100644 --- a/apps/web/modules/ee/license-check/types/enterprise-license.ts +++ b/apps/web/modules/ee/license-check/types/enterprise-license.ts @@ -6,6 +6,8 @@ export type TEnterpriseLicenseStatus = z.infer; const ZEnterpriseLicenseFeatures = z.object({ isMultiOrgEnabled: z.boolean(), + twoFactorAuth: z.boolean(), + sso: z.boolean(), }); export type TEnterpriseLicenseFeatures = z.infer; diff --git a/apps/web/modules/auth/components/SignupOptions/components/AzureButton.tsx b/apps/web/modules/ee/sso/components/azure-button.tsx similarity index 86% rename from apps/web/modules/auth/components/SignupOptions/components/AzureButton.tsx rename to apps/web/modules/ee/sso/components/azure-button.tsx index 0b7fae6602..e997f45bc1 100644 --- a/apps/web/modules/auth/components/SignupOptions/components/AzureButton.tsx +++ b/apps/web/modules/ee/sso/components/azure-button.tsx @@ -1,3 +1,5 @@ +"use client"; + import { Button } from "@/modules/ui/components/button"; import { MicrosoftIcon } from "@/modules/ui/components/icons"; import { signIn } from "next-auth/react"; @@ -5,17 +7,13 @@ import { useTranslations } from "next-intl"; import { useCallback, useEffect } from "react"; import { FORMBRICKS_LOGGED_IN_WITH_LS } from "@formbricks/lib/localStorage"; -export const AzureButton = ({ - text = "Continue with Azure", - inviteUrl, - directRedirect = false, - lastUsed, -}: { - text?: string; +interface AzureButtonProps { inviteUrl?: string | null; directRedirect?: boolean; lastUsed?: boolean; -}) => { +} + +export const AzureButton = ({ inviteUrl, directRedirect = false, lastUsed }: AzureButtonProps) => { const t = useTranslations(); const handleLogin = useCallback(async () => { if (typeof window !== "undefined") { @@ -43,7 +41,7 @@ export const AzureButton = ({ onClick={handleLogin} variant="secondary" className="relative w-full justify-center"> - {text} + {t("auth.continue_with_azure")} {lastUsed && {t("auth.last_used")}} ); diff --git a/apps/web/modules/auth/components/SignupOptions/components/GithubButton.tsx b/apps/web/modules/ee/sso/components/github-button.tsx similarity index 87% rename from apps/web/modules/auth/components/SignupOptions/components/GithubButton.tsx rename to apps/web/modules/ee/sso/components/github-button.tsx index e01e79ed03..77d88eca1b 100644 --- a/apps/web/modules/auth/components/SignupOptions/components/GithubButton.tsx +++ b/apps/web/modules/ee/sso/components/github-button.tsx @@ -6,15 +6,12 @@ import { signIn } from "next-auth/react"; import { useTranslations } from "next-intl"; import { FORMBRICKS_LOGGED_IN_WITH_LS } from "@formbricks/lib/localStorage"; -export const GithubButton = ({ - text = "Continue with Github", - inviteUrl, - lastUsed, -}: { - text?: string; +interface GithubButtonProps { inviteUrl?: string | null; lastUsed?: boolean; -}) => { +} + +export const GithubButton = ({ inviteUrl, lastUsed }: GithubButtonProps) => { const t = useTranslations(); const handleLogin = async () => { if (typeof window !== "undefined") { @@ -35,7 +32,7 @@ export const GithubButton = ({ onClick={handleLogin} variant="secondary" className="relative w-full justify-center"> - {text} + {t("auth.continue_with_github")} {lastUsed && {t("auth.last_used")}} ); diff --git a/apps/web/modules/auth/components/SignupOptions/components/GoogleButton.tsx b/apps/web/modules/ee/sso/components/google-button.tsx similarity index 87% rename from apps/web/modules/auth/components/SignupOptions/components/GoogleButton.tsx rename to apps/web/modules/ee/sso/components/google-button.tsx index f40602a7bc..f35d5902fa 100644 --- a/apps/web/modules/auth/components/SignupOptions/components/GoogleButton.tsx +++ b/apps/web/modules/ee/sso/components/google-button.tsx @@ -6,15 +6,12 @@ import { signIn } from "next-auth/react"; import { useTranslations } from "next-intl"; import { FORMBRICKS_LOGGED_IN_WITH_LS } from "@formbricks/lib/localStorage"; -export const GoogleButton = ({ - text = "Continue with Google", - inviteUrl, - lastUsed, -}: { - text?: string; +interface GoogleButtonProps { inviteUrl?: string | null; lastUsed?: boolean; -}) => { +} + +export const GoogleButton = ({ inviteUrl, lastUsed }: GoogleButtonProps) => { const t = useTranslations(); const handleLogin = async () => { if (typeof window !== "undefined") { @@ -35,7 +32,7 @@ export const GoogleButton = ({ onClick={handleLogin} variant="secondary" className="relative w-full justify-center"> - {text} + {t("auth.continue_with_google")} {lastUsed && {t("auth.last_used")}} ); diff --git a/apps/web/modules/auth/components/SignupOptions/components/OpenIdButton.tsx b/apps/web/modules/ee/sso/components/open-id-button.tsx similarity index 84% rename from apps/web/modules/auth/components/SignupOptions/components/OpenIdButton.tsx rename to apps/web/modules/ee/sso/components/open-id-button.tsx index ea475cb3da..f05eb69bcb 100644 --- a/apps/web/modules/auth/components/SignupOptions/components/OpenIdButton.tsx +++ b/apps/web/modules/ee/sso/components/open-id-button.tsx @@ -1,20 +1,19 @@ +"use client"; + import { Button } from "@/modules/ui/components/button"; import { signIn } from "next-auth/react"; import { useTranslations } from "next-intl"; import { useCallback, useEffect } from "react"; import { FORMBRICKS_LOGGED_IN_WITH_LS } from "@formbricks/lib/localStorage"; -export const OpenIdButton = ({ - text = "Continue with OpenId Connect", - inviteUrl, - directRedirect = false, - lastUsed, -}: { - text?: string; +interface OpenIdButtonProps { inviteUrl?: string | null; - directRedirect?: boolean; lastUsed?: boolean; -}) => { + directRedirect?: boolean; + text?: string; +} + +export const OpenIdButton = ({ inviteUrl, lastUsed, directRedirect = false, text }: OpenIdButtonProps) => { const t = useTranslations(); const handleLogin = useCallback(async () => { if (typeof window !== "undefined") { @@ -40,7 +39,7 @@ export const OpenIdButton = ({ onClick={handleLogin} variant="secondary" className="relative w-full justify-center"> - {text} + {text ? text : t("auth.continue_with_openid")} {lastUsed && {t("auth.last_used")}} ); diff --git a/apps/web/modules/ee/sso/components/sso-options.tsx b/apps/web/modules/ee/sso/components/sso-options.tsx new file mode 100644 index 0000000000..c0ca842ad1 --- /dev/null +++ b/apps/web/modules/ee/sso/components/sso-options.tsx @@ -0,0 +1,38 @@ +"use client"; + +import { useTranslations } from "next-intl"; +import { AzureButton } from "./azure-button"; +import { GithubButton } from "./github-button"; +import { GoogleButton } from "./google-button"; +import { OpenIdButton } from "./open-id-button"; + +interface SSOOptionsProps { + googleOAuthEnabled: boolean; + githubOAuthEnabled: boolean; + azureOAuthEnabled: boolean; + oidcOAuthEnabled: boolean; + oidcDisplayName?: string; + callbackUrl: string; +} + +export const SSOOptions = ({ + googleOAuthEnabled, + githubOAuthEnabled, + azureOAuthEnabled, + oidcOAuthEnabled, + oidcDisplayName, + callbackUrl, +}: SSOOptionsProps) => { + const t = useTranslations(); + + return ( +
+ {googleOAuthEnabled && } + {githubOAuthEnabled && } + {azureOAuthEnabled && } + {oidcOAuthEnabled && ( + + )} +
+ ); +}; diff --git a/apps/web/modules/ee/sso/lib/providers.ts b/apps/web/modules/ee/sso/lib/providers.ts new file mode 100644 index 0000000000..1891147d3c --- /dev/null +++ b/apps/web/modules/ee/sso/lib/providers.ts @@ -0,0 +1,59 @@ +import type { IdentityProvider } from "@prisma/client"; +import AzureAD from "next-auth/providers/azure-ad"; +import GitHubProvider from "next-auth/providers/github"; +import GoogleProvider from "next-auth/providers/google"; +import { + AZUREAD_CLIENT_ID, + AZUREAD_CLIENT_SECRET, + AZUREAD_TENANT_ID, + GITHUB_ID, + GITHUB_SECRET, + GOOGLE_CLIENT_ID, + GOOGLE_CLIENT_SECRET, + OIDC_CLIENT_ID, + OIDC_CLIENT_SECRET, + OIDC_DISPLAY_NAME, + OIDC_ISSUER, + OIDC_SIGNING_ALGORITHM, +} from "@formbricks/lib/constants"; + +export const getSSOProviders = () => [ + GitHubProvider({ + clientId: GITHUB_ID || "", + clientSecret: GITHUB_SECRET || "", + }), + GoogleProvider({ + clientId: GOOGLE_CLIENT_ID || "", + clientSecret: GOOGLE_CLIENT_SECRET || "", + allowDangerousEmailAccountLinking: true, + }), + AzureAD({ + clientId: AZUREAD_CLIENT_ID || "", + clientSecret: AZUREAD_CLIENT_SECRET || "", + tenantId: AZUREAD_TENANT_ID || "", + }), + { + id: "openid", + name: OIDC_DISPLAY_NAME || "OpenId", + type: "oauth" as const, + clientId: OIDC_CLIENT_ID || "", + clientSecret: OIDC_CLIENT_SECRET || "", + wellKnown: `${OIDC_ISSUER}/.well-known/openid-configuration`, + authorization: { params: { scope: "openid email profile" } }, + idToken: true, + client: { + id_token_signed_response_alg: OIDC_SIGNING_ALGORITHM || "RS256", + }, + checks: ["pkce" as const, "state" as const], + profile: (profile) => { + return { + id: profile.sub, + name: profile.name, + email: profile.email, + image: profile.picture, + }; + }, + }, +]; + +export type { IdentityProvider }; diff --git a/apps/web/modules/ee/sso/lib/sso-handlers.ts b/apps/web/modules/ee/sso/lib/sso-handlers.ts new file mode 100644 index 0000000000..10c1c70423 --- /dev/null +++ b/apps/web/modules/ee/sso/lib/sso-handlers.ts @@ -0,0 +1,119 @@ +import { getUserByEmail, updateUser } from "@/modules/auth/lib/user"; +import { createUser } from "@/modules/auth/lib/user"; +import type { IdentityProvider } from "@prisma/client"; +import type { Account } from "next-auth"; +import { prisma } from "@formbricks/database"; +import { createAccount } from "@formbricks/lib/account/service"; +import { DEFAULT_ORGANIZATION_ID, DEFAULT_ORGANIZATION_ROLE } from "@formbricks/lib/constants"; +import { createMembership } from "@formbricks/lib/membership/service"; +import { createOrganization, getOrganization } from "@formbricks/lib/organization/service"; +import { findMatchingLocale } from "@formbricks/lib/utils/locale"; +import type { TUser, TUserNotificationSettings } from "@formbricks/types/user"; + +export const handleSSOCallback = async ({ user, account }: { user: TUser; account: Account }) => { + if (!user.email || account.type !== "oauth") { + return false; + } + + if (account.provider) { + const provider = account.provider.toLowerCase().replace("-", "") as IdentityProvider; + // check if accounts for this provider / account Id already exists + const existingUserWithAccount = await prisma.user.findFirst({ + include: { + accounts: { + where: { + provider: account.provider, + }, + }, + }, + where: { + identityProvider: provider, + identityProviderAccountId: account.providerAccountId, + }, + }); + + if (existingUserWithAccount) { + // User with this provider found + // check if email still the same + if (existingUserWithAccount.email === user.email) { + return true; + } + + // user seemed to change his email within the provider + // check if user with this email already exist + // if not found just update user with new email address + // if found throw an error (TODO find better solution) + const otherUserWithEmail = await getUserByEmail(user.email); + + if (!otherUserWithEmail) { + await updateUser(existingUserWithAccount.id, { email: user.email }); + return true; + } + throw new Error( + "Looks like you updated your email somewhere else. A user with this new email exists already." + ); + } + + // There is no existing account for this identity provider / account id + // check if user account with this email already exists + // if user already exists throw error and request password login + const existingUserWithEmail = await getUserByEmail(user.email); + + if (existingUserWithEmail) { + // Sign in the user with the existing account + return true; + } + + const userProfile = await createUser({ + name: user.name || user.email.split("@")[0], + email: user.email, + emailVerified: new Date(Date.now()), + identityProvider: provider, + identityProviderAccountId: account.providerAccountId, + locale: await findMatchingLocale(), + }); + + // Default organization assignment if env variable is set + if (DEFAULT_ORGANIZATION_ID && DEFAULT_ORGANIZATION_ID.length > 0) { + // check if organization exists + let organization = await getOrganization(DEFAULT_ORGANIZATION_ID); + let isNewOrganization = false; + if (!organization) { + // create organization with id from env + organization = await createOrganization({ + id: DEFAULT_ORGANIZATION_ID, + name: userProfile.name + "'s Organization", + }); + isNewOrganization = true; + } + const role = isNewOrganization ? "owner" : DEFAULT_ORGANIZATION_ROLE || "manager"; + await createMembership(organization.id, userProfile.id, { role: role, accepted: true }); + await createAccount({ + ...account, + userId: userProfile.id, + }); + + const updatedNotificationSettings: TUserNotificationSettings = { + ...userProfile.notificationSettings, + alert: { + ...userProfile.notificationSettings?.alert, + }, + unsubscribedOrganizationIds: Array.from( + new Set([...(userProfile.notificationSettings?.unsubscribedOrganizationIds || []), organization.id]) + ), + weeklySummary: { + ...userProfile.notificationSettings?.weeklySummary, + }, + }; + + await updateUser(userProfile.id, { + notificationSettings: updatedNotificationSettings, + }); + return true; + } + // Without default organization assignment + return true; + } + + return true; +}; diff --git a/apps/web/modules/ee/teams/product-teams/page.tsx b/apps/web/modules/ee/teams/product-teams/page.tsx index ab29331a3f..0d94204d8d 100644 --- a/apps/web/modules/ee/teams/product-teams/page.tsx +++ b/apps/web/modules/ee/teams/product-teams/page.tsx @@ -1,4 +1,5 @@ import { ProductConfigNavigation } from "@/app/(app)/environments/[environmentId]/product/components/ProductConfigNavigation"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getMultiLanguagePermission, getRoleManagementPermission, @@ -8,7 +9,6 @@ import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper import { PageHeader } from "@/modules/ui/components/page-header"; import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; -import { authOptions } from "@formbricks/lib/authOptions"; import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; import { getAccessFlags } from "@formbricks/lib/membership/utils"; import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service"; diff --git a/apps/web/modules/ee/teams/team-details/page.tsx b/apps/web/modules/ee/teams/team-details/page.tsx index 2be285b820..8936d8ad1a 100644 --- a/apps/web/modules/ee/teams/team-details/page.tsx +++ b/apps/web/modules/ee/teams/team-details/page.tsx @@ -1,3 +1,4 @@ +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getRoleManagementPermission } from "@/modules/ee/license-check/lib/utils"; import { getTeamRoleByTeamIdUserId } from "@/modules/ee/teams/lib/roles"; import { DetailsView } from "@/modules/ee/teams/team-details/components/details-view"; @@ -12,7 +13,6 @@ import { PageContentWrapper } from "@/modules/ui/components/page-content-wrapper import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; import { notFound } from "next/navigation"; -import { authOptions } from "@formbricks/lib/authOptions"; import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; import { getAccessFlags } from "@formbricks/lib/membership/utils"; import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service"; diff --git a/apps/web/modules/ee/teams/team-list/page.tsx b/apps/web/modules/ee/teams/team-list/page.tsx index 0ac21e1740..3d9805398c 100644 --- a/apps/web/modules/ee/teams/team-list/page.tsx +++ b/apps/web/modules/ee/teams/team-list/page.tsx @@ -1,4 +1,5 @@ import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(organization)/components/OrganizationSettingsNavbar"; +import { authOptions } from "@/modules/auth/lib/authOptions"; import { getRoleManagementPermission } from "@/modules/ee/license-check/lib/utils"; import { TeamsView } from "@/modules/ee/teams/team-list/components/teams-view"; import { getTeams } from "@/modules/ee/teams/team-list/lib/teams"; @@ -7,7 +8,6 @@ import { PageHeader } from "@/modules/ui/components/page-header"; import { getServerSession } from "next-auth"; import { getTranslations } from "next-intl/server"; import { notFound } from "next/navigation"; -import { authOptions } from "@formbricks/lib/authOptions"; import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; import { getAccessFlags } from "@formbricks/lib/membership/utils"; diff --git a/apps/web/modules/ee/two-factor-auth/actions.ts b/apps/web/modules/ee/two-factor-auth/actions.ts new file mode 100644 index 0000000000..404dad84c3 --- /dev/null +++ b/apps/web/modules/ee/two-factor-auth/actions.ts @@ -0,0 +1,52 @@ +"use server"; + +import { authenticatedActionClient } from "@/lib/utils/action-client"; +import { getIsTwoFactorAuthEnabled } from "@/modules/ee/license-check/lib/utils"; +import { z } from "zod"; +import { OperationNotAllowedError } from "@formbricks/types/errors"; +import { disableTwoFactorAuth, enableTwoFactorAuth, setupTwoFactorAuth } from "./lib/two-factor-auth"; + +const ZSetupTwoFactorAuthAction = z.object({ + password: z.string(), +}); + +export const setupTwoFactorAuthAction = authenticatedActionClient + .schema(ZSetupTwoFactorAuthAction) + .action(async ({ parsedInput, ctx }) => { + const isTwoFactorAuthEnabled = await getIsTwoFactorAuthEnabled(); + if (!isTwoFactorAuthEnabled) { + throw new OperationNotAllowedError("Two factor auth is not available on your instance"); + } + return await setupTwoFactorAuth(ctx.user.id, parsedInput.password); + }); + +const ZEnableTwoFactorAuthAction = z.object({ + code: z.string(), +}); + +export const enableTwoFactorAuthAction = authenticatedActionClient + .schema(ZEnableTwoFactorAuthAction) + .action(async ({ parsedInput, ctx }) => { + const isTwoFactorAuthEnabled = await getIsTwoFactorAuthEnabled(); + if (!isTwoFactorAuthEnabled) { + throw new OperationNotAllowedError("Two factor auth is not available on your instance"); + } + return await enableTwoFactorAuth(ctx.user.id, parsedInput.code); + }); + +const ZDisableTwoFactorAuthAction = z + .object({ + code: z.string().optional(), + password: z.string(), + backupCode: z.string().optional(), + }) + .refine( + (data) => data.password !== undefined || data.code !== undefined, + "Please provide either the code or the backup code" + ); + +export const disableTwoFactorAuthAction = authenticatedActionClient + .schema(ZDisableTwoFactorAuthAction) + .action(async ({ parsedInput, ctx }) => { + return await disableTwoFactorAuth(ctx.user.id, parsedInput); + }); diff --git a/apps/web/modules/ee/two-factor-auth/components/confirm-password-form.tsx b/apps/web/modules/ee/two-factor-auth/components/confirm-password-form.tsx new file mode 100644 index 0000000000..2eaf5ced67 --- /dev/null +++ b/apps/web/modules/ee/two-factor-auth/components/confirm-password-form.tsx @@ -0,0 +1,111 @@ +"use client"; + +import { getFormattedErrorMessage } from "@/lib/utils/helper"; +import { setupTwoFactorAuthAction } from "@/modules/ee/two-factor-auth/actions"; +import { Button } from "@/modules/ui/components/button"; +import { FormControl, FormError, FormField, FormItem } from "@/modules/ui/components/form"; +import { PasswordInput } from "@/modules/ui/components/password-input"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useTranslations } from "next-intl"; +import { SubmitHandler, useForm } from "react-hook-form"; +import { FormProvider } from "react-hook-form"; +import toast from "react-hot-toast"; +import { z } from "zod"; +import { ZUserPassword } from "@formbricks/types/user"; +import { EnableTwoFactorModalStep } from "./enable-two-factor-modal"; + +const ZConfirmPasswordFormState = z.object({ + password: ZUserPassword, +}); +type TConfirmPasswordFormState = z.infer; + +interface ConfirmPasswordFormProps { + setCurrentStep: (step: EnableTwoFactorModalStep) => void; + setBackupCodes: (codes: string[]) => void; + setDataUri: (dataUri: string) => void; + setSecret: (secret: string) => void; + setOpen: (open: boolean) => void; +} +export const ConfirmPasswordForm = ({ + setBackupCodes, + setCurrentStep, + setDataUri, + setSecret, + setOpen, +}: ConfirmPasswordFormProps) => { + const form = useForm({ + defaultValues: { + password: "", + }, + resolver: zodResolver(ZConfirmPasswordFormState), + }); + const { handleSubmit } = form; + const t = useTranslations(); + + const onSubmit: SubmitHandler = async (data) => { + const setupTwoFactorAuthResponse = await setupTwoFactorAuthAction({ password: data.password }); + if (setupTwoFactorAuthResponse?.data) { + const { backupCodes, dataUri, secret } = setupTwoFactorAuthResponse.data; + setBackupCodes(backupCodes); + setDataUri(dataUri); + setSecret(secret); + setCurrentStep("scanQRCode"); + } else { + const errorMessage = getFormattedErrorMessage(setupTwoFactorAuthResponse); + toast.error(errorMessage); + } + }; + + return ( + +
+

+ {t("environments.settings.profile.two_factor_authentication")} +

+

+ {t("environments.settings.profile.confirm_your_current_password_to_get_started")} +

+
+
+
+ + ( + + + + field.onChange(password)} + value={field.value} + className="focus:border-brand-dark focus:ring-brand-dark block w-full rounded-md border-slate-300 shadow-sm sm:text-sm" + /> + {error?.message && {error.message}} + + + + )} + /> +
+ +
+ + + +
+
+
+ ); +}; diff --git a/apps/web/modules/ee/two-factor-auth/components/disable-two-factor-modal.tsx b/apps/web/modules/ee/two-factor-auth/components/disable-two-factor-modal.tsx new file mode 100644 index 0000000000..3c2c6d9ecb --- /dev/null +++ b/apps/web/modules/ee/two-factor-auth/components/disable-two-factor-modal.tsx @@ -0,0 +1,205 @@ +"use client"; + +import { getFormattedErrorMessage } from "@/lib/utils/helper"; +import { disableTwoFactorAuthAction } from "@/modules/ee/two-factor-auth/actions"; +import { Button } from "@/modules/ui/components/button"; +import { FormControl, FormError, FormField, FormItem } from "@/modules/ui/components/form"; +import { Input } from "@/modules/ui/components/input"; +import { Modal } from "@/modules/ui/components/modal"; +import { OTPInput } from "@/modules/ui/components/otp-input"; +import { PasswordInput } from "@/modules/ui/components/password-input"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useTranslations } from "next-intl"; +import { useRouter } from "next/navigation"; +import React, { useState } from "react"; +import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; +import toast from "react-hot-toast"; +import { z } from "zod"; +import { ZUserPassword } from "@formbricks/types/user"; + +const ZDisableTwoFactorFormState = z + .object({ + password: ZUserPassword, + code: z.string().optional(), + backupCode: z.string().optional(), + }) + .refine((data) => (!!data.code && !data.backupCode) || (!data.code && !!data.backupCode), { + message: "Please provide either the code OR the backup code", + path: ["code"], + }); + +type TDisableTwoFactorFormState = z.infer; + +interface DisableTwoFactorModalProps { + open: boolean; + setOpen: (open: boolean) => void; +} + +export const DisableTwoFactorModal = ({ open, setOpen }: DisableTwoFactorModalProps) => { + const router = useRouter(); + const form = useForm({ + defaultValues: { + password: "", + code: "", + backupCode: "", + }, + resolver: zodResolver(ZDisableTwoFactorFormState), + }); + const t = useTranslations(); + const [backupCodeInputVisible, setBackupCodeInputVisible] = useState(false); + + const onSubmit: SubmitHandler = async (data) => { + const { code, password, backupCode } = data; + const disableTwoFactorAuthResponse = await disableTwoFactorAuthAction({ code, password, backupCode }); + if (disableTwoFactorAuthResponse?.data) { + toast.success(disableTwoFactorAuthResponse.data.message); + router.refresh(); + form.reset(); + setOpen(false); + } else { + const errorMessage = getFormattedErrorMessage(disableTwoFactorAuthResponse); + toast.error(errorMessage); + } + }; + + return ( + { + form.reset(); + setOpen(false); + }} + noPadding> + +
+
+

+ {t("environments.settings.profile.disable_two_factor_authentication")} +

+

+ {t("environments.settings.profile.disable_two_factor_authentication_description")} +

+
+ +
+
+ + ( + + + + field.onChange(password)} + value={field.value} + className="focus:border-brand-dark focus:ring-brand-dark block w-full rounded-md border-slate-300 shadow-sm sm:text-sm" + /> + {error?.message && {error.message}} + + + + )} + /> +
+ +
+
+ + +

+ {backupCodeInputVisible + ? t( + "environments.settings.profile.each_backup_code_can_be_used_exactly_once_to_grant_access_without_your_authenticator" + ) + : t( + "environments.settings.profile.two_factor_authentication_enabled_please_enter_the_six_digit_code_from_your_authenticator_app" + )} +

+
+ + {backupCodeInputVisible ? ( + ( + + + + + {error?.message && {error.message}} + + + + )} + /> + ) : ( + ( + + + + + {error?.message && {error.message}} + + + + )} + /> + )} +
+ +
+
+ +
+
+ + + +
+
+
+
+
+
+ ); +}; diff --git a/apps/web/modules/ee/two-factor-auth/components/display-backup-codes.tsx b/apps/web/modules/ee/two-factor-auth/components/display-backup-codes.tsx new file mode 100644 index 0000000000..054f6e8bd7 --- /dev/null +++ b/apps/web/modules/ee/two-factor-auth/components/display-backup-codes.tsx @@ -0,0 +1,70 @@ +import { Button } from "@/modules/ui/components/button"; +import { useTranslations } from "next-intl"; +import { toast } from "react-hot-toast"; + +interface DisplayBackupCodesProps { + backupCodes: string[]; + setOpen: (open: boolean) => void; +} + +export const DisplayBackupCodes = ({ backupCodes, setOpen }: DisplayBackupCodesProps) => { + const t = useTranslations(); + const formatBackupCode = (code: string) => `${code.slice(0, 5)}-${code.slice(5, 10)}`; + + const handleDownloadBackupCode = () => { + const formattedCodes = backupCodes.map((code) => formatBackupCode(code)).join("\n"); + const blob = new Blob([formattedCodes], { type: "text/plain" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "formbricks-backup-codes.txt"; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }; + + return ( +
+
+

+ {t("environments.settings.profile.enable_two_factor_authentication")} +

+

+ {t("environments.settings.profile.save_the_following_backup_codes_in_a_safe_place")} +

+
+ +
+ {backupCodes.map((code) => ( +

+ {formatBackupCode(code)} +

+ ))} +
+ +
+ + + + + +
+
+ ); +}; diff --git a/apps/web/modules/ee/two-factor-auth/components/enable-two-factor-modal.tsx b/apps/web/modules/ee/two-factor-auth/components/enable-two-factor-modal.tsx new file mode 100644 index 0000000000..7aff2edb78 --- /dev/null +++ b/apps/web/modules/ee/two-factor-auth/components/enable-two-factor-modal.tsx @@ -0,0 +1,60 @@ +"use client"; + +import { ConfirmPasswordForm } from "@/modules/ee/two-factor-auth/components/confirm-password-form"; +import { DisplayBackupCodes } from "@/modules/ee/two-factor-auth/components/display-backup-codes"; +import { EnterCode } from "@/modules/ee/two-factor-auth/components/enter-code"; +import { ScanQRCode } from "@/modules/ee/two-factor-auth/components/scan-qr-code"; +import { Modal } from "@/modules/ui/components/modal"; +import { useRouter } from "next/navigation"; +import React, { useState } from "react"; + +export type EnableTwoFactorModalStep = "confirmPassword" | "scanQRCode" | "enterCode" | "backupCodes"; + +interface EnableTwoFactorModalProps { + open: boolean; + setOpen: (open: boolean) => void; +} + +export const EnableTwoFactorModal = ({ open, setOpen }: EnableTwoFactorModalProps) => { + const router = useRouter(); + const [currentStep, setCurrentStep] = useState("confirmPassword"); + const [backupCodes, setBackupCodes] = useState([]); + const [dataUri, setDataUri] = useState(""); + const [secret, setSecret] = useState(""); + + const refreshData = () => { + router.refresh(); + }; + + const resetState = () => { + setCurrentStep("confirmPassword"); + setBackupCodes([]); + setDataUri(""); + setSecret(""); + setOpen(false); + }; + + return ( + resetState()} noPadding> + {currentStep === "confirmPassword" && ( + + )} + + {currentStep === "scanQRCode" && ( + + )} + + {currentStep === "enterCode" && ( + + )} + + {currentStep === "backupCodes" && } + + ); +}; diff --git a/apps/web/modules/ee/two-factor-auth/components/enter-code.tsx b/apps/web/modules/ee/two-factor-auth/components/enter-code.tsx new file mode 100644 index 0000000000..b97d51a11b --- /dev/null +++ b/apps/web/modules/ee/two-factor-auth/components/enter-code.tsx @@ -0,0 +1,101 @@ +import { getFormattedErrorMessage } from "@/lib/utils/helper"; +import { enableTwoFactorAuthAction } from "@/modules/ee/two-factor-auth/actions"; +import { Button } from "@/modules/ui/components/button"; +import { OTPInput } from "@/modules/ui/components/otp-input"; +import { useTranslations } from "next-intl"; +import { Controller, SubmitHandler, useForm } from "react-hook-form"; +import toast from "react-hot-toast"; +import { z } from "zod"; +import { EnableTwoFactorModalStep } from "./enable-two-factor-modal"; + +interface EnterCodeProps { + setCurrentStep: (step: EnableTwoFactorModalStep) => void; + setOpen: (open: boolean) => void; + refreshData: () => void; +} + +const ZEnterCodeFormState = z.object({ + code: z.string().length(6), +}); + +type TEnterCodeFormState = z.infer; + +export const EnterCode = ({ setCurrentStep, setOpen, refreshData }: EnterCodeProps) => { + const t = useTranslations(); + const { control, handleSubmit, formState } = useForm({ + defaultValues: { + code: "", + }, + }); + + const onSubmit: SubmitHandler = async (data) => { + try { + const enableTwoFactorAuthResponse = await enableTwoFactorAuthAction({ code: data.code }); + if (enableTwoFactorAuthResponse?.data) { + toast.success(enableTwoFactorAuthResponse.data.message); + setCurrentStep("backupCodes"); + + // refresh data to update the UI + refreshData(); + } else { + const errorMessage = getFormattedErrorMessage(enableTwoFactorAuthResponse); + toast.error(errorMessage); + } + } catch (err) { + toast.error(err.message); + } + }; + + return ( + <> +
+
+

+ {t("environments.settings.profile.enable_two_factor_authentication")} +

+

+ {t("environments.settings.profile.enter_the_code_from_your_authenticator_app_below")} +

+
+ +
+
+ + ( + <> + + + {errors.code && ( +

+ {errors.code.message} +

+ )} + + )} + /> +
+ +
+ + + +
+
+
+ + ); +}; diff --git a/apps/web/modules/ee/two-factor-auth/components/scan-qr-code.tsx b/apps/web/modules/ee/two-factor-auth/components/scan-qr-code.tsx new file mode 100644 index 0000000000..8553826109 --- /dev/null +++ b/apps/web/modules/ee/two-factor-auth/components/scan-qr-code.tsx @@ -0,0 +1,59 @@ +import { Button } from "@/modules/ui/components/button"; +import { CopyIcon } from "lucide-react"; +import { useTranslations } from "next-intl"; +import Image from "next/image"; +import { toast } from "react-hot-toast"; +import { EnableTwoFactorModalStep } from "./enable-two-factor-modal"; + +interface ScanQRCodeProps { + setCurrentStep: (step: EnableTwoFactorModalStep) => void; + dataUri: string; + secret: string; + setOpen: (open: boolean) => void; +} + +export const ScanQRCode = ({ dataUri, secret, setCurrentStep, setOpen }: ScanQRCodeProps) => { + const t = useTranslations(); + return ( +
+
+

+ {t("environments.settings.profile.enable_two_factor_authentication")} +

+

+ {t("environments.settings.profile.scan_the_qr_code_below_with_your_authenticator_app")} +

+
+ +
+ QR code +

+ {t("environments.settings.profile.or_enter_the_following_code_manually")} +

+
+

{secret}

+ +
+
+ +
+ + + +
+
+ ); +}; diff --git a/apps/web/modules/auth/components/SigninForm/components/TwoFactorBackup.tsx b/apps/web/modules/ee/two-factor-auth/components/two-factor-backup.tsx similarity index 100% rename from apps/web/modules/auth/components/SigninForm/components/TwoFactorBackup.tsx rename to apps/web/modules/ee/two-factor-auth/components/two-factor-backup.tsx diff --git a/apps/web/modules/auth/components/SigninForm/components/TwoFactor.tsx b/apps/web/modules/ee/two-factor-auth/components/two-factor.tsx similarity index 100% rename from apps/web/modules/auth/components/SigninForm/components/TwoFactor.tsx rename to apps/web/modules/ee/two-factor-auth/components/two-factor.tsx diff --git a/packages/lib/auth/service.ts b/apps/web/modules/ee/two-factor-auth/lib/two-factor-auth.ts similarity index 69% rename from packages/lib/auth/service.ts rename to apps/web/modules/ee/two-factor-auth/lib/two-factor-auth.ts index 36ec920446..948bff8585 100644 --- a/packages/lib/auth/service.ts +++ b/apps/web/modules/ee/two-factor-auth/lib/two-factor-auth.ts @@ -1,12 +1,13 @@ +import { totpAuthenticatorCheck } from "@/modules/auth/lib/totp"; +import { verifyPassword } from "@/modules/auth/lib/utils"; import crypto from "crypto"; import { authenticator } from "otplib"; import qrcode from "qrcode"; import { prisma } from "@formbricks/database"; -import { verifyPassword } from "../auth"; -import { ENCRYPTION_KEY } from "../constants"; -import { symmetricDecrypt, symmetricEncrypt } from "../crypto"; -import { totpAuthenticatorCheck } from "../totp"; -import { userCache } from "../user/cache"; +import { ENCRYPTION_KEY } from "@formbricks/lib/constants"; +import { symmetricDecrypt, symmetricEncrypt } from "@formbricks/lib/crypto"; +import { userCache } from "@formbricks/lib/user/cache"; +import { InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors"; export const setupTwoFactorAuth = async ( userId: string, @@ -31,21 +32,21 @@ export const setupTwoFactorAuth = async ( }); if (!user) { - throw new Error("User not found"); + throw new ResourceNotFoundError("user", userId); } if (!user.password) { - throw new Error("User does not have a password set"); + throw new InvalidInputError("User does not have a password set"); } if (user.identityProvider !== "email") { - throw new Error("Third party login is already enabled"); + throw new InvalidInputError("Third party login is already enabled"); } const isCorrectPassword = await verifyPassword(password, user.password); if (!isCorrectPassword) { - throw new Error("Incorrect password"); + throw new InvalidInputError("Incorrect password"); } if (!ENCRYPTION_KEY) { @@ -78,23 +79,23 @@ export const enableTwoFactorAuth = async (id: string, code: string) => { }); if (!user) { - throw new Error("User not found"); + throw new ResourceNotFoundError("user", id); } if (!user.password) { - throw new Error("User does not have a password set"); + throw new InvalidInputError("User does not have a password set"); } if (user.identityProvider !== "email") { - throw new Error("Third party login is already enabled"); + throw new InvalidInputError("Third party login is already enabled"); } if (user.twoFactorEnabled) { - throw new Error("Two factor authentication is already enabled"); + throw new InvalidInputError("Two factor authentication is already enabled"); } if (!user.twoFactorSecret) { - throw new Error("Two factor setup has not been completed"); + throw new InvalidInputError("Two factor setup has not been completed"); } if (!ENCRYPTION_KEY) { @@ -103,12 +104,12 @@ export const enableTwoFactorAuth = async (id: string, code: string) => { const secret = symmetricDecrypt(user.twoFactorSecret, ENCRYPTION_KEY); if (secret.length !== 32) { - throw new Error("Invalid secret"); + throw new InvalidInputError("Invalid secret"); } const isValidCode = totpAuthenticatorCheck(code, secret); if (!isValidCode) { - throw new Error("Invalid code"); + throw new InvalidInputError("Invalid code"); } await prisma.user.update({ @@ -130,7 +131,7 @@ export const enableTwoFactorAuth = async (id: string, code: string) => { }; type TDisableTwoFactorAuthParams = { - code: string; + code?: string; password: string; backupCode?: string; }; @@ -143,26 +144,26 @@ export const disableTwoFactorAuth = async (id: string, params: TDisableTwoFactor }); if (!user) { - throw new Error("User not found"); + throw new ResourceNotFoundError("user", id); } if (!user.password) { - throw new Error("User does not have a password set"); + throw new InvalidInputError("User does not have a password set"); } if (!user.twoFactorEnabled) { - throw new Error("Two factor authentication is not enabled"); + throw new InvalidInputError("Two factor authentication is not enabled"); } if (user.identityProvider !== "email") { - throw new Error("Third party login is already enabled"); + throw new InvalidInputError("Third party login is already enabled"); } const { code, password, backupCode } = params; const isCorrectPassword = await verifyPassword(password, user.password); if (!isCorrectPassword) { - throw new Error("Incorrect password"); + throw new InvalidInputError("Incorrect password"); } // if user has 2fa and using backup code @@ -172,7 +173,7 @@ export const disableTwoFactorAuth = async (id: string, params: TDisableTwoFactor } if (!user.backupCodes) { - throw new Error("Missing backup codes"); + throw new InvalidInputError("Missing backup codes"); } const backupCodes = JSON.parse(symmetricDecrypt(user.backupCodes, ENCRYPTION_KEY)); @@ -180,7 +181,7 @@ export const disableTwoFactorAuth = async (id: string, params: TDisableTwoFactor // check if user-supplied code matches one const index = backupCodes.indexOf(backupCode.replaceAll("-", "")); if (index === -1) { - throw new Error("Incorrect backup code"); + throw new InvalidInputError("Incorrect backup code"); } // we delete all stored backup codes at the end, no need to do this here @@ -188,11 +189,11 @@ export const disableTwoFactorAuth = async (id: string, params: TDisableTwoFactor // if user has 2fa and NOT using backup code, try totp } else if (user.twoFactorEnabled) { if (!code) { - throw new Error("Second factor required"); + throw new InvalidInputError("Two factor code required"); } if (!user.twoFactorSecret) { - throw new Error("Two factor setup has not been completed"); + throw new InvalidInputError("Two factor setup has not been completed"); } if (!ENCRYPTION_KEY) { @@ -201,12 +202,12 @@ export const disableTwoFactorAuth = async (id: string, params: TDisableTwoFactor const secret = symmetricDecrypt(user.twoFactorSecret, ENCRYPTION_KEY); if (secret.length !== 32) { - throw new Error("Invalid secret"); + throw new InvalidInputError("Invalid secret"); } const isValidCode = totpAuthenticatorCheck(code, secret); if (!isValidCode) { - throw new Error("Invalid code"); + throw new InvalidInputError("Invalid code"); } } diff --git a/apps/web/modules/email/index.tsx b/apps/web/modules/email/index.tsx index a13f7c3bfb..b4d40815c9 100644 --- a/apps/web/modules/email/index.tsx +++ b/apps/web/modules/email/index.tsx @@ -17,6 +17,7 @@ import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/ser import type { TLinkSurveyEmailData } from "@formbricks/types/email"; import type { TResponse } from "@formbricks/types/responses"; import type { TSurvey } from "@formbricks/types/surveys/types"; +import { TUserEmail, TUserLocale } from "@formbricks/types/user"; import type { TWeeklySummaryNotificationResponse } from "@formbricks/types/weekly-summary"; import { ForgotPasswordEmail } from "./emails/auth/forgot-password-email"; import { PasswordResetNotifyEmail } from "./emails/auth/password-reset-notify-email"; @@ -42,12 +43,6 @@ interface SendEmailDataProps { html: string; } -interface TEmailUser { - id: string; - email: string; - locale: string; -} - const getEmailSubject = (productName: string): string => { return `${productName} User Insights - Last Week by Formbricks`; }; @@ -75,26 +70,38 @@ export const sendEmail = async (emailData: SendEmailDataProps): Promise => await transporter.sendMail({ ...emailDefaults, ...emailData }); }; -export const sendVerificationEmail = async (user: TEmailUser): Promise => { - const token = createToken(user.id, user.email, { +export const sendVerificationEmail = async ({ + id, + email, + locale, +}: { + id: string; + email: TUserEmail; + locale: TUserLocale; +}): Promise => { + const token = createToken(id, email, { expiresIn: "1d", }); const verifyLink = `${WEBAPP_URL}/auth/verify?token=${encodeURIComponent(token)}`; const verificationRequestLink = `${WEBAPP_URL}/auth/verification-requested?token=${encodeURIComponent(token)}`; - const html = await render(VerificationEmail({ verificationRequestLink, verifyLink, locale: user.locale })); + const html = await render(VerificationEmail({ verificationRequestLink, verifyLink, locale })); await sendEmail({ - to: user.email, - subject: translateEmailText("verification_email_subject", user.locale), + to: email, + subject: translateEmailText("verification_email_subject", locale), html, }); }; -export const sendForgotPasswordEmail = async (user: TEmailUser, locale: string): Promise => { +export const sendForgotPasswordEmail = async (user: { + id: string; + email: TUserEmail; + locale: TUserLocale; +}): Promise => { const token = createToken(user.id, user.email, { expiresIn: "1d", }); const verifyLink = `${WEBAPP_URL}/auth/forgot-password/reset?token=${encodeURIComponent(token)}`; - const html = await render(ForgotPasswordEmail({ verifyLink, locale })); + const html = await render(ForgotPasswordEmail({ verifyLink, locale: user.locale })); await sendEmail({ to: user.email, subject: "Reset your Formbricks password", @@ -102,7 +109,10 @@ export const sendForgotPasswordEmail = async (user: TEmailUser, locale: string): }); }; -export const sendPasswordResetNotifyEmail = async (user: TEmailUser): Promise => { +export const sendPasswordResetNotifyEmail = async (user: { + email: string; + locale: TUserLocale; +}): Promise => { const html = await render(PasswordResetNotifyEmail({ locale: user.locale })); await sendEmail({ to: user.email, diff --git a/apps/web/playwright/signup.spec.ts b/apps/web/playwright/signup.spec.ts index 9eccf5e109..c41076da43 100644 --- a/apps/web/playwright/signup.spec.ts +++ b/apps/web/playwright/signup.spec.ts @@ -29,7 +29,7 @@ test.describe("Email Signup Flow Test", async () => { await page.getByPlaceholder("work@email.com").press("Tab"); await page.fill('input[name="password"]', password); await page.press('input[name="password"]', "Enter"); - let alertMessage = "user with this email address already exists"; + let alertMessage = "User with this email already exists"; await (await page.waitForSelector(`text=${alertMessage}`)).isVisible(); }); diff --git a/packages/lib/authOptions.ts b/packages/lib/authOptions.ts deleted file mode 100644 index e5e05b0286..0000000000 --- a/packages/lib/authOptions.ts +++ /dev/null @@ -1,368 +0,0 @@ -import type { IdentityProvider } from "@prisma/client"; -import type { NextAuthOptions } from "next-auth"; -import AzureAD from "next-auth/providers/azure-ad"; -import CredentialsProvider from "next-auth/providers/credentials"; -import GitHubProvider from "next-auth/providers/github"; -import GoogleProvider from "next-auth/providers/google"; -import { prisma } from "@formbricks/database"; -import { TUserNotificationSettings } from "@formbricks/types/user"; -import { createAccount } from "./account/service"; -import { verifyPassword } from "./auth/utils"; -import { - AZUREAD_CLIENT_ID, - AZUREAD_CLIENT_SECRET, - AZUREAD_TENANT_ID, - DEFAULT_ORGANIZATION_ID, - DEFAULT_ORGANIZATION_ROLE, - EMAIL_VERIFICATION_DISABLED, - ENCRYPTION_KEY, - GITHUB_ID, - GITHUB_SECRET, - GOOGLE_CLIENT_ID, - GOOGLE_CLIENT_SECRET, - OIDC_CLIENT_ID, - OIDC_CLIENT_SECRET, - OIDC_DISPLAY_NAME, - OIDC_ISSUER, - OIDC_SIGNING_ALGORITHM, -} from "./constants"; -import { symmetricDecrypt, symmetricEncrypt } from "./crypto"; -import { verifyToken } from "./jwt"; -import { createMembership } from "./membership/service"; -import { createOrganization, getOrganization } from "./organization/service"; -import { createUser, getUserByEmail, updateUser } from "./user/service"; -import { findMatchingLocale } from "./utils/locale"; - -export const authOptions: NextAuthOptions = { - providers: [ - CredentialsProvider({ - id: "credentials", - // The name to display on the sign in form (e.g. "Sign in with...") - name: "Credentials", - // The credentials is used to generate a suitable form on the sign in page. - // You can specify whatever fields you are expecting to be submitted. - // e.g. domain, username, password, 2FA token, etc. - // You can pass any HTML attribute to the tag through the object. - credentials: { - email: { - label: "Email Address", - type: "email", - placeholder: "Your email address", - }, - password: { - label: "Password", - type: "password", - placeholder: "Your password", - }, - totpCode: { label: "Two-factor Code", type: "input", placeholder: "Code from authenticator app" }, - backupCode: { label: "Backup Code", type: "input", placeholder: "Two-factor backup code" }, - }, - async authorize(credentials, _req) { - let user; - try { - user = await prisma.user.findUnique({ - where: { - email: credentials?.email, - }, - }); - } catch (e) { - console.error(e); - throw Error("Internal server error. Please try again later"); - } - - if (!user || !credentials) { - throw new Error("Invalid credentials"); - } - if (!user.password) { - throw new Error("Invalid credentials"); - } - - const isValid = await verifyPassword(credentials.password, user.password); - - if (!isValid) { - throw new Error("Invalid credentials"); - } - - if (user.twoFactorEnabled && credentials.backupCode) { - if (!ENCRYPTION_KEY) { - console.error("Missing encryption key; cannot proceed with backup code login."); - throw new Error("Internal Server Error"); - } - - if (!user.backupCodes) throw new Error("No backup codes found"); - - const backupCodes = JSON.parse(symmetricDecrypt(user.backupCodes, ENCRYPTION_KEY)); - - // check if user-supplied code matches one - const index = backupCodes.indexOf(credentials.backupCode.replaceAll("-", "")); - if (index === -1) throw new Error("Invalid backup code"); - - // delete verified backup code and re-encrypt remaining - backupCodes[index] = null; - await prisma.user.update({ - where: { - id: user.id, - }, - data: { - backupCodes: symmetricEncrypt(JSON.stringify(backupCodes), ENCRYPTION_KEY), - }, - }); - } else if (user.twoFactorEnabled) { - if (!credentials.totpCode) { - throw new Error("second factor required"); - } - - if (!user.twoFactorSecret) { - throw new Error("Internal Server Error"); - } - - if (!ENCRYPTION_KEY) { - throw new Error("Internal Server Error"); - } - - const secret = symmetricDecrypt(user.twoFactorSecret, ENCRYPTION_KEY); - if (secret.length !== 32) { - throw new Error("Internal Server Error"); - } - - const isValidToken = (await import("./totp")).totpAuthenticatorCheck(credentials.totpCode, secret); - if (!isValidToken) { - throw new Error("Invalid second factor code"); - } - } - - return { - id: user.id, - email: user.email, - emailVerified: user.emailVerified, - imageUrl: user.imageUrl, - }; - }, - }), - CredentialsProvider({ - id: "token", - // The name to display on the sign in form (e.g. "Sign in with...") - name: "Token", - // The credentials is used to generate a suitable form on the sign in page. - // You can specify whatever fields you are expecting to be submitted. - // e.g. domain, username, password, 2FA token, etc. - // You can pass any HTML attribute to the tag through the object. - credentials: { - token: { - label: "Verification Token", - type: "string", - }, - }, - async authorize(credentials, _req) { - let user; - try { - if (!credentials?.token) { - throw new Error("Token not found"); - } - const { id } = await verifyToken(credentials?.token); - user = await prisma.user.findUnique({ - where: { - id: id, - }, - }); - } catch (e) { - console.error(e); - throw new Error("Either a user does not match the provided token or the token is invalid"); - } - - if (!user) { - throw new Error("Either a user does not match the provided token or the token is invalid"); - } - - if (user.emailVerified) { - throw new Error("Email already verified"); - } - - user = await updateUser(user.id, { emailVerified: new Date() }); - - return user; - }, - }), - GitHubProvider({ - clientId: GITHUB_ID || "", - clientSecret: GITHUB_SECRET || "", - }), - GoogleProvider({ - clientId: GOOGLE_CLIENT_ID || "", - clientSecret: GOOGLE_CLIENT_SECRET || "", - allowDangerousEmailAccountLinking: true, - }), - AzureAD({ - clientId: AZUREAD_CLIENT_ID || "", - clientSecret: AZUREAD_CLIENT_SECRET || "", - tenantId: AZUREAD_TENANT_ID || "", - }), - { - id: "openid", - name: OIDC_DISPLAY_NAME || "OpenId", - type: "oauth", - clientId: OIDC_CLIENT_ID || "", - clientSecret: OIDC_CLIENT_SECRET || "", - wellKnown: `${OIDC_ISSUER}/.well-known/openid-configuration`, - authorization: { params: { scope: "openid email profile" } }, - idToken: true, - client: { - id_token_signed_response_alg: OIDC_SIGNING_ALGORITHM || "RS256", - }, - checks: ["pkce", "state"], - profile: (profile) => { - return { - id: profile.sub, - name: profile.name, - email: profile.email, - image: profile.picture, - }; - }, - }, - ], - callbacks: { - async jwt({ token }) { - const existingUser = await getUserByEmail(token?.email!); - - if (!existingUser) { - return token; - } - - return { - ...token, - profile: { id: existingUser.id }, - }; - }, - async session({ session, token }) { - // @ts-expect-error - session.user.id = token?.id; - // @ts-expect-error - session.user = token.profile; - - return session; - }, - async signIn({ user, account }: any) { - if (account.provider === "credentials" || account.provider === "token") { - // check if user's email is verified or not - if (!user.emailVerified && !EMAIL_VERIFICATION_DISABLED) { - throw new Error("Email Verification is Pending"); - } - return true; - } - - if (!user.email || account.type !== "oauth") { - return false; - } - - if (account.provider) { - const provider = account.provider.toLowerCase().replace("-", "") as IdentityProvider; - // check if accounts for this provider / account Id already exists - const existingUserWithAccount = await prisma.user.findFirst({ - include: { - accounts: { - where: { - provider: account.provider, - }, - }, - }, - where: { - identityProvider: provider, - identityProviderAccountId: account.providerAccountId, - }, - }); - - if (existingUserWithAccount) { - // User with this provider found - // check if email still the same - if (existingUserWithAccount.email === user.email) { - return true; - } - - // user seemed to change his email within the provider - // check if user with this email already exist - // if not found just update user with new email address - // if found throw an error (TODO find better solution) - const otherUserWithEmail = await getUserByEmail(user.email); - - if (!otherUserWithEmail) { - await updateUser(existingUserWithAccount.id, { email: user.email }); - return true; - } - throw new Error( - "Looks like you updated your email somewhere else. A user with this new email exists already." - ); - } - - // There is no existing account for this identity provider / account id - // check if user account with this email already exists - // if user already exists throw error and request password login - const existingUserWithEmail = await getUserByEmail(user.email); - - if (existingUserWithEmail) { - // Sign in the user with the existing account - return true; - } - - const userProfile = await createUser({ - name: user.name || user.email.split("@")[0], - email: user.email, - emailVerified: new Date(Date.now()), - identityProvider: provider, - identityProviderAccountId: account.providerAccountId, - locale: await findMatchingLocale(), - }); - - // Default organization assignment if env variable is set - if (DEFAULT_ORGANIZATION_ID && DEFAULT_ORGANIZATION_ID.length > 0) { - // check if organization exists - let organization = await getOrganization(DEFAULT_ORGANIZATION_ID); - let isNewOrganization = false; - if (!organization) { - // create organization with id from env - organization = await createOrganization({ - id: DEFAULT_ORGANIZATION_ID, - name: userProfile.name + "'s Organization", - }); - isNewOrganization = true; - } - const role = isNewOrganization ? "owner" : DEFAULT_ORGANIZATION_ROLE || "manager"; - await createMembership(organization.id, userProfile.id, { role: role, accepted: true }); - await createAccount({ - ...account, - userId: userProfile.id, - }); - - const updatedNotificationSettings: TUserNotificationSettings = { - ...userProfile.notificationSettings, - alert: { - ...userProfile.notificationSettings?.alert, - }, - unsubscribedOrganizationIds: Array.from( - new Set([ - ...(userProfile.notificationSettings?.unsubscribedOrganizationIds || []), - organization.id, - ]) - ), - weeklySummary: { - ...userProfile.notificationSettings?.weeklySummary, - }, - }; - - await updateUser(userProfile.id, { - notificationSettings: updatedNotificationSettings, - }); - return true; - } - // Without default organization assignment - return true; - } - - return true; - }, - }, - pages: { - signIn: "/auth/login", - signOut: "/auth/logout", - error: "/auth/login", // Error code passed in query string as ?error= - }, -}; diff --git a/packages/lib/constants.ts b/packages/lib/constants.ts index 049644eab7..f9889aad4a 100644 --- a/packages/lib/constants.ts +++ b/packages/lib/constants.ts @@ -145,6 +145,7 @@ export const LOGIN_RATE_LIMIT = { interval: 15 * 60, // 15 minutes allowedPerInterval: 30, }; + export const CLIENT_SIDE_API_RATE_LIMIT = { interval: 5 * 60, // 5 minutes allowedPerInterval: 200, diff --git a/packages/lib/customerio.ts b/packages/lib/customerio.ts index b91b8abb09..9c7f5f5f8b 100644 --- a/packages/lib/customerio.ts +++ b/packages/lib/customerio.ts @@ -1,20 +1,25 @@ -import { TUser } from "@formbricks/types/user"; +import { ZId } from "@formbricks/types/common"; +import { TUserEmail, ZUserEmail } from "@formbricks/types/user"; import { CUSTOMER_IO_API_KEY, CUSTOMER_IO_SITE_ID } from "./constants"; +import { validateInputs } from "./utils/validate"; -export const createCustomerIoCustomer = async (user: TUser) => { +export const createCustomerIoCustomer = async ({ id, email }: { id: string; email: TUserEmail }) => { if (!CUSTOMER_IO_SITE_ID || !CUSTOMER_IO_API_KEY) { return; } + + validateInputs([id, ZId], [email, ZUserEmail]); + try { const auth = Buffer.from(`${CUSTOMER_IO_SITE_ID}:${CUSTOMER_IO_API_KEY}`).toString("base64"); - const res = await fetch(`https://track-eu.customer.io/api/v1/customers/${user.id}`, { + const res = await fetch(`https://track-eu.customer.io/api/v1/customers/${id}`, { method: "PUT", headers: { Authorization: `Basic ${auth}`, }, body: JSON.stringify({ - id: user.id, - email: user.email, + id: id, + email: email, }), }); if (res.status !== 200) { diff --git a/packages/lib/invite/service.ts b/packages/lib/invite/service.ts index 608a96cb1c..9c8039b254 100644 --- a/packages/lib/invite/service.ts +++ b/packages/lib/invite/service.ts @@ -1,15 +1,9 @@ import "server-only"; import { Prisma } from "@prisma/client"; -import { getServerSession } from "next-auth"; import { cache as reactCache } from "react"; import { prisma } from "@formbricks/database"; import { ZOptionalNumber, ZString } from "@formbricks/types/common"; -import { - AuthenticationError, - DatabaseError, - ResourceNotFoundError, - ValidationError, -} from "@formbricks/types/errors"; +import { DatabaseError, ResourceNotFoundError, ValidationError } from "@formbricks/types/errors"; import { TInvite, TInviteUpdateInput, @@ -17,7 +11,6 @@ import { ZInviteUpdateInput, ZInvitee, } from "@formbricks/types/invites"; -import { authOptions } from "../authOptions"; import { cache } from "../cache"; import { ITEMS_PER_PAGE } from "../constants"; import { getMembershipByUserIdOrganizationId } from "../membership/service"; @@ -212,19 +205,17 @@ export const resendInvite = async (inviteId: string): Promise => { export const inviteUser = async ({ invitee, organizationId, + currentUserId, }: { organizationId: string; invitee: TInvitee; + currentUserId: string; }): Promise => { validateInputs([organizationId, ZString], [invitee, ZInvitee]); - const session = await getServerSession(authOptions); - - if (!session) throw new AuthenticationError("Not Authenticated"); - const currentUser = session.user; try { const { name, email, role } = invitee; - const { id: currentUserId } = currentUser; + const existingInvite = await prisma.invite.findFirst({ where: { email, organizationId } }); if (existingInvite) { diff --git a/packages/lib/membership/hooks/actions.ts b/packages/lib/membership/hooks/actions.ts index 3baf8760e1..8c165c20be 100644 --- a/packages/lib/membership/hooks/actions.ts +++ b/packages/lib/membership/hooks/actions.ts @@ -1,27 +1,18 @@ "use server"; import "server-only"; -import { getServerSession } from "next-auth"; -import { AuthenticationError, AuthorizationError } from "@formbricks/types/errors"; -import { TUser } from "@formbricks/types/user"; -import { authOptions } from "../../authOptions"; +import { AuthorizationError } from "@formbricks/types/errors"; import { getOrganizationByEnvironmentId } from "../../organization/service"; import { getMembershipByUserIdOrganizationId } from "../service"; -export const getMembershipByUserIdOrganizationIdAction = async (environmentId: string) => { - const session = await getServerSession(authOptions); +export const getMembershipByUserIdOrganizationIdAction = async (environmentId: string, userId: string) => { const organization = await getOrganizationByEnvironmentId(environmentId); - const user = session?.user as TUser; - - if (!session) { - throw new AuthenticationError("Not authenticated"); - } if (!organization) { throw new Error("Organization not found"); } - const currentUserMembership = await getMembershipRole(user.id, organization.id); + const currentUserMembership = await getMembershipRole(userId, organization.id); return currentUserMembership; }; diff --git a/packages/lib/membership/hooks/useMembershipRole.tsx b/packages/lib/membership/hooks/useMembershipRole.tsx index f6ad7e4812..307f75d49f 100644 --- a/packages/lib/membership/hooks/useMembershipRole.tsx +++ b/packages/lib/membership/hooks/useMembershipRole.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from "react"; import { TOrganizationRole } from "@formbricks/types/memberships"; import { getMembershipByUserIdOrganizationIdAction } from "./actions"; -export const useMembershipRole = (environmentId: string) => { +export const useMembershipRole = (environmentId: string, userId: string) => { const [membershipRole, setMembershipRole] = useState(); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(""); @@ -11,7 +11,7 @@ export const useMembershipRole = (environmentId: string) => { const getRole = async () => { try { setIsLoading(true); - const role = await getMembershipByUserIdOrganizationIdAction(environmentId); + const role = await getMembershipByUserIdOrganizationIdAction(environmentId, userId); setMembershipRole(role); setIsLoading(false); } catch (err: any) { @@ -20,7 +20,7 @@ export const useMembershipRole = (environmentId: string) => { } }; getRole(); - }, [environmentId]); + }, [environmentId, userId]); return { membershipRole, isLoading, error }; }; diff --git a/packages/lib/messages/de-DE.json b/packages/lib/messages/de-DE.json index faa2e741a9..73ea4bc150 100644 --- a/packages/lib/messages/de-DE.json +++ b/packages/lib/messages/de-DE.json @@ -78,6 +78,8 @@ "testimonial_title": "Erschaffe einzigartige Kundenerlebnisse", "verification-requested": { "invalid_email_address": "Ungültige E-Mail-Adresse", + "invalid_token": "Ungültiges Token ☹️", + "no_email_provided": "Keine E-Mail bereitgestellt", "please_click_the_link_in_the_email_to_activate_your_account": "Bitte klicke auf den Link in der E-Mail, um dein Konto zu aktivieren.", "please_confirm_your_email_address": "Bitte bestätige deine E-Mail-Adresse", "resend_verification_email": "Bestätigungs-E-Mail erneut senden", @@ -1157,6 +1159,7 @@ "scan_the_qr_code_below_with_your_authenticator_app": "Scanne den QR-Code unten mit deiner Authentifizierungs-App.", "security_description": "Verwalte dein Passwort und andere Sicherheitseinstellungen.", "the_2fa_otp_is_incorrect_please_try_again": "Der 2FA-OTP ist falsch. Bitte versuche es erneut.", + "to_enable_two_factor_authentication_you_need_an_active": "Um die Zwei-Faktor-Authentifizierung zu aktivieren, brauchst du eine aktive", "two_factor_authentication": "Zwei-Faktor-Authentifizierung", "two_factor_authentication_description": "Füge eine zusätzliche Sicherheitsebene zu deinem Konto hinzu, falls dein Passwort gestohlen wird.", "two_factor_authentication_enabled_please_enter_the_six_digit_code_from_your_authenticator_app": "Zwei-Faktor-Authentifizierung aktiviert. Bitte gib den sechsstelligen Code aus deiner Authentifizierungs-App ein.", diff --git a/packages/lib/messages/en-US.json b/packages/lib/messages/en-US.json index 7df4e484c0..337275c7c9 100644 --- a/packages/lib/messages/en-US.json +++ b/packages/lib/messages/en-US.json @@ -78,6 +78,8 @@ "testimonial_title": "Turn customer insights into irresistible experiences.", "verification-requested": { "invalid_email_address": "Invalid email address", + "invalid_token": "Invalid token ☹️", + "no_email_provided": "No email provided", "please_click_the_link_in_the_email_to_activate_your_account": "Please click the link in the email to activate your account.", "please_confirm_your_email_address": "Please confirm your email address", "resend_verification_email": "Resend verification email", @@ -1157,6 +1159,7 @@ "scan_the_qr_code_below_with_your_authenticator_app": "Scan the QR code below with your authenticator app.", "security_description": "Manage your password and other security settings.", "the_2fa_otp_is_incorrect_please_try_again": "The 2FA OTP is incorrect. Please try again.", + "to_enable_two_factor_authentication_you_need_an_active": "To enable two-factor authentication, you need an active", "two_factor_authentication": "Two factor authentication", "two_factor_authentication_description": "Add an extra layer of security to your account in case your password is stolen.", "two_factor_authentication_enabled_please_enter_the_six_digit_code_from_your_authenticator_app": "Two-factor authentication enabled. Please enter the six-digit code from your authenticator app.", diff --git a/packages/lib/messages/pt-BR.json b/packages/lib/messages/pt-BR.json index c206776772..ef730ff75f 100644 --- a/packages/lib/messages/pt-BR.json +++ b/packages/lib/messages/pt-BR.json @@ -78,6 +78,8 @@ "testimonial_title": "Transforme insights dos clientes em experiências irresistíveis.", "verification-requested": { "invalid_email_address": "Endereço de email inválido", + "invalid_token": "Token inválido ☹️", + "no_email_provided": "Nenhum e-mail fornecido", "please_click_the_link_in_the_email_to_activate_your_account": "Por favor, clica no link do e-mail pra ativar sua conta.", "please_confirm_your_email_address": "Por favor, confirme seu endereço de e-mail", "resend_verification_email": "Reenviar e-mail de verificação", @@ -1157,6 +1159,7 @@ "scan_the_qr_code_below_with_your_authenticator_app": "Escaneie o código QR abaixo com seu app autenticador.", "security_description": "Gerencie sua senha e outras configurações de segurança.", "the_2fa_otp_is_incorrect_please_try_again": "O código OTP de 2FA está incorreto. Por favor, tente novamente.", + "to_enable_two_factor_authentication_you_need_an_active": "Pra ativar a autenticação de dois fatores, você precisa de um ativo", "two_factor_authentication": "Autenticação de dois fatores", "two_factor_authentication_description": "Adicione uma camada extra de segurança à sua conta caso sua senha seja roubada.", "two_factor_authentication_enabled_please_enter_the_six_digit_code_from_your_authenticator_app": "Autenticação de dois fatores ativada. Por favor, insira o código de seis dígitos do seu app autenticador.", diff --git a/packages/lib/user/service.ts b/packages/lib/user/service.ts index dd0b911c0c..3e2237a053 100644 --- a/packages/lib/user/service.ts +++ b/packages/lib/user/service.ts @@ -5,15 +5,8 @@ import { z } from "zod"; import { prisma } from "@formbricks/database"; import { ZId } from "@formbricks/types/common"; import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors"; -import { - TUser, - TUserCreateInput, - TUserLocale, - TUserUpdateInput, - ZUserUpdateInput, -} from "@formbricks/types/user"; +import { TUser, TUserLocale, TUserUpdateInput, ZUserUpdateInput } from "@formbricks/types/user"; import { cache } from "../cache"; -import { createCustomerIoCustomer } from "../customerio"; import { deleteOrganization } from "../organization/service"; import { validateInputs } from "../utils/validate"; import { userCache } from "./cache"; @@ -152,37 +145,6 @@ const deleteUserById = async (id: string): Promise => { } }; -export const createUser = async (data: TUserCreateInput): Promise => { - validateInputs([data, ZUserUpdateInput]); - try { - const user = await prisma.user.create({ - data: data, - select: responseSelection, - }); - - userCache.revalidate({ - email: user.email, - id: user.id, - count: true, - }); - - // send new user customer.io to customer.io - createCustomerIoCustomer(user); - - return user; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") { - throw new DatabaseError("User with this email already exists"); - } - - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError(error.message); - } - - throw error; - } -}; - // function to delete a user's user including organizations export const deleteUser = async (id: string): Promise => { validateInputs([id, ZId]); diff --git a/packages/lib/utils/users.ts b/packages/lib/utils/users.ts deleted file mode 100644 index d8dc6e0ae7..0000000000 --- a/packages/lib/utils/users.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { hashPassword } from "../auth/utils"; - -export const createUser = async ( - name: string, - email: string, - password: string, - locale: string, - inviteToken?: string | null -): Promise => { - const hashedPassword = await hashPassword(password); - try { - const res = await fetch(`/api/v1/users`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - name, - email, - password: hashedPassword, - locale, - inviteToken, - }), - }); - if (res.status !== 200) { - const json = await res.json(); - throw Error(json.error); - } - return await res.json(); - } catch (error: any) { - throw Error(`${error.message}`); - } -}; - -export const resendVerificationEmail = async (email: string): Promise => { - try { - const res = await fetch(`/api/v1/users/verification-email`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - email, - }), - }); - if (res.status !== 200) { - const json = await res.json(); - throw Error(json.error); - } - return await res.json(); - } catch (error: any) { - throw Error(`${error.message}`); - } -}; - -export const forgotPassword = async (email: string) => { - try { - const res = await fetch(`/api/v1/users/forgot-password`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - email, - }), - }); - if (res.status !== 200) { - const json = await res.json(); - throw Error(json.error); - } - return await res.json(); - } catch (error: any) { - throw Error(`${error.message}`); - } -}; - -export const resetPassword = async (token: string, password: string): Promise => { - const hashedPassword = await hashPassword(password); - try { - const res = await fetch(`/api/v1/users/reset-password`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - token, - hashedPassword, - }), - }); - if (res.status !== 200) { - const json = await res.json(); - throw Error(json.error); - } - return await res.json(); - } catch (error: any) { - throw Error(`${error.message}`); - } -}; - -export const deleteUser = async (): Promise => { - try { - const res = await fetch("/api/v1/users/me/", { - method: "DELETE", - headers: { "Content-Type": "application/json" }, - }); - if (res.status !== 200) { - const json = await res.json(); - throw Error(json.error); - } - return await res.json(); - } catch (error) { - throw Error(`${error.message}`); - } -}; diff --git a/packages/types/user.ts b/packages/types/user.ts index 0a6d53ceea..2753178cbb 100644 --- a/packages/types/user.ts +++ b/packages/types/user.ts @@ -2,7 +2,7 @@ import { z } from "zod"; const ZRole = z.enum(["project_manager", "engineer", "founder", "marketing_specialist", "other"]); -const ZUserLocale = z.enum(["en-US", "de-DE", "pt-BR"]); +export const ZUserLocale = z.enum(["en-US", "de-DE", "pt-BR"]); export type TUserLocale = z.infer; export const ZUserObjective = z.enum([ @@ -28,12 +28,23 @@ export const ZUserName = z .min(1, { message: "Name should be at least 1 character long" }) .regex(/^[\p{L}\p{M}\s'\d-]+$/u, "Invalid name format"); +export const ZUserEmail = z.string().email({ message: "Invalid email" }); + +export type TUserEmail = z.infer; + +export const ZUserPassword = z + .string() + .min(8) + .regex(/^(?=.*[A-Z])(?=.*\d).*$/); + +export type TUserPassword = z.infer; + export type TUserNotificationSettings = z.infer; export const ZUser = z.object({ id: z.string(), name: ZUserName, - email: z.string().email(), + email: ZUserEmail, emailVerified: z.date().nullable(), imageUrl: z.string().url().nullable(), twoFactorEnabled: z.boolean(), @@ -50,8 +61,9 @@ export type TUser = z.infer; export const ZUserUpdateInput = z.object({ name: ZUserName.optional(), - email: z.string().email().optional(), + email: ZUserEmail.optional(), emailVerified: z.date().nullish(), + password: ZUserPassword.optional(), role: ZRole.optional(), objective: ZUserObjective.nullish(), imageUrl: z.string().nullish(), @@ -63,7 +75,8 @@ export type TUserUpdateInput = z.infer; export const ZUserCreateInput = z.object({ name: ZUserName, - email: z.string().email(), + email: ZUserEmail, + password: ZUserPassword.optional(), emailVerified: z.date().optional(), role: ZRole.optional(), objective: ZUserObjective.nullish(),