diff --git a/OpenLearn/app.py b/OpenLearn/app.py index db8a061..138bb81 100644 --- a/OpenLearn/app.py +++ b/OpenLearn/app.py @@ -6,7 +6,7 @@ from flask import Flask, render_template -from OpenLearn import extensions, database +from OpenLearn import extensions, database, commands from . import views from . import settings @@ -31,6 +31,13 @@ def create_app(): def register_extensions(app): """Register Flask extensions.""" extensions.db.init_app(app) + extensions.bcrypt.init_app(app) + extensions.login_manager.init_app(app) + + # Flask Login + @extensions.login_manager.user_loader + def load_user(user_id): + return database.User.query.get(int(user_id)) def register_blueprints(app): @@ -75,7 +82,7 @@ def shell_context(): def register_commands(app): """Register Click commands.""" - pass + app.cli.add_command(commands.rebuild_database) def configure_logger(app): diff --git a/OpenLearn/database.py b/OpenLearn/database.py index 9b8987c..c854a1c 100644 --- a/OpenLearn/database.py +++ b/OpenLearn/database.py @@ -6,9 +6,10 @@ from decimal import Decimal from typing import Union, List +from flask_login import UserMixin from sqlalchemy import func -from .extensions import db +from .extensions import db, bcrypt # Alias common SQLAlchemy names Column = db.Column @@ -35,15 +36,30 @@ def type(self): }[self] -class User(db.Model): +class User(db.Model, UserMixin): id: MInteger = db.Column(db.Integer, primary_key=True) username: MString = db.Column(db.String(25), unique=True, nullable=False) - password = db.Column(db.Binary(60), nullable=False) + __password = db.Column("password", db.Binary(60), nullable=False) quizzes: List["Quiz"] = relationship("Quiz", back_populates="owner") created_on: MDateTime = db.Column(db.DateTime(timezone=True), nullable=False, server_default=func.now()) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + @property + def password(self): + return self.__password + + @password.setter + def password(self, value): + pw_hash = bcrypt.generate_password_hash(value) + self.__password = pw_hash + + def check_password(self, candidate) -> bool: + return bcrypt.check_password_hash(self.__password, candidate) + class Quiz(db.Model): id: MInteger = db.Column(db.Integer, primary_key=True) diff --git a/OpenLearn/extensions.py b/OpenLearn/extensions.py index 6aeb491..a9500fd 100644 --- a/OpenLearn/extensions.py +++ b/OpenLearn/extensions.py @@ -1,5 +1,9 @@ # -*- coding: utf-8 -*- +from flask_bcrypt import Bcrypt +from flask_login import LoginManager from flask_sqlalchemy import SQLAlchemy db = SQLAlchemy() +bcrypt = Bcrypt() +login_manager = LoginManager() \ No newline at end of file diff --git a/OpenLearn/views/public/controller.py b/OpenLearn/views/public/controller.py index b8f872d..6d43dbf 100644 --- a/OpenLearn/views/public/controller.py +++ b/OpenLearn/views/public/controller.py @@ -4,8 +4,11 @@ Blueprint, current_app, render_template, - abort, redirect) + abort, redirect, url_for) +from flask_login import login_user +from OpenLearn.database import User +from OpenLearn.extensions import db from .forms import RegisterForm, LoginForm blueprint = Blueprint("public", __name__, static_folder="../../static", template_folder="../../templates/public") @@ -24,11 +27,12 @@ def index(): return render_template("index.html") -@blueprint.route("/sign-in") +@blueprint.route("/login") def sign_in(): form = LoginForm() if form.validate_on_submit(): - return redirect("/success") + login_user(form.user) + return redirect(url_for("teacher.dashboard")) return render_template("sign-in.html", form=form) @@ -36,5 +40,9 @@ def sign_in(): def register(): form = RegisterForm() if form.validate_on_submit(): - return redirect("/success") + user = User(username=form.username.data, password=form.password.data) + db.session.add(user) + db.session.commit() + login_user(user) + return redirect(url_for("teacher.dashboard")) return render_template("register.html", form=form) diff --git a/OpenLearn/views/public/forms.py b/OpenLearn/views/public/forms.py index 1a3bc09..b40343b 100644 --- a/OpenLearn/views/public/forms.py +++ b/OpenLearn/views/public/forms.py @@ -1,8 +1,12 @@ # -*- coding: utf-8 -*- +from typing import Optional + from flask_wtf import FlaskForm from wtforms import StringField, PasswordField from wtforms.validators import Length, EqualTo, InputRequired +from OpenLearn.database import User + class LoginForm(FlaskForm): username = StringField("Username", validators=[ @@ -13,6 +17,24 @@ class LoginForm(FlaskForm): InputRequired(message="You must enter your username") ]) + def __init__(self, *args, **kwargs): + """Create instance.""" + super().__init__(*args, **kwargs) + self.user: Optional[User] = None + + def validate(self): + self.user = User.query.filter_by(username=self.username.data).first() + if not self.user: + self.username.errors.append("Unknown username") + return False + + if not self.user.check_password(self.password.data): + self.password.errors.append("Invalid password") + return False + + return True + + class RegisterForm(FlaskForm): username = StringField("Username", validators=[ diff --git a/OpenLearn/views/teacher/controller.py b/OpenLearn/views/teacher/controller.py index f10b876..14f8d3b 100644 --- a/OpenLearn/views/teacher/controller.py +++ b/OpenLearn/views/teacher/controller.py @@ -2,12 +2,19 @@ """The controller for all views related to teachers""" from flask import ( Blueprint, - current_app, - flash, - redirect, - render_template, - request, - url_for, -) + redirect, url_for) +from flask_login import login_required, logout_user blueprint = Blueprint("teacher", __name__, static_folder="../static", template_folder="../templates/teacher") + + +@blueprint.route("/dashboard") +@login_required +def dashboard(): + return "You are logged in" + + +@blueprint.route("/logout") +def logout(): + logout_user() + return redirect(url_for("public.sign_in"))