Remaining endpoints for MVP

This commit is contained in:
lewis
2025-12-12 19:20:39 +02:00
parent 2ededf32a6
commit b66e4fe291
76 changed files with 8803 additions and 564 deletions
+65 -2
View File
@@ -3,7 +3,7 @@ use crate::state::AppState;
use axum::{
Json,
extract::State,
http::StatusCode,
http::{HeaderMap, StatusCode},
response::{IntoResponse, Response},
};
use bcrypt::{DEFAULT_COST, hash};
@@ -16,6 +16,22 @@ use serde_json::json;
use std::sync::Arc;
use tracing::{error, info, warn};
fn extract_client_ip(headers: &HeaderMap) -> String {
if let Some(forwarded) = headers.get("x-forwarded-for") {
if let Ok(value) = forwarded.to_str() {
if let Some(first_ip) = value.split(',').next() {
return first_ip.trim().to_string();
}
}
}
if let Some(real_ip) = headers.get("x-real-ip") {
if let Ok(value) = real_ip.to_str() {
return value.trim().to_string();
}
}
"unknown".to_string()
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateAccountInput {
@@ -38,9 +54,24 @@ pub struct CreateAccountOutput {
pub async fn create_account(
State(state): State<AppState>,
headers: HeaderMap,
Json(input): Json<CreateAccountInput>,
) -> Response {
info!("create_account called");
let client_ip = extract_client_ip(&headers);
if state.rate_limiters.account_creation.check_key(&client_ip).is_err() {
warn!(ip = %client_ip, "Account creation rate limit exceeded");
return (
StatusCode::TOO_MANY_REQUESTS,
Json(json!({
"error": "RateLimitExceeded",
"message": "Too many account creation attempts. Please try again later."
})),
)
.into_response();
}
if input.handle.contains('!') || input.handle.contains('@') {
return (
StatusCode::BAD_REQUEST,
@@ -184,8 +215,40 @@ pub async fn create_account(
let user_id = match user_insert {
Ok(row) => row.id,
Err(e) => {
if let Some(db_err) = e.as_database_error() {
if db_err.code().as_deref() == Some("23505") {
let constraint = db_err.constraint().unwrap_or("");
if constraint.contains("handle") || constraint.contains("users_handle") {
return (
StatusCode::BAD_REQUEST,
Json(json!({
"error": "HandleNotAvailable",
"message": "Handle already taken"
})),
)
.into_response();
} else if constraint.contains("email") || constraint.contains("users_email") {
return (
StatusCode::BAD_REQUEST,
Json(json!({
"error": "InvalidEmail",
"message": "Email already registered"
})),
)
.into_response();
} else if constraint.contains("did") || constraint.contains("users_did") {
return (
StatusCode::BAD_REQUEST,
Json(json!({
"error": "AccountAlreadyExists",
"message": "An account with this DID already exists"
})),
)
.into_response();
}
}
}
error!("Error inserting user: {:?}", e);
// TODO: Check for unique constraint violation on email/did specifically
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
+24 -5
View File
@@ -1,6 +1,7 @@
use crate::api::ApiError;
use crate::circuit_breaker::{with_circuit_breaker, CircuitBreakerError};
use crate::plc::{
create_update_op, sign_operation, PlcClient, PlcError, PlcService,
create_update_op, sign_operation, PlcClient, PlcError, PlcOpOrTombstone, PlcService,
};
use crate::state::AppState;
use axum::{
@@ -14,7 +15,7 @@ use k256::ecdsa::SigningKey;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::collections::HashMap;
use tracing::{error, info};
use tracing::{error, info, warn};
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
@@ -166,9 +167,27 @@ pub async fn sign_plc_operation(
};
let plc_client = PlcClient::new(None);
let last_op = match plc_client.get_last_op(did).await {
let did_clone = did.clone();
let result: Result<PlcOpOrTombstone, CircuitBreakerError<PlcError>> = with_circuit_breaker(
&state.circuit_breakers.plc_directory,
|| async { plc_client.get_last_op(&did_clone).await },
)
.await;
let last_op = match result {
Ok(op) => op,
Err(PlcError::NotFound) => {
Err(CircuitBreakerError::CircuitOpen(e)) => {
warn!("PLC directory circuit breaker open: {}", e);
return (
StatusCode::SERVICE_UNAVAILABLE,
Json(json!({
"error": "ServiceUnavailable",
"message": "PLC directory service temporarily unavailable"
})),
)
.into_response();
}
Err(CircuitBreakerError::OperationFailed(PlcError::NotFound)) => {
return (
StatusCode::NOT_FOUND,
Json(json!({
@@ -178,7 +197,7 @@ pub async fn sign_plc_operation(
)
.into_response();
}
Err(e) => {
Err(CircuitBreakerError::OperationFailed(e)) => {
error!("Failed to fetch PLC operation: {:?}", e);
return (
StatusCode::BAD_GATEWAY,
+34 -11
View File
@@ -1,5 +1,6 @@
use crate::api::ApiError;
use crate::plc::{signing_key_to_did_key, validate_plc_operation, PlcClient};
use crate::circuit_breaker::{with_circuit_breaker, CircuitBreakerError};
use crate::plc::{signing_key_to_did_key, validate_plc_operation, PlcClient, PlcError};
use crate::state::AppState;
use axum::{
extract::State,
@@ -183,16 +184,38 @@ pub async fn submit_plc_operation(
}
let plc_client = PlcClient::new(None);
if let Err(e) = plc_client.send_operation(did, &input.operation).await {
error!("Failed to submit PLC operation: {:?}", e);
return (
StatusCode::BAD_GATEWAY,
Json(json!({
"error": "UpstreamError",
"message": format!("Failed to submit to PLC directory: {}", e)
})),
)
.into_response();
let operation_clone = input.operation.clone();
let did_clone = did.clone();
let result: Result<(), CircuitBreakerError<PlcError>> = with_circuit_breaker(
&state.circuit_breakers.plc_directory,
|| async { plc_client.send_operation(&did_clone, &operation_clone).await },
)
.await;
match result {
Ok(()) => {}
Err(CircuitBreakerError::CircuitOpen(e)) => {
warn!("PLC directory circuit breaker open: {}", e);
return (
StatusCode::SERVICE_UNAVAILABLE,
Json(json!({
"error": "ServiceUnavailable",
"message": "PLC directory service temporarily unavailable"
})),
)
.into_response();
}
Err(CircuitBreakerError::OperationFailed(e)) => {
error!("Failed to submit PLC operation: {:?}", e);
return (
StatusCode::BAD_GATEWAY,
Json(json!({
"error": "UpstreamError",
"message": format!("Failed to submit to PLC directory: {}", e)
})),
)
.into_response();
}
}
if let Err(e) = sqlx::query!(