feat: Transform project into a cargo-generate template with configurable authentication features and improved local user registration.
This commit is contained in:
14
Cargo.lock
generated
14
Cargo.lock
generated
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
17
README.md
17
README.md
@@ -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)
|
||||
|
||||
@@ -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
63
api/Cargo.toml.template
Normal 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"
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
55
infra/Cargo.toml.template
Normal 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 }
|
||||
@@ -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")]
|
||||
|
||||
Reference in New Issue
Block a user