ver 1
This commit is contained in:
commit
c109930ae0
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.env
|
||||||
|
.venv
|
||||||
|
app.db
|
BIN
flask_session/2029240f6d1128be89ddc32729463129
Normal file
BIN
flask_session/2029240f6d1128be89ddc32729463129
Normal file
Binary file not shown.
BIN
flask_session/95ca1eb92b3063fb8d5ea8eef8e9be3f
Normal file
BIN
flask_session/95ca1eb92b3063fb8d5ea8eef8e9be3f
Normal file
Binary file not shown.
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
flask
|
||||||
|
flask-session
|
BIN
src/__pycache__/database.cpython-313.pyc
Normal file
BIN
src/__pycache__/database.cpython-313.pyc
Normal file
Binary file not shown.
544
src/database.py
Normal file
544
src/database.py
Normal file
@ -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
|
43
src/html/base.html
Normal file
43
src/html/base.html
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Prismic</title>
|
||||||
|
<link rel="stylesheet" href="../static/base.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<div id="title">
|
||||||
|
<h1>Prismic</h1>
|
||||||
|
<h2>ver: 1.0</h2>
|
||||||
|
</div>
|
||||||
|
<nav>
|
||||||
|
<ul>
|
||||||
|
<li><a href="/">Home</a></li>
|
||||||
|
<li><a href="/boards">Boards</a></li>
|
||||||
|
<li><a href="/posts">Posts</a></li>
|
||||||
|
{% if session.name %}
|
||||||
|
<li><a href="/post">Post</a></li>
|
||||||
|
{% endif %}
|
||||||
|
<li>{% if session.name %}
|
||||||
|
<a href="/user/{{ session.name }}">{{ session.name }}</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="/login">Login</a>
|
||||||
|
{% endif %}</li>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
<footer>
|
||||||
|
<p>Created by <a href="https://alfieking.dev">Alfie King</a>
|
||||||
|
{% if session.name %}
|
||||||
|
| <a href="/logout">Logout</a>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
35
src/html/board.html
Normal file
35
src/html/board.html
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>{{ board.name }}</h1>
|
||||||
|
<h6>Created at: {{ board.created_at }}</h6>
|
||||||
|
<p>{{ board.description }}</p>
|
||||||
|
<br>
|
||||||
|
<h1>Posts:</h1>
|
||||||
|
<ul class="posts">
|
||||||
|
{% for post in board.posts %}
|
||||||
|
<li>
|
||||||
|
<h3>From {{ post.user.name }}</h3>
|
||||||
|
<h6>posted at {{ post.created_at }}</h6>
|
||||||
|
{% if post.reference %}
|
||||||
|
<h6><b>ref post:</b> <a href="/posts/{{ post.reference.id }}">{{ post.reference.content }}</a></h6>
|
||||||
|
{% endif %}
|
||||||
|
<p>{{ post.content }}</p>
|
||||||
|
<h6><a href="/posts/{{ post.id }}">View Post</a></h6>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
{% if board.posts|length == 0 %}
|
||||||
|
<li>No posts found.</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
<div id="nav">
|
||||||
|
<h5>Page {{ page }}</h5>
|
||||||
|
{% if page > 1 %}
|
||||||
|
<h5><a href="/boards/{{ board.name }}?page={{ page - 1 }}">Previous Page</a></h5>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if board.posts|length == 10 %}
|
||||||
|
<h5><a href="/boards/{{ board.name }}?page={{ page + 1 }}">Next Page</a></h5>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
30
src/html/boards.html
Normal file
30
src/html/boards.html
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Boards</h1>
|
||||||
|
<h5>Page {{ page }}</h5>
|
||||||
|
<br>
|
||||||
|
<ul class="posts">
|
||||||
|
{% for board in boards %}
|
||||||
|
<li>
|
||||||
|
<h3>/{{ board.name }}/</h3>
|
||||||
|
<h6>created at {{ board.created_at }}</h6>
|
||||||
|
<p>{{ board.description }}</p>
|
||||||
|
<h6><a href="/boards/{{ board.name }}">View Board</a></h6>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
{% if boards|length == 0 %}
|
||||||
|
<li>No boards found.</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
<div id="nav">
|
||||||
|
<h5>Page {{ page }}</h5>
|
||||||
|
{% if page > 1 %}
|
||||||
|
<h5><a href="/boards?page={{ page - 1 }}">Previous Page</a></h5>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if boards|length == 10 %}
|
||||||
|
<h5><a href="/boards?page={{ page + 1 }}">Next Page</a></h5>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
24
src/html/index.html
Normal file
24
src/html/index.html
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Welcome to Prismic</h1>
|
||||||
|
<p>Prismic is a simple textboard</p>
|
||||||
|
<br>
|
||||||
|
<h1>Latest Posts:</h1>
|
||||||
|
<ul class="posts">
|
||||||
|
{% for post in posts %}
|
||||||
|
<li>
|
||||||
|
<h3>From {{ post.user.name }} in /{{ post.board.name }}/</h3>
|
||||||
|
<h6>posted at {{ post.created_at }}</h6>
|
||||||
|
{% if post.reference %}
|
||||||
|
<h6><b>ref post:</b> <a href="/posts/{{ post.reference.id }}">{{ post.reference.content }}</a></h6>
|
||||||
|
{% endif %}
|
||||||
|
<p>{{ post.content }}</p>
|
||||||
|
<h6><a href="/posts/{{ post.id }}">View Post</a></h6>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
{% if posts|length == 0 %}
|
||||||
|
<li>No posts found.</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
{% endblock %}
|
20
src/html/login.html
Normal file
20
src/html/login.html
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Login</h1>
|
||||||
|
<p><a href="/register">Register</a> if you don't have an account.</p>
|
||||||
|
<br>
|
||||||
|
<form method="POST" action="/login">
|
||||||
|
<label for="username">Username:</label>
|
||||||
|
<input type="text" id="username" name="username" required>
|
||||||
|
<br>
|
||||||
|
<label for="password">Password:</label>
|
||||||
|
<input type="password" id="password" name="password" required>
|
||||||
|
<br>
|
||||||
|
<button type="submit">Login</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% if error %}
|
||||||
|
<p style="color: red;">{{ error }}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
20
src/html/newpost.html
Normal file
20
src/html/newpost.html
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form method="POST" action="/post">
|
||||||
|
<label for="board">Board:</label>
|
||||||
|
<select id="board" name="board">
|
||||||
|
{% for board in boards %}
|
||||||
|
<option value="{{ board.id }}">{{ board.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<br>
|
||||||
|
<label for="content">Content:</label>
|
||||||
|
<textarea id="content" name="content" rows="4" cols="50" required></textarea>
|
||||||
|
<br>
|
||||||
|
<button type="submit">Post</button>
|
||||||
|
</form>
|
||||||
|
{% if error %}
|
||||||
|
<p style="color: red;">{{ error }}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
48
src/html/post.html
Normal file
48
src/html/post.html
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2>From {{ post.user.name }} in /{{ post.board.name }}/</h2>
|
||||||
|
<h6>posted at {{ post.created_at }}</h6>
|
||||||
|
{% if post.reference %}
|
||||||
|
<h6><b>ref post:</b> <a href="/posts/{{ post.reference.id }}">{{ post.reference.content }}</a></h6>
|
||||||
|
{% endif %}
|
||||||
|
<p>{{ post.content }}</p>
|
||||||
|
<br>
|
||||||
|
<h3>Replies:</h3>
|
||||||
|
<ul class="posts">
|
||||||
|
{% for reply in post.replies %}
|
||||||
|
<li>
|
||||||
|
<h3>From {{ reply.user.name }} in /{{ reply.board.name }}/</h3>
|
||||||
|
<h6>posted at {{ reply.created_at }}</h6>
|
||||||
|
<p>{{ reply.content }}</p>
|
||||||
|
<h6><a href="/posts/{{ reply.id }}">View Reply</a></h6>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
{% if post.replies|length == 0 %}
|
||||||
|
<li>No replies found.</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
<div id="nav">
|
||||||
|
<h5>Page {{ page }}</h5>
|
||||||
|
{% if page > 1 %}
|
||||||
|
<h5><a href="/posts/{{ post.id }}?page={{ page - 1 }}">Previous Page</a></h5>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if post.replies|length == 10 %}
|
||||||
|
<h5><a href="/posts/{{ post.id }}?page={{ page + 1 }}">Next Page</a></h5>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<form method="POST" action="/post">
|
||||||
|
<input type="hidden" name="ref" value="{{ post.id }}">
|
||||||
|
<input type="hidden" name="board" value="{{ post.board.id }}">
|
||||||
|
<input type="hidden" name="redirect" value="/posts/{{ post.id }}">
|
||||||
|
<label for="content">Reply:</label>
|
||||||
|
<textarea id="content" name="content" rows="4" cols="50" required></textarea>
|
||||||
|
<br>
|
||||||
|
<button type="submit">Post</button>
|
||||||
|
</form>
|
||||||
|
{% if error %}
|
||||||
|
<p style="color: red;">{{ error }}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
33
src/html/posts.html
Normal file
33
src/html/posts.html
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Posts</h1>
|
||||||
|
<h5>Page {{ page }}</h5>
|
||||||
|
<br>
|
||||||
|
<ul class="posts">
|
||||||
|
{% for post in posts %}
|
||||||
|
<li>
|
||||||
|
<h3>From {{ post.user.name }} in /{{ post.board.name }}/</h3>
|
||||||
|
<h6>posted at {{ post.created_at }}</h6>
|
||||||
|
{% if post.reference %}
|
||||||
|
<h6><b>ref post:</b> <a href="/posts/{{ post.reference.id }}">{{ post.reference.content }}</a></h6>
|
||||||
|
{% endif %}
|
||||||
|
<p>{{ post.content }}</p>
|
||||||
|
<h6><a href="/posts/{{ post.id }}">View post</a></h6>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
{% if posts|length == 0 %}
|
||||||
|
<li>No replies found.</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
<div id="nav">
|
||||||
|
<h5>Page {{ page }}</h5>
|
||||||
|
{% if page > 1 %}
|
||||||
|
<h5><a href="/posts?page={{ page - 1 }}">Previous Page</a></h5>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if posts|length == 10 %}
|
||||||
|
<h5><a href="/posts?page={{ page + 1 }}">Next Page</a></h5>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
19
src/html/register.html
Normal file
19
src/html/register.html
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Register</h1>
|
||||||
|
<br>
|
||||||
|
<form method="POST" action="/register">
|
||||||
|
<label for="username">Username:</label>
|
||||||
|
<input type="text" id="username" name="username" required>
|
||||||
|
<br>
|
||||||
|
<label for="password">Password:</label>
|
||||||
|
<input type="password" id="password" name="password" required>
|
||||||
|
<br>
|
||||||
|
<button type="submit">Register</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% if error %}
|
||||||
|
<p style="color: red;">{{ error }}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
34
src/html/user.html
Normal file
34
src/html/user.html
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>{{ user.name }}</h1>
|
||||||
|
<h6>Joined at: {{ user.created_at }}</h6>
|
||||||
|
<br>
|
||||||
|
<h1>Posts:</h1>
|
||||||
|
<ul class="posts">
|
||||||
|
{% for post in user.posts %}
|
||||||
|
<li>
|
||||||
|
<h3>From {{ post.user.name }} in /{{ post.board.name }}/</h3>
|
||||||
|
<h6>posted at {{ post.created_at }}</h6>
|
||||||
|
{% if post.reference %}
|
||||||
|
<h6><b>ref post:</b> <a href="/posts/{{ post.reference.id }}">{{ post.reference.content }}</a></h6>
|
||||||
|
{% endif %}
|
||||||
|
<p>{{ post.content }}</p>
|
||||||
|
<h6><a href="/posts/{{ post.id }}">View Post</a></h6>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
{% if user.posts|length == 0 %}
|
||||||
|
<li>No posts found.</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
<div id="nav">
|
||||||
|
<h5>Page {{ page }}</h5>
|
||||||
|
{% if page > 1 %}
|
||||||
|
<h5><a href="/user/{{ user.name }}?page={{ page - 1 }}">Previous Page</a></h5>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if user.posts|length == 10 %}
|
||||||
|
<h5><a href="/user/{{ user.name }}?page={{ page + 1 }}">Next Page</a></h5>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
345
src/main.py
Normal file
345
src/main.py
Normal file
@ -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/<int:post_id>')
|
||||||
|
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/<string:username>')
|
||||||
|
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/<string:board>')
|
||||||
|
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')
|
182
src/static/base.css
Normal file
182
src/static/base.css
Normal file
@ -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;
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user