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 username

  • full_name - human name of the user

  • id - human readable id, visible only for admins - never expose this to the site visitors

  • uuid - randomized id, user all over the public site

  • user_data - JSONB dictionary, can hold any arbitrary data

  • activation - any pending websauna.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.

Events

Various events are fired during the user sign up and log in.

See

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")