41 create tenant twilio config (#62)

* add tenant config table

* add encryption/decryption + env vars

* generate secret and validate iv position is number

* expect errors

* remove TWILIO env vars

* settings page impl

* update schema definitions after Mostaphas Tenant impl

* load user env

* just return empty config

* add Settings menu item

* check if settings are present and provide warning if not

* correct form item names

* use correct locals value

* ree

* give twilio its own table

* lock prisma version

* event url is the correct param

* load twilio config from db

* commit migration

* use test script not bun command
This commit is contained in:
Baobeld 2025-01-26 23:36:06 -05:00 committed by GitHub
parent 8006d523c7
commit 8270c53509
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 515 additions and 57 deletions

View file

@ -0,0 +1,20 @@
import { describe, expect, it, vi } from 'vitest';
import { decrypt, encrypt } from './encryption';
describe('Encryption', () => {
it('should encrypt and decrypt data', () => {
const data = 'aye its ya boi!';
vi.mock('$env/static/private', () => ({
SECRETS_PASSWORD: 'aac7405eb3384e68c285fc252dbf68b2',
SECRETS_SALT: 'c4aeaf8bda72ea45e8c23269ca849013',
SECRETS_IV_POSITION: 9,
}));
const encrypted = encrypt(data);
console.log(encrypted);
const decrypted = decrypt(encrypted);
expect(decrypted).toEqual(data);
});
});

View file

@ -0,0 +1,42 @@
import { SECRETS_PASSWORD, SECRETS_IV_POSITION, SECRETS_SALT } from '$env/static/private';
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'node:crypto';
const algorithm = 'aes-256-gcm';
const password = SECRETS_PASSWORD;
const salt = SECRETS_SALT;
const iv_position = Number(SECRETS_IV_POSITION);
function construct(encrypted: string, tag: Buffer, iv: { value: Buffer; position: number }) {
return `${encrypted.slice(0, iv.position)}${iv.value.toString('hex')}${encrypted.slice(iv.position)}${tag.toString('hex')}`;
}
function deconstruct(encrypted: string, position: number): [Buffer, string, Buffer] {
const iv = encrypted.slice(position, position + 16);
const text = `${encrypted.slice(0, position)}${encrypted.slice(position + 16, encrypted.length - 32)}`;
const authTag = encrypted.slice(encrypted.length - 32);
return [Buffer.from(iv, 'hex'), text, Buffer.from(authTag, 'hex')];
}
export function encrypt(value: string): string {
const key = scryptSync(password, salt, 32);
const iv = randomBytes(8);
// @ts-expect-error Bun typing mismatch, but it still works!
const cipher = createCipheriv(algorithm, key, iv);
const encrypted = cipher.update(value, 'utf-8', 'hex') + cipher.final('hex');
const authTag = cipher.getAuthTag();
return construct(encrypted, authTag, { value: iv, position: iv_position });
}
export function decrypt(value: string): string {
const key = scryptSync(password, salt, 32);
const [iv, text, authTag] = deconstruct(value, iv_position);
// @ts-expect-error Bun typing mismatch, but it still works!
const decipher = createDecipheriv(algorithm, key, iv);
// @ts-expect-error Bun typing mismatch, but it still works!
decipher.setAuthTag(authTag);
return decipher.update(text, 'hex', 'utf-8') + decipher.final('utf-8');
}

View file

@ -0,0 +1 @@
export * from './encryption';

View file

@ -0,0 +1 @@
export * from './server';

View file

@ -0,0 +1 @@
export * from './loadUserEnv';

View file

@ -0,0 +1,40 @@
import { logger } from '$lib/server/logger';
import { prisma } from '$lib/server/prisma';
import type { Handle } from '@sveltejs/kit';
const publicRoutes = ['/login'];
export function loadUserEnv(): Handle {
return async ({ event, resolve }) => {
if (event.url !== null && publicRoutes.includes(event.url.pathname)) {
return resolve(event);
}
try {
const tenant = await prisma.tenant.findUniqueOrThrow({
where: {
clerkOrganizationId: event.locals.auth.orgId ?? undefined,
},
});
const user = await prisma.user.findUniqueOrThrow({
where: {
clerkId_tenantId: { clerkId: event.locals.auth.userId!, tenantId: tenant.id },
},
});
event.locals.tenant = tenant;
event.locals.user = user;
} catch (error) {
if (error instanceof Error) {
logger.error(error);
}
return new Response(null, {
status: 307,
headers: {
location: '/login',
},
});
}
return resolve(event);
};
}

View file

@ -1,4 +0,0 @@
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);

View file

@ -1 +0,0 @@
export * from './client';