LOOK MA, NO WARP
This commit is contained in:
parent
ab59cd4bdb
commit
f48c6f5054
1180
Cargo.lock
generated
1180
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
27
Cargo.toml
27
Cargo.toml
|
@ -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"
|
||||
]}
|
||||
|
||||
|
|
|
@ -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)?))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
40
src/main.rs
40
src/main.rs
|
@ -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(())
|
||||
|
|
33
src/task.rs
33
src/task.rs
|
@ -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
41
src/templates.rs
Normal 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();
|
||||
|
215
src/web.rs
215
src/web.rs
|
@ -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)),
|
||||
))
|
||||
})
|
||||
.bind(addr)?
|
||||
.run();
|
||||
Ok(App { server })
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
async fn task(db: web::Data<Arc<Database>>) -> impl Responder {
|
||||
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");
|
||||
}
|
||||
let html = tasks.render().unwrap();
|
||||
HttpResponseBuilder::new(StatusCode::OK).body(html)
|
||||
}
|
||||
|
||||
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 create() -> impl Responder {
|
||||
let create = templates::CreateTask{};
|
||||
let html = create.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_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()
|
||||
}
|
||||
|
||||
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 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()
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue