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

View File

@ -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,9 +32,14 @@ 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 = [
utoipa-swagger-ui = { version = "3", features = ["axum"]} "axum_extras",
"uuid",
"chrono",
"preserve_order",
] }
utoipa-swagger-ui = { version = "3", features = ["axum"] }
[profile.dev.package.sqlx-macros] [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>, 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>;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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