mirror of
https://github.com/diced/zipline.git
synced 2025-12-12 15:50:11 -08:00
366 lines
10 KiB
TypeScript
Executable File
366 lines
10 KiB
TypeScript
Executable File
import { tmpdir } from 'os';
|
|
import { join, resolve } from 'path';
|
|
import { type ZodIssue, z } from 'zod';
|
|
import { log } from '../logger';
|
|
import { PROP_TO_ENV, ParsedConfig } from './read';
|
|
|
|
declare global {
|
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
namespace NodeJS {
|
|
interface ProcessEnv {
|
|
ZIPLINE_BUILD?: string;
|
|
ZIPLINE_DB_LOG?: string;
|
|
ZIPLINE_OVERRIDE_DISABLED_WORKER_LOG?: string;
|
|
}
|
|
}
|
|
}
|
|
|
|
export const discordContent = z
|
|
.object({
|
|
webhookUrl: z.string().url().nullable().default(null),
|
|
username: z.string().nullable().default(null),
|
|
avatarUrl: z.string().nullable().default(null),
|
|
content: z.string().nullable().default(null),
|
|
embed: z
|
|
.object({
|
|
title: z.string().nullable().default(null),
|
|
description: z.string().nullable().default(null),
|
|
footer: z.string().nullable().default(null),
|
|
color: z
|
|
.string()
|
|
.regex(/^#?([a-f0-9]{6}|[a-f0-9]{3})$/)
|
|
.nullable()
|
|
.default(null),
|
|
thumbnail: z.boolean().default(false),
|
|
imageOrVideo: z.boolean().default(false),
|
|
timestamp: z.boolean().default(false),
|
|
url: z.boolean().default(false),
|
|
})
|
|
.nullable()
|
|
.default(null),
|
|
})
|
|
.nullable()
|
|
.default(null);
|
|
|
|
export const schema = z.object({
|
|
core: z.object({
|
|
port: z.number().default(3000),
|
|
hostname: z.string().default('localhost'),
|
|
secret: z.string().superRefine((s, c) => {
|
|
if (s === 'changethis')
|
|
return c.addIssue({
|
|
code: 'custom',
|
|
message: 'Secret must be changed from the default value',
|
|
});
|
|
|
|
if (s.length <= 32) {
|
|
return c.addIssue({
|
|
code: 'too_small',
|
|
minimum: 32,
|
|
type: 'string',
|
|
inclusive: true,
|
|
message: 'Secret must contain at least 32 characters',
|
|
exact: false,
|
|
});
|
|
}
|
|
}),
|
|
databaseUrl: z.string().url(),
|
|
returnHttpsUrls: z.boolean().default(false),
|
|
defaultDomain: z.string().nullable().default(null),
|
|
tempDirectory: z
|
|
.string()
|
|
.transform((s) => resolve(s))
|
|
.default(join(tmpdir(), 'zipline')),
|
|
}),
|
|
chunks: z.object({
|
|
max: z.string().default('95mb'),
|
|
size: z.string().default('25mb'),
|
|
enabled: z.boolean().default(true),
|
|
}),
|
|
tasks: z.object({
|
|
deleteInterval: z.string().default('30min'),
|
|
clearInvitesInterval: z.string().default('30min'),
|
|
maxViewsInterval: z.string().default('30min'),
|
|
thumbnailsInterval: z.string().default('30min'),
|
|
metricsInterval: z.string().default('30min'),
|
|
}),
|
|
files: z.object({
|
|
route: z.string().startsWith('/').min(1).trim().toLowerCase().default('/u'),
|
|
length: z.number().default(6),
|
|
defaultFormat: z.enum(['random', 'date', 'uuid', 'name', 'gfycat']).default('random'),
|
|
disabledExtensions: z.array(z.string()).default([]),
|
|
maxFileSize: z.string().default('100mb'),
|
|
defaultExpiration: z.string().nullable().default(null),
|
|
assumeMimetypes: z.boolean().default(false),
|
|
defaultDateFormat: z.string().default('YYYY-MM-DD_HH:mm:ss'),
|
|
removeGpsMetadata: z.boolean().default(false),
|
|
}),
|
|
urls: z.object({
|
|
route: z.string().startsWith('/').min(1).trim().toLowerCase().default('/go'),
|
|
length: z.number().default(6),
|
|
}),
|
|
datasource: z
|
|
.object({
|
|
type: z.enum(['local', 's3']).default('local'),
|
|
s3: z
|
|
.object({
|
|
accessKeyId: z.string(),
|
|
secretAccessKey: z.string(),
|
|
region: z.string(),
|
|
bucket: z.string(),
|
|
endpoint: z.string().nullable().default(null),
|
|
})
|
|
.optional(),
|
|
local: z
|
|
.object({
|
|
directory: z
|
|
.string()
|
|
.transform((s) => resolve(s))
|
|
.default('./uploads'),
|
|
})
|
|
.optional(),
|
|
})
|
|
.superRefine((s, c) => {
|
|
if (s.type === 's3' && !s.s3) {
|
|
for (const key of ['accessKeyId', 'secretAccessKey', 'region', 'bucket']) {
|
|
c.addIssue({
|
|
code: z.ZodIssueCode.invalid_type,
|
|
expected: 'string',
|
|
received: 'unknown',
|
|
path: ['s3', key],
|
|
});
|
|
}
|
|
} else if (s.type === 'local' && !s.local) {
|
|
c.addIssue({
|
|
code: z.ZodIssueCode.invalid_type,
|
|
expected: 'string',
|
|
received: 'unknown',
|
|
path: ['local', 'directory'],
|
|
});
|
|
}
|
|
}),
|
|
features: z.object({
|
|
imageCompression: z.boolean().default(true),
|
|
robotsTxt: z.boolean().default(false),
|
|
healthcheck: z.boolean().default(true),
|
|
userRegistration: z.boolean().default(false),
|
|
oauthRegistration: z.boolean().default(false),
|
|
deleteOnMaxViews: z.boolean().default(true),
|
|
thumbnails: z.object({
|
|
enabled: z.boolean().default(true),
|
|
num_threads: z.number().default(4),
|
|
}),
|
|
metrics: z.object({
|
|
enabled: z.boolean().default(true),
|
|
adminOnly: z.boolean().default(false),
|
|
showUserSpecific: z.boolean().default(true),
|
|
}),
|
|
}),
|
|
invites: z.object({
|
|
enabled: z.boolean().default(true),
|
|
length: z.number().default(8),
|
|
}),
|
|
website: z.object({
|
|
title: z.string().default('Zipline'),
|
|
titleLogo: z.string().url().nullable().default(null),
|
|
externalLinks: z
|
|
.array(
|
|
z.object({
|
|
name: z.string(),
|
|
url: z.string().url(),
|
|
}),
|
|
)
|
|
.default([
|
|
{
|
|
name: 'GitHub',
|
|
url: 'https://github.com/diced/zipline',
|
|
},
|
|
{
|
|
name: 'Documentation',
|
|
url: 'https://zipline.diced.tech',
|
|
},
|
|
]),
|
|
loginBackground: z.string().url().nullable().default(null),
|
|
defaultAvatar: z
|
|
.string()
|
|
.transform((s) => resolve(s))
|
|
.nullable()
|
|
.default(null),
|
|
theme: z.object({
|
|
default: z.string().default('system'),
|
|
dark: z.string().default('builtin:dark_gray'),
|
|
light: z.string().default('builtin:light_gray'),
|
|
}),
|
|
tos: z
|
|
.string()
|
|
.transform((s) => resolve(s))
|
|
.refine((v) => (v ? v.endsWith('.md') : true))
|
|
.nullable()
|
|
.default(null),
|
|
}),
|
|
mfa: z.object({
|
|
totp: z.object({
|
|
enabled: z.boolean().default(false),
|
|
issuer: z.string().default('Zipline'),
|
|
}),
|
|
passkeys: z.boolean().default(true),
|
|
}),
|
|
oauth: z.object({
|
|
bypassLocalLogin: z.boolean().default(false),
|
|
loginOnly: z.boolean().default(false),
|
|
discord: z
|
|
.object({
|
|
clientId: z.string(),
|
|
clientSecret: z.string(),
|
|
redirectUri: z.string().url().nullable().default(null),
|
|
})
|
|
.or(
|
|
z.object({
|
|
clientId: z.undefined(),
|
|
clientSecret: z.undefined(),
|
|
redirectUri: z.undefined(),
|
|
}),
|
|
),
|
|
github: z
|
|
.object({
|
|
clientId: z.string(),
|
|
clientSecret: z.string(),
|
|
redirectUri: z.string().url().nullable().default(null),
|
|
})
|
|
.or(
|
|
z.object({
|
|
clientId: z.undefined(),
|
|
clientSecret: z.undefined(),
|
|
redirectUri: z.undefined(),
|
|
}),
|
|
),
|
|
google: z
|
|
.object({
|
|
clientId: z.string(),
|
|
clientSecret: z.string(),
|
|
redirectUri: z.string().url().nullable().default(null),
|
|
})
|
|
.or(
|
|
z.object({
|
|
clientId: z.undefined(),
|
|
clientSecret: z.undefined(),
|
|
redirectUri: z.undefined(),
|
|
}),
|
|
),
|
|
oidc: z
|
|
.object({
|
|
clientId: z.string(),
|
|
clientSecret: z.string(),
|
|
authorizeUrl: z.string().url(),
|
|
userinfoUrl: z.string().url(),
|
|
tokenUrl: z.string().url(),
|
|
redirectUri: z.string().url().nullable().default(null),
|
|
})
|
|
.or(
|
|
z.object({
|
|
clientId: z.undefined(),
|
|
clientSecret: z.undefined(),
|
|
authorizeUrl: z.undefined(),
|
|
userinfoUrl: z.undefined(),
|
|
tokenUrl: z.undefined(),
|
|
redirectUri: z.undefined(),
|
|
}),
|
|
),
|
|
}),
|
|
discord: z
|
|
.object({
|
|
webhookUrl: z.string().url().nullable().default(null),
|
|
username: z.string().nullable().default(null),
|
|
avatarUrl: z.string().url().nullable().default(null),
|
|
onUpload: discordContent,
|
|
onShorten: discordContent,
|
|
})
|
|
.nullable()
|
|
.default(null),
|
|
ratelimit: z.object({
|
|
enabled: z.boolean().default(true),
|
|
max: z.number().default(10),
|
|
window: z
|
|
.number()
|
|
.nullable()
|
|
.default(null)
|
|
.refine((v) => (v ? v > 0 : true)),
|
|
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),
|
|
}),
|
|
ssl: z.object({
|
|
key: z
|
|
.string()
|
|
.transform((s) => resolve(s))
|
|
.nullable()
|
|
.default(null),
|
|
cert: z
|
|
.string()
|
|
.transform((s) => resolve(s))
|
|
.nullable()
|
|
.default(null),
|
|
}),
|
|
pwa: z.object({
|
|
enabled: z.boolean().default(true),
|
|
title: z.string().default('Zipline'),
|
|
shortName: z.string().default('Zipline'),
|
|
description: z.string().default('Zipline'),
|
|
themeColor: z.string().default('#000000'),
|
|
backgroundColor: z.string().default('#000000'),
|
|
}),
|
|
});
|
|
|
|
export type Config = z.infer<typeof schema>;
|
|
|
|
const logger = log('config').c('validate');
|
|
|
|
export function validateConfigObject(env: ParsedConfig): Config {
|
|
const building = !!process.env.ZIPLINE_BUILD;
|
|
|
|
if (building) {
|
|
logger.debug('building, skipping validation');
|
|
// @ts-ignore
|
|
return {};
|
|
}
|
|
|
|
const validated = schema.safeParse(env);
|
|
if (!validated.success) {
|
|
logger.error('There was an error while validating the environment.');
|
|
|
|
for (const error of validated.error.errors) {
|
|
handleError(error);
|
|
}
|
|
|
|
process.exit(1);
|
|
}
|
|
|
|
logger.debug('reloaded config');
|
|
|
|
return validated.data;
|
|
}
|
|
|
|
function handleError(error: ZodIssue) {
|
|
logger.debug(JSON.stringify(error));
|
|
|
|
if (error.code === 'invalid_union') {
|
|
for (const unionError of error.unionErrors) {
|
|
for (const subError of unionError.issues) {
|
|
handleError(subError);
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
const path =
|
|
error.path[1] === 'externalLinks'
|
|
? `WEBSITE_EXTERNAL_LINKS[${error.path[2]}]`
|
|
: (PROP_TO_ENV[<keyof typeof PROP_TO_ENV>error.path.join('.')] ?? error.path.join('.'));
|
|
|
|
logger.error(`${path}: ${error.message}`);
|
|
}
|