Skip to main content

Crate watchword_yew023

Crate watchword_yew023 

Source
Expand description

github crates.io docs.rs

watchword’s frontend for Yew 0.23: the sign-in, registration, account and user management components, the auth context and hook, and the service the components and the app make their requests with, over HTTP, the app’s socket, or a transport of the app’s. Built for wasm32-unknown-unknown.

Apps take it through watchword’s yew023 feature, as watchword::yew023, so a frontend needs only one dependency. A frontend that talks to its server over a musli-web 0.5 WebSocket also takes musliweb05, so the components use that socket (see below):

[dependencies]
watchword = { version = "0.3", features = ["yew023", "musliweb05"] }

It is a crate of its own because yew’s macros expand to ::yew:: paths, so its code must depend on yew under the key yew. A later yew gets a crate of its own beside it, so an app moves to it when it chooses. Released in lockstep with watchword.


§Features

yew needs the standard library, so everything here is behind std, which is on by default. The crate is no_std only so its features line up with watchword’s.

FeatureWhat it provides
std (default)everything here
allocnothing yet beyond watchword-core’s alloc
musliweb05std + AuthContext::set_socket: the components follow a musli-web 0.5 socket’s broadcasts and send their requests over it; AuthContext::on_update and use_auth_update pass the broadcasts on to the app

§Usage

Wrap the app in AuthProvider. It signs the user in (or registers them from a /register/{token} link) before rendering its children, and gives them an AuthContext through use_auth:

ⓘ
use watchword::yew023::{AuthProvider, use_auth};
use yew::prelude::*;

#[function_component]
fn Root() -> Html {
    html! {
        <AuthProvider title="My App">
            <App />
        </AuthProvider>
    }
}

#[function_component]
fn App() -> Html {
    let auth = use_auth();
    let onclick = Callback::from(move |_| auth.sign_out());
    html! { <button {onclick}>{"Sign out"}</button> }
}

Inside the provider, UserMenu gives the signed-in user Account, Users (for admins), Sign out, and a way back while an admin impersonates them. AccountPanel is the user’s own account, AdminUsers manages users for admins, and AdminCloudflare is the Cloudflare Access settings page for admins, which loads and saves the settings watchword keeps. CloudflareSettings is its form alone, for an app that stores the settings itself.


§Making requests

watchword’s components make every request with the AuthService that AuthProvider provides, and an app’s own components get the same one with use_auth_service, so an app’s account page behaves as watchword’s does: over the app’s socket once it is handed over (see below), and over HTTP otherwise. The service has a method for each operation. It only makes the request, so a component passes a changed user on to the context itself:

ⓘ
use watchword::types::UserChange;
use watchword::yew023::{use_auth, use_auth_service};

#[function_component]
fn ClearEmail() -> Html {
    let auth = use_auth();
    let service = use_auth_service();

    let onclick = Callback::from(move |_| {
        let auth = auth.clone();
        let service = service.clone();

        yew::platform::spawn_local(async move {
            let change = UserChange {
                email: Some(None),
                ..UserChange::default()
            };

            match service.update_account(change).await {
                Ok(user) => auth.set_user(user),
                Err(error) if error.is_unauthorized() => auth.check_session(),
                Err(..) => {}
            }
        });
    });

    html! { <button {onclick}>{"Forget my email"}</button> }
}

The service is a context of its own, apart from AuthContext, so a component that only shows the user does not render again when it changes.

Underneath is a Transport, which carries an AuthRequest to the server and brings back its AuthResponse. An app or a test supplies its own with the provider’s service prop, such as one that calls the server’s Auth::dispatch directly over an in-memory store, so components can be tested without HTTP or a socket:

ⓘ
use watchword::yew023::{AuthProvider, AuthService};

let service = AuthService::new(MyTransport::new());

html! {
    <AuthProvider title="My App" service={service}>
        <App />
    </AuthProvider>
}

The http module is the low-level layer under HttpTransport: one function per /api/auth route, always over HTTP.


§Following auth changes

The provider gives a new AuthContext whenever the user, their role or the config changes, so app components follow it by reading the context, without forwarding watchword’s events through broadcasts of their own:

  • Function components use use_auth, or use_is_admin for just the role; they render again on each change.
  • Struct components keep an AuthSubscription, which subscribes with a message of theirs, so they render again too. Reading the context once, with ctx.link().context::<AuthContext>(Callback::noop()), does not.
  • The users list is shared: use_users (or AuthContext::subscribe_users) gives the same Users that AdminUsers shows, fetched once for both and kept current, so an app page beside the admin page, such as one managing groups, does not fetch it again or reload it on each change.
ⓘ
use watchword::yew023::{use_is_admin, use_users};

#[function_component]
fn Groups() -> Html {
    let users = use_users();
    let count = users.entries().map_or(0, |entries| entries.len());
    html! { <p>{format!("{count} users")}</p> }
}

#[function_component]
fn Nav() -> Html {
    html! {
        if use_is_admin() {
            <a href="/admin">{"Admin"}</a>
        }
    }
}

With a musli-web socket (below), changes made elsewhere reach all of these as they happen. An app that acts on an update itself, such as reloading its own data when the user’s role changes, listens with AuthContext::on_update or use_auth_update. On the server, the app’s own authorization of a socket’s requests follows the session’s guard too: watchword’s musliweb05::Connection::require_admin reads the role as stored now, and the SessionGuard (user, watch_user) the role as last seen, so the server needs no event mirroring of its own either.


§With a musli-web socket

With the musliweb05 feature (watchword’s musliweb05 and yew023 together), an app that talks to its server over a musli-web 0.5 WebSocket hands its handle to the context once the socket is open inside AuthProvider:

ⓘ
let auth = use_auth();
auth.set_socket(Some(service.handle().clone()));

The components then follow the server’s AuthBroadcast: the context’s user is replaced when it changes elsewhere, and the shared Users (which AdminUsers shows) applies users created, changed and deleted by others in place, so the app no longer bumps its refresh prop. Each update is also passed to the app’s AuthContext::on_update listeners. The AuthService’s requests go over the socket too while it is open, the app’s own included, with the same AuthErrors as over HTTP, and over HTTP when the socket could not send them, such as while it reconnects; signing in and out, registration and impersonation always use HTTP. When the socket closes, and every 10 seconds while it stays closed, the provider checks the session over HTTP, so a session that ended, whose upgrade the server now refuses, shows the sign-in page.


§Translating

The components look their text up through phrasebook 0.2: in the Phrases the app provides with ContextProvider<Phrases>, falling back to watchword’s own English messages, L10N, for any message the app’s phrasebook lacks. Without a provider they render that English, so an app that does not translate needs nothing.

The messages are the watchword-* ids of locales/en-US.ftl. To translate them, an app adds those ids to its own .ftl resources and adds watchword’s catalog after its own, so its messages win:

ⓘ
let book = Phrasebook::builder()
    .language(Lang::EN_US, "English")
    .language(SV_SE, "Svenska")
    .catalog(&APP)
    .catalog(&watchword::yew023::L10N)
    .build();

Errors from the server, including why a request is invalid, are phrased from their codes here, so they are translated the same way. AuthProvider’s signed_out_notice and CloudflareSettings’s app_name override their messages when given.


§Styling

The components render plain semantic HTML with stable class names and take a class prop that is added to their outermost element. The class names are:

ClassElement
auth-pagethe outermost <main> of the splash, sign-in and registration pages
auth-splashthe splash while loading or when the server is unreachable
auth-cardthe box holding a page’s content
auth-titlethe app’s title, when given
auth-headingthe page’s <h1>
auth-messagethe live region holding auth-error and auth-notice
auth-erroran error
auth-noticea notice, such as that the session has ended
auth-statusprogress, such as checking a link
auth-forma form
auth-fielda label with its input or value
auth-labelthe label of a read-only value
auth-inputa text or password input
auth-valuea read-only value, such as the login on a registration page
auth-actionsa row of buttons
auth-submita form’s submit button
auth-retrythe splash’s Retry button
auth-alternativesthe other sign-in methods
auth-cloudflarethe “Sign in with Cloudflare” button
auth-googlethe “Sign in with Google” or “Sign up with Google” link, or “Switch to Google” on the account panel
auth-hinthelp text under a field or form
auth-subheadinga form’s <h3> inside a panel
auth-accountthe outermost <section> of AccountPanel
auth-detailsthe account’s role and sign-in method
auth-profilethe account’s login and email form
auth-passwordthe account’s change or set password form
auth-sign-outa Sign out button
auth-menuthe outermost <nav> of UserMenu, with auth-open while open
auth-menu-buttonthe button that opens the menu
auth-namethe user’s name in the menu button
auth-menu-itemsthe open menu’s <ul>
auth-menu-iteman item of the menu
auth-impersonatingthe banner in the menu while an admin impersonates the user, holding auth-impersonating-text and the auth-stop-impersonating button
auth-admin-usersthe outermost <section> of AdminUsers
auth-toolbarthe heading row of the users list
auth-addthe Add user button
auth-add-userthe add user form
auth-filterthe filter input and its auth-clear button
auth-usersthe users <table>
auth-usera user’s row, with auth-me on the admin’s own
auth-user-login, auth-user-email, auth-user-role, auth-user-access, auth-user-link, auth-user-actionsthe cells of a row
auth-badgethe “you” badge on the admin’s own row
auth-user-extrathe app’s extra content under a user
auth-muteda missing value, such as “No email”
auth-selecta role select
auth-accessa user’s account state, with auth-pending, auth-active or auth-revoked
auth-link-statewhen a user’s link expires, or “No link”
auth-linka link shown once: its auth-link-url input, auth-copy button and hint
auth-dangera destructive button, such as Delete
auth-confirman inline confirmation, with auth-confirm-no and auth-confirm-yes
auth-pagesthe paging row: auth-previous, auth-page-number and auth-next
auth-hiddentext for screen readers only, which the app should hide visually
auth-admin-cloudflarethe outermost <section> of AdminCloudflare, around its message and CloudflareSettings
auth-cloudflare-settingsthe outermost <section> of CloudflareSettings
auth-instructionsthe Cloudflare setup instructions
auth-togglea field with a checkbox

Every body cell of the auth-users table also carries a data-label attribute holding its column’s name, the same localized text as the column header. On a narrow screen an app can use it to show each row as a stacked card whose cells name themselves, without assuming the column order:

@media (max-width: 600px) {
  .auth-users thead { display: none; }
  .auth-users, .auth-users tbody, .auth-users tr, .auth-users td { display: block; }
  .auth-users tr { padding: 8px 0; border-bottom: 1px solid #ccc; }
  .auth-users td { display: grid; grid-template-columns: 7em 1fr; gap: 8px; border: 0; }
  .auth-users td::before { content: attr(data-label); color: #666; }
  .auth-users td.auth-user-actions { display: flex; flex-wrap: wrap; gap: 4px; }
  .auth-users td.auth-user-actions::before { content: none; }
}

Modules§

httpstd
The low-level HTTP layer: one function per /api/auth JSON route.

Structs§

AccountPanelstd
The signed-in user’s account: their role and sign-in method, their login (when the app uses logins) and email, changing or setting the password, switching to Google (when the server offers it), and Sign out.
AccountPanelPropsstd
The properties of AccountPanel.
AdminCloudflarestd
The Cloudflare Access settings page for admins, which loads and saves the settings the server keeps itself, over the /api/auth/cloudflare routes (GET and PUT).
AdminCloudflarePropsstd
The properties of AdminCloudflare.
AdminUsersstd
Admin user management over the /api/auth/users routes.
AdminUsersPropsstd
The properties of AdminUsers.
AuthContextstd
The signed-in user and what the app can do about the session, given by AuthProvider to its children. Get it with use_auth, or with AuthSubscription in a struct component.
AuthProviderstd
The gate in front of the app.
AuthProviderPropsstd
The properties of AuthProvider.
AuthServicestd
watchword’s operations, each an async method, sent over a Transport. Get it with use_auth_service.
AuthSubscriptionstd
The AuthContext for a struct component, kept current: it subscribes to the context with a message of the component’s, so the component renders again when the user, their role or the config changes, where reading the context once with Callback::noop() would not.
CloudflareSettingsstd
The Cloudflare Access settings form: setup instructions, whether signing in through Access is enabled, the team domain and audience, whether to verify the Access token and trust the email header, and a warning when the header is trusted without verifying the token.
CloudflareSettingsPropsstd
The properties of CloudflareSettings.
HttpTransportstd
The transport that sends every request over HTTP, to watchword’s /api/auth routes on the page’s origin, with http::send. The AuthService’s default outside an AuthProvider.
Listenerstd
Keeps a callback given to AuthContext called, such as by subscribe_users, until it is dropped. A struct component keeps it in a field.
Registerstd
The page of a registration or reset link: it shows the user’s login and email read-only, then the password and its confirmation, then “Sign up with Google” when the server offers it. Setting the password signs the user in.
RegisterPropsstd
The properties of Register.
SignInstd
The sign-in page, offering the enabled methods: a login (or email) and password form when config.password is set, “Sign in with Cloudflare” when config.cloudflare is, and “Sign in with Google” when config.google is. Signing in with Google comes back to the URL’s ?next= when it is a path on this origin, or else to this page.
SignInPropsstd
The properties of SignIn.
UserMenustd
A menu under the signed-in user’s name with Account, Users (for admins only) and Sign out.
UserMenuPropsstd
The properties of UserMenu.
Usersstd
The users list as last known, shared by AdminUsers and the app’s own components inside the same AuthProvider, so a page that shows both fetches it once. Get it with use_users, or with AuthContext::subscribe_users in a struct component.
UsersQuerystd
The filter and page of AdminUsers, which an app can keep in its URL.

Enums§

Errorstd
Why a request made with the AuthService, or one of the http functions, failed.

Constants§

PAGE_SIZEstd
The default page_size of AdminUsers.

Statics§

L10Nstd
watchword’s messages, in English, with every id under the watchword- prefix.

Traits§

Transportstd
Carries an AuthRequest to watchword’s service and brings back its AuthResponse: the seam under the AuthService that an app or a test supplies with AuthService::new and AuthProvider’s service prop.

Functions§

use_authstd
The AuthContext of the enclosing AuthProvider.
use_auth_servicestd
The AuthService of the enclosing AuthProvider, which is there for the sign-in and registration pages too. Outside a provider, a service over HttpTransport.
use_auth_updatemusliweb05
Calls f with each AuthUpdate the server broadcasts, as AuthContext::on_update does, for as long as the component lives. The latest f given is the one called.
use_is_adminstd
Whether the signed-in user is an admin, following changes to their role. false outside an AuthProvider, where nobody is signed in.
use_usersstd
The users list shared inside the AuthProvider, fetched when the first component asks for it. The component renders again whenever it changes. See Users.

Type Aliases§

TransportFuturestd
The future a Transport answers with. It need not be Send, since the browser has one thread.