3
0
Fork 0

feat: add auth views, for login/logout

This commit is contained in:
Lance Edgar 2024-08-04 23:09:29 -05:00
parent e296b50aa4
commit a505ef27fb
9 changed files with 390 additions and 4 deletions

View file

@ -41,6 +41,7 @@ import logging
from pyramid import threadlocal
from wuttaweb import helpers
from wuttaweb.db import Session
log = logging.getLogger(__name__)
@ -48,7 +49,7 @@ log = logging.getLogger(__name__)
def new_request(event):
"""
Event hook called when processing a new request.
Event hook called when processing a new :term:`request`.
The hook is auto-registered if this module is "included" by
Pyramid config object. Or you can explicitly register it::
@ -56,7 +57,7 @@ def new_request(event):
pyramid_config.add_subscriber('wuttaweb.subscribers.new_request',
'pyramid.events.NewRequest')
This will add some things to the request object:
This will add to the request object:
.. attribute:: request.wutta_config
@ -66,7 +67,7 @@ def new_request(event):
Flag indicating whether the frontend should be displayed using
Vue 3 + Oruga (if ``True``), or else Vue 2 + Buefy (if
``False``).
``False``). This flag is ``False`` by default.
"""
request = event.request
config = request.registry.settings['wutta_config']
@ -84,6 +85,42 @@ def new_request(event):
request.set_property(use_oruga, reify=True)
def new_request_set_user(event, db_session=None):
"""
Event hook called when processing a new :term:`request`, for sake
of setting the ``request.user`` property.
The hook is auto-registered if this module is "included" by
Pyramid config object. Or you can explicitly register it::
pyramid_config.add_subscriber('wuttaweb.subscribers.new_request_set_user',
'pyramid.events.NewRequest')
This will add to the request object:
.. attribute:: request.user
Reference to the authenticated
:class:`~wuttjamaican:wuttjamaican.db.model.auth.User` instance
(if logged in), or ``None``.
:param db_session: Optional :term:`db session` to use, instead of
:class:`wuttaweb.db.Session`. Probably only useful for tests.
"""
request = event.request
config = request.registry.settings['wutta_config']
app = config.get_app()
model = app.model
def user(request):
uuid = request.authenticated_userid
if uuid:
session = db_session or Session()
return session.get(model.User, uuid)
request.set_property(user, reify=True)
def before_render(event):
"""
Event hook called just before rendering a template.
@ -151,4 +188,5 @@ def before_render(event):
def includeme(config):
config.add_subscriber(new_request, 'pyramid.events.NewRequest')
config.add_subscriber(new_request_set_user, 'pyramid.events.NewRequest')
config.add_subscriber(before_render, 'pyramid.events.BeforeRender')

View file

@ -309,7 +309,18 @@
</div>
</%def>
<%def name="render_user_menu()"></%def>
<%def name="render_user_menu()">
% if request.user:
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link">${request.user}</a>
<div class="navbar-dropdown">
${h.link_to("Logout", url('logout'), class_='navbar-item')}
</div>
</div>
% else:
${h.link_to("Login", url('login'), class_='navbar-item')}
% endif
</%def>
<%def name="render_instance_header_title_extras()"></%def>

View file

@ -0,0 +1,46 @@
## -*- coding: utf-8; -*-
<%inherit file="/form.mako" />
<%def name="title()">Login</%def>
<%def name="render_this_page()">
${self.page_content()}
</%def>
<%def name="page_content()">
<div style="height: 100%; display: flex; align-items: center; justify-content: center;">
<div class="card">
<div class="card-content">
${form.render_vue_tag()}
</div>
</div>
</div>
</%def>
<%def name="modify_this_page_vars()">
<script>
${form.vue_component}Data.usernameInput = null
${form.vue_component}.mounted = function() {
this.$refs.username.focus()
this.usernameInput = this.$refs.username.$el.querySelector('input')
this.usernameInput.addEventListener('keydown', this.usernameKeydown)
}
${form.vue_component}.beforeDestroy = function() {
this.usernameInput.removeEventListener('keydown', this.usernameKeydown)
}
${form.vue_component}.methods.usernameKeydown = function(event) {
if (event.which == 13) { // ENTER
event.preventDefault()
this.$refs.password.focus()
}
}
</script>
</%def>
${parent.body()}

168
src/wuttaweb/views/auth.py Normal file
View file

@ -0,0 +1,168 @@
# -*- coding: utf-8; -*-
################################################################################
#
# wuttaweb -- Web App for Wutta Framework
# Copyright © 2024 Lance Edgar
#
# This file is part of Wutta Framework.
#
# Wutta Framework 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.
#
# Wutta Framework 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
# Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Auth Views
"""
import colander
from deform.widget import TextInputWidget, PasswordWidget
from wuttaweb.views import View
from wuttaweb.db import Session
from wuttaweb.auth import login_user, logout_user
class AuthView(View):
"""
Auth views shared by all apps.
"""
def login(self, session=None):
"""
View for user login.
This view shows the login form, and handles its submission.
Upon successful login, user is redirected to home page.
* route: ``login``
* template: ``/login.mako``
"""
auth = self.app.get_auth_handler()
# TODO: should call request.get_referrer()
referrer = self.request.route_url('home')
# redirect if already logged in
if self.request.user:
self.request.session.flash(f"{self.request.user} is already logged in", 'error')
return self.redirect(referrer)
form = self.make_form(schema=self.login_make_schema(),
align_buttons_right=True,
show_button_reset=True,
button_label_submit="Login",
button_icon_submit='user')
# TODO
# form.show_cancel = False
# validate basic form data (sanity check)
data = form.validate()
if data:
# truly validate user credentials
session = session or Session()
user = auth.authenticate_user(session, data['username'], data['password'])
if user:
# okay now they're truly logged in
headers = login_user(self.request, user)
return self.redirect(referrer, headers=headers)
else:
self.request.session.flash("Invalid user credentials", 'error')
return {
'index_title': self.app.get_title(),
'form': form,
# TODO
# 'referrer': referrer,
}
def login_make_schema(self):
schema = colander.Schema()
# nb. we must explicitly declare the widgets in order to also
# specify the ref attribute. this is needed for autofocus and
# keydown behavior for login form.
schema.add(colander.SchemaNode(
colander.String(),
name='username',
widget=TextInputWidget(attributes={
'ref': 'username',
})))
schema.add(colander.SchemaNode(
colander.String(),
name='password',
widget=PasswordWidget(attributes={
'ref': 'password',
})))
return schema
def logout(self):
"""
View for user logout.
This deletes/invalidates the current user session and then
redirects to the login page.
Note that a simple GET is sufficient; POST is not required.
* route: ``logout``
* template: n/a
"""
# truly logout the user
headers = logout_user(self.request)
# TODO
# # redirect to home page after logout, if so configured
# if self.config.get_bool('wuttaweb.home_after_logout', default=False):
# return self.redirect(self.request.route_url('home'), headers=headers)
# otherwise redirect to referrer, with 'login' page as fallback
# TODO: should call request.get_referrer()
# referrer = self.request.get_referrer(default=self.request.route_url('login'))
referrer = self.request.route_url('login')
return self.redirect(referrer, headers=headers)
@classmethod
def defaults(cls, config):
cls._auth_defaults(config)
@classmethod
def _auth_defaults(cls, config):
# login
config.add_route('login', '/login')
config.add_view(cls, attr='login',
route_name='login',
renderer='/login.mako')
# logout
config.add_route('logout', '/logout')
config.add_view(cls, attr='logout',
route_name='logout')
def defaults(config, **kwargs):
base = globals()
AuthView = kwargs.get('AuthView', base['AuthView'])
AuthView.defaults(config)
def includeme(config):
defaults(config)

View file

@ -29,6 +29,7 @@ Most apps should include this module::
That will in turn include the following modules:
* :mod:`wuttaweb.views.auth`
* :mod:`wuttaweb.views.common`
"""
@ -36,6 +37,7 @@ That will in turn include the following modules:
def defaults(config, **kwargs):
mod = lambda spec: kwargs.get(spec, spec)
config.include(mod('wuttaweb.views.auth'))
config.include(mod('wuttaweb.views.common'))