From 6a54c5b8059def4f39f78a6ccd2ac3c8ff422ff4 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Sat, 6 Apr 2024 18:16:54 -1000 Subject: [PATCH] Replace config manager with unjs/c12 --- benchmarks/timelines.ts | 4 +- bun.lockb | Bin 382896 -> 394536 bytes cli.ts | 4 +- config/config.example.toml | 174 ++++-- database/datasource.ts | 4 +- database/entities/Like.ts | 4 +- database/entities/Queue.ts | 4 +- database/entities/Status.ts | 4 +- database/entities/User.ts | 13 +- index.ts | 9 +- package.json | 8 +- packages/config-manager/config-type.type.ts | 377 ------------ packages/config-manager/config.type.ts | 579 ++++++++++++++++++ packages/config-manager/index.ts | 132 +--- .../tests/config-manager.test.ts | 96 --- prisma.ts | 3 +- server.ts | 10 +- .../v1/accounts/update_credentials/index.ts | 8 +- server/api/api/v1/statuses/[id]/index.ts | 4 +- server/api/api/v1/statuses/index.ts | 2 +- server/api/routes.type.ts | 6 +- tests/api.test.ts | 3 +- tests/api/accounts.test.ts | 3 +- tests/api/statuses.test.ts | 3 +- tests/oauth.test.ts | 1 - utils/api.ts | 4 +- utils/constants.ts | 4 +- utils/meilisearch.ts | 4 +- utils/redis.ts | 6 +- utils/sanitization.ts | 4 +- 30 files changed, 744 insertions(+), 733 deletions(-) delete mode 100644 packages/config-manager/config-type.type.ts create mode 100644 packages/config-manager/config.type.ts delete mode 100644 packages/config-manager/tests/config-manager.test.ts diff --git a/benchmarks/timelines.ts b/benchmarks/timelines.ts index b5348b69..c12fe3e6 100644 --- a/benchmarks/timelines.ts +++ b/benchmarks/timelines.ts @@ -3,9 +3,7 @@ */ import chalk from "chalk"; -import { ConfigManager } from "config-manager"; - -const config = await new ConfigManager({}).getConfig(); +import { config } from "config-manager"; const token = process.env.TOKEN; const requestCount = Number(process.argv[2]) || 100; diff --git a/bun.lockb b/bun.lockb index 5aa839bf57b3eb445e0506ef4ef8718d256b1901..fd9669d66e8265ca2dc34307035e4bfe35fc5b3d 100755 GIT binary patch delta 78830 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 delta 74802 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 diff --git a/cli.ts b/cli.ts index 232cde7d..69e8afb9 100644 --- a/cli.ts +++ b/cli.ts @@ -7,7 +7,7 @@ import extract from "extract-zip"; import { client } from "~database/datasource"; import { CliBuilder, CliCommand } from "cli-parser"; import { CliParameterType } from "~packages/cli-parser/cli-builder.type"; -import { ConfigManager } from "~packages/config-manager"; +import { config } from "~packages/config-manager"; import { Parser } from "@json2csv/plainjs"; import type { Prisma } from "@prisma/client"; import { MediaBackend } from "media-manager"; @@ -17,8 +17,6 @@ import { tmpdir } from "os"; const args = process.argv; -const config = await new ConfigManager({}).getConfig(); - const filterObjects = (output: T[], fields: string[]) => { if (fields.length === 0) return output; diff --git a/config/config.example.toml b/config/config.example.toml index de256901..dd075c67 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -1,17 +1,28 @@ +# Lysand Config +# All of these values can be changed via the CLI (they will be saved in a file named config.internal.toml +# in the same directory as this one) +# Changing this file does not require a restart, but might take a few seconds to apply +# This file will be merged with the CLI configuration, taking precedence over it + [database] +# Main PostgreSQL database connection host = "localhost" port = 5432 username = "lysand" -password = "password123" +password = "lysand" database = "lysand" [redis.queue] +# Redis instance for storing the federation queue +# Required for federation host = "localhost" port = 6379 password = "" database = 0 [redis.cache] +# Redis instance to be used as a timeline cache +# Optional, can be the same as the queue instance host = "localhost" port = 6379 password = "" @@ -19,14 +30,15 @@ database = 1 enabled = false [meilisearch] +# If Meilisearch is not configured, search will not be enabled host = "localhost" -port = 40007 -api_key = "" -enabled = true +port = 7700 +api_key = "______________________________" +enabled = false [signups] # URL of your Terms of Service -tos_url = "https://example.com/tos" +tos_url = "https://my-site.com/tos" # Whether to enable registrations or not registration = true rules = [ @@ -41,40 +53,64 @@ rules = [ # The provider MUST support OpenID Connect with .well-known discovery # Most notably, GitHub does not support this [[oidc.providers]] +# Test with custom Authentik instance name = "CPlusPatch ID" id = "cpluspatch-id" url = "https://id.cpluspatch.com/application/o/lysand-testing/" -client_id = "XXXXXXXXXXXXXXXX" -client_secret = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" +client_id = "______________________________" +client_secret = "__________________________________" icon = "https://cpluspatch.com/images/icons/logo.svg" [http] +# The full URL Lysand will be reachable by (paths are not supported) base_url = "https://lysand.social" -bind = "http://localhost" +# Address to bind to +bind = "0.0.0.0" bind_port = "8080" # Bans IPv4 or IPv6 IPs (wildcards, networks and ranges are supported) banned_ips = [] +# Banned user agents, regex format +banned_user_agents = [ + # "curl\/7.68.0", + # "wget\/1.20.3", +] + +[http.bait] +# Enable the bait feature (sends fake data to those who are flagged) +enabled = false +# Path to file of bait data (if not provided, Lysand will send the entire Bee Movie script) +send_file = "" +# IPs to send bait data to (wildcards, networks and ranges are supported) +bait_ips = ["127.0.0.1", "::1"] +# User agents to send bait data to (regex format) +bait_user_agents = ["curl", "wget"] [smtp] # SMTP server to use for sending emails server = "smtp.example.com" port = 465 username = "test@example.com" -password = "password123" +password = "____________" tls = true +# Disable all email functions (this will allow people to sign up without verifying +# their email) +enabled = false [media] # Can be "s3" or "local", where "local" uploads the file to the local filesystem # If you need to change this value after setting up your instance, you must move all the files -# from one backend to the other manually -backend = "s3" +# from one backend to the other manually (the CLI will have an option to do this later) +# TODO: Add CLI command to move files +backend = "local" # Whether to check the hash of media when uploading to avoid duplication deduplicate_media = true # If media backend is "local", this is the folder where the files will be stored +# Can be any path local_uploads_folder = "uploads" [media.conversion] +# Whether to automatically convert images to another format on upload convert_images = false # Can be: "jxl", "webp", "avif", "png", "jpg", "heif" # JXL support will likely not work @@ -82,26 +118,26 @@ convert_to = "webp" [s3] # Can be left blank if you don't use the S3 media backend -endpoint = "https://s3-us-west-2.amazonaws.com" -access_key = "" -secret_access_key = "" -region = "us-west-2" +endpoint = "myhostname.banana.com" +access_key = "_____________" +secret_access_key = "_________________" +region = "" bucket_name = "lysand" -public_url = "https://cdn.example.com" +public_url = "https://cdn.test.com" [email] # Sends an email to moderators when a report is received -# NOT IMPLEMENTED send_on_report = false # Sends an email to moderators when a user is suspended -# NOT IMPLEMENTED send_on_suspend = false # Sends an email to moderators when a user is unsuspended -# NOT IMPLEMENTED send_on_unsuspend = false +# Verify user emails when signing up (except via OIDC) +verify_email = false [validation] -# Self explanatory +# Checks user data +# Does not retroactively apply to previously entered data max_displayname_size = 50 max_bio_size = 160 max_note_size = 5000 @@ -115,7 +151,7 @@ max_poll_option_size = 500 min_poll_duration = 60 max_poll_duration = 1893456000 max_username_size = 30 -# An array of strings, defaults are from Akkoma +# Forbidden usernames, defaults are from Akkoma username_blacklist = [ ".well-known", "~", @@ -146,7 +182,7 @@ username_blacklist = [ ] # Whether to blacklist known temporary email providers blacklist_tempmail = false -# Additional email providers to blacklist +# Additional email providers to blacklist (list of domains) email_blacklist = [] # Valid URL schemes, otherwise the URL is parsed as text url_scheme_whitelist = [ @@ -167,8 +203,10 @@ url_scheme_whitelist = [ "mumble", "ssb", "gemini", -] # NOT IMPLEMENTED - +] +# Only allow those MIME types of data to be uploaded +# This can easily be spoofed, but if it is spoofed it will appear broken +# to normal clients until despoofed enforce_mime_types = false allowed_mime_types = [ "image/jpeg", @@ -203,46 +241,38 @@ allowed_mime_types = [ [defaults] # Default visibility for new notes +# Can be public, unlisted, private or direct +# Private only sends to followers, unlisted doesn't show up in timelines visibility = "public" -# Default language for new notes +# Default language for new notes (ISO code) language = "en" -# Default avatar, must be a valid URL or "" +# Default avatar, must be a valid URL or "" for none avatar = "" -# Default header, must be a valid URL or "" +# Default header, must be a valid URL or "" for none header = "" -[activitypub] -# Use ActivityPub Tombstones instead of deleting objects -use_tombstones = true -# Fetch all members of collections (followers, following, etc) when receiving them -# WARNING: This can be a lot of data, and is not recommended -fetch_all_collection_members = false # NOT IMPLEMENTED +[federation] +# This is a list of domain names, such as "mastodon.social" or "pleroma.site" +# These changes will not retroactively apply to existing data before they were changed +# For that, please use the CLI -# The following values must be instance domain names without "https" or glob patterns -# Rejects all activities from these instances (fediblocking) -reject_activities = [] -# Force posts from this instance to be followers only -force_followers_only = [] # NOT IMPLEMENTED -# Discard all reports from these instances -discard_reports = [] # NOT IMPLEMENTED -# Discard all deletes from these instances -discard_deletes = [] -# Discard all updates (edits) from these instances -discard_updates = [] -# Discard all banners from these instances -discard_banners = [] # NOT IMPLEMENTED -# Discard all avatars from these instances -discard_avatars = [] # NOT IMPLEMENTED -# Discard all follow requests from these instances -discard_follows = [] -# Force set these instances' media as sensitive -force_sensitive = [] # NOT IMPLEMENTED -# Remove theses instances' media -remove_media = [] # NOT IMPLEMENTED +# These instances will not be federated with +blocked = [] +# These instances' data will only be shown to followers, not in public timelines +followers_only = [] -# Whether to verify HTTP signatures for every request (warning: can slow down your server -# significantly depending on processing power) -authorized_fetch = false +[federation.discard] +# These objects will be discarded when received from these instances +reports = [] +deletes = [] +updates = [] +media = [] +follows = [] +# If instance reactions are blocked, likes will also be discarded +likes = [] +reactions = [] +banners = [] +avatars = [] [instance] name = "Lysand" @@ -254,22 +284,23 @@ banner = "" [filters] -# Drop notes with these regex filters (only applies to new activities) -note_filters = [ +# Regex filters for federated and local data +# Does not apply retroactively (try the CLI for that) + +# Note contents +note_content = [ # "(https?://)?(www\\.)?youtube\\.com/watch\\?v=[a-zA-Z0-9_-]+", # "(https?://)?(www\\.)?youtu\\.be/[a-zA-Z0-9_-]+", ] -# Drop users with these regex filters (only applies to new activities) -username_filters = [] -# Drop users with these regex filters (only applies to new activities) -displayname_filters = [] -# Drop users with these regex filters (only applies to new activities) -bio_filters = [] -emoji_filters = [] # NOT IMPLEMENTED +emoji = [] +# These will drop users matching the filters +username = [] +displayname = [] +bio = [] [logging] # Log all requests (warning: this is a lot of data) -log_requests = true +log_requests = false # Log request and their contents (warning: this is a lot of data) log_requests_verbose = false # For GDPR compliance, you can disable logging of IPs @@ -278,12 +309,19 @@ log_ip = false # Log all filtered objects log_filters = true +[logging.storage] +# Path to logfile for requests +requests = "logs/requests.log" + [ratelimits] +# These settings apply to every route at once # Amount to multiply every route's duration by duration_coeff = 1.0 -# Amount to multiply every route's max by +# Amount to multiply every route's max requests per [duration] by max_coeff = 1.0 [custom_ratelimits] # Add in any API route in this style here +# Applies before the global ratelimit changes +"/api/v1/accounts/:id/block" = { duration = 30, max = 60 } "/api/v1/timelines/public" = { duration = 60, max = 200 } diff --git a/database/datasource.ts b/database/datasource.ts index fc8078ae..05a32cad 100644 --- a/database/datasource.ts +++ b/database/datasource.ts @@ -1,8 +1,6 @@ // import { Queue } from "bullmq"; import { PrismaClient } from "@prisma/client"; -import { ConfigManager } from "config-manager"; - -const config = await new ConfigManager({}).getConfig(); +import { config } from "config-manager"; const client = new PrismaClient({ datasourceUrl: `postgresql://${config.database.username}:${config.database.password}@${config.database.host}:${config.database.port}/${config.database.database}`, diff --git a/database/entities/Like.ts b/database/entities/Like.ts index 037fd547..ea6f726e 100644 --- a/database/entities/Like.ts +++ b/database/entities/Like.ts @@ -4,9 +4,7 @@ import type { Like } from "@prisma/client"; import { client } from "~database/datasource"; import type { UserWithRelations } from "./User"; import type { StatusWithRelations } from "./Status"; -import { ConfigManager } from "config-manager"; - -const config = await new ConfigManager({}).getConfig(); +import { config } from "config-manager"; /** * Represents a Like entity in the database. diff --git a/database/entities/Queue.ts b/database/entities/Queue.ts index c5775142..570d79de 100644 --- a/database/entities/Queue.ts +++ b/database/entities/Queue.ts @@ -1,9 +1,7 @@ // import { Worker } from "bullmq"; import { statusToLysand, type StatusWithRelations } from "./Status"; import type { User } from "@prisma/client"; -import { ConfigManager } from "config-manager"; - -const config = await new ConfigManager({}).getConfig(); +import { config } from "config-manager"; /* export const federationWorker = new Worker( "federation", diff --git a/database/entities/Status.ts b/database/entities/Status.ts index a0fec241..6eb4c6f8 100644 --- a/database/entities/Status.ts +++ b/database/entities/Status.ts @@ -23,11 +23,9 @@ import { parse } from "marked"; import linkifyStr from "linkify-string"; import linkifyHtml from "linkify-html"; import { addStausToMeilisearch } from "@meilisearch"; -import { ConfigManager } from "config-manager"; +import { config } from "config-manager"; import { statusAndUserRelations, userRelations } from "./relations"; -const config = await new ConfigManager({}).getConfig(); - const statusRelations = Prisma.validator()({ include: statusAndUserRelations, }); diff --git a/database/entities/User.ts b/database/entities/User.ts index 1b06b1cf..141ab851 100644 --- a/database/entities/User.ts +++ b/database/entities/User.ts @@ -1,5 +1,5 @@ import type { APIAccount } from "~types/entities/account"; -import type { LysandUser as LysandUser } from "~types/lysand/Object"; +import type { LysandUser } from "~types/lysand/Object"; import { htmlToText } from "html-to-text"; import type { User } from "@prisma/client"; import { Prisma } from "@prisma/client"; @@ -8,13 +8,10 @@ import { addEmojiIfNotExists, emojiToAPI, emojiToLysand } from "./Emoji"; import { addInstanceIfNotExists } from "./Instance"; import type { APISource } from "~types/entities/source"; import { addUserToMeilisearch } from "@meilisearch"; -import { ConfigManager, type ConfigType } from "config-manager"; +import { config, type Config } from "config-manager"; import { userRelations } from "./relations"; import { MediaBackendType } from "~packages/media-manager"; -const configManager = new ConfigManager({}); -const config = await configManager.getConfig(); - export interface AuthData { user: UserWithRelations | null; token: string; @@ -36,7 +33,7 @@ export type UserWithRelations = Prisma.UserGetPayload; * @param config The config to use * @returns The raw URL for the user's avatar */ -export const getAvatarUrl = (user: User, config: ConfigType) => { +export const getAvatarUrl = (user: User, config: Config) => { if (!user.avatar) return config.defaults.avatar; if (config.media.backend === MediaBackendType.LOCAL) { return `${config.http.base_url}/media/${user.avatar}`; @@ -52,7 +49,7 @@ export const getAvatarUrl = (user: User, config: ConfigType) => { * @param config The config to use * @returns The raw URL for the user's header */ -export const getHeaderUrl = (user: User, config: ConfigType) => { +export const getHeaderUrl = (user: User, config: Config) => { if (!user.header) return config.defaults.header; if (config.media.backend === MediaBackendType.LOCAL) { return `${config.http.base_url}/media/${user.header}`; @@ -192,8 +189,6 @@ export const createNewLocalUser = async (data: { header?: string; admin?: boolean; }) => { - const config = await configManager.getConfig(); - const keys = await generateUserKeys(); const user = await client.user.create({ diff --git a/index.ts b/index.ts index 6006e96e..f27cc66c 100644 --- a/index.ts +++ b/index.ts @@ -1,7 +1,7 @@ import type { PrismaClientInitializationError } from "@prisma/client/runtime/library"; import { initializeRedisCache } from "@redis"; import { connectMeili } from "@meilisearch"; -import { ConfigManager } from "config-manager"; +import { config } from "config-manager"; import { client } from "~database/datasource"; import { LogLevel, LogManager, MultiLogManager } from "log-manager"; import { moduleIsEntry } from "@module"; @@ -10,9 +10,6 @@ import { exists, mkdir } from "fs/promises"; const timeAtStart = performance.now(); -const configManager = new ConfigManager({}); -const config = await configManager.getConfig(); - const requests_log = Bun.file(process.cwd() + "/logs/requests.log"); const isEntry = moduleIsEntry(import.meta.url); // If imported as a module, redirect logs to /dev/null to not pollute console (e.g. in tests) @@ -22,7 +19,7 @@ const consoleLogger = new LogManager( ); const dualLogger = new MultiLogManager([logger, consoleLogger]); -if (!(await exists(process.cwd() + "/logs/"))) { +if (!(await exists(config.logging.storage.requests))) { await consoleLogger.log( LogLevel.WARNING, "Lysand", @@ -59,7 +56,7 @@ try { process.exit(1); } -const server = createServer(config, configManager, dualLogger, isProd); +const server = createServer(config, dualLogger, isProd); await dualLogger.log( LogLevel.INFO, diff --git a/package.json b/package.json index 120ebae9..48f42842 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,8 @@ "@typescript-eslint/eslint-plugin": "latest", "@typescript-eslint/parser": "latest", "@unocss/cli": "latest", + "@vitejs/plugin-vue": "latest", + "@vueuse/head": "^2.0.0", "activitypub-types": "^1.0.3", "bun-types": "latest", "eslint": "^8.54.0", @@ -71,8 +73,7 @@ "prettier": "^3.1.0", "typescript": "latest", "unocss": "latest", - "@vitejs/plugin-vue": "latest", - "@vueuse/head": "^2.0.0", + "untyped": "^1.4.2", "vite": "latest", "vite-ssr": "^0.17.1", "vue": "^3.3.9", @@ -86,11 +87,12 @@ "@aws-sdk/client-s3": "^3.461.0", "@iarna/toml": "^2.2.5", "@json2csv/plainjs": "^7.0.6", - "cli-parser": "workspace:*", "@prisma/client": "^5.6.0", "blurhash": "^2.0.5", "bullmq": "latest", + "c12": "^1.10.0", "chalk": "^5.3.0", + "cli-parser": "workspace:*", "cli-table": "^0.3.11", "config-manager": "workspace:*", "eventemitter3": "^5.0.1", diff --git a/packages/config-manager/config-type.type.ts b/packages/config-manager/config-type.type.ts deleted file mode 100644 index b9a13306..00000000 --- a/packages/config-manager/config-type.type.ts +++ /dev/null @@ -1,377 +0,0 @@ -import { MediaBackendType } from "media-manager"; - -export interface ConfigType { - database: { - host: string; - port: number; - username: string; - password: string; - database: string; - }; - - redis: { - queue: { - host: string; - port: number; - password: string; - database: number | null; - }; - cache: { - host: string; - port: number; - password: string; - database: number | null; - enabled: boolean; - }; - }; - - meilisearch: { - host: string; - port: number; - api_key: string; - enabled: boolean; - }; - - signups: { - tos_url: string; - rules: string[]; - registration: boolean; - }; - - oidc: { - providers: { - name: string; - id: string; - url: string; - client_id: string; - client_secret: string; - icon: string; - }[]; - }; - - http: { - base_url: string; - bind: string; - bind_port: string; - banned_ips: string[]; - banned_user_agents: string[]; - bait: { - enabled: boolean; - send_file?: string; - bait_ips: string[]; - bait_user_agents: string[]; - }; - }; - - instance: { - name: string; - description: string; - banner: string; - logo: string; - }; - - smtp: { - server: string; - port: number; - username: string; - password: string; - tls: boolean; - }; - - validation: { - max_displayname_size: number; - max_bio_size: number; - max_username_size: number; - max_note_size: number; - max_avatar_size: number; - max_header_size: number; - max_media_size: number; - max_media_attachments: number; - max_media_description_size: number; - max_poll_options: number; - max_poll_option_size: number; - min_poll_duration: number; - max_poll_duration: number; - - username_blacklist: string[]; - blacklist_tempmail: boolean; - email_blacklist: string[]; - url_scheme_whitelist: string[]; - - enforce_mime_types: boolean; - allowed_mime_types: string[]; - }; - - media: { - backend: MediaBackendType; - deduplicate_media: boolean; - conversion: { - convert_images: boolean; - convert_to: string; - }; - local_uploads_folder: string; - }; - - s3: { - endpoint: string; - access_key: string; - secret_access_key: string; - region: string; - bucket_name: string; - public_url: string; - }; - - defaults: { - visibility: string; - language: string; - avatar: string; - header: string; - }; - - email: { - send_on_report: boolean; - send_on_suspend: boolean; - send_on_unsuspend: boolean; - }; - - activitypub: { - use_tombstones: boolean; - reject_activities: string[]; - force_followers_only: string[]; - discard_reports: string[]; - discard_deletes: string[]; - discard_banners: string[]; - discard_avatars: string[]; - discard_updates: string[]; - discard_follows: string[]; - force_sensitive: string[]; - remove_media: string[]; - fetch_all_collection_members: boolean; - authorized_fetch: boolean; - }; - - filters: { - note_filters: string[]; - username_filters: string[]; - displayname_filters: string[]; - bio_filters: string[]; - emoji_filters: string[]; - }; - - logging: { - log_requests: boolean; - log_requests_verbose: boolean; - log_ip: boolean; - log_filters: boolean; - }; - - ratelimits: { - duration_coeff: number; - max_coeff: number; - }; - - custom_ratelimits: Record< - string, - { - duration: number; - max: number; - } - >; - [key: string]: unknown; -} - -export const configDefaults: ConfigType = { - http: { - bind: "http://0.0.0.0", - bind_port: "8000", - base_url: "http://lysand.localhost:8000", - banned_ips: [], - banned_user_agents: [], - bait: { - enabled: false, - send_file: "", - bait_ips: [], - bait_user_agents: [], - }, - }, - database: { - host: "localhost", - port: 5432, - username: "postgres", - password: "postgres", - database: "lysand", - }, - redis: { - queue: { - host: "localhost", - port: 6379, - password: "", - database: 0, - }, - cache: { - host: "localhost", - port: 6379, - password: "", - database: 1, - enabled: false, - }, - }, - meilisearch: { - host: "localhost", - port: 1491, - api_key: "", - enabled: false, - }, - signups: { - tos_url: "", - rules: [], - registration: false, - }, - oidc: { - providers: [], - }, - instance: { - banner: "", - description: "", - logo: "", - name: "", - }, - smtp: { - password: "", - port: 465, - server: "", - tls: true, - username: "", - }, - media: { - backend: MediaBackendType.LOCAL, - deduplicate_media: true, - conversion: { - convert_images: false, - convert_to: "webp", - }, - local_uploads_folder: "uploads", - }, - email: { - send_on_report: false, - send_on_suspend: false, - send_on_unsuspend: false, - }, - s3: { - access_key: "", - bucket_name: "", - endpoint: "", - public_url: "", - region: "", - secret_access_key: "", - }, - validation: { - max_displayname_size: 50, - max_bio_size: 6000, - max_note_size: 5000, - max_avatar_size: 5_000_000, - max_header_size: 5_000_000, - max_media_size: 40_000_000, - max_media_attachments: 10, - max_media_description_size: 1000, - max_poll_options: 20, - max_poll_option_size: 500, - min_poll_duration: 60, - max_poll_duration: 1893456000, - max_username_size: 30, - - username_blacklist: [ - ".well-known", - "~", - "about", - "activities", - "api", - "auth", - "dev", - "inbox", - "internal", - "main", - "media", - "nodeinfo", - "notice", - "oauth", - "objects", - "proxy", - "push", - "registration", - "relay", - "settings", - "status", - "tag", - "users", - "web", - "search", - "mfa", - ], - - blacklist_tempmail: false, - - email_blacklist: [], - - url_scheme_whitelist: [ - "http", - "https", - "ftp", - "dat", - "dweb", - "gopher", - "hyper", - "ipfs", - "ipns", - "irc", - "xmpp", - "ircs", - "magnet", - "mailto", - "mumble", - "ssb", - ], - - enforce_mime_types: false, - allowed_mime_types: [], - }, - defaults: { - visibility: "public", - language: "en", - avatar: "", - header: "", - }, - activitypub: { - use_tombstones: true, - reject_activities: [], - force_followers_only: [], - discard_reports: [], - discard_deletes: [], - discard_banners: [], - discard_avatars: [], - force_sensitive: [], - discard_updates: [], - discard_follows: [], - remove_media: [], - fetch_all_collection_members: false, - authorized_fetch: false, - }, - filters: { - note_filters: [], - username_filters: [], - displayname_filters: [], - bio_filters: [], - emoji_filters: [], - }, - logging: { - log_requests: false, - log_requests_verbose: false, - log_ip: false, - log_filters: true, - }, - ratelimits: { - duration_coeff: 1, - max_coeff: 1, - }, - custom_ratelimits: {}, -}; diff --git a/packages/config-manager/config.type.ts b/packages/config-manager/config.type.ts new file mode 100644 index 00000000..8a0fc162 --- /dev/null +++ b/packages/config-manager/config.type.ts @@ -0,0 +1,579 @@ +import { MediaBackendType } from "~packages/media-manager"; + +export interface Config { + database: { + /** @default "localhost" */ + host: string; + + /** @default 5432 */ + port: number; + + /** @default "lysand" */ + username: string; + + /** @default "lysand" */ + password: string; + + /** @default "lysand" */ + database: string; + }; + + redis: { + queue: { + /** @default "localhost" */ + host: string; + + /** @default 6379 */ + port: number; + + /** @default "" */ + password: string; + + /** @default 0 */ + database: number; + }; + + cache: { + /** @default "localhost" */ + host: string; + + /** @default 6379 */ + port: number; + + /** @default "" */ + password: string; + + /** @default 1 */ + database: number; + + /** @default false */ + enabled: boolean; + }; + }; + + meilisearch: { + /** @default "localhost" */ + host: string; + + /** @default 7700 */ + port: number; + + /** @default "______________________________" */ + api_key: string; + + /** @default false */ + enabled: boolean; + }; + + signups: { + /** @default "https://my-site.com/tos" */ + tos_url: string; + + /** @default true */ + registration: boolean; + + /** @default ["Do not harass others","Be nice to people","Don't spam","Don't post illegal content"] */ + rules: string[]; + }; + + oidc: { + /** @default [] */ + providers: Record[]; + }; + + http: { + /** @default "https://lysand.social" */ + base_url: string; + + /** @default "0.0.0.0" */ + bind: string; + + /** @default "8080" */ + bind_port: string; + + banned_ips: any[]; + + banned_user_agents: any[]; + + bait: { + /** @default false */ + enabled: boolean; + + /** @default "" */ + send_file: string; + + /** @default ["127.0.0.1","::1"] */ + bait_ips: string[]; + + /** @default ["curl","wget"] */ + bait_user_agents: string[]; + }; + }; + + smtp: { + /** @default "smtp.example.com" */ + server: string; + + /** @default 465 */ + port: number; + + /** @default "test@example.com" */ + username: string; + + /** @default "____________" */ + password: string; + + /** @default true */ + tls: boolean; + + /** @default false */ + enabled: boolean; + }; + + media: { + /** @default "local" */ + backend: MediaBackendType; + + /** @default true */ + deduplicate_media: boolean; + + /** @default "uploads" */ + local_uploads_folder: string; + + conversion: { + /** @default false */ + convert_images: boolean; + + /** @default "webp" */ + convert_to: string; + }; + }; + + s3: { + /** @default "myhostname.banana.com" */ + endpoint: string; + + /** @default "_____________" */ + access_key: string; + + /** @default "_________________" */ + secret_access_key: string; + + /** @default "" */ + region: string; + + /** @default "lysand" */ + bucket_name: string; + + /** @default "https://cdn.test.com" */ + public_url: string; + }; + + email: { + /** @default false */ + send_on_report: boolean; + + /** @default false */ + send_on_suspend: boolean; + + /** @default false */ + send_on_unsuspend: boolean; + + /** @default false */ + verify_email: boolean; + }; + + validation: { + /** @default 50 */ + max_displayname_size: number; + + /** @default 160 */ + max_bio_size: number; + + /** @default 5000 */ + max_note_size: number; + + /** @default 5000000 */ + max_avatar_size: number; + + /** @default 5000000 */ + max_header_size: number; + + /** @default 40000000 */ + max_media_size: number; + + /** @default 10 */ + max_media_attachments: number; + + /** @default 1000 */ + max_media_description_size: number; + + /** @default 20 */ + max_poll_options: number; + + /** @default 500 */ + max_poll_option_size: number; + + /** @default 60 */ + min_poll_duration: number; + + /** @default 1893456000 */ + max_poll_duration: number; + + /** @default 30 */ + max_username_size: number; + + /** @default [".well-known","~","about","activities","api","auth","dev","inbox","internal","main","media","nodeinfo","notice","oauth","objects","proxy","push","registration","relay","settings","status","tag","users","web","search","mfa"] */ + username_blacklist: string[]; + + /** @default false */ + blacklist_tempmail: boolean; + + email_blacklist: any[]; + + /** @default ["http","https","ftp","dat","dweb","gopher","hyper","ipfs","ipns","irc","xmpp","ircs","magnet","mailto","mumble","ssb","gemini"] */ + url_scheme_whitelist: string[]; + + /** @default false */ + enforce_mime_types: boolean; + + /** @default ["image/jpeg","image/png","image/gif","image/heic","image/heif","image/webp","image/avif","video/webm","video/mp4","video/quicktime","video/ogg","audio/wave","audio/wav","audio/x-wav","audio/x-pn-wave","audio/vnd.wave","audio/ogg","audio/vorbis","audio/mpeg","audio/mp3","audio/webm","audio/flac","audio/aac","audio/m4a","audio/x-m4a","audio/mp4","audio/3gpp","video/x-ms-asf"] */ + allowed_mime_types: string[]; + }; + + defaults: { + /** @default "public" */ + visibility: string; + + /** @default "en" */ + language: string; + + /** @default "" */ + avatar: string; + + /** @default "" */ + header: string; + }; + + federation: { + blocked: any[]; + + followers_only: any[]; + + discard: { + reports: any[]; + + deletes: any[]; + + updates: any[]; + + media: any[]; + + follows: any[]; + + likes: any[]; + + reactions: any[]; + + banners: any[]; + + avatars: any[]; + }; + }; + + instance: { + /** @default "Lysand" */ + name: string; + + /** @default "A test instance of Lysand" */ + description: string; + + /** @default "" */ + logo: string; + + /** @default "" */ + banner: string; + }; + + filters: { + note_content: any[]; + + emoji: any[]; + + username: any[]; + + displayname: any[]; + + bio: any[]; + }; + + logging: { + /** @default false */ + log_requests: boolean; + + /** @default false */ + log_requests_verbose: boolean; + + /** @default false */ + log_ip: boolean; + + /** @default true */ + log_filters: boolean; + + storage: { + /** @default "logs/requests.log" */ + requests: string; + }; + }; + + ratelimits: { + /** @default 1 */ + duration_coeff: number; + + /** @default 1 */ + max_coeff: number; + }; + + /** @default {} */ + custom_ratelimits: Record< + string, + { + /** @default 30 */ + duration: number; + + /** @default 60 */ + max: number; + } + >; +} + +export const defaultConfig: Config = { + database: { + host: "localhost", + port: 5432, + username: "lysand", + password: "lysand", + database: "lysand", + }, + redis: { + queue: { + host: "localhost", + port: 6379, + password: "", + database: 0, + }, + cache: { + host: "localhost", + port: 6379, + password: "", + database: 1, + enabled: false, + }, + }, + meilisearch: { + host: "localhost", + port: 7700, + api_key: "______________________________", + enabled: false, + }, + signups: { + tos_url: "https://my-site.com/tos", + registration: true, + rules: [ + "Do not harass others", + "Be nice to people", + "Don't spam", + "Don't post illegal content", + ], + }, + oidc: { + providers: [[]], + }, + http: { + base_url: "https://lysand.social", + bind: "0.0.0.0", + bind_port: "8080", + banned_ips: [], + banned_user_agents: [], + bait: { + enabled: false, + send_file: "", + bait_ips: ["127.0.0.1", "::1"], + bait_user_agents: ["curl", "wget"], + }, + }, + smtp: { + server: "smtp.example.com", + port: 465, + username: "test@example.com", + password: "____________", + tls: true, + enabled: false, + }, + media: { + backend: MediaBackendType.LOCAL, + deduplicate_media: true, + local_uploads_folder: "uploads", + conversion: { + convert_images: false, + convert_to: "webp", + }, + }, + s3: { + endpoint: "myhostname.banana.com", + access_key: "_____________", + secret_access_key: "_________________", + region: "", + bucket_name: "lysand", + public_url: "https://cdn.test.com", + }, + email: { + send_on_report: false, + send_on_suspend: false, + send_on_unsuspend: false, + verify_email: false, + }, + validation: { + max_displayname_size: 50, + max_bio_size: 160, + max_note_size: 5000, + max_avatar_size: 5000000, + max_header_size: 5000000, + max_media_size: 40000000, + max_media_attachments: 10, + max_media_description_size: 1000, + max_poll_options: 20, + max_poll_option_size: 500, + min_poll_duration: 60, + max_poll_duration: 1893456000, + max_username_size: 30, + username_blacklist: [ + ".well-known", + "~", + "about", + "activities", + "api", + "auth", + "dev", + "inbox", + "internal", + "main", + "media", + "nodeinfo", + "notice", + "oauth", + "objects", + "proxy", + "push", + "registration", + "relay", + "settings", + "status", + "tag", + "users", + "web", + "search", + "mfa", + ], + blacklist_tempmail: false, + email_blacklist: [], + url_scheme_whitelist: [ + "http", + "https", + "ftp", + "dat", + "dweb", + "gopher", + "hyper", + "ipfs", + "ipns", + "irc", + "xmpp", + "ircs", + "magnet", + "mailto", + "mumble", + "ssb", + "gemini", + ], + enforce_mime_types: false, + allowed_mime_types: [ + "image/jpeg", + "image/png", + "image/gif", + "image/heic", + "image/heif", + "image/webp", + "image/avif", + "video/webm", + "video/mp4", + "video/quicktime", + "video/ogg", + "audio/wave", + "audio/wav", + "audio/x-wav", + "audio/x-pn-wave", + "audio/vnd.wave", + "audio/ogg", + "audio/vorbis", + "audio/mpeg", + "audio/mp3", + "audio/webm", + "audio/flac", + "audio/aac", + "audio/m4a", + "audio/x-m4a", + "audio/mp4", + "audio/3gpp", + "video/x-ms-asf", + ], + }, + defaults: { + visibility: "public", + language: "en", + avatar: "", + header: "", + }, + federation: { + blocked: [], + followers_only: [], + discard: { + reports: [], + deletes: [], + updates: [], + media: [], + follows: [], + likes: [], + reactions: [], + banners: [], + avatars: [], + }, + }, + instance: { + name: "Lysand", + description: "A test instance of Lysand", + logo: "", + banner: "", + }, + filters: { + note_content: [], + emoji: [], + username: [], + displayname: [], + bio: [], + }, + logging: { + log_requests: false, + log_requests_verbose: false, + log_ip: false, + log_filters: true, + storage: { + requests: "logs/requests.log", + }, + }, + ratelimits: { + duration_coeff: 1, + max_coeff: 1, + }, + custom_ratelimits: {}, +}; diff --git a/packages/config-manager/index.ts b/packages/config-manager/index.ts index fb507489..27d2fb9b 100644 --- a/packages/config-manager/index.ts +++ b/packages/config-manager/index.ts @@ -5,122 +5,22 @@ * Fuses both and provides a way to retrieve individual values */ -import { parse, stringify, type JsonMap } from "@iarna/toml"; -import type { ConfigType } from "./config-type.type"; -import { configDefaults } from "./config-type.type"; -import merge from "merge-deep-ts"; +import { watchConfig } from "c12"; +import { defaultConfig, type Config } from "./config.type"; -export class ConfigManager { - constructor( - public config: { - configPathOverride?: string; - internalConfigPathOverride?: string; - } - ) {} +const { config } = await watchConfig({ + configFile: "./config/config.toml", + defaultConfig: defaultConfig, + overrides: + ( + await watchConfig({ + configFile: "./config/config.internal.toml", + defaultConfig: {} as Config, + }) + ).config ?? undefined, +}); - /** - * @summary Reads the config files and returns the merge as a JSON object - * @returns {Promise} The merged config file as a JSON object - */ - async getConfig() { - const config = await this.readConfig(); - const internalConfig = await this.readInternalConfig(); +const exportedConfig = config ?? defaultConfig; - return this.mergeConfigs( - configDefaults as T, - config, - internalConfig - ); - } - - getConfigPath() { - return ( - this.config.configPathOverride || - process.cwd() + "/config/config.toml" - ); - } - - getInternalConfigPath() { - return ( - this.config.internalConfigPathOverride || - process.cwd() + "/config/config.internal.toml" - ); - } - - /** - * @summary Reads the internal config file and returns it as a JSON object - * @returns {Promise} The internal config file as a JSON object - */ - private async readInternalConfig() { - const config = Bun.file(this.getInternalConfigPath()); - - if (!(await config.exists())) { - await Bun.write(config, ""); - } - - return this.parseConfig(await config.text()); - } - - /** - * @summary Reads the config file and returns it as a JSON object - * @returns {Promise} The config file as a JSON object - */ - private async readConfig() { - const config = Bun.file(this.getConfigPath()); - - if (!(await config.exists())) { - throw new Error( - `Error while reading config at path ${this.getConfigPath()}: Config file not found` - ); - } - - return this.parseConfig(await config.text()); - } - - /** - * @summary Parses a TOML string and returns it as a JSON object - * @param text The TOML string to parse - * @returns {T = ConfigType} The parsed TOML string as a JSON object - * @throws {Error} If the TOML string is invalid - * @private - */ - private parseConfig(text: string) { - try { - // To all [Symbol] keys from the object - return JSON.parse(JSON.stringify(parse(text))) as T; - } catch (e: any) { - throw new Error( - `Error while parsing config at path ${this.getConfigPath()}: ${e}` - ); - } - } - - /** - * Writes changed values to the internal config - * @param config The new config object - */ - async writeConfig(config: T) { - const path = this.getInternalConfigPath(); - const file = Bun.file(path); - - await Bun.write( - file, - `# THIS FILE IS AUTOMATICALLY GENERATED. DO NOT EDIT IT MANUALLY, EDIT THE STANDARD CONFIG.TOML INSTEAD.\n${stringify( - config as JsonMap - )}` - ); - } - - /** - * @summary Merges two config objects together, with - * the latter configs' values taking precedence - * @param configs - * @returns - */ - private mergeConfigs(...configs: T[]) { - return merge(configs) as T; - } -} - -export type { ConfigType }; -export const defaultConfig = configDefaults; +export { exportedConfig as config }; +export type { Config }; diff --git a/packages/config-manager/tests/config-manager.test.ts b/packages/config-manager/tests/config-manager.test.ts deleted file mode 100644 index 9ae1bb06..00000000 --- a/packages/config-manager/tests/config-manager.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -// FILEPATH: /home/jessew/Dev/lysand/packages/config-manager/config-manager.test.ts -import { stringify } from "@iarna/toml"; -import { ConfigManager } from ".."; -import { describe, beforeEach, spyOn, it, expect } from "bun:test"; - -describe("ConfigManager", () => { - let configManager: ConfigManager; - - beforeEach(() => { - configManager = new ConfigManager({ - configPathOverride: "./config/config.toml", - internalConfigPathOverride: "./config/config.internal.toml", - }); - }); - - it("should get the correct config path", () => { - expect(configManager.getConfigPath()).toEqual("./config/config.toml"); - }); - - it("should get the correct internal config path", () => { - expect(configManager.getInternalConfigPath()).toEqual( - "./config/config.internal.toml" - ); - }); - - it("should read the config file correctly", async () => { - const mockConfig = { key: "value" }; - - // @ts-expect-error This is a mock - spyOn(Bun, "file").mockImplementationOnce(() => ({ - exists: () => - new Promise(resolve => { - resolve(true); - }), - text: () => - new Promise(resolve => { - resolve(stringify(mockConfig)); - }), - })); - - const config = await configManager.getConfig(); - - expect(config).toContainKeys(Object.keys(mockConfig)); - }); - - it("should read the internal config file correctly", async () => { - const mockConfig = { key: "value" }; - - // @ts-expect-error This is a mock - spyOn(Bun, "file").mockImplementationOnce(() => ({ - exists: () => - new Promise(resolve => { - resolve(true); - }), - text: () => - new Promise(resolve => { - resolve(stringify(mockConfig)); - }), - })); - - const config = - // @ts-expect-error Force call private function for testing - await configManager.readInternalConfig(); - - expect(config).toEqual(mockConfig); - }); - - it("should write to the internal config file correctly", async () => { - const mockConfig = { key: "value" }; - - spyOn(Bun, "write").mockImplementationOnce( - () => - new Promise(resolve => { - resolve(10); - }) - ); - - await configManager.writeConfig(mockConfig); - }); - - it("should merge configs correctly", () => { - const config1 = { key1: "value1", key2: "value2" }; - const config2 = { key2: "newValue2", key3: "value3" }; - // @ts-expect-error Force call private function for testing - const mergedConfig = configManager.mergeConfigs>( - config1, - config2 - ); - - expect(mergedConfig).toEqual({ - key1: "value1", - key2: "newValue2", - key3: "value3", - }); - }); -}); diff --git a/prisma.ts b/prisma.ts index 63566b3d..40312282 100644 --- a/prisma.ts +++ b/prisma.ts @@ -1,7 +1,6 @@ -import { ConfigManager } from "config-manager"; +import { config } from "config-manager"; // Proxies all `bunx prisma` commands with an environment variable -const config = await new ConfigManager({}).getConfig(); process.stdout.write( `postgresql://${config.database.username}:${config.database.password}@${config.database.host}:${config.database.port}/${config.database.database}\n` diff --git a/server.ts b/server.ts index 3a3f1c99..4ad77a76 100644 --- a/server.ts +++ b/server.ts @@ -1,15 +1,14 @@ import { errorResponse, jsonResponse } from "@response"; import { matches } from "ip-matching"; import { getFromRequest } from "~database/entities/User"; -import type { ConfigManager, ConfigType } from "config-manager"; +import { type Config } from "config-manager"; import type { LogManager, MultiLogManager } from "log-manager"; import { LogLevel } from "log-manager"; import { RequestParser } from "request-parser"; import { matchRoute } from "~routes"; export const createServer = ( - config: ConfigType, - configManager: ConfigManager, + config: Config, logger: LogManager | MultiLogManager, isProd: boolean ) => @@ -182,8 +181,11 @@ export const createServer = ( return await file.default(req.clone(), matchedRoute, { auth, - configManager, parsedRequest, + // To avoid having to rewrite each route + configManager: { + getConfig: () => Promise.resolve(config), + }, }); } else if (matchedRoute?.name === "/[...404]" || !matchedRoute) { if (new URL(req.url).pathname.startsWith("/api")) { diff --git a/server/api/api/v1/accounts/update_credentials/index.ts b/server/api/api/v1/accounts/update_credentials/index.ts index 5eaa20bf..18be18f9 100644 --- a/server/api/api/v1/accounts/update_credentials/index.ts +++ b/server/api/api/v1/accounts/update_credentials/index.ts @@ -103,7 +103,7 @@ export default apiRoute<{ // Check if display name doesnt match filters if ( - config.filters.displayname_filters.some(filter => + config.filters.displayname.some(filter => sanitizedDisplayName.match(filter) ) ) { @@ -126,11 +126,7 @@ export default apiRoute<{ } // Check if bio doesnt match filters - if ( - config.filters.bio_filters.some(filter => - sanitizedNote.match(filter) - ) - ) { + if (config.filters.bio.some(filter => sanitizedNote.match(filter))) { return errorResponse("Bio contains blocked words", 422); } diff --git a/server/api/api/v1/statuses/[id]/index.ts b/server/api/api/v1/statuses/[id]/index.ts index 31e10e83..39810f2c 100644 --- a/server/api/api/v1/statuses/[id]/index.ts +++ b/server/api/api/v1/statuses/[id]/index.ts @@ -89,7 +89,7 @@ export default apiRoute<{ content_type, "poll[expires_in]": expires_in, "poll[options]": options, - media_ids: media_ids, + media_ids, spoiler_text, sensitive, } = extraData.parsedRequest; @@ -181,7 +181,7 @@ export default apiRoute<{ // Check if status body doesnt match filters if ( - config.filters.note_filters.some(filter => + config.filters.note_content.some(filter => statusText?.match(filter) ) ) { diff --git a/server/api/api/v1/statuses/index.ts b/server/api/api/v1/statuses/index.ts index 418c0833..8e4e8674 100644 --- a/server/api/api/v1/statuses/index.ts +++ b/server/api/api/v1/statuses/index.ts @@ -194,7 +194,7 @@ export default apiRoute<{ } // Check if status body doesnt match filters - if (config.filters.note_filters.some(filter => status?.match(filter))) { + if (config.filters.note_content.some(filter => status?.match(filter))) { return errorResponse("Status contains blocked words", 422); } diff --git a/server/api/routes.type.ts b/server/api/routes.type.ts index d3cea716..b3ff1ef7 100644 --- a/server/api/routes.type.ts +++ b/server/api/routes.type.ts @@ -1,5 +1,5 @@ import type { MatchedRoute } from "bun"; -import type { ConfigManager } from "config-manager"; +import type { Config } from "config-manager"; import type { AuthData } from "~database/entities/User"; export type RouteHandler = ( @@ -8,6 +8,8 @@ export type RouteHandler = ( extraData: { auth: AuthData; parsedRequest: Partial; - configManager: ConfigManager; + configManager: { + getConfig: () => Promise; + }; } ) => Response | Promise; diff --git a/tests/api.test.ts b/tests/api.test.ts index 3416c9b3..54c1958a 100644 --- a/tests/api.test.ts +++ b/tests/api.test.ts @@ -1,6 +1,6 @@ import type { Token } from "@prisma/client"; import { afterAll, beforeAll, describe, expect, test } from "bun:test"; -import { ConfigManager } from "config-manager"; +import { config } from "config-manager"; import { client } from "~database/datasource"; import { TokenType } from "~database/entities/Token"; import { @@ -11,7 +11,6 @@ import type { APIEmoji } from "~types/entities/emoji"; import type { APIInstance } from "~types/entities/instance"; import { sendTestRequest, wrapRelativeUrl } from "./utils"; -const config = await new ConfigManager({}).getConfig(); const base_url = config.http.base_url; let token: Token; diff --git a/tests/api/accounts.test.ts b/tests/api/accounts.test.ts index 201d1066..a62ebc43 100644 --- a/tests/api/accounts.test.ts +++ b/tests/api/accounts.test.ts @@ -9,10 +9,9 @@ import { import type { APIAccount } from "~types/entities/account"; import type { APIRelationship } from "~types/entities/relationship"; import type { APIStatus } from "~types/entities/status"; -import { ConfigManager } from "config-manager"; +import { config } from "config-manager"; import { sendTestRequest, wrapRelativeUrl } from "~tests/utils"; -const config = await new ConfigManager({}).getConfig(); const base_url = config.http.base_url; let token: Token; diff --git a/tests/api/statuses.test.ts b/tests/api/statuses.test.ts index 94bd9287..4f65dcd3 100644 --- a/tests/api/statuses.test.ts +++ b/tests/api/statuses.test.ts @@ -10,10 +10,9 @@ import type { APIAccount } from "~types/entities/account"; import type { APIAsyncAttachment } from "~types/entities/async_attachment"; import type { APIContext } from "~types/entities/context"; import type { APIStatus } from "~types/entities/status"; -import { ConfigManager } from "config-manager"; +import { config } from "config-manager"; import { sendTestRequest, wrapRelativeUrl } from "~tests/utils"; -const config = await new ConfigManager({}).getConfig(); const base_url = config.http.base_url; let token: Token; diff --git a/tests/oauth.test.ts b/tests/oauth.test.ts index 9d56fa97..d8b1b41d 100644 --- a/tests/oauth.test.ts +++ b/tests/oauth.test.ts @@ -4,7 +4,6 @@ import { client } from "~database/datasource"; import { createNewLocalUser } from "~database/entities/User"; import { sendTestRequest, wrapRelativeUrl } from "./utils"; -// const config = await new ConfigManager({}).getConfig(); const base_url = "http://lysand.localhost:8080"; //config.http.base_url; let client_id: string; diff --git a/utils/api.ts b/utils/api.ts index 283cf136..d197aa74 100644 --- a/utils/api.ts +++ b/utils/api.ts @@ -1,9 +1,7 @@ -import { ConfigManager } from "config-manager"; +import { config } from "config-manager"; import type { RouteHandler } from "~server/api/routes.type"; import type { APIRouteMeta } from "~types/api"; -const config = await new ConfigManager({}).getConfig(); - export const applyConfig = (routeMeta: APIRouteMeta) => { const newMeta = routeMeta; diff --git a/utils/constants.ts b/utils/constants.ts index 4de00425..29b479bf 100644 --- a/utils/constants.ts +++ b/utils/constants.ts @@ -1,6 +1,4 @@ -import { ConfigManager } from "config-manager"; - -const config = await new ConfigManager({}).getConfig(); +import { config } from "config-manager"; export const oauthRedirectUri = (issuer: string) => `${config.http.base_url}/oauth/callback/${issuer}`; diff --git a/utils/meilisearch.ts b/utils/meilisearch.ts index e885ddbc..bfde2890 100644 --- a/utils/meilisearch.ts +++ b/utils/meilisearch.ts @@ -2,11 +2,9 @@ import chalk from "chalk"; import { client } from "~database/datasource"; import { Meilisearch } from "meilisearch"; import type { Status, User } from "@prisma/client"; -import { ConfigManager } from "config-manager"; +import { config } from "config-manager"; import { LogLevel, type LogManager, type MultiLogManager } from "log-manager"; -const config = await new ConfigManager({}).getConfig(); - export const meilisearch = new Meilisearch({ host: `${config.meilisearch.host}:${config.meilisearch.port}`, apiKey: config.meilisearch.api_key, diff --git a/utils/redis.ts b/utils/redis.ts index e6d61910..0f3714bb 100644 --- a/utils/redis.ts +++ b/utils/redis.ts @@ -1,17 +1,15 @@ import type { Prisma } from "@prisma/client"; import chalk from "chalk"; -import { ConfigManager } from "config-manager"; +import { config } from "config-manager"; import Redis from "ioredis"; import { createPrismaRedisCache } from "prisma-redis-middleware"; -const config = await new ConfigManager({}).getConfig(); - const cacheRedis = config.redis.cache.enabled ? new Redis({ host: config.redis.cache.host, port: Number(config.redis.cache.port), password: config.redis.cache.password, - db: Number(config.redis.cache.database ?? 0), + db: Number(config.redis.cache.database), }) : null; diff --git a/utils/sanitization.ts b/utils/sanitization.ts index e2426b95..89f85b08 100644 --- a/utils/sanitization.ts +++ b/utils/sanitization.ts @@ -1,9 +1,7 @@ -import { ConfigManager } from "config-manager"; +import { config } from "config-manager"; import { sanitize } from "isomorphic-dompurify"; export const sanitizeHtml = async (html: string) => { - const config = await new ConfigManager({}).getConfig(); - const sanitizedHtml = sanitize(html, { ALLOWED_TAGS: [ "a",