41 create tenant twilio config #62

Merged
BenjaminPalko merged 20 commits from 41-create-tenant-twilio-config into master 2025-01-26 23:36:06 -05:00
4 changed files with 194 additions and 3 deletions
Showing only changes of commit c57923c29c - Show all commits

View file

@ -21,5 +21,11 @@
"sms_label_phone": "Phone Number",
"sms_label_message": "Message",
"sms_button_submit": "Send Message",
"settings_title": "Settings",
"settings_category_twilio": "Twilio Config",
"settings_twilio_account_sid": "Account SID",
"settings_twilio_auth_token": "Auth Token",
"settings_twilio_phone_number": "Phone Number",
"settings_save": "Save Settings",
"error_page_go_home": "Go Home"
}
}

View file

@ -0,0 +1,77 @@
import { PhoneRegex } from '$lib/regex';
import { encrypt } from '$lib/server/crypto/encryption.js';
import { logger } from '$lib/server/logger';
import { prisma } from '$lib/server/prisma';
import { fail, type Actions } from '@sveltejs/kit';
import zod from 'zod';
export const load = async (event) => {
const tenantId = event.locals.auth!.orgId!;
const configs = await prisma.tenantConfig.findUnique({
where: { tenantId: tenantId },
select: { accountSID: true, authToken: true, phoneNumber: true },
});
return {
configs: configs,
};
};
export const actions = {
update: async (event) => {
const form = await event.request.formData();
const tenantId = event.locals.auth!.orgId!;
if (!form.has('accountSID')) {
return fail(400, { error: 'account_sid_missing' });
}
if (!form.has('authToken')) {
return fail(400, { error: 'auth_token_missing' });
}
if (!form.has('phoneNumber')) {
return fail(400, { error: 'phone_number_missing' });
}
const accountSID = form.get('accountSID');
if (typeof accountSID !== 'string') {
return fail(400, { error: 'invalid_account_sid' });
}
const authToken = form.get('authToken');
if (typeof authToken !== 'string') {
return fail(400, { error: 'invalid_auth_token' });
}
const {
success: phoneSuccess,
data: phoneNumber,
error: phoneError,
} = zod.string().regex(PhoneRegex).safeParse(form.get('phone'));
if (!phoneSuccess) {
logger.error(phoneError);
return fail(400, { error: 'invalid_phone_number' });
}
const configs = await prisma.tenantConfig.upsert({
where: {
tenantId: tenantId,
},
create: {
tenantId: tenantId,
accountSID: encrypt(accountSID),
authToken: encrypt(authToken),
phoneNumber: encrypt(phoneNumber),
},
update: {
tenantId: tenantId,
accountSID: accountSID,
authToken: authToken,
phoneNumber: phoneNumber,
},
select: { accountSID: true, authToken: true, phoneNumber: true },
});
return {
configs: configs,
};
},
} satisfies Actions;

View file

@ -1,4 +1,90 @@
<script lang="ts">
import { enhance } from '$app/forms';
import { Button } from '$lib/components/Actions';
import { TextInput } from '$lib/components/DataInput';
import { messages } from '$lib/i18n';
import { Fingerprint, KeyRound, PhoneOutgoing } from 'lucide-svelte';
import { fade } from 'svelte/transition';
import type { ActionData, PageData } from './$types';
import Divider from '$lib/components/Layout/Divider.svelte';
type Props = {
data: PageData;
form: ActionData;
};
let { data, form }: Props = $props();
let configs = $derived(form?.configs ?? data.configs);
</script>
<div class="container"></div>
<div class="page" transition:fade>
<div class="card w-full max-w-xl bg-base-200 px-4 pt-4 shadow-xl">
<div class="card-title justify-center">
<h2 class="text-2xl font-semibold">{messages.settings_title()}</h2>
{#if form?.error}
<div role="alert" class="alert alert-error absolute -top-20" transition:fade>
<i class="fi fi-bs-octagon-xmark h-6 w-6 shrink-0"></i>
<span>{form.error}</span>
</div>
{/if}
</div>
<form id="sms" method="POST" action="?/update" use:enhance>
<div class="card-body">
<Divider />
<h2 class="text-2xl font-semibold">{messages.settings_category_twilio()}</h2>
<TextInput
defaultvalue={configs?.accountSID}
name="accountID"
placeholder="..."
bordered
fade
>
{#snippet label()}
<div class="flex gap-2">
<Fingerprint size="18" />
{messages.settings_twilio_account_sid()}
</div>
{/snippet}
</TextInput>
<TextInput
defaultvalue={configs?.authToken}
name="authToken"
placeholder="..."
type="password"
bordered
fade
>
{#snippet label()}
<div class="flex gap-2">
<KeyRound size="18" />
{messages.settings_twilio_auth_token()}
</div>
{/snippet}
</TextInput>
<TextInput
defaultvalue={configs?.phoneNumber}
name="phoneNumber"
placeholder="+1XXX-XXX-XXXX"
bordered
fade
>
{#snippet label()}
<div class="flex gap-2">
<PhoneOutgoing size="18" />
{messages.settings_twilio_phone_number()}
</div>
{/snippet}
</TextInput>
</div>
<div class="card-actions justify-center px-8 pb-4">
<Button type="submit" variant="outline" full>{messages.settings_save()}</Button>
</div>
</form>
</div>
</div>
<style>
.page {
@apply flex flex-col items-center justify-around gap-24 py-[10%];
}
</style>

View file

@ -1,10 +1,32 @@
import { TWILIO_PHONE_NUMBER } from '$env/static/private';
import { PhoneRegex } from '$lib/regex';
import { logger } from '$lib/server/logger';
import { prisma } from '$lib/server/prisma/index.js';
import { TwilioClient } from '$lib/server/twilio';
import { fail, type Actions } from '@sveltejs/kit';
import { error, fail, type Actions } from '@sveltejs/kit';
import zod from 'zod';
export const load = async (event) => {
const tenantId = event.locals.auth!.orgId!;
const configs = await prisma.tenantConfig.findUnique({
where: { tenantId: tenantId },
select: {
accountSID: true,
authToken: true,
phoneNumber: true,
},
});
if (!configs || Object.keys(configs).length === 0) {
return error(500, new Error('twilio_configs_not_set'));
}
return {
configs: configs,
};
};
export const actions = {
push: async (event) => {
const form = await event.request.formData();