app: Implement login refresh

This commit is contained in:
Valentin Tolmer 2022-05-11 16:35:22 +02:00 committed by nitnelave
parent ebffc1c086
commit b54fe9128d
4 changed files with 112 additions and 59 deletions

View file

@ -85,7 +85,7 @@ impl Component for App {
}
if self.user_info.is_none() {
self.route_dispatcher
.send(RouteRequest::ReplaceRoute(Route::new_no_state("/login")));
.send(RouteRequest::ReplaceRoute(Route::from(AppRoute::Login)));
}
true
}
@ -138,7 +138,7 @@ impl App {
match &self.user_info {
None => {
self.route_dispatcher
.send(RouteRequest::ReplaceRoute(Route::new_no_state("/login")));
.send(RouteRequest::ReplaceRoute(Route::from(AppRoute::Login)));
}
Some((user_name, is_admin)) => match &self.redirect_to {
Some(url) => {
@ -148,7 +148,7 @@ impl App {
None => {
if *is_admin {
self.route_dispatcher
.send(RouteRequest::ReplaceRoute(Route::new_no_state("/users")));
.send(RouteRequest::ReplaceRoute(Route::from(AppRoute::ListUsers)));
} else {
self.route_dispatcher
.send(RouteRequest::ReplaceRoute(Route::from(

View file

@ -15,6 +15,7 @@ use yew_form_derive::Model;
pub struct LoginForm {
common: CommonComponentParts<Self>,
form: Form<FormModel>,
refreshing: bool,
}
/// The fields of the form, with the constraints.
@ -34,6 +35,7 @@ pub struct Props {
pub enum Msg {
Update,
Submit,
AuthenticationRefreshResponse(Result<(String, bool)>),
AuthenticationStartResponse(
(
opaque::client::login::ClientLogin,
@ -99,6 +101,14 @@ impl CommonComponent<LoginForm> for LoginForm {
.emit(user_info.context("Could not log in")?);
Ok(true)
}
Msg::AuthenticationRefreshResponse(user_info) => {
self.refreshing = false;
self.common.cancel_task();
if let Ok(user_info) = user_info {
self.common.on_logged_in.emit(user_info);
}
Ok(true)
}
}
}
@ -112,10 +122,19 @@ impl Component for LoginForm {
type Properties = Props;
fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
LoginForm {
let mut app = LoginForm {
common: CommonComponentParts::<Self>::create(props, link),
form: Form::<FormModel>::new(FormModel::default()),
refreshing: true,
};
if let Err(e) =
app.common
.call_backend(HostService::refresh, (), Msg::AuthenticationRefreshResponse)
{
ConsoleService::debug(&format!("Could not refresh auth: {}", e));
app.refreshing = false;
}
app
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
@ -128,63 +147,71 @@ impl Component for LoginForm {
fn view(&self) -> Html {
type Field = yew_form::Field<FormModel>;
html! {
<form
class="form center-block col-sm-4 col-offset-4">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">
<i class="bi-person-fill"/>
</span>
if self.refreshing {
html! {
<div>
<img src={"spinner.gif"} alt={"Loading"} />
</div>
}
} else {
html! {
<form
class="form center-block col-sm-4 col-offset-4">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">
<i class="bi-person-fill"/>
</span>
</div>
<Field
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"
form=&self.form
field_name="username"
placeholder="Username"
autocomplete="username"
oninput=self.common.callback(|_| Msg::Update) />
</div>
<Field
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"
form=&self.form
field_name="username"
placeholder="Username"
autocomplete="username"
oninput=self.common.callback(|_| Msg::Update) />
</div>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">
<i class="bi-lock-fill"/>
</span>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">
<i class="bi-lock-fill"/>
</span>
</div>
<Field
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"
form=&self.form
field_name="password"
input_type="password"
placeholder="Password"
autocomplete="current-password" />
</div>
<Field
class="form-control"
class_invalid="is-invalid has-error"
class_valid="has-success"
form=&self.form
field_name="password"
input_type="password"
placeholder="Password"
autocomplete="current-password" />
</div>
<div class="form-group mt-3">
<button
type="submit"
class="btn btn-primary"
disabled=self.common.is_task_running()
onclick=self.common.callback(|e: MouseEvent| {e.prevent_default(); Msg::Submit})>
{"Login"}
</button>
<NavButton
classes="btn-link btn"
disabled=self.common.is_task_running()
route=AppRoute::StartResetPassword>
{"Forgot your password?"}
</NavButton>
</div>
<div class="form-group">
{ if let Some(e) = &self.common.error {
html! { e.to_string() }
} else { html! {} }
}
</div>
</form>
<div class="form-group mt-3">
<button
type="submit"
class="btn btn-primary"
disabled=self.common.is_task_running()
onclick=self.common.callback(|e: MouseEvent| {e.prevent_default(); Msg::Submit})>
{"Login"}
</button>
<NavButton
classes="btn-link btn"
disabled=self.common.is_task_running()
route=AppRoute::StartResetPassword>
{"Forgot your password?"}
</NavButton>
</div>
<div class="form-group">
{ if let Some(e) = &self.common.error {
html! { e.to_string() }
} else { html! {} }
}
</div>
</form>
}
}
}
}

View file

@ -227,6 +227,32 @@ impl HostService {
)
}
pub fn refresh(_request: (), callback: Callback<Result<(String, bool)>>) -> Result<FetchTask> {
let set_cookies = |jwt_claims: JWTClaims| {
let is_admin = jwt_claims.groups.contains("lldap_admin");
set_cookie("user_id", &jwt_claims.user, &jwt_claims.exp)
.map(|_| set_cookie("is_admin", &is_admin.to_string(), &jwt_claims.exp))
.map(|_| (jwt_claims.user.clone(), is_admin))
.context("Error clearing cookie")
};
let parse_token = move |data: String| {
serde_json::from_str::<login::ServerLoginResponse>(&data)
.context("Could not parse response")
.and_then(|r| {
get_claims_from_jwt(r.token.as_str())
.context("Could not parse response")
.and_then(set_cookies)
})
};
call_server(
"/auth/refresh",
yew::format::Nothing,
callback,
"Could not start authentication: ",
parse_token,
)
}
// The `_request` parameter is to make it the same shape as the other functions.
pub fn logout(_request: (), callback: Callback<Result<()>>) -> Result<FetchTask> {
call_server_empty_response_with_error_message(

BIN
app/static/spinner.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB