LOOK MA, NO WARP

This commit is contained in:
famfo 2022-05-10 16:34:13 +02:00
parent ab59cd4bdb
commit f48c6f5054
10 changed files with 672 additions and 903 deletions

1180
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -8,15 +8,28 @@ strip = "debuginfo"
lto = true
[dependencies]
tokio = { version = "1", features = ["rt", "sync", "signal", "macros"] }
toml = "0.5"
serde = { version = "1.0", features = ["derive"] }
sqlx = { version = "0.5", default-features = false, features = ["runtime-tokio-rustls", "postgres"] }
tracing = "0.1"
tracing-subscriber = "0.3"
warp = { version = "0.3", default-features = false }
warp-sessions = "1.0"
tera = "1.15"
async-sqlx-session = { version = "0.4", default-features = false, features = ["pg"] }
anyhow = "1.0"
chrono = "0.4"
dotenv = "0.15"
blake2 = "0.10"
askama = "0.11"
actix-web = "4.0"
serde = { version = "1.0", features = ["derive"] }
async-sqlx-session = { version = "0.4", default-features = false, features = ["pg"] }
tokio = { version = "1.18", features = [
"rt",
"sync",
"signal",
"macros"
]}
sqlx = { version = "0.5", default-features = false, features = [
"runtime-tokio-rustls",
"postgres",
"macros"
]}

View file

@ -17,7 +17,9 @@
*/
use serde::Deserialize;
use std::env;
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::fs;
#[derive(Deserialize)]
@ -29,8 +31,11 @@ pub struct Config {
}
impl Config {
pub async fn load_from_file(filename: &str) -> anyhow::Result<Self> {
let config_str = fs::read_to_string(filename).await?;
Ok(toml::from_str(&config_str)?)
pub async fn load() -> anyhow::Result<Arc<Self>> {
let config_var = env::var("TMTD_CONFIG");
let config_path = config_var.as_deref().unwrap_or("tmtd.toml");
println!("Loading config from '{}'...", config_path);
let config_str = fs::read_to_string(config_path).await?;
Ok(Arc::new(toml::from_str(&config_str)?))
}
}

View file

@ -16,7 +16,7 @@
* this program. If not, see <http://www.gnu.org/licenses/>.
*/
use crate::task;
use crate::templates;
use sqlx::postgres::{PgConnectOptions, PgConnectionInfo, PgPoolOptions};
use sqlx::Executor;
use sqlx::{ConnectOptions, PgPool};
@ -51,9 +51,9 @@ impl Database {
}
// Async might become a problem here
pub fn get_tasks(&self) -> Vec<task::Task> {
pub fn get_tasks(&self) -> templates::Tasks {
// TODO: actually get the issues from the db
vec![task::Task {
let vec = vec![templates::Task {
title: "TODO".to_string(),
date: "TODO".to_string(), // Convert from unix timestamps to
// the actual date, also timezone info?
@ -61,14 +61,8 @@ impl Database {
assignee: "TODO".to_string(),
description: "TODO".to_string(),
id: 1,
}]
}
}];
pub async fn move_task(&self, task: task::MoveRequest) {
// TODO: move the task to the desired category
}
pub async fn create_task(&self, task: task::Create) {
// TODO: insert the task into the db
templates::Tasks { tasks: vec }
}
}

View file

@ -16,7 +16,6 @@
* this program. If not, see <http://www.gnu.org/licenses/>.
*/
use crate::web::App;
use crate::{config::Config, database::Database};
use async_sqlx_session::PostgresSessionStore;
use std::str::FromStr;
@ -25,40 +24,17 @@ use std::{env, sync::Arc};
use tokio::sync::broadcast;
use tokio::task::JoinHandle;
use tokio::time::sleep;
use tracing::{debug, error, info, Level};
use tracing::{error, info, Level};
mod config;
mod database;
mod task;
mod templates;
mod web;
#[cfg(unix)]
async fn terminate_signal() {
use tokio::signal::unix::{signal, SignalKind};
let mut sigterm = signal(SignalKind::terminate()).unwrap();
let mut sigint = signal(SignalKind::interrupt()).unwrap();
debug!("Installed ctrl+c handler");
tokio::select! {
_ = sigterm.recv() => {},
_ = sigint.recv() => {}
}
}
#[cfg(windows)]
async fn terminate_signal() {
use tokio::signal::windows::ctrl_c;
let mut ctrlc = ctrl_c().unwrap();
debug!("Installed ctrl+c handler");
let _ = ctrlc.recv().await;
}
#[tokio::main(flavor = "current_thread")]
async fn main() -> anyhow::Result<()> {
let config_var = env::var("TMTD_CONFIG");
let config_path = config_var.as_deref().unwrap_or("tmtd.toml");
println!("Loading config from '{}'...", config_path);
let cfg = Arc::new(Config::load_from_file(config_path).await?);
let cfg = Config::load().await?;
tracing_subscriber::fmt::fmt()
.with_max_level({
if let Some(o) = cfg.log_level.as_deref() {
@ -80,19 +56,15 @@ async fn main() -> anyhow::Result<()> {
spawn_session_cleanup_task(&session_store, Duration::from_secs(600), ctx.subscribe());
info!("Started session cleanup task");
let web_app = App::new(cfg.clone(), database.clone(), session_store)?;
let web_task = tokio::spawn(web_app.run(ctx.subscribe()));
info!("Started the web app at http://{}", cfg.listen_addr);
let web = web::App::new(cfg.listen_addr, database.clone()).await?;
terminate_signal().await;
info!("Started the web app at http://{}", cfg.listen_addr);
web.server.await?;
ctx.send(()).unwrap();
cleanup_task
.await
.unwrap_or_else(|e| error!("Couldn't join cleanup task: {}", e));
web_task
.await
.unwrap_or_else(|e| error!("Couldn't join web task: {}", e));
database.close().await;
Ok(())

View file

@ -16,33 +16,24 @@
* this program. If not, see <http://www.gnu.org/licenses/>.
*/
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
pub struct Request {
pub request: String,
}
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct MoveRequest {
pub request: String,
pub task_id: u32,
}
#[derive(Debug, Deserialize)]
pub struct Create {
pub struct CreateTask {
pub title: String,
pub status: String,
pub assignee: String,
pub description: String,
}
#[derive(Debug, Serialize)]
pub struct Task {
pub title: String,
pub date: String,
pub status: String,
pub assignee: String,
pub description: String,
pub id: u32,
#[derive(Debug, Deserialize)]
pub struct MoveTask {
pub category: String,
pub task_id: String,
}
#[derive(Debug, Deserialize)]
pub struct SortTask {
pub category: String,
}

41
src/templates.rs Normal file
View file

@ -0,0 +1,41 @@
/*
* tmtd - Suckless To Do list
* Copyright (C) 2022 C4TG1RL5
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*/
use askama::Template;
#[derive(Template)]
#[template(path = "task/task_page.html")]
pub struct Tasks {
pub tasks: Vec<Task>,
}
pub struct Task {
pub title: String,
pub date: String,
pub status: String,
pub assignee: String,
pub description: String,
pub id: u32,
}
#[derive(Template)]
#[template(path = "task/create_task.html")]
pub struct CreateTask();

View file

@ -16,189 +16,66 @@
* this program. If not, see <http://www.gnu.org/licenses/>.
*/
use crate::{task, templates, database::Database};
use actix_web::{http::StatusCode, web, HttpRequest, HttpResponse, HttpResponseBuilder, HttpServer, Responder};
use askama::Template;
use std::net::SocketAddr;
use std::sync::Arc;
use async_sqlx_session::PostgresSessionStore;
use chrono::Duration;
use sqlx::types::chrono::Utc;
use std::time::SystemTime;
use tera::{Context, Tera};
use tokio::sync::broadcast;
use tracing::info;
use warp::{Filter, Reply};
use warp_sessions::{CookieOptions, SameSiteCookieOption, SessionWithStore};
use crate::{task, Config, Database};
macro_rules! warp_try {
($expr:expr) => {
match $expr {
Ok(o) => o,
Err(e) => {
use warp::Reply;
tracing::warn!("Error in a request handler: {}", e);
return warp::reply::with_status(
format!("Error: {}", e),
warp::http::StatusCode::INTERNAL_SERVER_ERROR,
)
.into_response();
}
}
};
($store:expr, $expr:expr) => {
match $expr {
Ok(o) => o,
Err(e) => {
use warp::Reply;
tracing::warn!("Error in a request handler: {}", e);
return (
warp::reply::with_status(
format!("Error: {}", e),
warp::http::StatusCode::INTERNAL_SERVER_ERROR,
)
.into_response(),
$store,
);
}
}
};
}
macro_rules! warp_session {
($filter:expr, $store:expr, $then:expr) => {
$filter
.and(
warp_sessions::request::with_session(
$store.clone(),
Some(CookieOptions {
cookie_name: "tmtd_sid",
cookie_value: None,
max_age: Some(86400 * 7),
domain: None,
path: None,
secure: true,
http_only: true,
same_site: Some(SameSiteCookieOption::Strict),
}),
)
.map(|mut s: SessionWithStore<PostgresSessionStore>| {
s.session.set_expiry(Utc::now() + Duration::days(7));
s
}),
)
.then($then)
.untuple_one()
.and_then(warp_sessions::reply::with_session)
};
}
pub struct App {
config: Arc<Config>,
db: Arc<Database>,
session_store: PostgresSessionStore,
tera: Arc<Tera>,
pub server: actix_web::dev::Server,
}
impl App {
pub fn new(
config: Arc<Config>,
db: Arc<Database>,
session_store: PostgresSessionStore,
) -> Result<Self, tera::Error> {
Ok(Self {
config,
db,
session_store,
tera: Arc::new(Tera::new("templates/**/*.html")?),
pub async fn new(addr: SocketAddr, db: Arc<Database>) -> anyhow::Result<Self> {
let db = db.clone();
let server = HttpServer::new(move || {
actix_web::App::new().service((
web::resource("/task")
.app_data(web::Data::new(db.clone()))
.route(web::get().to(task)),
web::resource("/create")
.route(web::get().to(create)),
web::resource("/api/task/create")
.app_data(web::Data::new(db.clone()))
.route(web::post().to(create_task)),
web::resource("/api/task/move")
.app_data(web::Data::new(db.clone()))
.route(web::post().to(move_task)),
web::resource("/api/task/sort")
.app_data(web::Data::new(db.clone()))
.route(web::post().to(sort_task)),
))
})
}
pub async fn run(self, mut cancel: broadcast::Receiver<()>) {
let cloned_db = self.db.clone(); // Borrow checker moment
let db = warp::any().map(move || cloned_db.clone());
let route = warp_session!(
warp::path::end(),
self.session_store,
move |mut s: SessionWithStore<_>| async move {
s.session.insert_raw("test", "something".into());
(
warp::reply::html(format!("session id: {}", s.session.id())),
s,
)
}
)
.or(warp::path("task").map({
let tera = self.tera.clone();
let db = self.db.clone();
move || {
let mut ctx = Context::new();
// TODO: Replace the for loop with an SQL request to select only the tasks you can
// see after you logged in and the selected category which is stored in a cookie
// previously (see `post_task_sort` for more info)
// TODO: figure out what we need to pass to the get_issues function
let tasks = db.get_tasks();
ctx.insert("tasks", &tasks);
warp::reply::html(tera.render("task/task_page.html", &ctx).unwrap())
}
}))
.or(warp::path("create").map({
let tera = self.tera.clone();
move || {
let ctx = Context::new();
warp::reply::html(tera.render("task/create_task.html", &ctx).unwrap())
}
}))
.or(warp::post()
.and(warp::path("api"))
.and(warp::path("task"))
.and(warp::path("move"))
.and(warp::body::form::<task::MoveRequest>())
.and(db.clone())
.then(post_task_move))
.or(warp::post()
.and(warp::path("api"))
.and(warp::path("task"))
.and(warp::path("sort"))
.and(warp::body::form::<task::Request>())
.and(db.clone())
.then(post_task_sort))
.or(warp::post()
.and(warp::path("api"))
.and(warp::path("task"))
.and(warp::path("create"))
.and(warp::body::form::<task::Create>())
.and(db.clone())
.then(post_task_create));
let (_, server) =
warp::serve(route).bind_with_graceful_shutdown(self.config.listen_addr, async move {
cancel.recv().await.unwrap();
});
server.await;
info!("Web app has been shut down");
.bind(addr)?
.run();
Ok(App { server })
}
}
pub async fn post_task_move(form: task::MoveRequest, db: Arc<Database>) -> impl warp::Reply {
tracing::debug!("Got POST on /api/task/move: {:?}", form);
db.move_task(form).await;
warp::redirect(warp::http::Uri::from_static("/task"))
async fn task(db: web::Data<Arc<Database>>) -> impl Responder {
let tasks = db.get_tasks();
let html = tasks.render().unwrap();
HttpResponseBuilder::new(StatusCode::OK).body(html)
}
pub async fn post_task_sort(form: task::Request, db: Arc<Database>) -> impl warp::Reply {
tracing::debug!("Got POST on /api/task/move: {}", form.request);
// TODO: Store the information of the form in a cookie for the selection of the tasks in the
// website building
warp::redirect(warp::http::Uri::from_static("/task"))
async fn create() -> impl Responder {
let create = templates::CreateTask{};
let html = create.render().unwrap();
HttpResponseBuilder::new(StatusCode::OK).body(html)
}
pub async fn post_task_create(form: task::Create, db: Arc<Database>) -> impl warp::Reply {
let date = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap();
tracing::debug!("Got POST on /api/task/create: {:?}, date: {:?}", form, date);
db.create_task(form).await;
warp::redirect(warp::http::Uri::from_static("/task"))
async fn create_task(req: web::Form<task::CreateTask>, db: web::Data<Arc<Database>>) -> impl Responder {
tracing::debug!("Got POST request on /api/task/create: {:#?}", req);
HttpResponse::SeeOther().insert_header(("Location", "/task")).finish()
}
async fn move_task(req: web::Form<task::MoveTask>, db: web::Data<Arc<Database>>) -> impl Responder {
tracing::debug!("Got POST request on /api/task/move: {:#?}", req);
HttpResponse::SeeOther().insert_header(("Location", "/task")).finish()
}
async fn sort_task(req: web::Form<task::SortTask>, db: web::Data<Arc<Database>>) -> impl Responder {
tracing::debug!("Got POST request on /api/task/sort: {:#?}", req);
HttpResponse::SeeOther().insert_header(("Location", "/task")).finish()
}

View file

@ -11,8 +11,8 @@
<hr>
<form action="/api/task/move" method="post">
<label for="request">Move to:</label>
<select name="request" id="request" style="width:auto">
<label for="category">Move to:</label>
<select name="category" id="category" style="width:auto">
<option value="Ideas">ideas</option>
<option value="Assigned">assigned</option>
<option value="InProgress">in progreess</option>

View file

@ -7,8 +7,8 @@
<a href="/create">Create issue</a><br><br>
<form action="/api/task/sort" method="post">
<label for="request">Task group</label>
<select name="request" id="request" style="width:auto">
<label for="category">Task group</label>
<select name="category" id="category" style="width:auto">
<option value="Ideas">ideas</option>
<option value="Assigned">assigned</option>
<option value="InProgress">in progreess</option>