Expand description
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.
| Feature | What it provides |
|---|---|
std (default) | everything here |
alloc | nothing yet beyond watchword-core’s alloc |
musliweb05 | std + 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, oruse_is_adminfor 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, withctx.link().context::<AuthContext>(Callback::noop()), does not. - The users list is shared:
use_users(orAuthContext::subscribe_users) gives the sameUsersthatAdminUsersshows, 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:
| Class | Element |
|---|---|
auth-page | the outermost <main> of the splash, sign-in and registration pages |
auth-splash | the splash while loading or when the server is unreachable |
auth-card | the box holding a page’s content |
auth-title | the app’s title, when given |
auth-heading | the page’s <h1> |
auth-message | the live region holding auth-error and auth-notice |
auth-error | an error |
auth-notice | a notice, such as that the session has ended |
auth-status | progress, such as checking a link |
auth-form | a form |
auth-field | a label with its input or value |
auth-label | the label of a read-only value |
auth-input | a text or password input |
auth-value | a read-only value, such as the login on a registration page |
auth-actions | a row of buttons |
auth-submit | a form’s submit button |
auth-retry | the splash’s Retry button |
auth-alternatives | the other sign-in methods |
auth-cloudflare | the “Sign in with Cloudflare” button |
auth-google | the “Sign in with Google” or “Sign up with Google” link, or “Switch to Google” on the account panel |
auth-hint | help text under a field or form |
auth-subheading | a form’s <h3> inside a panel |
auth-account | the outermost <section> of AccountPanel |
auth-details | the account’s role and sign-in method |
auth-profile | the account’s login and email form |
auth-password | the account’s change or set password form |
auth-sign-out | a Sign out button |
auth-menu | the outermost <nav> of UserMenu, with auth-open while open |
auth-menu-button | the button that opens the menu |
auth-name | the user’s name in the menu button |
auth-menu-items | the open menu’s <ul> |
auth-menu-item | an item of the menu |
auth-impersonating | the banner in the menu while an admin impersonates the user, holding auth-impersonating-text and the auth-stop-impersonating button |
auth-admin-users | the outermost <section> of AdminUsers |
auth-toolbar | the heading row of the users list |
auth-add | the Add user button |
auth-add-user | the add user form |
auth-filter | the filter input and its auth-clear button |
auth-users | the users <table> |
auth-user | a 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-actions | the cells of a row |
auth-badge | the “you” badge on the admin’s own row |
auth-user-extra | the app’s extra content under a user |
auth-muted | a missing value, such as “No email” |
auth-select | a role select |
auth-access | a user’s account state, with auth-pending, auth-active or auth-revoked |
auth-link-state | when a user’s link expires, or “No link” |
auth-link | a link shown once: its auth-link-url input, auth-copy button and hint |
auth-danger | a destructive button, such as Delete |
auth-confirm | an inline confirmation, with auth-confirm-no and auth-confirm-yes |
auth-pages | the paging row: auth-previous, auth-page-number and auth-next |
auth-hidden | text for screen readers only, which the app should hide visually |
auth-admin-cloudflare | the outermost <section> of AdminCloudflare, around its message and CloudflareSettings |
auth-cloudflare-settings | the outermost <section> of CloudflareSettings |
auth-instructions | the Cloudflare setup instructions |
auth-toggle | a 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§
- http
std - The low-level HTTP layer: one function per
/api/authJSON route.
Structs§
- Account
Panel std - 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.
- Account
Panel Props std - The properties of
AccountPanel. - Admin
Cloudflare std - The Cloudflare Access settings page for admins, which loads and saves the
settings the server keeps itself, over the
/api/auth/cloudflareroutes (GETandPUT). - Admin
Cloudflare Props std - The properties of
AdminCloudflare. - Admin
Users std - Admin user management over the
/api/auth/usersroutes. - Admin
Users Props std - The properties of
AdminUsers. - Auth
Context std - The signed-in user and what the app can do about the session, given by
AuthProviderto its children. Get it withuse_auth, or withAuthSubscriptionin a struct component. - Auth
Provider std - The gate in front of the app.
- Auth
Provider Props std - The properties of
AuthProvider. - Auth
Service std - watchword’s operations, each an
asyncmethod, sent over aTransport. Get it withuse_auth_service. - Auth
Subscription std - The
AuthContextfor 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 withCallback::noop()would not. - Cloudflare
Settings std - 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.
- Cloudflare
Settings Props std - The properties of
CloudflareSettings. - Http
Transport std - The transport that sends every request over HTTP, to watchword’s
/api/authroutes on the page’s origin, withhttp::send. TheAuthService’s default outside anAuthProvider. - Listener
std - Keeps a callback given to
AuthContextcalled, such as bysubscribe_users, until it is dropped. A struct component keeps it in a field. - Register
std - 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.
- Register
Props std - The properties of
Register. - SignIn
std - The sign-in page, offering the enabled methods: a login (or email) and
password form when
config.passwordis set, “Sign in with Cloudflare” whenconfig.cloudflareis, and “Sign in with Google” whenconfig.googleis. Signing in with Google comes back to the URL’s?next=when it is a path on this origin, or else to this page. - Sign
InProps std - The properties of
SignIn. - User
Menu std - A menu under the signed-in user’s name with Account, Users (for admins only) and Sign out.
- User
Menu Props std - The properties of
UserMenu. - Users
std - The users list as last known, shared by
AdminUsersand the app’s own components inside the sameAuthProvider, so a page that shows both fetches it once. Get it withuse_users, or withAuthContext::subscribe_usersin a struct component. - Users
Query std - The filter and page of
AdminUsers, which an app can keep in its URL.
Enums§
- Error
std - Why a request made with the
AuthService, or one of thehttpfunctions, failed.
Constants§
- PAGE_
SIZE std - The default
page_sizeofAdminUsers.
Statics§
- L10N
std - watchword’s messages, in English, with every id under the
watchword-prefix.
Traits§
- Transport
std - Carries an
AuthRequestto watchword’s service and brings back itsAuthResponse: the seam under theAuthServicethat an app or a test supplies withAuthService::newandAuthProvider’sserviceprop.
Functions§
- use_
auth std - The
AuthContextof the enclosingAuthProvider. - use_
auth_ service std - The
AuthServiceof the enclosingAuthProvider, which is there for the sign-in and registration pages too. Outside a provider, a service overHttpTransport. - use_
auth_ update musliweb05 - Calls
fwith eachAuthUpdatethe server broadcasts, asAuthContext::on_updatedoes, for as long as the component lives. The latestfgiven is the one called. - use_
is_ admin std - Whether the signed-in user is an admin, following changes to their role.
falseoutside anAuthProvider, where nobody is signed in. - use_
users std - The users list shared inside the
AuthProvider, fetched when the first component asks for it. The component renders again whenever it changes. SeeUsers.
Type Aliases§
- Transport
Future std - The future a
Transportanswers with. It need not beSend, since the browser has one thread.