In the previous example we've made our todo app installable and now we'll provide authentication mechanisms. As always we're starting with the manifest and now we'll need to activate the auth feature of prest:

Cargo.toml:6

...
prest = { path = "../../", version = "0.4", features = ["auth"] }
...

Everything else remains just like in the previous one, and you can find it's full content at the end of this tutorial. Same with the build script and the library as they remain untouched. Since authentication is the server-side business we'll only need to modify our binary:

src/main.rs

use prest::*;
use todo_pwa_auth::{into_page, shared_routes};

embed_build_output_as!(BuiltAssets);

#[derive(Table, Default, Serialize, Deserialize)]
#[serde(default)]
struct Todo {
    #[serde(default = "Uuid::now_v7")]
    pub id: Uuid,
    #[serde(default)]
    pub owner: Uuid,
    pub task: String,
    pub done: bool,
}

impl Render for Todo {
    fn render(&self) -> Markup {
        html! {
            $"flex justify-between items-center" into="this" swap-full vals=(json!(self)) {
                input type="checkbox" patch="/todos" checked[self.done] {}
                label $"ml-4 text-lg" {(self.task)}
                button $"ml-auto" delete="/todos" {"Delete"}
            }
        }
    }
}

fn main() {
    init!(tables Todo);
    shared_routes()
        .route(
            "/todos",
            get(|auth: Auth| async move {
                html!(
                    @if let Some(user) = auth.user {
                        form put="/todos" into="#list" swap-beforeend after-request="this.reset()" {
                            input $"border rounded-md" type="text" name="task" {}
                            button $"ml-4" type="submit" {"Add"}
                        }
                        div #"list" $"w-full" {@for todo in Todo::find_by_owner(&user.id) {(todo)}}
                    } @else {
                        @if *WITH_GOOGLE_AUTH {
                            a $"p-4 border rounded-md" href=(GOOGLE_LOGIN_ROUTE) {"Login with Google"}
                            div {"OR"}
                        }
                        form $"flex flex-col gap-4 items-center" method="POST" action=(LOGIN_ROUTE) {
                            input $"border rounded-md mx-4" type="text" name="username" placeholder="username" {}
                            input $"border rounded-md mx-4" type="password" name="password" placeholder="password" {}
                            input type="hidden" name="signup" value="true" {}
                            button $"ml-4" type="submit" {"Sign in / Sign up"}
                        }
                    }
                )
            })
                .put(|user: User, Vals(mut todo): Vals<Todo>| async move {
                    todo.owner = user.id;
                    ok(todo.save()?.render())
                })
                .patch(|user: User, Vals(mut todo): Vals<Todo>| async move {
                    if !todo.check_owner(user.id)? {
                        return Err(Error::Unauthorized);
                    }
                    Ok(todo.update_done(!todo.done)?.render())
                })
                .delete(|user: User, Vals(todo): Vals<Todo>| async move {
                    if !todo.check_owner(user.id)? {
                        return Err(Error::Unauthorized);
                    }
                    Ok(todo.remove()?)
                }),
        )
        .wrap_non_htmx(into_page)
        .embed(BuiltAssets)
        .run();
}

Now our handlers will need to consider whether requests are authorized to read/write specific todos and their code becomes significantly larger, so we'll be moving away from closures and use functions. Also, here we introduced two new extractors:

Also, our todos handler got new templates with sign in / sign up forms, and an optional login with google button that renders depending on whether GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET env variables required for the auth are provided. By default after the auth flow they will redirect back to the /, but this behaviour can be customized by sending redirect field with the forms or by adding next query param to the google auth route.

Handlers for the routes specified in these forms and the button are automatically appended to the router in the .run() function. It will also set up the session and user management middleware, storage for them and other required utils.

That's it! Now users can install the app and handle their own todos without spying on each other. But maybe you actually want todos to be public? Let's make it happen in the next example.

Remaining code used in this example:

Cargo.toml

[package]
name = "todo-pwa-auth"
edition = "2021"

[dependencies]
prest = { path = "../../", version = "0.4", features = ["auth"] }
wasm-bindgen = "0.2"

[build-dependencies]
prest-build = "0.2"

build.rs

use prest_build::*;
fn main() {
    default_cfg_aliases();
    build_pwa(PWAOptions::default()).unwrap();
}

src/lib.rs

use prest::*;

pub fn shared_routes() -> Router {
    route("/", get(home))
}

async fn home() -> Markup {
    into_page(html!(
        a get="/todos" hx-trigger="load" swap-full hx-push-url="true"
            after-request="if (!event.detail.successful) { document.getElementById('error').style.display = 'flex'; this.remove() }" {}
        div #"error" style="display: none;" {"Couldn't connect to the server :("}
    ))
    .await
}

pub async fn into_page(content: Markup) -> Markup {
    html! { html { (Head::with_title("Todo PWA app with auth"))
        body $"max-w-screen-sm mx-auto px-8 mt-12 flex flex-col items-center" {
            (content)
            (Scripts::default())
        }
    }}
}

#[cfg(sw)]
#[wasm_bindgen(start)]
pub fn main() {
    shared_routes().handle_fetch_events()
}
v0.4.0
made by Egor Dezhic