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",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dotenv_rs"
|
|
||||||
version = "0.16.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "828401259441c2fefb04b5af2a5762cac529e17f76b87e8034b9987e541d4397"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dotenvy"
|
name = "dotenvy"
|
||||||
version = "0.15.7"
|
version = "0.15.7"
|
||||||
|
@ -819,7 +813,7 @@ dependencies = [
|
||||||
"axum-macros",
|
"axum-macros",
|
||||||
"chrono",
|
"chrono",
|
||||||
"cookie",
|
"cookie",
|
||||||
"dotenv_rs",
|
"dotenvy",
|
||||||
"figment",
|
"figment",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|
22
Cargo.toml
22
Cargo.toml
|
@ -10,8 +10,17 @@ axum-macros = "0.3"
|
||||||
cookie = "0.17"
|
cookie = "0.17"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = "0.3"
|
tracing-subscriber = "0.3"
|
||||||
tokio = { version = "1.28", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
sqlx = { version = "0.6", features = [ "runtime-tokio-rustls", "postgres", "uuid", "chrono", "macros", "migrate", "json", "offline"] }
|
sqlx = { version = "0.6", features = [
|
||||||
|
"runtime-tokio-rustls",
|
||||||
|
"postgres",
|
||||||
|
"uuid",
|
||||||
|
"chrono",
|
||||||
|
"macros",
|
||||||
|
"migrate",
|
||||||
|
"json",
|
||||||
|
"offline",
|
||||||
|
] }
|
||||||
uuid = { version = "1.4", features = ["v7", "serde", "std"] }
|
uuid = { version = "1.4", features = ["v7", "serde", "std"] }
|
||||||
serde = { version = "1" }
|
serde = { version = "1" }
|
||||||
serde_json = { version = "1", features = ["raw_value"] }
|
serde_json = { version = "1", features = ["raw_value"] }
|
||||||
|
@ -23,8 +32,13 @@ url = "2.4"
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
tower-http = { version = "0.4", features = ["cors"] }
|
tower-http = { version = "0.4", features = ["cors"] }
|
||||||
dotenv_rs = "0.16"
|
dotenvy = "0.15"
|
||||||
utoipa = { version = "3", features = ["axum_extras", "uuid", "chrono", "preserve_order"] }
|
utoipa = { version = "3", features = [
|
||||||
|
"axum_extras",
|
||||||
|
"uuid",
|
||||||
|
"chrono",
|
||||||
|
"preserve_order",
|
||||||
|
] }
|
||||||
utoipa-swagger-ui = { version = "3", features = ["axum"] }
|
utoipa-swagger-ui = { version = "3", features = ["axum"] }
|
||||||
|
|
||||||
[profile.dev.package.sqlx-macros]
|
[profile.dev.package.sqlx-macros]
|
||||||
|
|
|
@ -88,6 +88,22 @@ pub struct UpdateSiteData {
|
||||||
pub default_collection: Option<Uuid>,
|
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]
|
#[async_trait]
|
||||||
pub trait SiteRepository {
|
pub trait SiteRepository {
|
||||||
/// Retrieve site info from site name/slug
|
/// Retrieve site info from site name/slug
|
||||||
|
@ -109,4 +125,7 @@ pub trait SiteRepository {
|
||||||
|
|
||||||
/// Resolve a site's name to its ID
|
/// Resolve a site's name to its ID
|
||||||
async fn get_site_id_and_owner(&self, name: &str) -> Result<(Uuid, Uuid), Error>;
|
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::{
|
use crate::content::{
|
||||||
collection::CollectionData,
|
collection::CollectionData,
|
||||||
site::{CreateSiteData, SiteData, SiteRepository, UpdateSiteData},
|
site::{CreateSiteData, SiteData, SiteRepository, SiteShortInfo, UpdateSiteData},
|
||||||
Error,
|
Error,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -170,4 +170,25 @@ impl SiteRepository for Database {
|
||||||
})?;
|
})?;
|
||||||
Ok((record.id, record.owner))
|
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,
|
middleware, Server,
|
||||||
};
|
};
|
||||||
use dotenv_rs::dotenv;
|
|
||||||
use figment::{
|
use figment::{
|
||||||
providers::{Env, Format, Serialized, Toml},
|
providers::{Env, Format, Serialized, Toml},
|
||||||
Figment,
|
Figment,
|
||||||
|
@ -28,7 +27,7 @@ use tower_http::cors::{AllowOrigin, CorsLayer};
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
dotenv().ok();
|
dotenvy::dotenv()?;
|
||||||
|
|
||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ use crate::{
|
||||||
state::AppState,
|
state::AppState,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::pagination::PaginationQuery;
|
use super::pagination::TimePaginationQuery;
|
||||||
|
|
||||||
const DEFAULT_PAGE_SIZE: i64 = 10;
|
const DEFAULT_PAGE_SIZE: i64 = 10;
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ pub(super) async fn delete_collection<Repo: CollectionRepository>(repository: Re
|
||||||
params(
|
params(
|
||||||
("site" = String, Path, description = "Site slug"),
|
("site" = String, Path, description = "Site slug"),
|
||||||
("id" = Uuid, Path, description = "Collection ID"),
|
("id" = Uuid, Path, description = "Collection ID"),
|
||||||
PaginationQuery
|
TimePaginationQuery
|
||||||
),
|
),
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "List of collections fetched", body = [CollectionData])
|
(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>(
|
pub(super) async fn list_posts_in_collection<Repo: SiteRepository + PostRepository>(
|
||||||
repository: Repo,
|
repository: Repo,
|
||||||
Path((site, id)): Path<(String, Uuid)>,
|
Path((site, id)): Path<(String, Uuid)>,
|
||||||
query: Query<PaginationQuery>,
|
query: Query<TimePaginationQuery>,
|
||||||
) -> Result<impl IntoResponse, ApiError<'static>> {
|
) -> Result<impl IntoResponse, ApiError<'static>> {
|
||||||
// Resolve site
|
// Resolve site
|
||||||
let (site_id, _) = repository.get_site_id_and_owner(&site).await?;
|
let (site_id, _) = repository.get_site_id_and_owner(&site).await?;
|
||||||
|
|
|
@ -14,6 +14,7 @@ mod openapi;
|
||||||
mod pagination;
|
mod pagination;
|
||||||
mod posts;
|
mod posts;
|
||||||
mod sites;
|
mod sites;
|
||||||
|
mod users;
|
||||||
|
|
||||||
pub fn create_router() -> Router<Arc<AppState>> {
|
pub fn create_router() -> Router<Arc<AppState>> {
|
||||||
Router::new()
|
Router::new()
|
||||||
|
@ -22,5 +23,6 @@ pub fn create_router() -> Router<Arc<AppState>> {
|
||||||
.nest("/posts", posts::router())
|
.nest("/posts", posts::router())
|
||||||
.nest("/sites", sites::router())
|
.nest("/sites", sites::router())
|
||||||
.nest("/collections", collections::router())
|
.nest("/collections", collections::router())
|
||||||
|
.nest("/users", users::router())
|
||||||
.merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", ApiDoc::openapi()))
|
.merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", ApiDoc::openapi()))
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ use crate::content::{
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
admin::{self, BootstrapInfo},
|
admin::{self, BootstrapInfo},
|
||||||
auth, collections, posts, sites,
|
auth, collections, posts, sites, users,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(OpenApi)]
|
#[derive(OpenApi)]
|
||||||
|
@ -35,6 +35,8 @@ use super::{
|
||||||
// Collection routes
|
// Collection routes
|
||||||
collections::list_collections_for_site,
|
collections::list_collections_for_site,
|
||||||
collections::list_posts_in_collection,
|
collections::list_posts_in_collection,
|
||||||
|
// User routes
|
||||||
|
users::get_user_sites,
|
||||||
// Admin routes
|
// Admin routes
|
||||||
admin::bootstrap
|
admin::bootstrap
|
||||||
),
|
),
|
||||||
|
|
|
@ -3,7 +3,7 @@ use serde::Deserialize;
|
||||||
use utoipa::IntoParams;
|
use utoipa::IntoParams;
|
||||||
|
|
||||||
#[derive(Deserialize, Default, IntoParams)]
|
#[derive(Deserialize, Default, IntoParams)]
|
||||||
pub(super) struct PaginationQuery {
|
pub(super) struct TimePaginationQuery {
|
||||||
/// Cursor position (date-based)
|
/// Cursor position (date-based)
|
||||||
pub before: Option<NaiveDateTime>,
|
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