diff --git a/.gitignore b/.gitignore index 5626294..3b597ff 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,5 @@ .vscode # data -partitioncloud/partitioncloud.db - +instance/partitioncloud.sqlite +partitioncloud/partitions diff --git a/make.sh b/make.sh index ff7e3ad..4f4d919 100755 --- a/make.sh +++ b/make.sh @@ -1,12 +1,13 @@ #!/bin/bash init () { - if [ ! -x partitioncloud/partitioncloud.db ]; then + mkdir -p "instance" + if [ ! -x instance/partitioncloud.sqlite ]; then printf "Souhaitez vous supprimer la base de données existante ? [y/n] " read -r CONFIRMATION fi [[ $CONFIRMATION == y ]] || exit 1 - sqlite3 "partitioncloud/partitioncloud.db" '.read partitioncloud/schema.sql' + sqlite3 "instance/partitioncloud.sqlite" '.read partitioncloud/schema.sql' echo "Base de données initialisée" } @@ -27,4 +28,4 @@ else usage echo $(type "$1") exit 1 -fi; +fi diff --git a/partitioncloud/__init__.py b/partitioncloud/__init__.py new file mode 100644 index 0000000..0d9b0ec --- /dev/null +++ b/partitioncloud/__init__.py @@ -0,0 +1,30 @@ +#!/usr/bin/python3 +""" +Main file +""" +import os +from flask import Flask, render_template, request, send_file, g, redirect + +from . import auth, albums + +app = Flask(__name__) + +app.config.from_mapping( + # a default secret that should be overridden by instance config + SECRET_KEY="dev", + # store the database in the instance folder + DATABASE=os.path.join(app.instance_path, f"{__name__}.sqlite"), +) + +app.register_blueprint(auth.bp) +app.register_blueprint(albums.bp) + + +@app.route("/") +def home(): + """Redirect to home""" + return redirect("/albums/") + + +if __name__ == "__main__": + app.run(host="0.0.0.0") diff --git a/partitioncloud/albums.py b/partitioncloud/albums.py new file mode 100644 index 0000000..f18df50 --- /dev/null +++ b/partitioncloud/albums.py @@ -0,0 +1,138 @@ +#!/usr/bin/python3 +""" +Albums module +""" +import os +from uuid import uuid4 + +from flask import (Blueprint, abort, flash, redirect, render_template, request, + send_file, session) + +from .auth import login_required +from .db import get_db + +bp = Blueprint("albums", __name__, url_prefix="/albums") + + +@bp.route("/") +@login_required +def index(): + db = get_db() + albums = db.execute( + """ + SELECT album.id, name, uuid FROM album + JOIN contient_user ON album_id = album.id + JOIN user ON user_id = user.id + WHERE user.id = ? + """, + (session.get("user_id"),), + ).fetchall() + + return render_template("albums/index.html", albums=albums) + + +@bp.route("/") +def album(uuid): + """ + Album page + """ + db = get_db() + album = db.execute( + """ + SELECT id, name, uuid FROM album + WHERE uuid = ? + """, + (uuid,), + ).fetchone() + + if album is None: + return abort(404) + + partitions = db.execute( + """ + SELECT partition.uuid, partition.name, partition.author FROM partition + JOIN contient_partition ON partition_uuid = partition.uuid + JOIN album ON album.id = album_id + WHERE album.uuid = ? + """, + (uuid,), + ).fetchall() + + return render_template("albums/album.html", album=album, partitions=partitions) + + +@bp.route("//") +def partition(album_uuid, partition_uuid): + """ + Returns a partition in a given album + """ + db = get_db() + partition = db.execute( + """ + SELECT * FROM partition + JOIN contient_partition ON partition_uuid = partition.uuid + JOIN album ON album.id = album_id + WHERE album.uuid = ? + AND partition.uuid = ? + """, + (album_uuid, partition_uuid), + ).fetchone() + + if partition is None: + return abort(404) + + return send_file(os.path.join("partitions", f"{partition_uuid}.pdf")) + + +@bp.route("/create-album", methods=["GET", "POST"]) +@login_required +def create_album(): + if request.method == "POST": + name = request.form["name"] + db = get_db() + error = None + + if not name: + error = "Un nom est requis." + + if error is None: + while True: + try: + uuid = str(uuid4()) + + db.execute( + """ + INSERT INTO album (uuid, name) + VALUES (?, ?) + """, + (uuid, name), + ) + db.commit() + + album_id = db.execute( + """ + SELECT id FROM album + WHERE uuid = ? + """, + (uuid,), + ).fetchone()["id"] + + db.execute( + """ + INSERT INTO contient_user (user_id, album_id) + VALUES (?, ?) + """, + (session.get("user_id"), album_id), + ) + db.commit() + + break + except db.IntegrityError: + pass + + return redirect(f"/albums/{uuid}") + + flash(error) + return render_template("albums/create-album.html") + + return render_template("albums/create-album.html") diff --git a/partitioncloud/auth.py b/partitioncloud/auth.py new file mode 100644 index 0000000..1ffaf69 --- /dev/null +++ b/partitioncloud/auth.py @@ -0,0 +1,120 @@ +#!/usr/bin/python3 +""" +Authentification module +""" +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 .db import get_db + +bp = Blueprint("auth", __name__, url_prefix="/auth") + + +def login_required(view): + """View decorator that redirects anonymous users to the login page.""" + + @functools.wraps(view) + def wrapped_view(**kwargs): + if g.user is None: + return redirect(url_for("auth.login")) + + return view(**kwargs) + + return wrapped_view + + +@bp.before_app_request +def load_logged_in_user(): + """If a user id is stored in the session, load the user object from + the database into ``g.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("/register", methods=("GET", "POST")) +def register(): + """Register a new user. + Validates that the username is not already taken. Hashes the + password for security. + """ + if request.method == "POST": + username = request.form["username"] + password = request.form["password"] + db = get_db() + error = None + + if not username: + error = "Un nom d'utilisateur est requis." + elif not password: + error = "Un mot de passe est requis." + + if error is None: + try: + db.execute( + "INSERT INTO user (username, password) VALUES (?, ?)", + (username, generate_password_hash(password)), + ) + db.commit() + except db.IntegrityError: + # The username was already taken, which caused the + # commit to fail. Show a validation error. + error = f"Le nom d'utilisateur {username} est déjà pris." + else: + # Success, go to the login page. + return redirect(url_for("auth.login")) + + flash(error) + + return render_template("auth/register.html") + + +@bp.route("/login", methods=("GET", "POST")) +def login(): + """Log in a registered user by adding the user id to the session.""" + 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: + # store the user id in a new session and return to the index + session.clear() + session["user_id"] = user["id"] + return redirect(url_for("albums.index")) + + flash(error) + + return render_template("auth/login.html") + + +@bp.route("/logout") +def logout(): + """Clear the current session, including the stored user id.""" + session.clear() + return redirect(url_for("auth.login")) diff --git a/partitioncloud/db.py b/partitioncloud/db.py new file mode 100644 index 0000000..49e5cfa --- /dev/null +++ b/partitioncloud/db.py @@ -0,0 +1,28 @@ +import sqlite3 + +from flask import current_app +from flask import g + + +def get_db(): + """Connect to the application's configured database. The connection + is unique for each request and will be reused if this is called + again. + """ + if "db" not in g: + g.db = sqlite3.connect( + current_app.config["DATABASE"], detect_types=sqlite3.PARSE_DECLTYPES + ) + g.db.row_factory = sqlite3.Row + + return g.db + + +def close_db(e=None): + """If this request connected to the database, close the + connection. + """ + db = g.pop("db", None) + + if db is not None: + db.close() diff --git a/partitioncloud/schema.sql b/partitioncloud/schema.sql index 7a3ab8a..10dae17 100644 --- a/partitioncloud/schema.sql +++ b/partitioncloud/schema.sql @@ -5,28 +5,29 @@ DROP TABLE IF EXISTS contient_partition; DROP TABLE IF EXISTS contient_user; CREATE TABLE user ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT UNIQUE NOT NULL, - password TEXT NOT NULL, + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password TEXT NOT NULL, access_level INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE partition ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, + uuid TEXT(36) PRIMARY KEY, + name TEXT NOT NULL, author TEXT, body TEXT ); CREATE TABLE album ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + uuid TEXT(36) UNIQUE NOT NULL ); CREATE TABLE contient_partition ( - partition_id INTEGER NOT NULL, + partition_uuid TEXT(36) NOT NULL, album_id INTEGER NOT NULL, - PRIMARY KEY (partition_id, album_id) + PRIMARY KEY (partition_uuid, album_id) ); CREATE TABLE contient_user ( diff --git a/partitioncloud/templates/albums/album.html b/partitioncloud/templates/albums/album.html new file mode 100644 index 0000000..a0d0633 --- /dev/null +++ b/partitioncloud/templates/albums/album.html @@ -0,0 +1,19 @@ +{% extends 'base.html' %} + +{% block header %} +

{% block title %}{{ album["name"] }}{% endblock %}

+{% endblock %} + +{% block content %} +{% if partitions|length != 0 %} + {% for partition in partitions %} + +
+ {{ partition["name"] }} +
+
+ {% endfor %} +{% else %} +
Aucune partition disponible
+{% endif %} +{% endblock %} \ No newline at end of file diff --git a/partitioncloud/templates/albums/create-album.html b/partitioncloud/templates/albums/create-album.html new file mode 100644 index 0000000..2d620a0 --- /dev/null +++ b/partitioncloud/templates/albums/create-album.html @@ -0,0 +1,13 @@ +{% extends 'base.html' %} + +{% block header %} +

{% block title %}Nouvel Album{% endblock %}

+{% endblock %} + +{% block content %} +
+ + + +
+{% endblock %} \ No newline at end of file diff --git a/partitioncloud/templates/albums/index.html b/partitioncloud/templates/albums/index.html new file mode 100644 index 0000000..aab6c47 --- /dev/null +++ b/partitioncloud/templates/albums/index.html @@ -0,0 +1,22 @@ +{% extends 'base.html' %} + +{% block header %} +

{% block title %}Albums{% endblock %}

+{% endblock %} + +{% block content %} +{% if albums|length != 0 %} + {% for album in albums %} + +
+ {{ album["name"] }} +
+
+ {% endfor %} +{% else %} +
Aucun album disponible
+{% endif %} + + + +{% endblock %} \ No newline at end of file diff --git a/partitioncloud/templates/auth/login.html b/partitioncloud/templates/auth/login.html new file mode 100644 index 0000000..91ab850 --- /dev/null +++ b/partitioncloud/templates/auth/login.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} + +{% block header %} +

{% block title %}Se connecter{% endblock %}

+{% endblock %} + +{% block content %} +
+ + + + + +
+{% endblock %} \ No newline at end of file diff --git a/partitioncloud/templates/auth/register.html b/partitioncloud/templates/auth/register.html new file mode 100644 index 0000000..401f119 --- /dev/null +++ b/partitioncloud/templates/auth/register.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} + +{% block header %} +

{% block title %}Créer un compte{% endblock %}

+{% endblock %} + +{% block content %} +
+ + + + + +
+{% endblock %} \ No newline at end of file diff --git a/partitioncloud/templates/base.html b/partitioncloud/templates/base.html new file mode 100644 index 0000000..9696748 --- /dev/null +++ b/partitioncloud/templates/base.html @@ -0,0 +1,24 @@ + +{% block title %}{% endblock %} - PartitionCloud + + +
+
+ {% block header %}{% endblock %} +
+ {% for message in get_flashed_messages() %} +
{{ message }}
+ {% endfor %} + {% block content %}{% endblock %} +
\ No newline at end of file diff --git a/wsgi.py b/wsgi.py new file mode 100644 index 0000000..0c618e3 --- /dev/null +++ b/wsgi.py @@ -0,0 +1,5 @@ +#!/usr/bin/python3 +from partitioncloud import app + +if __name__ == "__main__": + app.run()