Example: Accounts
Registration, password hashing, sessions and protected pages.
- 111 lines of Python
- 3 pages
- 3 server functions
- 1526 B page JS (gzip)
This example needs its server (database, sessions or server functions). Run it locally:
Terminal
pyweb dev examples/auth/app.pyweb#Source
examples/auth/app.pyweb
"""Accounts: password hashing, signed-cookie sessions, protected pages."""
from pyweb import App, RPCError, redirect, server, session
from pyweb.auth import hash_password, verify_password
app = App(title="Accounts", stylesheets=["/static/app.css"])
# In-memory user store keeps the example self-contained; use a database
# table (see examples/blog) in a real app.
USERS = {}
@server
def register(email: str, name: str, password: str) -> bool:
email = email.strip().lower()
if "@" not in email or len(password) < 8:
raise RPCError("validation_error", "Use a valid email and a password of 8+ characters.")
if email in USERS:
raise RPCError("conflict", "That email is already registered.")
USERS[email] = {"name": name.strip() or email, "hash": hash_password(password)}
session.login(email, name=USERS[email]["name"])
return True
@server
def login(email: str, password: str) -> bool:
user = USERS.get(email.strip().lower())
if user is None or not verify_password(password, user["hash"]):
return False
session.login(email.strip().lower(), name=user["name"])
return True
@server
def logout() -> None:
session.logout()
@app.page("/", title="Sign in")
def Login():
if session.user():
return redirect("/account")
email = ""
password = ""
error = ""
def submit():
if login(email, password):
window.location.href = "/account"
else:
error = "Wrong email or password."
<main>
<h1>Sign in</h1>
<form onsubmit={submit}>
<div style={{"flex": "1"}}>
<input id="email" type="email" bind={email} placeholder="Email" />
<input id="password" type="password" bind={password} placeholder="Password" />
<p class="error" id="error">{error}</p>
<button id="login" class="primary">Sign in</button>
</div>
</form>
<p class="muted">No account? <a href="/register">Create one</a>.</p>
</main>
@app.page("/register", title="Create account")
def Register():
email = ""
name = ""
password = ""
error = ""
def submit():
try:
register(email, name, password)
window.location.href = "/account"
except RPCError as e:
error = str(e)
<main>
<h1>Create account</h1>
<form onsubmit={submit}>
<div style={{"flex": "1"}}>
<input id="name" bind={name} placeholder="Name" />
<input id="email" type="email" bind={email} placeholder="Email" />
<input id="password" type="password" bind={password} placeholder="Password (8+ characters)" />
<p class="error" id="error">{error}</p>
<button id="register" class="primary">Create account</button>
</div>
</form>
</main>
@app.page("/account", title="Your account")
def Account():
user = session.user()
if not user:
return redirect("/")
name = user["name"]
email = user["sub"]
def sign_out():
logout()
window.location.href = "/"
<main>
<h1>Hello, {name}</h1>
<p class="muted">Signed in as {email}</p>
<button id="logout" onclick={sign_out}>Sign out</button>
</main>
#What runs where
Output of pyweb inspect: every page variable, handler and server function, where it runs and why.
Output
page Login route=/
browser email reactive state: bound to an input (line 57); literal initial value
browser password reactive state: bound to an input (line 58); literal initial value
browser error reactive state: assigned in submit(); literal initial value
browser submit event handler (compiled to JavaScript)
page Register route=/register
browser email reactive state: bound to an input (line 86); literal initial value
browser name reactive state: bound to an input (line 85); literal initial value
browser password reactive state: bound to an input (line 87); literal initial value
browser error reactive state: assigned in submit(); literal initial value
browser submit event handler (compiled to JavaScript)
page Account route=/account
server user computed per request; never read by browser code, never sent
server name computed per request on the server; value sent because browser code reads it
server email computed per request on the server; value sent because browser code reads it
browser sign_out event handler (compiled to JavaScript)
rpc POST /__pyweb/rpc/register (email: str, name: str, password: str) -> bool
rpc POST /__pyweb/rpc/login (email: str, password: str) -> bool
rpc POST /__pyweb/rpc/logout () -> None#Generated JavaScript
The browser modules the compiler wrote (before minification). They import the shared runtime.
JavaScript
// Login.js
import { h as $h, t as $t, dyn as $dyn, list as $list, when as $when, signal as $signal, computed as $computed, mount as $mount, onMount as $onMount, py as $py, rpc as $rpc, subscribe as $subscribe, slot as $slot, onCleanup as $onCleanup, live as $live } from "./runtime.js";
function Login($s) {
const email = $signal("email" in $s ? $s["email"] : "");
const password = $signal("password" in $s ? $s["password"] : "");
const error = $signal("error" in $s ? $s["error"] : "");
async function submit() {
if ($py.truth((await $rpc("login", {"email": email(), "password": password()})))) {
window.location.href = "/account";
} else {
error("Wrong email or password.");
}
}
return [$h("main", null, () => [
$h("h1", null, () => [$t("Sign in")]),
$h("form", {"onsubmit": submit}, () => [$h("div", {"style": {"flex": "1"}}, () => [
$h("input", {"id": "email", "type": "email", "$bind": email, "placeholder": "Email"}),
$h("input", {"id": "password", "type": "password", "$bind": password, "placeholder": "Password"}),
$h("p", {"class": "error", "id": "error"}, () => [$dyn(() => error())]),
$h("button", {"id": "login", "class": "primary"}, () => [$t("Sign in")])
])]),
$h("p", {"class": "muted"}, () => [
$t("No account? "),
$h("a", {"href": "/register"}, () => [$t("Create one")]),
$t(".")
])
])];
}
$mount("Login", Login);
// Register.js
import { h as $h, t as $t, dyn as $dyn, list as $list, when as $when, signal as $signal, computed as $computed, mount as $mount, onMount as $onMount, py as $py, rpc as $rpc, subscribe as $subscribe, slot as $slot, onCleanup as $onCleanup, live as $live } from "./runtime.js";
function Register($s) {
const email = $signal("email" in $s ? $s["email"] : "");
const name = $signal("name" in $s ? $s["name"] : "");
const password = $signal("password" in $s ? $s["password"] : "");
const error = $signal("error" in $s ? $s["error"] : "");
async function submit() {
let e;
try {
(await $rpc("register", {"email": email(), "name": name(), "password": password()}));
window.location.href = "/account";
} catch ($err) {
if ($py.exc($err, ["RPCError"])) {
e = $err;
error($py.str(e));
} else { throw $err; }
}
}
return [$h("main", null, () => [
$h("h1", null, () => [$t("Create account")]),
$h("form", {"onsubmit": submit}, () => [$h("div", {"style": {"flex": "1"}}, () => [
$h("input", {"id": "name", "$bind": name, "placeholder": "Name"}),
$h("input", {"id": "email", "type": "email", "$bind": email, "placeholder": "Email"}),
$h("input", {"id": "password", "type": "password", "$bind": password, "placeholder": "Password (8+ characters)"}),
$h("p", {"class": "error", "id": "error"}, () => [$dyn(() => error())]),
$h("button", {"id": "register", "class": "primary"}, () => [$t("Create account")])
])])
])];
}
$mount("Register", Register);
// Account.js
import { h as $h, t as $t, dyn as $dyn, list as $list, when as $when, signal as $signal, computed as $computed, mount as $mount, onMount as $onMount, py as $py, rpc as $rpc, subscribe as $subscribe, slot as $slot, onCleanup as $onCleanup, live as $live } from "./runtime.js";
function Account($s) {
const name = $s["name"];
const email = $s["email"];
async function sign_out() {
(await $rpc("logout", {}));
window.location.href = "/";
}
return [$h("main", null, () => [
$h("h1", null, () => [
$t("Hello, "),
$t($py.text(name))
]),
$h("p", {"class": "muted"}, () => [
$t("Signed in as "),
$t($py.text(email))
]),
$h("button", {"id": "logout", "onclick": sign_out}, () => [$t("Sign out")])
])];
}
$mount("Account", Account);