Skip to content

Commit 848a6b1

Browse files
authored
Merge pull request #58 from tecladocode/master
Release lecture 5 on section 13, the @login_required decorator
2 parents 55d8466 + 3e3ddfa commit 848a6b1

File tree

17 files changed

+348
-0
lines changed

17 files changed

+348
-0
lines changed
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
---
2+
title: Making a 'login required' decorator
3+
slug: flask-login-required-decorator
4+
tags:
5+
- Written
6+
- How to
7+
categories:
8+
- Video
9+
section_number: 13
10+
excerpt: "Make a decorator for Flask endpoints that redirects to the login page if the user isn't logged in."
11+
draft: true
12+
---
13+
14+
# Making a 'login required' decorator
15+
16+
Earlier this section we wrote a simple check in our `/protected` endpoint which acted as a gate for logged-out users.
17+
18+
If the user is not logged in, they are redirected to the login page. Otherwise, the rest of the route runs as normal.
19+
20+
This is such a common thing to do, that it's likely almost all your endpoints will have a check like that one:
21+
22+
```py
23+
@app.get("/protected")
24+
def protected():
25+
if not session.get("email"):
26+
abort(401)
27+
return render_template("protected.html")
28+
```
29+
30+
## Writing the decorator
31+
32+
A decorator in Python is a function that acts on another function, extending it by running some code either before or after it.
33+
34+
I'd recommend learning about decorators in depth[^decorators_series_teclado] before continuing!
35+
36+
Here's what our decorator looks like:
37+
38+
```py
39+
def login_required(route):
40+
@functools.wraps(route)
41+
def route_wrapper(*args, **kwargs):
42+
if session.get("email") is None:
43+
return redirect(url_for("login"))
44+
45+
return route(*args, **kwargs)
46+
47+
return route_wrapper
48+
```
49+
50+
And this is how we use it:
51+
52+
```diff
53+
@app.get("/protected")
54+
+@login_required
55+
def protected():
56+
return render_template("protected.html")
57+
```
58+
59+
With that, the `protected` function is _replaced by_ the `route_wrapper` function (although it keeps its name and docstring, if any):
60+
61+
```py
62+
def route_wrapper(*args, **kwargs):
63+
if session.get("email") is None:
64+
return redirect(url_for("login"))
65+
66+
return route(*args, **kwargs)
67+
```
68+
69+
Note that `route(*args, **kwargs)` is calling what was previously the `protected` function. What we've done is extracted the log in check so that any of our endpoints can be decorated with `@login_required` so the check runs before anything else does in the endpoint.
70+
71+
[^decorators_series_teclado]: [How to write decorators in Python (The Teclado Blog)](https://blog.teclado.com/decorators-in-python/)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
FLASK_APP=app
2+
FLASK_ENV=development
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import os
2+
import functools
3+
from flask import (
4+
Flask,
5+
session,
6+
render_template,
7+
request,
8+
abort,
9+
flash,
10+
redirect,
11+
url_for,
12+
)
13+
from passlib.hash import pbkdf2_sha256
14+
15+
app = Flask(__name__)
16+
# Secret key generated with secrets.token_urlsafe()
17+
app.secret_key = "lkaQT-kAb6aIvqWETVcCQ28F-j-rP_PSEaCDdTynkXA"
18+
19+
users = {}
20+
21+
22+
def login_required(route):
23+
@functools.wraps(route)
24+
def route_wrapper(*args, **kwargs):
25+
if session.get("email") is None:
26+
return redirect(url_for("login"))
27+
28+
return route(*args, **kwargs)
29+
30+
return route_wrapper
31+
32+
33+
@app.get("/")
34+
def home():
35+
return render_template("home.html", email=session.get("email"))
36+
37+
38+
@app.get("/protected")
39+
@login_required
40+
def protected():
41+
return render_template("protected.html")
42+
43+
44+
@app.route("/login", methods=["GET", "POST"])
45+
def login():
46+
if request.method == "POST":
47+
email = request.form.get("email")
48+
password = request.form.get("password")
49+
50+
if pbkdf2_sha256.verify(password, users.get(email)):
51+
session["email"] = email
52+
return redirect(url_for("protected"))
53+
else:
54+
abort(401)
55+
return render_template("login.html")
56+
57+
58+
@app.route("/signup", methods=["GET", "POST"])
59+
def signup():
60+
if request.method == "POST":
61+
email = request.form.get("email")
62+
password = request.form.get("password")
63+
64+
users[email] = pbkdf2_sha256.hash(password)
65+
# session["email"] = email
66+
# - Setting the session here would be okay if you
67+
# - want users to be logged in immediately after
68+
# - signing up.
69+
flash("Successfully signed up.")
70+
return redirect(url_for("login"))
71+
return render_template("signup.html")
72+
73+
74+
@app.errorhandler(401)
75+
def auth_error():
76+
return "Not authorized"
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
7+
<title>Authentication example</title>
8+
</head>
9+
<body>
10+
{% for message in get_flashed_messages() %}
11+
<div class="alert">
12+
<p>{{ message }}</p>
13+
</div>
14+
{% endfor %}
15+
{% block content %}
16+
{% endblock %}
17+
</body>
18+
</html>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{% extends "base.html" %} {% block content %} {% if email %}
2+
<h1>Hello, {{ email }}</h1>
3+
{% else %}
4+
<h1>Hello. <a href="{{ url_for('login') }}">Log in</a>?</h1>
5+
{% endif %} {% endblock %}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{% extends "base.html" %} {% block content %}
2+
<form method="POST">
3+
<label>
4+
E-mail
5+
<input type="email" name="email" />
6+
</label>
7+
<label>
8+
Password
9+
<input type="password" name="password" />
10+
</label>
11+
<input type="submit" value="Log in" />
12+
</form>
13+
<p><a href="{{ url_for('signup') }}">Sign up</a> instead</p>
14+
{% endblock %}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
7+
<title>Protected endpoint</title>
8+
</head>
9+
<body>
10+
<h1>You are logged in!</h1>
11+
<p>Since you're seeing this page, you logged in successfully.</p>
12+
</body>
13+
</html>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{% extends "base.html" %} {% block content %}
2+
<form method="POST">
3+
<label>
4+
E-mail
5+
<input type="email" name="email" />
6+
</label>
7+
<label>
8+
Password
9+
<input type="password" name="password" />
10+
</label>
11+
<input type="submit" value="Sign up" />
12+
</form>
13+
<p><a href="{{ url_for('login') }}">Log in</a> instead</p>
14+
{% endblock %}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
flask
2+
python-dotenv
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
FLASK_APP=app
2+
FLASK_ENV=development

0 commit comments

Comments
 (0)