Showing the logged-in user
Showing the logged-in user
The auth-playground is functionally complete. Users can register, sign in, reach protected pages, and sign out. Behind the scenes, every authenticated request carries a session with the user’s identity.
But the app doesn’t really show any of this. The topbar says Auth playground on the left and Sign out on the right. Whether you’re Alice, Bob, or a guest who somehow slipped past @login_required, the chrome looks roughly identical. There’s no name, no avatar, no visible acknowledgement that you’re signed in. That’s a missed opportunity — both for UX and for trust.
This lesson is about putting the session data we already have on the screen, on every page, without writing the same code in every view.
By the end:
- The topbar will show the user’s name and a small avatar.
- The welcome page will greet the user by their first name.
- Every template will have direct access to a
current_uservariable — no need to pass it from view functions.
No new routes, no new database queries (well — one less, actually). Just smarter use of what’s already in the session.
What an “authenticated UI” looks like
Right now, the topbar from Lesson 5 shows just the brand on the left and the Sign out button on the right. By the end of this lesson, it’ll look like this when someone is signed in:
A small green circle with the user’s initial, their full name beside it, then the existing sign-out button. Quiet, visible confirmation that the app knows who you are.
The welcome page will also get a personal touch: instead of “Hello, Alice Carter” it’ll say “Hi Alice 👋” — first name only, the way you’d actually greet someone.
The naïve approach (and why it doesn’t scale)
The obvious way to get the user’s name into every page is to pass it from every route:
@app.route("/welcome")
@login_required
def welcome():
user = User.query.get(session["user_id"])
return render_template("welcome.html", user=user)
@app.route("/secret")
@login_required
def secret():
user = User.query.get(session["user_id"])
return render_template("secret.html", user=user)
That’s what we did in Lessons 4 and 6 — every protected route loads the user and hands them to the template.
But what about /users? It’s public, so it doesn’t have @login_required, and right now it doesn’t pass a user either. So when an authenticated user visits /users, the topbar can’t render their chip — there’s no user variable in scope. We’d have to add it to every view function, public or protected.
That’s the same problem we hit in Lesson 6 with the session check — repeating ourselves in every route, hoping we never forget. The solution is the same shape: write it once, apply it everywhere.
What a context processor is
Flask gives us a feature designed precisely for this: context processors. A context processor is a function that runs before every template render, and whatever it returns is merged into the template’s variables.
In other words: instead of passing user from every view function, we register one function that says “whenever you’re about to render any template, also make current_user available.” Templates get the variable for free, no view-side cooperation required.
Add this to app.py, right after db.init_app(app) (before the routes start):
@app.context_processor
def inject_current_user():
user_id = session.get("user_id")
if user_id is None:
return {"current_user": None}
user = User.query.get(user_id)
return {"current_user": user}
Walk through it:
@app.context_processor— registers the function. Flask calls it before every template render.session.get("user_id")— if there’s nouser_idin the session, the visitor is a guest. We return{"current_user": None}so templates can check with{% if current_user %}.User.query.get(user_id)— fetches the full user row from the database, so templates can usecurrent_user.name,current_user.email,current_user.created_at, etc.- Returns a dict — the keys become template variables.
Performance note: This adds one extra database query per page load — fetching the user row by ID. That’s a tiny, indexed lookup, so it’s fine for our scale. In bigger applications you might cache the user object on the request, but
User.query.get(user_id)is more than fast enough for now.
What about orphan sessions? If the user_id in the session points to a row that was deleted (we wiped the database between lessons, remember?),
User.query.get(...)returnsNone. The processor returns{"current_user": None}and templates treat that visitor as a guest — exactly the right behaviour.
Simplify the routes
Now that current_user is available in every template automatically, we can stop computing it in the route handlers.
Open app.py and update the welcome and secret routes:
@app.route("/welcome")
@login_required
def welcome():
return render_template("welcome.html")
@app.route("/secret")
@login_required
def secret():
return render_template("secret.html")
Two routes, one line each. The User.query.get(...) calls are gone — the context processor does that work, once per request, on behalf of every template.
This is one of the small joys of working with a templating system designed for this stuff. You delete code and the app gets more capable.
Update base.html with the user chip
Now the topbar can use current_user directly. Replace your base.html with this:
<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{% block title %}Auth playground{% endblock %}</title>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
max-width: 640px;
margin: 0 auto;
padding: 0 20px;
color: #1f2a26;
}
.topbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 18px 0;
border-bottom: 1px solid #e2ddd2;
margin-bottom: 32px;
}
.brand { font-size: 14px; font-weight: 600; color: #2f3e37; }
.brand a { color: inherit; text-decoration: none; }
.topbar-right {
display: flex;
align-items: center;
gap: 10px;
}
.user-chip {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 4px 12px 4px 4px;
background: #fff;
border: 1px solid #e2ddd2;
border-radius: 999px;
font-size: 13px;
font-weight: 500;
color: #1f2a26;
}
.user-chip .avatar {
width: 24px;
height: 24px;
border-radius: 50%;
background: #4a6b5c;
color: #f4f1ea;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 600;
}
.nav-form { margin: 0; }
.nav-form button {
background: #fff;
color: #1f2a26;
border: 1px solid #d8d3c7;
padding: 6px 14px;
border-radius: 8px;
font-size: 13px;
font-family: inherit;
cursor: pointer;
}
.nav-form button:hover { background: #f4f1ea; }
h1 { font-size: 22px; margin-bottom: 0.25rem; }
p.lead { color: #6b7a72; font-size: 14px; margin-top: 0; }
.card {
background: #fff;
border: 1px solid #e2ddd2;
border-radius: 12px;
padding: 24px;
margin-top: 20px;
}
.flash {
background: #f4e2dc;
border: 1px solid #e5c5bc;
color: #1f2a26;
padding: 10px 14px;
border-radius: 8px;
font-size: 13px;
margin-bottom: 16px;
}
label { display: block; font-size: 13px; font-weight: 600; margin-bottom: 4px; }
input {
width: 100%;
box-sizing: border-box;
padding: 9px 12px;
border: 1px solid #d8d3c7;
border-radius: 8px;
font-size: 14px;
font-family: inherit;
margin-bottom: 14px;
}
button.primary {
background: #2f3e37;
color: #fff;
border: none;
padding: 10px 18px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 16px;
background: #fff;
border: 1px solid #e2ddd2;
border-radius: 12px;
overflow: hidden;
}
th, td {
text-align: left;
padding: 10px 14px;
font-size: 14px;
border-bottom: 1px solid #e2ddd2;
}
th {
background: #f4f1ea;
font-weight: 600;
font-size: 13px;
color: #6b7a72;
}
tr:last-child td { border-bottom: none; }
.empty { text-align: center; color: #6b7a72; padding: 32px; }
.footer-nav { font-size: 13px; color: #6b7a72; margin-top: 14px; }
.footer-nav a { color: #4a6b5c; }
.field { margin-bottom: 12px; }
.field-label { font-size: 12px; color: #6b7a72; margin: 0 0 2px; }
.field-value { font-size: 14px; font-weight: 500; margin: 0; }
</style>
</head>
<body>
<div class="topbar">
<div class="brand">
<a href="{{ url_for('index') }}">Auth playground</a>
</div>
{% if current_user %}
<div class="topbar-right">
<div class="user-chip">
<span class="avatar">{{ current_user.name[0]|upper }}</span>
<span>{{ current_user.name }}</span>
</div>
<form action="{{ url_for('logout') }}" method="post" class="nav-form">
<button type="submit">Sign out</button>
</form>
</div>
{% endif %}
</div>
{% with messages = get_flashed_messages() %}
{% for msg in messages %}
<div class="flash">{{ msg }}</div>
{% endfor %}
{% endwith %}
{% block content %}{% endblock %}
</body>
</html>
The new pieces:
- The
.user-chipand.user-chip .avatarCSS — a small pill containing a coloured circle (with the user’s initial) followed by their name. Familiar pattern, used by Gmail, Slack, GitHub, and almost every signed-in product. - The
<div class="topbar-right">— groups the chip and the sign-out button on the right side of the bar with consistent spacing. {{ current_user.name[0]|upper }}— takes the first character of the name and uppercases it. Even if someone registers asbob lowercase, their avatar shows a proudB.{% if current_user %}wraps the whole block — guests never see a broken chip, only the brand on the left.
Personalise the welcome page
While we’re at it, let’s give the welcome page a more human greeting. Update templates/welcome.html:
<!-- templates/welcome.html -->
{% extends "base.html" %}
{% block title %}Welcome{% endblock %}
{% block content %}
<h1>Hi {{ current_user.name.split()[0] }} 👋</h1>
<p class="lead">You signed in as {{ current_user.email }}.</p>
<div class="card">
<div class="field">
<p class="field-label">Name</p>
<p class="field-value">{{ current_user.name }}</p>
</div>
<div class="field">
<p class="field-label">Email</p>
<p class="field-value">{{ current_user.email }}</p>
</div>
<div class="field">
<p class="field-label">Joined</p>
<p class="field-value">{{ current_user.created_at.strftime("%d %b %Y") }}</p>
</div>
</div>
<p class="footer-nav">
<a href="{{ url_for('secret') }}">Go to the secret page</a> ·
<a href="{{ url_for('users') }}">All users</a>
</p>
{% endblock %}
Changes from the Lesson 6 version:
{{ current_user.name.split()[0] }}— uses just the first name in the heading. “Hi Alice” reads better than “Hi Alice Carter”.- Every
user.somethingis nowcurrent_user.something. We’re not passinguserfrom the view anymore — we’re reading the variable the context processor injected.
While we’re cleaning up, do the same to secret.html: replace user with current_user. (The route already stopped passing user=user.)
<!-- templates/secret.html -->
{% extends "base.html" %}
{% block title %}The secret page{% endblock %}
{% block content %}
<h1>The secret page</h1>
<p class="lead">Only signed-in users can see this.</p>
<div class="card">
<p style="margin: 0 0 8px;">
<strong>{{ current_user.name }}</strong>, you successfully reached a protected route.
</p>
<p style="margin: 0; font-size: 13px; color: #6b7a72;">
A guest trying to visit this URL would have been bounced to the login page.
</p>
</div>
<p class="footer-nav">
<a href="{{ url_for('welcome') }}">Back to welcome</a>
</p>
{% endblock %}
The /users and /register and /login templates don’t need to change — they don’t reference any user-specific data in their content. They’ll get the chip automatically via base.html, which is the entire point.
Run it
Restart the app and walk through the loop one more time:
- Open
/loginas a guest. Topbar shows brand on the left, nothing on the right. - Sign in. The welcome page now greets you with “Hi Alice 👋”. The topbar has a green-circled
Anext to Alice Carter, with the Sign out button on the far right. - Click “Go to the secret page”. The chip is still there in the topbar — same shape, same position. The secret page’s body greets you as Alice too, pulling from
current_user.namewithout the route ever knowing. - Click “All users”. The chip is still there — yes, even on this public page. Because the chip lives in
base.htmland reads from a context processor, every page that extends the layout gets it automatically. - Sign out. The chip disappears. Back to clean topbar.
Notice how much simpler this felt than the alternative. Adding personalisation to a new page no longer requires any view changes — just the template extending base.html. The chip, the flash messages, the sign-out button — all for free.
Why visibility matters
This lesson didn’t change any authentication logic. We didn’t add new routes, new password checks, new decorators. We just made the existing session data visible to the user.
That visibility builds trust. When users see their name and avatar in the topbar, they get implicit confirmation that:
- They’re actually signed in (not somehow viewing a cached public copy).
- The app knows who they are.
- They’re looking at their data, not someone else’s.
Apps that hide this information feel anonymous and impersonal — even when they’re working correctly. Apps that show it feel personal, owned, and accountable.
Spend the ten minutes to wire this up in any project you build. It pays for itself every time the user opens the app.
Common mistakes
- Putting the context processor inside a route function. It must be at module level, registered once when the app starts. Otherwise it never fires.
- Calling the variable
userinstead ofcurrent_user.useris a common loop variable name and can collide with existing code. The conventioncurrent_useris borrowed from Flask-Login (which you’ll meet briefly in Lesson 9) — using the same name now will make that transition seamless. - Showing
current_user.namewithout first checkingcurrent_userexists. On a guest-facing page,current_userisNoneandNone.namewill crash. Always guard with{% if current_user %}first. - Storing too much in the session to avoid the DB lookup. It’s tempting to cram email, created_at, preferences, etc. into the session cookie. Don’t. Sessions are small for a reason — keep them to identifiers. The context processor’s DB lookup is the right place to fetch full user data.
- Personalising the page
<title>with the user’s name. “Alice’s playground” in the browser tab exposes the user’s name in browser history, share previews, and screenshots. Keep the title generic; put the personalisation in the body.
Summary
- A context processor injects variables into every template without you passing them from each route. Perfect for the current user.
- Wrap user-only UI in
{% if current_user %}so guest-facing pages don’t render broken navigation. - The user chip pattern (avatar with initials + name) is the standard “you’re signed in as” indicator.
- Personalise small touches — the greeting, the subtitle — using the user’s first name only.
- This lesson added zero new auth logic. It just showed the session data we already had.
Outcome
The auth-playground now feels like a real, signed-in product. Every page makes it clear who’s logged in, the topbar offers a clean way out, and the welcome page greets the user by name. The playground is functionally and visually complete.
Next lesson is the synthesis — the mini project. You’ll take every concept from Lessons 1–7 (sessions, hashing, login, logout, @login_required, context processors) and apply them to upgrade the Books app from Module 4 into a real multi-user application. No more playground — the real thing.