Bug fixes for video serving and updats to documentation for mobile use screenshots
This commit is contained in:
@@ -25,7 +25,12 @@ COPY tsconfig.json ./
|
||||
|
||||
# Development stage with hot reload
|
||||
FROM base AS development
|
||||
# su-exec for dropping privileges after fixing mounted volume permissions
|
||||
RUN apk add --no-cache su-exec
|
||||
RUN npm install tsx --save-dev
|
||||
COPY media-docker-entrypoint.sh /usr/local/bin/
|
||||
RUN chmod +x /usr/local/bin/media-docker-entrypoint.sh
|
||||
ENTRYPOINT ["media-docker-entrypoint.sh"]
|
||||
CMD ["npx", "tsx", "watch", "src/media-server.ts"]
|
||||
|
||||
# Build stage
|
||||
@@ -37,8 +42,9 @@ RUN npm run build
|
||||
FROM node:20-alpine AS production
|
||||
WORKDIR /app
|
||||
|
||||
# Install ffmpeg for video metadata, vips-dev for sharp HEIC support, yt-dlp for video fetching
|
||||
RUN apk add --no-cache ffmpeg vips-dev python3 py3-pip && pip3 install --break-system-packages yt-dlp
|
||||
# Install ffmpeg for video metadata, vips-dev for sharp HEIC support, yt-dlp for video fetching.
|
||||
# su-exec lets the entrypoint drop privileges after fixing mounted volume permissions.
|
||||
RUN apk add --no-cache ffmpeg vips-dev python3 py3-pip su-exec && pip3 install --break-system-packages yt-dlp
|
||||
|
||||
# Copy manifests and install production-only deps (no devDeps like typescript)
|
||||
COPY --from=build /app/package*.json ./
|
||||
@@ -50,7 +56,11 @@ COPY --from=build /app/dist ./dist
|
||||
COPY --from=build /app/prisma ./prisma
|
||||
RUN npx prisma generate
|
||||
|
||||
# Run as non-root user
|
||||
USER node
|
||||
# Entrypoint chowns mounted /media subdirs (root-owned on fresh deploys) then
|
||||
# drops to node via su-exec. Note: USER node is intentionally NOT set here —
|
||||
# the entrypoint must start as root to repair host bind-mount permissions.
|
||||
COPY media-docker-entrypoint.sh /usr/local/bin/
|
||||
RUN chmod +x /usr/local/bin/media-docker-entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["media-docker-entrypoint.sh"]
|
||||
CMD ["node", "dist/media-server.js"]
|
||||
|
||||
22
api/media-docker-entrypoint.sh
Executable file
22
api/media-docker-entrypoint.sh
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Fix permissions for mounted volumes (host dirs may be root-owned on first
|
||||
# run — Docker auto-creates missing bind-mount sources as root:root).
|
||||
# /media's :ro parent mount cannot be chowned, but the :rw subdirs that
|
||||
# media-api writes into can — and that's all the process needs.
|
||||
# /app/logs is shared with the api container via the ./api:/app dev mount;
|
||||
# api's entrypoint already chowns it but media-api may start independently.
|
||||
if [ "$(id -u)" = "0" ]; then
|
||||
for d in /media/local/inbox /media/local/thumbnails /media/local/photos \
|
||||
/media/local/documents /media/public /app/logs; do
|
||||
[ -d "$d" ] && chown -R node:node "$d" 2>/dev/null || true
|
||||
done
|
||||
fi
|
||||
|
||||
# Drop to node user if running as root (production image uses su-exec).
|
||||
if [ "$(id -u)" = "0" ] && command -v su-exec >/dev/null 2>&1; then
|
||||
exec su-exec node "$@"
|
||||
else
|
||||
exec "$@"
|
||||
fi
|
||||
@@ -3,8 +3,8 @@ import { prisma } from '../../../config/database';
|
||||
import { requireAdminRole } from '../middleware/auth';
|
||||
import { videoAnalyticsService } from '../services/video-analytics.service';
|
||||
import { logger } from '../../../utils/logger';
|
||||
import { sign } from 'jsonwebtoken';
|
||||
import { env } from '../../../config/env';
|
||||
import { signMediaPath } from '../../../utils/signed-url';
|
||||
import { copyFile } from 'fs/promises';
|
||||
import { join, dirname, basename, extname, normalize, resolve } from 'path';
|
||||
import { z } from 'zod';
|
||||
@@ -299,7 +299,13 @@ export async function videoActionsRoutes(fastify: FastifyInstance) {
|
||||
|
||||
/**
|
||||
* GET /videos/:id/preview-link
|
||||
* Generate a temporary preview link with expiring JWT token
|
||||
* Generate a shareable, time-limited preview link.
|
||||
*
|
||||
* Uses path-bound HMAC signatures (sig/exp/uid) — same scheme as
|
||||
* POST /api/media/sign — instead of the legacy `?token=<JWT>` form,
|
||||
* which leaked full session tokens via access logs/referer headers.
|
||||
* The signature carries only the admin's user-id and is bound to the
|
||||
* stream URL, so it can be safely shared with stakeholders.
|
||||
*/
|
||||
fastify.get<{ Params: { id: string } }>(
|
||||
'/:id/preview-link',
|
||||
@@ -318,24 +324,20 @@ export async function videoActionsRoutes(fastify: FastifyInstance) {
|
||||
return reply.code(404).send({ message: 'Video not found' });
|
||||
}
|
||||
|
||||
// Generate JWT token that expires in 24 hours
|
||||
const expiryHours = parseInt(process.env.VIDEO_PREVIEW_LINK_EXPIRY_HOURS || '24');
|
||||
const token = sign(
|
||||
{
|
||||
videoId,
|
||||
purpose: 'preview',
|
||||
},
|
||||
env.JWT_ACCESS_SECRET,
|
||||
{ expiresIn: `${expiryHours}h` }
|
||||
);
|
||||
const ttlSeconds = expiryHours * 60 * 60;
|
||||
const userId = request.user!.id;
|
||||
|
||||
const previewUrl = `${env.MEDIA_API_PUBLIC_URL}/api/videos/${videoId}/preview?token=${token}`;
|
||||
const streamPath = `/api/videos/${videoId}/stream`;
|
||||
const signed = signMediaPath(streamPath, userId, ttlSeconds);
|
||||
const query = `sig=${signed.sig}&exp=${signed.exp}&uid=${signed.uid}`;
|
||||
const previewUrl = `${env.MEDIA_API_PUBLIC_URL}${streamPath}?${query}`;
|
||||
|
||||
logger.info(`Generated preview link for video ${videoId}`, { expiresInHours: expiryHours });
|
||||
|
||||
return {
|
||||
previewUrl,
|
||||
expiresAt: new Date(Date.now() + expiryHours * 60 * 60 * 1000).toISOString(),
|
||||
expiresAt: new Date(Number(signed.exp) * 1000).toISOString(),
|
||||
expiryHours,
|
||||
};
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user