Users, login and sign up¶
Websauna provides subsystems for managing site users, logging in and signing up.
Introduction¶
Websauna supports different user sources like site’s own sign up form or Facebook login. The user data storage is abstracted. Users can be stored in an internal SQL database storage, LDAP server or any pluggable user source. Websauna comes with a default SQLAlchemy user and group models which allow you to get started quickly.
Default Bootstrap templates are provided for sign in, sign up and password reset.
Service architecture¶
Login and sign up process consists of different services which are registered as components in websauna.system.Initializer.configure_user_models()
. Each of the services can be individually overridden and customized for your specific needs.
User registry
Registration service
Login service
Credential activity service (password resets, etc.)
OAuth login services (social media logins)
Customizing user flow¶
Default user model¶
The default user model is websauna.system.user.models.User
. It is good for basic site bootstrapping, but eventualy you want to extend it for your own purposes.
All users are signed up and logged in via email/password. There is a username
field, but it is autogenerated default. If you are building e.g. a forum and wish to users choose their own usernames you can edit this field in profile or ask on the sign up form. Also this model is easier to adopt users for external sources, like Facebook.
The most important attributes of the default model are
friendly_name
- this property tries to give out the most friendlist choice from full name, email usernamefull_name
- human name of the userid
- human readable id, visible only for admins - never expose this to the site visitorsuuid
- randomized id, user all over the public siteuser_data
- JSONB dictionary, can hold any arbitrary dataactivation
- any pendingwebsauna.system.user.model.Activation
token for email activation, password reset
Abstract user registry¶
websauna.system.user.userregistry.DefaultEmailBasedUserRegistry
maintains users in the database. By default this is done by using a SQLAlchemy model :websauna.system.user.userregistry.DefaultEmailBasedUserRegistry
. UserRegistry
users can be based on anything - you can store users in LDAP, Cassandra, you-name-it.
Creating your own user class¶
If you just want to roll your own SQLAlchemy model for the user, you can do it by overriding websauna.system.Initializer.configure_user_models()
. and registering your own:
def configure_user_models(self):
# ... lot of copy paste from parent function ...
from websauna.system.user.interfaces import IUserModel
registry = self.config.registry
registry.registerUtility(myapp.models.User, IUserModel)
# ... lot of copy paste from parent function ...
This assumes the model is compatible with the default site user flow and has attributes like user.email
and user.password
. It is safe to inherit from the base websauna.system.user.usermixin.UserMixin
if you are not building user system from the scratch.
Login service¶
Login service is responsible for email/password and username/password logins. Unlike other related services, the login service must have knowledge of user model internals.
See websauna.system.user.loginservice.DefaultLoginService
.
You can override this in websauna.system.Initializer.configure_user()
.
Default views are found in websauna.system.user.views
.
Registration service¶
Registration service is responsible for users created through sign up form. Registration service does not know about user implementation and only interacts with user registry.
See websauna.system.user.registrationservice.DefaultRegistrationService
.
You can override this in websauna.system.Initializer.configure_user()
.
Default views are found in websauna.system.user.views
.
Credential activity service¶
Credential activity service is responsible for password reset requests. Credential activity service does not know about user implementation and only interacts with user registry.
See websauna.system.user.credentialactivityservice.DefaultCredentialActivityService
.
You can override this in websauna.system.Initializer.configure_user()
.
Default views are found in websauna.system.user.views
.
Overriding a service¶
Below is an example how to override a login service for your site.
Create a login service which extends the default login service in loginservice.py
:
from websauna.system.core import messages
from websauna.system.user.interfaces import IUser
from websauna.system.user.loginservice import DefaultLoginService
class MyLoginService(DefaultLoginService):
def greet_user(self, user: IUser):
if not user.last_login_at:
# User logging in for the first time, give a different message
messages.add(self.request, "Welcome to Myapp! A $5.00 credit has been added on your Wattcoin account as a sign up bonus.", kind="success", msg_id="msg-you-are-logged-in")
else:
# Normal user login
super(MyLoginService, self).greet_user(user)
Then override the service in websauna.system.Initializer.configure_user()
:
def configure_user(self):
from .loginservice import MyLoginService
from websauna.system.user.interfaces import ILoginService
from pyramid.interfaces import IRequest
# Initialize default user services
super(Initializer, self).configure_user()
# Swap in our login service
registry = self.config.registry
registry.unregisterAdapter(required=(IRequest,), provided=ILoginService)
registry.registerAdapter(factory=MyLoginService, required=(IRequest,), provided=ILoginService)
Groups¶
The default user implementation has groups. User can be member of any number of groups.
Pyramid ACL is used to assign permissions for groups.
Examples¶
Creating a user¶
For creating users see websauna.tests.utils.create_user()
or websauna.system.devop.scripts.createuser
.
Getting the logged in user¶
The logged in user can be accessed request.user
which gives you a websauna.system.user.model.User
instance. This is set to None
for anonymous users.
Manually authenticating user¶
See websauna.magiclogin for full example.
from websauna.system.http import Request
from websauna.system.core import messages
from websauna.system.user.models import User
from websauna.system.user.utils import get_login_service
from websauna.utils.time import now
def get_or_create_email_user(request: Request, email: str) -> User:
"""Fetch existing user or create new based on email."""
dbsession = request.dbsession
u = dbsession.query(User).filter_by(email=email).first()
if u is not None:
return u
u = User(email=email)
u.registration_source = "email"
u.activated_at = now()
return u
def verify_email_login(request, token):
# ...
email = data["email"]
# Create new user or get existing user based on this email
user = get_or_create_email_user(request, email)
login_service = get_login_service(request)
# Returns HTTPRedirect taking user to post-login page
return login_service.authenticate_user(user, login_source="email")
One time login links¶
One time email login links provide passwordless login and have been made popular by services like Medium and Slack.