commit c109930ae0ff448c34be0bc1c5085896f5f00770 Author: Alfie King Date: Mon Apr 21 16:36:33 2025 +0100 ver 1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..07346f7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.env +.venv +app.db \ No newline at end of file diff --git a/flask_session/2029240f6d1128be89ddc32729463129 b/flask_session/2029240f6d1128be89ddc32729463129 new file mode 100644 index 0000000..8b04914 Binary files /dev/null and b/flask_session/2029240f6d1128be89ddc32729463129 differ diff --git a/flask_session/95ca1eb92b3063fb8d5ea8eef8e9be3f b/flask_session/95ca1eb92b3063fb8d5ea8eef8e9be3f new file mode 100644 index 0000000..1f39869 Binary files /dev/null and b/flask_session/95ca1eb92b3063fb8d5ea8eef8e9be3f differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2114d63 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +flask +flask-session \ No newline at end of file diff --git a/src/__pycache__/database.cpython-313.pyc b/src/__pycache__/database.cpython-313.pyc new file mode 100644 index 0000000..a96c34f Binary files /dev/null and b/src/__pycache__/database.cpython-313.pyc differ diff --git a/src/database.py b/src/database.py new file mode 100644 index 0000000..94c9093 --- /dev/null +++ b/src/database.py @@ -0,0 +1,544 @@ +import sqlite3, logging, os + + +# Configure logging +logger = logging.getLogger(__name__) + + +class Database: + def __init__(self, db_name): + # Initialize the database connection + logger.info("Initializing database connection...") + self.connection = sqlite3.connect(db_name, check_same_thread=False) + self.cursor = self.connection.cursor() + logger.info("Database connection established.") + + # Create users table if it doesn't exist + logger.info("Creating users table if it doesn't exist...") + self.cursor.execute(''' + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL UNIQUE, + password TEXT NOT NULL, + session TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + logger.info("Users table created.") + + # Create boards table if it doesn't exist + logger.info("Creating boards table if it doesn't exist...") + self.cursor.execute(''' + CREATE TABLE IF NOT EXISTS boards ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + owner_id INTEGER NOT NULL, + description TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (owner_id) REFERENCES users (id) ON DELETE SET NULL + ) + ''') + logger.info("Boards table created.") + + # Create posts table if it doesn't exist + logger.info("Creating posts table if it doesn't exist...") + self.cursor.execute(''' + CREATE TABLE IF NOT EXISTS posts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + board_id INTEGER NOT NULL, + content TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + refence INTEGER, + FOREIGN KEY (refence) REFERENCES posts (id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE SET NULL, + FOREIGN KEY (board_id) REFERENCES boards (id) ON DELETE CASCADE + ) + ''') + logger.info("Posts table created.") + + + def close(self): + # Close the database connection + logger.info("Closing database connection...") + self.connection.commit() + self.connection.close() + logger.info("Database connection closed.") + + + def create_user(self, username, password): + logger.info(f"Creating user: {username}") + + # Check if the user already exists + logger.info(f"Checking if user {username} already exists...") + self.cursor.execute(''' + SELECT * FROM users WHERE username = ? + ''', (username,)) + user = self.cursor.fetchone() + + if user: + logger.warning(f"User {username} already exists.") + return False + logger.info(f"User {username} does not exist.") + + # Create a new user + self.cursor.execute(''' + INSERT INTO users (username, password) + VALUES (?, ?) + ''', (username, password)) + + self.connection.commit() + logger.info(f"User {username} created.") + return True + + + def get_user(self, username): + # Get user by username + logger.info(f"Getting user {username}...") + self.cursor.execute(''' + SELECT * FROM users WHERE username = ? + ''', (username,)) + user = self.cursor.fetchone() + + if user: + logger.info(f"User {username} found.") + return user + logger.warning(f"User {username} not found.") + return None + + + def create_board(self, name, description, owner_id): + logger.info(f"Creating board: {name}") + + # Check if the board already exists + logger.info(f"Checking if board {name} already exists...") + self.cursor.execute(''' + SELECT * FROM boards WHERE name = ? + ''', (name,)) + board = self.cursor.fetchone() + + if board: + logger.warning(f"Board {name} already exists.") + return False + logger.info(f"Board {name} does not exist.") + + # Check if the owner exists + logger.info(f"Checking if owner {owner_id} exists...") + self.cursor.execute(''' + SELECT * FROM users WHERE id = ? + ''', (owner_id,)) + owner = self.cursor.fetchone() + + if not owner: + logger.warning(f"Owner {owner_id} does not exist.") + return False + logger.info(f"Owner {owner_id} exists.") + + # Create a new board + self.cursor.execute(''' + INSERT INTO boards (name, owner_id, description) + VALUES (?, ?, ?) + ''', (name, owner_id, description)) + + self.connection.commit() + logger.info(f"Board {name} created.") + return True + + + def get_board(self, name): + # Get board by name + logger.info(f"Getting board {name}...") + self.cursor.execute(''' + SELECT * FROM boards WHERE name = ? + ''', (name,)) + board = self.cursor.fetchone() + + if board: + logger.info(f"Board {name} found.") + return board + logger.warning(f"Board {name} not found.") + return None + + + def create_post(self, user_id, board_id, content, ref=None): + logger.info(f"Creating post for user {user_id} in board {board_id}...") + + # Check if the user exists + logger.info(f"Checking if user {user_id} exists...") + self.cursor.execute(''' + SELECT * FROM users WHERE id = ? + ''', (user_id,)) + user = self.cursor.fetchone() + + if not user: + logger.warning(f"User {user_id} does not exist.") + return False + logger.info(f"User {user_id} exists.") + + # Check if the board exists + logger.info(f"Checking if board {board_id} exists...") + self.cursor.execute(''' + SELECT * FROM boards WHERE id = ? + ''', (board_id,)) + board = self.cursor.fetchone() + + if not board: + logger.warning(f"Board {board_id} does not exist.") + return False + logger.info(f"Board {board_id} exists.") + + # Check if the reference post exists + if ref is not None: + logger.info(f"Checking if reference post {ref} exists...") + self.cursor.execute(''' + SELECT * FROM posts WHERE id = ? + ''', (ref,)) + reference_post = self.cursor.fetchone() + + if not reference_post: + logger.warning(f"Reference post {ref} does not exist.") + return False + logger.info(f"Reference post {ref} exists.") + else: + logger.info("No reference post provided.") + + # Create a new post + self.cursor.execute(''' + INSERT INTO posts (user_id, board_id, content, refence) + VALUES (?, ?, ?, ?) + ''', (user_id, board_id, content, ref)) + self.connection.commit() + logger.info(f"Post created for user {user_id} in board {board_id}.") + post_id = self.cursor.lastrowid + logger.info(f"Post ID: {post_id}") + return post_id + + + def get_post(self, post_id): + # Get post by ID + logger.info(f"Getting post {post_id}...") + self.cursor.execute(''' + SELECT * FROM posts WHERE id = ? + ''', (post_id,)) + post = self.cursor.fetchone() + + if post: + logger.info(f"Post {post_id} found.") + return post + logger.warning(f"Post {post_id} not found.") + return None + + + def get_post_references(self, post_id, limit=10, offset=0): + # Get references for a post + logger.info(f"Getting references for post {post_id}...") + self.cursor.execute(''' + SELECT * FROM posts WHERE refence = ? LIMIT ? OFFSET ? + ''', (post_id, limit, offset)) + references = self.cursor.fetchall() + + if references: + logger.info(f"References found for post {post_id}.") + return references + logger.warning(f"No references found for post {post_id}.") + return None + + + def get_posts(self, board_id, limit, offset): + if board_id: + logger.info(f"Getting posts for board {board_id}...") + + # Check if the board exists + logger.info(f"Checking if board {board_id} exists...") + self.cursor.execute(''' + SELECT * FROM boards WHERE id = ? + ''', (board_id,)) + board = self.cursor.fetchone() + + if not board: + logger.warning(f"Board {board_id} does not exist.") + return None + logger.info(f"Board {board_id} exists.") + + # Get posts for the board + logger.info(f"Getting posts for board {board_id}...") + self.cursor.execute(''' + SELECT * FROM posts WHERE board_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ? + ''', (board_id, limit, offset)) + posts = self.cursor.fetchall() + + if posts: + logger.info(f"Posts found for board {board_id}.") + return posts + logger.warning(f"No posts found for board {board_id}.") + return None + + else: + # Get all posts + logger.info(f"Getting all posts...") + self.cursor.execute(''' + SELECT * FROM posts ORDER BY created_at DESC LIMIT ? OFFSET ? + ''', (limit, offset)) + posts = self.cursor.fetchall() + + if posts: + logger.info(f"Posts found.") + return posts + logger.warning(f"No posts found.") + return None + + + def get_latest_posts(self, limit): + logger.info(f"Getting latest posts...") + + # Get latest posts + logger.info(f"Getting latest posts...") + self.cursor.execute(''' + SELECT * FROM posts ORDER BY created_at DESC LIMIT ? + ''', (limit,)) + posts = self.cursor.fetchall() + + if posts: + logger.info(f"Latest posts found.") + return posts + logger.warning(f"No latest posts found.") + return None + + + def get_boards(self, limit, offset): + logger.info(f"Listing boards with limit {limit} and offset {offset}...") + + # Get boards with pagination + self.cursor.execute(''' + SELECT * FROM boards ORDER BY created_at DESC LIMIT ? OFFSET ? + ''', (limit, offset)) + boards = self.cursor.fetchall() + + if boards: + logger.info(f"Boards found.") + return boards + logger.warning(f"No boards found.") + return None + + + def post_to_dict(self, post): + # Convert post to dictionary + logger.info(f"Converting post to dictionary...") + data = { + 'id': post[0], + 'content': post[3], + 'created_at': post[4], + 'url': f"/post/{post[0]}", + } + + # Get user information + logger.info(f"Getting user information for post {post[0]}...") + self.cursor.execute(''' + SELECT * FROM users WHERE id = ? + ''', (post[1],)) + user = self.cursor.fetchone() + + if user: + data['user'] = { + 'id': user[0], + 'name': user[1], + 'created_at': user[3] + } + logger.info(f"User information found for post {post[0]}.") + else: + logger.warning(f"User information not found for post {post[0]}.") + data['user'] = { + 'id': None, + 'name': None, + 'created_at': None + } + + # Get board information + logger.info(f"Getting board information for post {post[0]}...") + self.cursor.execute(''' + SELECT * FROM boards WHERE id = ? + ''', (post[2],)) + board = self.cursor.fetchone() + + if board: + data['board'] = { + 'id': board[0], + 'name': board[1], + 'description': board[3], + 'created_at': board[4] + } + logger.info(f"Board information found for post {post[0]}.") + else: + logger.warning(f"Board information not found for post {post[0]}.") + data['board'] = { + 'id': None, + 'name': None, + 'description': None, + 'created_at': None + } + + # Get reference post information + if post[5] is not None: + logger.info(f"Getting reference post information for post {post[0]}...") + self.cursor.execute(''' + SELECT * FROM posts WHERE id = ? + ''', (post[5],)) + reference_post = self.cursor.fetchone() + + if reference_post: + data['reference'] = { + 'id': reference_post[0], + 'content': reference_post[3], + 'created_at': reference_post[4] + } + logger.info(f"Reference post information found for post {post[0]}.") + else: + logger.warning(f"Reference post information not found for post {post[0]}.") + data['reference'] = { + 'id': None, + 'content': None, + 'created_at': None + } + else: + logger.info("No reference post.") + data['reference'] = None + + logger.info(f"Post converted to dictionary.") + return data + + + def create_session(self, username, password): + logger.info(f"Creating session for user {username}...") + + # Check if the user exists + logger.info(f"Checking if user {username} exists...") + self.cursor.execute(''' + SELECT * FROM users WHERE username = ? + ''', (username,)) + user = self.cursor.fetchone() + + if not user: + logger.warning(f"User {username} does not exist.") + return None + logger.info(f"User {username} exists.") + + # Check if the password is correct + logger.info(f"Checking password for user {username}...") + if user[2] != password: + logger.warning(f"Incorrect password for user {username}.") + return None + logger.info(f"Password for user {username} is correct.") + + # Create a new session and overwrite the old one if it exists + session = os.urandom(16).hex() + self.cursor.execute(''' + UPDATE users SET session = ? WHERE username = ? + ''', (session, username)) + + self.connection.commit() + logger.info(f"Session created for user {username}.") + return session + + + def get_session(self, session): + logger.info(f"Getting session {session}...") + + # Get session information + self.cursor.execute(''' + SELECT * FROM users WHERE session = ? + ''', (session,)) + user = self.cursor.fetchone() + + if user: + logger.info(f"Session {session} found.") + return user + logger.warning(f"Session {session} not found.") + return None + + + def delete_session(self, session): + logger.info(f"Deleting session {session}...") + + # Delete session + self.cursor.execute(''' + UPDATE users SET session = NULL WHERE session = ? + ''', (session,)) + + self.connection.commit() + logger.info(f"Session {session} deleted.") + return True + + + def user_to_dict(self, user): + # Convert user to dictionary + logger.info(f"Converting user to dictionary...") + data = { + 'id': user[0], + 'name': user[1], + 'created_at': user[4] + } + logger.info(f"User converted to dictionary.") + return data + + + def get_user_posts(self, user_id, limit=10, offset=0): + logger.info(f"Getting posts for user {user_id}...") + + # Get posts for the user + self.cursor.execute(''' + SELECT * FROM posts WHERE user_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ? + ''', (user_id, limit, offset)) + posts = self.cursor.fetchall() + + if posts: + logger.info(f"Posts found for user {user_id}.") + return posts + logger.warning(f"No posts found for user {user_id}.") + return None + + + def board_to_dict(self, board): + # Convert board to dictionary + logger.info(f"Converting board to dictionary...") + data = { + 'id': board[0], + 'name': board[1], + 'description': board[3], + 'created_at': board[4] + } + logger.info(f"Board converted to dictionary.") + return data + + + def get_board_posts(self, board_id, limit=10, offset=0): + logger.info(f"Getting posts for board {board_id}...") + + # Get posts for the board + self.cursor.execute(''' + SELECT * FROM posts WHERE board_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ? + ''', (board_id, limit, offset)) + posts = self.cursor.fetchall() + + if posts: + logger.info(f"Posts found for board {board_id}.") + return posts + logger.warning(f"No posts found for board {board_id}.") + return None + + + def get_all_boards_for_post(self): + logger.info(f"Getting all boards...") + + # Get all boards + self.cursor.execute(''' + SELECT * FROM boards + ''') + boards = self.cursor.fetchall() + + if boards: + logger.info(f"Boards found.") + # Convert boards to dictionary + boards = [{"id": board[0], "name": board[1]} for board in boards] + return boards + logger.warning(f"No boards found.") + return None \ No newline at end of file diff --git a/src/html/base.html b/src/html/base.html new file mode 100644 index 0000000..679fc3c --- /dev/null +++ b/src/html/base.html @@ -0,0 +1,43 @@ + + + + + + Prismic + + + +
+
+

Prismic

+

ver: 1.0

+
+ +
+
+ {% block content %}{% endblock %} +
+ + + \ No newline at end of file diff --git a/src/html/board.html b/src/html/board.html new file mode 100644 index 0000000..b6050c9 --- /dev/null +++ b/src/html/board.html @@ -0,0 +1,35 @@ +{% extends "base.html" %} + +{% block content %} +

{{ board.name }}

+
Created at: {{ board.created_at }}
+

{{ board.description }}

+
+

Posts:

+ + +{% endblock %} \ No newline at end of file diff --git a/src/html/boards.html b/src/html/boards.html new file mode 100644 index 0000000..39a0e6e --- /dev/null +++ b/src/html/boards.html @@ -0,0 +1,30 @@ +{% extends "base.html" %} + +{% block content %} +

Boards

+
Page {{ page }}
+
+ + +{% endblock %} \ No newline at end of file diff --git a/src/html/index.html b/src/html/index.html new file mode 100644 index 0000000..42c1bfd --- /dev/null +++ b/src/html/index.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} + +{% block content %} +

Welcome to Prismic

+

Prismic is a simple textboard

+
+

Latest Posts:

+ +{% endblock %} \ No newline at end of file diff --git a/src/html/login.html b/src/html/login.html new file mode 100644 index 0000000..d7c7aaf --- /dev/null +++ b/src/html/login.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} + +{% block content %} +

Login

+

Register if you don't have an account.

+
+
+ + +
+ + +
+ +
+ + {% if error %} +

{{ error }}

+ {% endif %} +{% endblock %} \ No newline at end of file diff --git a/src/html/newpost.html b/src/html/newpost.html new file mode 100644 index 0000000..1237dc3 --- /dev/null +++ b/src/html/newpost.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} + +{% block content %} +
+ + +
+ + +
+ +
+ {% if error %} +

{{ error }}

+ {% endif %} +{% endblock %} \ No newline at end of file diff --git a/src/html/post.html b/src/html/post.html new file mode 100644 index 0000000..7d2dd41 --- /dev/null +++ b/src/html/post.html @@ -0,0 +1,48 @@ +{% extends "base.html" %} + +{% block content %} +

From {{ post.user.name }} in /{{ post.board.name }}/

+
posted at {{ post.created_at }}
+ {% if post.reference %} +
ref post: {{ post.reference.content }}
+ {% endif %} +

{{ post.content }}

+
+

Replies:

+ + +
+
+ + + + + +
+ +
+ {% if error %} +

{{ error }}

+ {% endif %} +{% endblock %} \ No newline at end of file diff --git a/src/html/posts.html b/src/html/posts.html new file mode 100644 index 0000000..68b435a --- /dev/null +++ b/src/html/posts.html @@ -0,0 +1,33 @@ +{% extends "base.html" %} + +{% block content %} +

Posts

+
Page {{ page }}
+
+ + +{% endblock %} \ No newline at end of file diff --git a/src/html/register.html b/src/html/register.html new file mode 100644 index 0000000..936010c --- /dev/null +++ b/src/html/register.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} + +{% block content %} +

Register

+
+
+ + +
+ + +
+ +
+ + {% if error %} +

{{ error }}

+ {% endif %} +{% endblock %} \ No newline at end of file diff --git a/src/html/user.html b/src/html/user.html new file mode 100644 index 0000000..a9cf5e8 --- /dev/null +++ b/src/html/user.html @@ -0,0 +1,34 @@ +{% extends "base.html" %} + +{% block content %} +

{{ user.name }}

+
Joined at: {{ user.created_at }}
+
+

Posts:

+ + +{% endblock %} \ No newline at end of file diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..97c4c1a --- /dev/null +++ b/src/main.py @@ -0,0 +1,345 @@ +from flask import Flask, request, render_template, session, redirect +import database, logging, os, hashlib, html +from flask_session import Session + + +# Global variables +SYSTEMUID = None +SYSTEMBID = None +allowed_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%£^&()-_=+[]{};:'\",.<>?/\\|`~ " + + +# Configure logging +console_log = logging.StreamHandler() +console_log.setFormatter(logging.Formatter("\033[1;32m%(asctime)s\033[0m - \033[1;34m%(levelname)s\033[0m - \033[1;31m%(name)s\033[0m - %(message)s")) +logger = logging.getLogger() +logger.addHandler(console_log) +logger.setLevel(logging.INFO) + + +# Initialize Flask app +logger.info("Initializing Flask app...") +app = Flask(__name__, template_folder=os.getenv('TEMPLATE_FOLDER', 'html'), static_folder=os.getenv('STATIC_FOLDER', 'static')) +app.config["SESSION_PERMANENT"] = True +app.config["SESSION_TYPE"] = "filesystem" +Session(app) +logger.info("Flask app initialized.") + + +# Initialize database +logger.info("Initializing database...") +db = database.Database('app.db') + +if db.get_user('SYSTEM') is None: + logger.info("Running first time setup...") + + logger.info("Creating SYSTEM user...") + db.create_user('SYSTEM', 'SYSTEM') + SYSTEMUID = db.get_user('SYSTEM')[0] + logger.info("SYSTEM user created with UID: %s", SYSTEMUID) + + logger.info("Creating SYSTEM board...") + db.create_board('System', 'System messages', SYSTEMUID) + db.create_board('General', 'General discussion', SYSTEMUID) + db.create_board('Linux', 'Linux discussion', SYSTEMUID) + db.create_board('Random', 'Random discussion', SYSTEMUID) + db.create_board('Tech', 'Tech discussion', SYSTEMUID) + db.create_board('Games', 'Games discussion', SYSTEMUID) + SYSTEMBID = db.get_board('System')[0] + logger.info("SYSTEM board created.") + + logger.info("Creating First time setup post...") + db.create_post(SYSTEMBID, SYSTEMUID, 'Welcome!') + logger.info("First time setup post created.") + +else: + SYSTEMUID = db.get_user('SYSTEM')[0] + SYSTEMBID = db.get_board('System')[0] + logger.info("SYSTEM user and board already exist.") + logger.info("SYSTEM user UID: %s", SYSTEMUID) + logger.info("SYSTEM board BID: %s", SYSTEMBID) + +logger.info("Database initialized.") + + +# Helper functions + +def sanitize_input(input_string): + logger.info("Sanitizing input...") + # Sanitize input to allow only certain characters + if not isinstance(input_string, str): + logger.error("Input is not a string.") + return None + sanitized = ''.join(c for c in input_string if c in allowed_chars) + sanitized = html.escape(sanitized) + logger.info("Sanitized input") + return sanitized + + +def hash_password(password): + logger.info("Hashing password...") + # Hash the password using SHA-256 + if not isinstance(password, str): + logger.error("Password is not a string.") + return None + hashed = hashlib.sha256(password.encode()).hexdigest() + logger.info("Hashed password") + return hashed + + +# Define routes +@app.route('/') +def index(): + logger.info("Rendering index page...") + latest_posts = db.get_latest_posts(5) + return render_template('index.html', posts=[db.post_to_dict(post) for post in latest_posts]) + + +@app.route('/posts/') +def post(post_id): + logger.info("Rendering post page for post ID: %s", post_id) + post = db.get_post(post_id) + if post is None: + logger.error("Post not found: %s", post_id) + return "Post not found", 404 + + post = db.post_to_dict(post) + page = request.args.get('page', 1, type=int) + + logger.info("Post found: %s", post_id) + replies = db.get_post_references(post_id, 10, 10 * (page - 1)) + if replies is None: + logger.error("No replies found for post ID: %s", post_id) + post['replies'] = [] + else: + logger.info("Found %s replies for post ID: %s", len(replies), post_id) + post['replies'] = [db.post_to_dict(reply) for reply in replies] + post['replies'].sort(key=lambda x: x['created_at'], reverse=True) + + return render_template('post.html', post=post, page=page) + + +@app.route('/login', methods=['GET', 'POST']) +def login(): + logger.info("Rendering login page...") + if request.method == 'POST': + username = request.form['username'] + password = request.form['password'] + username = sanitize_input(username) + password = sanitize_input(password) + token = db.create_session(username, hash_password(password)) + if token: + logger.info("User %s logged in successfully.", username) + session['name'] = username + session['id'] = db.get_user(username)[0] + session['session'] = token + return redirect('/') + else: + logger.error("Invalid login attempt for user: %s", username) + return render_template('login.html', error="Invalid username or password.") + + return render_template('login.html') + + +@app.route('/user/') +def user(username): + logger.info("Rendering user page for user: %s", username) + username = sanitize_input(username) + user = db.get_user(username) + if user is None: + logger.error("User not found: %s", username) + return "User not found", 404 + + user = db.user_to_dict(user) + page = request.args.get('page', 1, type=int) + + posts = db.get_user_posts(user['id'], 10, 10 * (page - 1)) + if posts is None: + logger.error("No posts found for user: %s", username) + user['posts'] = [] + else: + logger.info("Found %s posts for user: %s", len(posts), username) + user['posts'] = [db.post_to_dict(post) for post in posts] + + return render_template('user.html', user=user, page=page) + + +@app.route('/logout') +def logout(): + logger.info("Logging out user: %s", session.get('name')) + session.clear() + return redirect('/') + + +@app.route('/posts') +def posts(): + logger.info("Rendering posts page...") + page = request.args.get('page', 1, type=int) + posts = db.get_posts(None, 10, 10 * (page - 1)) + if posts is None: + logger.error("No posts found.") + return "No posts found", 404 + + logger.info("Found %s posts.", len(posts)) + return render_template('posts.html', posts=[db.post_to_dict(post) for post in posts], page=page) + + +@app.route('/boards') +def boards(): + logger.info("Rendering boards page...") + + page = request.args.get('page', 1, type=int) + boards = db.get_boards(10, 10 * (page - 1)) + + if boards is None: + logger.error("No boards found.") + return "No boards found", 404 + + logger.info("Found %s boards.", len(boards)) + return render_template('boards.html', boards=[db.board_to_dict(board) for board in boards], page=page) + + +@app.route('/boards/') +def board(board): + logger.info("Rendering board page for board: %s", board) + board = sanitize_input(board) + board = db.get_board(board) + if board is None: + logger.error("Board not found: %s", board) + return "Board not found", 404 + board_id = board[0] + + board = db.board_to_dict(board) + page = request.args.get('page', 1, type=int) + + posts = db.get_board_posts(board_id, 10, 10 * (page - 1)) + if posts is None: + logger.error("No posts found for board ID: %s", board_id) + board['posts'] = [] + else: + logger.info("Found %s posts for board ID: %s", len(posts), board_id) + board['posts'] = [db.post_to_dict(post) for post in posts] + + return render_template('board.html', board=board, page=page) + + +@app.route('/post', methods=['POST', 'GET']) +def post_create(): + logger.info("Rendering post creation page...") + if request.method == 'POST': + content = request.form['content'] + board_id = request.form['board'] + ref = request.form.get('ref', None) + token = session.get('session') + + # sanitize input + content = sanitize_input(content) + try: + board_id = int(board_id) + except ValueError: + logger.error("Invalid board ID: %s", board_id) + return render_template('newpost.html', error="Invalid board ID.") + if not content: + logger.error("Post content is empty.") + return render_template('newpost.html', error="Post content cannot be empty.") + if not board_id: + logger.error("Board ID is empty.") + return render_template('newpost.html', error="Board ID cannot be empty.") + if not token: + logger.error("Session token is missing.") + return render_template('newpost.html', error="Session expired. Please log in again.") + if ref: + try: + ref = int(ref) + except ValueError: + logger.error("Invalid reference post ID: %s", ref) + return render_template('newpost.html', error="Invalid reference post ID.") + if len(content) > 10000: + logger.error("Post content exceeds maximum length.") + return render_template('newpost.html', error="Post content exceeds maximum length.") + + user = db.get_session(token) + if user is None: + logger.error("Session not found or expired.") + return render_template('newpost.html', error="Session expired. Please log in again.") + user_id = user[0] + + if board_id == SYSTEMBID and user_id != SYSTEMUID: + logger.error("User %s is not allowed to post in SYSTEM board.", user_id) + return render_template('newpost.html', error="You are not allowed to post in this board.") + + if ref: + ref = db.get_post(ref) + if ref is None: + logger.error("Reference post not found: %s", ref) + return render_template('newpost.html', error="Reference post not found.") + ref = ref[0] + + logger.info("Creating post in board ID: %s", board_id) + status = db.create_post(board_id, user_id, content, ref) + + if type(status) is not int: + logger.error("Post creation failed.") + return "Post creation failed", 500 + + logger.info("Post created successfully.") + if request.form.get('redirect'): + return redirect(request.form.get('redirect')) + return redirect('/posts/' + str(status)) + return render_template('newpost.html', boards=db.get_all_boards_for_post()) + + +@app.route('/register', methods=['GET', 'POST']) +def register(): + logger.info("Rendering registration page...") + if request.method == 'POST': + username = request.form['username'] + password = request.form['password'] + username = sanitize_input(username) + password = sanitize_input(password) + + if not username or not password: + logger.error("Username or password is empty.") + return render_template('register.html', error="Username and password cannot be empty.") + + if len(username) > 20: + logger.error("Username length is invalid.") + return render_template('register.html', error="Username must be less than 20 characters.") + + if len(password) < 6 or len(password) > 20: + logger.error("Password length is invalid.") + return render_template('register.html', error="Password must be between 6 and 20 characters.") + + if db.get_user(username): + logger.error("Username already exists: %s", username) + return render_template('register.html', error="Username already exists.") + + hashed_password = hash_password(password) + db.create_user(username, hashed_password) + logger.info("User %s registered successfully.", username) + db.create_post(SYSTEMUID, SYSTEMUID, f"New user \"{username}\" registered.") + # Create a session for the new user + token = db.create_session(username, hashed_password) + if token: + session['name'] = username + session['id'] = db.get_user(username)[0] + session['session'] = token + logger.info("User %s logged in after registration.", username) + return redirect('/') + else: + logger.error("Session creation failed for user: %s", username) + return render_template('register.html', error="Session creation failed.") + + logger.info("GET request for registration page.") + if session.get('name'): + logger.info("User %s is already logged in.", session['name']) + return redirect('/') + logger.info("Rendering registration form.") + + return render_template('register.html') + + +# Main function +if __name__ == '__main__': + logger.info("Starting Flask app...") + app.run(host=os.getenv('FLASK_HOST', '0.0.0.0'), port=int(os.getenv('FLASK_PORT', 5000)), debug=os.getenv('FLASK_DEBUG', 'false').lower() == 'true') \ No newline at end of file diff --git a/src/static/base.css b/src/static/base.css new file mode 100644 index 0000000..22fe1f3 --- /dev/null +++ b/src/static/base.css @@ -0,0 +1,182 @@ +:root { + --bg: #000; + --fg: #fff; + --accent: #7139f3; +} + +body { + background-color: var(--bg); + color: var(--fg); + font-family: 'code', sans-serif; + margin: 20px; + display: flex; + flex-direction: column; +} + +h1, h2, h3, h4, h5, h6, p { + margin: 0; +} + +header { + display: flex; + align-items: center; + gap: 30px; + padding: 20px; + border-bottom: 1px solid var(--fg); +} + +header #title { + display: flex; + align-items: baseline; + gap: 2px; +} + +header h1 { + font-size: 2rem; +} +header h2 { + font-size: .8rem; + text-wrap: nowrap; +} + +header nav { + width: 100%; +} + +header nav ul { + display: flex; + gap: 15px; + list-style: none; + padding: 0; +} + +header nav ul li:last-child { + margin-left: auto; +} + +header nav ul li a { + text-decoration: none; + color: var(--fg); + font-size: 1.2rem; + font-weight: 600; + transition: color 0.3s ease; +} + +header nav ul li a:hover { + color: var(--accent); +} + +main { + height: 100%; + padding: 20px; +} + +footer { + margin-top: 10px; + padding: 20px; + border-top: 1px solid var(--fg); + font-size: 0.8rem; +} + +a { + color: var(--accent); + text-decoration: none; + transition: all 0.3s ease; +} +a:hover { + color: var(--fg); +} + +ul.posts { + list-style: none; + padding: 0; + margin: 0; + margin-left: 10px; + display: flex; + flex-direction: column; + gap: 10px; +} + +form { + display: flex; + flex-direction: column; + gap: 10px; +} + +input[type="text"], +input[type="password"] { + padding: 10px; + border: 1px solid var(--fg); + border-radius: 5px; + box-sizing: border-box; + background-color: var(--bg); + color: var(--fg); + font-size: 1rem; + width: 300px; +} + +input[type="text"]:focus, +input[type="password"]:focus { + outline: none; + border-color: var(--accent); +} + +button[type="submit"] { + padding: 10px 20px; + border: none; + border-radius: 5px; + background-color: var(--accent); + color: var(--bg); + font-size: 1rem; + cursor: pointer; + transition: background-color 0.3s ease; + width: 300px; + font-weight: 600; +} + +button[type="submit"]:hover { + background-color: var(--fg); + color: var(--bg); +} +button[type="submit"]:disabled { + background-color: var(--bg); + color: var(--fg); + cursor: not-allowed; +} + +select { + padding: 10px; + border: 1px solid var(--fg); + border-radius: 5px; + background-color: var(--bg); + color: var(--fg); + font-size: 1rem; + width: 300px; +} + +select:focus { + outline: none; + border-color: var(--accent); +} + +textarea { + padding: 10px; + border: 1px solid var(--fg); + border-radius: 5px; + background-color: var(--bg); + color: var(--fg); + font-size: 1rem; + width: 300px; + height: 100px; +} + +textarea:focus { + outline: none; + border-color: var(--accent); +} + +div#nav { + display: flex; + gap: 10px; + margin-top: 20px; +} \ No newline at end of file