Patch: Tidy up of style code and fix to authentication logic

Feat: Added ability to access user's username through AuthContext
This commit is contained in:
Chris-1010
2025-01-27 16:11:42 +00:00
parent 4e9fa011fa
commit 93b3ffbc0b
16 changed files with 97 additions and 119 deletions

View File

@@ -15,7 +15,7 @@ services:
build: build:
context: ./web_server context: ./web_server
ports: ports:
- "5000" - "5000:5000"
networks: networks:
- app_network - app_network
env_file: env_file:

View File

@@ -2,18 +2,20 @@ import { useState, useEffect } from "react";
import { AuthContext } from "./context/AuthContext"; import { AuthContext } from "./context/AuthContext";
import { StreamsProvider } from "./context/StreamsContext"; import { StreamsProvider } from "./context/StreamsContext";
import { BrowserRouter, Routes, Route } from "react-router-dom"; import { BrowserRouter, Routes, Route } from "react-router-dom";
import HomePage, { PersonalisedHomePage } from "./pages/HomePage"; import HomePage from "./pages/HomePage";
import StreamerRoute from "./components/Stream/StreamerRoute"; import StreamerRoute from "./components/Stream/StreamerRoute";
import NotFoundPage from "./pages/NotFoundPage"; import NotFoundPage from "./pages/NotFoundPage";
function App() { function App() {
const [isLoggedIn, setIsLoggedIn] = useState(false); const [isLoggedIn, setIsLoggedIn] = useState(false);
const [username, setUsername] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
fetch("/api/get_login_status") fetch("/api/get_login_status")
.then((response) => response.json()) .then((response) => response.json())
.then((loggedIn) => { .then((data) => {
setIsLoggedIn(loggedIn); setIsLoggedIn(data.status);
setUsername(data.username);
}) })
.catch((error) => { .catch((error) => {
console.error("Error fetching login status:", error); console.error("Error fetching login status:", error);
@@ -22,13 +24,13 @@ function App() {
}, []); }, []);
return ( return (
<AuthContext.Provider value={{ isLoggedIn, setIsLoggedIn }}> <AuthContext.Provider value={{ isLoggedIn, username, setIsLoggedIn, setUsername }}>
<StreamsProvider> <StreamsProvider>
<BrowserRouter> <BrowserRouter>
<Routes> <Routes>
<Route <Route
path="/" path="/"
element={isLoggedIn ? <PersonalisedHomePage /> : <HomePage />} element={isLoggedIn ? <HomePage variant="personalised" /> : <HomePage />}
/> />
<Route path="/:streamerName" element={<StreamerRoute />} /> <Route path="/:streamerName" element={<StreamerRoute />} />

View File

@@ -20,25 +20,6 @@
background: #555; background: #555;
} }
.bg-repeat {
animation: moving_bg 200s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
.bg-repeat {
animation: none;
}
}
@keyframes moving_bg {
0% {
background-position: 0% 0%;
}
100% {
background-position: 100% 0%;
}
}
/* /*
:root { :root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;

View File

@@ -7,14 +7,14 @@ interface LogoProps {
const Logo: React.FC<LogoProps> = ({ variant = "default" }) => { const Logo: React.FC<LogoProps> = ({ variant = "default" }) => {
const gradient = const gradient =
"bg-gradient-to-br from-yellow-400 via-red-500 to-indigo-500 text-transparent bg-clip-text group-hover:mx-1 transition-all"; "text-transparent group-hover:mx-1 transition-all";
return ( return (
<Link to="/" className="cursor-pointer"> <Link to="/" className="cursor-pointer">
<div id="logo" className={`group py-3 text-center font-bold hover:scale-110 transition-all ${variant === "home" ? "text-[12vh]" : "text-[4vh]"}`}> <div id="logo" className={`group py-3 text-center font-bold hover:scale-110 transition-all ${variant === "home" ? "text-[12vh]" : "text-[4vh]"}`}>
<h6 className="text-sm bg-gradient-to-br from-blue-400 via-green-500 to-indigo-500 font-black text-transparent bg-clip-text"> <h6 className="text-sm bg-gradient-to-br from-blue-400 via-green-500 to-indigo-500 font-black text-transparent bg-clip-text">
Go on, have a... Go on, have a...
</h6> </h6>
<div className="flex w-fit min-w-[30vw] justify-center leading-none transition-all"> <div className="flex w-fit min-w-[30vw] bg-logo bg-clip-text animate-moving_text_colour bg-[length:300%_300%] justify-center leading-none transition-all">
<span className={gradient}>G</span> <span className={gradient}>G</span>
<span className={gradient}>A</span> <span className={gradient}>A</span>
<span className={gradient}>N</span> <span className={gradient}>N</span>

View File

@@ -1,11 +0,0 @@
const Name = () => {
return (
<div id="logo" className="text-center">
<span className="text-7xl font-bold italic bg-agog bg-clip-text text-transparent leading-none p-1 hover:scale-110 transition-all hover:animate-agog bg-[length:300%_300%]">
AGOG
</span>
</div>
);
};
export default Name;

View File

@@ -26,7 +26,7 @@ const StreamerRoute: React.FC = () => {
checkStreamStatus(); checkStreamStatus();
// Poll for live status changes // Poll for live status changes
const interval = setInterval(checkStreamStatus, 90000); // Check every 90 seconds const interval = setInterval(checkStreamStatus, 1000); // Check every 90 seconds
return () => clearInterval(interval); return () => clearInterval(interval);
}, [streamerName]); }, [streamerName]);

View File

@@ -1,6 +1,7 @@
import React, { useState, useEffect, useRef } from "react"; import React, { useState, useEffect, useRef } from "react";
import { io, Socket } from "socket.io-client"; import { io, Socket } from "socket.io-client";
import Input from "../Layout/Input"; import Input from "../Layout/Input";
import { useAuth } from "../../context/AuthContext";
interface ChatMessage { interface ChatMessage {
chatter_id: string; chatter_id: string;
@@ -10,21 +11,21 @@ interface ChatMessage {
interface ChatPanelProps { interface ChatPanelProps {
streamId: number; streamId: number;
chatterId?: string; // Optional as user might not be logged in
} }
const ChatPanel: React.FC<ChatPanelProps> = ({ streamId, chatterId }) => { const ChatPanel: React.FC<ChatPanelProps> = ({ streamId }) => {
const [messages, setMessages] = useState<ChatMessage[]>([]); const [messages, setMessages] = useState<ChatMessage[]>([]);
const [inputMessage, setInputMessage] = useState(""); const [inputMessage, setInputMessage] = useState("");
const [socket, setSocket] = useState<Socket | null>(null); const [socket, setSocket] = useState<Socket | null>(null);
const chatContainerRef = useRef<HTMLDivElement>(null); const chatContainerRef = useRef<HTMLDivElement>(null);
const { isLoggedIn, username } = useAuth();
// Initialize socket connection // Initialize socket connection
useEffect(() => { useEffect(() => {
const newSocket = io("/", { const newSocket = io("/", {
path: "/api/socket.io", path: "/api/socket.io",
withCredentials: true withCredentials: true
}); // Make sure this matches your backend URL });
setSocket(newSocket); setSocket(newSocket);
newSocket.on("connect", () => { newSocket.on("connect", () => {
@@ -37,6 +38,14 @@ const ChatPanel: React.FC<ChatPanelProps> = ({ streamId, chatterId }) => {
setMessages(prev => [...prev, data]); setMessages(prev => [...prev, data]);
}); });
newSocket.on("connect_error", (error) => {
console.error("Socket connection error:", error);
});
newSocket.on("connect_timeout", () => {
console.error("Socket connection timeout");
});
newSocket.on("error", (error) => { newSocket.on("error", (error) => {
console.error("Socket error:", error); console.error("Socket error:", error);
}); });
@@ -74,10 +83,12 @@ const ChatPanel: React.FC<ChatPanelProps> = ({ streamId, chatterId }) => {
}, [messages]); }, [messages]);
const sendChat = () => { const sendChat = () => {
if (!inputMessage.trim() || !chatterId || !socket) return; if (!inputMessage.trim() || !socket) {
console.log("No message to send or socket not initialized!");
return;
};
socket.emit("send_message", { socket.emit("send_message", {
chatter_id: chatterId,
stream_id: streamId, stream_id: streamId,
message: inputMessage.trim() message: inputMessage.trim()
}); });
@@ -106,7 +117,7 @@ const ChatPanel: React.FC<ChatPanelProps> = ({ streamId, chatterId }) => {
<span className="text-gray-400 text-sm"> <span className="text-gray-400 text-sm">
{new Date(msg.time_sent).toLocaleTimeString()} {new Date(msg.time_sent).toLocaleTimeString()}
</span> </span>
<span className={`font-bold ${msg.chatter_id === chatterId ? "text-blue-400" : "text-green-400"}`}> {msg.chatter_id}: </span> <span className={`font-bold ${msg.chatter_id === username ? "text-blue-400" : "text-green-400"}`}> {msg.chatter_id}: </span>
<span>{msg.message}</span> <span>{msg.message}</span>
</div> </div>
))} ))}
@@ -118,13 +129,13 @@ const ChatPanel: React.FC<ChatPanelProps> = ({ streamId, chatterId }) => {
value={inputMessage} value={inputMessage}
onChange={(e) => setInputMessage(e.target.value)} onChange={(e) => setInputMessage(e.target.value)}
onKeyDown={handleKeyPress} onKeyDown={handleKeyPress}
placeholder={chatterId ? "Type a message..." : "Login to chat"} placeholder={isLoggedIn ? "Type a message..." : "Login to chat"}
disabled={!chatterId} disabled={!isLoggedIn}
extraClasses="flex-grow disabled:cursor-not-allowed" extraClasses="flex-grow disabled:cursor-not-allowed"
/> />
<button <button
onClick={sendChat} onClick={sendChat}
disabled={!chatterId} disabled={!isLoggedIn}
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed" className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed"
> >
Send Send

View File

@@ -2,7 +2,9 @@ import { createContext, useContext } from "react";
interface AuthContextType { interface AuthContextType {
isLoggedIn: boolean; isLoggedIn: boolean;
username: string | null;
setIsLoggedIn: (value: boolean) => void; setIsLoggedIn: (value: boolean) => void;
setUsername: (value: string | null) => void;
} }
export const AuthContext = createContext<AuthContextType | undefined>( export const AuthContext = createContext<AuthContextType | undefined>(

View File

@@ -3,9 +3,12 @@ import Navbar from "../components/Layout/Navbar";
import StreamListRow from "../components/Layout/StreamListRow"; import StreamListRow from "../components/Layout/StreamListRow";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useStreams } from "../context/StreamsContext"; import { useStreams } from "../context/StreamsContext";
import Name from "../components/Layout/Name";
const HomePage: React.FC = () => { interface HomePageProps {
variant?: "default" | "personalised";
}
const HomePage: React.FC<HomePageProps> = ({ variant = "default" }) => {
const { featuredStreams, featuredCategories } = useStreams(); const { featuredStreams, featuredCategories } = useStreams();
const navigate = useNavigate(); const navigate = useNavigate();
@@ -17,54 +20,22 @@ const HomePage: React.FC = () => {
return ( return (
<div <div
id="home-page" id="home-page"
className="bg-repeat" className="animate-moving_bg"
style={{ backgroundImage: "url(/images/background-pattern.svg)" }} style={{ backgroundImage: "url(/images/background-pattern.svg)" }}
> >
<Navbar variant="home" /> <Navbar variant="home" />
<Name></Name>
{/*//TODO Extract StreamListRow away, to ListRow so that it makes sense for categories to be there also */}
<StreamListRow <StreamListRow
title="Live Now" title={"Live Now" + (variant === "personalised" ? " - Recommended" : "")}
description="Streamers that are currently live" description={variant === "personalised" ? "We think you might like these streams - Streamers recommended for you" : "Streamers that are currently live"}
streams={featuredStreams} streams={featuredStreams}
onStreamClick={handleStreamClick} onStreamClick={handleStreamClick}
/> />
<StreamListRow <StreamListRow
title="Trending Categories" title={variant === "personalised" ? "Followed Categories" : "Trending Categories"}
description="Categories that have been 'popping off' lately" description={variant === "personalised" ? "Current streams from your followed categories" : "Categories that have been 'popping off' lately"}
streams={featuredCategories}
onStreamClick={() => {}} //TODO
/>
</div>
);
};
export const PersonalisedHomePage: React.FC = () => {
const { featuredStreams, featuredCategories } = useStreams();
const navigate = useNavigate();
const handleStreamClick = (streamId: number, streamerName: string) => {
console.log(`Navigating to ${streamId}`);
navigate(`/${streamerName}`);
};
return (
<div
id="personalised-home-page"
className="bg-repeat"
style={{ backgroundImage: "url(/images/background-pattern.svg)" }}
>
<Navbar variant="home" />
{/*//TODO Extract StreamListRow away to ListRow so that it makes sense for categories to be there also */}
<StreamListRow
title="Live Now - Recommended"
description="We think you might like these streams - Streamers recommended for you"
streams={featuredStreams}
onStreamClick={handleStreamClick}
/>
<StreamListRow
title="Followed Categories"
description="Current streams from your followed categories"
streams={featuredCategories} streams={featuredCategories}
onStreamClick={() => {}} //TODO onStreamClick={() => {}} //TODO
/> />

View File

@@ -45,7 +45,7 @@ const VideoPage: React.FC<VideoPageProps> = ({ streamId }) => {
<VideoPlayer streamId={streamId} /> <VideoPlayer streamId={streamId} />
{isLoggedIn ? ( {isLoggedIn ? (
<ChatPanel streamId={streamId} chatterId="chatter-man" /> <ChatPanel streamId={streamId} />
) : ( ) : (
<ChatPanel streamId={streamId} /> <ChatPanel streamId={streamId} />
)} )}

View File

@@ -6,20 +6,27 @@ export default {
], ],
theme: { theme: {
extend: { extend: {
animation: {
moving_text_colour: "moving_text_colour 6s ease-in-out infinite alternate",
moving_bg: 'moving_bg 200s linear infinite'
},
backgroundImage: {
logo: "linear-gradient(45deg, #60A5FA, #8B5CF6, #EC4899, #FACC15,#60A5FA, #8B5CF6, #EC4899, #FACC15)",
},
keyframes: { keyframes: {
agog: { moving_text_colour: {
"0%": { backgroundPosition: "0% 50%" }, "0%": { backgroundPosition: "0% 50%" },
"100%": { backgroundPosition: "100% 50%" }, "100%": { backgroundPosition: "100% 50%" },
}, },
}, moving_bg: {
'0%': { backgroundPosition: '0% 0%' },
animation: { '100%': { backgroundPosition: '100% 0%' }
agog: "agog 6s linear infinite", }
}, }
backgroundImage: {
agog: "linear-gradient(to right, #60A5FA, #8B5CF6, #EC4899, #FACC15,#60A5FA, #8B5CF6, #EC4899, #FACC15)",
},
}, },
}, },
plugins: [ plugins: [

View File

@@ -25,13 +25,10 @@ def signup():
# Validation - ensure all fields exist, users cannot have an empty field # Validation - ensure all fields exist, users cannot have an empty field
if not all([username, email, password]): if not all([username, email, password]):
fields = ["username", "email", "password"] error_fields = get_error_fields([username, email, password]), #!←← find the error_fields, to highlight them in red to the user on the frontend
for x in fields:
if not [username, email, password][fields.index(x)]:
fields.remove(x)
return jsonify({ return jsonify({
"account_created": False, "account_created": False,
"error_fields": fields, "error_fields": error_fields,
"message": "Missing required fields" "message": "Missing required fields"
}), 400 }), 400
@@ -41,9 +38,10 @@ def signup():
email = sanitizer(email, "email") email = sanitizer(email, "email")
password = sanitizer(password, "password") password = sanitizer(password, "password")
except ValueError as e: except ValueError as e:
error_fields = get_error_fields([username, email, password])
return jsonify({ return jsonify({
"account_created": False, "account_created": False,
"error_fields": fields, "error_fields": error_fields,
"message": "Invalid input received" "message": "Invalid input received"
}), 400 }), 400
@@ -204,3 +202,10 @@ def logout() -> dict:
""" """
session.clear() session.clear()
return {"logged_in": False} return {"logged_in": False}
def get_error_fields(values: list):
fields = ["username", "email", "password"]
for x in fields:
if not values[fields.index(x)]:
fields.remove(x)
return fields

View File

@@ -9,6 +9,7 @@ socketio = SocketIO()
# <---------------------- ROUTES NEEDS TO BE CHANGED TO VIDEO OR DELETED AS DEEMED APPROPRIATE ----------------------> # <---------------------- ROUTES NEEDS TO BE CHANGED TO VIDEO OR DELETED AS DEEMED APPROPRIATE ---------------------->
# TODO: Add a route that deletes all chat logs when the stream is finished # TODO: Add a route that deletes all chat logs when the stream is finished
@socketio.on("connect") @socketio.on("connect")
def handle_connection() -> None: def handle_connection() -> None:
""" """
@@ -16,6 +17,7 @@ def handle_connection() -> None:
""" """
print("Client Connected") # Confirmation connect has been made print("Client Connected") # Confirmation connect has been made
@socketio.on("join") @socketio.on("join")
def handle_join(data) -> None: def handle_join(data) -> None:
""" """
@@ -26,6 +28,7 @@ def handle_join(data) -> None:
join_room(stream_id) join_room(stream_id)
emit("status", {"message": f"Welcome to the chat, stream_id: {stream_id}"}, room=stream_id) emit("status", {"message": f"Welcome to the chat, stream_id: {stream_id}"}, room=stream_id)
@socketio.on("leave") @socketio.on("leave")
def handle_leave(data) -> None: def handle_leave(data) -> None:
""" """
@@ -36,11 +39,12 @@ def handle_leave(data) -> None:
leave_room(stream_id) leave_room(stream_id)
emit("status", {"message": f"user left room {stream_id}"}, room=stream_id) emit("status", {"message": f"user left room {stream_id}"}, room=stream_id)
@chat_bp.route("/chat/<int:stream_id>") @chat_bp.route("/chat/<int:stream_id>")
def get_past_chat(stream_id: int): def get_past_chat(stream_id: int):
""" """
Returns a JSON object to be passed to the server. Returns a JSON object to be passed to the server.
Output structure in the following format: `{chatter_id: message}` for all chats. Output structure in the following format: `{chatter_id: message}` for all chats.
Ran once when a user first logs into a stream to get the most recent 50 chat messages. Ran once when a user first logs into a stream to get the most recent 50 chat messages.
@@ -49,7 +53,7 @@ def get_past_chat(stream_id: int):
# Connect to the database # Connect to the database
db = Database() db = Database()
cursor = db.create_connection() cursor = db.create_connection()
# fetched in format: [(chatter_id, message, time_sent)] # fetched in format: [(chatter_id, message, time_sent)]
all_chats = cursor.execute(""" all_chats = cursor.execute("""
SELECT * SELECT *
@@ -62,13 +66,15 @@ def get_past_chat(stream_id: int):
) )
ORDER BY time_sent ASC;""", (stream_id,)).fetchall() ORDER BY time_sent ASC;""", (stream_id,)).fetchall()
db.close_connection() db.close_connection()
# Create JSON output of chat_history to pass through NGINX proxy # Create JSON output of chat_history to pass through NGINX proxy
chat_history = [{"chatter_id": chat[0], "message": chat[1], "time_sent": chat[2]} for chat in all_chats] chat_history = [{"chatter_id": chat[0], "message": chat[1],
"time_sent": chat[2]} for chat in all_chats]
# Pass the chat history to the proxy # Pass the chat history to the proxy
return jsonify({"chat_history": chat_history}), 200 return jsonify({"chat_history": chat_history}), 200
@socketio.on("send_message") @socketio.on("send_message")
def send_chat(data) -> None: def send_chat(data) -> None:
""" """
@@ -84,7 +90,7 @@ def send_chat(data) -> None:
if not all([chatter_id, message, stream_id]): if not all([chatter_id, message, stream_id]):
emit("error", {"error": "Unable to send a chat"}, broadcast=False) emit("error", {"error": "Unable to send a chat"}, broadcast=False)
return return
# Save chat information to database so other users can see # Save chat information to database so other users can see
db = Database() db = Database()
cursor = db.create_connection() cursor = db.create_connection()
@@ -96,7 +102,7 @@ def send_chat(data) -> None:
# Send the chat message to the client so it can be displayed # Send the chat message to the client so it can be displayed
emit("new_message", { emit("new_message", {
"chatter_id":chatter_id, "chatter_id": chatter_id,
"message":message, "message": message,
"time_sent": datetime.now().strftime("%Y-%m-%d %H:%M:%S") "time_sent": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}, room=stream_id) }, room=stream_id)

View File

@@ -3,6 +3,7 @@ from utils.user_utils import is_subscribed, is_following, subscription_expiratio
user_bp = Blueprint("user", __name__) user_bp = Blueprint("user", __name__)
@user_bp.route('/is_subscribed/<int:user_id>/<int:streamer_id>') @user_bp.route('/is_subscribed/<int:user_id>/<int:streamer_id>')
def user_subscribed(user_id: int, streamer_id: int): def user_subscribed(user_id: int, streamer_id: int):
""" """
@@ -19,7 +20,7 @@ def user_following(user_id: int, streamer_id: int):
""" """
if is_following(user_id, streamer_id): if is_following(user_id, streamer_id):
return jsonify({"following": True}) return jsonify({"following": True})
return jsonify({"following": False}) return jsonify({"following": False})
@user_bp.route('/subscription_remaining/<int:user_id>/<int:streamer_id>') @user_bp.route('/subscription_remaining/<int:user_id>/<int:streamer_id>')
@@ -28,7 +29,7 @@ def user_subscription_expiration(user_id: int, streamer_id: int):
Returns remaining time until subscription expiration Returns remaining time until subscription expiration
""" """
remaining_time = subscription_expiration(user_id, streamer_id) remaining_time = subscription_expiration(user_id, streamer_id)
return jsonify({"remaining_time": remaining_time}) return jsonify({"remaining_time": remaining_time})
@user_bp.route('/get_login_status') @user_bp.route('/get_login_status')
@@ -36,7 +37,9 @@ def get_login_status():
""" """
Returns whether the user is logged in or not Returns whether the user is logged in or not
""" """
return jsonify(session.get("username") is not None) username = session.get("username")
return jsonify({'status': username is not None, 'username': username})
@user_bp.route('/authenticate_user') @user_bp.route('/authenticate_user')
def authenticate_user() -> dict: def authenticate_user() -> dict:
@@ -45,6 +48,7 @@ def authenticate_user() -> dict:
""" """
return {"authenticated": True} return {"authenticated": True}
@user_bp.route('/forgot_password', methods=['POST']) @user_bp.route('/forgot_password', methods=['POST'])
def forgot_password(): def forgot_password():
""" """

View File

@@ -8,6 +8,7 @@ Flask==3.1.0
Flask-Session==0.8.0 Flask-Session==0.8.0
Flask-WTF==1.2.2 Flask-WTF==1.2.2
Flask_CORS==5.0.0 Flask_CORS==5.0.0
flask-socketio==5.5.1
python-dotenv==1.0.1 python-dotenv==1.0.1
idna==3.10 idna==3.10
itsdangerous==2.2.0 itsdangerous==2.2.0
@@ -20,5 +21,4 @@ typing_extensions==4.12.2
urllib3==2.3.0 urllib3==2.3.0
Werkzeug==3.1.3 Werkzeug==3.1.3
WTForms==3.2.1 WTForms==3.2.1
Gunicorn==20.1.0 Gunicorn==20.1.0
flask-socketio==5.5.1