This commit is contained in:
parent
238e57e106
commit
74d5d58359
10 changed files with 105 additions and 21 deletions
8
Cargo.lock
generated
8
Cargo.lock
generated
|
@ -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",
|
||||
|
|
22
Cargo.toml
22
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,8 +32,13 @@ 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"] }
|
||||
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]
|
||||
|
|
|
@ -88,6 +88,22 @@ pub struct UpdateSiteData {
|
|||
pub default_collection: Option<Uuid>,
|
||||
}
|
||||
|
||||
/// 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<String>,
|
||||
|
||||
// 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<Vec<SiteShortInfo>, Error>;
|
||||
}
|
||||
|
|
|
@ -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<Vec<SiteShortInfo>, 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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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<Repo: CollectionRepository>(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<Repo: CollectionRepository>(repository: Re
|
|||
pub(super) async fn list_posts_in_collection<Repo: SiteRepository + PostRepository>(
|
||||
repository: Repo,
|
||||
Path((site, id)): Path<(String, Uuid)>,
|
||||
query: Query<PaginationQuery>,
|
||||
query: Query<TimePaginationQuery>,
|
||||
) -> Result<impl IntoResponse, ApiError<'static>> {
|
||||
// Resolve site
|
||||
let (site_id, _) = repository.get_site_id_and_owner(&site).await?;
|
||||
|
|
|
@ -14,6 +14,7 @@ mod openapi;
|
|||
mod pagination;
|
||||
mod posts;
|
||||
mod sites;
|
||||
mod users;
|
||||
|
||||
pub fn create_router() -> Router<Arc<AppState>> {
|
||||
Router::new()
|
||||
|
@ -22,5 +23,6 @@ pub fn create_router() -> Router<Arc<AppState>> {
|
|||
.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()))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
),
|
||||
|
|
|
@ -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<NaiveDateTime>,
|
||||
|
||||
|
|
33
src/routes/users.rs
Normal file
33
src/routes/users.rs
Normal file
|
@ -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<Repo: SiteRepository>(
|
||||
repository: Repo,
|
||||
RequireUser(user): RequireUser,
|
||||
) -> Result<impl IntoResponse, ApiError<'static>> {
|
||||
let results = repository.get_sites_by_owner(&user.id).await?;
|
||||
Ok(Json(results))
|
||||
}
|
||||
|
||||
pub(super) fn router() -> Router<Arc<AppState>> {
|
||||
Router::new().route("/@current/sites", get(get_user_sites::<Database>))
|
||||
}
|
Loading…
Reference in a new issue