Improve billing and access control by adding creator subscription check; update route components to enforce user authentication before viewing streams and replays.

This commit is contained in:
Nikita
2025-12-24 17:14:01 -08:00
parent 9d7ea0ada1
commit f81affe448
4 changed files with 64 additions and 8 deletions

View File

@@ -1,7 +1,7 @@
import { getFlowgladServer } from "./flowglad"
import { getAuth } from "./auth"
import { db } from "@/db/connection"
import { stripe_subscriptions, storage_usage } from "@/db/schema"
import { stripe_subscriptions, storage_usage, creator_subscriptions } from "@/db/schema"
import { eq, and, gte, lte } from "drizzle-orm"
// Usage limits
@@ -528,3 +528,27 @@ export async function hasActiveSubscription(userId: string): Promise<boolean> {
return !!subscription
}
/**
* Check if a user has an active subscription to a specific creator (server-side only)
*/
export async function hasCreatorSubscription(
subscriberId: string,
creatorId: string
): Promise<boolean> {
const database = db()
const [subscription] = await database
.select()
.from(creator_subscriptions)
.where(
and(
eq(creator_subscriptions.subscriber_id, subscriberId),
eq(creator_subscriptions.creator_id, creatorId),
eq(creator_subscriptions.status, "active")
)
)
.limit(1)
return !!subscription
}

View File

@@ -1,5 +1,5 @@
import { useEffect, useRef, useState } from "react"
import { createFileRoute } from "@tanstack/react-router"
import { createFileRoute, Link } from "@tanstack/react-router"
import { getStreamByUsername, type StreamPageData } from "@/lib/stream/db"
import { VideoPlayer } from "@/components/VideoPlayer"
import { CloudflareStreamPlayer } from "@/components/CloudflareStreamPlayer"
@@ -13,6 +13,7 @@ import {
type SpotifyNowPlayingResponse,
} from "@/lib/spotify/now-playing"
import { getStreamStatus } from "@/lib/stream/status"
import { authClient } from "@/lib/auth-client"
export const Route = createFileRoute("/$username")({
ssr: false,
@@ -48,6 +49,7 @@ const NIKIV_DATA: StreamPageData = {
function StreamPage() {
const { username } = Route.useParams()
const { data: session, isPending: sessionLoading } = authClient.useSession()
const [data, setData] = useState<StreamPageData | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
@@ -63,6 +65,8 @@ function StreamPage() {
const [showReadyPulse, setShowReadyPulse] = useState(false)
const readyPulseTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const isAuthenticated = !sessionLoading && !!session?.user
useEffect(() => {
let isActive = true
const setReadySafe = (ready: boolean) => {
@@ -312,6 +316,34 @@ function StreamPage() {
}
}, [shouldFetchSpotify])
// Auth gate - require login to view streams
if (sessionLoading) {
return (
<div className="flex min-h-screen items-center justify-center bg-black text-white">
<div className="text-xl">Loading...</div>
</div>
)
}
if (!isAuthenticated) {
return (
<div className="flex min-h-screen items-center justify-center bg-black text-white">
<div className="text-center">
<h1 className="text-4xl font-bold mb-4">Sign in to watch</h1>
<p className="text-neutral-400 mb-8">
Create an account or sign in to view this stream
</p>
<Link
to="/login"
className="inline-block rounded-lg bg-white px-6 py-3 font-medium text-black hover:bg-neutral-200 transition-colors"
>
Sign in
</Link>
</div>
</div>
)
}
if (loading) {
return (
<div className="flex min-h-screen items-center justify-center bg-black text-white">

View File

@@ -2,7 +2,7 @@ import { createFileRoute } from "@tanstack/react-router"
import { and, eq } from "drizzle-orm"
import { db } from "@/db/connection"
import { getAuth } from "@/lib/auth"
import { hasActiveSubscription } from "@/lib/billing"
import { hasCreatorSubscription } from "@/lib/billing"
import { stream_replays, streams } from "@/db/schema"
const json = (data: unknown, status = 200) =>
@@ -99,7 +99,7 @@ const handleGet = async ({
return json({ replay })
}
// Non-owners need subscription to view replays
// Non-owners need subscription to this creator to view replays
if (!session?.user?.id) {
return json(
{ error: "Subscription required", code: "SUBSCRIPTION_REQUIRED" },
@@ -107,7 +107,7 @@ const handleGet = async ({
)
}
const hasSubscription = await hasActiveSubscription(session.user.id)
const hasSubscription = await hasCreatorSubscription(session.user.id, replay.user_id)
if (!hasSubscription) {
return json(
{ error: "Subscription required", code: "SUBSCRIPTION_REQUIRED" },

View File

@@ -2,7 +2,7 @@ import { createFileRoute } from "@tanstack/react-router"
import { and, desc, eq } from "drizzle-orm"
import { db } from "@/db/connection"
import { getAuth } from "@/lib/auth"
import { hasActiveSubscription } from "@/lib/billing"
import { hasCreatorSubscription } from "@/lib/billing"
import { stream_replays, users } from "@/db/schema"
const json = (data: unknown, status = 200) =>
@@ -56,7 +56,7 @@ const handleGet = async ({
}
}
// Non-owners need subscription to view replays
// Non-owners need subscription to this creator to view replays
if (!session?.user?.id) {
return json(
{ error: "Subscription required", code: "SUBSCRIPTION_REQUIRED" },
@@ -64,7 +64,7 @@ const handleGet = async ({
)
}
const hasSubscription = await hasActiveSubscription(session.user.id)
const hasSubscription = await hasCreatorSubscription(session.user.id, user.id)
if (!hasSubscription) {
return json(
{ error: "Subscription required", code: "SUBSCRIPTION_REQUIRED" },