Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

10 add crate docs and readme #11

Merged
merged 8 commits into from
Feb 19, 2024
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
Loading