client js bundle fixes
This commit is contained in:
parent
f1c593ff17
commit
1f4d4938c9
17
services/our/package-lock.json
generated
17
services/our/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "futureporn-our",
|
"name": "futureporn-our",
|
||||||
"version": "2.6.0",
|
"version": "2.7.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "futureporn-our",
|
"name": "futureporn-our",
|
||||||
"version": "2.6.0",
|
"version": "2.7.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "3.726.1",
|
"@aws-sdk/client-s3": "3.726.1",
|
||||||
"@aws-sdk/s3-request-presigner": "^3.844.0",
|
"@aws-sdk/s3-request-presigner": "^3.844.0",
|
||||||
@ -37,6 +37,7 @@
|
|||||||
"concurrently": "^9.2.0",
|
"concurrently": "^9.2.0",
|
||||||
"create-torrent": "^6.1.0",
|
"create-torrent": "^6.1.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
|
"env-paths": "^3.0.0",
|
||||||
"fastify": "^5.4.0",
|
"fastify": "^5.4.0",
|
||||||
"fastify-favicon": "^5.0.0",
|
"fastify-favicon": "^5.0.0",
|
||||||
"fastify-plugin": "^5.0.1",
|
"fastify-plugin": "^5.0.1",
|
||||||
@ -6729,6 +6730,18 @@
|
|||||||
"once": "^1.4.0"
|
"once": "^1.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/env-paths": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/error-ex": {
|
"node_modules/error-ex": {
|
||||||
"version": "1.3.2",
|
"version": "1.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "futureporn-our",
|
"name": "futureporn-our",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2.7.1",
|
"version": "2.8.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "concurrently npm:dev:serve npm:dev:build:server npm:dev:build:client npm:dev:worker npm:dev:compose npm:dev:sftp npm:dev:qbittorrent",
|
"dev": "concurrently npm:dev:serve npm:dev:build:server npm:dev:build:client npm:dev:worker npm:dev:compose npm:dev:sftp npm:dev:qbittorrent npm:dev:tailscale",
|
||||||
"dev:serve": "npx @dotenvx/dotenvx run -f ../../.env.development.local -- tsx watch ./src/index.ts",
|
"dev:serve": "npx @dotenvx/dotenvx run -f ../../.env.development.local -- tsx watch ./src/index.ts",
|
||||||
"dev:compose": "docker compose -f compose.development.yaml up",
|
"dev:compose": "docker compose -f compose.development.yaml up",
|
||||||
"dev:worker": "npx @dotenvx/dotenvx run -e GRAPHILE_LOGGER_DEBUG=1 -f ../../.env.development.local -- tsx watch ./src/worker.ts",
|
"dev:worker": "npx @dotenvx/dotenvx run -e GRAPHILE_LOGGER_DEBUG=1 -f ../../.env.development.local -- tsx watch ./src/worker.ts",
|
||||||
@ -13,13 +13,14 @@
|
|||||||
"dev:build:client": "chokidar 'src/client/**/*.{js,css}' -c 'node build.mjs'",
|
"dev:build:client": "chokidar 'src/client/**/*.{js,css}' -c 'node build.mjs'",
|
||||||
"dev:qbittorrent": "npx @dotenvx/dotenvx run -f ../../.env.development.local -- sh -c 'docker run --rm --name fp-dev-qbittorrent -e QBT_LEGAL_NOTICE -e QBT_TORRENTING_PORT -e QBT_WEBUI_PORT -e QBT_DISABLE_NETWORK -e QBT_USERNAME -e QBT_PASSWORD -v \"$CACHE_ROOT\":\"$CACHE_ROOT\" -p 8083:8083 gitea.futureporn.net/futureporn/qbittorrent-nox:latest'",
|
"dev:qbittorrent": "npx @dotenvx/dotenvx run -f ../../.env.development.local -- sh -c 'docker run --rm --name fp-dev-qbittorrent -e QBT_LEGAL_NOTICE -e QBT_TORRENTING_PORT -e QBT_WEBUI_PORT -e QBT_DISABLE_NETWORK -e QBT_USERNAME -e QBT_PASSWORD -v \"$CACHE_ROOT\":\"$CACHE_ROOT\" -p 8083:8083 gitea.futureporn.net/futureporn/qbittorrent-nox:latest'",
|
||||||
"dev:sftp": "docker run -p 2222:22 --rm atmoz/sftp user:pass:::watch",
|
"dev:sftp": "docker run -p 2222:22 --rm atmoz/sftp user:pass:::watch",
|
||||||
|
"dev:tailscale": "tailscale funnel --bg 5000",
|
||||||
"start": "echo please use either start:server or start:worker; exit 1",
|
"start": "echo please use either start:server or start:worker; exit 1",
|
||||||
"start:server": "tsx ./src/index.ts",
|
"start:server": "tsx ./src/index.ts",
|
||||||
"start:worker": "tsx ./src/worker.ts",
|
"start:worker": "tsx ./src/worker.ts",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"build": "tsup --clean",
|
"build": "tsup --clean",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"clean": "rm -rf node_modules && rm -rf pnpm-lock.yaml",
|
"clean": "rimraf ./dist",
|
||||||
"deploy": "npx prisma migrate deploy",
|
"deploy": "npx prisma migrate deploy",
|
||||||
"test:watch": "npx vitest --watch",
|
"test:watch": "npx vitest --watch",
|
||||||
"test": "npx vitest"
|
"test": "npx vitest"
|
||||||
@ -80,6 +81,7 @@
|
|||||||
"concurrently": "^9.2.0",
|
"concurrently": "^9.2.0",
|
||||||
"create-torrent": "^6.1.0",
|
"create-torrent": "^6.1.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
|
"env-paths": "^3.0.0",
|
||||||
"fastify": "^5.4.0",
|
"fastify": "^5.4.0",
|
||||||
"fastify-favicon": "^5.0.0",
|
"fastify-favicon": "^5.0.0",
|
||||||
"fastify-plugin": "^5.0.1",
|
"fastify-plugin": "^5.0.1",
|
||||||
|
7566
services/our/pnpm-lock.yaml
generated
7566
services/our/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -25,6 +25,7 @@ import fastifyFlash from '@fastify/flash'
|
|||||||
import { registerHbsHelpers } from './utils/hbsHelpers.ts'
|
import { registerHbsHelpers } from './utils/hbsHelpers.ts'
|
||||||
import fastifyFavicon from 'fastify-favicon'
|
import fastifyFavicon from 'fastify-favicon'
|
||||||
|
|
||||||
|
|
||||||
const __dirname = import.meta.dirname;
|
const __dirname = import.meta.dirname;
|
||||||
|
|
||||||
export async function buildApp() {
|
export async function buildApp() {
|
||||||
@ -61,7 +62,7 @@ export async function buildApp() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
app.register(fastifyFavicon, { path: path.join(__dirname, '..', 'dist', 'client'), name: 'favicon.ico', maxAge: 3600 })
|
app.register(fastifyFavicon, { path: path.join(__dirname, '..', 'src', 'assets'), name: 'favicon.ico', maxAge: 3600 })
|
||||||
app.register(fastifyStatic, {
|
app.register(fastifyStatic, {
|
||||||
root: path.join(__dirname, '..', 'dist', 'client'),
|
root: path.join(__dirname, '..', 'dist', 'client'),
|
||||||
prefix: '/assets', // optional: default '/'
|
prefix: '/assets', // optional: default '/'
|
||||||
|
@ -1,33 +1,5 @@
|
|||||||
|
|
||||||
export const constants = {
|
export const constants = {
|
||||||
twitch: {
|
|
||||||
maxGeneralApiRequestsPerMinute: 800,
|
|
||||||
maxChannelPointRewards: 50,
|
|
||||||
dev: {
|
|
||||||
paths: {
|
|
||||||
auth: '/auth/twitchmock',
|
|
||||||
users: '/mock/users',
|
|
||||||
channelPointRewards: '/mock/channel_points/custom_rewards'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
staging: {
|
|
||||||
paths: {
|
|
||||||
auth: '/auth/twitch',
|
|
||||||
users: '/helix/users',
|
|
||||||
channelPointRewards: '/helix/channel_points/custom_rewards'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
prod: {
|
|
||||||
paths: {
|
|
||||||
auth: '/auth/twitch',
|
|
||||||
users: '/helix/users',
|
|
||||||
channelPointRewards: '/helix/channel_points/custom_rewards'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
authScopes: [
|
|
||||||
"channel:manage:redemptions", // manage custom channel point redeems
|
|
||||||
],
|
|
||||||
},
|
|
||||||
patreon: {
|
patreon: {
|
||||||
authScopes: [
|
authScopes: [
|
||||||
'identity'
|
'identity'
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
// ./config/env.ts
|
// ./config/env.ts
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
// import dotenvx from '@dotenvx/dotenvx'
|
import { homedir } from 'node:os';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
|
||||||
|
const defaultCachePath = join(homedir(), '.cache', 'futureporn');
|
||||||
|
|
||||||
|
|
||||||
// dotenvx.config({ path: ['../../.env.development'] })
|
|
||||||
// if (process.env.NODE_ENV === 'development') {
|
|
||||||
// }
|
|
||||||
|
|
||||||
const EnvSchema = z.object({
|
const EnvSchema = z.object({
|
||||||
NODE_ENV: z.enum(['development', 'production', 'test']),
|
NODE_ENV: z.enum(['development', 'production', 'test']),
|
||||||
@ -26,7 +27,7 @@ const EnvSchema = z.object({
|
|||||||
S3_FORCE_PATH_STYLE: z.coerce.boolean().default(false),
|
S3_FORCE_PATH_STYLE: z.coerce.boolean().default(false),
|
||||||
CDN_ORIGIN: z.string(),
|
CDN_ORIGIN: z.string(),
|
||||||
CDN_TOKEN_SECRET: z.string(),
|
CDN_TOKEN_SECRET: z.string(),
|
||||||
CACHE_ROOT: z.string().default('/tmp/our'),
|
CACHE_ROOT: z.string().default(defaultCachePath),
|
||||||
VIBEUI_DIR: z.string().default('/opt/futureporn/apps/vibeui'),
|
VIBEUI_DIR: z.string().default('/opt/futureporn/apps/vibeui'),
|
||||||
APP_DIR: z.string().default('/app'),
|
APP_DIR: z.string().default('/app'),
|
||||||
WHISPER_DIR: z.string(),
|
WHISPER_DIR: z.string(),
|
||||||
|
@ -1,175 +0,0 @@
|
|||||||
// 24358114\
|
|
||||||
|
|
||||||
import { PrismaClient, type User } from '../../generated/prisma'
|
|
||||||
import { withAccelerate } from "@prisma/extension-accelerate"
|
|
||||||
import { env } from '../config/env'
|
|
||||||
import { type FastifyInstance, type FastifyReply, type FastifyRequest } from 'fastify'
|
|
||||||
import crypto from 'crypto'
|
|
||||||
import { type IncomingHttpHeaders } from 'http';
|
|
||||||
import logger from './utils/logger.ts'
|
|
||||||
|
|
||||||
|
|
||||||
export interface ChannelPointRedemptionEvent {
|
|
||||||
id: string;
|
|
||||||
broadcaster_user_id: string;
|
|
||||||
broadcaster_user_login: string;
|
|
||||||
broadcaster_user_name: string;
|
|
||||||
user_id: string;
|
|
||||||
user_login: string;
|
|
||||||
user_name: string;
|
|
||||||
user_input: string;
|
|
||||||
status: 'unfulfilled' | 'fulfilled' | 'canceled';
|
|
||||||
reward: {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
cost: number;
|
|
||||||
prompt: string;
|
|
||||||
};
|
|
||||||
redeemed_at: string; // ISO 8601 timestamp
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const prisma = new PrismaClient().$extends(withAccelerate())
|
|
||||||
|
|
||||||
// Notification request headers
|
|
||||||
const TWITCH_MESSAGE_ID = 'twitch-eventsub-message-id';
|
|
||||||
const TWITCH_MESSAGE_TIMESTAMP = 'twitch-eventsub-message-timestamp';
|
|
||||||
const TWITCH_MESSAGE_SIGNATURE = 'twitch-eventsub-message-signature';
|
|
||||||
const MESSAGE_TYPE = 'twitch-eventsub-message-type';
|
|
||||||
|
|
||||||
// Notification message types
|
|
||||||
const MESSAGE_TYPE_VERIFICATION = 'webhook_callback_verification';
|
|
||||||
const MESSAGE_TYPE_NOTIFICATION = 'notification';
|
|
||||||
const MESSAGE_TYPE_REVOCATION = 'revocation';
|
|
||||||
|
|
||||||
const HMAC_PREFIX = 'sha256=';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// HMAC functions
|
|
||||||
function getSecret() {
|
|
||||||
// Get this from your secure storage
|
|
||||||
return env.TWITCH_EVENTSUB_SECRET;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getHmac(secret: string, message: string) {
|
|
||||||
return crypto.createHmac('sha256', secret).update(message).digest('hex');
|
|
||||||
}
|
|
||||||
|
|
||||||
function verifyMessage(hmac: string, signature: string) {
|
|
||||||
return crypto.timingSafeEqual(Buffer.from(hmac), Buffer.from(signature));
|
|
||||||
}
|
|
||||||
|
|
||||||
function getHeader(headers: IncomingHttpHeaders, key: string): string {
|
|
||||||
const value = headers[key];
|
|
||||||
if (!value) throw new Error(`Missing header: ${key}`);
|
|
||||||
return Array.isArray(value) ? value[0] : value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toRFC3339Nano(date = new Date()): string {
|
|
||||||
const pad = (n: number, width = 2) => n.toString().padStart(width, '0');
|
|
||||||
const year = date.getUTCFullYear();
|
|
||||||
const month = pad(date.getUTCMonth() + 1);
|
|
||||||
const day = pad(date.getUTCDate());
|
|
||||||
const hour = pad(date.getUTCHours());
|
|
||||||
const minute = pad(date.getUTCMinutes());
|
|
||||||
const second = pad(date.getUTCSeconds());
|
|
||||||
|
|
||||||
const ms = date.getUTCMilliseconds().toString().padStart(3, '0');
|
|
||||||
|
|
||||||
// Simulate extra digits (not real nanoseconds)
|
|
||||||
const extra = process.hrtime.bigint() % 1_000_000n; // fake last 6 digits
|
|
||||||
const extraStr = extra.toString().padStart(6, '0');
|
|
||||||
|
|
||||||
return `${year}-${month}-${day}T${hour}:${minute}:${second}.${ms}${extraStr}Z`;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export default async function redeemsRoutes(
|
|
||||||
fastify: FastifyInstance,
|
|
||||||
): Promise<void> {
|
|
||||||
|
|
||||||
|
|
||||||
fastify.post('/eventsub', async (request: FastifyRequest, reply: FastifyReply) => {
|
|
||||||
logger.debug('eventsub ablagafkadlfijaldf ')
|
|
||||||
const secret = getSecret();
|
|
||||||
const rawBody = request.body;
|
|
||||||
const headers = request.headers;
|
|
||||||
logger.debug(headers)
|
|
||||||
|
|
||||||
logger.debug(`twitch_message_timestamp=${getHeader(headers, TWITCH_MESSAGE_TIMESTAMP)}`)
|
|
||||||
|
|
||||||
const message =
|
|
||||||
getHeader(headers, TWITCH_MESSAGE_ID) +
|
|
||||||
getHeader(headers, TWITCH_MESSAGE_TIMESTAMP) +
|
|
||||||
rawBody;
|
|
||||||
|
|
||||||
|
|
||||||
const hmac = HMAC_PREFIX + getHmac(secret, message);
|
|
||||||
|
|
||||||
if (verifyMessage(hmac, getHeader(headers, TWITCH_MESSAGE_SIGNATURE))) {
|
|
||||||
logger.debug('signatures match');
|
|
||||||
|
|
||||||
if (!(rawBody instanceof Buffer)) {
|
|
||||||
throw new Error("Expected rawBody to be a Buffer");
|
|
||||||
}
|
|
||||||
|
|
||||||
const notification = JSON.parse(rawBody.toString());
|
|
||||||
|
|
||||||
const messageType = headers[MESSAGE_TYPE];
|
|
||||||
|
|
||||||
if (messageType === MESSAGE_TYPE_NOTIFICATION) {
|
|
||||||
logger.debug(`Event type: ${notification.subscription.type}`);
|
|
||||||
logger.debug(JSON.stringify(notification.event, null, 4));
|
|
||||||
|
|
||||||
if (notification.subscription.type === 'channel.channel_points_custom_reward_redemption.add') {
|
|
||||||
const event = notification.event as ChannelPointRedemptionEvent
|
|
||||||
logger.debug(`looking for reward id ${event.reward.id}`)
|
|
||||||
const pick = await prisma.pick.findFirstOrThrow({
|
|
||||||
where: {
|
|
||||||
twitchChannelPointRewardId: event.reward.id
|
|
||||||
}
|
|
||||||
})
|
|
||||||
logger.debug(`looking for broadcaster user id =${event.broadcaster_user_id}`)
|
|
||||||
const user = await prisma.user.findFirstOrThrow({
|
|
||||||
where: {
|
|
||||||
twitchId: event.broadcaster_user_id
|
|
||||||
}
|
|
||||||
})
|
|
||||||
await prisma.redeem.create({
|
|
||||||
data: {
|
|
||||||
twitchEventId: event.id,
|
|
||||||
viewerTwitchId: event.user_id,
|
|
||||||
waifuId: pick.waifuId,
|
|
||||||
userId: user.id
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return reply.code(204).send();
|
|
||||||
} else if (messageType === MESSAGE_TYPE_VERIFICATION) {
|
|
||||||
return reply.type('text/plain').code(200).send(notification.challenge);
|
|
||||||
} else if (messageType === MESSAGE_TYPE_REVOCATION) {
|
|
||||||
logger.debug(`${notification.subscription.type} notifications revoked!`);
|
|
||||||
logger.debug(`reason: ${notification.subscription.status}`);
|
|
||||||
logger.debug(`condition: ${JSON.stringify(notification.subscription.condition, null, 4)}`);
|
|
||||||
return reply.code(204).send();
|
|
||||||
} else {
|
|
||||||
logger.debug(`Unknown message type: ${messageType}`);
|
|
||||||
return reply.code(204).send();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.debug('403 - Invalid signature');
|
|
||||||
return reply.code(403).send();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Register a content-type parser for raw application/json
|
|
||||||
fastify.addContentTypeParser('application/json', { parseAs: 'buffer' }, function (req, body, done) {
|
|
||||||
done(null, body);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -62,7 +62,7 @@ function rewriteMp4ReferencesWithSignedUrls(
|
|||||||
signFn: (path: string) => string
|
signFn: (path: string) => string
|
||||||
): string {
|
): string {
|
||||||
|
|
||||||
logger.debug(`rewriteMp4ReferencesWithSignedUrls called with ${playlistContent} ${cdnBasePath} ${signFn}`)
|
logger.trace(`rewriteMp4ReferencesWithSignedUrls called with ${playlistContent} ${cdnBasePath} ${signFn}`)
|
||||||
|
|
||||||
const cleanBase = cdnBasePath.replace(/^\/|\/$/g, '') // remove leading/trailing slash
|
const cleanBase = cdnBasePath.replace(/^\/|\/$/g, '') // remove leading/trailing slash
|
||||||
|
|
||||||
@ -120,7 +120,7 @@ export default async function registerHlsRoute(app: FastifyInstance) {
|
|||||||
|
|
||||||
// Otherwise, rewrite .mp4 references with signed URLs
|
// Otherwise, rewrite .mp4 references with signed URLs
|
||||||
const tokenPath = `/${dirname(vod.hlsPlaylist)}/`
|
const tokenPath = `/${dirname(vod.hlsPlaylist)}/`
|
||||||
logger.debug(`tokenPath=${tokenPath} hlsPlaylist=${vod.hlsPlaylist}`)
|
logger.trace(`tokenPath=${tokenPath} hlsPlaylist=${vod.hlsPlaylist}`)
|
||||||
|
|
||||||
if (!tokenPath.startsWith('/')) {
|
if (!tokenPath.startsWith('/')) {
|
||||||
throw new Error('tokenPath did not start with a forward slash');
|
throw new Error('tokenPath did not start with a forward slash');
|
||||||
|
@ -297,7 +297,6 @@ export default async function streamsRoutes(
|
|||||||
// const isSelfRequest = onBehalfOf === requester.twitchName;
|
// const isSelfRequest = onBehalfOf === requester.twitchName;
|
||||||
|
|
||||||
// if (!isSelfRequest) {
|
// if (!isSelfRequest) {
|
||||||
// const authorized = await isEditorAuthorized(requester.twitchName, onBehalfOf);
|
|
||||||
|
|
||||||
// if (!authorized) {
|
// if (!authorized) {
|
||||||
// return reply.status(401).send({
|
// return reply.status(401).send({
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { type FastifyInstance } from 'fastify'
|
import { type FastifyInstance } from 'fastify'
|
||||||
import { isEditorAuthorized } from '../utils/authorization'
|
|
||||||
import { OnBehalfQuery } from '../types'
|
import { OnBehalfQuery } from '../types'
|
||||||
import { PrismaClient, type User } from '../../generated/prisma'
|
import { PrismaClient, type User } from '../../generated/prisma'
|
||||||
import { withAccelerate } from "@prisma/extension-accelerate"
|
import { withAccelerate } from "@prisma/extension-accelerate"
|
||||||
@ -88,65 +87,6 @@ export default async function usersRoutes(
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
fastify.get('/settings', async function (request, reply) {
|
|
||||||
const { onBehalfOf } = request.query as {
|
|
||||||
onBehalfOf?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const userId = request.session.get('user_id');
|
|
||||||
const requester = await prisma.user.findFirstOrThrow({
|
|
||||||
where: { id: userId }
|
|
||||||
});
|
|
||||||
|
|
||||||
let targetUser = requester;
|
|
||||||
|
|
||||||
if (onBehalfOf) {
|
|
||||||
if (!requester.twitchName) {
|
|
||||||
return reply.status(500).send({
|
|
||||||
error: true,
|
|
||||||
message:
|
|
||||||
'Requesting editor does not have a twitchName defined. Please log out and in, then contact admin if error persists.'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const isSelfRequest = onBehalfOf === requester.twitchName;
|
|
||||||
|
|
||||||
if (!isSelfRequest) {
|
|
||||||
const authorized = await isEditorAuthorized(requester.twitchName, onBehalfOf);
|
|
||||||
|
|
||||||
if (!authorized) {
|
|
||||||
return reply.status(401).send({
|
|
||||||
error: true,
|
|
||||||
message: 'Requesting editor is not authorized to edit settings for this channel.'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
targetUser = await prisma.user.findFirstOrThrow({
|
|
||||||
where: { twitchName: onBehalfOf }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
twitchChannels,
|
|
||||||
waifuChoicePoolSize,
|
|
||||||
maxOnScreenWaifus,
|
|
||||||
editorTwitchNames,
|
|
||||||
modsAreEditors,
|
|
||||||
twitchName,
|
|
||||||
redeemCost,
|
|
||||||
} = targetUser;
|
|
||||||
|
|
||||||
reply.send({
|
|
||||||
waifuChoicePoolSize,
|
|
||||||
maxOnScreenWaifus,
|
|
||||||
editorTwitchNames,
|
|
||||||
modsAreEditors,
|
|
||||||
twitchChannels,
|
|
||||||
twitchName,
|
|
||||||
redeemCost
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
@ -7,36 +7,12 @@ import logger from './logger';
|
|||||||
|
|
||||||
const prisma = new PrismaClient().$extends(withAccelerate())
|
const prisma = new PrismaClient().$extends(withAccelerate())
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the editor is authorized to act on behalf of a given Twitch user.
|
|
||||||
*
|
|
||||||
* @param editorName - The Twitch username of the editor.
|
|
||||||
* @param onBehalfOf - The Twitch username of the user being represented.
|
|
||||||
* @returns `true` if editorName is listed in onBehalfOf's editorTwitchNames, otherwise `false`.
|
|
||||||
*/
|
|
||||||
export async function isEditorAuthorized(editorName: string, onBehalfOf: string): Promise<boolean> {
|
|
||||||
const user = await prisma.user.findFirst({
|
|
||||||
where: {
|
|
||||||
twitchName: onBehalfOf,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
editorTwitchNames: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) return false;
|
|
||||||
|
|
||||||
return user.editorTwitchNames.includes(editorName);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export async function getTargetUser(
|
export async function getTargetUser(
|
||||||
request: FastifyRequest,
|
request: FastifyRequest,
|
||||||
reply: FastifyReply
|
reply: FastifyReply
|
||||||
) {
|
) {
|
||||||
const { onBehalfOf } = request.query as OnBehalfQuery;
|
|
||||||
|
|
||||||
const userId = request.session.get('user_id');
|
const userId = request.session.get('user_id');
|
||||||
|
|
||||||
@ -44,35 +20,17 @@ export async function getTargetUser(
|
|||||||
where: { id: userId }
|
where: { id: userId }
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!onBehalfOf) {
|
|
||||||
logger.warn(`we have found the condition where onBehalfOf not set`)
|
|
||||||
return requester
|
|
||||||
} else if (onBehalfOf === requester.twitchName) {
|
|
||||||
logger.warn(`we have found the condtion where onBehalfOf is the same name as requester.twitchName`)
|
|
||||||
return requester;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!requester.twitchName) {
|
|
||||||
|
if (!requester.patreonId) {
|
||||||
reply.status(500).send({
|
reply.status(500).send({
|
||||||
error: true,
|
error: true,
|
||||||
message:
|
message:
|
||||||
'Requesting editor does not have a twitchName defined. Please log out and in, then contact admin if error persists.'
|
'Requesting user does not have a patreonId defined. Please log out and in, then contact admin if error persists.'
|
||||||
});
|
});
|
||||||
throw new Error('Unauthorized'); // Prevent downstream execution
|
throw new Error('Unauthorized'); // Prevent downstream execution
|
||||||
}
|
}
|
||||||
|
|
||||||
const authorized = await isEditorAuthorized(requester.twitchName, onBehalfOf);
|
return requester;
|
||||||
if (!authorized) {
|
|
||||||
reply.status(401).send({
|
|
||||||
error: true,
|
|
||||||
message: 'Requesting editor is not authorized to edit settings for this channel.'
|
|
||||||
});
|
|
||||||
throw new Error('Unauthorized');
|
|
||||||
}
|
|
||||||
|
|
||||||
const targetUser = await prisma.user.findFirstOrThrow({
|
|
||||||
where: { twitchName: onBehalfOf }
|
|
||||||
});
|
|
||||||
|
|
||||||
return targetUser;
|
|
||||||
}
|
}
|
@ -39,6 +39,7 @@ export async function getOrDownloadAsset(client: S3Client, bucket: string, key:
|
|||||||
const cacheKey = `${bucket}:${key}`;
|
const cacheKey = `${bucket}:${key}`;
|
||||||
const lockKey = `${cacheKey}:lock`;
|
const lockKey = `${cacheKey}:lock`;
|
||||||
|
|
||||||
|
|
||||||
// 1. Check cache first (non-blocking)
|
// 1. Check cache first (non-blocking)
|
||||||
const cachedPath = await cache.get<string>(cacheKey);
|
const cachedPath = await cache.get<string>(cacheKey);
|
||||||
if (cachedPath && existsSync(cachedPath)) {
|
if (cachedPath && existsSync(cachedPath)) {
|
||||||
@ -48,6 +49,7 @@ export async function getOrDownloadAsset(client: S3Client, bucket: string, key:
|
|||||||
// 2. Ensure directory exists
|
// 2. Ensure directory exists
|
||||||
await mkdirp(dir);
|
await mkdirp(dir);
|
||||||
|
|
||||||
|
|
||||||
// 3. Acquire distributed lock
|
// 3. Acquire distributed lock
|
||||||
let acquiredLock = false;
|
let acquiredLock = false;
|
||||||
let retryCount = 0;
|
let retryCount = 0;
|
||||||
|
@ -57,7 +57,7 @@ export function registerHbsHelpers(Handlebars: typeof HandlebarsLib) {
|
|||||||
});
|
});
|
||||||
Handlebars.registerHelper('getCdnUrl', function (s3Key) {
|
Handlebars.registerHelper('getCdnUrl', function (s3Key) {
|
||||||
// Before you remove this log, find a way to memoize this function!
|
// Before you remove this log, find a way to memoize this function!
|
||||||
logger.info(`getCdnUrl called with CDN_ORIGIN=${env.CDN_ORIGIN} and CDN_TOKEN_SECRET=${env.CDN_TOKEN_SECRET}`)
|
logger.trace(`getCdnUrl called with CDN_ORIGIN=${env.CDN_ORIGIN} and CDN_TOKEN_SECRET=${env.CDN_TOKEN_SECRET}`)
|
||||||
return signUrl(`${env.CDN_ORIGIN}/${s3Key}`, {
|
return signUrl(`${env.CDN_ORIGIN}/${s3Key}`, {
|
||||||
securityKey: env.CDN_TOKEN_SECRET,
|
securityKey: env.CDN_TOKEN_SECRET,
|
||||||
expirationTime: constants.timeUnits.sevenDaysInSeconds,
|
expirationTime: constants.timeUnits.sevenDaysInSeconds,
|
||||||
@ -75,7 +75,7 @@ export function registerHbsHelpers(Handlebars: typeof HandlebarsLib) {
|
|||||||
isDirectory: true,
|
isDirectory: true,
|
||||||
expirationTime: constants.timeUnits.sevenDaysInSeconds,
|
expirationTime: constants.timeUnits.sevenDaysInSeconds,
|
||||||
})
|
})
|
||||||
logger.debug(`pathAllowed=${pathAllowed} url=${url}`)
|
logger.trace(`pathAllowed=${pathAllowed} url=${url}`)
|
||||||
return url
|
return url
|
||||||
})
|
})
|
||||||
Handlebars.registerHelper('basename', function (url: string) {
|
Handlebars.registerHelper('basename', function (url: string) {
|
||||||
|
@ -43,6 +43,7 @@
|
|||||||
|
|
||||||
{{{body}}}
|
{{{body}}}
|
||||||
|
|
||||||
|
|
||||||
<script src="/assets/js/htmx.min.js"></script>
|
<script src="/assets/js/htmx.min.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
@ -18,13 +18,14 @@
|
|||||||
<a class="navbar-item" href="/vods">VODs</a>
|
<a class="navbar-item" href="/vods">VODs</a>
|
||||||
{{!-- <a class="navbar-item" href="/streams"><s>🚧 Streams</s></a> @todo --}}
|
{{!-- <a class="navbar-item" href="/streams"><s>🚧 Streams</s></a> @todo --}}
|
||||||
<a class="navbar-item" href="/vt">VTubers</a>
|
<a class="navbar-item" href="/vt">VTubers</a>
|
||||||
<a class="navbar-item" href="/perks">Perks</a>
|
<a class="navbar-item" href="/perks">Patron Perks</a>
|
||||||
|
|
||||||
{{#if (hasRole "supporterTier1" "moderator" "admin" user)}}
|
{{#if (hasRole "supporterTier1" "moderator" "admin" user)}}
|
||||||
<a class="navbar-item" href="/uploads">Uploads</a>
|
<a class="navbar-item" href="/uploads">Uploads</a>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#if (hasRole "supporterTier1" "supporterTier2" "supporterTier3" "supporterTier4" "supporterTier5" "supporterTier6" "moderator" "admin" user)}}
|
{{#if (hasRole "supporterTier1" "supporterTier2" "supporterTier3" "supporterTier4" "supporterTier5"
|
||||||
|
"supporterTier6" "moderator" "admin" user)}}
|
||||||
<a class="navbar-item" href="/upload">
|
<a class="navbar-item" href="/upload">
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
<main class="container">
|
<main class="container">
|
||||||
<section id="perks">
|
<section id="perks">
|
||||||
<h2 class="title is-1">Perks</h2>
|
<h2 class="title is-1">Patron Perks</h2>
|
||||||
|
|
||||||
<p class="subtitle">We need your help to keep the site running! In return, we offer
|
<p class="subtitle">We need your help to keep the site running! In return, we offer
|
||||||
extra perks
|
extra perks
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{{#> main}}
|
{{#> main}}
|
||||||
|
|
||||||
|
|
||||||
<link href="/assets/vod.css" rel="stylesheet">
|
<link href="/assets/vod.css" rel="stylesheet">
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user