39 Commits
1.2.0 ... dev

Author SHA1 Message Date
William Oldham
f5cec7ba24 Merge pull request #40 from qtchaos/index-OK
Add a success message to the base path
2024-02-26 12:41:12 +00:00
qtchaos
db00030df0 feat: add a success message to the base path 2024-02-26 14:13:59 +02:00
mrjvs
2fe48d24d7 Merge branch 'master' into dev 2024-01-25 22:33:24 +01:00
William Oldham
b0f5b28084 Merge pull request #38 from movie-web/scrapetools
Add tool metrics
2024-01-25 21:29:24 +00:00
mrjvs
b5ee9f1f7a Merge branch 'dev' into scrapetools 2024-01-25 22:28:33 +01:00
mrjvs
eb0f47ba53 Bump version 2024-01-25 22:28:08 +01:00
mrjvs
26b46876c5 fix linting 2024-01-25 22:27:54 +01:00
mrjvs
f58c2c86e5 Merge pull request #37 from gsi-kevincarrera/fix/backend-always-return-true
Fix: Implement function to correctly handle boolean strings with zod
2024-01-25 22:27:15 +01:00
mrjvs
4af2d32b72 Add tool metrics 2024-01-25 22:24:58 +01:00
Kevin Carrera Calzado
a911e52ddb Add default false value to boolean schema 2024-01-24 12:54:24 -05:00
Kevin Carrera Calzado
7b26b029de Fix:Implement function to handle boolean strings 2024-01-24 12:34:01 -05:00
William Oldham
06ad2249d6 Merge pull request #35 from movie-web/remove-arm
Remove ARM 32-bit from Docker Build
2024-01-09 21:36:01 +00:00
William Oldham
14601b9010 Remove ARM 32-bit from Docker Build 2024-01-09 21:01:52 +00:00
William Oldham
b1eeb21ba4 Merge pull request #34 from weeryan17/feature/arm-build
Update workflows to support arm
2024-01-07 17:58:15 +00:00
weeryan17
5855943a56 Update .github/workflows/linting_testing.yml
Co-authored-by: William Oldham <github@binaryoverload.co.uk>
2024-01-07 12:55:30 -05:00
weeryan17
9073010f4c Update .github/workflows/linting_testing.yml
Co-authored-by: William Oldham <github@binaryoverload.co.uk>
2024-01-07 12:55:24 -05:00
weeryan17
78e6b6443f Update CODEOWNERS file 2024-01-07 12:51:09 -05:00
weeryan17
e7c5d93cc3 Update workflows to support arm 2024-01-07 12:42:45 -05:00
mrjvs
9fe8cb4877 Merge pull request #31 from Caio-Nogueira/fix-backend-captcha
update deprecated composer syntax; change body format in API request
2024-01-05 21:04:32 +01:00
mrjvs
66840d7894 Merge branch 'dev' into fix-backend-captcha 2024-01-05 20:19:26 +01:00
William Oldham
befa173445 Merge pull request #32 from movie-web/dev
Version 1.3.0 backend
2024-01-02 21:45:53 +00:00
mrjvs
1c1d70fa05 Merge branch 'master' into dev 2024-01-02 22:41:31 +01:00
mrjvs
6b83fcd158 Merge branch 'dev' of https://github.com/movie-web/backend into dev 2024-01-02 22:40:32 +01:00
mrjvs
f8870b5929 Bump version 2024-01-02 22:40:15 +01:00
mrjvs
727a9cdd43 Merge pull request #28 from qtchaos/proxy-syncing
Add proxyUrls column to UserSettings model
2024-01-01 19:04:34 +01:00
mrjvs
206844e758 Merge branch 'dev' into proxy-syncing 2024-01-01 19:03:43 +01:00
qtchaos
b2e0841d0b Swap to using ArrayType 2024-01-01 19:46:46 +02:00
mrjvs
886df2ffb4 Merge pull request #29 from qtchaos/ssl
Add config support for SSL postgres connections.
2024-01-01 18:09:39 +01:00
mrjvs
6ea07ea685 Merge branch 'dev' into ssl 2024-01-01 18:06:13 +01:00
mrjvs
1010eaf71e Merge branch 'dev' into proxy-syncing 2024-01-01 18:04:59 +01:00
mrjvs
1632f138b0 Merge branch 'dev' into fix-backend-captcha 2023-12-31 14:10:18 +01:00
caio_nogueira_27
0c3de831fb update deprecated composer syntax; change body format in API request 2023-12-31 13:03:32 +00:00
mrjvs
ebe00ecbd6 Merge pull request #30 from qtchaos/pnpm
Swap to using `pnpm exec` instead of `npx` in scripts
2023-12-31 12:59:16 +01:00
qtchaos
3bb427401f Update package.json scripts to use pnpm 2023-12-30 14:07:38 +02:00
qtchaos
b2598e3d82 Add SSL support for PostgreSQL connection 2023-12-30 01:09:04 +02:00
qtchaos
bf55be2978 Add proxyUrls column to UserSettings model 2023-12-29 23:56:25 +02:00
William Oldham
8f52dad296 Merge pull request #27 from movie-web/dev
Register Captcha Solves Metric
2023-12-21 21:06:09 +00:00
William Oldham
9a27b0b0b4 Merge branch 'master' into dev 2023-12-21 21:04:00 +00:00
William Oldham
46baac466d Register Captcha Solves Metric 2023-12-21 21:03:18 +00:00
19 changed files with 119 additions and 42 deletions

View File

@@ -41,7 +41,7 @@ services:
links: links:
- postgres:postgres - postgres:postgres
environment: environment:
- DATABASE_URL=postgres://postgres:postgres@postgres:5432/postgres?sslmode=disable - PGWEB_DATABASE_URL=postgres://postgres:postgres@postgres:5432/postgres?sslmode=disable
depends_on: depends_on:
- postgres - postgres

4
.github/CODEOWNERS vendored
View File

@@ -1,3 +1 @@
* @movie-web/core * @movie-web/project-leads
.github @binaryoverload

View File

@@ -14,16 +14,16 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v4
- uses: pnpm/action-setup@v2 - uses: pnpm/action-setup@v2
with: with:
version: 8 version: 8
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version: 18 node-version: 20
cache: 'pnpm' cache: 'pnpm'
- name: Install packages - name: Install packages
@@ -38,16 +38,16 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v4
- uses: pnpm/action-setup@v2 - uses: pnpm/action-setup@v2
with: with:
version: 8 version: 8
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version: 18 node-version: 20
cache: 'pnpm' cache: 'pnpm'
- name: Install packages - name: Install packages
@@ -62,10 +62,10 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v3
- name: Build - name: Build
uses: docker/build-push-action@v4 uses: docker/build-push-action@v5

View File

@@ -12,7 +12,7 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Get version - name: Get version
id: package-version id: package-version
@@ -42,10 +42,10 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Setup Docker buildx - name: Setup Docker buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v3
- name: Get version - name: Get version
id: package-version id: package-version
@@ -70,9 +70,12 @@ jobs:
- name: Build and push Docker image - name: Build and push Docker image
id: build-and-push id: build-and-push
uses: docker/build-push-action@v4 uses: docker/build-push-action@v5
with: with:
push: true push: true
platforms: linux/amd64,linux/arm64
context: . context: .
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -1,6 +1,6 @@
{ {
"name": "backend", "name": "backend",
"version": "1.2.0", "version": "1.3.1",
"private": true, "private": true,
"homepage": "https://github.com/movie-web/backend", "homepage": "https://github.com/movie-web/backend",
"engines": { "engines": {
@@ -8,16 +8,16 @@
}, },
"scripts": { "scripts": {
"dev": "nodemon -r tsconfig-paths/register src/main.ts", "dev": "nodemon -r tsconfig-paths/register src/main.ts",
"build": "npm run build:pre && npm run build:compile", "build": "pnpm run build:pre && pnpm run build:compile",
"start": "node dist/main.js", "start": "node dist/main.js",
"lint": "eslint --ext .ts,.js,.json,.tsx src/", "lint": "eslint --ext .ts,.js,.json,.tsx src/",
"lint:fix": "eslint --fix --ext .ts,.js,.json,.tsx src/", "lint:fix": "eslint --fix --ext .ts,.js,.json,.tsx src/",
"build:pre": "rimraf dist/", "build:pre": "rimraf dist/",
"build:compile": "tsc && tsc-alias", "build:compile": "tsc && tsc-alias",
"preinstall": "npx -y only-allow pnpm", "preinstall": "npx -y only-allow pnpm",
"migration:create": "npx -y mikro-orm migration:create", "migration:create": "pnpm exec mikro-orm migration:create",
"migration:up": "npx -y mikro-orm migration:up", "migration:up": "pnpm exec mikro-orm migration:up",
"migration:down": "npx -y mikro-orm migration:down" "migration:down": "pnpm exec mikro-orm migration:down"
}, },
"mikro-orm": { "mikro-orm": {
"useTsNode": true, "useTsNode": true,

View File

@@ -2,6 +2,7 @@ import { devFragment } from '@/config/fragments/dev';
import { dockerFragment } from '@/config/fragments/docker'; import { dockerFragment } from '@/config/fragments/docker';
import { createConfigLoader } from 'neat-config'; import { createConfigLoader } from 'neat-config';
import { z } from 'zod'; import { z } from 'zod';
import { booleanSchema } from './schema';
const fragments = { const fragments = {
dev: devFragment, dev: devFragment,
@@ -12,6 +13,8 @@ export const ormConfigSchema = z.object({
postgres: z.object({ postgres: z.object({
// connection URL for postgres database // connection URL for postgres database
connection: z.string(), connection: z.string(),
// whether to use SSL for the connection
ssl: booleanSchema.default(false),
}), }),
}); });

View File

@@ -1,5 +1,7 @@
import { z } from 'zod'; import { z } from 'zod';
export const booleanSchema = z.preprocess((val) => val === 'true', z.boolean());
export const configSchema = z.object({ export const configSchema = z.object({
server: z server: z
.object({ .object({
@@ -11,13 +13,13 @@ export const configSchema = z.object({
// disable cross origin restrictions, allow any site. // disable cross origin restrictions, allow any site.
// overwrites the cors option above // overwrites the cors option above
allowAnySite: z.coerce.boolean().default(false), allowAnySite: booleanSchema.default(false),
// should it trust reverse proxy headers? (for ip gathering) // 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) // 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 // 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 // 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'), format: z.enum(['json', 'pretty']).default('pretty'),
// show debug logs? // show debug logs?
debug: z.coerce.boolean().default(false), debug: booleanSchema.default(false),
}) })
.default({}), .default({}),
postgres: z.object({ postgres: z.object({
@@ -38,16 +40,19 @@ export const configSchema = z.object({
connection: z.string(), connection: z.string(),
// run all migrations on boot of the application // 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 // try to sync the schema on boot, useful for development
// will always keep the database schema in sync with the connected database // will always keep the database schema in sync with the connected database
// it is extremely destructive, do not use it EVER in production // 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 // Enable debug logging for MikroORM - Outputs queries and entity management logs
// Do NOT use in production, leaks all sensitive data // 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: booleanSchema.default(false),
}), }),
crypto: z.object({ crypto: z.object({
// session secret. used for signing session tokens // session secret. used for signing session tokens
@@ -62,7 +67,7 @@ export const configSchema = z.object({
captcha: z captcha: z
.object({ .object({
// enabled captchas on register // enabled captchas on register
enabled: z.coerce.boolean().default(false), enabled: booleanSchema.default(false),
// captcha secret // captcha secret
secret: z.string().min(1).optional(), secret: z.string().min(1).optional(),
@@ -73,7 +78,7 @@ export const configSchema = z.object({
ratelimits: z ratelimits: z
.object({ .object({
// enabled captchas on register // enabled captchas on register
enabled: z.coerce.boolean().default(false), enabled: booleanSchema.default(false),
redisUrl: z.string().optional(), redisUrl: z.string().optional(),
}) })
.default({}), .default({}),

View File

@@ -483,6 +483,15 @@
"primary": false, "primary": false,
"nullable": true, "nullable": true,
"mappedType": "string" "mappedType": "string"
},
"proxy_urls": {
"name": "proxy_urls",
"type": "text[]",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": true,
"mappedType": "array"
} }
}, },
"name": "user_settings", "name": "user_settings",

View File

@@ -0,0 +1,13 @@
import { Migration } from '@mikro-orm/migrations';
export class Migration20231229214215 extends Migration {
async up(): Promise<void> {
this.addSql('alter table "user_settings" add column "proxy_urls" text[] null;');
}
async down(): Promise<void> {
this.addSql('alter table "user_settings" drop column "proxy_urls";');
}
}

View File

@@ -1,4 +1,4 @@
import { Entity, PrimaryKey, Property } from '@mikro-orm/core'; import { ArrayType, Entity, PrimaryKey, Property } from '@mikro-orm/core';
@Entity({ tableName: 'user_settings' }) @Entity({ tableName: 'user_settings' })
export class UserSettings { export class UserSettings {
@@ -13,6 +13,9 @@ export class UserSettings {
@Property({ name: 'default_subtitle_language', nullable: true }) @Property({ name: 'default_subtitle_language', nullable: true })
defaultSubtitleLanguage?: string | null; defaultSubtitleLanguage?: string | null;
@Property({ name: 'proxy_urls', type: ArrayType, nullable: true })
proxyUrls?: string[] | null;
} }
export interface UserSettingsDTO { export interface UserSettingsDTO {
@@ -20,6 +23,7 @@ export interface UserSettingsDTO {
applicationTheme?: string | null; applicationTheme?: string | null;
applicationLanguage?: string | null; applicationLanguage?: string | null;
defaultSubtitleLanguage?: string | null; defaultSubtitleLanguage?: string | null;
proxyUrls?: string[] | null;
} }
export function formatUserSettings( export function formatUserSettings(
@@ -30,5 +34,6 @@ export function formatUserSettings(
applicationTheme: userSettings.applicationTheme, applicationTheme: userSettings.applicationTheme,
applicationLanguage: userSettings.applicationLanguage, applicationLanguage: userSettings.applicationLanguage,
defaultSubtitleLanguage: userSettings.defaultSubtitleLanguage, defaultSubtitleLanguage: userSettings.defaultSubtitleLanguage,
proxyUrls: userSettings.proxyUrls,
}; };
} }

View File

@@ -1,4 +1,4 @@
import { ormConf } from '@/config/orm'; import { ormConf } from '@/config/orm';
import { makeOrmConfig } from '@/modules/mikro/orm'; import { makeOrmConfig } from '@/modules/mikro/orm';
export default makeOrmConfig(ormConf.postgres.connection); export default makeOrmConfig(ormConf.postgres.connection, ormConf.postgres.ssl);

View File

@@ -1,3 +1,4 @@
import { indexRouter } from '@/routes';
import { loginAuthRouter } from '@/routes/auth/login'; import { loginAuthRouter } from '@/routes/auth/login';
import { manageAuthRouter } from '@/routes/auth/manage'; import { manageAuthRouter } from '@/routes/auth/manage';
import { metaRouter } from '@/routes/meta'; import { metaRouter } from '@/routes/meta';
@@ -25,4 +26,5 @@ export async function setupRoutes(app: FastifyInstance) {
await app.register(userSettingsRouter.register); await app.register(userSettingsRouter.register);
await app.register(userGetRouter.register); await app.register(userGetRouter.register);
await app.register(metricsRouter.register); await app.register(metricsRouter.register);
await app.register(indexRouter.register);
} }

View File

@@ -13,6 +13,7 @@ export type Metrics = {
providerHostnames: Counter<'hostname'>; providerHostnames: Counter<'hostname'>;
providerStatuses: Counter<'provider_id' | 'status'>; providerStatuses: Counter<'provider_id' | 'status'>;
watchMetrics: Counter<'title' | 'tmdb_full_id' | 'provider_id' | 'success'>; watchMetrics: Counter<'title' | 'tmdb_full_id' | 'provider_id' | 'success'>;
toolMetrics: Counter<'tool'>;
}; };
let metrics: null | Metrics = null; let metrics: null | Metrics = null;
@@ -59,6 +60,11 @@ export async function setupMetrics(app: FastifyInstance) {
help: 'mw_media_watch_count', help: 'mw_media_watch_count',
labelNames: ['title', 'tmdb_full_id', 'provider_id', 'success'], 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; const promClient = app.metrics.client;
@@ -67,6 +73,8 @@ export async function setupMetrics(app: FastifyInstance) {
promClient.register.registerMetric(metrics.providerHostnames); promClient.register.registerMetric(metrics.providerHostnames);
promClient.register.registerMetric(metrics.providerStatuses); promClient.register.registerMetric(metrics.providerStatuses);
promClient.register.registerMetric(metrics.watchMetrics); promClient.register.registerMetric(metrics.watchMetrics);
promClient.register.registerMetric(metrics.captchaSolves);
promClient.register.registerMetric(metrics.toolMetrics);
const orm = getORM(); const orm = getORM();
const em = orm.em.fork(); const em = orm.em.fork();

View File

@@ -18,6 +18,7 @@ export async function setupMikroORM() {
conf.postgres.connection, conf.postgres.connection,
conf.postgres.debugLogging, conf.postgres.debugLogging,
(msg) => log.info(msg), (msg) => log.info(msg),
conf.postgres.ssl,
); );
if (conf.postgres.syncSchema) { if (conf.postgres.syncSchema) {

View File

@@ -2,7 +2,10 @@ import { Options } from '@mikro-orm/core';
import { MikroORM, PostgreSqlDriver } from '@mikro-orm/postgresql'; import { MikroORM, PostgreSqlDriver } from '@mikro-orm/postgresql';
import path from 'path'; import path from 'path';
export function makeOrmConfig(url: string): Options<PostgreSqlDriver> { export function makeOrmConfig(
url: string,
ssl: boolean,
): Options<PostgreSqlDriver> {
return { return {
type: 'postgresql', type: 'postgresql',
clientUrl: url, clientUrl: url,
@@ -13,6 +16,11 @@ export function makeOrmConfig(url: string): Options<PostgreSqlDriver> {
pathTs: './migrations', pathTs: './migrations',
path: './migrations', path: './migrations',
}, },
driverOptions: {
connection: {
ssl,
},
},
}; };
} }
@@ -20,9 +28,10 @@ export async function createORM(
url: string, url: string,
debug: boolean, debug: boolean,
log: (msg: string) => void, log: (msg: string) => void,
ssl: boolean,
) { ) {
return await MikroORM.init<PostgreSqlDriver>({ return await MikroORM.init<PostgreSqlDriver>({
...makeOrmConfig(url), ...makeOrmConfig(url, ssl),
logger: log, logger: log,
debug, debug,
}); });

14
src/routes/index.ts Normal file
View File

@@ -0,0 +1,14 @@
import { version } from '@/config';
import { handle } from '@/services/handler';
import { makeRouter } from '@/services/router';
export const indexRouter = makeRouter((app) => {
app.get(
'/',
handle(async () => {
return {
message: `Backend is working as expected (${version})`,
};
}),
);
});

View File

@@ -19,6 +19,7 @@ const metricsProviderSchema = z.object({
const metricsProviderInputSchema = z.object({ const metricsProviderInputSchema = z.object({
items: z.array(metricsProviderSchema).max(10).min(1), items: z.array(metricsProviderSchema).max(10).min(1),
tool: z.string().optional(),
}); });
export const metricsRouter = makeRouter((app) => { 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; return true;
}), }),
); );

View File

@@ -41,6 +41,7 @@ export const userSettingsRouter = makeRouter((app) => {
applicationLanguage: z.string().nullable().optional(), applicationLanguage: z.string().nullable().optional(),
applicationTheme: z.string().nullable().optional(), applicationTheme: z.string().nullable().optional(),
defaultSubtitleLanguage: z.string().nullable().optional(), defaultSubtitleLanguage: z.string().nullable().optional(),
proxyUrls: z.string().array().nullable().optional(),
}), }),
}, },
}, },
@@ -64,6 +65,7 @@ export const userSettingsRouter = makeRouter((app) => {
settings.defaultSubtitleLanguage = body.defaultSubtitleLanguage; settings.defaultSubtitleLanguage = body.defaultSubtitleLanguage;
if (body.applicationTheme !== undefined) if (body.applicationTheme !== undefined)
settings.applicationTheme = body.applicationTheme; settings.applicationTheme = body.applicationTheme;
if (body.proxyUrls !== undefined) settings.proxyUrls = body.proxyUrls;
await em.persistAndFlush(settings); await em.persistAndFlush(settings);
return formatUserSettings(settings); return formatUserSettings(settings);

View File

@@ -4,16 +4,14 @@ import { StatusError } from '@/services/error';
export async function isValidCaptcha(token: string): Promise<boolean> { export async function isValidCaptcha(token: string): Promise<boolean> {
if (!conf.captcha.secret) if (!conf.captcha.secret)
throw new Error('isValidCaptcha() is called but no secret set'); 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', { const res = await fetch('https://www.google.com/recaptcha/api/siteverify', {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: formData,
secret: conf.captcha.secret,
response: token,
}),
headers: {
'content-type': 'application/json',
},
}); });
const json = await res.json(); const json = await res.json();
return !!json.success; return !!json.success;
} }