Add user routes
continuous-integration/drone/push Build is failing Details

This commit is contained in:
Hamcha 2023-07-30 13:53:07 +02:00
parent 238e57e106
commit 74d5d58359
Signed by: hamcha
GPG Key ID: 1669C533B8CF6D89
10 changed files with 105 additions and 21 deletions

8
Cargo.lock generated
View File

@ -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",

View File

@ -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
opt-level = 3

View File

@ -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>;
}

View File

@ -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)
}
}

View File

@ -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();

View File

@ -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?;

View File

@ -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()))
}

View File

@ -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
),

View File

@ -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
View 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>))
}