diff --git a/.env b/.env index ac58967..a706777 100644 --- a/.env +++ b/.env @@ -1,2 +1,7 @@ -VITE_APP_VERSION=1.0.0-alpha -DATABASE_URL="postgres://hestia:test123@localhost:5432/hestia" +# TWILIO +TWILIO_ACCOUNT_SID= +TWILIO_AUTH_TOKEN= +TWILIO_PHONE_NUMBER= + +# PRISMA +DATABASE_URL="postgres://hestia:test123@localhost:5432/hestia" \ No newline at end of file diff --git a/bun.lockb b/bun.lockb index c322195..4d06590 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/messages/en.json b/messages/en.json index c2a811e..0deea6c 100644 --- a/messages/en.json +++ b/messages/en.json @@ -13,5 +13,9 @@ "login_error_email_incorrect": "Email is incorrect", "login_error_password_incorrect": "Password is incorrect", "login_error_email_inuse": "Email is already in use", - "login_error_user_not_found": "The user could not be found" + "login_error_user_not_found": "The user could not be found", + "sms_prompt": "Send a Message", + "sms_label_phone": "Phone Number", + "sms_label_message": "Message", + "sms_button_submit": "Send Message" } diff --git a/package.json b/package.json index 8ce2687..307fe62 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.0.1", "type": "module", "scripts": { - "dev": "bun database:up && bun prisma:push && vite dev", + "dev": "bun validate-env && bun database:up && bun prisma:push && vite dev", "build": "vite build", "build-storybook": "storybook build", "database:up": "docker compose -p hestia -f devops/docker-compose.dev.yml up -d && docker compose -p hestia -f devops/docker-compose.dev.yml -f devops/docker-compose.wait.yml run --rm wait -c hestia-database:5432", @@ -24,7 +24,8 @@ "prisma:reset": "prisma migrate reset --force", "prisma:studio": "prisma studio", "prisma:validate": "prisma validate", - "prepare": "husky" + "prepare": "husky", + "validate-env": "bun ./scripts/validate-env.ts" }, "devDependencies": { "@chromatic-com/storybook": "^3.2.2", @@ -80,6 +81,7 @@ "pino": "^9.5.0", "pino-pretty": "^13.0.0", "tailwind-merge": "^2.5.5", + "twilio": "^5.4.0", "zod": "^3.24.0" } } diff --git a/scripts/validate-env.ts b/scripts/validate-env.ts new file mode 100644 index 0000000..da28326 --- /dev/null +++ b/scripts/validate-env.ts @@ -0,0 +1,19 @@ +import { PhoneRegex } from '../src/lib/regex/phone'; +import { z } from 'zod'; + +const ValidateEnvironment = () => { + const { success, error } = z + .object({ + TWILIO_ACCOUNT_SID: z.string().min(1), + TWILIO_AUTH_TOKEN: z.string().min(1), + TWILIO_PHONE_NUMBER: z.string().regex(PhoneRegex), + }) + .safeParse(process.env); + + if (!success) { + console.error(error.message); + process.exit(1); + } +}; + +ValidateEnvironment(); diff --git a/src/app.d.ts b/src/app.d.ts index 7cce717..c8414b8 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -1,4 +1,5 @@ // See https://svelte.dev/docs/kit/types#app.d.ts + // for information about these interfaces declare global { namespace App { diff --git a/src/lib/components/common/Button/Button.stories.svelte b/src/lib/components/Actions/Button.stories.svelte similarity index 65% rename from src/lib/components/common/Button/Button.stories.svelte rename to src/lib/components/Actions/Button.stories.svelte index d182565..abd7504 100644 --- a/src/lib/components/common/Button/Button.stories.svelte +++ b/src/lib/components/Actions/Button.stories.svelte @@ -1,7 +1,8 @@ - +{#snippet template({ children: _, ...props }: Partial>)} + +{/snippet} + + diff --git a/src/lib/components/common/Button/Button.svelte b/src/lib/components/Actions/Button.svelte similarity index 89% rename from src/lib/components/common/Button/Button.svelte rename to src/lib/components/Actions/Button.svelte index e7c4391..f550841 100644 --- a/src/lib/components/common/Button/Button.svelte +++ b/src/lib/components/Actions/Button.svelte @@ -1,12 +1,14 @@ + + + + diff --git a/src/lib/components/DataInput/Textarea.stories.svelte b/src/lib/components/DataInput/Textarea.stories.svelte new file mode 100644 index 0000000..8aa4d9b --- /dev/null +++ b/src/lib/components/DataInput/Textarea.stories.svelte @@ -0,0 +1,51 @@ + + +{#snippet template(props: ComponentProps)} + + diff --git a/src/lib/components/DataInput/index.ts b/src/lib/components/DataInput/index.ts new file mode 100644 index 0000000..e678a3a --- /dev/null +++ b/src/lib/components/DataInput/index.ts @@ -0,0 +1,2 @@ +export { default as Textarea } from './Textarea.svelte'; +export { default as TextInput } from './TextInput.svelte'; diff --git a/src/lib/components/common/Button/index.ts b/src/lib/components/common/Button/index.ts deleted file mode 100644 index 14aed5d..0000000 --- a/src/lib/components/common/Button/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import Button from './Button.svelte'; - -export default Button; diff --git a/src/lib/components/common/TextInput/TextInput.svelte b/src/lib/components/common/TextInput/TextInput.svelte deleted file mode 100644 index affbd2c..0000000 --- a/src/lib/components/common/TextInput/TextInput.svelte +++ /dev/null @@ -1,68 +0,0 @@ - - - - - diff --git a/src/lib/components/common/TextInput/index.ts b/src/lib/components/common/TextInput/index.ts deleted file mode 100644 index 89470e9..0000000 --- a/src/lib/components/common/TextInput/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import TextInput from './TextInput.svelte'; - -export default TextInput; diff --git a/src/lib/regex/index.ts b/src/lib/regex/index.ts new file mode 100644 index 0000000..c0158b2 --- /dev/null +++ b/src/lib/regex/index.ts @@ -0,0 +1 @@ +export * from './phone'; diff --git a/src/lib/regex/phone.ts b/src/lib/regex/phone.ts new file mode 100644 index 0000000..7491632 --- /dev/null +++ b/src/lib/regex/phone.ts @@ -0,0 +1 @@ +export const PhoneRegex = /^\+?\d?(\d{3}\d{3}\d{4}|\d{3}-\d{3}-\d{4})$/; diff --git a/src/lib/server/config/index.ts b/src/lib/server/config/index.ts deleted file mode 100644 index 0239640..0000000 --- a/src/lib/server/config/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { logger } from '$lib/server/logger'; -import { z } from 'zod'; - -export interface Configuration { - app_version: string; -} - -export const LoadConfig = (): Configuration => { - const { success, data, error } = z - .object({ - VITE_APP_VERSION: z.string().default('development'), - }) - .safeParse(import.meta.env); - - if (!success) { - logger.error(error.message); - } - - return { - app_version: data!.VITE_APP_VERSION, - }; -}; - -export const Config = LoadConfig(); diff --git a/src/lib/server/pothos/schema/index.ts b/src/lib/server/pothos/schema/index.ts index 84e199f..b0d89b6 100644 --- a/src/lib/server/pothos/schema/index.ts +++ b/src/lib/server/pothos/schema/index.ts @@ -1,3 +1,4 @@ +import { version } from '$app/environment'; import { builder } from '../builder'; builder.queryType({}); @@ -5,7 +6,7 @@ builder.queryType({}); builder.queryField('version', (t) => t.string({ description: 'Application version', - resolve: (parent, args, context) => context.config.app_version, + resolve: () => version, }) ); diff --git a/src/lib/server/twilio/client.ts b/src/lib/server/twilio/client.ts new file mode 100644 index 0000000..40bdee2 --- /dev/null +++ b/src/lib/server/twilio/client.ts @@ -0,0 +1,4 @@ +import { TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN } from '$env/static/private'; +import twilio from 'twilio'; + +export const TwilioClient = twilio(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN); diff --git a/src/lib/server/twilio/index.ts b/src/lib/server/twilio/index.ts new file mode 100644 index 0000000..4f1cce4 --- /dev/null +++ b/src/lib/server/twilio/index.ts @@ -0,0 +1 @@ +export * from './client'; diff --git a/src/lib/server/yoga/context.ts b/src/lib/server/yoga/context.ts index e7eff70..76bb9eb 100644 --- a/src/lib/server/yoga/context.ts +++ b/src/lib/server/yoga/context.ts @@ -1,7 +1,6 @@ -import { Config } from '$lib/server/config'; import type { YogaInitialContext } from 'graphql-yoga'; export const Context = (initialContext: YogaInitialContext) => ({ ...initialContext, - config: Config, + config: {}, }); diff --git a/src/routes/app/+page.svelte b/src/routes/app/+page.svelte index 0fbba99..e3cf09f 100644 --- a/src/routes/app/+page.svelte +++ b/src/routes/app/+page.svelte @@ -1,2 +1,4 @@ + +SMS diff --git a/src/routes/app/sms/+page.server.ts b/src/routes/app/sms/+page.server.ts new file mode 100644 index 0000000..b447710 --- /dev/null +++ b/src/routes/app/sms/+page.server.ts @@ -0,0 +1,49 @@ +import { TWILIO_PHONE_NUMBER } from '$env/static/private'; +import { PhoneRegex } from '$lib/regex'; +import { logger } from '$lib/server/logger'; +import { TwilioClient } from '$lib/server/twilio'; +import { fail, type Actions } from '@sveltejs/kit'; +import zod from 'zod'; + +export const actions = { + push: async (event) => { + const form = await event.request.formData(); + + if (!form.has('phone')) { + return fail(400, { error: 'phone_missing' }); + } + if (!form.get('message')) { + return fail(400, { error: 'message_missing' }); + } + + const { + success: phoneSuccess, + data: phone, + error: phoneError, + } = zod.string().regex(PhoneRegex).safeParse(form.get('phone')); + if (!phoneSuccess) { + logger.error(phoneError); + return fail(400, { error: 'invalid_phone' }); + } + + const message = form.get('message'); + if (typeof message !== 'string') { + return fail(400, { error: 'invalid_message' }); + } + + try { + const result = await TwilioClient.messages.create({ + to: phone, + body: message, + from: TWILIO_PHONE_NUMBER, + }); + logger.debug(result); + } catch (e) { + logger.error(e); + fail(500, { success: false }); + } + return { + success: true, + }; + }, +} satisfies Actions; diff --git a/src/routes/app/sms/+page.svelte b/src/routes/app/sms/+page.svelte new file mode 100644 index 0000000..16d4a90 --- /dev/null +++ b/src/routes/app/sms/+page.svelte @@ -0,0 +1,68 @@ + + +{#snippet PhoneLabel()} + {messages.sms_label_phone()} +{/snippet} + +{#snippet MessageLabel()} + {messages.sms_label_message()} +{/snippet} + +{#snippet alert(message: string)} + +{/snippet} + +
+
+
+

{messages.sms_prompt()}

+ {#if form?.error} + {@render alert(form.error)} + {/if} +
+
+
+ +