Major: Added Authentication Functionality, interfaces with backend;
Added new styles to HomePage & VideoPage; Added AuthContext to persist logged_in status;
This commit is contained in:
7
.gitignore
vendored
7
.gitignore
vendored
@@ -41,6 +41,7 @@ coverage/
|
|||||||
*.local
|
*.local
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
|
.vscode
|
||||||
.vscode/*
|
.vscode/*
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
.idea/
|
.idea/
|
||||||
@@ -57,7 +58,5 @@ hls/
|
|||||||
sockets/
|
sockets/
|
||||||
dev-env/
|
dev-env/
|
||||||
project_structure.txt
|
project_structure.txt
|
||||||
|
*.db
|
||||||
#Env
|
flask_session/
|
||||||
.env
|
|
||||||
web_server/frontend/.env
|
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Team Software Project</title>
|
<title>Team Software Project</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="h-screen">
|
||||||
<div id="root" class="bg-gradient-to-tr from-[#07001F] via-[#1D0085] to-[#CC00FF]"></div>
|
<div id="root" class="bg-gradient-to-tr from-[#07001F] via-[#1D0085] to-[#CC00AF]"></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -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 { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||||
import HomePage from "./pages/HomePage";
|
import HomePage, { PersonalisedHomePage } from "./pages/HomePage";
|
||||||
import VideoPage from "./pages/VideoPage";
|
import VideoPage from "./pages/VideoPage";
|
||||||
import LoginPage from "./pages/LoginPage";
|
import LoginPage from "./pages/LoginPage";
|
||||||
import SignupPage from "./pages/SignupPage";
|
import SignupPage from "./pages/SignupPage";
|
||||||
// import CheckoutPage from "./pages/CheckoutPage";
|
|
||||||
import NotFoundPage from "./pages/NotFoundPage";
|
import NotFoundPage from "./pages/NotFoundPage";
|
||||||
|
|
||||||
function App() {
|
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 (
|
return (
|
||||||
|
<AuthContext.Provider value={{ isLoggedIn, setIsLoggedIn }}>
|
||||||
|
<StreamsProvider>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<HomePage />} />
|
<Route
|
||||||
<Route path="/video" element={<VideoPage />} />
|
path="/"
|
||||||
|
element={isLoggedIn ? <PersonalisedHomePage /> : <HomePage />}
|
||||||
|
/>
|
||||||
|
<Route path="/:streamerName" element={<VideoPage />} />
|
||||||
<Route path="/login" element={<LoginPage />} />
|
<Route path="/login" element={<LoginPage />} />
|
||||||
<Route path="/signup" element={<SignupPage />} />
|
<Route path="/signup" element={<SignupPage />} />
|
||||||
{/* <Route path="/checkout" element={<CheckoutPage />} /> */}
|
|
||||||
|
|
||||||
{/* <Route path="/checkout" element={<CheckoutForm />} /> */}
|
|
||||||
{/* <Route path="/return" element={<Return />} /> */}
|
|
||||||
|
|
||||||
<Route path="*" element={<NotFoundPage />} />
|
<Route path="*" element={<NotFoundPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
</StreamsProvider>
|
||||||
|
</AuthContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,25 @@
|
|||||||
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;
|
||||||
|
|||||||
49
frontend/src/components/Auth/AuthModal.tsx
Normal file
49
frontend/src/components/Auth/AuthModal.tsx
Normal file
@@ -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<AuthModalProps> = ({ onClose }) => {
|
||||||
|
const [selectedTab, setSelectedTab] = useState<string>("Login");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="blurring-layer fixed z-10 inset-0 w-screen h-screen backdrop-blur-sm group-has-[input:focus]:backdrop-blur-[5px]"></div>
|
||||||
|
|
||||||
|
<div className="modal-container fixed inset-0 bg-black/30 has-[input:focus]:bg-black/40 flex flex-col items-center justify-around z-50 h-[70vh] m-auto min-w-[40vw] w-fit py-[50px] rounded-[2rem] transition-all">
|
||||||
|
<div className="login-methods w-full flex flex-row items-center justify-evenly">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="absolute top-[1rem] right-[2rem] text-[2rem] text-white hover:text-red-500 font-black hover:text-[2.5rem] transition-all"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
<ToggleButton
|
||||||
|
toggled={selectedTab === "Login"}
|
||||||
|
extraClasses="flex flex-col items-center px-8"
|
||||||
|
onClick={() => setSelectedTab("Login")}
|
||||||
|
>
|
||||||
|
<LogInIcon className="h-[40px] w-[40px] mr-1" />
|
||||||
|
Login
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton
|
||||||
|
toggled={selectedTab === "Register"}
|
||||||
|
extraClasses="flex flex-col items-center px-8"
|
||||||
|
onClick={() => setSelectedTab("Register")}
|
||||||
|
>
|
||||||
|
<UserIcon className="h-[40px] w-[40px] mr-1" />
|
||||||
|
Register
|
||||||
|
</ToggleButton>
|
||||||
|
</div>
|
||||||
|
{selectedTab === "Login" ? <LoginForm /> : <RegisterForm />}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AuthModal;
|
||||||
@@ -1 +1,127 @@
|
|||||||
// login.html
|
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<LoginFormData>({
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
});
|
||||||
|
const [errors, setErrors] = useState<FormErrors>({});
|
||||||
|
|
||||||
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
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 (
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
className="login-form h-[100%] flex flex-col h-full justify-evenly items-center"
|
||||||
|
>
|
||||||
|
{errors.general && (
|
||||||
|
<p className="text-red-500 text-sm text-center">{errors.general}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{errors.username && (
|
||||||
|
<p className="text-red-500 mt-3 text-sm">{errors.username}</p>
|
||||||
|
)}
|
||||||
|
<Input
|
||||||
|
name="username"
|
||||||
|
placeholder="Username"
|
||||||
|
value={formData.username}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
extraClasses={`${errors.username ? "border-red-500" : ""}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{errors.password && (
|
||||||
|
<p className="text-red-500 mt-3 text-sm">{errors.password}</p>
|
||||||
|
)}
|
||||||
|
<Input
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
placeholder="Password"
|
||||||
|
value={formData.password}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
extraClasses={`${errors.password ? "border-red-500" : ""}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button type="submit">Login</Button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoginForm;
|
||||||
|
|||||||
155
frontend/src/components/Auth/RegisterForm.tsx
Normal file
155
frontend/src/components/Auth/RegisterForm.tsx
Normal file
@@ -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<RegisterFormData>({
|
||||||
|
username: "",
|
||||||
|
email: "",
|
||||||
|
password: "",
|
||||||
|
confirmPassword: "",
|
||||||
|
});
|
||||||
|
const [errors, setErrors] = useState<FormErrors>({});
|
||||||
|
|
||||||
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
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 (
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
className="register-form h-[100%] flex flex-col h-full justify-evenly items-center"
|
||||||
|
>
|
||||||
|
{errors.username && (
|
||||||
|
<p className="text-red-500 mt-3 text-sm">{errors.username}</p>
|
||||||
|
)}
|
||||||
|
<Input
|
||||||
|
name="username"
|
||||||
|
placeholder="Username"
|
||||||
|
value={formData.username}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
extraClasses={`${errors.username ? "border-red-500" : ""}`}
|
||||||
|
/>
|
||||||
|
{errors.email && (
|
||||||
|
<p className="text-red-500 mt-3 text-sm">{errors.email}</p>
|
||||||
|
)}
|
||||||
|
<Input
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
placeholder="Email"
|
||||||
|
value={formData.email}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
extraClasses={`${errors.email ? "border-red-500" : ""}`}
|
||||||
|
/>
|
||||||
|
{errors.password && (
|
||||||
|
<p className="text-red-500 mt-3 text-sm">{errors.password}</p>
|
||||||
|
)}
|
||||||
|
<Input
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
placeholder="Password"
|
||||||
|
value={formData.password}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
extraClasses={`${errors.password ? "border-red-500" : ""}`}
|
||||||
|
/>
|
||||||
|
{errors.confirmPassword && (
|
||||||
|
<p className="text-red-500 mt-3 text-sm">{errors.confirmPassword}</p>
|
||||||
|
)}
|
||||||
|
<Input
|
||||||
|
name="confirmPassword"
|
||||||
|
type="password"
|
||||||
|
placeholder="Confirm Password"
|
||||||
|
value={formData.confirmPassword}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
extraClasses={`${errors.confirmPassword ? "border-red-500" : ""}`}
|
||||||
|
/>
|
||||||
|
<Button type="submit">Register</Button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RegisterForm;
|
||||||
@@ -11,7 +11,6 @@ const API_URL = import.meta.env.VITE_API_URL;
|
|||||||
// Initialize Stripe once
|
// Initialize Stripe once
|
||||||
const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY);
|
const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY);
|
||||||
|
|
||||||
|
|
||||||
export const Return: React.FC = () => {
|
export const Return: React.FC = () => {
|
||||||
const [status, setStatus] = useState<string | null>(null);
|
const [status, setStatus] = useState<string | null>(null);
|
||||||
const [customerEmail, setCustomerEmail] = useState("");
|
const [customerEmail, setCustomerEmail] = useState("");
|
||||||
@@ -69,18 +68,18 @@ const CheckoutForm: React.FC<CheckoutFormProps> = ({ onClose }) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="blurring-layer fixed z-10 inset-0 w-screen h-screen backdrop-blur-sm"></div>
|
<div className="blurring-layer fixed z-10 inset-0 w-screen h-screen backdrop-blur-sm"></div>
|
||||||
<div className="fixed inset-0 bg-black/30 flex items-center justify-center z-50 h-[70vh] m-auto w-fit py-[50px] px-[100px] rounded-[2rem]">
|
<div className="modal-container fixed inset-0 bg-black/30 flex items-center justify-center z-50 h-[70vh] m-auto w-fit py-[50px] px-[100px] rounded-[2rem]">
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="absolute top-[1rem] right-[3rem] text-[2rem] text-red-600 font-black hover:text-[2.5rem] transition-all"
|
className="absolute top-[1rem] right-[3rem] text-[2rem] text-white hover:text-red-500 font-black hover:text-[2.5rem] transition-all"
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
<div className="bg-white p-6 rounded-lg w-full max-w-2xl relative h-full rounded-[2rem]" style={{ width: "clamp(300px, 60vw, 800px)" }}>
|
|
||||||
<div
|
<div
|
||||||
id="checkout"
|
className="bg-white p-6 rounded-lg w-full max-w-2xl relative h-full rounded-[2rem]"
|
||||||
className="h-full overflow-auto min-w-[30vw]"
|
style={{ width: "clamp(300px, 60vw, 800px)" }}
|
||||||
>
|
>
|
||||||
|
<div id="checkout" className="h-full overflow-auto min-w-[30vw]">
|
||||||
<EmbeddedCheckoutProvider stripe={stripePromise} options={options}>
|
<EmbeddedCheckoutProvider stripe={stripePromise} options={options}>
|
||||||
<EmbeddedCheckout />
|
<EmbeddedCheckout />
|
||||||
</EmbeddedCheckoutProvider>
|
</EmbeddedCheckoutProvider>
|
||||||
|
|||||||
@@ -1,18 +1,50 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
interface ButtonProps {
|
interface ButtonProps {
|
||||||
title?: string;
|
type?: "button" | "submit" | "reset";
|
||||||
|
extraClasses?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Button: React.FC<ButtonProps> = ({
|
const Button: React.FC<ButtonProps> = ({
|
||||||
title = "Submit",
|
type = "button",
|
||||||
|
children = "Submit",
|
||||||
|
extraClasses = "",
|
||||||
onClick,
|
onClick,
|
||||||
}) => {
|
}) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type={type}
|
||||||
|
className={`${extraClasses} p-2 text-[1.5rem] text-white hover:text-purple-600 bg-black/30 hover:bg-black/80 rounded-md border border-gray-300 hover:border-purple-500 hover:border-b-4 hover:border-l-4 active:border-b-2 active:border-l-2 transition-all`}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ToggleButtonProps extends ButtonProps {
|
||||||
|
toggled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ToggleButton: React.FC<ToggleButtonProps> = ({
|
||||||
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<button className="underline bg-blue-600/30 p-4 rounded-[5rem] transition-all hover:text-[3.2rem]" onClick={onClick}>
|
<button
|
||||||
{title}
|
className={`${extraClasses} p-2 text-[1.5rem] text-white bg-black/30 rounded-[1rem] border border-gray-300 transition-all`}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
29
frontend/src/components/Layout/Input.tsx
Normal file
29
frontend/src/components/Layout/Input.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||||
|
extraClasses?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Input: React.FC<InputProps> = ({
|
||||||
|
name,
|
||||||
|
type = "text",
|
||||||
|
placeholder = "",
|
||||||
|
value = "",
|
||||||
|
extraClasses = "",
|
||||||
|
onChange = () => {},
|
||||||
|
...props // all other HTML input props
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
name={name}
|
||||||
|
type={type}
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
{...props}
|
||||||
|
className={`${extraClasses} p-2 rounded-[1rem] w-[20vw] focus:w-[120%] bg-black/40 border border-gray-300 focus:border-purple-500 focus:outline-purple-500 text-center text-white text-xl transition-all`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Input;
|
||||||
@@ -17,14 +17,14 @@ interface ListRowProps {
|
|||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
streams: StreamItem[];
|
streams: StreamItem[];
|
||||||
onStreamClick?: (streamId: string) => void;
|
onStreamClick?: (streamerId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Individual stream entry component
|
// Individual stream entry component
|
||||||
const ListEntry: React.FC<ListEntryProps> = ({ stream, onClick }) => {
|
const ListEntry: React.FC<ListEntryProps> = ({ stream, onClick }) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex flex-col bg-gray-800 rounded-lg overflow-hidden cursor-pointer hover:bg-gray-700 transition-colors"
|
className="flex flex-col bg-gray-800 rounded-lg overflow-hidden cursor-pointer hover:bg-gray-700 border border-gray-100 hover:border-purple-500 hover:border-b-4 hover:border-l-4 transition-all"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
<div className="relative w-full pt-[56.25%]">
|
<div className="relative w-full pt-[56.25%]">
|
||||||
@@ -55,7 +55,7 @@ const ListRow: React.FC<ListRowProps> = ({
|
|||||||
onStreamClick,
|
onStreamClick,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col space-y-4 py-6">
|
<div className="flex flex-col space-y-4 py-6 mx-4">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h2 className="text-2xl font-bold">{title}</h2>
|
<h2 className="text-2xl font-bold">{title}</h2>
|
||||||
<p className="text-gray-400">{description}</p>
|
<p className="text-gray-400">{description}</p>
|
||||||
@@ -65,7 +65,7 @@ const ListRow: React.FC<ListRowProps> = ({
|
|||||||
<ListEntry
|
<ListEntry
|
||||||
key={stream.id}
|
key={stream.id}
|
||||||
stream={stream}
|
stream={stream}
|
||||||
onClick={() => onStreamClick?.(stream.id)}
|
onClick={() => onStreamClick?.(stream.streamer)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,18 +1,25 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
const Logo: React.FC = () => {
|
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 (
|
return (
|
||||||
<div className="text-center text-[12vh] font-bold text-orange-500">
|
<Link to="/" className="cursor-pointer">
|
||||||
<h6 className="text-sm">Go on, have a...</h6>
|
<div className="logo group py-3 text-center text-[12vh] font-bold hover:scale-110 transition-all">
|
||||||
<div className="flex justify-center items-center space-x-6">
|
<h6 className="text-sm bg-gradient-to-br from-blue-400 via-green-500 to-indigo-500 font-black text-transparent bg-clip-text">
|
||||||
<span>G</span>
|
Go on, have a...
|
||||||
<span>A</span>
|
</h6>
|
||||||
<span>N</span>
|
<div className="flex w-fit min-w-[30vw] justify-evenly leading-none transition-all">
|
||||||
<span>D</span>
|
<span className={gradient}>G</span>
|
||||||
<span>E</span>
|
<span className={gradient}>A</span>
|
||||||
<span>R</span>
|
<span className={gradient}>N</span>
|
||||||
|
<span className={gradient}>D</span>
|
||||||
|
<span className={gradient}>E</span>
|
||||||
|
<span className={gradient}>R</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</Link>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,56 +1,90 @@
|
|||||||
import React from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import Logo from "./Logo";
|
import Logo from "./Logo";
|
||||||
import Button from "./Button";
|
import Button from "./Button";
|
||||||
import { Link } from "react-router-dom";
|
import Sidebar from "./Sidebar";
|
||||||
import { Search, User, LogIn } from "lucide-react";
|
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 {
|
const Navbar: React.FC = () => {
|
||||||
logged_in: boolean;
|
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<NavbarProps> = ({ logged_in }) => {
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col justify-around items-center h-[45vh]">
|
<div className="flex flex-col justify-around items-center h-[45vh]">
|
||||||
<Logo />
|
<Logo />
|
||||||
<div className="nav-buttons flex items-center space-x-4">
|
<Button
|
||||||
{logged_in ? (
|
extraClasses="absolute top-[20px] left-[20px] text-[1rem] flex items-center flex-nowrap"
|
||||||
<div>
|
onClick={() => (isLoggedIn ? handleLogout() : setShowAuthModal(true))}
|
||||||
<Link
|
|
||||||
to="/logout"
|
|
||||||
className="flex items-center text-gray-700 hover:text-purple-600"
|
|
||||||
>
|
>
|
||||||
<Button title="" />
|
{isLoggedIn ? (
|
||||||
</Link>
|
<>
|
||||||
</div>
|
<LogOutIcon className="h-15 w-15 mr-1" />
|
||||||
|
Logout
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<>
|
||||||
<Link
|
<LogInIcon className="h-15 w-15 mr-1" />
|
||||||
to="/login"
|
Login / Register
|
||||||
className="flex items-center text-gray-700 hover:text-purple-600"
|
</>
|
||||||
>
|
|
||||||
<LogIn className="h-5 w-5 mr-1" />
|
|
||||||
Login
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
to="/signup"
|
|
||||||
className="flex items-center text-gray-700 hover:text-purple-600"
|
|
||||||
>
|
|
||||||
<User className="h-5 w-5 mr-1" />
|
|
||||||
Sign Up
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
<Button title="Quick Settings" />
|
</Button>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="search-bar relative">
|
{isLoggedIn && (
|
||||||
<input
|
<>
|
||||||
|
<Button extraClasses="absolute top-[75px] left-[20px]">
|
||||||
|
<SidebarIcon className="h-15 w-15 mr-1" />
|
||||||
|
</Button>
|
||||||
|
<Sidebar />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
extraClasses="absolute top-[20px] right-[20px] text-[1rem] flex items-center flex-nowrap"
|
||||||
|
onClick={() => console.log("Settings")}
|
||||||
|
>
|
||||||
|
<SettingsIcon className="h-15 w-15 mr-1" />
|
||||||
|
Quick Settings
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="search-bar flex items-center">
|
||||||
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search..."
|
placeholder="Search..."
|
||||||
className="w-64 px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:border-purple-500"
|
extraClasses="pr-[30px] focus:outline-none focus:border-purple-500 focus:w-[30vw]"
|
||||||
/>
|
/>
|
||||||
<Search className="absolute right-3 top-2.5 h-5 w-5 text-gray-400" />
|
<SearchIcon className="-translate-x-[28px] top-1/2 h-6 w-6 text-white" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{showAuthModal && <AuthModal onClose={() => setShowAuthModal(false)} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
7
frontend/src/components/Layout/Sidebar.tsx
Normal file
7
frontend/src/components/Layout/Sidebar.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const Sidebar: React.FC = () => {
|
||||||
|
return <div></div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Sidebar;
|
||||||
18
frontend/src/context/AuthContext.tsx
Normal file
18
frontend/src/context/AuthContext.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { createContext, useContext } from "react";
|
||||||
|
|
||||||
|
interface AuthContextType {
|
||||||
|
isLoggedIn: boolean;
|
||||||
|
setIsLoggedIn: (value: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AuthContext = createContext<AuthContextType | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
export function useAuth() {
|
||||||
|
const context = useContext(AuthContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error("useAuth must be used within an AuthProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
42
frontend/src/context/StreamsContext.tsx
Normal file
42
frontend/src/context/StreamsContext.tsx
Normal file
@@ -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<StreamsContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export function StreamsProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const [featuredStreams, setFeaturedStreams] = useState<StreamItem[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch("/api/get_streams")
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data: StreamItem[]) => {
|
||||||
|
setFeaturedStreams(data);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StreamsContext.Provider value={{ featuredStreams, setFeaturedStreams }}>
|
||||||
|
{children}
|
||||||
|
</StreamsContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useStreams() {
|
||||||
|
const context = useContext(StreamsContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error("useStreams must be used within a StreamsProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
@@ -1,47 +1,23 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React from "react";
|
||||||
import Navbar from "../components/Layout/Navbar";
|
import Navbar from "../components/Layout/Navbar";
|
||||||
import ListRow from "../components/Layout/ListRow";
|
import ListRow from "../components/Layout/ListRow";
|
||||||
// import { data, Link } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useStreams } from "../context/StreamsContext";
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const HomePage: React.FC = () => {
|
const HomePage: React.FC = () => {
|
||||||
const [featuredStreams, setFeaturedStreams] = useState<StreamItem[]>([]);
|
const { featuredStreams } = useStreams();
|
||||||
const [loggedInStatus, setLoggedInStatus] = useState<boolean>(false);
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// ↓↓ runs twice when in development mode
|
const handleStreamClick = (streamerId: string) => {
|
||||||
useEffect(() => {
|
navigate(`/${streamerId}`);
|
||||||
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);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="home-page bg-repeat"
|
className="home-page bg-repeat"
|
||||||
style={{ backgroundImage: "url(/images/background-pattern.svg)" }}
|
style={{ backgroundImage: "url(/images/background-pattern.svg)" }}
|
||||||
>
|
>
|
||||||
<Navbar logged_in={loggedInStatus} />
|
<Navbar />
|
||||||
|
|
||||||
<ListRow
|
<ListRow
|
||||||
title="Live Now"
|
title="Live Now"
|
||||||
@@ -59,4 +35,35 @@ const HomePage: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const PersonalisedHomePage: React.FC = () => {
|
||||||
|
const { featuredStreams } = useStreams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleStreamClick = (streamerId: string) => {
|
||||||
|
navigate(`/${streamerId}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="home-page bg-repeat"
|
||||||
|
style={{ backgroundImage: "url(/images/background-pattern.svg)" }}
|
||||||
|
>
|
||||||
|
<Navbar />
|
||||||
|
|
||||||
|
<ListRow
|
||||||
|
title="Live Now - Recommended"
|
||||||
|
description="We think you might like these streams - Streamers recommended for you"
|
||||||
|
streams={featuredStreams}
|
||||||
|
onStreamClick={handleStreamClick}
|
||||||
|
/>
|
||||||
|
<ListRow
|
||||||
|
title="Followed Categories"
|
||||||
|
description="Current streams from your followed categories"
|
||||||
|
streams={featuredStreams}
|
||||||
|
onStreamClick={handleStreamClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default HomePage;
|
export default HomePage;
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
|
import Navbar from "../components/Layout/Navbar";
|
||||||
import Button from "../components/Layout/Button";
|
import Button from "../components/Layout/Button";
|
||||||
import CheckoutForm, { Return } from "../components/Checkout/CheckoutForm";
|
import CheckoutForm, { Return } from "../components/Checkout/CheckoutForm";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
|
||||||
const VideoPage: React.FC = () => {
|
const VideoPage: React.FC = () => {
|
||||||
const [showCheckout, setShowCheckout] = useState(false);
|
const [showCheckout, setShowCheckout] = useState(false);
|
||||||
const showReturn = window.location.search.includes("session_id");
|
const showReturn = window.location.search.includes("session_id");
|
||||||
|
const { streamerName } = useParams<{ streamerName: string }>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Prevent scrolling when checkout is open
|
||||||
if (showCheckout) {
|
if (showCheckout) {
|
||||||
document.body.style.overflow = "hidden";
|
document.body.style.overflow = "hidden";
|
||||||
} else {
|
} else {
|
||||||
@@ -17,17 +21,23 @@ const VideoPage: React.FC = () => {
|
|||||||
document.body.style.overflow = "unset";
|
document.body.style.overflow = "unset";
|
||||||
};
|
};
|
||||||
}, [showCheckout]);
|
}, [showCheckout]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (streamerName) {
|
||||||
|
// Fetch stream data for this streamer
|
||||||
|
console.log(`Loading stream for ${streamerName}`);
|
||||||
|
// fetch(`/api/streams/${streamerName}`)
|
||||||
|
}
|
||||||
|
}, [streamerName]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="text-5xl text-red-600 flex flex-col justify-evenly align-center h-screen text-center">
|
<div className="text-5xl text-red-600 flex flex-col justify-evenly align-center h-screen text-center">
|
||||||
|
<Navbar />
|
||||||
|
|
||||||
<h1>
|
<h1>
|
||||||
Hello! Welcome to the soon-to-be-awesome Video Page where you'll watch
|
Hello! Welcome to the soon-to-be-awesome Video Page where you'll watch
|
||||||
the best streams ever!
|
the best streams ever!
|
||||||
</h1>
|
</h1>
|
||||||
<Button
|
<Button onClick={() => setShowCheckout(true)}>Payment Screen Test</Button>
|
||||||
title="Payment Screen Test"
|
|
||||||
onClick={() => setShowCheckout(true)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{showCheckout && <CheckoutForm onClose={() => setShowCheckout(false)} />}
|
{showCheckout && <CheckoutForm onClose={() => setShowCheckout(false)} />}
|
||||||
{showReturn && <Return />}
|
{showReturn && <Return />}
|
||||||
|
|||||||
@@ -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 flask_session import Session
|
||||||
from blueprints.utils import logged_in_user
|
from blueprints.utils import logged_in_user
|
||||||
from flask_cors import CORS
|
from flask_cors import CORS
|
||||||
import os
|
import os
|
||||||
|
|
||||||
print("Environment variables:")
|
# csrf = CSRFProtect()
|
||||||
print(f"FLASK_SECRET_KEY: {os.getenv('FLASK_SECRET_KEY')}")
|
|
||||||
print(f"STRIPE_SECRET_KEY: {os.getenv('STRIPE_SECRET_KEY')}")
|
|
||||||
|
|
||||||
|
|
||||||
def create_app():
|
def create_app():
|
||||||
@@ -14,12 +13,17 @@ def create_app():
|
|||||||
app.config["SECRET_KEY"] = os.getenv("FLASK_SECRET_KEY")
|
app.config["SECRET_KEY"] = os.getenv("FLASK_SECRET_KEY")
|
||||||
app.config["SESSION_PERMANENT"] = False
|
app.config["SESSION_PERMANENT"] = False
|
||||||
app.config["SESSION_TYPE"] = "filesystem"
|
app.config["SESSION_TYPE"] = "filesystem"
|
||||||
#! ↓↓↓ For development purposes only
|
#! ↓↓↓ For development purposes only - Allow cross-origin requests for the frontend
|
||||||
CORS(app) # Allow cross-origin requests for the frontend
|
CORS(app, supports_credentials=True)
|
||||||
|
# csrf.init_app(app)
|
||||||
|
|
||||||
Session(app)
|
Session(app)
|
||||||
app.before_request(logged_in_user)
|
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():
|
with app.app_context():
|
||||||
from blueprints.authentication import auth_bp
|
from blueprints.authentication import auth_bp
|
||||||
from blueprints.main import main_bp
|
from blueprints.main import main_bp
|
||||||
|
|||||||
@@ -1,89 +1,158 @@
|
|||||||
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 werkzeug.security import generate_password_hash, check_password_hash
|
||||||
from forms import SignupForm, LoginForm
|
from forms import SignupForm, LoginForm
|
||||||
|
from flask_cors import cross_origin
|
||||||
from database.database import Database
|
from database.database import Database
|
||||||
from blueprints.utils import login_required
|
from blueprints.utils import login_required
|
||||||
|
|
||||||
auth_bp = Blueprint("auth", __name__)
|
auth_bp = Blueprint("auth", __name__)
|
||||||
|
|
||||||
@auth_bp.route("/signup", methods=["GET", "POST"])
|
|
||||||
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
|
|
||||||
|
|
||||||
# Store in database and hash to avoid exposing sensitive information
|
@auth_bp.route("/signup", methods=["POST"])
|
||||||
|
@cross_origin(supports_credentials=True)
|
||||||
|
def signup():
|
||||||
|
if not request.is_json:
|
||||||
|
return jsonify({"message": "Expected JSON data"}), 400
|
||||||
|
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
# 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()
|
db = Database()
|
||||||
cursor = db.create_connection()
|
cursor = db.create_connection()
|
||||||
|
|
||||||
# Check if user already exists to avoid duplicates
|
try:
|
||||||
dup_email = cursor.execute("""SELECT * FROM users
|
# Check for duplicate email/username
|
||||||
WHERE email = ?;""", (email,)).fetchone()
|
dup_email = cursor.execute(
|
||||||
dup_username = cursor.execute("""SELECT * FROM users
|
"SELECT * FROM users WHERE email = ?",
|
||||||
WHERE username = ?;""", (username,)).fetchone()
|
(email,)
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
dup_username = cursor.execute(
|
||||||
|
"SELECT * FROM users WHERE username = ?",
|
||||||
|
(username,)
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
if dup_email is not None:
|
if dup_email is not None:
|
||||||
form.email.errors.append("Email already taken.")
|
return jsonify({
|
||||||
elif dup_username is not None:
|
"account_created": False,
|
||||||
form.username.errors.append("Username already taken.")
|
"errors": {"email": "Email already taken"}
|
||||||
elif password != password2:
|
}), 400
|
||||||
form.password.errors.append("Passwords must match.")
|
|
||||||
else:
|
if dup_username is not None:
|
||||||
cursor.execute("""INSERT INTO users (username, password, email, num_followers, isPartenered, bio)
|
return jsonify({
|
||||||
VALUES (?, ?, ?, ?, ?, ?);""", (username, generate_password_hash(password), email, 0, 0, "This user does not have a Bio."))
|
"account_created": False,
|
||||||
|
"errors": {"username": "Username already taken"}
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
# 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()
|
db.commit_data()
|
||||||
return {"account_created": True}
|
|
||||||
|
|
||||||
|
# Create session for new user
|
||||||
# Close connection to prevent data leaks
|
|
||||||
db.close_connection()
|
|
||||||
|
|
||||||
return {"account_created": False}
|
|
||||||
|
|
||||||
@auth_bp.route("/login", methods=["GET", "POST"])
|
|
||||||
def login():
|
|
||||||
form = LoginForm()
|
|
||||||
if form.validate_on_submit():
|
|
||||||
# Retrieve data from the login form
|
|
||||||
username = form.username.data
|
|
||||||
password = form.username.data
|
|
||||||
|
|
||||||
# Compare with database
|
|
||||||
db = Database()
|
|
||||||
cursor = db.create_connection()
|
|
||||||
|
|
||||||
# Check if user exists so only users who have signed up can login
|
|
||||||
user_exists = cursor.execute("""SELECT * FROM users
|
|
||||||
WHERE username = ?;""", (username,)).fetchone()
|
|
||||||
|
|
||||||
if not user_exists:
|
|
||||||
form.username.errors.append("Incorrect username or password.")
|
|
||||||
db.close_connection()
|
|
||||||
|
|
||||||
# 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()
|
|
||||||
|
|
||||||
else:
|
|
||||||
# Create a new session to prevent users from exploiting horizontal access control
|
|
||||||
session.clear()
|
session.clear()
|
||||||
session["username"] = username
|
session["username"] = username
|
||||||
|
|
||||||
# Return to previous page if applicable
|
return jsonify({
|
||||||
next_page = request.args.get("next")
|
"account_created": True,
|
||||||
|
"message": "Account created successfully"
|
||||||
|
}), 201
|
||||||
|
|
||||||
# Otherwise return home
|
except Exception as e:
|
||||||
if not next_page:
|
print(f"Error during signup: {e}") # Log the error
|
||||||
next_page = url_for("app.index")
|
return jsonify({
|
||||||
|
"account_created": False,
|
||||||
|
"message": "Server error occurred: " + str(e)
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
finally:
|
||||||
|
db.close_connection()
|
||||||
|
|
||||||
|
|
||||||
|
@auth_bp.route("/login", methods=["POST"])
|
||||||
|
@cross_origin(supports_credentials=True)
|
||||||
|
def login():
|
||||||
|
if not request.is_json:
|
||||||
|
return jsonify({"message": "Expected JSON data"}), 400
|
||||||
|
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
# Extract data from request
|
||||||
|
username = data.get('username')
|
||||||
|
password = data.get('password')
|
||||||
|
|
||||||
|
# Basic server-side validation
|
||||||
|
if not all([username, password]):
|
||||||
|
return jsonify({
|
||||||
|
"logged_in": False,
|
||||||
|
"message": "Missing required fields"
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
db = Database()
|
||||||
|
cursor = db.create_connection()
|
||||||
|
|
||||||
|
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()
|
db.close_connection()
|
||||||
return {"logged_in": True}
|
|
||||||
|
|
||||||
return {"logged_in": False}
|
|
||||||
|
|
||||||
@auth_bp.route("/logout")
|
@auth_bp.route("/logout")
|
||||||
@login_required
|
@login_required
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
from flask import Blueprint, render_template
|
from flask import Blueprint, render_template, session, jsonify
|
||||||
|
|
||||||
main_bp = Blueprint("app", __name__)
|
main_bp = Blueprint("app", __name__)
|
||||||
|
|
||||||
## temp, showcasing HLS
|
# temp, showcasing HLS
|
||||||
|
|
||||||
|
|
||||||
@main_bp.route('/hls1/<stream_id>')
|
@main_bp.route('/hls1/<stream_id>')
|
||||||
def hls(stream_id):
|
def hls(stream_id):
|
||||||
stream_url = f"http://127.0.0.1:8080/hls/{stream_id}/index.m3u8"
|
stream_url = f"http://127.0.0.1:8080/hls/{stream_id}/index.m3u8"
|
||||||
|
|||||||
@@ -53,10 +53,7 @@ def get_login_status():
|
|||||||
"""
|
"""
|
||||||
Returns whether the user is logged in or not
|
Returns whether the user is logged in or not
|
||||||
"""
|
"""
|
||||||
username = session.get("username", None)
|
return jsonify(session.get("username") is not None)
|
||||||
if not username:
|
|
||||||
return {"logged_in": True}
|
|
||||||
return {"logged_in": False}
|
|
||||||
|
|
||||||
@user_bp.route('/authenticate_user')
|
@user_bp.route('/authenticate_user')
|
||||||
def authenticate_user():
|
def authenticate_user():
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
-- View all tables in the database
|
||||||
|
SELECT name FROM sqlite_master WHERE type='table';
|
||||||
|
|
||||||
DROP TABLE IF EXISTS users;
|
DROP TABLE IF EXISTS users;
|
||||||
CREATE TABLE users
|
CREATE TABLE users
|
||||||
(
|
(
|
||||||
|
|||||||
Reference in New Issue
Block a user