mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-30 03:33:48 -05:00
chore: clean-up new cache package (#6532)
This commit is contained in:
committed by
GitHub
parent
3879d86f63
commit
935e24bd43
@@ -61,6 +61,7 @@ RUN pnpm build --filter=@formbricks/database
|
||||
# This mounts the secrets only during this build step without storing them in layers
|
||||
RUN --mount=type=secret,id=database_url \
|
||||
--mount=type=secret,id=encryption_key \
|
||||
--mount=type=secret,id=redis_url \
|
||||
--mount=type=secret,id=sentry_auth_token \
|
||||
/tmp/read-secrets.sh pnpm build --filter=@formbricks/web...
|
||||
|
||||
|
||||
@@ -151,7 +151,6 @@ export const DEBUG = env.DEBUG === "1";
|
||||
export const ENTERPRISE_LICENSE_KEY = env.ENTERPRISE_LICENSE_KEY;
|
||||
|
||||
export const REDIS_URL = env.REDIS_URL;
|
||||
export const REDIS_HTTP_URL = env.REDIS_HTTP_URL;
|
||||
export const RATE_LIMITING_DISABLED = env.RATE_LIMITING_DISABLED === "1";
|
||||
|
||||
export const BREVO_API_KEY = env.BREVO_API_KEY;
|
||||
|
||||
+4
-3
@@ -54,8 +54,10 @@ export const env = createEnv({
|
||||
OIDC_ISSUER: z.string().optional(),
|
||||
OIDC_SIGNING_ALGORITHM: z.string().optional(),
|
||||
OPENTELEMETRY_LISTENER_URL: z.string().optional(),
|
||||
REDIS_URL: z.string().optional(),
|
||||
REDIS_HTTP_URL: z.string().optional(),
|
||||
REDIS_URL:
|
||||
process.env.NODE_ENV === "test"
|
||||
? z.string().optional()
|
||||
: z.string().url("REDIS_URL is required for caching, rate limiting, and audit logging"),
|
||||
PASSWORD_RESET_DISABLED: z.enum(["1", "0"]).optional(),
|
||||
POSTHOG_API_HOST: z.string().optional(),
|
||||
POSTHOG_API_KEY: z.string().optional(),
|
||||
@@ -182,7 +184,6 @@ export const env = createEnv({
|
||||
OIDC_ISSUER: process.env.OIDC_ISSUER,
|
||||
OIDC_SIGNING_ALGORITHM: process.env.OIDC_SIGNING_ALGORITHM,
|
||||
REDIS_URL: process.env.REDIS_URL,
|
||||
REDIS_HTTP_URL: process.env.REDIS_HTTP_URL,
|
||||
PASSWORD_RESET_DISABLED: process.env.PASSWORD_RESET_DISABLED,
|
||||
PRIVACY_URL: process.env.PRIVACY_URL,
|
||||
RATE_LIMITING_DISABLED: process.env.RATE_LIMITING_DISABLED,
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { deepDiff, redactPII, sanitizeUrlForLogging } from "./logger-helpers";
|
||||
|
||||
// Patch redis multi before any imports
|
||||
// Patch cache before any imports
|
||||
beforeEach(async () => {
|
||||
const redis = (await import("@/modules/cache/redis")).default;
|
||||
if ((redis?.multi as any)?.mockReturnValue) {
|
||||
(redis?.multi as any).mockReturnValue({
|
||||
set: vi.fn(),
|
||||
exec: vi.fn().mockResolvedValue([["OK"]]),
|
||||
});
|
||||
}
|
||||
// Mock the cache service for tests
|
||||
vi.doMock("@/lib/cache", () => ({
|
||||
cache: {
|
||||
getRedisClient: vi.fn().mockResolvedValue({
|
||||
multi: vi.fn().mockReturnValue({
|
||||
set: vi.fn(),
|
||||
exec: vi.fn().mockResolvedValue([["OK"]]),
|
||||
}),
|
||||
watch: vi.fn().mockResolvedValue("OK"),
|
||||
get: vi.fn().mockResolvedValue(null),
|
||||
}),
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
vi.mock("@/modules/ee/license-check/lib/utils", () => ({
|
||||
@@ -37,16 +43,7 @@ vi.mock("@/modules/ee/audit-logs/lib/service", () => ({
|
||||
logAuditEvent: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
vi.mock("@/modules/cache/redis", () => ({
|
||||
default: {
|
||||
watch: vi.fn().mockResolvedValue("OK"),
|
||||
multi: vi.fn().mockReturnValue({
|
||||
set: vi.fn(),
|
||||
exec: vi.fn().mockResolvedValue([["OK"]]),
|
||||
}),
|
||||
get: vi.fn().mockResolvedValue(null),
|
||||
},
|
||||
}));
|
||||
// Cache mock is handled in beforeEach above
|
||||
|
||||
// Set ENCRYPTION_KEY for all tests unless explicitly testing its absence
|
||||
process.env.ENCRYPTION_KEY = "testsecret";
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
|
||||
describe("in-memory rate limiter", () => {
|
||||
test("allows requests within limit and throws after limit", async () => {
|
||||
const { rateLimit } = await import("./rate-limit");
|
||||
const limiterFn = rateLimit({ interval: 1, allowedPerInterval: 2 });
|
||||
await expect(limiterFn("a")).resolves.toBeUndefined();
|
||||
await expect(limiterFn("a")).resolves.toBeUndefined();
|
||||
await expect(limiterFn("a")).rejects.toThrow("Rate limit exceeded");
|
||||
});
|
||||
|
||||
test("separate tokens have separate counts", async () => {
|
||||
const { rateLimit } = await import("./rate-limit");
|
||||
const limiterFn = rateLimit({ interval: 1, allowedPerInterval: 2 });
|
||||
await expect(limiterFn("x")).resolves.toBeUndefined();
|
||||
await expect(limiterFn("y")).resolves.toBeUndefined();
|
||||
await expect(limiterFn("x")).resolves.toBeUndefined();
|
||||
await expect(limiterFn("y")).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("redis rate limiter", () => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
const constants = await vi.importActual("@/lib/constants");
|
||||
vi.doMock("@/lib/constants", () => ({
|
||||
...constants,
|
||||
REDIS_HTTP_URL: "http://redis",
|
||||
}));
|
||||
});
|
||||
|
||||
test("sets expire on first use and does not throw", async () => {
|
||||
global.fetch = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ ok: true, json: async () => ({ INCR: 1 }) })
|
||||
.mockResolvedValueOnce({ ok: true });
|
||||
const { rateLimit } = await import("./rate-limit");
|
||||
const limiter = rateLimit({ interval: 10, allowedPerInterval: 2 });
|
||||
await expect(limiter("t")).resolves.toBeUndefined();
|
||||
expect(fetch).toHaveBeenCalledTimes(2);
|
||||
expect(fetch).toHaveBeenCalledWith("http://redis/INCR/t");
|
||||
expect(fetch).toHaveBeenCalledWith("http://redis/EXPIRE/t/10");
|
||||
});
|
||||
|
||||
test("does not throw when redis INCR response not ok", async () => {
|
||||
global.fetch = vi.fn().mockResolvedValueOnce({ ok: false });
|
||||
const { rateLimit } = await import("./rate-limit");
|
||||
const limiter = rateLimit({ interval: 10, allowedPerInterval: 2 });
|
||||
await expect(limiter("t")).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
test("throws when INCR exceeds limit", async () => {
|
||||
global.fetch = vi.fn().mockResolvedValueOnce({ ok: true, json: async () => ({ INCR: 3 }) });
|
||||
const { rateLimit } = await import("./rate-limit");
|
||||
const limiter = rateLimit({ interval: 10, allowedPerInterval: 2 });
|
||||
await expect(limiter("t")).rejects.toThrow("Rate limit exceeded for IP: t");
|
||||
});
|
||||
});
|
||||
@@ -1,54 +0,0 @@
|
||||
import { REDIS_HTTP_URL } from "@/lib/constants";
|
||||
import { LRUCache } from "lru-cache";
|
||||
import { logger } from "@formbricks/logger";
|
||||
|
||||
interface Options {
|
||||
interval: number;
|
||||
allowedPerInterval: number;
|
||||
}
|
||||
|
||||
const inMemoryRateLimiter = (options: Options) => {
|
||||
const tokenCache = new LRUCache<string, number>({
|
||||
max: 1000,
|
||||
ttl: options.interval * 1000, // converts to expected input of milliseconds
|
||||
});
|
||||
|
||||
return async (token: string) => {
|
||||
const currentUsage = tokenCache.get(token) ?? 0;
|
||||
if (currentUsage >= options.allowedPerInterval) {
|
||||
throw new Error("Rate limit exceeded");
|
||||
}
|
||||
tokenCache.set(token, currentUsage + 1);
|
||||
};
|
||||
};
|
||||
|
||||
const redisRateLimiter = (options: Options) => async (token: string) => {
|
||||
try {
|
||||
if (!REDIS_HTTP_URL) {
|
||||
throw new Error("Redis HTTP URL is not set");
|
||||
}
|
||||
const tokenCountResponse = await fetch(`${REDIS_HTTP_URL}/INCR/${token}`);
|
||||
if (!tokenCountResponse.ok) {
|
||||
logger.error({ tokenCountResponse }, "Failed to increment token count in Redis");
|
||||
return;
|
||||
}
|
||||
|
||||
const { INCR } = await tokenCountResponse.json();
|
||||
if (INCR === 1) {
|
||||
await fetch(`${REDIS_HTTP_URL}/EXPIRE/${token}/${options.interval.toString()}`);
|
||||
} else if (INCR > options.allowedPerInterval) {
|
||||
throw new Error();
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error({ error: e }, "Rate limit exceeded");
|
||||
throw new Error("Rate limit exceeded for IP: " + token);
|
||||
}
|
||||
};
|
||||
|
||||
export const rateLimit = (options: Options) => {
|
||||
if (REDIS_HTTP_URL) {
|
||||
return redisRateLimiter(options);
|
||||
} else {
|
||||
return inMemoryRateLimiter(options);
|
||||
}
|
||||
};
|
||||
@@ -748,7 +748,6 @@
|
||||
"api_key_label": "API-Schlüssel Label",
|
||||
"api_key_security_warning": "Aus Sicherheitsgründen wird der API-Schlüssel nur einmal nach der Erstellung angezeigt. Bitte kopiere ihn sofort an einen sicheren Ort.",
|
||||
"api_key_updated": "API-Schlüssel aktualisiert",
|
||||
"delete_permission": "Berechtigung löschen",
|
||||
"duplicate_access": "Doppelter Projektzugriff nicht erlaubt",
|
||||
"no_api_keys_yet": "Du hast noch keine API-Schlüssel",
|
||||
"no_env_permissions_found": "Keine Umgebungsberechtigungen gefunden",
|
||||
|
||||
@@ -748,7 +748,6 @@
|
||||
"api_key_label": "API Key Label",
|
||||
"api_key_security_warning": "For security reasons, the API key will only be shown once after creation. Please copy it to your destination right away.",
|
||||
"api_key_updated": "API Key updated",
|
||||
"delete_permission": "Delete permission",
|
||||
"duplicate_access": "Duplicate project access not allowed",
|
||||
"no_api_keys_yet": "You don't have any API keys yet",
|
||||
"no_env_permissions_found": "No environment permissions found",
|
||||
|
||||
@@ -748,7 +748,6 @@
|
||||
"api_key_label": "Étiquette de clé API",
|
||||
"api_key_security_warning": "Pour des raisons de sécurité, la clé API ne sera affichée qu'une seule fois après sa création. Veuillez la copier immédiatement à votre destination.",
|
||||
"api_key_updated": "Clé API mise à jour",
|
||||
"delete_permission": "Supprimer une permission",
|
||||
"duplicate_access": "L'accès en double au projet n'est pas autorisé",
|
||||
"no_api_keys_yet": "Vous n'avez pas encore de clés API.",
|
||||
"no_env_permissions_found": "Aucune autorisation d'environnement trouvée",
|
||||
|
||||
@@ -748,7 +748,6 @@
|
||||
"api_key_label": "APIキーのラベル",
|
||||
"api_key_security_warning": "セキュリティ上の理由から、APIキーは作成時に一度だけ表示されます。すぐにコピーしてください。",
|
||||
"api_key_updated": "APIキーを更新しました",
|
||||
"delete_permission": "削除権限",
|
||||
"duplicate_access": "重複したプロジェクトアクセスは許可されません",
|
||||
"no_api_keys_yet": "まだAPIキーがありません",
|
||||
"no_env_permissions_found": "環境権限が見つかりません",
|
||||
|
||||
@@ -748,7 +748,6 @@
|
||||
"api_key_label": "Rótulo da Chave API",
|
||||
"api_key_security_warning": "Por motivos de segurança, a chave da API será mostrada apenas uma vez após a criação. Por favor, copie-a para o seu destino imediatamente.",
|
||||
"api_key_updated": "Chave de API atualizada",
|
||||
"delete_permission": "Remover permissão",
|
||||
"duplicate_access": "Acesso duplicado ao projeto não permitido",
|
||||
"no_api_keys_yet": "Você ainda não tem nenhuma chave de API",
|
||||
"no_env_permissions_found": "Nenhuma permissão de ambiente encontrada",
|
||||
|
||||
@@ -748,7 +748,6 @@
|
||||
"api_key_label": "Etiqueta da Chave API",
|
||||
"api_key_security_warning": "Por razões de segurança, a chave API será mostrada apenas uma vez após a criação. Por favor, copie-a para o seu destino imediatamente.",
|
||||
"api_key_updated": "Chave API atualizada",
|
||||
"delete_permission": "Eliminar permissão",
|
||||
"duplicate_access": "Acesso duplicado ao projeto não permitido",
|
||||
"no_api_keys_yet": "Ainda não tem nenhuma chave API",
|
||||
"no_env_permissions_found": "Nenhuma permissão de ambiente encontrada",
|
||||
|
||||
@@ -748,7 +748,6 @@
|
||||
"api_key_label": "Etichetă Cheie API",
|
||||
"api_key_security_warning": "Din motive de securitate, cheia API va fi afișată o singură dată după creare. Vă rugăm să o copiați imediat la destinație.",
|
||||
"api_key_updated": "Cheie API actualizată",
|
||||
"delete_permission": "Șterge permisiunea",
|
||||
"duplicate_access": "Accesul dublu la proiect nu este permis",
|
||||
"no_api_keys_yet": "Nu aveți încă chei API",
|
||||
"no_env_permissions_found": "Nu s-au găsit permisiuni pentru mediu",
|
||||
|
||||
@@ -748,7 +748,6 @@
|
||||
"api_key_label": "API Key Label",
|
||||
"api_key_security_warning": "出于安全原因,API 密钥在创建后只会显示一次。 请立即将其复制到您的目标位置。",
|
||||
"api_key_updated": "API Key 已更新",
|
||||
"delete_permission": "删除 权限",
|
||||
"duplicate_access": "不允许 复制 项目 访问",
|
||||
"no_api_keys_yet": "您 还 没有 任 何 API key",
|
||||
"no_env_permissions_found": "未找到环境权限",
|
||||
|
||||
@@ -748,7 +748,6 @@
|
||||
"api_key_label": "API 金鑰標籤",
|
||||
"api_key_security_warning": "為安全起見,API 金鑰僅在建立後顯示一次。請立即將其複製到您的目的地。",
|
||||
"api_key_updated": "API 金鑰已更新",
|
||||
"delete_permission": "刪除 權限",
|
||||
"duplicate_access": "不允許重複的 project 存取",
|
||||
"no_api_keys_yet": "您還沒有任何 API 金鑰",
|
||||
"no_env_permissions_found": "找不到環境權限",
|
||||
|
||||
@@ -38,7 +38,6 @@
|
||||
"@hookform/resolvers": "5.0.1",
|
||||
"@intercom/messenger-js-sdk": "0.0.14",
|
||||
"@json2csv/node": "7.0.6",
|
||||
"@keyv/redis": "4.4.0",
|
||||
"@lexical/code": "0.31.0",
|
||||
"@lexical/link": "0.31.0",
|
||||
"@lexical/list": "0.31.0",
|
||||
@@ -85,7 +84,6 @@
|
||||
"@vercel/og": "0.8.5",
|
||||
"bcryptjs": "3.0.2",
|
||||
"boring-avatars": "2.0.1",
|
||||
"cache-manager": "6.4.3",
|
||||
"class-variance-authority": "0.7.1",
|
||||
"clsx": "2.1.1",
|
||||
"cmdk": "1.1.1",
|
||||
@@ -98,7 +96,6 @@
|
||||
"https-proxy-agent": "7.0.6",
|
||||
"jiti": "2.4.2",
|
||||
"jsonwebtoken": "9.0.2",
|
||||
"keyv": "5.3.3",
|
||||
"lexical": "0.31.0",
|
||||
"lodash": "4.17.21",
|
||||
"lru-cache": "11.1.0",
|
||||
|
||||
@@ -6,7 +6,7 @@ if [ -f "/run/secrets/database_url" ]; then
|
||||
DATABASE_URL=${DATABASE_URL%$'\n'}
|
||||
export DATABASE_URL
|
||||
else
|
||||
echo "DATABASE_URL secret not found. Build may fail if this is required."
|
||||
echo "DATABASE_URL secret not found. Build will fail because it is required by the application."
|
||||
fi
|
||||
|
||||
if [ -f "/run/secrets/encryption_key" ]; then
|
||||
@@ -14,7 +14,15 @@ if [ -f "/run/secrets/encryption_key" ]; then
|
||||
ENCRYPTION_KEY=${ENCRYPTION_KEY%$'\n'}
|
||||
export ENCRYPTION_KEY
|
||||
else
|
||||
echo "ENCRYPTION_KEY secret not found. Build may fail if this is required."
|
||||
echo "ENCRYPTION_KEY secret not found. Build will fail because it is required by the application."
|
||||
fi
|
||||
|
||||
if [ -f "/run/secrets/redis_url" ]; then
|
||||
IFS= read -r REDIS_URL < /run/secrets/redis_url || true
|
||||
REDIS_URL=${REDIS_URL%$'\n'}
|
||||
export REDIS_URL
|
||||
else
|
||||
echo "REDIS_URL secret not found. Build will fail because it is required by the application."
|
||||
fi
|
||||
|
||||
if [ -f "/run/secrets/sentry_auth_token" ]; then
|
||||
@@ -39,6 +47,7 @@ fi
|
||||
# Verify environment variables are set before starting build
|
||||
echo " DATABASE_URL: $([ -n "${DATABASE_URL:-}" ] && printf '[SET]' || printf '[NOT SET]')"
|
||||
echo " ENCRYPTION_KEY: $([ -n "${ENCRYPTION_KEY:-}" ] && printf '[SET]' || printf '[NOT SET]')"
|
||||
echo " REDIS_URL: $([ -n "${REDIS_URL:-}" ] && printf '[SET]' || printf '[NOT SET]')"
|
||||
echo " SENTRY_AUTH_TOKEN: $([ -n "${SENTRY_AUTH_TOKEN:-}" ] && printf '[SET]' || printf '[NOT SET]')"
|
||||
echo " TARGETARCH: $([ -n "${TARGETARCH:-}" ] && printf '%s' "${TARGETARCH}" || printf '[NOT SET]')"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user