diff --git a/.env.example b/.env.example index 79cb895..4ab3dbc 100644 --- a/.env.example +++ b/.env.example @@ -11,4 +11,6 @@ VITE_BREVO_API_KEY="" # Default sender email for system notifications VITE_BREVO_SENDER_EMAIL="noreply@example.com" # Display name for the sender email -VITE_BREVO_SENDER_NAME="No Reply" \ No newline at end of file +VITE_BREVO_SENDER_NAME="No Reply" +# Contact email for the contact page form to send to +VITE_CONTACT_EMAIL="" \ No newline at end of file diff --git a/src/lib/utils/rateLimiter.ts b/src/lib/utils/rateLimiter.ts new file mode 100644 index 0000000..1328b24 --- /dev/null +++ b/src/lib/utils/rateLimiter.ts @@ -0,0 +1,30 @@ +type RateLimitStore = { + [key: string]: { count: number; lastRequest: number }; +}; + +const rateLimitStore: RateLimitStore = {}; +const RATE_LIMIT_WINDOW = 60 * 5000; // 5 minutes +const MAX_REQUESTS = 5; + +export function isRateLimited(ip: string): boolean { + const currentTime = Date.now(); + const requestInfo = rateLimitStore[ip]; + + if (!requestInfo) { + rateLimitStore[ip] = { count: 1, lastRequest: currentTime }; + return false; + } + + if (currentTime - requestInfo.lastRequest > RATE_LIMIT_WINDOW) { + rateLimitStore[ip] = { count: 1, lastRequest: currentTime }; + return false; + } + + if (requestInfo.count >= MAX_REQUESTS) { + return true; + } + + rateLimitStore[ip].count += 1; + rateLimitStore[ip].lastRequest = currentTime; + return false; +} diff --git a/src/routes/account/+page.svelte b/src/routes/account/+page.svelte index 88fc742..1c1e4fb 100644 --- a/src/routes/account/+page.svelte +++ b/src/routes/account/+page.svelte @@ -34,7 +34,7 @@ } else { alert('Please sign in to delete your account.'); } - + return async () => { loading = false; }; @@ -184,12 +184,11 @@ Provide Feedback - - -
-
- -
-
+
+
+ +
+
+ diff --git a/src/routes/contact/+page.server.ts b/src/routes/contact/+page.server.ts new file mode 100644 index 0000000..150a908 --- /dev/null +++ b/src/routes/contact/+page.server.ts @@ -0,0 +1,38 @@ +import type { Actions } from '@sveltejs/kit'; +import { fail } from '@sveltejs/kit'; +import { sendEmail } from '$lib/utils/brevo/email'; +import { VITE_CONTACT_EMAIL } from '$env/static/private'; +import logger from '$lib/utils/logger/logger'; +import { isRateLimited } from '$lib/utils/rateLimiter'; + +export const actions: Actions = { + default: async ({ request }) => { + const ip = + request.headers.get('x-forwarded-for') ?? request.headers.get('remote-addr') ?? 'unknown'; + + if (isRateLimited(ip)) { + return fail(429, { error: 'Too many requests. Please try again later.' }); + } + + const formData = await request.formData(); + const name = formData.get('name') as string; + const email = formData.get('email') as string; + const message = formData.get('message') as string; + + const emailContent = ` +

You have a new contact form submission:

+

Name: ${name}

+

Email: ${email}

+

Message:

+

${message}

+ `; + + try { + await sendEmail('New Contact Form Submission', emailContent, VITE_CONTACT_EMAIL); + return { success: true }; + } catch (error) { + logger.error('Failed to send email:', error); + return fail(400, { error }); + } + } +}; diff --git a/src/routes/contact/+page.svelte b/src/routes/contact/+page.svelte new file mode 100644 index 0000000..ef2e5a8 --- /dev/null +++ b/src/routes/contact/+page.svelte @@ -0,0 +1,103 @@ + + +{#if submitted} +
+
+ Thank you for your message, {name}! +
+
+{:else} +
+

Contact Us

+ {#if error} +
+
+ {error} +
+
+ {/if} +
+ + +
+ +
+ + +
+ +
+ + + Maximum 1000 characters +
+ +
+ +
+
+{/if}