some experimentation with frontend

This commit is contained in:
Lennart
2024-10-04 21:36:07 +02:00
parent 8e1e1d5af5
commit 59006bc9f2
9 changed files with 188 additions and 20 deletions

View File

@@ -1,15 +1,19 @@
[package]
name = "rustical_frontend"
version = "0.1.0"
edition = "2021"
version.workspace = true
edition.workspace = true
description.workspace = true
repository.workspace = true
publish = false
[dependencies]
actix-web = "4.9"
askama = { version = "0.12", features = ["serde", "with-actix-web"] }
askama_actix = "0.14"
tokio = { version = "1.40", features = ["sync", "full"] }
rustical_store = { path = "../store/" }
anyhow = "1.0"
thiserror = "1.0"
actix-session = { version = "0.10", features = ["cookie-session"] }
serde = { version = "1.0", features = ["derive", "rc"] }
askama = { workspace = true }
askama_actix = { workspace = true }
actix-session = { workspace = true }
serde = { workspace = true }
anyhow = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true }
actix-web = { workspace = true }
rustical_store = { workspace = true }
actix-files = "0.6.6"

View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title></title>
<link href="style.css" rel="stylesheet">
</head>
<body>
ok wow!
</body>
</html>

View File

View File

@@ -0,0 +1,6 @@
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct FrontendConfig {
secret_key: String,
}

View File

@@ -1,31 +1,93 @@
use actix_session::{storage::CookieSessionStore, Session, SessionMiddleware};
use actix_web::{
cookie::Key,
get,
http::Method,
web::{self, Data},
web::{self, Data, Path},
Responder,
};
use askama::Template;
use rustical_store::CalendarStore;
use login::{route_get_login, route_post_login};
use rustical_store::{
auth::{AuthenticationMiddleware, AuthenticationProvider, User},
model::Calendar,
CalendarStore,
};
use std::sync::Arc;
use tokio::sync::RwLock;
mod config;
mod login;
pub use config::FrontendConfig;
#[derive(Template)]
#[template(path = "index.html")]
struct IndexTemplate {}
#[get("")]
async fn route_index() -> IndexTemplate {
async fn route_index(session: Session) -> IndexTemplate {
if let Some(user) = session.get::<User>("user").unwrap() {
dbg!(user);
} else {
session.insert("user", "lennart").unwrap();
}
dbg!(session.status());
IndexTemplate {}
}
async fn route_user<C: CalendarStore + ?Sized>(store: Data<RwLock<C>>) -> IndexTemplate {
IndexTemplate {}
#[derive(Template)]
#[template(path = "components/calendar_list.html")]
struct CalendarList {
pub owner: String,
pub calendars: Vec<Calendar>,
}
pub fn configure_frontend<C: CalendarStore + ?Sized>(
#[derive(Template)]
#[template(path = "layouts/default.html")]
struct DefaultTemplate<Body: Template> {
pub body: Body,
}
async fn route_user<C: CalendarStore + ?Sized>(
path: Path<String>,
store: Data<RwLock<C>>,
) -> impl Responder {
let store = store.read().await;
let owner = path.into_inner();
DefaultTemplate {
body: CalendarList {
owner: owner.to_owned(),
calendars: store.get_calendars(&owner).await.unwrap(),
},
}
}
pub fn configure_frontend<AP: AuthenticationProvider, C: CalendarStore + ?Sized>(
cfg: &mut web::ServiceConfig,
auth_provider: Arc<AP>,
store: Arc<RwLock<C>>,
) {
cfg.app_data(Data::from(store.clone()))
cfg.service(
web::scope("")
.wrap(AuthenticationMiddleware::new(auth_provider.clone()))
.wrap(
SessionMiddleware::builder(CookieSessionStore::default(), Key::from(&[0; 64]))
.cookie_secure(true)
.cookie_content_security(actix_session::config::CookieContentSecurity::Private)
.build(),
)
.app_data(Data::from(auth_provider))
.app_data(Data::from(store.clone()))
.service(actix_files::Files::new("/public", "crates/frontend/public").prefer_utf8(true))
.service(route_index)
.service(web::resource("/user/{user}").route(web::method(Method::GET).to(route_user::<C>)));
.service(
web::resource("/user/{user}").route(web::method(Method::GET).to(route_user::<C>)),
)
.service(
web::resource("/login")
.route(web::method(Method::GET).to(route_get_login))
.route(web::method(Method::POST).to(route_post_login::<AP>)),
),
);
}

View File

@@ -0,0 +1,47 @@
use actix_session::Session;
use actix_web::{
error::ErrorUnauthorized,
web::{Data, Form, Redirect},
HttpRequest, HttpResponse, Responder,
};
use askama::Template;
use rustical_store::auth::AuthenticationProvider;
use serde::Deserialize;
use crate::DefaultTemplate;
#[derive(Template)]
#[template(path = "components/login.html")]
struct LoginForm;
pub async fn route_get_login() -> impl Responder {
DefaultTemplate { body: LoginForm }
}
#[derive(Deserialize)]
pub struct PostLoginForm {
username: String,
password: String,
}
pub async fn route_post_login<AP: AuthenticationProvider>(
req: HttpRequest,
form: Form<PostLoginForm>,
session: Session,
auth_provider: Data<AP>,
) -> HttpResponse {
// TODO: implement auth check
dbg!(&form.username, &form.password);
if let Ok(Some(user)) = auth_provider
.validate_user_token(&form.username, &form.password)
.await
{
session.insert("user", user).unwrap();
Redirect::to(format!("/frontend/user/{}", &form.username))
.see_other()
.respond_to(&req)
.map_into_boxed_body()
} else {
ErrorUnauthorized("Unauthorized").error_response()
}
}

View File

@@ -0,0 +1,12 @@
<h2>Welcome {{ owner }}!</h2>
<ul>
{% for calendar in calendars %}
<li>
{% if let Some(name) = calendar.displayname %}
{{ name }}
{% else %}
{{ calendar.id }}
{% endif %}
</li>
{% endfor %}
</ul>

View File

@@ -0,0 +1,7 @@
<form action="login" method="post">
<label for="username">Username</label>
<input type="text" name="username" placeholder="username">
<label for="password">Password</label>
<input type="password" name="password" placeholder="password">
<button type="submit">Login</button>
</form>

View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>RustiCal</title>
<link href="css/style.css" rel="stylesheet">
</head>
<body>
{{ body|safe }}
</body>
</html>