diff --git a/crates/artifact/src/lib.rs b/crates/artifact/src/lib.rs index 99b5627..64ef5ff 100644 --- a/crates/artifact/src/lib.rs +++ b/crates/artifact/src/lib.rs @@ -1,3 +1,26 @@ +#![warn(missing_docs)] + +//! # Artifact +//! +//! `artifact` is a library for managing artifacts. It uses the +//! [`PrivateArtifact`] and [`PublicArtifact`] types from [`core_types`] and +//! implements its [`Artifact`] trait on them, which has convenient methods for +//! pulling/pushing to and from SurrealDB, and methods for downloading/uploading +//! to and from the object store. +//! +//! This crate is built with vendor-agnosticism in mind, but currently does not +//! implement it. Right now we only use AWS' S3 service. As such, the library +//! requires environment variables to configure access. It expects the +//! following: +//! - `AWS_ACCESS_KEY_ID`: The access key ID for the AWS account +//! - `AWS_SECRET_ACCESS_KEY`: The secret access key for the AWS account +//! - `AWS_DEFAULT_REGION`: The region that the buckets exist in. +//! +//! We use one private bucket and one public bucket. The private bucket is only +//! accessible to the root account, and the public bucket is completely public. +//! We have separate types (and tables) to enforce the publicity state of our +//! artifacts with the type system. + use std::future::Future; use color_eyre::eyre::{OptionExt, Result, WrapErr}; @@ -6,20 +29,35 @@ use core_types::{NewId, PrivateArtifact, PublicArtifact}; const ARTIFACT_PRIVATE_LTS_BUCKET: &str = "picturepro-artifact-private-lts"; const ARTIFACT_PUBLIC_LTS_BUCKET: &str = "picturepro-artifact-public-lts"; +/// The core artifact trait. pub trait Artifact { + /// The type of the ID of the artifact. type Id: core_types::NewId; + /// Create a new artifact with the given contents. fn new(contents: Option) -> Self; + /// Create a new artifact with the given ID and contents. fn new_with_id(id: Self::Id, contents: Option) -> Self; + /// Download the artifact from the object store. + /// + /// The data is stored in the `contents` field of the artifact. fn download(&mut self) -> impl Future> + Send; + /// Upload the artifact to the object store. + /// + /// The data is taken from the `contents` field of the artifact. The method + /// fails if the `contents` field is `None`. fn upload(&self) -> impl Future> + Send; + /// Convenience method for uploading and pushing to SurrealDB. fn upload_and_push(&self) -> impl Future> + Send; + /// Push the artifact to SurrealDB. fn push_to_surreal(&self) -> impl Future> + Send; + /// Pull an artifact from SurrealDB. fn pull_from_surreal( id: Self::Id, ) -> impl Future>> + Send; + /// Get the object store for the artifact. fn object_store(&self) -> Result>; } @@ -33,7 +71,7 @@ impl Artifact for PublicArtifact { fn new_with_id(id: Self::Id, contents: Option) -> Self { Self { - id: id.clone(), + id, contents, url: format!( "https://s3.{}.amazonaws.com/{}/{}", diff --git a/crates/auth/src/lib.rs b/crates/auth/src/lib.rs index 2f73188..3119865 100644 --- a/crates/auth/src/lib.rs +++ b/crates/auth/src/lib.rs @@ -1,3 +1,8 @@ +#![warn(missing_docs)] + +//! This crate implements [`axum_login`] for picturepro types, using a SurrealDB +//! backend. + use axum_login::{ AuthManagerLayer, AuthManagerLayerBuilder, AuthnBackend, UserId, }; @@ -6,24 +11,39 @@ use core_types::NewId; use serde::{Deserialize, Serialize}; use surrealdb::engine::remote::ws::Client; +/// The credentials type for the authentication layer. +/// +/// This type will be transformed into an enum when we implement additional +/// authentication methods. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Credentials { + /// The email address of the user. pub email: String, + /// The password of the user. pub password: String, } +/// The backend type for the authentication layer. +/// +/// This type implements the [`AuthnBackend`] trait for the picturepro types, +/// and has a [`signup`](Backend::signup) method for creating new users. #[derive(Clone, Debug)] pub struct Backend { surreal_client: clients::surreal::SurrealRootClient, } impl Backend { + /// Create a new backend instance. pub async fn new() -> color_eyre::Result { Ok(Self { surreal_client: clients::surreal::SurrealRootClient::new().await?, }) } + /// Create a new user. + /// + /// This method has checks to ensure that a user with the given email does + /// not already exist. pub async fn signup( &self, name: String, @@ -109,6 +129,12 @@ impl AuthnBackend for Backend { } } +/// The authentication session type. +/// +/// This is an alias for the [`axum_login::AuthSession`] type with our backend +/// type. We can pull this type out of the axum router after we've added the +/// auth layer, and it's generally all we need to read at runtime for auth +/// state. pub type AuthSession = axum_login::AuthSession; /// Builds an authentication layer for use with an Axum router. diff --git a/crates/bl/src/lib.rs b/crates/bl/src/lib.rs index e6487bc..976f86b 100644 --- a/crates/bl/src/lib.rs +++ b/crates/bl/src/lib.rs @@ -3,10 +3,9 @@ use bytes::Bytes; use clients::surreal::SurrealRootClient; use color_eyre::eyre::{Context, Result}; use core_types::{ - AsThing, NewId, Photo, PhotoArtifacts, PhotoGroup, PhotoGroupUploadMeta, + NewId, Photo, PhotoArtifacts, PhotoGroup, PhotoGroupUploadMeta, PrivateArtifact, PublicArtifact, }; -use image::ImageEncoder; use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Deserialize, Serialize, thiserror::Error)] @@ -79,8 +78,8 @@ pub async fn upload_single_photo( // create a photo and upload it to surreal let photo = Photo { id: core_types::PhotoRecordId(ulid::Ulid::new()), - photographer: user_id.clone(), - owner: user_id.clone(), + photographer: user_id, + owner: user_id, artifacts: PhotoArtifacts { original: core_types::PrivateImageArtifact { artifact_id: original_artifact.id, @@ -119,8 +118,8 @@ pub async fn upload_single_photo( // create a photo group and upload it to surreal let group = PhotoGroup { id: core_types::PhotoGroupRecordId(ulid::Ulid::new()), - owner: user_id.clone(), - photos: vec![photo.id.clone()], + owner: user_id, + photos: vec![photo.id], public: group_meta.public, }; diff --git a/crates/core_types/src/artifact.rs b/crates/core_types/src/artifact.rs index 3824d0d..14b3a38 100644 --- a/crates/core_types/src/artifact.rs +++ b/crates/core_types/src/artifact.rs @@ -1,53 +1,38 @@ use serde::{Deserialize, Serialize}; +/// The table name for the private artifact table. pub const PRIVATE_ARTIFACT_TABLE: &str = "private_artifact"; +/// The table name for the public artifact table. pub const PUBLIC_ARTIFACT_TABLE: &str = "public_artifact"; +/// The record ID for a private artifact. #[derive(Clone, Debug, Deserialize, Serialize, Copy)] -#[cfg_attr(feature = "ssr", serde(from = "crate::conv::UlidOrThing"))] +#[cfg_attr(feature = "ssr", serde(from = "crate::ssr::UlidOrThing"))] pub struct PrivateArtifactRecordId(pub ulid::Ulid); +/// A private artifact. #[derive(Debug, Clone, Deserialize, Serialize)] pub struct PrivateArtifact { + /// The record ID. pub id: PrivateArtifactRecordId, + /// The contents of the artifact (skipped by serde) #[serde(skip)] pub contents: Option, } +/// The record ID for a public artifact. #[derive(Clone, Debug, Deserialize, Serialize, Copy)] -#[cfg_attr(feature = "ssr", serde(from = "crate::conv::UlidOrThing"))] +#[cfg_attr(feature = "ssr", serde(from = "crate::ssr::UlidOrThing"))] pub struct PublicArtifactRecordId(pub ulid::Ulid); +/// A public artifact. #[derive(Debug, Clone, Deserialize, Serialize)] pub struct PublicArtifact { + /// The record ID. pub id: PublicArtifactRecordId, + /// The public URL to the artifact. pub url: String, #[serde(skip)] + /// The contents of the artifact (skipped by serde) pub contents: Option, } - -#[cfg(feature = "ssr")] -mod ssr { - use surreal_id::NewId; - use surrealdb::sql::Id; - - use super::*; - - impl NewId for PrivateArtifactRecordId { - const TABLE: &'static str = PRIVATE_ARTIFACT_TABLE; - - fn from_inner_id>(inner_id: T) -> Self { - Self(inner_id.into().to_string().parse().unwrap()) - } - fn get_inner_string(&self) -> String { self.0.to_string() } - } - - impl NewId for PublicArtifactRecordId { - const TABLE: &'static str = PUBLIC_ARTIFACT_TABLE; - - fn from_inner_id>(inner_id: T) -> Self { - Self(inner_id.into().to_string().parse().unwrap()) - } - fn get_inner_string(&self) -> String { self.0.to_string() } - } -} diff --git a/crates/core_types/src/auth.rs b/crates/core_types/src/auth.rs index ee73cc0..cf842a1 100644 --- a/crates/core_types/src/auth.rs +++ b/crates/core_types/src/auth.rs @@ -1,26 +1,48 @@ use serde::{Deserialize, Serialize}; +/// The table name for the user table. pub const USER_TABLE: &str = "user"; +/// The record ID for a user. #[derive(Serialize, Deserialize, Debug, Clone, Copy)] -#[cfg_attr(feature = "ssr", serde(from = "crate::conv::UlidOrThing"))] +#[cfg_attr(feature = "ssr", serde(from = "crate::ssr::UlidOrThing"))] pub struct UserRecordId(pub ulid::Ulid); +/// A user. +/// +/// This is the full user record, including the password hash. It's gated behind +/// the `ssr` feature because we don't want to send the password hash to the +/// client. If you need to send user data to the client, use [`PublicUser`] +/// instead. +#[cfg(feature = "ssr")] #[derive(Serialize, Deserialize, Debug, Clone)] pub struct User { + /// The record ID. pub id: UserRecordId, + /// The user's name. pub name: String, + /// The user's email. pub email: String, + /// The user's password hash. pub pw_hash: String, } +/// A user, with the password hash removed. +/// +/// This is in the `core_types` crate not because it's a DB model, but because +/// it's common to multiple crates in picturepro. It's used in place of the +/// [`User`] type when we want to send user data to the client. #[derive(Serialize, Deserialize, Debug, Clone)] pub struct PublicUser { + /// The record ID. pub id: UserRecordId, + /// The user's name. pub name: String, + /// The user's email. pub email: String, } +#[cfg(feature = "ssr")] impl From for PublicUser { fn from(u: User) -> PublicUser { PublicUser { @@ -31,28 +53,18 @@ impl From for PublicUser { } } +/// A logged-in user. +/// +/// This is the type provided to the leptos context that's used whenever we need +/// to fetch the user. We don't use the full `AuthSession` type from the `auth` +/// crate because we need this context type to be isomorphic, and `AuthSession` +/// depends on the `axum_login` crate, which would leak all sorts of +/// dependencies into the client bundle. #[derive(Clone, Debug)] pub struct LoggedInUser(pub Option); -#[cfg(feature = "ssr")] -mod ssr { - use surreal_id::NewId; - use surrealdb::sql::Id; - - use super::*; - - impl NewId for UserRecordId { - const TABLE: &'static str = USER_TABLE; - - fn from_inner_id>(inner_id: T) -> Self { - Self(inner_id.into().to_string().parse().unwrap()) - } - fn get_inner_string(&self) -> String { self.0.to_string() } - } -} - #[cfg(feature = "auth")] -mod auth { +mod auth_traits { use axum_login::AuthUser; use super::*; diff --git a/crates/core_types/src/conv.rs b/crates/core_types/src/conv.rs deleted file mode 100644 index ec88aba..0000000 --- a/crates/core_types/src/conv.rs +++ /dev/null @@ -1,84 +0,0 @@ -use serde::Deserialize; -use surreal_id::NewId; -use surrealdb::{ - opt::{IntoResource, Resource}, - sql::{Id, Thing}, -}; - -use crate::{ - PhotoGroupRecordId, PhotoRecordId, PrivateArtifactRecordId, - PublicArtifactRecordId, UserRecordId, -}; - -#[derive(Deserialize, Debug, Clone)] -#[serde(untagged)] -pub enum UlidOrThing { - Ulid(ulid::Ulid), - Thing(Thing), -} - -impl From for ulid::Ulid { - fn from(u: UlidOrThing) -> ulid::Ulid { - match u { - UlidOrThing::Ulid(u) => u, - UlidOrThing::Thing(t) => t.id.to_string().parse().unwrap(), - } - } -} - -impl From for UserRecordId { - fn from(u: UlidOrThing) -> UserRecordId { UserRecordId(ulid::Ulid::from(u)) } -} - -impl From for PhotoRecordId { - fn from(u: UlidOrThing) -> PhotoRecordId { - PhotoRecordId(ulid::Ulid::from(u)) - } -} - -impl From for PhotoGroupRecordId { - fn from(u: UlidOrThing) -> PhotoGroupRecordId { - PhotoGroupRecordId(ulid::Ulid::from(u)) - } -} - -impl From for PrivateArtifactRecordId { - fn from(u: UlidOrThing) -> PrivateArtifactRecordId { - PrivateArtifactRecordId(ulid::Ulid::from(u)) - } -} - -impl From for PublicArtifactRecordId { - fn from(u: UlidOrThing) -> PublicArtifactRecordId { - PublicArtifactRecordId(ulid::Ulid::from(u)) - } -} - -pub trait AsThing { - fn as_thing(&self) -> Thing; -} - -impl AsThing for T { - fn as_thing(&self) -> Thing { - Thing { - tb: T::TABLE.to_string(), - id: Id::String(self.get_inner_string()), - } - } -} - -macro_rules! impl_into_resource { - ($type:ty) => { - impl IntoResource> for $type { - fn into_resource(self) -> Result { - Ok(Resource::RecordId(self.as_thing())) - } - } - }; -} - -impl_into_resource!(UserRecordId); -impl_into_resource!(PhotoRecordId); -impl_into_resource!(PhotoGroupRecordId); -impl_into_resource!(PrivateArtifactRecordId); -impl_into_resource!(PublicArtifactRecordId); diff --git a/crates/core_types/src/lib.rs b/crates/core_types/src/lib.rs index 6cb329d..e7767f5 100644 --- a/crates/core_types/src/lib.rs +++ b/crates/core_types/src/lib.rs @@ -1,13 +1,46 @@ +#![warn(missing_docs)] + +//! Core types for all of picturepro. +//! +//! This crate contains the model types and ID types for the entire picturepro +//! codebase. +//! +//! This crate is isomorphic and can be used in both the client and server. +//! Generally, on the client side, models and IDs will simply serialize and +//! deserialize to themselves. On the server side, we implement a number of +//! traits to allow more convenient usage with SurrealDB. +//! +//! We also have other features for when we need specialized traits implemented +//! on models, such as the [`AuthUser`](axum_login::AuthUser) trait. +//! +//! # Adding a New Table +//! +//! To add a new table, you need a record ID type, a table name constant, and a +//! model type. The record ID type should be a newtype around a `Ulid` and the +//! model type should be a struct with a `pub id: RecordId` field. +//! +//! The record ID needs the following: +//! - `#[derive(Clone, Debug, Deserialize, Serialize, Copy)]` +//! - `#[cfg_attr(feature = "ssr", serde(from = "crate::ssr::UlidOrThing"))]`: +//! to allow deserializing from a surrealdb `Thing` when on the server. +//! - `impl_record_id!(UserRecordId, USER_TABLE);` in the `ssr` module, which +//! implements the following: +//! - `NewId` +//! - `From` +//! - `IntoResource>` +//! +//! The model type needs `#[derive(Clone, Debug, Deserialize, Serialize)]`. + mod artifact; mod auth; -#[cfg(feature = "ssr")] -pub(crate) mod conv; mod photo; +#[cfg(feature = "ssr")] +pub(crate) mod ssr; #[cfg(feature = "ssr")] pub use surreal_id::NewId; pub use ulid::Ulid; #[cfg(feature = "ssr")] -pub use self::conv::AsThing; +pub use self::ssr::AsThing; pub use self::{artifact::*, auth::*, photo::*}; diff --git a/crates/core_types/src/photo.rs b/crates/core_types/src/photo.rs index 7f1f3d8..6764108 100644 --- a/crates/core_types/src/photo.rs +++ b/crates/core_types/src/photo.rs @@ -1,81 +1,90 @@ use serde::{Deserialize, Serialize}; +/// The table name for the photo table. pub const PHOTO_TABLE: &str = "photo"; +/// The table name for the photo group table. pub const PHOTO_GROUP_TABLE: &str = "photo_group"; use crate::{ auth::UserRecordId, PrivateArtifactRecordId, PublicArtifactRecordId, }; +/// The record ID for a photo. #[derive(Clone, Debug, Deserialize, Serialize, Copy)] -#[cfg_attr(feature = "ssr", serde(from = "crate::conv::UlidOrThing"))] +#[cfg_attr(feature = "ssr", serde(from = "crate::ssr::UlidOrThing"))] pub struct PhotoRecordId(pub ulid::Ulid); +/// A photo. #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Photo { + /// The record ID. pub id: PhotoRecordId, + /// The user who created the photo. pub photographer: UserRecordId, + /// The user who owns the photo. pub owner: UserRecordId, + /// The photo's artifacts. pub artifacts: PhotoArtifacts, } +/// The artifacts for a photo. Not a table. +/// +/// This is a separate type to make it easier to work with the photo table. #[derive(Clone, Debug, Deserialize, Serialize)] pub struct PhotoArtifacts { + /// The original image. + /// + /// This is a private artifact bc honestly it's our whole product :) pub original: PrivateImageArtifact, + /// The thumbnail, with a max size of 200x200. pub thumbnail: PublicImageArtifact, } +/// A public image artifact. Not a table. +/// +/// This is a descriptor of the public artifact type, with some image +/// metadata. #[derive(Clone, Debug, Deserialize, Serialize)] pub struct PublicImageArtifact { + /// The record ID. pub artifact_id: PublicArtifactRecordId, + /// The size of the image. pub size: (u32, u32), } +/// A private image artifact. Not a table. +/// +/// This is a descriptor of the private artifact type, with some image +/// metadata. #[derive(Clone, Debug, Deserialize, Serialize)] pub struct PrivateImageArtifact { + /// The record ID. pub artifact_id: PrivateArtifactRecordId, + /// The size of the image. pub size: (u32, u32), } +/// The record ID for a photo group. #[derive(Clone, Debug, Deserialize, Serialize, Copy)] -#[cfg_attr(feature = "ssr", serde(from = "crate::conv::UlidOrThing"))] +#[cfg_attr(feature = "ssr", serde(from = "crate::ssr::UlidOrThing"))] pub struct PhotoGroupRecordId(pub ulid::Ulid); +/// A photo group. #[derive(Clone, Debug, Deserialize, Serialize)] pub struct PhotoGroup { + /// The record ID. pub id: PhotoGroupRecordId, + /// The user who owns the photo group. pub owner: UserRecordId, + /// The photos in the group. pub photos: Vec, + /// Whether the group is publicly visible. pub public: bool, } +/// The metadata for uploading a photo group. Not a table. #[derive(Clone, Debug, Default, Deserialize, Serialize)] pub struct PhotoGroupUploadMeta { + /// Whether the group should be publicly visible. pub public: bool, } - -#[cfg(feature = "ssr")] -mod ssr { - use surreal_id::NewId; - use surrealdb::sql::Id; - - use super::*; - - impl NewId for PhotoRecordId { - const TABLE: &'static str = PHOTO_TABLE; - - fn from_inner_id>(inner_id: T) -> Self { - Self(inner_id.into().to_string().parse().unwrap()) - } - fn get_inner_string(&self) -> String { self.0.to_string() } - } - - impl NewId for PhotoGroupRecordId { - const TABLE: &'static str = PHOTO_GROUP_TABLE; - - fn from_inner_id>(inner_id: T) -> Self { - Self(inner_id.into().to_string().parse().unwrap()) - } - fn get_inner_string(&self) -> String { self.0.to_string() } - } -} diff --git a/crates/core_types/src/ssr.rs b/crates/core_types/src/ssr.rs new file mode 100644 index 0000000..ebc8db1 --- /dev/null +++ b/crates/core_types/src/ssr.rs @@ -0,0 +1,75 @@ +use serde::Deserialize; +use surreal_id::NewId; +use surrealdb::{ + opt::{IntoResource, Resource}, + sql::{Id, Thing}, +}; + +use crate::{ + PhotoGroupRecordId, PhotoRecordId, PrivateArtifactRecordId, + PublicArtifactRecordId, UserRecordId, PHOTO_GROUP_TABLE, PHOTO_TABLE, + PRIVATE_ARTIFACT_TABLE, PUBLIC_ARTIFACT_TABLE, USER_TABLE, +}; + +#[derive(Deserialize, Debug, Clone)] +#[serde(untagged)] +pub enum UlidOrThing { + Ulid(ulid::Ulid), + Thing(Thing), +} + +impl From for ulid::Ulid { + fn from(u: UlidOrThing) -> ulid::Ulid { + match u { + UlidOrThing::Ulid(u) => u, + UlidOrThing::Thing(t) => t.id.to_string().parse().unwrap(), + } + } +} + +/// A trait for converting a type into a [`Thing`]. +/// +/// This is its own trait for ownership reasons, so that we can have a blanket +/// implementation. +pub trait AsThing { + /// Convert the type into a [`Thing`]. + fn as_thing(&self) -> Thing; +} + +impl AsThing for T { + fn as_thing(&self) -> Thing { + Thing { + tb: T::TABLE.to_string(), + id: Id::String(self.get_inner_string()), + } + } +} + +macro_rules! impl_record_id { + ($type:ident, $table:ident) => { + impl NewId for $type { + const TABLE: &'static str = $table; + + fn from_inner_id>(inner_id: T) -> Self { + Self(inner_id.into().to_string().parse().unwrap()) + } + fn get_inner_string(&self) -> String { self.0.to_string() } + } + + impl From for $type { + fn from(u: UlidOrThing) -> $type { $type(ulid::Ulid::from(u)) } + } + + impl IntoResource> for $type { + fn into_resource(self) -> Result { + Ok(Resource::RecordId(self.as_thing())) + } + } + }; +} + +impl_record_id!(UserRecordId, USER_TABLE); +impl_record_id!(PhotoRecordId, PHOTO_TABLE); +impl_record_id!(PhotoGroupRecordId, PHOTO_GROUP_TABLE); +impl_record_id!(PrivateArtifactRecordId, PRIVATE_ARTIFACT_TABLE); +impl_record_id!(PublicArtifactRecordId, PUBLIC_ARTIFACT_TABLE); diff --git a/crates/site-app/src/components/gallery.rs b/crates/site-app/src/components/gallery.rs index fac3d20..7e88234 100644 --- a/crates/site-app/src/components/gallery.rs +++ b/crates/site-app/src/components/gallery.rs @@ -9,10 +9,8 @@ pub fn Gallery() -> impl IntoView { .into_view(); }; - let photo_groups = create_resource( - move || (), - move |_| fetch_user_photo_groups(user.id.clone()), - ); + let photo_groups = + create_resource(move || (), move |_| fetch_user_photo_groups(user.id)); view! { @@ -26,7 +24,7 @@ pub fn Gallery() -> impl IntoView { } Err(e) => { view! { -

"Failed to load photo groups: {e}"

+

{ format!("Failed to load photo groups: {e}") }

} .into_view() } diff --git a/crates/site-app/src/components/navigation.rs b/crates/site-app/src/components/navigation.rs index c740165..f71e2d3 100644 --- a/crates/site-app/src/components/navigation.rs +++ b/crates/site-app/src/components/navigation.rs @@ -17,7 +17,7 @@ pub fn navigate_to(path: &str) { let result = web_sys::window() .expect("Failed to get window") .location() - .set_href(&path); + .set_href(path); if let Err(e) = result { logging::error!("failed to navigate: {:?}", e); }