diff --git a/.env.example b/.env.example index c2499ff..a0dacb5 100644 --- a/.env.example +++ b/.env.example @@ -45,6 +45,9 @@ RESEND_FROM=moshcoding # OpenAI — AI hero image for ?style= tenant pages (gpt-image-*). OPENAI_API_KEY=sk-xxx OPENAI_IMAGE_MODEL=gpt-image-2 -# Where generated images are cached. Mount a Railway volume here to persist -# across deploys (otherwise images regenerate after each deploy). +# Where generated images are cached AND uploaded reels (dashboard → Videos) are +# stored. Mount a Railway volume here to persist across deploys (otherwise +# generated images regenerate and uploaded videos are lost after each deploy). DATA_DIR=.data +# Max size per uploaded reel, in bytes (default 100 MB). +# MEDIA_MAX_BYTES=104857600 diff --git a/README.md b/README.md index 661513f..4530f55 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,10 @@ while (alive) { ## Brand kit -Everything in `images/` and `videos/` is the visual identity. +Everything in `images/` and `videos/` is the visual identity. A curated pair of +brand reels is served at [`/videos`](https://moshcoding.com/videos) (from +`public/videos/`); page owners can also upload their own mp4 reels per parked +domain from the dashboard. | Asset | File | |-------|------| diff --git a/app/api/account/route.ts b/app/api/account/route.ts index ac0c5ac..d39f730 100644 --- a/app/api/account/route.ts +++ b/app/api/account/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; -import { readSession, authConfigured, SESSION_COOKIE } from "@/lib/session"; -import { getAccountById, updateAccountProfile, updateAccountConfig, findOrCreateAccountByEmail, setAccountDomain, listParkedDomains } from "@/lib/db"; +import { getAccountById, updateAccountProfile, updateAccountConfig, setAccountDomain, listParkedDomains } from "@/lib/db"; +import { resolveAccountId } from "@/lib/api"; import { normalizeHandle, normalizeUrl, coerceRgba, parseHashtags, safeDomain } from "@/lib/config"; import { payUrl } from "@/lib/coinpay"; import { provisionTenant } from "@/lib/provision"; @@ -12,16 +12,6 @@ export const dynamic = "force-dynamic"; const PLATFORMS = ["x", "bluesky", "instagram", "tiktok", "github", "youtube"]; const TEXT_FIELDS = ["brand", "headline", "tagline", "sub"] as const; -/** Resolves the tenant account for the session: native (acct:) or CoinPay (by email). */ -async function resolveAccountId(req: NextRequest): Promise { - if (!authConfigured()) return null; - const s = readSession(req.cookies.get(SESSION_COOKIE)?.value); - if (!s) return null; - if (s.sub?.startsWith("acct:")) return s.sub.slice("acct:".length); - if (s.email) return (await findOrCreateAccountByEmail(s.email)).id; - return null; -} - function cleanWallet(v: unknown): string | null { if (typeof v !== "string") return null; const w = v.trim(); diff --git a/app/api/media/[id]/route.ts b/app/api/media/[id]/route.ts new file mode 100644 index 0000000..5e09d72 --- /dev/null +++ b/app/api/media/[id]/route.ts @@ -0,0 +1,64 @@ +import { NextRequest, NextResponse } from "next/server"; +import { Readable } from "node:stream"; +import { resolveAccountId, bad, unauthorized } from "@/lib/api"; +import { accountOwnsDomain, getMedia, deleteMedia } from "@/lib/db"; +import { mediaSize, mediaStream, deleteMediaFile } from "@/lib/media"; + +export const runtime = "nodejs"; +export const dynamic = "force-dynamic"; + +// Stream a reel with HTTP range support so