Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions apps/web/app/(app)/account/authorize/page.tsx
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
import { headers } from "next/headers";
import Link from "next/link";
import { redirect } from "next/navigation";
import { getTranslate } from "@/lingodotdev/server";
import { auth } from "@/modules/auth/lib/auth";
Expand All@@ -10,6 +11,7 @@ import {
} from "@/modules/auth/lib/oauth-client-metadata";
import { getSession } from "@/modules/auth/lib/session";
import { Alert, AlertDescription, AlertTitle } from "@/modules/ui/components/alert";
import { Logo } from "@/modules/ui/components/logo";
import { OAuthConsentActions } from "./components/OAuthConsentActions";

type TSearchParams = Record<string, string | string[] | undefined>;
Expand DownExpand Up@@ -57,6 +59,18 @@ const getPublicOAuthClient = async (clientId: string): Promise<TOAuthPublicClien
}
};

const OAuthConsentLogo = ({ label }: Readonly<{ label: string }>) => (
<div className="mb-6 flex justify-center">
<Link
target="_blank"
href="https://formbricks.com?utm_source=formbricks-app&utm_medium=webapp&utm_campaign=oauth_consent_logo"
rel="noopener noreferrer"
aria-label={label}>
<Logo aria-hidden="true" className="h-8 w-auto" />
</Link>
</div>
);

const Page = async ({ searchParams }: Readonly<{ searchParams: Promise<TSearchParams> }>) => {
const resolvedSearchParams = await searchParams;
const session = await getSession();
Expand All@@ -76,6 +90,7 @@ const Page = async ({ searchParams }: Readonly<{ searchParams: Promise<TSearchPa
return (
<div className="flex min-h-screen items-center justify-center bg-slate-50 px-4 py-10">
<div className="w-full max-w-xl rounded-lg border border-slate-200 bg-white p-6 shadow-sm">
<OAuthConsentLogo label={t("auth.oauth.formbricks_website")} />
<Alert variant="error" role="status">
<AlertTitle>{t("auth.oauth.invalid_oauth_request")}</AlertTitle>
<AlertDescription>{t("auth.oauth.invalid_oauth_request_description")}</AlertDescription>
Expand All@@ -91,6 +106,7 @@ const Page = async ({ searchParams }: Readonly<{ searchParams: Promise<TSearchPa
return (
<div className="flex min-h-screen items-center justify-center bg-slate-50 px-4 py-10">
<div className="w-full max-w-xl rounded-lg border border-slate-200 bg-white p-6 shadow-sm">
<OAuthConsentLogo label={t("auth.oauth.formbricks_website")} />
<div className="space-y-1">
<p className="text-sm font-medium text-slate-500">{t("auth.oauth.authorization_request")}</p>
<h1 className="text-2xl font-semibold text-slate-900">
Expand Down
1 change: 1 addition & 0 deletions apps/web/i18n.lock
Original file line numberDiff line numberDiff line change
Expand Up@@ -68,6 +68,7 @@ checksums:
auth/oauth/consent_failed: d6c92762fac20817ff0ec8d9cbc0412c
auth/oauth/consent_revoked: f1c243aaacd7c466b9986e484b93247b
auth/oauth/deny_access: ba3ac7dc12f362da277df5c9ba21e752
auth/oauth/formbricks_website: 95c2f69c6b6428e1aa6c26f4dd427e22
auth/oauth/invalid_oauth_request: 02936ced8cd4df963282fe5d413f6900
auth/oauth/invalid_oauth_request_description: 39322e962c0c20c766ae82d0b8917ebb
auth/oauth/localhost_redirect_warning: e0368262d25dc98ea02158eebd0066c1
Expand Down
2 changes: 1 addition & 1 deletion apps/web/lib/constants.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -184,7 +184,7 @@ export const DEBUG = env.DEBUG === "1";
export const ENTERPRISE_LICENSE_KEY = env.ENTERPRISE_LICENSE_KEY;

export const ENTERPRISE_LICENSE_REQUEST_FORM_URL =
"https://app.formbricks.com/s/trvp8tzy5uvsps9rc9qi9l9w?delivery=onpremise&source=ce";
"https://app.formbricks.com/s/trvp8tzy5uvsps9rc9qi9l9w?delivery=onpremise&source=ce&type=licenseRequest";

export const REDIS_URL = env.REDIS_URL;
export const RATE_LIMITING_DISABLED = env.RATE_LIMITING_DISABLED === "1";
Expand Down
1 change: 1 addition & 0 deletions apps/web/locales/de-DE.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -82,6 +82,7 @@
"consent_failed": "OAuth consent failed. Please try again.",
"consent_revoked": "OAuth app access revoked.",
"deny_access": "Deny access",
"formbricks_website": "Formbricks",
"invalid_oauth_request": "Invalid OAuth request",
"invalid_oauth_request_description": "The OAuth client could not be found or the authorization request is missing required details.",
"localhost_redirect_warning": "Local redirect URI",
Expand Down
1 change: 1 addition & 0 deletions apps/web/locales/en-US.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -82,6 +82,7 @@
"consent_failed": "OAuth consent failed. Please try again.",
"consent_revoked": "OAuth app access revoked.",
"deny_access": "Deny access",
"formbricks_website": "Formbricks",
"invalid_oauth_request": "Invalid OAuth request",
"invalid_oauth_request_description": "The OAuth client could not be found or the authorization request is missing required details.",
"localhost_redirect_warning": "Local redirect URI",
Expand Down
1 change: 1 addition & 0 deletions apps/web/locales/es-ES.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -82,6 +82,7 @@
"consent_failed": "OAuth consent failed. Please try again.",
"consent_revoked": "OAuth app access revoked.",
"deny_access": "Deny access",
"formbricks_website": "Formbricks",
"invalid_oauth_request": "Invalid OAuth request",
"invalid_oauth_request_description": "The OAuth client could not be found or the authorization request is missing required details.",
"localhost_redirect_warning": "Local redirect URI",
Expand Down
1 change: 1 addition & 0 deletions apps/web/locales/fr-FR.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -82,6 +82,7 @@
"consent_failed": "OAuth consent failed. Please try again.",
"consent_revoked": "OAuth app access revoked.",
"deny_access": "Deny access",
"formbricks_website": "Formbricks",
"invalid_oauth_request": "Invalid OAuth request",
"invalid_oauth_request_description": "The OAuth client could not be found or the authorization request is missing required details.",
"localhost_redirect_warning": "Local redirect URI",
Expand Down
1 change: 1 addition & 0 deletions apps/web/locales/hu-HU.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -82,6 +82,7 @@
"consent_failed": "OAuth consent failed. Please try again.",
"consent_revoked": "OAuth app access revoked.",
"deny_access": "Deny access",
"formbricks_website": "Formbricks",
"invalid_oauth_request": "Invalid OAuth request",
"invalid_oauth_request_description": "The OAuth client could not be found or the authorization request is missing required details.",
"localhost_redirect_warning": "Local redirect URI",
Expand Down
1 change: 1 addition & 0 deletions apps/web/locales/ja-JP.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -82,6 +82,7 @@
"consent_failed": "OAuth consent failed. Please try again.",
"consent_revoked": "OAuth app access revoked.",
"deny_access": "Deny access",
"formbricks_website": "Formbricks",
"invalid_oauth_request": "Invalid OAuth request",
"invalid_oauth_request_description": "The OAuth client could not be found or the authorization request is missing required details.",
"localhost_redirect_warning": "Local redirect URI",
Expand Down
1 change: 1 addition & 0 deletions apps/web/locales/nl-NL.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -82,6 +82,7 @@
"consent_failed": "OAuth consent failed. Please try again.",
"consent_revoked": "OAuth app access revoked.",
"deny_access": "Deny access",
"formbricks_website": "Formbricks",
"invalid_oauth_request": "Invalid OAuth request",
"invalid_oauth_request_description": "The OAuth client could not be found or the authorization request is missing required details.",
"localhost_redirect_warning": "Local redirect URI",
Expand Down
1 change: 1 addition & 0 deletions apps/web/locales/pt-BR.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -82,6 +82,7 @@
"consent_failed": "OAuth consent failed. Please try again.",
"consent_revoked": "OAuth app access revoked.",
"deny_access": "Deny access",
"formbricks_website": "Formbricks",
"invalid_oauth_request": "Invalid OAuth request",
"invalid_oauth_request_description": "The OAuth client could not be found or the authorization request is missing required details.",
"localhost_redirect_warning": "Local redirect URI",
Expand Down
1 change: 1 addition & 0 deletions apps/web/locales/pt-PT.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -82,6 +82,7 @@
"consent_failed": "OAuth consent failed. Please try again.",
"consent_revoked": "OAuth app access revoked.",
"deny_access": "Deny access",
"formbricks_website": "Formbricks",
"invalid_oauth_request": "Invalid OAuth request",
"invalid_oauth_request_description": "The OAuth client could not be found or the authorization request is missing required details.",
"localhost_redirect_warning": "Local redirect URI",
Expand Down
1 change: 1 addition & 0 deletions apps/web/locales/ro-RO.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -82,6 +82,7 @@
"consent_failed": "OAuth consent failed. Please try again.",
"consent_revoked": "OAuth app access revoked.",
"deny_access": "Deny access",
"formbricks_website": "Formbricks",
"invalid_oauth_request": "Invalid OAuth request",
"invalid_oauth_request_description": "The OAuth client could not be found or the authorization request is missing required details.",
"localhost_redirect_warning": "Local redirect URI",
Expand Down
1 change: 1 addition & 0 deletions apps/web/locales/ru-RU.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -82,6 +82,7 @@
"consent_failed": "OAuth consent failed. Please try again.",
"consent_revoked": "OAuth app access revoked.",
"deny_access": "Deny access",
"formbricks_website": "Formbricks",
"invalid_oauth_request": "Invalid OAuth request",
"invalid_oauth_request_description": "The OAuth client could not be found or the authorization request is missing required details.",
"localhost_redirect_warning": "Local redirect URI",
Expand Down
1 change: 1 addition & 0 deletions apps/web/locales/sv-SE.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -82,6 +82,7 @@
"consent_failed": "OAuth consent failed. Please try again.",
"consent_revoked": "OAuth app access revoked.",
"deny_access": "Deny access",
"formbricks_website": "Formbricks",
"invalid_oauth_request": "Invalid OAuth request",
"invalid_oauth_request_description": "The OAuth client could not be found or the authorization request is missing required details.",
"localhost_redirect_warning": "Local redirect URI",
Expand Down
1 change: 1 addition & 0 deletions apps/web/locales/tr-TR.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -82,6 +82,7 @@
"consent_failed": "OAuth consent failed. Please try again.",
"consent_revoked": "OAuth app access revoked.",
"deny_access": "Deny access",
"formbricks_website": "Formbricks",
"invalid_oauth_request": "Invalid OAuth request",
"invalid_oauth_request_description": "The OAuth client could not be found or the authorization request is missing required details.",
"localhost_redirect_warning": "Local redirect URI",
Expand Down
1 change: 1 addition & 0 deletions apps/web/locales/zh-Hans-CN.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -82,6 +82,7 @@
"consent_failed": "OAuth consent failed. Please try again.",
"consent_revoked": "OAuth app access revoked.",
"deny_access": "Deny access",
"formbricks_website": "Formbricks",
"invalid_oauth_request": "Invalid OAuth request",
"invalid_oauth_request_description": "The OAuth client could not be found or the authorization request is missing required details.",
"localhost_redirect_warning": "Local redirect URI",
Expand Down
1 change: 1 addition & 0 deletions apps/web/locales/zh-Hant-TW.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -82,6 +82,7 @@
"consent_failed": "OAuth consent failed. Please try again.",
"consent_revoked": "OAuth app access revoked.",
"deny_access": "Deny access",
"formbricks_website": "Formbricks",
"invalid_oauth_request": "Invalid OAuth request",
"invalid_oauth_request_description": "The OAuth client could not be found or the authorization request is missing required details.",
"localhost_redirect_warning": "Local redirect URI",
Expand Down
116 changes: 116 additions & 0 deletions apps/web/modules/api/v2/management/responses/route.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
import { beforeEach, describe, expect, test, vi } from "vitest";
import type { authenticatedApiClient } from "@/modules/api/v2/auth/authenticated-api-client";

const { mockAuthenticatedApiClient, mockGetResponses, mockHandleApiError, mockSuccessResponse } = vi.hoisted(
() => ({
mockAuthenticatedApiClient: vi.fn(),
mockGetResponses: vi.fn(),
mockHandleApiError: vi.fn(),
mockSuccessResponse: vi.fn(),
})
);

vi.mock("@/modules/api/v2/auth/authenticated-api-client", () => ({
authenticatedApiClient: mockAuthenticatedApiClient,
}));

vi.mock("@/modules/api/v2/lib/response", () => ({
responses: {
createdResponse: mockSuccessResponse,
successResponse: mockSuccessResponse,
},
}));

vi.mock("@/modules/api/v2/lib/utils", () => ({
handleApiError: mockHandleApiError,
}));

vi.mock("./lib/response", () => ({
createResponseWithQuotaEvaluation: vi.fn(),
getResponses: mockGetResponses,
}));

vi.mock("@/app/lib/pipelines", () => ({
sendToPipeline: vi.fn(),
}));

// Not an identity stub: the success case asserts the *rewritten* value, so the test fails if the
// route stops piping each row's data through this.
vi.mock("@/modules/storage/utils", () => ({
resolveStorageUrlsInObject: (data: Record<string, unknown>) =>
Object.fromEntries(
Object.entries(data).map(([key, value]) => [
key,
value === "storage://private/file.png" ? "https://cdn.example.com/file.png" : value,
])
),
validateClientFileUploads: vi.fn(),
}));

const query = { limit: 2, skip: 10, sortBy: "createdAt", order: "desc" } as const;

const buildRequest = () => new Request("http://localhost/api/v2/management/responses?limit=2&skip=10");

describe("GET /management/responses", () => {
beforeEach(() => {
vi.clearAllMocks();

mockAuthenticatedApiClient.mockImplementation(
async ({ handler }: Parameters<typeof authenticatedApiClient>[0]) =>
await handler({
request: buildRequest(),
auditLog: undefined,
authentication: {
type: "apiKey",
apiKeyId: "apiKey123",
organizationId: "org123",
workspacePermissions: [
{ workspaceId: "ws123", workspaceName: "Test Workspace", permission: "read" },
],
organizationAccess: { accessControl: { read: true, write: true } },
},
parsedInput: { query },
})
);
mockHandleApiError.mockImplementation((_request, error) => Response.json({ error }, { status: 400 }));
mockSuccessResponse.mockImplementation((body: unknown) => Response.json(body, { status: 200 }));
});

test("returns the pagination meta the service computed alongside the data", async () => {
mockGetResponses.mockResolvedValue({
ok: true,
data: {
data: [{ id: "res1", data: { q1: "a", upload: "storage://private/file.png" } }],
meta: { total: 137, limit: 2, offset: 10 },
},
});

const { GET } = await import("./route");
const response = await GET(buildRequest() as any);
const body = await response.json();

expect(mockGetResponses).toHaveBeenCalledWith(["ws123"], query);
expect(response.status).toBe(200);
expect(body).toEqual({
data: [{ id: "res1", data: { q1: "a", upload: "https://cdn.example.com/file.png" } }],
meta: { total: 137, limit: 2, offset: 10 },
});
});

test("surfaces the service error instead of an envelope", async () => {
mockGetResponses.mockResolvedValue({
ok: false,
error: { type: "internal_server_error", details: [{ field: "responses", issue: "boom" }] },
});

const { GET } = await import("./route");
const response = await GET(buildRequest() as any);

expect(mockSuccessResponse).not.toHaveBeenCalled();
expect(mockHandleApiError).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({ type: "internal_server_error" })
);
expect(response.status).toBe(400);
});
});
7 changes: 2 additions & 5 deletions apps/web/modules/api/v2/management/responses/route.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
import { NextRequest } from "next/server";
import { Response } from "@formbricks/database/prisma";
import { sendToPipeline } from "@/app/lib/pipelines";
import { getWorkspaceLegacyStoragePrefixes } from "@/lib/workspace/service";
import { formatValidationErrorsForV2Api, validateResponseData } from "@/modules/api/lib/validation";
Expand DownExpand Up@@ -36,17 +35,15 @@ export const GET = async (request: NextRequest) =>
...new Set(authentication.workspacePermissions.map((permission) => permission.workspaceId)),
];

const workspaceResponses: Response[] = [];
const res = await getResponses(workspaceIds, query);

if (!res.ok) {
return handleApiError(request, res.error);
}

workspaceResponses.push(...res.data.data);

return responses.successResponse({
data: workspaceResponses.map((r) => ({ ...r, data: resolveStorageUrlsInObject(r.data) })),
data: res.data.data.map((r) => ({ ...r, data: resolveStorageUrlsInObject(r.data) })),
meta: res.data.meta,
});
},
});
Expand Down
19 changes: 19 additions & 0 deletions apps/web/modules/auth/lib/better-auth-observability.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -414,6 +414,25 @@ describe("betterAuthLogger (Sentry capture gating, ENG-2037)", () => {
expect(contextLoggerMock.warn).toHaveBeenCalledWith("account isn't linked");
});

test("preserves only an allowlisted warn-level error summary in application logs", () => {
const dbError = Object.assign(new Error("Connection for admin@example.com included token-secret"), {
code: "P1001",
databaseUrl: "postgres://user:password-secret@db.example.com/formbricks",
});
dbError.stack = "Error: stack-secret";

log("warn", "OAuth resource seed for admin@example.com failed", dbError);

expect(Sentry.captureException).not.toHaveBeenCalled();
expect(contextLoggerMock.warn).toHaveBeenCalledWith(
{ errorType: "Error", errorCode: "P1001" },
"OAuth resource seed for [redacted]@example.com failed"
);
expect(JSON.stringify(contextLoggerMock.warn.mock.calls)).not.toMatch(
/token-secret|password-secret|stack-secret/
);
});

test("info/debug-level logs go to info and are never captured", () => {
log("info", "some info");

Expand Down
Loading
Loading