feat: add access control to channels with various modes

- Introduced AccessMode enum to define channel access levels: Public, PasswordProtected, AccountRequired, and OwnerOnly.
- Updated Channel and ProgrammingBlock entities to include access_mode and access_password_hash fields.
- Enhanced create and update channel functionality to handle access mode and password.
- Implemented access checks in channel routes based on the defined access modes.
- Modified frontend components to support channel creation and editing with access control options.
- Added ChannelPasswordModal for handling password input when accessing restricted channels.
- Updated API calls to include channel and block passwords as needed.
- Created database migrations to add access_mode and access_password_hash columns to channels table.
This commit is contained in:
2026-03-14 01:45:10 +01:00
parent 924e162563
commit 81df6eb8ff
25 changed files with 635 additions and 53 deletions

View File

@@ -0,0 +1,61 @@
"use client";
import { useState } from "react";
interface ChannelPasswordModalProps {
label: string;
onSubmit: (password: string) => void;
onCancel: () => void;
}
export function ChannelPasswordModal({
label,
onSubmit,
onCancel,
}: ChannelPasswordModalProps) {
const [value, setValue] = useState("");
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (value.trim()) onSubmit(value.trim());
};
return (
<div className="absolute inset-0 z-40 flex items-center justify-center bg-black/80 backdrop-blur-sm">
<form
onSubmit={handleSubmit}
className="flex w-80 flex-col gap-4 rounded-xl border border-zinc-700 bg-zinc-900 p-6 shadow-2xl"
>
<div className="text-center">
<p className="text-sm font-semibold uppercase tracking-widest text-zinc-400">
{label}
</p>
</div>
<input
type="password"
autoFocus
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="Enter password"
className="rounded-md border border-zinc-700 bg-zinc-800 px-3 py-2 text-sm text-zinc-100 placeholder:text-zinc-600 focus:border-zinc-500 focus:outline-none"
/>
<div className="flex gap-2">
<button
type="button"
onClick={onCancel}
className="flex-1 rounded-md border border-zinc-700 bg-zinc-800/60 px-4 py-2 text-xs text-zinc-400 transition-colors hover:bg-zinc-700 hover:text-zinc-100"
>
Cancel
</button>
<button
type="submit"
disabled={!value.trim()}
className="flex-1 rounded-md bg-zinc-100 px-4 py-2 text-xs font-medium text-zinc-900 transition-colors hover:bg-white disabled:opacity-40"
>
Unlock
</button>
</div>
</form>
</div>
);
}

View File

@@ -16,3 +16,5 @@ export type { UpNextBannerProps } from "./up-next-banner";
export { NoSignal } from "./no-signal";
export type { NoSignalProps, NoSignalVariant } from "./no-signal";
export { ChannelPasswordModal } from "./channel-password-modal";

View File

@@ -1,6 +1,6 @@
import { WifiOff, AlertTriangle, Loader2 } from "lucide-react";
import { WifiOff, AlertTriangle, Loader2, Lock } from "lucide-react";
type NoSignalVariant = "no-signal" | "error" | "loading";
type NoSignalVariant = "no-signal" | "error" | "loading" | "locked";
interface NoSignalProps {
variant?: NoSignalVariant;
@@ -27,6 +27,11 @@ const VARIANTS: Record<
heading: "Loading",
defaultMessage: "Tuning in…",
},
locked: {
icon: <Lock className="h-10 w-10 text-zinc-600" />,
heading: "Access Restricted",
defaultMessage: "You don't have permission to watch this channel.",
},
};
export function NoSignal({ variant = "no-signal", message, children }: NoSignalProps) {