Skip to content

Commit

Permalink
Merge pull request #11 from picture-pro/10-add-crate-docs-and-readme
Browse files Browse the repository at this point in the history
10 add crate docs and readme
  • Loading branch information
johnbchron authored Feb 19, 2024
2 parents 7a1d48b + 5a01080 commit 1344ba4
Show file tree
Hide file tree
Showing 11 changed files with 266 additions and 175 deletions.
40 changes: 39 additions & 1 deletion crates/artifact/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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<bytes::Bytes>) -> Self;
/// Create a new artifact with the given ID and contents.
fn new_with_id(id: Self::Id, contents: Option<bytes::Bytes>) -> 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<Output = Result<()>> + 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<Output = Result<()>> + Send;
/// Convenience method for uploading and pushing to SurrealDB.
fn upload_and_push(&self) -> impl Future<Output = Result<()>> + Send;

/// Push the artifact to SurrealDB.
fn push_to_surreal(&self) -> impl Future<Output = Result<()>> + Send;
/// Pull an artifact from SurrealDB.
fn pull_from_surreal(
id: Self::Id,
) -> impl Future<Output = Result<Box<Self>>> + Send;
/// Get the object store for the artifact.
fn object_store(&self) -> Result<Box<dyn object_store::ObjectStore>>;
}

Expand All @@ -33,7 +71,7 @@ impl Artifact for PublicArtifact {

fn new_with_id(id: Self::Id, contents: Option<bytes::Bytes>) -> Self {
Self {
id: id.clone(),
id,
contents,
url: format!(
"https://s3.{}.amazonaws.com/{}/{}",
Expand Down
26 changes: 26 additions & 0 deletions crates/auth/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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,
};
Expand All @@ -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<Self> {
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,
Expand Down Expand Up @@ -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<Backend>;

/// Builds an authentication layer for use with an Axum router.
Expand Down
11 changes: 5 additions & 6 deletions crates/bl/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
};

Expand Down
41 changes: 13 additions & 28 deletions crates/core_types/src/artifact.rs
Original file line number Diff line number Diff line change
@@ -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<bytes::Bytes>,
}

/// 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<bytes::Bytes>,
}

#[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<T: Into<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<T: Into<Id>>(inner_id: T) -> Self {
Self(inner_id.into().to_string().parse().unwrap())
}
fn get_inner_string(&self) -> String { self.0.to_string() }
}
}
50 changes: 31 additions & 19 deletions crates/core_types/src/auth.rs
Original file line number Diff line number Diff line change
@@ -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<User> for PublicUser {
fn from(u: User) -> PublicUser {
PublicUser {
Expand All @@ -31,28 +53,18 @@ impl From<User> 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<PublicUser>);

#[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<T: Into<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::*;
Expand Down
Loading

0 comments on commit 1344ba4

Please sign in to comment.