GitHub

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);
Edit this page on GitHub