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]]
|
[[package]]
|
||||||
name = "jsonwebtoken"
|
name = "jsonwebtoken"
|
||||||
version = "9.3.1"
|
version = "10.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde"
|
checksum = "c76e1c7d7df3e34443b3621b459b066a7b79644f059fc8b2db7070c825fd417e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
|
"ed25519-dalek",
|
||||||
|
"getrandom 0.2.16",
|
||||||
|
"hmac",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
|
"p256",
|
||||||
|
"p384",
|
||||||
"pem",
|
"pem",
|
||||||
"ring",
|
"rand 0.8.5",
|
||||||
|
"rsa",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"sha2",
|
||||||
|
"signature",
|
||||||
"simple_asn1",
|
"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
|
## 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
|
```bash
|
||||||
git clone https://github.com/GKaszewski/k-template.git my-api
|
git clone https://github.com/GKaszewski/k-template.git my-api
|
||||||
@@ -22,7 +35,7 @@ cp .env.example .env
|
|||||||
# Edit .env with your configuration
|
# Edit .env with your configuration
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Run
|
### Run
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Development (with hot reload via cargo-watch)
|
# Development (with hot reload via cargo-watch)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ edition = "2024"
|
|||||||
default-run = "api"
|
default-run = "api"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["sqlite", "auth-axum-login", "auth-oidc", "auth-jwt"]
|
default = ["sqlite"]
|
||||||
sqlite = ["infra/sqlite", "tower-sessions-sqlx-store/sqlite"]
|
sqlite = ["infra/sqlite", "tower-sessions-sqlx-store/sqlite"]
|
||||||
postgres = ["infra/postgres", "tower-sessions-sqlx-store/postgres"]
|
postgres = ["infra/postgres", "tower-sessions-sqlx-store/postgres"]
|
||||||
auth-axum-login = ["infra/auth-axum-login"]
|
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
|
/// Login request with validated email and password newtypes
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[allow(dead_code)]
|
||||||
pub struct LoginRequest {
|
pub struct LoginRequest {
|
||||||
/// Email is validated on deserialization
|
/// Email is validated on deserialization
|
||||||
pub email: Email,
|
pub email: Email,
|
||||||
@@ -19,6 +20,7 @@ pub struct LoginRequest {
|
|||||||
|
|
||||||
/// Register request with validated email and password newtypes
|
/// Register request with validated email and password newtypes
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[allow(dead_code)]
|
||||||
pub struct RegisterRequest {
|
pub struct RegisterRequest {
|
||||||
/// Email is validated on deserialization
|
/// Email is validated on deserialization
|
||||||
pub email: Email,
|
pub email: Email,
|
||||||
|
|||||||
@@ -131,20 +131,3 @@ async fn try_session_auth(parts: &mut Parts) -> Result<Option<User>, ApiError> {
|
|||||||
|
|
||||||
Ok(None)
|
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
|
let user = state
|
||||||
.user_service
|
.user_service
|
||||||
.find_or_create(&email.as_ref().to_string(), email.as_ref())
|
.create_local(email.as_ref(), &password_hash)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let auth_mode = state.config.auth_mode;
|
let auth_mode = state.config.auth_mode;
|
||||||
|
|||||||
@@ -1,21 +1,46 @@
|
|||||||
[template]
|
[template]
|
||||||
cargo_generate_version = ">=0.21.0"
|
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]
|
[filenames]
|
||||||
project_name = { type = "string", prompt = "Project name" }
|
"api/Cargo.toml.template" = "api/Cargo.toml"
|
||||||
database = { type = "string", prompt = "Database type", choices = [
|
"infra/Cargo.toml.template" = "infra/Cargo.toml"
|
||||||
"sqlite",
|
|
||||||
"postgres",
|
|
||||||
], default = "sqlite" }
|
|
||||||
|
|
||||||
[conditional]
|
[placeholders.project_name]
|
||||||
# Conditional dependencies based on database choice
|
type = "string"
|
||||||
sqlite = { condition = "database == 'sqlite'", ignore = [
|
prompt = "Project name"
|
||||||
"infra/src/user_repository_postgres.rs",
|
|
||||||
"migrations_postgres",
|
[placeholders.database]
|
||||||
] }
|
type = "string"
|
||||||
postgres = { condition = "database == 'postgres'", ignore = [
|
prompt = "Database type"
|
||||||
"infra/src/user_repository_sqlite.rs",
|
choices = ["sqlite", "postgres"]
|
||||||
"migrations",
|
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>> {
|
pub async fn find_by_email(&self, email: &str) -> DomainResult<Option<User>> {
|
||||||
self.user_repository.find_by_email(email).await
|
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"
|
edition = "2024"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["sqlite", "broker-nats"]
|
default = ["sqlite"]
|
||||||
sqlite = [
|
sqlite = [
|
||||||
"sqlx/sqlite",
|
"sqlx/sqlite",
|
||||||
"k-core/sqlite",
|
"k-core/sqlite",
|
||||||
@@ -51,5 +51,10 @@ axum-login = { version = "0.18", optional = true }
|
|||||||
password-auth = { version = "1.0", optional = true }
|
password-auth = { version = "1.0", optional = true }
|
||||||
openidconnect = { version = "4.0.1", optional = true }
|
openidconnect = { version = "4.0.1", optional = true }
|
||||||
url = { version = "2.5.8", optional = true }
|
url = { version = "2.5.8", optional = true }
|
||||||
jsonwebtoken = { version = "9.3", optional = true }
|
jsonwebtoken = { version = "10.2.0", features = [
|
||||||
# reqwest = { version = "0.13.1", features = ["blocking", "json"], optional = true }
|
"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();
|
let auth_layer = axum_login::AuthManagerLayerBuilder::new(backend, session_layer).build();
|
||||||
Ok(auth_layer)
|
Ok(auth_layer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn hash_password(password: &str) -> String {
|
||||||
|
password_auth::generate_hash(password)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "auth-oidc")]
|
#[cfg(feature = "auth-oidc")]
|
||||||
|
|||||||
Reference in New Issue
Block a user