This commit is contained in:
2025-02-11 20:57:54 +00:00
16 changed files with 377 additions and 151 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -25,7 +25,7 @@ const AuthModal: React.FC<AuthModalProps> = ({ onClose }) => {
const authSwitch = () => { const authSwitch = () => {
const formMap: { [key: string]: JSX.Element} = { const formMap: { [key: string]: JSX.Element} = {
Login: <LoginForm onSubmit={(handleSubmit)}/>, Login: <LoginForm onSubmit={(handleSubmit)} onForgotPassword={() => setSelectedTab("Forgot")}/>,
Register: <RegisterForm onSubmit={(handleSubmit)}/>, Register: <RegisterForm onSubmit={(handleSubmit)}/>,
Forgot: <ForgotPasswordForm onSubmit={(handleSubmit)}/> Forgot: <ForgotPasswordForm onSubmit={(handleSubmit)}/>
}; };
@@ -78,13 +78,7 @@ const AuthModal: React.FC<AuthModalProps> = ({ onClose }) => {
Register Register
</ToggleButton> </ToggleButton>
<ToggleButton
toggled={selectedTab==="Forgot"}
extraClasses="flex flex-col items-center px-8 duration-250 transition-transform hover:translate-y-[-50px] z-[9001]"
onClick={() => setSelectedTab("Forgot")}>
<ForgotIcon />
Forgot Password
</ToggleButton>
</div> </div>
<div <div
className="container fixed inset-0 flex flex-col items-center justify-around z-[9999] className="container fixed inset-0 flex flex-col items-center justify-around z-[9999]

View File

@@ -1,7 +1,9 @@
import React, { useState } from "react"; import React, { useState } from "react";
import Input from "../Layout/Input"; import Input from "../Layout/Input";
import Button from "../Layout/Button"; import Button, { ToggleButton } from "../Layout/Button";
import { useAuth } from "../../context/AuthContext"; import { useAuth } from "../../context/AuthContext";
import GoogleLogin from "./OAuth";
import { CircleHelp as ForgotIcon} from "lucide-react";
interface LoginFormData { interface LoginFormData {
username: string; username: string;
@@ -17,9 +19,10 @@ interface FormErrors {
//Speed up border animation //Speed up border animation
interface SubmitProps { interface SubmitProps {
onSubmit: () => void; onSubmit: () => void;
onForgotPassword: () => void;
} }
const LoginForm: React.FC<SubmitProps> = ({ onSubmit }) => { const LoginForm: React.FC<SubmitProps> = ({ onSubmit, onForgotPassword }) => {
const { setIsLoggedIn } = useAuth(); const { setIsLoggedIn } = useAuth();
const [formData, setFormData] = useState<LoginFormData>({ const [formData, setFormData] = useState<LoginFormData>({
@@ -97,44 +100,54 @@ const LoginForm: React.FC<SubmitProps> = ({ onSubmit }) => {
}; };
return ( return (
<> <>
<div className="h-[100%] flex flex-col justify-evenly items-center"> <div className="h-[100%] flex flex-col justify-evenly items-center">
<h1 className="text-white text-lg"> Login </h1> <h1 className="text-white text-[2.5em] font-[800]"> Login </h1>
<form <form
onSubmit={handleSubmit} onSubmit={handleSubmit}
id="login-form" id="login-form"
className="h-[100%] flex flex-col justify-evenly items-center" className="h-[100%] flex flex-col items-center"
> >
{errors.general && ( {errors.general && (
<p className="text-red-500 text-sm text-center">{errors.general}</p> <p className="text-red-500 text-sm text-center">{errors.general}</p>
)} )}
{errors.username && ( {errors.username && (
<p className="text-red-500 mt-3 text-sm">{errors.username}</p> <p className="text-red-500 mt-3 text-sm">{errors.username}</p>
)} )}
<Input <Input
name="username" name="username"
placeholder="Username" placeholder="Username"
value={formData.username} value={formData.username}
onChange={handleInputChange} onChange={handleInputChange}
extraClasses={`${errors.username ? "border-red-500" : ""}`} extraClasses={`${errors.username ? "border-red-500" : ""}`}
/> />
{errors.password && ( {errors.password && (
<p className="text-red-500 mt-3 text-sm">{errors.password}</p> <p className="text-red-500 mt-3 text-sm">{errors.password}</p>
)} )}
<Input <Input
name="password" name="password"
type="password" type="password"
placeholder="Password" placeholder="Password"
value={formData.password} value={formData.password}
onChange={handleInputChange} onChange={handleInputChange}
extraClasses={`${errors.password ? "border-red-500" : ""}`} extraClasses={`${errors.password ? "border-red-500" : ""}`}
/> />
<button
type="button"
className="flex items-center justify-start bg-white text-gray-600 font-semibold py-1 px-2 rounded shadow-md w-[220px] hover:bg-gray-100 active:bg-gray-200"
onClick={onForgotPassword}>
<ForgotIcon className="flex flex-row justify-content "/>
Forgot Password
</button>
<Button type="submit">Login</Button> <Button type="submit">Login</Button>
</form> </form>
</div> <div className="flex flex-col flex-items justify-evenly items-center w-full h-[5em]">
<GoogleLogin />
</div>
</div>
</> </>
); );
}; };

View File

@@ -0,0 +1,28 @@
import { useEffect } from "react";
export default function GoogleLogin() {
const handleLoginClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
window.location.href = "/api/login/google";
};
return (
<div className="w-full">
<div className="flex flex-wrap justify-center w-full">
<button
onClick={handleLoginClick}
//w-full basis-[90%] (% size of original container)
className="flex w-full max-w-[19em] basis-[90%] flex-grow flex-shrink items-center justify-start bg-white text-gray-600
font-semibold py-[0.15em] pl-[0.3em] pr-[0.6em] rounded shadow-md flex-grow flex-shrink
hover:bg-gray-100 active:bg-gray-200 sm:max-w-[18em] mx-[1em]">
<img
src="../../../images/icons/google-icon.png"
alt="Google logo"
className="w-[2em] h-[2em] mr-2"
/>
<span className="flex-grow">Sign in with Google</span>
</button>
</div>
</div>
);
}

View File

@@ -4,12 +4,11 @@ import Button from "./Button";
import Sidebar from "./Sidebar"; import Sidebar from "./Sidebar";
import { Sidebar as SidebarIcon } from "lucide-react"; import { Sidebar as SidebarIcon } from "lucide-react";
import { import {
Search as SearchIcon,
LogIn as LogInIcon, LogIn as LogInIcon,
LogOut as LogOutIcon, LogOut as LogOutIcon,
Settings as SettingsIcon, Settings as SettingsIcon,
} from "lucide-react"; } from "lucide-react";
import Input from "./Input"; import SearchBar from "./SearchBar";
import AuthModal from "../Auth/AuthModal"; import AuthModal from "../Auth/AuthModal";
import { useAuthModal } from "../../hooks/useAuthModal"; import { useAuthModal } from "../../hooks/useAuthModal";
import { useAuth } from "../../context/AuthContext"; import { useAuth } from "../../context/AuthContext";
@@ -109,15 +108,7 @@ const Navbar: React.FC<NavbarProps> = ({ variant = "default" }) => {
<QuickSettings /> <QuickSettings />
</div> </div>
<div id="search-bar" className="flex items-center"> <SearchBar />
<Input
type="text"
placeholder="Search..."
extraClasses="pr-[30px] focus:outline-none focus:border-purple-500 focus:w-[30vw]"
/>
<SearchIcon className="-translate-x-[28px] top-1/2 h-6 w-6 text-white" />
</div>
{showAuthModal && <AuthModal onClose={() => setShowAuthModal(false)} />} {showAuthModal && <AuthModal onClose={() => setShowAuthModal(false)} />}
</div> </div>

View File

@@ -0,0 +1,63 @@
import React, { useState, useEffect } from "react";
import Input from "./Input";
import { Search as SearchIcon } from "lucide-react";
const SearchBar: React.FC = () => {
const [searchQuery, setSearchQuery] = useState("");
const [debouncedQuery, setDebouncedQuery] = useState(searchQuery);
// Debounce the search query
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedQuery(searchQuery);
}, 500); // Wait 500ms after user stops typing
return () => clearTimeout(timer);
}, [searchQuery]);
// Perform search when debounced query changes
useEffect(() => {
if (debouncedQuery.trim()) {
const fetchSearchResults = async () => {
try {
const response = await fetch("/api/search", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ query: debouncedQuery }), // <-- Fixed payload
});
const data = await response.json();
console.log("Search results:", data);
// Handle the search results here
} catch (error) {
console.error("Error performing search:", error);
}
};
fetchSearchResults(); // Call the async function
}
}, [debouncedQuery]);
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchQuery(e.target.value);
};
return (
<div id="search-bar" className="flex items-center">
<Input
type="text"
placeholder="Search..."
value={searchQuery}
onChange={handleSearchChange}
extraClasses="pr-[30px] focus:outline-none focus:border-purple-500 focus:w-[30vw]"
/>
<SearchIcon className="-translate-x-[28px] top-1/2 h-6 w-6 text-white" />
</div>
);
};
export default SearchBar;

View File

@@ -9,9 +9,11 @@ from blueprints.stripe import stripe_bp
from blueprints.user import user_bp from blueprints.user import user_bp
from blueprints.streams import stream_bp from blueprints.streams import stream_bp
from blueprints.chat import chat_bp from blueprints.chat import chat_bp
from blueprints.oauth import oauth_bp, init_oauth
from blueprints.socket import socketio from blueprints.socket import socketio
from celery import Celery from celery import Celery
from celery_tasks import celery_init_app from celery_tasks import celery_init_app#
from blueprints.search_bar import search_bp
from os import getenv from os import getenv
@@ -24,10 +26,13 @@ def create_app():
And setup web sockets to be used throughout the project. And setup web sockets to be used throughout the project.
""" """
app = Flask(__name__) app = Flask(__name__)
app.config["SERVER_NAME"] = "127.0.0.1:8080"
app.config["SECRET_KEY"] = getenv("FLASK_SECRET_KEY") app.config["SECRET_KEY"] = getenv("FLASK_SECRET_KEY")
app.config["SESSION_PERMANENT"] = False app.config["SESSION_PERMANENT"] = False
app.config["SESSION_TYPE"] = "filesystem" app.config["SESSION_TYPE"] = "filesystem"
app.config["PROPAGATE_EXCEPTIONS"] = True app.config["PROPAGATE_EXCEPTIONS"] = True
app.config['GOOGLE_CLIENT_ID'] = getenv("GOOGLE_CLIENT_ID")
app.config['GOOGLE_CLIENT_SECRET'] = getenv("GOOGLE_CLIENT_SECRET")
app.config.from_mapping( app.config.from_mapping(
CELERY=dict( CELERY=dict(
@@ -47,6 +52,7 @@ def create_app():
Session(app) Session(app)
app.before_request(logged_in_user) app.before_request(logged_in_user)
init_oauth(app)
# adds in error handlers # adds in error handlers
register_error_handlers(app) register_error_handlers(app)
@@ -63,6 +69,8 @@ def create_app():
app.register_blueprint(user_bp) app.register_blueprint(user_bp)
app.register_blueprint(stream_bp) app.register_blueprint(stream_bp)
app.register_blueprint(chat_bp) app.register_blueprint(chat_bp)
app.register_blueprint(oauth_bp)
app.register_blueprint(search_bp)
socketio.init_app(app) socketio.init_app(app)

View File

@@ -81,18 +81,13 @@ def signup():
# Create new user once input is validated # Create new user once input is validated
db.execute( db.execute(
"""INSERT INTO users """INSERT INTO users
(username, password, email, num_followers, stream_key, is_partnered, bio, current_stream_title, current_selected_category_id) (username, password, email, stream_key)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""", VALUES (?, ?, ?, ?)""",
( (
username, username,
generate_password_hash(password), generate_password_hash(password),
email, email,
0, token_hex(32)
token_hex(32),
0,
"This user does not have a Bio.",
"My Stream",
None
) )
) )

View File

@@ -32,6 +32,9 @@ def admin_required(view):
return wrapped_view return wrapped_view
def register_error_handlers(app): def register_error_handlers(app):
"""
Default reponses to status codes
"""
error_responses = { error_responses = {
400: "Bad Request", 400: "Bad Request",
403: "Forbidden", 403: "Forbidden",

View File

@@ -1,60 +1,102 @@
from authlib.integrations.flask_client import OAuth, OAuthError from authlib.integrations.flask_client import OAuth, OAuthError
from flask import Blueprint, url_for, jsonify, session from flask import Blueprint, jsonify, session, redirect
from blueprints.user import get_session_info_email from blueprints.user import get_session_info_email
from database.database import Database
from secrets import token_hex, token_urlsafe
from random import randint
oauth_bp = Blueprint("oauth", __name__) oauth_bp = Blueprint("oauth", __name__)
google = None
def init_oauth(app): def init_oauth(app):
oauth = OAuth(app) oauth = OAuth(app)
global google
google = oauth.register( google = oauth.register(
'google', 'google',
client_id=app.config['GOOGLE_CLIENT_ID'], client_id=app.config['GOOGLE_CLIENT_ID'],
client_secret=app.config['GOOGLE_CLIENT_SECRET'], client_secret=app.config['GOOGLE_CLIENT_SECRET'],
authorize_url='https://accounts.google.com/o/oauth2/auth', authorize_url='https://accounts.google.com/o/oauth2/auth',
authorize_params=None, access_token_url='https://oauth2.googleapis.com/token',
access_token_url='https://accounts.google.com/o/oauth2/token', client_kwargs={'scope': 'openid profile email'},
access_token_params=None, api_base_url='https://www.googleapis.com/oauth2/v1/',
refresh_token_url=None, userinfo_endpoint='https://openidconnect.googleapis.com/v1/userinfo',
redirect_uri=url_for('google.google_auth', _external=True), server_metadata_url='https://accounts.google.com/.well-known/openid-configuration',
scope='openid profile email', redirect_uri="http://127.0.0.1:8080/api/google_auth"
) )
@oauth_bp.route('/login/google')
def login_google():
"""
Redirects to Google's OAuth authorization page
"""
# Creates nonce to be sent
session["nonce"] = token_urlsafe(16)
return google.authorize_redirect(
'http://127.0.0.1:8080/api/google_auth',
nonce=session['nonce']
)
@oauth_bp.route('/login/google') @oauth_bp.route('/google_auth')
def login_google(): def google_auth():
""" """
Redirects to Google's OAuth authorization page Receives token from Google OAuth and authenticates it to validate login
""" """
return google.authorize_redirect(url_for('google.google_auth', _external=True)) try:
token = google.authorize_access_token()
@oauth_bp.route('/google_auth') # Verifies token as well as nonce
def google_auth(): nonce = session.pop('nonce', None)
try: if not nonce:
token = google.authorize_access_token() return jsonify({'error': 'Missing nonce in session'}), 400
user = google.parse_id_token(token)
user = google.parse_id_token(token, nonce=nonce)
# check if email exists else create a database entry print(user, flush=True)
user_email = user.get("email")
# Check if email exists to login else create a database entry
user_email = user.get("email")
user_data = get_session_info_email(user_email)
if not user_data:
with Database() as db:
# Generates a new username for the user
for _ in range(1000000):
username = user.get("given_name") + str(randint(1, 1000000))
taken = db.fetchone("""
SELECT * FROM users
WHERE username = ?
""", (username,))
if not taken:
break
db.execute(
"""INSERT INTO users
(username, email, stream_key)
VALUES (?, ?, ?)""",
(
username,
user_email,
token_hex(32),
)
)
user_data = get_session_info_email(user_email) user_data = get_session_info_email(user_email)
session.clear() session.clear()
session["username"] = user_data["username"] session["username"] = user_data["username"]
session["user_id"] = user_data["user_id"] session["user_id"] = user_data["user_id"]
return jsonify({ # TODO: redirect back to original page user started on, or other pages based on success failure of login
'message': 'User authenticated successfully', return redirect("http://127.0.0.1:8080/")
})
except OAuthError as e:
# Handle OAuth errors like failed authentication or invalid token
return jsonify({
'message': 'Authentication failed',
'error': str(e)
}), 400
except Exception as e: except OAuthError as e:
# Handle other unexpected errors return jsonify({
return jsonify({ 'message': 'Authentication failed',
'message': 'An unexpected error occurred', 'error': str(e)
'error': str(e) }), 400
}), 500
except Exception as e:
return jsonify({
'message': 'An unexpected error occurred',
'error': str(e)
}), 500

View File

@@ -1,15 +1,19 @@
from flask import Blueprint, jsonify from flask import Blueprint, jsonify, request
from database.database import Database from database.database import Database
from utils.utils import sanitize
search_bp = Blueprint("search", __name__) search_bp = Blueprint("search", __name__)
@search_bp.route("/search/<string:query>", methods=["GET", "POST"]) @search_bp.route("/search", methods=["POST"])
def search_results(query: str): def search_results():
""" """
Return the most similar search results Return the most similar search results
This is the main route that displays a subsection of each search topic This is the main route that displays a subsection of each search topic
""" """
data = request.get_json()
query = sanitize(data["query"])
# Create the connection to the database # Create the connection to the database
db = Database() db = Database()
db.create_connection() db.create_connection()
@@ -17,63 +21,86 @@ def search_results(query: str):
# Get the most accurate search results # Get the most accurate search results
# 3 categories # 3 categories
categories = db.fetchall(""" categories = db.fetchall("""
SELECT bm25(category_fts), rank, f.category_id, f.category_name SELECT bm25(category_fts) AS score, c.category_id, c.category_name
FROM categories AS c FROM categories AS c
INNER JOIN category_fts AS f ON c.category_id = f.category_id INNER JOIN category_fts AS f ON c.category_id = f.category_id
WHERE category_fts MATCH ? WHERE f.category_name LIKE '%' || ? || '%'
LIMIT 3; ORDER BY score ASC
LIMIT 3;
""", (query,)) """, (query,))
# 3 users # 3 users
users = db.fetchall(""" users = db.fetchall("""
SELECT bm25(user_fts), rank, f.user_id, f.username, f.is_live SELECT bm25(user_fts) AS score, u.user_id, u.username, u.is_live
FROM users u FROM users AS u
INNER JOIN user_fts f ON u.user_id = f.user_id INNER JOIN user_fts AS f ON u.user_id = f.user_id
WHERE user_fts MATCH ? WHERE f.username LIKE '%' || ? || '%'
LIMIT 3; ORDER BY score ASC
LIMIT 3;
""", (query,)) """, (query,))
# 3 streams # 3 streams
streams = db.fetchall(""" streams = db.fetchall("""
SELECT bm25(stream_fts), rank, f.user_id, f.title, f.num_viewers, f.category_id SELECT bm25(stream_fts) AS score, s.user_id, s.title, s.num_viewers, s.category_id
FROM streams s FROM streams AS s
INNER JOIN stream_fts f ON s.user_id = f.user_id INNER JOIN stream_fts AS f ON s.user_id = f.user_id
WHERE stream_fts MATCH ? WHERE f.title LIKE '%' || ? || '%'
LIMIT 3; ORDER BY score ASC
LIMIT 3;
""", (query,)) """, (query,))
db.close_connection() db.close_connection()
print(query, streams, users, categories, flush=True)
return jsonify({"categories": categories, "users": users, "streams": streams}) return jsonify({"categories": categories, "users": users, "streams": streams})
@search_bp.route("/search/categories/<string:query>", methods=["GET", "POST"]) @search_bp.route("/search/categories", methods=["GET", "POST"])
def search_categories(query: str): def search_categories():
"""
Display all the results for categories from the specified user query
"""
# Receive the query data from the user
data = request.get_json()
query = sanitize(data["query"])
# Create the connection to the database # Create the connection to the database
db = Database() db = Database()
db.create_connection() db.create_connection()
# Fetch the ranked data and send to JSON to be displayed
categories = db.fetchall(""" categories = db.fetchall("""
SELECT bm25(category_fts), rank, f.category_id, f.category_name SELECT bm25(category_fts) AS score, c.category_id, c.category_name
FROM categories AS c FROM categories AS c
INNER JOIN category_fts AS f ON c.category_id = f.category_id INNER JOIN category_fts AS f ON c.category_id = f.category_id
WHERE category_fts MATCH ?; WHERE f.category_name LIKE '%' || ? || '%'
ORDER BY score ASC;
""", (query,)) """, (query,))
db.close_connection() db.close_connection()
return jsonify({"categories": categories}) return jsonify({"categories": categories})
@search_bp.route("/search/users/<string:query>", methods=["GET", "POST"]) @search_bp.route("/search/users", methods=["GET", "POST"])
def search_users(query: str): def search_users():
"""
Display all the results for users from the specified user query
"""
# Receive the query data from the user
data = request.get_json()
query = sanitize(data["query"])
# Create the connection to the database # Create the connection to the database
db = Database() db = Database()
db.create_connection() db.create_connection()
# Fetch the ranked data and send to JSON to be displayed
users = db.fetchall(""" users = db.fetchall("""
SELECT bm25(user_fts), rank, f.user_id, f.username, f.is_live SELECT bm25(user_fts) AS score, u.user_id, u.username, u.is_live
FROM users u FROM users AS u
INNER JOIN user_fts f ON u.user_id = f.user_id INNER JOIN user_fts AS f ON u.user_id = f.user_id
WHERE user_fts MATCH ?; WHERE f.username LIKE '%' || ? || '%'
ORDER BY score ASC;
""", (query,)) """, (query,))
db.close_connection() db.close_connection()
@@ -81,17 +108,26 @@ def search_users(query: str):
return jsonify({"users": users}) return jsonify({"users": users})
@search_bp.route("/search/streams/<string:query>", methods=["GET", "POST"]) @search_bp.route("/search/streams", methods=["GET", "POST"])
def search_streams(query: str): def search_streams():
"""
Display all the results for streams from the specified user query
"""
# Receive the query data from the user
data = request.get_json()
query = sanitize(data["query"])
# Create the connection to the database # Create the connection to the database
db = Database() db = Database()
db.create_connection() db.create_connection()
# Fetch the ranked data and send to JSON to be displayed
streams = db.fetchall(""" streams = db.fetchall("""
SELECT bm25(stream_fts), rank, f.user_id, f.title, f.num_viewers, f.category_id SELECT bm25(stream_fts) AS score, s.user_id, s.title, s.num_viewers, s.category_id
FROM streams s FROM streams AS s
INNER JOIN stream_fts f ON s.user_id = f.user_id INNER JOIN stream_fts AS f ON s.user_id = f.user_id
WHERE stream_fts MATCH ?; WHERE f.title LIKE '%' || ? || '%'
ORDER BY score ASC;
""", (query,)) """, (query,))
db.close_connection() db.close_connection()

View File

@@ -22,6 +22,17 @@ def user_data(username: str):
return jsonify(data) return jsonify(data)
## Subscription Routes ## Subscription Routes
@login_required
@user_bp.route('/user/subscribe/<int:streamer_id>')
def user_subscribe(streamer_id):
"""
Given a streamer subscribes as user
"""
#TODO: Keep this route secure so only webhooks from Stripe payment can trigger it
user_id = session.get("user_id")
subscribe(user_id, streamer_id)
return jsonify({"status": True})
@login_required @login_required
@user_bp.route('/user/subscription/<int:subscribed_id>') @user_bp.route('/user/subscription/<int:subscribed_id>')
def user_subscribed(subscribed_id: int): def user_subscribed(subscribed_id: int):
@@ -42,6 +53,9 @@ def user_subscription_expiration(subscribed_id: int):
user_id = session.get("user_id") user_id = session.get("user_id")
remaining_time = subscription_expiration(user_id, subscribed_id) remaining_time = subscription_expiration(user_id, subscribed_id)
# Remove any expired subscriptions from the table
if remaining_time == 0:
delete_subscription(user_id, subscribed_id)
return jsonify({"remaining_time": remaining_time}) return jsonify({"remaining_time": remaining_time})

View File

@@ -3,13 +3,13 @@ CREATE TABLE users
( (
user_id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER PRIMARY KEY AUTOINCREMENT,
username VARCHAR(50) NOT NULL, username VARCHAR(50) NOT NULL,
password VARCHAR(256) NOT NULL, password VARCHAR(256),
email VARCHAR(128) NOT NULL, email VARCHAR(128) NOT NULL,
num_followers INTEGER NOT NULL, num_followers INTEGER NOT NULL DEFAULT 0,
stream_key VARCHAR(60) NOT NULL, stream_key VARCHAR(60) NOT NULL,
is_partnered BOOLEAN NOT NULL DEFAULT 0, is_partnered BOOLEAN NOT NULL DEFAULT 0,
is_live BOOLEAN NOT NULL DEFAULT 0, is_live BOOLEAN NOT NULL DEFAULT 0,
bio VARCHAR(1024), bio VARCHAR(1024) DEFAULT 'This user does not have a Bio.',
current_stream_title VARCHAR(100), current_stream_title VARCHAR(100),
current_selected_category_id INTEGER current_selected_category_id INTEGER

View File

@@ -27,4 +27,5 @@ gevent-websocket
flask-oauthlib==0.9.6 flask-oauthlib==0.9.6
celery==5.2.3 celery==5.2.3
redis==5.2.1 redis==5.2.1
python-dateutil python-dateutil
Authlib==1.4.1

View File

@@ -1,6 +1,6 @@
from database.database import Database from database.database import Database
from typing import Optional, List from typing import Optional, List
from datetime import datetime from datetime import datetime, timedelta
from dateutil import parser from dateutil import parser
def get_user_id(username: str) -> Optional[int]: def get_user_id(username: str) -> Optional[int]:
@@ -22,7 +22,7 @@ def get_username(user_id: str) -> Optional[str]:
with Database() as db: with Database() as db:
data = db.fetchone(""" data = db.fetchone("""
SELECT username SELECT username
FROM user FROM users
WHERE user_id = ? WHERE user_id = ?
""", (user_id,)) """, (user_id,))
return data['username'] if data else None return data['username'] if data else None
@@ -31,10 +31,10 @@ def get_session_info_email(email: str) -> dict:
""" """
Returns username and user_id given email Returns username and user_id given email
""" """
with Database as db: with Database() as db:
session_info = db.fetchone(""" session_info = db.fetchone("""
SELECT user_id, username SELECT user_id, username
FROM user FROM users
WHERE email = ? WHERE email = ?
""", (email,)) """, (email,))
return session_info return session_info
@@ -109,6 +109,39 @@ def unfollow(user_id: int, followed_id: int):
""", (user_id, followed_id)) """, (user_id, followed_id))
return {"success": True} return {"success": True}
def subscribe(user_id: int, streamer_id: int):
"""
Subscribes user_id to streamer_id
"""
# If user is already subscribed then extend the expiration date else create a new entry
with Database() as db:
existing = db.fetchone("""
SELECT expires
FROM subscribes
WHERE user_id = ? AND subscribed_id = ?
""", (user_id, streamer_id))
if existing:
db.execute("""
UPDATE subscribes SET expires = expires + ?
WHERE user_id = ? AND subscribed_id = ?
""", (timedelta(days=30), user_id, streamer_id))
else:
db.execute("""
INSERT INTO subscribes
(user_id, subscribed_id, since, expires)
VALUES (?,?,?,?)
""", (user_id, streamer_id, datetime.now(), datetime.now() + timedelta(days=30)))
def delete_subscription(user_id: int, subscribed_id: int):
"""
Deletes a subscription entry given user_id and streamer_id
"""
with Database() as db:
db.execute("""
DELETE FROM subscribes
WHERE user_id = ? AND subscribed_id = ?
""", (user_id, subscribed_id))
def subscription_expiration(user_id: int, subscribed_id: int) -> int: def subscription_expiration(user_id: int, subscribed_id: int) -> int:
""" """

View File

@@ -37,7 +37,7 @@ def get_most_popular_category() -> Optional[List[dict]]:
return category return category
def sanitize(user_input: str, input_type="username") -> str: def sanitize(user_input: str, input_type="default") -> str:
""" """
Sanitizes user input based on the specified input type. Sanitizes user input based on the specified input type.
@@ -63,6 +63,11 @@ def sanitize(user_input: str, input_type="username") -> str:
"min_length": 8, "min_length": 8,
"max_length": 256, "max_length": 256,
}, },
"default": {
"pattern": r"^[\S]+$", # Non-whitespace characters only
"min_length": 1,
"max_length": 50,
},
} }
# Get the validation rules for the specified type # Get the validation rules for the specified type