Skip to content
This repository has been archived by the owner on Dec 7, 2023. It is now read-only.

Commit

Permalink
feat(web): add support for server authentication
Browse files Browse the repository at this point in the history
With this change, the web client now supports authentication as
implemented by the server in 411e8e2.

The GraphqlService now redirects the client to the newly added `Login`
page if the server indicates authentication is required. On this page,
a session token can be provided. Once provided and verified, the page
redirects to the home page of Automaat.

The session token is stored in a secure cookie. Visiting the login page
removes the cookie, acting as both a login and logout page.

It's still a bit rough around the edges, and could use another refactor
or two, but the concept works fine as is.

This is another step towards solving
#19.

Now that there is a way to fetch and store session data, the path is
clear to introduce persistent state to be able to store favorite tasks,
and implement other functionality that requires state to be stored.
  • Loading branch information
JeanMertz committed Jul 22, 2019
1 parent 1d3122f commit 5552179
Show file tree
Hide file tree
Showing 18 changed files with 631 additions and 18 deletions.
1 change: 1 addition & 0 deletions src/web-client/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions src/web-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ console = ["console_log", "console_error_panic_hook"]
console_error_panic_hook = { version = "0.1", optional = true }
console_log = { version = "0.1", optional = true }
dodrio = "0.1"
failure = { version = "0.1", default-features = false }
futures = { version = "0.1", default-features = false }
gloo-events = { git = "https://github.com/rustwasm/gloo.git", rev = "1078ca3166b16ea8e19d9d691935e1f0fc23f87a" }
graphql_client = { version = "0.8", default-features = false, features = [
Expand All @@ -61,6 +62,7 @@ features = [
"HashChangeEvent",
"History",
"HtmlBodyElement",
"HtmlDocument",
"HtmlElement",
"HtmlFormElement",
"HtmlInputElement",
Expand Down
232 changes: 232 additions & 0 deletions src/web-client/scss/bulma/layout/_hero.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
// Main container

.hero {
align-items: stretch;
display: flex;
flex-direction: column;
justify-content: space-between;

.navbar {
background: none;
}

.tabs {
ul {
border-bottom: none;
}
}

// Colors
@each $name, $pair in $colors {
$color: nth($pair, 1);
$color-invert: nth($pair, 2);

&.is-#{$name} {
background-color: $color;
color: $color-invert;

a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),
strong {
color: inherit;
}

.title {
color: $color-invert;
}

.subtitle {
color: rgba($color-invert, 0.9);

a:not(.button),
strong {
color: $color-invert;
}
}

.navbar-menu {
@include touch {
background-color: $color;
}
}

.navbar-item,
.navbar-link {
color: rgba($color-invert, 0.7);
}

a.navbar-item,
.navbar-link {
&:hover,
&.is-active {
background-color: darken($color, 5%);
color: $color-invert;
}
}

.tabs {
a {
color: $color-invert;
opacity: 0.9;

&:hover {
opacity: 1;
}
}

li {
&.is-active a {
opacity: 1;
}
}

&.is-boxed,
&.is-toggle {
a {
color: $color-invert;

&:hover {
background-color: rgba($black, 0.1);
}
}

li.is-active a {
&,
&:hover {
background-color: $color-invert;
border-color: $color-invert;
color: $color;
}
}
}
}

// Modifiers
&.is-bold {
$gradient-top-left: darken(saturate(adjust-hue($color, -10deg), 10%), 10%);
$gradient-bottom-right: lighten(saturate(adjust-hue($color, 10deg), 5%), 5%);

background-image: linear-gradient(141deg, $gradient-top-left 0%, $color 71%, $gradient-bottom-right 100%);

@include mobile {
.navbar-menu {
background-image: linear-gradient(141deg, $gradient-top-left 0%, $color 71%, $gradient-bottom-right 100%);
}
}
}
}
}

// Sizes
&.is-small {
.hero-body {
padding-bottom: 1.5rem;
padding-top: 1.5rem;
}
}

&.is-medium {
@include tablet {
.hero-body {
padding-bottom: 9rem;
padding-top: 9rem;
}
}
}

&.is-large {
@include tablet {
.hero-body {
padding-bottom: 18rem;
padding-top: 18rem;
}
}
}

&.is-halfheight,
&.is-fullheight,
&.is-fullheight-with-navbar {
.hero-body {
align-items: center;
display: flex;

& > .container {
flex-grow: 1;
flex-shrink: 1;
}
}
}

&.is-halfheight {
min-height: 50vh;
}

&.is-fullheight {
min-height: 100vh;
}
}

// Components

.hero-video {
@extend %overlay;

overflow: hidden;

video {
left: 50%;
min-height: 100%;
min-width: 100%;
position: absolute;
top: 50%;
transform: translate3d(-50%, -50%, 0);
}

// Modifiers
&.is-transparent {
opacity: 0.3;
}

// Responsiveness
@include mobile {
display: none;
}
}

.hero-buttons {
margin-top: 1.5rem;

// Responsiveness
@include mobile {
.button {
display: flex;

&:not(:last-child) {
margin-bottom: 0.75rem;
}
}
}


@include tablet {
display: flex;
justify-content: center;

.button:not(:last-child) {
margin-right: 1.5rem;
}
}
}

// Containers

.hero-head,
.hero-foot {
flex-grow: 0;
flex-shrink: 0;
}

.hero-body {
flex-grow: 1;
flex-shrink: 0;
padding: 3rem 1.5rem;
}
20 changes: 16 additions & 4 deletions src/web-client/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@

use crate::component;
use crate::controller::Controller;
use crate::model::{job, statistics, task, tasks};
use crate::service::GraphqlService;
use crate::model::{job, session, statistics, task, tasks};
use crate::router::Route;
use crate::service::{CookieService, GraphqlService};
use dodrio::{Node, Render, RenderContext};
use std::cell::{Ref, RefCell, RefMut};
use std::marker::PhantomData;
Expand All @@ -16,6 +17,9 @@ pub(crate) struct App<C = Controller> {
/// The GraphQL client to fetch and mutate data.
pub(crate) client: GraphqlService,

/// The cookie service to modify cookie data.
pub(crate) cookie: CookieService,

/// All tasks fetched since the start of the application session.
///
/// This is purely meant for caching purposes, the source of truth lives on
Expand All @@ -33,9 +37,10 @@ pub(crate) struct App<C = Controller> {

impl<C> App<C> {
/// Create a new application instance, with the provided GraphQL service.
pub(crate) fn new(client: GraphqlService) -> Self {
pub(crate) fn new(client: GraphqlService, cookie: CookieService) -> Self {
Self {
client,
cookie,
tasks: Rc::default(),
stats: Rc::default(),
_controller: PhantomData,
Expand Down Expand Up @@ -65,11 +70,18 @@ impl<C> App<C> {

impl<C> Render for App<C>
where
C: tasks::Actions + task::Actions + job::Actions + Clone + 'static,
C: tasks::Actions + task::Actions + job::Actions + session::Actions + Clone + 'static,
{
fn render<'b>(&self, cx: &mut RenderContext<'b>) -> Node<'b> {
use dodrio::builder::*;

// TODO: once we have actual session data to store, we should add an
// `Option<Session>` to the `App`, and trigger this route if that value
// is set to `None`, instead of reading the current path.
if let Some(Route::Login) = Route::active() {
return component::Login::<C>::new().render(cx);
}

let stats = self.stats.try_borrow().unwrap_throw();
let tasks = self.tasks().unwrap_throw();
let filtered_tasks = tasks.filtered_tasks();
Expand Down
85 changes: 85 additions & 0 deletions src/web-client/src/component/login.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
//! The login dialogue shown when authentication is required.

use crate::model::session;
use crate::utils;
use dodrio::{Node, Render, RenderContext};
use futures::prelude::*;
use std::marker::PhantomData;
use wasm_bindgen::{prelude::*, JsCast};
use wasm_bindgen_futures::spawn_local;
use web_sys::{Element, HtmlInputElement};

/// The Login component.
pub(crate) struct Login<C> {
/// Reference to application controller.
_controller: PhantomData<C>,
}

impl<C> Login<C> {
/// Create a new Login component.
pub(crate) const fn new() -> Self {
Self {
_controller: PhantomData,
}
}

/// Mark the login field as "failed" when the provided input is incorrect.
pub(crate) fn as_failed() {
let _ =
utils::element(".login input").map(|s: Element| s.set_class_name("has-text-danger"));
}
}

impl<C> Render for Login<C>
where
C: session::Actions,
{
fn render<'b>(&self, cx: &mut RenderContext<'b>) -> Node<'b> {
use dodrio::builder::*;

let logo = img(&cx)
.attr("src", "img/logo-white.svg")
.attr("alt", "Automaat logo")
.finish();

let field = input(&cx)
.attr("type", "text")
.attr("name", "token")
.attr("aria-label", "login token")
.attr("placeholder", "Login Token...")
.on("input", move |root, vdom, event| {
let target = event.target().unwrap_throw();
let value = target.unchecked_ref::<HtmlInputElement>().value();

spawn_local(C::authenticate(root, vdom, value).map_err(|_| Self::as_failed()));
})
.finish();

let text = div(&cx)
.attr("class", "description")
.children([
p(&cx)
.child(text(
"This instance of Automaat requires you to \
identify yourself.",
))
.finish(),
p(&cx)
.child(text(
"Please provide your personal token or ask someone \
to generate a new token for you.",
))
.finish(),
])
.finish();

let content = div(&cx)
.children([logo, div(&cx).child(field).finish(), text])
.finish();

section(&cx)
.attr("class", "login")
.child(div(&cx).child(content).finish())
.finish()
}
}
Loading

0 comments on commit 5552179

Please sign in to comment.