feat: Transform project into a cargo-generate template with configurable authentication features and improved local user registration.

This commit is contained in:
2026-01-06 05:53:01 +01:00
parent 9219a586b1
commit c368293cd4
12 changed files with 213 additions and 45 deletions

14
Cargo.lock generated
View File

@@ -1405,16 +1405,24 @@ dependencies = [
[[package]]
name = "jsonwebtoken"
version = "9.3.1"
version = "10.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde"
checksum = "c76e1c7d7df3e34443b3621b459b066a7b79644f059fc8b2db7070c825fd417e"
dependencies = [
"base64 0.22.1",
"ed25519-dalek",
"getrandom 0.2.16",
"hmac",
"js-sys",
"p256",
"p384",
"pem",
"ring",
"rand 0.8.5",
"rsa",
"serde",
"serde_json",
"sha2",
"signature",
"simple_asn1",
]

View File

@@ -13,7 +13,20 @@ A production-ready, modular Rust API template for K-Suite applications, followin
## Quick Start
### 1. Clone and Configure
### Option 1: Use cargo-generate (Recommended)
```bash
cargo generate --git https://github.com/GKaszewski/k-template.git
```
You'll be prompted to choose:
- **Project name**: Your new service name
- **Database**: `sqlite` or `postgres`
- **Session auth**: Enable cookie-based sessions
- **JWT auth**: Enable Bearer token authentication
- **OIDC**: Enable OpenID Connect integration
### Option 2: Clone directly
```bash
git clone https://github.com/GKaszewski/k-template.git my-api
@@ -22,7 +35,7 @@ cp .env.example .env
# Edit .env with your configuration
```
### 2. Run
### Run
```bash
# Development (with hot reload via cargo-watch)

View File

@@ -5,7 +5,7 @@ edition = "2024"
default-run = "api"
[features]
default = ["sqlite", "auth-axum-login", "auth-oidc", "auth-jwt"]
default = ["sqlite"]
sqlite = ["infra/sqlite", "tower-sessions-sqlx-store/sqlite"]
postgres = ["infra/postgres", "tower-sessions-sqlx-store/postgres"]
auth-axum-login = ["infra/auth-axum-login"]

63
api/Cargo.toml.template Normal file
View File

@@ -0,0 +1,63 @@
[package]
name = "api"
version = "0.1.0"
edition = "2024"
default-run = "api"
[features]
default = ["{{database}}"{% if auth_session %}, "auth-axum-login"{% endif %}{% if auth_oidc %}, "auth-oidc"{% endif %}{% if auth_jwt %}, "auth-jwt"{% endif %}]
sqlite = ["infra/sqlite", "tower-sessions-sqlx-store/sqlite"]
postgres = ["infra/postgres", "tower-sessions-sqlx-store/postgres"]
auth-axum-login = ["infra/auth-axum-login"]
auth-oidc = ["infra/auth-oidc"]
auth-jwt = ["infra/auth-jwt"]
auth-full = ["auth-axum-login", "auth-oidc", "auth-jwt"]
[dependencies]
k-core = { git = "https://git.gabrielkaszewski.dev/GKaszewski/k-core", features = [
"logging",
"db-sqlx",
"{{database}}",
"http",
"auth",
"sessions-db",
] }
domain = { path = "../domain" }
infra = { path = "../infra", default-features = false, features = ["{{database}}"] }
#Web framework
axum = { version = "0.8.8", features = ["macros"] }
tower = "0.5.2"
tower-http = { version = "0.6.2", features = ["cors", "trace"] }
# Authentication
# Moved to infra
tower-sessions-sqlx-store = { version = "0.15", features = ["{{database}}"] }
# password-auth removed
time = "0.3"
async-trait = "0.1.89"
# Async runtime
tokio = { version = "1.48.0", features = ["full"] }
# Serialization
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0"
# Validation via domain newtypes (Email, Password)
# Error handling
thiserror = "2.0.17"
anyhow = "1.0"
# Utilities
chrono = { version = "0.4.42", features = ["serde"] }
uuid = { version = "1.19.0", features = ["v4", "serde"] }
# Logging
tracing = "0.1"
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
dotenvy = "0.15.7"
config = "0.15.19"
tower-sessions = "0.14.0"

View File

@@ -10,6 +10,7 @@ use uuid::Uuid;
/// Login request with validated email and password newtypes
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct LoginRequest {
/// Email is validated on deserialization
pub email: Email,
@@ -19,6 +20,7 @@ pub struct LoginRequest {
/// Register request with validated email and password newtypes
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct RegisterRequest {
/// Email is validated on deserialization
pub email: Email,

View File

@@ -131,20 +131,3 @@ async fn try_session_auth(parts: &mut Parts) -> Result<Option<User>, ApiError> {
Ok(None)
}
/// Fallback for when auth-axum-login is not enabled
#[cfg(not(feature = "auth-axum-login"))]
async fn try_session_auth(_parts: &mut Parts) -> Result<Option<User>, ApiError> {
Ok(None)
}
/// Fallback for when auth-jwt is not enabled but auth mode requires it
#[cfg(not(feature = "auth-jwt"))]
async fn try_jwt_auth(_parts: &mut Parts, state: &AppState) -> Result<Option<User>, ApiError> {
if matches!(state.config.auth_mode, AuthMode::Jwt) {
return Err(ApiError::Internal(
"JWT auth mode configured but auth-jwt feature not enabled".to_string(),
));
}
Ok(None)
}

View File

@@ -154,10 +154,13 @@ async fn register(
)));
}
// Using email as subject for local auth for now
// Hash password
let password_hash = infra::auth::backend::hash_password(payload.password.as_ref());
// Create user with password
let user = state
.user_service
.find_or_create(&email.as_ref().to_string(), email.as_ref())
.create_local(email.as_ref(), &password_hash)
.await?;
let auth_mode = state.config.auth_mode;

View File

@@ -1,21 +1,46 @@
[template]
cargo_generate_version = ">=0.21.0"
ignore = [".git", "target", ".idea", ".vscode"]
ignore = [
".git",
"target",
".idea",
".vscode",
"data.db",
"api/Cargo.toml",
"infra/Cargo.toml",
]
[placeholders]
project_name = { type = "string", prompt = "Project name" }
database = { type = "string", prompt = "Database type", choices = [
"sqlite",
"postgres",
], default = "sqlite" }
[filenames]
"api/Cargo.toml.template" = "api/Cargo.toml"
"infra/Cargo.toml.template" = "infra/Cargo.toml"
[conditional]
# Conditional dependencies based on database choice
sqlite = { condition = "database == 'sqlite'", ignore = [
"infra/src/user_repository_postgres.rs",
"migrations_postgres",
] }
postgres = { condition = "database == 'postgres'", ignore = [
"infra/src/user_repository_sqlite.rs",
"migrations",
] }
[placeholders.project_name]
type = "string"
prompt = "Project name"
[placeholders.database]
type = "string"
prompt = "Database type"
choices = ["sqlite", "postgres"]
default = "sqlite"
[placeholders.auth_session]
type = "bool"
prompt = "Enable session-based authentication (cookies)?"
default = true
[placeholders.auth_jwt]
type = "bool"
prompt = "Enable JWT authentication (Bearer tokens)?"
default = true
[placeholders.auth_oidc]
type = "bool"
prompt = "Enable OIDC integration (Login with Google, etc.)?"
default = true
[conditional.'database == "sqlite"']
ignore = ["migrations_postgres"]
[conditional.'database == "postgres"']
ignore = ["migrations_sqlite"]

View File

@@ -54,4 +54,11 @@ impl UserService {
pub async fn find_by_email(&self, email: &str) -> DomainResult<Option<User>> {
self.user_repository.find_by_email(email).await
}
pub async fn create_local(&self, email: &str, password_hash: &str) -> DomainResult<User> {
let email = Email::try_from(email)?;
let user = User::new_local(email, password_hash);
self.user_repository.save(&user).await?;
Ok(user)
}
}

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
edition = "2024"
[features]
default = ["sqlite", "broker-nats"]
default = ["sqlite"]
sqlite = [
"sqlx/sqlite",
"k-core/sqlite",
@@ -51,5 +51,10 @@ axum-login = { version = "0.18", optional = true }
password-auth = { version = "1.0", optional = true }
openidconnect = { version = "4.0.1", optional = true }
url = { version = "2.5.8", optional = true }
jsonwebtoken = { version = "9.3", optional = true }
# reqwest = { version = "0.13.1", features = ["blocking", "json"], optional = true }
jsonwebtoken = { version = "10.2.0", features = [
"sha2",
"p256",
"hmac",
"rsa",
"rust_crypto",
], optional = true }

55
infra/Cargo.toml.template Normal file
View File

@@ -0,0 +1,55 @@
[package]
name = "infra"
version = "0.1.0"
edition = "2024"
[features]
default = ["{{database}}"{% if auth_session %}, "auth-axum-login"{% endif %}{% if auth_oidc %}, "auth-oidc"{% endif %}{% if auth_jwt %}, "auth-jwt"{% endif %}]
sqlite = [
"sqlx/sqlite",
"k-core/sqlite",
"tower-sessions-sqlx-store",
"k-core/sessions-db",
]
postgres = [
"sqlx/postgres",
"k-core/postgres",
"tower-sessions-sqlx-store",
"k-core/sessions-db",
]
broker-nats = ["dep:futures-util", "k-core/broker-nats"]
auth-axum-login = ["dep:axum-login", "dep:password-auth"]
auth-oidc = ["dep:openidconnect", "dep:url"]
auth-jwt = ["dep:jsonwebtoken"]
[dependencies]
k-core = { git = "https://git.gabrielkaszewski.dev/GKaszewski/k-core", features = [
"logging",
"db-sqlx",
"sessions-db",
] }
domain = { path = "../domain" }
async-trait = "0.1.89"
chrono = { version = "0.4.42", features = ["serde"] }
sqlx = { version = "0.8.6", features = ["runtime-tokio", "chrono", "migrate"] }
thiserror = "2.0.17"
anyhow = "1.0"
tokio = { version = "1.48.0", features = ["full"] }
tracing = "0.1"
uuid = { version = "1.19.0", features = ["v4", "serde"] }
tower-sessions-sqlx-store = { version = "0.15.0", optional = true }
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
async-nats = { version = "0.45", optional = true }
futures-util = { version = "0.3", optional = true }
futures-core = "0.3"
tower-sessions = "0.14"
# Auth dependencies (optional)
axum-login = { version = "0.18", optional = true }
password-auth = { version = "1.0", optional = true }
openidconnect = { version = "4.0.1", optional = true }
url = { version = "2.5.8", optional = true }
jsonwebtoken = { version = "9.3", optional = true }
# reqwest = { version = "0.13.1", features = ["blocking", "json"], optional = true }

View File

@@ -114,6 +114,10 @@ pub mod backend {
let auth_layer = axum_login::AuthManagerLayerBuilder::new(backend, session_layer).build();
Ok(auth_layer)
}
pub fn hash_password(password: &str) -> String {
password_auth::generate_hash(password)
}
}
#[cfg(feature = "auth-oidc")]