mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-02 10:30:23 -06:00
Compare commits
5 Commits
feat-reset
...
fix-broken
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c34f43c83 | ||
|
|
979fd71a11 | ||
|
|
1be23eebbb | ||
|
|
d10cff917d | ||
|
|
da72101320 |
@@ -25,21 +25,9 @@ RUN corepack prepare pnpm@9.15.9 --activate
|
||||
# Install necessary build tools and compilers
|
||||
RUN apk update && apk add --no-cache cmake g++ gcc jq make openssl-dev python3
|
||||
|
||||
# BuildKit secret handling without hardcoded fallback values
|
||||
# This approach relies entirely on secrets passed from GitHub Actions
|
||||
RUN echo '#!/bin/sh' > /tmp/read-secrets.sh && \
|
||||
echo 'if [ -f "/run/secrets/database_url" ]; then' >> /tmp/read-secrets.sh && \
|
||||
echo ' export DATABASE_URL=$(cat /run/secrets/database_url)' >> /tmp/read-secrets.sh && \
|
||||
echo 'else' >> /tmp/read-secrets.sh && \
|
||||
echo ' echo "DATABASE_URL secret not found. Build may fail if this is required."' >> /tmp/read-secrets.sh && \
|
||||
echo 'fi' >> /tmp/read-secrets.sh && \
|
||||
echo 'if [ -f "/run/secrets/encryption_key" ]; then' >> /tmp/read-secrets.sh && \
|
||||
echo ' export ENCRYPTION_KEY=$(cat /run/secrets/encryption_key)' >> /tmp/read-secrets.sh && \
|
||||
echo 'else' >> /tmp/read-secrets.sh && \
|
||||
echo ' echo "ENCRYPTION_KEY secret not found. Build may fail if this is required."' >> /tmp/read-secrets.sh && \
|
||||
echo 'fi' >> /tmp/read-secrets.sh && \
|
||||
echo 'exec "$@"' >> /tmp/read-secrets.sh && \
|
||||
chmod +x /tmp/read-secrets.sh
|
||||
# Copy the secrets handling script
|
||||
COPY apps/web/scripts/docker/read-secrets.sh /tmp/read-secrets.sh
|
||||
RUN chmod +x /tmp/read-secrets.sh
|
||||
|
||||
# Increase Node.js memory limit as a regular build argument
|
||||
ARG NODE_OPTIONS="--max_old_space_size=4096"
|
||||
@@ -62,6 +50,9 @@ RUN touch apps/web/.env
|
||||
# Install the dependencies
|
||||
RUN pnpm install --ignore-scripts
|
||||
|
||||
# Build the database package first
|
||||
RUN pnpm build --filter=@formbricks/database
|
||||
|
||||
# Build the project using our secret reader script
|
||||
# This mounts the secrets only during this build step without storing them in layers
|
||||
RUN --mount=type=secret,id=database_url \
|
||||
@@ -106,20 +97,8 @@ RUN chown -R nextjs:nextjs ./apps/web/public && chmod -R 755 ./apps/web/public
|
||||
COPY --from=installer /app/packages/database/schema.prisma ./packages/database/schema.prisma
|
||||
RUN chown nextjs:nextjs ./packages/database/schema.prisma && chmod 644 ./packages/database/schema.prisma
|
||||
|
||||
COPY --from=installer /app/packages/database/package.json ./packages/database/package.json
|
||||
RUN chown nextjs:nextjs ./packages/database/package.json && chmod 644 ./packages/database/package.json
|
||||
|
||||
COPY --from=installer /app/packages/database/migration ./packages/database/migration
|
||||
RUN chown -R nextjs:nextjs ./packages/database/migration && chmod -R 755 ./packages/database/migration
|
||||
|
||||
COPY --from=installer /app/packages/database/src ./packages/database/src
|
||||
RUN chown -R nextjs:nextjs ./packages/database/src && chmod -R 755 ./packages/database/src
|
||||
|
||||
COPY --from=installer /app/packages/database/node_modules ./packages/database/node_modules
|
||||
RUN chown -R nextjs:nextjs ./packages/database/node_modules && chmod -R 755 ./packages/database/node_modules
|
||||
|
||||
COPY --from=installer /app/packages/logger/dist ./packages/database/node_modules/@formbricks/logger/dist
|
||||
RUN chown -R nextjs:nextjs ./packages/database/node_modules/@formbricks/logger/dist && chmod -R 755 ./packages/database/node_modules/@formbricks/logger/dist
|
||||
COPY --from=installer /app/packages/database/dist ./packages/database/dist
|
||||
RUN chown -R nextjs:nextjs ./packages/database/dist && chmod -R 755 ./packages/database/dist
|
||||
|
||||
COPY --from=installer /app/node_modules/@prisma/client ./node_modules/@prisma/client
|
||||
RUN chown -R nextjs:nextjs ./node_modules/@prisma/client && chmod -R 755 ./node_modules/@prisma/client
|
||||
@@ -142,12 +121,14 @@ RUN chmod -R 755 ./node_modules/@noble/hashes
|
||||
COPY --from=installer /app/node_modules/zod ./node_modules/zod
|
||||
RUN chmod -R 755 ./node_modules/zod
|
||||
|
||||
RUN npm install --ignore-scripts -g tsx typescript pino-pretty
|
||||
RUN npm install -g prisma
|
||||
|
||||
# Create a startup script to handle the conditional logic
|
||||
COPY --from=installer /app/apps/web/scripts/docker/next-start.sh /home/nextjs/start.sh
|
||||
RUN chown nextjs:nextjs /home/nextjs/start.sh && chmod +x /home/nextjs/start.sh
|
||||
|
||||
EXPOSE 3000
|
||||
ENV HOSTNAME "0.0.0.0"
|
||||
ENV NODE_ENV="production"
|
||||
ENV HOSTNAME="0.0.0.0"
|
||||
USER nextjs
|
||||
|
||||
# Prepare volume for uploads
|
||||
@@ -158,12 +139,4 @@ VOLUME /home/nextjs/apps/web/uploads/
|
||||
RUN mkdir -p /home/nextjs/apps/web/saml-connection
|
||||
VOLUME /home/nextjs/apps/web/saml-connection
|
||||
|
||||
CMD if [ "${DOCKER_CRON_ENABLED:-1}" = "1" ]; then \
|
||||
echo "Starting cron jobs..."; \
|
||||
supercronic -quiet /app/docker/cronjobs & \
|
||||
else \
|
||||
echo "Docker cron jobs are disabled via DOCKER_CRON_ENABLED=0"; \
|
||||
fi; \
|
||||
(cd packages/database && npm run db:migrate:deploy) && \
|
||||
(cd packages/database && npm run db:create-saml-database:deploy) && \
|
||||
exec node apps/web/server.js
|
||||
CMD ["/home/nextjs/start.sh"]
|
||||
@@ -94,6 +94,7 @@ describe("LandingSidebar component", () => {
|
||||
organizationId: "o1",
|
||||
redirect: true,
|
||||
callbackUrl: "/auth/login",
|
||||
clearEnvironmentId: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -130,6 +130,7 @@ export const LandingSidebar = ({
|
||||
organizationId: organization.id,
|
||||
redirect: true,
|
||||
callbackUrl: "/auth/login",
|
||||
clearEnvironmentId: true,
|
||||
});
|
||||
}}
|
||||
icon={<LogOutIcon className="mr-2 h-4 w-4" strokeWidth={1.5} />}>
|
||||
|
||||
@@ -221,7 +221,6 @@ describe("MainNavigation", () => {
|
||||
vi.mocked(useSignOut).mockReturnValue({ signOut: mockSignOut });
|
||||
|
||||
// Set up localStorage spy on the mocked localStorage
|
||||
const removeItemSpy = vi.spyOn(window.localStorage, "removeItem");
|
||||
|
||||
render(<MainNavigation {...defaultProps} />);
|
||||
|
||||
@@ -243,23 +242,18 @@ describe("MainNavigation", () => {
|
||||
const logoutButton = screen.getByText("common.logout");
|
||||
await userEvent.click(logoutButton);
|
||||
|
||||
// Verify localStorage.removeItem is called with the correct key
|
||||
expect(removeItemSpy).toHaveBeenCalledWith("formbricks-environment-id");
|
||||
|
||||
expect(mockSignOut).toHaveBeenCalledWith({
|
||||
reason: "user_initiated",
|
||||
redirectUrl: "/auth/login",
|
||||
organizationId: "org1",
|
||||
redirect: false,
|
||||
callbackUrl: "/auth/login",
|
||||
clearEnvironmentId: true,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockRouterPush).toHaveBeenCalledWith("/auth/login");
|
||||
});
|
||||
|
||||
// Clean up spy
|
||||
removeItemSpy.mockRestore();
|
||||
});
|
||||
|
||||
test("handles organization switching", async () => {
|
||||
|
||||
@@ -4,7 +4,6 @@ import { getLatestStableFbReleaseAction } from "@/app/(app)/environments/[enviro
|
||||
import { NavigationLink } from "@/app/(app)/environments/[environmentId]/components/NavigationLink";
|
||||
import FBLogo from "@/images/formbricks-wordmark.svg";
|
||||
import { cn } from "@/lib/cn";
|
||||
import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@/lib/localStorage";
|
||||
import { getAccessFlags } from "@/lib/membership/utils";
|
||||
import { capitalizeFirstLetter } from "@/lib/utils/strings";
|
||||
import { useSignOut } from "@/modules/auth/hooks/use-sign-out";
|
||||
@@ -391,14 +390,13 @@ export const MainNavigation = ({
|
||||
|
||||
<DropdownMenuItem
|
||||
onClick={async () => {
|
||||
localStorage.removeItem(FORMBRICKS_ENVIRONMENT_ID_LS);
|
||||
|
||||
const route = await signOutWithAudit({
|
||||
reason: "user_initiated",
|
||||
redirectUrl: "/auth/login",
|
||||
organizationId: organization.id,
|
||||
redirect: false,
|
||||
callbackUrl: "/auth/login",
|
||||
clearEnvironmentId: true,
|
||||
});
|
||||
router.push(route?.url || "/auth/login"); // NOSONAR // We want to check for empty strings
|
||||
}}
|
||||
|
||||
@@ -13,7 +13,7 @@ import { AuthenticatedActionClientCtx } from "@/lib/utils/action-client/types/co
|
||||
import { rateLimit } from "@/lib/utils/rate-limit";
|
||||
import { updateBrevoCustomer } from "@/modules/auth/lib/brevo";
|
||||
import { withAuditLogging } from "@/modules/ee/audit-logs/lib/handler";
|
||||
import { sendVerificationNewEmail } from "@/modules/email";
|
||||
import { sendForgotPasswordEmail, sendVerificationNewEmail } from "@/modules/email";
|
||||
import { z } from "zod";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import {
|
||||
@@ -162,3 +162,21 @@ export const removeAvatarAction = authenticatedActionClient.schema(ZRemoveAvatar
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export const resetPasswordAction = authenticatedActionClient.action(
|
||||
withAuditLogging(
|
||||
"passwordReset",
|
||||
"user",
|
||||
async ({ ctx }: { ctx: AuthenticatedActionClientCtx; parsedInput: undefined }) => {
|
||||
if (ctx.user.identityProvider !== "email") {
|
||||
throw new OperationNotAllowedError("auth.reset-password.not-allowed");
|
||||
}
|
||||
|
||||
await sendForgotPasswordEmail(ctx.user);
|
||||
|
||||
ctx.auditLoggingCtx.userId = ctx.user.id;
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
@@ -3,7 +3,7 @@ import userEvent from "@testing-library/user-event";
|
||||
import toast from "react-hot-toast";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import { updateUserAction } from "../actions";
|
||||
import { resetPasswordAction, updateUserAction } from "../actions";
|
||||
import { EditProfileDetailsForm } from "./EditProfileDetailsForm";
|
||||
|
||||
const mockUser = {
|
||||
@@ -24,6 +24,8 @@ const mockUser = {
|
||||
objective: "other",
|
||||
} as unknown as TUser;
|
||||
|
||||
vi.mock("next-auth/react", () => ({ signOut: vi.fn() }));
|
||||
|
||||
// Mock window.location.reload
|
||||
const originalLocation = window.location;
|
||||
beforeEach(() => {
|
||||
@@ -35,6 +37,11 @@ beforeEach(() => {
|
||||
|
||||
vi.mock("@/app/(app)/environments/[environmentId]/settings/(account)/profile/actions", () => ({
|
||||
updateUserAction: vi.fn(),
|
||||
resetPasswordAction: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/auth/forgot-password/actions", () => ({
|
||||
forgotPasswordAction: vi.fn(),
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
@@ -50,7 +57,13 @@ describe("EditProfileDetailsForm", () => {
|
||||
test("renders with initial user data and updates successfully", async () => {
|
||||
vi.mocked(updateUserAction).mockResolvedValue({ ...mockUser, name: "New Name" } as any);
|
||||
|
||||
render(<EditProfileDetailsForm user={mockUser} emailVerificationDisabled={true} />);
|
||||
render(
|
||||
<EditProfileDetailsForm
|
||||
user={mockUser}
|
||||
emailVerificationDisabled={true}
|
||||
isPasswordResetEnabled={false}
|
||||
/>
|
||||
);
|
||||
|
||||
const nameInput = screen.getByPlaceholderText("common.full_name");
|
||||
expect(nameInput).toHaveValue(mockUser.name);
|
||||
@@ -91,7 +104,13 @@ describe("EditProfileDetailsForm", () => {
|
||||
const errorMessage = "Update failed";
|
||||
vi.mocked(updateUserAction).mockRejectedValue(new Error(errorMessage));
|
||||
|
||||
render(<EditProfileDetailsForm user={mockUser} emailVerificationDisabled={false} />);
|
||||
render(
|
||||
<EditProfileDetailsForm
|
||||
user={mockUser}
|
||||
emailVerificationDisabled={false}
|
||||
isPasswordResetEnabled={false}
|
||||
/>
|
||||
);
|
||||
|
||||
const nameInput = screen.getByPlaceholderText("common.full_name");
|
||||
await userEvent.clear(nameInput);
|
||||
@@ -109,7 +128,13 @@ describe("EditProfileDetailsForm", () => {
|
||||
});
|
||||
|
||||
test("update button is disabled initially and enables on change", async () => {
|
||||
render(<EditProfileDetailsForm user={mockUser} emailVerificationDisabled={false} />);
|
||||
render(
|
||||
<EditProfileDetailsForm
|
||||
user={mockUser}
|
||||
emailVerificationDisabled={false}
|
||||
isPasswordResetEnabled={false}
|
||||
/>
|
||||
);
|
||||
const updateButton = screen.getByText("common.update");
|
||||
expect(updateButton).toBeDisabled();
|
||||
|
||||
@@ -117,4 +142,68 @@ describe("EditProfileDetailsForm", () => {
|
||||
await userEvent.type(nameInput, " updated");
|
||||
expect(updateButton).toBeEnabled();
|
||||
});
|
||||
|
||||
test("reset password button works", async () => {
|
||||
vi.mocked(resetPasswordAction).mockResolvedValue({ data: { success: true } });
|
||||
|
||||
render(
|
||||
<EditProfileDetailsForm
|
||||
user={mockUser}
|
||||
emailVerificationDisabled={false}
|
||||
isPasswordResetEnabled={true}
|
||||
/>
|
||||
);
|
||||
|
||||
const resetButton = screen.getByRole("button", { name: "auth.forgot-password.reset_password" });
|
||||
await userEvent.click(resetButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(resetPasswordAction).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.success).toHaveBeenCalledWith("auth.forgot-password.email-sent.heading");
|
||||
});
|
||||
});
|
||||
|
||||
test("reset password button handles error correctly", async () => {
|
||||
const errorMessage = "Reset failed";
|
||||
vi.mocked(resetPasswordAction).mockResolvedValue({ serverError: errorMessage });
|
||||
|
||||
render(
|
||||
<EditProfileDetailsForm
|
||||
user={mockUser}
|
||||
emailVerificationDisabled={false}
|
||||
isPasswordResetEnabled={true}
|
||||
/>
|
||||
);
|
||||
|
||||
const resetButton = screen.getByRole("button", { name: "auth.forgot-password.reset_password" });
|
||||
await userEvent.click(resetButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(resetPasswordAction).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith(errorMessage);
|
||||
});
|
||||
});
|
||||
|
||||
test("reset password button shows loading state", async () => {
|
||||
vi.mocked(resetPasswordAction).mockImplementation(() => new Promise(() => {})); // Never resolves
|
||||
|
||||
render(
|
||||
<EditProfileDetailsForm
|
||||
user={mockUser}
|
||||
emailVerificationDisabled={false}
|
||||
isPasswordResetEnabled={true}
|
||||
/>
|
||||
);
|
||||
|
||||
const resetButton = screen.getByRole("button", { name: "auth.forgot-password.reset_password" });
|
||||
await userEvent.click(resetButton);
|
||||
|
||||
expect(resetButton).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from "@/modules/ui/components/dropdown-menu";
|
||||
import { FormControl, FormError, FormField, FormItem, FormLabel } from "@/modules/ui/components/form";
|
||||
import { Input } from "@/modules/ui/components/input";
|
||||
import { Label } from "@/modules/ui/components/label";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import { ChevronDownIcon } from "lucide-react";
|
||||
@@ -22,7 +23,7 @@ import { FormProvider, SubmitHandler, useForm } from "react-hook-form";
|
||||
import toast from "react-hot-toast";
|
||||
import { z } from "zod";
|
||||
import { TUser, TUserUpdateInput, ZUser, ZUserEmail } from "@formbricks/types/user";
|
||||
import { updateUserAction } from "../actions";
|
||||
import { resetPasswordAction, updateUserAction } from "../actions";
|
||||
|
||||
// Schema & types
|
||||
const ZEditProfileNameFormSchema = ZUser.pick({ name: true, locale: true, email: true }).extend({
|
||||
@@ -30,13 +31,17 @@ const ZEditProfileNameFormSchema = ZUser.pick({ name: true, locale: true, email:
|
||||
});
|
||||
type TEditProfileNameForm = z.infer<typeof ZEditProfileNameFormSchema>;
|
||||
|
||||
interface IEditProfileDetailsFormProps {
|
||||
user: TUser;
|
||||
isPasswordResetEnabled?: boolean;
|
||||
emailVerificationDisabled: boolean;
|
||||
}
|
||||
|
||||
export const EditProfileDetailsForm = ({
|
||||
user,
|
||||
isPasswordResetEnabled,
|
||||
emailVerificationDisabled,
|
||||
}: {
|
||||
user: TUser;
|
||||
emailVerificationDisabled: boolean;
|
||||
}) => {
|
||||
}: IEditProfileDetailsFormProps) => {
|
||||
const { t } = useTranslate();
|
||||
|
||||
const form = useForm<TEditProfileNameForm>({
|
||||
@@ -50,6 +55,8 @@ export const EditProfileDetailsForm = ({
|
||||
});
|
||||
|
||||
const { isSubmitting, isDirty } = form.formState;
|
||||
|
||||
const [isResettingPassword, setIsResettingPassword] = useState(false);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const { signOut: signOutWithAudit } = useSignOut({ id: user.id, email: user.email });
|
||||
|
||||
@@ -90,6 +97,7 @@ export const EditProfileDetailsForm = ({
|
||||
redirectUrl: "/email-change-without-verification-success",
|
||||
redirect: true,
|
||||
callbackUrl: "/email-change-without-verification-success",
|
||||
clearEnvironmentId: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -121,6 +129,28 @@ export const EditProfileDetailsForm = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetPassword = async () => {
|
||||
setIsResettingPassword(true);
|
||||
|
||||
const result = await resetPasswordAction();
|
||||
if (result?.data) {
|
||||
toast.success(t("auth.forgot-password.email-sent.heading"));
|
||||
|
||||
await signOutWithAudit({
|
||||
reason: "password_reset",
|
||||
redirectUrl: "/auth/login",
|
||||
redirect: true,
|
||||
callbackUrl: "/auth/login",
|
||||
clearEnvironmentId: true,
|
||||
});
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(result);
|
||||
toast.error(t(errorMessage));
|
||||
}
|
||||
|
||||
setIsResettingPassword(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormProvider {...form}>
|
||||
@@ -205,6 +235,26 @@ export const EditProfileDetailsForm = ({
|
||||
)}
|
||||
/>
|
||||
|
||||
{isPasswordResetEnabled && (
|
||||
<div className="mt-4 space-y-2">
|
||||
<Label htmlFor="reset-password">{t("auth.forgot-password.reset_password")}</Label>
|
||||
<p className="mt-1 text-sm text-slate-500">
|
||||
{t("auth.forgot-password.reset_password_description")}
|
||||
</p>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<Input type="email" id="reset-password" defaultValue={user.email} disabled />
|
||||
<Button
|
||||
onClick={handleResetPassword}
|
||||
loading={isResettingPassword}
|
||||
disabled={isResettingPassword}
|
||||
size="default"
|
||||
variant="secondary">
|
||||
{t("auth.forgot-password.reset_password")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="mt-4"
|
||||
|
||||
@@ -12,7 +12,8 @@ import Page from "./page";
|
||||
|
||||
// Mock services and utils
|
||||
vi.mock("@/lib/constants", () => ({
|
||||
IS_FORMBRICKS_CLOUD: true,
|
||||
IS_FORMBRICKS_CLOUD: 1,
|
||||
PASSWORD_RESET_DISABLED: 1,
|
||||
EMAIL_VERIFICATION_DISABLED: true,
|
||||
}));
|
||||
vi.mock("@/lib/organization/service", () => ({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AccountSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(account)/components/AccountSettingsNavbar";
|
||||
import { AccountSecurity } from "@/app/(app)/environments/[environmentId]/settings/(account)/profile/components/AccountSecurity";
|
||||
import { EMAIL_VERIFICATION_DISABLED, IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
||||
import { EMAIL_VERIFICATION_DISABLED, IS_FORMBRICKS_CLOUD, PASSWORD_RESET_DISABLED } from "@/lib/constants";
|
||||
import { getOrganizationsWhereUserIsSingleOwner } from "@/lib/organization/service";
|
||||
import { getUser } from "@/lib/user/service";
|
||||
import { getIsMultiOrgEnabled, getIsTwoFactorAuthEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
@@ -32,6 +32,8 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
||||
throw new Error(t("common.user_not_found"));
|
||||
}
|
||||
|
||||
const isPasswordResetEnabled = !PASSWORD_RESET_DISABLED && user.identityProvider === "email";
|
||||
|
||||
return (
|
||||
<PageContentWrapper>
|
||||
<PageHeader pageTitle={t("common.account_settings")}>
|
||||
@@ -42,7 +44,11 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
||||
<SettingsCard
|
||||
title={t("environments.settings.profile.personal_information")}
|
||||
description={t("environments.settings.profile.update_personal_info")}>
|
||||
<EditProfileDetailsForm emailVerificationDisabled={EMAIL_VERIFICATION_DISABLED} user={user} />
|
||||
<EditProfileDetailsForm
|
||||
user={user}
|
||||
emailVerificationDisabled={EMAIL_VERIFICATION_DISABLED}
|
||||
isPasswordResetEnabled={isPasswordResetEnabled}
|
||||
/>
|
||||
</SettingsCard>
|
||||
<SettingsCard
|
||||
title={t("common.avatar")}
|
||||
|
||||
@@ -34,7 +34,8 @@
|
||||
"text": "Du kannst Dich jetzt mit deinem neuen Passwort einloggen"
|
||||
}
|
||||
},
|
||||
"reset_password": "Passwort zurücksetzen"
|
||||
"reset_password": "Passwort zurücksetzen",
|
||||
"reset_password_description": "Du wirst abgemeldet, um dein Passwort zurückzusetzen."
|
||||
},
|
||||
"invite": {
|
||||
"create_account": "Konto erstellen",
|
||||
@@ -1230,7 +1231,7 @@
|
||||
"copy_survey_error": "Kopieren der Umfrage fehlgeschlagen",
|
||||
"copy_survey_link_to_clipboard": "Umfragelink in die Zwischenablage kopieren",
|
||||
"copy_survey_success": "Umfrage erfolgreich kopiert!",
|
||||
"delete_survey_and_responses_warning": "Bist Du sicher, dass Du diese Umfrage und alle ihre Antworten löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"delete_survey_and_responses_warning": "Bist Du sicher, dass Du diese Umfrage und alle ihre Antworten löschen möchtest?",
|
||||
"edit": {
|
||||
"1_choose_the_default_language_for_this_survey": "1. Wähle die Standardsprache für diese Umfrage:",
|
||||
"2_activate_translation_for_specific_languages": "2. Übersetzung für bestimmte Sprachen aktivieren:",
|
||||
|
||||
@@ -34,7 +34,8 @@
|
||||
"text": "You can now log in with your new password"
|
||||
}
|
||||
},
|
||||
"reset_password": "Reset password"
|
||||
"reset_password": "Reset password",
|
||||
"reset_password_description": "You will be logged out to reset your password."
|
||||
},
|
||||
"invite": {
|
||||
"create_account": "Create an account",
|
||||
@@ -1230,7 +1231,7 @@
|
||||
"copy_survey_error": "Failed to copy survey",
|
||||
"copy_survey_link_to_clipboard": "Copy survey link to clipboard",
|
||||
"copy_survey_success": "Survey copied successfully!",
|
||||
"delete_survey_and_responses_warning": "Are you sure you want to delete this survey and all of its responses? This action cannot be undone.",
|
||||
"delete_survey_and_responses_warning": "Are you sure you want to delete this survey and all of its responses?",
|
||||
"edit": {
|
||||
"1_choose_the_default_language_for_this_survey": "1. Choose the default language for this survey:",
|
||||
"2_activate_translation_for_specific_languages": "2. Activate translation for specific languages:",
|
||||
|
||||
@@ -34,7 +34,8 @@
|
||||
"text": "Vous pouvez maintenant vous connecter avec votre nouveau mot de passe."
|
||||
}
|
||||
},
|
||||
"reset_password": "Réinitialiser le mot de passe"
|
||||
"reset_password": "Réinitialiser le mot de passe",
|
||||
"reset_password_description": "Vous serez déconnecté pour réinitialiser votre mot de passe."
|
||||
},
|
||||
"invite": {
|
||||
"create_account": "Créer un compte",
|
||||
@@ -1230,7 +1231,7 @@
|
||||
"copy_survey_error": "Échec de la copie du sondage",
|
||||
"copy_survey_link_to_clipboard": "Copier le lien du sondage dans le presse-papiers",
|
||||
"copy_survey_success": "Enquête copiée avec succès !",
|
||||
"delete_survey_and_responses_warning": "Êtes-vous sûr de vouloir supprimer cette enquête et toutes ses réponses ? Cette action ne peut pas être annulée.",
|
||||
"delete_survey_and_responses_warning": "Êtes-vous sûr de vouloir supprimer cette enquête et toutes ses réponses?",
|
||||
"edit": {
|
||||
"1_choose_the_default_language_for_this_survey": "1. Choisissez la langue par défaut pour ce sondage :",
|
||||
"2_activate_translation_for_specific_languages": "2. Activer la traduction pour des langues spécifiques :",
|
||||
|
||||
@@ -34,7 +34,8 @@
|
||||
"text": "Agora você pode fazer login com sua nova senha"
|
||||
}
|
||||
},
|
||||
"reset_password": "Redefinir senha"
|
||||
"reset_password": "Redefinir senha",
|
||||
"reset_password_description": "Você será desconectado para redefinir sua senha."
|
||||
},
|
||||
"invite": {
|
||||
"create_account": "Cria uma conta",
|
||||
@@ -1230,7 +1231,7 @@
|
||||
"copy_survey_error": "Falha ao copiar pesquisa",
|
||||
"copy_survey_link_to_clipboard": "Copiar link da pesquisa para a área de transferência",
|
||||
"copy_survey_success": "Pesquisa copiada com sucesso!",
|
||||
"delete_survey_and_responses_warning": "Você tem certeza de que quer deletar essa pesquisa e todas as suas respostas? Essa ação não pode ser desfeita.",
|
||||
"delete_survey_and_responses_warning": "Você tem certeza de que quer deletar essa pesquisa e todas as suas respostas?",
|
||||
"edit": {
|
||||
"1_choose_the_default_language_for_this_survey": "1. Escolha o idioma padrão para essa pesquisa:",
|
||||
"2_activate_translation_for_specific_languages": "2. Ativar tradução para idiomas específicos:",
|
||||
|
||||
@@ -34,7 +34,8 @@
|
||||
"text": "Pode agora iniciar sessão com a sua nova palavra-passe"
|
||||
}
|
||||
},
|
||||
"reset_password": "Redefinir palavra-passe"
|
||||
"reset_password": "Redefinir palavra-passe",
|
||||
"reset_password_description": "Será desconectado para redefinir a sua palavra-passe."
|
||||
},
|
||||
"invite": {
|
||||
"create_account": "Criar uma conta",
|
||||
@@ -1230,7 +1231,7 @@
|
||||
"copy_survey_error": "Falha ao copiar inquérito",
|
||||
"copy_survey_link_to_clipboard": "Copiar link do inquérito para a área de transferência",
|
||||
"copy_survey_success": "Inquérito copiado com sucesso!",
|
||||
"delete_survey_and_responses_warning": "Tem a certeza de que deseja eliminar este inquérito e todas as suas respostas? Esta ação não pode ser desfeita.",
|
||||
"delete_survey_and_responses_warning": "Tem a certeza de que deseja eliminar este inquérito e todas as suas respostas?",
|
||||
"edit": {
|
||||
"1_choose_the_default_language_for_this_survey": "1. Escolha o idioma padrão para este inquérito:",
|
||||
"2_activate_translation_for_specific_languages": "2. Ativar tradução para idiomas específicos:",
|
||||
|
||||
@@ -34,7 +34,8 @@
|
||||
"text": "您現在可以使用新密碼登入"
|
||||
}
|
||||
},
|
||||
"reset_password": "重設密碼"
|
||||
"reset_password": "重設密碼",
|
||||
"reset_password_description": "您將被登出以重設您的密碼。"
|
||||
},
|
||||
"invite": {
|
||||
"create_account": "建立帳戶",
|
||||
@@ -1230,7 +1231,7 @@
|
||||
"copy_survey_error": "無法複製問卷",
|
||||
"copy_survey_link_to_clipboard": "將問卷連結複製到剪貼簿",
|
||||
"copy_survey_success": "問卷已成功複製!",
|
||||
"delete_survey_and_responses_warning": "您確定要刪除此問卷及其所有回應嗎?此操作無法復原。",
|
||||
"delete_survey_and_responses_warning": "您確定要刪除此問卷及其所有回應嗎?",
|
||||
"edit": {
|
||||
"1_choose_the_default_language_for_this_survey": "1. 選擇此問卷的預設語言:",
|
||||
"2_activate_translation_for_specific_languages": "2. 啟用特定語言的翻譯:",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@/lib/localStorage";
|
||||
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||
import { TOrganization } from "@formbricks/types/organizations";
|
||||
@@ -100,8 +99,6 @@ describe("DeleteAccountModal", () => {
|
||||
/>
|
||||
);
|
||||
|
||||
const removeItemSpy = vi.spyOn(window.localStorage, "removeItem");
|
||||
|
||||
const input = screen.getByTestId("deleteAccountConfirmation");
|
||||
fireEvent.change(input, { target: { value: mockUser.email } });
|
||||
|
||||
@@ -113,8 +110,8 @@ describe("DeleteAccountModal", () => {
|
||||
expect(mockSignOut).toHaveBeenCalledWith({
|
||||
reason: "account_deletion",
|
||||
redirect: false, // Updated to match new implementation
|
||||
clearEnvironmentId: true,
|
||||
});
|
||||
expect(removeItemSpy).toHaveBeenCalledWith(FORMBRICKS_ENVIRONMENT_ID_LS);
|
||||
expect(window.location.replace).toHaveBeenCalledWith("/auth/login");
|
||||
expect(mockSetOpen).toHaveBeenCalledWith(false);
|
||||
});
|
||||
@@ -151,15 +148,13 @@ describe("DeleteAccountModal", () => {
|
||||
const form = screen.getByTestId("deleteAccountForm");
|
||||
fireEvent.submit(form);
|
||||
|
||||
const removeItemSpy = vi.spyOn(window.localStorage, "removeItem");
|
||||
|
||||
await waitFor(() => {
|
||||
expect(deleteUserAction).toHaveBeenCalled();
|
||||
expect(mockSignOut).toHaveBeenCalledWith({
|
||||
reason: "account_deletion",
|
||||
redirect: false, // Updated to match new implementation
|
||||
clearEnvironmentId: true,
|
||||
});
|
||||
expect(removeItemSpy).toHaveBeenCalledWith(FORMBRICKS_ENVIRONMENT_ID_LS);
|
||||
expect(window.location.replace).toHaveBeenCalledWith(
|
||||
"https://app.formbricks.com/s/clri52y3z8f221225wjdhsoo2"
|
||||
);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@/lib/localStorage";
|
||||
import { useSignOut } from "@/modules/auth/hooks/use-sign-out";
|
||||
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
|
||||
import { Input } from "@/modules/ui/components/input";
|
||||
@@ -39,12 +38,11 @@ export const DeleteAccountModal = ({
|
||||
setDeleting(true);
|
||||
await deleteUserAction();
|
||||
|
||||
localStorage.removeItem(FORMBRICKS_ENVIRONMENT_ID_LS);
|
||||
|
||||
// Sign out with account deletion reason (no automatic redirect)
|
||||
await signOutWithAudit({
|
||||
reason: "account_deletion",
|
||||
redirect: false, // Prevent NextAuth automatic redirect
|
||||
clearEnvironmentId: true,
|
||||
});
|
||||
|
||||
// Manual redirect after signOut completes
|
||||
|
||||
@@ -13,7 +13,13 @@ export const logSignOutAction = async (
|
||||
userId: string,
|
||||
userEmail: string,
|
||||
context: {
|
||||
reason?: "user_initiated" | "account_deletion" | "email_change" | "session_timeout" | "forced_logout";
|
||||
reason?:
|
||||
| "user_initiated"
|
||||
| "account_deletion"
|
||||
| "email_change"
|
||||
| "session_timeout"
|
||||
| "forced_logout"
|
||||
| "password_reset";
|
||||
redirectUrl?: string;
|
||||
organizationId?: string;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"use server";
|
||||
|
||||
import { PASSWORD_RESET_DISABLED } from "@/lib/constants";
|
||||
import { actionClient } from "@/lib/utils/action-client";
|
||||
import { getUserByEmail } from "@/modules/auth/lib/user";
|
||||
import { sendForgotPasswordEmail } from "@/modules/email";
|
||||
import { z } from "zod";
|
||||
import { OperationNotAllowedError } from "@formbricks/types/errors";
|
||||
import { ZUserEmail } from "@formbricks/types/user";
|
||||
|
||||
const ZForgotPasswordAction = z.object({
|
||||
@@ -13,9 +15,15 @@ const ZForgotPasswordAction = z.object({
|
||||
export const forgotPasswordAction = actionClient
|
||||
.schema(ZForgotPasswordAction)
|
||||
.action(async ({ parsedInput }) => {
|
||||
if (PASSWORD_RESET_DISABLED) {
|
||||
throw new OperationNotAllowedError("Password reset is disabled");
|
||||
}
|
||||
|
||||
const user = await getUserByEmail(parsedInput.email);
|
||||
if (user) {
|
||||
|
||||
if (user && user.identityProvider === "email") {
|
||||
await sendForgotPasswordEmail(user);
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@/lib/localStorage";
|
||||
import { logSignOutAction } from "@/modules/auth/actions/sign-out";
|
||||
import { signOut } from "next-auth/react";
|
||||
import { logger } from "@formbricks/logger";
|
||||
|
||||
interface UseSignOutOptions {
|
||||
reason?: "user_initiated" | "account_deletion" | "email_change" | "session_timeout" | "forced_logout";
|
||||
reason?:
|
||||
| "user_initiated"
|
||||
| "account_deletion"
|
||||
| "email_change"
|
||||
| "session_timeout"
|
||||
| "forced_logout"
|
||||
| "password_reset";
|
||||
redirectUrl?: string;
|
||||
organizationId?: string;
|
||||
redirect?: boolean;
|
||||
callbackUrl?: string;
|
||||
clearEnvironmentId?: boolean;
|
||||
}
|
||||
|
||||
interface SessionUser {
|
||||
@@ -36,6 +44,10 @@ export const useSignOut = (sessionUser?: SessionUser | null) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (options?.clearEnvironmentId) {
|
||||
localStorage.removeItem(FORMBRICKS_ENVIRONMENT_ID_LS);
|
||||
}
|
||||
|
||||
// Call NextAuth signOut
|
||||
return await signOut({
|
||||
redirect: options?.redirect,
|
||||
|
||||
@@ -78,6 +78,7 @@ export const getUserByEmail = reactCache(async (email: string) => {
|
||||
email: true,
|
||||
emailVerified: true,
|
||||
isActive: true,
|
||||
identityProvider: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -283,7 +283,13 @@ export const logSignOut = (
|
||||
userId: string,
|
||||
userEmail: string,
|
||||
context?: {
|
||||
reason?: "user_initiated" | "account_deletion" | "email_change" | "session_timeout" | "forced_logout";
|
||||
reason?:
|
||||
| "user_initiated"
|
||||
| "account_deletion"
|
||||
| "email_change"
|
||||
| "session_timeout"
|
||||
| "forced_logout"
|
||||
| "password_reset";
|
||||
redirectUrl?: string;
|
||||
organizationId?: string;
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ export const ZAuditAction = z.enum([
|
||||
"twoFactorRequired",
|
||||
"emailVerificationAttempted",
|
||||
"userSignedOut",
|
||||
"passwordReset",
|
||||
]);
|
||||
export const ZActor = z.enum(["user", "api", "system"]);
|
||||
export const ZAuditStatus = z.enum(["success", "failure"]);
|
||||
|
||||
@@ -60,11 +60,7 @@ const getRatingContent = (scale: string, i: number, range: number, isColorCoding
|
||||
);
|
||||
}
|
||||
if (scale === "number") {
|
||||
return (
|
||||
<Text className="m-0 h-[44px] text-center text-[14px] leading-[44px]">
|
||||
{i + 1}
|
||||
</Text>
|
||||
);
|
||||
return <Text className="m-0 h-[44px] text-center text-[14px] leading-[44px]">{i + 1}</Text>;
|
||||
}
|
||||
if (scale === "star") {
|
||||
return <Text className="m-auto text-3xl">⭐</Text>;
|
||||
@@ -232,8 +228,8 @@ export async function PreviewEmailTemplate({
|
||||
{ "rounded-l-lg border-l": i === 0 },
|
||||
{ "rounded-r-lg": i === firstQuestion.range - 1 },
|
||||
firstQuestion.isColorCodingEnabled &&
|
||||
firstQuestion.scale === "number" &&
|
||||
`border border-t-[6px] border-t-${getRatingNumberOptionColor(firstQuestion.range, i + 1)}`,
|
||||
firstQuestion.scale === "number" &&
|
||||
`border border-t-[6px] border-t-${getRatingNumberOptionColor(firstQuestion.range, i + 1)}`,
|
||||
firstQuestion.scale === "star" && "border-transparent"
|
||||
)}
|
||||
href={`${urlWithPrefilling}${firstQuestion.id}=${(i + 1).toString()}`}
|
||||
|
||||
@@ -129,9 +129,9 @@ export const QuestionFormInput = ({
|
||||
(question &&
|
||||
(id.includes(".")
|
||||
? // Handle nested properties
|
||||
(question[id.split(".")[0] as keyof TSurveyQuestion] as any)?.[id.split(".")[1]]
|
||||
(question[id.split(".")[0] as keyof TSurveyQuestion] as any)?.[id.split(".")[1]]
|
||||
: // Original behavior
|
||||
(question[id as keyof TSurveyQuestion] as TI18nString))) ||
|
||||
(question[id as keyof TSurveyQuestion] as TI18nString))) ||
|
||||
createI18nString("", surveyLanguageCodes)
|
||||
);
|
||||
}, [
|
||||
@@ -351,8 +351,9 @@ export const QuestionFormInput = ({
|
||||
<div className="h-10 w-full"></div>
|
||||
<div
|
||||
ref={highlightContainerRef}
|
||||
className={`no-scrollbar absolute top-0 z-0 mt-0.5 flex h-10 w-full overflow-scroll whitespace-nowrap px-3 py-2 text-center text-sm text-transparent ${localSurvey.languages?.length > 1 ? "pr-24" : ""
|
||||
}`}
|
||||
className={`no-scrollbar absolute top-0 z-0 mt-0.5 flex h-10 w-full overflow-scroll whitespace-nowrap px-3 py-2 text-center text-sm text-transparent ${
|
||||
localSurvey.languages?.length > 1 ? "pr-24" : ""
|
||||
}`}
|
||||
dir="auto"
|
||||
key={highlightedJSX.toString()}>
|
||||
{highlightedJSX}
|
||||
@@ -379,8 +380,9 @@ export const QuestionFormInput = ({
|
||||
maxLength={maxLength}
|
||||
ref={inputRef}
|
||||
onBlur={onBlur}
|
||||
className={`absolute top-0 text-black caret-black ${localSurvey.languages?.length > 1 ? "pr-24" : ""
|
||||
} ${className}`}
|
||||
className={`absolute top-0 text-black caret-black ${
|
||||
localSurvey.languages?.length > 1 ? "pr-24" : ""
|
||||
} ${className}`}
|
||||
isInvalid={
|
||||
isInvalid &&
|
||||
text[usedLanguageCode]?.trim() === "" &&
|
||||
|
||||
@@ -42,7 +42,7 @@ export const EndScreenForm = ({
|
||||
|
||||
const [showEndingCardCTA, setshowEndingCardCTA] = useState<boolean>(
|
||||
endingCard.type === "endScreen" &&
|
||||
(!!getLocalizedValue(endingCard.buttonLabel, selectedLanguageCode) || !!endingCard.buttonLink)
|
||||
(!!getLocalizedValue(endingCard.buttonLabel, selectedLanguageCode) || !!endingCard.buttonLink)
|
||||
);
|
||||
return (
|
||||
<form>
|
||||
|
||||
@@ -3,14 +3,6 @@ import { render } from "@testing-library/react";
|
||||
import { type MockedFunction, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { ClientLogout } from "./index";
|
||||
|
||||
// Mock the localStorage
|
||||
const mockRemoveItem = vi.fn();
|
||||
Object.defineProperty(window, "localStorage", {
|
||||
value: {
|
||||
removeItem: mockRemoveItem,
|
||||
},
|
||||
});
|
||||
|
||||
// Mock next-auth/react
|
||||
const mockSignOut = vi.fn();
|
||||
vi.mock("@/modules/auth/hooks/use-sign-out", () => ({
|
||||
@@ -37,6 +29,7 @@ describe("ClientLogout", () => {
|
||||
redirectUrl: "/auth/login",
|
||||
redirect: false,
|
||||
callbackUrl: "/auth/login",
|
||||
clearEnvironmentId: true,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -50,14 +43,10 @@ describe("ClientLogout", () => {
|
||||
redirectUrl: "/auth/login",
|
||||
redirect: false,
|
||||
callbackUrl: "/auth/login",
|
||||
clearEnvironmentId: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("removes environment ID from localStorage", () => {
|
||||
render(<ClientLogout />);
|
||||
expect(mockRemoveItem).toHaveBeenCalledWith("formbricks-environment-id");
|
||||
});
|
||||
|
||||
test("renders null", () => {
|
||||
const { container } = render(<ClientLogout />);
|
||||
expect(container.firstChild).toBeNull();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@/lib/localStorage";
|
||||
import { useSignOut } from "@/modules/auth/hooks/use-sign-out";
|
||||
import { useEffect } from "react";
|
||||
|
||||
@@ -8,12 +7,12 @@ export const ClientLogout = () => {
|
||||
const { signOut: signOutWithAudit } = useSignOut();
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.removeItem(FORMBRICKS_ENVIRONMENT_ID_LS);
|
||||
signOutWithAudit({
|
||||
reason: "forced_logout",
|
||||
redirectUrl: "/auth/login",
|
||||
redirect: false,
|
||||
callbackUrl: "/auth/login",
|
||||
clearEnvironmentId: true,
|
||||
});
|
||||
});
|
||||
return null;
|
||||
|
||||
@@ -21,16 +21,27 @@ export const OptionsSwitch = ({
|
||||
const [highlightStyle, setHighlightStyle] = useState({});
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
if (containerRef.current) {
|
||||
const activeElement = containerRef.current.querySelector(`[data-value="${currentOption}"]`);
|
||||
if (activeElement) {
|
||||
const { offsetLeft, offsetWidth } = activeElement as HTMLElement;
|
||||
setHighlightStyle({
|
||||
left: `${offsetLeft}px`,
|
||||
width: `${offsetWidth}px`,
|
||||
});
|
||||
const updateHighlight = () => {
|
||||
if (containerRef.current) {
|
||||
const activeElement = containerRef.current.querySelector(`[data-value="${currentOption}"]`);
|
||||
if (activeElement) {
|
||||
const { offsetLeft, offsetWidth } = activeElement as HTMLElement;
|
||||
setHighlightStyle({
|
||||
left: `${offsetLeft}px`,
|
||||
width: `${offsetWidth}px`,
|
||||
});
|
||||
} else {
|
||||
// Hide highlight if no matching element found
|
||||
setHighlightStyle({ opacity: 0 });
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
// Initial call
|
||||
updateHighlight();
|
||||
|
||||
// Listen to resize
|
||||
window.addEventListener("resize", updateHighlight);
|
||||
return () => window.removeEventListener("resize", updateHighlight);
|
||||
}, [currentOption]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"test": "dotenv -e ../../.env -- vitest run",
|
||||
"test:coverage": "dotenv -e ../../.env -- vitest run --coverage",
|
||||
"generate-api-specs": "dotenv -e ../../.env tsx ./modules/api/v2/openapi-document.ts > ../../docs/api-v2-reference/openapi.yml",
|
||||
"merge-client-endpoints": "tsx ./scripts/merge-client-endpoints.ts",
|
||||
"merge-client-endpoints": "tsx ./scripts/openapi/merge-client-endpoints.ts",
|
||||
"generate-and-merge-api-specs": "npm run generate-api-specs && npm run merge-client-endpoints"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
13
apps/web/scripts/docker/next-start.sh
Normal file
13
apps/web/scripts/docker/next-start.sh
Normal file
@@ -0,0 +1,13 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
export NODE_ENV=production
|
||||
if [ "${DOCKER_CRON_ENABLED:-1}" = "1" ]; then
|
||||
echo "Starting cron jobs...";
|
||||
supercronic -quiet /app/docker/cronjobs &
|
||||
else
|
||||
echo "Docker cron jobs are disabled via DOCKER_CRON_ENABLED=0";
|
||||
fi;
|
||||
(cd packages/database && npm run db:migrate:deploy) &&
|
||||
(cd packages/database && npm run db:create-saml-database:deploy) &&
|
||||
exec node apps/web/server.js
|
||||
16
apps/web/scripts/docker/read-secrets.sh
Normal file
16
apps/web/scripts/docker/read-secrets.sh
Normal file
@@ -0,0 +1,16 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
if [ -f "/run/secrets/database_url" ]; then
|
||||
export DATABASE_URL=$(cat /run/secrets/database_url)
|
||||
else
|
||||
echo "DATABASE_URL secret not found. Build may fail if this is required."
|
||||
fi
|
||||
|
||||
if [ -f "/run/secrets/encryption_key" ]; then
|
||||
export ENCRYPTION_KEY=$(cat /run/secrets/encryption_key)
|
||||
else
|
||||
echo "ENCRYPTION_KEY secret not found. Build may fail if this is required."
|
||||
fi
|
||||
|
||||
exec "$@"
|
||||
@@ -83,16 +83,18 @@ The Enterprise Edition allows us to fund the development of Formbricks sustainab
|
||||
| Email follow-ups | ✅ | ✅ |
|
||||
| Multi-language UI | ✅ | ✅ |
|
||||
| All integrations (Slack, Zapier, Notion, etc.) | ✅ | ✅ |
|
||||
| Domain Split Configuration | ✅ | ✅ |
|
||||
| Hide "Powered by Formbricks" | ❌ | ✅ |
|
||||
| Whitelabel email follow-ups | ❌ | ✅ |
|
||||
| Teams & access roles | ❌ | ✅ |
|
||||
| Contact management & segments | ❌ | ✅ |
|
||||
| Multi-language surveys | ❌ | ✅ |
|
||||
| Audit Logs | ❌ | ✅ |
|
||||
| OIDC SSO (AzureAD, Google, OpenID) | ❌ | ✅ |
|
||||
| SAML SSO | ❌ | ✅ |
|
||||
| Spam protection (ReCaptchaV3) | ❌ | ✅ |
|
||||
| Two-factor authentication | ❌ | ✅ |
|
||||
| Custom 'Project' count | ❌ | ✅ |
|
||||
| Spam protection (ReCaptchaV3) | ❌ | ✅ |
|
||||
| Two-factor authentication | ❌ | ✅ |
|
||||
| Custom 'Project' count | ❌ | ✅ |
|
||||
| White-glove onboarding | ❌ | ✅ |
|
||||
| Support SLAs | ❌ | ✅ |
|
||||
|
||||
|
||||
@@ -3,23 +3,41 @@
|
||||
"packageManager": "pnpm@9.15.9",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"main": "./src/index.ts",
|
||||
"main": "./dist/index.cjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"src"
|
||||
"dist",
|
||||
"schema.prisma",
|
||||
"migration"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.cjs"
|
||||
},
|
||||
"./types/*": {
|
||||
"import": "./types/*.ts"
|
||||
},
|
||||
"./zod/*": {
|
||||
"import": "./zod/*.ts"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rimraf .turbo node_modules",
|
||||
"db:migrate:deploy": "env DATABASE_URL=\"${MIGRATE_DATABASE_URL:-$DATABASE_URL}\" tsx ./src/scripts/apply-migrations.ts",
|
||||
"db:migrate:dev": "dotenv -e ../../.env -- sh -c \"pnpm prisma generate && tsx ./src/scripts/apply-migrations.ts\"",
|
||||
"db:create-saml-database:deploy": "env SAML_DATABASE_URL=\"${SAML_DATABASE_URL}\" tsx ./src/scripts/create-saml-database.ts",
|
||||
"db:create-saml-database:dev": "dotenv -e ../../.env -- tsx ./src/scripts/create-saml-database.ts",
|
||||
"clean": "rimraf .turbo node_modules dist",
|
||||
"build": "pnpm generate && vite build",
|
||||
"dev": "vite build --watch",
|
||||
"db:migrate:deploy": "env DATABASE_URL=\"${MIGRATE_DATABASE_URL:-$DATABASE_URL}\" node ./dist/scripts/apply-migrations.js",
|
||||
"db:migrate:dev": "dotenv -e ../../.env -- sh -c \"pnpm prisma generate && node ./dist/scripts/apply-migrations.js\"",
|
||||
"db:create-saml-database:deploy": "env SAML_DATABASE_URL=\"${SAML_DATABASE_URL}\" node ./dist/scripts/create-saml-database.js",
|
||||
"db:create-saml-database:dev": "dotenv -e ../../.env -- node ./dist/scripts/create-saml-database.js",
|
||||
"db:push": "prisma db push --accept-data-loss",
|
||||
"db:setup": "pnpm db:migrate:dev && pnpm db:create-saml-database:dev",
|
||||
"db:start": "pnpm db:setup",
|
||||
"format": "prisma format",
|
||||
"generate": "prisma generate",
|
||||
"lint": "eslint ./src --fix",
|
||||
"build": "pnpm generate",
|
||||
"generate-data-migration": "tsx ./src/scripts/generate-data-migration.ts",
|
||||
"create-migration": "dotenv -e ../../.env -- tsx ./src/scripts/create-migration.ts"
|
||||
},
|
||||
@@ -34,8 +52,11 @@
|
||||
"@formbricks/config-typescript": "workspace:*",
|
||||
"@formbricks/eslint-config": "workspace:*",
|
||||
"dotenv-cli": "8.0.0",
|
||||
"glob": "11.0.2",
|
||||
"prisma": "6.7.0",
|
||||
"prisma-json-types-generator": "3.4.1",
|
||||
"ts-node": "10.9.2"
|
||||
"ts-node": "10.9.2",
|
||||
"vite": "6.3.5",
|
||||
"vite-plugin-dts": "4.5.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
const prismaClientSingleton = () => {
|
||||
const prismaClientSingleton = (): PrismaClient => {
|
||||
return new PrismaClient({
|
||||
datasources: { db: { url: process.env.DATABASE_URL } },
|
||||
...(process.env.DEBUG === "1" && {
|
||||
@@ -15,6 +15,6 @@ const globalForPrisma = globalThis as unknown as {
|
||||
prisma: PrismaClientSingleton | undefined;
|
||||
};
|
||||
|
||||
export const prisma = globalForPrisma.prisma ?? prismaClientSingleton();
|
||||
export const prisma: PrismaClient = globalForPrisma.prisma ?? prismaClientSingleton();
|
||||
|
||||
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
|
||||
|
||||
@@ -2,10 +2,14 @@ import { exec } from "node:child_process";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import readline from "node:readline";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { applyMigrations } from "./migration-runner";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
|
||||
@@ -2,8 +2,12 @@ import { createId } from "@paralleldrive/cuid2";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import readline from "node:readline";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { logger } from "@formbricks/logger";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
|
||||
@@ -2,9 +2,12 @@ import { type Prisma, PrismaClient } from "@prisma/client";
|
||||
import { exec } from "node:child_process";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { logger } from "@formbricks/logger";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
export interface DataMigrationContext {
|
||||
@@ -24,7 +27,12 @@ export interface MigrationScript {
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
const TRANSACTION_TIMEOUT = 30 * 60 * 1000; // 30 minutes
|
||||
const MIGRATIONS_DIR = path.resolve(__dirname, "../../migration");
|
||||
|
||||
// Determine if we're running from built or source code
|
||||
const isBuilt = __filename.split(path.sep).includes("dist");
|
||||
const MIGRATIONS_DIR = isBuilt
|
||||
? path.resolve(__dirname, "../migration") // From dist/scripts to dist/migration
|
||||
: path.resolve(__dirname, "../../migration"); // From src/scripts to migration
|
||||
const PRISMA_MIGRATIONS_DIR = path.resolve(__dirname, "../../migrations");
|
||||
|
||||
const runMigrations = async (migrations: MigrationScript[]): Promise<void> => {
|
||||
@@ -194,11 +202,13 @@ const loadMigrations = async (): Promise<MigrationScript[]> => {
|
||||
const files = await fs.readdir(migrationPath);
|
||||
|
||||
const hasSchemaMigration = files.includes("migration.sql");
|
||||
const hasDataMigration = files.includes("migration.ts");
|
||||
// Check for the appropriate data migration file extension based on build status
|
||||
const dataMigrationFileName = isBuilt ? "migration.js" : "migration.ts";
|
||||
const hasDataMigration = files.includes(dataMigrationFileName);
|
||||
|
||||
if (hasSchemaMigration && hasDataMigration) {
|
||||
throw new Error(
|
||||
`Migration directory ${dirName} has both migration.sql and migration.ts. This should not happen.`
|
||||
`Migration directory ${dirName} has both migration.sql and ${dataMigrationFileName}. This should not happen.`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -233,7 +243,8 @@ const loadMigrations = async (): Promise<MigrationScript[]> => {
|
||||
}
|
||||
|
||||
// It's a data migration, dynamically import and extract the scripts
|
||||
const modulePath = path.join(migrationPath, "migration.ts");
|
||||
// Use .js extension when running from built code, .ts when running from source
|
||||
const modulePath = path.join(migrationPath, dataMigrationFileName);
|
||||
const mod = (await import(modulePath)) as Record<string, MigrationScript | undefined>;
|
||||
|
||||
// Check each export in the module for a DataMigrationScript (type: "data")
|
||||
@@ -245,7 +256,7 @@ const loadMigrations = async (): Promise<MigrationScript[]> => {
|
||||
}
|
||||
} else {
|
||||
logger.warn(
|
||||
`Migration directory ${dirName} doesn't have migration.sql or data-migration.ts. Skipping...`
|
||||
`Migration directory ${dirName} doesn't have migration.sql or ${dataMigrationFileName}. Skipping...`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -271,7 +282,11 @@ const loadMigrations = async (): Promise<MigrationScript[]> => {
|
||||
export async function applyMigrations(): Promise<void> {
|
||||
try {
|
||||
const allMigrations = await loadMigrations();
|
||||
logger.info(`Loaded ${allMigrations.length.toString()} migrations from ${MIGRATIONS_DIR}`);
|
||||
logger.info(
|
||||
`Loaded ${allMigrations.length.toString()} migrations from ${MIGRATIONS_DIR} (source: ${
|
||||
isBuilt ? "dist" : "src"
|
||||
})`
|
||||
);
|
||||
await runMigrations(allMigrations);
|
||||
} catch (error) {
|
||||
await prisma.$disconnect();
|
||||
|
||||
80
packages/database/vite.config.ts
Normal file
80
packages/database/vite.config.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { promises as fs } from "fs";
|
||||
import { glob } from "glob";
|
||||
import { dirname, resolve } from "path";
|
||||
import { Plugin, UserConfig, defineConfig } from "vite";
|
||||
import dts from "vite-plugin-dts";
|
||||
|
||||
const copySqlMigrationsPlugin: Plugin = {
|
||||
name: "copy-sql-migrations",
|
||||
async writeBundle() {
|
||||
const sqlFiles = await glob("migration/**/migration.sql", { cwd: __dirname });
|
||||
|
||||
await Promise.all(
|
||||
sqlFiles.map(async (file) => {
|
||||
const srcPath = resolve(__dirname, file);
|
||||
const destPath = resolve(__dirname, "dist", file);
|
||||
await fs.mkdir(dirname(destPath), { recursive: true });
|
||||
await fs.copyFile(srcPath, destPath);
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default defineConfig(async (): Promise<UserConfig> => {
|
||||
const migrationTsFiles = await glob("migration/**/migration.ts", { cwd: __dirname });
|
||||
const migrationEntries = migrationTsFiles.reduce((acc: Record<string, string>, file: string) => {
|
||||
const dir = dirname(file);
|
||||
const entryName = `${dir}/migration`;
|
||||
acc[entryName] = resolve(__dirname, file);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return {
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": resolve(__dirname, "src"),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
input: {
|
||||
index: resolve(__dirname, "src/index.ts"),
|
||||
"scripts/apply-migrations": resolve(__dirname, "src/scripts/apply-migrations.ts"),
|
||||
"scripts/create-saml-database": resolve(__dirname, "src/scripts/create-saml-database.ts"),
|
||||
"scripts/migration-runner": resolve(__dirname, "src/scripts/migration-runner.ts"),
|
||||
...migrationEntries,
|
||||
},
|
||||
output: [
|
||||
{
|
||||
format: "esm",
|
||||
entryFileNames: "[name].js",
|
||||
chunkFileNames: "[name].js",
|
||||
},
|
||||
{
|
||||
format: "cjs",
|
||||
entryFileNames: "[name].cjs",
|
||||
chunkFileNames: "[name].cjs",
|
||||
},
|
||||
],
|
||||
external: [
|
||||
// External dependencies that should not be bundled
|
||||
"@prisma/client",
|
||||
"zod",
|
||||
"zod-openapi",
|
||||
"@paralleldrive/cuid2",
|
||||
],
|
||||
},
|
||||
emptyOutDir: true,
|
||||
ssr: true, // Server-side rendering mode for Node.js
|
||||
},
|
||||
plugins: [
|
||||
dts({
|
||||
rollupTypes: false,
|
||||
include: ["src/**/*"],
|
||||
exclude: ["src/**/*.test.ts", "src/**/*.spec.ts"],
|
||||
insertTypesEntry: true,
|
||||
}),
|
||||
copySqlMigrationsPlugin,
|
||||
],
|
||||
};
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type Survey, SurveyStatus, SurveyType } from "@prisma/client";
|
||||
import { SurveyStatus, SurveyType } from "@prisma/client";
|
||||
import { z } from "zod";
|
||||
import { extendZodWithOpenApi } from "zod-openapi";
|
||||
// eslint-disable-next-line import/no-relative-packages -- Need to import from parent package
|
||||
@@ -92,10 +92,10 @@ const ZSurveyBase = z.object({
|
||||
}),
|
||||
questions: z.array(ZSurveyQuestion).openapi({
|
||||
description: "The questions of the survey",
|
||||
}) as z.ZodType<Survey["questions"]>,
|
||||
}),
|
||||
endings: z.array(ZSurveyEnding).default([]).openapi({
|
||||
description: "The endings of the survey",
|
||||
}) as z.ZodType<Survey["endings"]>,
|
||||
}),
|
||||
thankYouCard: z
|
||||
.object({
|
||||
enabled: z.boolean(),
|
||||
@@ -115,7 +115,7 @@ const ZSurveyBase = z.object({
|
||||
}),
|
||||
variables: z.array(ZSurveyVariable).openapi({
|
||||
description: "Survey variables",
|
||||
}) as z.ZodType<Survey["variables"]>,
|
||||
}),
|
||||
displayOption: z.enum(["displayOnce", "displayMultiple", "displaySome", "respondMultiple"]).openapi({
|
||||
description: "Display options for the survey",
|
||||
}),
|
||||
@@ -219,10 +219,10 @@ const ZSurveyBase = z.object({
|
||||
}),
|
||||
displayPercentage: z.number().nullable().openapi({
|
||||
description: "The display percentage of the survey",
|
||||
}) as z.ZodType<Survey["displayPercentage"]>,
|
||||
}),
|
||||
});
|
||||
|
||||
export const ZSurvey = ZSurveyBase satisfies z.ZodType<Survey>;
|
||||
export const ZSurvey = ZSurveyBase;
|
||||
|
||||
export const ZSurveyWithoutQuestionType = ZSurveyBase.omit({
|
||||
questions: true,
|
||||
|
||||
@@ -14,7 +14,7 @@ export function Headline({ headline, questionId, required = true, alignTextCente
|
||||
<div
|
||||
className={`fb-flex fb-items-center ${alignTextCenter ? "fb-justify-center" : "fb-justify-between"}`}
|
||||
dir="auto">
|
||||
{headline}
|
||||
<p>{headline}</p>
|
||||
{!required && (
|
||||
<span
|
||||
className="fb-text-heading fb-mx-2 fb-self-start fb-text-sm fb-font-normal fb-leading-7 fb-opacity-60"
|
||||
|
||||
@@ -7,6 +7,9 @@ describe("i18n", () => {
|
||||
test("should return empty string for undefined value", () => {
|
||||
expect(getLocalizedValue(undefined, "en")).toBe("");
|
||||
});
|
||||
test("should return empty string for empty string", () => {
|
||||
expect(getLocalizedValue({ default: "" }, "en")).toBe("");
|
||||
});
|
||||
|
||||
test("should return empty string for non-i18n string", () => {
|
||||
expect(getLocalizedValue("not an i18n string" as any, "en")).toBe("");
|
||||
|
||||
@@ -10,7 +10,7 @@ export const getLocalizedValue = (value: TI18nString | undefined, languageId: st
|
||||
return "";
|
||||
}
|
||||
if (isI18nObject(value)) {
|
||||
if (value[languageId]) {
|
||||
if (typeof value[languageId] === "string") {
|
||||
return value[languageId];
|
||||
}
|
||||
return value.default;
|
||||
|
||||
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
@@ -645,6 +645,9 @@ importers:
|
||||
dotenv-cli:
|
||||
specifier: 8.0.0
|
||||
version: 8.0.0
|
||||
glob:
|
||||
specifier: 11.0.2
|
||||
version: 11.0.2
|
||||
prisma:
|
||||
specifier: 6.7.0
|
||||
version: 6.7.0(typescript@5.8.3)
|
||||
@@ -654,6 +657,12 @@ importers:
|
||||
ts-node:
|
||||
specifier: 10.9.2
|
||||
version: 10.9.2(@types/node@22.15.18)(typescript@5.8.3)
|
||||
vite:
|
||||
specifier: 6.3.5
|
||||
version: 6.3.5(@types/node@22.15.18)(jiti@2.4.2)(terser@5.39.1)(tsx@4.19.4)(yaml@2.8.0)
|
||||
vite-plugin-dts:
|
||||
specifier: 4.5.3
|
||||
version: 4.5.3(@types/node@22.15.18)(rollup@4.40.2)(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(terser@5.39.1)(tsx@4.19.4)(yaml@2.8.0))
|
||||
|
||||
packages/i18n-utils:
|
||||
devDependencies:
|
||||
|
||||
@@ -21,5 +21,5 @@ sonar.scm.exclusions.disabled=false
|
||||
sonar.sourceEncoding=UTF-8
|
||||
|
||||
# Coverage
|
||||
sonar.coverage.exclusions=**/*.test.*,**/*.spec.*,**/*.mdx,**/*.config.mts,**/*.config.ts,**/constants.ts,**/route.ts,**/route.tsx,**/types/**,**/types.ts,**/stories.*,**/*.mock.*,**/mocks/**,**/__mocks__/**,**/openapi.ts,**/openapi-document.ts,**/instrumentation.ts,scripts/merge-client-endpoints.ts,**/playwright/**,**/Dockerfile,**/*.config.cjs,**/*.css,**/templates.ts,**/actions.ts,apps/web/modules/ui/components/icons/*,**/*.json,apps/web/vitestSetup.ts,packages/js-core/src/index.ts,apps/web/tailwind.config.js,apps/web/postcss.config.js,apps/web/next.config.mjs,apps/web/scripts/**,packages/js-core/vitest.setup.ts,**/*.mjs,apps/web/modules/auth/lib/mock-data.ts,apps/web/modules/analysis/components/SingleResponseCard/components/Smileys.tsx,packages/surveys/src/components/general/smileys.tsx,**/cache.ts,apps/web/app/**/billing-confirmation/**,apps/web/modules/ee/billing/**,apps/web/modules/ee/multi-language-surveys/**,apps/web/modules/email/**,apps/web/modules/integrations/**,apps/web/modules/setup/**/intro/**,apps/web/modules/setup/**/signup/**,apps/web/modules/setup/**/layout.tsx,apps/web/modules/survey/follow-ups/**,apps/web/app/share/**,apps/web/lib/shortUrl/**,apps/web/modules/ee/contacts/[contactId]/**,apps/web/modules/ee/contacts/components/**,apps/web/modules/ee/two-factor-auth/**,apps/web/lib/posthogServer.ts,apps/web/lib/slack/**,apps/web/lib/notion/**,apps/web/lib/googleSheet/**,apps/web/app/api/google-sheet/**,apps/web/app/api/billing/**,apps/web/lib/airtable/**,apps/web/app/api/v1/integrations/**,apps/web/lib/env.ts,**/instrumentation-node.ts,**/cache/**,**/*.svg,apps/web/modules/ui/components/icons/**,apps/web/modules/ui/components/table/**
|
||||
sonar.cpd.exclusions=**/*.test.*,**/*.spec.*,**/*.mdx,**/*.config.mts,**/*.config.ts,**/constants.ts,**/route.ts,**/route.tsx,**/types/**,**/types.ts,**/stories.*,**/*.mock.*,**/mocks/**,**/__mocks__/**,**/openapi.ts,**/openapi-document.ts,**/instrumentation.ts,scripts/merge-client-endpoints.ts,**/playwright/**,**/Dockerfile,**/*.config.cjs,**/*.css,**/templates.ts,**/actions.ts,apps/web/modules/ui/components/icons/*,**/*.json,apps/web/vitestSetup.ts,apps/web/tailwind.config.js,apps/web/postcss.config.js,apps/web/next.config.mjs,apps/web/scripts/**,packages/js-core/vitest.setup.ts,packages/js-core/src/index.ts,**/*.mjs,apps/web/modules/auth/lib/mock-data.ts,apps/web/modules/analysis/components/SingleResponseCard/components/Smileys.tsx,packages/surveys/src/components/general/smileys.tsx,**/cache.ts,apps/web/app/**/billing-confirmation/**,apps/web/modules/ee/billing/**,apps/web/modules/ee/multi-language-surveys/**,apps/web/modules/email/**,apps/web/modules/integrations/**,apps/web/modules/setup/**/intro/**,apps/web/modules/setup/**/signup/**,apps/web/modules/setup/**/layout.tsx,apps/web/modules/survey/follow-ups/**,apps/web/app/share/**,apps/web/lib/shortUrl/**,apps/web/modules/ee/contacts/[contactId]/**,apps/web/modules/ee/contacts/components/**,apps/web/modules/ee/two-factor-auth/**,apps/web/lib/posthogServer.ts,apps/web/lib/slack/**,apps/web/lib/notion/**,apps/web/lib/googleSheet/**,apps/web/app/api/google-sheet/**,apps/web/app/api/billing/**,apps/web/lib/airtable/**,apps/web/app/api/v1/integrations/**,apps/web/lib/env.ts,**/instrumentation-node.ts,**/cache/**,**/*.svg,apps/web/modules/ui/components/icons/**,apps/web/modules/ui/components/table/**
|
||||
sonar.coverage.exclusions=**/*.test.*,**/*.spec.*,**/*.mdx,**/*.config.mts,**/*.config.ts,**/constants.ts,**/route.ts,**/route.tsx,**/types/**,**/types.ts,**/stories.*,**/*.mock.*,**/mocks/**,**/__mocks__/**,**/openapi.ts,**/openapi-document.ts,**/instrumentation.ts,scripts/openapi/merge-client-endpoints.ts,**/playwright/**,**/Dockerfile,**/*.config.cjs,**/*.css,**/templates.ts,**/actions.ts,apps/web/modules/ui/components/icons/*,**/*.json,apps/web/vitestSetup.ts,packages/js-core/src/index.ts,apps/web/tailwind.config.js,apps/web/postcss.config.js,apps/web/next.config.mjs,apps/web/scripts/**,packages/js-core/vitest.setup.ts,**/*.mjs,apps/web/modules/auth/lib/mock-data.ts,apps/web/modules/analysis/components/SingleResponseCard/components/Smileys.tsx,packages/surveys/src/components/general/smileys.tsx,**/cache.ts,apps/web/app/**/billing-confirmation/**,apps/web/modules/ee/billing/**,apps/web/modules/ee/multi-language-surveys/**,apps/web/modules/email/**,apps/web/modules/integrations/**,apps/web/modules/setup/**/intro/**,apps/web/modules/setup/**/signup/**,apps/web/modules/setup/**/layout.tsx,apps/web/modules/survey/follow-ups/**,apps/web/app/share/**,apps/web/lib/shortUrl/**,apps/web/modules/ee/contacts/[contactId]/**,apps/web/modules/ee/contacts/components/**,apps/web/modules/ee/two-factor-auth/**,apps/web/lib/posthogServer.ts,apps/web/lib/slack/**,apps/web/lib/notion/**,apps/web/lib/googleSheet/**,apps/web/app/api/google-sheet/**,apps/web/app/api/billing/**,apps/web/lib/airtable/**,apps/web/app/api/v1/integrations/**,apps/web/lib/env.ts,**/instrumentation-node.ts,**/cache/**,**/*.svg,apps/web/modules/ui/components/icons/**,apps/web/modules/ui/components/table/**
|
||||
sonar.cpd.exclusions=**/*.test.*,**/*.spec.*,**/*.mdx,**/*.config.mts,**/*.config.ts,**/constants.ts,**/route.ts,**/route.tsx,**/types/**,**/types.ts,**/stories.*,**/*.mock.*,**/mocks/**,**/__mocks__/**,**/openapi.ts,**/openapi-document.ts,**/instrumentation.ts,scripts/openapi/merge-client-endpoints.ts,**/playwright/**,**/Dockerfile,**/*.config.cjs,**/*.css,**/templates.ts,**/actions.ts,apps/web/modules/ui/components/icons/*,**/*.json,apps/web/vitestSetup.ts,apps/web/tailwind.config.js,apps/web/postcss.config.js,apps/web/next.config.mjs,apps/web/scripts/**,packages/js-core/vitest.setup.ts,packages/js-core/src/index.ts,**/*.mjs,apps/web/modules/auth/lib/mock-data.ts,apps/web/modules/analysis/components/SingleResponseCard/components/Smileys.tsx,packages/surveys/src/components/general/smileys.tsx,**/cache.ts,apps/web/app/**/billing-confirmation/**,apps/web/modules/ee/billing/**,apps/web/modules/ee/multi-language-surveys/**,apps/web/modules/email/**,apps/web/modules/integrations/**,apps/web/modules/setup/**/intro/**,apps/web/modules/setup/**/signup/**,apps/web/modules/setup/**/layout.tsx,apps/web/modules/survey/follow-ups/**,apps/web/app/share/**,apps/web/lib/shortUrl/**,apps/web/modules/ee/contacts/[contactId]/**,apps/web/modules/ee/contacts/components/**,apps/web/modules/ee/two-factor-auth/**,apps/web/lib/posthogServer.ts,apps/web/lib/slack/**,apps/web/lib/notion/**,apps/web/lib/googleSheet/**,apps/web/app/api/google-sheet/**,apps/web/app/api/billing/**,apps/web/lib/airtable/**,apps/web/app/api/v1/integrations/**,apps/web/lib/env.ts,**/instrumentation-node.ts,**/cache/**,**/*.svg,apps/web/modules/ui/components/icons/**,apps/web/modules/ui/components/table/**
|
||||
|
||||
11
turbo.json
11
turbo.json
@@ -6,7 +6,7 @@
|
||||
"outputs": ["dist/**"]
|
||||
},
|
||||
"@formbricks/database#lint": {
|
||||
"dependsOn": ["@formbricks/logger#build"]
|
||||
"dependsOn": ["@formbricks/logger#build", "@formbricks/database#build"]
|
||||
},
|
||||
"@formbricks/database#setup": {
|
||||
"dependsOn": ["db:up"]
|
||||
@@ -30,6 +30,9 @@
|
||||
"dependsOn": ["@formbricks/database#db:setup"],
|
||||
"persistent": true
|
||||
},
|
||||
"@formbricks/js-core#lint": {
|
||||
"dependsOn": ["@formbricks/database#build"]
|
||||
},
|
||||
"@formbricks/react-native#build": {
|
||||
"dependsOn": ["^build", "@formbricks/database#build"],
|
||||
"outputs": ["dist/**"]
|
||||
@@ -58,10 +61,10 @@
|
||||
"persistent": true
|
||||
},
|
||||
"@formbricks/web#test": {
|
||||
"dependsOn": ["@formbricks/logger#build"]
|
||||
"dependsOn": ["@formbricks/logger#build", "@formbricks/database#build"]
|
||||
},
|
||||
"@formbricks/web#test:coverage": {
|
||||
"dependsOn": ["@formbricks/logger#build"]
|
||||
"dependsOn": ["@formbricks/logger#build", "@formbricks/database#build"]
|
||||
},
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
@@ -205,7 +208,7 @@
|
||||
},
|
||||
"db:setup": {
|
||||
"cache": false,
|
||||
"dependsOn": ["@formbricks/logger#build"],
|
||||
"dependsOn": ["@formbricks/logger#build", "@formbricks/database#build"],
|
||||
"outputs": []
|
||||
},
|
||||
"db:start": {
|
||||
|
||||
Reference in New Issue
Block a user