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",
|
||||
"version": "2.6.0",
|
||||
"version": "2.7.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "futureporn-our",
|
||||
"version": "2.6.0",
|
||||
"version": "2.7.1",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.726.1",
|
||||
"@aws-sdk/s3-request-presigner": "^3.844.0",
|
||||
@ -37,6 +37,7 @@
|
||||
"concurrently": "^9.2.0",
|
||||
"create-torrent": "^6.1.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"env-paths": "^3.0.0",
|
||||
"fastify": "^5.4.0",
|
||||
"fastify-favicon": "^5.0.0",
|
||||
"fastify-plugin": "^5.0.1",
|
||||
@ -6729,6 +6730,18 @@
|
||||
"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": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
|
||||
|
@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "futureporn-our",
|
||||
"private": true,
|
||||
"version": "2.7.1",
|
||||
"version": "2.8.0",
|
||||
"type": "module",
|
||||
"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: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",
|
||||
@ -13,13 +13,14 @@
|
||||
"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: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:server": "tsx ./src/index.ts",
|
||||
"start:worker": "tsx ./src/worker.ts",
|
||||
"preview": "vite preview",
|
||||
"build": "tsup --clean",
|
||||
"lint": "eslint .",
|
||||
"clean": "rm -rf node_modules && rm -rf pnpm-lock.yaml",
|
||||
"clean": "rimraf ./dist",
|
||||
"deploy": "npx prisma migrate deploy",
|
||||
"test:watch": "npx vitest --watch",
|
||||
"test": "npx vitest"
|
||||
@ -80,6 +81,7 @@
|
||||
"concurrently": "^9.2.0",
|
||||
"create-torrent": "^6.1.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"env-paths": "^3.0.0",
|
||||
"fastify": "^5.4.0",
|
||||
"fastify-favicon": "^5.0.0",
|
||||
"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 fastifyFavicon from 'fastify-favicon'
|
||||
|
||||
|
||||
const __dirname = import.meta.dirname;
|
||||
|
||||
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, {
|
||||
root: path.join(__dirname, '..', 'dist', 'client'),
|
||||
prefix: '/assets', // optional: default '/'
|
||||
|
@ -1,33 +1,5 @@
|
||||
|
||||
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: {
|
||||
authScopes: [
|
||||
'identity'
|
||||
|
@ -1,10 +1,11 @@
|
||||
// ./config/env.ts
|
||||
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({
|
||||
NODE_ENV: z.enum(['development', 'production', 'test']),
|
||||
@ -26,7 +27,7 @@ const EnvSchema = z.object({
|
||||
S3_FORCE_PATH_STYLE: z.coerce.boolean().default(false),
|
||||
CDN_ORIGIN: 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'),
|
||||
APP_DIR: z.string().default('/app'),
|
||||
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
|
||||
): 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
|
||||
|
||||
@ -120,7 +120,7 @@ export default async function registerHlsRoute(app: FastifyInstance) {
|
||||
|
||||
// Otherwise, rewrite .mp4 references with signed URLs
|
||||
const tokenPath = `/${dirname(vod.hlsPlaylist)}/`
|
||||
logger.debug(`tokenPath=${tokenPath} hlsPlaylist=${vod.hlsPlaylist}`)
|
||||
logger.trace(`tokenPath=${tokenPath} hlsPlaylist=${vod.hlsPlaylist}`)
|
||||
|
||||
if (!tokenPath.startsWith('/')) {
|
||||
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;
|
||||
|
||||
// if (!isSelfRequest) {
|
||||
// const authorized = await isEditorAuthorized(requester.twitchName, onBehalfOf);
|
||||
|
||||
// if (!authorized) {
|
||||
// return reply.status(401).send({
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { type FastifyInstance } from 'fastify'
|
||||
import { isEditorAuthorized } from '../utils/authorization'
|
||||
import { OnBehalfQuery } from '../types'
|
||||
import { PrismaClient, type User } from '../../generated/prisma'
|
||||
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())
|
||||
|
||||
/**
|
||||
* 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(
|
||||
request: FastifyRequest,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
const { onBehalfOf } = request.query as OnBehalfQuery;
|
||||
|
||||
const userId = request.session.get('user_id');
|
||||
|
||||
@ -44,35 +20,17 @@ export async function getTargetUser(
|
||||
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({
|
||||
error: true,
|
||||
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
|
||||
}
|
||||
|
||||
const authorized = await isEditorAuthorized(requester.twitchName, onBehalfOf);
|
||||
if (!authorized) {
|
||||
reply.status(401).send({
|
||||
error: true,
|
||||
message: 'Requesting editor is not authorized to edit settings for this channel.'
|
||||
});
|
||||
throw new Error('Unauthorized');
|
||||
}
|
||||
return requester;
|
||||
|
||||
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 lockKey = `${cacheKey}:lock`;
|
||||
|
||||
|
||||
// 1. Check cache first (non-blocking)
|
||||
const cachedPath = await cache.get<string>(cacheKey);
|
||||
if (cachedPath && existsSync(cachedPath)) {
|
||||
@ -48,6 +49,7 @@ export async function getOrDownloadAsset(client: S3Client, bucket: string, key:
|
||||
// 2. Ensure directory exists
|
||||
await mkdirp(dir);
|
||||
|
||||
|
||||
// 3. Acquire distributed lock
|
||||
let acquiredLock = false;
|
||||
let retryCount = 0;
|
||||
|
@ -57,7 +57,7 @@ export function registerHbsHelpers(Handlebars: typeof HandlebarsLib) {
|
||||
});
|
||||
Handlebars.registerHelper('getCdnUrl', function (s3Key) {
|
||||
// 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}`, {
|
||||
securityKey: env.CDN_TOKEN_SECRET,
|
||||
expirationTime: constants.timeUnits.sevenDaysInSeconds,
|
||||
@ -75,7 +75,7 @@ export function registerHbsHelpers(Handlebars: typeof HandlebarsLib) {
|
||||
isDirectory: true,
|
||||
expirationTime: constants.timeUnits.sevenDaysInSeconds,
|
||||
})
|
||||
logger.debug(`pathAllowed=${pathAllowed} url=${url}`)
|
||||
logger.trace(`pathAllowed=${pathAllowed} url=${url}`)
|
||||
return url
|
||||
})
|
||||
Handlebars.registerHelper('basename', function (url: string) {
|
||||
|
@ -43,6 +43,7 @@
|
||||
|
||||
{{{body}}}
|
||||
|
||||
|
||||
<script src="/assets/js/htmx.min.js"></script>
|
||||
|
||||
<script>
|
||||
|
@ -18,13 +18,14 @@
|
||||
<a class="navbar-item" href="/vods">VODs</a>
|
||||
{{!-- <a class="navbar-item" href="/streams"><s>🚧 Streams</s></a> @todo --}}
|
||||
<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)}}
|
||||
<a class="navbar-item" href="/uploads">Uploads</a>
|
||||
{{/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">
|
||||
<span class="icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
|
@ -6,7 +6,7 @@
|
||||
|
||||
<main class="container">
|
||||
<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
|
||||
extra perks
|
||||
|
@ -1,5 +1,6 @@
|
||||
{{#> main}}
|
||||
|
||||
|
||||
<link href="/assets/vod.css" rel="stylesheet">
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user