feat: http webhooks

This commit is contained in:
diced
2024-06-13 22:27:13 -07:00
parent 080a3a2c19
commit ca766bb1d8
10 changed files with 151 additions and 33 deletions

View File

@@ -3,13 +3,13 @@ import { FastifyRequest } from 'fastify';
import { writeFile } from 'fs/promises';
import { extname, join } from 'path';
import { Worker } from 'worker_threads';
import { config } from '../../config';
import { hashPassword } from '../../crypto';
import { prisma } from '../../db';
import { log } from '../../logger';
import { guess } from '../../mimes';
import { formatFileName } from '../../uploader/formatFileName';
import { UploadHeaders, UploadOptions } from '../../uploader/parseHeaders';
import { config } from '@/lib/config';
import { hashPassword } from '@/lib/crypto';
import { prisma } from '@/lib/db';
import { log } from '@/lib/logger';
import { guess } from '@/lib/mimes';
import { formatFileName } from '@/lib/uploader/formatFileName';
import { UploadHeaders, UploadOptions } from '@/lib/uploader/parseHeaders';
const logger = log('api').c('upload');
export async function handlePartialUpload({

View File

@@ -1,19 +1,19 @@
import { ApiUploadResponse, MultipartFileBuffer } from '@/server/routes/api/upload';
import { FastifyRequest } from 'fastify';
import { extname } from 'path';
import { bytes } from '../../bytes';
import { compress } from '../../compress';
import { config } from '../../config';
import { hashPassword } from '../../crypto';
import { datasource } from '../../datasource';
import { prisma } from '../../db';
import { fileSelect } from '../../db/models/file';
import { onUpload } from '../../discord';
import { removeGps } from '../../gps';
import { log } from '../../logger';
import { guess } from '../../mimes';
import { formatFileName } from '../../uploader/formatFileName';
import { UploadHeaders, UploadOptions } from '../../uploader/parseHeaders';
import { bytes } from '@/lib/bytes';
import { compress } from '@/lib/compress';
import { config } from '@/lib/config';
import { hashPassword } from '@/lib/crypto';
import { datasource } from '@/lib/datasource';
import { prisma } from '@/lib/db';
import { fileSelect } from '@/lib/db/models/file';
import { onUpload } from '@/lib/webhooks';
import { removeGps } from '@/lib/gps';
import { log } from '@/lib/logger';
import { guess } from '@/lib/mimes';
import { formatFileName } from '@/lib/uploader/formatFileName';
import { UploadHeaders, UploadOptions } from '@/lib/uploader/parseHeaders';
const logger = log('api').c('upload');

View File

@@ -117,6 +117,10 @@ export const rawConfig: any = {
adminBypass: undefined,
allowList: undefined,
},
httpWebhook: {
onUpload: undefined,
onShorten: undefined,
},
};
export const PROP_TO_ENV = {
@@ -239,6 +243,9 @@ export const PROP_TO_ENV = {
'ratelimit.window': 'RATELIMIT_WINDOW',
'ratelimit.adminBypass': 'RATELIMIT_ADMIN_BYPASS',
'ratelimit.allowList': 'RATELIMIT_ALLOW_LIST',
'httpWebhook.onUpload': 'HTTP_WEBHOOK_ONUPLOAD',
'httpWebhook.onShorten': 'HTTP_WEBHOOK_ONSHORTEN',
};
const logger = log('config').c('read');
@@ -359,6 +366,9 @@ export function readEnv() {
env('ratelimit.window', 'ms'),
env('ratelimit.adminBypass', 'boolean'),
env('ratelimit.allowList', 'string[]'),
env('httpWebhook.onUpload', 'string'),
env('httpWebhook.onShorten', 'string'),
];
// clone raw

View File

@@ -2,7 +2,10 @@ import { config } from '.';
import enabled from '../oauth/enabled';
import { Config } from './validate';
export type SafeConfig = Omit<Config, 'oauth' | 'datasource' | 'core'> & {
export type SafeConfig = Omit<
Config,
'oauth' | 'datasource' | 'core' | 'discord' | 'httpWebhook' | 'ratelimit'
> & {
oauthEnabled: ReturnType<typeof enabled>;
oauth: {
bypassLocalLogin: boolean;
@@ -11,7 +14,7 @@ export type SafeConfig = Omit<Config, 'oauth' | 'datasource' | 'core'> & {
};
export function safeConfig(): SafeConfig {
const { datasource: _d, core: _c, oauth, ...rest } = config;
const { datasource: _d, core: _c, oauth, discord: _di, ratelimit: _r, httpWebhook: _h, ...rest } = config;
(rest as SafeConfig).oauthEnabled = enabled(config);
(rest as SafeConfig).oauth = {

View File

@@ -275,6 +275,10 @@ export const schema = z.object({
adminBypass: z.boolean().default(true),
allowList: z.array(z.string()).default([]),
}),
httpWebhook: z.object({
onUpload: z.string().url().nullable().default(null),
onShorten: z.string().url().nullable().default(null),
}),
});
export type Config = z.infer<typeof schema>;

View File

@@ -1,14 +1,14 @@
import { z } from 'zod';
import { discordContent } from './config/validate';
import { ParseValue, parseString } from './parser';
import { config } from './config';
import { File } from './db/models/file';
import { User } from './db/models/user';
import { log } from './logger';
import { Url } from './db/models/url';
import { parserMetrics } from './parser/metrics';
import { discordContent } from '../config/validate';
import { ParseValue, parseString } from '../parser';
import { config } from '../config';
import { File } from '../db/models/file';
import { User } from '../db/models/user';
import { log } from '../logger';
import { Url } from '../db/models/url';
import { parserMetrics } from '../parser/metrics';
const logger = log('discord');
const logger = log('webhooks').c('discord');
export type DiscordContent = z.infer<typeof discordContent>;
export type WebhooksExecuteBody = {

87
src/lib/webhooks/http.ts Normal file
View File

@@ -0,0 +1,87 @@
import { config } from '../config';
import { log } from '../logger';
import { onUpload as discordOnUpload, onShorten as discordOnShorten } from './discord';
const logger = log('webhooks').c('http');
export async function onUpload({ user, file, link }: Parameters<typeof discordOnUpload>[0]) {
if (!config.httpWebhook.onUpload) return;
if (!URL.canParse(config.httpWebhook.onUpload)) {
logger.debug('invalid url for http onUpload');
return;
}
delete (<any>user).oauthProviders;
delete user.passkeys;
delete user.token;
delete user.password;
delete user.totpSecret;
delete (<any>file).password;
const payload = {
type: 'upload',
data: {
user,
file,
link,
},
};
const res = await fetch(config.httpWebhook.onUpload, {
method: 'POST',
body: JSON.stringify(payload),
headers: {
'Content-Type': 'application/json',
'x-zipline-webhook': 'true',
'x-zipline-webhook-type': 'upload',
},
});
if (!res.ok) {
const text = await res.text();
logger.error('webhook failed', { response: text, status: res.status });
}
return;
}
export async function onShorten({ user, url, link }: Parameters<typeof discordOnShorten>[0]) {
if (!config.httpWebhook.onShorten) return;
if (!URL.canParse(config.httpWebhook.onShorten)) {
logger.debug('invalid url for http onShorten');
return;
}
delete (<any>user).oauthProviders;
delete user.passkeys;
delete user.token;
delete user.password;
delete user.totpSecret;
delete (<any>url).password;
const payload = {
type: 'shorten',
data: {
user,
url,
link,
},
};
const res = await fetch(config.httpWebhook.onShorten, {
method: 'POST',
body: JSON.stringify(payload),
headers: {
'Content-Type': 'application/json',
'x-zipline-webhook': 'true',
'x-zipline-webhook-type': 'shorten',
},
});
if (!res.ok) {
const text = await res.text();
logger.error('webhook failed', { response: text, status: res.status });
}
return;
}

14
src/lib/webhooks/index.ts Normal file
View File

@@ -0,0 +1,14 @@
import { onUpload as discordOnUpload, onShorten as discordOnShorten } from './discord';
import { onUpload as httpOnUpload, onShorten as httpOnShorten } from './http';
export async function onUpload(args: Parameters<typeof discordOnUpload>[0]) {
Promise.all([discordOnUpload(args), httpOnUpload(args)]);
return;
}
export async function onShorten(args: Parameters<typeof discordOnShorten>[0]) {
Promise.all([discordOnShorten(args), httpOnShorten(args)]);
return;
}

View File

@@ -3,7 +3,7 @@ import { config } from '@/lib/config';
import { prisma } from '@/lib/db';
import { fileSelect } from '@/lib/db/models/file';
import { userSelect } from '@/lib/db/models/user';
import { onUpload } from '@/lib/discord';
import { onUpload } from '@/lib/webhooks';
import { log } from '@/lib/logger';
import { UploadOptions } from '@/lib/uploader/parseHeaders';
import { open, readFile, readdir, rm } from 'fs/promises';

View File

@@ -5,7 +5,7 @@ import { Url } from '@/lib/db/models/url';
import { log } from '@/lib/logger';
import { z } from 'zod';
import { Prisma } from '@prisma/client';
import { onShorten } from '@/lib/discord';
import { onShorten } from '@/lib/webhooks';
import fastifyPlugin from 'fastify-plugin';
import { userMiddleware } from '@/server/middleware/user';