mirror of
https://github.com/CloudNebulaProject/webfingerd.git
synced 2026-04-10 13:10:41 +00:00
feat: project scaffold with config and error types
This commit is contained in:
parent
59d7c88707
commit
8123752c9c
9 changed files with 5250 additions and 0 deletions
5028
Cargo.lock
generated
Normal file
5028
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
42
Cargo.toml
Normal file
42
Cargo.toml
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
[workspace]
|
||||
members = [".", "migration"]
|
||||
|
||||
[package]
|
||||
name = "webfingerd"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
axum = { version = "0.8", features = ["macros"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
sea-orm = { version = "1", features = ["sqlx-sqlite", "runtime-tokio-rustls"] }
|
||||
sea-orm-migration = "1"
|
||||
dashmap = "6"
|
||||
governor = "0.8"
|
||||
askama = "0.12"
|
||||
askama_axum = "0.4"
|
||||
argon2 = "0.5"
|
||||
config = "0.14"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["json", "env-filter"] }
|
||||
metrics = "0.24"
|
||||
metrics-exporter-prometheus = "0.16"
|
||||
hickory-resolver = "0.25"
|
||||
reqwest = { version = "0.12", features = ["rustls-tls"], default-features = false }
|
||||
glob-match = "0.2"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
tower = "0.5"
|
||||
tower-http = { version = "0.6", features = ["cors", "request-id", "trace", "util"] }
|
||||
axum-extra = { version = "0.10", features = ["cookie-signed"] }
|
||||
rand = "0.8"
|
||||
base64 = "0.22"
|
||||
thiserror = "2"
|
||||
urlencoding = "2"
|
||||
async-trait = "0.1"
|
||||
|
||||
[dev-dependencies]
|
||||
axum-test = "16"
|
||||
tempfile = "3"
|
||||
25
config.toml
Normal file
25
config.toml
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
[server]
|
||||
listen = "0.0.0.0:8080"
|
||||
base_url = "http://localhost:8080"
|
||||
|
||||
[database]
|
||||
path = "webfingerd.db"
|
||||
wal_mode = true
|
||||
|
||||
[cache]
|
||||
reaper_interval_secs = 30
|
||||
|
||||
[rate_limit]
|
||||
public_rpm = 60
|
||||
api_rpm = 300
|
||||
batch_rpm = 10
|
||||
batch_max_links = 500
|
||||
|
||||
[challenge]
|
||||
dns_txt_prefix = "_webfinger-challenge"
|
||||
http_well_known_path = ".well-known/webfinger-verify"
|
||||
challenge_ttl_secs = 3600
|
||||
|
||||
[ui]
|
||||
enabled = false
|
||||
session_secret = ""
|
||||
6
migration/Cargo.toml
Normal file
6
migration/Cargo.toml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[package]
|
||||
name = "migration"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
14
migration/src/lib.rs
Normal file
14
migration/src/lib.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
pub fn add(left: u64, right: u64) -> u64 {
|
||||
left + right
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn it_works() {
|
||||
let result = add(2, 2);
|
||||
assert_eq!(result, 4);
|
||||
}
|
||||
}
|
||||
71
src/config.rs
Normal file
71
src/config.rs
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct Settings {
|
||||
pub server: ServerConfig,
|
||||
pub database: DatabaseConfig,
|
||||
pub cache: CacheConfig,
|
||||
pub rate_limit: RateLimitConfig,
|
||||
pub challenge: ChallengeConfig,
|
||||
pub ui: UiConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct ServerConfig {
|
||||
pub listen: String,
|
||||
pub base_url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct DatabaseConfig {
|
||||
pub path: String,
|
||||
pub wal_mode: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct CacheConfig {
|
||||
pub reaper_interval_secs: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct RateLimitConfig {
|
||||
pub public_rpm: u32,
|
||||
pub api_rpm: u32,
|
||||
pub batch_rpm: u32,
|
||||
pub batch_max_links: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct ChallengeConfig {
|
||||
pub dns_txt_prefix: String,
|
||||
pub http_well_known_path: String,
|
||||
pub challenge_ttl_secs: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct UiConfig {
|
||||
pub enabled: bool,
|
||||
pub session_secret: String,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
pub fn load() -> Result<Self, config::ConfigError> {
|
||||
let settings = config::Config::builder()
|
||||
.add_source(config::File::with_name("config").required(false))
|
||||
.add_source(
|
||||
config::Environment::with_prefix("WEBFINGERD")
|
||||
.separator("__"),
|
||||
)
|
||||
.build()?;
|
||||
|
||||
let s: Self = settings.try_deserialize()?;
|
||||
|
||||
if s.ui.enabled && s.ui.session_secret.is_empty() {
|
||||
return Err(config::ConfigError::Message(
|
||||
"ui.session_secret is required when ui is enabled".into(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(s)
|
||||
}
|
||||
}
|
||||
49
src/error.rs
Normal file
49
src/error.rs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
use axum::http::StatusCode;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::Json;
|
||||
use serde_json::json;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum AppError {
|
||||
#[error("not found")]
|
||||
NotFound,
|
||||
#[error("bad request: {0}")]
|
||||
BadRequest(String),
|
||||
#[error("unauthorized")]
|
||||
Unauthorized,
|
||||
#[error("forbidden: {0}")]
|
||||
Forbidden(String),
|
||||
#[error("conflict: {0}")]
|
||||
Conflict(String),
|
||||
#[error("rate limited")]
|
||||
RateLimited,
|
||||
#[error("internal error: {0}")]
|
||||
Internal(String),
|
||||
#[error("database error: {0}")]
|
||||
Database(#[from] sea_orm::DbErr),
|
||||
}
|
||||
|
||||
impl IntoResponse for AppError {
|
||||
fn into_response(self) -> Response {
|
||||
let (status, message) = match &self {
|
||||
AppError::NotFound => (StatusCode::NOT_FOUND, self.to_string()),
|
||||
AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg.clone()),
|
||||
AppError::Unauthorized => (StatusCode::UNAUTHORIZED, self.to_string()),
|
||||
AppError::Forbidden(msg) => (StatusCode::FORBIDDEN, msg.clone()),
|
||||
AppError::Conflict(msg) => (StatusCode::CONFLICT, msg.clone()),
|
||||
AppError::RateLimited => (StatusCode::TOO_MANY_REQUESTS, self.to_string()),
|
||||
AppError::Internal(msg) => {
|
||||
tracing::error!("internal error: {msg}");
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, "internal error".into())
|
||||
}
|
||||
AppError::Database(err) => {
|
||||
tracing::error!("database error: {err}");
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, "internal error".into())
|
||||
}
|
||||
};
|
||||
|
||||
(status, Json(json!({ "error": message }))).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
pub type AppResult<T> = Result<T, AppError>;
|
||||
2
src/lib.rs
Normal file
2
src/lib.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
pub mod config;
|
||||
pub mod error;
|
||||
13
src/main.rs
Normal file
13
src/main.rs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
use tracing_subscriber::{fmt, EnvFilter};
|
||||
use webfingerd::config::Settings;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
fmt()
|
||||
.with_env_filter(EnvFilter::from_default_env())
|
||||
.json()
|
||||
.init();
|
||||
|
||||
let settings = Settings::load().expect("failed to load configuration");
|
||||
tracing::info!(listen = %settings.server.listen, "starting webfingerd");
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue