Initial boilerplate

This commit is contained in:
Robert Perce 2025-10-14 08:26:18 -05:00
commit 519fb49901
7 changed files with 4520 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

4396
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

18
Cargo.toml Normal file
View 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
View 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
View 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
View file

@ -0,0 +1,4 @@
#[derive(Clone)]
pub struct Contact {
pub name: String,
}

57
src/main.rs Normal file
View 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(())
}