mirror of
https://github.com/partitioncloud/partitioncloud-server.git
synced 2025-01-23 09:16:25 +01:00
Add base code
This commit is contained in:
parent
241b947f92
commit
1072956d26
4
.gitignore
vendored
4
.gitignore
vendored
@ -5,5 +5,5 @@
|
|||||||
.vscode
|
.vscode
|
||||||
|
|
||||||
# data
|
# data
|
||||||
partitioncloud/partitioncloud.db
|
instance/partitioncloud.sqlite
|
||||||
|
partitioncloud/partitions
|
||||||
|
7
make.sh
7
make.sh
@ -1,12 +1,13 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
init () {
|
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] "
|
printf "Souhaitez vous supprimer la base de données existante ? [y/n] "
|
||||||
read -r CONFIRMATION
|
read -r CONFIRMATION
|
||||||
fi
|
fi
|
||||||
[[ $CONFIRMATION == y ]] || exit 1
|
[[ $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"
|
echo "Base de données initialisée"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,4 +28,4 @@ else
|
|||||||
usage
|
usage
|
||||||
echo $(type "$1")
|
echo $(type "$1")
|
||||||
exit 1
|
exit 1
|
||||||
fi;
|
fi
|
||||||
|
30
partitioncloud/__init__.py
Normal file
30
partitioncloud/__init__.py
Normal file
@ -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")
|
138
partitioncloud/albums.py
Normal file
138
partitioncloud/albums.py
Normal file
@ -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("/<uuid>")
|
||||||
|
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("/<album_uuid>/<partition_uuid>")
|
||||||
|
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")
|
120
partitioncloud/auth.py
Normal file
120
partitioncloud/auth.py
Normal file
@ -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"))
|
28
partitioncloud/db.py
Normal file
28
partitioncloud/db.py
Normal file
@ -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()
|
@ -5,28 +5,29 @@ DROP TABLE IF EXISTS contient_partition;
|
|||||||
DROP TABLE IF EXISTS contient_user;
|
DROP TABLE IF EXISTS contient_user;
|
||||||
|
|
||||||
CREATE TABLE user (
|
CREATE TABLE user (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
username TEXT UNIQUE NOT NULL,
|
username TEXT UNIQUE NOT NULL,
|
||||||
password TEXT NOT NULL,
|
password TEXT NOT NULL,
|
||||||
access_level INTEGER NOT NULL DEFAULT 0
|
access_level INTEGER NOT NULL DEFAULT 0
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE partition (
|
CREATE TABLE partition (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
uuid TEXT(36) PRIMARY KEY,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
author TEXT,
|
author TEXT,
|
||||||
body TEXT
|
body TEXT
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE album (
|
CREATE TABLE album (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY,
|
||||||
name TEXT NOT NULL
|
name TEXT NOT NULL,
|
||||||
|
uuid TEXT(36) UNIQUE NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE contient_partition (
|
CREATE TABLE contient_partition (
|
||||||
partition_id INTEGER NOT NULL,
|
partition_uuid TEXT(36) NOT NULL,
|
||||||
album_id INTEGER NOT NULL,
|
album_id INTEGER NOT NULL,
|
||||||
PRIMARY KEY (partition_id, album_id)
|
PRIMARY KEY (partition_uuid, album_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE contient_user (
|
CREATE TABLE contient_user (
|
||||||
|
19
partitioncloud/templates/albums/album.html
Normal file
19
partitioncloud/templates/albums/album.html
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block header %}
|
||||||
|
<h1>{% block title %}{{ album["name"] }}{% endblock %}</h1>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% if partitions|length != 0 %}
|
||||||
|
{% for partition in partitions %}
|
||||||
|
<a href="{{ album['uuid'] }}/{{ partition['uuid'] }}">
|
||||||
|
<div class="partition-cover" id="partition-{{ partition['uuid'] }}">
|
||||||
|
{{ partition["name"] }}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div>Aucune partition disponible</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
13
partitioncloud/templates/albums/create-album.html
Normal file
13
partitioncloud/templates/albums/create-album.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block header %}
|
||||||
|
<h1>{% block title %}Nouvel Album{% endblock %}</h1>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form method="post">
|
||||||
|
<label for="name">Nom</label>
|
||||||
|
<input name="name" id="name" required>
|
||||||
|
<input type="submit" value="Créer">
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
22
partitioncloud/templates/albums/index.html
Normal file
22
partitioncloud/templates/albums/index.html
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block header %}
|
||||||
|
<h1>{% block title %}Albums{% endblock %}</h1>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% if albums|length != 0 %}
|
||||||
|
{% for album in albums %}
|
||||||
|
<a href="{{ album['uuid'] }}">
|
||||||
|
<div class="album-cover" id="album-{{ album['id'] }}">
|
||||||
|
{{ album["name"] }}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div>Aucun album disponible</div>
|
||||||
|
{% endif %}
|
||||||
|
<a href="create-album">
|
||||||
|
<button id="new-album">Nouvel Album</button>
|
||||||
|
</a>
|
||||||
|
{% endblock %}
|
15
partitioncloud/templates/auth/login.html
Normal file
15
partitioncloud/templates/auth/login.html
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block header %}
|
||||||
|
<h1>{% block title %}Se connecter{% endblock %}</h1>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form method="post">
|
||||||
|
<label for="username">Nom d'utilisateur</label>
|
||||||
|
<input name="username" id="username" required>
|
||||||
|
<label for="password">Mot de passe</label>
|
||||||
|
<input type="password" name="password" id="password" required>
|
||||||
|
<input type="submit" value="Se connecter">
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
15
partitioncloud/templates/auth/register.html
Normal file
15
partitioncloud/templates/auth/register.html
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block header %}
|
||||||
|
<h1>{% block title %}Créer un compte{% endblock %}</h1>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form method="post">
|
||||||
|
<label for="username">Nom d'utilisateur</label>
|
||||||
|
<input name="username" id="username" required>
|
||||||
|
<label for="password">Mot de passe</label>
|
||||||
|
<input type="password" name="password" id="password" required>
|
||||||
|
<input type="submit" value="Créer un compte">
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
24
partitioncloud/templates/base.html
Normal file
24
partitioncloud/templates/base.html
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<title>{% block title %}{% endblock %} - PartitionCloud</title>
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||||
|
<nav>
|
||||||
|
<h1><a href="{{ url_for('albums.index') }}">PartitionCloud</a></h1>
|
||||||
|
<ul>
|
||||||
|
{% if g.user %}
|
||||||
|
<li><span>{{ g.user['username'] }}</span>
|
||||||
|
<li><a href="{{ url_for('auth.logout') }}">Déconnexion</a>
|
||||||
|
{% else %}
|
||||||
|
<li><a href="{{ url_for('auth.register') }}">Créer un compte</a>
|
||||||
|
<li><a href="{{ url_for('auth.login') }}">Se connecter</a>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<section class="content">
|
||||||
|
<header>
|
||||||
|
{% block header %}{% endblock %}
|
||||||
|
</header>
|
||||||
|
{% for message in get_flashed_messages() %}
|
||||||
|
<div class="flash">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</section>
|
Loading…
Reference in New Issue
Block a user