Add complete ai.card Rust implementation
- Implement complete Rust API server with axum framework - Add database abstraction supporting PostgreSQL and SQLite - Implement comprehensive gacha system with probability calculations - Add JWT authentication with atproto DID integration - Create card master data system with rarities (Normal, Rare, SuperRare, Kira, Unique) - Implement draw history tracking and collection management - Add API endpoints for authentication, card drawing, and collection viewing - Include database migrations for both PostgreSQL and SQLite - Maintain full compatibility with Python API implementation - Add comprehensive documentation and development guide 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
161
src/handlers/auth.rs
Normal file
161
src/handlers/auth.rs
Normal file
@ -0,0 +1,161 @@
|
||||
use axum::{
|
||||
extract::State,
|
||||
response::Json,
|
||||
routing::post,
|
||||
Router,
|
||||
};
|
||||
use validator::Validate;
|
||||
|
||||
use crate::{
|
||||
auth::AtprotoAuthService,
|
||||
error::{AppError, AppResult},
|
||||
models::*,
|
||||
AppState,
|
||||
};
|
||||
|
||||
pub fn create_routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/login", post(login))
|
||||
.route("/verify", post(verify_token))
|
||||
}
|
||||
|
||||
/// Authenticate user with atproto credentials
|
||||
async fn login(
|
||||
State(state): State<AppState>,
|
||||
Json(request): Json<LoginRequest>,
|
||||
) -> AppResult<Json<LoginResponse>> {
|
||||
// Validate request
|
||||
request.validate().map_err(|e| AppError::validation(e.to_string()))?;
|
||||
|
||||
// Create auth service
|
||||
let auth_service = AtprotoAuthService::new(&state.settings.secret_key);
|
||||
|
||||
// Authenticate user
|
||||
let user = auth_service
|
||||
.authenticate(&request.identifier, &request.password)
|
||||
.await?;
|
||||
|
||||
// Create access token
|
||||
let access_token = auth_service
|
||||
.create_access_token(&user, state.settings.access_token_expire_minutes)?;
|
||||
|
||||
// Create or update user in database
|
||||
let db_user = create_or_update_user(&state, &user.did, &user.handle).await?;
|
||||
|
||||
Ok(Json(LoginResponse {
|
||||
access_token,
|
||||
token_type: "Bearer".to_string(),
|
||||
expires_in: state.settings.access_token_expire_minutes * 60, // Convert to seconds
|
||||
user: UserInfo {
|
||||
did: user.did,
|
||||
handle: user.handle,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
/// Verify JWT token
|
||||
async fn verify_token(
|
||||
State(state): State<AppState>,
|
||||
Json(token): Json<serde_json::Value>,
|
||||
) -> AppResult<Json<serde_json::Value>> {
|
||||
let token_str = token["token"]
|
||||
.as_str()
|
||||
.ok_or_else(|| AppError::validation("Token is required"))?;
|
||||
|
||||
let auth_service = AtprotoAuthService::new(&state.settings.secret_key);
|
||||
let claims = auth_service.verify_access_token(token_str)?;
|
||||
|
||||
Ok(Json(serde_json::json!({
|
||||
"valid": true,
|
||||
"did": claims.did,
|
||||
"handle": claims.handle,
|
||||
"exp": claims.exp
|
||||
})))
|
||||
}
|
||||
|
||||
/// Create or update user in database
|
||||
async fn create_or_update_user(
|
||||
state: &AppState,
|
||||
did: &str,
|
||||
handle: &str,
|
||||
) -> AppResult<User> {
|
||||
let now = chrono::Utc::now();
|
||||
|
||||
// Try to get existing user
|
||||
let existing_user = match &state.db {
|
||||
crate::database::Database::Postgres(pool) => {
|
||||
sqlx::query_as::<_, User>("SELECT * FROM users WHERE did = $1")
|
||||
.bind(did)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
.map_err(AppError::Database)?
|
||||
}
|
||||
crate::database::Database::Sqlite(pool) => {
|
||||
sqlx::query_as::<_, User>("SELECT * FROM users WHERE did = ?")
|
||||
.bind(did)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
.map_err(AppError::Database)?
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(mut user) = existing_user {
|
||||
// Update handle if changed
|
||||
if user.handle != handle {
|
||||
user = match &state.db {
|
||||
crate::database::Database::Postgres(pool) => {
|
||||
sqlx::query_as::<_, User>(
|
||||
"UPDATE users SET handle = $1, updated_at = $2 WHERE did = $3 RETURNING *"
|
||||
)
|
||||
.bind(handle)
|
||||
.bind(now)
|
||||
.bind(did)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.map_err(AppError::Database)?
|
||||
}
|
||||
crate::database::Database::Sqlite(pool) => {
|
||||
sqlx::query_as::<_, User>(
|
||||
"UPDATE users SET handle = ?, updated_at = ? WHERE did = ? RETURNING *"
|
||||
)
|
||||
.bind(handle)
|
||||
.bind(now)
|
||||
.bind(did)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.map_err(AppError::Database)?
|
||||
}
|
||||
};
|
||||
}
|
||||
Ok(user)
|
||||
} else {
|
||||
// Create new user
|
||||
let user = match &state.db {
|
||||
crate::database::Database::Postgres(pool) => {
|
||||
sqlx::query_as::<_, User>(
|
||||
"INSERT INTO users (did, handle, created_at, updated_at) VALUES ($1, $2, $3, $4) RETURNING *"
|
||||
)
|
||||
.bind(did)
|
||||
.bind(handle)
|
||||
.bind(now)
|
||||
.bind(now)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.map_err(AppError::Database)?
|
||||
}
|
||||
crate::database::Database::Sqlite(pool) => {
|
||||
sqlx::query_as::<_, User>(
|
||||
"INSERT INTO users (did, handle, created_at, updated_at) VALUES (?, ?, ?, ?) RETURNING *"
|
||||
)
|
||||
.bind(did)
|
||||
.bind(handle)
|
||||
.bind(now)
|
||||
.bind(now)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.map_err(AppError::Database)?
|
||||
}
|
||||
};
|
||||
Ok(user)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user