refactor: deps cleanup, split openapi, extract api-types crate
This commit is contained in:
52
Cargo.lock
generated
52
Cargo.lock
generated
@@ -15,7 +15,6 @@ dependencies = [
|
|||||||
"domain",
|
"domain",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tokio",
|
|
||||||
"tracing",
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
@@ -31,10 +30,8 @@ dependencies = [
|
|||||||
"axum",
|
"axum",
|
||||||
"chrono",
|
"chrono",
|
||||||
"enum_delegate",
|
"enum_delegate",
|
||||||
"reqwest 0.13.3",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror 2.0.18",
|
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
@@ -288,6 +285,15 @@ version = "1.0.102"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "api-types"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"utoipa",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "apple-native-keyring-store"
|
name = "apple-native-keyring-store"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
@@ -1443,17 +1449,6 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "doc"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"axum",
|
|
||||||
"tracing",
|
|
||||||
"utoipa",
|
|
||||||
"utoipa-scalar",
|
|
||||||
"utoipa-swagger-ui",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "document-features"
|
name = "document-features"
|
||||||
version = "0.2.12"
|
version = "0.2.12"
|
||||||
@@ -1467,7 +1462,6 @@ dependencies = [
|
|||||||
name = "domain"
|
name = "domain"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"chrono",
|
"chrono",
|
||||||
"email_address",
|
"email_address",
|
||||||
@@ -1655,7 +1649,6 @@ dependencies = [
|
|||||||
name = "event-payload"
|
name = "event-payload"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
|
||||||
"chrono",
|
"chrono",
|
||||||
"domain",
|
"domain",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -1671,7 +1664,6 @@ dependencies = [
|
|||||||
"domain",
|
"domain",
|
||||||
"futures",
|
"futures",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2427,7 +2419,6 @@ dependencies = [
|
|||||||
"csv",
|
"csv",
|
||||||
"domain",
|
"domain",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror 2.0.18",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2913,9 +2904,7 @@ dependencies = [
|
|||||||
"domain",
|
"domain",
|
||||||
"event-payload",
|
"event-payload",
|
||||||
"futures",
|
"futures",
|
||||||
"serde",
|
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror 2.0.18",
|
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"uuid",
|
"uuid",
|
||||||
@@ -3470,16 +3459,13 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"chrono",
|
|
||||||
"domain",
|
"domain",
|
||||||
"event-payload",
|
"event-payload",
|
||||||
"futures",
|
"futures",
|
||||||
"serde",
|
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"uuid",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3493,7 +3479,6 @@ dependencies = [
|
|||||||
"chrono",
|
"chrono",
|
||||||
"domain",
|
"domain",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"tokio",
|
|
||||||
"tracing",
|
"tracing",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
@@ -3528,13 +3513,13 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"activitypub",
|
"activitypub",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"api-types",
|
||||||
"application",
|
"application",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"auth",
|
"auth",
|
||||||
"axum",
|
"axum",
|
||||||
"axum-governor",
|
"axum-governor",
|
||||||
"chrono",
|
"chrono",
|
||||||
"doc",
|
|
||||||
"domain",
|
"domain",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
"export",
|
"export",
|
||||||
@@ -3557,13 +3542,14 @@ dependencies = [
|
|||||||
"sqlite-federation",
|
"sqlite-federation",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"template-askama",
|
"template-askama",
|
||||||
"thiserror 2.0.18",
|
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower",
|
"tower",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"utoipa",
|
"utoipa",
|
||||||
|
"utoipa-scalar",
|
||||||
|
"utoipa-swagger-ui",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -4057,7 +4043,6 @@ name = "rss"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"application",
|
"application",
|
||||||
"chrono",
|
|
||||||
"domain",
|
"domain",
|
||||||
"rss 2.0.12",
|
"rss 2.0.12",
|
||||||
]
|
]
|
||||||
@@ -4625,16 +4610,13 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"chrono",
|
|
||||||
"domain",
|
"domain",
|
||||||
"event-payload",
|
"event-payload",
|
||||||
"futures",
|
"futures",
|
||||||
"serde",
|
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"uuid",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4997,7 +4979,6 @@ dependencies = [
|
|||||||
"askama",
|
"askama",
|
||||||
"chrono",
|
"chrono",
|
||||||
"domain",
|
"domain",
|
||||||
"serde",
|
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -5424,6 +5405,7 @@ name = "tui"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"api-types",
|
||||||
"apple-native-keyring-store",
|
"apple-native-keyring-store",
|
||||||
"csv",
|
"csv",
|
||||||
"directories",
|
"directories",
|
||||||
@@ -5432,7 +5414,6 @@ dependencies = [
|
|||||||
"reqwest 0.13.3",
|
"reqwest 0.13.3",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tempfile",
|
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
"uuid",
|
"uuid",
|
||||||
@@ -6333,13 +6314,10 @@ dependencies = [
|
|||||||
"activitypub",
|
"activitypub",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"application",
|
"application",
|
||||||
"async-trait",
|
|
||||||
"auth",
|
"auth",
|
||||||
"chrono",
|
|
||||||
"domain",
|
"domain",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
"export",
|
"export",
|
||||||
"futures",
|
|
||||||
"image-storage",
|
"image-storage",
|
||||||
"importer",
|
"importer",
|
||||||
"metadata",
|
"metadata",
|
||||||
@@ -6349,17 +6327,13 @@ dependencies = [
|
|||||||
"postgres",
|
"postgres",
|
||||||
"postgres-event-queue",
|
"postgres-event-queue",
|
||||||
"postgres-federation",
|
"postgres-federation",
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"sqlite",
|
"sqlite",
|
||||||
"sqlite-event-queue",
|
"sqlite-event-queue",
|
||||||
"sqlite-federation",
|
"sqlite-federation",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"thiserror 2.0.18",
|
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"uuid",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -19,18 +19,18 @@ members = [
|
|||||||
"crates/adapters/export",
|
"crates/adapters/export",
|
||||||
"crates/adapters/event-payload",
|
"crates/adapters/event-payload",
|
||||||
"crates/adapters/nats",
|
"crates/adapters/nats",
|
||||||
|
"crates/api-types",
|
||||||
"crates/application",
|
"crates/application",
|
||||||
"crates/domain",
|
"crates/domain",
|
||||||
"crates/presentation",
|
"crates/presentation",
|
||||||
"crates/tui",
|
"crates/tui",
|
||||||
"crates/doc",
|
|
||||||
"crates/worker",
|
"crates/worker",
|
||||||
"crates/adapters/importer",
|
"crates/adapters/importer",
|
||||||
]
|
]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
tokio = { version = "1.0", features = ["full"] }
|
tokio = { version = "1.0", features = ["macros", "net", "rt", "rt-multi-thread", "sync", "time"] }
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
dotenvy = "0.15"
|
dotenvy = "0.15"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
@@ -53,6 +53,7 @@ object_store = { version = "0.11", features = ["aws"] }
|
|||||||
axum = { version = "0.8.8", features = ["macros", "multipart"] }
|
axum = { version = "0.8.8", features = ["macros", "multipart"] }
|
||||||
csv = "1"
|
csv = "1"
|
||||||
|
|
||||||
|
api-types = { path = "crates/api-types" }
|
||||||
domain = { path = "crates/domain" }
|
domain = { path = "crates/domain" }
|
||||||
application = { path = "crates/application" }
|
application = { path = "crates/application" }
|
||||||
presentation = { path = "crates/presentation" }
|
presentation = { path = "crates/presentation" }
|
||||||
@@ -71,7 +72,6 @@ postgres-federation = { path = "crates/adapters/postgres-federation" }
|
|||||||
template-askama = { path = "crates/adapters/template-askama" }
|
template-askama = { path = "crates/adapters/template-askama" }
|
||||||
activitypub = { path = "crates/adapters/activitypub" }
|
activitypub = { path = "crates/adapters/activitypub" }
|
||||||
activitypub-base = { path = "crates/adapters/activitypub-base" }
|
activitypub-base = { path = "crates/adapters/activitypub-base" }
|
||||||
doc = { path = "crates/doc" }
|
|
||||||
event-payload = { path = "crates/adapters/event-payload" }
|
event-payload = { path = "crates/adapters/event-payload" }
|
||||||
nats = { path = "crates/adapters/nats" }
|
nats = { path = "crates/adapters/nats" }
|
||||||
sqlite-event-queue = { path = "crates/adapters/sqlite-event-queue" }
|
sqlite-event-queue = { path = "crates/adapters/sqlite-event-queue" }
|
||||||
|
|||||||
@@ -27,10 +27,10 @@ COPY crates/adapters/postgres/Cargo.toml crates/adapters/postgres/Car
|
|||||||
COPY crates/adapters/postgres-federation/Cargo.toml crates/adapters/postgres-federation/Cargo.toml
|
COPY crates/adapters/postgres-federation/Cargo.toml crates/adapters/postgres-federation/Cargo.toml
|
||||||
COPY crates/adapters/postgres-event-queue/Cargo.toml crates/adapters/postgres-event-queue/Cargo.toml
|
COPY crates/adapters/postgres-event-queue/Cargo.toml crates/adapters/postgres-event-queue/Cargo.toml
|
||||||
COPY crates/adapters/template-askama/Cargo.toml crates/adapters/template-askama/Cargo.toml
|
COPY crates/adapters/template-askama/Cargo.toml crates/adapters/template-askama/Cargo.toml
|
||||||
|
COPY crates/api-types/Cargo.toml crates/api-types/Cargo.toml
|
||||||
COPY crates/application/Cargo.toml crates/application/Cargo.toml
|
COPY crates/application/Cargo.toml crates/application/Cargo.toml
|
||||||
COPY crates/domain/Cargo.toml crates/domain/Cargo.toml
|
COPY crates/domain/Cargo.toml crates/domain/Cargo.toml
|
||||||
COPY crates/presentation/Cargo.toml crates/presentation/Cargo.toml
|
COPY crates/presentation/Cargo.toml crates/presentation/Cargo.toml
|
||||||
COPY crates/doc/Cargo.toml crates/doc/Cargo.toml
|
|
||||||
COPY crates/tui/Cargo.toml crates/tui/Cargo.toml
|
COPY crates/tui/Cargo.toml crates/tui/Cargo.toml
|
||||||
COPY crates/worker/Cargo.toml crates/worker/Cargo.toml
|
COPY crates/worker/Cargo.toml crates/worker/Cargo.toml
|
||||||
|
|
||||||
|
|||||||
@@ -24,9 +24,10 @@ A self-hosted, server-side rendered movie logging system with a full REST API. B
|
|||||||
Hexagonal (Ports & Adapters) with Domain-Driven Design:
|
Hexagonal (Ports & Adapters) with Domain-Driven Design:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
api-types — shared REST API request/response DTOs (Serialize/Deserialize + utoipa schemas); used by presentation and tui
|
||||||
domain — pure types and trait definitions, no external deps
|
domain — pure types and trait definitions, no external deps
|
||||||
application — use cases / business logic orchestration
|
application — use cases / business logic orchestration
|
||||||
presentation — Axum HTTP router, composition root for the HTTP process
|
presentation — Axum HTTP router, OpenAPI spec assembly, Swagger UI + Scalar serving, composition root for the HTTP process
|
||||||
worker — standalone worker binary (event consumer, poster sync, federation)
|
worker — standalone worker binary (event consumer, poster sync, federation)
|
||||||
adapters/
|
adapters/
|
||||||
auth — JWT issuance and validation (Argon2 passwords)
|
auth — JWT issuance and validation (Argon2 passwords)
|
||||||
@@ -44,13 +45,12 @@ adapters/
|
|||||||
sqlite-event-queue — durable polling event queue backed by SQLite
|
sqlite-event-queue — durable polling event queue backed by SQLite
|
||||||
postgres-event-queue — durable polling event queue backed by PostgreSQL
|
postgres-event-queue — durable polling event queue backed by PostgreSQL
|
||||||
nats — NATS Core / JetStream event publisher and consumer
|
nats — NATS Core / JetStream event publisher and consumer
|
||||||
event-publisher — in-memory event channel (tests only)
|
event-publisher — in-memory event channel (used in tests)
|
||||||
activitypub — ActivityPub federation wiring (follow, inbox/outbox, actor)
|
activitypub — ActivityPub federation wiring (follow, inbox/outbox, actor)
|
||||||
activitypub-base — core ActivityPub protocol types and service
|
activitypub-base — core ActivityPub protocol types and service
|
||||||
sqlite-federation — SQLite-backed federation repository
|
sqlite-federation — SQLite-backed federation repository
|
||||||
postgres-federation — PostgreSQL-backed federation repository
|
postgres-federation — PostgreSQL-backed federation repository
|
||||||
doc — OpenAPI spec assembly and Swagger UI / Scalar serving
|
tui — terminal UI client (ratatui); shares api-types with presentation for typed API access
|
||||||
tui — terminal UI client (ratatui)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|||||||
@@ -7,10 +7,8 @@ edition = "2024"
|
|||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
reqwest = { workspace = true }
|
|
||||||
uuid = { workspace = true }
|
uuid = { workspace = true }
|
||||||
chrono = { workspace = true }
|
chrono = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
async-trait = { workspace = true }
|
async-trait = { workspace = true }
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ edition = "2024"
|
|||||||
activitypub-base = { workspace = true }
|
activitypub-base = { workspace = true }
|
||||||
domain = { workspace = true }
|
domain = { workspace = true }
|
||||||
axum = { workspace = true }
|
axum = { workspace = true }
|
||||||
tokio = { workspace = true }
|
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
uuid = { workspace = true }
|
uuid = { workspace = true }
|
||||||
|
|||||||
@@ -8,5 +8,4 @@ domain = { workspace = true }
|
|||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
chrono = { workspace = true }
|
chrono = { workspace = true }
|
||||||
uuid = { workspace = true }
|
uuid = { workspace = true }
|
||||||
anyhow = { workspace = true }
|
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
|
|||||||
@@ -7,5 +7,4 @@ edition = "2024"
|
|||||||
domain = { workspace = true }
|
domain = { workspace = true }
|
||||||
async-trait = { workspace = true }
|
async-trait = { workspace = true }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
tracing = { workspace = true }
|
|
||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
|
|||||||
@@ -9,6 +9,5 @@ xlsx = ["dep:calamine"]
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
domain = { workspace = true }
|
domain = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
|
||||||
csv = { workspace = true }
|
csv = { workspace = true }
|
||||||
calamine = { version = "0.26", optional = true }
|
calamine = { version = "0.26", optional = true }
|
||||||
|
|||||||
@@ -10,9 +10,7 @@ domain = { workspace = true }
|
|||||||
event-payload = { workspace = true }
|
event-payload = { workspace = true }
|
||||||
async-trait = { workspace = true }
|
async-trait = { workspace = true }
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
serde = { workspace = true }
|
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
uuid = { workspace = true }
|
uuid = { workspace = true }
|
||||||
chrono = { workspace = true }
|
chrono = { workspace = true }
|
||||||
|
|||||||
@@ -9,10 +9,7 @@ domain = { workspace = true }
|
|||||||
event-payload = { workspace = true }
|
event-payload = { workspace = true }
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
async-trait = { workspace = true }
|
async-trait = { workspace = true }
|
||||||
serde = { workspace = true }
|
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
chrono = { workspace = true }
|
|
||||||
uuid = { workspace = true }
|
|
||||||
|
|||||||
@@ -20,5 +20,3 @@ tracing = { workspace = true }
|
|||||||
async-trait = { workspace = true }
|
async-trait = { workspace = true }
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
tokio = { workspace = true }
|
|
||||||
|
|||||||
@@ -5,6 +5,5 @@ edition = "2024"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rss-feed = { package = "rss", version = "2" }
|
rss-feed = { package = "rss", version = "2" }
|
||||||
chrono = { workspace = true }
|
|
||||||
domain = { workspace = true }
|
domain = { workspace = true }
|
||||||
application = { workspace = true }
|
application = { workspace = true }
|
||||||
|
|||||||
@@ -9,10 +9,7 @@ domain = { workspace = true }
|
|||||||
event-payload = { workspace = true }
|
event-payload = { workspace = true }
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
async-trait = { workspace = true }
|
async-trait = { workspace = true }
|
||||||
serde = { workspace = true }
|
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
chrono = { workspace = true }
|
|
||||||
uuid = { workspace = true }
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ edition = "2024"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
askama = { version = "0.16.0" }
|
askama = { version = "0.16.0" }
|
||||||
|
|
||||||
serde = { workspace = true }
|
|
||||||
chrono = { workspace = true }
|
chrono = { workspace = true }
|
||||||
uuid = { workspace = true }
|
uuid = { workspace = true }
|
||||||
|
|
||||||
|
|||||||
9
crates/api-types/Cargo.toml
Normal file
9
crates/api-types/Cargo.toml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[package]
|
||||||
|
name = "api-types"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { workspace = true }
|
||||||
|
uuid = { workspace = true }
|
||||||
|
utoipa = { version = "5.5.0", features = ["axum_extras", "uuid"] }
|
||||||
23
crates/api-types/src/auth.rs
Normal file
23
crates/api-types/src/auth.rs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct LoginRequest {
|
||||||
|
pub email: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct LoginResponse {
|
||||||
|
pub token: String,
|
||||||
|
pub user_id: Uuid,
|
||||||
|
pub email: String,
|
||||||
|
pub expires_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct RegisterRequest {
|
||||||
|
pub email: String,
|
||||||
|
pub username: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
7
crates/api-types/src/common.rs
Normal file
7
crates/api-types/src/common.rs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Default)]
|
||||||
|
pub struct PaginationQueryParams {
|
||||||
|
pub limit: Option<u32>,
|
||||||
|
pub offset: Option<u32>,
|
||||||
|
}
|
||||||
78
crates/api-types/src/diary.rs
Normal file
78
crates/api-types/src/diary.rs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::movies::{MovieDto, ReviewDto};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct LogReviewRequest {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub external_metadata_id: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub manual_title: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub manual_release_year: Option<u16>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub manual_director: Option<String>,
|
||||||
|
pub rating: u8,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub comment: Option<String>,
|
||||||
|
pub watched_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct DiaryEntryDto {
|
||||||
|
pub movie: MovieDto,
|
||||||
|
pub review: ReviewDto,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct DiaryResponse {
|
||||||
|
pub items: Vec<DiaryEntryDto>,
|
||||||
|
pub total_count: u64,
|
||||||
|
pub limit: u32,
|
||||||
|
pub offset: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, utoipa::IntoParams)]
|
||||||
|
#[into_params(parameter_in = Query)]
|
||||||
|
pub struct DiaryQueryParams {
|
||||||
|
pub limit: Option<u32>,
|
||||||
|
pub offset: Option<u32>,
|
||||||
|
pub sort_by: Option<String>,
|
||||||
|
pub movie_id: Option<Uuid>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, utoipa::IntoParams)]
|
||||||
|
#[into_params(parameter_in = Query)]
|
||||||
|
pub struct ActivityFeedQueryParams {
|
||||||
|
pub limit: Option<u32>,
|
||||||
|
pub offset: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct FeedEntryDto {
|
||||||
|
pub movie: MovieDto,
|
||||||
|
pub review: ReviewDto,
|
||||||
|
pub user_email: String,
|
||||||
|
pub user_display_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct ActivityFeedResponse {
|
||||||
|
pub items: Vec<FeedEntryDto>,
|
||||||
|
pub total_count: u64,
|
||||||
|
pub limit: u32,
|
||||||
|
pub offset: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, utoipa::IntoParams)]
|
||||||
|
#[into_params(parameter_in = Query)]
|
||||||
|
pub struct ExportQueryParams {
|
||||||
|
/// Output format: `csv` (default) or `json`
|
||||||
|
#[serde(default = "default_export_format")]
|
||||||
|
pub format: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_export_format() -> String {
|
||||||
|
"csv".to_string()
|
||||||
|
}
|
||||||
47
crates/api-types/src/import.rs
Normal file
47
crates/api-types/src/import.rs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct SessionCreatedResponse {
|
||||||
|
pub session_id: String,
|
||||||
|
pub columns: Vec<String>,
|
||||||
|
pub sample_rows: Vec<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct SessionStateResponse {
|
||||||
|
pub session_id: String,
|
||||||
|
pub columns: Vec<String>,
|
||||||
|
pub has_mappings: bool,
|
||||||
|
pub row_count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct ApiFieldMapping {
|
||||||
|
/// Column name in the source file
|
||||||
|
pub source_column: String,
|
||||||
|
/// Domain field: title | release_year | director | rating | watched_at | comment | external_metadata_id
|
||||||
|
pub domain_field: String,
|
||||||
|
/// For rating fields: multiply raw value by this factor (e.g. 0.5 for 10-point → 5-point scale)
|
||||||
|
pub rating_scale: Option<f64>,
|
||||||
|
/// For watched_at fields: strftime format hint (e.g. "%d/%m/%Y")
|
||||||
|
pub date_format: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct ApplyMappingRequest {
|
||||||
|
pub mappings: Vec<ApiFieldMapping>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct ConfirmRequest {
|
||||||
|
/// Indices (0-based) of rows from the mapping preview to import
|
||||||
|
pub confirmed_indices: Vec<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct SaveProfileRequest {
|
||||||
|
/// Session UUID whose current field_mappings to save
|
||||||
|
pub session_id: String,
|
||||||
|
/// Human-readable profile name (e.g. "Letterboxd")
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
15
crates/api-types/src/lib.rs
Normal file
15
crates/api-types/src/lib.rs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
pub mod auth;
|
||||||
|
pub mod common;
|
||||||
|
pub mod diary;
|
||||||
|
pub mod import;
|
||||||
|
pub mod movies;
|
||||||
|
pub mod social;
|
||||||
|
pub mod users;
|
||||||
|
|
||||||
|
pub use auth::*;
|
||||||
|
pub use common::*;
|
||||||
|
pub use diary::*;
|
||||||
|
pub use import::*;
|
||||||
|
pub use movies::*;
|
||||||
|
pub use social::*;
|
||||||
|
pub use users::*;
|
||||||
58
crates/api-types/src/movies.rs
Normal file
58
crates/api-types/src/movies.rs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct MovieDto {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub title: String,
|
||||||
|
pub release_year: u16,
|
||||||
|
pub director: Option<String>,
|
||||||
|
pub poster_path: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct ReviewDto {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub rating: u8,
|
||||||
|
pub comment: Option<String>,
|
||||||
|
pub watched_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct ReviewHistoryResponse {
|
||||||
|
pub movie: MovieDto,
|
||||||
|
pub viewings: Vec<ReviewDto>,
|
||||||
|
pub trend: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct MovieStatsDto {
|
||||||
|
pub total_count: u64,
|
||||||
|
pub avg_rating: Option<f64>,
|
||||||
|
pub federated_count: u64,
|
||||||
|
pub rating_histogram: [u64; 5],
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct SocialReviewDto {
|
||||||
|
pub user_display: String,
|
||||||
|
pub rating: u8,
|
||||||
|
pub comment: Option<String>,
|
||||||
|
pub watched_at: String,
|
||||||
|
pub is_federated: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct SocialFeedResponse {
|
||||||
|
pub items: Vec<SocialReviewDto>,
|
||||||
|
pub total_count: u64,
|
||||||
|
pub limit: u32,
|
||||||
|
pub offset: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct MovieDetailResponse {
|
||||||
|
pub movie: MovieDto,
|
||||||
|
pub stats: MovieStatsDto,
|
||||||
|
pub reviews: SocialFeedResponse,
|
||||||
|
}
|
||||||
44
crates/api-types/src/social.rs
Normal file
44
crates/api-types/src/social.rs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct FollowRequest {
|
||||||
|
pub handle: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct ActorUrlRequest {
|
||||||
|
pub actor_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct RemoteActorDto {
|
||||||
|
pub handle: String,
|
||||||
|
pub display_name: Option<String>,
|
||||||
|
pub url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct ActorListResponse {
|
||||||
|
pub actors: Vec<RemoteActorDto>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct BlockedDomainResponse {
|
||||||
|
pub domain: String,
|
||||||
|
pub reason: Option<String>,
|
||||||
|
pub blocked_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct AddBlockedDomainRequest {
|
||||||
|
pub domain: String,
|
||||||
|
pub reason: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct BlockedActorResponse {
|
||||||
|
pub url: String,
|
||||||
|
pub handle: String,
|
||||||
|
pub display_name: Option<String>,
|
||||||
|
pub avatar_url: Option<String>,
|
||||||
|
}
|
||||||
85
crates/api-types/src/users.rs
Normal file
85
crates/api-types/src/users.rs
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::diary::{DiaryEntryDto, DiaryResponse};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct UserSummaryDto {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub email: String,
|
||||||
|
pub total_movies: i64,
|
||||||
|
pub avg_rating: Option<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct UsersResponse {
|
||||||
|
pub users: Vec<UserSummaryDto>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, utoipa::IntoParams)]
|
||||||
|
#[into_params(parameter_in = Query)]
|
||||||
|
pub struct UserProfileQueryParams {
|
||||||
|
/// One of: `recent` (default), `ratings`, `history`, `trends`
|
||||||
|
pub view: Option<String>,
|
||||||
|
pub limit: Option<u32>,
|
||||||
|
pub offset: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct UserStatsDto {
|
||||||
|
pub total_movies: i64,
|
||||||
|
pub avg_rating: Option<f64>,
|
||||||
|
pub favorite_director: Option<String>,
|
||||||
|
pub most_active_month: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct MonthActivityDto {
|
||||||
|
pub year_month: String,
|
||||||
|
pub month_label: String,
|
||||||
|
pub count: i64,
|
||||||
|
pub entries: Vec<DiaryEntryDto>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct MonthlyRatingDto {
|
||||||
|
pub year_month: String,
|
||||||
|
pub month_label: String,
|
||||||
|
pub avg_rating: f64,
|
||||||
|
pub count: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct DirectorStatDto {
|
||||||
|
pub director: String,
|
||||||
|
pub count: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct UserTrendsDto {
|
||||||
|
pub monthly_ratings: Vec<MonthlyRatingDto>,
|
||||||
|
pub top_directors: Vec<DirectorStatDto>,
|
||||||
|
pub max_director_count: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct UserProfileResponse {
|
||||||
|
pub user_id: Uuid,
|
||||||
|
pub username: String,
|
||||||
|
pub stats: UserStatsDto,
|
||||||
|
pub following_count: usize,
|
||||||
|
pub followers_count: usize,
|
||||||
|
/// Populated for view=recent and view=ratings
|
||||||
|
pub entries: Option<DiaryResponse>,
|
||||||
|
/// Populated for view=history
|
||||||
|
pub history: Option<Vec<MonthActivityDto>>,
|
||||||
|
/// Populated for view=trends
|
||||||
|
pub trends: Option<UserTrendsDto>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
|
pub struct ProfileResponse {
|
||||||
|
pub username: String,
|
||||||
|
pub bio: Option<String>,
|
||||||
|
pub avatar_url: Option<String>,
|
||||||
|
}
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "doc"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2024"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
axum = { workspace = true }
|
|
||||||
tracing = { workspace = true }
|
|
||||||
utoipa = { version = "5.5.0", features = ["axum_extras"] }
|
|
||||||
utoipa-scalar = { version = "0.3.0", features = [
|
|
||||||
"axum",
|
|
||||||
], default-features = false }
|
|
||||||
utoipa-swagger-ui = { version = "9.0.2", features = ["axum", "vendored"] }
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
use axum::Router;
|
|
||||||
use utoipa::openapi::OpenApi;
|
|
||||||
use utoipa_scalar::{Scalar, Servable};
|
|
||||||
use utoipa_swagger_ui::SwaggerUi;
|
|
||||||
|
|
||||||
pub trait ApiDocExt {
|
|
||||||
fn with_api_doc(self, spec: OpenApi) -> Self;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ApiDocExt for Router {
|
|
||||||
fn with_api_doc(self, spec: OpenApi) -> Self {
|
|
||||||
tracing::info!("API docs at /docs (Swagger) and /scalar");
|
|
||||||
self.merge(SwaggerUi::new("/docs").url("/openapi.json", spec.clone()))
|
|
||||||
.merge(Scalar::with_url("/scalar", spec))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,6 @@ edition = "2024"
|
|||||||
uuid = { workspace = true }
|
uuid = { workspace = true }
|
||||||
chrono = { workspace = true }
|
chrono = { workspace = true }
|
||||||
async-trait = { workspace = true }
|
async-trait = { workspace = true }
|
||||||
anyhow = { workspace = true }
|
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ axum = { workspace = true }
|
|||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
tracing-subscriber = { workspace = true }
|
tracing-subscriber = { workspace = true }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
@@ -42,6 +41,7 @@ uuid = { workspace = true }
|
|||||||
chrono = { workspace = true }
|
chrono = { workspace = true }
|
||||||
async-trait = { workspace = true }
|
async-trait = { workspace = true }
|
||||||
|
|
||||||
|
api-types = { workspace = true }
|
||||||
domain = { workspace = true }
|
domain = { workspace = true }
|
||||||
application = { workspace = true }
|
application = { workspace = true }
|
||||||
auth = { workspace = true }
|
auth = { workspace = true }
|
||||||
@@ -52,10 +52,11 @@ template-askama = { workspace = true }
|
|||||||
nats = { workspace = true, optional = true }
|
nats = { workspace = true, optional = true }
|
||||||
rss = { workspace = true }
|
rss = { workspace = true }
|
||||||
export = { workspace = true }
|
export = { workspace = true }
|
||||||
doc = { workspace = true }
|
|
||||||
importer = { workspace = true }
|
importer = { workspace = true }
|
||||||
sqlx = { workspace = true }
|
sqlx = { workspace = true }
|
||||||
utoipa = { version = "5.5.0", features = ["axum_extras", "uuid"] }
|
utoipa = { version = "5.5.0", features = ["axum_extras", "uuid"] }
|
||||||
|
utoipa-scalar = { version = "0.3.0", features = ["axum"], default-features = false }
|
||||||
|
utoipa-swagger-ui = { version = "9.0.2", features = ["axum", "vendored"] }
|
||||||
|
|
||||||
# Optional — database backends
|
# Optional — database backends
|
||||||
sqlite = { workspace = true, optional = true }
|
sqlite = { workspace = true, optional = true }
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::Deserialize;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use application::{commands::LogReviewCommand, queries::GetDiaryQuery};
|
use application::{commands::LogReviewCommand, queries::GetDiaryQuery};
|
||||||
use domain::{errors::DomainError, models::SortDirection};
|
use domain::{errors::DomainError, models::SortDirection};
|
||||||
|
|
||||||
fn empty_string_as_none<'de, D, T>(de: D) -> Result<Option<T>, D::Error>
|
use api_types::{DiaryQueryParams, LogReviewRequest};
|
||||||
|
|
||||||
|
pub fn empty_string_as_none<'de, D, T>(de: D) -> Result<Option<T>, D::Error>
|
||||||
where
|
where
|
||||||
D: serde::Deserializer<'de>,
|
D: serde::Deserializer<'de>,
|
||||||
T: std::str::FromStr,
|
T: std::str::FromStr,
|
||||||
@@ -18,15 +20,6 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, utoipa::IntoParams)]
|
|
||||||
#[into_params(parameter_in = Query)]
|
|
||||||
pub struct DiaryQueryParams {
|
|
||||||
pub limit: Option<u32>,
|
|
||||||
pub offset: Option<u32>,
|
|
||||||
pub sort_by: Option<String>,
|
|
||||||
pub movie_id: Option<Uuid>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct LogReviewForm {
|
pub struct LogReviewForm {
|
||||||
#[serde(default, deserialize_with = "empty_string_as_none")]
|
#[serde(default, deserialize_with = "empty_string_as_none")]
|
||||||
@@ -67,7 +60,7 @@ pub struct ErrorQuery {
|
|||||||
pub error: Option<String>,
|
pub error: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize, Default)]
|
#[derive(Deserialize, Default)]
|
||||||
pub struct FeedQueryParams {
|
pub struct FeedQueryParams {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub filter: String,
|
pub filter: String,
|
||||||
@@ -87,74 +80,60 @@ pub struct DeleteRedirectForm {
|
|||||||
pub csrf_token: String,
|
pub csrf_token: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, utoipa::ToSchema)]
|
#[derive(Deserialize)]
|
||||||
pub struct LogReviewRequest {
|
pub struct FollowForm {
|
||||||
pub external_metadata_id: Option<String>,
|
pub handle: String,
|
||||||
pub manual_title: Option<String>,
|
#[serde(rename = "_csrf", default)]
|
||||||
pub manual_release_year: Option<u16>,
|
pub csrf_token: String,
|
||||||
pub manual_director: Option<String>,
|
|
||||||
pub rating: u8,
|
|
||||||
pub comment: Option<String>,
|
|
||||||
pub watched_at: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, utoipa::ToSchema)]
|
#[derive(Deserialize)]
|
||||||
pub struct MovieDto {
|
pub struct UnfollowForm {
|
||||||
pub id: Uuid,
|
pub actor_url: String,
|
||||||
pub title: String,
|
#[serde(rename = "_csrf", default)]
|
||||||
pub release_year: u16,
|
pub csrf_token: String,
|
||||||
pub director: Option<String>,
|
|
||||||
pub poster_path: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, utoipa::ToSchema)]
|
#[derive(Deserialize)]
|
||||||
pub struct ReviewDto {
|
pub struct FollowerActionForm {
|
||||||
pub id: Uuid,
|
pub actor_url: String,
|
||||||
pub rating: u8,
|
#[serde(rename = "_csrf", default)]
|
||||||
pub comment: Option<String>,
|
pub csrf_token: String,
|
||||||
pub watched_at: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, utoipa::ToSchema)]
|
#[derive(Deserialize)]
|
||||||
pub struct DiaryEntryDto {
|
pub struct BlockDomainForm {
|
||||||
pub movie: MovieDto,
|
pub domain: String,
|
||||||
pub review: ReviewDto,
|
#[serde(default)]
|
||||||
|
pub reason: Option<String>,
|
||||||
|
#[serde(rename = "_csrf", default)]
|
||||||
|
pub csrf_token: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, utoipa::ToSchema)]
|
#[derive(Deserialize)]
|
||||||
pub struct DiaryResponse {
|
pub struct RemoveDomainForm {
|
||||||
pub items: Vec<DiaryEntryDto>,
|
pub domain: String,
|
||||||
pub total_count: u64,
|
#[serde(rename = "_csrf", default)]
|
||||||
pub limit: u32,
|
pub csrf_token: String,
|
||||||
pub offset: u32,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, utoipa::ToSchema)]
|
#[derive(Deserialize)]
|
||||||
pub struct ReviewHistoryResponse {
|
pub struct ActorUrlForm {
|
||||||
pub movie: MovieDto,
|
pub actor_url: String,
|
||||||
pub viewings: Vec<ReviewDto>,
|
#[serde(rename = "_csrf", default)]
|
||||||
pub trend: String,
|
pub csrf_token: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, utoipa::ToSchema)]
|
#[derive(Deserialize, Default)]
|
||||||
pub struct LoginRequest {
|
pub struct ProfileQueryParams {
|
||||||
pub email: String,
|
pub view: Option<String>,
|
||||||
pub password: String,
|
pub limit: Option<u32>,
|
||||||
}
|
pub offset: Option<u32>,
|
||||||
|
pub error: Option<String>,
|
||||||
#[derive(Serialize, utoipa::ToSchema)]
|
#[serde(default)]
|
||||||
pub struct LoginResponse {
|
pub sort_by: String,
|
||||||
pub token: String,
|
#[serde(default)]
|
||||||
pub user_id: Uuid,
|
pub search: String,
|
||||||
pub email: String,
|
|
||||||
pub expires_at: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, utoipa::ToSchema)]
|
|
||||||
pub struct RegisterRequest {
|
|
||||||
pub email: String,
|
|
||||||
pub username: String,
|
|
||||||
pub password: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct LogReviewData {
|
pub struct LogReviewData {
|
||||||
@@ -239,283 +218,22 @@ impl LogReviewData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<DiaryQueryParams> for GetDiaryQuery {
|
pub fn to_diary_query(p: DiaryQueryParams) -> GetDiaryQuery {
|
||||||
fn from(p: DiaryQueryParams) -> Self {
|
GetDiaryQuery {
|
||||||
GetDiaryQuery {
|
limit: p.limit,
|
||||||
limit: p.limit,
|
offset: p.offset,
|
||||||
offset: p.offset,
|
sort_by: p.sort_by.as_deref().map(|s| {
|
||||||
sort_by: p.sort_by.as_deref().map(|s| {
|
if s == "asc" {
|
||||||
if s == "asc" {
|
SortDirection::Ascending
|
||||||
SortDirection::Ascending
|
} else {
|
||||||
} else {
|
SortDirection::Descending
|
||||||
SortDirection::Descending
|
}
|
||||||
}
|
}),
|
||||||
}),
|
movie_id: p.movie_id,
|
||||||
movie_id: p.movie_id,
|
user_id: None,
|
||||||
user_id: None,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct FollowForm {
|
|
||||||
pub handle: String,
|
|
||||||
#[serde(rename = "_csrf", default)]
|
|
||||||
pub csrf_token: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct UnfollowForm {
|
|
||||||
pub actor_url: String,
|
|
||||||
#[serde(rename = "_csrf", default)]
|
|
||||||
pub csrf_token: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct FollowerActionForm {
|
|
||||||
pub actor_url: String,
|
|
||||||
#[serde(rename = "_csrf", default)]
|
|
||||||
pub csrf_token: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct BlockDomainForm {
|
|
||||||
pub domain: String,
|
|
||||||
#[serde(default)]
|
|
||||||
pub reason: Option<String>,
|
|
||||||
#[serde(rename = "_csrf", default)]
|
|
||||||
pub csrf_token: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct RemoveDomainForm {
|
|
||||||
pub domain: String,
|
|
||||||
#[serde(rename = "_csrf", default)]
|
|
||||||
pub csrf_token: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct ActorUrlForm {
|
|
||||||
pub actor_url: String,
|
|
||||||
#[serde(rename = "_csrf", default)]
|
|
||||||
pub csrf_token: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize, Default)]
|
|
||||||
pub struct ProfileQueryParams {
|
|
||||||
pub view: Option<String>,
|
|
||||||
pub limit: Option<u32>,
|
|
||||||
pub offset: Option<u32>,
|
|
||||||
pub error: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub sort_by: String,
|
|
||||||
#[serde(default)]
|
|
||||||
pub search: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Activity feed ─────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
#[derive(Deserialize, utoipa::IntoParams)]
|
|
||||||
#[into_params(parameter_in = Query)]
|
|
||||||
pub struct ActivityFeedQueryParams {
|
|
||||||
pub limit: Option<u32>,
|
|
||||||
pub offset: Option<u32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, utoipa::ToSchema)]
|
|
||||||
pub struct FeedEntryDto {
|
|
||||||
pub movie: MovieDto,
|
|
||||||
pub review: ReviewDto,
|
|
||||||
pub user_email: String,
|
|
||||||
pub user_display_name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, utoipa::ToSchema)]
|
|
||||||
pub struct ActivityFeedResponse {
|
|
||||||
pub items: Vec<FeedEntryDto>,
|
|
||||||
pub total_count: u64,
|
|
||||||
pub limit: u32,
|
|
||||||
pub offset: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Users ──────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
#[derive(Serialize, utoipa::ToSchema)]
|
|
||||||
pub struct UserSummaryDto {
|
|
||||||
pub id: Uuid,
|
|
||||||
pub email: String,
|
|
||||||
pub total_movies: i64,
|
|
||||||
pub avg_rating: Option<f64>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, utoipa::ToSchema)]
|
|
||||||
pub struct UsersResponse {
|
|
||||||
pub users: Vec<UserSummaryDto>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── User profile ───────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
#[derive(Deserialize, utoipa::IntoParams)]
|
|
||||||
#[into_params(parameter_in = Query)]
|
|
||||||
pub struct UserProfileQueryParams {
|
|
||||||
/// One of: `recent` (default), `ratings`, `history`, `trends`
|
|
||||||
pub view: Option<String>,
|
|
||||||
pub limit: Option<u32>,
|
|
||||||
pub offset: Option<u32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, utoipa::ToSchema)]
|
|
||||||
pub struct UserStatsDto {
|
|
||||||
pub total_movies: i64,
|
|
||||||
pub avg_rating: Option<f64>,
|
|
||||||
pub favorite_director: Option<String>,
|
|
||||||
pub most_active_month: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, utoipa::ToSchema)]
|
|
||||||
pub struct MonthActivityDto {
|
|
||||||
pub year_month: String,
|
|
||||||
pub month_label: String,
|
|
||||||
pub count: i64,
|
|
||||||
pub entries: Vec<DiaryEntryDto>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, utoipa::ToSchema)]
|
|
||||||
pub struct MonthlyRatingDto {
|
|
||||||
pub year_month: String,
|
|
||||||
pub month_label: String,
|
|
||||||
pub avg_rating: f64,
|
|
||||||
pub count: i64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, utoipa::ToSchema)]
|
|
||||||
pub struct DirectorStatDto {
|
|
||||||
pub director: String,
|
|
||||||
pub count: i64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, utoipa::ToSchema)]
|
|
||||||
pub struct UserTrendsDto {
|
|
||||||
pub monthly_ratings: Vec<MonthlyRatingDto>,
|
|
||||||
pub top_directors: Vec<DirectorStatDto>,
|
|
||||||
pub max_director_count: i64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, utoipa::ToSchema)]
|
|
||||||
pub struct UserProfileResponse {
|
|
||||||
pub user_id: Uuid,
|
|
||||||
pub username: String,
|
|
||||||
pub stats: UserStatsDto,
|
|
||||||
pub following_count: usize,
|
|
||||||
pub followers_count: usize,
|
|
||||||
/// Populated for view=recent and view=ratings
|
|
||||||
pub entries: Option<DiaryResponse>,
|
|
||||||
/// Populated for view=history
|
|
||||||
pub history: Option<Vec<MonthActivityDto>>,
|
|
||||||
/// Populated for view=trends
|
|
||||||
pub trends: Option<UserTrendsDto>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, utoipa::ToSchema)]
|
|
||||||
pub struct FollowRequest {
|
|
||||||
pub handle: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, utoipa::ToSchema)]
|
|
||||||
pub struct ActorUrlRequest {
|
|
||||||
pub actor_url: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, utoipa::ToSchema)]
|
|
||||||
pub struct RemoteActorDto {
|
|
||||||
pub handle: String,
|
|
||||||
pub display_name: Option<String>,
|
|
||||||
pub url: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, utoipa::ToSchema)]
|
|
||||||
pub struct ActorListResponse {
|
|
||||||
pub actors: Vec<RemoteActorDto>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize, utoipa::IntoParams)]
|
|
||||||
#[into_params(parameter_in = Query)]
|
|
||||||
pub struct ExportQueryParams {
|
|
||||||
/// Output format: `csv` (default) or `json`
|
|
||||||
#[serde(default = "default_export_format")]
|
|
||||||
pub format: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_export_format() -> String {
|
|
||||||
"csv".to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize, Default)]
|
|
||||||
pub struct PaginationQueryParams {
|
|
||||||
pub limit: Option<u32>,
|
|
||||||
pub offset: Option<u32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Serialize, utoipa::ToSchema)]
|
|
||||||
pub struct ProfileResponse {
|
|
||||||
pub username: String,
|
|
||||||
pub bio: Option<String>,
|
|
||||||
pub avatar_url: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Serialize, utoipa::ToSchema)]
|
|
||||||
pub struct MovieStatsDto {
|
|
||||||
pub total_count: u64,
|
|
||||||
pub avg_rating: Option<f64>,
|
|
||||||
pub federated_count: u64,
|
|
||||||
pub rating_histogram: [u64; 5],
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Serialize, utoipa::ToSchema)]
|
|
||||||
pub struct SocialReviewDto {
|
|
||||||
pub user_display: String,
|
|
||||||
pub rating: u8,
|
|
||||||
pub comment: Option<String>,
|
|
||||||
pub watched_at: String,
|
|
||||||
pub is_federated: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Serialize, utoipa::ToSchema)]
|
|
||||||
pub struct SocialFeedResponse {
|
|
||||||
pub items: Vec<SocialReviewDto>,
|
|
||||||
pub total_count: u64,
|
|
||||||
pub limit: u32,
|
|
||||||
pub offset: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Serialize, utoipa::ToSchema)]
|
|
||||||
pub struct MovieDetailResponse {
|
|
||||||
pub movie: MovieDto,
|
|
||||||
pub stats: MovieStatsDto,
|
|
||||||
pub reviews: SocialFeedResponse,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Serialize, utoipa::ToSchema)]
|
|
||||||
pub struct BlockedDomainResponse {
|
|
||||||
pub domain: String,
|
|
||||||
pub reason: Option<String>,
|
|
||||||
pub blocked_at: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Deserialize, utoipa::ToSchema)]
|
|
||||||
pub struct AddBlockedDomainRequest {
|
|
||||||
pub domain: String,
|
|
||||||
pub reason: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Serialize, utoipa::ToSchema)]
|
|
||||||
pub struct BlockedActorResponse {
|
|
||||||
pub url: String,
|
|
||||||
pub handle: String,
|
|
||||||
pub display_name: Option<String>,
|
|
||||||
pub avatar_url: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -602,7 +320,7 @@ mod tests {
|
|||||||
offset: None,
|
offset: None,
|
||||||
movie_id: None,
|
movie_id: None,
|
||||||
};
|
};
|
||||||
let query = GetDiaryQuery::from(params);
|
let query = to_diary_query(params);
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
query.sort_by,
|
query.sort_by,
|
||||||
Some(domain::models::SortDirection::Ascending)
|
Some(domain::models::SortDirection::Ascending)
|
||||||
@@ -617,7 +335,7 @@ mod tests {
|
|||||||
offset: None,
|
offset: None,
|
||||||
movie_id: None,
|
movie_id: None,
|
||||||
};
|
};
|
||||||
let query = GetDiaryQuery::from(params);
|
let query = to_diary_query(params);
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
query.sort_by,
|
query.sort_by,
|
||||||
Some(domain::models::SortDirection::Descending)
|
Some(domain::models::SortDirection::Descending)
|
||||||
@@ -31,19 +31,22 @@ use domain::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "federation")]
|
#[cfg(feature = "federation")]
|
||||||
use crate::dtos::{ActorListResponse, ActorUrlRequest, FollowRequest, RemoteActorDto};
|
use api_types::{
|
||||||
|
ActorListResponse, ActorUrlRequest, AddBlockedDomainRequest, BlockedActorResponse,
|
||||||
|
BlockedDomainResponse, FollowRequest, RemoteActorDto,
|
||||||
|
};
|
||||||
|
use api_types::{
|
||||||
|
ActivityFeedQueryParams, ActivityFeedResponse, DiaryEntryDto, DiaryQueryParams, DiaryResponse,
|
||||||
|
DirectorStatDto, ExportQueryParams, FeedEntryDto, LogReviewRequest, LoginRequest, LoginResponse,
|
||||||
|
MonthActivityDto, MonthlyRatingDto, MovieDetailResponse, MovieDto, MovieStatsDto,
|
||||||
|
PaginationQueryParams, ProfileResponse, RegisterRequest, ReviewDto, ReviewHistoryResponse,
|
||||||
|
SocialFeedResponse, SocialReviewDto, UserProfileQueryParams, UserProfileResponse, UserStatsDto,
|
||||||
|
UserSummaryDto, UserTrendsDto, UsersResponse,
|
||||||
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
dtos::{
|
|
||||||
ActivityFeedQueryParams, ActivityFeedResponse, DiaryEntryDto, DiaryQueryParams,
|
|
||||||
DiaryResponse, DirectorStatDto, ExportQueryParams, FeedEntryDto, LogReviewData,
|
|
||||||
LogReviewRequest, LoginRequest, LoginResponse, MonthActivityDto, MonthlyRatingDto,
|
|
||||||
MovieDetailResponse, MovieDto, MovieStatsDto, PaginationQueryParams, ProfileResponse,
|
|
||||||
RegisterRequest, ReviewDto, ReviewHistoryResponse, SocialFeedResponse, SocialReviewDto,
|
|
||||||
UserProfileQueryParams, UserProfileResponse, UserStatsDto, UserSummaryDto, UserTrendsDto,
|
|
||||||
UsersResponse,
|
|
||||||
},
|
|
||||||
errors::ApiError,
|
errors::ApiError,
|
||||||
extractors::AuthenticatedUser,
|
extractors::AuthenticatedUser,
|
||||||
|
forms::{to_diary_query, LogReviewData},
|
||||||
state::AppState,
|
state::AppState,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -60,7 +63,7 @@ pub async fn get_diary(
|
|||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Query(params): Query<DiaryQueryParams>,
|
Query(params): Query<DiaryQueryParams>,
|
||||||
) -> Result<Json<DiaryResponse>, ApiError> {
|
) -> Result<Json<DiaryResponse>, ApiError> {
|
||||||
let page = get_diary::execute(&state.app_ctx, params.into()).await?;
|
let page = get_diary::execute(&state.app_ctx, to_diary_query(params)).await?;
|
||||||
|
|
||||||
Ok(Json(DiaryResponse {
|
Ok(Json(DiaryResponse {
|
||||||
items: page.items.iter().map(entry_to_dto).collect(),
|
items: page.items.iter().map(entry_to_dto).collect(),
|
||||||
@@ -415,7 +418,7 @@ fn entry_to_dto(entry: &DiaryEntry) -> DiaryEntryDto {
|
|||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
get, path = "/api/v1/admin/blocked-domains",
|
get, path = "/api/v1/admin/blocked-domains",
|
||||||
responses(
|
responses(
|
||||||
(status = 200, body = Vec<crate::dtos::BlockedDomainResponse>),
|
(status = 200, body = Vec<BlockedDomainResponse>),
|
||||||
(status = 401, description = "Unauthorized"),
|
(status = 401, description = "Unauthorized"),
|
||||||
(status = 403, description = "Forbidden — admin only"),
|
(status = 403, description = "Forbidden — admin only"),
|
||||||
),
|
),
|
||||||
@@ -427,9 +430,9 @@ pub async fn get_blocked_domains_admin(
|
|||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match state.ap_service.get_blocked_domains().await {
|
match state.ap_service.get_blocked_domains().await {
|
||||||
Ok(domains) => {
|
Ok(domains) => {
|
||||||
let response: Vec<crate::dtos::BlockedDomainResponse> = domains
|
let response: Vec<BlockedDomainResponse> = domains
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|d| crate::dtos::BlockedDomainResponse {
|
.map(|d| BlockedDomainResponse {
|
||||||
domain: d.domain,
|
domain: d.domain,
|
||||||
reason: d.reason,
|
reason: d.reason,
|
||||||
blocked_at: d.blocked_at,
|
blocked_at: d.blocked_at,
|
||||||
@@ -444,7 +447,7 @@ pub async fn get_blocked_domains_admin(
|
|||||||
#[cfg(feature = "federation")]
|
#[cfg(feature = "federation")]
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
post, path = "/api/v1/admin/blocked-domains",
|
post, path = "/api/v1/admin/blocked-domains",
|
||||||
request_body = crate::dtos::AddBlockedDomainRequest,
|
request_body = AddBlockedDomainRequest,
|
||||||
responses(
|
responses(
|
||||||
(status = 201, description = "Domain blocked"),
|
(status = 201, description = "Domain blocked"),
|
||||||
(status = 401, description = "Unauthorized"),
|
(status = 401, description = "Unauthorized"),
|
||||||
@@ -455,7 +458,7 @@ pub async fn get_blocked_domains_admin(
|
|||||||
pub async fn add_blocked_domain_admin(
|
pub async fn add_blocked_domain_admin(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
_admin: crate::extractors::AdminUser,
|
_admin: crate::extractors::AdminUser,
|
||||||
axum::Json(body): axum::Json<crate::dtos::AddBlockedDomainRequest>,
|
axum::Json(body): axum::Json<AddBlockedDomainRequest>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match state.ap_service.add_blocked_domain(&body.domain, body.reason.as_deref()).await {
|
match state.ap_service.add_blocked_domain(&body.domain, body.reason.as_deref()).await {
|
||||||
Ok(()) => StatusCode::CREATED.into_response(),
|
Ok(()) => StatusCode::CREATED.into_response(),
|
||||||
@@ -531,7 +534,7 @@ pub async fn unblock_actor_api(
|
|||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
get, path = "/api/v1/social/blocked",
|
get, path = "/api/v1/social/blocked",
|
||||||
responses(
|
responses(
|
||||||
(status = 200, body = Vec<crate::dtos::BlockedActorResponse>),
|
(status = 200, body = Vec<BlockedActorResponse>),
|
||||||
(status = 401, description = "Unauthorized"),
|
(status = 401, description = "Unauthorized"),
|
||||||
),
|
),
|
||||||
security(("bearer_auth" = []))
|
security(("bearer_auth" = []))
|
||||||
@@ -542,9 +545,9 @@ pub async fn get_blocked_actors_api(
|
|||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
match state.ap_service.get_blocked_actors(user.0.value()).await {
|
match state.ap_service.get_blocked_actors(user.0.value()).await {
|
||||||
Ok(actors) => {
|
Ok(actors) => {
|
||||||
let response: Vec<crate::dtos::BlockedActorResponse> = actors
|
let response: Vec<BlockedActorResponse> = actors
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|a| crate::dtos::BlockedActorResponse {
|
.map(|a| BlockedActorResponse {
|
||||||
url: a.url,
|
url: a.url,
|
||||||
handle: a.handle,
|
handle: a.handle,
|
||||||
display_name: a.display_name,
|
display_name: a.display_name,
|
||||||
|
|||||||
@@ -30,12 +30,10 @@ use domain::models::ExportFormat;
|
|||||||
use domain::{errors::DomainError, value_objects::UserId};
|
use domain::{errors::DomainError, value_objects::UserId};
|
||||||
|
|
||||||
#[cfg(feature = "federation")]
|
#[cfg(feature = "federation")]
|
||||||
use crate::dtos::{ActorUrlForm, BlockDomainForm, FollowForm, FollowerActionForm, RemoveDomainForm, UnfollowForm};
|
use crate::forms::{ActorUrlForm, BlockDomainForm, FollowForm, FollowerActionForm, RemoveDomainForm, UnfollowForm};
|
||||||
use crate::{
|
use crate::{
|
||||||
csrf::CsrfToken,
|
csrf::CsrfToken,
|
||||||
dtos::{
|
forms::{ErrorQuery, FeedQueryParams, LogReviewData, LogReviewForm, LoginForm, RegisterForm},
|
||||||
ErrorQuery, FeedQueryParams, LogReviewData, LogReviewForm, LoginForm, RegisterForm,
|
|
||||||
},
|
|
||||||
extractors::{AdminUser, OptionalCookieUser, RequiredCookieUser},
|
extractors::{AdminUser, OptionalCookieUser, RequiredCookieUser},
|
||||||
state::AppState,
|
state::AppState,
|
||||||
};
|
};
|
||||||
@@ -280,7 +278,7 @@ pub async fn post_delete_review(
|
|||||||
RequiredCookieUser(user_id): RequiredCookieUser,
|
RequiredCookieUser(user_id): RequiredCookieUser,
|
||||||
Extension(csrf): Extension<CsrfToken>,
|
Extension(csrf): Extension<CsrfToken>,
|
||||||
Path(review_id): Path<Uuid>,
|
Path(review_id): Path<Uuid>,
|
||||||
Form(form): Form<crate::dtos::DeleteRedirectForm>,
|
Form(form): Form<crate::forms::DeleteRedirectForm>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
if crate::csrf::mismatch(&csrf, &form.csrf_token) {
|
if crate::csrf::mismatch(&csrf, &form.csrf_token) {
|
||||||
return StatusCode::FORBIDDEN.into_response();
|
return StatusCode::FORBIDDEN.into_response();
|
||||||
@@ -311,7 +309,7 @@ pub async fn post_delete_review(
|
|||||||
pub async fn get_export(
|
pub async fn get_export(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
RequiredCookieUser(user_id): RequiredCookieUser,
|
RequiredCookieUser(user_id): RequiredCookieUser,
|
||||||
Query(params): Query<crate::dtos::ExportQueryParams>,
|
Query(params): Query<api_types::ExportQueryParams>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let format = match params.format.as_str() {
|
let format = match params.format.as_str() {
|
||||||
"csv" => ExportFormat::Csv,
|
"csv" => ExportFormat::Csv,
|
||||||
@@ -504,7 +502,7 @@ pub async fn get_user_profile(
|
|||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path(profile_user_uuid): Path<Uuid>,
|
Path(profile_user_uuid): Path<Uuid>,
|
||||||
headers: axum::http::HeaderMap,
|
headers: axum::http::HeaderMap,
|
||||||
Query(params): Query<crate::dtos::ProfileQueryParams>,
|
Query(params): Query<crate::forms::ProfileQueryParams>,
|
||||||
Extension(csrf): Extension<CsrfToken>,
|
Extension(csrf): Extension<CsrfToken>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
// Content negotiation: AP clients request application/activity+json
|
// Content negotiation: AP clients request application/activity+json
|
||||||
@@ -800,7 +798,7 @@ pub async fn get_following_page(
|
|||||||
RequiredCookieUser(user_id): RequiredCookieUser,
|
RequiredCookieUser(user_id): RequiredCookieUser,
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path(profile_user_uuid): Path<Uuid>,
|
Path(profile_user_uuid): Path<Uuid>,
|
||||||
Query(params): Query<crate::dtos::ErrorQuery>,
|
Query(params): Query<crate::forms::ErrorQuery>,
|
||||||
Extension(csrf): Extension<CsrfToken>,
|
Extension(csrf): Extension<CsrfToken>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
if user_id.value() != profile_user_uuid {
|
if user_id.value() != profile_user_uuid {
|
||||||
@@ -850,7 +848,7 @@ pub async fn get_followers_page(
|
|||||||
RequiredCookieUser(user_id): RequiredCookieUser,
|
RequiredCookieUser(user_id): RequiredCookieUser,
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path(profile_user_uuid): Path<Uuid>,
|
Path(profile_user_uuid): Path<Uuid>,
|
||||||
Query(params): Query<crate::dtos::ErrorQuery>,
|
Query(params): Query<crate::forms::ErrorQuery>,
|
||||||
Extension(csrf): Extension<CsrfToken>,
|
Extension(csrf): Extension<CsrfToken>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
if user_id.value() != profile_user_uuid {
|
if user_id.value() != profile_user_uuid {
|
||||||
@@ -935,7 +933,7 @@ pub async fn get_movie_detail(
|
|||||||
OptionalCookieUser(user_id): OptionalCookieUser,
|
OptionalCookieUser(user_id): OptionalCookieUser,
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path(movie_id): Path<uuid::Uuid>,
|
Path(movie_id): Path<uuid::Uuid>,
|
||||||
Query(params): Query<crate::dtos::PaginationQueryParams>,
|
Query(params): Query<api_types::PaginationQueryParams>,
|
||||||
Extension(csrf): Extension<CsrfToken>,
|
Extension(csrf): Extension<CsrfToken>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let ctx = build_page_context(&state, user_id, csrf.0).await;
|
let ctx = build_page_context(&state, user_id, csrf.0).await;
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ use axum::{
|
|||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
response::{Html, IntoResponse, Redirect},
|
response::{Html, IntoResponse, Redirect},
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use api_types::{
|
||||||
|
ApplyMappingRequest, ConfirmRequest, SaveProfileRequest, SessionCreatedResponse,
|
||||||
|
SessionStateResponse,
|
||||||
|
};
|
||||||
|
use serde::Deserialize;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use application::{
|
use application::{
|
||||||
@@ -465,13 +469,6 @@ pub async fn get_import_done(
|
|||||||
|
|
||||||
// ── REST API handlers ──────────────────────────────────────────────────────
|
// ── REST API handlers ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
#[derive(Serialize, utoipa::ToSchema)]
|
|
||||||
pub struct SessionCreatedResponse {
|
|
||||||
pub session_id: String,
|
|
||||||
pub columns: Vec<String>,
|
|
||||||
pub sample_rows: Vec<Vec<String>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
post, path = "/api/v1/import/sessions",
|
post, path = "/api/v1/import/sessions",
|
||||||
request_body(content_type = "multipart/form-data", description = "file (binary) + format (csv|json|xlsx)"),
|
request_body(content_type = "multipart/form-data", description = "file (binary) + format (csv|json|xlsx)"),
|
||||||
@@ -544,14 +541,6 @@ pub async fn api_post_session(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, utoipa::ToSchema)]
|
|
||||||
pub struct SessionStateResponse {
|
|
||||||
pub session_id: String,
|
|
||||||
pub columns: Vec<String>,
|
|
||||||
pub has_mappings: bool,
|
|
||||||
pub row_count: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
get, path = "/api/v1/import/sessions/{id}",
|
get, path = "/api/v1/import/sessions/{id}",
|
||||||
params(("id" = String, Path, description = "Import session UUID")),
|
params(("id" = String, Path, description = "Import session UUID")),
|
||||||
@@ -607,23 +596,6 @@ pub async fn api_get_session(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, utoipa::ToSchema)]
|
|
||||||
pub struct ApiFieldMapping {
|
|
||||||
/// Column name in the source file
|
|
||||||
pub source_column: String,
|
|
||||||
/// Domain field: title | release_year | director | rating | watched_at | comment | external_metadata_id
|
|
||||||
pub domain_field: String,
|
|
||||||
/// For rating fields: multiply raw value by this factor (e.g. 0.5 for 10-point → 5-point scale)
|
|
||||||
pub rating_scale: Option<f64>,
|
|
||||||
/// For watched_at fields: strftime format hint (e.g. "%d/%m/%Y")
|
|
||||||
pub date_format: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, utoipa::ToSchema)]
|
|
||||||
pub struct ApplyMappingRequest {
|
|
||||||
pub mappings: Vec<ApiFieldMapping>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
put, path = "/api/v1/import/sessions/{id}/mapping",
|
put, path = "/api/v1/import/sessions/{id}/mapping",
|
||||||
params(("id" = String, Path, description = "Import session UUID")),
|
params(("id" = String, Path, description = "Import session UUID")),
|
||||||
@@ -692,12 +664,6 @@ pub async fn api_put_mapping(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, utoipa::ToSchema)]
|
|
||||||
pub struct ConfirmRequest {
|
|
||||||
/// Indices (0-based) of rows from the mapping preview to import
|
|
||||||
pub confirmed_indices: Vec<usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
post, path = "/api/v1/import/sessions/{id}/confirm",
|
post, path = "/api/v1/import/sessions/{id}/confirm",
|
||||||
params(("id" = String, Path, description = "Import session UUID")),
|
params(("id" = String, Path, description = "Import session UUID")),
|
||||||
@@ -776,14 +742,6 @@ pub async fn api_get_profiles(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, utoipa::ToSchema)]
|
|
||||||
pub struct SaveProfileRequest {
|
|
||||||
/// Session UUID whose current field_mappings to save
|
|
||||||
pub session_id: String,
|
|
||||||
/// Human-readable profile name (e.g. "Letterboxd")
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
post, path = "/api/v1/import/profiles",
|
post, path = "/api/v1/import/profiles",
|
||||||
request_body = SaveProfileRequest,
|
request_body = SaveProfileRequest,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
pub mod csrf;
|
pub mod csrf;
|
||||||
pub mod dtos;
|
pub mod forms;
|
||||||
pub mod errors;
|
pub mod errors;
|
||||||
pub mod extractors;
|
pub mod extractors;
|
||||||
pub mod handlers;
|
pub mod handlers;
|
||||||
|
|||||||
@@ -11,9 +11,7 @@ use importer::ImporterDocumentParser;
|
|||||||
use rss::RssAdapter;
|
use rss::RssAdapter;
|
||||||
use template_askama::AskamaHtmlRenderer;
|
use template_askama::AskamaHtmlRenderer;
|
||||||
|
|
||||||
use doc::ApiDocExt;
|
use presentation::{openapi, routes, state::AppState};
|
||||||
use presentation::{openapi::ApiDoc, routes, state::AppState};
|
|
||||||
use utoipa::OpenApi as _;
|
|
||||||
|
|
||||||
use domain::ports::{DiaryExporter, DocumentParser, EventPublisher, ImportProfileRepository, ImportSessionRepository};
|
use domain::ports::{DiaryExporter, DocumentParser, EventPublisher, ImportProfileRepository, ImportSessionRepository};
|
||||||
|
|
||||||
@@ -29,7 +27,7 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
.await
|
.await
|
||||||
.context("Failed to wire dependencies")?;
|
.context("Failed to wire dependencies")?;
|
||||||
|
|
||||||
let app = routes::build_router(state, ap_router).with_api_doc(ApiDoc::openapi());
|
let app = openapi::serve(routes::build_router(state, ap_router));
|
||||||
|
|
||||||
let host = std::env::var("HOST").unwrap_or_else(|_| "0.0.0.0".to_string());
|
let host = std::env::var("HOST").unwrap_or_else(|_| "0.0.0.0".to_string());
|
||||||
let port = std::env::var("PORT").unwrap_or_else(|_| "3000".to_string());
|
let port = std::env::var("PORT").unwrap_or_else(|_| "3000".to_string());
|
||||||
|
|||||||
@@ -1,188 +0,0 @@
|
|||||||
use utoipa::{
|
|
||||||
Modify, OpenApi,
|
|
||||||
openapi::security::{Http, HttpAuthScheme, SecurityScheme},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::dtos::{
|
|
||||||
ActivityFeedResponse, DiaryEntryDto, DiaryResponse,
|
|
||||||
DirectorStatDto, FeedEntryDto, LoginRequest, LoginResponse, LogReviewRequest,
|
|
||||||
MonthActivityDto, MonthlyRatingDto, MovieDetailResponse, MovieDto, MovieStatsDto,
|
|
||||||
ProfileResponse, RegisterRequest, ReviewDto, ReviewHistoryResponse, SocialFeedResponse,
|
|
||||||
SocialReviewDto, UserProfileResponse, UserStatsDto, UserSummaryDto, UserTrendsDto, UsersResponse,
|
|
||||||
};
|
|
||||||
use crate::handlers::import::{
|
|
||||||
ApiFieldMapping, ApplyMappingRequest, ConfirmRequest, SaveProfileRequest,
|
|
||||||
SessionCreatedResponse, SessionStateResponse,
|
|
||||||
};
|
|
||||||
#[cfg(feature = "federation")]
|
|
||||||
use crate::dtos::{
|
|
||||||
ActorListResponse, ActorUrlRequest, BlockedActorResponse, BlockedDomainResponse,
|
|
||||||
AddBlockedDomainRequest, FollowRequest, RemoteActorDto,
|
|
||||||
};
|
|
||||||
|
|
||||||
struct SecurityAddon;
|
|
||||||
|
|
||||||
impl Modify for SecurityAddon {
|
|
||||||
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
|
|
||||||
let components = openapi.components.get_or_insert_with(Default::default);
|
|
||||||
components.add_security_scheme(
|
|
||||||
"bearer_auth",
|
|
||||||
SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "federation"))]
|
|
||||||
#[derive(OpenApi)]
|
|
||||||
#[openapi(
|
|
||||||
info(
|
|
||||||
title = "Movies Diary API",
|
|
||||||
version = "1.0.0",
|
|
||||||
description = "REST API for Movies Diary. Authenticate with `POST /api/v1/auth/login` to get a Bearer token."
|
|
||||||
),
|
|
||||||
paths(
|
|
||||||
crate::handlers::api::get_diary,
|
|
||||||
crate::handlers::api::get_review_history,
|
|
||||||
crate::handlers::api::get_movie_detail,
|
|
||||||
crate::handlers::api::post_review,
|
|
||||||
crate::handlers::api::delete_review,
|
|
||||||
crate::handlers::api::sync_poster,
|
|
||||||
crate::handlers::api::login,
|
|
||||||
crate::handlers::api::register,
|
|
||||||
crate::handlers::api::export_diary,
|
|
||||||
crate::handlers::api::get_activity_feed,
|
|
||||||
crate::handlers::api::list_users,
|
|
||||||
crate::handlers::api::get_user_profile,
|
|
||||||
crate::handlers::import::api_post_session,
|
|
||||||
crate::handlers::import::api_get_session,
|
|
||||||
crate::handlers::import::api_put_mapping,
|
|
||||||
crate::handlers::import::api_post_confirm,
|
|
||||||
crate::handlers::import::api_get_profiles,
|
|
||||||
crate::handlers::import::api_post_profile,
|
|
||||||
crate::handlers::import::api_delete_profile,
|
|
||||||
crate::handlers::api::get_profile,
|
|
||||||
crate::handlers::api::update_profile_handler,
|
|
||||||
),
|
|
||||||
components(schemas(
|
|
||||||
DiaryResponse,
|
|
||||||
DiaryEntryDto,
|
|
||||||
MovieDto,
|
|
||||||
ReviewDto,
|
|
||||||
LogReviewRequest,
|
|
||||||
LoginRequest,
|
|
||||||
LoginResponse,
|
|
||||||
RegisterRequest,
|
|
||||||
ReviewHistoryResponse,
|
|
||||||
MovieDetailResponse,
|
|
||||||
MovieStatsDto,
|
|
||||||
SocialFeedResponse,
|
|
||||||
SocialReviewDto,
|
|
||||||
ActivityFeedResponse,
|
|
||||||
FeedEntryDto,
|
|
||||||
UsersResponse,
|
|
||||||
UserSummaryDto,
|
|
||||||
UserProfileResponse,
|
|
||||||
UserStatsDto,
|
|
||||||
MonthActivityDto,
|
|
||||||
MonthlyRatingDto,
|
|
||||||
DirectorStatDto,
|
|
||||||
UserTrendsDto,
|
|
||||||
ProfileResponse,
|
|
||||||
SessionCreatedResponse,
|
|
||||||
SessionStateResponse,
|
|
||||||
ApiFieldMapping,
|
|
||||||
ApplyMappingRequest,
|
|
||||||
ConfirmRequest,
|
|
||||||
SaveProfileRequest,
|
|
||||||
)),
|
|
||||||
modifiers(&SecurityAddon),
|
|
||||||
)]
|
|
||||||
pub struct ApiDoc;
|
|
||||||
|
|
||||||
#[cfg(feature = "federation")]
|
|
||||||
#[derive(OpenApi)]
|
|
||||||
#[openapi(
|
|
||||||
info(
|
|
||||||
title = "Movies Diary API",
|
|
||||||
version = "1.0.0",
|
|
||||||
description = "REST API for Movies Diary. Authenticate with `POST /api/v1/auth/login` to get a Bearer token."
|
|
||||||
),
|
|
||||||
paths(
|
|
||||||
crate::handlers::api::get_diary,
|
|
||||||
crate::handlers::api::get_review_history,
|
|
||||||
crate::handlers::api::get_movie_detail,
|
|
||||||
crate::handlers::api::post_review,
|
|
||||||
crate::handlers::api::delete_review,
|
|
||||||
crate::handlers::api::sync_poster,
|
|
||||||
crate::handlers::api::login,
|
|
||||||
crate::handlers::api::register,
|
|
||||||
crate::handlers::api::export_diary,
|
|
||||||
crate::handlers::api::get_activity_feed,
|
|
||||||
crate::handlers::api::list_users,
|
|
||||||
crate::handlers::api::get_user_profile,
|
|
||||||
crate::handlers::api::get_following,
|
|
||||||
crate::handlers::api::get_followers,
|
|
||||||
crate::handlers::api::get_pending_followers,
|
|
||||||
crate::handlers::api::follow,
|
|
||||||
crate::handlers::api::unfollow,
|
|
||||||
crate::handlers::api::accept_follower,
|
|
||||||
crate::handlers::api::reject_follower,
|
|
||||||
crate::handlers::api::remove_follower,
|
|
||||||
crate::handlers::api::get_profile,
|
|
||||||
crate::handlers::api::update_profile_handler,
|
|
||||||
crate::handlers::api::get_blocked_domains_admin,
|
|
||||||
crate::handlers::api::add_blocked_domain_admin,
|
|
||||||
crate::handlers::api::remove_blocked_domain_admin,
|
|
||||||
crate::handlers::api::block_actor_api,
|
|
||||||
crate::handlers::api::unblock_actor_api,
|
|
||||||
crate::handlers::api::get_blocked_actors_api,
|
|
||||||
crate::handlers::import::api_post_session,
|
|
||||||
crate::handlers::import::api_get_session,
|
|
||||||
crate::handlers::import::api_put_mapping,
|
|
||||||
crate::handlers::import::api_post_confirm,
|
|
||||||
crate::handlers::import::api_get_profiles,
|
|
||||||
crate::handlers::import::api_post_profile,
|
|
||||||
crate::handlers::import::api_delete_profile,
|
|
||||||
),
|
|
||||||
components(schemas(
|
|
||||||
DiaryResponse,
|
|
||||||
DiaryEntryDto,
|
|
||||||
MovieDto,
|
|
||||||
ReviewDto,
|
|
||||||
LogReviewRequest,
|
|
||||||
LoginRequest,
|
|
||||||
LoginResponse,
|
|
||||||
RegisterRequest,
|
|
||||||
ReviewHistoryResponse,
|
|
||||||
MovieDetailResponse,
|
|
||||||
MovieStatsDto,
|
|
||||||
SocialFeedResponse,
|
|
||||||
SocialReviewDto,
|
|
||||||
ActorListResponse,
|
|
||||||
RemoteActorDto,
|
|
||||||
FollowRequest,
|
|
||||||
ActorUrlRequest,
|
|
||||||
ProfileResponse,
|
|
||||||
BlockedDomainResponse,
|
|
||||||
AddBlockedDomainRequest,
|
|
||||||
BlockedActorResponse,
|
|
||||||
ActivityFeedResponse,
|
|
||||||
FeedEntryDto,
|
|
||||||
UsersResponse,
|
|
||||||
UserSummaryDto,
|
|
||||||
UserProfileResponse,
|
|
||||||
UserStatsDto,
|
|
||||||
MonthActivityDto,
|
|
||||||
MonthlyRatingDto,
|
|
||||||
DirectorStatDto,
|
|
||||||
UserTrendsDto,
|
|
||||||
SessionCreatedResponse,
|
|
||||||
SessionStateResponse,
|
|
||||||
ApiFieldMapping,
|
|
||||||
ApplyMappingRequest,
|
|
||||||
ConfirmRequest,
|
|
||||||
SaveProfileRequest,
|
|
||||||
)),
|
|
||||||
modifiers(&SecurityAddon),
|
|
||||||
)]
|
|
||||||
pub struct ApiDoc;
|
|
||||||
12
crates/presentation/src/openapi/auth.rs
Normal file
12
crates/presentation/src/openapi/auth.rs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
use api_types::{LoginRequest, LoginResponse, RegisterRequest};
|
||||||
|
use utoipa::OpenApi;
|
||||||
|
|
||||||
|
#[derive(OpenApi)]
|
||||||
|
#[openapi(
|
||||||
|
paths(
|
||||||
|
crate::handlers::api::login,
|
||||||
|
crate::handlers::api::register,
|
||||||
|
),
|
||||||
|
components(schemas(LoginRequest, LoginResponse, RegisterRequest)),
|
||||||
|
)]
|
||||||
|
pub struct AuthDoc;
|
||||||
22
crates/presentation/src/openapi/diary.rs
Normal file
22
crates/presentation/src/openapi/diary.rs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
use api_types::{ActivityFeedResponse, DiaryEntryDto, DiaryResponse, FeedEntryDto, LogReviewRequest, ReviewDto};
|
||||||
|
use utoipa::OpenApi;
|
||||||
|
|
||||||
|
#[derive(OpenApi)]
|
||||||
|
#[openapi(
|
||||||
|
paths(
|
||||||
|
crate::handlers::api::get_diary,
|
||||||
|
crate::handlers::api::post_review,
|
||||||
|
crate::handlers::api::delete_review,
|
||||||
|
crate::handlers::api::export_diary,
|
||||||
|
crate::handlers::api::get_activity_feed,
|
||||||
|
),
|
||||||
|
components(schemas(
|
||||||
|
DiaryResponse,
|
||||||
|
DiaryEntryDto,
|
||||||
|
ReviewDto,
|
||||||
|
LogReviewRequest,
|
||||||
|
ActivityFeedResponse,
|
||||||
|
FeedEntryDto,
|
||||||
|
)),
|
||||||
|
)]
|
||||||
|
pub struct DiaryDoc;
|
||||||
27
crates/presentation/src/openapi/import.rs
Normal file
27
crates/presentation/src/openapi/import.rs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
use api_types::{
|
||||||
|
ApiFieldMapping, ApplyMappingRequest, ConfirmRequest, SaveProfileRequest,
|
||||||
|
SessionCreatedResponse, SessionStateResponse,
|
||||||
|
};
|
||||||
|
use utoipa::OpenApi;
|
||||||
|
|
||||||
|
#[derive(OpenApi)]
|
||||||
|
#[openapi(
|
||||||
|
paths(
|
||||||
|
crate::handlers::import::api_post_session,
|
||||||
|
crate::handlers::import::api_get_session,
|
||||||
|
crate::handlers::import::api_put_mapping,
|
||||||
|
crate::handlers::import::api_post_confirm,
|
||||||
|
crate::handlers::import::api_get_profiles,
|
||||||
|
crate::handlers::import::api_post_profile,
|
||||||
|
crate::handlers::import::api_delete_profile,
|
||||||
|
),
|
||||||
|
components(schemas(
|
||||||
|
SessionCreatedResponse,
|
||||||
|
SessionStateResponse,
|
||||||
|
ApiFieldMapping,
|
||||||
|
ApplyMappingRequest,
|
||||||
|
ConfirmRequest,
|
||||||
|
SaveProfileRequest,
|
||||||
|
)),
|
||||||
|
)]
|
||||||
|
pub struct ImportDoc;
|
||||||
51
crates/presentation/src/openapi/mod.rs
Normal file
51
crates/presentation/src/openapi/mod.rs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
mod auth;
|
||||||
|
mod diary;
|
||||||
|
mod import;
|
||||||
|
mod movies;
|
||||||
|
mod social;
|
||||||
|
mod users;
|
||||||
|
|
||||||
|
use axum::Router;
|
||||||
|
use utoipa::{
|
||||||
|
Modify, OpenApi,
|
||||||
|
openapi::security::{Http, HttpAuthScheme, SecurityScheme},
|
||||||
|
};
|
||||||
|
use utoipa_scalar::{Scalar, Servable};
|
||||||
|
use utoipa_swagger_ui::SwaggerUi;
|
||||||
|
|
||||||
|
struct SecurityAddon;
|
||||||
|
|
||||||
|
impl Modify for SecurityAddon {
|
||||||
|
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
|
||||||
|
let components = openapi.components.get_or_insert_with(Default::default);
|
||||||
|
components.add_security_scheme(
|
||||||
|
"bearer_auth",
|
||||||
|
SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build() -> utoipa::openapi::OpenApi {
|
||||||
|
let mut api = auth::AuthDoc::openapi();
|
||||||
|
api.info = utoipa::openapi::InfoBuilder::new()
|
||||||
|
.title("Movies Diary API")
|
||||||
|
.version("1.0.0")
|
||||||
|
.description(Some("REST API for Movies Diary. Authenticate with `POST /api/v1/auth/login` to get a Bearer token."))
|
||||||
|
.build();
|
||||||
|
api.merge(diary::DiaryDoc::openapi());
|
||||||
|
api.merge(movies::MoviesDoc::openapi());
|
||||||
|
api.merge(users::UsersDoc::openapi());
|
||||||
|
api.merge(import::ImportDoc::openapi());
|
||||||
|
#[cfg(feature = "federation")]
|
||||||
|
api.merge(social::SocialDoc::openapi());
|
||||||
|
SecurityAddon.modify(&mut api);
|
||||||
|
api
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn serve(router: Router) -> Router {
|
||||||
|
tracing::info!("API docs at /docs (Swagger) and /scalar");
|
||||||
|
let spec = build();
|
||||||
|
router
|
||||||
|
.merge(SwaggerUi::new("/docs").url("/openapi.json", spec.clone()))
|
||||||
|
.merge(Scalar::with_url("/scalar", spec))
|
||||||
|
}
|
||||||
27
crates/presentation/src/openapi/movies.rs
Normal file
27
crates/presentation/src/openapi/movies.rs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
use api_types::{
|
||||||
|
DirectorStatDto, MonthActivityDto, MonthlyRatingDto, MovieDetailResponse, MovieDto,
|
||||||
|
MovieStatsDto, ReviewHistoryResponse, SocialFeedResponse, SocialReviewDto, UserTrendsDto,
|
||||||
|
};
|
||||||
|
use utoipa::OpenApi;
|
||||||
|
|
||||||
|
#[derive(OpenApi)]
|
||||||
|
#[openapi(
|
||||||
|
paths(
|
||||||
|
crate::handlers::api::get_movie_detail,
|
||||||
|
crate::handlers::api::get_review_history,
|
||||||
|
crate::handlers::api::sync_poster,
|
||||||
|
),
|
||||||
|
components(schemas(
|
||||||
|
MovieDto,
|
||||||
|
MovieDetailResponse,
|
||||||
|
MovieStatsDto,
|
||||||
|
ReviewHistoryResponse,
|
||||||
|
SocialFeedResponse,
|
||||||
|
SocialReviewDto,
|
||||||
|
MonthActivityDto,
|
||||||
|
MonthlyRatingDto,
|
||||||
|
DirectorStatDto,
|
||||||
|
UserTrendsDto,
|
||||||
|
)),
|
||||||
|
)]
|
||||||
|
pub struct MoviesDoc;
|
||||||
38
crates/presentation/src/openapi/social.rs
Normal file
38
crates/presentation/src/openapi/social.rs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
#[cfg(feature = "federation")]
|
||||||
|
use api_types::{
|
||||||
|
ActorListResponse, ActorUrlRequest, AddBlockedDomainRequest, BlockedActorResponse,
|
||||||
|
BlockedDomainResponse, FollowRequest, RemoteActorDto,
|
||||||
|
};
|
||||||
|
#[cfg(feature = "federation")]
|
||||||
|
use utoipa::OpenApi;
|
||||||
|
|
||||||
|
#[cfg(feature = "federation")]
|
||||||
|
#[derive(OpenApi)]
|
||||||
|
#[openapi(
|
||||||
|
paths(
|
||||||
|
crate::handlers::api::get_following,
|
||||||
|
crate::handlers::api::get_followers,
|
||||||
|
crate::handlers::api::get_pending_followers,
|
||||||
|
crate::handlers::api::follow,
|
||||||
|
crate::handlers::api::unfollow,
|
||||||
|
crate::handlers::api::accept_follower,
|
||||||
|
crate::handlers::api::reject_follower,
|
||||||
|
crate::handlers::api::remove_follower,
|
||||||
|
crate::handlers::api::get_blocked_domains_admin,
|
||||||
|
crate::handlers::api::add_blocked_domain_admin,
|
||||||
|
crate::handlers::api::remove_blocked_domain_admin,
|
||||||
|
crate::handlers::api::block_actor_api,
|
||||||
|
crate::handlers::api::unblock_actor_api,
|
||||||
|
crate::handlers::api::get_blocked_actors_api,
|
||||||
|
),
|
||||||
|
components(schemas(
|
||||||
|
ActorListResponse,
|
||||||
|
RemoteActorDto,
|
||||||
|
FollowRequest,
|
||||||
|
ActorUrlRequest,
|
||||||
|
BlockedDomainResponse,
|
||||||
|
AddBlockedDomainRequest,
|
||||||
|
BlockedActorResponse,
|
||||||
|
)),
|
||||||
|
)]
|
||||||
|
pub struct SocialDoc;
|
||||||
20
crates/presentation/src/openapi/users.rs
Normal file
20
crates/presentation/src/openapi/users.rs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
use api_types::{ProfileResponse, UserProfileResponse, UserStatsDto, UserSummaryDto, UsersResponse};
|
||||||
|
use utoipa::OpenApi;
|
||||||
|
|
||||||
|
#[derive(OpenApi)]
|
||||||
|
#[openapi(
|
||||||
|
paths(
|
||||||
|
crate::handlers::api::list_users,
|
||||||
|
crate::handlers::api::get_user_profile,
|
||||||
|
crate::handlers::api::get_profile,
|
||||||
|
crate::handlers::api::update_profile_handler,
|
||||||
|
),
|
||||||
|
components(schemas(
|
||||||
|
UsersResponse,
|
||||||
|
UserSummaryDto,
|
||||||
|
UserProfileResponse,
|
||||||
|
UserStatsDto,
|
||||||
|
ProfileResponse,
|
||||||
|
)),
|
||||||
|
)]
|
||||||
|
pub struct UsersDoc;
|
||||||
@@ -24,6 +24,7 @@ apple-native-keyring-store = { version = "1.0.0", optional = true, features = [
|
|||||||
zbus-secret-service-keyring-store = { version = "1.0.0", optional = true }
|
zbus-secret-service-keyring-store = { version = "1.0.0", optional = true }
|
||||||
windows-native-keyring-store = { version = "1.0.0", optional = true }
|
windows-native-keyring-store = { version = "1.0.0", optional = true }
|
||||||
|
|
||||||
|
api-types = { workspace = true }
|
||||||
reqwest = { workspace = true }
|
reqwest = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
@@ -32,5 +33,3 @@ anyhow = { workspace = true }
|
|||||||
uuid = { workspace = true }
|
uuid = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
tempfile = "3"
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::client::{DiaryEntryDto, LogReviewRequest, ReviewHistoryResponse};
|
use api_types::{DiaryEntryDto, LogReviewRequest, ReviewHistoryResponse};
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@@ -995,7 +995,7 @@ pub fn update(app: &mut App, action: Action) -> Vec<Command> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::client::{DiaryEntryDto, MovieDto, ReviewDto};
|
use api_types::{DiaryEntryDto, MovieDto, ReviewDto};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
fn setup_app() -> App {
|
fn setup_app() -> App {
|
||||||
@@ -1038,6 +1038,7 @@ mod tests {
|
|||||||
title: "The Matrix".into(),
|
title: "The Matrix".into(),
|
||||||
release_year: 1999,
|
release_year: 1999,
|
||||||
director: None,
|
director: None,
|
||||||
|
poster_path: None,
|
||||||
},
|
},
|
||||||
review: ReviewDto {
|
review: ReviewDto {
|
||||||
id: Uuid::new_v4(),
|
id: Uuid::new_v4(),
|
||||||
|
|||||||
@@ -1,92 +1,9 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use api_types::{
|
||||||
|
ActorListResponse, ActorUrlRequest, DiaryResponse, FollowRequest, LogReviewRequest,
|
||||||
|
LoginRequest, LoginResponse, ReviewHistoryResponse,
|
||||||
|
};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
// ── DTOs (mirror backend dtos.rs exactly) ────────────────────────────────────
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
pub struct LoginRequest {
|
|
||||||
pub email: String,
|
|
||||||
pub password: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct LoginResponse {
|
|
||||||
pub token: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct LogReviewRequest {
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub external_metadata_id: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub manual_title: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub manual_release_year: Option<u16>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub manual_director: Option<String>,
|
|
||||||
pub rating: u8,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub comment: Option<String>,
|
|
||||||
pub watched_at: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct DiaryResponse {
|
|
||||||
pub items: Vec<DiaryEntryDto>,
|
|
||||||
pub total_count: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct DiaryEntryDto {
|
|
||||||
pub movie: MovieDto,
|
|
||||||
pub review: ReviewDto,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct MovieDto {
|
|
||||||
pub id: Uuid,
|
|
||||||
pub title: String,
|
|
||||||
pub release_year: u16,
|
|
||||||
pub director: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct ReviewDto {
|
|
||||||
pub id: Uuid,
|
|
||||||
pub rating: u8,
|
|
||||||
pub comment: Option<String>,
|
|
||||||
pub watched_at: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct ReviewHistoryResponse {
|
|
||||||
pub movie: MovieDto,
|
|
||||||
pub viewings: Vec<ReviewDto>,
|
|
||||||
pub trend: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
pub struct FollowRequest {
|
|
||||||
pub handle: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
pub struct ActorUrlRequest {
|
|
||||||
pub actor_url: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct RemoteActorDto {
|
|
||||||
pub handle: String,
|
|
||||||
pub display_name: Option<String>,
|
|
||||||
pub url: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct ActorListResponse {
|
|
||||||
pub actors: Vec<RemoteActorDto>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Error ─────────────────────────────────────────────────────────────────────
|
// ── Error ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
|||||||
@@ -17,16 +17,9 @@ domain = { workspace = true }
|
|||||||
application = { workspace = true }
|
application = { workspace = true }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
|
||||||
chrono = { workspace = true }
|
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
tracing-subscriber = { workspace = true }
|
tracing-subscriber = { workspace = true }
|
||||||
futures = { workspace = true }
|
|
||||||
dotenvy = { workspace = true }
|
dotenvy = { workspace = true }
|
||||||
uuid = { workspace = true }
|
|
||||||
serde = { workspace = true }
|
|
||||||
serde_json = { workspace = true }
|
|
||||||
async-trait = { workspace = true }
|
|
||||||
auth = { workspace = true }
|
auth = { workspace = true }
|
||||||
metadata = { workspace = true }
|
metadata = { workspace = true }
|
||||||
poster-fetcher = { workspace = true }
|
poster-fetcher = { workspace = true }
|
||||||
|
|||||||
Reference in New Issue
Block a user