diff --git a/.docker/development/docker-compose.yml b/.docker/development/docker-compose.yml index e094d89..01f76d6 100644 --- a/.docker/development/docker-compose.yml +++ b/.docker/development/docker-compose.yml @@ -41,7 +41,7 @@ services: links: - postgres:postgres environment: - - DATABASE_URL=postgres://postgres:postgres@postgres:5432/postgres?sslmode=disable + - PGWEB_DATABASE_URL=postgres://postgres:postgres@postgres:5432/postgres?sslmode=disable depends_on: - postgres diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d0f0ca6..7458772 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,3 +1 @@ -* @movie-web/core - -.github @binaryoverload +* @movie-web/project-leads diff --git a/.github/workflows/linting_testing.yml b/.github/workflows/linting_testing.yml index ed9f673..8e98406 100644 --- a/.github/workflows/linting_testing.yml +++ b/.github/workflows/linting_testing.yml @@ -14,16 +14,16 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - uses: pnpm/action-setup@v2 with: version: 8 - name: Install Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 20 cache: 'pnpm' - name: Install packages @@ -38,16 +38,16 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - uses: pnpm/action-setup@v2 with: version: 8 - name: Install Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 20 cache: 'pnpm' - name: Install packages @@ -62,10 +62,10 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Build - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e342cb7..22da909 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Get version id: package-version @@ -42,10 +42,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Docker buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Get version id: package-version @@ -70,9 +70,12 @@ jobs: - name: Build and push Docker image id: build-and-push - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: push: true + platforms: linux/amd64,linux/arm64 context: . labels: ${{ steps.meta.outputs.labels }} tags: ${{ steps.meta.outputs.tags }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/package.json b/package.json index 80acfde..18b344d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "backend", - "version": "1.3.0", + "version": "1.3.1", "private": true, "homepage": "https://github.com/movie-web/backend", "engines": { diff --git a/src/config/orm.ts b/src/config/orm.ts index bfbe038..a226a20 100644 --- a/src/config/orm.ts +++ b/src/config/orm.ts @@ -2,6 +2,7 @@ import { devFragment } from '@/config/fragments/dev'; import { dockerFragment } from '@/config/fragments/docker'; import { createConfigLoader } from 'neat-config'; import { z } from 'zod'; +import { booleanSchema } from './schema'; const fragments = { dev: devFragment, @@ -13,7 +14,7 @@ export const ormConfigSchema = z.object({ // connection URL for postgres database connection: z.string(), // whether to use SSL for the connection - ssl: z.coerce.boolean().default(false), + ssl: booleanSchema.default(false), }), }); diff --git a/src/config/schema.ts b/src/config/schema.ts index d42327e..cb6d405 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -1,5 +1,7 @@ import { z } from 'zod'; +export const booleanSchema = z.preprocess((val) => val === 'true', z.boolean()); + export const configSchema = z.object({ server: z .object({ @@ -11,13 +13,13 @@ export const configSchema = z.object({ // disable cross origin restrictions, allow any site. // overwrites the cors option above - allowAnySite: z.coerce.boolean().default(false), + allowAnySite: booleanSchema.default(false), // should it trust reverse proxy headers? (for ip gathering) - trustProxy: z.coerce.boolean().default(false), + trustProxy: booleanSchema.default(false), // should it trust cloudflare headers? (for ip gathering, cloudflare has priority) - trustCloudflare: z.coerce.boolean().default(false), + trustCloudflare: booleanSchema.default(false), // prefix for where the instance is run on. for example set it to /backend if you're hosting it on example.com/backend // if this is set, do not apply url rewriting before proxing @@ -30,7 +32,7 @@ export const configSchema = z.object({ format: z.enum(['json', 'pretty']).default('pretty'), // show debug logs? - debug: z.coerce.boolean().default(false), + debug: booleanSchema.default(false), }) .default({}), postgres: z.object({ @@ -38,19 +40,19 @@ export const configSchema = z.object({ connection: z.string(), // run all migrations on boot of the application - migrateOnBoot: z.coerce.boolean().default(false), + migrateOnBoot: booleanSchema.default(false), // try to sync the schema on boot, useful for development // will always keep the database schema in sync with the connected database // it is extremely destructive, do not use it EVER in production - syncSchema: z.coerce.boolean().default(false), + syncSchema: booleanSchema.default(false), // Enable debug logging for MikroORM - Outputs queries and entity management logs // Do NOT use in production, leaks all sensitive data - debugLogging: z.coerce.boolean().default(false), + debugLogging: booleanSchema.default(false), // Enable SSL for the postgres connection - ssl: z.coerce.boolean().default(false), + ssl: booleanSchema.default(false), }), crypto: z.object({ // session secret. used for signing session tokens @@ -65,7 +67,7 @@ export const configSchema = z.object({ captcha: z .object({ // enabled captchas on register - enabled: z.coerce.boolean().default(false), + enabled: booleanSchema.default(false), // captcha secret secret: z.string().min(1).optional(), @@ -76,7 +78,7 @@ export const configSchema = z.object({ ratelimits: z .object({ // enabled captchas on register - enabled: z.coerce.boolean().default(false), + enabled: booleanSchema.default(false), redisUrl: z.string().optional(), }) .default({}), diff --git a/src/modules/metrics/index.ts b/src/modules/metrics/index.ts index da35f11..61cc652 100644 --- a/src/modules/metrics/index.ts +++ b/src/modules/metrics/index.ts @@ -13,6 +13,7 @@ export type Metrics = { providerHostnames: Counter<'hostname'>; providerStatuses: Counter<'provider_id' | 'status'>; watchMetrics: Counter<'title' | 'tmdb_full_id' | 'provider_id' | 'success'>; + toolMetrics: Counter<'tool'>; }; let metrics: null | Metrics = null; @@ -59,6 +60,11 @@ export async function setupMetrics(app: FastifyInstance) { help: 'mw_media_watch_count', labelNames: ['title', 'tmdb_full_id', 'provider_id', 'success'], }), + toolMetrics: new Counter({ + name: 'mw_provider_tool_count', + help: 'mw_provider_tool_count', + labelNames: ['tool'], + }), }; const promClient = app.metrics.client; @@ -68,6 +74,7 @@ export async function setupMetrics(app: FastifyInstance) { promClient.register.registerMetric(metrics.providerStatuses); promClient.register.registerMetric(metrics.watchMetrics); promClient.register.registerMetric(metrics.captchaSolves); + promClient.register.registerMetric(metrics.toolMetrics); const orm = getORM(); const em = orm.em.fork(); diff --git a/src/routes/metrics.ts b/src/routes/metrics.ts index e560505..e20e39f 100644 --- a/src/routes/metrics.ts +++ b/src/routes/metrics.ts @@ -19,6 +19,7 @@ const metricsProviderSchema = z.object({ const metricsProviderInputSchema = z.object({ items: z.array(metricsProviderSchema).max(10).min(1), + tool: z.string().optional(), }); export const metricsRouter = makeRouter((app) => { @@ -65,6 +66,12 @@ export const metricsRouter = makeRouter((app) => { }); } + if (body.tool) { + getMetrics().toolMetrics.inc({ + tool: body.tool, + }); + } + return true; }), ); diff --git a/src/services/captcha.ts b/src/services/captcha.ts index e6cf4ad..68d7c4d 100644 --- a/src/services/captcha.ts +++ b/src/services/captcha.ts @@ -4,16 +4,14 @@ import { StatusError } from '@/services/error'; export async function isValidCaptcha(token: string): Promise { if (!conf.captcha.secret) throw new Error('isValidCaptcha() is called but no secret set'); + const formData = new URLSearchParams(); + formData.append('secret', conf.captcha.secret); + formData.append('response', token); const res = await fetch('https://www.google.com/recaptcha/api/siteverify', { method: 'POST', - body: JSON.stringify({ - secret: conf.captcha.secret, - response: token, - }), - headers: { - 'content-type': 'application/json', - }, + body: formData, }); + const json = await res.json(); return !!json.success; }