Agent oriented logged in state
continuous-integration/drone/push Build is passing Details

This commit is contained in:
Dominic Grimm 2022-11-05 21:27:49 +01:00
parent 860ae7ed5e
commit ff2b884d42
No known key found for this signature in database
GPG Key ID: 6F294212DEAAC530
25 changed files with 541 additions and 2151 deletions

View File

@ -26,7 +26,7 @@ module Backend
class Mutation < GraphQL::BaseMutation
@[GraphQL::Field]
# Logs in as *username* with credential *password*
def login(username : String, password : String) : LoginPayload
def login(username : String, password : String) : LoginPayload?
raise Errors::Authentication.new if username.empty? || password.empty?
user = Db::User.query.find { var(:username) == username }

View File

@ -9,11 +9,11 @@ wasm-logger = "0.2.0"
log = "0.4.6"
yew-router = "0.16.0"
wee_alloc = "0.4.5"
graphql_client = { version = "0.11.0", features = ["reqwest"] }
graphql_client = { git = "https://github.com/graphql-rust/graphql-client.git", branch = "main", features = ["reqwest"] }
reqwest = "0.11.12"
wasm-bindgen-futures = "0.4.33"
serde = "1.0.147"
web-sys = { version = "0.3.60", features = ["Window", "Location"] }
wasm-cookies = "0.1.0"
lazy_static = "1.4.0"
const_format = "0.2.30"
yew-agent = "0.1.0"

View File

@ -21,6 +21,11 @@ COPY ./graphql ./graphql
COPY ./src ./src
RUN trunk build --release
FROM tdewolff/minify as public
WORKDIR /usr/src/public
COPY --from=builder /usr/src/frontend/dist .
RUN minify . -r -o .
FROM nginx:alpine as runner
COPY ./nginx.conf /etc/nginx/nginx.conf
COPY --from=builder /usr/src/frontend/dist /var/www/html
COPY --from=public /usr/src/public /var/www/html

View File

@ -0,0 +1,5 @@
mutation Login($username: String!, $password: String!) {
login(username: $username, password: $password) {
token
}
}

View File

@ -0,0 +1,10 @@
query Me {
me {
firstName
role
student {
vote
}
teacher
}
}

View File

@ -78,7 +78,7 @@ type Mutation {
createUser(checkLdap: Boolean! = true, input: UserCreateInput!): User!
createVote(input: VoteCreateInput!): Vote!
deleteUser(id: Int!): Int!
login(password: String!, username: String!): LoginPayload!
login(password: String!, username: String!): LoginPayload
}
input UserCreateInput {

View File

@ -0,0 +1,47 @@
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use yew_agent::{Agent, AgentLink, Context, HandlerId};
#[derive(Serialize, Deserialize, Debug)]
pub enum Request {
LoggedIn(bool),
}
pub struct EventBus {
link: AgentLink<Self>,
subscribers: HashSet<HandlerId>,
}
impl Agent for EventBus {
type Reach = Context<Self>;
type Message = ();
type Input = Request;
type Output = bool;
fn create(link: AgentLink<Self>) -> Self {
Self {
link,
subscribers: HashSet::new(),
}
}
fn update(&mut self, _msg: Self::Message) {}
fn handle_input(&mut self, msg: Self::Input, _id: HandlerId) {
match msg {
Request::LoggedIn(x) => {
for sub in self.subscribers.iter() {
self.link.respond(*sub, x);
}
}
}
}
fn connected(&mut self, id: HandlerId) {
self.subscribers.insert(id);
}
fn disconnected(&mut self, id: HandlerId) {
self.subscribers.remove(&id);
}
}

View File

@ -0,0 +1 @@
pub mod logged_in;

View File

@ -0,0 +1,54 @@
use yew::prelude::*;
use yew_agent::{Bridge, Bridged};
use yew_router::prelude::*;
use crate::agents;
use crate::cookie_names;
use crate::routes;
pub enum Msg {
LoggedIn(bool),
}
#[derive(Properties, PartialEq)]
pub struct LoggedInHandlerProps {
pub logged_in: bool,
}
pub struct LoggedInHandler {
logged_in: bool,
_logged_in_producer: Box<dyn Bridge<agents::logged_in::EventBus>>,
}
impl Component for LoggedInHandler {
type Message = Msg;
type Properties = LoggedInHandlerProps;
fn create(ctx: &Context<Self>) -> Self {
Self {
logged_in: ctx.props().logged_in,
_logged_in_producer: agents::logged_in::EventBus::bridge(
ctx.link().callback(Msg::LoggedIn),
),
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::LoggedIn(x) => {
if self.logged_in && !x {
log::info!("Global logout!");
wasm_cookies::delete(cookie_names::TOKEN);
ctx.link().history().unwrap().push(routes::Route::Login);
}
self.logged_in = x;
false
}
}
}
fn view(&self, _ctx: &Context<Self>) -> Html {
html! {}
}
}

View File

@ -0,0 +1 @@
pub mod logged_in_handler;

View File

@ -1,6 +1,7 @@
use lazy_static::lazy_static;
use std::path::Path;
pub mod mutations;
pub mod queries;
lazy_static! {

View File

@ -0,0 +1,9 @@
use graphql_client::GraphQLQuery;
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "graphql/schema.graphql",
query_path = "graphql/mutations/login.graphql",
response_derives = "Debug,Serialize,Deserialize"
)]
pub struct Login;

View File

@ -0,0 +1 @@
pub mod login;

View File

@ -0,0 +1,9 @@
use graphql_client::GraphQLQuery;
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "graphql/schema.graphql",
query_path = "graphql/queries/me.graphql",
response_derives = "Debug"
)]
pub struct Me;

View File

@ -0,0 +1,2 @@
pub mod me;
pub mod ok;

View File

@ -6,4 +6,4 @@ use graphql_client::GraphQLQuery;
query_path = "graphql/queries/ok.graphql",
response_derives = "Debug"
)]
pub struct Ok;
pub struct Ok;

View File

@ -0,0 +1,58 @@
use yew::prelude::*;
use yew_agent::{Bridge, Bridged};
use yew_router::prelude::*;
use yew_router::scope_ext::RouterScopeExt;
use crate::agents;
use crate::routes;
pub enum Msg {
LoggedIn(bool),
}
#[derive(Properties, PartialEq)]
pub struct LoggedInProps {
#[prop_or_default]
pub children: Children,
pub logged_in: bool,
}
pub struct LoggedIn {
logged_in: bool,
_logged_in_producer: Box<dyn Bridge<agents::logged_in::EventBus>>,
}
impl Component for LoggedIn {
type Message = Msg;
type Properties = LoggedInProps;
fn create(ctx: &Context<Self>) -> Self {
Self {
logged_in: ctx.props().logged_in,
_logged_in_producer: agents::logged_in::EventBus::bridge(
ctx.link().callback(Msg::LoggedIn),
),
}
}
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::LoggedIn(x) => {
let prev = self.logged_in;
self.logged_in = x;
prev != self.logged_in
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
if !self.logged_in {
log::info!("Viewing logged in required site while not logged in!");
ctx.link().history().unwrap().push(routes::Route::Login);
}
html! {
{ for ctx.props().children.iter() }
}
}
}

View File

@ -1,56 +1,79 @@
use graphql_client::reqwest::post_graphql;
use wasm_bindgen_futures;
use yew::prelude::*;
use yew_agent::{Dispatched, Dispatcher};
use yew_router::prelude::*;
use crate::cookie_names;
use crate::graphql;
use crate::agents;
use crate::components;
use crate::routes;
pub enum Msg {
LogOut,
}
#[derive(Properties, PartialEq)]
pub struct MainProps {
pub logged_in: bool,
#[prop_or_default]
pub children: Children,
}
#[function_component(Main)]
pub fn main(props: &MainProps) -> Html {
let client = reqwest::Client::new();
wasm_bindgen_futures::spawn_local(async move {
let response = post_graphql::<graphql::queries::Ok, _>(
&client,
graphql::URL.as_str(),
graphql::queries::ok::Variables {},
)
.await
.unwrap();
log::debug!("{:?}", response);
log::debug!("{:?}", wasm_cookies::get(cookie_names::TOKEN));
});
pub struct Main {
logged_in: bool,
logged_in_event_bus: Dispatcher<agents::logged_in::EventBus>,
}
let history = use_history().unwrap();
let loginout_onclick = Callback::once(move |_| history.push(routes::Route::Login));
impl Component for Main {
type Message = Msg;
type Properties = MainProps;
html! {
<>
<nav>
<ul>
<li>
<Link<routes::Route> to={routes::Route::Home}>
<button>{ "Home" }</button>
</Link<routes::Route>>
</li>
<li>
<button onclick={loginout_onclick}>{ "Login/Logout" }</button>
</li>
</ul>
</nav>
fn create(ctx: &Context<Self>) -> Self {
Self {
logged_in: ctx.props().logged_in,
logged_in_event_bus: agents::logged_in::EventBus::dispatcher(),
}
}
<hr />
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::LogOut => {
self.logged_in_event_bus
.send(agents::logged_in::Request::LoggedIn(false));
false
}
}
}
<main>
{ for props.children.iter() }
</main>
</>
fn view(&self, ctx: &Context<Self>) -> Html {
let logout_onclick = ctx.link().callback(move |_| Msg::LogOut);
html! {
<>
<nav>
<ul>
<li>
<Link<routes::Route> to={routes::Route::Home}>
<button>{ "Home" }</button>
</Link<routes::Route>>
</li>
<li>
if self.logged_in {
<button onclick={logout_onclick}>{ "Logout" }</button>
} else {
<Link<routes::Route> to={routes::Route::Login}>
<button>{ "Login" }</button>
</Link<routes::Route>>
}
</li>
</ul>
</nav>
<hr />
<components::logged_in_handler::LoggedInHandler logged_in={self.logged_in} />
<main>
{ for ctx.props().children.iter() }
</main>
</>
}
}
}

View File

@ -1 +1,2 @@
pub mod logged_in;
pub mod main;

View File

@ -1,3 +1,4 @@
pub mod agents;
pub mod components;
pub mod cookie_names;
pub mod graphql;

View File

@ -1,17 +1,27 @@
use yew::prelude::*;
use yew_router::prelude::*;
use frontend::routes;
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
use frontend::routes;
pub struct App;
#[function_component(App)]
fn app() -> Html {
html! {
<BrowserRouter>
<Switch<routes::Route> render={Switch::render(routes::switch)} />
</BrowserRouter>
impl Component for App {
type Message = ();
type Properties = ();
fn create(_ctx: &Context<Self>) -> Self {
Self
}
fn view(&self, _ctx: &Context<Self>) -> Html {
html! {
<BrowserRouter>
<Switch<routes::Route> render={Switch::render(routes::switch)} />
</BrowserRouter>
}
}
}

View File

@ -1,8 +1,100 @@
use graphql_client::reqwest::post_graphql;
use yew::prelude::*;
#[function_component(Home)]
pub fn home() -> Html {
html! {
<h1>{ "HOME!" }</h1>
use crate::graphql;
pub enum Msg {
DoneFetching {
errors: Option<Vec<String>>,
data: graphql::queries::me::me::ResponseData,
},
}
#[derive(Properties, PartialEq)]
pub struct HomeProps {
pub token: String,
}
enum State {
Fetching,
Done,
}
pub struct Home {
state: State,
errors: Option<Vec<String>>,
data: Option<graphql::queries::me::me::ResponseData>,
}
impl Component for Home {
type Message = Msg;
type Properties = HomeProps;
fn create(_ctx: &Context<Self>) -> Self {
Self {
state: State::Fetching,
errors: None,
data: None,
}
}
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::DoneFetching { errors, data } => {
self.state = State::Done;
self.errors = errors;
true
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
match self.state {
State::Fetching => {
let mut headers = reqwest::header::HeaderMap::new();
headers.insert(
"Authorization",
reqwest::header::HeaderValue::from_str(&format!(
"Bearer {}",
ctx.props().token
))
.unwrap(),
);
ctx.link().send_future(async move {
let response = post_graphql::<graphql::queries::me::Me, _>(
&reqwest::Client::builder()
.default_headers(headers)
.build()
.unwrap(),
graphql::URL.as_str(),
graphql::queries::me::me::Variables,
)
.await
.unwrap();
log::debug!("{:?}", response.data);
Msg::DoneFetching {
errors: response
.errors
.map(|x| x.iter().map(|e| e.message.to_owned()).collect()),
data: response.data.unwrap(),
}
});
html! {
<p>{ "Fetching..." }</p>
}
}
State::Done => html! {
if let Some(errors) = &self.errors {
<p>{ "Errors:" }</p>
<div>
{ for errors.iter().map(|e| html! { <p style="color: red;">{ e }</p> }) }
</div>
} else {
<h1>{ "Hey, !" }</h1>
}
},
}
}
}

View File

@ -1,8 +1,123 @@
use graphql_client::reqwest::post_graphql;
use web_sys::HtmlInputElement;
use yew::prelude::*;
use yew_router::prelude::*;
#[function_component(Login)]
pub fn login() -> Html {
html! {
<h1>{ "LOGIN!" }</h1>
use crate::cookie_names;
use crate::graphql;
use crate::routes;
pub enum Msg {
Submit,
Login(Option<Vec<String>>),
}
pub struct Login {
username: NodeRef,
password: NodeRef,
errors: Option<Vec<String>>,
}
impl Component for Login {
type Message = Msg;
type Properties = ();
fn create(_ctx: &Context<Self>) -> Self {
Self {
username: NodeRef::default(),
password: NodeRef::default(),
errors: None,
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::Submit => {
if let (Some(username), Some(password)) = (
self.username.cast::<HtmlInputElement>().map(|x| x.value()),
self.password.cast::<HtmlInputElement>().map(|x| x.value()),
) {
if !username.is_empty() && !password.is_empty() {
ctx.link().send_future(async {
let response = post_graphql::<graphql::mutations::login::Login, _>(
&reqwest::Client::new(),
graphql::URL.as_str(),
graphql::mutations::login::login::Variables { username, password },
)
.await
.unwrap();
if response.errors.is_some() {
wasm_cookies::delete(cookie_names::TOKEN);
} else {
wasm_cookies::set(
cookie_names::TOKEN,
&response.data.unwrap().login.unwrap().token,
&wasm_cookies::CookieOptions::default(),
)
}
Msg::Login(
response
.errors
.map(|x| x.iter().map(|e| e.message.to_owned()).collect()),
)
});
}
}
false
}
Msg::Login(errors) => {
self.errors = errors;
if self.errors.is_none() {
ctx.link().history().unwrap().push(routes::Route::Home);
false
} else {
true
}
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
let onsubmit = link.callback(|e: FocusEvent| {
e.prevent_default();
Msg::Submit
});
html! {
<>
<form {onsubmit}>
<label for="username">
{ "Benutzername:" }
<br />
<input ref={self.username.clone()} type="text" id="username" />
</label>
<br />
<label for="password">
{ "Kennwort:" }
<br />
<input ref={self.password.clone()} type="password" id="password" />
</label>
<br /><br />
<button type="submit">{ "Login" }</button>
</form>
if let Some(errors) = &self.errors {
<div>
{ for errors.iter().map(|e| html! { <p style="color: red;">{ e }</p> }) }
</div>
}
</>
}
}
}

View File

@ -1,13 +1,14 @@
use yew::prelude::*;
use yew_router::prelude::*;
use crate::cookie_names;
use crate::layouts;
pub mod home;
pub mod login;
pub mod not_found;
#[derive(Clone, Routable, PartialEq)]
#[derive(Clone, Routable, PartialEq, Eq)]
pub enum Route {
#[at("/")]
Home,
@ -19,11 +20,40 @@ pub enum Route {
}
pub fn switch(routes: &Route) -> Html {
match routes {
Route::Home => html! { <layouts::main::Main><home::Home /></layouts::main::Main> },
Route::Login => html! { <layouts::main::Main><login::Login /></layouts::main::Main> },
Route::NotFound => {
html! { <layouts::main::Main><not_found::NotFound /></layouts::main::Main> }
let token = {
let tmp = wasm_cookies::get(cookie_names::TOKEN);
if let Some(x) = tmp {
if let Ok(y) = x {
Some(y)
} else {
wasm_cookies::delete(cookie_names::TOKEN);
None
}
} else {
None
}
};
let logged_in = token.is_some();
match routes {
Route::Home => {
html! {
<layouts::logged_in::LoggedIn {logged_in}>
<layouts::main::Main {logged_in}>
<home::Home token={token.unwrap()} />
</layouts::main::Main>
</layouts::logged_in::LoggedIn>
}
}
Route::Login => html! {
<layouts::main::Main {logged_in}>
<login::Login />
</layouts::main::Main>
},
Route::NotFound => html! {
<layouts::main::Main {logged_in}>
<not_found::NotFound />
</layouts::main::Main>
},
}
}

File diff suppressed because it is too large Load Diff