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 231d55e..0000000 Binary files a/application.db and /dev/null differ diff --git a/helpers.py b/helpers.py index 5e5b26b..9cdd2cf 100644 --- a/helpers.py +++ b/helpers.py @@ -1,3 +1,42 @@ +import os +import random +import re +import smtplib +import string +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from functools import wraps +from flask import session, render_template, redirect + + +def getCode(length): + # generate random string with length size""" + code = string.ascii_lowercase + return ''.join(random.choice(code) for i in range(length)) + + +def activationMail(email, username, code): + msg = MIMEMultipart() + msg['From'] = 'moviesearch@noreply.npak0382.odns.fr' + msg['To'] = email + msg['Subject'] = 'movieSearch Account activation' + message = """ Hello """ + username + """ + +Please confirm your account using the following code: """ + code + """ + +You can use this link to activate your account http://127.0.0.1:5000/activate?email=""" + email + """&code=""" + code + """ + +Best regards, + +Movie-Search Team + """ + msg.attach(MIMEText(message)) + mailServer = smtplib.SMTP_SSL('kapre.o2switch.net', 465) + mailServer.login('moviesearch@noreply.npak0382.odns.fr', os.environ.get('EMAIL_PASSWORD')) + mailServer.sendmail('moviesearch@noreply.npak0382.odns.fr', email, msg.as_string()) + mailServer.quit() + return True + # Check if user is logged in def login_required(f): @@ -11,4 +50,31 @@ def decorated_function(*args, **kwargs): if session.get("user_id") is None: return redirect("/login") return f(*args, **kwargs) + return decorated_function + + +# Check if the password match minimum requirements +# Here one capital letter, one number and one special character +def match_requirements(password, min_size=0): + if not password: + return False + if re.search("[A-Z]", password) and re.search("[0-9]", password) and re.search("[!@#$%^&*(),.?:{}|<>+-]", 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 @@