mirror of
https://github.com/linsa-io/linsa.git
synced 2026-01-12 12:20:23 +01:00
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:
@@ -1,7 +1,7 @@
|
|||||||
import { getFlowgladServer } from "./flowglad"
|
import { getFlowgladServer } from "./flowglad"
|
||||||
import { getAuth } from "./auth"
|
import { getAuth } from "./auth"
|
||||||
import { db } from "@/db/connection"
|
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"
|
import { eq, and, gte, lte } from "drizzle-orm"
|
||||||
|
|
||||||
// Usage limits
|
// Usage limits
|
||||||
@@ -528,3 +528,27 @@ export async function hasActiveSubscription(userId: string): Promise<boolean> {
|
|||||||
|
|
||||||
return !!subscription
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useRef, useState } from "react"
|
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 { getStreamByUsername, type StreamPageData } from "@/lib/stream/db"
|
||||||
import { VideoPlayer } from "@/components/VideoPlayer"
|
import { VideoPlayer } from "@/components/VideoPlayer"
|
||||||
import { CloudflareStreamPlayer } from "@/components/CloudflareStreamPlayer"
|
import { CloudflareStreamPlayer } from "@/components/CloudflareStreamPlayer"
|
||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
type SpotifyNowPlayingResponse,
|
type SpotifyNowPlayingResponse,
|
||||||
} from "@/lib/spotify/now-playing"
|
} from "@/lib/spotify/now-playing"
|
||||||
import { getStreamStatus } from "@/lib/stream/status"
|
import { getStreamStatus } from "@/lib/stream/status"
|
||||||
|
import { authClient } from "@/lib/auth-client"
|
||||||
|
|
||||||
export const Route = createFileRoute("/$username")({
|
export const Route = createFileRoute("/$username")({
|
||||||
ssr: false,
|
ssr: false,
|
||||||
@@ -48,6 +49,7 @@ const NIKIV_DATA: StreamPageData = {
|
|||||||
|
|
||||||
function StreamPage() {
|
function StreamPage() {
|
||||||
const { username } = Route.useParams()
|
const { username } = Route.useParams()
|
||||||
|
const { data: session, isPending: sessionLoading } = authClient.useSession()
|
||||||
const [data, setData] = useState<StreamPageData | null>(null)
|
const [data, setData] = useState<StreamPageData | null>(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
@@ -63,6 +65,8 @@ function StreamPage() {
|
|||||||
const [showReadyPulse, setShowReadyPulse] = useState(false)
|
const [showReadyPulse, setShowReadyPulse] = useState(false)
|
||||||
const readyPulseTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
const readyPulseTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
|
|
||||||
|
const isAuthenticated = !sessionLoading && !!session?.user
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isActive = true
|
let isActive = true
|
||||||
const setReadySafe = (ready: boolean) => {
|
const setReadySafe = (ready: boolean) => {
|
||||||
@@ -312,6 +316,34 @@ function StreamPage() {
|
|||||||
}
|
}
|
||||||
}, [shouldFetchSpotify])
|
}, [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) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center bg-black text-white">
|
<div className="flex min-h-screen items-center justify-center bg-black text-white">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { createFileRoute } from "@tanstack/react-router"
|
|||||||
import { and, eq } from "drizzle-orm"
|
import { and, eq } from "drizzle-orm"
|
||||||
import { db } from "@/db/connection"
|
import { db } from "@/db/connection"
|
||||||
import { getAuth } from "@/lib/auth"
|
import { getAuth } from "@/lib/auth"
|
||||||
import { hasActiveSubscription } from "@/lib/billing"
|
import { hasCreatorSubscription } from "@/lib/billing"
|
||||||
import { stream_replays, streams } from "@/db/schema"
|
import { stream_replays, streams } from "@/db/schema"
|
||||||
|
|
||||||
const json = (data: unknown, status = 200) =>
|
const json = (data: unknown, status = 200) =>
|
||||||
@@ -99,7 +99,7 @@ const handleGet = async ({
|
|||||||
return json({ replay })
|
return json({ replay })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Non-owners need subscription to view replays
|
// Non-owners need subscription to this creator to view replays
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) {
|
||||||
return json(
|
return json(
|
||||||
{ error: "Subscription required", code: "SUBSCRIPTION_REQUIRED" },
|
{ 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) {
|
if (!hasSubscription) {
|
||||||
return json(
|
return json(
|
||||||
{ error: "Subscription required", code: "SUBSCRIPTION_REQUIRED" },
|
{ error: "Subscription required", code: "SUBSCRIPTION_REQUIRED" },
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { createFileRoute } from "@tanstack/react-router"
|
|||||||
import { and, desc, eq } from "drizzle-orm"
|
import { and, desc, eq } from "drizzle-orm"
|
||||||
import { db } from "@/db/connection"
|
import { db } from "@/db/connection"
|
||||||
import { getAuth } from "@/lib/auth"
|
import { getAuth } from "@/lib/auth"
|
||||||
import { hasActiveSubscription } from "@/lib/billing"
|
import { hasCreatorSubscription } from "@/lib/billing"
|
||||||
import { stream_replays, users } from "@/db/schema"
|
import { stream_replays, users } from "@/db/schema"
|
||||||
|
|
||||||
const json = (data: unknown, status = 200) =>
|
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) {
|
if (!session?.user?.id) {
|
||||||
return json(
|
return json(
|
||||||
{ error: "Subscription required", code: "SUBSCRIPTION_REQUIRED" },
|
{ 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) {
|
if (!hasSubscription) {
|
||||||
return json(
|
return json(
|
||||||
{ error: "Subscription required", code: "SUBSCRIPTION_REQUIRED" },
|
{ error: "Subscription required", code: "SUBSCRIPTION_REQUIRED" },
|
||||||
|
|||||||
Reference in New Issue
Block a user