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 urllib.parse import urlparse, urlunparse
2
3from plain.exceptions import PermissionDenied
4from plain.http import (
5 Http404,
6 QueryDict,
7 Response,
8 ResponseRedirect,
9)
10from plain.runtime import settings
11from plain.urls import reverse
12from plain.views import View
13
14from .sessions import logout
15from .utils import resolve_url
16
17
18class LoginRequired(Exception):
19 def __init__(self, login_url=None, redirect_field_name="next"):
20 self.login_url = login_url or settings.AUTH_LOGIN_URL
21 self.redirect_field_name = redirect_field_name
22
23
24class AuthViewMixin:
25 login_required = True
26 staff_required = False
27 login_url = None
28
29 def check_auth(self) -> None:
30 """
31 Raises either LoginRequired or PermissionDenied.
32 - LoginRequired can specify a login_url and redirect_field_name
33 - PermissionDenied can specify a message
34 """
35
36 if not hasattr(self, "request"):
37 raise AttributeError(
38 "AuthViewMixin requires the request attribute to be set."
39 )
40
41 if self.login_required and not self.request.user:
42 raise LoginRequired(login_url=self.login_url)
43
44 if impersonator := getattr(self.request, "impersonator", None):
45 # Impersonators should be able to view staff pages while impersonating.
46 # There's probably never a case where an impersonator isn't staff, but it can be configured.
47 if self.staff_required and not impersonator.is_staff:
48 raise PermissionDenied(
49 "You do not have permission to access this page."
50 )
51 elif self.staff_required and not self.request.user.is_staff:
52 # Show a 404 so we don't expose staff urls to non-staff users
53 raise Http404()
54
55 def get_response(self) -> Response:
56 if not hasattr(self, "request"):
57 raise AttributeError(
58 "AuthViewMixin requires the request attribute to be set."
59 )
60
61 try:
62 self.check_auth()
63 except LoginRequired as e:
64 # Ideally this could be handled elsewhere... like PermissionDenied
65 # also seems like this code is used multiple places anyway...
66 # could be easier to get redirect query param
67 path = self.request.build_absolute_uri()
68 resolved_login_url = reverse(e.login_url)
69 # If the login url is the same scheme and net location then use the
70 # path as the "next" url.
71 login_scheme, login_netloc = urlparse(resolved_login_url)[:2]
72 current_scheme, current_netloc = urlparse(path)[:2]
73 if (not login_scheme or login_scheme == current_scheme) and (
74 not login_netloc or login_netloc == current_netloc
75 ):
76 path = self.request.get_full_path()
77 return redirect_to_login(
78 path,
79 resolved_login_url,
80 e.redirect_field_name,
81 )
82
83 return super().get_response() # type: ignore
84
85
86class LogoutView(View):
87 def post(self):
88 logout(self.request)
89 return ResponseRedirect("/")
90
91
92def redirect_to_login(next, login_url=None, redirect_field_name="next"):
93 """
94 Redirect the user to the login page, passing the given 'next' page.
95 """
96 resolved_url = resolve_url(login_url or settings.AUTH_LOGIN_URL)
97
98 login_url_parts = list(urlparse(resolved_url))
99 if redirect_field_name:
100 querystring = QueryDict(login_url_parts[4], mutable=True)
101 querystring[redirect_field_name] = next
102 login_url_parts[4] = querystring.urlencode(safe="/")
103
104 return ResponseRedirect(urlunparse(login_url_parts))