give twilio its own table

This commit is contained in:
Benjamin Palko 2025-01-18 23:22:15 -05:00
parent 3d562e4009
commit aeaafcefe1
5 changed files with 75 additions and 33 deletions

View file

@ -32,10 +32,11 @@ model User {
} }
model Tenant { model Tenant {
id String @id @default(uuid()) id String @id @default(uuid())
clerkOrganizationId String @unique clerkOrganizationId String @unique
users User[]
tenantConfig TenantConfig? users User[]
tenantConfig TenantConfig?
name String name String
slug String @unique slug String @unique
@ -49,6 +50,17 @@ model TenantConfig {
tenant Tenant @relation(fields: [tenantId], references: [id]) tenant Tenant @relation(fields: [tenantId], references: [id])
tenantId String @unique tenantId String @unique
twilioConfig TwilioConfig?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model TwilioConfig {
id String @id @default(uuid())
tenantConfig TenantConfig @relation(fields: [tenantConfigId], references: [id])
tenantConfigId String @unique
accountSID String accountSID String
authToken String authToken String
phoneNumber String phoneNumber String

View file

@ -10,7 +10,15 @@ export const load = async (event) => {
const configs = await prisma.tenantConfig.findUnique({ const configs = await prisma.tenantConfig.findUnique({
where: { tenantId: tenantId }, where: { tenantId: tenantId },
select: { accountSID: true, authToken: true, phoneNumber: true }, select: {
twilioConfig: {
select: {
accountSID: true,
authToken: true,
phoneNumber: true,
},
},
},
}); });
return { return {
@ -23,21 +31,21 @@ export const actions = {
const form = await event.request.formData(); const form = await event.request.formData();
const tenantId = event.locals.tenant.id; const tenantId = event.locals.tenant.id;
if (!form.has('accountSID')) { if (!form.has('twilioAccountSID')) {
return fail(400, { error: 'account_sid_missing' }); return fail(400, { error: 'account_sid_missing' });
} }
if (!form.has('authToken')) { if (!form.has('twilioAuthToken')) {
return fail(400, { error: 'auth_token_missing' }); return fail(400, { error: 'auth_token_missing' });
} }
if (!form.has('phoneNumber')) { if (!form.has('twilioPhoneNumber')) {
return fail(400, { error: 'phone_number_missing' }); return fail(400, { error: 'phone_number_missing' });
} }
const accountSID = form.get('accountSID'); const accountSID = form.get('twilioAccountSID');
if (typeof accountSID !== 'string') { if (typeof accountSID !== 'string') {
return fail(400, { error: 'invalid_account_sid' }); return fail(400, { error: 'invalid_account_sid' });
} }
const authToken = form.get('authToken'); const authToken = form.get('twilioAuthToken');
if (typeof authToken !== 'string') { if (typeof authToken !== 'string') {
return fail(400, { error: 'invalid_auth_token' }); return fail(400, { error: 'invalid_auth_token' });
} }
@ -45,7 +53,7 @@ export const actions = {
success: phoneSuccess, success: phoneSuccess,
data: phoneNumber, data: phoneNumber,
error: phoneError, error: phoneError,
} = zod.string().regex(PhoneRegex).safeParse(form.get('phoneNumber')); } = zod.string().regex(PhoneRegex).safeParse(form.get('twilioPhoneNumber'));
if (!phoneSuccess) { if (!phoneSuccess) {
logger.error(phoneError); logger.error(phoneError);
return fail(400, { error: 'invalid_phone_number' }); return fail(400, { error: 'invalid_phone_number' });
@ -57,17 +65,25 @@ export const actions = {
}, },
create: { create: {
tenantId: tenantId, tenantId: tenantId,
accountSID: encrypt(accountSID), twilioConfig: {
authToken: encrypt(authToken), create: {
phoneNumber: encrypt(phoneNumber), accountSID: encrypt(accountSID),
authToken: encrypt(authToken),
phoneNumber: encrypt(phoneNumber),
},
},
}, },
update: { update: {
tenantId: tenantId, tenantId: tenantId,
accountSID: accountSID, twilioConfig: {
authToken: authToken, update: {
phoneNumber: phoneNumber, accountSID: accountSID,
authToken: authToken,
phoneNumber: phoneNumber,
},
},
}, },
select: { accountSID: true, authToken: true, phoneNumber: true }, select: { twilioConfig: true },
}); });
return { return {

View file

@ -31,10 +31,11 @@
<form id="sms" method="POST" action="?/update" use:enhance> <form id="sms" method="POST" action="?/update" use:enhance>
<div class="card-body"> <div class="card-body">
<Divider /> <Divider />
<!-- Twilio -->
<h2 class="text-2xl font-semibold">{messages.settings_category_twilio()}</h2> <h2 class="text-2xl font-semibold">{messages.settings_category_twilio()}</h2>
<TextInput <TextInput
defaultvalue={configs?.accountSID} defaultvalue={configs?.twilioConfig?.accountSID}
name="accountSID" name="twilioAccountSID"
placeholder="..." placeholder="..."
bordered bordered
fade fade
@ -47,8 +48,8 @@
{/snippet} {/snippet}
</TextInput> </TextInput>
<TextInput <TextInput
defaultvalue={configs?.authToken} defaultvalue={configs?.twilioConfig?.authToken}
name="authToken" name="twilioAuthToken"
placeholder="..." placeholder="..."
type="password" type="password"
bordered bordered
@ -62,8 +63,8 @@
{/snippet} {/snippet}
</TextInput> </TextInput>
<TextInput <TextInput
defaultvalue={configs?.phoneNumber} defaultvalue={configs?.twilioConfig?.phoneNumber}
name="phoneNumber" name="twilioPhoneNumber"
placeholder="+1XXX-XXX-XXXX" placeholder="+1XXX-XXX-XXXX"
bordered bordered
fade fade

View file

@ -12,14 +12,28 @@ export const load = async (event) => {
const configs = await prisma.tenantConfig.findUnique({ const configs = await prisma.tenantConfig.findUnique({
where: { tenantId: tenantId }, where: { tenantId: tenantId },
select: { select: {
accountSID: true, twilioConfig: {
authToken: true, select: {
phoneNumber: true, accountSID: true,
authToken: true,
phoneNumber: true,
},
},
}, },
}); });
const { success, error: validationError } = zod
.object({
accountSID: zod.string(),
})
.safeParse(configs?.twilioConfig);
if (!success) {
logger.warn(validationError.message);
}
return { return {
configs: configs, isTwilioConfigured: success,
}; };
}; };

View file

@ -15,7 +15,6 @@
form: ActionData; form: ActionData;
}; };
let { data, form }: Props = $props(); let { data, form }: Props = $props();
let isConfigMissing = $derived(!data.configs);
</script> </script>
{#snippet PhoneLabel()} {#snippet PhoneLabel()}
@ -35,7 +34,7 @@
{/snippet} {/snippet}
<span>{form.error}</span> <span>{form.error}</span>
</Alert> </Alert>
{:else if isConfigMissing} {:else if !data.isTwilioConfigured}
<Alert status="warning"> <Alert status="warning">
{#snippet icon()} {#snippet icon()}
<TriangleAlert /> <TriangleAlert />
@ -53,7 +52,7 @@
<form id="sms" method="POST" action="?/push" use:enhance> <form id="sms" method="POST" action="?/push" use:enhance>
<div class="card-body"> <div class="card-body">
<TextInput <TextInput
disabled={isConfigMissing} disabled={!data.isTwilioConfigured}
type="tel" type="tel"
name="phone" name="phone"
label={PhoneLabel} label={PhoneLabel}
@ -62,7 +61,7 @@
fade fade
/> />
<Textarea <Textarea
disabled={isConfigMissing} disabled={!data.isTwilioConfigured}
label={MessageLabel} label={MessageLabel}
size="lg" size="lg"
error={form?.error} error={form?.error}
@ -72,7 +71,7 @@
/> />
</div> </div>
<div class="card-actions justify-center px-8 pb-4"> <div class="card-actions justify-center px-8 pb-4">
<Button disabled={isConfigMissing} type="submit" variant="outline" full> <Button disabled={!data.isTwilioConfigured} type="submit" variant="outline" full>
{messages.sms_button_submit()} {messages.sms_button_submit()}
</Button> </Button>
</div> </div>