Flask-login usermixin class with a MongoDB
Solution 1
What you need to know about Flask-login: this extension works with the application's user model, and expects certain properties and methods to be implemented in it. (source : https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-v-user-logins).
The four required items are listed below:
is_authenticated
: a property that is True if the user has valid credentials or False otherwise.is_active
: a property that is True if the user's account is active or False otherwise.is_anonymous
: a property that is False for regular users, and True for a special, anonymous user.get_id()
: a method that returns a unique identifier for the user as a string
Unfortunately all the examples in the official documentation and on Miguel Grinberg's excellent blog use SQLAlchemy. Good news, it is possible to implement it with Pymongo...
THE SOLUTION
-
routes.py
from flask import Flask from flask_pymongo import PyMongo from flask_login import LoginManager from flask import render_template, url_for, request, flash from app.forms import Login from flask import request from werkzeug.urls import url_parse from werkzeug.security import generate_password_hash, check_password_hash from flask_login import current_user, login_user, logout_user, login_required mongo = PyMongo(app) login = LoginManager(app) login.login_view = 'login' class User: def __init__(self, username): self.username = username @staticmethod def is_authenticated(): return True @staticmethod def is_active(): return True @staticmethod def is_anonymous(): return False def get_id(self): return self.username @staticmethod def check_password(password_hash, password): return check_password_hash(password_hash, password) @login.user_loader def load_user(username): u = mongo.db.Users.find_one({"Name": username}) if not u: return None return User(username=u['Name']) @app.route('/login', methods=['GET', 'POST']) def login(): if current_user.is_authenticated: return redirect(url_for('index')) form = Login() if form.validate_on_submit(): user = mongo.db.Users.find_one({"Name": form.name.data}) if user and User.check_password(user['Password'], form.password.data): user_obj = User(username=user['Name']) login_user(user_obj) next_page = request.args.get('next') if not next_page or url_parse(next_page).netloc != '': next_page = url_for('index') return redirect(next_page) else: flash("Invalid username or password") return render_template('login.html', title='Sign In', form=form) @app.route('/logout') def logout(): logout_user() return redirect(url_for('login'))
-
form.py
from flask_wtf import FlaskForm from wtforms import StringField, SubmitField, PasswordField from wtforms.validators import DataRequired class Login(FlaskForm): name = StringField('name' validators=[DataRequired()]) password = PasswordField('Password', validators=[DataRequired()]) login = SubmitField('Login')
Assuming we have, on the side of Mongodb, a collection (Users) that contains some login information. For example:
{
Name: [username],
Password: [hashed_password]
}
For further explanation on what each line of code does, I recommend you to consult the following links:
- https://boh717.github.io/post/flask-login-and-mongodb/
- https://flask-login.readthedocs.io/en/latest/
Solution 2
I found the following works with flask-login
, UserMixin
, pymongo
Here is User model
import datetime
import uuid
from depo import bcrypt, login_manager
from flask import session, flash
from depo.common.database import Database
from depo.models.blog import Blog
from flask_login import UserMixin
class User(UserMixin):
def __init__(self, username, email, password, _id=None):
self.username = username
self.email = email
self.password = password
self._id = uuid.uuid4().hex if _id is None else _id
def is_authenticated(self):
return True
def is_active(self):
return True
def is_anonymous(self):
return False
def get_id(self):
return self._id
@classmethod
def get_by_username(cls, username):
data = Database.find_one("users", {"username": username})
if data is not None:
return cls(**data)
@classmethod
def get_by_email(cls, email):
data = Database.find_one("users", {"email": email})
if data is not None:
return cls(**data)
@classmethod
def get_by_id(cls, _id):
data = Database.find_one("users", {"_id": _id})
if data is not None:
return cls(**data)
@staticmethod
def login_valid(email, password):
verify_user = User.get_by_email(email)
if verify_user is not None:
return bcrypt.check_password_hash(verify_user.password, password)
return False
@classmethod
def register(cls, username, email, password):
user = cls.get_by_email(email)
if user is None:
new_user = cls( username, email, password)
new_user.save_to_mongo()
session['email'] = email
return True
else:
return False
def json(self):
return {
"username": self.username,
"email": self.email,
"_id": self._id,
"password": self.password
}
def save_to_mongo(self):
Database.insert("users", self.json())
Here is routes
from flask import flash, render_template, request, session, make_response, redirect, url_for
from depo import app, bcrypt, login_manager
from depo.models.blog import Blog
from depo.models.post import Post
from depo.models.user import User
from depo.common.database import Database
from depo.usercon.forms import RegistrationForm, LoginForm
from flask_login import login_user
@app.before_first_request
def initialize_database():
Database.initialize()
@app.route("/register", methods=['GET', 'POST'])
def register():
form = RegistrationForm()
if form.validate_on_submit():
if request.method == 'POST':
username = request.form["username"]
email = request.form["email"]
password = bcrypt.generate_password_hash(request.form["password"])
.decode('utf-8')
find_user = User.get_by_email(email)
if find_user is None:
User.register(username, email, password)
flash(f'Account created for {form.username.data}!', 'success')
return redirect(url_for('home'))
else:
flash(f'Account already exists for {form.username.data}!', 'success')
return render_template('register.html', title='Register', form=form)
@app.route("/login", methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
email = request.form["email"]
password = request.form["password"]
find_user = Database.find_one("users", {"email": email})
if User.login_valid(email, password):
loguser = User(find_user["_id"], find_user["email"], find_user["password"])
login_user(loguser, remember=form.remember.data)
flash('You have been logged in!', 'success')
return redirect(url_for('home'))
else:
flash('Login Unsuccessful. Please check email and password', 'danger')
return render_template('login.html', title='Login', form=form)
@login_manager.user_loader
def load_user(user_id):
user =User.get_by_id(user_id)
if user is not None:
return User(user["_id"])
else:
return None
May be needed some codes for app
also if already not initialized
from flask_login import LoginManager
login_manager = LoginManager(app)
Ben
Updated on June 14, 2022Comments
-
Ben almost 2 years
I am working to try and build a login method for a while now. I am running a Flask app and have it working well. It all runs locally on my machine. Currently, I am using
pymongo
andMongoClient
to make my connection to the DB. This is all working well and I would like to not change this if possible.I am trying to use
Flask-Login
to create ausers
class usingusermixin
. This is where I have been grossly unsuccessful. I have tried a few different things and my issue is how to I pull the data from my DB. I have done this previously with an SQL DB but for this project I expressly want to use MongoDB. This is the tutorial I was attempting to follow but I am having difficulty understanding everything because it is not explained well what every line is doing.This is my connection to my DB:
client = MongoClient('mongodb://localhost:27017')
and this is my current users class that I don't have working and where I need the help.
class User(UserMixin): def __init__(self, username, password_hash): self.username = username self.password_hash = password_hash def check_password(self, password): return check_password_hash(self.password_hash, password) def get_id(self): return self.username @login_manager.user_loader def load_user(user_id): return User.objects(pk=user_id).first()
Then my last part is my login form:
@app.route('/login', methods=["GET" , "POST"]) def login(): if request.method == "GET": return render_template("login.html", error=False) if request.method == "POST": check_user = request.form["username"] if check_user: if check_password_hash(check_user['password'], request.form["password"]): login_user(check_user) return redirect(url_for('index'))
I am aware that this tutorial uses
MongoEngine
which I am not using, or not yet but some help here either how to get this code above to work or how to adapt it would be great. When I run this code I am not getting any errors it just doens't work. My test is I try to login and then I try to go to the logout page which is loaded with the following code:@app.route("/logout") @login_required def logout(): logout_user() return redirect(url_for('index'))
When I do it doesn't load the page and I get and Unauthorized Page notice. Thus I know that my code is not working. Lastly, I have all of templates in a static file location.
Thanks in advance for the help and please if anything is not clear ask and I will try to add more details. The more specific the better I will be able to help.
UPDATE:
I realized that it is also probably important to describe how my DB is structured to make sure that I am accessing it properly because that is a major point where I am having issues. I have a DB with my collection called Users and it is structured with each document being a different user record, like this:
{ "_id" : 1, "Reset" : false, "FirstName" : "John", "LastName" : "Doe", "Email" : "[email protected]", "Username" : "", "admin" : false, "Pass" : "[hashed_password]" } { "_id" : 2, "Reset" : true, "FirstName" : "Jane", "LastName" : "Smith", "Email" : "[email protected]", "Username" : "Jane", "admin" : false, "Pass" : "[hashed_password]" } { "_id" : 3, "Reset" : true, "FirstName" : "Gary", "LastName" : "Bettman", "Email" : "[email protected]", "Username" : "HockeyGuy", "admin" : false, "Pass" : "[hashed_password]" }
-
Ben about 5 yearsI still have a few questions, I have been looking at all of these links you provided and tried with them before posting so I am still in need of some assistance. Is there a reason that the form is in a separate py file and not just an html template, and with that why is is
from app.form import Login
and not justimport form
. Also why is everything being called out as an@staticmethod
in theUser
Class, I've never seen this and would like to understand it. Last I think I might need to run the users call to the DB differently, see update above for how DB is structured. -
Ben about 5 yearsPlease see the update to the original question as the DB is structured differently and I think that is part of my issue.
-
Tobin about 5 yearsBy definition, a static method (reported by the decorator
@staticmethod
) is a method that does not modify the class, nor any instance of the class to which it belongs. This article explains in more detail how a static method works. The interest for me to use them here is that they only serve as utility functions that do not necessarily need to access the current class. The class in this case serves only as a "drawer" in which they are arranged. -
Tobin about 5 yearsFor the rest sorry but I pulled the code directly from one of my projects and I did not pay attention to imports, hence the presence of the
from app.form import Login
. You probably have to know the principle of modularity, the more the code is growing, the more interesting it is to split it into easy to maintain modules later. It is with this in mind that I subdivided my code into modules (a module for routes, another for forms, etc.). -
Tobin about 5 yearsI urge you to browse Flask's mega tutorial, it will teach you a lot of interesting things about how to code effectively with Flask
-
Mauricio Maroto over 3 yearsI think find_user["_id"] is misplaced at the loguser statement.