diff --git a/app/__init__.py b/app/__init__.py index f589d4f..f057191 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -16,7 +16,6 @@ from flask import * -from flask_user import * from flask_gravatar import Gravatar import flask_menu as menu from flask_mail import Mail @@ -24,6 +23,7 @@ from flask_github import GitHub from flask_wtf.csrf import CSRFProtect from flask_flatpages import FlatPages from flask_babel import Babel +from flask_login import logout_user, current_user import os, redis app = Flask(__name__, static_folder="public/static") @@ -64,13 +64,10 @@ init_app(app) # def get_locale(): # return request.accept_languages.best_match(app.config['LANGUAGES'].keys()) -from . import models, tasks, template_filters - +from . import models, tasks, template_filters, usermgr from .blueprints import create_blueprints create_blueprints(app) -from flask_login import logout_user - @app.route("/uploads/") def send_upload(path): return send_from_directory(app.config['UPLOAD_DIR'], path) @@ -88,7 +85,7 @@ def check_for_ban(): if current_user.rank == models.UserRank.BANNED: flash("You have been banned.", "danger") logout_user() - return redirect(url_for('user.login')) + return redirect(url_for('users.login')) elif current_user.rank == models.UserRank.NOT_JOINED: current_user.rank = models.UserRank.MEMBER models.db.session.commit() diff --git a/app/blueprints/admin/admin.py b/app/blueprints/admin/admin.py index 7280ab9..f1104a8 100644 --- a/app/blueprints/admin/admin.py +++ b/app/blueprints/admin/admin.py @@ -19,7 +19,7 @@ import os from celery import group from flask import * -from flask_user import * +from flask_login import current_user from flask_wtf import FlaskForm from wtforms import * diff --git a/app/blueprints/admin/tagseditor.py b/app/blueprints/admin/tagseditor.py index 2919d4d..0aa6776 100644 --- a/app/blueprints/admin/tagseditor.py +++ b/app/blueprints/admin/tagseditor.py @@ -16,7 +16,7 @@ from flask import * -from flask_user import * +from flask_login import current_user, login_required from flask_wtf import FlaskForm from wtforms import * from wtforms.validators import * diff --git a/app/blueprints/api/endpoints.py b/app/blueprints/api/endpoints.py index 3145954..8afc2af 100644 --- a/app/blueprints/api/endpoints.py +++ b/app/blueprints/api/endpoints.py @@ -16,7 +16,7 @@ from flask import * -from flask_user import * +from flask_login import current_user, login_required from . import bp from .auth import is_api_authd from .support import error, handleCreateRelease diff --git a/app/blueprints/api/tokens.py b/app/blueprints/api/tokens.py index 2150efc..ef145ea 100644 --- a/app/blueprints/api/tokens.py +++ b/app/blueprints/api/tokens.py @@ -16,7 +16,7 @@ from flask import render_template, redirect, request, session, url_for, abort -from flask_user import login_required, current_user +from flask_login import login_required, current_user from flask_wtf import FlaskForm from wtforms import * from wtforms.ext.sqlalchemy.fields import QuerySelectField diff --git a/app/blueprints/github/__init__.py b/app/blueprints/github/__init__.py index f4b764f..fec824e 100644 --- a/app/blueprints/github/__init__.py +++ b/app/blueprints/github/__init__.py @@ -19,7 +19,7 @@ from flask import Blueprint bp = Blueprint("github", __name__) from flask import redirect, url_for, request, flash, abort, render_template, jsonify, current_app -from flask_user import current_user, login_required +from flask_login import current_user, login_required from sqlalchemy import func, or_, and_ from app import github, csrf from app.models import db, User, APIToken, Package, Permission @@ -46,7 +46,7 @@ def callback(oauth_token): next_url = request.args.get("next") if oauth_token is None: flash("Authorization failed [err=gh-oauth-login-failed]", "danger") - return redirect(url_for("user.login")) + return redirect(url_for("users.login")) # Get Github username url = "https://api.github.com/user" @@ -79,7 +79,7 @@ def callback(oauth_token): return redirect(next_url or url_for("homepage.home")) else: flash("Authorization failed [err=gh-login-failed]", "danger") - return redirect(url_for("user.login")) + return redirect(url_for("users.login")) @bp.route("/github/webhook/", methods=["POST"]) diff --git a/app/blueprints/metapackages/__init__.py b/app/blueprints/metapackages/__init__.py index abaf943..8882b87 100644 --- a/app/blueprints/metapackages/__init__.py +++ b/app/blueprints/metapackages/__init__.py @@ -16,10 +16,11 @@ from flask import * +from sqlalchemy import func +from app.models import MetaPackage, Package, db, Dependency, PackageState, ForumTopic bp = Blueprint("metapackages", __name__) -from app.models import * @bp.route("/metapackages/") def list_all(): @@ -29,6 +30,7 @@ def list_all(): .group_by(MetaPackage.id).all() return render_template("metapackages/list.html", mpackages=mpackages) + @bp.route("/metapackages//") def view(name): mpackage = MetaPackage.query.filter_by(name=name).first() diff --git a/app/blueprints/notifications/__init__.py b/app/blueprints/notifications/__init__.py index ba3e1c6..6e8cce4 100644 --- a/app/blueprints/notifications/__init__.py +++ b/app/blueprints/notifications/__init__.py @@ -16,7 +16,7 @@ from flask import Blueprint, render_template, redirect, url_for -from flask_user import current_user, login_required +from flask_login import current_user, login_required from app.models import db, Notification bp = Blueprint("notifications", __name__) diff --git a/app/blueprints/packages/packages.py b/app/blueprints/packages/packages.py index 0f39b55..44a6512 100644 --- a/app/blueprints/packages/packages.py +++ b/app/blueprints/packages/packages.py @@ -21,6 +21,7 @@ import flask_menu as menu from celery import uuid from flask import render_template from flask_wtf import FlaskForm +from flask_login import login_required from sqlalchemy import or_, func from sqlalchemy.orm import joinedload, subqueryload from wtforms import * diff --git a/app/blueprints/packages/releases.py b/app/blueprints/packages/releases.py index 2c66732..68635d1 100644 --- a/app/blueprints/packages/releases.py +++ b/app/blueprints/packages/releases.py @@ -18,6 +18,7 @@ from celery import uuid from flask import * from flask_wtf import FlaskForm +from flask_login import login_required from wtforms import * from wtforms.ext.sqlalchemy.fields import QuerySelectField from wtforms.validators import * diff --git a/app/blueprints/packages/reviews.py b/app/blueprints/packages/reviews.py index a52d617..9fe8bc0 100644 --- a/app/blueprints/packages/reviews.py +++ b/app/blueprints/packages/reviews.py @@ -17,7 +17,7 @@ from . import bp from flask import * -from flask_user import * +from flask_login import current_user, login_required from flask_wtf import FlaskForm from wtforms import * from wtforms.validators import * diff --git a/app/blueprints/packages/screenshots.py b/app/blueprints/packages/screenshots.py index b9ceb81..c177307 100644 --- a/app/blueprints/packages/screenshots.py +++ b/app/blueprints/packages/screenshots.py @@ -17,6 +17,7 @@ from flask import * from flask_wtf import FlaskForm +from flask_login import login_required from wtforms import * from wtforms.validators import * diff --git a/app/blueprints/tasks/__init__.py b/app/blueprints/tasks/__init__.py index 838718b..c1cdfff 100644 --- a/app/blueprints/tasks/__init__.py +++ b/app/blueprints/tasks/__init__.py @@ -16,6 +16,7 @@ from flask import * +from flask_login import login_required from app import csrf from app.tasks import celery diff --git a/app/blueprints/threads/__init__.py b/app/blueprints/threads/__init__.py index a081938..bf38718 100644 --- a/app/blueprints/threads/__init__.py +++ b/app/blueprints/threads/__init__.py @@ -19,7 +19,7 @@ from flask import * bp = Blueprint("threads", __name__) -from flask_user import * +from flask_login import current_user, login_required from app.models import * from app.utils import addNotification, isYes, addAuditLog diff --git a/app/blueprints/todo/__init__.py b/app/blueprints/todo/__init__.py index 9051e35..230cf26 100644 --- a/app/blueprints/todo/__init__.py +++ b/app/blueprints/todo/__init__.py @@ -15,7 +15,7 @@ # along with this program. If not, see . from flask import * -from flask_user import * +from flask_login import current_user, login_required from sqlalchemy import or_ from app.models import * diff --git a/app/blueprints/users/__init__.py b/app/blueprints/users/__init__.py index 2e2f965..3a2814e 100644 --- a/app/blueprints/users/__init__.py +++ b/app/blueprints/users/__init__.py @@ -2,4 +2,4 @@ from flask import Blueprint bp = Blueprint("users", __name__) -from . import profile, claim +from . import profile, claim, account diff --git a/app/blueprints/users/account.py b/app/blueprints/users/account.py new file mode 100644 index 0000000..10afbfe --- /dev/null +++ b/app/blueprints/users/account.py @@ -0,0 +1,167 @@ +# ContentDB +# Copyright (C) 2020 rubenwardy +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +from flask import * +from flask_login import current_user, login_required, logout_user, login_user +from flask_wtf import FlaskForm +from sqlalchemy import or_ +from wtforms import * +from wtforms.validators import * + +from app.models import * +from app.tasks.emails import sendVerifyEmail +from app.utils import randomString, make_flask_login_password, is_safe_url, check_password_hash +from . import bp + + +class LoginForm(FlaskForm): + username = StringField("Username or email", [InputRequired()]) + password = PasswordField("Password", [InputRequired(), Length(6, 100)]) + remember_me = BooleanField("Remember me") + submit = SubmitField("Login") + + +@bp.route("/user/login/", methods=["GET", "POST"]) +def login(): + form = LoginForm(request.form) + if form.validate_on_submit(): + username = form.username.data.strip() + user = User.query.filter(or_(User.username==username, User.email==username)).first() + if user is None: + err = "User {} does not exist".format(username) + + elif not check_password_hash(user.password, form.password.data): + err = "Incorrect password. Did you set one?" + + else: + login_user(user) + flash("Logged in successfully.") + + next = request.args.get("r") + if next and not is_safe_url(next): + abort(400) + + return redirect(next or url_for("homepage.home")) + + if err: + # The existence of a username is public, but emails are not + if "@" in username: + flash("Incorrect email or password", "danger") + else: + flash(err, "error") + + + return render_template("users/login.html", form=form) + + +@bp.route("/user/logout/", methods=["GET", "POST"]) +def logout(): + logout_user() + return redirect(url_for("homepage.home")) + + +class RegisterForm(FlaskForm): + username = StringField("Username", [InputRequired()]) + email = StringField("Email", [InputRequired(), Email()]) + password = PasswordField("Password", [InputRequired(), Length(6, 100)]) + submit = SubmitField("Register") + + +@bp.route("/user/register/", methods=["GET", "POST"]) +def register(): + form = RegisterForm(request.form) + return render_template("users/register.html", form=form) + + +@bp.route("/user/forgot-password/", methods=["GET", "POST"]) +def forgot_password(): + return "Forgot password page" + + +class SetPasswordForm(FlaskForm): + email = StringField("Email", [Optional(), Email()]) + password = PasswordField("New password", [InputRequired(), Length(8, 100)]) + password2 = PasswordField("Verify password", [InputRequired(), Length(8, 100)]) + submit = SubmitField("Save") + + +@bp.route("/user/change-password/", methods=["GET", "POST"]) +@login_required +def change_password(): + return "change" + + +@bp.route("/user/set-password/", methods=["GET", "POST"]) +@login_required +def set_password(): + if current_user.hasPassword(): + return redirect(url_for("users.change_password")) + + form = SetPasswordForm(request.form) + if current_user.email is None: + form.email.validators = [InputRequired(), Email()] + + if request.method == "POST" and form.validate(): + one = form.password.data + two = form.password2.data + if one == two: + # Hash password + hashed_password = make_flask_login_password(form.password.data) + + # Change password + current_user.password = hashed_password + db.session.commit() + + # Prepare one-time system message + flash('Your password has been changed successfully.', 'success') + + newEmail = form["email"].data + if newEmail != current_user.email and newEmail.strip() != "": + token = randomString(32) + + ver = UserEmailVerification() + ver.user = current_user + ver.token = token + ver.email = newEmail + db.session.add(ver) + db.session.commit() + + task = sendVerifyEmail.delay(newEmail, token) + return redirect(url_for("tasks.check", id=task.id, r=url_for("users.profile", username=current_user.username))) + else: + return redirect(url_for("users.login")) + else: + flash("Passwords do not match", "danger") + + return render_template("users/set_password.html", form=form, optional=request.args.get("optional")) + + +@bp.route("/users/verify/") +def verify_email(): + token = request.args.get("token") + ver = UserEmailVerification.query.filter_by(token=token).first() + if ver is None: + flash("Unknown verification token!", "danger") + else: + ver.user.email = ver.email + db.session.delete(ver) + db.session.commit() + + if current_user.is_authenticated: + return redirect(url_for("users.profile", username=current_user.username)) + else: + return redirect(url_for("homepage.home")) diff --git a/app/blueprints/users/profile.py b/app/blueprints/users/profile.py index 9d3275e..df4100c 100644 --- a/app/blueprints/users/profile.py +++ b/app/blueprints/users/profile.py @@ -16,7 +16,7 @@ from flask import * -from flask_user import signals, current_user, user_manager, login_required +from flask_login import current_user, login_required from flask_wtf import FlaskForm from sqlalchemy import func from wtforms import * @@ -26,7 +26,7 @@ from app.markdown import render_markdown from app.models import * from app.tasks.emails import sendVerifyEmail, sendEmailRaw from app.tasks.forumtasks import checkForumAccount -from app.utils import randomString, rank_required, nonEmptyOrNone, addAuditLog +from app.utils import randomString, rank_required, nonEmptyOrNone, addAuditLog, make_flask_login_password from . import bp @@ -182,79 +182,3 @@ def send_email(username): return redirect(url_for("tasks.check", id=task.id, r=next_url)) return render_template("users/send_email.html", form=form) - - - -class SetPasswordForm(FlaskForm): - email = StringField("Email", [Optional(), Email()]) - password = PasswordField("New password", [InputRequired(), Length(2, 100)]) - password2 = PasswordField("Verify password", [InputRequired(), Length(2, 100)]) - submit = SubmitField("Save") - -@bp.route("/user/set-password/", methods=["GET", "POST"]) -@login_required -def set_password(): - if current_user.hasPassword(): - return redirect(url_for("user.change_password")) - - form = SetPasswordForm(request.form) - if current_user.email is None: - form.email.validators = [InputRequired(), Email()] - - if request.method == "POST" and form.validate(): - one = form.password.data - two = form.password2.data - if one == two: - # Hash password - hashed_password = user_manager.hash_password(form.password.data) - - # Change password - current_user.password = hashed_password - db.session.commit() - - # Send 'password_changed' email - if user_manager.USER_ENABLE_EMAIL and current_user.email: - user_manager.email_manager.send_password_changed_email(current_user) - - # Send password_changed signal - signals.user_changed_password.send(current_app._get_current_object(), user=current_user) - - # Prepare one-time system message - flash('Your password has been changed successfully.', 'success') - - newEmail = form["email"].data - if newEmail != current_user.email and newEmail.strip() != "": - token = randomString(32) - - ver = UserEmailVerification() - ver.user = current_user - ver.token = token - ver.email = newEmail - db.session.add(ver) - db.session.commit() - - task = sendVerifyEmail.delay(newEmail, token) - return redirect(url_for("tasks.check", id=task.id, r=url_for("users.profile", username=current_user.username))) - else: - return redirect(url_for("user.login")) - else: - flash("Passwords do not match", "danger") - - return render_template("users/set_password.html", form=form, optional=request.args.get("optional")) - - -@bp.route("/users/verify/") -def verify_email(): - token = request.args.get("token") - ver = UserEmailVerification.query.filter_by(token=token).first() - if ver is None: - flash("Unknown verification token!", "danger") - else: - ver.user.email = ver.email - db.session.delete(ver) - db.session.commit() - - if current_user.is_authenticated: - return redirect(url_for("users.profile", username=current_user.username)) - else: - return redirect(url_for("homepage.home")) diff --git a/app/default_data.py b/app/default_data.py index 2b7a256..508b2a7 100644 --- a/app/default_data.py +++ b/app/default_data.py @@ -1,11 +1,11 @@ from .models import * -from .utils import make_flask_user_password +from .utils import make_flask_login_password def populate(session): admin_user = User("rubenwardy") - admin_user.active = True - admin_user.password = make_flask_user_password("tuckfrump") + admin_user.is_active = True + admin_user.password = make_flask_login_password("tuckfrump") admin_user.github_username = "rubenwardy" admin_user.forums_username = "rubenwardy" admin_user.rank = UserRank.ADMIN diff --git a/app/models.py b/app/models.py index cb699cd..cd49588 100644 --- a/app/models.py +++ b/app/models.py @@ -22,11 +22,11 @@ from urllib.parse import urlparse from flask import url_for from flask_migrate import Migrate from flask_sqlalchemy import SQLAlchemy, BaseQuery -from flask_user import UserManager, UserMixin from sqlalchemy_searchable import SearchQueryMixin, make_searchable from sqlalchemy_utils.types import TSVectorType -from app import app, gravatar +from .usermgr import UserMixin, login_manager +from . import app, gravatar # Initialise database db = SQLAlchemy(app) @@ -138,6 +138,9 @@ class User(db.Model, UserMixin): password = db.Column(db.String(255), nullable=False, server_default="") reset_password_token = db.Column(db.String(100), nullable=False, server_default="") + def get_id(self): + return self.username + rank = db.Column(db.Enum(UserRank)) # Account linking @@ -153,7 +156,7 @@ class User(db.Model, UserMixin): # User information profile_pic = db.Column(db.String(255), nullable=True, server_default=None) - active = db.Column("is_active", db.Boolean, nullable=False, server_default="0") + is_active = db.Column("is_active", db.Boolean, nullable=False, server_default="0") display_name = db.Column(db.String(100), nullable=False, default=display_name_default) # Links @@ -174,7 +177,7 @@ class User(db.Model, UserMixin): self.username = username self.email_confirmed_at = datetime.datetime.now() - datetime.timedelta(days=6000) self.display_name = username - self.active = active + self.is_active = active self.email = email self.password = password self.rank = UserRank.NOT_JOINED @@ -718,7 +721,7 @@ class Package(db.Model): def getSetStateURL(self, state): if type(state) == str: - state = PackageState[perm] + state = PackageState[state] elif type(state) != PackageState: raise Exception("Unknown state given to Package.canMoveToState()") @@ -1474,10 +1477,11 @@ class ForumTopic(db.Model): raise Exception("Permission {} is not related to topics".format(perm.name)) -# Setup Flask-User -user_manager = UserManager(app, db, User) - if app.config.get("LOG_SQL"): import logging logging.basicConfig() logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO) + +@login_manager.user_loader +def load_user(user_id): + return User.query.filter_by(username=user_id).first() diff --git a/app/template_filters.py b/app/template_filters.py index 93b7203..230daa2 100644 --- a/app/template_filters.py +++ b/app/template_filters.py @@ -1,7 +1,7 @@ from . import app from .models import Permission, Package, PackageState, PackageRelease from .utils import abs_url_for, url_set_query -from flask_user import current_user +from flask_login import current_user from flask_babel import format_timedelta from urllib.parse import urlparse diff --git a/app/templates/base.html b/app/templates/base.html index fabe2f6..970b489 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -131,11 +131,11 @@ {% endif %} {% endif %} - + {% else %} -
  • {{ _("Sign in") }}
  • +
  • {{ _("Sign in") }}
  • {% endif %} diff --git a/app/templates/users/claim.html b/app/templates/users/claim.html index 75b6cdc..b1815c3 100644 --- a/app/templates/users/claim.html +++ b/app/templates/users/claim.html @@ -126,7 +126,7 @@ Creating an Account options.

    - Register + Register diff --git a/app/templates/flask_user/login.html b/app/templates/users/login.html similarity index 56% rename from app/templates/flask_user/login.html rename to app/templates/users/login.html index 87f5428..96a7651 100644 --- a/app/templates/flask_user/login.html +++ b/app/templates/users/login.html @@ -8,33 +8,21 @@ Sign in
    - {% from "flask_user/_macros.html" import render_field, render_checkbox_field, render_submit_field %} + {% from "macros/forms.html" import render_field, render_checkbox_field, render_submit_field %}

    {%trans%}Sign in{%endtrans%}

    {{ form.hidden_tag() }} {# Username or Email field #} - {% set field = form.username if user_manager.USER_ENABLE_USERNAME else form.email %} -
    - {# Label on left, "New here? Register." on right #} - - {{ field(class_='form-control', tabindex=110) }} - {% if field.errors %} - {% for e in field.errors %} -

    {{ e }}

    - {% endfor %} - {% endif %} -
    + {{ render_field(form.username) }} {# Password field #} {% set field = form.password %}
    {{ field(class_='form-control', tabindex=120) }} {% if field.errors %} @@ -45,9 +33,7 @@ Sign in
    {# Remember me #} - {% if user_manager.USER_ENABLE_REMEMBER_ME %} - {{ render_checkbox_field(login_form.remember_me, tabindex=130) }} - {% endif %} + {{ render_checkbox_field(form.remember_me, tabindex=130) }} {# Submit button #}

    @@ -57,7 +43,6 @@ Sign in

    - {% from "flask_user/_macros.html" import render_field, render_checkbox_field, render_submit_field %}

    {%trans%}Sign in with Github{%endtrans%}

    GitHub @@ -67,7 +52,6 @@ Sign in