diff --git a/.gitignore b/.gitignore index 5e396ef..e2f15b0 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,7 @@ coverage/ *.local # Editor directories and files +.vscode .vscode/* !.vscode/extensions.json .idea/ @@ -57,7 +58,5 @@ hls/ sockets/ dev-env/ project_structure.txt - -#Env -.env -web_server/frontend/.env +*.db +flask_session/ diff --git a/frontend/index.html b/frontend/index.html index e6ceec8..113daa2 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -6,8 +6,8 @@ Team Software Project - -
+ +
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 009b0ca..f15f93e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,27 +1,46 @@ +import { useState, useEffect } from "react"; +import { AuthContext } from "./context/AuthContext"; +import { StreamsProvider } from "./context/StreamsContext"; import { BrowserRouter, Routes, Route } from "react-router-dom"; -import HomePage from "./pages/HomePage"; +import HomePage, { PersonalisedHomePage } from "./pages/HomePage"; import VideoPage from "./pages/VideoPage"; import LoginPage from "./pages/LoginPage"; import SignupPage from "./pages/SignupPage"; -// import CheckoutPage from "./pages/CheckoutPage"; import NotFoundPage from "./pages/NotFoundPage"; function App() { + const [isLoggedIn, setIsLoggedIn] = useState(false); + + useEffect(() => { + fetch("/api/get_login_status") + .then((response) => response.json()) + .then((loggedIn) => { + setIsLoggedIn(loggedIn); + }) + .catch((error) => { + console.error("Error fetching login status:", error); + setIsLoggedIn(false); + }); + }, []); + return ( - - - } /> - } /> - } /> - } /> - {/* } /> */} + + + + + : } + /> + } /> + } /> + } /> - {/* } /> */} - {/* } /> */} - - } /> - - + } /> + + + + ); } diff --git a/frontend/src/assets/styles/index.css b/frontend/src/assets/styles/index.css index fc89180..d4af09f 100644 --- a/frontend/src/assets/styles/index.css +++ b/frontend/src/assets/styles/index.css @@ -20,6 +20,25 @@ 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 { font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; diff --git a/frontend/src/components/Auth/AuthModal.tsx b/frontend/src/components/Auth/AuthModal.tsx new file mode 100644 index 0000000..640f3c6 --- /dev/null +++ b/frontend/src/components/Auth/AuthModal.tsx @@ -0,0 +1,49 @@ +import React, { useState } from "react"; +import { ToggleButton } from "../Layout/Button"; +import { LogIn as LogInIcon, User as UserIcon } from "lucide-react"; +import LoginForm from "./LoginForm"; +import RegisterForm from "./RegisterForm"; + +interface AuthModalProps { + onClose: () => void; +} + +const AuthModal: React.FC = ({ onClose }) => { + const [selectedTab, setSelectedTab] = useState("Login"); + + return ( + <> +
+ +
+
+ + setSelectedTab("Login")} + > + + Login + + setSelectedTab("Register")} + > + + Register + +
+ {selectedTab === "Login" ? : } +
+ + ); +}; + +export default AuthModal; diff --git a/frontend/src/components/Auth/LoginForm.tsx b/frontend/src/components/Auth/LoginForm.tsx index be6587b..2392510 100644 --- a/frontend/src/components/Auth/LoginForm.tsx +++ b/frontend/src/components/Auth/LoginForm.tsx @@ -1 +1,127 @@ -// login.html \ No newline at end of file +import React, { useState } from "react"; +import Input from "../Layout/Input"; +import Button from "../Layout/Button"; +import { useAuth } from "../../context/AuthContext"; + +interface LoginFormData { + username: string; + password: string; +} + +interface FormErrors { + username?: string; + password?: string; + general?: string; // For general authentication errors +} + +const LoginForm: React.FC = () => { + const { setIsLoggedIn } = useAuth(); + + const [formData, setFormData] = useState({ + username: "", + password: "", + }); + const [errors, setErrors] = useState({}); + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData((prev) => ({ + ...prev, + [name]: value, + })); + }; + + const validateForm = (): boolean => { + const newErrors: FormErrors = {}; + + // Check for empty fields + Object.keys(formData).forEach((key) => { + if (!formData[key as keyof LoginFormData]) { + newErrors[key as keyof FormErrors] = "This field is required"; + } + }); + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (validateForm()) { + try { + const response = await fetch("/api/login", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + body: JSON.stringify(formData), + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.message || "Login failed"); + } + + if (data.logged_in) { + //TODO: Handle successful login (e.g., redirect to home page) + console.log("Login successful"); + setIsLoggedIn(true); + window.location.reload(); + } else { + // Handle authentication errors + if (data.errors) { + setErrors({ + general: "Invalid username or password", + }); + } + } + } catch (error) { + console.error("Login error:", error); + setErrors({ + general: "An error occurred during login. Please try again.", + }); + } + } + }; + + return ( +
+ {errors.general && ( +

{errors.general}

+ )} + + {errors.username && ( +

{errors.username}

+ )} + + + {errors.password && ( +

{errors.password}

+ )} + + + +
+ ); +}; + +export default LoginForm; diff --git a/frontend/src/components/Auth/RegisterForm.tsx b/frontend/src/components/Auth/RegisterForm.tsx new file mode 100644 index 0000000..162e991 --- /dev/null +++ b/frontend/src/components/Auth/RegisterForm.tsx @@ -0,0 +1,155 @@ +import React, { useState } from "react"; +import Input from "../Layout/Input"; +import Button from "../Layout/Button"; +import { useAuth } from "../../context/AuthContext"; + +interface RegisterFormData { + username: string; + email: string; + password: string; + confirmPassword: string; +} + +interface FormErrors { + username?: string; + email?: string; + password?: string; + confirmPassword?: string; +} + +const RegisterForm: React.FC = () => { + const { setIsLoggedIn } = useAuth(); + + const [formData, setFormData] = useState({ + username: "", + email: "", + password: "", + confirmPassword: "", + }); + const [errors, setErrors] = useState({}); + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData((prev) => ({ + ...prev, + [name]: value, + })); + }; + + const validateForm = (): boolean => { + const newErrors: FormErrors = {}; + + // Check for empty fields + Object.keys(formData).forEach((key) => { + if (!formData[key as keyof RegisterFormData]) { + newErrors[key as keyof FormErrors] = "This field is required"; + } + }); + + // Check password match + if (formData.password !== formData.confirmPassword) { + newErrors.confirmPassword = "Passwords do not match"; + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (validateForm()) { + try { + const response = await fetch("/api/signup", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + body: JSON.stringify(formData), + }); + console.log(`sending data: ${JSON.stringify(formData)}`); + const data = await response.json(); + + if (!response.ok) { + throw new Error( + data.message || `Registration failed. ${response.body}` + ); + } + + if (data.account_created) { + //TODO Handle successful registration (e.g., redirect or show success message) + console.log("Registration Successful! Account created successfully"); + setIsLoggedIn(true); + window.location.reload(); + } else { + // Handle validation errors from server + const serverErrors: FormErrors = {}; + if (data.errors) { + Object.entries(data.errors).forEach(([field, message]) => { + serverErrors[field as keyof FormErrors] = message as string; + }); + setErrors(serverErrors); + } + } + } catch (error) { + console.error("Registration error:", error); + //TODO Show user-friendly error message via Alert component maybe + } + } + }; + + return ( +
+ {errors.username && ( +

{errors.username}

+ )} + + {errors.email && ( +

{errors.email}

+ )} + + {errors.password && ( +

{errors.password}

+ )} + + {errors.confirmPassword && ( +

{errors.confirmPassword}

+ )} + + +
+ ); +}; + +export default RegisterForm; diff --git a/frontend/src/components/Checkout/CheckoutForm.tsx b/frontend/src/components/Checkout/CheckoutForm.tsx index 2140c4a..a1903f8 100644 --- a/frontend/src/components/Checkout/CheckoutForm.tsx +++ b/frontend/src/components/Checkout/CheckoutForm.tsx @@ -11,7 +11,6 @@ const API_URL = import.meta.env.VITE_API_URL; // Initialize Stripe once const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY); - export const Return: React.FC = () => { const [status, setStatus] = useState(null); const [customerEmail, setCustomerEmail] = useState(""); @@ -69,18 +68,18 @@ const CheckoutForm: React.FC = ({ onClose }) => { return ( <>
-
+
-
-
+
+
@@ -91,4 +90,4 @@ const CheckoutForm: React.FC = ({ onClose }) => { ); }; -export default CheckoutForm; \ No newline at end of file +export default CheckoutForm; diff --git a/frontend/src/components/Layout/Button.tsx b/frontend/src/components/Layout/Button.tsx index 51cb01e..9390319 100644 --- a/frontend/src/components/Layout/Button.tsx +++ b/frontend/src/components/Layout/Button.tsx @@ -1,18 +1,50 @@ import React from "react"; interface ButtonProps { - title?: string; + type?: "button" | "submit" | "reset"; + extraClasses?: string; + children?: React.ReactNode; onClick?: () => void; } const Button: React.FC = ({ - title = "Submit", + type = "button", + children = "Submit", + extraClasses = "", onClick, }) => { + return ( + + ); +}; + +interface ToggleButtonProps extends ButtonProps { + toggled?: boolean; +} + +export const ToggleButton: React.FC = ({ + children = "Toggle", + extraClasses = "", + onClick, + toggled = false, +}) => { + toggled + ? (extraClasses += " cursor-default bg-purple-600") + : (extraClasses += + " cursor-pointer hover:text-purple-600 hover:bg-black/80 hover:border-purple-500 hover:border-b-4 hover:border-l-4"); return (
-
); diff --git a/frontend/src/components/Layout/Input.tsx b/frontend/src/components/Layout/Input.tsx new file mode 100644 index 0000000..6b63b1e --- /dev/null +++ b/frontend/src/components/Layout/Input.tsx @@ -0,0 +1,29 @@ +import React from "react"; + +interface InputProps extends React.InputHTMLAttributes { + extraClasses?: string; +} + +const Input: React.FC = ({ + name, + type = "text", + placeholder = "", + value = "", + extraClasses = "", + onChange = () => {}, + ...props // all other HTML input props +}) => { + return ( + + ); +}; + +export default Input; diff --git a/frontend/src/components/Layout/ListRow.tsx b/frontend/src/components/Layout/ListRow.tsx index 2537a64..a7d2fbe 100644 --- a/frontend/src/components/Layout/ListRow.tsx +++ b/frontend/src/components/Layout/ListRow.tsx @@ -17,20 +17,20 @@ interface ListRowProps { title: string; description: string; streams: StreamItem[]; - onStreamClick?: (streamId: string) => void; + onStreamClick?: (streamerId: string) => void; } // Individual stream entry component const ListEntry: React.FC = ({ stream, onClick }) => { return (
{stream.thumbnail ? ( {stream.title} @@ -55,7 +55,7 @@ const ListRow: React.FC = ({ onStreamClick, }) => { return ( -
+

{title}

{description}

@@ -65,7 +65,7 @@ const ListRow: React.FC = ({ onStreamClick?.(stream.id)} + onClick={() => onStreamClick?.(stream.streamer)} /> ))}
diff --git a/frontend/src/components/Layout/Logo.tsx b/frontend/src/components/Layout/Logo.tsx index 040b558..83b84aa 100644 --- a/frontend/src/components/Layout/Logo.tsx +++ b/frontend/src/components/Layout/Logo.tsx @@ -1,18 +1,25 @@ import React from "react"; +import { Link } from "react-router-dom"; const Logo: React.FC = () => { + 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"; return ( -
-
Go on, have a...
-
- G - A - N - D - E - R + +
+
+ Go on, have a... +
+
+ G + A + N + D + E + R +
-
+ ); }; diff --git a/frontend/src/components/Layout/Navbar.tsx b/frontend/src/components/Layout/Navbar.tsx index bf6f2c5..29a4e44 100644 --- a/frontend/src/components/Layout/Navbar.tsx +++ b/frontend/src/components/Layout/Navbar.tsx @@ -1,56 +1,90 @@ -import React from "react"; +import React, { useState, useEffect } from "react"; import Logo from "./Logo"; import Button from "./Button"; -import { Link } from "react-router-dom"; -import { Search, User, LogIn } from "lucide-react"; +import Sidebar from "./Sidebar"; +import { Sidebar as SidebarIcon } from "lucide-react"; +import { + Search as SearchIcon, + LogIn as LogInIcon, + LogOut as LogOutIcon, + Settings as SettingsIcon, +} from "lucide-react"; +import Input from "./Input"; +import AuthModal from "../Auth/AuthModal"; +import { useAuth } from "../../context/AuthContext"; -interface NavbarProps { - logged_in: boolean; -} +const Navbar: React.FC = () => { + const [showAuthModal, setShowAuthModal] = useState(false); + const { isLoggedIn } = useAuth(); + + useEffect(() => { + if (showAuthModal) { + document.body.style.overflow = "hidden"; + } else { + document.body.style.overflow = "unset"; + } + return () => { + document.body.style.overflow = "unset"; + }; + }, [showAuthModal]); + + const handleLogout = () => { + console.log("Logging out..."); + fetch("/api/logout") + .then((response) => response.json()) + .then((data) => { + console.log(data); + window.location.reload(); + }); + }; -const Navbar: React.FC = ({ logged_in }) => { return (
-
- {logged_in ? ( -
- -
+
+ -
- + + + + )} + + + +
+ - +
+ + {showAuthModal && setShowAuthModal(false)} />}
); }; diff --git a/frontend/src/components/Layout/Sidebar.tsx b/frontend/src/components/Layout/Sidebar.tsx new file mode 100644 index 0000000..9c31bb7 --- /dev/null +++ b/frontend/src/components/Layout/Sidebar.tsx @@ -0,0 +1,7 @@ +import React from "react"; + +const Sidebar: React.FC = () => { + return
; +}; + +export default Sidebar; diff --git a/frontend/src/context/AuthContext.tsx b/frontend/src/context/AuthContext.tsx new file mode 100644 index 0000000..47d675a --- /dev/null +++ b/frontend/src/context/AuthContext.tsx @@ -0,0 +1,18 @@ +import { createContext, useContext } from "react"; + +interface AuthContextType { + isLoggedIn: boolean; + setIsLoggedIn: (value: boolean) => void; +} + +export const AuthContext = createContext( + undefined +); + +export function useAuth() { + const context = useContext(AuthContext); + if (context === undefined) { + throw new Error("useAuth must be used within an AuthProvider"); + } + return context; +} diff --git a/frontend/src/context/StreamsContext.tsx b/frontend/src/context/StreamsContext.tsx new file mode 100644 index 0000000..e6957c5 --- /dev/null +++ b/frontend/src/context/StreamsContext.tsx @@ -0,0 +1,42 @@ +import { createContext, useContext, useState, useEffect } from "react"; + +interface StreamItem { + id: number; + title: string; + streamer: string; + viewers: number; + thumbnail?: string; +} + +interface StreamsContextType { + featuredStreams: StreamItem[]; + setFeaturedStreams: (streams: StreamItem[]) => void; +} + +const StreamsContext = createContext(undefined); + +export function StreamsProvider({ children }: { children: React.ReactNode }) { + const [featuredStreams, setFeaturedStreams] = useState([]); + + useEffect(() => { + fetch("/api/get_streams") + .then((response) => response.json()) + .then((data: StreamItem[]) => { + setFeaturedStreams(data); + }); + }, []); + + return ( + + {children} + + ); +} + +export function useStreams() { + const context = useContext(StreamsContext); + if (context === undefined) { + throw new Error("useStreams must be used within a StreamsProvider"); + } + return context; +} diff --git a/frontend/src/pages/CheckoutPage.tsx b/frontend/src/pages/CheckoutPage.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index 2d7a621..f1d90aa 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -1,47 +1,23 @@ -import React, { useState, useEffect } from "react"; +import React from "react"; import Navbar from "../components/Layout/Navbar"; import ListRow from "../components/Layout/ListRow"; -// import { data, Link } from "react-router-dom"; - -const handleStreamClick = (streamId: string) => { - // Handle navigation to stream page - console.log(`Navigating to stream ${streamId}`); -}; - -interface StreamItem { - id: number; - title: string; - streamer: string; - viewers: number; - thumbnail?: string; -} +import { useNavigate } from "react-router-dom"; +import { useStreams } from "../context/StreamsContext"; const HomePage: React.FC = () => { - const [featuredStreams, setFeaturedStreams] = useState([]); - const [loggedInStatus, setLoggedInStatus] = useState(false); + const { featuredStreams } = useStreams(); + const navigate = useNavigate(); - // ↓↓ runs twice when in development mode - useEffect(() => { - fetch("/api/get_login_status") - .then((response) => response.json()) - .then((data) => { - setLoggedInStatus(data); - console.log(data); - }); - fetch("/api/get_streams") - .then((response) => response.json()) - .then((data: StreamItem[]) => { - setFeaturedStreams(data); - console.log(data); - }); - }, []); + const handleStreamClick = (streamerId: string) => { + navigate(`/${streamerId}`); + }; return (
- + { ); }; +export const PersonalisedHomePage: React.FC = () => { + const { featuredStreams } = useStreams(); + const navigate = useNavigate(); + + const handleStreamClick = (streamerId: string) => { + navigate(`/${streamerId}`); + }; + + return ( +
+ + + + +
+ ); +}; + export default HomePage; diff --git a/frontend/src/pages/VideoPage.tsx b/frontend/src/pages/VideoPage.tsx index c6b5cc4..49c4eff 100644 --- a/frontend/src/pages/VideoPage.tsx +++ b/frontend/src/pages/VideoPage.tsx @@ -1,12 +1,16 @@ import React, { useState, useEffect } from "react"; +import Navbar from "../components/Layout/Navbar"; import Button from "../components/Layout/Button"; import CheckoutForm, { Return } from "../components/Checkout/CheckoutForm"; +import { useParams } from "react-router-dom"; const VideoPage: React.FC = () => { const [showCheckout, setShowCheckout] = useState(false); const showReturn = window.location.search.includes("session_id"); + const { streamerName } = useParams<{ streamerName: string }>(); useEffect(() => { + // Prevent scrolling when checkout is open if (showCheckout) { document.body.style.overflow = "hidden"; } else { @@ -17,17 +21,23 @@ const VideoPage: React.FC = () => { document.body.style.overflow = "unset"; }; }, [showCheckout]); + useEffect(() => { + if (streamerName) { + // Fetch stream data for this streamer + console.log(`Loading stream for ${streamerName}`); + // fetch(`/api/streams/${streamerName}`) + } + }, [streamerName]); return (
+ +

Hello! Welcome to the soon-to-be-awesome Video Page where you'll watch the best streams ever!

- {showCheckout && setShowCheckout(false)} />} {showReturn && } diff --git a/web_server/blueprints/__init__.py b/web_server/blueprints/__init__.py index 3aad87a..794b94a 100644 --- a/web_server/blueprints/__init__.py +++ b/web_server/blueprints/__init__.py @@ -1,12 +1,11 @@ -from flask import Flask +from flask import Flask, jsonify +# from flask_wtf.csrf import CSRFProtect, generate_csrf from flask_session import Session from blueprints.utils import logged_in_user from flask_cors import CORS import os -print("Environment variables:") -print(f"FLASK_SECRET_KEY: {os.getenv('FLASK_SECRET_KEY')}") -print(f"STRIPE_SECRET_KEY: {os.getenv('STRIPE_SECRET_KEY')}") +# csrf = CSRFProtect() def create_app(): @@ -14,12 +13,17 @@ def create_app(): app.config["SECRET_KEY"] = os.getenv("FLASK_SECRET_KEY") app.config["SESSION_PERMANENT"] = False app.config["SESSION_TYPE"] = "filesystem" - #! ↓↓↓ For development purposes only - CORS(app) # Allow cross-origin requests for the frontend + #! ↓↓↓ For development purposes only - Allow cross-origin requests for the frontend + CORS(app, supports_credentials=True) + # csrf.init_app(app) Session(app) app.before_request(logged_in_user) + # @app.route('/csrf-token') + # def get_csrf_token(): + # return jsonify({'csrf_token': generate_csrf()}), 200 + with app.app_context(): from blueprints.authentication import auth_bp from blueprints.main import main_bp @@ -33,4 +37,4 @@ def create_app(): app.register_blueprint(user_bp) app.register_blueprint(stream_bp) - return app \ No newline at end of file + return app diff --git a/web_server/blueprints/authentication.py b/web_server/blueprints/authentication.py index 526e5db..f861520 100644 --- a/web_server/blueprints/authentication.py +++ b/web_server/blueprints/authentication.py @@ -1,92 +1,161 @@ -from flask import Blueprint, render_template, session, request, url_for, redirect, g +from flask import Blueprint, session, request, url_for, redirect, g, jsonify from werkzeug.security import generate_password_hash, check_password_hash from forms import SignupForm, LoginForm +from flask_cors import cross_origin from database.database import Database from blueprints.utils import login_required auth_bp = Blueprint("auth", __name__) -@auth_bp.route("/signup", methods=["GET", "POST"]) + +@auth_bp.route("/signup", methods=["POST"]) +@cross_origin(supports_credentials=True) def signup(): - form = SignupForm() - if form.validate_on_submit(): - # Retrieve data from the sign up form - username = form.username.data - email = form.email.data - password = form.password.data - password2 = form.password2.data + if not request.is_json: + return jsonify({"message": "Expected JSON data"}), 400 - # Store in database and hash to avoid exposing sensitive information - db = Database() - cursor = db.create_connection() + data = request.get_json() - # Check if user already exists to avoid duplicates - dup_email = cursor.execute("""SELECT * FROM users - WHERE email = ?;""", (email,)).fetchone() - dup_username = cursor.execute("""SELECT * FROM users - WHERE username = ?;""", (username,)).fetchone() + # Extract data from request + username = data.get('username') + email = data.get('email') + password = data.get('password') + + # Basic server-side validation + if not all([username, email, password]): + return jsonify({ + "account_created": False, + "message": "Missing required fields" + }), 400 + + db = Database() + cursor = db.create_connection() + + try: + # Check for duplicate email/username + dup_email = cursor.execute( + "SELECT * FROM users WHERE email = ?", + (email,) + ).fetchone() + + dup_username = cursor.execute( + "SELECT * FROM users WHERE username = ?", + (username,) + ).fetchone() if dup_email is not None: - form.email.errors.append("Email already taken.") - elif dup_username is not None: - form.username.errors.append("Username already taken.") - elif password != password2: - form.password.errors.append("Passwords must match.") - else: - cursor.execute("""INSERT INTO users (username, password, email, num_followers, isPartenered, bio) - VALUES (?, ?, ?, ?, ?, ?);""", (username, generate_password_hash(password), email, 0, 0, "This user does not have a Bio.")) - db.commit_data() - return {"account_created": True} + return jsonify({ + "account_created": False, + "errors": {"email": "Email already taken"} + }), 400 + if dup_username is not None: + return jsonify({ + "account_created": False, + "errors": {"username": "Username already taken"} + }), 400 - # Close connection to prevent data leaks + # Create new user + cursor.execute( + """INSERT INTO users + (username, password, email, num_followers, isPartenered, bio) + VALUES (?, ?, ?, ?, ?, ?)""", + ( + username, + generate_password_hash(password), + email, + 0, + 0, + "This user does not have a Bio." + ) + ) + db.commit_data() + + # Create session for new user + session.clear() + session["username"] = username + + return jsonify({ + "account_created": True, + "message": "Account created successfully" + }), 201 + + except Exception as e: + print(f"Error during signup: {e}") # Log the error + return jsonify({ + "account_created": False, + "message": "Server error occurred: " + str(e) + }), 500 + + finally: db.close_connection() - return {"account_created": False} -@auth_bp.route("/login", methods=["GET", "POST"]) +@auth_bp.route("/login", methods=["POST"]) +@cross_origin(supports_credentials=True) def login(): - form = LoginForm() - if form.validate_on_submit(): - # Retrieve data from the login form - username = form.username.data - password = form.username.data + if not request.is_json: + return jsonify({"message": "Expected JSON data"}), 400 - # Compare with database - db = Database() - cursor = db.create_connection() + data = request.get_json() - # Check if user exists so only users who have signed up can login - user_exists = cursor.execute("""SELECT * FROM users - WHERE username = ?;""", (username,)).fetchone() + # Extract data from request + username = data.get('username') + password = data.get('password') - if not user_exists: - form.username.errors.append("Incorrect username or password.") - db.close_connection() + # Basic server-side validation + if not all([username, password]): + return jsonify({ + "logged_in": False, + "message": "Missing required fields" + }), 400 - # Check is hashed passwords match to verify the user logging in - elif not check_password_hash(user_exists["password"], password): - form.username.errors.append("Incorrect username or password.") - db.close_connection() + db = Database() + cursor = db.create_connection() - else: - # Create a new session to prevent users from exploiting horizontal access control - session.clear() - session["username"] = username + try: + # Check if user exists + user = cursor.execute( + "SELECT * FROM users WHERE username = ?", + (username,) + ).fetchone() + + if not user: + return jsonify({ + "logged_in": False, + "errors": {"general": "Invalid username or password"} + }), 401 + + # Verify password + if not check_password_hash(user["password"], password): + return jsonify({ + "logged_in": False, + "errors": {"general": "Invalid username or password"} + }), 401 + + # Set up session + session.clear() + session["username"] = username + + return jsonify({ + "logged_in": True, + "message": "Login successful", + "username": username + }), 200 + + except Exception as e: + print(f"Error during login: {e}") # Log the error + return jsonify({ + "logged_in": False, + "message": "Server error occurred" + }), 500 + + finally: + db.close_connection() - # Return to previous page if applicable - next_page = request.args.get("next") - # Otherwise return home - if not next_page: - next_page = url_for("app.index") - db.close_connection() - return {"logged_in": True} - - return {"logged_in": False} - @auth_bp.route("/logout") @login_required def logout(): session.clear() - return {"logged_in": False} \ No newline at end of file + return {"logged_in": False} diff --git a/web_server/blueprints/main.py b/web_server/blueprints/main.py index b16913f..f43cb21 100644 --- a/web_server/blueprints/main.py +++ b/web_server/blueprints/main.py @@ -1,17 +1,19 @@ -from flask import Blueprint, render_template +from flask import Blueprint, render_template, session, jsonify main_bp = Blueprint("app", __name__) -## temp, showcasing HLS +# temp, showcasing HLS + + @main_bp.route('/hls1/') def hls(stream_id): stream_url = f"http://127.0.0.1:8080/hls/{stream_id}/index.m3u8" return render_template("video.html", video_url=stream_url) -#-------------------------------------------------------- +# -------------------------------------------------------- -#TODO Route for saving uploaded thumbnails to database, serving these images to the frontend upon request: →→→ @main_bp.route('/images/') \n def serve_image(filename): ←←← \ No newline at end of file +# TODO Route for saving uploaded thumbnails to database, serving these images to the frontend upon request: →→→ @main_bp.route('/images/') \n def serve_image(filename): ←←← diff --git a/web_server/blueprints/user.py b/web_server/blueprints/user.py index fe73306..ad7738e 100644 --- a/web_server/blueprints/user.py +++ b/web_server/blueprints/user.py @@ -53,10 +53,7 @@ def get_login_status(): """ Returns whether the user is logged in or not """ - username = session.get("username", None) - if not username: - return {"logged_in": True} - return {"logged_in": False} + return jsonify(session.get("username") is not None) @user_bp.route('/authenticate_user') def authenticate_user(): diff --git a/web_server/database/schema.sql b/web_server/database/schema.sql index c07e901..ffc4b89 100644 --- a/web_server/database/schema.sql +++ b/web_server/database/schema.sql @@ -1,3 +1,6 @@ +-- View all tables in the database +SELECT name FROM sqlite_master WHERE type='table'; + DROP TABLE IF EXISTS users; CREATE TABLE users (