diff --git a/package-lock.json b/package-lock.json index 85e544c..c318bab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,14 +8,15 @@ "name": "mistymountains-frontend", "version": "0.0.1", "dependencies": { - "@sendgrid/mail": "^7.6.2", "@supabase/supabase-js": "^1.33.3", - "dotenv": "^16.0.0" + "dotenv": "^16.0.0", + "nodemailer": "^6.9.8" }, "devDependencies": { "@sveltejs/adapter-auto": "next", "@sveltejs/adapter-node": "^1.0.0-next.73", "@sveltejs/kit": "next", + "@types/nodemailer": "^6.4.14", "@typescript-eslint/eslint-plugin": "^5.10.1", "@typescript-eslint/parser": "^5.10.1", "@zerodevx/svelte-toast": "^0.7.1", @@ -239,41 +240,6 @@ "node": ">= 8.0.0" } }, - "node_modules/@sendgrid/client": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/@sendgrid/client/-/client-7.6.2.tgz", - "integrity": "sha512-Yw3i3vPBBwfiIi+4i7+1f1rwQoLlLsu3qW16d1UuRp6RgX6H6yHYb2/PfqwNyCC0qzqIWGUKPWwYe5ggcr5Guw==", - "dependencies": { - "@sendgrid/helpers": "^7.6.2", - "axios": "^0.26.0" - }, - "engines": { - "node": "6.* || 8.* || >=10.*" - } - }, - "node_modules/@sendgrid/helpers": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/@sendgrid/helpers/-/helpers-7.6.2.tgz", - "integrity": "sha512-kGW0kM2AOHfXjcvB6Lgwa/nMv8IALu0KyNY9X4HSa3MtLohymuhbG9HgjrOh66+BkbsfA03H3bcT0+sPVJ0GKQ==", - "dependencies": { - "deepmerge": "^4.2.2" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/@sendgrid/mail": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/@sendgrid/mail/-/mail-7.6.2.tgz", - "integrity": "sha512-IHHZFvgU95aqb11AevQvAfautj2pb8iW8UCiUJ2ae9pRF37e6EkBmU9NgdFjbQ/8Xhhm+KDVDzn/JLxDN/GiBw==", - "dependencies": { - "@sendgrid/client": "^7.6.2", - "@sendgrid/helpers": "^7.6.2" - }, - "engines": { - "node": "6.* || 8.* || >=10.*" - } - }, "node_modules/@supabase/functions-js": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-1.3.3.tgz", @@ -435,6 +401,15 @@ "integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==", "dev": true }, + "node_modules/@types/nodemailer": { + "version": "6.4.14", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.14.tgz", + "integrity": "sha512-fUWthHO9k9DSdPCSPRqcu6TWhYyxTBg382vlNIttSe9M7XfsT06y0f24KHXtbnijPGGRIcVvdKHTNikOI6qiHA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/phoenix": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.5.4.tgz", @@ -755,14 +730,6 @@ "node": ">=8" } }, - "node_modules/axios": { - "version": "0.26.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", - "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", - "dependencies": { - "follow-redirects": "^1.14.8" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -951,14 +918,6 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, - "node_modules/deepmerge": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", - "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/detect-indent": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", @@ -1807,25 +1766,6 @@ "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", "dev": true }, - "node_modules/follow-redirects": { - "version": "1.14.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", - "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -2317,6 +2257,14 @@ "node-gyp-build-test": "build-test.js" } }, + "node_modules/nodemailer": { + "version": "6.9.8", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.8.tgz", + "integrity": "sha512-cfrYUk16e67Ks051i4CntM9kshRYei1/o/Gi8K1d+R34OIs21xdFnW7Pt7EucmVKA0LKtqUGNcjMZ7ehjl49mQ==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -3493,32 +3441,6 @@ "picomatch": "^2.2.2" } }, - "@sendgrid/client": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/@sendgrid/client/-/client-7.6.2.tgz", - "integrity": "sha512-Yw3i3vPBBwfiIi+4i7+1f1rwQoLlLsu3qW16d1UuRp6RgX6H6yHYb2/PfqwNyCC0qzqIWGUKPWwYe5ggcr5Guw==", - "requires": { - "@sendgrid/helpers": "^7.6.2", - "axios": "^0.26.0" - } - }, - "@sendgrid/helpers": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/@sendgrid/helpers/-/helpers-7.6.2.tgz", - "integrity": "sha512-kGW0kM2AOHfXjcvB6Lgwa/nMv8IALu0KyNY9X4HSa3MtLohymuhbG9HgjrOh66+BkbsfA03H3bcT0+sPVJ0GKQ==", - "requires": { - "deepmerge": "^4.2.2" - } - }, - "@sendgrid/mail": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/@sendgrid/mail/-/mail-7.6.2.tgz", - "integrity": "sha512-IHHZFvgU95aqb11AevQvAfautj2pb8iW8UCiUJ2ae9pRF37e6EkBmU9NgdFjbQ/8Xhhm+KDVDzn/JLxDN/GiBw==", - "requires": { - "@sendgrid/client": "^7.6.2", - "@sendgrid/helpers": "^7.6.2" - } - }, "@supabase/functions-js": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-1.3.3.tgz", @@ -3658,6 +3580,15 @@ "integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==", "dev": true }, + "@types/nodemailer": { + "version": "6.4.14", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.14.tgz", + "integrity": "sha512-fUWthHO9k9DSdPCSPRqcu6TWhYyxTBg382vlNIttSe9M7XfsT06y0f24KHXtbnijPGGRIcVvdKHTNikOI6qiHA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/phoenix": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.5.4.tgz", @@ -3856,14 +3787,6 @@ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true }, - "axios": { - "version": "0.26.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", - "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", - "requires": { - "follow-redirects": "^1.14.8" - } - }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -4005,11 +3928,6 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, - "deepmerge": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", - "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==" - }, "detect-indent": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", @@ -4564,11 +4482,6 @@ "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", "dev": true }, - "follow-redirects": { - "version": "1.14.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", - "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==" - }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -4932,6 +4845,11 @@ "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.4.0.tgz", "integrity": "sha512-amJnQCcgtRVw9SvoebO3BKGESClrfXGCUTX9hSn1OuGQTQBOZmVd0Z0OlecpuRksKvbsUqALE8jls/ErClAPuQ==" }, + "nodemailer": { + "version": "6.9.8", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.8.tgz", + "integrity": "sha512-cfrYUk16e67Ks051i4CntM9kshRYei1/o/Gi8K1d+R34OIs21xdFnW7Pt7EucmVKA0LKtqUGNcjMZ7ehjl49mQ==" + }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", diff --git a/package.json b/package.json index faebd16..c8e9f82 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@sveltejs/adapter-auto": "next", "@sveltejs/adapter-node": "^1.0.0-next.73", "@sveltejs/kit": "next", + "@types/nodemailer": "^6.4.14", "@typescript-eslint/eslint-plugin": "^5.10.1", "@typescript-eslint/parser": "^5.10.1", "@zerodevx/svelte-toast": "^0.7.1", @@ -34,8 +35,8 @@ }, "type": "module", "dependencies": { - "@sendgrid/mail": "^7.6.2", "@supabase/supabase-js": "^1.33.3", - "dotenv": "^16.0.0" + "dotenv": "^16.0.0", + "nodemailer": "^6.9.8" } } diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts new file mode 100644 index 0000000..61d383d --- /dev/null +++ b/src/lib/utils/index.ts @@ -0,0 +1,3 @@ +export * from './setImageUrl'; +export * from './mailer'; +export * from './retry'; diff --git a/src/lib/utils/mailer.ts b/src/lib/utils/mailer.ts new file mode 100644 index 0000000..e5edd74 --- /dev/null +++ b/src/lib/utils/mailer.ts @@ -0,0 +1,61 @@ +import * as nodemailer from 'nodemailer'; +import { continueRetryUntilValidation } from './retry'; + +export interface Mailer { + sendMail(to: string, subject: string, message: string): Promise; +} + +export class MistyMountainsMailer implements Mailer { + private from: string; + private domain: string; + private username: string; + private password: string; + private port: number; + + private transporter: nodemailer.Transporter; + + constructor(username: string, password: string, from: string, domain: string, port: number) { + this.from = from; + this.username = username; + this.password = password; + this.domain = domain; + this.port = port; + + this.transporter = nodemailer.createTransport({ + host: this.domain, + port: this.port, + auth: { + user: this.username, + pass: this.password + }, + requireTLS: true, + tls: { + rejectUnauthorized: true + } + }); + } + + public async sendMail(to: string, subject: string, message: string) { + const mail = { + from: this.from, + subject, + html: message, + to + }; + + return !!(await continueRetryUntilValidation(async () => { + const { messageId } = await this.transporter.sendMail(mail); + return messageId; + })); + } +} + +export const EnvMistyMountainsMailerFactory = () => { + return new MistyMountainsMailer( + process.env.SMTP_USERNAME, + process.env.SMTP_PASSWORD, + process.env.FROM_EMAIL, + process.env.SMTP_SERVER, + Number(process.env.SMTP_PORT) + ); +}; diff --git a/src/lib/utils/retry.ts b/src/lib/utils/retry.ts new file mode 100644 index 0000000..34bc73b --- /dev/null +++ b/src/lib/utils/retry.ts @@ -0,0 +1,45 @@ +export type RetryStrategyF = (retries: number) => number; + +export const MAX_DEFAULT_RETRY_AMOUNT = 5; +export const WAIT_MS = 1_000; +export const RETRY_EXPONENT = 2; +export const RETRY_EXPONENTIAL_FACTOR = 1.1; +export const RETRY_JITTER_MAX = 3_000; + +const waitFor = (ms: number) => new Promise((res) => setTimeout(res, ms)); + +const exponentialStrategyWithJitter: RetryStrategyF = (retries: number) => + WAIT_MS * Math.pow(RETRY_EXPONENT, RETRY_EXPONENTIAL_FACTOR * retries) + + RETRY_JITTER_MAX * Math.random(); + +export const continueRetryUntilValidation = async ( + promiseFn: () => Promise | T | Promise | void, + validationFn: (x: T) => Promise | boolean = (x: T) => + Promise.resolve(!!x), + maxRetries = MAX_DEFAULT_RETRY_AMOUNT, + waitTimeStrategy: RetryStrategyF = exponentialStrategyWithJitter, + retries = 0, + lastError: undefined | unknown = undefined +): Promise => { + if (retries >= maxRetries) { + if (lastError) throw lastError; + throw new Error("Exceeded maximum retry amount"); + } + try { + if (retries) await waitFor(waitTimeStrategy(retries)); + + const res = await promiseFn(); + if (res && (await validationFn(res))) return res; + + throw new Error("Validation predicate unsuccessful"); + } catch (e: unknown) { + return continueRetryUntilValidation( + promiseFn, + validationFn, + maxRetries, + waitTimeStrategy, + retries + 1, + e + ); + } +}; \ No newline at end of file diff --git a/src/lib/utils.ts b/src/lib/utils/setImageUrl.ts similarity index 66% rename from src/lib/utils.ts rename to src/lib/utils/setImageUrl.ts index 5586b47..15566a8 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils/setImageUrl.ts @@ -1,6 +1,6 @@ -import { supabase } from './supabase'; +import { supabase } from '../supabase'; -const setImageUrl = (imageSpec) => { +export const setImageUrl = (imageSpec) => { const { publicURL, error } = supabase .storage .from('mistymountains') @@ -10,5 +10,3 @@ const setImageUrl = (imageSpec) => { } return imageSpec; } - -export default setImageUrl; \ No newline at end of file diff --git a/src/routes/contact/submit.js b/src/routes/contact/submit.js index da1ab59..24f4559 100644 --- a/src/routes/contact/submit.js +++ b/src/routes/contact/submit.js @@ -1,58 +1,56 @@ import 'dotenv/config'; -import sgMail from '@sendgrid/mail'; -sgMail.setApiKey(process.env.SENDGRID_API_KEY); +import { EnvMistyMountainsMailerFactory } from '$lib/utils'; export async function post({ request }) { - const body = await request.json(); - const { HCAPTCHA_SECRET, FORM_FROM_EMAIL, FORM_TO_EMAIL } = process.env; + const body = await request.json(); + const { HCAPTCHA_SECRET, FORM_TO_EMAIL } = process.env; + const mailer = EnvMistyMountainsMailerFactory(); - const captchaVerified = await fetch(`https://hcaptcha.com/siteverify?response=${body.captchaToken}&secret=${HCAPTCHA_SECRET}`, { - method: 'POST', - }) - .then((res) => res.json()) - .then((json) => json.success) - .catch(() => false); + const captchaVerified = await fetch( + `https://hcaptcha.com/siteverify?response=${body.captchaToken}&secret=${HCAPTCHA_SECRET}`, + { + method: 'POST' + } + ) + .then((res) => res.json()) + .then((json) => json.success) + .catch(() => false); - if (!captchaVerified) { - return { - statusCode: 400, - body: { - error: 'Captcha verification failed', - }, - }; - } + if (!captchaVerified) { + return { + statusCode: 400, + body: { + error: 'Captcha verification failed' + } + }; + } - const msg = { - to: FORM_TO_EMAIL, - from: FORM_FROM_EMAIL, - subject: `Form Submission from ${body.name}`, - text: ` - Name: ${body.name} - Phone Number: ${body.phone || "Not Given"} - Email: ${body.email} - Message: ${body.message} - `, - }; + const text = `Name: ${body.name} +Phone Number: ${body.phone || 'Not Given'} +Email: ${body.email} +Message: ${body.message} +`; - const messageSent = await sgMail.send(msg) - .then(() => true) - .catch((error) => { - console.error(error); - return false; - }); + const messageSent = await mailer + .sendMail(FORM_TO_EMAIL, `Form Submission from ${body.name}`, text) + .then(() => true) + .catch((error) => { + console.error(error); + return false; + }); - if (!messageSent) { - return { - statusCode: 500, - body: { - error: 'Message could not be sent', - }, - }; - } + if (!messageSent) { + return { + statusCode: 500, + body: { + error: 'Message could not be sent' + } + }; + } - return { - body: { - success: true, - }, - }; + return { + body: { + success: true + } + }; } diff --git a/src/routes/index.svelte b/src/routes/index.svelte index 55eb4ac..146dbb6 100644 --- a/src/routes/index.svelte +++ b/src/routes/index.svelte @@ -1,73 +1,82 @@ + +
-
- console.log('slide change')} - on:swiper={(e) => console.log(e.detail[0])} - > - {#each images as { image, alt } (image)} - - {alt} - - {/each} - -
-
-

Helping you conquer Mount Doom

-

FREE 15 Minute Consultation

-
+
+ console.log('slide change')} + on:swiper={(e) => console.log(e.detail[0])} + > + {#each images as { image, alt } (image)} + + + + {/each} + +
+
+

Helping you conquer Mount Doom

+

FREE 15 Minute Consultation

+
-
-
-

"Darkness must pass, a new day will come, and when the sun shines, it will shine out the clearer."

-
- - Samwise Gamgee -
-
-

- Misty Mountains Therapy is a privately owned, high quality, specialty therapy clinic, founded in January 2020 by Jefferson Hunt. We are dedicated to providing comprehensive therapy evaluation and treatment services to children and adults for a wide variety of disorders in the most efficient and effective manner possible in the Rexburg area. We believe that therapy should be fun, engaging, and most importantly, useful to our clients. -

-

- We are currently accepting new clients and offer a variety of services to help you live the life you desire. To find the right fit for you, schedule a free 15-minute consultation. -

-
-
+
+
+

+ "Darkness must pass, a new day will come, and when the sun shines, it will shine out the + clearer." +

+
- Samwise Gamgee
+
+

+ Misty Mountains Therapy is a privately owned, high quality, specialty therapy clinic, + founded in January 2020 by Jefferson Hunt. We are dedicated to providing comprehensive + therapy evaluation and treatment services to children and adults for a wide variety of + disorders in the most efficient and effective manner possible in the Rexburg area. We + believe that therapy should be fun, engaging, and most importantly, useful to our clients. +

+

+ We are currently accepting new clients and offer a variety of services to help you live the + life you desire. To find the right fit for you, schedule a free 15-minute consultation. +

+
+
-
-
-
- We do not have a crisis line. If you or someone you know is in danger please call 911, visit your nearest emergency room, call the National Suicide Prevention Lifeline for free crisis counseling at (800)273-TALK (8255), or text HELLO to 741-741. -
-
-
-
+
+
+
+ We do not have a crisis line. If you or someone you know is in danger please call 911, visit + your nearest emergency room, call the National Suicide Prevention Lifeline for free crisis + counseling at (800)273-TALK (8255), or text HELLO to 741-741. +
+
+
+
- - diff --git a/src/routes/team/index.svelte b/src/routes/team/index.svelte index 04c62b9..88e5db0 100644 --- a/src/routes/team/index.svelte +++ b/src/routes/team/index.svelte @@ -1,35 +1,38 @@
-

Our Team

- {#if people.length} - {#each people as person, i} -
- -

{person.name}, {person.position}

-

{person.bio}

-

{person.email}

-
-
- {/each} - {/if} +

Our Team

+ {#if people.length} + {#each people as person, i} +
+ +

{person.name}, {person.position}

+

{person.bio}

+

{person.email}

+
+
+ {/each} + {/if}