Search code examples
pythonflaskflask-loginflask-admin

Using flask-login with flask-admin


enter image description here

I'm looking at an open source app https://github.com/AlvinCJin/deepfit-app/tree/master/app

This is using flask-admin for admin purposes/ On the page https://github.com/AlvinCJin/deepfit-app/blob/master/app/main/views.py:

# admin management setup
admin.add_view(ModelView(User, db.session))
admin.add_view(ModelView(Post, db.session))
path = op.join(os.path.abspath(__file__ + "/../../"), 'static')  # need to get parent path of this code
admin.add_view(FileAdmin(path, '/static/', name='Static Files'))

This works fine as far as producing a helpful dashboard but is unsecured. I have flask-login installed and I read the section http://flask-admin.readthedocs.io/en/latest/introduction/#rolling-your-own , but it is unclear to me how to link the class discussed:

class MicroBlogModelView(sqla.ModelView):

def is_accessible(self):
    return login.current_user.is_authenticated

def inaccessible_callback(self, name, **kwargs):
    # redirect to login page if user doesn't have access
    return redirect(url_for('login', next=request.url))

with the admin route.

The user table is defined in models.py as:

class User(UserMixin, db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    nickname = db.Column(db.String(64), unique=True)
    firstname = db.Column(db.String(100))
    lastname = db.Column(db.String(100))
    email = db.Column(db.String(120), index=True, unique=True)
    pwdhash = db.Column(db.String(54))
    phone = db.Column(db.Integer)
    address = db.Column(db.String(64))
    confirmed = db.Column(db.Boolean, default=False)

    role = db.Column(db.SmallInteger, default=ROLE_APPLICANT)
    comments = db.relationship('Comment', backref='author', lazy='dynamic')
    posts = db.relationship('Post', order_by="Post.timestamp", backref='author',
                            lazy='dynamic', cascade="all, delete, delete-orphan")
    about_me = db.Column(db.Text())
    last_seen = db.Column(db.DateTime, default=datetime.utcnow)
    member_since = db.Column(db.DateTime(), default=datetime.utcnow)
    portrait = db.Column(db.String(140))
    pref = db.relationship('Preference', uselist=False, backref='author')
    fav = db.relationship('Favourite', backref='user', lazy='dynamic')

    active = db.Column(db.Boolean, default=False)

    @staticmethod
    def make_unique_nickname(nickname):
        if User.query.filter_by(nickname=nickname).first() is None:
            return nickname
        version = 2
        while True:
            new_nickname = nickname + str(version)
            if User.query.filter_by(nickname=new_nickname).first() is None:
                break
            version += 1
        return new_nickname

    def __init__(self, nickname, firstname, lastname, email, password, role):
        self.nickname = nickname.title()
        self.firstname = firstname.title()
        self.lastname = lastname.title()
        self.email = email.lower()
        self.set_password(password)
        self.role = role

    def ping(self):
        self.last_seen = datetime.utcnow()
        db.session.add(self)

    def set_password(self, password):
        self.pwdhash = generate_password_hash(password)

    def check_password(self, password):
        return check_password_hash(self.pwdhash, password)

    def is_authenticated(self):
        return True

    def generate_confirmation_token(self, expiration=3600):
        s = Serializer(current_app.config['SECRET_KEY'], expiration)
        return s.dumps({'confirm': self.id})

    def confirm(self, token):
        s = Serializer(current_app.config['SECRET_KEY'])
        try:
            data = s.loads(token)
        except:
            return False
        if data.get('confirm') != self.id:
            return False
        self.confirmed = True
        db.confirmed = True
        db.session.add(self)
        return True

    def to_json(self):
        json_user = {
            'url': url_for('api.get_post', id=self.id, _external=True),
            'nickname': self.nickname,
            'member_since': self.member_since,
            'last_seen': self.last_seen,
            'posts': url_for('api.get_user_posts', id=self.id, _external=True),
            'post_count': self.posts.count(),
        }
        return json_user

    def generate_reset_token(self, expiration=3600):
        s = Serializer(current_app.config['SECRET_KEY'], expiration)
        return s.dumps({'reset': self.id})

    def generate_auth_token(self, expiration):
        s = Serializer(current_app.config['SECRET_KEY'], expiration)
        return s.dumps({'id': self.id})

    @staticmethod
    def verify_auth_token(token):
        s = Serializer(current_app.config['SECRET_KEY'])
        try:
            data = s.loads(token)
        except:
            return None
        return User.query.get(data['id'])

    def is_active(self):
        if self.active is True:
            return True
        else:
            return False

    def is_anonymous(self):
        return False

    def get_id(self):
        return unicode(self.id)

    def __repr__(self):
        return '<User %r>' % self.nickname

And has a is_authenticated method, but how do I use this to require login of as specific user?

I have tried :

class MyView(BaseView):
    @expose('/')
    def index(self):
        return self.render('admin/index.html')

    def is_accessible(self):
        return login.current_user.is_authenticated()

# admin management setup
admin.add_view(ModelView(User, db.session))
admin.add_view(ModelView(Post, db.session))
path = op.join(os.path.abspath(__file__ + "/../../"), 'static')  # need to get parent path of this code
admin.add_view(FileAdmin(path, '/static/', name='Static Files'))

Based on:

https://flask-admin.readthedocs.io/en/v1.0.7/quickstart/

EDIT:

So just for my understanding you are subclassing the ModelViews and adding the ability to have routes?

I've changed it to:

class MyView(ModelView):
    @expose('/')
    def index(self):
        return self.render('admin/index.html')

    def is_accessible(self):
        return login.current_user.is_authenticated()

# admin management setup
admin.add_view(MyView(User, db.session))
admin.add_view(MyView(Post, db.session))
path = op.join(os.path.abspath(__file__ + "/../../"), 'static')  # need to get parent path of this code
admin.add_view(FileAdmin(path, '/static/', name='Static Files'))

That is getting closer but I need to integrate this with flask login - I'm getting:

NameError: global name 'login' is not defined

EDIT2:

I've changed it to:

class MyView(ModelView):
    @expose('/')
    @login_required
    def index(self):
        return self.render('admin/index.html')

removing the is_accessible function, so its not overridden, The user class already has a built in is_accessible function.

This is at least partially working, but I would like to only allow access to admins that have a ROLE in user defined as 0


Solution

  • You need it do it like below

    from flask_login.utils import current_user
    
    class MyView(ModelView):
        @expose('/')
        def index(self):
            return self.render('admin/index.html')
    
        def is_accessible(self):
            return current_user.is_authenticated()
    
    # admin management setup
    admin.add_view(MyView(User, db.session))
    admin.add_view(MyView(Post, db.session))
    path = op.join(os.path.abspath(__file__ + "/../../"), 'static')  # need to get parent path of this code
    admin.add_view(FileAdmin(path, '/static/', name='Static Files'))
    

    You need to use MyView class while registering the views. If you need more customization then you need to implement them in your User object used by Flask login. There you can create check group and anything else you need