Agent oriented logged in state
All checks were successful
continuous-integration/drone/push Build is passing

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 class Mutation < GraphQL::BaseMutation
@[GraphQL::Field] @[GraphQL::Field]
# Logs in as *username* with credential *password* # 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? raise Errors::Authentication.new if username.empty? || password.empty?
user = Db::User.query.find { var(:username) == username } user = Db::User.query.find { var(:username) == username }

View file

@ -9,11 +9,11 @@ wasm-logger = "0.2.0"
log = "0.4.6" log = "0.4.6"
yew-router = "0.16.0" yew-router = "0.16.0"
wee_alloc = "0.4.5" 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" reqwest = "0.11.12"
wasm-bindgen-futures = "0.4.33"
serde = "1.0.147" serde = "1.0.147"
web-sys = { version = "0.3.60", features = ["Window", "Location"] } web-sys = { version = "0.3.60", features = ["Window", "Location"] }
wasm-cookies = "0.1.0" wasm-cookies = "0.1.0"
lazy_static = "1.4.0" lazy_static = "1.4.0"
const_format = "0.2.30" const_format = "0.2.30"
yew-agent = "0.1.0"

View file

@ -21,6 +21,11 @@ COPY ./graphql ./graphql
COPY ./src ./src COPY ./src ./src
RUN trunk build --release 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 FROM nginx:alpine as runner
COPY ./nginx.conf /etc/nginx/nginx.conf 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! createUser(checkLdap: Boolean! = true, input: UserCreateInput!): User!
createVote(input: VoteCreateInput!): Vote! createVote(input: VoteCreateInput!): Vote!
deleteUser(id: Int!): Int! deleteUser(id: Int!): Int!
login(password: String!, username: String!): LoginPayload! login(password: String!, username: String!): LoginPayload
} }
input UserCreateInput { 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 lazy_static::lazy_static;
use std::path::Path; use std::path::Path;
pub mod mutations;
pub mod queries; pub mod queries;
lazy_static! { 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", query_path = "graphql/queries/ok.graphql",
response_derives = "Debug" 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::prelude::*;
use yew_agent::{Dispatched, Dispatcher};
use yew_router::prelude::*; use yew_router::prelude::*;
use crate::cookie_names; use crate::agents;
use crate::graphql; use crate::components;
use crate::routes; use crate::routes;
pub enum Msg {
LogOut,
}
#[derive(Properties, PartialEq)] #[derive(Properties, PartialEq)]
pub struct MainProps { pub struct MainProps {
pub logged_in: bool,
#[prop_or_default] #[prop_or_default]
pub children: Children, pub children: Children,
} }
#[function_component(Main)] pub struct Main {
pub fn main(props: &MainProps) -> Html { logged_in: bool,
let client = reqwest::Client::new(); logged_in_event_bus: Dispatcher<agents::logged_in::EventBus>,
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));
});
let history = use_history().unwrap(); impl Component for Main {
let loginout_onclick = Callback::once(move |_| history.push(routes::Route::Login)); type Message = Msg;
type Properties = MainProps;
html! { fn create(ctx: &Context<Self>) -> Self {
<> Self {
<nav> logged_in: ctx.props().logged_in,
<ul> logged_in_event_bus: agents::logged_in::EventBus::dispatcher(),
<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>
<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> fn view(&self, ctx: &Context<Self>) -> Html {
{ for props.children.iter() } let logout_onclick = ctx.link().callback(move |_| Msg::LogOut);
</main>
</> 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; pub mod main;

View file

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

View file

@ -1,17 +1,27 @@
use yew::prelude::*; use yew::prelude::*;
use yew_router::prelude::*; use yew_router::prelude::*;
use frontend::routes;
#[global_allocator] #[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
use frontend::routes; pub struct App;
#[function_component(App)] impl Component for App {
fn app() -> Html { type Message = ();
html! { type Properties = ();
<BrowserRouter>
<Switch<routes::Route> render={Switch::render(routes::switch)} /> fn create(_ctx: &Context<Self>) -> Self {
</BrowserRouter> 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::*; use yew::prelude::*;
#[function_component(Home)] use crate::graphql;
pub fn home() -> Html {
html! { pub enum Msg {
<h1>{ "HOME!" }</h1> 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::prelude::*;
use yew_router::prelude::*;
#[function_component(Login)] use crate::cookie_names;
pub fn login() -> Html { use crate::graphql;
html! { use crate::routes;
<h1>{ "LOGIN!" }</h1>
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::prelude::*;
use yew_router::prelude::*; use yew_router::prelude::*;
use crate::cookie_names;
use crate::layouts; use crate::layouts;
pub mod home; pub mod home;
pub mod login; pub mod login;
pub mod not_found; pub mod not_found;
#[derive(Clone, Routable, PartialEq)] #[derive(Clone, Routable, PartialEq, Eq)]
pub enum Route { pub enum Route {
#[at("/")] #[at("/")]
Home, Home,
@ -19,11 +20,40 @@ pub enum Route {
} }
pub fn switch(routes: &Route) -> Html { pub fn switch(routes: &Route) -> Html {
match routes { let token = {
Route::Home => html! { <layouts::main::Main><home::Home /></layouts::main::Main> }, let tmp = wasm_cookies::get(cookie_names::TOKEN);
Route::Login => html! { <layouts::main::Main><login::Login /></layouts::main::Main> }, if let Some(x) = tmp {
Route::NotFound => { if let Ok(y) = x {
html! { <layouts::main::Main><not_found::NotFound /></layouts::main::Main> } 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