diff --git a/docker-compose.yml b/docker-compose.yml index 00db07c..692dcf5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -125,6 +125,24 @@ services: timeout: 5s retries: 5 + # Video Catalogue Microservice + video_service: + build: + context: ./services/Glense.VideoCatalogue + dockerfile: Dockerfile + container_name: glense_video_service + environment: + - ASPNETCORE_ENVIRONMENT=Development + - ConnectionStrings__VideoCatalogue=Host=postgres_video;Port=5432;Database=glense_video;Username=glense;Password=glense123 + ports: + - "5002:5002" + depends_on: + postgres_video: + condition: service_healthy + networks: + - glense_network + restart: unless-stopped + # Frontend (existing React app) frontend: build: @@ -155,7 +173,7 @@ services: - ConnectionStrings__DefaultConnection=Host=postgres_chat;Port=5432;Database=glense_chat;Username=glense;Password=glense123 - JwtSettings__Issuer=GlenseAccountService - JwtSettings__Audience=GlenseApp - - JwtSettings__SecretKey=ChangeMeToA32CharSecret + - JwtSettings__SecretKey=YourSuperSecretKeyThatIsAtLeast32CharactersLongForHS256Algorithm ports: - "5004:5000" depends_on: diff --git a/glense.client/src/components/ChannelDetail.jsx b/glense.client/src/components/ChannelDetail.jsx index 17d83a9..79afb96 100644 --- a/glense.client/src/components/ChannelDetail.jsx +++ b/glense.client/src/components/ChannelDetail.jsx @@ -1,14 +1,23 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import { useParams } from "react-router-dom"; import { Box, CardMedia, Typography } from "@mui/material"; import { Videos, ChannelCard } from "."; -import { videos } from "../utils/constants"; +import { getVideos } from "../utils/videoApi"; import "../css/ChannelDetail.css"; function ChannelDetail() { const [channelDetail, setChannelDetail] = useState(null); + const [videos, setVideos] = useState([]); + + useEffect(() => { + let mounted = true; + getVideos() + .then(data => { if (mounted && Array.isArray(data)) setVideos(data); }) + .catch(() => {}); + return () => { mounted = false; }; + }, []); const channelBanner = channelDetail?.brandingSettings?.image?.bannerExternalUrl; diff --git a/glense.client/src/components/Chat/ChatSidebar.jsx b/glense.client/src/components/Chat/ChatSidebar.jsx index e251c0f..ba397fa 100644 --- a/glense.client/src/components/Chat/ChatSidebar.jsx +++ b/glense.client/src/components/Chat/ChatSidebar.jsx @@ -1,6 +1,7 @@ import React, { useState } from "react"; import { Box, List, ListItem, ListItemAvatar, Avatar, ListItemText, TextField, Button, ListItemButton } from "@mui/material"; import "../../css/Chat/ChatSideBar.css"; +import { stringToColor } from "../../utils/constants"; const ChatSidebar = ({ chats, onSelectChat, onCreate }) => { const [topic, setTopic] = useState(""); @@ -26,7 +27,9 @@ const ChatSidebar = ({ chats, onSelectChat, onCreate }) => { onSelectChat(chat)}> - + + {(chat.topic || chat.Topic || chat.name || '?').charAt(0).toUpperCase()} + diff --git a/glense.client/src/components/Chat/ChatWindow.jsx b/glense.client/src/components/Chat/ChatWindow.jsx index 91d3df1..0fcc074 100644 --- a/glense.client/src/components/Chat/ChatWindow.jsx +++ b/glense.client/src/components/Chat/ChatWindow.jsx @@ -1,9 +1,9 @@ import React from "react"; import MessageBubble from "./MessageBubble"; -import { Box, TextField, IconButton } from "@mui/material"; +import { Box, TextField, IconButton, Avatar } from "@mui/material"; import SendIcon from "@mui/icons-material/Send"; import "../../css/Chat/ChatWindow.css"; - +import { stringToColor } from "../../utils/constants"; import { useState } from "react"; const ChatWindow = ({ chat, onSend }) => { @@ -27,11 +27,9 @@ const ChatWindow = ({ chat, onSend }) => {
{/* Chat Header */}
- {chat.name + + {(chat.topic || chat.Topic || chat.name || '?').charAt(0).toUpperCase()} + {chat.name || chat.Topic || chat.topic || chat.title || 'Chat'}
diff --git a/glense.client/src/components/Chat/MessageBubble.jsx b/glense.client/src/components/Chat/MessageBubble.jsx index 62dd6b4..7f002ed 100644 --- a/glense.client/src/components/Chat/MessageBubble.jsx +++ b/glense.client/src/components/Chat/MessageBubble.jsx @@ -1,12 +1,23 @@ import React from "react"; -import { Box } from "@mui/material"; +import { Box, Avatar } from "@mui/material"; import "../../css/Chat/MessageBubble.css"; +import { stringToColor } from "../../utils/constants"; const MessageBubble = ({ message }) => { const isMe = message.isMe; + const senderName = message.sender || ''; + return ( + {!isMe && ( + + {senderName.charAt(0).toUpperCase() || '?'} + + )} + {!isMe && senderName && ( +
{senderName}
+ )}
{message.message}
{message.time}
diff --git a/glense.client/src/components/Feed.jsx b/glense.client/src/components/Feed.jsx index 748d297..7d87cd4 100644 --- a/glense.client/src/components/Feed.jsx +++ b/glense.client/src/components/Feed.jsx @@ -1,6 +1,6 @@ import { useState, useEffect } from "react"; import { Box, Stack } from "@mui/material"; -import { Sidebar, Videos, ChatList } from "./"; +import { Sidebar, Videos } from "./"; import "../css/Feed.css"; import { getVideos } from "../utils/videoApi"; @@ -26,7 +26,6 @@ function Feed() { selectedCategory={selectedCategory} setSelectedCategory={setSelectedCategory} /> -
diff --git a/glense.client/src/components/Navbar.jsx b/glense.client/src/components/Navbar.jsx index 3ad8ee4..f0c92d4 100644 --- a/glense.client/src/components/Navbar.jsx +++ b/glense.client/src/components/Navbar.jsx @@ -1,6 +1,5 @@ -import { Stack, Typography, Button, IconButton, Menu, MenuItem } from "@mui/material"; +import { Stack, Typography, Button, IconButton, Menu, MenuItem, Avatar } from "@mui/material"; import { Link } from "react-router-dom"; -import AccountCircleIcon from "@mui/icons-material/AccountCircle"; import logo from "../assets/logo_transparent.png"; import SearchBar from "../components/SearchBar"; import { useState } from "react"; @@ -60,8 +59,10 @@ function Navbar() { Donations - - + + + {user?.username?.charAt(0).toUpperCase() || '?'} + setTitle(e.target.value)} fullWidth margin="normal" /> setDescription(e.target.value)} fullWidth multiline rows={4} margin="normal" /> - setUploaderId(e.target.value)} fullWidth margin="normal" /> - diff --git a/glense.client/src/components/VideoComments.jsx b/glense.client/src/components/VideoComments.jsx index 297fbd7..fcedaf9 100644 --- a/glense.client/src/components/VideoComments.jsx +++ b/glense.client/src/components/VideoComments.jsx @@ -1,18 +1,37 @@ -import React from "react"; -import { Stack, CardMedia, Typography } from "@mui/material"; +import { useState, useEffect } from "react"; +import { Stack, Typography, Avatar } from "@mui/material"; import { ThumbUpOutlined } from "@mui/icons-material"; - -import { comments } from "../utils/constants"; +import { getComments } from "../utils/videoApi"; import "../css/VideoComments.css"; +import { stringToColor } from "../utils/constants"; + +function VideoComments({ videoId, id }) { + const resolvedVideoId = videoId || id; + const [comments, setComments] = useState(null); -function VideoComments({ }) { - if (!comments) + useEffect(() => { + if (!resolvedVideoId) return; + let mounted = true; + getComments(resolvedVideoId) + .then(data => { if (mounted) setComments(data); }) + .catch(() => { if (mounted) setComments([]); }); + return () => { mounted = false; }; + }, [resolvedVideoId]); + + if (comments === null) return ( Loading comments.. ); + if (comments.length === 0) + return ( + + No comments yet. Be the first! + + ); + return ( {comments.map((comment) => ( @@ -21,19 +40,26 @@ function VideoComments({ }) { className="comment-item" key={comment.id} > - - + + {comment.username?.charAt(0).toUpperCase()} + + - {comment.name} + {comment.username} - {comment.commentText} + {comment.content} - + {Number(comment.likeCount).toLocaleString()} diff --git a/glense.client/src/components/VideoStream.jsx b/glense.client/src/components/VideoStream.jsx index 3786293..94a2548 100644 --- a/glense.client/src/components/VideoStream.jsx +++ b/glense.client/src/components/VideoStream.jsx @@ -2,7 +2,7 @@ import { useState, useEffect } from "react"; import { useParams } from "react-router-dom"; import { Link } from "react-router-dom"; import ReactPlayer from "react-player"; -import { Typography, Box, Stack, Button, FormControl, Select, MenuItem } from "@mui/material"; +import { Typography, Box, Stack, Button, FormControl, Select, MenuItem, Avatar } from "@mui/material"; import PlaylistAddIcon from '@mui/icons-material/PlaylistAdd'; import { CheckCircle, @@ -11,8 +11,12 @@ import { } from "@mui/icons-material"; import { Videos, VideoComments } from "."; -import { videoInfo as demoVideoInfo } from "../utils/constants"; +const demoVideoInfo = { + title: 'Loading...', channelTitle: '', viewCount: 0, + likeCount: 0, dislikeCount: 0, publishedAt: '', tags: [], description: '' +}; import { getVideo, getVideos, getPlaylists, addVideoToPlaylist } from "../utils/videoApi"; +import { profileService } from "../services/profileService"; import { useAuth } from "../context/AuthContext"; import "../css/VideoStream.css"; @@ -22,6 +26,7 @@ function VideoStream() { const [showMoreDesc, setShowMoreDesc] = useState(false); const { id } = useParams(); const [video, setVideo] = useState(null); + const [uploader, setUploader] = useState(null); const [related, setRelated] = useState([]); const [playlists, setPlaylists] = useState([]); const [addingTo, setAddingTo] = useState(""); @@ -30,7 +35,15 @@ function VideoStream() { useEffect(() => { let mounted = true; if (!id) return; - getVideo(id).then(d => { if (mounted) setVideo(d); }).catch(() => {}); + getVideo(id).then(d => { + if (!mounted) return; + setVideo(d); + if (d?.uploaderId && d.uploaderId !== '00000000-0000-0000-0000-000000000000') { + profileService.getUserById(d.uploaderId) + .then(p => { if (mounted) setUploader(p); }) + .catch(() => {}); + } + }).catch(() => {}); getVideos().then(list => { if (mounted && Array.isArray(list)) setRelated(list.filter(v => String(v.id) !== String(id)).slice(0, 12)); }).catch(() => {}); return () => { mounted = false; }; }, [id]); @@ -56,9 +69,12 @@ function VideoStream() { {video?.title || demoVideoInfo.title} - - - {video?.channelTitle || demoVideoInfo.channelTitle} + + + {(uploader?.username || video?.title || '?').charAt(0).toUpperCase()} + + + {uploader?.username || 'Glense'} @@ -162,25 +178,6 @@ function VideoStream() { Comments - {/* Add to playlist */} - - Add to playlist - - - diff --git a/glense.client/src/css/Chat/MessageBubble.css b/glense.client/src/css/Chat/MessageBubble.css index 1d74f70..0c5f0d8 100644 --- a/glense.client/src/css/Chat/MessageBubble.css +++ b/glense.client/src/css/Chat/MessageBubble.css @@ -9,6 +9,14 @@ .message-bubble.other { justify-content: flex-start !important; + align-items: flex-start !important; +} + +.message-bubble-sender { + font-size: 12px !important; + font-weight: bold !important; + color: #90caf9 !important; + margin-bottom: 2px !important; } .message-bubble-content { diff --git a/glense.client/src/css/Navbar.css b/glense.client/src/css/Navbar.css index fbfbee0..f8e0c24 100644 --- a/glense.client/src/css/Navbar.css +++ b/glense.client/src/css/Navbar.css @@ -68,6 +68,7 @@ .navbar-option-stack { flex-direction: row !important; + align-items: center !important; gap: 20px !important; } diff --git a/glense.client/src/css/Upload.css b/glense.client/src/css/Upload.css index 874c101..0e9a479 100644 --- a/glense.client/src/css/Upload.css +++ b/glense.client/src/css/Upload.css @@ -1,2 +1,69 @@ -.upload-page { padding: 24px; display:flex; justify-content:center; } -.upload-form { max-width:720px; width:100%; display:flex; flex-direction:column; gap:12px; } +.upload-page { + padding: 24px; + display: flex; + justify-content: center; + min-height: calc(100vh - var(--navbar-height)); +} + +.upload-form { + max-width: 720px; + width: 100%; + display: flex; + flex-direction: column; + gap: 16px; +} + +.upload-form .MuiTypography-h5 { + color: var(--color-text-white); + font-weight: bold; + margin-bottom: 8px; +} + +.upload-form .MuiTextField-root .MuiOutlinedInput-root { + color: var(--color-text-white); + background-color: rgba(255, 255, 255, 0.05); +} + +.upload-form .MuiTextField-root .MuiOutlinedInput-root fieldset { + border-color: rgba(255, 255, 255, 0.2); +} + +.upload-form .MuiTextField-root .MuiOutlinedInput-root:hover fieldset { + border-color: rgba(255, 255, 255, 0.4); +} + +.upload-form .MuiTextField-root .MuiOutlinedInput-root.Mui-focused fieldset { + border-color: var(--color-primary); +} + +.upload-form .MuiInputLabel-root { + color: var(--color-text-secondary); +} + +.upload-form .MuiInputLabel-root.Mui-focused { + color: var(--color-primary); +} + +.upload-form input[type="file"] { + color: var(--color-text-secondary); + padding: 12px; + border: 1px dashed rgba(255, 255, 255, 0.2); + border-radius: 8px; + cursor: pointer; +} + +.upload-form input[type="file"]:hover { + border-color: rgba(255, 255, 255, 0.4); +} + +.upload-form .MuiButton-contained { + background-color: var(--color-primary); + text-transform: none; + font-weight: bold; + padding: 10px 24px; + align-self: flex-start; +} + +.upload-form .MuiTypography-root:last-child { + color: var(--color-text-secondary); +} diff --git a/glense.client/src/utils/constants.jsx b/glense.client/src/utils/constants.jsx index 02b42a7..037666e 100644 --- a/glense.client/src/utils/constants.jsx +++ b/glense.client/src/utils/constants.jsx @@ -11,6 +11,7 @@ import TheaterComedyIcon from "@mui/icons-material/TheaterComedy"; import FitnessCenterIcon from "@mui/icons-material/FitnessCenter"; import DeveloperModeIcon from "@mui/icons-material/DeveloperMode"; +// Sidebar categories (UI-only, not from database) export const categories = [ { name: "New Videos", icon: }, { name: "Music", icon: }, @@ -27,38 +28,7 @@ export const categories = [ { name: "Crypto", icon: }, ]; -export const chats = [ - { - name: "Keki", - profileImage: "http://dergipark.org.tr/assets/app/images/buddy_sample.png", - messages: [ - {sender: "Keki", message: "Hello", time: "12:00", isMe: false}, - {sender: "Branko", message: "Hello", time: "12:05", isMe: true}, - {sender: "Keki", message: "How are you", time: "12:10", isMe: false}, - {sender: "Keki", message: "Ok.", time: "12:10", isMe: false}, - ] - }, - { - name: "Irena", - profileImage: "http://dergipark.org.tr/assets/app/images/buddy_sample.png", - messages: [ - {sender: "Irena", message: "Smorena sam nesto danas", time: "12:00", isMe: false}, - {sender: "Branko", message: "Briga mee", time: "12:05", isMe: true}, - {sender: "Irena", message: ":(", time: "12:10", isMe: false}, - {sender: "Irena", message: "Sta radis", time: "13:10", isMe: false}, - ] - } -]; - -export const videos = []; -for (let i = 0; i < 100; i++) { - videos.push({ - id: { videoId: "haDjmBT9tu4" }, - title: "An Honest Review of Apple Intelligence\... So Far", - url: "https://www.youtube.com/watch?v=haDjmBT9tu4" - }); -} - +// Fallback values for when API data hasn't loaded yet export const demoThumbnailUrl = "https://i.ibb.co/G2L2Gwp/API-Course.png"; export const demoChannelUrl = "/channel/UCmXmlB4-HJytD7wek0Uo97A"; export const demoVideoUrl = "/video/GDa8kZLNhJ4"; @@ -68,7 +38,6 @@ export const demoVideoTitle = export const demoProfilePicture = "http://dergipark.org.tr/assets/app/images/buddy_sample.png"; - // Comment export const comments = []; for (let i = 0; i < 50; i++) { @@ -95,5 +64,13 @@ export const videoInfo = { dislikeCount: 1234 }; +// Consistent avatar color from a username/string +const avatarColors = ['#e91e63', '#9c27b0', '#673ab7', '#3f51b5', '#2196f3', '#00bcd4', '#009688', '#4caf50', '#ff9800', '#ff5722']; +export function stringToColor(str) { + let h = 0; + for (let i = 0; i < (str || '').length; i++) h = str.charCodeAt(i) + ((h << 5) - h); + return avatarColors[Math.abs(h) % avatarColors.length]; +} + // Video catalogue API -export const VIDEO_CATALOGUE_API = import.meta.env.VITE_VIDEO_CATALOGUE_API || 'http://localhost:5088'; \ No newline at end of file +export const VIDEO_CATALOGUE_API = import.meta.env.VITE_VIDEO_CATALOGUE_API || 'http://localhost:5088'; diff --git a/glense.client/src/utils/videoApi.js b/glense.client/src/utils/videoApi.js index 81aaecf..0db0726 100644 --- a/glense.client/src/utils/videoApi.js +++ b/glense.client/src/utils/videoApi.js @@ -1,4 +1,4 @@ -const BASE = import.meta.env.VITE_VIDEO_CATALOGUE_API || 'http://localhost:5088'; +const BASE = import.meta.env.VITE_VIDEO_CATALOGUE_API || 'http://localhost:5002'; async function handleRes(res) { if (!res.ok) { @@ -100,6 +100,24 @@ export async function getPlaylists(creatorId = 0) { return handleRes(res); } +export async function getComments(videoId) { + const res = await fetch(`${BASE}/api/videos/${videoId}/comments`); + return handleRes(res); +} + +export async function postComment(videoId, content, userId = '', username = 'Anonymous') { + const res = await fetch(`${BASE}/api/videos/${videoId}/comments`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(userId ? { 'X-User-Id': String(userId) } : {}), + ...(username ? { 'X-Username': username } : {}), + }, + body: JSON.stringify({ content }), + }); + return handleRes(res); +} + export async function getSubscriptions(userId = 0) { const headers = userId ? { 'X-User-Id': String(userId) } : {}; const res = await fetch(`${BASE}/api/subscriptions`, { headers }); @@ -119,4 +137,6 @@ export default { subscribeTo, unsubscribeFrom, getSubscriptions, + getComments, + postComment, }; diff --git a/scripts/seed.sh b/scripts/seed.sh new file mode 100755 index 0000000..19a0b34 --- /dev/null +++ b/scripts/seed.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash +# Seed all test data for local development +# Usage: ./scripts/seed.sh [ACCOUNT_URL] [DONATION_URL] [VIDEO_URL] + +ACCOUNT=${1:-http://localhost:5001} +DONATION=${2:-http://localhost:5100} +VIDEO=${3:-http://localhost:5002} +PG_VIDEO=${DOCKER_PG_VIDEO:-glense_postgres_video} + +echo "=== Seeding test users ===" + +register_or_find() { + local username=$1 email=$2 type=$3 + local result user_id + + result=$(curl -s -X POST "$ACCOUNT/api/auth/register" \ + -H "Content-Type: application/json" \ + -d "{ + \"username\": \"$username\", + \"email\": \"$email\", + \"password\": \"Password123!\", + \"confirmPassword\": \"Password123!\", + \"accountType\": \"$type\" + }") + + user_id=$(echo "$result" | python3 -c "import sys,json; print(json.load(sys.stdin)['user']['id'])" 2>/dev/null) + + if [ -n "$user_id" ]; then + echo " Created $username ($type) -> $user_id" >&2 + curl -s -X POST "$DONATION/api/wallet/user/$user_id/topup" \ + -H "Content-Type: application/json" -d '{"amount": 500}' > /dev/null 2>&1 + echo " Wallet topped up with \$500" >&2 + else + user_id=$(curl -s "$ACCOUNT/api/profile/search?q=$username" | \ + python3 -c "import sys,json; [print(u['id']) for u in json.load(sys.stdin) if u['username']=='$username']" 2>/dev/null) + if [ -n "$user_id" ]; then + echo " $username already exists -> $user_id" >&2 + else + echo " $username: could not register or find" >&2 + fi + fi + + echo "$user_id" +} + +KEKI_ID=$(register_or_find "keki" "keki@glense.test" "creator") +IRENA_ID=$(register_or_find "irena" "irena@glense.test" "creator") +BRANKO_ID=$(register_or_find "branko" "branko@glense.test" "user") + +echo "" +echo "=== Seeding sample donations ===" + +send_donation() { + local from_name=$1 from_id=$2 to_name=$3 to_id=$4 amount=$5 message=$6 + if [ -z "$from_id" ] || [ -z "$to_id" ]; then + echo " Skipping $from_name -> $to_name (missing ID)" + return + fi + curl -s -X POST "$DONATION/api/donation" \ + -H "Content-Type: application/json" \ + -d "{\"donorUserId\":\"$from_id\",\"recipientUserId\":\"$to_id\",\"amount\":$amount,\"message\":\"$message\"}" > /dev/null 2>&1 + echo " $from_name -> $to_name: \$$amount ($message)" +} + +send_donation "branko" "$BRANKO_ID" "keki" "$KEKI_ID" 25 "Great content, keep it up!" +send_donation "branko" "$BRANKO_ID" "irena" "$IRENA_ID" 10 "Love your streams!" +send_donation "keki" "$KEKI_ID" "branko" "$BRANKO_ID" 50 "Thanks for the support!" +send_donation "irena" "$IRENA_ID" "keki" "$KEKI_ID" 15 "Collab soon?" + +echo "" +echo "=== Seeding videos & comments ===" + +VIDEO_COUNT=$(curl -s "$VIDEO/api/videos" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null) +if [ "$VIDEO_COUNT" -gt 0 ] 2>/dev/null; then + echo " Videos already seeded ($VIDEO_COUNT found), skipping" +else + TMPFILE=$(mktemp) + python3 - "$KEKI_ID" "$IRENA_ID" "$BRANKO_ID" > "$TMPFILE" << 'PYEOF' +import uuid, random, sys +random.seed(42) + +uids = sys.argv[1:4] +names = ['keki', 'irena', 'branko'] + +videos = [ + ('Microservices Explained in 5 Minutes', 'Quick overview of microservice architecture patterns', 'lL_j7ilk7rc', 320000, 15000, 200), + ('Docker in 100 Seconds', 'Everything you need to know about Docker, fast', 'Gjnup-PuquQ', 890000, 42000, 300), + ('How Do APIs Work?', 'APIs explained with real-world examples', 's7wmiS2mSXY', 234175, 12300, 40), + ('Build and Deploy 5 JavaScript and React API Projects', 'Full course covering 5 real-world API projects', 'GDa8kZLNhJ4', 54321, 4560, 10), + ('.NET 8 Full Course for Beginners', 'Complete beginner guide to .NET 8 and C#', 'AhAxLiGC7Pc', 98000, 5600, 30), + ('Node.js Ultimate Beginners Guide', 'Learn Node.js from scratch in this crash course', 'ENrzD9HAZK4', 445000, 21000, 1800), + ('PostgreSQL Tutorial for Beginners', 'Learn PostgreSQL from the ground up', 'qw--VYLpSFk', 187000, 9800, 120), + ('Git and GitHub for Beginners', 'Full crash course on Git and GitHub', 'RGOj5yH7evk', 150000, 8700, 95), +] + +comments_list = [ + 'This is amazing content, keep it up!', + 'Finally someone explains this properly', + 'Great video, learned a lot!', + 'I have been waiting for this video', + 'Can you do a follow-up on this topic?', + 'This changed my perspective completely', + 'Subscribed! More content like this please', + 'The production quality is insane', + 'Watching this at 2am, no regrets', + 'This deserves way more views', + 'Thanks for sharing your knowledge', + 'Bookmarked for later reference', +] + +video_ids = [] +for i, (title, desc, ytid, views, likes, dislikes) in enumerate(videos): + vid = str(uuid.uuid4()) + video_ids.append(vid) + uid = uids[i % 3] + days = i * 7 + 1 + print(f'INSERT INTO "Videos" (id, title, description, upload_date, uploader_id, thumbnail_url, video_url, view_count, like_count, dislike_count) ' + f"VALUES ('{vid}', '{title}', '{desc}', NOW() - interval '{days} days', '{uid}', " + f"'https://img.youtube.com/vi/{ytid}/hqdefault.jpg', 'https://www.youtube.com/watch?v={ytid}', {views}, {likes}, {dislikes});") + +for vid in video_ids: + for j in range(3): + cid = str(uuid.uuid4()) + ni = (abs(hash(vid)) + j) % 3 + ci = (abs(hash(vid)) + j) % len(comments_list) + lc = random.randint(0, 200) + hrs = random.randint(1, 720) + print(f'INSERT INTO "Comments" (id, video_id, user_id, username, content, like_count, created_at) ' + f"VALUES ('{cid}', '{vid}', '{uids[ni]}', '{names[ni]}', '{comments_list[ci]}', {lc}, NOW() - interval '{hrs} hours');") +PYEOF + + cat "$TMPFILE" | docker exec -i "$PG_VIDEO" psql -U glense -d glense_video > /dev/null 2>&1 + if [ $? -eq 0 ]; then + echo " Inserted 8 videos with comments" + else + echo " ERROR: Failed to insert videos (is $PG_VIDEO running?)" + fi + rm -f "$TMPFILE" +fi + +echo "" +echo "=== Done! ===" +echo "All users have password: Password123!" +echo "Log in via the frontend at http://localhost:5173 (or next free port)" diff --git a/services/Glense.ChatService/Controllers/MessagesController.cs b/services/Glense.ChatService/Controllers/MessagesController.cs index 238edc4..9377ad5 100644 --- a/services/Glense.ChatService/Controllers/MessagesController.cs +++ b/services/Glense.ChatService/Controllers/MessagesController.cs @@ -1,3 +1,4 @@ +using System.Security.Claims; using Glense.ChatService.DTOs; using Glense.ChatService.Services; using Microsoft.AspNetCore.Mvc; @@ -31,7 +32,9 @@ public async Task CreateMessage(Guid chatId, [FromBody] CreateMes if (!ModelState.IsValid) return ValidationProblem(ModelState); try { - var dto = await _svc.CreateMessageAsync(chatId, req, HttpContext.RequestAborted); + var userIdClaim = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; + var userId = Guid.TryParse(userIdClaim, out var uid) ? uid : Guid.Empty; + var dto = await _svc.CreateMessageAsync(chatId, userId, req, HttpContext.RequestAborted); // Return Location header pointing to GET /api/messages/{messageId} return CreatedAtAction(nameof(MessageRootController.GetMessage), "MessageRoot", new { messageId = dto.Id }, dto); } diff --git a/services/Glense.ChatService/DTOs/MessageDto.cs b/services/Glense.ChatService/DTOs/MessageDto.cs index 79478ad..949ce55 100644 --- a/services/Glense.ChatService/DTOs/MessageDto.cs +++ b/services/Glense.ChatService/DTOs/MessageDto.cs @@ -1,3 +1,3 @@ namespace Glense.ChatService.DTOs; -public record MessageDto(Guid Id, Guid ChatId, string Sender, string Content, DateTime CreatedAtUtc); +public record MessageDto(Guid Id, Guid ChatId, Guid UserId, string Sender, string Content, DateTime CreatedAtUtc); diff --git a/services/Glense.ChatService/Data/Configurations/MessageConfig.cs b/services/Glense.ChatService/Data/Configurations/MessageConfig.cs index b3f6c90..c903317 100644 --- a/services/Glense.ChatService/Data/Configurations/MessageConfig.cs +++ b/services/Glense.ChatService/Data/Configurations/MessageConfig.cs @@ -10,6 +10,7 @@ public void Configure(EntityTypeBuilder builder) { builder.ToTable("messages"); builder.HasKey(x => x.Id); + builder.Property(x => x.UserId).IsRequired(); builder.Property(x => x.Content).IsRequired(); builder.Property(x => x.CreatedAtUtc).IsRequired(); builder.Property(x => x.Sender).HasConversion().IsRequired(); diff --git a/services/Glense.ChatService/Migrations/20251214194130_InitialCreate.Designer.cs b/services/Glense.ChatService/Migrations/20251214194130_InitialCreate.Designer.cs deleted file mode 100644 index 46beaa5..0000000 --- a/services/Glense.ChatService/Migrations/20251214194130_InitialCreate.Designer.cs +++ /dev/null @@ -1,95 +0,0 @@ -// -using System; -using Glense.ChatService.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Glense.ChatService.Migrations -{ - [DbContext(typeof(ChatDbContext))] - [Migration("20251214194130_InitialCreate")] - partial class InitialCreate - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.0") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Glense.ChatService.Models.Chat", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAtUtc") - .HasColumnType("timestamp with time zone"); - - b.Property("Topic") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAtUtc") - .HasDatabaseName("IX_chats_created_at"); - - b.ToTable("chats", (string)null); - }); - - modelBuilder.Entity("Glense.ChatService.Models.Message", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ChatId") - .HasColumnType("uuid"); - - b.Property("Content") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAtUtc") - .HasColumnType("timestamp with time zone"); - - b.Property("Sender") - .HasColumnType("smallint"); - - b.HasKey("Id"); - - b.HasIndex("ChatId", "CreatedAtUtc") - .HasDatabaseName("IX_messages_chat_id_created_at"); - - b.ToTable("messages", (string)null); - }); - - modelBuilder.Entity("Glense.ChatService.Models.Message", b => - { - b.HasOne("Glense.ChatService.Models.Chat", "Chat") - .WithMany("Messages") - .HasForeignKey("ChatId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chat"); - }); - - modelBuilder.Entity("Glense.ChatService.Models.Chat", b => - { - b.Navigation("Messages"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/services/Glense.ChatService/Migrations/20251214194130_InitialCreate.cs b/services/Glense.ChatService/Migrations/20251214194130_InitialCreate.cs deleted file mode 100644 index 0210364..0000000 --- a/services/Glense.ChatService/Migrations/20251214194130_InitialCreate.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Glense.ChatService.Migrations -{ - /// - public partial class InitialCreate : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "chats", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - Topic = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), - CreatedAtUtc = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_chats", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "messages", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - ChatId = table.Column(type: "uuid", nullable: false), - Sender = table.Column(type: "smallint", nullable: false), - Content = table.Column(type: "text", nullable: false), - CreatedAtUtc = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_messages", x => x.Id); - table.ForeignKey( - name: "FK_messages_chats_ChatId", - column: x => x.ChatId, - principalTable: "chats", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_chats_created_at", - table: "chats", - column: "CreatedAtUtc"); - - migrationBuilder.CreateIndex( - name: "IX_messages_chat_id_created_at", - table: "messages", - columns: new[] { "ChatId", "CreatedAtUtc" }); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "messages"); - - migrationBuilder.DropTable( - name: "chats"); - } - } -} diff --git a/services/Glense.ChatService/Migrations/ChatDbContextModelSnapshot.cs b/services/Glense.ChatService/Migrations/ChatDbContextModelSnapshot.cs deleted file mode 100644 index 85336f8..0000000 --- a/services/Glense.ChatService/Migrations/ChatDbContextModelSnapshot.cs +++ /dev/null @@ -1,92 +0,0 @@ -// -using System; -using Glense.ChatService.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Glense.ChatService.Migrations -{ - [DbContext(typeof(ChatDbContext))] - partial class ChatDbContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.0") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Glense.ChatService.Models.Chat", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAtUtc") - .HasColumnType("timestamp with time zone"); - - b.Property("Topic") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.HasKey("Id"); - - b.HasIndex("CreatedAtUtc") - .HasDatabaseName("IX_chats_created_at"); - - b.ToTable("chats", (string)null); - }); - - modelBuilder.Entity("Glense.ChatService.Models.Message", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("ChatId") - .HasColumnType("uuid"); - - b.Property("Content") - .IsRequired() - .HasColumnType("text"); - - b.Property("CreatedAtUtc") - .HasColumnType("timestamp with time zone"); - - b.Property("Sender") - .HasColumnType("smallint"); - - b.HasKey("Id"); - - b.HasIndex("ChatId", "CreatedAtUtc") - .HasDatabaseName("IX_messages_chat_id_created_at"); - - b.ToTable("messages", (string)null); - }); - - modelBuilder.Entity("Glense.ChatService.Models.Message", b => - { - b.HasOne("Glense.ChatService.Models.Chat", "Chat") - .WithMany("Messages") - .HasForeignKey("ChatId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Chat"); - }); - - modelBuilder.Entity("Glense.ChatService.Models.Chat", b => - { - b.Navigation("Messages"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/services/Glense.ChatService/Models/Message.cs b/services/Glense.ChatService/Models/Message.cs index 3db5a3d..8c7b5cc 100644 --- a/services/Glense.ChatService/Models/Message.cs +++ b/services/Glense.ChatService/Models/Message.cs @@ -12,6 +12,8 @@ public class Message public Guid ChatId { get; set; } public Chat Chat { get; set; } = default!; + public Guid UserId { get; set; } + public MessageSender Sender { get; set; } [Required] diff --git a/services/Glense.ChatService/Program.cs b/services/Glense.ChatService/Program.cs index d7fe1cf..0b97142 100644 --- a/services/Glense.ChatService/Program.cs +++ b/services/Glense.ChatService/Program.cs @@ -207,14 +207,43 @@ var startupChatConn = app.Configuration.GetConnectionString("DefaultConnection") ?? app.Configuration["ConnectionStrings:DefaultConnection"]; - if (!startupChatUseInMemory && !string.IsNullOrEmpty(startupChatConn)) + using var scope = app.Services.CreateScope(); + var db = scope.ServiceProvider.GetService(); + if (db != null) { - using var scope = app.Services.CreateScope(); - var db = scope.ServiceProvider.GetService(); - if (db != null) + db.Database.EnsureCreated(); + + // Seed demo chats if empty + if (!db.Chats.Any()) { - // EnsureCreated is safe for simple local testing; for production use migrations. - db.Database.EnsureCreated(); + var generalChat = new Glense.ChatService.Models.Chat + { + Id = Guid.NewGuid(), + Topic = "General", + CreatedAtUtc = DateTime.UtcNow.AddDays(-7) + }; + var techChat = new Glense.ChatService.Models.Chat + { + Id = Guid.NewGuid(), + Topic = "Tech Talk", + CreatedAtUtc = DateTime.UtcNow.AddDays(-3) + }; + var gamingChat = new Glense.ChatService.Models.Chat + { + Id = Guid.NewGuid(), + Topic = "Gaming", + CreatedAtUtc = DateTime.UtcNow.AddDays(-1) + }; + db.Chats.AddRange(generalChat, techChat, gamingChat); + + db.Messages.AddRange( + new Glense.ChatService.Models.Message { Id = Guid.NewGuid(), ChatId = generalChat.Id, UserId = Guid.Empty, Sender = Glense.ChatService.Models.MessageSender.User, Content = "Welcome to Glense!", CreatedAtUtc = DateTime.UtcNow.AddDays(-7) }, + new Glense.ChatService.Models.Message { Id = Guid.NewGuid(), ChatId = generalChat.Id, UserId = Guid.Empty, Sender = Glense.ChatService.Models.MessageSender.User, Content = "Hey everyone, glad to be here", CreatedAtUtc = DateTime.UtcNow.AddDays(-6) }, + new Glense.ChatService.Models.Message { Id = Guid.NewGuid(), ChatId = techChat.Id, UserId = Guid.Empty, Sender = Glense.ChatService.Models.MessageSender.User, Content = "Anyone tried .NET 8 yet?", CreatedAtUtc = DateTime.UtcNow.AddDays(-3) }, + new Glense.ChatService.Models.Message { Id = Guid.NewGuid(), ChatId = techChat.Id, UserId = Guid.Empty, Sender = Glense.ChatService.Models.MessageSender.User, Content = "Yeah, the performance improvements are solid", CreatedAtUtc = DateTime.UtcNow.AddDays(-2) }, + new Glense.ChatService.Models.Message { Id = Guid.NewGuid(), ChatId = gamingChat.Id, UserId = Guid.Empty, Sender = Glense.ChatService.Models.MessageSender.User, Content = "What games are you all playing?", CreatedAtUtc = DateTime.UtcNow.AddDays(-1) } + ); + db.SaveChanges(); } } } diff --git a/services/Glense.ChatService/Services/ChatService.cs b/services/Glense.ChatService/Services/ChatService.cs index a40bb5a..8e2df27 100644 --- a/services/Glense.ChatService/Services/ChatService.cs +++ b/services/Glense.ChatService/Services/ChatService.cs @@ -77,27 +77,27 @@ public async Task> GetMessagesAsync(Guid chatId, Guid? var hasMore = items.Count > pageSize; if (hasMore) items = items.Take(pageSize).ToList(); - var dtos = items.Select(m => new MessageDto(m.Id, m.ChatId, m.Sender.ToString().ToLower(), m.Content, m.CreatedAtUtc)); + var dtos = items.Select(m => new MessageDto(m.Id, m.ChatId, m.UserId, m.Sender.ToString().ToLower(), m.Content, m.CreatedAtUtc)); var next = hasMore ? items.Last().Id : (Guid?)null; return new PagedResponse(dtos, next); } - public async Task CreateMessageAsync(Guid chatId, CreateMessageRequest req, CancellationToken ct = default) + public async Task CreateMessageAsync(Guid chatId, Guid userId, CreateMessageRequest req, CancellationToken ct = default) { var chat = await _db.Chats.FindAsync(new object[] { chatId }, ct); if (chat == null) throw new KeyNotFoundException("Chat not found"); if (!Enum.TryParse(req.Sender, true, out var sender)) sender = MessageSender.User; - var m = new Message { Id = Guid.NewGuid(), ChatId = chatId, Content = req.Content, Sender = sender, CreatedAtUtc = DateTime.UtcNow }; + var m = new Message { Id = Guid.NewGuid(), ChatId = chatId, UserId = userId, Content = req.Content, Sender = sender, CreatedAtUtc = DateTime.UtcNow }; _db.Messages.Add(m); await _db.SaveChangesAsync(ct); - return new MessageDto(m.Id, m.ChatId, m.Sender.ToString().ToLower(), m.Content, m.CreatedAtUtc); + return new MessageDto(m.Id, m.ChatId, m.UserId, m.Sender.ToString().ToLower(), m.Content, m.CreatedAtUtc); } public async Task GetMessageAsync(Guid messageId, CancellationToken ct = default) { var m = await _db.Messages.AsNoTracking().FirstOrDefaultAsync(x => x.Id == messageId, ct); if (m == null) return null; - return new MessageDto(m.Id, m.ChatId, m.Sender.ToString().ToLower(), m.Content, m.CreatedAtUtc); + return new MessageDto(m.Id, m.ChatId, m.UserId, m.Sender.ToString().ToLower(), m.Content, m.CreatedAtUtc); } public async Task DeleteMessageAsync(Guid messageId, CancellationToken ct = default) diff --git a/services/Glense.ChatService/Services/IChatService.cs b/services/Glense.ChatService/Services/IChatService.cs index ff923d2..9e7c8fa 100644 --- a/services/Glense.ChatService/Services/IChatService.cs +++ b/services/Glense.ChatService/Services/IChatService.cs @@ -10,7 +10,7 @@ public interface IChatService Task DeleteChatAsync(Guid id, CancellationToken ct = default); Task> GetMessagesAsync(Guid chatId, Guid? cursor, int pageSize, CancellationToken ct = default); - Task CreateMessageAsync(Guid chatId, CreateMessageRequest req, CancellationToken ct = default); + Task CreateMessageAsync(Guid chatId, Guid userId, CreateMessageRequest req, CancellationToken ct = default); Task GetMessageAsync(Guid messageId, CancellationToken ct = default); Task DeleteMessageAsync(Guid messageId, CancellationToken ct = default); } diff --git a/services/Glense.ChatService/database/schema.sql b/services/Glense.ChatService/database/schema.sql index db6540c..d5589b8 100644 --- a/services/Glense.ChatService/database/schema.sql +++ b/services/Glense.ChatService/database/schema.sql @@ -10,6 +10,7 @@ CREATE INDEX IF NOT EXISTS IX_chats_created_at ON chats (created_at_utc); CREATE TABLE IF NOT EXISTS messages ( id uuid PRIMARY KEY, chat_id uuid NOT NULL REFERENCES chats(id) ON DELETE CASCADE, + user_id uuid NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000', sender smallint NOT NULL, content text NOT NULL, created_at_utc timestamptz NOT NULL diff --git a/services/Glense.VideoCatalogue/Controllers/CommentsController.cs b/services/Glense.VideoCatalogue/Controllers/CommentsController.cs new file mode 100644 index 0000000..905cbc6 --- /dev/null +++ b/services/Glense.VideoCatalogue/Controllers/CommentsController.cs @@ -0,0 +1,91 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Glense.VideoCatalogue.Data; +using Glense.VideoCatalogue.Models; +using Glense.VideoCatalogue.DTOs; + +namespace Glense.VideoCatalogue.Controllers; + +[ApiController] +[Route("api/videos/{videoId:guid}/comments")] +public class CommentsController : ControllerBase +{ + private readonly VideoCatalogueDbContext _db; + + public CommentsController(VideoCatalogueDbContext db) + { + _db = db; + } + + [HttpGet] + public async Task GetComments(Guid videoId) + { + var comments = await _db.Comments + .Where(c => c.VideoId == videoId) + .OrderByDescending(c => c.CreatedAt) + .Select(c => new CommentResponseDTO + { + Id = c.Id, + VideoId = c.VideoId, + UserId = c.UserId, + Username = c.Username, + Content = c.Content, + LikeCount = c.LikeCount, + CreatedAt = c.CreatedAt + }) + .ToListAsync(); + + return Ok(comments); + } + + [HttpPost] + public async Task CreateComment( + Guid videoId, + [FromBody] CreateCommentRequestDTO dto, + [FromHeader(Name = "X-User-Id")] Guid userId = default, + [FromHeader(Name = "X-Username")] string username = "Anonymous") + { + if (!ModelState.IsValid) return BadRequest(ModelState); + + var video = await _db.Videos.FindAsync(videoId); + if (video == null) return NotFound("Video not found"); + + var comment = new Comment + { + Id = Guid.NewGuid(), + VideoId = videoId, + UserId = userId, + Username = username, + Content = dto.Content, + LikeCount = 0, + CreatedAt = DateTime.UtcNow + }; + + _db.Comments.Add(comment); + await _db.SaveChangesAsync(); + + var resp = new CommentResponseDTO + { + Id = comment.Id, + VideoId = comment.VideoId, + UserId = comment.UserId, + Username = comment.Username, + Content = comment.Content, + LikeCount = comment.LikeCount, + CreatedAt = comment.CreatedAt + }; + + return Created($"/api/videos/{videoId}/comments", resp); + } + + [HttpDelete("{commentId:guid}")] + public async Task DeleteComment(Guid videoId, Guid commentId) + { + var comment = await _db.Comments.FirstOrDefaultAsync(c => c.Id == commentId && c.VideoId == videoId); + if (comment == null) return NotFound(); + + _db.Comments.Remove(comment); + await _db.SaveChangesAsync(); + return NoContent(); + } +} diff --git a/services/Glense.VideoCatalogue/Controllers/PlaylistsController.cs b/services/Glense.VideoCatalogue/Controllers/PlaylistsController.cs index e1fedc1..7b44f94 100644 --- a/services/Glense.VideoCatalogue/Controllers/PlaylistsController.cs +++ b/services/Glense.VideoCatalogue/Controllers/PlaylistsController.cs @@ -17,7 +17,7 @@ public PlaylistsController(VideoCatalogueDbContext db) } [HttpPost] - public async Task Create([FromBody] DTOs.CreatePlaylistRequestDTO dto, [FromHeader(Name = "X-Creator-Id")] int creatorId = 0) + public async Task Create([FromBody] DTOs.CreatePlaylistRequestDTO dto, [FromHeader(Name = "X-Creator-Id")] Guid creatorId = default) { if (!ModelState.IsValid) return BadRequest(ModelState); @@ -46,10 +46,10 @@ public async Task Create([FromBody] DTOs.CreatePlaylistRequestDTO } [HttpGet] - public async Task List([FromHeader(Name = "X-Creator-Id")] int creatorId = 0) + public async Task List([FromHeader(Name = "X-Creator-Id")] Guid creatorId = default) { var q = _db.Playlists.AsQueryable(); - if (creatorId > 0) q = q.Where(p => p.CreatorId == creatorId); + if (creatorId != Guid.Empty) q = q.Where(p => p.CreatorId == creatorId); var list = await q.Select(playlist => new DTOs.PlaylistResponseDTO { diff --git a/services/Glense.VideoCatalogue/Controllers/SubscriptionsController.cs b/services/Glense.VideoCatalogue/Controllers/SubscriptionsController.cs index f343694..3e56b1f 100644 --- a/services/Glense.VideoCatalogue/Controllers/SubscriptionsController.cs +++ b/services/Glense.VideoCatalogue/Controllers/SubscriptionsController.cs @@ -17,7 +17,7 @@ public SubscriptionsController(VideoCatalogueDbContext db) } [HttpPost] - public async Task Subscribe([FromBody] DTOs.SubscribeRequestDTO dto, [FromHeader(Name = "X-User-Id")] int subscriberId = 0) + public async Task Subscribe([FromBody] DTOs.SubscribeRequestDTO dto, [FromHeader(Name = "X-User-Id")] Guid subscriberId = default) { if (!ModelState.IsValid) return BadRequest(ModelState); @@ -33,7 +33,7 @@ public async Task Subscribe([FromBody] DTOs.SubscribeRequestDTO d } [HttpDelete] - public async Task Unsubscribe([FromBody] DTOs.SubscribeRequestDTO dto, [FromHeader(Name = "X-User-Id")] int subscriberId = 0) + public async Task Unsubscribe([FromBody] DTOs.SubscribeRequestDTO dto, [FromHeader(Name = "X-User-Id")] Guid subscriberId = default) { var s = await _db.Subscriptions.FirstOrDefaultAsync(x => x.SubscriberId == subscriberId && x.SubscribedToId == dto.SubscribedToId); if (s == null) return NotFound(); diff --git a/services/Glense.VideoCatalogue/Controllers/VideoLikesController.cs b/services/Glense.VideoCatalogue/Controllers/VideoLikesController.cs index 7d21676..66a2fab 100644 --- a/services/Glense.VideoCatalogue/Controllers/VideoLikesController.cs +++ b/services/Glense.VideoCatalogue/Controllers/VideoLikesController.cs @@ -17,7 +17,7 @@ public VideoLikesController(VideoCatalogueDbContext db) } [HttpPost] - public async Task Like([FromBody] DTOs.LikeRequestDTO dto, [FromHeader(Name = "X-User-Id")] int userId = 0) + public async Task Like([FromBody] DTOs.LikeRequestDTO dto, [FromHeader(Name = "X-User-Id")] Guid userId = default) { if (!ModelState.IsValid) return BadRequest(ModelState); diff --git a/services/Glense.VideoCatalogue/Controllers/VideosController.cs b/services/Glense.VideoCatalogue/Controllers/VideosController.cs index 6cae849..6fac2fe 100644 --- a/services/Glense.VideoCatalogue/Controllers/VideosController.cs +++ b/services/Glense.VideoCatalogue/Controllers/VideosController.cs @@ -25,7 +25,7 @@ public VideosController(Upload uploader, VideoCatalogueDbContext db, IVideoStora } [HttpPost("upload")] - public async Task Upload([FromForm] DTOs.UploadRequestDTO dto, [FromHeader(Name = "X-Uploader-Id")] int uploaderId = 0) + public async Task Upload([FromForm] DTOs.UploadRequestDTO dto, [FromHeader(Name = "X-Uploader-Id")] Guid uploaderId = default) { if (dto == null || dto.File == null || dto.File.Length == 0) return BadRequest("No file provided"); diff --git a/services/Glense.VideoCatalogue/DTOs/CommentDto.cs b/services/Glense.VideoCatalogue/DTOs/CommentDto.cs new file mode 100644 index 0000000..9e0b134 --- /dev/null +++ b/services/Glense.VideoCatalogue/DTOs/CommentDto.cs @@ -0,0 +1,22 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace Glense.VideoCatalogue.DTOs; + +public class CommentResponseDTO +{ + public Guid Id { get; set; } + public Guid VideoId { get; set; } + public Guid UserId { get; set; } + public string Username { get; set; } = null!; + public string Content { get; set; } = null!; + public int LikeCount { get; set; } + public DateTime CreatedAt { get; set; } +} + +public class CreateCommentRequestDTO +{ + [Required] + [MaxLength(2000)] + public string Content { get; set; } = null!; +} diff --git a/services/Glense.VideoCatalogue/DTOs/CreatePlaylistResponseDTO.cs b/services/Glense.VideoCatalogue/DTOs/CreatePlaylistResponseDTO.cs index 4b96964..03bc44c 100644 --- a/services/Glense.VideoCatalogue/DTOs/CreatePlaylistResponseDTO.cs +++ b/services/Glense.VideoCatalogue/DTOs/CreatePlaylistResponseDTO.cs @@ -12,5 +12,5 @@ public class CreatePlaylistResponseDTO public string? Description { get; set; } public DateTime CreationDate { get; set; } [Required] - public int CreatorId { get; set; } + public Guid CreatorId { get; set; } } diff --git a/services/Glense.VideoCatalogue/DTOs/PlaylistResponseDTO.cs b/services/Glense.VideoCatalogue/DTOs/PlaylistResponseDTO.cs index 35dd578..dc4f8f9 100644 --- a/services/Glense.VideoCatalogue/DTOs/PlaylistResponseDTO.cs +++ b/services/Glense.VideoCatalogue/DTOs/PlaylistResponseDTO.cs @@ -12,5 +12,5 @@ public class PlaylistResponseDTO public string? Description { get; set; } public DateTime CreationDate { get; set; } [Required] - public int CreatorId { get; set; } + public Guid CreatorId { get; set; } } diff --git a/services/Glense.VideoCatalogue/DTOs/SubscribeRequestDTO.cs b/services/Glense.VideoCatalogue/DTOs/SubscribeRequestDTO.cs index 020329d..1136f12 100644 --- a/services/Glense.VideoCatalogue/DTOs/SubscribeRequestDTO.cs +++ b/services/Glense.VideoCatalogue/DTOs/SubscribeRequestDTO.cs @@ -6,5 +6,5 @@ namespace Glense.VideoCatalogue.DTOs; public class SubscribeRequestDTO { [Required] - public int SubscribedToId { get; set; } + public Guid SubscribedToId { get; set; } } diff --git a/services/Glense.VideoCatalogue/DTOs/SubscribeResponseDTO.cs b/services/Glense.VideoCatalogue/DTOs/SubscribeResponseDTO.cs index 2c244bf..a8f3782 100644 --- a/services/Glense.VideoCatalogue/DTOs/SubscribeResponseDTO.cs +++ b/services/Glense.VideoCatalogue/DTOs/SubscribeResponseDTO.cs @@ -6,8 +6,8 @@ namespace Glense.VideoCatalogue.DTOs; public class SubscribeResponseDTO { [Required] - public int SubscriberId { get; set; } + public Guid SubscriberId { get; set; } [Required] - public int SubscribedToId { get; set; } + public Guid SubscribedToId { get; set; } public DateTime SubscriptionDate { get; set; } } diff --git a/services/Glense.VideoCatalogue/DTOs/UploadResponseDTO.cs b/services/Glense.VideoCatalogue/DTOs/UploadResponseDTO.cs index a0258ae..83c7b89 100644 --- a/services/Glense.VideoCatalogue/DTOs/UploadResponseDTO.cs +++ b/services/Glense.VideoCatalogue/DTOs/UploadResponseDTO.cs @@ -15,7 +15,7 @@ public class UploadResponseDTO public string? ThumbnailUrl { get; set; } public DateTime UploadDate { get; set; } [Required] - public int UploaderId { get; set; } + public Guid UploaderId { get; set; } public int ViewCount { get; set; } public int LikeCount { get; set; } public int DislikeCount { get; set; } diff --git a/services/Glense.VideoCatalogue/Data/VideoCatalogueDbContext.cs b/services/Glense.VideoCatalogue/Data/VideoCatalogueDbContext.cs index 41224d3..76dea9a 100644 --- a/services/Glense.VideoCatalogue/Data/VideoCatalogueDbContext.cs +++ b/services/Glense.VideoCatalogue/Data/VideoCatalogueDbContext.cs @@ -13,6 +13,7 @@ public VideoCatalogueDbContext(DbContextOptions options public DbSet PlaylistVideos { get; set; } = null!; public DbSet Subscriptions { get; set; } = null!; public DbSet VideoLikes { get; set; } = null!; + public DbSet Comments { get; set; } = null!; protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -68,5 +69,19 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.Property(e => e.IsLiked).HasColumnName("is_liked"); entity.HasOne(e => e.Video).WithMany(v => v.VideoLikes).HasForeignKey(e => e.VideoId).HasConstraintName("FK_VideoLikes_Videos_video_id"); }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id).HasName("PK_Comments"); + entity.Property(e => e.Id).HasColumnName("id"); + entity.Property(e => e.VideoId).HasColumnName("video_id"); + entity.Property(e => e.UserId).HasColumnName("user_id"); + entity.Property(e => e.Username).HasColumnName("username").HasMaxLength(50).IsRequired(); + entity.Property(e => e.Content).HasColumnName("content").HasMaxLength(2000).IsRequired(); + entity.Property(e => e.LikeCount).HasColumnName("like_count"); + entity.Property(e => e.CreatedAt).HasColumnName("created_at"); + entity.HasOne(e => e.Video).WithMany().HasForeignKey(e => e.VideoId).HasConstraintName("FK_Comments_Videos_video_id"); + entity.HasIndex(e => e.VideoId).HasDatabaseName("IX_Comments_video_id"); + }); } } diff --git a/services/Glense.VideoCatalogue/Dockerfile b/services/Glense.VideoCatalogue/Dockerfile index d63b055..f54bad4 100644 --- a/services/Glense.VideoCatalogue/Dockerfile +++ b/services/Glense.VideoCatalogue/Dockerfile @@ -13,7 +13,7 @@ FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime WORKDIR /app COPY --from=build /app/publish . -ENV ASPNETCORE_URLS=http://+:80 -EXPOSE 80 +ENV ASPNETCORE_URLS=http://+:5002 +EXPOSE 5002 ENTRYPOINT ["dotnet", "Glense.VideoCatalogue.dll"] diff --git a/services/Glense.VideoCatalogue/Migrations/20251217123000_InitialCreate.cs b/services/Glense.VideoCatalogue/Migrations/20251217123000_InitialCreate.cs deleted file mode 100644 index 307eb61..0000000 --- a/services/Glense.VideoCatalogue/Migrations/20251217123000_InitialCreate.cs +++ /dev/null @@ -1,168 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Glense.VideoCatalogue.Migrations -{ - public partial class InitialCreate : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Videos", - columns: table => new - { - id = table.Column(type: "uuid", nullable: false, defaultValueSql: "uuid_generate_v4()"), - title = table.Column(type: "text", nullable: false), - description = table.Column(type: "text", nullable: true), - upload_date = table.Column(type: "timestamp without time zone", nullable: false), - uploader_id = table.Column(type: "integer", nullable: false), - thumbnail_url = table.Column(type: "character varying(512)", maxLength: 512, nullable: true), - video_url = table.Column(type: "character varying(512)", maxLength: 512, nullable: false), - view_count = table.Column(type: "integer", nullable: false), - like_count = table.Column(type: "integer", nullable: false), - dislike_count = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Videos", x => x.id); - table.ForeignKey( - name: "FK_Videos_Users_uploader_id", - column: x => x.uploader_id, - principalTable: "Users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "Playlists", - columns: table => new - { - id = table.Column(type: "uuid", nullable: false, defaultValueSql: "uuid_generate_v4()"), - name = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), - description = table.Column(type: "text", nullable: true), - creator_id = table.Column(type: "integer", nullable: false), - creation_date = table.Column(type: "timestamp without time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Playlists", x => x.id); - table.ForeignKey( - name: "FK_Playlists_Users_creator_id", - column: x => x.creator_id, - principalTable: "Users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "PlaylistVideos", - columns: table => new - { - playlist_id = table.Column(type: "uuid", nullable: false), - video_id = table.Column(type: "uuid", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_PlaylistVideos", x => new { x.playlist_id, x.video_id }); - table.ForeignKey( - name: "FK_PlaylistVideos_Playlists_playlist_id", - column: x => x.playlist_id, - principalTable: "Playlists", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_PlaylistVideos_Videos_video_id", - column: x => x.video_id, - principalTable: "Videos", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "Subscriptions", - columns: table => new - { - subscriber_id = table.Column(type: "integer", nullable: false), - subscribed_to_id = table.Column(type: "integer", nullable: false), - subscription_date = table.Column(type: "timestamp without time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Subscriptions", x => new { x.subscriber_id, x.subscribed_to_id }); - table.ForeignKey( - name: "FK_Subscriptions_Users_subscriber_id", - column: x => x.subscriber_id, - principalTable: "Users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_Subscriptions_Users_subscribed_to_id", - column: x => x.subscribed_to_id, - principalTable: "Users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "VideoLikes", - columns: table => new - { - user_id = table.Column(type: "integer", nullable: false), - video_id = table.Column(type: "uuid", nullable: false), - is_liked = table.Column(type: "boolean", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_VideoLikes", x => new { x.user_id, x.video_id }); - table.ForeignKey( - name: "FK_VideoLikes_Users_user_id", - column: x => x.user_id, - principalTable: "Users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_VideoLikes_Videos_video_id", - column: x => x.video_id, - principalTable: "Videos", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_PlaylistVideos_video_id", - table: "PlaylistVideos", - column: "video_id"); - - migrationBuilder.CreateIndex( - name: "IX_Playlists_creator_id", - table: "Playlists", - column: "creator_id"); - - migrationBuilder.CreateIndex( - name: "IX_Subscriptions_subscribed_to_id", - table: "Subscriptions", - column: "subscribed_to_id"); - - migrationBuilder.CreateIndex( - name: "IX_VideoLikes_video_id", - table: "VideoLikes", - column: "video_id"); - - migrationBuilder.CreateIndex( - name: "IX_Videos_uploader_id", - table: "Videos", - column: "uploader_id"); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable(name: "PlaylistVideos"); - migrationBuilder.DropTable(name: "VideoLikes"); - migrationBuilder.DropTable(name: "Subscriptions"); - migrationBuilder.DropTable(name: "Playlists"); - migrationBuilder.DropTable(name: "Videos"); - } - } -} diff --git a/services/Glense.VideoCatalogue/Migrations/VideoCatalogueModelSnapshot.cs b/services/Glense.VideoCatalogue/Migrations/VideoCatalogueModelSnapshot.cs deleted file mode 100644 index 37460bf..0000000 --- a/services/Glense.VideoCatalogue/Migrations/VideoCatalogueModelSnapshot.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Glense.VideoCatalogue.Data; - -#nullable disable - -namespace Glense.VideoCatalogue.Migrations -{ - [DbContext(typeof(VideoCatalogueDbContext))] - partial class VideoCatalogueModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { - modelBuilder - .HasAnnotation("ProductVersion", "efcore") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - modelBuilder.Entity("Glense.VideoCatalogue.Models.Videos", b => - { - b.Property("Id").HasColumnName("id"); - b.Property("Title").HasColumnName("title").IsRequired(); - b.Property("Description").HasColumnName("description"); - b.Property("UploadDate").HasColumnName("upload_date"); - b.Property("UploaderId").HasColumnName("uploader_id"); - b.Property("ThumbnailUrl").HasColumnName("thumbnail_url"); - b.Property("VideoUrl").HasColumnName("video_url").IsRequired(); - b.Property("ViewCount").HasColumnName("view_count"); - b.Property("LikeCount").HasColumnName("like_count"); - b.Property("DislikeCount").HasColumnName("dislike_count"); - b.HasKey("Id"); - b.HasIndex("UploaderId"); - b.ToTable("Videos"); - }); - - modelBuilder.Entity("Glense.VideoCatalogue.Models.Playlists", b => - { - b.Property("Id").HasColumnName("id"); - b.Property("Name").HasColumnName("name").HasMaxLength(255).IsRequired(); - b.Property("Description").HasColumnName("description"); - b.Property("CreatorId").HasColumnName("creator_id"); - b.Property("CreationDate").HasColumnName("creation_date"); - b.HasKey("Id"); - b.HasIndex("CreatorId"); - b.ToTable("Playlists"); - }); - - modelBuilder.Entity("Glense.VideoCatalogue.Models.PlaylistVideos", b => - { - b.Property("PlaylistId").HasColumnName("playlist_id"); - b.Property("VideoId").HasColumnName("video_id"); - b.HasKey("PlaylistId", "VideoId"); - b.HasIndex("VideoId"); - b.ToTable("PlaylistVideos"); - }); - - modelBuilder.Entity("Glense.VideoCatalogue.Models.Subscriptions", b => - { - b.Property("SubscriberId").HasColumnName("subscriber_id"); - b.Property("SubscribedToId").HasColumnName("subscribed_to_id"); - b.Property("SubscriptionDate").HasColumnName("subscription_date"); - b.HasKey("SubscriberId", "SubscribedToId"); - b.HasIndex("SubscribedToId"); - b.ToTable("Subscriptions"); - }); - - modelBuilder.Entity("Glense.VideoCatalogue.Models.VideoLikes", b => - { - b.Property("UserId").HasColumnName("user_id"); - b.Property("VideoId").HasColumnName("video_id"); - b.Property("IsLiked").HasColumnName("is_liked"); - b.HasKey("UserId", "VideoId"); - b.HasIndex("VideoId"); - b.ToTable("VideoLikes"); - }); - } - } -} diff --git a/services/Glense.VideoCatalogue/Models/Comment.cs b/services/Glense.VideoCatalogue/Models/Comment.cs new file mode 100644 index 0000000..9346a80 --- /dev/null +++ b/services/Glense.VideoCatalogue/Models/Comment.cs @@ -0,0 +1,39 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Glense.VideoCatalogue.Models; + +[Table("Comments")] +public class Comment +{ + [Key] + [Column("id")] + public Guid Id { get; set; } + + [Required] + [Column("video_id")] + public Guid VideoId { get; set; } + + [Required] + [Column("user_id")] + public Guid UserId { get; set; } + + [Required] + [MaxLength(50)] + [Column("username")] + public string Username { get; set; } = null!; + + [Required] + [MaxLength(2000)] + [Column("content")] + public string Content { get; set; } = null!; + + [Column("like_count")] + public int LikeCount { get; set; } + + [Column("created_at")] + public DateTime CreatedAt { get; set; } + + public Videos? Video { get; set; } +} diff --git a/services/Glense.VideoCatalogue/Models/Playlists.cs b/services/Glense.VideoCatalogue/Models/Playlists.cs index 4a42dde..d4dbfa4 100644 --- a/services/Glense.VideoCatalogue/Models/Playlists.cs +++ b/services/Glense.VideoCatalogue/Models/Playlists.cs @@ -19,7 +19,7 @@ public class Playlists public string? Description { get; set; } [Column("creator_id")] - public int CreatorId { get; set; } + public Guid CreatorId { get; set; } [Column("creation_date")] public DateTime CreationDate { get; set; } diff --git a/services/Glense.VideoCatalogue/Models/Subscriptions.cs b/services/Glense.VideoCatalogue/Models/Subscriptions.cs index 4a2ad5a..df3d92f 100644 --- a/services/Glense.VideoCatalogue/Models/Subscriptions.cs +++ b/services/Glense.VideoCatalogue/Models/Subscriptions.cs @@ -7,10 +7,10 @@ namespace Glense.VideoCatalogue.Models; public class Subscriptions { [Column("subscriber_id")] - public int SubscriberId { get; set; } + public Guid SubscriberId { get; set; } [Column("subscribed_to_id")] - public int SubscribedToId { get; set; } + public Guid SubscribedToId { get; set; } [Column("subscription_date")] public DateTime SubscriptionDate { get; set; } diff --git a/services/Glense.VideoCatalogue/Models/VideoLikes.cs b/services/Glense.VideoCatalogue/Models/VideoLikes.cs index 5e24f05..69b2c77 100644 --- a/services/Glense.VideoCatalogue/Models/VideoLikes.cs +++ b/services/Glense.VideoCatalogue/Models/VideoLikes.cs @@ -7,7 +7,7 @@ namespace Glense.VideoCatalogue.Models; public class VideoLikes { [Column("user_id")] - public int UserId { get; set; } + public Guid UserId { get; set; } [Column("video_id")] public Guid VideoId { get; set; } diff --git a/services/Glense.VideoCatalogue/Models/Videos.cs b/services/Glense.VideoCatalogue/Models/Videos.cs index f25d375..063359c 100644 --- a/services/Glense.VideoCatalogue/Models/Videos.cs +++ b/services/Glense.VideoCatalogue/Models/Videos.cs @@ -25,7 +25,7 @@ public class Videos [Required] [Column("uploader_id")] - public int UploaderId { get; set; } + public Guid UploaderId { get; set; } [MaxLength(512)] [Column("thumbnail_url")] diff --git a/services/Glense.VideoCatalogue/Program.cs b/services/Glense.VideoCatalogue/Program.cs index aa68188..ed9d5af 100644 --- a/services/Glense.VideoCatalogue/Program.cs +++ b/services/Glense.VideoCatalogue/Program.cs @@ -56,49 +56,17 @@ app.MapControllers(); app.MapHealthChecks("/health"); -// Seed demo videos when using in-memory provider to ensure frontend shows content +// Ensure database schema exists using (var scope = app.Services.CreateScope()) { try { var db = scope.ServiceProvider.GetRequiredService(); db.Database.EnsureCreated(); - if (!db.Videos.Any()) - { - db.Videos.AddRange( - new Videos - { - Id = Guid.NewGuid(), - Title = "An Honest Review of Apple Intelligence... So Far", - Description = "Demo video seeded for development", - UploadDate = DateTime.UtcNow, - UploaderId = 1, - ThumbnailUrl = "https://i.ibb.co/G2L2Gwp/API-Course.png", - VideoUrl = "https://www.youtube.com/watch?v=haDjmBT9tu4", - ViewCount = 12345, - LikeCount = 123, - DislikeCount = 4 - }, - new Videos - { - Id = Guid.NewGuid(), - Title = "Build and Deploy 5 JavaScript & React API Projects", - Description = "Demo playlist video", - UploadDate = DateTime.UtcNow, - UploaderId = 2, - ThumbnailUrl = "https://i.ibb.co/G2L2Gwp/API-Course.png", - VideoUrl = "https://www.youtube.com/watch?v=GDa8kZLNhJ4", - ViewCount = 54321, - LikeCount = 456, - DislikeCount = 10 - } - ); - db.SaveChanges(); - } } catch { - // ignore seeding errors in environments where DB isn't available + // ignore DB errors on startup } } diff --git a/services/Glense.VideoCatalogue/Services/CreatePlaylist.cs b/services/Glense.VideoCatalogue/Services/CreatePlaylist.cs index ed2422a..5b3e60e 100644 --- a/services/Glense.VideoCatalogue/Services/CreatePlaylist.cs +++ b/services/Glense.VideoCatalogue/Services/CreatePlaylist.cs @@ -14,7 +14,7 @@ public CreatePlaylist(VideoCatalogueDbContext db) _db = db; } - public async Task CreateAsync(string name, string? description, int creatorId, CancellationToken cancellationToken = default) + public async Task CreateAsync(string name, string? description, Guid creatorId, CancellationToken cancellationToken = default) { var playlist = new Playlists { diff --git a/services/Glense.VideoCatalogue/Services/LikeVideo.cs b/services/Glense.VideoCatalogue/Services/LikeVideo.cs index 65250f8..93360bb 100644 --- a/services/Glense.VideoCatalogue/Services/LikeVideo.cs +++ b/services/Glense.VideoCatalogue/Services/LikeVideo.cs @@ -15,7 +15,7 @@ public LikeVideo(VideoCatalogueDbContext db) _db = db; } - public async Task<(int LikeCount, int DislikeCount)> SetLikeAsync(int userId, Guid videoId, bool isLiked, CancellationToken cancellationToken = default) + public async Task<(int LikeCount, int DislikeCount)> SetLikeAsync(Guid userId, Guid videoId, bool isLiked, CancellationToken cancellationToken = default) { var like = await _db.VideoLikes.FirstOrDefaultAsync(l => l.UserId == userId && l.VideoId == videoId, cancellationToken).ConfigureAwait(false); if (like == null) diff --git a/services/Glense.VideoCatalogue/Services/Subscribe.cs b/services/Glense.VideoCatalogue/Services/Subscribe.cs index 2ce9621..03f7f66 100644 --- a/services/Glense.VideoCatalogue/Services/Subscribe.cs +++ b/services/Glense.VideoCatalogue/Services/Subscribe.cs @@ -14,7 +14,7 @@ public Subscribe(VideoCatalogueDbContext db) _db = db; } - public async Task SubscribeAsync(int subscriberId, int subscribedToId, CancellationToken cancellationToken = default) + public async Task SubscribeAsync(Guid subscriberId, Guid subscribedToId, CancellationToken cancellationToken = default) { var exists = await _db.Subscriptions.FindAsync(new object[] { subscriberId, subscribedToId }, cancellationToken).ConfigureAwait(false); if (exists != null) return exists; @@ -25,7 +25,7 @@ public async Task SubscribeAsync(int subscriberId, int subscribed return s; } - public async Task UnsubscribeAsync(int subscriberId, int subscribedToId, CancellationToken cancellationToken = default) + public async Task UnsubscribeAsync(Guid subscriberId, Guid subscribedToId, CancellationToken cancellationToken = default) { var s = await _db.Subscriptions.FirstOrDefaultAsync(x => x.SubscriberId == subscriberId && x.SubscribedToId == subscribedToId, cancellationToken: cancellationToken).ConfigureAwait(false); if (s == null) return false; diff --git a/services/Glense.VideoCatalogue/Services/Upload.cs b/services/Glense.VideoCatalogue/Services/Upload.cs index ec574ba..7b6ddba 100644 --- a/services/Glense.VideoCatalogue/Services/Upload.cs +++ b/services/Glense.VideoCatalogue/Services/Upload.cs @@ -17,7 +17,7 @@ public Upload(IVideoStorage storage, VideoCatalogueDbContext db) _db = db; } - public async Task UploadFileAsync(IFormFile file, string? title, string? description, int uploaderId, CancellationToken cancellationToken = default) + public async Task UploadFileAsync(IFormFile file, string? title, string? description, Guid uploaderId, CancellationToken cancellationToken = default) { if (file == null) throw new ArgumentNullException(nameof(file));