From d0d5063314d3d40558c0f9df882ad837fb63d956 Mon Sep 17 00:00:00 2001 From: Joshua Vayer <77019747+joshuaVayer@users.noreply.github.com> Date: Sat, 26 Jun 2021 18:31:20 +0200 Subject: [PATCH] =?UTF-8?q?Release=20V1.b:=20Add=20login=20procedure,=20ba?= =?UTF-8?q?sic=20API=20call=20and=20main=20routes=20=F0=9F=8E=89=20(#15)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Modify username by email and improve auth and register form * Add activation mail with code * Update email password to env.variable * Improve readability of code, add active link in email * Bug fix at check for account already activate * Feature: add the login_required decorator to required routes (#9) * Feature: add the error handling function (#10) * Feature: Add password minimum requirements and checks (#11) * Feature: add the API querying function and parsing request (#13) * Maintenance: removing database from remote (#14) Co-authored-by: HNTQ --- .gitignore | 3 + .idea/Movie-Search-CS50.iml | 1 + .idea/misc.xml | 2 +- README.md | 4 + api.py | 55 +++++ app.py | 187 +++++++++++++---- application.db | Bin 61440 -> 0 bytes helpers.py | 66 ++++++ templates/activation.html | 23 ++ templates/errors.html | 4 + templates/index.html | 10 + templates/layout.html | 4 +- templates/login.html | 12 +- templates/{user_profil.html => profil.html} | 0 templates/register.html | 37 +++- templates/results.html | 5 - templates/scripts/passirate.js | 221 ++++++++++++++++++++ templates/search.html | 41 ++++ 18 files changed, 619 insertions(+), 56 deletions(-) create mode 100644 api.py delete mode 100644 application.db create mode 100644 templates/activation.html rename templates/{user_profil.html => profil.html} (100%) delete mode 100644 templates/results.html create mode 100644 templates/scripts/passirate.js create mode 100644 templates/search.html diff --git a/.gitignore b/.gitignore index 179a312..a1cb451 100644 --- a/.gitignore +++ b/.gitignore @@ -145,3 +145,6 @@ dmypy.json # Cython debug symbols cython_debug/ +# Database +application.db + diff --git a/.idea/Movie-Search-CS50.iml b/.idea/Movie-Search-CS50.iml index 6ba3af4..70b2d6d 100644 --- a/.idea/Movie-Search-CS50.iml +++ b/.idea/Movie-Search-CS50.iml @@ -6,6 +6,7 @@ + diff --git a/.idea/misc.xml b/.idea/misc.xml index 9f95cf4..f94c9de 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/README.md b/README.md index b290085..d26496d 100644 --- a/README.md +++ b/README.md @@ -27,3 +27,7 @@ then run application ```bash flask run ``` + +## Database + +Versions of the database are stored [here](https://drive.google.com/drive/folders/1HBSa8qETHNVWOOtPGPnlrb54g0_1akBr?usp=sharing). (Only accessible for authorized members) diff --git a/api.py b/api.py new file mode 100644 index 0000000..d27adc7 --- /dev/null +++ b/api.py @@ -0,0 +1,55 @@ +""" +All API queries, parsing, and formatting functions are stored in this file +This project use the TMD API. +""" +import os +import requests + +API_KEY = os.environ.get("API_KEY") + + +# Queries +def query_by_title(title): + """Will look for all types""" + try: + url = f"https://api.themoviedb.org/3/search/multi?api_key={API_KEY}&query={title}" + response = requests.get(url) + response.raise_for_status() + except requests.RequestException: + return None + + return response.json() + + +# Parse and formats +def parse_query_by_title(response): + """ Will return the data used in search movie for the query by title""" + parsed_response = { + "movies": [], + "series": [] + } + if not response: + return response + + def generic(r): + return { + "id": r["id"], + "poster_path": r["poster_path"], + "backdrop_path": r["backdrop_path"], + "overview": r["overview"], + "media_type": r["media_type"], + "vote_average": r["vote_average"] + } + + for result in response["results"]: + if "movie" in result["media_type"]: + movie_dict = generic(result) + movie_dict["title"] = result["original_title"] + movie_dict["release_date"] = result["release_date"] + parsed_response["movies"].append(movie_dict) + elif "tv" in result["media_type"]: + series_dict = generic(result) + movie_dict["title"] = result["name"] + parsed_response["series"].append(series_dict) + + return parsed_response diff --git a/app.py b/app.py index 3c797ed..12fa929 100644 --- a/app.py +++ b/app.py @@ -1,8 +1,13 @@ -from flask import Flask, redirect, render_template, request, session +from flask import Flask, redirect, render_template, request, session, url_for from cs50 import SQL from flask_session import Session from tempfile import mkdtemp from werkzeug.security import check_password_hash, generate_password_hash + +from api import query_by_title, parse_query_by_title +from helpers import getCode, activationMail, login_required, handle_error, match_requirements + + app = Flask(__name__) # Ensure templates are auto-reloaded @@ -34,22 +39,30 @@ def login(): # Forget any user_id session.clear() if request.method == "POST": + + email = request.form.get("email") + password = request.form.get("password") + # Ensure username was submitted - if not request.form.get("username"): - # TODO apology "must provide username" - return render_template("register.html") + if not email: + message = "Must provide email" + return render_template("login.html", message=message) + # Ensure password was submitted - elif not request.form.get("password"): - #TODO apology "must provide password" - return render_template("register.html") + if not password: + message = "Must provide password" + return render_template("login.html", message=message) # Query database for username - rows = db.execute("SELECT * FROM user WHERE username = ?", request.form.get("username")) + rows = db.execute("SELECT * FROM user WHERE email = ?", email.lower()) # Ensure username exists and password is correct - if len(rows) != 1 or not check_password_hash(rows[0]["hash"], request.form.get("password")): - return render_template("register.html") - # TODO Apology "invalid username and/or password", 403 + if len(rows) != 1 or not check_password_hash(rows[0]["hash"], password): + message = "Invalid username and/or password" + return render_template("login.html", message=message) + + if not rows[0]["active"]: + return redirect(url_for("activate", email=email.lower())) # Remember which user has logged in session["user_id"] = rows[0]["id"] @@ -58,42 +71,124 @@ def login(): return redirect("/") # User reached route via GET (as by clicking a link or via redirect) else: - return render_template("login.html") + if request.args.get('message'): + message = request.args.get('message') + else: + message = "" + return render_template("login.html", message=message) @app.route("/register", methods=["GET", "POST"]) def register(): """Register user""" if request.method == "POST": - # Ensure username was fill - if not request.form.get("username"): - return render_template("register.html") - #TODO return apology("must provide username", 400) + + username = request.form.get("username") + email = request.form.get("email") + password = request.form.get("password") + confirmPassword = request.form.get("confirmation") + + # Ensure username was submitted + if not username: + message = "Must provide username" + return render_template("register.html", message=message) + + # Ensure Email was submitted + if not email: + message = "Must provide email" + return render_template("register.html", message=message) # Ensure password was submitted - if not request.form.get("password"): - return render_template("register.html") - #TODO return apology("must provide password", 400) + if not password: + message = "Must provide password" + return render_template("register.html", message=message) # Ensure password was confirmed - if not request.form.get("confirmation"): - return render_template("register.html") - #TODO return apology("must confirm password ", 400) + if not confirmPassword: + message = "Must confirm password" + return render_template("register.html", message=message) + + if confirmPassword != password: + message = "Passwords do not match" + return render_template("register.html", message=message) + + if not match_requirements(password, 10): + message = "Password do not match the minimum requirements" + return render_template("register.html", message=message) + + if db.execute("SELECT * FROM user WHERE email = ?", email.lower()) != []: + message = "Email already used" + return render_template("register.html", message=message) + hash = generate_password_hash(password, method='pbkdf2:sha256', salt_length=8) + db.execute("INSERT INTO user (username, email, hash) VALUES(?, ?, ?)", username, email.lower(), hash) + + #mailing + code = getCode(8) + activationMail(email, username, code) + + #save activation code + userId = db.execute("SELECT id FROM user WHERE email = ?", email.lower()) + db.execute("INSERT INTO activation (user_id, activation_code) VALUES(?, ?)", userId[0]["id"], code) + return redirect(url_for("activate", email=email.lower())) + else: + return render_template("register.html") - if request.form.get("confirmation") != request.form.get("password"): - return render_template("register.html") - #TODO return apology("passwords do not match", 400) +@app.route("/activate", methods=["GET","POST"]) +def activate(): + if request.method == "POST": - if db.execute("SELECT * FROM user WHERE username = ?", request.form.get("username")) != []: - return render_template("register.html") - #TODO return apology("user exist", 400) + email = request.form.get("email") + confirmCode = request.form.get("confirm") + # Ensure Email was submitted + if not email: + message = "Must provide email" + return render_template("activation.html", message=message) - db.execute("INSERT INTO user (username, hash) VALUES(?, ?)", request.form.get("username"), - generate_password_hash(request.form.get("password"), method='pbkdf2:sha256', salt_length=8)) - return redirect("/login") + # Ensure password was submitted + if not confirmCode: + message = "Must provide confirmation code" + return render_template("activation.html", message=message) + + rows = db.execute("SELECT * FROM activation WHERE user_id = (SELECT id FROM user WHERE email = ?)", email.lower()) + user = db.execute("SELECT active FROM user WHERE email = ?", email.lower()) + + + if len(user) == 1 and user[0]["active"] == 1: + message = "Account already activated" + return redirect(url_for("login", message=message)) + if len(rows) != 1 or rows[0]["activation_code"] != confirmCode: + message = "Invalid Email or confirmation code" + return render_template("activation.html", message=message) + db.execute("DELETE FROM activation WHERE id =?",rows[0]["id"]) + db.execute("UPDATE user SET active = true WHERE id = ?", rows[0]["user_id"]) + message = "Account activated" + return redirect(url_for("login", message=message)) else: - return render_template("register.html") + code = "" + email = "" + if request.args.get('email'): + email = request.args.get('email') + + if request.args.get('code'): + code = request.args.get('code') + + if code and email: + rows = db.execute("SELECT * FROM activation WHERE user_id = (SELECT id FROM user WHERE email = ?)", email.lower()) + user = db.execute("SELECT active FROM user WHERE email = ?", email.lower()) + if len(user) == 1 and user[0]["active"] == 1: + message = "Account already activated" + return redirect(url_for("login", message=message)) + if len(rows) != 1 or rows[0]["activation_code"] != code: + message = "Invalid Email or confirmation code" + return render_template("activation.html", message=message) + db.execute("DELETE FROM activation WHERE id =?", rows[0]["id"]) + db.execute("UPDATE user SET active = true WHERE id = ?", rows[0]["user_id"]) + message = "Account activated" + return redirect(url_for("login", message=message)) + return render_template("activation.html", email=email, code=code) + @app.route("/logout") +@login_required def logout(): """Log user out""" # Forget any user_id @@ -102,17 +197,33 @@ def logout(): # Redirect user to login form return redirect("/") -@app.route("/userProfil", methods=["GET", "POST"]) -def userProfil(): - return render_template("user_profil.html") +@app.route("/profil", methods=["GET", "POST"]) +@login_required +def profil(): + return render_template("profil.html") @app.route("/parameters", methods=["GET", "POST"]) +@login_required def parameters(): return render_template("parameters.html") -@app.route("/results", methods=["GET", "POST"]) -def results(): - return render_template("results.html") +@app.route("/search") +def search(): + """Basic search by title, can take category filters""" + # Assignment and checks + title = request.args.get('title') + # filters = get_categories(request.args.get('filters')) + + if not title: + return render_template("/search.html", error="Please submit a valid search") + + # Corresponding Api request + query = query_by_title(title) + results = parse_query_by_title(query) + + return render_template("search.html", + movies=results["movies"], + series=results["series"]) @app.route("/details", methods=["GET", "POST"]) def details(): diff --git a/application.db b/application.db deleted file mode 100644 index 231d55e44cf77a58d073a4d7dbac3d68525e7145..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 61440 zcmeI)-)`Gf9KdnA+ma@0n!zZhbV$~^X^GZC;xy?WKoCZ>Rr|x}3ev7pCw5nBiPPE6 zv`R=*Iv#*$;6b?J1(3MoYT|-Nn7HHkPcnO=1qme;)z_;39sBs4-{%}Vb)EXb*LN(> zRCjw$SNGJ4bU~73>2p<;BEf|xYJPI*tcNoU2{p~ES~>PQj~$SkRI3{q?N@* z`Ng7t@WGB)o12T)%)VQ_Sjw!U@aR(-2bt`$dl#dfo+TbLmWL{zbnHoLv2mgzRj@wL>Ta51f1y(%AG44NUnExvpL@1>Mam7{hH3T&2x zS$gn9N&7W=QGcUG=aO*2-v7WBwu|fOj_aRTLP~ForfLRxqZGH-w~a(f-?pBbj+;mu z^#(sz`~-_AkLrDi&gIjO4is;N>+hzN&*qQfiL#W#d2VoXF0IVZ%ZF9~6sCsLnIut| zc$`u4>LPrCEKRR#wEX@QnkB9;VTUtJx9wQ_p4GEQk*}5fu4wcHZag{6u-|h%(;4ln zl}e)X72iItSA?fxc+JQUmfs3`#o>Aobg(U$%qBw)(vFK*RPk@(X^VdUNSwo?3jWvg zN|NNI;KKhPsIT8k{BbH#+Z{h?M=j*Cs=8wtJL**OEB@cVJ>-tJ{RVx|06|JO|mrJ$f@|voZHcG1-m6h=R|CN+~mH%Tr zA#zj%5I_I{1Q0*~0R#|0009ILn4G|T@O`!DD-G$XGjqYZfB!GO^v{3*0tg_000Iag zfB*srAb$* zfbtPQ009ILKmY**5I_I{1P~a%K+-]", password) and re.search("[a-z]", password): + if len(password) >= min_size: + return True + return False + + +def handle_error(message, code=400): + """Render message as an apology to user.""" + def escape(s): + """ + Escape special characters. + + https://github.com/jacebrowning/memegen#special-characters + """ + for old, new in [("-", "--"), (" ", "-"), ("_", "__"), ("?", "~q"), + ("%", "~p"), ("#", "~h"), ("/", "~s"), ("\"", "''")]: + s = s.replace(old, new) + return s + return render_template("errors.html", message=message), code diff --git a/templates/activation.html b/templates/activation.html new file mode 100644 index 0000000..97cb296 --- /dev/null +++ b/templates/activation.html @@ -0,0 +1,23 @@ +{% extends "layout.html" %} + +{% block title %} + Activation +{% endblock %} + +{% block main %} +
+
+ +
+
+ +
+ +
+ {% if message %} +
{{ message }}
+ {% endif %} + +{% endblock %} \ No newline at end of file diff --git a/templates/errors.html b/templates/errors.html index 80cdaad..a4af5e0 100644 --- a/templates/errors.html +++ b/templates/errors.html @@ -2,4 +2,8 @@ {% block title %} Error +{% endblock %} + +{% block main %} +

{{ message }}

{% endblock %} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index d23714d..1eb9e99 100644 --- a/templates/index.html +++ b/templates/index.html @@ -2,4 +2,14 @@ {% block title %} Home +{% endblock %} + +{% block main %} + +
+ + + +
+ {% endblock %} \ No newline at end of file diff --git a/templates/layout.html b/templates/layout.html index 56385a8..c2feb76 100644 --- a/templates/layout.html +++ b/templates/layout.html @@ -11,7 +11,7 @@