Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a622372
Initial plan
Copilot Jan 7, 2026
2b85267
Initial analysis and planning for Bilibili dynamic posting API
Copilot Jan 7, 2026
d5e2e05
Implement Bilibili dynamic posting API endpoint
Copilot Jan 7, 2026
42a84a2
Improve code quality: use shared HTTP client and better error handling
Copilot Jan 7, 2026
1037779
Add comprehensive documentation for Bilibili API endpoint
Copilot Jan 7, 2026
68a890f
Implement JWT authentication with ES256 and update API routes
Copilot Jan 8, 2026
f4d22ac
Update documentation for JWT authentication changes
Copilot Jan 8, 2026
944698e
Simplify JWT config and authentication error responses
Copilot Jan 8, 2026
5a2b5da
Refactor router to use single OpenApiRouter instead of merge
Copilot Jan 8, 2026
32668c3
Simplify JWT claims to use only sub and iat fields
Copilot Jan 8, 2026
a92fc30
fix
daflyinbed Jan 8, 2026
eb5f482
update
daflyinbed Jan 8, 2026
70f57fe
Update docs/BILIBILI_API.md
daflyinbed Jan 8, 2026
50c8995
update
daflyinbed Jan 8, 2026
29eb829
Optimize bilibili_handlers.rs: reduce duplication and improve OpenAPI…
Copilot Jan 8, 2026
31d0a5a
Add generic error handling with thiserror and IntoResponse
Copilot Jan 9, 2026
19f05ec
Simplify error handling: use anyhow::Error internally
Copilot Jan 9, 2026
0a412ea
Add back Unauthorized error variant
Copilot Jan 9, 2026
2d813e2
Refactor auth.rs to use AppError::Unauthorized
Copilot Jan 9, 2026
83129c3
Use AppResult type alias in jwt_auth_middleware
Copilot Jan 9, 2026
cd734cf
Update docs/BILIBILI_API.md
daflyinbed Jan 9, 2026
fa53744
Update docs/BILIBILI_API.md
daflyinbed Jan 9, 2026
f2e499a
Update docs/BILIBILI_API.md
daflyinbed Jan 9, 2026
e086acf
Update src/routes/bilibili_handlers.rs
daflyinbed Jan 9, 2026
470111d
Update example.toml
daflyinbed Jan 9, 2026
e4437f1
Extract duplicate dynamic creation logic into helper function
Copilot Jan 9, 2026
2085c0d
update
daflyinbed Jan 10, 2026
29f53c4
update
daflyinbed Jan 10, 2026
1620981
update
daflyinbed Jan 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 141 additions & 38 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,55 @@ This file provides guidance to AI agent when working with code in this repositor

## Project Overview

This is an Axum-based RESTful API template with PostgreSQL support, Docker containerization, and GitHub Actions CI/CD. The application uses:
**Janus** is a RESTful API service that provides Bilibili dynamic posting capabilities with JWT authentication and PostgreSQL persistence. The application:
- Posts content (text and images) to Bilibili dynamics via API integration
- Uses ES256 JWT authentication for API security
- Stores data in PostgreSQL with SQLx (compile-time checked queries)
- Auto-generates OpenAPI documentation with Scalar UI
- Supports multipart file uploads for images

**Tech Stack:**
- **Axum 0.8** web framework with Tower middleware
- **PostgreSQL** with SQLx for database operations (compile-time checked queries)
- **Utoipa** for OpenAPI documentation with Scalar UI
- **Sentry** for optional error tracking
- **PostgreSQL** with SQLx for database operations
- **Utoipa** for OpenAPI documentation
- **Reqwest** HTTP client for Bilibili API calls
- **ES256 JWT** (ECDSA with P-256) for authentication
- **Tracing** subscriber for structured logging
- **Sentry** for optional error tracking

## Build and Development Commands

### Building and Testing
### Essential Commands
```bash
# Build the project
cargo build

# Run with config file
# Run server (requires config.toml)
cargo run -- server --config config.toml

# Generate JWT token for authentication
cargo run -- generate-jwt --config config.toml --subject user_id

# Show version and build SHA
cargo run -- version

# Format code
cargo fmt

# Run linter
# Run linter (strict mode - warnings as errors)
cargo clippy --all-features -- -D warnings
```

# Show version
cargo run -- version
### Testing
```bash
# Run all tests
cargo test

# Run specific test
cargo test test_name

# Run tests with output
cargo test -- --nocapture
```

### Database Operations
Expand All @@ -55,67 +79,146 @@ just pre-release <version>
## Architecture

### Configuration System
All configuration is TOML-based and loaded via `config.rs`. The config file path is passed via CLI argument `--config`. Configuration structure:
- **Logger**: tracing-subscriber with configurable level (trace/debug/info/warn/error) and format (compact/pretty/json)
All configuration is TOML-based and loaded via `config.rs`. The config file path is passed via CLI argument `--config`. Configuration structure (`AppSettings`):
- **Logger**: enable, level (trace/debug/info/warn/error), format (compact/pretty/json)
- **Server**: binding address, port, and host URL
- **Database**: PostgreSQL URI with optional connection pool settings
- **Mailer**: Optional SMTP configuration for emails
- **Sentry**: Optional error tracking with DSN and sampling rate
- **Database**: PostgreSQL URI with max connections and timeout
- **Smtp**: Optional SMTP configuration for emails
- **Sentry**: Optional DSN and traces_sample_rate
- **Bilibili**: sessdata and bili_jct cookies for API authentication
- **Jwt**: ES256 private/public keys in PEM format

### Application Flow
1. `main.rs`: Entry point using mimalloc global allocator
2. `app.rs::run()`: CLI parser (`clap`) with two commands:
- `server`: Loads config, initializes tracing/Sentry, starts web server
- `version`: Displays version and build SHA
2. `app.rs::run()`: CLI parser (`clap`) with three commands:
- `server --config <path>`: Load config, initialize tracing/Sentry, start web server
- `generate-jwt --config <path> --subject <id>`: Generate ES256 JWT token
- `version`: Display version and build SHA
3. Server initialization sequence:
- Load TOML config
- Initialize tracing (based on logger config)
- Load TOML config via `AppSettings::new()`
- Initialize tracing subscriber with module whitelist: `["tower_http", "sqlx::query", "my_axum_template"]`
- Initialize Sentry (if configured)
- Create AppState with PostgreSQL pool
- Run database migrations
- Build Axum router with middleware
- Start server with graceful shutdown handling
- Create `AppState` with PostgreSQL pool, shared HTTP client, and configs
- Run database migrations automatically via `repository.migrate()`
- Build Axum router with OpenAPI support
- Apply Tower middleware (timeout, compression)
- Bind TCP listener and start server with graceful shutdown handling

### State Management
`AppState` (src/state.rs) holds the `PostgresRepository` with a SQLx connection pool. The repository pattern provides:
`AppState` (src/state.rs) holds application-wide state:
- `repository: PostgresRepository` - Database operations with SQLx pool
- `bilibili_config: BilibiliConfig` - Bilibili API credentials
- `jwt_config: JwtConfig` - JWT signing/verification keys
- `http_client: reqwest::Client` - Shared HTTP client for Bilibili API calls

Repository trait provides:
- `health_check()`: Database connectivity check
- `migrate()`: Run SQLx migrations from `./migrations` directory

### Routing Structure
Routes are organized in `src/routes/` using Utoipa's OpenApiRouter:
- All API routes are prefixed with `/api/v1`
- All API routes prefixed with `/api` (note: NOT `/api/v1`)
- **Public routes** (no auth required):
- `GET /api/_ping` - Simple ping endpoint
- `GET /api/_health` - Database health check
- **Protected routes** (JWT required):
- `POST /api/bilibili/createDynamic` - Create Bilibili dynamic with multipart file upload
- OpenAPI documentation available at:
- `/api/v1/scalar` - Scalar UI
- `/api/v1/openapi.json` - OpenAPI spec JSON
- Route handlers use `utoipa` macros for automatic OpenAPI spec generation
- `/api/scalar` - Scalar UI
- `/api/openapi.json` - OpenAPI spec JSON
- JWT auth middleware applied via `middleware::from_fn_with_state()` to protected routes
- Security scheme: HTTP Bearer with JWT format

### Authentication
JWT-based authentication using ES256 algorithm (ECDSA with P-256):
- `Claims` structure: `sub` (subject) and `iat` (issued at timestamp)
- No expiration validation (`validate_exp = false`) - tokens are long-lived
- Token generation: `generate_token(subject, private_key_pem)` via CLI command
- Token verification: `verify_token(token, public_key_pem)`
- Middleware: `jwt_auth_middleware()` extracts `Authorization: Bearer <token>` header

### Middleware Layer
Applied in `src/middleware.rs` via Tower:
Applied in `src/middleware.rs` via Tower layers:
- **RequestBodyTimeoutLayer**: 10-second timeout on request body
- **CompressionLayer**: Response compression (tower-http compression-full)
- **RequestBodyTimeoutLayer**: 10-second request timeout

### Error Handling
- **anyhow**: Used in `app.rs` for main application errors
- **thiserror**: Used in `config.rs` for typed config errors
- **SQLx**: Database errors propagate as Result types
Custom `AppError` enum (src/error.rs) with three variants:
- `BadRequest(anyhow::Error)` → HTTP 400
- `Unauthorized(anyhow::Error)` → HTTP 401
- `InternalError(anyhow::Error)` → HTTP 500

Response format: `{ "code": 1 }` (errors logged server-side with details)
Automatic conversions from: `sqlx::Error`, `serde_json::Error`, `reqwest::Error`, `anyhow::Error`
`AppResult<T>` type alias: `Result<T, AppError>`

### Testing
- CI runs `rustfmt` and `clippy` checks
- `SQLX_OFFLINE=true` is set in CI to allow offline builds (requires `sqlx-data.json` files)
- No dedicated test files in codebase (relies on manual testing via HTTP clients)

## Key Development Notes

### Adding New Routes
1. Create handler functions in `src/routes/` modules
2. Add `utoipa` OpenAPI macros to handlers
3. Register routes in `build_router()` using `routes!()` macro
4. Routes are automatically prefixed with `/api/v1` and documented
1. Create handler function in appropriate module under `src/routes/`:
```rust
use crate::{error::AppResult, state::AppState};
use axum::{Json, extract::State};
use utoipa::ToSchema;

#[derive(ToSchema, Serialize)]
pub struct MyResponse { pub field: String }

#[utoipa::path(
post,
tag = "mytag",
path = "/myendpoint",
request_body(...),
responses(...),
security(("bearer_auth" = []))
)]
pub async fn my_handler(
State(state): State<AppState>,
) -> AppResult<Json<MyResponse>> {
Ok(Json(MyResponse { field: "value".to_string() }))
}
```
2. Register route in `src/routes/mod.rs`:
- For public routes: Add before `.route_layer(jwt_auth_middleware)`
- For protected routes: Add after `.route_layer(jwt_auth_middleware)`
3. Add tag to `ApiDoc` struct's `tags()` macro if creating new tag
4. Routes automatically prefixed with `/api` and documented in OpenAPI spec

### Database Queries
- Use SQLx with compile-time query verification
- Access pool via `state.repository.pool` (it's public but used directly in queries)
- Place migration files in `./migrations` directory
- Create migrations: `sqlx migrate add -r migration_name`
- Repository trait allows for alternative database backends
- All database operations go through `PostgresRepository`

### Error Handling in Handlers
```rust
use crate::error::{AppError, AppResult};

pub async fn my_handler() -> AppResult<Json<Response>> {
// Use ? operator for automatic conversion
let data = risky_operation()?;

// Or use AppError explicitly
if invalid {
return Err(AppError::BadRequest(anyhow::anyhow!("Invalid input")));
}

Ok(Json(data))
}
```

### Logging
Use `tracing` macros, not `println!` or `log!`:
```rust
use tracing::info;
info!("Creating dynamic with {} images", image_count);
```

### Memory Allocation
The application uses **mimalloc** as the global allocator (configured in `main.rs`) for improved performance.
Expand Down
Loading