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

View File

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

BIN
app/static/spinner.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB