feat: implement find_all method in ChannelRepository and update related services and routes for public access
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
//! Channel routes
|
//! Channel routes
|
||||||
//!
|
//!
|
||||||
//! CRUD for channels and broadcast/EPG endpoints.
|
//! CRUD + schedule generation require authentication (Bearer JWT).
|
||||||
//!
|
//! Viewing endpoints (list, now, epg, stream) are intentionally public so the
|
||||||
//! All routes require authentication (Bearer JWT).
|
//! TV page works without login.
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
Json, Router,
|
Json, Router,
|
||||||
@@ -49,9 +49,8 @@ pub fn router() -> Router<AppState> {
|
|||||||
|
|
||||||
async fn list_channels(
|
async fn list_channels(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
CurrentUser(user): CurrentUser,
|
|
||||||
) -> Result<impl IntoResponse, ApiError> {
|
) -> Result<impl IntoResponse, ApiError> {
|
||||||
let channels = state.channel_service.find_by_owner(user.id).await?;
|
let channels = state.channel_service.find_all().await?;
|
||||||
let response: Vec<ChannelResponse> = channels.into_iter().map(Into::into).collect();
|
let response: Vec<ChannelResponse> = channels.into_iter().map(Into::into).collect();
|
||||||
Ok(Json(response))
|
Ok(Json(response))
|
||||||
}
|
}
|
||||||
@@ -173,11 +172,9 @@ async fn get_active_schedule(
|
|||||||
/// Returns 204 No Content when the channel is in a gap between blocks (no-signal).
|
/// Returns 204 No Content when the channel is in a gap between blocks (no-signal).
|
||||||
async fn get_current_broadcast(
|
async fn get_current_broadcast(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
CurrentUser(user): CurrentUser,
|
|
||||||
Path(channel_id): Path<Uuid>,
|
Path(channel_id): Path<Uuid>,
|
||||||
) -> Result<Response, ApiError> {
|
) -> Result<Response, ApiError> {
|
||||||
let channel = state.channel_service.find_by_id(channel_id).await?;
|
let _channel = state.channel_service.find_by_id(channel_id).await?;
|
||||||
require_owner(&channel, user.id)?;
|
|
||||||
|
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
let schedule = state
|
let schedule = state
|
||||||
@@ -209,12 +206,10 @@ struct EpgQuery {
|
|||||||
|
|
||||||
async fn get_epg(
|
async fn get_epg(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
CurrentUser(user): CurrentUser,
|
|
||||||
Path(channel_id): Path<Uuid>,
|
Path(channel_id): Path<Uuid>,
|
||||||
Query(params): Query<EpgQuery>,
|
Query(params): Query<EpgQuery>,
|
||||||
) -> Result<impl IntoResponse, ApiError> {
|
) -> Result<impl IntoResponse, ApiError> {
|
||||||
let channel = state.channel_service.find_by_id(channel_id).await?;
|
let _channel = state.channel_service.find_by_id(channel_id).await?;
|
||||||
require_owner(&channel, user.id)?;
|
|
||||||
|
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
let from = parse_optional_dt(params.from, now)?;
|
let from = parse_optional_dt(params.from, now)?;
|
||||||
@@ -244,11 +239,9 @@ async fn get_epg(
|
|||||||
/// Returns 204 No Content when the channel is in a gap (no-signal).
|
/// Returns 204 No Content when the channel is in a gap (no-signal).
|
||||||
async fn get_stream(
|
async fn get_stream(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
CurrentUser(user): CurrentUser,
|
|
||||||
Path(channel_id): Path<Uuid>,
|
Path(channel_id): Path<Uuid>,
|
||||||
) -> Result<Response, ApiError> {
|
) -> Result<Response, ApiError> {
|
||||||
let channel = state.channel_service.find_by_id(channel_id).await?;
|
let _channel = state.channel_service.find_by_id(channel_id).await?;
|
||||||
require_owner(&channel, user.id)?;
|
|
||||||
|
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
let schedule = state
|
let schedule = state
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ pub trait UserRepository: Send + Sync {
|
|||||||
pub trait ChannelRepository: Send + Sync {
|
pub trait ChannelRepository: Send + Sync {
|
||||||
async fn find_by_id(&self, id: ChannelId) -> DomainResult<Option<Channel>>;
|
async fn find_by_id(&self, id: ChannelId) -> DomainResult<Option<Channel>>;
|
||||||
async fn find_by_owner(&self, owner_id: UserId) -> DomainResult<Vec<Channel>>;
|
async fn find_by_owner(&self, owner_id: UserId) -> DomainResult<Vec<Channel>>;
|
||||||
|
async fn find_all(&self) -> DomainResult<Vec<Channel>>;
|
||||||
/// Insert or update a channel.
|
/// Insert or update a channel.
|
||||||
async fn save(&self, channel: &Channel) -> DomainResult<()>;
|
async fn save(&self, channel: &Channel) -> DomainResult<()>;
|
||||||
async fn delete(&self, id: ChannelId) -> DomainResult<()>;
|
async fn delete(&self, id: ChannelId) -> DomainResult<()>;
|
||||||
|
|||||||
@@ -112,6 +112,10 @@ impl ChannelService {
|
|||||||
.ok_or(DomainError::ChannelNotFound(id))
|
.ok_or(DomainError::ChannelNotFound(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn find_all(&self) -> DomainResult<Vec<crate::entities::Channel>> {
|
||||||
|
self.channel_repo.find_all().await
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn find_by_owner(
|
pub async fn find_by_owner(
|
||||||
&self,
|
&self,
|
||||||
owner_id: crate::value_objects::UserId,
|
owner_id: crate::value_objects::UserId,
|
||||||
|
|||||||
@@ -113,6 +113,16 @@ impl ChannelRepository for SqliteChannelRepository {
|
|||||||
rows.into_iter().map(Channel::try_from).collect()
|
rows.into_iter().map(Channel::try_from).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn find_all(&self) -> DomainResult<Vec<Channel>> {
|
||||||
|
let sql = format!("SELECT {SELECT_COLS} FROM channels ORDER BY created_at ASC");
|
||||||
|
let rows: Vec<ChannelRow> = sqlx::query_as(&sql)
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| DomainError::RepositoryError(e.to_string()))?;
|
||||||
|
|
||||||
|
rows.into_iter().map(Channel::try_from).collect()
|
||||||
|
}
|
||||||
|
|
||||||
async fn save(&self, channel: &Channel) -> DomainResult<()> {
|
async fn save(&self, channel: &Channel) -> DomainResult<()> {
|
||||||
let schedule_config = serde_json::to_string(&channel.schedule_config).map_err(|e| {
|
let schedule_config = serde_json::to_string(&channel.schedule_config).map_err(|e| {
|
||||||
DomainError::RepositoryError(format!("Failed to serialize schedule_config: {}", e))
|
DomainError::RepositoryError(format!("Failed to serialize schedule_config: {}", e))
|
||||||
@@ -205,6 +215,16 @@ impl ChannelRepository for PostgresChannelRepository {
|
|||||||
rows.into_iter().map(Channel::try_from).collect()
|
rows.into_iter().map(Channel::try_from).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn find_all(&self) -> DomainResult<Vec<Channel>> {
|
||||||
|
let sql = format!("SELECT {SELECT_COLS} FROM channels ORDER BY created_at ASC");
|
||||||
|
let rows: Vec<ChannelRow> = sqlx::query_as(&sql)
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| DomainError::RepositoryError(e.to_string()))?;
|
||||||
|
|
||||||
|
rows.into_iter().map(Channel::try_from).collect()
|
||||||
|
}
|
||||||
|
|
||||||
async fn save(&self, channel: &Channel) -> DomainResult<()> {
|
async fn save(&self, channel: &Channel) -> DomainResult<()> {
|
||||||
let schedule_config = serde_json::to_string(&channel.schedule_config).map_err(|e| {
|
let schedule_config = serde_json::to_string(&channel.schedule_config).map_err(|e| {
|
||||||
DomainError::RepositoryError(format!("Failed to serialize schedule_config: {}", e))
|
DomainError::RepositoryError(format!("Failed to serialize schedule_config: {}", e))
|
||||||
|
|||||||
@@ -27,14 +27,12 @@ export async function GET(
|
|||||||
const { channelId } = await params;
|
const { channelId } = await params;
|
||||||
const token = request.nextUrl.searchParams.get("token");
|
const token = request.nextUrl.searchParams.get("token");
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
return new Response(null, { status: 401 });
|
|
||||||
}
|
|
||||||
|
|
||||||
let res: Response;
|
let res: Response;
|
||||||
try {
|
try {
|
||||||
|
const headers: Record<string, string> = {};
|
||||||
|
if (token) headers["Authorization"] = `Bearer ${token}`;
|
||||||
res = await fetch(`${API_URL}/channels/${channelId}/stream`, {
|
res = await fetch(`${API_URL}/channels/${channelId}/stream`, {
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
headers,
|
||||||
redirect: "manual",
|
redirect: "manual",
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ export function useChannels() {
|
|||||||
const { token } = useAuthContext();
|
const { token } = useAuthContext();
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["channels"],
|
queryKey: ["channels"],
|
||||||
queryFn: () => api.channels.list(token!),
|
// Public endpoint — no token needed for TV viewing
|
||||||
enabled: !!token,
|
queryFn: () => api.channels.list(token ?? ""),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,8 +85,8 @@ export function useCurrentBroadcast(channelId: string) {
|
|||||||
const { token } = useAuthContext();
|
const { token } = useAuthContext();
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["broadcast", channelId],
|
queryKey: ["broadcast", channelId],
|
||||||
queryFn: () => api.schedule.getCurrentBroadcast(channelId, token!),
|
queryFn: () => api.schedule.getCurrentBroadcast(channelId, token ?? ""),
|
||||||
enabled: !!token && !!channelId,
|
enabled: !!channelId,
|
||||||
refetchInterval: 30_000,
|
refetchInterval: 30_000,
|
||||||
retry: false,
|
retry: false,
|
||||||
});
|
});
|
||||||
@@ -96,7 +96,7 @@ export function useEpg(channelId: string, from?: string, until?: string) {
|
|||||||
const { token } = useAuthContext();
|
const { token } = useAuthContext();
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["epg", channelId, from, until],
|
queryKey: ["epg", channelId, from, until],
|
||||||
queryFn: () => api.schedule.getEpg(channelId, token!, from, until),
|
queryFn: () => api.schedule.getEpg(channelId, token ?? "", from, until),
|
||||||
enabled: !!token && !!channelId,
|
enabled: !!channelId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,7 +106,8 @@ export function useStreamUrl(
|
|||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["stream-url", channelId, slotId],
|
queryKey: ["stream-url", channelId, slotId],
|
||||||
queryFn: async (): Promise<string | null> => {
|
queryFn: async (): Promise<string | null> => {
|
||||||
const params = new URLSearchParams({ token: token! });
|
const params = new URLSearchParams();
|
||||||
|
if (token) params.set("token", token);
|
||||||
const res = await fetch(`/api/stream/${channelId}?${params}`, {
|
const res = await fetch(`/api/stream/${channelId}?${params}`, {
|
||||||
cache: "no-store",
|
cache: "no-store",
|
||||||
});
|
});
|
||||||
@@ -115,7 +116,7 @@ export function useStreamUrl(
|
|||||||
const { url } = await res.json();
|
const { url } = await res.json();
|
||||||
return url as string;
|
return url as string;
|
||||||
},
|
},
|
||||||
enabled: !!channelId && !!token && !!slotId,
|
enabled: !!channelId && !!slotId,
|
||||||
staleTime: Infinity,
|
staleTime: Infinity,
|
||||||
retry: false,
|
retry: false,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user