Compare commits

..

3 Commits

Author SHA1 Message Date
miloschwartz
6e6fa77625 bump version 2025-12-04 17:10:59 -05:00
Owen
5c0c12cabe Update lock 2025-12-04 17:02:45 -05:00
miloschwartz
10a00ff225 update next version 2025-12-04 16:56:39 -05:00
16 changed files with 2014 additions and 3837 deletions

View File

@@ -39,7 +39,7 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1

View File

@@ -3,8 +3,8 @@ module installer
go 1.24.0 go 1.24.0
require ( require (
golang.org/x/term v0.37.0 golang.org/x/term v0.36.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
require golang.org/x/sys v0.38.0 // indirect require golang.org/x/sys v0.37.0 // indirect

View File

@@ -1,7 +1,7 @@
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@@ -238,6 +238,7 @@ func main() {
} }
fmt.Println("CrowdSec installed successfully!") fmt.Println("CrowdSec installed successfully!")
return
} }
} }
} }

View File

@@ -1421,9 +1421,6 @@
"and": "and", "and": "and",
"privacyPolicy": "privacy policy" "privacyPolicy": "privacy policy"
}, },
"signUpMarketing": {
"keepMeInTheLoop": "Keep me in the loop with news, updates, and new features by email."
},
"siteRequired": "Site is required.", "siteRequired": "Site is required.",
"olmTunnel": "Olm Tunnel", "olmTunnel": "Olm Tunnel",
"olmTunnelDescription": "Use Olm for client connectivity", "olmTunnelDescription": "Use Olm for client connectivity",

5575
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -39,20 +39,20 @@
"@node-rs/argon2": "^2.0.2", "@node-rs/argon2": "^2.0.2",
"@oslojs/crypto": "1.0.1", "@oslojs/crypto": "1.0.1",
"@oslojs/encoding": "1.1.0", "@oslojs/encoding": "1.1.0",
"@radix-ui/react-avatar": "1.1.11", "@radix-ui/react-avatar": "1.1.10",
"@radix-ui/react-checkbox": "1.3.3", "@radix-ui/react-checkbox": "1.3.3",
"@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collapsible": "1.1.12",
"@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-dialog": "1.1.15",
"@radix-ui/react-dropdown-menu": "2.1.16", "@radix-ui/react-dropdown-menu": "2.1.16",
"@radix-ui/react-icons": "1.3.2", "@radix-ui/react-icons": "1.3.2",
"@radix-ui/react-label": "2.1.8", "@radix-ui/react-label": "2.1.7",
"@radix-ui/react-popover": "1.1.15", "@radix-ui/react-popover": "1.1.15",
"@radix-ui/react-progress": "^1.1.8", "@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-radio-group": "1.3.8", "@radix-ui/react-radio-group": "1.3.8",
"@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "2.2.6", "@radix-ui/react-select": "2.2.6",
"@radix-ui/react-separator": "1.1.8", "@radix-ui/react-separator": "1.1.7",
"@radix-ui/react-slot": "1.2.4", "@radix-ui/react-slot": "1.2.3",
"@radix-ui/react-switch": "1.2.6", "@radix-ui/react-switch": "1.2.6",
"@radix-ui/react-tabs": "1.1.13", "@radix-ui/react-tabs": "1.1.13",
"@radix-ui/react-toast": "1.2.15", "@radix-ui/react-toast": "1.2.15",
@@ -65,7 +65,7 @@
"@tailwindcss/forms": "^0.5.10", "@tailwindcss/forms": "^0.5.10",
"@tanstack/react-table": "8.21.3", "@tanstack/react-table": "8.21.3",
"arctic": "^3.7.0", "arctic": "^3.7.0",
"axios": "^1.13.2", "axios": "^1.13.1",
"better-sqlite3": "11.7.0", "better-sqlite3": "11.7.0",
"canvas-confetti": "1.9.4", "canvas-confetti": "1.9.4",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
@@ -78,8 +78,8 @@
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"date-fns": "4.1.0", "date-fns": "4.1.0",
"drizzle-orm": "0.44.7", "drizzle-orm": "0.44.7",
"eslint": "9.39.1", "eslint": "9.39.0",
"eslint-config-next": "16.0.3", "eslint-config-next": "16.0.1",
"express": "5.1.0", "express": "5.1.0",
"express-rate-limit": "8.2.1", "express-rate-limit": "8.2.1",
"glob": "11.0.3", "glob": "11.0.3",
@@ -89,12 +89,12 @@
"input-otp": "1.4.2", "input-otp": "1.4.2",
"ioredis": "5.8.2", "ioredis": "5.8.2",
"jmespath": "^0.16.0", "jmespath": "^0.16.0",
"js-yaml": "4.1.1", "js-yaml": "4.1.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"lucide-react": "^0.552.0", "lucide-react": "^0.552.0",
"maxmind": "5.0.1", "maxmind": "5.0.0",
"moment": "2.30.1", "moment": "2.30.1",
"next": "15.5.6", "next": "15.5.7",
"next-intl": "^4.4.0", "next-intl": "^4.4.0",
"next-themes": "0.4.6", "next-themes": "0.4.6",
"nextjs-toploader": "^3.9.17", "nextjs-toploader": "^3.9.17",
@@ -105,7 +105,7 @@
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"oslo": "1.2.1", "oslo": "1.2.1",
"pg": "^8.16.2", "pg": "^8.16.2",
"posthog-node": "^5.11.2", "posthog-node": "^5.11.0",
"qrcode.react": "4.2.0", "qrcode.react": "4.2.0",
"react": "19.2.0", "react": "19.2.0",
"react-day-picker": "9.11.1", "react-day-picker": "9.11.1",
@@ -115,7 +115,7 @@
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"rebuild": "0.1.2", "rebuild": "0.1.2",
"reodotdev": "^1.0.0", "reodotdev": "^1.0.0",
"resend": "^6.4.2", "resend": "^6.4.0",
"semver": "^7.7.3", "semver": "^7.7.3",
"stripe": "18.2.1", "stripe": "18.2.1",
"swagger-ui-express": "^5.0.1", "swagger-ui-express": "^5.0.1",
@@ -147,7 +147,7 @@
"@types/js-yaml": "4.0.9", "@types/js-yaml": "4.0.9",
"@types/jsonwebtoken": "^9.0.10", "@types/jsonwebtoken": "^9.0.10",
"@types/nprogress": "^0.2.3", "@types/nprogress": "^0.2.3",
"@types/node": "24.10.1", "@types/node": "24.9.2",
"@types/nodemailer": "7.0.3", "@types/nodemailer": "7.0.3",
"@types/pg": "8.15.6", "@types/pg": "8.15.6",
"@types/react": "19.2.2", "@types/react": "19.2.2",
@@ -157,8 +157,8 @@
"@types/ws": "8.18.1", "@types/ws": "8.18.1",
"@types/yargs": "17.0.34", "@types/yargs": "17.0.34",
"drizzle-kit": "0.31.6", "drizzle-kit": "0.31.6",
"esbuild": "0.27.0", "esbuild": "0.25.12",
"esbuild-node-externals": "1.19.1", "esbuild-node-externals": "1.18.0",
"postcss": "^8", "postcss": "^8",
"react-email": "4.3.2", "react-email": "4.3.2",
"tailwindcss": "^4.1.4", "tailwindcss": "^4.1.4",

View File

@@ -79,12 +79,6 @@ export function createApiServer() {
// Add request timeout middleware // Add request timeout middleware
apiServer.use(requestTimeoutMiddleware(60000)); // 60 second timeout apiServer.use(requestTimeoutMiddleware(60000)); // 60 second timeout
apiServer.use(logIncomingMiddleware);
if (build !== "oss") {
apiServer.use(`${prefix}/hybrid`, hybridRouter); // put before rate limiting because we will rate limit there separately because some of the routes are heavily used
}
if (!dev) { if (!dev) {
apiServer.use( apiServer.use(
rateLimit({ rateLimit({
@@ -107,7 +101,11 @@ export function createApiServer() {
} }
// API routes // API routes
apiServer.use(logIncomingMiddleware);
apiServer.use(prefix, unauthenticated); apiServer.use(prefix, unauthenticated);
if (build !== "oss") {
apiServer.use(`${prefix}/hybrid`, hybridRouter);
}
apiServer.use(prefix, authenticated); apiServer.use(prefix, authenticated);
// WebSocket routes // WebSocket routes

View File

@@ -2,7 +2,7 @@ import path from "path";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
// This is a placeholder value replaced by the build process // This is a placeholder value replaced by the build process
export const APP_VERSION = "1.12.1"; export const APP_VERSION = "1.12.3";
export const __FILENAME = fileURLToPath(import.meta.url); export const __FILENAME = fileURLToPath(import.meta.url);
export const __DIRNAME = path.dirname(__FILENAME); export const __DIRNAME = path.dirname(__FILENAME);

View File

@@ -72,43 +72,6 @@ export class RateLimitService {
return `ratelimit:${clientId}:${messageType}`; return `ratelimit:${clientId}:${messageType}`;
} }
// Helper function to clean up old timestamp fields from a Redis hash
private async cleanupOldTimestamps(key: string, windowStart: number): Promise<void> {
if (!redisManager.isRedisEnabled()) return;
try {
const client = redisManager.getClient();
if (!client) return;
// Get all fields in the hash
const allData = await redisManager.hgetall(key);
if (!allData || Object.keys(allData).length === 0) return;
// Find fields that are older than the window
const fieldsToDelete: string[] = [];
for (const timestamp of Object.keys(allData)) {
const time = parseInt(timestamp);
if (time < windowStart) {
fieldsToDelete.push(timestamp);
}
}
// Delete old fields in batches to avoid call stack size exceeded errors
// The spread operator can cause issues with very large arrays
if (fieldsToDelete.length > 0) {
const batchSize = 1000; // Process 1000 fields at a time
for (let i = 0; i < fieldsToDelete.length; i += batchSize) {
const batch = fieldsToDelete.slice(i, i + batchSize);
await client.hdel(key, ...batch);
}
logger.debug(`Cleaned up ${fieldsToDelete.length} old timestamp fields from ${key}`);
}
} catch (error) {
logger.error(`Failed to cleanup old timestamps for key ${key}:`, error);
// Don't throw - cleanup failures shouldn't block rate limiting
}
}
// Helper function to sync local rate limit data to Redis // Helper function to sync local rate limit data to Redis
private async syncRateLimitToRedis( private async syncRateLimitToRedis(
clientId: string, clientId: string,
@@ -118,12 +81,8 @@ export class RateLimitService {
try { try {
const currentTime = Math.floor(Date.now() / 1000); const currentTime = Math.floor(Date.now() / 1000);
const windowStart = currentTime - RATE_LIMIT_WINDOW;
const globalKey = this.getRateLimitKey(clientId); const globalKey = this.getRateLimitKey(clientId);
// Clean up old timestamp fields before writing
await this.cleanupOldTimestamps(globalKey, windowStart);
// Get current value and add pending count // Get current value and add pending count
const currentValue = await redisManager.hget( const currentValue = await redisManager.hget(
globalKey, globalKey,
@@ -134,7 +93,7 @@ export class RateLimitService {
).toString(); ).toString();
await redisManager.hset(globalKey, currentTime.toString(), newValue); await redisManager.hset(globalKey, currentTime.toString(), newValue);
// Set TTL using the client directly - this prevents the key from persisting forever // Set TTL using the client directly
if (redisManager.getClient()) { if (redisManager.getClient()) {
await redisManager await redisManager
.getClient() .getClient()
@@ -160,12 +119,8 @@ export class RateLimitService {
try { try {
const currentTime = Math.floor(Date.now() / 1000); const currentTime = Math.floor(Date.now() / 1000);
const windowStart = currentTime - RATE_LIMIT_WINDOW;
const messageTypeKey = this.getMessageTypeRateLimitKey(clientId, messageType); const messageTypeKey = this.getMessageTypeRateLimitKey(clientId, messageType);
// Clean up old timestamp fields before writing
await this.cleanupOldTimestamps(messageTypeKey, windowStart);
// Get current value and add pending count // Get current value and add pending count
const currentValue = await redisManager.hget( const currentValue = await redisManager.hget(
messageTypeKey, messageTypeKey,
@@ -180,7 +135,7 @@ export class RateLimitService {
newValue newValue
); );
// Set TTL using the client directly - this prevents the key from persisting forever // Set TTL using the client directly
if (redisManager.getClient()) { if (redisManager.getClient()) {
await redisManager await redisManager
.getClient() .getClient()
@@ -215,10 +170,6 @@ export class RateLimitService {
try { try {
const globalKey = this.getRateLimitKey(clientId); const globalKey = this.getRateLimitKey(clientId);
// Clean up old timestamp fields before reading
await this.cleanupOldTimestamps(globalKey, windowStart);
const globalRateLimitData = await redisManager.hgetall(globalKey); const globalRateLimitData = await redisManager.hgetall(globalKey);
let count = 0; let count = 0;
@@ -264,10 +215,6 @@ export class RateLimitService {
try { try {
const messageTypeKey = this.getMessageTypeRateLimitKey(clientId, messageType); const messageTypeKey = this.getMessageTypeRateLimitKey(clientId, messageType);
// Clean up old timestamp fields before reading
await this.cleanupOldTimestamps(messageTypeKey, windowStart);
const messageTypeRateLimitData = await redisManager.hgetall(messageTypeKey); const messageTypeRateLimitData = await redisManager.hgetall(messageTypeKey);
let count = 0; let count = 0;

View File

@@ -16,7 +16,7 @@ import privateConfig from "#private/lib/config";
import logger from "@server/logger"; import logger from "@server/logger";
export enum AudienceIds { export enum AudienceIds {
SignUps = "6c4e77b2-0851-4bd6-bac8-f51f91360f1a", SignUps = "5cfbf99b-c592-40a9-9b8a-577a4681c158",
Subscribed = "870b43fd-387f-44de-8fc1-707335f30b20", Subscribed = "870b43fd-387f-44de-8fc1-707335f30b20",
Churned = "f3ae92bd-2fdb-4d77-8746-2118afd62549", Churned = "f3ae92bd-2fdb-4d77-8746-2118afd62549",
Newsletter = "5500c431-191c-42f0-a5d4-8b6d445b4ea0" Newsletter = "5500c431-191c-42f0-a5d4-8b6d445b4ea0"

View File

@@ -227,8 +227,6 @@ export type UserSessionWithUser = {
export const hybridRouter = Router(); export const hybridRouter = Router();
hybridRouter.use(verifySessionRemoteExitNodeMiddleware); hybridRouter.use(verifySessionRemoteExitNodeMiddleware);
// TODO: ADD RATE LIMITING TO THESE ROUTES AS NEEDED BASED ON USAGE PATTERNS
hybridRouter.get( hybridRouter.get(
"/general-config", "/general-config",
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {

View File

@@ -1,14 +1 @@
/*
* This file is part of a proprietary work.
*
* Copyright (c) 2025 Fossorial, Inc.
* All rights reserved.
*
* This file is licensed under the Fossorial Commercial License.
* You may not use this file except in compliance with the License.
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
*
* This file is not licensed under the AGPLv3.
*/
export * from "./sendSupportEmail"; export * from "./sendSupportEmail";

View File

@@ -68,7 +68,7 @@ export async function sendSupportEmail(
{ {
name: req.user?.email || "Support User", name: req.user?.email || "Support User",
to: "support@pangolin.net", to: "support@pangolin.net",
from: config.getNoReplyEmail(), from: req.user?.email || config.getNoReplyEmail(),
subject: `Support Request: ${subject}` subject: `Support Request: ${subject}`
} }
); );

View File

@@ -30,8 +30,7 @@ export const signupBodySchema = z.object({
password: passwordSchema, password: passwordSchema,
inviteToken: z.string().optional(), inviteToken: z.string().optional(),
inviteId: z.string().optional(), inviteId: z.string().optional(),
termsAcceptedTimestamp: z.string().nullable().optional(), termsAcceptedTimestamp: z.string().nullable().optional()
marketingEmailConsent: z.boolean().optional()
}); });
export type SignUpBody = z.infer<typeof signupBodySchema>; export type SignUpBody = z.infer<typeof signupBodySchema>;
@@ -56,7 +55,7 @@ export async function signup(
); );
} }
const { email, password, inviteToken, inviteId, termsAcceptedTimestamp, marketingEmailConsent } = const { email, password, inviteToken, inviteId, termsAcceptedTimestamp } =
parsedBody.data; parsedBody.data;
const passwordHash = await hashPassword(password); const passwordHash = await hashPassword(password);
@@ -221,8 +220,8 @@ export async function signup(
new Date(sess.expiresAt) new Date(sess.expiresAt)
); );
res.appendHeader("Set-Cookie", cookie); res.appendHeader("Set-Cookie", cookie);
if (build == "saas" && marketingEmailConsent) {
logger.debug(`User ${email} opted in to marketing emails during signup.`); if (build == "saas") {
moveEmailToAudience(email, AudienceIds.SignUps); moveEmailToAudience(email, AudienceIds.SignUps);
} }

View File

@@ -92,8 +92,7 @@ const formSchema = z
message: message:
"You must agree to the terms of service and privacy policy" "You must agree to the terms of service and privacy policy"
} }
), )
marketingEmailConsent: z.boolean().optional()
}) })
.refine((data) => data.password === data.confirmPassword, { .refine((data) => data.password === data.confirmPassword, {
path: ["confirmPassword"], path: ["confirmPassword"],
@@ -124,8 +123,7 @@ export default function SignupForm({
email: emailParam || "", email: emailParam || "",
password: "", password: "",
confirmPassword: "", confirmPassword: "",
agreeToTerms: false, agreeToTerms: false
marketingEmailConsent: false
}, },
mode: "onChange" // Enable real-time validation mode: "onChange" // Enable real-time validation
}); });
@@ -137,7 +135,7 @@ export default function SignupForm({
passwordValue === confirmPasswordValue; passwordValue === confirmPasswordValue;
async function onSubmit(values: z.infer<typeof formSchema>) { async function onSubmit(values: z.infer<typeof formSchema>) {
const { email, password, marketingEmailConsent } = values; const { email, password } = values;
setLoading(true); setLoading(true);
const res = await api const res = await api
@@ -146,8 +144,7 @@ export default function SignupForm({
password, password,
inviteId, inviteId,
inviteToken, inviteToken,
termsAcceptedTimestamp: termsAgreedAt, termsAcceptedTimestamp: termsAgreedAt
marketingEmailConsent: build === "saas" ? marketingEmailConsent : undefined
}) })
.catch((e) => { .catch((e) => {
console.error(e); console.error(e);
@@ -492,78 +489,56 @@ export default function SignupForm({
)} )}
/> />
{build === "saas" && ( {build === "saas" && (
<> <FormField
<FormField control={form.control}
control={form.control} name="agreeToTerms"
name="agreeToTerms" render={({ field }) => (
render={({ field }) => ( <FormItem className="flex flex-row items-center">
<FormItem className="flex flex-row items-center"> <FormControl>
<FormControl> <Checkbox
<Checkbox checked={field.value}
checked={field.value} onCheckedChange={(checked) => {
onCheckedChange={(checked) => { field.onChange(checked);
field.onChange(checked); handleTermsChange(
handleTermsChange( checked as boolean
checked as boolean );
); }}
}} />
/> </FormControl>
</FormControl> <div className="leading-none">
<div className="leading-none"> <FormLabel className="text-sm font-normal">
<FormLabel className="text-sm font-normal"> <div>
<div> {t(
"signUpTerms.IAgreeToThe"
)}{" "}
<a
href="https://pangolin.net/terms-of-service.html"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
{t( {t(
"signUpTerms.IAgreeToThe" "signUpTerms.termsOfService"
)}{" "} )}{" "}
<a </a>
href="https://pangolin.net/terms-of-service.html" {t("signUpTerms.and")}{" "}
target="_blank" <a
rel="noopener noreferrer" href="https://pangolin.net/privacy-policy.html"
className="text-primary hover:underline" target="_blank"
> rel="noopener noreferrer"
{t( className="text-primary hover:underline"
"signUpTerms.termsOfService" >
)}{" "} {t(
</a> "signUpTerms.privacyPolicy"
{t("signUpTerms.and")}{" "} )}
<a </a>
href="https://pangolin.net/privacy-policy.html" </div>
target="_blank" </FormLabel>
rel="noopener noreferrer" <FormMessage />
className="text-primary hover:underline" </div>
> </FormItem>
{t( )}
"signUpTerms.privacyPolicy" />
)}
</a>
</div>
</FormLabel>
<FormMessage />
</div>
</FormItem>
)}
/>
<FormField
control={form.control}
name="marketingEmailConsent"
render={({ field }) => (
<FormItem className="flex flex-row items-start">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className="leading-none">
<FormLabel className="text-sm font-normal">
{t("signUpMarketing.keepMeInTheLoop")}
</FormLabel>
<FormMessage />
</div>
</FormItem>
)}
/>
</>
)} )}
{error && ( {error && (