Initial boilerplate
This commit is contained in:
commit
519fb49901
7 changed files with 4520 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/target
|
||||
4396
Cargo.lock
generated
Normal file
4396
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
18
Cargo.toml
Normal file
18
Cargo.toml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
[package]
|
||||
name = "mascarpone"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
maud = { version = "0.27.0", features = ["axum"] }
|
||||
anyhow = "1.0.100"
|
||||
axum = { version = "0.8.6", features = ["macros"] }
|
||||
axum-htmx = "0.8.1"
|
||||
sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls-aws-lc-rs"] }
|
||||
tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] }
|
||||
tower-sessions = "0.14.0"
|
||||
listenfd = "1.0.2"
|
||||
|
||||
[dev-dependencies]
|
||||
cargo-watch = "8.5.3"
|
||||
systemfd = "0.4.6"
|
||||
36
README.md
Normal file
36
README.md
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# Mascarpone CRM
|
||||
|
||||
I always write "cream cheese" on my grocery list as "crm chs", so that's what
|
||||
I think of when I see "CRM".
|
||||
|
||||
## Planned Features
|
||||
|
||||
* Local contacts
|
||||
* Contacts stored on a remote CardDAV server
|
||||
* Act as CardDAV server for other clients
|
||||
* For each contact:
|
||||
* Name, address as single fields (plus code? lat/long? go crazy!)
|
||||
* Relationship mapping
|
||||
* Birthday reminders
|
||||
* Desired contact periodicity
|
||||
* Last-contact-time mapping
|
||||
* Additional arbitrary fields
|
||||
* Journal with Obsidian-like `[[link]]` syntax
|
||||
* Contact groups (e.g. "Met with `[[Brunch Bunch]]` at the diner")
|
||||
* "Named in journal but has no contact entry" detection
|
||||
* CalDAV server for birthday reminders
|
||||
* Email birthday reminders over SMTP
|
||||
|
||||
## Tech
|
||||
|
||||
axum: fast and scalable, lots of middleware from tower
|
||||
axum-htmx: helpers when dealing with htmx headers
|
||||
axum-login: user auth, has oauth2 and user permissions
|
||||
tower-sessions: save user sessions (Redis, sqlite, memory, etc.)
|
||||
fred: Redis client for user sessions
|
||||
tracing: trace and instrument async logs
|
||||
reqwest: for API calls, oath2
|
||||
anyhow: turn any error into an AppError returning: “Internal Server Error”
|
||||
maud: templating html, can split fragments into functions in a single file (LoB)
|
||||
sqlx: dealing with a database, migrations, reverts
|
||||
|
||||
8
mise.toml
Normal file
8
mise.toml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
[tools]
|
||||
"cargo:systemfd" = "latest"
|
||||
"watchexec" = "latest"
|
||||
"rust-analyzer" = "latest"
|
||||
"jj" = "latest"
|
||||
|
||||
[tasks."serve:dev"]
|
||||
run = "systemfd --no-pid -s http::0.0.0.0:3000 -- watchexec -r cargo run"
|
||||
4
src/contact/mod.rs
Normal file
4
src/contact/mod.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
#[derive(Clone)]
|
||||
pub struct Contact {
|
||||
pub name: String,
|
||||
}
|
||||
57
src/main.rs
Normal file
57
src/main.rs
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
use axum::{Router, extract::State, response::IntoResponse, routing::get};
|
||||
use maud::html;
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
mod contact;
|
||||
use contact::Contact;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AppState {
|
||||
contacts: Vec<Contact>,
|
||||
}
|
||||
|
||||
#[axum::debug_handler]
|
||||
async fn contacts(
|
||||
// access the state via the `State` extractor
|
||||
// extracting a state of the wrong type results in a compile error
|
||||
State(state): State<AppState>,
|
||||
) -> impl IntoResponse {
|
||||
html! {
|
||||
ul {
|
||||
@for contact in &state.contacts {
|
||||
li { (&contact.name) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), anyhow::Error> {
|
||||
let state = AppState {
|
||||
contacts: vec![
|
||||
Contact {
|
||||
name: "Foo Bar".to_string(),
|
||||
},
|
||||
Contact {
|
||||
name: "Baz Qux".to_string(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
let app = Router::new()
|
||||
.route("/", get(|| async { "Hello, World!" }))
|
||||
.route("/contacts", get(contacts))
|
||||
.with_state(state);
|
||||
|
||||
let mut listenfd = listenfd::ListenFd::from_env();
|
||||
let listener = match listenfd.take_tcp_listener(0)? {
|
||||
Some(listener) => {
|
||||
listener.set_nonblocking(true)?;
|
||||
TcpListener::from_std(listener)
|
||||
}
|
||||
None => TcpListener::bind("0.0.0.0:3000").await,
|
||||
}?;
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue