This repository has been archived by the owner on Dec 7, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(web): add support for server authentication
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
Showing
18 changed files
with
631 additions
and
18 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
} |
Oops, something went wrong.