diff --git a/README.md b/README.md index 6949af2..482789c 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,15 @@ The blog is intended to be used to review and critique things. ## Features/To-Dos -- [ ] Plain text support for blog entries - - [ ] HTML files (.html) - - [ ] Markdown Files (.md) + +- [ ] Accounts + - [x] Login + - [x] Logout + - [ ] Register +- [ ] Review blog entries + - [ ] Writing entries + - [ ] Editing entries + - [ ] Deleting entries - [ ] Infinite-scroll blog page - [ ] Archive page - [ ] Months as headings @@ -16,14 +22,16 @@ The blog is intended to be used to review and critique things. - [ ] Standalone article page - [ ] Links to scrolling blog page - [ ] RSS feed -- [ ] Navigation - - [ ] Header - - [ ] Footer -- [ ] Switchable CSS - - [ ] CSS dark-theme - - [ ] CSS light-theme -- [ ] Config file -- [ ] Docker installation +- [ ] Eye candy + - [ ] Star rating + - [ ] Rich text editor +- [x] Navigation + - [x] Header + - [x] Footer +- [x] Switchable CSS + - [x] CSS dark-theme + - [x] CSS light-theme +- [x] Docker installation - [ ] Logo ## Usage @@ -48,12 +56,11 @@ The `config.py` can be found in the `src` folder. Set the following volumes with the -v tag. -| Volume-Name | Container mount | Description | -| ------------- | --------------------------- | ------------------------------------------------------------ | -| `config-file` | `/blog/src/config.py` | Config file | -| `entries` | `/blog/src/templates/entry` | Directory for blog entries | -| `css` | `/blog/src/static/css` | (optional) Directory for css files | -| `html` | `/blog/src/templates` | (optional) Directory for templates (entry-volume not needed) | +| Volume-Name | Container mount | Description | +| ------------- | ---------------------- | ---------------------------------- | +| `config-file` | `/blog/src/config.py` | Config file | +| `css` | `/blog/src/static/css` | (optional) Directory for css files | +| `html` | `/blog/src/templates` | (optional) Directory for templates | #### Ports diff --git a/beaker_blog_alt.png b/beaker_blog_alt.png deleted file mode 100644 index 5ad0e0f..0000000 Binary files a/beaker_blog_alt.png and /dev/null differ diff --git a/src/app.py b/src/app.py index 04a8f49..ab3ad17 100644 --- a/src/app.py +++ b/src/app.py @@ -1,58 +1,24 @@ from flask import Flask, flash, make_response, render_template, request, redirect, abort, url_for from flask_login import current_user, login_user, LoginManager, logout_user +from flask_wtf import CSRFProtect import content as con_gen import config -from flask_wtf import FlaskForm -from wtforms import StringField, PasswordField, SubmitField, BooleanField -from wtforms.validators import DataRequired - app = Flask(__name__) -login = LoginManager(app) -login.login_view = 'login' +csrf = CSRFProtect() +app.secret_key = "123534" +csrf.init_app(app) -class LoginForm(FlaskForm): - username = StringField("Username", validators=[DataRequired()]) - password = PasswordField("Password", validators=[DataRequired()]) - remember_me = BooleanField("Remember Me") - submit = SubmitField("Sign In") +login = LoginManager(app) +login.login_view = "login" TITLE = config.TITLE STYLE = config.STYLE DESCRIPTION = config.DESCRIPTION WEBSITE = config.WEBSITE -from werkzeug.security import generate_password_hash, check_password_hash - -class User(): - - def __init__(self, username): - self.username = username - self.id = 1 - self.is_active = True - self.is_authenticated = False - self.is_anonymous = False - - def set_password(self, password): - self.password_hash = generate_password_hash(password) - - def check_password(self, password): - return check_password_hash(self.password_hash, password) - - def get_id(self): - return self.id - -u = User("marten") -u.set_password("test") - -class Config(object): - SECRET_KEY = "123534" - -app.config.from_object(Config) - - @app.errorhandler(404) def page_not_found(e): return render_template("error.html", title=TITLE, errorcode="404", style=STYLE), 404 @@ -67,7 +33,7 @@ def index(): @app.route("/archive") @app.route("/archive.html") -def blog_archive(): +def archive(): content = con_gen.gen_arch_string() return render_template("archive.html", title=TITLE, content_string=content, style=STYLE) @@ -91,24 +57,36 @@ def feed(): return response @login.user_loader -def load_user(id): +def load_user(ident): ## TODO: load user from db by id - return id + db_user = db.get_by_id(ident) + if db_user is not None: + return db.db_to_user(*db_user) + return None + +from login import LoginForm, User +from database import Database + +db = Database() @app.route("/login", methods=["GET", "POST"]) +@app.route("/login.html", methods=["GET", "POST"]) def login(): - #if current_user.is_authenticated: - # return redirect("/index") + if current_user.is_authenticated: + return redirect(url_for("index")) form = LoginForm() if form.validate_on_submit(): - user = u - #user = form.username.data - if user is None or not user.check_password(form.password.data): + db_user = db.get_by_name(form.username.data) + if db_user is None: + flash("Invalid username or password") + return redirect(url_for("login")) + user = db.db_to_user(*db_user) + if not user.check_password(form.password.data): flash("Invalid username or password") return redirect(url_for("login")) login_user(user, remember=form.remember_me.data) return redirect(url_for("index")) - return render_template("login.html", title="Sign In", form=form, style=STYLE) + return render_template("login.html", title=TITLE, form=form, style=STYLE) @app.route('/logout') def logout(): diff --git a/src/content.py b/src/content.py index a634cd0..458c6d2 100644 --- a/src/content.py +++ b/src/content.py @@ -1,5 +1,3 @@ -ENTRY_DIR = "templates/entry" - def gen_arch_string(): """ Creates and returns a archive string of every file in ENTRY_DIR. diff --git a/src/data.db b/src/data.db new file mode 100644 index 0000000..ae25c7d Binary files /dev/null and b/src/data.db differ diff --git a/src/database.py b/src/database.py new file mode 100644 index 0000000..6ef3a3a --- /dev/null +++ b/src/database.py @@ -0,0 +1,66 @@ +import os +import sqlite3 + +from login import User + + +class Database: + + def __init__(self): + self.TABLE_FILE = 'USERS' + self.DB_DIR = os.path.dirname(".") + self.setup_db() + + def connect(self): + """Connect to an existing database instance based on the object + attributes. + """ + path = os.path.join(self.DB_DIR, "data.db") + return sqlite3.connect(path) + + def setup_db(self): + """Creates a database with tables.""" + db = self.connect() + crs = db.cursor() + query = "CREATE TABLE IF NOT EXISTS " + self.TABLE_FILE + \ + "(id INTEGER PRIMARY KEY AUTOINCREMENT," + \ + "name CHAR(32) NOT NULL UNIQUE," + \ + "password CHAR(32) NOT NULL)" + crs.execute(query) + + def insert_user(self, name, password): + """Insert a new user into the database. + """ + if self.check_name(name): + db = self.connect() + crs = db.cursor() + query = "INSERT INTO " + self.TABLE_FILE + "(`name`,`password`)" + \ + "VALUES (?, ?) ON CONFLICT DO NOTHING" + crs.execute(query, (name, password)) + db.commit() + return True + return False + + def check_name(self, name): + if self.get_by_name(name) is None: + return True + return False + + def get_by_id(self, ident): + db = self.connect() + crs = db.cursor() + query = "SELECT * FROM " + self.TABLE_FILE + " WHERE id = ?" + crs.execute(query, (ident, )) + return crs.fetchone() + + def get_by_name(self, name): + db = self.connect() + crs = db.cursor() + query = "SELECT * FROM " + self.TABLE_FILE + " WHERE name = ?" + crs.execute(query, (name, )) + return crs.fetchone() + + def db_to_user(self, ident, name, pass_hash): + user = User(name, pass_hash) + user.set_id(ident) + return user diff --git a/src/login.py b/src/login.py new file mode 100644 index 0000000..c7220c5 --- /dev/null +++ b/src/login.py @@ -0,0 +1,34 @@ +from flask_wtf import FlaskForm +from wtforms import StringField, PasswordField, SubmitField, BooleanField +from wtforms.validators import DataRequired + +from werkzeug.security import generate_password_hash, check_password_hash + + +class User(): + + def __init__(self, name, pass_hash=None): + self.name = name + self.id = 0 + self.is_active = True + self.is_authenticated = True + self.is_anonymous = False + self.pass_hash = pass_hash + + def set_password(self, password): + self.pass_hash = generate_password_hash(password) + + def set_id(self, ident): + self.id = ident + + def check_password(self, password): + return check_password_hash(self.pass_hash, password) + + def get_id(self): + return self.id + +class LoginForm(FlaskForm): + username = StringField("Username", validators=[DataRequired()]) + password = PasswordField("Password", validators=[DataRequired()]) + remember_me = BooleanField("Remember Me") + submit = SubmitField("Sign In") diff --git a/src/requirements.txt b/src/requirements.txt index 6b32b3a..120c115 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -1,4 +1,5 @@ Flask==2.1.2 -flask_login==0.6.2 -flask_wtf==1.0.1 -WTForms==3.0.1 +Flask_Login==0.6.2 +Flask_WTF==0.14.3 +Werkzeug==2.1.2 +WTForms==2.2.1 diff --git a/src/static/images/logo.png b/src/static/images/logo.png deleted file mode 100644 index e69de29..0000000 diff --git a/src/templates/login.html b/src/templates/login.html index 705201c..0c1f14c 100644 --- a/src/templates/login.html +++ b/src/templates/login.html @@ -1,18 +1,26 @@ {% extends "template.html" %} {% block content %} -

Sign In

-
- {{ form.hidden_tag() }} -

- {{ form.username.label }}
- {{ form.username(size=32) }} -

-

- {{ form.password.label }}
- {{ form.password(size=32) }} -

-

{{ form.remember_me() }} {{ form.remember_me.label }}

-

{{ form.submit() }}

-
+ +
+
+

Sign In

+
+ {{ form.hidden_tag() }} +

+ {{ form.username.label }}
+ {{ form.username(size=32) }} +

+

+ {{ form.password.label }}
+ {{ form.password(size=32) }} +

+

{{ form.remember_me() }} {{ form.remember_me.label }}

+

{{ form.submit() }}

+ {% for mesg in get_flashed_messages() %} +

{{ mesg }}

+ {% endfor %} +
+
+
{% endblock %} diff --git a/src/templates/template.html b/src/templates/template.html index cd4ce82..d900ed4 100644 --- a/src/templates/template.html +++ b/src/templates/template.html @@ -13,13 +13,13 @@