diff --git a/Cargo.lock b/Cargo.lock index db65606..d68a3f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -373,12 +373,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "dotenv_rs" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "828401259441c2fefb04b5af2a5762cac529e17f76b87e8034b9987e541d4397" - [[package]] name = "dotenvy" version = "0.15.7" @@ -819,7 +813,7 @@ dependencies = [ "axum-macros", "chrono", "cookie", - "dotenv_rs", + "dotenvy", "figment", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 3353758..7821cd2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,8 +10,17 @@ axum-macros = "0.3" cookie = "0.17" tracing = "0.1" tracing-subscriber = "0.3" -tokio = { version = "1.28", features = ["full"] } -sqlx = { version = "0.6", features = [ "runtime-tokio-rustls", "postgres", "uuid", "chrono", "macros", "migrate", "json", "offline"] } +tokio = { version = "1", features = ["full"] } +sqlx = { version = "0.6", features = [ + "runtime-tokio-rustls", + "postgres", + "uuid", + "chrono", + "macros", + "migrate", + "json", + "offline", +] } uuid = { version = "1.4", features = ["v7", "serde", "std"] } serde = { version = "1" } serde_json = { version = "1", features = ["raw_value"] } @@ -23,9 +32,14 @@ url = "2.4" thiserror = "1.0" async-trait = "0.1" tower-http = { version = "0.4", features = ["cors"] } -dotenv_rs = "0.16" -utoipa = { version = "3", features = ["axum_extras", "uuid", "chrono", "preserve_order"] } -utoipa-swagger-ui = { version = "3", features = ["axum"]} +dotenvy = "0.15" +utoipa = { version = "3", features = [ + "axum_extras", + "uuid", + "chrono", + "preserve_order", +] } +utoipa-swagger-ui = { version = "3", features = ["axum"] } [profile.dev.package.sqlx-macros] -opt-level = 3 \ No newline at end of file +opt-level = 3 diff --git a/src/content/site.rs b/src/content/site.rs index de3a25f..c1ee1a1 100644 --- a/src/content/site.rs +++ b/src/content/site.rs @@ -88,6 +88,22 @@ pub struct UpdateSiteData { pub default_collection: Option, } +/// Data required to show a summary of the website for lists +#[derive(Deserialize, Serialize, ToSchema)] +pub struct SiteShortInfo { + /// Site name (unique per instance, shows up in URLs) + pub name: String, + + /// Site's displayed name + pub title: String, + + /// Site description (like a user's bio) + pub description: Option, + + // Creation time + pub created_at: NaiveDateTime, +} + #[async_trait] pub trait SiteRepository { /// Retrieve site info from site name/slug @@ -109,4 +125,7 @@ pub trait SiteRepository { /// Resolve a site's name to its ID async fn get_site_id_and_owner(&self, name: &str) -> Result<(Uuid, Uuid), Error>; + + /// Get a list of all sites owned by a user + async fn get_sites_by_owner(&self, owner: &Uuid) -> Result, Error>; } diff --git a/src/database/site.rs b/src/database/site.rs index 42b641c..d577e8a 100644 --- a/src/database/site.rs +++ b/src/database/site.rs @@ -4,7 +4,7 @@ use uuid::Uuid; use crate::content::{ collection::CollectionData, - site::{CreateSiteData, SiteData, SiteRepository, UpdateSiteData}, + site::{CreateSiteData, SiteData, SiteRepository, SiteShortInfo, UpdateSiteData}, Error, }; @@ -170,4 +170,25 @@ impl SiteRepository for Database { })?; Ok((record.id, record.owner)) } + + async fn get_sites_by_owner(&self, owner: &Uuid) -> Result, Error> { + let sites = sqlx::query_as!( + SiteShortInfo, + r#"SELECT + name, title, description, created_at + FROM + sites + WHERE + owner = $1 + "#, + owner, + ) + .fetch_all(&self.pool) + .await + .map_err(|e| match e { + sqlx::Error::RowNotFound => Error::NotFound, + _ => e.into(), + })?; + Ok(sites) + } } diff --git a/src/main.rs b/src/main.rs index d38bce5..33c6474 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,7 +16,6 @@ use axum::{ }, middleware, Server, }; -use dotenv_rs::dotenv; use figment::{ providers::{Env, Format, Serialized, Toml}, Figment, @@ -28,7 +27,7 @@ use tower_http::cors::{AllowOrigin, CorsLayer}; #[tokio::main] async fn main() -> Result<()> { - dotenv().ok(); + dotenvy::dotenv()?; tracing_subscriber::fmt::init(); diff --git a/src/routes/collections.rs b/src/routes/collections.rs index 5c3b173..1e4fa2c 100644 --- a/src/routes/collections.rs +++ b/src/routes/collections.rs @@ -14,7 +14,7 @@ use crate::{ state::AppState, }; -use super::pagination::PaginationQuery; +use super::pagination::TimePaginationQuery; const DEFAULT_PAGE_SIZE: i64 = 10; @@ -61,7 +61,7 @@ pub(super) async fn delete_collection(repository: Re params( ("site" = String, Path, description = "Site slug"), ("id" = Uuid, Path, description = "Collection ID"), - PaginationQuery + TimePaginationQuery ), responses( (status = 200, description = "List of collections fetched", body = [CollectionData]) @@ -70,7 +70,7 @@ pub(super) async fn delete_collection(repository: Re pub(super) async fn list_posts_in_collection( repository: Repo, Path((site, id)): Path<(String, Uuid)>, - query: Query, + query: Query, ) -> Result> { // Resolve site let (site_id, _) = repository.get_site_id_and_owner(&site).await?; diff --git a/src/routes/mod.rs b/src/routes/mod.rs index f33e5a0..62d414b 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -14,6 +14,7 @@ mod openapi; mod pagination; mod posts; mod sites; +mod users; pub fn create_router() -> Router> { Router::new() @@ -22,5 +23,6 @@ pub fn create_router() -> Router> { .nest("/posts", posts::router()) .nest("/sites", sites::router()) .nest("/collections", collections::router()) + .nest("/users", users::router()) .merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", ApiDoc::openapi())) } diff --git a/src/routes/openapi.rs b/src/routes/openapi.rs index b6b0f3a..9e4dd05 100644 --- a/src/routes/openapi.rs +++ b/src/routes/openapi.rs @@ -11,7 +11,7 @@ use crate::content::{ use super::{ admin::{self, BootstrapInfo}, - auth, collections, posts, sites, + auth, collections, posts, sites, users, }; #[derive(OpenApi)] @@ -35,6 +35,8 @@ use super::{ // Collection routes collections::list_collections_for_site, collections::list_posts_in_collection, + // User routes + users::get_user_sites, // Admin routes admin::bootstrap ), diff --git a/src/routes/pagination.rs b/src/routes/pagination.rs index 61bdaa3..d3da042 100644 --- a/src/routes/pagination.rs +++ b/src/routes/pagination.rs @@ -3,7 +3,7 @@ use serde::Deserialize; use utoipa::IntoParams; #[derive(Deserialize, Default, IntoParams)] -pub(super) struct PaginationQuery { +pub(super) struct TimePaginationQuery { /// Cursor position (date-based) pub before: Option, diff --git a/src/routes/users.rs b/src/routes/users.rs new file mode 100644 index 0000000..3600b9c --- /dev/null +++ b/src/routes/users.rs @@ -0,0 +1,33 @@ +use std::sync::Arc; + +use axum::{response::IntoResponse, routing::get, Json, Router}; + +use crate::{ + content::site::SiteRepository, + database::Database, + http::{error::ApiError, session::RequireUser}, + state::AppState, +}; + +/// List sites owned by user +#[utoipa::path( + get, + path = "/users/{name}/sites", + params( + ("name" = String, Path, description = "User name") + ), + responses( + (status = 200, description = "List of sites fetched", body = [SiteShortInfo]) + ) +)] +pub(super) async fn get_user_sites( + repository: Repo, + RequireUser(user): RequireUser, +) -> Result> { + let results = repository.get_sites_by_owner(&user.id).await?; + Ok(Json(results)) +} + +pub(super) fn router() -> Router> { + Router::new().route("/@current/sites", get(get_user_sites::)) +}