mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-12 11:28:58 -05:00
Merge branch 'main' of https://github.com/Dhruwang/formbricks into issues/SingleUseLinkSurveys
This commit is contained in:
-107
@@ -1,107 +0,0 @@
|
||||
########################################################################
|
||||
# ------------ MANDATORY (CHANGE ACCORDING TO YOUR SETUP) ------------#
|
||||
########################################################################
|
||||
|
||||
|
||||
############
|
||||
# BASICS #
|
||||
############
|
||||
|
||||
WEBAPP_URL=http://localhost:3000
|
||||
|
||||
##############
|
||||
# DATABASE #
|
||||
##############
|
||||
|
||||
DATABASE_URL='postgresql://postgres:postgres@postgres:5432/formbricks?schema=public'
|
||||
|
||||
# Uncomment to enable a dedicated connection pool for Prisma using Prisma Data Proxy
|
||||
# Cold boots will be faster and you'll be able to scale your DB independently of your app.
|
||||
# @see https://www.prisma.io/docs/data-platform/data-proxy/use-data-proxy
|
||||
# PRISMA_GENERATE_DATAPROXY=true
|
||||
PRISMA_GENERATE_DATAPROXY=
|
||||
|
||||
###############
|
||||
# NEXT AUTH #
|
||||
###############
|
||||
|
||||
# @see: https://next-auth.js.org/configuration/options#nextauth_secret
|
||||
# You can use: `openssl rand -base64 32` to generate one
|
||||
NEXTAUTH_SECRET=RANDOM_STRING
|
||||
|
||||
# Set this to your public-facing URL, e.g., https://example.com
|
||||
# You do not need the NEXTAUTH_URL environment variable in Vercel.
|
||||
NEXTAUTH_URL=http://localhost:3000
|
||||
|
||||
# If you encounter NEXT_AUTH URL problems this should always be localhost:3000 (or whatever port your app is running on)
|
||||
# NEXTAUTH_URL_INTERNAL=http://localhost:3000
|
||||
|
||||
################
|
||||
# MAIL SETUP #
|
||||
################
|
||||
|
||||
# Necessary if email verification and password reset are enabled.
|
||||
# See optional configurations below if you want to disable these features.
|
||||
|
||||
# MAIL_FROM=noreply@example.com
|
||||
# SMTP_HOST=localhost
|
||||
# SMTP_PORT=1025
|
||||
# Enable SMTP_SECURE_ENABLED for TLS (port 465)
|
||||
# SMTP_SECURE_ENABLED=0
|
||||
# SMTP_USER=smtpUser
|
||||
# SMTP_PASSWORD=smtpPassword
|
||||
|
||||
|
||||
########################################################################
|
||||
# ------------------------------ OPTIONAL -----------------------------#
|
||||
########################################################################
|
||||
|
||||
# Uncomment the variables you would like to use and customize the values.
|
||||
|
||||
#####################
|
||||
# Disable Features #
|
||||
#####################
|
||||
|
||||
# Email Verification. If you enable Email Verification you have to setup SMTP-Settings, too.
|
||||
EMAIL_VERIFICATION_DISABLED=1
|
||||
|
||||
# Password Reset. If you enable Password Reset functionality you have to setup SMTP-Settings, too.
|
||||
PASSWORD_RESET_DISABLED=1
|
||||
|
||||
# Signup. Disable the ability for new users to create an account.
|
||||
# SIGNUP_DISABLED=1
|
||||
|
||||
# Team Invite. Disable the ability for invited users to create an account.
|
||||
# INVITE_DISABLED=1
|
||||
|
||||
##########
|
||||
# Other #
|
||||
##########
|
||||
|
||||
# Display privacy policy, imprint and terms of service links in the footer of signup & public pages.
|
||||
PRIVACY_URL=
|
||||
TERMS_URL=
|
||||
IMPRINT_URL=
|
||||
|
||||
# Disable Sentry warning
|
||||
SENTRY_IGNORE_API_RESOLUTION_ERROR=1
|
||||
|
||||
# Enable Sentry Error Tracking
|
||||
NEXT_PUBLIC_SENTRY_DSN=
|
||||
|
||||
# Configure Github Login
|
||||
GITHUB_AUTH_ENABLED=0
|
||||
GITHUB_ID=
|
||||
GITHUB_SECRET=
|
||||
|
||||
# Configure Google Login
|
||||
GOOGLE_AUTH_ENABLED=0
|
||||
GOOGLE_CLIENT_ID=
|
||||
GOOGLE_CLIENT_SECRET=
|
||||
|
||||
# Cron Secret
|
||||
CRON_SECRET=
|
||||
|
||||
# Encryption key
|
||||
# You can use: `openssl rand -base64 16` to generate one
|
||||
FORMBRICKS_ENCRYPTION_KEY=
|
||||
+14
-13
@@ -10,19 +10,17 @@
|
||||
|
||||
WEBAPP_URL=http://localhost:3000
|
||||
|
||||
SURVEY_BASE_URL=http://localhost:3000/i
|
||||
|
||||
# Set this if you want to have a shorter link for surveys
|
||||
SHORT_SURVEY_BASE_URL=
|
||||
|
||||
##############
|
||||
# DATABASE #
|
||||
##############
|
||||
|
||||
DATABASE_URL='postgresql://postgres:postgres@localhost:5432/formbricks?schema=public'
|
||||
|
||||
# Uncomment to enable a dedicated connection pool for Prisma using Prisma Data Proxy
|
||||
# Cold boots will be faster and you'll be able to scale your DB independently of your app.
|
||||
# @see https://www.prisma.io/docs/data-platform/data-proxy/use-data-proxy
|
||||
# PRISMA_GENERATE_DATAPROXY=true
|
||||
# i dont think we use it so ask Matti and remove it
|
||||
PRISMA_GENERATE_DATAPROXY=
|
||||
|
||||
###############
|
||||
# NEXT AUTH #
|
||||
###############
|
||||
@@ -92,6 +90,15 @@ GOOGLE_AUTH_ENABLED=0
|
||||
GOOGLE_CLIENT_ID=
|
||||
GOOGLE_CLIENT_SECRET=
|
||||
|
||||
# Cron Secret
|
||||
CRON_SECRET=
|
||||
|
||||
# Encryption key
|
||||
# You can use: `openssl rand -base64 16` to generate one
|
||||
FORMBRICKS_ENCRYPTION_KEY=
|
||||
|
||||
# Configure this when you want to ship JS & CSS files from a complete URL instead of the current domain
|
||||
# ASSET_PREFIX_URL=
|
||||
|
||||
# Stripe Billing Variables
|
||||
STRIPE_SECRET_KEY=
|
||||
@@ -102,10 +109,4 @@ NEXT_PUBLIC_FORMBRICKS_API_HOST=
|
||||
NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID=
|
||||
NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID=
|
||||
|
||||
# Cron Secret
|
||||
CRON_SECRET=
|
||||
|
||||
# Encryption key
|
||||
# You can use: `openssl rand -base64 16` to generate one
|
||||
FORMBRICKS_ENCRYPTION_KEY=
|
||||
*/
|
||||
|
||||
@@ -12,7 +12,13 @@ jobs:
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/formbricks?schema=public"
|
||||
steps:
|
||||
- name: Generate Random NEXTAUTH_SECRET
|
||||
run: |
|
||||
SECRET=$(openssl rand -hex 16)
|
||||
echo "NEXTAUTH_SECRET=$SECRET" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
@@ -41,3 +47,6 @@ jobs:
|
||||
tags: |
|
||||
${{ secrets.DOCKER_USERNAME }}/formbricks:${{ env.RELEASE_TAG }}
|
||||
${{ secrets.DOCKER_USERNAME }}/formbricks:latest
|
||||
build-args: |
|
||||
NEXTAUTH_SECRET=${{ env.NEXTAUTH_SECRET }}
|
||||
DATABASE_URL=${{ env.DATABASE_URL }}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"dependencies": {
|
||||
"@formbricks/js": "workspace:*",
|
||||
"@heroicons/react": "^2.0.18",
|
||||
"next": "13.5.3",
|
||||
"next": "13.5.4",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
|
||||
@@ -61,10 +61,12 @@ Ensure `docker` & `docker compose` are installed on your server/system. Both are
|
||||
|
||||
You're now ready to start the Formbricks Docker setup. The following command will start Formbricks together with a postgreSQL database using Docker Compose:
|
||||
|
||||
We pass the `--env-file /dev/null` flag to docker-compose to prevent it from reading the .env file. This is because we're using environment variables directly in the docker-compose.yml file as the env file is currently in a format not well recognised by docker systems.
|
||||
|
||||
<CodeGroup title="Launch Docker Instance">
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
docker compose --env-file /dev/null up -d
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
@@ -103,7 +105,7 @@ Ensure `docker` & `docker compose` are installed on your server/system. Both are
|
||||
<CodeGroup title="Relaunch the Docker Instance">
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
docker compose --env-file /dev/null up -d
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
@@ -29,7 +29,7 @@ Ensure `docker` & `docker compose` are installed on your server/system. Both are
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
2. **Modify the `.env.docker` file as required by your setup.** <br/> This file comes with a basic setup and Formbricks works without making any changes to the file. To enable email sending functionality you need to configure the SMTP settings. If you configured your email credentials, you can also comment the following lines to enable email verification (`# EMAIL_VERIFICATION_DISABLED=1`) and password reset (`# PASSWORD_RESET_DISABLED=1`)
|
||||
2. **Modify the environment variables in your `docker-compose.yml` file as required by your setup.** <br/> This file comes with a basic setup and Formbricks works without making any changes to the file. To enable email sending functionality you need to configure the SMTP settings. If you configured your email credentials, you can also comment the following lines to enable email verification (`# NEXT_PUBLIC_EMAIL_VERIFICATION_DISABLED=1`) and password reset (`# NEXT_PUBLIC_PASSWORD_RESET_DISABLED=1`)
|
||||
|
||||
<Note>
|
||||
## Editing a NEXT_PUBLIC_* variable?
|
||||
@@ -38,12 +38,26 @@ Ensure `docker` & `docker compose` are installed on your server/system. Both are
|
||||
to take effect.
|
||||
</Note>
|
||||
|
||||
3. **Start the docker compose process.** <br/> Finally start the docker compose process to build and spin up the Formbricks container as well as the PostgreSQL database. <br/> _Use docker-compose if you are on an older docker version_
|
||||
3. **Generate NextAuth Secret**
|
||||
|
||||
Next, you need to generate a NextAuth secret. This will be used for session signing and encryption. The `sed` command below generates a random string using `openssl`, then replaces the `NEXTAUTH_SECRET:` placeholder in the `docker-compose.yml` file with this generated secret:
|
||||
|
||||
<CodeGroup title="Generate NextAuth Secret">
|
||||
|
||||
```bash
|
||||
sed -i "/x-nextauth-secret: &nextauth_secret/s/RANDOM_STRING/$(openssl rand -base64 32 | tr -dc 'a-zA-Z0-9' | head -c 32)/" docker-compose.yml
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
4. **Start the docker compose process.** <br/> Finally start the docker compose process to build and spin up the Formbricks container as well as the PostgreSQL database. <br/> _Use docker-compose if you are on an older docker version_
|
||||
|
||||
We pass the `--env-file /dev/null` flag to docker-compose to prevent it from reading the .env file. This is because we're using environment variables directly in the docker-compose.yml file as the env file is currently in a format not well recognised by docker systems.
|
||||
|
||||
<CodeGroup title="Launch docker instances">
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
docker compose --env-file /dev/null up
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
@@ -59,7 +73,6 @@ These variables must also be provided at runtime.
|
||||
| WEBAPP_URL | Base URL of the site. | required | `http://localhost:3000` |
|
||||
| SURVEY_BASE_URL | Base URL of the link surveys. | required | `http://localhost:3000/s/` |
|
||||
| DATABASE_URL | Database URL with credentials. | required | `postgresql://postgres:postgres@postgres:5432/formbricks?schema=public` |
|
||||
| PRISMA_GENERATE_DATAPROXY | Enables a dedicated connection pool for Prisma using Prisma Data Proxy. Uncomment to enable. | optional | |
|
||||
| NEXTAUTH_SECRET | Secret for NextAuth, used for session signing and encryption. | required | (Generated by the user) |
|
||||
| NEXTAUTH_URL | Location of the auth server. By default, this is the Formbricks docker instance itself. | required | `http://localhost:3000` |
|
||||
| PRIVACY_URL | URL for privacy policy. | optional | |
|
||||
|
||||
@@ -75,12 +75,6 @@ x-environment: &environment
|
||||
# PostgreSQL DB for Formbricks to connect to
|
||||
DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/formbricks?schema=public"
|
||||
|
||||
# Uncomment to enable a dedicated connection pool for Prisma using Prisma Data Proxy
|
||||
# Cold boots will be faster and you'll be able to scale your DB independently of your app.
|
||||
# @see https://www.prisma.io/docs/data-platform/data-proxy/use-data-proxy
|
||||
# PRISMA_GENERATE_DATAPROXY=true
|
||||
PRISMA_GENERATE_DATAPROXY:
|
||||
|
||||
# NextJS Auth
|
||||
# @see: https://next-auth.js.org/configuration/options#nextauth_secret
|
||||
# You can use: `openssl rand -base64 32` to generate one
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"@sindresorhus/slugify": "^2.2.1",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@types/node": "20.6.0",
|
||||
"@types/react-highlight-words": "^0.16.4",
|
||||
"@types/react-highlight-words": "^0.16.5",
|
||||
"acorn": "^8.10.0",
|
||||
"autoprefixer": "^10.4.15",
|
||||
"clsx": "^2.0.0",
|
||||
|
||||
@@ -64,6 +64,12 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
|
||||
"Open-source authentication and user management for the passkey era. Integrated in minutes, for web and mobile apps.",
|
||||
href: "https://www.hanko.io",
|
||||
},
|
||||
{
|
||||
name: "Hook0",
|
||||
description:
|
||||
"Open-Source Webhooks-as-a-service (WaaS) that makes it easy for developers to send webhooks.",
|
||||
href: "https://www.hook0.com/",
|
||||
},
|
||||
{
|
||||
name: "HTMX",
|
||||
description:
|
||||
|
||||
@@ -267,12 +267,12 @@ const FAQ = [
|
||||
const Leaderboard = [
|
||||
{
|
||||
name: "Piyush",
|
||||
points: "550",
|
||||
points: "1250",
|
||||
link: "https://github.com/gupta-piyush19",
|
||||
},
|
||||
{
|
||||
name: "Suman",
|
||||
points: "200",
|
||||
points: "600",
|
||||
},
|
||||
{
|
||||
name: "Subhdeep",
|
||||
@@ -360,20 +360,20 @@ const Leaderboard = [
|
||||
},
|
||||
{
|
||||
name: "Aditya Deshlahre",
|
||||
points: "550",
|
||||
points: "820",
|
||||
link: "https://github.com/adityadeshlahre",
|
||||
},
|
||||
{
|
||||
name: "Rutam",
|
||||
points: "350",
|
||||
points: "805",
|
||||
},
|
||||
{
|
||||
name: "Sagnik Sahoo",
|
||||
points: "100",
|
||||
points: "250",
|
||||
},
|
||||
{
|
||||
name: "Prasoon Mahawar",
|
||||
points: "100",
|
||||
points: "500",
|
||||
},
|
||||
{
|
||||
name: "Dushmanta",
|
||||
@@ -409,8 +409,28 @@ const Leaderboard = [
|
||||
},
|
||||
{
|
||||
name: "Rajarshi Misra",
|
||||
points: "300",
|
||||
},
|
||||
{
|
||||
name: "Anjaneya Gupta",
|
||||
points: "300",
|
||||
},
|
||||
{
|
||||
name: "Sachin Kuber",
|
||||
points: "100",
|
||||
},
|
||||
{
|
||||
name: "Manpreet Singh",
|
||||
points: "100",
|
||||
},
|
||||
{
|
||||
name: "Vaibhav Gupta",
|
||||
points: "100",
|
||||
},
|
||||
{
|
||||
name: "maciek",
|
||||
points: "300",
|
||||
},
|
||||
];
|
||||
|
||||
export default function FormTribeHackathon() {
|
||||
|
||||
+11
-5
@@ -1,11 +1,17 @@
|
||||
FROM node:18-alpine AS installer
|
||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||
|
||||
ARG DATABASE_URL
|
||||
ENV DATABASE_URL=$DATABASE_URL
|
||||
|
||||
ARG NEXTAUTH_SECRET
|
||||
ENV NEXTAUTH_SECRET=$NEXTAUTH_SECRET
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
# Copy .env file because Docker don't follow symlinks
|
||||
COPY .env.docker /app/apps/web/.env
|
||||
RUN touch /app/apps/web/.env
|
||||
|
||||
|
||||
RUN pnpm install
|
||||
|
||||
@@ -34,13 +40,13 @@ COPY --from=installer --chown=nextjs:nodejs /app/apps/web/public ./apps/web/publ
|
||||
COPY --from=installer --chown=nextjs:nodejs /app/packages/database/schema.prisma ./packages/database/schema.prisma
|
||||
COPY --from=installer --chown=nextjs:nodejs /app/packages/database/migrations ./packages/database/migrations
|
||||
|
||||
ENV NEXTAUTH_SECRET=$NEXTAUTH_SECRET
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENV HOSTNAME "0.0.0.0"
|
||||
|
||||
CMD if [ "$NEXTAUTH_SECRET" != "RANDOM_STRING" ]; then \
|
||||
pnpm dlx prisma migrate deploy && node apps/web/server.js; \
|
||||
else \
|
||||
echo "ERROR: Please set a value for NEXTAUTH_SECRET in .env.docker!"; \
|
||||
echo "ERROR: Please set a value for NEXTAUTH_SECRET in your docker compose variables!"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
@@ -2,17 +2,33 @@
|
||||
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { SHORT_SURVEY_BASE_URL, SURVEY_BASE_URL } from "@formbricks/lib/constants";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { createMembership } from "@formbricks/lib/membership/service";
|
||||
import { createProduct } from "@formbricks/lib/product/service";
|
||||
import { createTeam, getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
import { createShortUrl } from "@formbricks/lib/shortUrl/service";
|
||||
import { canUserAccessSurvey } from "@formbricks/lib/survey/auth";
|
||||
import { deleteSurvey, getSurvey } from "@formbricks/lib/survey/service";
|
||||
import { AuthorizationError, ResourceNotFoundError } from "@formbricks/types/v1/errors";
|
||||
import { createTeam, getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
import { AuthenticationError, AuthorizationError, ResourceNotFoundError } from "@formbricks/types/v1/errors";
|
||||
import { Team } from "@prisma/client";
|
||||
import { Prisma as prismaClient } from "@prisma/client/";
|
||||
import { getServerSession } from "next-auth";
|
||||
|
||||
export const createShortUrlAction = async (url: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthenticationError("Not authenticated");
|
||||
|
||||
const regexPattern = new RegExp("^" + SURVEY_BASE_URL);
|
||||
const isValidUrl = regexPattern.test(url);
|
||||
|
||||
if (!isValidUrl) throw new Error("Only Formbricks survey URLs are allowed");
|
||||
|
||||
const shortUrl = await createShortUrl(url);
|
||||
const fullShortUrl = SHORT_SURVEY_BASE_URL + shortUrl.id;
|
||||
return fullShortUrl;
|
||||
};
|
||||
|
||||
export async function createTeamAction(teamName: string): Promise<Team> {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
+3
-2
@@ -1,7 +1,7 @@
|
||||
export const revalidate = REVALIDATION_INTERVAL;
|
||||
|
||||
import Navigation from "@/app/(app)/environments/[environmentId]/Navigation";
|
||||
import { IS_FORMBRICKS_CLOUD, REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
|
||||
import Navigation from "@/app/(app)/environments/[environmentId]/components/Navigation";
|
||||
import { IS_FORMBRICKS_CLOUD, REVALIDATION_INTERVAL, SURVEY_BASE_URL } from "@formbricks/lib/constants";
|
||||
import { getEnvironment, getEnvironments } from "@formbricks/lib/environment/service";
|
||||
import { getProducts } from "@formbricks/lib/product/service";
|
||||
import { getTeamByEnvironmentId, getTeamsByUserId } from "@formbricks/lib/team/service";
|
||||
@@ -43,6 +43,7 @@ export default async function EnvironmentsNavbar({ environmentId, session }: Env
|
||||
environments={environments}
|
||||
session={session}
|
||||
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
|
||||
surveyBaseUrl={SURVEY_BASE_URL}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+19
-1
@@ -17,6 +17,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/shared/DropdownMenu";
|
||||
import CreateTeamModal from "@/components/team/CreateTeamModal";
|
||||
import UrlShortenerModal from "./UrlShortenerModal";
|
||||
import { formbricksLogout } from "@/lib/formbricks";
|
||||
import { capitalizeFirstLetter, truncate } from "@/lib/utils";
|
||||
import formbricks from "@formbricks/js";
|
||||
@@ -52,6 +53,7 @@ import {
|
||||
PlusIcon,
|
||||
UserCircleIcon,
|
||||
UsersIcon,
|
||||
LinkIcon,
|
||||
} from "@heroicons/react/24/solid";
|
||||
import clsx from "clsx";
|
||||
import { MenuIcon } from "lucide-react";
|
||||
@@ -71,6 +73,7 @@ interface NavigationProps {
|
||||
products: TProduct[];
|
||||
environments: TEnvironment[];
|
||||
isFormbricksCloud: boolean;
|
||||
surveyBaseUrl: string;
|
||||
}
|
||||
|
||||
export default function Navigation({
|
||||
@@ -81,6 +84,7 @@ export default function Navigation({
|
||||
products,
|
||||
environments,
|
||||
isFormbricksCloud,
|
||||
surveyBaseUrl,
|
||||
}: NavigationProps) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
@@ -89,6 +93,7 @@ export default function Navigation({
|
||||
const [widgetSetupCompleted, setWidgetSetupCompleted] = useState(false);
|
||||
const [showAddProductModal, setShowAddProductModal] = useState(false);
|
||||
const [showCreateTeamModal, setShowCreateTeamModal] = useState(false);
|
||||
const [showLinkShortenerModal, setShowLinkShortenerModal] = useState(false);
|
||||
const product = products.find((product) => product.id === environment.productId);
|
||||
const [mobileNavMenuOpen, setMobileNavMenuOpen] = useState(false);
|
||||
|
||||
@@ -185,6 +190,14 @@ export default function Navigation({
|
||||
href: `/environments/${environment.id}/settings/setup`,
|
||||
hidden: widgetSetupCompleted,
|
||||
},
|
||||
{
|
||||
icon: LinkIcon,
|
||||
label: "Link Shortener",
|
||||
href: pathname,
|
||||
onClick: () => {
|
||||
setShowLinkShortenerModal(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: CodeBracketIcon,
|
||||
label: "Developer Docs",
|
||||
@@ -441,7 +454,7 @@ export default function Navigation({
|
||||
(link) =>
|
||||
!link.hidden && (
|
||||
<Link href={link.href} target={link.target} key={link.label}>
|
||||
<DropdownMenuItem key={link.label}>
|
||||
<DropdownMenuItem key={link.label} onClick={link?.onClick}>
|
||||
<div className="flex items-center">
|
||||
<link.icon className="mr-2 h-4 w-4" />
|
||||
<span>{link.label}</span>
|
||||
@@ -489,6 +502,11 @@ export default function Navigation({
|
||||
environmentId={environment.id}
|
||||
/>
|
||||
<CreateTeamModal open={showCreateTeamModal} setOpen={(val) => setShowCreateTeamModal(val)} />
|
||||
<UrlShortenerModal
|
||||
open={showLinkShortenerModal}
|
||||
setOpen={(val) => setShowLinkShortenerModal(val)}
|
||||
surveyBaseUrl={surveyBaseUrl}
|
||||
/>
|
||||
</nav>
|
||||
)}
|
||||
</>
|
||||
@@ -0,0 +1,152 @@
|
||||
import Modal from "@/components/shared/Modal";
|
||||
import { Button, Input, Label } from "@formbricks/ui";
|
||||
import { LinkIcon } from "@heroicons/react/24/outline";
|
||||
import clsx from "clsx";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import toast from "react-hot-toast";
|
||||
import { createShortUrlAction } from "../actions";
|
||||
|
||||
type UrlShortenerModalProps = {
|
||||
open: boolean;
|
||||
setOpen: (v: boolean) => void;
|
||||
surveyBaseUrl: string;
|
||||
};
|
||||
type UrlShortenerFormDataProps = {
|
||||
url: string;
|
||||
};
|
||||
type UrlValidationState = "default" | "valid" | "invalid";
|
||||
|
||||
export default function UrlShortenerModal({ open, setOpen, surveyBaseUrl }: UrlShortenerModalProps) {
|
||||
const [urlValidationState, setUrlValidationState] = useState<UrlValidationState>("default");
|
||||
const [shortUrl, setShortUrl] = useState("");
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
watch,
|
||||
formState: { isSubmitting },
|
||||
} = useForm<UrlShortenerFormDataProps>({
|
||||
mode: "onSubmit",
|
||||
defaultValues: {
|
||||
url: "",
|
||||
},
|
||||
});
|
||||
|
||||
const handleUrlValidation = () => {
|
||||
const value = watch("url").trim();
|
||||
if (!value) {
|
||||
setUrlValidationState("default");
|
||||
return;
|
||||
}
|
||||
|
||||
const regexPattern = new RegExp("^" + surveyBaseUrl);
|
||||
const isValid = regexPattern.test(value);
|
||||
if (!isValid) {
|
||||
setUrlValidationState("invalid");
|
||||
toast.error("Only formbricks survey links allowed.");
|
||||
} else {
|
||||
setUrlValidationState("valid");
|
||||
}
|
||||
};
|
||||
|
||||
const shortenUrl = async (data: UrlShortenerFormDataProps) => {
|
||||
if (urlValidationState !== "valid") return;
|
||||
|
||||
const shortUrl = await createShortUrlAction(data.url.trim());
|
||||
setShortUrl(shortUrl);
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
setUrlValidationState("default");
|
||||
setShortUrl("");
|
||||
};
|
||||
|
||||
const copyShortUrlToClipboard = () => {
|
||||
navigator.clipboard.writeText(shortUrl);
|
||||
toast.success("URL copied to clipboard!");
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
setOpen={(v) => {
|
||||
setOpen(v);
|
||||
resetForm();
|
||||
}}
|
||||
noPadding
|
||||
closeOnOutsideClick={false}>
|
||||
<div className="flex h-full flex-col rounded-lg pb-4">
|
||||
<div className="rounded-t-lg bg-slate-100">
|
||||
<div className="flex items-center justify-between p-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="mr-1.5 h-10 w-10 text-slate-500">
|
||||
<LinkIcon />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xl font-medium text-slate-700">URL shortener</div>
|
||||
<div className="text-sm text-slate-500">
|
||||
Create a short URL to make URL params less obvious.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit(shortenUrl)}>
|
||||
<div className="grid w-full space-y-2 rounded-lg px-6 py-4">
|
||||
<Label>Paste URL</Label>
|
||||
<div className="grid grid-cols-6 gap-3">
|
||||
<Input
|
||||
autoFocus
|
||||
placeholder={`${surveyBaseUrl}...`}
|
||||
className={clsx(
|
||||
"col-span-5",
|
||||
urlValidationState === "valid"
|
||||
? "border-green-500 bg-green-50"
|
||||
: urlValidationState === "invalid"
|
||||
? "border-red-200 bg-red-50"
|
||||
: urlValidationState === "default"
|
||||
? "border-slate-200"
|
||||
: "bg-white"
|
||||
)}
|
||||
{...register("url", {
|
||||
required: true,
|
||||
})}
|
||||
onBlur={handleUrlValidation}
|
||||
/>
|
||||
<Button
|
||||
variant="darkCTA"
|
||||
size="sm"
|
||||
className="col-span-1 text-center"
|
||||
type="submit"
|
||||
loading={isSubmitting}>
|
||||
Shorten
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div className="grid w-full space-y-2 rounded-lg px-6 py-4">
|
||||
<Label>Short URL</Label>
|
||||
<div className="grid grid-cols-6 gap-3">
|
||||
<span
|
||||
className="col-span-5 cursor-pointer rounded-md border border-slate-300 bg-slate-100 px-3 py-2 text-sm text-slate-700"
|
||||
onClick={() => {
|
||||
if (shortUrl) {
|
||||
copyShortUrlToClipboard();
|
||||
}
|
||||
}}>
|
||||
{shortUrl}
|
||||
</span>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="col-span-1 justify-center"
|
||||
type="button"
|
||||
onClick={() => copyShortUrlToClipboard()}>
|
||||
<span>Copy</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +1,33 @@
|
||||
"use server";
|
||||
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { createWebhook, deleteWebhook, updateWebhook } from "@formbricks/lib/webhook/service";
|
||||
import { TWebhook, TWebhookInput } from "@formbricks/types/v1/webhooks";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { AuthorizationError } from "@formbricks/types/v1/errors";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { canUserAccessWebhook } from "@formbricks/lib/webhook/auth";
|
||||
|
||||
export const createWebhookAction = async (
|
||||
environmentId: string,
|
||||
webhookInput: TWebhookInput
|
||||
): Promise<TWebhook> => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const isAuthorized = await hasUserEnvironmentAccess(session.user.id, environmentId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await createWebhook(environmentId, webhookInput);
|
||||
};
|
||||
|
||||
export const deleteWebhookAction = async (id: string): Promise<TWebhook> => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const isAuthorized = await canUserAccessWebhook(session.user.id, id);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await deleteWebhook(id);
|
||||
};
|
||||
|
||||
@@ -19,5 +36,11 @@ export const updateWebhookAction = async (
|
||||
webhookId: string,
|
||||
webhookInput: Partial<TWebhookInput>
|
||||
): Promise<TWebhook> => {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const isAuthorized = await canUserAccessWebhook(session.user.id, webhookId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
return await updateWebhook(environmentId, webhookId, webhookInput);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import EnvironmentsNavbar from "@/app/(app)/environments/[environmentId]/EnvironmentsNavbar";
|
||||
import EnvironmentsNavbar from "@/app/(app)/environments/[environmentId]/components/EnvironmentsNavbar";
|
||||
import ToasterClient from "@/components/ToasterClient";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { redirect } from "next/navigation";
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import FormbricksClient from "../../FormbricksClient";
|
||||
import { ResponseFilterProvider } from "@/app/(app)/environments/[environmentId]/ResponseFilterContext";
|
||||
import { ResponseFilterProvider } from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||
import { AuthorizationError } from "@formbricks/types/v1/errors";
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import SummaryHeader from "@/app/(app)/environments/[environmentId]/surveys/[sur
|
||||
import SurveyResultsTabs from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/SurveyResultsTabs";
|
||||
import ResponseTimeline from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTimeline";
|
||||
import ContentWrapper from "@/components/shared/ContentWrapper";
|
||||
import { useResponseFilter } from "@/app/(app)/environments/[environmentId]/ResponseFilterContext";
|
||||
import { useResponseFilter } from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
|
||||
import { getFilterResponses } from "@/lib/surveys/surveys";
|
||||
import { TResponse } from "@formbricks/types/v1/responses";
|
||||
import { TSurvey } from "@formbricks/types/v1/surveys";
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import SurveyResultsTabs from "@/app/(app)/environments/[environmentId]/surveys/
|
||||
import SummaryList from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryList";
|
||||
import SummaryMetadata from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryMetadata";
|
||||
import ContentWrapper from "@/components/shared/ContentWrapper";
|
||||
import { useResponseFilter } from "@/app/(app)/environments/[environmentId]/ResponseFilterContext";
|
||||
import { useResponseFilter } from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
|
||||
import { getFilterResponses } from "@/lib/surveys/surveys";
|
||||
import { TResponse } from "@formbricks/types/v1/responses";
|
||||
import { TSurveyWithAnalytics } from "@formbricks/types/v1/surveys";
|
||||
|
||||
@@ -23,7 +23,10 @@ import { TResponse } from "@formbricks/types/v1/responses";
|
||||
import { TSurvey } from "@formbricks/types/v1/surveys";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import ResponseFilter from "./ResponseFilter";
|
||||
import { DateRange, useResponseFilter } from "@/app/(app)/environments/[environmentId]/ResponseFilterContext";
|
||||
import {
|
||||
DateRange,
|
||||
useResponseFilter,
|
||||
} from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
|
||||
import { TTag } from "@formbricks/types/v1/tags";
|
||||
|
||||
enum DateSelected {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useResponseFilter } from "@/app/(app)/environments/[environmentId]/ResponseFilterContext";
|
||||
import { useResponseFilter } from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
|
||||
import QuestionFilterComboBox from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/QuestionFilterComboBox";
|
||||
import { TSurveyQuestionType } from "@formbricks/types/v1/surveys";
|
||||
import { Button, Checkbox, Popover, PopoverContent, PopoverTrigger } from "@formbricks/ui";
|
||||
|
||||
+1
-26
@@ -244,24 +244,9 @@ export default function QuestionCard({
|
||||
<Input
|
||||
id="buttonLabel"
|
||||
name="buttonLabel"
|
||||
className={cn(
|
||||
isInValid &&
|
||||
question.backButtonLabel?.trim() === "" &&
|
||||
"border border-red-600 focus:border-red-600"
|
||||
)}
|
||||
value={question.buttonLabel}
|
||||
placeholder={lastQuestion ? "Finish" : "Next"}
|
||||
onChange={(e) => {
|
||||
const trimmedValue = e.target.value.trim(); // Remove spaces from the start and end
|
||||
const hasInternalSpaces = /\S\s\S/.test(trimmedValue); // Test if there are spaces between words
|
||||
|
||||
if (
|
||||
!trimmedValue.includes(" ") &&
|
||||
(trimmedValue === "" || hasInternalSpaces || !/\s/.test(trimmedValue))
|
||||
) {
|
||||
updateQuestion(questionIdx, { backButtonLabel: trimmedValue });
|
||||
}
|
||||
}}
|
||||
onChange={(e) => updateQuestion(questionIdx, { buttonLabel: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -269,11 +254,6 @@ export default function QuestionCard({
|
||||
<BackButtonInput
|
||||
value={question.backButtonLabel}
|
||||
onChange={(e) => updateQuestion(questionIdx, { backButtonLabel: e.target.value })}
|
||||
className={cn(
|
||||
isInValid &&
|
||||
question.backButtonLabel?.trim() === "" &&
|
||||
"border border-red-600 focus:border-red-600"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -284,11 +264,6 @@ export default function QuestionCard({
|
||||
<BackButtonInput
|
||||
value={question.backButtonLabel}
|
||||
onChange={(e) => updateQuestion(questionIdx, { backButtonLabel: e.target.value })}
|
||||
className={cn(
|
||||
isInValid &&
|
||||
question.backButtonLabel?.trim() === "" &&
|
||||
"border border-red-600 focus:border-red-600"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
+2
-1
@@ -98,7 +98,6 @@ export default function SurveyMenuBar({
|
||||
};
|
||||
|
||||
const validateSurvey = (survey) => {
|
||||
const existingLogicConditions = new Set();
|
||||
const existingQuestionIds = new Set();
|
||||
|
||||
if (survey.questions.length === 0) {
|
||||
@@ -123,6 +122,8 @@ export default function SurveyMenuBar({
|
||||
}
|
||||
|
||||
for (const question of survey.questions) {
|
||||
const existingLogicConditions = new Set();
|
||||
|
||||
if (existingQuestionIds.has(question.id)) {
|
||||
toast.error("There are 2 identical question IDs. Please update one.");
|
||||
return false;
|
||||
|
||||
+1
-6
@@ -18,12 +18,7 @@ const validationRules = {
|
||||
return question.label.trim() !== "";
|
||||
},
|
||||
defaultValidation: (question: TSurveyQuestion) => {
|
||||
console.log(question);
|
||||
return (
|
||||
question.headline.trim() !== "" &&
|
||||
question.buttonLabel?.trim() !== "" &&
|
||||
question.backButtonLabel?.trim() !== ""
|
||||
);
|
||||
return question.headline.trim() !== "";
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<div className="flex h-1/2 w-1/4 flex-col">
|
||||
<div className="ph-no-capture h-16 w-1/3 animate-pulse rounded-lg bg-gray-200 font-medium text-slate-900"></div>
|
||||
<div className="ph-no-capture mt-4 h-full animate-pulse rounded-lg bg-gray-200 text-slate-900"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { getShortUrl } from "@formbricks/lib/shortUrl/service";
|
||||
import { ZShortUrlId } from "@formbricks/types/v1/shortUrl";
|
||||
|
||||
export default async function ShortUrlPage({ params }) {
|
||||
if (!params.shortUrlId) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
if (ZShortUrlId.safeParse(params.shortUrlId).success !== true) {
|
||||
// return not found if unable to parse short url id
|
||||
notFound();
|
||||
}
|
||||
|
||||
let shortUrl;
|
||||
|
||||
try {
|
||||
shortUrl = await getShortUrl(params.shortUrlId);
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
}
|
||||
|
||||
if (shortUrl) {
|
||||
redirect(shortUrl.url);
|
||||
}
|
||||
|
||||
// return not found if short url not found
|
||||
notFound();
|
||||
}
|
||||
@@ -12,7 +12,7 @@ export default function NotFound() {
|
||||
<div className="flex flex-col items-center space-y-3 text-slate-300">
|
||||
<QuestionMarkCircleIcon className="h-20 w-20" />,
|
||||
<h1 className="text-4xl font-bold text-slate-800">Survey not found.</h1>
|
||||
<p className="text-lg leading-10 text-gray-500">There is not survey with this ID.</p>
|
||||
<p className="text-lg leading-10 text-gray-500">There is no survey with this ID.</p>
|
||||
<Button variant="darkCTA" className="mt-2" href="https://formbricks.com">
|
||||
Create your own
|
||||
</Button>
|
||||
|
||||
+3
-3
@@ -9,7 +9,6 @@ export const env = createEnv({
|
||||
server: {
|
||||
WEBAPP_URL: z.string().url().optional(),
|
||||
DATABASE_URL: z.string().url(),
|
||||
PRISMA_GENERATE_DATAPROXY: z.enum(["true", ""]).optional(),
|
||||
NEXTAUTH_SECRET: z.string().min(1),
|
||||
NEXTAUTH_URL: z.string().url().optional(),
|
||||
MAIL_FROM: z.string().email().optional(),
|
||||
@@ -48,7 +47,8 @@ export const env = createEnv({
|
||||
INVITE_DISABLED: z.enum(["1", "0"]).optional(),
|
||||
IS_FORMBRICKS_CLOUD: z.enum(["1", "0"]).optional(),
|
||||
VERCEL_URL: z.string().optional(),
|
||||
SURVEY_BASE_URL: z.string().optional(),
|
||||
SURVEY_BASE_URL: z.string().url().optional(),
|
||||
SHORT_SURVEY_BASE_URL: z.string().url().optional().or(z.string().length(0)),
|
||||
GOOGLE_SHEETS_CLIENT_ID: z.string().optional(),
|
||||
GOOGLE_SHEETS_CLIENT_SECRET: z.string().optional(),
|
||||
GOOGLE_SHEETS_REDIRECT_URL: z.string().optional(),
|
||||
@@ -81,7 +81,6 @@ export const env = createEnv({
|
||||
runtimeEnv: {
|
||||
WEBAPP_URL: process.env.WEBAPP_URL,
|
||||
DATABASE_URL: process.env.DATABASE_URL,
|
||||
PRISMA_GENERATE_DATAPROXY: process.env.PRISMA_GENERATE_DATAPROXY,
|
||||
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
|
||||
NEXTAUTH_URL: process.env.NEXTAUTH_URL,
|
||||
MAIL_FROM: process.env.MAIL_FROM,
|
||||
@@ -118,6 +117,7 @@ export const env = createEnv({
|
||||
FORMBRICKS_ENCRYPTION_KEY: process.env.FORMBRICKS_ENCRYPTION_KEY,
|
||||
VERCEL_URL: process.env.VERCEL_URL,
|
||||
SURVEY_BASE_URL: process.env.SURVEY_BASE_URL,
|
||||
SHORT_SURVEY_BASE_URL: process.env.SHORT_SURVEY_BASE_URL,
|
||||
NEXT_PUBLIC_SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
DateRange,
|
||||
SelectedFilterValue,
|
||||
} from "@/app/(app)/environments/[environmentId]/ResponseFilterContext";
|
||||
} from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
|
||||
import {
|
||||
OptionsType,
|
||||
QuestionOptions,
|
||||
|
||||
@@ -4,10 +4,8 @@ import { createId } from "@paralleldrive/cuid2";
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
|
||||
const isCloud = process.env.IS_FORMBRICKS_CLOUD === "1";
|
||||
|
||||
const nextConfig = {
|
||||
assetPrefix: isCloud ? process.env.WEBAPP_URL : undefined,
|
||||
assetPrefix: process.env.ASSET_PREFIX_URL || undefined,
|
||||
output: "standalone",
|
||||
experimental: {
|
||||
serverActions: true,
|
||||
|
||||
+11
-11
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@formbricks/web",
|
||||
"version": "1.0.3",
|
||||
"version": "1.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"clean": "rimraf .turbo node_modules .next",
|
||||
@@ -26,25 +26,25 @@
|
||||
"@radix-ui/react-collapsible": "^1.0.3",
|
||||
"@react-email/components": "^0.0.7",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@sentry/nextjs": "^7.72.0",
|
||||
"@t3-oss/env-nextjs": "^0.6.1",
|
||||
"@sentry/nextjs": "^7.73.0",
|
||||
"@t3-oss/env-nextjs": "^0.7.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"encoding": "^0.1.13",
|
||||
"eslint-config-next": "^13.5.3",
|
||||
"eslint-config-next": "^13.5.4",
|
||||
"googleapis": "^126.0.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.279.0",
|
||||
"next": "13.5.3",
|
||||
"next-auth": "^4.23.1",
|
||||
"lucide-react": "^0.284.0",
|
||||
"next": "13.5.4",
|
||||
"next-auth": "^4.23.2",
|
||||
"nodemailer": "^6.9.5",
|
||||
"posthog-js": "^1.81.1",
|
||||
"posthog-js": "^1.82.1",
|
||||
"prismjs": "^1.29.0",
|
||||
"react": "18.2.0",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-dom": "18.2.0",
|
||||
"react-email": "^1.9.4",
|
||||
"react-hook-form": "^7.46.2",
|
||||
"react-email": "^1.9.5",
|
||||
"react-hook-form": "^7.47.0",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-icons": "^4.11.0",
|
||||
"swr": "^2.2.4",
|
||||
@@ -55,7 +55,7 @@
|
||||
"@formbricks/tsconfig": "workspace:*",
|
||||
"@types/bcryptjs": "^2.4.4",
|
||||
"@types/lodash": "^4.14.199",
|
||||
"@types/markdown-it": "^13.0.1",
|
||||
"@types/markdown-it": "^13.0.2",
|
||||
"eslint-config-formbricks": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
+115
-2
@@ -1,4 +1,83 @@
|
||||
version: "3.3"
|
||||
|
||||
# If you already have a local .env then please run this using
|
||||
# docker compose --env-file /dev/null up
|
||||
|
||||
# This should be the same as below if you are running via docker compose up
|
||||
x-webapp-url: &webapp_url http://localhost:3000
|
||||
|
||||
# PostgreSQL DB for Formbricks to connect to
|
||||
x-database-url: &database_url postgresql://postgres:postgres@postgres:5432/formbricks?schema=public
|
||||
|
||||
# NextJS Auth
|
||||
# @see: https://next-auth.js.org/configuration/options#nextauth_secret
|
||||
# You can use: `openssl rand -base64 32` to generate one
|
||||
x-nextauth-secret: &nextauth_secret luJthrnoDpVgGakjVYlccsZ1FdlwxIWogWIsrxzoQ6E=
|
||||
|
||||
# Set this to your public-facing URL, e.g., https://example.com
|
||||
# You do not need the NEXTAUTH_URL environment variable in Vercel.
|
||||
x-nextauth-url: &nextauth_url http://localhost:3000
|
||||
|
||||
# Encryption key
|
||||
# You can use: `openssl rand -base64 16` to generate one
|
||||
x-formbricks-encryption-key: &formbricks_encryption_key
|
||||
|
||||
# Necessary if email verification and password reset are enabled.
|
||||
# See further below if you want to disable these features.
|
||||
x-mail-from: &mail_from
|
||||
x-smtp-host: &smtp_host
|
||||
x-smtp-port: &smtp_port
|
||||
# Enable SMTP_SECURE_ENABLED for TLS (port 465)
|
||||
x-smtp-secure-enabled: &smtp_secure_enabled
|
||||
x-smtp-user: &smtp_user
|
||||
x-smtp-password: &smtp_password
|
||||
|
||||
# Set the below value to your public-facing URL, e.g., https://example.com
|
||||
x-survey-base-url: &survey_base_url http://localhost:3000/s
|
||||
|
||||
# Set the below value if you have and want to share a shorter base URL than the x-survey-base-url
|
||||
x-short-survey-base-url: &short_survey_base_url
|
||||
|
||||
# Email Verification. If you enable Email Verification you have to setup SMTP-Settings, too.
|
||||
x-email-verification-disabled: &email_verification_disabled 1
|
||||
|
||||
# Password Reset. If you enable Password Reset functionality you have to setup SMTP-Settings, too.
|
||||
x-password-reset-disabled: &password_reset_disabled 1
|
||||
|
||||
# Signup. Disable the ability for new users to create an account.
|
||||
x-signup-disabled: &signup_disabled 0
|
||||
|
||||
# Team Invite. Disable the ability for invited users to create an account.
|
||||
x-invite-disabled: &invite_disabled 0
|
||||
|
||||
# Set the below values to display privacy policy, imprint and terms of service links in the footer of signup & public pages.
|
||||
x-privacy-url: &privacy_url
|
||||
x-terms-url: &terms_url
|
||||
x-imprint-url: &imprint_url
|
||||
|
||||
|
||||
# Configure Github Login
|
||||
x-github-auth-enabled: &github_auth_enabled 0
|
||||
x-github-id: &github_id
|
||||
x-github-secret: &github_secret
|
||||
|
||||
# Configure Google Login
|
||||
x-google-auth-enabled: &google_auth_enabled 0
|
||||
x-google-client-id: &google_client_id
|
||||
x-google-client-secret: &google_client_secret
|
||||
|
||||
# Disable Sentry warning
|
||||
x-sentry-ignore-api-resolution-error: &sentry_ignore_api_resolution_error
|
||||
|
||||
# Enable Sentry Error Tracking
|
||||
x-next-public-sentry-dsn: &next_public_sentry_dsn
|
||||
|
||||
# Cron Secret
|
||||
x-cron-secret: &cron_secret
|
||||
|
||||
# Configure ASSET_PREFIX_URL when you want to ship JS & CSS files from a complete URL instead of the current domain
|
||||
x-asset-prefix-url: &asset_prefix_url
|
||||
|
||||
services:
|
||||
postgres:
|
||||
restart: always
|
||||
@@ -13,13 +92,47 @@ services:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./apps/web/Dockerfile
|
||||
args:
|
||||
DATABASE_URL: *database_url
|
||||
NEXTAUTH_SECRET: *nextauth_secret
|
||||
|
||||
depends_on:
|
||||
- postgres
|
||||
env_file:
|
||||
- .env.docker
|
||||
ports:
|
||||
- 3000:3000
|
||||
|
||||
environment:
|
||||
WEBAPP_URL: *webapp_url
|
||||
DATABASE_URL: *database_url
|
||||
NEXTAUTH_SECRET: *nextauth_secret
|
||||
NEXTAUTH_URL: *nextauth_url
|
||||
MAIL_FROM: *mail_from
|
||||
SMTP_HOST: *smtp_host
|
||||
SMTP_PORT: *smtp_port
|
||||
SMTP_SECURE_ENABLED: *smtp_secure_enabled
|
||||
SMTP_USER: *smtp_user
|
||||
SMTP_PASSWORD: *smtp_password
|
||||
FORMBRICKS_ENCRYPTION_KEY: *formbricks_encryption_key
|
||||
SURVEY_BASE_URL: *survey_base_url
|
||||
SHORT_SURVEY_BASE_URL: *short_survey_base_url
|
||||
PRIVACY_URL: *privacy_url
|
||||
TERMS_URL: *terms_url
|
||||
IMPRINT_URL: *imprint_url
|
||||
EMAIL_VERIFICATION_DISABLED: *email_verification_disabled
|
||||
PASSWORD_RESET_DISABLED: *password_reset_disabled
|
||||
SIGNUP_DISABLED: *signup_disabled
|
||||
INVITE_DISABLED: *invite_disabled
|
||||
SENTRY_IGNORE_API_RESOLUTION_ERROR: *sentry_ignore_api_resolution_error
|
||||
NEXT_PUBLIC_SENTRY_DSN: *next_public_sentry_dsn
|
||||
GITHUB_AUTH_ENABLED: *github_auth_enabled
|
||||
GITHUB_ID: *github_id
|
||||
GITHUB_SECRET: *github_secret
|
||||
GOOGLE_AUTH_ENABLED: *google_auth_enabled
|
||||
GOOGLE_CLIENT_ID: *google_client_id
|
||||
GOOGLE_CLIENT_SECRET: *google_client_secret
|
||||
CRON_SECRET: *cron_secret
|
||||
ASSET_PREFIX_URL: *asset_prefix_url
|
||||
|
||||
volumes:
|
||||
postgres:
|
||||
driver: local
|
||||
|
||||
@@ -7,12 +7,6 @@ x-environment: &environment
|
||||
# PostgreSQL DB for Formbricks to connect to
|
||||
DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/formbricks?schema=public"
|
||||
|
||||
# Uncomment to enable a dedicated connection pool for Prisma using Prisma Data Proxy
|
||||
# Cold boots will be faster and you'll be able to scale your DB independently of your app.
|
||||
# @see https://www.prisma.io/docs/data-platform/data-proxy/use-data-proxy
|
||||
# PRISMA_GENERATE_DATAPROXY=true
|
||||
PRISMA_GENERATE_DATAPROXY:
|
||||
|
||||
# NextJS Auth
|
||||
# @see: https://next-auth.js.org/configuration/options#nextauth_secret
|
||||
# You can use: `openssl rand -base64 32` to generate one
|
||||
@@ -22,6 +16,10 @@ x-environment: &environment
|
||||
# You do not need the NEXTAUTH_URL environment variable in Vercel.
|
||||
NEXTAUTH_URL: http://localhost:3000
|
||||
|
||||
# Formbricks Encryption Key is used to generate encrypted single use URLs for Link Surveys
|
||||
# You can use: $(openssl rand -base64 16) to generate one
|
||||
# FORMBRICKS_ENCRYPTION_KEY:
|
||||
|
||||
# PostgreSQL password
|
||||
POSTGRES_PASSWORD: postgres
|
||||
|
||||
@@ -33,6 +31,12 @@ x-environment: &environment
|
||||
SMTP_USER:
|
||||
SMTP_PASSWORD:
|
||||
|
||||
# Set the below value if you want to have another base URL apart from your Domain Name
|
||||
# SURVEY_BASE_URL:
|
||||
|
||||
# Set the below value if you have and want to use a custom URL for the links created by the Link Shortener
|
||||
# SHORT_SURVEY_BASE_URL:
|
||||
|
||||
# Uncomment the below and set it to 1 to disable Email Verification for new signups
|
||||
# EMAIL_VERIFICATION_DISABLED:
|
||||
|
||||
@@ -64,6 +68,9 @@ x-environment: &environment
|
||||
# GOOGLE_CLIENT_ID:
|
||||
# GOOGLE_CLIENT_SECRET:
|
||||
|
||||
# Configure ASSET_PREFIX_URL when you want to ship JS & CSS files from a complete URL instead of the current domain
|
||||
# ASSET_PREFIX_URL: *asset_prefix_url
|
||||
|
||||
services:
|
||||
postgres:
|
||||
restart: always
|
||||
|
||||
+53
-7
@@ -161,12 +161,6 @@ x-environment: &environment
|
||||
# PostgreSQL DB for Formbricks to connect to
|
||||
DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/formbricks?schema=public"
|
||||
|
||||
# Uncomment to enable a dedicated connection pool for Prisma using Prisma Data Proxy
|
||||
# Cold boots will be faster and you'll be able to scale your DB independently of your app.
|
||||
# @see https://www.prisma.io/docs/data-platform/data-proxy/use-data-proxy
|
||||
# PRISMA_GENERATE_DATAPROXY: true
|
||||
PRISMA_GENERATE_DATAPROXY:
|
||||
|
||||
# NextJS Auth
|
||||
# @see: https://next-auth.js.org/configuration/options#nextauth_secret
|
||||
# You can use: $(openssl rand -base64 32) to generate one
|
||||
@@ -176,12 +170,57 @@ x-environment: &environment
|
||||
# You do not need the NEXTAUTH_URL environment variable in Vercel.
|
||||
NEXTAUTH_URL: "https://$domain_name"
|
||||
|
||||
# Formbricks Encryption Key is used to generate encrypted single use URLs for Link Surveys
|
||||
# You can use: $(openssl rand -base64 16) to generate one
|
||||
FORMBRICKS_ENCRYPTION_KEY:
|
||||
|
||||
# PostgreSQL password
|
||||
POSTGRES_PASSWORD: postgres
|
||||
|
||||
# Email configuration
|
||||
$email_config
|
||||
|
||||
# Set the below value if you want to have another base URL apart from your Domain Name
|
||||
# SURVEY_BASE_URL:
|
||||
|
||||
# Set the below value if you have and want to use a custom URL for the links created by the Link Shortener
|
||||
# SHORT_SURVEY_BASE_URL:
|
||||
|
||||
# Uncomment the below and set it to 1 to disable Email Verification for new signups
|
||||
# EMAIL_VERIFICATION_DISABLED:
|
||||
|
||||
# Uncomment the below and set it to 1 to disable Password Reset
|
||||
# PASSWORD_RESET_DISABLED:
|
||||
|
||||
# Uncomment the below and set it to 1 to disable Signups
|
||||
# SIGNUP_DISABLED:
|
||||
|
||||
# Uncomment the below and set it to 1 to disable Invites
|
||||
# INVITE_DISABLED:
|
||||
|
||||
# Uncomment the below and set a value to have your own Privacy Page URL on the signup & login page
|
||||
# PRIVACY_URL:
|
||||
|
||||
# Uncomment the below and set a value to have your own Terms Page URL on the auth and the surveys page
|
||||
# TERMS_URL:
|
||||
|
||||
# Uncomment the below and set a value to have your own Imprint Page URL on the auth and the surveys page
|
||||
# IMPRINT_URL:
|
||||
|
||||
# Uncomment the below and set to 1 if you want to enable GitHub OAuth
|
||||
# GITHUB_AUTH_ENABLED:
|
||||
# GITHUB_ID:
|
||||
# GITHUB_SECRET:
|
||||
|
||||
# Uncomment the below and set to 1 if you want to enable Google OAuth
|
||||
# GOOGLE_AUTH_ENABLED:
|
||||
# GOOGLE_CLIENT_ID:
|
||||
# GOOGLE_CLIENT_SECRET:
|
||||
|
||||
# Configure ASSET_PREFIX_URL when you want to ship JS & CSS files from a complete URL instead of the current domain
|
||||
# ASSET_PREFIX_URL: *asset_prefix_url
|
||||
|
||||
|
||||
services:
|
||||
postgres:
|
||||
restart: always
|
||||
@@ -226,9 +265,16 @@ echo "🚙 Updating NEXTAUTH_SECRET in the Formbricks container..."
|
||||
nextauth_secret=$(openssl rand -base64 32 | tr -dc 'a-zA-Z0-9' | head -c 32) && sed -i "/NEXTAUTH_SECRET:$/s/NEXTAUTH_SECRET:.*/NEXTAUTH_SECRET: $nextauth_secret/" docker-compose.yml
|
||||
echo "🚗 NEXTAUTH_SECRET updated successfully!"
|
||||
|
||||
echo "🚙 Updating FORMBRICKS_ENCRYPTION_KEY in the Formbricks container..."
|
||||
formbricks_encryption_key=$(openssl rand -base64 16 | tr -dc 'a-zA-Z0-9' | head -c 16) && sed -i "/FORMBRICKS_ENCRYPTION_KEY:$/s/FORMBRICKS_ENCRYPTION_KEY:.*/FORMBRICKS_ENCRYPTION_KEY: $formbricks_encryption_key/" docker-compose.yml
|
||||
echo "🚗 FORMBRICKS_ENCRYPTION_KEY updated successfully!"
|
||||
|
||||
|
||||
newgrp docker <<END
|
||||
|
||||
docker compose up -d
|
||||
docker compose --env-file /dev/null up
|
||||
|
||||
echo "🔗 To edit more variables and deeper config, go to the formbricks/docker-compose.yml, edit the file, and restart the container!"
|
||||
|
||||
echo "🚨 Make sure you have set up the DNS records as well as inbound rules for the domain name and IP address of this instance."
|
||||
echo ""
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"@formbricks/lib": "workspace:*",
|
||||
"@formbricks/tsconfig": "workspace:*",
|
||||
"tsup": "^7.2.0",
|
||||
"typescript": "5.1.6",
|
||||
"typescript": "5.2.2",
|
||||
"eslint-config-formbricks": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "ShortUrl" (
|
||||
"id" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
"url" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "ShortUrl_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ShortUrl_url_key" ON "ShortUrl"("url");
|
||||
@@ -25,7 +25,7 @@
|
||||
"predev": "pnpm generate"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^5.3.1",
|
||||
"@prisma/client": "^5.4.1",
|
||||
"@prisma/extension-accelerate": "^0.6.2",
|
||||
"dotenv-cli": "^7.3.0"
|
||||
},
|
||||
@@ -33,10 +33,10 @@
|
||||
"@formbricks/tsconfig": "workspace:*",
|
||||
"@formbricks/types": "workspace:*",
|
||||
"eslint-config-formbricks": "workspace:*",
|
||||
"prisma": "^5.3.1",
|
||||
"prisma": "^5.4.1",
|
||||
"prisma-dbml-generator": "^0.10.0",
|
||||
"prisma-json-types-generator": "^3.0.1",
|
||||
"zod": "^3.22.3",
|
||||
"zod": "^3.22.4",
|
||||
"zod-prisma": "^0.5.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -513,3 +513,10 @@ model User {
|
||||
/// [UserNotificationSettings]
|
||||
notificationSettings Json @default("{}")
|
||||
}
|
||||
|
||||
model ShortUrl {
|
||||
id String @id // generate nanoId in service
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @updatedAt @map(name: "updated_at")
|
||||
url String @unique
|
||||
}
|
||||
|
||||
@@ -16,6 +16,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@formbricks/database": "workspace:*",
|
||||
"stripe": "^13.7.0"
|
||||
"stripe": "^13.8.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.50.0",
|
||||
"eslint-config-next": "^13.5.3",
|
||||
"eslint-config-next": "^13.5.4",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-config-turbo": "latest",
|
||||
"eslint-plugin-react": "7.33.2"
|
||||
|
||||
@@ -42,8 +42,8 @@
|
||||
"@formbricks/tsconfig": "workspace:*",
|
||||
"@formbricks/types": "workspace:*",
|
||||
"@types/jest": "^29.5.5",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.3",
|
||||
"@typescript-eslint/parser": "^6.7.3",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.4",
|
||||
"@typescript-eslint/parser": "^6.7.4",
|
||||
"babel-jest": "^29.7.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint-config-formbricks": "workspace:*",
|
||||
@@ -54,11 +54,11 @@
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"jest-preset-preact": "^4.1.0",
|
||||
"microbundle": "^0.15.1",
|
||||
"preact": "10.17.1",
|
||||
"preact": "10.18.1",
|
||||
"preact-cli": "^3.5.0",
|
||||
"preact-render-to-string": "^6.2.1",
|
||||
"preact-render-to-string": "^6.2.2",
|
||||
"regenerator-runtime": "^0.14.0",
|
||||
"terser": "^5.20.0"
|
||||
"terser": "^5.21.0"
|
||||
},
|
||||
"jest": {
|
||||
"transformIgnorePatterns": [
|
||||
|
||||
@@ -13,6 +13,10 @@ export const WEBAPP_URL =
|
||||
|
||||
export const SURVEY_BASE_URL = env.SURVEY_BASE_URL ? env.SURVEY_BASE_URL + "/" : `${WEBAPP_URL}/s/`;
|
||||
|
||||
export const SHORT_SURVEY_BASE_URL = env.SHORT_SURVEY_BASE_URL
|
||||
? env.SHORT_SURVEY_BASE_URL + "/"
|
||||
: `${WEBAPP_URL}/i/`;
|
||||
|
||||
// Other
|
||||
export const INTERNAL_SECRET = process.env.INTERNAL_SECRET || "";
|
||||
export const FORMBRICKS_ENCRYPTION_KEY = env.FORMBRICKS_ENCRYPTION_KEY || undefined;
|
||||
|
||||
@@ -16,9 +16,10 @@
|
||||
"@formbricks/types": "*",
|
||||
"@paralleldrive/cuid2": "^2.2.2",
|
||||
"date-fns": "^2.30.0",
|
||||
"next-auth": "^4.22.3",
|
||||
"next-auth": "^4.23.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"markdown-it": "^13.0.2",
|
||||
"nanoid": "^5.0.1",
|
||||
"nodemailer": "^6.9.5",
|
||||
"posthog-node": "^3.1.2",
|
||||
"server-only": "^0.0.1",
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { DatabaseError } from "@formbricks/types/v1/errors";
|
||||
import { TShortUrl, ZShortUrlId } from "@formbricks/types/v1/shortUrl";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { validateInputs } from "../utils/validate";
|
||||
import z from "zod";
|
||||
|
||||
// Create the short url and return it
|
||||
export const createShortUrl = async (url: string): Promise<TShortUrl> => {
|
||||
validateInputs([url, z.string().url()]);
|
||||
|
||||
try {
|
||||
// Check if an entry with the provided fullUrl already exists.
|
||||
const existingShortUrl = await getShortUrlByUrl(url);
|
||||
|
||||
if (existingShortUrl) {
|
||||
return existingShortUrl;
|
||||
}
|
||||
|
||||
// If an entry with the provided fullUrl does not exist, create a new one.
|
||||
const id = customAlphabet("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", 10)();
|
||||
|
||||
return await prisma.shortUrl.create({
|
||||
data: {
|
||||
id,
|
||||
url,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError("Database operation failed");
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Get the full url from short url and return it
|
||||
export const getShortUrl = async (id: string): Promise<TShortUrl | null> => {
|
||||
validateInputs([id, ZShortUrlId]);
|
||||
try {
|
||||
return await prisma.shortUrl.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError("Database operation failed");
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getShortUrlByUrl = async (url: string): Promise<TShortUrl | null> => {
|
||||
validateInputs([url, z.string().url()]);
|
||||
try {
|
||||
return await prisma.shortUrl.findUnique({
|
||||
where: {
|
||||
url,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError("Database operation failed");
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -99,6 +99,9 @@ export const getSurveyWithAnalytics = async (surveyId: string): Promise<TSurveyW
|
||||
select: selectSurveyWithAnalytics,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.error(error.message);
|
||||
}
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError("Database operation failed");
|
||||
}
|
||||
@@ -132,7 +135,9 @@ export const getSurveyWithAnalytics = async (surveyId: string): Promise<TSurveyW
|
||||
const survey = ZSurveyWithAnalytics.parse(transformedSurvey);
|
||||
return survey;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (error instanceof Error) {
|
||||
console.error(error.message);
|
||||
}
|
||||
if (error instanceof z.ZodError) {
|
||||
console.error(JSON.stringify(error.errors, null, 2)); // log the detailed error information
|
||||
}
|
||||
@@ -172,6 +177,9 @@ export const getSurvey = async (surveyId: string): Promise<TSurvey | null> => {
|
||||
select: selectSurvey,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.error(error.message);
|
||||
}
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError("Database operation failed");
|
||||
}
|
||||
@@ -192,6 +200,9 @@ export const getSurvey = async (surveyId: string): Promise<TSurvey | null> => {
|
||||
const survey = ZSurvey.parse(transformedSurvey);
|
||||
return survey;
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.error(error.message);
|
||||
}
|
||||
if (error instanceof z.ZodError) {
|
||||
console.error(JSON.stringify(error.errors, null, 2)); // log the detailed error information
|
||||
}
|
||||
@@ -243,6 +254,9 @@ export const getSurveysByAttributeClassId = async (attributeClassId: string): Pr
|
||||
}
|
||||
return surveys;
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.error(error.message);
|
||||
}
|
||||
if (error instanceof z.ZodError) {
|
||||
console.error(JSON.stringify(error.errors, null, 2)); // log the detailed error information
|
||||
}
|
||||
@@ -277,6 +291,9 @@ export const getSurveysByActionClassId = async (actionClassId: string): Promise<
|
||||
}
|
||||
return surveys;
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.error(error.message);
|
||||
}
|
||||
if (error instanceof z.ZodError) {
|
||||
console.error(JSON.stringify(error.errors, null, 2)); // log the detailed error information
|
||||
}
|
||||
@@ -297,6 +314,9 @@ export const getSurveys = async (environmentId: string): Promise<TSurvey[]> => {
|
||||
select: selectSurvey,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.error(error.message);
|
||||
}
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError("Database operation failed");
|
||||
}
|
||||
@@ -317,6 +337,9 @@ export const getSurveys = async (environmentId: string): Promise<TSurvey[]> => {
|
||||
}
|
||||
return surveys;
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.error(error.message);
|
||||
}
|
||||
if (error instanceof z.ZodError) {
|
||||
console.error(JSON.stringify(error.errors, null, 2)); // log the detailed error information
|
||||
}
|
||||
@@ -353,6 +376,9 @@ export const getSurveysWithAnalytics = async (environmentId: string): Promise<TS
|
||||
select: selectSurveyWithAnalytics,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.error(error.message);
|
||||
}
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError("Database operation failed");
|
||||
}
|
||||
@@ -381,6 +407,9 @@ export const getSurveysWithAnalytics = async (environmentId: string): Promise<TS
|
||||
}
|
||||
return surveys;
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.error(error.message);
|
||||
}
|
||||
if (error instanceof z.ZodError) {
|
||||
console.error(JSON.stringify(error.errors, null, 2)); // log the detailed error information
|
||||
}
|
||||
@@ -578,7 +607,9 @@ export async function updateSurvey(updatedSurvey: Partial<TSurvey>): Promise<TSu
|
||||
|
||||
return modifiedSurvey;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (error instanceof Error) {
|
||||
console.error(error.message);
|
||||
}
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError("Database operation failed");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { validateInputs } from "../utils/validate";
|
||||
import { hasUserEnvironmentAccess } from "../environment/auth";
|
||||
import { getWebhook } from "./service";
|
||||
import { unstable_cache } from "next/cache";
|
||||
import { ZId } from "@formbricks/types/v1/environment";
|
||||
|
||||
export const canUserAccessWebhook = async (userId: string, webhookId: string): Promise<boolean> =>
|
||||
await unstable_cache(
|
||||
async () => {
|
||||
validateInputs([userId, ZId], [webhookId, ZId]);
|
||||
|
||||
const webhook = await getWebhook(webhookId);
|
||||
if (!webhook) return false;
|
||||
|
||||
const hasAccessToEnvironment = await hasUserEnvironmentAccess(userId, webhook.environmentId);
|
||||
if (!hasAccessToEnvironment) return false;
|
||||
|
||||
return true;
|
||||
},
|
||||
[`${userId}-${webhookId}`],
|
||||
{
|
||||
revalidate: 30 * 60, // 30 minutes
|
||||
}
|
||||
)();
|
||||
@@ -45,7 +45,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.18",
|
||||
"@types/request-promise-native": "~1.0.18",
|
||||
"@types/request-promise-native": "~1.0.19",
|
||||
"@typescript-eslint/parser": "~6.7",
|
||||
"eslint-plugin-n8n-nodes-base": "^1.16.0",
|
||||
"gulp": "^4.0.2",
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^3.0.3",
|
||||
"prettier-plugin-tailwindcss": "^0.5.4"
|
||||
"prettier-plugin-tailwindcss": "^0.5.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,10 +21,10 @@
|
||||
"autoprefixer": "^10.4.16",
|
||||
"eslint-config-formbricks": "workspace:*",
|
||||
"postcss": "^8.4.31",
|
||||
"preact": "^10.17.1",
|
||||
"preact": "^10.18.1",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"terser": "^5.20.0",
|
||||
"terser": "^5.21.0",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^4.4.9"
|
||||
"vite": "^4.4.11"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
"clean": "rimraf node_modules"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "20.7.0",
|
||||
"@types/react": "18.2.23",
|
||||
"@types/react-dom": "18.2.7",
|
||||
"@types/node": "20.8.2",
|
||||
"@types/react": "18.2.25",
|
||||
"@types/react-dom": "18.2.10",
|
||||
"typescript": "5.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,6 @@
|
||||
"@formbricks/tsconfig": "workspace:*"
|
||||
},
|
||||
"dependencies": {
|
||||
"zod": "^3.22.3"
|
||||
"zod": "^3.22.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import z from "zod";
|
||||
|
||||
export const ZShortUrlId = z.string().length(10);
|
||||
|
||||
export type TShortUrlId = z.infer<typeof ZShortUrlId>;
|
||||
|
||||
export const ZShortUrl = z.object({
|
||||
id: ZShortUrlId,
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
url: z.string().url(),
|
||||
});
|
||||
|
||||
export type TShortUrl = z.infer<typeof ZShortUrl>;
|
||||
@@ -43,8 +43,8 @@
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"cmdk": "^0.2.0",
|
||||
"lucide-react": "^0.279.0",
|
||||
"next": "13.5.3",
|
||||
"lucide-react": "^0.284.0",
|
||||
"next": "13.5.4",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-confetti": "^6.1.0",
|
||||
"react-day-picker": "^8.8.2",
|
||||
|
||||
Generated
+604
-620
File diff suppressed because it is too large
Load Diff
+3
-3
@@ -27,9 +27,9 @@
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": ["dist/**", ".next/**"],
|
||||
"env": [
|
||||
"ASSET_PREFIX_URL",
|
||||
"CRON_SECRET",
|
||||
"DEBUG",
|
||||
"PRISMA_GENERATE_DATAPROXY",
|
||||
"GITHUB_ID",
|
||||
"GITHUB_SECRET",
|
||||
"GOOGLE_CLIENT_ID",
|
||||
@@ -74,6 +74,7 @@
|
||||
"IMPRINT_URL",
|
||||
"NEXT_PUBLIC_SENTRY_DSN",
|
||||
"SURVEY_BASE_URL",
|
||||
"SHORT_SURVEY_BASE_URL",
|
||||
"NODE_ENV",
|
||||
"NEXT_PUBLIC_POSTHOG_API_HOST",
|
||||
"NEXT_PUBLIC_POSTHOG_API_KEY",
|
||||
@@ -94,8 +95,7 @@
|
||||
"cache": false,
|
||||
"dependsOn": [],
|
||||
"outputs": [],
|
||||
"inputs": ["./schema.prisma"],
|
||||
"env": ["PRISMA_GENERATE_DATAPROXY"]
|
||||
"inputs": ["./schema.prisma"]
|
||||
},
|
||||
"db:setup": {
|
||||
"cache": false,
|
||||
|
||||
Reference in New Issue
Block a user