prepping for our deploy2

This commit is contained in:
CJ_Clippy 2025-06-13 10:10:11 -08:00
parent 02f31a628d
commit aaf4355e36
343 changed files with 348599 additions and 0 deletions

View File

@ -0,0 +1,23 @@
node_modules
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

171
services/our/.gitignore vendored Normal file
View File

@ -0,0 +1,171 @@
generated
backup
node_modules
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
# Created by https://www.toptal.com/developers/gitignore/api/node
# Edit at https://www.toptal.com/developers/gitignore?templates=node
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
### Node Patch ###
# Serverless Webpack directories
.webpack/
# Optional stylelint cache
# SvelteKit build / generate output
.svelte-kit
# End of https://www.toptal.com/developers/gitignore/api/node

1
services/our/.npmrc Normal file
View File

@ -0,0 +1 @@
engine-strict=true

1
services/our/.nvmrc Normal file
View File

@ -0,0 +1 @@
lts/jod

14
services/our/Caddyfile Normal file
View File

@ -0,0 +1,14 @@
:80
## reverse_proxy /api/* nocodb:8080
## reverse_proxy /download/* nocodb:8080
## twitch mock api server
reverse_proxy /mock/* 172.17.0.1:8080
## fastify server
reverse_proxy 172.17.0.1:3000
## file_server
## root * /srv

80
services/our/README.md Normal file
View File

@ -0,0 +1,80 @@
# futureporn
## projekt requirements
* [x] NO BUNDLER (esbuild/vite/webpack/parcel/swc/etc.). IF YOU REACH FOR A BUNDLER, YOU'RE OVERCOMPLICATING IT!
* [x] NO JWT. IF YOU REACH FOR JWT, YOU'RE OVERCOMPLICATING IT!
* [x] Uploads backend
* [x] Uploads frontend
* [x] backend task processor
* [x] Vods
* [x] HLS
* [ ] Deploy
* [ ] Write
* [ ] Publish
* [ ] Streams
* [x] VTubers
* [ ] Tags
* [ ] Toys
* [ ] Monetized affiliate links
## Tiers & Privs
* user - view, torrent, download
* supporterTier1 - view, torrent, download, adfree, upload
* supporterTier6 - view, torrent, download, adfree, upload, csv, sql
## troubleshooting
### keyv
We use tsup to convert ESM to CJS for graphile-worker.
We're staying on version ^4 because that's the one that supports CJS. https://github.com/jaredwray/keyv/issues/1224
### sharp
sharp is often a pain in the ass to install.
```
[dev:serve] /home/cj/Documents/futureporn-monorepo/services/our/node_modules/.pnpm/sharp@0.32.6/node_modules/sharp/lib/sharp.js:37
[dev:serve] throw new Error(help.join('\n'));
[dev:serve] ^
[dev:serve]
[dev:serve] Error:
[dev:serve] Something went wrong installing the "sharp" module
[dev:serve]
[dev:serve] Cannot find module '../build/Release/sharp-linux-x64.node'
[dev:serve] Require stack:
[dev:serve] - /home/cj/Documents/futureporn-monorepo/services/our/node_modules/.pnpm/sharp@0.32.6/node_modules/sharp/lib/sharp.js
[dev:serve] - /home/cj/Documents/futureporn-monorepo/services/our/node_modules/.pnpm/sharp@0.32.6/node_modules/sharp/lib/constructor.js
[dev:serve] - /home/cj/Documents/futureporn-monorepo/services/our/node_modules/.pnpm/sharp@0.32.6/node_modules/sharp/lib/index.js
[dev:serve]
[dev:serve] Possible solutions:
[dev:serve] - Install with verbose logging and look for errors: "npm install --ignore-scripts=false --foreground-scripts --verbose sharp"
[dev:serve] - Install for the current linux-x64 runtime: "npm install --platform=linux --arch=x64 sharp"
[dev:serve] - Consult the installation documentation: https://sharp.pixelplumbing.com/install
[dev:serve] at Object.<anonymous> (/home/cj/Documents/futureporn-monorepo/services/our/node_modules/.pnpm/sharp@0.32.6/node_modules/sharp/lib/sharp.js:37:9)
[dev:serve] at Module._compile (node:internal/modules/cjs/loader:1469:14)
[dev:serve] at Object.transformer (/home/cj/Documents/node_modules/.pnpm/tsx@4.15.7/node_modules/tsx/dist/register-BujtrvNV.cjs:2:1265)
[dev:serve] at Module.load (node:internal/modules/cjs/loader:1288:32)
[dev:serve] at Module._load (node:internal/modules/cjs/loader:1104:12)
[dev:serve] at Module.require (node:internal/modules/cjs/loader:1311:19)
[dev:serve] at require (node:internal/modules/helpers:179:18)
[dev:serve] at Object.<anonymous> (/home/cj/Documents/futureporn-monorepo/services/our/node_modules/.pnpm/sharp@0.32.6/node_modules/sharp/lib/constructor.js:11:1)
[dev:serve] at Module._compile (node:internal/modules/cjs/loader:1469:14)
[dev:serve] at Object.transformer (/home/cj/Documents/node_modules/.pnpm/tsx@4.15.7/node_modules/tsx/dist/register-BujtrvNV.cjs:2:1265)
[dev:serve]
```
If you have trouble installing sharp, try ignoring the system's installed libvips.
SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install --ignore-scripts=false --foreground-scripts --verbose --platform=linux --arch=x64 sharp
SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm install --ignore-scripts=false --foreground-scripts --verbose sharp
Actually, better advice is to probably **remove libvips from the system**. This way, a compatible libvips is always pulled during `npm install`.
Annoyingly, it might be necessary to re-install sharp after every new npm package, even if said package is unrelated to sharp.

15
services/our/crontab Normal file
View File

@ -0,0 +1,15 @@
# ┌───────────── UTC minute (0 - 59)
# │ ┌───────────── UTC hour (0 - 23)
# │ │ ┌───────────── UTC day of the month (1 - 31)
# │ │ │ ┌───────────── UTC month (1 - 12)
# │ │ │ │ ┌───────────── UTC day of the week (0 - 6) (Sunday to Saturday)
# │ │ │ │ │ ┌───────────── task (identifier) to schedule
# │ │ │ │ │ │ ┌────────── optional scheduling options
# │ │ │ │ │ │ │ ┌────── optional payload to merge
# │ │ │ │ │ │ │ │
# │ │ │ │ │ │ │ │
# * * * * * task ?opts {payload}
# * * * * * hello {name: "MY DUDE"}
* * * * * findWork
# * */1 * * * cleanup # faulty-- deletes files too soon, files that were created just a minute ago.

View File

@ -0,0 +1,62 @@
services:
# caddy:
# image: caddy:alpine
# ports:
# - "8081:80"
# volumes:
# - ./public:/srv
# - ./Caddyfile:/etc/caddy/Caddyfile
postgres:
container_name: our-postgres
image: postgres:17
restart: unless-stopped
environment:
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: ${DB_NAME}
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD", "pg_isready", "-U", "postgres"]
interval: 10s
retries: 5
start_period: 10s
timeout: 10s
pgweb:
container_name: out-pgweb
image: sosedoff/pgweb
depends_on:
postgres:
condition: service_healthy
environment:
PGWEB_DATABASE_URL: postgres://${DB_USER}:${DB_PASSWORD}@postgres:5432/${DB_NAME}?sslmode=disable
ports:
- "8091:8081"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8081"]
interval: 10s
retries: 5
start_period: 10s
timeout: 10s
# pgadmin:
# image: dpage/pgadmin4:latest
# environment:
# PGADMIN_LISTEN_PORT: 5050
# PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL}
# PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD}
# PGADMIN_DISABLE_POSTFIX: true
# ports:
# - "5050:5050"
volumes:
pgdata:

View File

@ -0,0 +1,22 @@
import { WorkerPreset } from "graphile-worker"
import { env } from "./src/config/env"
import path from 'node:path'
const __dirname = import.meta.dirname;
const preset: GraphileConfig.Preset = {
extends: [WorkerPreset],
worker: {
connectionString: env.DATABASE_URL,
maxPoolSize: 10,
pollInterval: 2000,
preparedStatements: true,
schema: "graphile_worker",
crontabFile: "crontab",
concurrentJobs: 1,
fileExtensions: [".cjs"],
taskDirectory: path.join(__dirname, 'dist', 'tasks')
},
};
export default preset

12764
services/our/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

112
services/our/package.json Normal file
View File

@ -0,0 +1,112 @@
{
"name": "futureporn",
"private": true,
"version": "2.0.1",
"type": "module",
"overrides": {
"@sinclair/typebox": "0.34.0"
},
"scripts": {
"dev": "concurrently npm:dev:serve npm:dev:build npm:dev:worker",
"dev:serve": "tsx watch ./src/index.ts",
"start": "node ./src/index.ts",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"dev:worker": "tsx watch ./src/worker.ts",
"dev:build": "chokidar 'src/**/*.{js,ts}' -c tsup --clean",
"build": "tsup --clean",
"lint": "eslint .",
"postinstall": "npm install --platform=linux --arch=x64 sharp"
},
"devDependencies": {
"@eslint/compat": "^1.2.5",
"@eslint/js": "^9.18.0",
"@fontsource/fira-mono": "^5.0.0",
"@neoconfetti/svelte": "^2.0.0",
"@sveltejs/adapter-auto": "^6.0.0",
"@sveltejs/kit": "^2.16.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"eslint": "^9.18.0",
"eslint-plugin-svelte": "^3.0.0",
"globals": "^16.0.0",
"nodemon": "^3.1.10",
"prisma": "6.8.2",
"svelte": "^5.25.0",
"svelte-check": "^4.0.0",
"typescript": "^5.0.0",
"typescript-eslint": "^8.20.0",
"vite": "^6.2.6"
},
"pnpm": {
"onlyBuiltDependencies": [
"esbuild"
]
},
"dependencies": {
"@auth0/auth0-spa-js": "^2.1.3",
"@aws-sdk/client-s3": "3.726.1",
"@aws-sdk/s3-request-presigner": "^3.824.0",
"@bogeychan/elysia-logger": "^0.1.8",
"@dotenvx/dotenvx": "^1.44.1",
"@elysiajs/static": "^1.3.0",
"@elysiajs/swagger": "^1.3.0",
"@fastify/flash": "^6.0.3",
"@fastify/formbody": "^8.0.2",
"@fastify/multipart": "^9.0.3",
"@fastify/oauth2": "^8.1.2",
"@fastify/secure-session": "^8.2.0",
"@fastify/static": "^8.1.1",
"@fastify/swagger": "^9.5.1",
"@fastify/swagger-ui": "^5.2.3",
"@fastify/view": "^11.1.0",
"@imgly/background-removal-node": "^1.4.5",
"@imqueue/pg-pubsub": "^1.10.0",
"@keyv/postgres": "^1.4.11",
"@paralleldrive/cuid2": "^2.2.2",
"@prisma/client": "6.8.2",
"@prisma/extension-accelerate": "^1.3.0",
"@types/node": "^22.15.17",
"@types/node-fetch": "^2.6.12",
"arctic": "^3.6.0",
"axios": "^1.9.0",
"cache-manager": "^7.0.0",
"chokidar-cli": "^3.0.0",
"concurrently": "^9.1.2",
"date-fns": "^4.1.0",
"drizzle-orm": "^0.43.1",
"drizzle-typebox": "^0.3.2",
"ejs": "^3.1.10",
"elysia": "^1.2.25",
"elysia-clerk": "^0.9.10",
"elysia-connect-middleware": "^0.0.5",
"elysia-oauth2": "^2.0.0",
"fastify": "^5.3.2",
"fastify-plugin": "^5.0.1",
"fastify-sse-v2": "^4.2.1",
"form-data": "^4.0.2",
"fs-extra": "^11.3.0",
"graphile-worker": "^0.16.6",
"handlebars": "4.7.8",
"jdenticon": "^3.3.0",
"keyv": "^4.5.4",
"lodash-es": "^4.17.21",
"mime-types": "^3.0.1",
"nanoid": "^5.1.5",
"nocodb-sdk": "^0.263.0",
"node-fetch": "^3.3.2",
"rate-limiter-flexible": "^7.1.1",
"rimraf": "6.0.1",
"sharp": "^0.34.2",
"slugify": "^1.6.6",
"ts-node": "^10.9.2",
"tsup": "^8.5.0",
"vitest": "^3.2.1",
"zod": "^3.25.49"
},
"packageManager": "pnpm@10.8.1+sha512.c50088ba998c67b8ca8c99df8a5e02fd2ae2e2b29aaf238feaa9e124248d3f48f9fb6db2424949ff901cffbb5e0f0cc1ad6aedb602cd29450751d11c35023677",
"prisma": {
"seed": "tsx prisma/seed.ts"
}
}

8617
services/our/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,113 @@
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL,
"patreonId" TEXT NOT NULL,
"patreonFullName" TEXT,
"imageUrl" TEXT NOT NULL,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "RateLimiterFlexible" (
"key" TEXT NOT NULL,
"points" INTEGER NOT NULL,
"expire" TIMESTAMP(3),
CONSTRAINT "RateLimiterFlexible_pkey" PRIMARY KEY ("key")
);
-- CreateTable
CREATE TABLE "stream_entity" (
"id" TEXT NOT NULL,
"date" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"announcementUrl" VARCHAR(2048),
CONSTRAINT "stream_entity_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Vod" (
"id" TEXT NOT NULL,
"streamId" TEXT NOT NULL,
CONSTRAINT "Vod_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Upload" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"vodId" TEXT,
"fileKeys" TEXT[],
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Upload_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Vtuber" (
"id" TEXT NOT NULL,
"image" TEXT,
"slug" TEXT,
"displayName" TEXT,
"chaturbate" TEXT,
"twitter" TEXT,
"patreon" TEXT,
"twitch" TEXT,
"tiktok" TEXT,
"onlyfans" TEXT,
"youtube" TEXT,
"linktree" TEXT,
"carrd" TEXT,
"fansly" TEXT,
"pornhub" TEXT,
"discord" TEXT,
"reddit" TEXT,
"throne" TEXT,
"instagram" TEXT,
"facebook" TEXT,
"merch" TEXT,
"description" TEXT,
"themeColor" TEXT,
"fanslyId" TEXT,
"chaturbateId" TEXT,
"twitterId" TEXT,
CONSTRAINT "Vtuber_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "_VodToVtuber" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL,
CONSTRAINT "_VodToVtuber_AB_pkey" PRIMARY KEY ("A","B")
);
-- CreateIndex
CREATE UNIQUE INDEX "User_patreonId_key" ON "User"("patreonId");
-- CreateIndex
CREATE UNIQUE INDEX "Upload_vodId_key" ON "Upload"("vodId");
-- CreateIndex
CREATE INDEX "_VodToVtuber_B_index" ON "_VodToVtuber"("B");
-- AddForeignKey
ALTER TABLE "Vod" ADD CONSTRAINT "Vod_streamId_fkey" FOREIGN KEY ("streamId") REFERENCES "stream_entity"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Upload" ADD CONSTRAINT "Upload_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Upload" ADD CONSTRAINT "Upload_vodId_fkey" FOREIGN KEY ("vodId") REFERENCES "Vod"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_VodToVtuber" ADD CONSTRAINT "_VodToVtuber_A_fkey" FOREIGN KEY ("A") REFERENCES "Vod"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_VodToVtuber" ADD CONSTRAINT "_VodToVtuber_B_fkey" FOREIGN KEY ("B") REFERENCES "Vtuber"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Upload" ADD COLUMN "hlsPlaylist" TEXT;

View File

@ -0,0 +1,30 @@
-- AlterTable
ALTER TABLE "Upload" ADD COLUMN "thumbnail" TEXT;
-- CreateTable
CREATE TABLE "Role" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
CONSTRAINT "Role_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "_UserRoles" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL,
CONSTRAINT "_UserRoles_AB_pkey" PRIMARY KEY ("A","B")
);
-- CreateIndex
CREATE UNIQUE INDEX "Role_name_key" ON "Role"("name");
-- CreateIndex
CREATE INDEX "_UserRoles_B_index" ON "_UserRoles"("B");
-- AddForeignKey
ALTER TABLE "_UserRoles" ADD CONSTRAINT "_UserRoles_A_fkey" FOREIGN KEY ("A") REFERENCES "Role"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_UserRoles" ADD CONSTRAINT "_UserRoles_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1,37 @@
/*
Warnings:
- You are about to drop the `_UserRoles` table. If the table is not empty, all the data it contains will be lost.
*/
-- CreateEnum
CREATE TYPE "RoleName" AS ENUM ('ADMIN', 'USER', 'MODERATOR', 'SUPPORTER_LEVEL_1', 'SUPPORTER_LEVEL_2', 'SUPPORTER_LEVEL_3', 'SUPPORTER_LEVEL_4', 'SUPPORTER_LEVEL_5', 'SUPPORTER_LEVEL_6');
-- DropForeignKey
ALTER TABLE "_UserRoles" DROP CONSTRAINT "_UserRoles_A_fkey";
-- DropForeignKey
ALTER TABLE "_UserRoles" DROP CONSTRAINT "_UserRoles_B_fkey";
-- AlterTable
ALTER TABLE "User" ALTER COLUMN "imageUrl" DROP NOT NULL;
-- DropTable
DROP TABLE "_UserRoles";
-- CreateTable
CREATE TABLE "_RoleToUser" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL,
CONSTRAINT "_RoleToUser_AB_pkey" PRIMARY KEY ("A","B")
);
-- CreateIndex
CREATE INDEX "_RoleToUser_B_index" ON "_RoleToUser"("B");
-- AddForeignKey
ALTER TABLE "_RoleToUser" ADD CONSTRAINT "_RoleToUser_A_fkey" FOREIGN KEY ("A") REFERENCES "Role"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_RoleToUser" ADD CONSTRAINT "_RoleToUser_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1,17 @@
/*
Warnings:
- The values [SUPPORTER_LEVEL_1,SUPPORTER_LEVEL_2,SUPPORTER_LEVEL_3,SUPPORTER_LEVEL_4,SUPPORTER_LEVEL_5,SUPPORTER_LEVEL_6] on the enum `RoleName` will be removed. If these variants are still used in the database, this will fail.
*/
-- AlterEnum
BEGIN;
CREATE TYPE "RoleName_new" AS ENUM ('USER', 'SUPPORTER_TIER_1', 'SUPPORTER_TIER_2', 'SUPPORTER_TIER_3', 'SUPPORTER_TIER_4', 'SUPPORTER_TIER_5', 'SUPPORTER_TIER_6', 'MODERATOR', 'ADMIN');
ALTER TYPE "RoleName" RENAME TO "RoleName_old";
ALTER TYPE "RoleName_new" RENAME TO "RoleName";
DROP TYPE "RoleName_old";
COMMIT;
-- AlterTable
ALTER TABLE "Upload" ADD COLUMN "notes" TEXT,
ADD COLUMN "streamDate" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Upload" ALTER COLUMN "streamDate" DROP DEFAULT;

View File

@ -0,0 +1,19 @@
-- AlterTable
ALTER TABLE "Vtuber" ADD COLUMN "uploadId" TEXT;
-- CreateTable
CREATE TABLE "_UploadToVtuber" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL,
CONSTRAINT "_UploadToVtuber_AB_pkey" PRIMARY KEY ("A","B")
);
-- CreateIndex
CREATE INDEX "_UploadToVtuber_B_index" ON "_UploadToVtuber"("B");
-- AddForeignKey
ALTER TABLE "_UploadToVtuber" ADD CONSTRAINT "_UploadToVtuber_A_fkey" FOREIGN KEY ("A") REFERENCES "Upload"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_UploadToVtuber" ADD CONSTRAINT "_UploadToVtuber_B_fkey" FOREIGN KEY ("B") REFERENCES "Vtuber"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1,5 @@
-- CreateEnum
CREATE TYPE "UploadStatus" AS ENUM ('PENDING', 'APPROVED', 'REJECTED');
-- AlterTable
ALTER TABLE "Upload" ADD COLUMN "status" "UploadStatus" NOT NULL DEFAULT 'PENDING';

View File

@ -0,0 +1,19 @@
/*
Warnings:
- The values [PENDING,APPROVED,REJECTED] on the enum `UploadStatus` will be removed. If these variants are still used in the database, this will fail.
*/
-- AlterEnum
BEGIN;
CREATE TYPE "UploadStatus_new" AS ENUM ('pending', 'approved', 'rejected');
ALTER TABLE "Upload" ALTER COLUMN "status" DROP DEFAULT;
ALTER TABLE "Upload" ALTER COLUMN "status" TYPE "UploadStatus_new" USING ("status"::text::"UploadStatus_new");
ALTER TYPE "UploadStatus" RENAME TO "UploadStatus_old";
ALTER TYPE "UploadStatus_new" RENAME TO "UploadStatus";
DROP TYPE "UploadStatus_old";
ALTER TABLE "Upload" ALTER COLUMN "status" SET DEFAULT 'pending';
COMMIT;
-- AlterTable
ALTER TABLE "Upload" ALTER COLUMN "status" SET DEFAULT 'pending';

View File

@ -0,0 +1,63 @@
/*
Warnings:
- The values [USER,SUPPORTER_TIER_1,SUPPORTER_TIER_2,SUPPORTER_TIER_3,SUPPORTER_TIER_4,SUPPORTER_TIER_5,SUPPORTER_TIER_6,MODERATOR,ADMIN] on the enum `RoleName` will be removed. If these variants are still used in the database, this will fail.
- You are about to drop the column `uploadId` on the `Vtuber` table. All the data in the column will be lost.
- You are about to drop the `Upload` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `_UploadToVtuber` table. If the table is not empty, all the data it contains will be lost.
- Added the required column `streamDate` to the `Vod` table without a default value. This is not possible if the table is not empty.
- Added the required column `updatedAt` to the `Vod` table without a default value. This is not possible if the table is not empty.
- Added the required column `uploaderId` to the `Vod` table without a default value. This is not possible if the table is not empty.
*/
-- CreateEnum
CREATE TYPE "VodStatus" AS ENUM ('pending', 'approved', 'rejected', 'processing', 'processed');
-- AlterEnum
BEGIN;
CREATE TYPE "RoleName_new" AS ENUM ('user', 'supporterTier1', 'supporterTier2', 'supporterTier3', 'supporterTier4', 'supporterTier5', 'supporterTier6', 'moderator', 'admin');
ALTER TYPE "RoleName" RENAME TO "RoleName_old";
ALTER TYPE "RoleName_new" RENAME TO "RoleName";
DROP TYPE "RoleName_old";
COMMIT;
-- DropForeignKey
ALTER TABLE "Upload" DROP CONSTRAINT "Upload_userId_fkey";
-- DropForeignKey
ALTER TABLE "Upload" DROP CONSTRAINT "Upload_vodId_fkey";
-- DropForeignKey
ALTER TABLE "_UploadToVtuber" DROP CONSTRAINT "_UploadToVtuber_A_fkey";
-- DropForeignKey
ALTER TABLE "_UploadToVtuber" DROP CONSTRAINT "_UploadToVtuber_B_fkey";
-- AlterTable
ALTER TABLE "Vod" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "hlsPlaylist" TEXT,
ADD COLUMN "notes" TEXT,
ADD COLUMN "segmentKeys" JSONB,
ADD COLUMN "status" "VodStatus" NOT NULL DEFAULT 'pending',
ADD COLUMN "streamDate" TIMESTAMP(3) NOT NULL,
ADD COLUMN "thumbnail" TEXT,
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL,
ADD COLUMN "uploaderId" TEXT NOT NULL;
-- AlterTable
ALTER TABLE "Vtuber" DROP COLUMN "uploadId";
-- AlterTable
ALTER TABLE "stream_entity" ALTER COLUMN "announcementUrl" SET DATA TYPE TEXT;
-- DropTable
DROP TABLE "Upload";
-- DropTable
DROP TABLE "_UploadToVtuber";
-- DropEnum
DROP TYPE "UploadStatus";
-- AddForeignKey
ALTER TABLE "Vod" ADD CONSTRAINT "Vod_uploaderId_fkey" FOREIGN KEY ("uploaderId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -0,0 +1,8 @@
-- DropForeignKey
ALTER TABLE "Vod" DROP CONSTRAINT "Vod_streamId_fkey";
-- AlterTable
ALTER TABLE "Vod" ALTER COLUMN "streamId" DROP NOT NULL;
-- AddForeignKey
ALTER TABLE "Vod" ADD CONSTRAINT "Vod_streamId_fkey" FOREIGN KEY ("streamId") REFERENCES "stream_entity"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Vod" ADD COLUMN "sourceVideo" TEXT;

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Vod" ADD COLUMN "asrVtt" TEXT;

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Vod" ADD COLUMN "sha256sum" TEXT;

View File

@ -0,0 +1,9 @@
/*
Warnings:
- Added the required column `updatedAt` to the `Vtuber` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "Vtuber" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL;

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Vod" ADD COLUMN "cidv1" TEXT;

View File

@ -0,0 +1,11 @@
/*
Warnings:
- Added the required column `uploaderId` to the `Vtuber` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "Vtuber" ADD COLUMN "uploaderId" TEXT NOT NULL;
-- AddForeignKey
ALTER TABLE "Vtuber" ADD CONSTRAINT "Vtuber_uploaderId_fkey" FOREIGN KEY ("uploaderId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "VodStatus" ADD VALUE 'ordering';

View File

@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"

View File

@ -0,0 +1,125 @@
generator client {
provider = "prisma-client-js"
output = "../generated/prisma"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(uuid())
patreonId String @unique
patreonFullName String?
imageUrl String?
roles Role[]
vods Vod[]
Vtuber Vtuber[]
}
enum RoleName {
user
supporterTier1
supporterTier2
supporterTier3
supporterTier4
supporterTier5
supporterTier6
moderator
admin
}
model Role {
id String @id @default(cuid())
name String @unique
users User[]
}
model RateLimiterFlexible {
key String @id
points Int
expire DateTime?
}
model Stream {
id String @id @default(cuid(2))
date DateTime
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
announcementUrl String?
vods Vod[]
@@map("stream_entity")
}
enum VodStatus {
ordering
pending
approved
rejected
processing
processed
}
model Vod {
id String @id @default(cuid(2))
streamId String?
stream Stream? @relation(fields: [streamId], references: [id])
uploaderId String // previously in Upload
uploader User @relation(fields: [uploaderId], references: [id])
streamDate DateTime
notes String?
segmentKeys Json?
sourceVideo String?
hlsPlaylist String?
thumbnail String?
asrVtt String?
status VodStatus @default(pending)
sha256sum String?
cidv1 String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
vtubers Vtuber[]
}
model Vtuber {
id String @id @default(cuid(2))
image String?
slug String?
displayName String?
chaturbate String?
twitter String?
patreon String?
twitch String?
tiktok String?
onlyfans String?
youtube String?
linktree String?
carrd String?
fansly String?
pornhub String?
discord String?
reddit String?
throne String?
instagram String?
facebook String?
merch String?
description String?
themeColor String?
fanslyId String?
chaturbateId String?
twitterId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
vods Vod[]
uploaderId String
uploader User @relation(fields: [uploaderId], references: [id])
}

View File

@ -0,0 +1,91 @@
import { randomInt } from 'crypto';
import { PrismaClient } from '../generated/prisma';
import { withAccelerate } from '@prisma/extension-accelerate';
const prisma = new PrismaClient().$extends(withAccelerate());
const statuses = ['pending', 'approved', 'rejected'] as const;
function getRandomStatus() {
return statuses[Math.floor(Math.random() * statuses.length)];
}
async function main() {
console.log('🌱 Seeding database...');
const length = 9;
// Create Roles
await Promise.all([
prisma.role.create({ data: { name: 'user' } }),
prisma.role.create({ data: { name: 'admin' } }),
prisma.role.create({ data: { name: 'supporterTier1' } }),
prisma.role.create({ data: { name: 'supporterTier2' } }),
prisma.role.create({ data: { name: 'supporterTier3' } }),
prisma.role.create({ data: { name: 'supporterTier4' } }),
prisma.role.create({ data: { name: 'supporterTier5' } }),
prisma.role.create({ data: { name: 'supporterTier6' } }),
]);
// Create Users
const users = await Promise.all(
Array.from({ length }).map((_, i) =>
prisma.user.create({
data: {
patreonFullName: `User ${i + 1}`,
patreonId: `${randomInt(9558925894)}`,
imageUrl: 'https://placehold.co/48',
},
})
)
);
// Create Vtubers
const vtubers = await Promise.all(
['Alpha', 'Bravo', 'Charlie'].map((name) =>
prisma.vtuber.create({
data: {
displayName: name,
slug: name.toLowerCase(),
},
})
)
);
// Create Streams and VODs (with embedded upload data)
await Promise.all(
users.map((user, i) =>
prisma.vod.create({
data: {
uploader: { connect: { id: user.id } },
segmentKeys: [`vod${i + 1}_part1.mp4`, `vod${i + 1}_part2.mp4`],
streamDate: new Date(),
notes: `Seeded VOD ${i + 1}`,
status: getRandomStatus(),
hlsPlaylist: `https://cdn.example.com/hls/vod${i + 1}/index.m3u8`,
thumbnail: `https://placehold.co/320x180?text=VOD${i + 1}`,
vtubers: {
connect: [{ id: vtubers[i % vtubers.length].id }],
},
stream: {
create: {
date: new Date(),
announcementUrl: `https://example.com/announcement/${i + 1}`,
},
},
},
})
)
);
console.log(`✅ Seed complete with ${length} users and VODs (with vtubers)!`);
}
main()
.catch((e) => {
console.error('❌ Seed error:', e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});

181
services/our/src/app.ts Normal file
View File

@ -0,0 +1,181 @@
import Fastify from 'fastify'
import prismaPlugin from './plugins/prisma'
import vodsRoutes from './plugins/vods'
import uploadsRoutes from './plugins/uploads'
import vtubersRoutes from './plugins/vtubers'
import usersRoutes from './plugins/users'
import indexRoutes from './plugins/index'
import streamsRoutes from './plugins/streams'
import hls from './plugins/hls.ts'
import fastifyStatic from '@fastify/static'
import fastifySecureSession from '@fastify/secure-session'
import path, { basename } from 'node:path'
// import fastifyMultipart from '@fastify/multipart'
import fastifyFormbody from '@fastify/formbody'
import fastifyView from "@fastify/view"
import { env } from './config/env'
import { constants } from './config/constants'
import authRoutes from './plugins/auth'
import Handlebars from 'handlebars'
import graphileWorker from './plugins/graphileWorker'
import fastifySwagger from "@fastify/swagger";
import fastifySwaggerUi from "@fastify/swagger-ui";
import { join } from 'node:path'
import { format } from 'date-fns'
import * as jdenticon from 'jdenticon'
import { Role } from '../generated/prisma'
import fastifyFlash from '@fastify/flash'
import { isModerator, hasRole } from './utils/privs'
import { signUrl } from './utils/cdn'
import { extractBasePath } from './utils/filesystem'
export function buildApp() {
const app = Fastify()
Handlebars.registerHelper('formatDate', function (dateString) {
if (!dateString) return ''
return format(new Date(dateString), 'yyyy-MM-dd')
})
Handlebars.registerHelper('identicon', function (str, size = 48) {
return jdenticon.toSvg(str, size)
})
Handlebars.registerHelper('safeJson', function (context) {
return new Handlebars.SafeString(JSON.stringify(context));
});
Handlebars.registerHelper('json', function (context) {
return JSON.stringify(context)
})
Handlebars.registerHelper('patron', function (user) {
if (!user.roles) {
throw new Error(
'patron hbs helper was called without roles. This usually means you forgot to include roles relationship in the query.'
);
}
return user.roles.some((r: Role) => r.name.startsWith('supporter'));
});
Handlebars.registerHelper('notEqual', function (a, b) {
return a !== b;
});
Handlebars.registerHelper('isEqual', function (a, b) {
// console.log(`isEqual a=${a} b=${b}`)
return a == b
});
Handlebars.registerHelper('isModerator', function (user) {
return isModerator(user)
})
Handlebars.registerHelper('hasRole', hasRole)
Handlebars.registerHelper('breaklines', function (text) {
text = Handlebars.Utils.escapeExpression(text);
text = text.replace(/(\r\n|\n|\r)/gm, '<br>');
return new Handlebars.SafeString(text);
});
Handlebars.registerHelper('getCdnUrl', function (s3Key) {
return signUrl(`${env.CDN_ORIGIN}/${s3Key}`, {
securityKey: env.CDN_TOKEN_SECRET,
expirationTime: constants.timeUnits.sevenDaysInSeconds,
})
})
/**
* @see https://github.com/video-dev/hls.js/issues/2152
*/
Handlebars.registerHelper('signedHlsUrl', function (s3Key) {
const pathAllowed = extractBasePath(s3Key)
const url = signUrl(`${env.CDN_ORIGIN}/${s3Key}`, {
securityKey: env.CDN_TOKEN_SECRET,
pathAllowed,
isDirectory: true,
expirationTime: constants.timeUnits.sevenDaysInSeconds,
})
console.log(`pathAllowed=${pathAllowed} url=${url}`)
return url
})
Handlebars.registerHelper('basename', function (url: string) {
return basename(url)
})
Handlebars.registerHelper('trunc', function (str, length = 6) {
if (str && str.length > length) {
return new Handlebars.SafeString(str.substring(0, length)) + '…';
} else {
return str;
}
});
const __dirname = import.meta.dirname;
const swaggerOptions = {
swagger: {
info: {
title: constants.site.title,
description: constants.site.description,
version: constants.site.version,
},
host: env.ORIGIN,
schemes: ["http", "https"],
consumes: ["application/json"],
produces: ["application/json"],
},
};
const swaggerUiOptions = {
routePrefix: "/api/docs",
exposeRoute: true,
};
app.register(fastifySwagger, swaggerOptions);
app.register(fastifySwaggerUi, swaggerUiOptions);
app.register(fastifyStatic, {
root: path.join(__dirname, 'assets'),
prefix: '/', // optional: default '/'
constraints: {} // optional: default {}
})
app.register(fastifyFormbody)
// app.register(fastifyMultipart, {
// limits: {
// fileSize: 30 * 1024 * 1024 // 30MB
// }
// })
app.register(fastifySecureSession, {
// the name of the attribute decorated on the request-object, defaults to 'session'
sessionName: 'session',
cookieName: 'fp-session',
// adapt this to point to the directory where secret-key is located
key: Buffer.from(env.COOKIE_SECRET, 'hex'),
// the amount of time the session is considered valid; this is different from the cookie options
// and based on value within the session.
expiry: 24 * 60 * 60, // Default 1 day
cookie: {
path: '/'
// options for setCookie, see https://github.com/fastify/fastify-cookie
}
})
app.register(fastifyFlash)
app.register(fastifyView, {
engine: {
handlebars: Handlebars,
},
templates: join(__dirname, '..', 'src', 'views'),
layout: 'layouts/main',
viewExt: 'hbs',
options: {
partials: {
navbar: 'partials/navbar.hbs',
footer: 'partials/footer.hbs',
commentForm: 'partials/commentForm.hbs'
}
}
})
app.register(graphileWorker)
app.register(prismaPlugin)
app.register(hls)
app.register(vodsRoutes)
app.register(streamsRoutes)
app.register(vtubersRoutes)
app.register(uploadsRoutes)
app.register(usersRoutes)
app.register(indexRoutes)
app.register(authRoutes)
return app
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More