diff --git a/.gitignore b/.gitignore index 05a0f7a..8e92c84 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,12 @@ docker-compose.yaml # Ignore images in posts *.jpg *.png + +# Flask Data & Config +*.ini +data +.flask_session +*.json *.gif # Writing diff --git a/Dockerfile b/Dockerfile index 9360969..05c6fe5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,8 +4,6 @@ FROM python:3.12.2-slim-bookworm RUN apt-get update && apt-get upgrade -y -RUN groupadd -r app && useradd -r -g app app - COPY . . RUN python3 -m pip install --upgrade pip @@ -13,6 +11,4 @@ RUN python3 -m pip install -r requirements.txt WORKDIR ./app -USER app - CMD ["python3", "-u", "app.py"] diff --git a/app/app.py b/app/app.py index 4036eb5..037de4f 100644 --- a/app/app.py +++ b/app/app.py @@ -2,17 +2,25 @@ import os import glob import configparser import random +import base64 import datetime import requests import flask +import flask_wtf.csrf +import flask_session import waitress import markdown from post import Post +import comment +import user app = flask.Flask(__name__, static_url_path='', static_folder='static') +app.register_blueprint(comment.comments) +app.register_blueprint(user.user) +# CONFIG CONFIG_PATH = "./config.ini" config = configparser.ConfigParser() config.read(CONFIG_PATH) @@ -23,11 +31,22 @@ STATUS_FILE = config['STATUS']['STATUS_FILE'] PORT = int(config['NETWORK']['PORT']) DEV = int(config['NETWORK']['DEV']) + +# CSRF Protect +app.config['SECRET_KEY'] = base64.b64decode(config["FLASK"]["SECRET"]) +csrf = flask_wtf.csrf.CSRFProtect() +csrf.init_app(app) + +# Session Setup +app.config['SESSION_TYPE'] = 'filesystem' +app.config['SESSION_FILE_DIR'] = './data/.flask_session/' +flask_session.Session(app) + MUSIC_API_TOKEN = config['AUTH']['MUSIC_API_TOKEN'] MUSIC_API_URL = config['NETWORK']['MUSIC_API_URL'] statuses = {} -def get_posts(category_filter : str | None = None) -> list[Post]: +def get_posts(category_filter : str | None = None) -> list[tuple[dict, list]]: post_files = glob.glob(f'{POSTS_FOLDER}/*') try: post_files.remove(f'{POSTS_FOLDER}/POST_TEMPLATE.md') @@ -57,7 +76,12 @@ def get_posts(category_filter : str | None = None) -> list[Post]: ordered_posts.append(most_recent) posts.remove(most_recent) - return reversed(ordered_posts) + # Convert to dict + posts = [] + for post in reversed(ordered_posts): + posts.append(post.__dict__) + + return posts def read_status_file() -> dict: with open(STATUS_FILE, 'r', encoding='utf-8') as file: @@ -97,40 +121,57 @@ def index(): # Get posts posts = get_posts() - post_bodies = [] - for post in posts: - post_bodies.append(post.body) + if 'username' in flask.session: + user = flask.session['username'] + else: + user = 'Anon' # Get status status = get_status() - return flask.render_template('index.html', posts=post_bodies, status=status) + # Setup Comment Form + form = comment.CommentForm() + + return flask.render_template('index.html', posts=posts, status=status, form=form, user=user, title='0x01fe.net') # Posts @app.route('/post/') def post(post_name: str): for post in get_posts(): - if post.title.replace(' ', '-') == post_name: - return flask.render_template('index.html', posts=[post.body], status=get_status()) + if post['title'] == post_name: + + if 'username' in flask.session: + user = flask.session['username'] + else: + user = 'Anon' + + # Setup Comment Form + form = comment.CommentForm() + + return flask.render_template('index.html', posts=[post], status=get_status(), form=form, user=user, title='0x01fe.net') flask.abort(404) -# Games Page -@app.route('/games/') -def games(): +# Category's Endpoint +@app.route('/category//') +def category_filter(category: str): # Get posts - posts = get_posts(category_filter="games") + posts = get_posts(category_filter=category) - post_bodies = [] - for post in posts: - post_bodies.append(post.body) + if 'username' in flask.session: + user = flask.session['username'] + else: + user = 'Anon' # Get status status = get_status() - return flask.render_template('games.html', posts=post_bodies, status=status) + # Setup Comment Form + form = comment.CommentForm() + + return flask.render_template('index.html', posts=posts, status=status, form=form, user=user, title=category.replace('-', ' ')) # Music Page @app.route('/music/') @@ -139,9 +180,10 @@ def music(): # Get posts posts = get_posts(category_filter="music") - post_bodies = [] - for post in posts: - post_bodies.append(post.body) + if 'username' in flask.session: + user = flask.session['username'] + else: + user = 'Anon' # Get status status = get_status() @@ -164,39 +206,30 @@ def music(): top_albums[album_index]['listen_time'] = hours - return flask.render_template('music.html', posts=post_bodies, status=status, top_albums=top_albums) + # Setup Comment Form + form = comment.CommentForm() -# Motion Pictures Page -@app.route('/motion-pictures/') -def motion_pictures(): - - # Get posts - posts = get_posts(category_filter="motion-pictures") - - post_bodies = [] - for post in posts: - post_bodies.append(post.body) - - # Get status - status = get_status() - - return flask.render_template('motion-pictures.html', posts=post_bodies, status=status) + return flask.render_template('music.html', posts=posts, status=status, top_albums=top_albums, form=form, user=user) # Programming Page @app.route('/programming/') def programming(): # Get posts - posts = get_posts(category_filter="programming") + posts_and_comments = get_posts(category_filter="programming") - post_bodies = [] - for post in posts: - post_bodies.append(post.body) + if 'username' in flask.session: + user = flask.session['username'] + else: + user = 'Anon' # Get status status = get_status() - return flask.render_template('programming.html', posts=post_bodies, status=status) + # Setup Comment Form + form = comment.CommentForm() + + return flask.render_template('programming.html', posts=posts_and_comments, form=form, user=user, status=status) @app.route('/writing/') def writing(): @@ -219,10 +252,6 @@ def writing(): return flask.render_template('writing.html', works=works) - - - - # About Page @app.route('/about/') def about(): diff --git a/app/comment.py b/app/comment.py new file mode 100644 index 0000000..1aa046d --- /dev/null +++ b/app/comment.py @@ -0,0 +1,61 @@ +import json +import os + +import flask +import flask_wtf.csrf +import wtforms + +COMMENTS_PATH = "./data/comments.json" +comments = flask.Blueprint('comment', __name__, template_folder='./templates') + +class CommentForm(flask_wtf.FlaskForm): + textbox = wtforms.TextAreaField('Input') + +@comments.route('/comment/', methods=['POST']) +def comment(post_title: str): + form = CommentForm(csrf_enabled=True) + + save_comment(form.textbox.data, post_title) + + return flask.redirect('/') + +def save_comment(content: str, post_title: str): + with open(COMMENTS_PATH, 'r') as file: + comment_data = json.loads(file.read()) + + # See if user is logged in, otherwise setup as anon + if 'username' in flask.session: + username = flask.session['username'] + else: + username = 'Anon' + + comment = { + "username" : username, + "content" : content + } + + # Add comment to JSON data + if post_title in comment_data: + comment_data[post_title].append(comment) + else: + comment_data[post_title] = [comment] + + # Save JSON data + with open(COMMENTS_PATH, 'w') as file: + file.write(json.dumps(comment_data)) + +def get_comments(post_title : int) -> list[dict]: + + with open(COMMENTS_PATH, 'r') as file: + comment_data = json.loads(file.read()) + + if post_title in comment_data: + return comment_data[post_title] + else: + return [] + +# Check Comments file exists +if not os.path.exists(COMMENTS_PATH): + with open(COMMENTS_PATH, 'w+') as file: + file.write('{}') + diff --git a/app/data/.flask_session/2029240f6d1128be89ddc32729463129 b/app/data/.flask_session/2029240f6d1128be89ddc32729463129 deleted file mode 100644 index 7f5741f..0000000 Binary files a/app/data/.flask_session/2029240f6d1128be89ddc32729463129 and /dev/null differ diff --git a/app/data/.flask_session/aa43147407c8a8c678cd91f678f2a023 b/app/data/.flask_session/aa43147407c8a8c678cd91f678f2a023 deleted file mode 100644 index d818306..0000000 Binary files a/app/data/.flask_session/aa43147407c8a8c678cd91f678f2a023 and /dev/null differ diff --git a/app/data/comments.json b/app/data/comments.json deleted file mode 100644 index 964e9aa..0000000 --- a/app/data/comments.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "2" : [ - { - "username" : "0x01FE", - "content" : "Hello, this is an example comment!" - } - ] -} diff --git a/app/data/users.json b/app/data/users.json deleted file mode 100644 index c3227f9..0000000 --- a/app/data/users.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "users" : { - "0x01FE" : "cGFzc3dvcmQ=" - } -} diff --git a/app/post.py b/app/post.py index 954fbf2..24e4113 100644 --- a/app/post.py +++ b/app/post.py @@ -1,15 +1,19 @@ import markdown import datetime +import comment + class Post: category : str author : str date : datetime.datetime + date_str : str body : str file : str title : str url : str + comments : list[dict] def __init__(self, file_path): self.file = file_path @@ -19,11 +23,12 @@ class Post: self.category = lines[1].split(":")[1].strip() self.author = lines[2].split(":")[1].strip() - self.title = lines[6][2:-1] - self.url = '/post/' + self.title.replace(' ', '-') + self.title = lines[6].replace('#', '').strip() + self.url = '/post/' + self.title date = lines[3].split(":")[1].strip() self.date = datetime.datetime.strptime(date, "%d-%m-%Y") + self.date_str = self.date.strftime("%B %d, %Y") - self.body = markdown.markdown(f'# [{self.title}]({self.url})\n' + ''.join(lines[7:])) - + self.body = markdown.markdown(f'# [{self.title}]({self.url})\n' + ''.join(lines[7:]), extensions=['footnotes']) + self.comments = comment.get_comments(self.title) diff --git a/app/posts/POST_TEMPLATE.md b/app/posts/POST_TEMPLATE.md index 7fd0fb8..23f96f1 100644 --- a/app/posts/POST_TEMPLATE.md +++ b/app/posts/POST_TEMPLATE.md @@ -4,6 +4,6 @@ author: author date: date # POST -## TITLE +# TITLE ### DATE OR SUBTITLE POST TEXT diff --git a/app/resources/status.text b/app/resources/status.text index 0fece64..00201ed 100644 --- a/app/resources/status.text +++ b/app/resources/status.text @@ -101,6 +101,7 @@ Buildings that you know // People come and go // Are a certain way And then you get the news that obliterates your view // Amputate your truths // The signifiance has changed Hospital inane // Meaningless and grey // But lie within the walls and the signifiance will change What would it take for us to change the game? // Maybe our existence is signifiance in vain +Is that what we consider changing? // Ah, save me # Candles by King Gizzard & The Lizard Wizard This little man is too hot to handle // Set the funeral on fire with the candles diff --git a/app/static/style.css b/app/static/style.css index 67ad496..9bdca92 100644 --- a/app/static/style.css +++ b/app/static/style.css @@ -19,6 +19,10 @@ html { --primary20: hsla(209, 61%, 71%, 20%); --primary40: hsla(209, 61%, 71%, 40%); --secondary: hsl(277, 81%, 33%); + --secondary10: hsla(277, 81%, 33%, 10%); + --secondary20: hsla(277, 81%, 33%, 20%); + --secondary30: hsla(277, 81%, 33%, 30%); + --secondary40: hsla(277, 81%, 33%, 40%); --secondary50: hsla(277, 81%, 33%, 50%); --accent: hsl(291, 81%, 60%); --accent75: hsla(291, 81%, 60%, 75%); @@ -43,7 +47,24 @@ a:hover { color: var(--accent); } +blockquote { + background-color: var(--secondary10); + border-style: var(--borders-style); + border-radius: 7.5px; + padding: 0.25em; +} +.post blockquote p { + text-indent: 1.5em; +} + +.post blockquote li p { + text-indent: 0; +} + +li { + margin-left: 4em; +} /* Other */ @@ -206,6 +227,50 @@ a:hover { text-indent: 3em; } +.post-date { + float: right; +} + +.comment-container { + background-color: var(--primary40); + border-style: var(--borders-style); + border-radius: var(--border-radius); + + padding: 1em; + margin: 1em; +} + +.comment { + background-color: var(--primary40); + border-style: var(--borders-style); + border-radius: 5px; + + margin: 0.25em; + padding: 0.25em; +} + +.comment h4 { + margin: 0.25em; +} + +.comment p { + margin: 0.25em; + text-indent: 1em; +} + +.comment-editor textarea { + width: 80%; + height: 6em; + padding: 0.75em; + + border-style: solid; + border-color: var(--secondary50); +} + +.comment-editor textarea:focus { + border: 3px solid var(--accent); +} + /* MUSIC */ .albums { height: fit-content; diff --git a/app/templates/about.html b/app/templates/about.html index ce7774c..d85d1ce 100644 --- a/app/templates/about.html +++ b/app/templates/about.html @@ -13,13 +13,14 @@
diff --git a/app/templates/games.html b/app/templates/games.html deleted file mode 100644 index 0ffded7..0000000 --- a/app/templates/games.html +++ /dev/null @@ -1,34 +0,0 @@ - - -
- - - 0x01fe.net - Games -
- -
-

Games

- {{ status|safe }} -
-
- - - - - -
- {% for post in posts %} -
{{ post|safe }}
- {% endfor %} -
-
- - diff --git a/app/templates/index.html b/app/templates/index.html index def98bd..6110f5c 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -3,30 +3,55 @@
- 0x01fe.net + 0x01fe.net - {{ title }}
-

0x01fe.net

+

{{ title }}

{{ status|safe }}
{% for post in posts %} -
{{ post|safe }}
+
+ + {{ post.body|safe }} +
+

Comments

+ {% for comment in post.comments %} +
+

{{ comment.username }}

+

{{ comment.content }}

+
+ {% endfor %} + {% if user %} +
+

{{ user }}

+
+ {{ form.hidden_tag() }} + {{ form.textbox }} + +
+
+ {% endif %} +
+
{% endfor %}
diff --git a/app/templates/motion-pictures.html b/app/templates/motion-pictures.html deleted file mode 100644 index 87d36f9..0000000 --- a/app/templates/motion-pictures.html +++ /dev/null @@ -1,34 +0,0 @@ - - -
- - - 0x01fe.net - Motion Pictures -
- -
-

Motion Pictures

- {{ status|safe }} -
-
- - - - - -
- {% for post in posts %} -
{{ post|safe }}
- {% endfor %} -
-
- - diff --git a/app/templates/music.html b/app/templates/music.html index 5ea89f3..df91746 100644 --- a/app/templates/music.html +++ b/app/templates/music.html @@ -13,20 +13,45 @@
{% for post in posts %} -
{{ post|safe }}
+
+ + {{ post.body|safe }} +
+

Comments

+ {% for comment in post.comments %} +
+

{{ comment.username }}

+

{{ comment.content }}

+
+ {% endfor %} + {% if user %} +
+

{{ user }}

+
+ {{ form.hidden_tag() }} + {{ form.textbox }} + +
+
+ {% endif %} +
+
{% endfor %}
diff --git a/app/templates/programming.html b/app/templates/programming.html index d146ccd..cb81887 100644 --- a/app/templates/programming.html +++ b/app/templates/programming.html @@ -13,20 +13,45 @@
{% for post in posts %} -
{{ post|safe }}
+
+ + {{ post.body|safe }} +
+

Comments

+ {% for comment in post.comments %} +
+

{{ comment.username }}

+

{{ comment.content }}

+
+ {% endfor %} + {% if user %} +
+

{{ user }}

+
+ {{ form.hidden_tag() }} + {{ form.textbox }} + +
+
+ {% endif %} +
+
{% endfor %}
diff --git a/app/templates/user/login.html b/app/templates/user/login.html new file mode 100644 index 0000000..9659b9c --- /dev/null +++ b/app/templates/user/login.html @@ -0,0 +1,40 @@ + + +
+ + + 0x01fe.net - Login +
+ +
+

0x01fe.net

+ {{ status|safe }} +
+
+ + + +
+

Login

+
+ {{ form.hidden_tag() }} + Username: {{ form.username }}
+ Password: {{ form.password }}
+ +
+
+ +

Need to Register?

+ Register +
+
+ + diff --git a/app/templates/user/register.html b/app/templates/user/register.html new file mode 100644 index 0000000..b5cc4da --- /dev/null +++ b/app/templates/user/register.html @@ -0,0 +1,36 @@ + + +
+ + + 0x01fe.net - Register +
+ +
+

0x01fe.net

+ {{ status|safe }} +
+
+ + + +
+

Register

+
+ {{ form.hidden_tag() }} + Username: {{ form.username }}
+ Password: {{ form.password }}
+ +
+
+
+ + diff --git a/app/user.py b/app/user.py new file mode 100644 index 0000000..198ff5d --- /dev/null +++ b/app/user.py @@ -0,0 +1,102 @@ +import base64 +import json +import os + +import flask +import flask_wtf.csrf +import wtforms + +user = flask.Blueprint('user', __name__, template_folder='./templates/user') +USERS_PATH = "./data/users.json" +class RegisterUserForm(flask_wtf.FlaskForm): + username = wtforms.StringField("Username", [ + wtforms.validators.Length(min=4, max=32), + wtforms.validators.DataRequired() + ]) + password = wtforms.PasswordField("Password", [ + wtforms.validators.Length(min=8, max=64), + wtforms.validators.DataRequired() + ]) + +class LoginUserForm(flask_wtf.FlaskForm): + username = wtforms.StringField("Username", [ + wtforms.validators.DataRequired() + ]) + password = wtforms.PasswordField("Password", [ + wtforms.validators.DataRequired() + ]) + +@user.route('/user/add/', methods=["POST"]) +def add_user(): + + # Get form data + form = RegisterUserForm(csrf_enabled=True) + + username = form.username.data + password = form.password.data + + # Read existing user data + with open(USERS_PATH, 'r') as file: + user_data = json.loads(file.read()) + + # check if user exists + if username in user_data: + return 'ERROR PROCESSING REQUEST - That user already exists' + + # Store password / server side cookie + user_data[username] = base64.b64encode(password.encode()).decode() + flask.session['username'] = username + + # Write user data + with open(USERS_PATH, 'w') as file: + file.write(json.dumps(user_data)) + + return flask.redirect('/') + +@user.route('/user/register/') +def register_page(): + form = RegisterUserForm() + + return flask.render_template('register.html', form=form) + +@user.route('/user/login/', methods=["POST"]) +def login_user(): + form = LoginUserForm(csrf_enabled=True) + + username = form.username.data + password = base64.b64encode(form.password.data.encode()).decode() + + # Read existing user data + with open(USERS_PATH, 'r') as file: + user_data = json.loads(file.read()) + + # check if user exists + if username not in user_data: + return 'ERROR PROCESSING REQUEST - Bad username OR password' + + # Does password match? + if user_data[username] != password: + return 'ERROR PROCESSING REQUEST - Bad username OR password' + + flask.session['username'] = username + + return flask.redirect('/') + +@user.route('/login/') +def login_page(): + form = LoginUserForm() + + return flask.render_template('login.html', form=form) + +@user.route('/logout/') +def logout_user(): + + if 'username' in flask.session: + flask.session.pop('username') + + return flask.redirect('/') + +# Check User file exists +if not os.path.exists(USERS_PATH): + with open(USERS_PATH, 'w+') as file: + file.write('{}') diff --git a/requirements.txt b/requirements.txt index 85ba8d2..16b32de 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,6 @@ Markdown==3.5.2 Flask==2.2.3 waitress==2.1.2 Werkzeug==2.2.3 +Flask-Session==0.5.0 +Flask-WTF==1.1.1 requests==2.31.0 \ No newline at end of file