From 43f9d78f8c00e13732b809519f0529ac293fd5ab Mon Sep 17 00:00:00 2001 From: Mitsuo Tokumori Date: Sun, 29 Oct 2023 07:41:04 -0500 Subject: Rename to ustayml (u-stayML) --- ustayml/views/auth.py | 102 ++++++++++++++++++++++++++++++++++++++++++++ ustayml/views/blog.py | 104 +++++++++++++++++++++++++++++++++++++++++++++ ustayml/views/dashboard.py | 0 3 files changed, 206 insertions(+) create mode 100644 ustayml/views/auth.py create mode 100644 ustayml/views/blog.py create mode 100644 ustayml/views/dashboard.py (limited to 'ustayml/views') diff --git a/ustayml/views/auth.py b/ustayml/views/auth.py new file mode 100644 index 0000000..92710a1 --- /dev/null +++ b/ustayml/views/auth.py @@ -0,0 +1,102 @@ +"""Authentication blueprint""" + +import functools + +from flask import ( + Blueprint, flash, g, redirect, render_template, request, session, url_for +) +from werkzeug.security import check_password_hash, generate_password_hash +from ustayml.db import get_db + +bp = Blueprint('auth', __name__, url_prefix='/auth') + +@bp.route('/register', methods=('GET', 'POST')) +def register(): + if request.method == 'POST': + # Form validation + username = request.form['username'] + password = request.form['password'] + db = get_db() + error = None + + if not username: + error = 'Username is required.' + elif not password: + error = 'Password is required.' + + if error is None: + try: + # NOTE: don't use f-string here. Use `?` placeholders so that + # database library can escape the fields + # (otherwise SQL injection vulnerability) + db.execute( + "INSERT INTO user (username, password) VALUES (?, ?)", + (username, generate_password_hash(password)) + ) + db.commit() + except db.IntegrityError: + error = f"User {username} is already registered." + else: + return redirect(url_for("auth.login")) + + flash(error) + + return render_template('auth/register.html') + + +@bp.route('/login', methods=('GET', 'POST')) +def login(): + if request.method == 'POST': + username = request.form['username'] + password = request.form['password'] + db = get_db() + error = None + user = db.execute( + 'SELECT * FROM user WHERE username = ?', (username,) + ).fetchone() + + if user is None: + error = 'Incorrect username.' + elif not check_password_hash(user['password'], password): + error = 'Incorrect password.' + + if error is None: + session.clear() + session['user_id'] = user['id'] + return redirect(url_for('index')) + + flash(error) + + return render_template('auth/login.html') + + +# runs before the view function, no matter what URL is requested +@bp.before_app_request +def load_logged_in_user(): + user_id = session.get('user_id') + + if user_id is None: + g.user = None + else: + g.user = get_db().execute( + 'SELECT * FROM user WHERE id = ?', (user_id,) + ).fetchone() + + +@bp.route('/logout') +def logout(): + session.clear() + return redirect(url_for('index')) + + +# Define decorator to require authentication in other views +def login_required(view): + """view is a function that returns HTML (and is part of a blueprint)""" + @functools.wraps(view) + def wrapped_view(**kwargs): + if g.user is None: + return redirect(url_for('auth.login')) + + return view(**kwargs) + + return wrapped_view \ No newline at end of file diff --git a/ustayml/views/blog.py b/ustayml/views/blog.py new file mode 100644 index 0000000..57ba4ba --- /dev/null +++ b/ustayml/views/blog.py @@ -0,0 +1,104 @@ +from flask import ( + Blueprint, flash, g, redirect, render_template, request, url_for +) +from werkzeug.exceptions import abort + +from ustayml.views.auth import login_required +from ustayml.db import get_db + +# NOTE: no URL prefix +bp = Blueprint('blog', __name__) + + +@bp.route('/') +def index(): + db = get_db() + posts = db.execute( + 'SELECT p.id, title, body, created, author_id, username' + ' FROM post p JOIN user u ON p.author_id = u.id' + ' ORDER BY created DESC' + ).fetchall() + return render_template('blog/index.html', posts=posts) + + +@bp.route('/create', methods=('GET', 'POST')) +@login_required +def create(): + if request.method == 'POST': + title = request.form['title'] + body = request.form['body'] + error = None + + if not title: + error = 'Title is required.' + + if error is not None: + flash(error) + else: + db = get_db() + db.execute( + 'INSERT INTO post (title, body, author_id)' + ' VALUES (?, ?, ?)', + (title, body, g.user['id']) + ) + db.commit() + return redirect(url_for('blog.index')) + + return render_template('blog/create.html') + + +@bp.route('//update', methods=('GET', 'POST')) +@login_required +def update(id): + post = get_post(id) + + if request.method == 'POST': + title = request.form['title'] + body = request.form['body'] + error = None + + if not title: + error = 'Title is required.' + + if error is not None: + flash(error) + else: + db = get_db() + db.execute( + 'UPDATE post SET title = ?, body = ?' + ' WHERE id = ?', + (title, body, id) + ) + db.commit() + return redirect(url_for('blog.index')) + + return render_template('blog/update.html', post=post) + + +@bp.route('//delete', methods=('POST',)) +@login_required +def delete(id): + get_post(id) + db = get_db() + db.execute('DELETE FROM post WHERE id = ?', (id,)) + db.commit() + return redirect(url_for('blog.index')) + + +# Helper functions: + +def get_post(id, check_author=True): + post = get_db().execute( + 'SELECT p.id, title, body, created, author_id, username' + ' FROM post p JOIN user u ON p.author_id = u.id' + ' WHERE p.id = ?', + (id,) + ).fetchone() + + if post is None: + abort(404, f"Post id {id} doesn't exist.") + + if check_author and post['author_id'] != g.user['id']: + abort(403) + + return post diff --git a/ustayml/views/dashboard.py b/ustayml/views/dashboard.py new file mode 100644 index 0000000..e69de29 -- cgit v1.2.3