forked from boos/BoosAPI
refactor: purge Node.js, Rust is the one true backend
- Moved boosapi-rs/* to repo root - Deleted all Node.js source (src/server/, src/web/, scripts/) - Deleted node_modules, package.json, tsconfig, vite.config - Updated .gitignore and AGENTS.md for Rust-only stack - Fixed static file path for frontend dist/web/ - Dockerfile stays the same (already Rust-only) - Kept dist/web/ for Rust static serving
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
use crate::{
|
||||
auth::jwt::validate_jwt,
|
||||
db::models::{downstream_api_key::DownstreamApiKey, user::User},
|
||||
error::{AppError, AppResult},
|
||||
AppState,
|
||||
};
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::{HeaderMap, StatusCode},
|
||||
routing::{delete, get},
|
||||
Json, Router,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
pub fn routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/v1/user/profile", get(get_profile).put(update_profile))
|
||||
.route("/v1/user/api-keys", get(list_api_keys).post(create_api_key))
|
||||
.route("/v1/user/api-keys/{id}", delete(delete_api_key))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct UpdateProfilePayload {
|
||||
username: Option<String>,
|
||||
email: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct CreateApiKeyPayload {
|
||||
name: String,
|
||||
description: Option<String>,
|
||||
expires_at: Option<String>,
|
||||
max_requests: Option<i64>,
|
||||
max_cost: Option<f64>,
|
||||
}
|
||||
|
||||
fn current_user_id(headers: &HeaderMap, state: &AppState) -> AppResult<i64> {
|
||||
let auth = headers
|
||||
.get("Authorization")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|v| v.strip_prefix("Bearer "))
|
||||
.ok_or_else(|| AppError::Unauthorized("Missing or invalid Authorization header".into()))?;
|
||||
|
||||
let claims = validate_jwt(&state.config.auth.jwt_secret, auth)
|
||||
.map_err(|_| AppError::Unauthorized("Invalid or expired token".into()))?;
|
||||
|
||||
claims
|
||||
.sub
|
||||
.parse()
|
||||
.map_err(|_| AppError::Unauthorized("Invalid user ID in token".into()))
|
||||
}
|
||||
|
||||
async fn get_profile(State(state): State<AppState>, headers: HeaderMap) -> AppResult<Json<Value>> {
|
||||
let user_id = current_user_id(&headers, &state)?;
|
||||
let user: User = sqlx::query_as("SELECT * FROM users WHERE id = ?")
|
||||
.bind(user_id)
|
||||
.fetch_optional(&*state.db)
|
||||
.await?
|
||||
.ok_or_else(|| AppError::NotFound("User not found".into()))?;
|
||||
|
||||
Ok(Json(json!({ "user": user })))
|
||||
}
|
||||
|
||||
async fn update_profile(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
Json(payload): Json<UpdateProfilePayload>,
|
||||
) -> AppResult<Json<Value>> {
|
||||
let user_id = current_user_id(&headers, &state)?;
|
||||
let current: User = sqlx::query_as("SELECT * FROM users WHERE id = ?")
|
||||
.bind(user_id)
|
||||
.fetch_optional(&*state.db)
|
||||
.await?
|
||||
.ok_or_else(|| AppError::NotFound("User not found".into()))?;
|
||||
|
||||
let username = payload.username.unwrap_or(current.username);
|
||||
let email = payload.email.unwrap_or(current.email);
|
||||
|
||||
sqlx::query(
|
||||
"UPDATE users
|
||||
SET username = ?, email = ?, updated_at = datetime('now')
|
||||
WHERE id = ?",
|
||||
)
|
||||
.bind(username)
|
||||
.bind(email)
|
||||
.bind(user_id)
|
||||
.execute(&*state.db)
|
||||
.await?;
|
||||
|
||||
get_profile(State(state), headers).await
|
||||
}
|
||||
|
||||
async fn list_api_keys(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
) -> AppResult<Json<Value>> {
|
||||
let user_id = current_user_id(&headers, &state)?;
|
||||
let keys: Vec<DownstreamApiKey> = sqlx::query_as(
|
||||
"SELECT * FROM downstream_api_keys WHERE user_id = ? ORDER BY created_at DESC",
|
||||
)
|
||||
.bind(user_id)
|
||||
.fetch_all(&*state.db)
|
||||
.await?;
|
||||
|
||||
Ok(Json(json!({ "keys": keys })))
|
||||
}
|
||||
|
||||
async fn create_api_key(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
Json(payload): Json<CreateApiKeyPayload>,
|
||||
) -> AppResult<(StatusCode, Json<Value>)> {
|
||||
let user_id = current_user_id(&headers, &state)?;
|
||||
if payload.name.trim().is_empty() {
|
||||
return Err(AppError::BadRequest("name is required".into()));
|
||||
}
|
||||
|
||||
let token = format!("sk-user-{}", uuid::Uuid::new_v4().simple());
|
||||
let result = sqlx::query(
|
||||
"INSERT INTO downstream_api_keys
|
||||
(name, key, description, enabled, expires_at, max_requests, max_cost, user_id)
|
||||
VALUES (?, ?, ?, 1, ?, ?, ?, ?)",
|
||||
)
|
||||
.bind(payload.name)
|
||||
.bind(&token)
|
||||
.bind(payload.description)
|
||||
.bind(payload.expires_at)
|
||||
.bind(payload.max_requests)
|
||||
.bind(payload.max_cost)
|
||||
.bind(user_id)
|
||||
.execute(&*state.db)
|
||||
.await?;
|
||||
|
||||
let key: DownstreamApiKey = sqlx::query_as("SELECT * FROM downstream_api_keys WHERE id = ?")
|
||||
.bind(result.last_insert_rowid())
|
||||
.fetch_one(&*state.db)
|
||||
.await?;
|
||||
|
||||
Ok((StatusCode::CREATED, Json(json!({ "key": key }))))
|
||||
}
|
||||
|
||||
async fn delete_api_key(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
Path(id): Path<i64>,
|
||||
) -> AppResult<StatusCode> {
|
||||
let user_id = current_user_id(&headers, &state)?;
|
||||
let result = sqlx::query("DELETE FROM downstream_api_keys WHERE id = ? AND user_id = ?")
|
||||
.bind(id)
|
||||
.bind(user_id)
|
||||
.execute(&*state.db)
|
||||
.await?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err(AppError::NotFound("API key not found".into()));
|
||||
}
|
||||
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
Reference in New Issue
Block a user