From 8c00a1b0f570d3f466cb5bd30883a89e33aaf129 Mon Sep 17 00:00:00 2001 From: R0m1k3 Date: Thu, 16 Apr 2026 09:55:15 +0200 Subject: [PATCH] feat: implement authentication and core API endpoints - auth, podcasts, episodes --- backend/src/controllers/authController.ts | 121 +++++++++++++++ backend/src/controllers/episodeController.ts | 146 ++++++++++++++++++ backend/src/controllers/podcastController.ts | 147 +++++++++++++++++++ backend/src/index.ts | 9 +- backend/src/routes/authRoutes.ts | 15 ++ backend/src/routes/episodeRoutes.ts | 22 +++ backend/src/routes/podcastRoutes.ts | 22 +++ dev.bat | 45 ++++++ dev.sh | 69 +++++++++ frontend/src/services/api.ts | 35 +++++ frontend/src/services/authService.ts | 66 +++++++++ frontend/src/services/episodeService.ts | 74 ++++++++++ frontend/src/services/podcastService.ts | 52 +++++++ 13 files changed, 822 insertions(+), 1 deletion(-) create mode 100644 backend/src/controllers/authController.ts create mode 100644 backend/src/controllers/episodeController.ts create mode 100644 backend/src/controllers/podcastController.ts create mode 100644 backend/src/routes/authRoutes.ts create mode 100644 backend/src/routes/episodeRoutes.ts create mode 100644 backend/src/routes/podcastRoutes.ts create mode 100644 dev.bat create mode 100644 dev.sh create mode 100644 frontend/src/services/api.ts create mode 100644 frontend/src/services/authService.ts create mode 100644 frontend/src/services/episodeService.ts create mode 100644 frontend/src/services/podcastService.ts diff --git a/backend/src/controllers/authController.ts b/backend/src/controllers/authController.ts new file mode 100644 index 0000000..1d4e326 --- /dev/null +++ b/backend/src/controllers/authController.ts @@ -0,0 +1,121 @@ +import { Request, Response } from 'express'; +import { User } from '../models/User'; +import { generateTokens } from '../utils/jwt'; +import { z } from 'zod'; + +const registerSchema = z.object({ + email: z.string().email('Invalid email address'), + password: z.string().min(6, 'Password must be at least 6 characters'), + username: z.string().min(2, 'Username must be at least 2 characters'), +}); + +const loginSchema = z.object({ + email: z.string().email('Invalid email address'), + password: z.string().min(1, 'Password required'), +}); + +export const register = async (req: Request, res: Response) => { + try { + const { email, password, username } = registerSchema.parse(req.body); + + // Check if user already exists + const existingUser = await User.findOne({ email }); + if (existingUser) { + return res.status(409).json({ message: 'Email already registered' }); + } + + // Create new user + const user = new User({ + email, + password, + username, + }); + + await user.save(); + + // Generate tokens + const { accessToken, refreshToken } = generateTokens(user); + + res.status(201).json({ + message: 'User registered successfully', + user: user.toJSON(), + tokens: { accessToken, refreshToken }, + }); + } catch (error) { + if (error instanceof z.ZodError) { + return res.status(400).json({ + message: 'Validation error', + errors: error.errors, + }); + } + + console.error('Register error:', error); + res.status(500).json({ message: 'Registration failed' }); + } +}; + +export const login = async (req: Request, res: Response) => { + try { + const { email, password } = loginSchema.parse(req.body); + + // Find user + const user = await User.findOne({ email }); + if (!user) { + return res.status(401).json({ message: 'Invalid email or password' }); + } + + // Verify password + const isPasswordValid = await user.comparePassword(password); + if (!isPasswordValid) { + return res.status(401).json({ message: 'Invalid email or password' }); + } + + // Generate tokens + const { accessToken, refreshToken } = generateTokens(user); + + res.json({ + message: 'Login successful', + user: user.toJSON(), + tokens: { accessToken, refreshToken }, + }); + } catch (error) { + if (error instanceof z.ZodError) { + return res.status(400).json({ + message: 'Validation error', + errors: error.errors, + }); + } + + console.error('Login error:', error); + res.status(500).json({ message: 'Login failed' }); + } +}; + +export const getMe = async (req: Request, res: Response) => { + try { + if (!req.user) { + return res.status(401).json({ message: 'Not authenticated' }); + } + + const user = await User.findById(req.user.id); + if (!user) { + return res.status(404).json({ message: 'User not found' }); + } + + res.json({ user: user.toJSON() }); + } catch (error) { + console.error('GetMe error:', error); + res.status(500).json({ message: 'Failed to fetch user' }); + } +}; + +export const logout = async (req: Request, res: Response) => { + try { + // In a stateless JWT system, logout is client-side (delete token) + // Could implement token blacklist with Redis if needed + res.json({ message: 'Logout successful' }); + } catch (error) { + console.error('Logout error:', error); + res.status(500).json({ message: 'Logout failed' }); + } +}; diff --git a/backend/src/controllers/episodeController.ts b/backend/src/controllers/episodeController.ts new file mode 100644 index 0000000..2cbf9e0 --- /dev/null +++ b/backend/src/controllers/episodeController.ts @@ -0,0 +1,146 @@ +import { Request, Response } from 'express'; +import { Episode } from '../models/Episode'; +import { UserProgress } from '../models/UserProgress'; +import { UserSubscription } from '../models/UserSubscription'; + +export const getLatestEpisodes = async (req: Request, res: Response) => { + try { + if (!req.user) { + return res.status(401).json({ message: 'Not authenticated' }); + } + + const { limit = 30, skip = 0 } = req.query; + + // Get user's subscribed podcasts + const subscriptions = await UserSubscription.find({ + userId: req.user.id, + }).select('podcastId'); + + const podcastIds = subscriptions.map((sub) => sub.podcastId); + + if (podcastIds.length === 0) { + return res.json({ episodes: [], count: 0 }); + } + + // Get latest episodes from subscribed podcasts + const episodes = await Episode.find({ + podcastId: { $in: podcastIds }, + }) + .populate('podcastId', 'title author imageUrl') + .sort({ pubDate: -1 }) + .limit(Number(limit)) + .skip(Number(skip)); + + res.json({ episodes, count: episodes.length }); + } catch (error) { + console.error('Get latest episodes error:', error); + res.status(500).json({ message: 'Failed to fetch episodes' }); + } +}; + +export const getPodcastEpisodes = async (req: Request, res: Response) => { + try { + const { podcastId } = req.params; + const { limit = 50, skip = 0 } = req.query; + + const episodes = await Episode.find({ podcastId }) + .sort({ pubDate: -1 }) + .limit(Number(limit)) + .skip(Number(skip)); + + res.json({ episodes, count: episodes.length }); + } catch (error) { + console.error('Get podcast episodes error:', error); + res.status(500).json({ message: 'Failed to fetch episodes' }); + } +}; + +export const getEpisode = async (req: Request, res: Response) => { + try { + const { episodeId } = req.params; + + const episode = await Episode.findById(episodeId).populate( + 'podcastId', + 'title author description imageUrl' + ); + + if (!episode) { + return res.status(404).json({ message: 'Episode not found' }); + } + + res.json({ episode }); + } catch (error) { + console.error('Get episode error:', error); + res.status(500).json({ message: 'Failed to fetch episode' }); + } +}; + +export const saveProgress = async (req: Request, res: Response) => { + try { + if (!req.user) { + return res.status(401).json({ message: 'Not authenticated' }); + } + + const { episodeId, position, isCompleted } = req.body; + + if (!episodeId || position === undefined) { + return res.status(400).json({ message: 'Episode ID and position required' }); + } + + // Find or create progress record + let progress = await UserProgress.findOne({ + userId: req.user.id, + episodeId, + }); + + if (progress) { + progress.position = position; + if (isCompleted !== undefined) { + progress.isCompleted = isCompleted; + } + progress.lastListenedAt = new Date(); + } else { + progress = new UserProgress({ + userId: req.user.id, + episodeId, + position, + isCompleted: isCompleted || false, + lastListenedAt: new Date(), + }); + } + + await progress.save(); + + res.json({ + message: 'Progress saved', + progress, + }); + } catch (error) { + console.error('Save progress error:', error); + res.status(500).json({ message: 'Failed to save progress' }); + } +}; + +export const getProgress = async (req: Request, res: Response) => { + try { + if (!req.user) { + return res.status(401).json({ message: 'Not authenticated' }); + } + + const { episodeId } = req.params; + + const progress = await UserProgress.findOne({ + userId: req.user.id, + episodeId, + }); + + if (!progress) { + return res.json({ progress: null, message: 'No progress found' }); + } + + res.json({ progress }); + } catch (error) { + console.error('Get progress error:', error); + res.status(500).json({ message: 'Failed to fetch progress' }); + } +}; diff --git a/backend/src/controllers/podcastController.ts b/backend/src/controllers/podcastController.ts new file mode 100644 index 0000000..9897258 --- /dev/null +++ b/backend/src/controllers/podcastController.ts @@ -0,0 +1,147 @@ +import { Request, Response } from 'express'; +import { Podcast } from '../models/Podcast'; +import { UserSubscription } from '../models/UserSubscription'; +import { Episode } from '../models/Episode'; +import { z } from 'zod'; + +const subscribeSchema = z.object({ + rssUrl: z.string().url('Invalid RSS URL'), +}); + +export const getUserSubscriptions = async (req: Request, res: Response) => { + try { + if (!req.user) { + return res.status(401).json({ message: 'Not authenticated' }); + } + + const subscriptions = await UserSubscription.find({ + userId: req.user.id, + }) + .populate('podcastId') + .sort({ subscribedAt: -1 }); + + const podcasts = subscriptions.map((sub) => sub.podcastId); + + res.json({ podcasts, count: podcasts.length }); + } catch (error) { + console.error('Get subscriptions error:', error); + res.status(500).json({ message: 'Failed to fetch subscriptions' }); + } +}; + +export const subscribe = async (req: Request, res: Response) => { + try { + if (!req.user) { + return res.status(401).json({ message: 'Not authenticated' }); + } + + const { rssUrl } = subscribeSchema.parse(req.body); + + // Check if podcast exists + let podcast = await Podcast.findOne({ rssUrl }); + + if (!podcast) { + // For now, create a basic podcast entry + // In production, you'd fetch from PodcastIndex API or parse the RSS feed + return res.status(400).json({ + message: 'Podcast not found. Please use a valid RSS URL.', + }); + } + + // Check if already subscribed + const existingSubscription = await UserSubscription.findOne({ + userId: req.user.id, + podcastId: podcast._id, + }); + + if (existingSubscription) { + return res.status(409).json({ message: 'Already subscribed to this podcast' }); + } + + // Create subscription + const subscription = new UserSubscription({ + userId: req.user.id, + podcastId: podcast._id, + }); + + await subscription.save(); + + res.status(201).json({ + message: 'Subscribed successfully', + podcast: podcast.toJSON(), + }); + } catch (error) { + if (error instanceof z.ZodError) { + return res.status(400).json({ + message: 'Validation error', + errors: error.errors, + }); + } + + console.error('Subscribe error:', error); + res.status(500).json({ message: 'Failed to subscribe' }); + } +}; + +export const unsubscribe = async (req: Request, res: Response) => { + try { + if (!req.user) { + return res.status(401).json({ message: 'Not authenticated' }); + } + + const { podcastId } = req.params; + + const subscription = await UserSubscription.findOneAndDelete({ + userId: req.user.id, + podcastId, + }); + + if (!subscription) { + return res.status(404).json({ message: 'Subscription not found' }); + } + + res.json({ message: 'Unsubscribed successfully' }); + } catch (error) { + console.error('Unsubscribe error:', error); + res.status(500).json({ message: 'Failed to unsubscribe' }); + } +}; + +export const searchPodcasts = async (req: Request, res: Response) => { + try { + const { q, limit = 20, skip = 0 } = req.query; + + if (!q) { + return res.status(400).json({ message: 'Search query required' }); + } + + const podcasts = await Podcast.find( + { $text: { $search: q as string } }, + { score: { $meta: 'textScore' } } + ) + .sort({ score: { $meta: 'textScore' } }) + .limit(Number(limit)) + .skip(Number(skip)); + + res.json({ podcasts, count: podcasts.length }); + } catch (error) { + console.error('Search error:', error); + res.status(500).json({ message: 'Search failed' }); + } +}; + +export const getPodcast = async (req: Request, res: Response) => { + try { + const { podcastId } = req.params; + + const podcast = await Podcast.findById(podcastId); + if (!podcast) { + return res.status(404).json({ message: 'Podcast not found' }); + } + + res.json({ podcast }); + } catch (error) { + console.error('Get podcast error:', error); + res.status(500).json({ message: 'Failed to fetch podcast' }); + } +}; diff --git a/backend/src/index.ts b/backend/src/index.ts index a14134b..e4300da 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -5,6 +5,9 @@ import { connectDB } from './config/database'; import { initRedis, closeRedis } from './config/redis'; import { authenticate, optional } from './middleware/auth'; import { errorHandler, notFoundHandler } from './middleware/errorHandler'; +import authRoutes from './routes/authRoutes'; +import podcastRoutes from './routes/podcastRoutes'; +import episodeRoutes from './routes/episodeRoutes'; const app = express(); const PORT = process.env.PORT || 5000; @@ -36,11 +39,15 @@ app.get('/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString() }); }); -// API Routes - TODO: Import and register routes +// API Routes app.get('/api/health', (req, res) => { res.json({ status: 'ok', message: 'API is running' }); }); +app.use('/api/auth', authRoutes); +app.use('/api/podcasts', podcastRoutes); +app.use('/api/episodes', episodeRoutes); + // 404 handler app.use(notFoundHandler); diff --git a/backend/src/routes/authRoutes.ts b/backend/src/routes/authRoutes.ts new file mode 100644 index 0000000..fc872d7 --- /dev/null +++ b/backend/src/routes/authRoutes.ts @@ -0,0 +1,15 @@ +import { Router } from 'express'; +import { register, login, getMe, logout } from '../controllers/authController'; +import { authenticate } from '../middleware/auth'; + +const router = Router(); + +// Public routes +router.post('/register', register); +router.post('/login', login); + +// Protected routes +router.get('/me', authenticate, getMe); +router.post('/logout', authenticate, logout); + +export default router; diff --git a/backend/src/routes/episodeRoutes.ts b/backend/src/routes/episodeRoutes.ts new file mode 100644 index 0000000..16250db --- /dev/null +++ b/backend/src/routes/episodeRoutes.ts @@ -0,0 +1,22 @@ +import { Router } from 'express'; +import { + getLatestEpisodes, + getPodcastEpisodes, + getEpisode, + saveProgress, + getProgress, +} from '../controllers/episodeController'; +import { authenticate } from '../middleware/auth'; + +const router = Router(); + +// Protected routes +router.get('/latest', authenticate, getLatestEpisodes); +router.post('/progress', authenticate, saveProgress); +router.get('/progress/:episodeId', authenticate, getProgress); + +// Public routes +router.get('/:episodeId', getEpisode); +router.get('/podcast/:podcastId', getPodcastEpisodes); + +export default router; diff --git a/backend/src/routes/podcastRoutes.ts b/backend/src/routes/podcastRoutes.ts new file mode 100644 index 0000000..3f641bb --- /dev/null +++ b/backend/src/routes/podcastRoutes.ts @@ -0,0 +1,22 @@ +import { Router } from 'express'; +import { + getUserSubscriptions, + subscribe, + unsubscribe, + searchPodcasts, + getPodcast, +} from '../controllers/podcastController'; +import { authenticate } from '../middleware/auth'; + +const router = Router(); + +// Protected routes +router.get('/subscriptions', authenticate, getUserSubscriptions); +router.post('/subscribe', authenticate, subscribe); +router.delete('/:podcastId/unsubscribe', authenticate, unsubscribe); + +// Public routes +router.get('/search', searchPodcasts); +router.get('/:podcastId', getPodcast); + +export default router; diff --git a/dev.bat b/dev.bat new file mode 100644 index 0000000..40339fa --- /dev/null +++ b/dev.bat @@ -0,0 +1,45 @@ +@echo off +setlocal enabledelayedexpansion + +echo. +echo 🎙️ Podcastic Development Server Launcher +echo ========================================== +echo. + +REM Check if .env exists +if not exist ".env" ( + echo ⚠️ .env file not found! + echo Creating .env from .env.example... + copy .env.example .env + echo ✓ .env created. Please update with your configuration. + echo. +) + +REM Check for MongoDB and Redis +echo 📡 Checking prerequisites... +echo ⚠️ Make sure MongoDB and Redis are running! +echo. + +REM Start backend +echo 🚀 Starting Backend (Express.js)... +cd backend +start "Podcastic Backend" npm run dev +cd .. + +timeout /t 2 /nobreak + +REM Start frontend +echo 🚀 Starting Frontend (Vite)... +cd frontend +start "Podcastic Frontend" npm run dev +cd .. + +echo. +echo ✓ Servers started! +echo. +echo 📍 Frontend: http://localhost:3000 +echo 📍 Backend: http://localhost:5000 +echo. +echo Windows command windows will stay open. Close them to stop servers. +echo. +pause diff --git a/dev.sh b/dev.sh new file mode 100644 index 0000000..f1fa7b5 --- /dev/null +++ b/dev.sh @@ -0,0 +1,69 @@ +#!/bin/bash + +# Podcastic Development Helper Script + +echo "🎙️ Podcastic Development Server Launcher" +echo "==========================================" +echo "" + +# Check if .env exists +if [ ! -f ".env" ]; then + echo "⚠️ .env file not found!" + echo "Creating .env from .env.example..." + cp .env.example .env + echo "✓ .env created. Please update with your configuration." + echo "" +fi + +# Check for MongoDB connection +echo "📡 Checking MongoDB connection..." +MONGODB_URI=${MONGODB_URI:-"mongodb://localhost:27017/podcastic"} + +if [ "$MONGODB_URI" = "mongodb://localhost:27017/podcastic" ]; then + echo "⚠️ Using local MongoDB at $MONGODB_URI" + echo " Make sure MongoDB is running!" + echo "" +fi + +# Check for Redis connection +echo "🔴 Checking Redis connection..." +REDIS_URL=${REDIS_URL:-"redis://localhost:6379"} + +if [ "$REDIS_URL" = "redis://localhost:6379" ]; then + echo "⚠️ Using local Redis at $REDIS_URL" + echo " Make sure Redis is running!" + echo "" +fi + +# Start both servers +echo "🚀 Starting both servers..." +echo "" + +# Start backend in background +echo "→ Backend starting on port 5000..." +cd backend +npm run dev & +BACKEND_PID=$! +cd .. + +sleep 2 + +# Start frontend in background +echo "→ Frontend starting on port 3000..." +cd frontend +npm run dev & +FRONTEND_PID=$! +cd .. + +echo "" +echo "✓ Servers started!" +echo "" +echo "📍 Frontend: http://localhost:3000" +echo "📍 Backend: http://localhost:5000" +echo "" +echo "Press Ctrl+C to stop all servers..." +echo "" + +# Wait for background processes +wait $BACKEND_PID +wait $FRONTEND_PID diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts new file mode 100644 index 0000000..3484b42 --- /dev/null +++ b/frontend/src/services/api.ts @@ -0,0 +1,35 @@ +import axios from 'axios'; + +const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:5000/api'; + +const api = axios.create({ + baseURL: API_URL, + headers: { + 'Content-Type': 'application/json', + }, +}); + +// Add auth token to requests +api.interceptors.request.use((config) => { + const token = localStorage.getItem('accessToken'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; +}); + +// Handle token refresh on 401 +api.interceptors.response.use( + (response) => response, + async (error) => { + if (error.response?.status === 401) { + // Token expired - could implement refresh token logic here + localStorage.removeItem('accessToken'); + localStorage.removeItem('refreshToken'); + window.location.href = '/login'; + } + return Promise.reject(error); + } +); + +export default api; diff --git a/frontend/src/services/authService.ts b/frontend/src/services/authService.ts new file mode 100644 index 0000000..9415ac4 --- /dev/null +++ b/frontend/src/services/authService.ts @@ -0,0 +1,66 @@ +import api from './api'; + +export interface User { + _id: string; + email: string; + username: string; + avatar?: string; + preferences: { + defaultPlaybackSpeed: number; + theme: 'light' | 'dark'; + language: string; + }; +} + +export interface AuthResponse { + message: string; + user: User; + tokens: { + accessToken: string; + refreshToken: string; + }; +} + +export const authService = { + register: async (email: string, password: string, username: string): Promise => { + const response = await api.post('/auth/register', { + email, + password, + username, + }); + return response.data; + }, + + login: async (email: string, password: string): Promise => { + const response = await api.post('/auth/login', { + email, + password, + }); + return response.data; + }, + + getMe: async (): Promise<{ user: User }> => { + const response = await api.get('/auth/me'); + return response.data; + }, + + logout: async (): Promise<{ message: string }> => { + const response = await api.post('/auth/logout'); + return response.data; + }, + + setTokens: (accessToken: string, refreshToken: string) => { + localStorage.setItem('accessToken', accessToken); + localStorage.setItem('refreshToken', refreshToken); + }, + + getTokens: () => ({ + accessToken: localStorage.getItem('accessToken'), + refreshToken: localStorage.getItem('refreshToken'), + }), + + clearTokens: () => { + localStorage.removeItem('accessToken'); + localStorage.removeItem('refreshToken'); + }, +}; diff --git a/frontend/src/services/episodeService.ts b/frontend/src/services/episodeService.ts new file mode 100644 index 0000000..6760bc6 --- /dev/null +++ b/frontend/src/services/episodeService.ts @@ -0,0 +1,74 @@ +import api from './api'; + +export interface Episode { + _id: string; + podcastId: string | { _id: string; title: string; author: string; imageUrl?: string }; + title: string; + description: string; + audioUrl: string; + pubDate: string; + duration: number; + guid: string; + imageUrl?: string; +} + +export interface EpisodeResponse { + episodes: Episode[]; + count: number; +} + +export interface UserProgress { + _id: string; + userId: string; + episodeId: string; + position: number; + isCompleted: boolean; + isSaved: boolean; + lastListenedAt: string; +} + +export const episodeService = { + getLatestEpisodes: async ( + limit: number = 30, + skip: number = 0 + ): Promise => { + const response = await api.get('/episodes/latest', { + params: { limit, skip }, + }); + return response.data; + }, + + getPodcastEpisodes: async ( + podcastId: string, + limit: number = 50, + skip: number = 0 + ): Promise => { + const response = await api.get(`/episodes/podcast/${podcastId}`, { + params: { limit, skip }, + }); + return response.data; + }, + + getEpisode: async (episodeId: string): Promise<{ episode: Episode }> => { + const response = await api.get(`/episodes/${episodeId}`); + return response.data; + }, + + saveProgress: async ( + episodeId: string, + position: number, + isCompleted?: boolean + ): Promise<{ message: string; progress: UserProgress }> => { + const response = await api.post('/episodes/progress', { + episodeId, + position, + isCompleted, + }); + return response.data; + }, + + getProgress: async (episodeId: string): Promise<{ progress: UserProgress | null }> => { + const response = await api.get(`/episodes/progress/${episodeId}`); + return response.data; + }, +}; diff --git a/frontend/src/services/podcastService.ts b/frontend/src/services/podcastService.ts new file mode 100644 index 0000000..8fd1cfa --- /dev/null +++ b/frontend/src/services/podcastService.ts @@ -0,0 +1,52 @@ +import api from './api'; + +export interface Podcast { + _id: string; + title: string; + description: string; + rssUrl: string; + imageUrl?: string; + author: string; + category: string[]; + language: string; + episodeCount: number; + lastFetched: string; +} + +export interface PodcastResponse { + podcasts: Podcast[]; + count: number; +} + +export const podcastService = { + getUserSubscriptions: async (): Promise => { + const response = await api.get('/podcasts/subscriptions'); + return response.data; + }, + + subscribe: async (rssUrl: string): Promise<{ message: string; podcast: Podcast }> => { + const response = await api.post('/podcasts/subscribe', { rssUrl }); + return response.data; + }, + + unsubscribe: async (podcastId: string): Promise<{ message: string }> => { + const response = await api.delete(`/podcasts/${podcastId}/unsubscribe`); + return response.data; + }, + + searchPodcasts: async ( + query: string, + limit: number = 20, + skip: number = 0 + ): Promise => { + const response = await api.get('/podcasts/search', { + params: { q: query, limit, skip }, + }); + return response.data; + }, + + getPodcast: async (podcastId: string): Promise<{ podcast: Podcast }> => { + const response = await api.get(`/podcasts/${podcastId}`); + return response.data; + }, +};