41 create tenant twilio config #62
4 changed files with 194 additions and 3 deletions
|
|
@ -21,5 +21,11 @@
|
||||||
"sms_label_phone": "Phone Number",
|
"sms_label_phone": "Phone Number",
|
||||||
"sms_label_message": "Message",
|
"sms_label_message": "Message",
|
||||||
"sms_button_submit": "Send 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"
|
"error_page_go_home": "Go Home"
|
||||||
}
|
}
|
||||||
77
src/routes/app/settings/+page.server.ts
Normal file
77
src/routes/app/settings/+page.server.ts
Normal 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;
|
||||||
|
|
@ -1,4 +1,90 @@
|
||||||
<script lang="ts">
|
<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>
|
</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>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,32 @@
|
||||||
import { TWILIO_PHONE_NUMBER } from '$env/static/private';
|
import { TWILIO_PHONE_NUMBER } from '$env/static/private';
|
||||||
import { PhoneRegex } from '$lib/regex';
|
import { PhoneRegex } from '$lib/regex';
|
||||||
import { logger } from '$lib/server/logger';
|
import { logger } from '$lib/server/logger';
|
||||||
|
import { prisma } from '$lib/server/prisma/index.js';
|
||||||
import { TwilioClient } from '$lib/server/twilio';
|
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';
|
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 = {
|
export const actions = {
|
||||||
push: async (event) => {
|
push: async (event) => {
|
||||||
const form = await event.request.formData();
|
const form = await event.request.formData();
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue