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
5 changed files with 75 additions and 33 deletions
Showing only changes of commit aeaafcefe1 - Show all commits

View file

@ -34,6 +34,7 @@ model User {
model Tenant {
id String @id @default(uuid())
clerkOrganizationId String @unique
users User[]
tenantConfig TenantConfig?
@ -49,6 +50,17 @@ model TenantConfig {
tenant Tenant @relation(fields: [tenantId], references: [id])
tenantId String @unique
twilioConfig TwilioConfig?
piopi commented 2025-01-17 11:52:58 -05:00 (Migrated from github.com)
Review

Not a fan of calling this TenantConfig while it contains twillio specific fields, if you have a new integration like sendgrid, will you just add a new column here? It doesn't feel scalable, I think you could either rename this to TwillioTenantConfig, if you don't wanna deal with it now or have a generic way to store integration credentials. Like have two column, integrationName, integrationValues (JSON column)

Not a fan of calling this TenantConfig while it contains twillio specific fields, if you have a new integration like sendgrid, will you just add a new column here? It doesn't feel scalable, I think you could either rename this to TwillioTenantConfig, if you don't wanna deal with it now or have a generic way to store integration credentials. Like have two column, integrationName, integrationValues (JSON column)
piopi commented 2025-01-17 11:53:52 -05:00 (Migrated from github.com)
Review

and use Zod for validation

and use Zod for validation
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
authToken String
phoneNumber String

View file

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

View file

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

View file

@ -11,15 +11,29 @@ export const load = async (event) => {
const configs = await prisma.tenantConfig.findUnique({
where: { tenantId: tenantId },
select: {
twilioConfig: {
select: {
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 {
configs: configs,
isTwilioConfigured: success,
};
};

View file

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