diff --git a/README.md b/README.md index a948011..8d556d9 100644 --- a/README.md +++ b/README.md @@ -60,8 +60,18 @@ The frontend is automatically configured to talk to the backend. cargo run -p notes-api ``` + By default, this uses the **SQLite** backend. +#### Configuration + +The application is configured via environment variables (or `.env` file): + +- `ALLOW_REGISTRATION`: Set to `false` to disable new user registration (default: `true`). +- `DATABASE_URL`: Connection string for the database. +- `SESSION_SECRET`: Secret key for session encryption. +- `CORS_ALLOWED_ORIGINS`: Comma-separated list of allowed origins. + **Running with Postgres:** To use PostgreSQL, build with the `postgres` feature: diff --git a/compose.yml b/compose.yml index ff8ab73..a327e56 100644 --- a/compose.yml +++ b/compose.yml @@ -10,6 +10,7 @@ services: - CORS_ALLOWED_ORIGINS=http://localhost:8080,http://localhost:5173 - HOST=0.0.0.0 - PORT=3000 + - ALLOW_REGISTRATION=true volumes: - ./data:/app/data diff --git a/notes-api/src/config.rs b/notes-api/src/config.rs index 0b3a7f3..2cadff2 100644 --- a/notes-api/src/config.rs +++ b/notes-api/src/config.rs @@ -8,6 +8,7 @@ pub struct Config { pub database_url: String, pub session_secret: String, pub cors_allowed_origins: Vec, + pub allow_registration: bool, } impl Default for Config { @@ -19,6 +20,7 @@ impl Default for Config { session_secret: "k-notes-super-secret-key-must-be-at-least-64-bytes-long!!!!" .to_string(), cors_allowed_origins: vec!["http://localhost:5173".to_string()], + allow_registration: true, } } } @@ -50,12 +52,17 @@ impl Config { .filter(|s| !s.is_empty()) .collect(); + let allow_registration = env::var("ALLOW_REGISTRATION") + .map(|s| s.to_lowercase() == "true") + .unwrap_or(true); + Self { host, port, database_url, session_secret, cors_allowed_origins, + allow_registration, } } } diff --git a/notes-api/src/error.rs b/notes-api/src/error.rs index 307cd93..755195f 100644 --- a/notes-api/src/error.rs +++ b/notes-api/src/error.rs @@ -23,6 +23,9 @@ pub enum ApiError { #[error("Internal server error")] Internal(String), + + #[error("Forbidden: {0}")] + Forbidden(String), } /// Error response body @@ -83,6 +86,14 @@ impl IntoResponse for ApiError { }, ) } + + ApiError::Forbidden(msg) => ( + StatusCode::FORBIDDEN, + ErrorResponse { + error: "Forbidden".to_string(), + details: Some(msg.clone()), + }, + ), }; (status, Json(error_response)).into_response() diff --git a/notes-api/src/main.rs b/notes-api/src/main.rs index f8b0872..86f7a0a 100644 --- a/notes-api/src/main.rs +++ b/notes-api/src/main.rs @@ -88,6 +88,7 @@ async fn main() -> anyhow::Result<()> { note_service, tag_service, user_service, + config.clone(), ); // Auth backend diff --git a/notes-api/src/routes/auth.rs b/notes-api/src/routes/auth.rs index bc496de..22ff697 100644 --- a/notes-api/src/routes/auth.rs +++ b/notes-api/src/routes/auth.rs @@ -22,6 +22,11 @@ pub async fn register( .validate() .map_err(|e| ApiError::validation(e.to_string()))?; + // Check if registration is allowed + if !state.config.allow_registration { + return Err(ApiError::Forbidden("Registration is disabled".to_string())); + } + // Check if user exists if state .user_repo diff --git a/notes-api/src/state.rs b/notes-api/src/state.rs index de3796e..2fea070 100644 --- a/notes-api/src/state.rs +++ b/notes-api/src/state.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use crate::config::Config; use notes_domain::{ NoteRepository, NoteService, TagRepository, TagService, UserRepository, UserService, }; @@ -13,6 +14,7 @@ pub struct AppState { pub note_service: Arc, pub tag_service: Arc, pub user_service: Arc, + pub config: Config, } impl AppState { @@ -23,6 +25,7 @@ impl AppState { note_service: Arc, tag_service: Arc, user_service: Arc, + config: Config, ) -> Self { Self { note_repo, @@ -31,6 +34,7 @@ impl AppState { note_service, tag_service, user_service, + config, } } }