Plain is headed towards 1.0! Subscribe for development updates →

plain.auth

Add users to your app and define which views they can access.

To log a user in, you'll want to pair this package with:

  • plain-passwords
  • plain-oauth
  • plain-passkeys (TBD)
  • plain-passlinks (TBD)

Installation

# app/settings.py
INSTALLED_PACKAGES = [
    # ...
    "plain.auth",
    "plain.sessions",
    "plain.passwords",
]

MIDDLEWARE = [
    "plain.middleware.security.SecurityMiddleware",
    "plain.sessions.middleware.SessionMiddleware",  # <--
    "plain.middleware.common.CommonMiddleware",
    "plain.csrf.middleware.CsrfViewMiddleware",
    "plain.auth.middleware.AuthenticationMiddleware",  # <--
]

AUTH_USER_MODEL = "users.User"
AUTH_LOGIN_URL = "login"

Create your own user model (plain create users).

# app/users/models.py
from plain import models
from plain.passwords.models import PasswordField


class User(models.Model):
    email = models.EmailField(unique=True)
    password = PasswordField()
    is_staff = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.email

Define your URL/view where users can log in.

# app/urls.py
from plain.auth.views import LoginView, LogoutView
from plain.urls import include, path
from plain.passwords.views import PasswordLoginView


class LoginView(PasswordLoginView):
    template_name = "login.html"


urlpatterns = [
    path("logout/", LogoutView, name="logout"),
    path("login/", LoginView, name="login"),
]

Checking if a user is logged in

A request.user will either be None or point to an instance of a your AUTH_USER_MODEL.

So in templates you can do:

{% if request.user %}
    <p>Hello, {{ request.user.email }}!</p>
{% else %}
    <p>You are not logged in.</p>
{% endif %}

Or in Python:

if request.user:
    print(f"Hello, {request.user.email}!")
else:
    print("You are not logged in.")

Restricting views

Use the AuthViewMixin to restrict views to logged in users, staff users, or custom logic.

from plain.auth.views import AuthViewMixin
from plain.exceptions import PermissionDenied
from plain.views import View


class LoggedInView(AuthViewMixin, View):
    login_required = True


class StaffOnlyView(AuthViewMixin, View):
    login_required = True
    staff_required = True


class CustomPermissionView(AuthViewMixin, View):
    def check_auth(self):
        super().check_auth()
        if not self.request.user.is_special:
            raise PermissionDenied("You're not special!")
 1from plain.urls import NoReverseMatch, reverse
 2from plain.utils.functional import Promise
 3
 4
 5def resolve_url(to, *args, **kwargs):
 6    """
 7    Return a URL appropriate for the arguments passed.
 8
 9    The arguments could be:
10
11        * A model: the model's `get_absolute_url()` function will be called.
12
13        * A view name, possibly with arguments: `urls.reverse()` will be used
14          to reverse-resolve the name.
15
16        * A URL, which will be returned as-is.
17    """
18    # If it's a model, use get_absolute_url()
19    if hasattr(to, "get_absolute_url"):
20        return to.get_absolute_url()
21
22    if isinstance(to, Promise):
23        # Expand the lazy instance, as it can cause issues when it is passed
24        # further to some Python functions like urlparse.
25        to = str(to)
26
27    # Handle relative URLs
28    if isinstance(to, str) and to.startswith(("./", "../")):
29        return to
30
31    # Next try a reverse URL resolution.
32    try:
33        return reverse(to, args=args, kwargs=kwargs)
34    except NoReverseMatch:
35        # If this is a callable, re-raise.
36        if callable(to):
37            raise
38        # If this doesn't "feel" like a URL, re-raise.
39        if "/" not in to and "." not in to:
40            raise
41
42    # Finally, fall back and assume it's a URL
43    return to