feat(api_key): implement API key management with creation, retrieval, and deletion endpoints
This commit is contained in:
@@ -8,7 +8,7 @@ use once_cell::sync::Lazy;
|
||||
use sea_orm::prelude::Uuid;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use app::state::AppState;
|
||||
use app::{persistence::api_key, state::AppState};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Claims {
|
||||
@@ -28,14 +28,24 @@ impl FromRequestParts<AppState> for AuthUser {
|
||||
|
||||
async fn from_request_parts(
|
||||
parts: &mut Parts,
|
||||
_state: &AppState,
|
||||
state: &AppState,
|
||||
) -> Result<Self, Self::Rejection> {
|
||||
// --- Test User ID (Keep for testing) ---
|
||||
if let Some(user_id_header) = parts.headers.get("x-test-user-id") {
|
||||
let user_id_str = user_id_header.to_str().unwrap_or("0");
|
||||
let user_id = user_id_str.parse::<Uuid>().unwrap_or(Uuid::nil());
|
||||
return Ok(AuthUser { id: user_id });
|
||||
}
|
||||
|
||||
// --- API Key Authentication ---
|
||||
if let Some(api_key) = get_api_key_from_header(&parts.headers) {
|
||||
return match api_key::validate_api_key(&state.conn, &api_key).await {
|
||||
Ok(user) => Ok(AuthUser { id: user.id }),
|
||||
Err(_) => Err((StatusCode::UNAUTHORIZED, "Invalid API Key")),
|
||||
};
|
||||
}
|
||||
|
||||
// --- JWT Authentication (Fallback) ---
|
||||
let token = get_token_from_header(&parts.headers)
|
||||
.ok_or((StatusCode::UNAUTHORIZED, "Missing or invalid token"))?;
|
||||
|
||||
@@ -56,3 +66,11 @@ fn get_token_from_header(headers: &HeaderMap) -> Option<String> {
|
||||
.and_then(|header| header.strip_prefix("Bearer "))
|
||||
.map(|token| token.to_owned())
|
||||
}
|
||||
|
||||
fn get_api_key_from_header(headers: &HeaderMap) -> Option<String> {
|
||||
headers
|
||||
.get("Authorization")
|
||||
.and_then(|header| header.to_str().ok())
|
||||
.and_then(|header| header.strip_prefix("ApiKey "))
|
||||
.map(|key| key.to_owned())
|
||||
}
|
||||
|
93
thoughts-backend/api/src/routers/api_key.rs
Normal file
93
thoughts-backend/api/src/routers/api_key.rs
Normal file
@@ -0,0 +1,93 @@
|
||||
use crate::{
|
||||
error::ApiError,
|
||||
extractor::{AuthUser, Json},
|
||||
models::ApiErrorResponse,
|
||||
};
|
||||
use app::{persistence::api_key, state::AppState};
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
routing::{delete, get},
|
||||
Router,
|
||||
};
|
||||
use models::schemas::api_key::{ApiKeyListSchema, ApiKeyRequest, ApiKeyResponse};
|
||||
use sea_orm::prelude::Uuid;
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/me/api-keys",
|
||||
responses(
|
||||
(status = 200, description = "List of API keys", body = ApiKeyListSchema),
|
||||
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("bearerAuth" = [])
|
||||
)
|
||||
)]
|
||||
async fn get_keys(
|
||||
State(state): State<AppState>,
|
||||
auth_user: AuthUser,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
let keys = api_key::get_api_keys_for_user(&state.conn, auth_user.id).await?;
|
||||
Ok(Json(ApiKeyListSchema::from(keys)))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/me/api-keys",
|
||||
request_body = ApiKeyRequest,
|
||||
responses(
|
||||
(status = 201, description = "API key created", body = ApiKeyResponse),
|
||||
(status = 400, description = "Bad request", body = ApiErrorResponse),
|
||||
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
|
||||
(status = 422, description = "Validation error", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
security(
|
||||
("bearerAuth" = [])
|
||||
)
|
||||
)]
|
||||
async fn create_key(
|
||||
State(state): State<AppState>,
|
||||
auth_user: AuthUser,
|
||||
Json(params): Json<ApiKeyRequest>,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
let (key_model, plaintext_key) =
|
||||
api_key::create_api_key(&state.conn, auth_user.id, params.name).await?;
|
||||
|
||||
let response = ApiKeyResponse::from_parts(key_model, Some(plaintext_key));
|
||||
Ok((StatusCode::CREATED, Json(response)))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/me/api-keys/{key_id}",
|
||||
responses(
|
||||
(status = 204, description = "API key deleted"),
|
||||
(status = 401, description = "Unauthorized", body = ApiErrorResponse),
|
||||
(status = 404, description = "API key not found", body = ApiErrorResponse),
|
||||
(status = 500, description = "Internal server error", body = ApiErrorResponse),
|
||||
),
|
||||
params(
|
||||
("key_id" = Uuid, Path, description = "The ID of the API key to delete")
|
||||
),
|
||||
security(
|
||||
("bearerAuth" = [])
|
||||
)
|
||||
)]
|
||||
async fn delete_key(
|
||||
State(state): State<AppState>,
|
||||
auth_user: AuthUser,
|
||||
Path(key_id): Path<Uuid>,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
api_key::delete_api_key(&state.conn, key_id, auth_user.id).await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
pub fn create_api_key_router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/", get(get_keys).post(create_key))
|
||||
.route("/{key_id}", delete(delete_key))
|
||||
}
|
@@ -1,5 +1,6 @@
|
||||
use axum::Router;
|
||||
|
||||
pub mod api_key;
|
||||
pub mod auth;
|
||||
pub mod feed;
|
||||
pub mod root;
|
||||
|
@@ -19,17 +19,11 @@ async fn get_thoughts_by_tag(
|
||||
Path(tag_name): Path<String>,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
let thoughts_with_authors = get_thoughts_by_tag_name(&state.conn, &tag_name).await;
|
||||
println!(
|
||||
"Result from get_thoughts_by_tag_name: {:?}",
|
||||
thoughts_with_authors
|
||||
);
|
||||
let thoughts_with_authors = thoughts_with_authors?;
|
||||
println!("Thoughts with authors: {:?}", thoughts_with_authors);
|
||||
let thoughts_schema: Vec<ThoughtSchema> = thoughts_with_authors
|
||||
.into_iter()
|
||||
.map(ThoughtSchema::from)
|
||||
.collect();
|
||||
println!("Thoughts schema: {:?}", thoughts_schema);
|
||||
Ok(Json(ThoughtListSchema::from(thoughts_schema)))
|
||||
}
|
||||
|
||||
|
@@ -19,9 +19,12 @@ use models::schemas::user::{UserListSchema, UserSchema};
|
||||
use models::{params::user::UpdateUserParams, schemas::thought::ThoughtListSchema};
|
||||
use models::{queries::user::UserQuery, schemas::thought::ThoughtSchema};
|
||||
|
||||
use crate::extractor::{Json, Valid};
|
||||
use crate::models::ApiErrorResponse;
|
||||
use crate::{error::ApiError, extractor::AuthUser};
|
||||
use crate::{
|
||||
extractor::{Json, Valid},
|
||||
routers::api_key::create_api_key_router,
|
||||
};
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
@@ -358,6 +361,7 @@ pub fn create_user_router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/", get(users_get))
|
||||
.route("/me", get(get_me).put(update_me))
|
||||
.nest("/me/api-keys", create_api_key_router())
|
||||
.route("/{param}", get(get_user_by_param))
|
||||
.route("/{username}/thoughts", get(user_thoughts_get))
|
||||
.route(
|
||||
|
Reference in New Issue
Block a user