From 58842c6f1ec8d1bf79c4664f744ca2e5747292d5 Mon Sep 17 00:00:00 2001 From: Josh Creek <8179928+jcreek@users.noreply.github.com> Date: Sun, 10 Nov 2024 19:31:23 +0000 Subject: [PATCH 1/6] feat(#79): Create contact page --- .env.example | 4 +- src/routes/contact/+page.server.ts | 30 +++++++++++ src/routes/contact/+page.svelte | 87 ++++++++++++++++++++++++++++++ 3 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 src/routes/contact/+page.server.ts create mode 100644 src/routes/contact/+page.svelte 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/routes/contact/+page.server.ts b/src/routes/contact/+page.server.ts new file mode 100644 index 0000000..7073ebc --- /dev/null +++ b/src/routes/contact/+page.server.ts @@ -0,0 +1,30 @@ +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'; + +export const actions: Actions = { + default: async ({ request }) => { + 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..271ffec --- /dev/null +++ b/src/routes/contact/+page.svelte @@ -0,0 +1,87 @@ + + +{#if submitted} +
+
+ Thank you for your message, {name}! +
+
+{:else} +
+ {#if error} +
+
+ {error} +
+
+ {/if} +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+{/if} From 4137f85190025eb3a51cbd40122a5944e91b6f48 Mon Sep 17 00:00:00 2001 From: Josh Creek <8179928+jcreek@users.noreply.github.com> Date: Sun, 10 Nov 2024 19:33:32 +0000 Subject: [PATCH 2/6] fix(#79): Add check for result.data --- src/routes/contact/+page.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/contact/+page.svelte b/src/routes/contact/+page.svelte index 271ffec..0c7bdde 100644 --- a/src/routes/contact/+page.svelte +++ b/src/routes/contact/+page.svelte @@ -10,7 +10,7 @@ const handleSubmit: SubmitFunction = ({}) => { return async ({ result, update }) => { - if (result.data.success) { + if (result.data && result.data.success) { submitted = true; } else { error = result.error?.message || 'Form submission failed'; From 6cdc0f0bdec24a90e8ca2bb3f2e6804b47764933 Mon Sep 17 00:00:00 2001 From: Josh Creek <8179928+jcreek@users.noreply.github.com> Date: Sun, 10 Nov 2024 19:40:59 +0000 Subject: [PATCH 3/6] build(*): Fix broken closing tag preventing build --- src/routes/account/+page.svelte | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) 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 - - -
-
- -
-
+
+
+ +
+
+ From 829d8cc031a1fb684ddab089ace71c1a74c48501 Mon Sep 17 00:00:00 2001 From: Josh Creek <8179928+jcreek@users.noreply.github.com> Date: Sun, 10 Nov 2024 19:45:40 +0000 Subject: [PATCH 4/6] feat(#79): Add rate limiting to contact page form --- src/lib/utils/rateLimiter.ts | 30 ++++++++++++++++++++++++++++++ src/routes/contact/+page.server.ts | 8 ++++++++ 2 files changed, 38 insertions(+) create mode 100644 src/lib/utils/rateLimiter.ts 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/contact/+page.server.ts b/src/routes/contact/+page.server.ts index 7073ebc..150a908 100644 --- a/src/routes/contact/+page.server.ts +++ b/src/routes/contact/+page.server.ts @@ -3,9 +3,17 @@ 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; From 3b1ffb21a67c7b17fd1423beea21e0f616a1fb27 Mon Sep 17 00:00:00 2001 From: Josh Creek <8179928+jcreek@users.noreply.github.com> Date: Sun, 10 Nov 2024 19:55:57 +0000 Subject: [PATCH 5/6] feat(#79): Enhance form security and accessibility --- src/routes/contact/+page.svelte | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/routes/contact/+page.svelte b/src/routes/contact/+page.svelte index 0c7bdde..3ad77a8 100644 --- a/src/routes/contact/+page.svelte +++ b/src/routes/contact/+page.svelte @@ -31,7 +31,9 @@ action="?" use:enhance={handleSubmit} class="max-w-lg mx-auto p-4 bg-base-200 rounded-lg shadow-md" + aria-labelledby="contact-form-title" > +

Contact Us

{#if error}
@@ -41,7 +43,7 @@ {/if}
+ Maximum 1000 characters
From c3125a0db1051cd87d82d282f8b5b37fa9774063 Mon Sep 17 00:00:00 2001 From: Josh Creek <8179928+jcreek@users.noreply.github.com> Date: Sun, 10 Nov 2024 19:58:50 +0000 Subject: [PATCH 6/6] feat(#79): Handle loading state --- src/routes/contact/+page.svelte | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/routes/contact/+page.svelte b/src/routes/contact/+page.svelte index 3ad77a8..ef2e5a8 100644 --- a/src/routes/contact/+page.svelte +++ b/src/routes/contact/+page.svelte @@ -8,13 +8,17 @@ let submitted = false; let error = ''; - const handleSubmit: SubmitFunction = ({}) => { + const handleSubmit: SubmitFunction = () => { + let loading = false; return async ({ result, update }) => { + loading = true; if (result.data && result.data.success) { submitted = true; } else { error = result.error?.message || 'Form submission failed'; } + loading = false; + await update(); }; };